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(() => {}),
)