From 85776832f1c83108579a2fcbdd5b8ca81a55d8ad Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 8 Jun 2026 17:42:55 +0800 Subject: [PATCH 1/4] feat(app): add /new-session draft route --- packages/app/src/app.tsx | 73 +++++++++++++++++-- .../app/src/components/prompt-input/submit.ts | 16 +++- packages/app/src/pages/directory-layout.tsx | 4 +- 3 files changed, 82 insertions(+), 11 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index ad9aa3543ff5..a5b833e83d9c 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -9,7 +9,7 @@ import { Font } from "@opencode-ai/ui/font" import { Splash } from "@opencode-ai/ui/logo" import { ThemeProvider } from "@opencode-ai/ui/theme/context" import { MetaProvider } from "@solidjs/meta" -import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router" +import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router" import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" import { Effect } from "effect" import { @@ -43,9 +43,10 @@ import { PromptProvider } from "@/context/prompt" import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server" import { SettingsProvider, useSettings } from "@/context/settings" import { TerminalProvider } from "@/context/terminal" -import { TabsProvider } from "@/context/tabs" +import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs" +import { SDKProvider, useSDK } from "@/context/sdk" import { WslServersProvider } from "@/wsl/context" -import DirectoryLayout from "@/pages/directory-layout" +import DirectoryLayout, { DirectoryDataProvider } from "@/pages/directory-layout" import Layout from "@/pages/layout" import { ErrorPage } from "./pages/error" import { useCheckServerHealth } from "./utils/server-health" @@ -54,14 +55,69 @@ const HomeRoute = lazy(() => import("@/pages/home")) const Session = lazy(() => import("@/pages/session")) const SessionRoute = Object.assign( - () => ( - - - - ), + () => { + const settings = useSettings() + const params = useParams() + const [search] = useSearchParams<{ draftId?: string; prompt?: string }>() + const sdk = useSDK() + const server = useServer() + const tabs = useTabs() + + // When the new layout is enabled, the legacy new-session route (/:dir/session with no id) + // is replaced by a draft at /new-session?draftId=… + createEffect(() => { + if (!settings.general.newLayoutDesigns()) return + if (params.id || search.draftId) return + if (!tabs.ready() || !sdk.directory) return + tabs.newDraft({ server: server.key, directory: sdk.directory }, search.prompt) + }) + + return ( + + + + ) + }, { preload: Session.preload }, ) +function DraftRoute() { + const [search] = useSearchParams<{ draftId?: string }>() + const tabs = useTabs() + return ( + + }> + {(draftID) => } + + + ) +} + +function ResolvedDraftRoute(props: { draftID: string }) { + const server = useServer() + const tabs = useTabs() + const draft = createMemo(() => + tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === props.draftID), + ) + + createEffect(() => { + const current = draft() + if (current && current.server !== server.key) server.setActive(current.server) + }) + + return ( + }> + {(current) => ( + + + + + + )} + + ) +} + function UiI18nBridge(props: ParentProps) { const language = useLanguage() return {props.children} @@ -335,6 +391,7 @@ export function AppInterface(props: { )} > + } /> diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 024bdd7ae2e6..bd4d98508856 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -2,9 +2,11 @@ import type { Message, Session } from "@opencode-ai/sdk/v2/client" import { showToast } from "@/utils/toast" import { base64Encode } from "@opencode-ai/core/util/encode" import { Binary } from "@opencode-ai/core/util/binary" -import { useNavigate, useParams } from "@solidjs/router" +import { useNavigate, useParams, useSearchParams } from "@solidjs/router" import { batch, type Accessor } from "solid-js" import type { FileSelection } from "@/context/file" +import { useServer } from "@/context/server" +import { useTabs } from "@/context/tabs" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" @@ -213,6 +215,9 @@ export function createPromptSubmit(input: PromptSubmitInput) { const layout = useLayout() const language = useLanguage() const params = useParams() + const [search] = useSearchParams<{ draftId?: string }>() + const server = useServer() + const tabs = useTabs() const pendingKey = (sessionID: string) => ScopedKey.from(sdk.scope, sessionID) const errorMessage = (err: unknown) => { @@ -381,7 +386,14 @@ export function createPromptSubmit(input: PromptSubmitInput) { if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory) local.session.promote(sessionDirectory, session.id) layout.handoff.setTabs(base64Encode(sessionDirectory), session.id) - navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`) + const draftID = search.draftId + if (draftID) + tabs.promoteDraft(draftID, { + server: server.key, + dirBase64: base64Encode(sessionDirectory), + sessionId: session.id, + }) + else navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`) } } if (!session) { diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index 2d0dfd81dc7b..e03d5c206fcc 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -10,7 +10,7 @@ import { useSync } from "@/context/sync" import { decode64 } from "@/utils/base64" import { Schema } from "effect" -function DirectoryDataProvider(props: ParentProps<{ directory: string }>) { +export function DirectoryDataProvider(props: ParentProps<{ directory: string; draftID?: string }>) { const location = useLocation() const navigate = useNavigate() const params = useParams() @@ -18,6 +18,8 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) { const slug = createMemo(() => base64Encode(props.directory)) createEffect(() => { + // A draft lives at /new-session?draftId=… and has no directory segment to normalize. + if (props.draftID) return const next = sync.data.path.directory if (!next || next === props.directory) return const path = location.pathname.slice(slug().length + 1) From e99c354591cec8512c88a00c7495ffc7c654acfb Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 9 Jun 2026 18:01:28 +0800 Subject: [PATCH 2/4] move new-session to a different page --- packages/app/src/app.tsx | 29 +++++-- packages/app/src/components/prompt-input.tsx | 15 +++- packages/app/src/components/titlebar.tsx | 85 +++++++++++++++++-- packages/app/src/context/layout.tsx | 11 ++- packages/app/src/context/tabs.tsx | 25 ++++-- packages/app/src/pages/new-session.tsx | 78 +++++++++++++++++ packages/app/src/pages/session.tsx | 16 ++-- .../pages/session/new-session-layout.test.ts | 14 --- .../src/pages/session/new-session-layout.ts | 4 - 9 files changed, 225 insertions(+), 52 deletions(-) create mode 100644 packages/app/src/pages/new-session.tsx delete mode 100644 packages/app/src/pages/session/new-session-layout.test.ts diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index a5b833e83d9c..5bf18b657a1f 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -53,6 +53,7 @@ import { useCheckServerHealth } from "./utils/server-health" const HomeRoute = lazy(() => import("@/pages/home")) const Session = lazy(() => import("@/pages/session")) +const NewSession = lazy(() => import("@/pages/new-session")) const SessionRoute = Object.assign( () => { @@ -105,12 +106,18 @@ function ResolvedDraftRoute(props: { draftID: string }) { if (current && current.server !== server.key) server.setActive(current.server) }) + // Key on the directory so retargeting the draft's project re-instantiates the + // SDK/data providers for the new directory while keeping the same draft id. + const directory = () => draft()?.directory + return ( - }> - {(current) => ( - - - + + {(dir) => ( + + + + + )} @@ -197,6 +204,18 @@ function SessionProviders(props: ParentProps) { ) } +// The draft page only renders the prompt composer, so it drops TerminalProvider. +// FileProvider and CommentsProvider stay because PromptInput uses file search and comment context. +function DraftProviders(props: ParentProps) { + return ( + + + {props.children} + + + ) +} + function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) { return ( diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index bdf55fee0564..835bc23819d6 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -31,9 +31,10 @@ import { FileAttachmentPart, } from "@/context/prompt" import { useLayout } from "@/context/layout" -import { useNavigate } from "@solidjs/router" +import { useNavigate, useSearchParams } from "@solidjs/router" import { useSDK } from "@/context/sdk" import { useServer } from "@/context/server" +import { useTabs } from "@/context/tabs" import { useSync } from "@/context/sync" import { useComments } from "@/context/comments" import { Button } from "@opencode-ai/ui/button" @@ -144,6 +145,8 @@ export const PromptInput: Component = (props) => { const platform = usePlatform() const pickDirectory = useDirectoryPicker() const settings = useSettings() + const tabsStore = useTabs() + const [search] = useSearchParams<{ draftId?: string }>() const { params, tabs, view } = useSessionLayout() let editorRef!: HTMLDivElement let fileInputRef: HTMLInputElement | undefined @@ -1398,6 +1401,16 @@ export const PromptInput: Component = (props) => { } layout.projects.open(worktree) server.projects.touch(worktree) + + // On the draft route, retarget the existing draft in place so we keep the same + // draft id (and its tab/prompt) instead of spawning a new draft for the new directory. + const draftID = search.draftId + if (draftID) { + tabsStore.updateDraft(draftID, { server: server.key, directory: worktree }) + restoreFocus() + return + } + navigate(`/${base64Encode(worktree)}/session`) } const addProject = () => { diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 019f4f47089b..a4fcde5aac21 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -280,7 +280,8 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { const matchRoute = (route: LayoutRoute) => { if (route.type === "home") return - if (route.type === "dir-new-sesssion") { + if (route.type === "draft") { + return tabsStore.find((item) => item.type === "draft" && item.draftID === route.draftID) } if (route.type === "session") { const main = tabsStore.find( @@ -447,13 +448,33 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { refreshTabsAreOverflowing() }) - if (tab.type !== "session") return null + const divider = () => + i() !== 0 && ( +
+ ) + + if (tab.type === "draft") { + return ( + <> + {divider()} + { + navigateTab(tab) + ref.scrollIntoView({ behavior: "instant" }) + }} + onClose={() => tabsStoreActions.removeTab(i())} + /> + + ) + } return ( <> - {i() !== 0 && ( -
- )} + {divider()} {(session) => { - console.log({ session: session() }) const project = createMemo(() => projectForSession(session(), serverCtx()?.projects.list() ?? [])) return ( @@ -853,6 +873,59 @@ function ProjectTabAvatar(props: { ) } +function DraftTabItem(props: { + ref?: HTMLDivElement + href: string + title: string + active?: boolean + onNavigate: () => void + onClose: () => void +}) { + const closeTab = (event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + props.onClose() + } + return ( +
{ + if (event.button !== 1) return + closeTab(event) + }} + > + { + event.preventDefault() + props.onNavigate() + }} + class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden text-[13px] font-medium leading-5 text-v2-text-text-faint group-data-[active='true']:text-[var(--v2-text-text-base)]" + > + + + + {props.title} + +
+ { + event.preventDefault() + event.stopPropagation() + }} + onClick={closeTab} + icon={} + aria-label="Close tab" + /> +
+
+ ) +} + function NewSessionTabItem(props: { ref?: HTMLDivElement; href: string; title: string; onClose: () => void }) { const closeTab = (event: MouseEvent) => { event.preventDefault() diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 49d04df54665..f9e954b98b11 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -77,6 +77,7 @@ export type ReviewDiffStyle = "unified" | "split" export type LayoutRoute = | { type: "home" } + | { type: "draft"; draftID: string; server?: ServerConnection.Key } | { type: "dir-new-sesssion"; dir: string; dirBase64: string; server?: ServerConnection.Key } | { type: "session"; dir: string; dirBase64: string; sessionId: string; server?: ServerConnection.Key } @@ -120,10 +121,16 @@ const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => { } } -const currentRoute = (pathname: string): LayoutRoute => { +const currentRoute = (pathname: string, search: string): LayoutRoute => { const parts = pathname.split("/").filter(Boolean) if (parts.length === 0) return { type: "home" } + if (parts[0] === "new-session") { + const draftID = new URLSearchParams(search).get("draftId") + if (!draftID) return { type: "home" } + return { type: "draft", draftID } + } + const dirBase64 = parts[0] const dir = decode64(dirBase64) if (!dir) return { type: "home" } @@ -145,7 +152,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const platform = usePlatform() const location = useLocation() const route = createMemo(() => { - const value = currentRoute(location.pathname) + const value = currentRoute(location.pathname, location.search) if (value.type === "home") return value return { ...value, server: server.key } }) diff --git a/packages/app/src/context/tabs.tsx b/packages/app/src/context/tabs.tsx index cc43bac03bf5..7f6274cf9922 100644 --- a/packages/app/src/context/tabs.tsx +++ b/packages/app/src/context/tabs.tsx @@ -5,7 +5,7 @@ import { createStore, produce } from "solid-js/store" import { Persist, persisted, removePersisted, draftPersistedKeys } from "@/utils/persist" import { ServerConnection, useServer } from "./server" import { createEffect, startTransition } from "solid-js" -import { useNavigate, useParams } from "@solidjs/router" +import { useLocation, useNavigate, useParams } from "@solidjs/router" import { usePlatform } from "./platform" import { uuid } from "@/utils/uuid" import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events" @@ -65,6 +65,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ const params = useParams() const navigate = useNavigate() + const location = useLocation() const closing = new Set() @@ -123,14 +124,20 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ ) }, promoteDraft(draftID: string, session: Omit) { - const active = `${location.pathname}${location.search}` === draftHref(draftID) - setStore( - produce((tabs) => { - const index = tabs.findIndex((tab) => tab.type === "draft" && tab.draftID === draftID) - if (index !== -1) tabs[index] = { type: "session", ...session } - }), - ) - if (active) navigateTab({ type: "session", ...session }) + // We're viewing this draft when /new-session?draftId=… points at it. Promoting + // replaces the draft tab with a session tab, so the draft route would stop resolving + // and fall back home. Navigate to the new session first so we leave /new-session + // before the draft is removed from the store. + const active = location.pathname === "/new-session" && location.query.draftId === draftID + startTransition(() => { + setStore( + produce((tabs) => { + const index = tabs.findIndex((tab) => tab.type === "draft" && tab.draftID === draftID) + if (index !== -1) tabs[index] = { type: "session", ...session } + }), + ) + if (active) navigateTab({ type: "session", ...session }) + }) removeDraftPersisted(draftID) }, removeTab: (index: number) => { diff --git a/packages/app/src/pages/new-session.tsx b/packages/app/src/pages/new-session.tsx new file mode 100644 index 000000000000..16f30af37825 --- /dev/null +++ b/packages/app/src/pages/new-session.tsx @@ -0,0 +1,78 @@ +import { createEffect, createMemo, onMount, untrack } from "solid-js" +import { createStore } from "solid-js/store" +import { useSearchParams } from "@solidjs/router" +import { NewSessionDesignView } from "@/components/session" +import { useComments } from "@/context/comments" +import { usePrompt } from "@/context/prompt" +import { useSDK } from "@/context/sdk" +import { useSync } from "@/context/sync" +import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer" + +/** + * 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 comments = useComments() + const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>() + + let inputRef: HTMLDivElement | undefined + + const composer = createSessionComposerState() + + const [store, setStore] = createStore({ + worktree: "main", + }) + + const newSessionWorktree = createMemo(() => { + if (store.worktree === "create") return "create" + const project = sync.project + if (project && sdk.directory !== project.worktree) return sdk.directory + return "main" + }) + + 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 }) + }) + }) + + onMount(() => { + requestAnimationFrame(() => inputRef?.focus()) + }) + + return ( +
+
+
+
+ + { + inputRef = el + }} + newSessionWorktree={newSessionWorktree()} + onNewSessionWorktreeReset={() => setStore("worktree", "main")} + onSubmit={() => comments.clear()} + onResponseSubmit={() => {}} + setPromptDockRef={() => {}} + /> + +
+
+
+
+ ) +} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 336fefd5e192..a0d961c695be 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -31,7 +31,7 @@ import { Button } from "@opencode-ai/ui/button" import { showToast } from "@/utils/toast" import { checksum } from "@opencode-ai/core/util/encode" import { useLocation, useSearchParams } from "@solidjs/router" -import { NewSessionDesignView, NewSessionView, SessionHeader } from "@/components/session" +import { NewSessionView, SessionHeader } from "@/components/session" import { useComments } from "@/context/comments" import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch" import { useServerSync } from "@/context/server-sync" @@ -61,7 +61,6 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel" import { TerminalPanel } from "@/pages/session/terminal-panel" import { useSessionCommands } from "@/pages/session/use-session-commands" import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll" -import { shouldUseV2NewSessionPage } from "@/pages/session/new-session-layout" import { Identifier } from "@/utils/id" import { diffs as list } from "@/utils/diffs" import { Persist, persisted } from "@/utils/persist" @@ -268,10 +267,8 @@ export default function Page() { const isDesktop = createMediaQuery("(min-width: 768px)") const size = createSizing() - const isV2NewSessionPage = () => - shouldUseV2NewSessionPage({ newLayoutDesigns: newSessionDesign(), sessionID: params.id }) - const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage()) - const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened() && !isV2NewSessionPage()) + const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) + const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened()) const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen()) const sessionPanelWidth = createMemo(() => { if (!desktopSidePanelOpen()) return "100%" @@ -1745,10 +1742,9 @@ export default function Page() {
- }> - {composerRegion("inline")} - +
diff --git a/packages/app/src/pages/session/new-session-layout.test.ts b/packages/app/src/pages/session/new-session-layout.test.ts deleted file mode 100644 index 436e0a59c788..000000000000 --- a/packages/app/src/pages/session/new-session-layout.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { shouldUseV2NewSessionPage } from "./new-session-layout" - -describe("shouldUseV2NewSessionPage", () => { - test("keeps disabled pages on the legacy layout", () => { - expect(shouldUseV2NewSessionPage({ newLayoutDesigns: false, sessionID: "ses_123" })).toBe(false) - expect(shouldUseV2NewSessionPage({ newLayoutDesigns: false })).toBe(false) - }) - - test("uses the v2 layout only for enabled new-session pages", () => { - expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true })).toBe(true) - expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true, sessionID: "ses_123" })).toBe(false) - }) -}) diff --git a/packages/app/src/pages/session/new-session-layout.ts b/packages/app/src/pages/session/new-session-layout.ts index 7429c7c7e888..edd953ed2d3e 100644 --- a/packages/app/src/pages/session/new-session-layout.ts +++ b/packages/app/src/pages/session/new-session-layout.ts @@ -1,6 +1,2 @@ /** Inline new-session content width — keep in sync with session composer `placement === "inline"`. */ export const NEW_SESSION_CONTENT_WIDTH = "w-full max-w-[720px] px-0" - -export function shouldUseV2NewSessionPage(input: { newLayoutDesigns: boolean; sessionID?: string }) { - return input.newLayoutDesigns && !input.sessionID -} From a923d7901cd30972854ecf55710eccf759448f8b Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 9 Jun 2026 18:43:02 +0800 Subject: [PATCH 3/4] add useLocation to solid router mocks --- packages/app/src/components/file-tree.test.ts | 2 ++ packages/app/src/components/prompt-input/submit.test.ts | 2 ++ packages/app/src/context/comments.test.ts | 2 ++ packages/app/src/context/terminal.test.ts | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/app/src/components/file-tree.test.ts b/packages/app/src/components/file-tree.test.ts index 29e20b4807c5..20bffc41a3dc 100644 --- a/packages/app/src/components/file-tree.test.ts +++ b/packages/app/src/components/file-tree.test.ts @@ -8,6 +8,8 @@ beforeAll(async () => { mock.module("@solidjs/router", () => ({ useNavigate: () => () => undefined, useParams: () => ({}), + useLocation: () => ({}), + useSearchParams: () => [{}, () => undefined], })) mock.module("@/context/file", () => ({ useFile: () => ({ diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts index 092731a9ea7a..60655113e792 100644 --- a/packages/app/src/components/prompt-input/submit.test.ts +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -61,6 +61,8 @@ beforeAll(async () => { mock.module("@solidjs/router", () => ({ useNavigate: () => () => undefined, useParams: () => params, + useLocation: () => ({}), + useSearchParams: () => [{}, () => undefined], })) mock.module("@opencode-ai/sdk/v2/client", () => ({ diff --git a/packages/app/src/context/comments.test.ts b/packages/app/src/context/comments.test.ts index 82fa170f2fc7..5050409a8133 100644 --- a/packages/app/src/context/comments.test.ts +++ b/packages/app/src/context/comments.test.ts @@ -8,6 +8,8 @@ beforeAll(async () => { mock.module("@solidjs/router", () => ({ useNavigate: () => () => undefined, useParams: () => ({}), + useLocation: () => ({}), + useSearchParams: () => [{}, () => undefined], })) mock.module("@opencode-ai/ui/context", () => ({ createSimpleContext: () => ({ diff --git a/packages/app/src/context/terminal.test.ts b/packages/app/src/context/terminal.test.ts index 4f16953c79dd..25ef35dd4138 100644 --- a/packages/app/src/context/terminal.test.ts +++ b/packages/app/src/context/terminal.test.ts @@ -9,6 +9,8 @@ beforeAll(async () => { mock.module("@solidjs/router", () => ({ useNavigate: () => () => undefined, useParams: () => ({}), + useLocation: () => ({}), + useSearchParams: () => [{}, () => undefined], })) mock.module("@opencode-ai/ui/context", () => ({ createSimpleContext: () => ({ From 944f46ecee540ab87c6a9a093cf382134c8b0806 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 9 Jun 2026 18:56:30 +0800 Subject: [PATCH 4/4] more mock --- .../app/src/components/prompt-input/submit.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts index 60655113e792..3e5f0ff1a4d6 100644 --- a/packages/app/src/components/prompt-input/submit.test.ts +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -105,6 +105,16 @@ beforeAll(async () => { }), })) + mock.module("@/context/server", () => ({ + useServer: () => ({ key: "server-key" }), + })) + + mock.module("@/context/tabs", () => ({ + useTabs: () => ({ + promoteDraft: () => undefined, + }), + })) + mock.module("@/context/prompt", () => ({ usePrompt: () => ({ current: () => promptValue,