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

This commit is contained in:
Brendan Allan 2026-06-25 13:28:32 +08:00 committed by GitHub
parent 9c0d813b8e
commit bdee94ca40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 73 additions and 22 deletions

View file

@ -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)

View file

@ -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] : []))

View file

@ -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)
})
})

View file

@ -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>