import { Show, createEffect, createMemo, createResource, createSignal, onCleanup, untrack } from "solid-js" import { createStore } from "solid-js/store" import { Portal } from "solid-js/web" import { useSearchParams } from "@solidjs/router" import { Tooltip } from "@opencode-ai/ui/tooltip" import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" import { NewSessionDesignView } from "@/components/session" import { PromptInput } from "@/components/prompt-input" import { StatusPopoverV2 } from "@/components/status-popover" import { PromptProjectAddButton, PromptProjectSelector, createPromptProjectController, } from "@/components/prompt-project-selector" import { useComments } from "@/context/comments" import { usePrompt } from "@/context/prompt" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" import { createPromptInputController, createPromptProjectControls } from "@/pages/session/composer" import { useSessionKey } from "@/pages/session/session-layout" import { useComposerCommands } from "@/pages/session/use-composer-commands" import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout" import { PromptWorkspaceSelector } from "@/components/prompt-workspace-selector" import { useTitlebarRightMount } from "@/components/titlebar" import { useCommand } from "@/context/command" import { useProviders } from "@/hooks/use-providers" import { useSettingsDialog } from "@/components/settings-dialog" import { Persist, persisted } from "@/utils/persist" import createPresence from "solid-presence" const workspaceBarEnabled = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" const providerTipDismissalDuration = 30 * 24 * 60 * 60 * 1000 const providerTipExitDuration = 250 /** * The `/new-session` draft page. Unlike `session.tsx`, this only renders the prompt * composer for a brand-new session — no terminal, review pane, file tree, or message * timeline. Submitting promotes the draft into a real session (see prompt-input/submit). */ export default function NewSessionPage() { const prompt = usePrompt() const sdk = useSDK() const sync = useSync() const serverSync = useServerSync() const comments = useComments() const language = useLanguage() const settings = useSettings() const command = useCommand() const providers = useProviders(() => sdk().directory) const openProviderSettings = useSettingsDialog("providers") const route = useSessionKey() const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>() useComposerCommands() let inputRef: HTMLDivElement | undefined const inputController = createPromptInputController({ sessionKey: route.sessionKey, sessionID: () => route.params.id, queryOptions: serverSync().queryOptions, }) const projectControls = createPromptProjectControls() const projectController = createPromptProjectController({ controls: projectControls, onDone: () => inputRef?.focus(), }) command.register("new-session", () => [ { id: "input.focus", title: language.t("command.input.focus"), category: language.t("command.category.view"), keybind: "ctrl+l", onSelect: () => inputRef?.focus(), }, ]) const [store, setStore] = createStore<{ worktree?: string }>({}) const rightMount = useTitlebarRightMount() const showWorkspaceBar = createMemo(() => workspaceBarEnabled && sync().project?.vcs === "git") const newSessionWorktree = createMemo(() => { if (!showWorkspaceBar()) return "main" if (store.worktree) return store.worktree const project = sync().project if (project && sdk().directory !== project.worktree) return sdk().directory return "main" }) const projectRoot = createMemo(() => sync().project?.worktree ?? sdk().directory) const localBranch = createMemo(() => serverSync().child(projectRoot())[0].vcs?.branch) const selectedBranch = createMemo(() => { const worktree = newSessionWorktree() if (worktree === "main" || worktree === "create") return localBranch() return serverSync().child(worktree)[0].vcs?.branch ?? localBranch() }) createEffect(() => { if (!prompt.ready()) return untrack(() => { const text = searchParams.prompt if (!text) return prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) setSearchParams({ ...searchParams, prompt: undefined }) }) }) createEffect(() => { if (!prompt.ready()) return requestAnimationFrame(() => inputRef?.focus()) }) const ready = Promise.resolve() const [promptReady] = createResource( () => prompt.ready.promise ?? ready, (promise) => promise.then(() => true), ) return (
{(mount) => ( )}
{language.t("prompt.loading")}
} >
{ inputRef = el }} newSessionWorktree={newSessionWorktree()} onNewSessionWorktreeReset={() => setStore("worktree", undefined)} onSubmit={() => comments.clear()} toolbar={ } />
setStore( "worktree", value === "main" && sync().project?.worktree !== sdk().directory ? sync().project?.worktree : value, ) } onDone={() => inputRef?.focus()} />
serverSync().child(sdk().directory)[0].provider_ready} connected={() => providers.paid().length > 0} openProviders={openProviderSettings} />
) } function ProviderTip(props: { ready: () => boolean; connected: () => boolean; openProviders: () => void }) { const language = useLanguage() const [persistedState, setPersistedState, , persistedReady] = persisted( Persist.global("new-session.provider-tip"), createStore({ dismissedAt: 0 }), ) const visible = createMemo( () => props.ready() && persistedReady() && !props.connected() && Date.now() - persistedState.dismissedAt >= providerTipDismissalDuration, ) function dismiss() { setPersistedState("dismissedAt", Date.now()) } const [ref, setRef] = createSignal() const presence = createPresence({ show: () => visible(), element: () => ref() ?? null, }) return (
) }