Przeglądaj źródła

fix(app): use tab-scoped servers in sessions (#33946)

Brendan Allan 1 miesiąc temu
rodzic
commit
b60c0a5e8c

+ 42 - 27
packages/app/src/app.tsx

@@ -32,7 +32,7 @@ import { CommentsProvider } from "@/context/comments"
 import { FileProvider } from "@/context/file"
 import { ServerSDKProvider, useServerSDK } from "@/context/server-sdk"
 import { ServerSyncProvider, useServerSync } from "@/context/server-sync"
-import { GlobalProvider } from "@/context/global"
+import { GlobalProvider, useGlobal } from "@/context/global"
 import { HighlightsProvider } from "@/context/highlights"
 import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
 import { LayoutProvider } from "@/context/layout"
@@ -103,10 +103,10 @@ const SessionRoute = () => {
 
 const TargetSessionRoute = () => {
   const params = useParams<{ serverKey: string; id: string }>()
-  const server = useServer()
+  const global = useGlobal()
   const conn = createMemo(() => {
     const key = requireServerKey(params.serverKey)
-    return server.list.find((item) => ServerConnection.key(item) === key)
+    return global.servers.list().find((item) => ServerConnection.key(item) === key)
   })
 
   return (
@@ -221,25 +221,27 @@ function DraftRoute() {
 }
 
 function ResolvedDraftRoute(props: { draft: DraftTab }) {
-  const server = useServer()
-  const conn = createMemo(() => server.list.find((item) => ServerConnection.key(item) === props.draft.server))
+  const global = useGlobal()
+  const conn = createMemo(() => global.servers.list().find((item) => ServerConnection.key(item) === props.draft.server))
   const directory = () => props.draft.directory
   const serverKey = () => props.draft.server
 
   return (
-    <ServerSDKProvider server={conn}>
-      <ServerSyncProvider server={conn}>
-        <TargetServerScopedProviders directory={directory}>
-          <SDKProvider directory={directory}>
-            <DirectoryDataProvider directory={directory} server={serverKey}>
-              <DraftProviders>
-                <NewSession />
-              </DraftProviders>
-            </DirectoryDataProvider>
-          </SDKProvider>
-        </TargetServerScopedProviders>
-      </ServerSyncProvider>
-    </ServerSDKProvider>
+    <Show when={`${props.draft.server}\0${props.draft.directory}`} keyed>
+      <ServerSDKProvider server={conn}>
+        <ServerSyncProvider server={conn}>
+          <TargetServerScopedProviders directory={directory}>
+            <SDKProvider directory={directory}>
+              <DirectoryDataProvider directory={directory} server={serverKey}>
+                <DraftProviders>
+                  <NewSession />
+                </DraftProviders>
+              </DirectoryDataProvider>
+            </SDKProvider>
+          </TargetServerScopedProviders>
+        </ServerSyncProvider>
+      </ServerSDKProvider>
+    </Show>
   )
 }
 
@@ -588,18 +590,31 @@ function Routes() {
       </Route>
       <Show when={settings.general.newLayoutDesigns()}>
         <Route path="/" component={NewHome} />
-        <Route
-          path="/:dir/session/:id"
-          component={() => {
-            const server = useServer()
-            const { id } = useParams()
-
-            return <Navigate href={`/server/${server.key}/session/${id}`} />
-          }}
-        />
+        <Route path="/:dir/session/:id" component={LegacyTargetSessionRoute} />
       </Show>
       <Route path="/new-session" component={DraftRoute} />
       <Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
     </>
   )
 }
+
+function LegacyTargetSessionRoute() {
+  const server = useServer()
+  const tabs = useTabs()
+  const params = useParams<{ id: string }>()
+
+  return (
+    <Show when={tabs.ready()}>
+      <Navigate
+        href={sessionHref(
+          legacySessionServer(
+            tabs.store.filter((item) => item.type === "session"),
+            params.id,
+            server.key,
+          ),
+          params.id,
+        )}
+      />
+    </Show>
+  )
+}

+ 2 - 4
packages/app/src/components/dialog-select-file.tsx

@@ -15,7 +15,6 @@ import { useLayout } from "@/context/layout"
 import { useFile } from "@/context/file"
 import { useLanguage } from "@/context/language"
 import { usePlatform } from "@/context/platform"
-import { useServer } from "@/context/server"
 import { useSettings } from "@/context/settings"
 import { useSessionLayout } from "@/pages/session/session-layout"
 import { createSessionTabs } from "@/pages/session/helpers"
@@ -272,7 +271,6 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
   const command = useCommand()
   const language = useLanguage()
   const platform = usePlatform()
-  const server = useServer()
   const settings = useSettings()
   const layout = useLayout()
   const file = useFile()
@@ -393,10 +391,10 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
     state.cleanup?.()
   })
 
-  if (filesOnly() && platform.platform === "desktop" && settings.general.newLayoutDesigns() && server.current) {
+  if (filesOnly() && platform.platform === "desktop" && settings.general.newLayoutDesigns()) {
     return (
       <DialogSelectFileV2
-        server={server.current}
+        server={serverSDK().server}
         mode="file"
         start={projectDirectory()}
         title={language.t("session.header.searchFiles")}

+ 4 - 3
packages/app/src/components/status-popover.tsx

@@ -5,7 +5,8 @@ import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
 import { Popover } from "@opencode-ai/ui/popover"
 import { Suspense, createMemo, createSignal, lazy, Show, type JSX } from "solid-js"
 import { useLanguage } from "@/context/language"
-import { useServer } from "@/context/server"
+import { ServerConnection, useServer } from "@/context/server"
+import { useServerSDK } from "@/context/server-sdk"
 import { useSync } from "@/context/sync"
 import { useGlobal } from "@/context/global"
 
@@ -81,11 +82,11 @@ export function StatusPopoverV2(props: { scope?: "server" }) {
 
 function DirectoryStatusPopover() {
   const language = useLanguage()
-  const server = useServer()
+  const server = useServerSDK()
   const global = useGlobal()
   const sync = useSync()
   const [shown, setShown] = createSignal(false)
-  const serverHealth = () => global.servers.health[server.key]?.healthy
+  const serverHealth = () => global.servers.health[ServerConnection.key(server().server)]?.healthy
   const ready = createMemo(() => serverHealth() === false || sync().data.mcp_ready)
   const mcpIssue = createMemo(() => {
     const mcp = Object.values(sync().data.mcp ?? {})

+ 6 - 4
packages/app/src/components/terminal.tsx

@@ -10,7 +10,7 @@ import { matchKeybind, parseKeybind } from "@/context/command"
 import { useLanguage } from "@/context/language"
 import { usePlatform } from "@/context/platform"
 import { useSDK } from "@/context/sdk"
-import { useServer } from "@/context/server"
+import { useServerSDK } from "@/context/server-sdk"
 import { terminalFontFamily, useSettings } from "@/context/settings"
 import type { LocalPTY } from "@/context/terminal"
 import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
@@ -160,13 +160,15 @@ export const Terminal = (props: TerminalProps) => {
   const settings = useSettings()
   const theme = useTheme()
   const language = useLanguage()
-  const server = useServer()
+  // Terminal captures its connection for the PTY lifetime, so callers must key it per server/session.
+  const connection = useServerSDK()().server
   const directory = sdk().directory
   const client = sdk().client
   const url = sdk().url
-  const auth = server.current?.http
+  const auth = connection.http
   const username = auth?.username ?? "opencode"
   const password = auth?.password ?? ""
+  const authToken = connection.type === "http" ? connection.authToken : false
   const sameOrigin = new URL(url, location.href).origin === location.origin
   let container!: HTMLDivElement
   const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
@@ -540,7 +542,7 @@ export const Terminal = (props: TerminalProps) => {
             sameOrigin,
             username,
             password,
-            authToken: server.current?.type === "http" ? server.current.authToken : false,
+            authToken,
           }),
         )
         socket.binaryType = "arraybuffer"

+ 6 - 0
packages/app/src/components/titlebar.tsx

@@ -319,6 +319,12 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
                 return
               }
 
+              const activeTab = currentTab()
+              if (activeTab?.type === "draft") {
+                tabs.newDraft({ server: activeTab.server, directory: activeTab.directory }, "")
+                return
+              }
+
               const current = layout.projects.list()[0]
               if (current) {
                 tabs.newDraft({ server: server.key, directory: current.worktree }, "")

+ 6 - 0
packages/app/src/context/layout.tsx

@@ -17,6 +17,7 @@ import type { ProjectAvatarVariant } from "@opencode-ai/ui/v2/project-avatar-v2"
 import { migrateLegacySessionStateKeys, ServerScope, SessionStateKey } from "@/utils/server-scope"
 import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
 import { requireServerKey } from "@/utils/session-route"
+import { type DraftTab, useTabs } from "./tabs"
 
 export { createSessionKeyReader, ensureSessionKey, pruneSessionKeys }
 
@@ -159,12 +160,17 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
     const serverSdk = useServerSDK()
     const serverSync = useServerSync()
     const server = useServer()
+    const tabs = useTabs()
     const platform = usePlatform()
     const location = useLocation()
     const route = createMemo(() => {
       const value = currentRoute(location.pathname, location.search)
       if (value.type === "home") return value
       if (value.server) return value
+      if (value.type === "draft") {
+        const draft = tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === value.draftID)
+        if (draft) return { ...value, server: draft.server }
+      }
       return { ...value, server: server.key }
     })
 

+ 3 - 3
packages/app/src/context/prompt.tsx

@@ -9,7 +9,7 @@ import { useServerSDK } from "./server-sdk"
 import type { ServerScope } from "@/utils/server-scope"
 import { useSDK } from "./sdk"
 import { useTabs, type Tab } from "./tabs"
-import { ServerConnection, useServer } from "./server"
+import { ServerConnection } from "./server"
 import { requireServerKey } from "@/utils/session-route"
 import { useSettings } from "./settings"
 
@@ -287,7 +287,6 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
     const sdk = useSDK()
     const [search] = useSearchParams<{ draftId?: string }>()
     const serverSDK = useServerSDK()
-    const server = useServer()
     const tabs = useTabs()
     const settings = useSettings()
     const cache = new Map<string, PromptCacheEntry>()
@@ -312,7 +311,8 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
     }
 
     const owner = getOwner()
-    const serverKey = () => (params.serverKey ? requireServerKey(params.serverKey) : server.key)
+    const serverKey = () =>
+      params.serverKey ? requireServerKey(params.serverKey) : ServerConnection.key(serverSDK().server)
     const scope = () =>
       search.draftId ? { draftID: search.draftId } : { dir: base64Encode(sdk().directory), id: params.id }
     const load = (scope: Scope) => {

+ 1 - 0
packages/app/src/context/server-sdk.tsx

@@ -269,6 +269,7 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
   })
 
   return {
+    server,
     scope,
     url: server.http.url,
     client: sdk,

+ 7 - 17
packages/app/src/pages/session/composer/session-composer-controls.ts

@@ -8,11 +8,12 @@ import { useGlobal } from "@/context/global"
 import { useLayout } from "@/context/layout"
 import { useLocal } from "@/context/local"
 import type { QueryOptionsApi } from "@/context/server-sync"
+import { useServerSDK } from "@/context/server-sdk"
 import { serverName, ServerConnection, useServer } from "@/context/server"
 import { useSDK } from "@/context/sdk"
 import { useSettings } from "@/context/settings"
 import { useSync } from "@/context/sync"
-import { type DraftTab, useTabs } from "@/context/tabs"
+import { useTabs } from "@/context/tabs"
 import { useProviders } from "@/hooks/use-providers"
 import { pathKey } from "@/utils/path-key"
 
@@ -27,6 +28,7 @@ export function createSessionComposerControls(input: {
   const providers = useProviders()
   const settings = useSettings()
   const server = useServer()
+  const serverSDK = useServerSDK()
   const sync = useSync()
   const sdk = useSDK()
   const tabs = useTabs()
@@ -35,23 +37,11 @@ export function createSessionComposerControls(input: {
   const [search] = useSearchParams<{ draftId?: string }>()
   const view = layout.view(input.sessionKey)
 
-  const draft = createMemo(() => {
-    if (!search.draftId) return
-    return tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === search.draftId)
-  })
-  const projectServer = createMemo(() => {
-    if (!search.draftId) return server.current
-    const target = draft()?.server
-    if (!target) return
-    return server.list.find((conn) => ServerConnection.key(conn) === target)
-  })
-  const projectServerCtx = createMemo(() => {
-    const conn = projectServer()
-    if (conn) return global.ensureServerCtx(conn)
-  })
+  const projectServer = () => serverSDK().server
+  const projectServerCtx = createMemo(() => global.ensureServerCtx(projectServer()))
   const projects = createMemo(() => {
     if (server.list.length <= 1) {
-      return search.draftId ? (projectServerCtx()?.projects.list() ?? []) : layout.projects.list()
+      return search.draftId ? projectServerCtx().projects.list() : layout.projects.list()
     }
     return server.list.flatMap((conn) => {
       const server = { key: ServerConnection.key(conn), name: serverName(conn) }
@@ -121,7 +111,7 @@ export function createSessionComposerControls(input: {
     projects: {
       available: projects(),
       directory: sdk().directory,
-      server: server.list.length > 1 && projectServer() ? ServerConnection.key(projectServer()!) : undefined,
+      server: server.list.length > 1 ? ServerConnection.key(projectServer()) : undefined,
       select: selectProject,
       add: addProject,
     },