From 800d41ddab5b9e41e28c730089cf8fc52967f653 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:37:08 +0800 Subject: [PATCH] refactor(app): simplify layout hierarchy (#33588) --- .../app/e2e/smoke/session-timeline.spec.ts | 1 - packages/app/src/app.tsx | 288 +++++++++--------- packages/app/src/components/titlebar.tsx | 14 +- 3 files changed, 148 insertions(+), 155 deletions(-) diff --git a/packages/app/e2e/smoke/session-timeline.spec.ts b/packages/app/e2e/smoke/session-timeline.spec.ts index dba7eae7e..597b3577a 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() diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 7c06b90c3..215e07739 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -91,19 +91,90 @@ const SessionRoute = Object.assign( const TargetSessionRoute = Object.assign( () => { - const sdk = useSDK() - const serverSDK = useServerSDK() + const params = useParams<{ serverKey: string; id: 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 +196,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 +294,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 +320,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 +522,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 +548,11 @@ export function AppInterface(props: { component={props.router ?? Router} root={(routerProps) => ( - {routerProps.children} + + + {routerProps.children} + + )} > @@ -578,28 +578,20 @@ function Routes() { - - - { - <> - - - { - const server = useServer() - const { id } = useParams() + + + { + const server = useServer() + const { id } = useParams() - return - }} - /> - - - } - - - - + return + }} + /> + + + ) } diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index f111e7efe..0e5e44295 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(() => {}), )