فهرست منبع

feat(app): add server-keyed session routes (#32570)

Brendan Allan 2 ماه پیش
والد
کامیت
0c4f508c50

+ 2 - 0
packages/app/e2e/smoke/session-timeline.fixture.ts

@@ -21,6 +21,7 @@ const words = [
   "vector",
   "vector",
 ]
 ]
 
 
+const serverKey = "http://127.0.0.1:4096"
 const sourceID = "ses_smoke_source"
 const sourceID = "ses_smoke_source"
 const targetID = "ses_smoke_target"
 const targetID = "ses_smoke_target"
 const directory = "C:/OpenCode/SmokeProject"
 const directory = "C:/OpenCode/SmokeProject"
@@ -240,6 +241,7 @@ function orderedParts(message: Message) {
 
 
 export const fixture = {
 export const fixture = {
   directory,
   directory,
+  serverKey,
   project: {
   project: {
     id: projectID,
     id: projectID,
     worktree: directory,
     worktree: directory,

+ 2 - 1
packages/app/e2e/smoke/session-timeline.spec.ts

@@ -718,7 +718,8 @@ 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) {
-  const href = `/${base64Encode(fixture.directory)}/session/${sessionID}`
+  console.log(process.env)
+  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()
   await tab.click()
   await tab.click()

+ 198 - 63
packages/app/src/app.tsx

@@ -10,7 +10,7 @@ import { Splash } from "@opencode-ai/ui/logo"
 import { ThemeProvider } from "@opencode-ai/ui/theme/context"
 import { ThemeProvider } from "@opencode-ai/ui/theme/context"
 import { MetaProvider } from "@solidjs/meta"
 import { MetaProvider } from "@solidjs/meta"
 import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router"
 import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router"
-import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
+import { keepPreviousData, QueryClient, QueryClientProvider, useQuery } from "@tanstack/solid-query"
 import { Effect } from "effect"
 import { Effect } from "effect"
 import {
 import {
   type Component,
   type Component,
@@ -30,7 +30,7 @@ import { Dynamic } from "solid-js/web"
 import { CommandProvider } from "@/context/command"
 import { CommandProvider } from "@/context/command"
 import { CommentsProvider } from "@/context/comments"
 import { CommentsProvider } from "@/context/comments"
 import { FileProvider } from "@/context/file"
 import { FileProvider } from "@/context/file"
-import { ServerSDKProvider } from "@/context/server-sdk"
+import { ServerSDKProvider, useServerSDK } from "@/context/server-sdk"
 import { ServerSyncProvider } from "@/context/server-sync"
 import { ServerSyncProvider } from "@/context/server-sync"
 import { GlobalProvider } from "@/context/global"
 import { GlobalProvider } from "@/context/global"
 import { HighlightsProvider } from "@/context/highlights"
 import { HighlightsProvider } from "@/context/highlights"
@@ -47,11 +47,14 @@ import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs"
 import { SDKProvider, useSDK } from "@/context/sdk"
 import { SDKProvider, useSDK } from "@/context/sdk"
 import { WslServersProvider } from "@/wsl/context"
 import { WslServersProvider } from "@/wsl/context"
 import DirectoryLayout, { DirectoryDataProvider } from "@/pages/directory-layout"
 import DirectoryLayout, { DirectoryDataProvider } from "@/pages/directory-layout"
-import Layout from "@/pages/layout"
+import LegacyLayout from "@/pages/layout"
+import NewLayout from "@/pages/layout-new"
 import { ErrorPage } from "./pages/error"
 import { ErrorPage } from "./pages/error"
 import { useCheckServerHealth } from "./utils/server-health"
 import { useCheckServerHealth } from "./utils/server-health"
+import { legacySessionHref, requireServerKey, rootSession, sessionHref } from "./utils/session-route"
 
 
-const HomeRoute = lazy(() => import("@/pages/home"))
+const LegacyHome = lazy(() => import("@/pages/home").then((module) => ({ default: module.LegacyHome })))
+const NewHome = lazy(() => import("@/pages/home").then((module) => ({ default: module.NewHome })))
 const Session = lazy(() => import("@/pages/session"))
 const Session = lazy(() => import("@/pages/session"))
 const NewSession = lazy(() => import("@/pages/new-session"))
 const NewSession = lazy(() => import("@/pages/new-session"))
 
 
@@ -64,6 +67,10 @@ const SessionRoute = Object.assign(
     const server = useServer()
     const server = useServer()
     const tabs = useTabs()
     const tabs = useTabs()
 
 
+    if (params.id && settings.general.newLayoutDesigns()) {
+      return <Navigate href={sessionHref(server.key, params.id)} />
+    }
+
     // When the new layout is enabled, the legacy new-session route (/:dir/session with no id)
     // 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=…
     // is replaced by a draft at /new-session?draftId=…
     createEffect(() => {
     createEffect(() => {
@@ -82,29 +89,55 @@ const SessionRoute = Object.assign(
   { preload: Session.preload },
   { preload: Session.preload },
 )
 )
 
 
+const TargetSessionRoute = Object.assign(
+  () => {
+    const sdk = useSDK()
+    const serverSDK = useServerSDK()
+    return (
+      <Show when={`${serverSDK().scope}\0${sdk().directory}`} keyed>
+        <SessionProviders>
+          <Session />
+        </SessionProviders>
+      </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/
 // Notification/Models + the visual Layout) for that server.
 // Notification/Models + the visual Layout) for that server.
-function SelectedServerLayout(props: ParentProps) {
+function SelectedServerProviders(props: ParentProps) {
   return (
   return (
     <ServerKey>
     <ServerKey>
       <ServerSDKProvider>
       <ServerSDKProvider>
-        <ServerSyncProvider>
-          <ServerScopedShell>{props.children}</ServerScopedShell>
-        </ServerSyncProvider>
+        <ServerSyncProvider>{props.children}</ServerSyncProvider>
       </ServerSDKProvider>
       </ServerSDKProvider>
     </ServerKey>
     </ServerKey>
   )
   )
 }
 }
 
 
+function LegacyServerLayout(props: ParentProps) {
+  return (
+    <SelectedServerProviders>
+      <LegacyServerScopedShell>{props.children}</LegacyServerScopedShell>
+    </SelectedServerProviders>
+  )
+}
+
 // Wraps /new-session. It resolves the draft's target server and provides the
 // 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
 // server-scoped shell for that server — without ServerKey, so the page never depends
 // on the globally "selected" server.
 // on the globally "selected" server.
-function DraftServerLayout(props: ParentProps) {
+function TargetServerLayout(props: ParentProps) {
   const server = useServer()
   const server = useServer()
   const tabs = useTabs()
   const tabs = useTabs()
+  const params = useParams<{ serverKey?: string }>()
   const [search] = useSearchParams<{ draftId?: string }>()
   const [search] = useSearchParams<{ draftId?: string }>()
   const conn = createMemo(() => {
   const conn = createMemo(() => {
+    if (params.serverKey) {
+      const key = requireServerKey(params.serverKey)
+      return server.list.find((item) => ServerConnection.key(item) === key)
+    }
     const id = search.draftId
     const id = search.draftId
     if (!id) return undefined
     if (!id) return undefined
     const draft = tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === id)
     const draft = tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === id)
@@ -115,48 +148,98 @@ function DraftServerLayout(props: ParentProps) {
   return (
   return (
     <ServerSDKProvider server={conn}>
     <ServerSDKProvider server={conn}>
       <ServerSyncProvider server={conn}>
       <ServerSyncProvider server={conn}>
-        <ServerScopedShell>{props.children}</ServerScopedShell>
+        <TargetDirectoryLayout>{props.children}</TargetDirectoryLayout>
       </ServerSyncProvider>
       </ServerSyncProvider>
     </ServerSDKProvider>
     </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(() => ({
+    queryKey: [serverSDK().scope, "session-route", params.id] as const,
+    enabled: !!params.serverKey && !!params.id,
+    placeholderData: keepPreviousData,
+    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) => 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 when={search.draftId} keyed fallback={<Navigate href="/" />}>
-        {(draftID) => <ResolvedDraftRoute draftID={draftID} />}
+        <ResolvedDraftRoute />
       </Show>
       </Show>
     </Show>
     </Show>
   )
   )
 }
 }
 
 
-function ResolvedDraftRoute(props: { draftID: string }) {
-  const tabs = useTabs()
-  const draft = createMemo(() =>
-    tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === props.draftID),
-  )
-
-  // Key on the directory so retargeting the draft's project re-instantiates the
-  // directory-scoped providers while keeping the same draft id. The draft's target
-  // server is provided by DraftServerLayout, so changing only the server updates the
-  // SDK/sync hooks without remounting the composer.
-  const directory = () => draft()?.directory
-
+function ResolvedDraftRoute() {
   return (
   return (
-    <Show when={directory()} keyed>
-      {(dir) => (
-        <SDKProvider directory={dir}>
-          <DirectoryDataProvider directory={dir} draftID={props.draftID}>
-            <DraftProviders>
-              <NewSession />
-            </DraftProviders>
-          </DirectoryDataProvider>
-        </SDKProvider>
-      )}
-    </Show>
+    <DraftProviders>
+      <NewSession />
+    </DraftProviders>
   )
   )
 }
 }
 
 
@@ -210,32 +293,51 @@ function BodyDesignClass() {
 // shell (router root) so they stay mounted regardless of the active server/route.
 // shell (router root) so they stay mounted regardless of the active server/route.
 function SharedProviders(props: ParentProps) {
 function SharedProviders(props: ParentProps) {
   return (
   return (
-    <SettingsProvider>
+    <>
       <BodyDesignClass />
       <BodyDesignClass />
       <CommandProvider>
       <CommandProvider>
         <HighlightsProvider>{props.children}</HighlightsProvider>
         <HighlightsProvider>{props.children}</HighlightsProvider>
       </CommandProvider>
       </CommandProvider>
-    </SettingsProvider>
+    </>
   )
   )
 }
 }
 
 
 // Server-scoped providers plus the visual Layout (tabs/sidebar). These live inside
 // 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
 // each per-route server layout so they resolve to that route's server (selected vs
 // draft). The Layout remounts when crossing between those groups.
 // draft). The Layout remounts when crossing between those groups.
-function ServerScopedShell(props: ParentProps) {
+type ServerScopedShellProps = ParentProps<{
+  directory?: () => string | undefined
+  sessionID?: () => string | undefined
+}>
+
+function ServerScopedProviders(props: ServerScopedShellProps) {
   return (
   return (
-    <PermissionProvider>
+    <PermissionProvider directory={props.directory}>
       <LayoutProvider>
       <LayoutProvider>
-        <NotificationProvider>
-          <ModelsProvider>
-            <Layout>{props.children}</Layout>
-          </ModelsProvider>
+        <NotificationProvider directory={props.directory} sessionID={props.sessionID}>
+          <ModelsProvider>{props.children}</ModelsProvider>
         </NotificationProvider>
         </NotificationProvider>
       </LayoutProvider>
       </LayoutProvider>
     </PermissionProvider>
     </PermissionProvider>
   )
   )
 }
 }
 
 
+function LegacyServerScopedShell(props: ServerScopedShellProps) {
+  return (
+    <ServerScopedProviders directory={props.directory} sessionID={props.sessionID}>
+      <LegacyLayout>{props.children}</LegacyLayout>
+    </ServerScopedProviders>
+  )
+}
+
+function NewServerScopedShell(props: ServerScopedShellProps) {
+  return (
+    <ServerScopedProviders directory={props.directory} sessionID={props.sessionID}>
+      <NewLayout>{props.children}</NewLayout>
+    </ServerScopedProviders>
+  )
+}
+
 function SessionProviders(props: ParentProps) {
 function SessionProviders(props: ParentProps) {
   return (
   return (
     <TerminalProvider>
     <TerminalProvider>
@@ -439,28 +541,61 @@ export function AppInterface(props: {
       servers={props.servers}
       servers={props.servers}
     >
     >
       <GlobalProvider>
       <GlobalProvider>
-        <ConnectionGate disableHealthCheck={props.disableHealthCheck}>
-          <Dynamic
-            component={props.router ?? Router}
-            root={(routerProps) => (
-              <TabsProvider>
-                <ServerShell>{routerProps.children}</ServerShell>
-              </TabsProvider>
-            )}
-          >
-            <Route component={SelectedServerLayout}>
-              <Route path="/" component={HomeRoute} />
-              <Route path="/:dir" component={DirectoryLayout}>
-                <Route path="/" component={() => <Navigate href="session" />} />
-                <Route path="/session/:id?" component={SessionRoute} />
-              </Route>
-            </Route>
-            <Route component={DraftServerLayout}>
-              <Route path="/new-session" component={DraftRoute} />
-            </Route>
-          </Dynamic>
-        </ConnectionGate>
+        <SettingsProvider>
+          <ConnectionGate disableHealthCheck={props.disableHealthCheck}>
+            <Show when={useSettings().general.newLayoutDesigns().toString()} keyed>
+              <Dynamic
+                component={props.router ?? Router}
+                root={(routerProps) => (
+                  <TabsProvider>
+                    <ServerShell>{routerProps.children}</ServerShell>
+                  </TabsProvider>
+                )}
+              >
+                <Routes />
+              </Dynamic>
+            </Show>
+          </ConnectionGate>
+        </SettingsProvider>
       </GlobalProvider>
       </GlobalProvider>
     </ServerProvider>
     </ServerProvider>
   )
   )
 }
 }
+
+function Routes() {
+  const settings = useSettings()
+
+  return (
+    <>
+      <Route component={LegacyServerLayout}>
+        <Show when={!settings.general.newLayoutDesigns()}>{<Route path="/" component={LegacyHome} />}</Show>
+        <Route path="/:dir" component={DirectoryLayout}>
+          <Route path="/" component={() => <Navigate href="session" />} />
+          <Route path="/session/:id?" component={SessionRoute} />
+        </Route>
+      </Route>
+      <Route component={TargetServerLayout}>
+        <Show when={settings.general.newLayoutDesigns()}>
+          {
+            <>
+              <Route path="/" component={NewHome} />
+              <Route path="/:dir" component={DirectoryLayout}>
+                <Route
+                  path="/session/:id"
+                  component={() => {
+                    const server = useServer()
+                    const { id } = useParams()
+
+                    return <Navigate href={`/server/${server.key}/session/${id}`} />
+                  }}
+                />
+              </Route>
+            </>
+          }
+        </Show>
+        <Route path="/new-session" component={DraftRoute} />
+        <Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
+      </Route>
+    </>
+  )
+}

+ 1 - 6
packages/app/src/components/prompt-input/submit.ts

@@ -388,12 +388,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
         local.session.promote(sessionDirectory, session.id)
         local.session.promote(sessionDirectory, session.id)
         layout.handoff.setTabs(base64Encode(sessionDirectory), session.id)
         layout.handoff.setTabs(base64Encode(sessionDirectory), session.id)
         const draftID = search.draftId
         const draftID = search.draftId
-        if (draftID)
-          tabs.promoteDraft(draftID, {
-            server: server.key,
-            dirBase64: base64Encode(sessionDirectory),
-            sessionId: session.id,
-          })
+        if (draftID) tabs.promoteDraft(draftID, { server: server.key, sessionId: session.id })
         else navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
         else navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
       }
       }
     }
     }

+ 55 - 50
packages/app/src/components/titlebar.tsx

@@ -29,7 +29,6 @@ import { useLanguage } from "@/context/language"
 import { useSettings } from "@/context/settings"
 import { useSettings } from "@/context/settings"
 import { WindowsAppMenu } from "./windows-app-menu"
 import { WindowsAppMenu } from "./windows-app-menu"
 import { applyPath, backPath, forwardPath } from "./titlebar-history"
 import { applyPath, backPath, forwardPath } from "./titlebar-history"
-import { useServerSync } from "@/context/server-sync"
 import { base64Encode } from "@opencode-ai/core/util/encode"
 import { base64Encode } from "@opencode-ai/core/util/encode"
 import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
 import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
 import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/layout/helpers"
 import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/layout/helpers"
@@ -38,10 +37,11 @@ import { makeEventListener } from "@solid-primitives/event-listener"
 import { createResizeObserver } from "@solid-primitives/resize-observer"
 import { createResizeObserver } from "@solid-primitives/resize-observer"
 import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "@/components/titlebar-session-events"
 import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "@/components/titlebar-session-events"
 import { useGlobal } from "@/context/global"
 import { useGlobal } from "@/context/global"
-import { decode64 } from "@/utils/base64"
 import { ServerConnection, useServer } from "@/context/server"
 import { ServerConnection, useServer } from "@/context/server"
-import { tabHref, useTabs, type Tab } 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"
 
 
 type TauriDesktopWindow = {
 type TauriDesktopWindow = {
   startDragging?: () => Promise<void>
   startDragging?: () => Promise<void>
@@ -252,7 +252,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
       <Switch>
       <Switch>
         <Match when={useV2Titlebar()}>
         <Match when={useV2Titlebar()}>
           {(_) => {
           {(_) => {
-            const serverSync = useServerSync()
+            const serverSdk = useServerSDK()
             const navigate = useNavigate()
             const navigate = useNavigate()
             const layout = useLayout()
             const layout = useLayout()
 
 
@@ -268,6 +268,17 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
             const tabs = useTabs()
             const tabs = useTabs()
             const tabsStore = tabs.store
             const tabsStore = tabs.store
             const tabsStoreActions = tabs
             const tabsStoreActions = tabs
+            const [session] = createResource(
+              () => {
+                const route = layout.route()
+                return route.type === "session" ? route : undefined
+              },
+              (route) =>
+                serverSdk()
+                  .client.session.get({ sessionID: route.sessionId })
+                  .then((x) => x.data)
+                  .catch(() => {}),
+            )
 
 
             const matchRoute = (route: LayoutRoute) => {
             const matchRoute = (route: LayoutRoute) => {
               if (route.type === "home") return
               if (route.type === "home") return
@@ -280,10 +291,9 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
                     item.type === "session" && item.server === route.server && item.sessionId === route.sessionId,
                     item.type === "session" && item.server === route.server && item.sessionId === route.sessionId,
                 )
                 )
                 if (main) return main
                 if (main) return main
-                const sync = serverSync().createDirSyncContext(route.dir)
-                const session = sync.session.get(route.sessionId)
-                if (session?.parentID) {
-                  const parentID = session.parentID
+                const s = session()
+                if (s?.parentID) {
+                  const parentID = s.parentID
                   const parent = tabsStore.find(
                   const parent = tabsStore.find(
                     (item) => item.type === "session" && item.server === route.server && item.sessionId === parentID,
                     (item) => item.type === "session" && item.server === route.server && item.sessionId === parentID,
                   )
                   )
@@ -304,15 +314,10 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
               }
               }
 
 
               if (route.type === "session") {
               if (route.type === "session") {
-                const sync = serverSync().createDirSyncContext(route.dir)
-                const session = sync.session.get(route.sessionId)
-                if (!session) return
-                const sessionId = session.parentID ?? session.id
-                const next = {
-                  server: route.server ?? server.key,
-                  dirBase64: route.dirBase64,
-                  sessionId,
-                }
+                const s = session()
+                if (!s) return
+                const sessionId = s.parentID ?? s.id
+                const next = { server: route.server ?? server.key, sessionId }
                 tabsStoreActions.addSessionTab(next)
                 tabsStoreActions.addSessionTab(next)
               }
               }
             })
             })
@@ -495,25 +500,38 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
                             )
                             )
                           }
                           }
 
 
+                          const [session] = createResource(
+                            () => tab.sessionId,
+                            (sessionID) =>
+                              serverSdk()
+                                .client.session.get({ sessionID })
+                                .then((x) => x.data)
+                                .catch(() => undefined),
+                          )
+
                           return (
                           return (
                             <>
                             <>
                               {divider()}
                               {divider()}
-                              <TabNavItem
-                                ref={ref}
-                                href={tabHref(tab)}
-                                server={tab.server}
-                                directory={decode64(tab.dirBase64)!}
-                                sessionId={tab.sessionId}
-                                onNavigate={() => {
-                                  tabs.select(tab)
-
-                                  ref.scrollIntoView({ behavior: "instant" })
-                                }}
-                                onClose={() => tabsStoreActions.removeTab(i())}
-                                active={currentTab() === tab}
-                                activeServer={tab.server === server.key}
-                                forceTruncate={tabsAreOverflowing()}
-                              />
+                              <Show when={session()}>
+                                {(session) => (
+                                  <TabNavItem
+                                    ref={ref}
+                                    href={tabHref(tab)}
+                                    server={tab.server}
+                                    sessionId={tab.sessionId}
+                                    session={session()}
+                                    onNavigate={() => {
+                                      tabs.select(tab)
+
+                                      ref.scrollIntoView({ behavior: "instant" })
+                                    }}
+                                    onClose={() => tabsStoreActions.removeTab(i())}
+                                    active={currentTab() === tab}
+                                    activeServer={tab.server === server.key}
+                                    forceTruncate={tabsAreOverflowing()}
+                                  />
+                                )}
+                              </Show>
                             </>
                             </>
                           )
                           )
                         }}
                         }}
@@ -793,7 +811,6 @@ function TabNavItem(props: {
   ref?: HTMLDivElement
   ref?: HTMLDivElement
   href: string
   href: string
   server: ServerConnection.Key
   server: ServerConnection.Key
-  directory: string
   sessionId?: string
   sessionId?: string
   hideClose?: boolean
   hideClose?: boolean
   onClose: () => void
   onClose: () => void
@@ -801,31 +818,19 @@ function TabNavItem(props: {
   active?: boolean
   active?: boolean
   activeServer: boolean
   activeServer: boolean
   forceTruncate?: boolean
   forceTruncate?: boolean
+  session: Session
 }) {
 }) {
   const closeTab = (event: MouseEvent) => {
   const closeTab = (event: MouseEvent) => {
     event.preventDefault()
     event.preventDefault()
     event.stopPropagation()
     event.stopPropagation()
     props.onClose()
     props.onClose()
   }
   }
+
   const global = useGlobal()
   const global = useGlobal()
   const serverCtx = createMemo(() => {
   const serverCtx = createMemo(() => {
     const conn = global.servers.list().find((item) => ServerConnection.key(item) === props.server)
     const conn = global.servers.list().find((item) => ServerConnection.key(item) === props.server)
     if (conn) return global.createServerCtx(conn)
     if (conn) return global.createServerCtx(conn)
   })
   })
-  const dirSyncCtx = createMemo(() => serverCtx()?.sync.createDirSyncContext(props.directory))
-
-  const [session] = createResource(
-    () => {
-      const ctx = dirSyncCtx()
-      if (!ctx || !props.sessionId) return
-      return [props.sessionId, ctx] as const
-    },
-    async ([sessionId, dirSyncCtx]) => {
-      await dirSyncCtx.session.sync(sessionId).catch(() => {})
-      return dirSyncCtx.session.get(sessionId)
-    },
-    { initialValue: props.sessionId ? dirSyncCtx()?.session.get(props.sessionId) : undefined },
-  )
 
 
   return (
   return (
     <div
     <div
@@ -837,7 +842,7 @@ function TabNavItem(props: {
         closeTab(event)
         closeTab(event)
       }}
       }}
     >
     >
-      <Show when={session.latest}>
+      <Show when={props.session}>
         {(session) => {
         {(session) => {
           const project = createMemo(() => projectForSession(session(), serverCtx()?.projects.list() ?? []))
           const project = createMemo(() => projectForSession(session(), serverCtx()?.projects.list() ?? []))
 
 
@@ -853,7 +858,7 @@ function TabNavItem(props: {
               <span data-slot="project-avatar-slot">
               <span data-slot="project-avatar-slot">
                 <ProjectTabAvatar
                 <ProjectTabAvatar
                   project={project()}
                   project={project()}
-                  directory={props.directory}
+                  directory={session().directory}
                   sessionId={session().id}
                   sessionId={session().id}
                   activeServer={props.activeServer}
                   activeServer={props.activeServer}
                 />
                 />

+ 4 - 1
packages/app/src/context/comments.tsx

@@ -2,12 +2,14 @@ import { batch, createMemo, createRoot, onCleanup } from "solid-js"
 import { createStore, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
 import { createStore, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useParams } from "@solidjs/router"
 import { useParams } from "@solidjs/router"
+import { base64Encode } from "@opencode-ai/core/util/encode"
 import { Persist, persisted } from "@/utils/persist"
 import { Persist, persisted } from "@/utils/persist"
 import { useServerSDK } from "./server-sdk"
 import { useServerSDK } from "./server-sdk"
 import type { ServerScope } from "@/utils/server-scope"
 import type { ServerScope } from "@/utils/server-scope"
 import { createScopedCache } from "@/utils/scoped-cache"
 import { createScopedCache } from "@/utils/scoped-cache"
 import { uuid } from "@/utils/uuid"
 import { uuid } from "@/utils/uuid"
 import type { SelectedLineRange } from "@/context/file"
 import type { SelectedLineRange } from "@/context/file"
+import { useSDK } from "./sdk"
 
 
 export type LineComment = {
 export type LineComment = {
   id: string
   id: string
@@ -202,6 +204,7 @@ export const { use: useComments, provider: CommentsProvider } = createSimpleCont
   gate: false,
   gate: false,
   init: () => {
   init: () => {
     const params = useParams()
     const params = useParams()
+    const sdk = useSDK()
     const serverSDK = useServerSDK()
     const serverSDK = useServerSDK()
     const cache = createScopedCache(
     const cache = createScopedCache(
       (key) => {
       (key) => {
@@ -228,7 +231,7 @@ export const { use: useComments, provider: CommentsProvider } = createSimpleCont
       return cache.get(key).value
       return cache.get(key).value
     }
     }
 
 
-    const session = createMemo(() => load(params.dir!, params.id))
+    const session = createMemo(() => load(base64Encode(sdk().directory), params.id))
 
 
     return {
     return {
       ready: () => session().ready(),
       ready: () => session().ready(),

+ 2 - 1
packages/app/src/context/file.tsx

@@ -3,6 +3,7 @@ import { createStore, produce, reconcile } from "solid-js/store"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { showToast } from "@/utils/toast"
 import { showToast } from "@/utils/toast"
 import { useParams } from "@solidjs/router"
 import { useParams } from "@solidjs/router"
+import { base64Encode } from "@opencode-ai/core/util/encode"
 import { getFilename } from "@opencode-ai/core/util/path"
 import { getFilename } from "@opencode-ai/core/util/path"
 import { useSDK } from "./sdk"
 import { useSDK } from "./sdk"
 import { useSync } from "./sync"
 import { useSync } from "./sync"
@@ -65,7 +66,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
     const scope = createMemo(() => sdk().directory)
     const scope = createMemo(() => sdk().directory)
     const path = createPathHelpers(scope)
     const path = createPathHelpers(scope)
     const tabs = layout.tabs(() =>
     const tabs = layout.tabs(() =>
-      SessionStateKey.from(serverSDK().scope, SessionRouteKey.fromRoute(params.dir, params.id)),
+      SessionStateKey.from(serverSDK().scope, SessionRouteKey.fromRoute(base64Encode(sdk().directory), params.id)),
     )
     )
 
 
     const inflight = new Map<string, Promise<void>>()
     const inflight = new Map<string, Promise<void>>()

+ 13 - 3
packages/app/src/context/layout.tsx

@@ -16,6 +16,7 @@ import { createPathHelpers } from "./file/path"
 import type { ProjectAvatarVariant } from "@opencode-ai/ui/v2/project-avatar-v2"
 import type { ProjectAvatarVariant } from "@opencode-ai/ui/v2/project-avatar-v2"
 import { migrateLegacySessionStateKeys, ServerScope, SessionStateKey } from "@/utils/server-scope"
 import { migrateLegacySessionStateKeys, ServerScope, SessionStateKey } from "@/utils/server-scope"
 import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
 import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
+import { requireServerKey } from "@/utils/session-route"
 
 
 export { createSessionKeyReader, ensureSessionKey, pruneSessionKeys }
 export { createSessionKeyReader, ensureSessionKey, pruneSessionKeys }
 
 
@@ -79,7 +80,7 @@ export type LayoutRoute =
   | { type: "home" }
   | { type: "home" }
   | { type: "draft"; draftID: string; server?: ServerConnection.Key }
   | { type: "draft"; draftID: string; server?: ServerConnection.Key }
   | { type: "dir-new-sesssion"; dir: string; dirBase64: 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 }
+  | { type: "session"; sessionId: string; server?: ServerConnection.Key }
 
 
 function nextSessionTabsForOpen(current: SessionTabs | undefined, tab: string): SessionTabs {
 function nextSessionTabsForOpen(current: SessionTabs | undefined, tab: string): SessionTabs {
   const all = current?.all ?? []
   const all = current?.all ?? []
@@ -131,6 +132,14 @@ const currentRoute = (pathname: string, search: string): LayoutRoute => {
     return { type: "draft", draftID }
     return { type: "draft", draftID }
   }
   }
 
 
+  if (parts[0] === "server" && parts[2] === "session" && parts[3]) {
+    return {
+      type: "session",
+      sessionId: parts[3],
+      server: requireServerKey(parts[1]),
+    }
+  }
+
   const dirBase64 = parts[0]
   const dirBase64 = parts[0]
   const dir = decode64(dirBase64)
   const dir = decode64(dirBase64)
   if (!dir) return { type: "home" }
   if (!dir) return { type: "home" }
@@ -138,7 +147,7 @@ const currentRoute = (pathname: string, search: string): LayoutRoute => {
   if (parts[1] !== "session") return { type: "home" }
   if (parts[1] !== "session") return { type: "home" }
 
 
   const id = parts[2]
   const id = parts[2]
-  if (id) return { type: "session", dir, dirBase64, sessionId: id }
+  if (id) return { type: "session", sessionId: id }
   return { type: "dir-new-sesssion", dir, dirBase64 }
   return { type: "dir-new-sesssion", dir, dirBase64 }
 }
 }
 
 
@@ -154,6 +163,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
     const route = createMemo(() => {
     const route = createMemo(() => {
       const value = currentRoute(location.pathname, location.search)
       const value = currentRoute(location.pathname, location.search)
       if (value.type === "home") return value
       if (value.type === "home") return value
+      if (value.server) return value
       return { ...value, server: server.key }
       return { ...value, server: server.key }
     })
     })
 
 
@@ -572,7 +582,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
       handoff: {
       handoff: {
         tabs: createMemo(() => store.handoff?.tabs),
         tabs: createMemo(() => store.handoff?.tabs),
         setTabs(dir: string, id: string) {
         setTabs(dir: string, id: string) {
-          setStore("handoff", "tabs", { scope: server.scope(), dir, id, at: Date.now() })
+          setStore("handoff", "tabs", { scope: serverSdk().scope, dir, id, at: Date.now() })
         },
         },
         clearTabs() {
         clearTabs() {
           if (!store.handoff?.tabs) return
           if (!store.handoff?.tabs) return

+ 4 - 4
packages/app/src/context/notification.tsx

@@ -1,5 +1,5 @@
 import { createStore, reconcile } from "solid-js/store"
 import { createStore, reconcile } from "solid-js/store"
-import { batch, createEffect, createMemo, onCleanup } from "solid-js"
+import { type Accessor, batch, createEffect, createMemo, onCleanup } from "solid-js"
 import { useParams } from "@solidjs/router"
 import { useParams } from "@solidjs/router"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useServerSDK } from "./server-sdk"
 import { useServerSDK } from "./server-sdk"
@@ -108,7 +108,7 @@ function buildNotificationIndex(list: Notification[]) {
 export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
 export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
   name: "Notification",
   name: "Notification",
   gate: false,
   gate: false,
-  init: () => {
+  init: (props: { directory?: Accessor<string | undefined>; sessionID?: Accessor<string | undefined> }) => {
     const params = useParams()
     const params = useParams()
     const serverSDK = useServerSDK()
     const serverSDK = useServerSDK()
     const serverSync = useServerSync()
     const serverSync = useServerSync()
@@ -119,10 +119,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
     const empty: Notification[] = []
     const empty: Notification[] = []
 
 
     const currentDirectory = createMemo(() => {
     const currentDirectory = createMemo(() => {
-      return decode64(params.dir)
+      return props.directory?.() ?? decode64(params.dir)
     })
     })
 
 
-    const currentSession = createMemo(() => params.id)
+    const currentSession = createMemo(() => props.sessionID?.() ?? params.id)
 
 
     const [store, setStore, _, ready] = persisted(
     const [store, setStore, _, ready] = persisted(
       Persist.serverGlobal(serverSDK().scope, "notification", ["notification.v1"]),
       Persist.serverGlobal(serverSDK().scope, "notification", ["notification.v1"]),

+ 4 - 4
packages/app/src/context/permission.tsx

@@ -1,4 +1,4 @@
-import { createEffect, createMemo, onCleanup } from "solid-js"
+import { type Accessor, createEffect, createMemo, onCleanup } from "solid-js"
 import { createStore, produce } from "solid-js/store"
 import { createStore, produce } from "solid-js/store"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
 import type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
@@ -47,13 +47,13 @@ function hasPermissionPromptRules(permission: unknown) {
 export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
 export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
   name: "Permission",
   name: "Permission",
   gate: false,
   gate: false,
-  init: () => {
+  init: (props: { directory?: Accessor<string | undefined> }) => {
     const params = useParams()
     const params = useParams()
     const serverSDK = useServerSDK()
     const serverSDK = useServerSDK()
     const serverSync = useServerSync()
     const serverSync = useServerSync()
 
 
     const permissionsEnabled = createMemo(() => {
     const permissionsEnabled = createMemo(() => {
-      const directory = decode64(params.dir)
+      const directory = props.directory?.() ?? decode64(params.dir)
       if (!directory) return false
       if (!directory) return false
       const [store] = serverSync().child(directory)
       const [store] = serverSync().child(directory)
       return hasPermissionPromptRules(store.config.permission)
       return hasPermissionPromptRules(store.config.permission)
@@ -85,7 +85,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
     // When config has permission: "allow", auto-enable directory-level auto-accept
     // When config has permission: "allow", auto-enable directory-level auto-accept
     createEffect(() => {
     createEffect(() => {
       if (!ready()) return
       if (!ready()) return
-      const directory = decode64(params.dir)
+      const directory = props.directory?.() ?? decode64(params.dir)
       if (!directory) return
       if (!directory) return
       const [childStore] = serverSync().child(directory)
       const [childStore] = serverSync().child(directory)
       const perm = childStore.config.permission
       const perm = childStore.config.permission

+ 4 - 2
packages/app/src/context/prompt.tsx

@@ -1,5 +1,5 @@
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createSimpleContext } from "@opencode-ai/ui/context"
-import { checksum } from "@opencode-ai/core/util/encode"
+import { base64Encode, checksum } from "@opencode-ai/core/util/encode"
 import { useParams, useSearchParams } from "@solidjs/router"
 import { useParams, useSearchParams } from "@solidjs/router"
 import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
 import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
 import { createStore, type SetStoreFunction } from "solid-js/store"
 import { createStore, type SetStoreFunction } from "solid-js/store"
@@ -7,6 +7,7 @@ import type { FileSelection } from "@/context/file"
 import { Persist, persisted } from "@/utils/persist"
 import { Persist, persisted } from "@/utils/persist"
 import { useServerSDK } from "./server-sdk"
 import { useServerSDK } from "./server-sdk"
 import type { ServerScope } from "@/utils/server-scope"
 import type { ServerScope } from "@/utils/server-scope"
+import { useSDK } from "./sdk"
 
 
 interface PartBase {
 interface PartBase {
   content: string
   content: string
@@ -256,6 +257,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
   gate: false,
   gate: false,
   init: () => {
   init: () => {
     const params = useParams()
     const params = useParams()
+    const sdk = useSDK()
     const [search] = useSearchParams<{ draftId?: string }>()
     const [search] = useSearchParams<{ draftId?: string }>()
     const serverSDK = useServerSDK()
     const serverSDK = useServerSDK()
     const cache = new Map<string, PromptCacheEntry>()
     const cache = new Map<string, PromptCacheEntry>()
@@ -303,7 +305,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
     }
     }
 
 
     const session = createMemo(() =>
     const session = createMemo(() =>
-      load(search.draftId ? { draftID: search.draftId } : { dir: params.dir!, id: params.id }),
+      load(search.draftId ? { draftID: search.draftId } : { dir: base64Encode(sdk().directory), id: params.id }),
     )
     )
     const pick = (scope?: Scope) => (scope ? load(scope) : session())
     const pick = (scope?: Scope) => (scope ? load(scope) : session())
 
 

+ 5 - 23
packages/app/src/context/tabs.tsx

@@ -1,6 +1,5 @@
 import type { Session } from "@opencode-ai/sdk/v2/client"
 import type { Session } from "@opencode-ai/sdk/v2/client"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createSimpleContext } from "@opencode-ai/ui/context"
-import { base64Encode } from "@opencode-ai/core/util/encode"
 import { createStore, produce } from "solid-js/store"
 import { createStore, produce } from "solid-js/store"
 import { Persist, persisted, removePersisted, draftPersistedKeys } from "@/utils/persist"
 import { Persist, persisted, removePersisted, draftPersistedKeys } from "@/utils/persist"
 import { ServerConnection, useServer } from "./server"
 import { ServerConnection, useServer } from "./server"
@@ -9,11 +8,11 @@ import { useLocation, useNavigate, useParams } from "@solidjs/router"
 import { usePlatform } from "./platform"
 import { usePlatform } from "./platform"
 import { uuid } from "@/utils/uuid"
 import { uuid } from "@/utils/uuid"
 import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events"
 import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events"
+import { sessionHref } from "@/utils/session-route"
 
 
 export type SessionTab = {
 export type SessionTab = {
   type: "session"
   type: "session"
   server: ServerConnection.Key
   server: ServerConnection.Key
-  dirBase64: string
   sessionId: string
   sessionId: string
 }
 }
 
 
@@ -34,16 +33,12 @@ type RecentTab = {
 export const draftHref = (draftID: string) => `/new-session?draftId=${encodeURIComponent(draftID)}`
 export const draftHref = (draftID: string) => `/new-session?draftId=${encodeURIComponent(draftID)}`
 
 
 export const tabHref = (tab: Tab) =>
 export const tabHref = (tab: Tab) =>
-  tab.type === "draft" ? draftHref(tab.draftID) : `/${tab.dirBase64}/session/${tab.sessionId}`
+  tab.type === "draft" ? draftHref(tab.draftID) : sessionHref(tab.server, tab.sessionId)
 
 
 export const tabKey = (tab: Tab) => (tab.type === "draft" ? `draft:${tab.draftID}` : `${tab.server}\n${tabHref(tab)}`)
 export const tabKey = (tab: Tab) => (tab.type === "draft" ? `draft:${tab.draftID}` : `${tab.server}\n${tabHref(tab)}`)
 
 
 export function sessionHasOpenTab(tabs: Tab[], server: ServerConnection.Key, session: Session) {
 export function sessionHasOpenTab(tabs: Tab[], server: ServerConnection.Key, session: Session) {
-  const dirBase64 = base64Encode(session.directory)
-  return tabs.some(
-    (tab) =>
-      tab.type === "session" && tab.server === server && tab.dirBase64 === dirBase64 && tab.sessionId === session.id,
-  )
+  return tabs.some((tab) => tab.type === "session" && tab.server === server && tab.sessionId === session.id)
 }
 }
 
 
 export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
 export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
@@ -105,14 +100,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
     const navigateTab = (tab: Tab) => {
     const navigateTab = (tab: Tab) => {
       const href = tabHref(tab)
       const href = tabHref(tab)
       setRecentKey(tabKey(tab))
       setRecentKey(tabKey(tab))
-      if (tab.server === server.key) {
-        navigate(href)
-        return
-      }
-      void startTransition(() => {
-        server.setActive(tab.server)
-        navigate(href)
-      })
+      navigate(href)
     }
     }
 
 
     const actions = {
     const actions = {
@@ -196,10 +184,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
         const removed = store
         const removed = store
           .filter(
           .filter(
             (tab) =>
             (tab) =>
-              tab.type === "session" &&
-              tab.server === server.key &&
-              atob(tab.dirBase64) === input.directory &&
-              input.sessionIDs.includes(tab.sessionId),
+              tab.type === "session" && tab.server === server.key && input.sessionIDs.includes(tab.sessionId),
           )
           )
           .map(tabKey)
           .map(tabKey)
         void startTransition(() => {
         void startTransition(() => {
@@ -211,7 +196,6 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
                   ? tabHref({
                   ? tabHref({
                       type: "session",
                       type: "session",
                       server: server.key,
                       server: server.key,
-                      dirBase64: params.dir,
                       sessionId: params.id,
                       sessionId: params.id,
                     })
                     })
                   : undefined
                   : undefined
@@ -224,14 +208,12 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
               const removedCurrent =
               const removedCurrent =
                 currentTab?.type === "session" &&
                 currentTab?.type === "session" &&
                 currentTab.server === server.key &&
                 currentTab.server === server.key &&
-                atob(currentTab.dirBase64) === input.directory &&
                 sessionIDs.has(currentTab.sessionId)
                 sessionIDs.has(currentTab.sessionId)
 
 
               for (let i = tabs.length - 1; i >= 0; i--) {
               for (let i = tabs.length - 1; i >= 0; i--) {
                 const tab = tabs[i]
                 const tab = tabs[i]
                 if (!tab || tab.type !== "session") continue
                 if (!tab || tab.type !== "session") continue
                 if (tab.server !== server.key) continue
                 if (tab.server !== server.key) continue
-                if (atob(tab.dirBase64) !== input.directory) continue
                 if (!sessionIDs.has(tab.sessionId)) continue
                 if (!sessionIDs.has(tab.sessionId)) continue
                 tabs.splice(i, 1)
                 tabs.splice(i, 1)
               }
               }

+ 7 - 5
packages/app/src/context/terminal.tsx

@@ -4,7 +4,8 @@ import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "soli
 import { useParams } from "@solidjs/router"
 import { useParams } from "@solidjs/router"
 import { useSDK, type DirectorySDK } from "./sdk"
 import { useSDK, type DirectorySDK } from "./sdk"
 import type { Platform } from "./platform"
 import type { Platform } from "./platform"
-import { useServer } from "./server"
+import { useServerSDK } from "./server-sdk"
+import { base64Encode } from "@opencode-ai/core/util/encode"
 import { defaultTitle, titleNumber } from "./terminal-title"
 import { defaultTitle, titleNumber } from "./terminal-title"
 import { Persist, persisted, removePersisted } from "@/utils/persist"
 import { Persist, persisted, removePersisted } from "@/utils/persist"
 import { ScopedKey, ServerScope, type ServerScope as ServerScopeValue } from "@/utils/server-scope"
 import { ScopedKey, ServerScope, type ServerScope as ServerScopeValue } from "@/utils/server-scope"
@@ -374,10 +375,11 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
   gate: false,
   gate: false,
   init: () => {
   init: () => {
     const sdk = useSDK()
     const sdk = useSDK()
-    const server = useServer()
+    const serverSDK = useServerSDK()
     const params = useParams()
     const params = useParams()
     const cache = new Map<string, TerminalCacheEntry>()
     const cache = new Map<string, TerminalCacheEntry>()
-    const scope = server.scope()
+    const scope = () => serverSDK().scope
+    const directory = createMemo(() => base64Encode(sdk().directory))
 
 
     caches.add(cache)
     caches.add(cache)
     onCleanup(() => caches.delete(cache))
     onCleanup(() => caches.delete(cache))
@@ -421,11 +423,11 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
       return entry.value
       return entry.value
     }
     }
 
 
-    const workspace = createMemo(() => loadWorkspace(params.dir!, params.id, scope))
+    const workspace = createMemo(() => loadWorkspace(directory(), params.id, scope()))
 
 
     createEffect(
     createEffect(
       on(
       on(
-        () => ({ dir: params.dir, id: params.id, scope }),
+        () => ({ dir: directory(), id: params.id, scope: scope() }),
         (next, prev) => {
         (next, prev) => {
           if (!prev?.dir) return
           if (!prev?.dir) return
           if (next.dir === prev.dir && next.id === prev.id && next.scope === prev.scope) return
           if (next.dir === prev.dir && next.id === prev.id && next.scope === prev.scope) return

+ 22 - 8
packages/app/src/pages/directory-layout.tsx

@@ -2,26 +2,40 @@ import { DataProvider } from "@opencode-ai/ui/context"
 import { showToast } from "@/utils/toast"
 import { showToast } from "@/utils/toast"
 import { base64Encode } from "@opencode-ai/core/util/encode"
 import { base64Encode } from "@opencode-ai/core/util/encode"
 import { useLocation, useNavigate, useParams } from "@solidjs/router"
 import { useLocation, useNavigate, useParams } from "@solidjs/router"
-import { createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js"
+import { type Accessor, createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js"
 import { useLanguage } from "@/context/language"
 import { useLanguage } from "@/context/language"
 import { LocalProvider } from "@/context/local"
 import { LocalProvider } from "@/context/local"
 import { SDKProvider } from "@/context/sdk"
 import { SDKProvider } from "@/context/sdk"
 import { useSync } from "@/context/sync"
 import { useSync } from "@/context/sync"
 import { decode64 } from "@/utils/base64"
 import { decode64 } from "@/utils/base64"
 import { Schema } from "effect"
 import { Schema } from "effect"
+import type { ServerConnection } from "@/context/server"
+import { sessionHref } from "@/utils/session-route"
 
 
-export function DirectoryDataProvider(props: ParentProps<{ directory: string; draftID?: string }>) {
+export function DirectoryDataProvider(
+  props: ParentProps<{
+    directory: string | Accessor<string>
+    draftID?: string
+    server?: Accessor<ServerConnection.Key | undefined>
+  }>,
+) {
   const location = useLocation()
   const location = useLocation()
   const navigate = useNavigate()
   const navigate = useNavigate()
   const params = useParams()
   const params = useParams()
   const sync = useSync()
   const sync = useSync()
-  const slug = createMemo(() => base64Encode(props.directory))
+  const directory = () => (typeof props.directory === "function" ? props.directory() : props.directory)
+  const slug = createMemo(() => base64Encode(directory()))
+  const href = (sessionID: string) => {
+    const server = props.server?.()
+    if (server) return sessionHref(server, sessionID)
+    return `/${slug()}/session/${sessionID}`
+  }
 
 
   createEffect(() => {
   createEffect(() => {
     // A draft lives at /new-session?draftId=… and has no directory segment to normalize.
     // A draft lives at /new-session?draftId=… and has no directory segment to normalize.
-    if (props.draftID) return
+    if (props.draftID || props.server?.()) return
     const next = sync().data.path.directory
     const next = sync().data.path.directory
-    if (!next || next === props.directory) return
+    if (!next || next === directory()) return
     const path = location.pathname.slice(slug().length + 1)
     const path = location.pathname.slice(slug().length + 1)
     navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
     navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
   })
   })
@@ -37,9 +51,9 @@ export function DirectoryDataProvider(props: ParentProps<{ directory: string; dr
   return (
   return (
     <DataProvider
     <DataProvider
       data={sync().data}
       data={sync().data}
-      directory={props.directory}
-      onNavigateToSession={(sessionID: string) => navigate(`/${slug()}/session/${sessionID}`)}
-      onSessionHref={(sessionID: string) => `/${slug()}/session/${sessionID}`}
+      directory={directory()}
+      onNavigateToSession={(sessionID: string) => navigate(href(sessionID))}
+      onSessionHref={href}
     >
     >
       <LocalProvider>{props.children}</LocalProvider>
       <LocalProvider>{props.children}</LocalProvider>
     </DataProvider>
     </DataProvider>

+ 6 - 16
packages/app/src/pages/home.tsx

@@ -43,7 +43,6 @@ import { sessionTitle } from "@/utils/session-title"
 import { pathKey } from "@/utils/path-key"
 import { pathKey } from "@/utils/path-key"
 import { useGlobal } from "@/context/global"
 import { useGlobal } from "@/context/global"
 import { useCommand } from "@/context/command"
 import { useCommand } from "@/context/command"
-import { useSettings } from "@/context/settings"
 import { ServerRowMenu } from "@/components/server/server-row-menu"
 import { ServerRowMenu } from "@/components/server/server-row-menu"
 import { ServerHealthIndicator } from "@/components/server/server-row"
 import { ServerHealthIndicator } from "@/components/server/server-row"
 import { type ServerHealth } from "@/utils/server-health"
 import { type ServerHealth } from "@/utils/server-health"
@@ -113,16 +112,7 @@ function homeSessionSearchKey(record: HomeSessionRecord) {
   return `${pathKey(record.session.directory)}:${record.session.id}`
   return `${pathKey(record.session.directory)}:${record.session.id}`
 }
 }
 
 
-export default function Home() {
-  const settings = useSettings()
-  return (
-    <Show when={settings.general.newLayoutDesigns()} fallback={<LegacyHome />}>
-      <HomeDesign />
-    </Show>
-  )
-}
-
-function HomeDesign() {
+export function NewHome() {
   const sync = useServerSync()
   const sync = useServerSync()
   const layout = useLayout()
   const layout = useLayout()
   const platform = usePlatform()
   const platform = usePlatform()
@@ -313,7 +303,7 @@ function HomeDesign() {
     const ctx = global.createServerCtx(conn)
     const ctx = global.createServerCtx(conn)
     ctx.projects.open(directory)
     ctx.projects.open(directory)
     ctx.projects.touch(directory)
     ctx.projects.touch(directory)
-    navigateOnServer(conn, `/${base64Encode(session.directory)}/session/${session.id}`)
+    navigateOnServer(conn, `/server/${base64Encode(ServerConnection.key(conn))}/session/${session.id}`)
   }
   }
 
 
   function chooseProject(conn: ServerConnection.Any) {
   function chooseProject(conn: ServerConnection.Any) {
@@ -416,7 +406,7 @@ function HomeDesign() {
                                 record={record}
                                 record={record}
                                 server={state.selection.server}
                                 server={state.selection.server}
                                 activeServer={state.selection.server === server.key}
                                 activeServer={state.selection.server === server.key}
-                                openSession={openSession}
+                                onClick={() => openSession(record.session)}
                               />
                               />
                             )}
                             )}
                           </For>
                           </For>
@@ -1024,7 +1014,7 @@ function HomeSessionRow(props: {
   record: HomeSessionRecord
   record: HomeSessionRecord
   server: ServerConnection.Key
   server: ServerConnection.Key
   activeServer: boolean
   activeServer: boolean
-  openSession: (session: Session) => void
+  onClick: () => void
 }) {
 }) {
   const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
   const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
 
 
@@ -1033,7 +1023,7 @@ function HomeSessionRow(props: {
       type="button"
       type="button"
       data-component="home-session-row"
       data-component="home-session-row"
       class={`${HOME_ROW} h-10 gap-2 px-6 py-3 pl-4`}
       class={`${HOME_ROW} h-10 gap-2 px-6 py-3 pl-4`}
-      onClick={() => props.openSession(props.record.session)}
+      onClick={props.onClick}
     >
     >
       <HomeSessionLeading
       <HomeSessionLeading
         project={props.record.project}
         project={props.record.project}
@@ -1093,7 +1083,7 @@ function groupSessions(records: HomeSessionRecord[], language: ReturnType<typeof
   ].filter((group) => group.sessions.length > 0)
   ].filter((group) => group.sessions.length > 0)
 }
 }
 
 
-function LegacyHome() {
+export function LegacyHome() {
   const sync = useServerSync()
   const sync = useServerSync()
   const platform = usePlatform()
   const platform = usePlatform()
   const pickDirectory = useDirectoryPicker()
   const pickDirectory = useDirectoryPicker()

+ 38 - 0
packages/app/src/pages/layout-new.tsx

@@ -0,0 +1,38 @@
+import { createEffect, type ParentProps } from "solid-js"
+import { useNavigate } from "@solidjs/router"
+import { DebugBar } from "@/components/debug-bar"
+import { HelpButton } from "@/components/help-button"
+import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
+import { usePlatform } from "@/context/platform"
+import { setNavigate } from "@/utils/notification-click"
+import { setV2Toast, ToastRegion } from "@/utils/toast"
+
+export default function NewLayout(props: ParentProps) {
+  const platform = usePlatform()
+  const navigate = useNavigate()
+  setNavigate(navigate)
+
+  createEffect(() => setV2Toast(true))
+
+  const update: TitlebarUpdate = {
+    version: () => {
+      const state = platform.updater?.state()
+      if (state?.status !== "ready") return
+      return state.version
+    },
+    installing: () => platform.updater?.state().status === "installing",
+    install: () => void platform.updater?.install(),
+  }
+
+  return (
+    <div class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
+      <Titlebar update={update} />
+      <main class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict">
+        {props.children}
+      </main>
+      {import.meta.env.DEV && <DebugBar />}
+      <HelpButton />
+      <ToastRegion v2 />
+    </div>
+  )
+}

+ 150 - 171
packages/app/src/pages/layout.tsx

@@ -13,7 +13,7 @@ import {
   type Accessor,
   type Accessor,
 } from "solid-js"
 } from "solid-js"
 import { makeEventListener } from "@solid-primitives/event-listener"
 import { makeEventListener } from "@solid-primitives/event-listener"
-import { useLocation, useNavigate, useParams } from "@solidjs/router"
+import { useNavigate, useParams } from "@solidjs/router"
 import { useLayout, LocalProject } from "@/context/layout"
 import { useLayout, LocalProject } from "@/context/layout"
 import { useServerSync } from "@/context/server-sync"
 import { useServerSync } from "@/context/server-sync"
 import { Persist, persisted } from "@/utils/persist"
 import { Persist, persisted } from "@/utils/persist"
@@ -92,7 +92,7 @@ import {
 import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project"
 import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project"
 import { SidebarContent } from "./layout/sidebar-shell"
 import { SidebarContent } from "./layout/sidebar-shell"
 
 
-export default function Layout(props: ParentProps) {
+export default function LegacyLayout(props: ParentProps) {
   const serverSDK = useServerSDK()
   const serverSDK = useServerSDK()
   const [store, setStore, , ready] = persisted(
   const [store, setStore, , ready] = persisted(
     Persist.serverGlobal(serverSDK().scope, "layout.page", ["layout.page.v1"]),
     Persist.serverGlobal(serverSDK().scope, "layout.page", ["layout.page.v1"]),
@@ -131,10 +131,8 @@ export default function Layout(props: ParentProps) {
   const command = useCommand()
   const command = useCommand()
   const theme = useTheme()
   const theme = useTheme()
   const language = useLanguage()
   const language = useLanguage()
-  const newDesign = createMemo(() => settings.general.newLayoutDesigns())
-  createEffect(() => setV2Toast(newDesign()))
+  createEffect(() => setV2Toast(false))
   const initialDirectory = decode64(params.dir)
   const initialDirectory = decode64(params.dir)
-  const location = useLocation()
   const route = createMemo(() => {
   const route = createMemo(() => {
     const slug = params.dir
     const slug = params.dir
     if (!slug) return { slug, dir: "" }
     if (!slug) return { slug, dir: "" }
@@ -158,7 +156,7 @@ export default function Layout(props: ParentProps) {
   const currentDir = createMemo(() => route().dir)
   const currentDir = createMemo(() => route().dir)
 
 
   const [state, setState] = createStore({
   const [state, setState] = createStore({
-    autoselect: !initialDirectory && !newDesign(),
+    autoselect: !initialDirectory,
     busyWorkspaces: {} as Record<string, boolean>,
     busyWorkspaces: {} as Record<string, boolean>,
     hoverProject: undefined as string | undefined,
     hoverProject: undefined as string | undefined,
     scrollSessionKey: undefined as string | undefined,
     scrollSessionKey: undefined as string | undefined,
@@ -996,7 +994,7 @@ export default function Layout(props: ParentProps) {
         id: "sidebar.toggle",
         id: "sidebar.toggle",
         title: language.t("command.sidebar.toggle"),
         title: language.t("command.sidebar.toggle"),
         category: language.t("command.category.view"),
         category: language.t("command.category.view"),
-        keybind: newDesign() ? undefined : "mod+b",
+        keybind: "mod+b",
         onSelect: () => layout.sidebar.toggle(),
         onSelect: () => layout.sidebar.toggle(),
       },
       },
       {
       {
@@ -1134,20 +1132,19 @@ export default function Layout(props: ParentProps) {
       },
       },
     ]
     ]
 
 
-    if (!newDesign())
-      Array.from({ length: 9 }, (_, i) => {
-        const index = i
-        const number = index + 1
-        commands.push({
-          id: `project.${number}`,
-          category: language.t("command.category.project"),
-          title: `Open Project {number}`,
-          keybind: `mod+${number}`,
-          disabled: layout.projects.list().length <= index,
-          hidden: true,
-          onSelect: () => navigateToProjectIndex(index),
-        })
+    Array.from({ length: 9 }, (_, i) => {
+      const index = i
+      const number = index + 1
+      commands.push({
+        id: `project.${number}`,
+        category: language.t("command.category.project"),
+        title: `Open Project {number}`,
+        keybind: `mod+${number}`,
+        disabled: layout.projects.list().length <= index,
+        hidden: true,
+        onSelect: () => navigateToProjectIndex(index),
       })
       })
+    })
 
 
     for (const [id] of availableThemeEntries()) {
     for (const [id] of availableThemeEntries()) {
       commands.push({
       commands.push({
@@ -1812,7 +1809,7 @@ export default function Layout(props: ParentProps) {
   createEffect(() => {
   createEffect(() => {
     document.documentElement.style.setProperty(
     document.documentElement.style.setProperty(
       "--dialog-left-margin",
       "--dialog-left-margin",
-      newDesign() ? "0px" : `${layout.sidebar.opened() ? layout.sidebar.width() : 48}px`,
+      `${layout.sidebar.opened() ? layout.sidebar.width() : 48}px`,
     )
     )
   })
   })
 
 
@@ -2355,176 +2352,158 @@ export default function Layout(props: ParentProps) {
   )
   )
 
 
   return (
   return (
-    <Show
-      when={!newDesign()}
-      fallback={
-        <div class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
-          {autoselecting() ?? ""}
-          <Titlebar update={titlebarUpdate} />
-          <main class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict">
-            <Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
-              {props.children}
-            </Show>
-          </main>
-          {import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && <DebugBar />}
-          <HelpButton />
-          <ToastRegion v2={newDesign()} />
-        </div>
-      }
-    >
-      <div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
-        {autoselecting() ?? ""}
-        <Titlebar update={titlebarUpdate} />
-        <Show when={updateVersion() !== undefined}>
-          <UpdateAvailableToast version={updateVersion() ?? ""} install={installUpdate} language={language} />
-        </Show>
-        <div class="flex-1 min-h-0 min-w-0 flex">
-          <div class="flex-1 min-h-0 relative">
-            <div class="size-full relative overflow-x-hidden">
-              <nav
-                aria-label={language.t("sidebar.nav.projectsAndSessions")}
-                data-component="sidebar-nav-desktop"
-                classList={{
-                  "hidden xl:block": true,
-                  "absolute inset-y-0 left-0": true,
-                  "z-10": true,
-                }}
-                style={{ width: `${side()}px` }}
-                ref={(el) => {
-                  setState("nav", el)
-                }}
-                onMouseEnter={() => {
-                  disarm()
-                }}
-                onMouseLeave={() => {
-                  aim.reset()
-                  if (!sidebarHovering()) return
-
-                  arm()
-                }}
-              >
-                <div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
-              </nav>
-
-              <Show when={layout.sidebar.opened()}>
-                <div
-                  class="hidden xl:block absolute inset-y-0 z-30 w-0 overflow-visible"
-                  style={{ left: `${side()}px` }}
-                  onPointerDown={() => setState("sizing", true)}
-                >
-                  <ResizeHandle
-                    direction="horizontal"
-                    size={layout.sidebar.width()}
-                    min={244}
-                    max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
-                    onResize={(w) => {
-                      setState("sizing", true)
-                      if (sizet !== undefined) clearTimeout(sizet)
-                      sizet = window.setTimeout(() => setState("sizing", false), 120)
-                      layout.sidebar.resize(w)
-                    }}
-                  />
-                </div>
-              </Show>
-
+    <div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
+      {autoselecting() ?? ""}
+      <Titlebar update={titlebarUpdate} />
+      <Show when={updateVersion() !== undefined}>
+        <UpdateAvailableToast version={updateVersion() ?? ""} install={installUpdate} language={language} />
+      </Show>
+      <div class="flex-1 min-h-0 min-w-0 flex">
+        <div class="flex-1 min-h-0 relative">
+          <div class="size-full relative overflow-x-hidden">
+            <nav
+              aria-label={language.t("sidebar.nav.projectsAndSessions")}
+              data-component="sidebar-nav-desktop"
+              classList={{
+                "hidden xl:block": true,
+                "absolute inset-y-0 left-0": true,
+                "z-10": true,
+              }}
+              style={{ width: `${side()}px` }}
+              ref={(el) => {
+                setState("nav", el)
+              }}
+              onMouseEnter={() => {
+                disarm()
+              }}
+              onMouseLeave={() => {
+                aim.reset()
+                if (!sidebarHovering()) return
+
+                arm()
+              }}
+            >
+              <div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
+            </nav>
+
+            <Show when={layout.sidebar.opened()}>
               <div
               <div
-                class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
-                style={{ left: "calc(4rem + 12px)" }}
-              />
-
-              <div class="xl:hidden">
-                <div
-                  classList={{
-                    "fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
-                    "opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
-                    "opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
-                  }}
-                  onClick={(e) => {
-                    if (e.target === e.currentTarget) layout.mobileSidebar.hide()
+                class="hidden xl:block absolute inset-y-0 z-30 w-0 overflow-visible"
+                style={{ left: `${side()}px` }}
+                onPointerDown={() => setState("sizing", true)}
+              >
+                <ResizeHandle
+                  direction="horizontal"
+                  size={layout.sidebar.width()}
+                  min={244}
+                  max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
+                  onResize={(w) => {
+                    setState("sizing", true)
+                    if (sizet !== undefined) clearTimeout(sizet)
+                    sizet = window.setTimeout(() => setState("sizing", false), 120)
+                    layout.sidebar.resize(w)
                   }}
                   }}
                 />
                 />
-                <nav
-                  aria-label={language.t("sidebar.nav.projectsAndSessions")}
-                  data-component="sidebar-nav-mobile"
-                  classList={{
-                    "@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
-                    "translate-x-0": layout.mobileSidebar.opened(),
-                    "-translate-x-full": !layout.mobileSidebar.opened(),
-                  }}
-                  onClick={(e) => e.stopPropagation()}
-                >
-                  {sidebarContent(true)}
-                </nav>
               </div>
               </div>
+            </Show>
 
 
-              <div
-                classList={{
-                  "absolute inset-0": true,
-                  "xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
-                  "z-20": true,
-                  "transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
-                    !state.sizing,
-                }}
-                style={{
-                  "--main-left": layout.sidebar.opened() ? `${side()}px` : "4rem",
-                }}
-              >
-                <main
-                  classList={{
-                    "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
-                  }}
-                >
-                  <Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
-                    {props.children}
-                  </Show>
-                </main>
-              </div>
+            <div
+              class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
+              style={{ left: "calc(4rem + 12px)" }}
+            />
 
 
+            <div class="xl:hidden">
               <div
               <div
                 classList={{
                 classList={{
-                  "hidden xl:flex absolute inset-y-0 left-16 z-30": true,
-                  "opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(),
-                  "opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(),
-                  "transition-[opacity,transform] motion-reduce:transition-none": true,
-                  "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
-                  "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
+                  "fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
+                  "opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
+                  "opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
                 }}
                 }}
-                onMouseMove={disarm}
-                onMouseEnter={() => {
-                  disarm()
-                  aim.reset()
+                onClick={(e) => {
+                  if (e.target === e.currentTarget) layout.mobileSidebar.hide()
                 }}
                 }}
-                onPointerDown={disarm}
-                onMouseLeave={() => {
-                  arm()
+              />
+              <nav
+                aria-label={language.t("sidebar.nav.projectsAndSessions")}
+                data-component="sidebar-nav-mobile"
+                classList={{
+                  "@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
+                  "translate-x-0": layout.mobileSidebar.opened(),
+                  "-translate-x-full": !layout.mobileSidebar.opened(),
                 }}
                 }}
+                onClick={(e) => e.stopPropagation()}
               >
               >
-                <Show when={peekProject()}>
-                  <SidebarPanel project={peekProject} merged={false} />
-                </Show>
-              </div>
+                {sidebarContent(true)}
+              </nav>
+            </div>
 
 
-              <div
+            <div
+              classList={{
+                "absolute inset-0": true,
+                "xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
+                "z-20": true,
+                "transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
+                  !state.sizing,
+              }}
+              style={{
+                "--main-left": layout.sidebar.opened() ? `${side()}px` : "4rem",
+              }}
+            >
+              <main
                 classList={{
                 classList={{
-                  "hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
-                  "opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(),
-                  "opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(),
-                  "transition-[opacity,transform] motion-reduce:transition-none": true,
-                  "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
-                  "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
+                  "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
                 }}
                 }}
-                style={{ left: `calc(4rem + ${panel()}px)` }}
               >
               >
-                <div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
-              </div>
+                <Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
+                  {props.children}
+                </Show>
+              </main>
+            </div>
+
+            <div
+              classList={{
+                "hidden xl:flex absolute inset-y-0 left-16 z-30": true,
+                "opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(),
+                "opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(),
+                "transition-[opacity,transform] motion-reduce:transition-none": true,
+                "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
+                "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
+              }}
+              onMouseMove={disarm}
+              onMouseEnter={() => {
+                disarm()
+                aim.reset()
+              }}
+              onPointerDown={disarm}
+              onMouseLeave={() => {
+                arm()
+              }}
+            >
+              <Show when={peekProject()}>
+                <SidebarPanel project={peekProject} merged={false} />
+              </Show>
+            </div>
+
+            <div
+              classList={{
+                "hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
+                "opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(),
+                "opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(),
+                "transition-[opacity,transform] motion-reduce:transition-none": true,
+                "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
+                "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
+              }}
+              style={{ left: `calc(4rem + ${panel()}px)` }}
+            >
+              <div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
             </div>
             </div>
           </div>
           </div>
-          {import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && <DebugBar />}
         </div>
         </div>
-        <HelpButton />
-        <ToastRegion v2={newDesign()} />
+        {import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && <DebugBar />}
       </div>
       </div>
-    </Show>
+      <HelpButton />
+      <ToastRegion v2={false} />
+    </div>
   )
   )
 }
 }
 
 

+ 5 - 7
packages/app/src/pages/session.tsx

@@ -28,7 +28,7 @@ import { createAutoScroll } from "@opencode-ai/ui/hooks"
 import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
 import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
 import { Button } from "@opencode-ai/ui/button"
 import { Button } from "@opencode-ai/ui/button"
 import { showToast } from "@/utils/toast"
 import { showToast } from "@/utils/toast"
-import { checksum } from "@opencode-ai/core/util/encode"
+import { base64Encode, checksum } from "@opencode-ai/core/util/encode"
 import { useLocation, useSearchParams } from "@solidjs/router"
 import { useLocation, useSearchParams } from "@solidjs/router"
 import { NewSessionView, SessionHeader } from "@/components/session"
 import { NewSessionView, SessionHeader } from "@/components/session"
 import { useComments } from "@/context/comments"
 import { useComments } from "@/context/comments"
@@ -56,7 +56,6 @@ import { MessageTimeline } from "@/pages/session/timeline/message-timeline"
 import { createTimelineModel } from "@/pages/session/timeline/model"
 import { createTimelineModel } from "@/pages/session/timeline/model"
 import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
 import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
 import { useSessionLayout } from "@/pages/session/session-layout"
 import { useSessionLayout } from "@/pages/session/session-layout"
-import { useServer } from "@/context/server"
 import { syncSessionModel } from "@/pages/session/session-model-helpers"
 import { syncSessionModel } from "@/pages/session/session-model-helpers"
 import { SessionSidePanel } from "@/pages/session/session-side-panel"
 import { SessionSidePanel } from "@/pages/session/session-side-panel"
 import { TerminalPanel } from "@/pages/session/terminal-panel"
 import { TerminalPanel } from "@/pages/session/terminal-panel"
@@ -92,7 +91,6 @@ export default function Page() {
   const prompt = usePrompt()
   const prompt = usePrompt()
   const comments = useComments()
   const comments = useComments()
   const terminal = useTerminal()
   const terminal = useTerminal()
-  const server = useServer()
   const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
   const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
   const location = useLocation()
   const location = useLocation()
   const { params, sessionKey, workspaceKey, tabs, view } = useSessionLayout()
   const { params, sessionKey, workspaceKey, tabs, view } = useSessionLayout()
@@ -137,11 +135,11 @@ export default function Page() {
           layout.handoff.clearTabs()
           layout.handoff.clearTabs()
           return
           return
         }
         }
-        if (pending.scope !== server.scope()) return
+        if (pending.scope !== serverSDK().scope) return
 
 
         if (pending.id !== id) return
         if (pending.id !== id) return
         layout.handoff.clearTabs()
         layout.handoff.clearTabs()
-        if (pending.dir !== (params.dir ?? "")) return
+        if (pending.dir !== base64Encode(sdk().directory)) return
 
 
         const from = workspaceTabs().tabs()
         const from = workspaceTabs().tabs()
         if (from.all.length === 0 && !from.active) return
         if (from.all.length === 0 && !from.active) return
@@ -247,7 +245,7 @@ export default function Page() {
 
 
   createEffect(
   createEffect(
     on(
     on(
-      () => ({ dir: params.dir, id: params.id }),
+      () => ({ dir: sdk().directory, id: params.id }),
       (next, prev) => {
       (next, prev) => {
         if (!prev) return
         if (!prev) return
         if (next.dir === prev.dir && next.id === prev.id) return
         if (next.dir === prev.dir && next.id === prev.id) return
@@ -575,7 +573,7 @@ export default function Page() {
 
 
   createEffect(
   createEffect(
     on(
     on(
-      () => params.dir,
+      () => sdk().directory,
       (dir) => {
       (dir) => {
         if (!dir) return
         if (!dir) return
         setStore("newSessionWorktree", "main")
         setStore("newSessionWorktree", "main")

+ 6 - 1
packages/app/src/pages/session/composer/session-composer-region.tsx

@@ -29,6 +29,7 @@ import { useServer } from "@/context/server"
 import { useTabs } from "@/context/tabs"
 import { useTabs } from "@/context/tabs"
 import { useDirectoryPicker } from "@/components/directory-picker"
 import { useDirectoryPicker } from "@/components/directory-picker"
 import { base64Encode } from "@opencode-ai/core/util/encode"
 import { base64Encode } from "@opencode-ai/core/util/encode"
+import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route"
 
 
 export function SessionComposerRegion(props: {
 export function SessionComposerRegion(props: {
   state: SessionComposerState
   state: SessionComposerState
@@ -200,7 +201,11 @@ export function SessionComposerRegion(props: {
   const openParent = () => {
   const openParent = () => {
     const id = parentID()
     const id = parentID()
     if (!id) return
     if (!id) return
-    navigate(`/${route.params.dir}/session/${id}`)
+    navigate(
+      route.params.serverKey
+        ? sessionHref(requireServerKey(route.params.serverKey), id)
+        : legacySessionHref(sdk().directory, id),
+    )
   }
   }
 
 
   createEffect(() => {
   createEffect(() => {

+ 9 - 5
packages/app/src/pages/session/session-layout.ts

@@ -1,15 +1,19 @@
 import { useParams } from "@solidjs/router"
 import { useParams } from "@solidjs/router"
 import { createMemo } from "solid-js"
 import { createMemo } from "solid-js"
 import { useLayout } from "@/context/layout"
 import { useLayout } from "@/context/layout"
-import { useServer } from "@/context/server"
 import { SessionRouteKey, SessionStateKey } from "@/utils/server-scope"
 import { SessionRouteKey, SessionStateKey } from "@/utils/server-scope"
+import { useSDK } from "@/context/sdk"
+import { useServerSDK } from "@/context/server-sdk"
+import { base64Encode } from "@opencode-ai/core/util/encode"
 
 
 export const useSessionKey = () => {
 export const useSessionKey = () => {
   const params = useParams()
   const params = useParams()
-  const server = useServer()
-  const scope = createMemo(() => server.scope())
-  const workspaceKey = createMemo(() => SessionStateKey.from(scope(), SessionRouteKey.fromRoute(params.dir)))
-  const sessionKey = createMemo(() => SessionStateKey.from(scope(), SessionRouteKey.fromRoute(params.dir, params.id)))
+  const sdk = useSDK()
+  const serverSDK = useServerSDK()
+  const scope = createMemo(() => serverSDK().scope)
+  const directory = createMemo(() => base64Encode(sdk().directory))
+  const workspaceKey = createMemo(() => SessionStateKey.from(scope(), SessionRouteKey.fromRoute(directory())))
+  const sessionKey = createMemo(() => SessionStateKey.from(scope(), SessionRouteKey.fromRoute(directory(), params.id)))
   return { params, sessionKey, workspaceKey }
   return { params, sessionKey, workspaceKey }
 }
 }
 
 

+ 5 - 3
packages/app/src/pages/session/terminal-panel.tsx

@@ -16,6 +16,7 @@ import { useLanguage } from "@/context/language"
 import { useLayout } from "@/context/layout"
 import { useLayout } from "@/context/layout"
 import { useSettings } from "@/context/settings"
 import { useSettings } from "@/context/settings"
 import { useTerminal } from "@/context/terminal"
 import { useTerminal } from "@/context/terminal"
+import { useSDK } from "@/context/sdk"
 import { terminalTabLabel } from "@/pages/session/terminal-label"
 import { terminalTabLabel } from "@/pages/session/terminal-label"
 import { createSizing, focusTerminalById } from "@/pages/session/helpers"
 import { createSizing, focusTerminalById } from "@/pages/session/helpers"
 import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff"
 import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff"
@@ -25,10 +26,11 @@ export function TerminalPanel() {
   const delays = [120, 240]
   const delays = [120, 240]
   const layout = useLayout()
   const layout = useLayout()
   const terminal = useTerminal()
   const terminal = useTerminal()
+  const sdk = useSDK()
   const language = useLanguage()
   const language = useLanguage()
   const command = useCommand()
   const command = useCommand()
   const settings = useSettings()
   const settings = useSettings()
-  const { params, workspaceKey, view } = useSessionLayout()
+  const { workspaceKey, view } = useSessionLayout()
 
 
   const opened = createMemo(() => view().terminal.opened())
   const opened = createMemo(() => view().terminal.opened())
   const size = createSizing()
   const size = createSizing()
@@ -122,7 +124,7 @@ export function TerminalPanel() {
   })
   })
 
 
   createEffect(() => {
   createEffect(() => {
-    const dir = params.dir
+    const dir = sdk().directory
     if (!dir) return
     if (!dir) return
     if (!terminal.ready()) return
     if (!terminal.ready()) return
     language.locale()
     language.locale()
@@ -140,7 +142,7 @@ export function TerminalPanel() {
   })
   })
 
 
   const handoff = createMemo(() => {
   const handoff = createMemo(() => {
-    const dir = params.dir
+    const dir = sdk().directory
     if (!dir) return []
     if (!dir) return []
     return getTerminalHandoff(workspaceKey()) ?? []
     return getTerminalHandoff(workspaceKey()) ?? []
   })
   })

+ 14 - 3
packages/app/src/pages/session/timeline/message-timeline.tsx

@@ -62,6 +62,8 @@ import { useSessionKey } from "@/pages/session/session-layout"
 import { useServerSDK } from "@/context/server-sdk"
 import { useServerSDK } from "@/context/server-sdk"
 import { usePlatform } from "@/context/platform"
 import { usePlatform } from "@/context/platform"
 import { useSettings } from "@/context/settings"
 import { useSettings } from "@/context/settings"
+import { useTabs } from "@/context/tabs"
+import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route"
 import { useSDK } from "@/context/sdk"
 import { useSDK } from "@/context/sdk"
 import { useSync } from "@/context/sync"
 import { useSync } from "@/context/sync"
 import { notifySessionTabsRemoved } from "@/components/titlebar-session-events"
 import { notifySessionTabsRemoved } from "@/components/titlebar-session-events"
@@ -260,6 +262,7 @@ export function MessageTimeline(props: {
   const sdk = useSDK()
   const sdk = useSDK()
   const sync = useSync()
   const sync = useSync()
   const settings = useSettings()
   const settings = useSettings()
+  const tabs = useTabs()
   const dialog = useDialog()
   const dialog = useDialog()
   const language = useLanguage()
   const language = useLanguage()
   const { params, sessionKey } = useSessionKey()
   const { params, sessionKey } = useSessionKey()
@@ -757,12 +760,18 @@ export function MessageTimeline(props: {
 
 
   const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
   const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
     if (params.id !== sessionID) return
     if (params.id !== sessionID) return
+    const href = (id: string) =>
+      params.serverKey ? sessionHref(requireServerKey(params.serverKey), id) : legacySessionHref(sdk().directory, id)
     if (parentID) {
     if (parentID) {
-      navigate(`/${params.dir}/session/${parentID}`)
+      navigate(href(parentID))
       return
       return
     }
     }
     if (nextSessionID) {
     if (nextSessionID) {
-      navigate(`/${params.dir}/session/${nextSessionID}`)
+      navigate(href(nextSessionID))
+      return
+    }
+    if (params.serverKey) {
+      tabs.newDraft({ server: requireServerKey(params.serverKey), directory: sdk().directory })
       return
       return
     }
     }
     navigate(`/${params.dir}/session`)
     navigate(`/${params.dir}/session`)
@@ -864,7 +873,9 @@ export function MessageTimeline(props: {
   const navigateParent = () => {
   const navigateParent = () => {
     const id = parentID()
     const id = parentID()
     if (!id) return
     if (!id) return
-    navigate(`/${params.dir}/session/${id}`)
+    navigate(
+      params.serverKey ? sessionHref(requireServerKey(params.serverKey), id) : legacySessionHref(sdk().directory, id),
+    )
   }
   }
 
 
   function DialogDeleteSession(props: { sessionID: string }) {
   function DialogDeleteSession(props: { sessionID: string }) {

+ 10 - 1
packages/app/src/pages/session/use-session-commands.tsx

@@ -18,6 +18,8 @@ import { createSessionTabs } from "@/pages/session/helpers"
 import { extractPromptFromParts } from "@/utils/prompt"
 import { extractPromptFromParts } from "@/utils/prompt"
 import { UserMessage } from "@opencode-ai/sdk/v2"
 import { UserMessage } from "@opencode-ai/sdk/v2"
 import { useSessionLayout } from "@/pages/session/session-layout"
 import { useSessionLayout } from "@/pages/session/session-layout"
+import { useTabs } from "@/context/tabs"
+import { requireServerKey } from "@/utils/session-route"
 
 
 export type SessionCommandContext = {
 export type SessionCommandContext = {
   navigateMessageByOffset: (offset: number) => void
   navigateMessageByOffset: (offset: number) => void
@@ -45,6 +47,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
   const settings = useSettings()
   const settings = useSettings()
   const sync = useSync()
   const sync = useSync()
   const terminal = useTerminal()
   const terminal = useTerminal()
+  const sessionTabs = useTabs()
   const layout = useLayout()
   const layout = useLayout()
   const navigate = useNavigate()
   const navigate = useNavigate()
   const { params, tabs, view } = useSessionLayout()
   const { params, tabs, view } = useSessionLayout()
@@ -381,7 +384,13 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
       title: language.t("command.session.new"),
       title: language.t("command.session.new"),
       keybind: "mod+shift+s",
       keybind: "mod+shift+s",
       slash: "new",
       slash: "new",
-      onSelect: () => navigate(`/${params.dir}/session`),
+      onSelect: () => {
+        if (params.serverKey) {
+          sessionTabs.newDraft({ server: requireServerKey(params.serverKey), directory: sdk().directory })
+          return
+        }
+        navigate(`/${params.dir}/session`)
+      },
     }),
     }),
     sessionCommand({
     sessionCommand({
       id: "session.undo",
       id: "session.undo",

+ 39 - 0
packages/app/src/utils/session-route.test.ts

@@ -0,0 +1,39 @@
+import { describe, expect, test } from "bun:test"
+import { ServerConnection } from "@/context/server"
+import { legacySessionHref, requireServerKey, rootSession, sessionHref } from "./session-route"
+
+describe("session routes", () => {
+  test("builds and decodes a server-keyed session route", () => {
+    const server = ServerConnection.Key.make("https://example.com:4096")
+    const href = sessionHref(server, "session-1")
+
+    expect(href).toBe("/server/aHR0cHM6Ly9leGFtcGxlLmNvbTo0MDk2/session/session-1")
+    expect(requireServerKey(href.split("/")[2])).toBe(server)
+  })
+
+  test("rejects malformed server keys", () => {
+    expect(() => requireServerKey("not-base64")).toThrow("Invalid server route")
+  })
+
+  test("builds the legacy directory-keyed route", () => {
+    expect(legacySessionHref("/Users/example/project", "session-1")).toBe(
+      "/L1VzZXJzL2V4YW1wbGUvcHJvamVjdA/session/session-1",
+    )
+  })
+
+  test("resolves the root session", async () => {
+    const sessions: Record<string, { id: string; parentID?: string }> = {
+      child: { id: "child", parentID: "parent" },
+      parent: { id: "parent", parentID: "root" },
+      root: { id: "root" },
+    }
+
+    expect(
+      await rootSession(sessions.child, async (id) => {
+        const session = sessions[id]
+        if (!session) throw new Error(`Missing session: ${id}`)
+        return session
+      }),
+    ).toBe(sessions.root)
+  })
+})

+ 25 - 0
packages/app/src/utils/session-route.ts

@@ -0,0 +1,25 @@
+import { base64Encode } from "@opencode-ai/core/util/encode"
+import { ServerConnection } from "@/context/server"
+import { decode64 } from "@/utils/base64"
+
+export function sessionHref(server: ServerConnection.Key, sessionID: string) {
+  return `/server/${base64Encode(server)}/session/${sessionID}`
+}
+
+export function legacySessionHref(directory: string, sessionID: string) {
+  return `/${base64Encode(directory)}/session/${sessionID}`
+}
+
+export function requireServerKey(segment: string | undefined) {
+  const key = decode64(segment)
+  if (!key || base64Encode(key) !== segment) throw new Error("Invalid server route")
+  return ServerConnection.Key.make(key)
+}
+
+type SessionParent = { id: string; parentID?: string }
+
+export async function rootSession(session: SessionParent, get: (sessionID: string) => Promise<SessionParent>) {
+  let current = session
+  while (current.parentID) current = await get(current.parentID)
+  return current
+}