diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index dea5ee62ba..b618df3980 100644 --- a/packages/app/src/app.tsx +++ b/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) diff --git a/packages/app/src/context/tabs.tsx b/packages/app/src/context/tabs.tsx index a7e9777f00..c9f7ab88c6 100644 --- a/packages/app/src/context/tabs.tsx +++ b/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) => { 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) { + 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] : [])) diff --git a/packages/app/src/utils/server-errors.test.ts b/packages/app/src/utils/server-errors.test.ts index 84f7c07d60..9c735fe689 100644 --- a/packages/app/src/utils/server-errors.test.ts +++ b/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) { 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) + }) +}) diff --git a/packages/app/src/utils/server-errors.ts b/packages/app/src/utils/server-errors.ts index 8a8db17811..409b49f54e 100644 --- a/packages/app/src/utils/server-errors.ts +++ b/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 + 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