refactor(app): simplify layout hierarchy (#33588)
This commit is contained in:
parent
5647ed8ba3
commit
800d41ddab
@ -718,7 +718,6 @@ async function navigateToSession(page: Page, directory: string, sessionId: strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function switchTitlebarSession(page: Page, sessionID: string, title: string) {
|
async function switchTitlebarSession(page: Page, sessionID: string, title: string) {
|
||||||
console.log(process.env)
|
|
||||||
const href = `/server/${base64Encode(fixture.serverKey)}/session/${sessionID}`
|
const href = `/server/${base64Encode(fixture.serverKey)}/session/${sessionID}`
|
||||||
const tab = page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first()
|
const tab = page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first()
|
||||||
await expect(tab).toBeVisible()
|
await expect(tab).toBeVisible()
|
||||||
|
|||||||
@ -91,6 +91,79 @@ const SessionRoute = Object.assign(
|
|||||||
|
|
||||||
const TargetSessionRoute = Object.assign(
|
const TargetSessionRoute = Object.assign(
|
||||||
() => {
|
() => {
|
||||||
|
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 (
|
||||||
|
<Show when={`${params.serverKey}\0${params.id}`} keyed>
|
||||||
|
<ServerSDKProvider server={conn}>
|
||||||
|
<ServerSyncProvider server={conn}>
|
||||||
|
<ResolvedTargetSessionRoute />
|
||||||
|
</ServerSyncProvider>
|
||||||
|
</ServerSDKProvider>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ 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<string | undefined>((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 (
|
||||||
|
<TargetServerScopedProviders directory={directory} sessionID={() => params.id}>
|
||||||
|
<Show when={!resolved.error} fallback={<ErrorPage error={resolved.error} />}>
|
||||||
|
<Show when={directory()}>
|
||||||
|
<Show
|
||||||
|
when={settings.general.newLayoutDesigns()}
|
||||||
|
fallback={<Navigate href={legacySessionHref(directory()!, params.id)} />}
|
||||||
|
>
|
||||||
|
<SDKProvider directory={targetDirectory}>
|
||||||
|
<DirectoryDataProvider directory={targetDirectory} server={serverKey}>
|
||||||
|
<Show when={resolved.data && !resolved.isPlaceholderData}>
|
||||||
|
<TargetSessionPage />
|
||||||
|
</Show>
|
||||||
|
</DirectoryDataProvider>
|
||||||
|
</SDKProvider>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</TargetServerScopedProviders>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TargetSessionPage() {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const serverSDK = useServerSDK()
|
const serverSDK = useServerSDK()
|
||||||
return (
|
return (
|
||||||
@ -100,9 +173,7 @@ const TargetSessionRoute = Object.assign(
|
|||||||
</SessionProviders>
|
</SessionProviders>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
{ preload: Session.preload },
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wraps the non-draft routes. They are gated on (and keyed to) the globally selected
|
// 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/
|
// server via ServerKey, then provide the server-scoped shell (Permission/Layout/
|
||||||
@ -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 (
|
|
||||||
<ServerSDKProvider server={conn}>
|
|
||||||
<ServerSyncProvider server={conn}>
|
|
||||||
<TargetDirectoryLayout>{props.children}</TargetDirectoryLayout>
|
|
||||||
</ServerSyncProvider>
|
|
||||||
</ServerSDKProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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<string | undefined>((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 (
|
|
||||||
<NewServerScopedShell directory={() => (home() ? undefined : directory())} sessionID={() => params.id}>
|
|
||||||
<Show when={!home()} fallback={props.children}>
|
|
||||||
<Show when={!resolved.error} fallback={<ErrorPage error={resolved.error} />}>
|
|
||||||
<Show when={directory()}>
|
|
||||||
<Show
|
|
||||||
when={!params.serverKey || settings.general.newLayoutDesigns()}
|
|
||||||
fallback={<Navigate href={legacySessionHref(directory()!, params.id!)} />}
|
|
||||||
>
|
|
||||||
<SDKProvider directory={targetDirectory}>
|
|
||||||
<DirectoryDataProvider directory={targetDirectory} server={serverKey}>
|
|
||||||
<Show when={!params.serverKey || (resolved.data && !resolved.isPlaceholderData)}>
|
|
||||||
{props.children}
|
|
||||||
</Show>
|
|
||||||
</DirectoryDataProvider>
|
|
||||||
</SDKProvider>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
</NewServerScopedShell>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DraftRoute() {
|
function DraftRoute() {
|
||||||
const [search] = useSearchParams<{ draftId?: string }>()
|
const [search] = useSearchParams<{ draftId?: string }>()
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
return (
|
return (
|
||||||
<Show when={tabs.ready()}>
|
<Show when={tabs.ready()}>
|
||||||
<Show when={search.draftId} keyed fallback={<Navigate href="/" />}>
|
<Show
|
||||||
<ResolvedDraftRoute />
|
when={tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === search.draftId)}
|
||||||
|
keyed
|
||||||
|
fallback={<Navigate href="/" />}
|
||||||
|
>
|
||||||
|
{(draft) => <ResolvedDraftRoute draft={draft} />}
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
|
<ServerSDKProvider server={conn}>
|
||||||
|
<ServerSyncProvider server={conn}>
|
||||||
|
<TargetServerScopedProviders directory={directory}>
|
||||||
|
<SDKProvider directory={directory}>
|
||||||
|
<DirectoryDataProvider directory={directory} server={serverKey}>
|
||||||
<DraftProviders>
|
<DraftProviders>
|
||||||
<NewSession />
|
<NewSession />
|
||||||
</DraftProviders>
|
</DraftProviders>
|
||||||
|
</DirectoryDataProvider>
|
||||||
|
</SDKProvider>
|
||||||
|
</TargetServerScopedProviders>
|
||||||
|
</ServerSyncProvider>
|
||||||
|
</ServerSDKProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,9 +294,7 @@ function SharedProviders(props: ParentProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server-scoped providers plus the visual Layout (tabs/sidebar). These live inside
|
// Server-scoped providers shared by the legacy shell and the top-level new shell.
|
||||||
// each per-route server layout so they resolve to that route's server (selected vs
|
|
||||||
// draft). The Layout remounts when crossing between those groups.
|
|
||||||
type ServerScopedShellProps = ParentProps<{
|
type ServerScopedShellProps = ParentProps<{
|
||||||
directory?: () => string | undefined
|
directory?: () => string | undefined
|
||||||
sessionID?: () => string | undefined
|
sessionID?: () => string | undefined
|
||||||
@ -334,11 +320,23 @@ function LegacyServerScopedShell(props: ServerScopedShellProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NewServerScopedShell(props: ServerScopedShellProps) {
|
function NewAppLayout(props: ParentProps) {
|
||||||
return (
|
return (
|
||||||
<ServerScopedProviders directory={props.directory} sessionID={props.sessionID}>
|
<SelectedServerProviders>
|
||||||
|
<ServerScopedProviders>
|
||||||
<NewLayout>{props.children}</NewLayout>
|
<NewLayout>{props.children}</NewLayout>
|
||||||
</ServerScopedProviders>
|
</ServerScopedProviders>
|
||||||
|
</SelectedServerProviders>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TargetServerScopedProviders(props: ServerScopedShellProps) {
|
||||||
|
return (
|
||||||
|
<PermissionProvider directory={props.directory}>
|
||||||
|
<NotificationProvider directory={props.directory} sessionID={props.sessionID}>
|
||||||
|
<ModelsProvider>{props.children}</ModelsProvider>
|
||||||
|
</NotificationProvider>
|
||||||
|
</PermissionProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -524,11 +522,9 @@ export function AppInterface(props: {
|
|||||||
router?: Component<BaseRouterProps>
|
router?: Component<BaseRouterProps>
|
||||||
disableHealthCheck?: boolean
|
disableHealthCheck?: boolean
|
||||||
}) {
|
}) {
|
||||||
// The shared shell holds only server-agnostic providers (QueryClient + Settings/
|
// The visual new layout lives in the router root so it remains mounted across
|
||||||
// Command/Highlights) and stays mounted across every route. The server-scoped
|
// route changes. Draft and session routes override only their server-bound data
|
||||||
// providers and the visual Layout live in the per-route layouts below, so they
|
// providers beneath it.
|
||||||
// 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.
|
|
||||||
const ServerShell = (shellProps: ParentProps) => (
|
const ServerShell = (shellProps: ParentProps) => (
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<SharedProviders>
|
<SharedProviders>
|
||||||
@ -552,7 +548,11 @@ export function AppInterface(props: {
|
|||||||
component={props.router ?? Router}
|
component={props.router ?? Router}
|
||||||
root={(routerProps) => (
|
root={(routerProps) => (
|
||||||
<TabsProvider>
|
<TabsProvider>
|
||||||
<ServerShell>{routerProps.children}</ServerShell>
|
<ServerShell>
|
||||||
|
<Show when={useSettings().general.newLayoutDesigns()} fallback={routerProps.children}>
|
||||||
|
<NewAppLayout>{routerProps.children}</NewAppLayout>
|
||||||
|
</Show>
|
||||||
|
</ServerShell>
|
||||||
</TabsProvider>
|
</TabsProvider>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -578,14 +578,10 @@ function Routes() {
|
|||||||
<Route path="/session/:id?" component={SessionRoute} />
|
<Route path="/session/:id?" component={SessionRoute} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
<Route component={TargetServerLayout}>
|
|
||||||
<Show when={settings.general.newLayoutDesigns()}>
|
<Show when={settings.general.newLayoutDesigns()}>
|
||||||
{
|
|
||||||
<>
|
|
||||||
<Route path="/" component={NewHome} />
|
<Route path="/" component={NewHome} />
|
||||||
<Route path="/:dir" component={DirectoryLayout}>
|
|
||||||
<Route
|
<Route
|
||||||
path="/session/:id"
|
path="/:dir/session/:id"
|
||||||
component={() => {
|
component={() => {
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
@ -593,13 +589,9 @@ function Routes() {
|
|||||||
return <Navigate href={`/server/${server.key}/session/${id}`} />
|
return <Navigate href={`/server/${server.key}/session/${id}`} />
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Route>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</Show>
|
</Show>
|
||||||
<Route path="/new-session" component={DraftRoute} />
|
<Route path="/new-session" component={DraftRoute} />
|
||||||
<Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
|
<Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
|
||||||
</Route>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,6 @@ import { useGlobal } from "@/context/global"
|
|||||||
import { ServerConnection, useServer } from "@/context/server"
|
import { ServerConnection, useServer } from "@/context/server"
|
||||||
import { tabHref, useTabs } from "@/context/tabs"
|
import { tabHref, useTabs } from "@/context/tabs"
|
||||||
import "./titlebar.css"
|
import "./titlebar.css"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
|
||||||
import { Session } from "@opencode-ai/sdk/v2"
|
import { Session } from "@opencode-ai/sdk/v2"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { createTabPromptState } from "@/context/prompt"
|
import { createTabPromptState } from "@/context/prompt"
|
||||||
@ -262,7 +261,6 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Match when={useV2Titlebar()}>
|
<Match when={useV2Titlebar()}>
|
||||||
{(_) => {
|
{(_) => {
|
||||||
const serverSdk = useServerSDK()
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const global = useGlobal()
|
const global = useGlobal()
|
||||||
@ -273,11 +271,15 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
const [session] = createResource(
|
const [session] = createResource(
|
||||||
() => {
|
() => {
|
||||||
const route = layout.route()
|
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) =>
|
({ route, sdk }) =>
|
||||||
serverSdk()
|
sdk.client.session
|
||||||
.client.session.get({ sessionID: route.sessionId })
|
.get({ sessionID: route.sessionId })
|
||||||
.then((x) => x.data)
|
.then((x) => x.data)
|
||||||
.catch(() => {}),
|
.catch(() => {}),
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user