From d8c5c6f8771332b7a58dc93a2e391d6a5095528e Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 24 Jun 2026 12:04:37 +0800 Subject: [PATCH 1/3] refactor(app): simplify layout hierarchy Remove TargetServerLayout and TargetDirectoryLayout, moving directory resolution and SDK providers into route-specific components. NewLayout now renders from the router root so it stays mounted across home, draft, and session routes. Titlebar resolves session metadata from LayoutRoute.server instead of the surrounding server provider. --- packages/app/src/app.tsx | 290 +++++++++++------------ packages/app/src/components/titlebar.tsx | 14 +- 2 files changed, 148 insertions(+), 156 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 7c06b90c36c7..44e2aecd4310 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -91,19 +91,88 @@ const SessionRoute = Object.assign( const TargetSessionRoute = Object.assign( () => { - const sdk = useSDK() - const serverSDK = useServerSDK() + const params = useParams<{ serverKey: string }>() + const server = useServer() + const conn = createMemo(() => { + const key = requireServerKey(params.serverKey) + return server.list.find((item) => ServerConnection.key(item) === key) + }) + return ( - - - - - + + + + + ) }, { preload: Session.preload }, ) +function ResolvedTargetSessionRoute() { + const params = useParams<{ serverKey: string; id: string }>() + const settings = useSettings() + const tabs = useTabs() + const serverSDK = useServerSDK() + const serverKey = createMemo(() => requireServerKey(params.serverKey)) + const resolved = useQuery(() => ({ + queryKey: [serverSDK().scope, "session-route", params.id] as const, + queryFn: async () => { + const session = (await serverSDK().client.session.get({ sessionID: params.id })).data! + const root = await rootSession(session, (sessionID) => + serverSDK() + .client.session.get({ sessionID }) + .then((result) => result.data!), + ) + return { session, rootID: root.id } + }, + })) + const directory = createMemo((prev) => prev ?? resolved.data?.session.directory) + const targetDirectory = () => directory()! + + createEffect(() => { + const current = resolved.data + if (!current) return + tabs.addSessionTab({ + server: serverKey(), + sessionId: current.rootID, + }) + }) + + return ( + params.id}> + }> + + } + > + + + + + + + + + + + + ) +} + +function TargetSessionPage() { + const sdk = useSDK() + const serverSDK = useServerSDK() + return ( + + + + + + ) +} + // Wraps the non-draft routes. They are gated on (and keyed to) the globally selected // server via ServerKey, then provide the server-scoped shell (Permission/Layout/ // Notification/Models + the visual Layout) for that server. @@ -125,125 +194,42 @@ function LegacyServerLayout(props: ParentProps) { ) } -// Wraps /new-session. It resolves the draft's target server and provides the -// server-scoped shell for that server — without ServerKey, so the page never depends -// on the globally "selected" server. -function TargetServerLayout(props: ParentProps) { - const server = useServer() - const tabs = useTabs() - const params = useParams<{ serverKey?: string }>() - const [search] = useSearchParams<{ draftId?: string }>() - const conn = createMemo(() => { - if (params.serverKey) { - const key = requireServerKey(params.serverKey) - return server.list.find((item) => ServerConnection.key(item) === key) - } - const id = search.draftId - if (!id) return undefined - const draft = tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === id) - if (!draft) return undefined - return server.list.find((c) => ServerConnection.key(c) === draft.server) - }) - - return ( - - - {props.children} - - - ) -} - -function TargetDirectoryLayout(props: ParentProps) { - const params = useParams<{ serverKey?: string; id?: string }>() - const [search] = useSearchParams<{ draftId?: string }>() - const settings = useSettings() - const tabs = useTabs() - const serverSDK = useServerSDK() - const serverKey = createMemo(() => { - if (params.serverKey) return requireServerKey(params.serverKey) - if (!search.draftId) return undefined - return tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === search.draftId)?.server - }) - - const resolved = useQuery(() => { - const id = params.id - return { - queryKey: [serverSDK().scope, "session-route", id] as const, - enabled: !!params.serverKey && !!params.id, - queryFn: async () => { - const session = (await serverSDK().client.session.get({ sessionID: params.id! })).data! - const root = await rootSession(session, (sessionID) => - serverSDK() - .client.session.get({ sessionID }) - .then((result) => result.data!), - ) - return { session, rootID: root.id } - }, - } - }) - const resolvedDirectory = createMemo(() => { - if (params.serverKey) return resolved.data?.session.directory - if (!search.draftId) return undefined - return tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === search.draftId)?.directory - }) - const directory = createMemo((prev) => - search.draftId ? resolvedDirectory() : (prev ?? resolvedDirectory()), - ) - const home = () => !params.serverKey && !search.draftId - const targetDirectory = () => directory()! - - createEffect(() => { - const current = resolved.data - const key = serverKey() - if (!current || !key) return - tabs.addSessionTab({ - server: key, - sessionId: current.rootID, - }) - }) - - return ( - (home() ? undefined : directory())} sessionID={() => params.id}> - - }> - - } - > - - - - {props.children} - - - - - - - - - ) -} - function DraftRoute() { const [search] = useSearchParams<{ draftId?: string }>() const tabs = useTabs() return ( - }> - + tab.type === "draft" && tab.draftID === search.draftId)} + keyed + fallback={} + > + {(draft) => } ) } -function ResolvedDraftRoute() { +function ResolvedDraftRoute(props: { draft: DraftTab }) { + const server = useServer() + const conn = createMemo(() => server.list.find((item) => ServerConnection.key(item) === props.draft.server)) + const directory = () => props.draft.directory + const serverKey = () => props.draft.server + return ( - - - + + + + + + + + + + + + + ) } @@ -306,9 +292,7 @@ function SharedProviders(props: ParentProps) { ) } -// Server-scoped providers plus the visual Layout (tabs/sidebar). These live inside -// each per-route server layout so they resolve to that route's server (selected vs -// draft). The Layout remounts when crossing between those groups. +// Server-scoped providers shared by the legacy shell and the top-level new shell. type ServerScopedShellProps = ParentProps<{ directory?: () => string | undefined sessionID?: () => string | undefined @@ -334,11 +318,23 @@ function LegacyServerScopedShell(props: ServerScopedShellProps) { ) } -function NewServerScopedShell(props: ServerScopedShellProps) { +function NewAppLayout(props: ParentProps) { return ( - - {props.children} - + + + {props.children} + + + ) +} + +function TargetServerScopedProviders(props: ServerScopedShellProps) { + return ( + + + {props.children} + + ) } @@ -524,11 +520,9 @@ export function AppInterface(props: { router?: Component disableHealthCheck?: boolean }) { - // The shared shell holds only server-agnostic providers (QueryClient + Settings/ - // Command/Highlights) and stays mounted across every route. The server-scoped - // providers and the visual Layout live in the per-route layouts below, so they - // resolve to that route's server (selected for most routes, the draft's server for - // /new-session). appChildren is server-agnostic, so it renders here once. + // The visual new layout lives in the router root so it remains mounted across + // route changes. Draft and session routes override only their server-bound data + // providers beneath it. const ServerShell = (shellProps: ParentProps) => ( @@ -552,7 +546,11 @@ export function AppInterface(props: { component={props.router ?? Router} root={(routerProps) => ( - {routerProps.children} + + + {routerProps.children} + + )} > @@ -578,28 +576,20 @@ function Routes() { - - - { - <> - - - { - const server = useServer() - const { id } = useParams() - - return - }} - /> - - - } - - - - + + + { + const server = useServer() + const { id } = useParams() + + return + }} + /> + + + ) } diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index f111e7efe62e..0e5e44295807 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -39,7 +39,6 @@ import { useGlobal } from "@/context/global" import { ServerConnection, useServer } from "@/context/server" import { tabHref, useTabs } from "@/context/tabs" import "./titlebar.css" -import { useServerSDK } from "@/context/server-sdk" import { Session } from "@opencode-ai/sdk/v2" import { base64Encode } from "@opencode-ai/core/util/encode" import { createTabPromptState } from "@/context/prompt" @@ -262,7 +261,6 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { {(_) => { - const serverSdk = useServerSDK() const navigate = useNavigate() const layout = useLayout() const global = useGlobal() @@ -273,11 +271,15 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { const [session] = createResource( () => { const route = layout.route() - return route.type === "session" ? route : undefined + if (route.type !== "session") return undefined + const conn = global.servers + .list() + .find((item) => ServerConnection.key(item) === (route.server ?? server.key)) + return conn ? { route, sdk: global.createServerCtx(conn).sdk } : undefined }, - (route) => - serverSdk() - .client.session.get({ sessionID: route.sessionId }) + ({ route, sdk }) => + sdk.client.session + .get({ sessionID: route.sessionId }) .then((x) => x.data) .catch(() => {}), ) From a0f4e8546cc3a2e8af32be445b314051dcbfc400 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 24 Jun 2026 12:13:30 +0800 Subject: [PATCH 2/3] remove console log --- packages/app/e2e/smoke/session-timeline.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app/e2e/smoke/session-timeline.spec.ts b/packages/app/e2e/smoke/session-timeline.spec.ts index dba7eae7e363..597b3577a712 100644 --- a/packages/app/e2e/smoke/session-timeline.spec.ts +++ b/packages/app/e2e/smoke/session-timeline.spec.ts @@ -718,7 +718,6 @@ async function navigateToSession(page: Page, directory: string, sessionId: strin } async function switchTitlebarSession(page: Page, sessionID: string, title: string) { - console.log(process.env) const href = `/server/${base64Encode(fixture.serverKey)}/session/${sessionID}` const tab = page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first() await expect(tab).toBeVisible() From 145e8a3b7eb9b7c882d62df7f774565428f1ea42 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 24 Jun 2026 12:24:07 +0800 Subject: [PATCH 3/3] key session route --- packages/app/src/app.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 44e2aecd4310..215e077397c9 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -91,7 +91,7 @@ const SessionRoute = Object.assign( const TargetSessionRoute = Object.assign( () => { - const params = useParams<{ serverKey: string }>() + const params = useParams<{ serverKey: string; id: string }>() const server = useServer() const conn = createMemo(() => { const key = requireServerKey(params.serverKey) @@ -99,11 +99,13 @@ const TargetSessionRoute = Object.assign( }) return ( - - - - - + + + + + + + ) }, { preload: Session.preload },