Ver código fonte

fix(app): close missing session tabs (#33785)

Brendan Allan 1 mês atrás
pai
commit
bdee94ca40

+ 7 - 2
packages/app/src/app.tsx

@@ -58,6 +58,7 @@ import {
   selectSessionLineage,
   sessionHref,
 } from "./utils/session-route"
+import { isSessionNotFoundError } from "./utils/server-errors"
 
 import Session from "@/pages/session"
 import { NewHome, LegacyHome } from "@/pages/home"
@@ -129,9 +130,13 @@ function ResolvedTargetSessionRoute() {
   const [resolved] = createResource(
     () => {
       if (cached()) return
-      return { id: params.id, sync: sync() }
+      return { id: params.id, server: serverKey(), sync: sync() }
     },
-    ({ id, sync }) => sync.session.lineage.resolve(id),
+    ({ id, server, sync }) =>
+      sync.session.lineage.resolve(id).catch((error) => {
+        if (isSessionNotFoundError(error, id)) tabs.removeSessionTab({ server, sessionId: id })
+        throw error
+      }),
   )
   const current = createMemo(() => selectSessionLineage(params.id, cached(), resolved()))
   const directory = createMemo(() => current()?.session.directory)

+ 27 - 19
packages/app/src/context/tabs.tsx

@@ -112,6 +112,27 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
       navigate(href)
     }
 
+    const removeTab = (index: number) => {
+      const tab = store[index]
+      if (!tab) return
+      const key = tabKey(tab)
+      const draftID = tab.type === "draft" ? tab.draftID : undefined
+      const nextTab = store[index + 1] ?? store[index - 1]
+      closing.add(key)
+      void startTransition(() => {
+        setStore(
+          produce((tabs) => {
+            tabs.splice(index, 1)
+          }),
+        )
+        if (recent.key === key) setRecentKey(nextTab && tabKey(nextTab))
+        if (nextTab) navigateTab(nextTab)
+        else navigate("/")
+      }).finally(() => closing.delete(key))
+      memory.remove(key)
+      if (draftID) removeDraftPersisted(draftID)
+    }
+
     const actions = {
       addSessionTab: (tab: Omit<SessionTab, "type">) => {
         const next = { type: "session" as const, ...tab }
@@ -175,25 +196,12 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
         memory.remove(`draft:${draftID}`)
         removeDraftPersisted(draftID)
       },
-      removeTab: (index: number) => {
-        const tab = store[index]
-        if (!tab) return
-        const key = tabKey(tab)
-        const draftID = tab.type === "draft" ? tab.draftID : undefined
-        const nextTab = store[index + 1] ?? store[index - 1]
-        closing.add(key)
-        void startTransition(() => {
-          setStore(
-            produce((tabs) => {
-              tabs.splice(index, 1)
-            }),
-          )
-          if (recent.key === key) setRecentKey(nextTab && tabKey(nextTab))
-          if (nextTab) navigateTab(nextTab)
-          else navigate("/")
-        }).finally(() => closing.delete(key))
-        memory.remove(key)
-        if (draftID) removeDraftPersisted(draftID)
+      removeTab,
+      removeSessionTab(input: Omit<SessionTab, "type">) {
+        const index = store.findIndex(
+          (tab) => tab.type === "session" && tab.server === input.server && tab.sessionId === input.sessionId,
+        )
+        if (index !== -1) removeTab(index)
       },
       removeServer(key: ServerConnection.Key) {
         const drafts = store.flatMap((tab) => (tab.type === "draft" && tab.server === key ? [tab.draftID] : []))

+ 32 - 1
packages/app/src/utils/server-errors.test.ts

@@ -1,6 +1,7 @@
 import { describe, expect, test } from "bun:test"
+import type { SessionNotFoundError } from "@opencode-ai/sdk/v2/client"
 import type { ConfigInvalidError, ProviderModelNotFoundError } from "./server-errors"
-import { formatServerError, parseReadableConfigInvalidError } from "./server-errors"
+import { formatServerError, isSessionNotFoundError, parseReadableConfigInvalidError } from "./server-errors"
 
 function fill(text: string, vars?: Record<string, string | number>) {
   if (!vars) return text
@@ -142,3 +143,33 @@ describe("formatServerError", () => {
     expect(formatServerError(wrapped, language.t)).toBe("Arquivo de config em config invalido: Missing host")
   })
 })
+
+describe("isSessionNotFoundError", () => {
+  test("matches an SDK-wrapped error for the requested session", () => {
+    const body = {
+      _tag: "SessionNotFoundError",
+      sessionID: "ses_missing",
+      message: "Session not found",
+    } satisfies SessionNotFoundError
+
+    expect(isSessionNotFoundError(new Error(body.message, { cause: { body, status: 404 } }), body.sessionID)).toBe(true)
+  })
+
+  test("rejects errors for other sessions and other 404 responses", () => {
+    const body = {
+      _tag: "SessionNotFoundError",
+      sessionID: "ses_parent",
+      message: "Session not found",
+    } satisfies SessionNotFoundError
+
+    expect(isSessionNotFoundError(new Error(body.message, { cause: { body, status: 404 } }), "ses_tab")).toBe(false)
+    expect(
+      isSessionNotFoundError(
+        new Error("Provider not found", {
+          cause: { body: { _tag: "ProviderNotFoundError", providerID: "missing" }, status: 404 },
+        }),
+        "ses_tab",
+      ),
+    ).toBe(false)
+  })
+})

+ 7 - 0
packages/app/src/utils/server-errors.ts

@@ -42,6 +42,13 @@ function unwrapNamedError(error: unknown): unknown {
   return error
 }
 
+export function isSessionNotFoundError(error: unknown, sessionID: string) {
+  const unwrapped = unwrapNamedError(error)
+  if (typeof unwrapped !== "object" || unwrapped === null) return false
+  const value = unwrapped as Record<string, unknown>
+  return value._tag === "SessionNotFoundError" && value.sessionID === sessionID
+}
+
 function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError {
   if (typeof error !== "object" || error === null) return false
   const o = error as Record<string, unknown>