fix(app): close missing session tabs (#33785)
This commit is contained in:
parent
9c0d813b8e
commit
bdee94ca40
4 changed files with 73 additions and 22 deletions
|
|
@ -58,6 +58,7 @@ import {
|
||||||
selectSessionLineage,
|
selectSessionLineage,
|
||||||
sessionHref,
|
sessionHref,
|
||||||
} from "./utils/session-route"
|
} from "./utils/session-route"
|
||||||
|
import { isSessionNotFoundError } from "./utils/server-errors"
|
||||||
|
|
||||||
import Session from "@/pages/session"
|
import Session from "@/pages/session"
|
||||||
import { NewHome, LegacyHome } from "@/pages/home"
|
import { NewHome, LegacyHome } from "@/pages/home"
|
||||||
|
|
@ -129,9 +130,13 @@ function ResolvedTargetSessionRoute() {
|
||||||
const [resolved] = createResource(
|
const [resolved] = createResource(
|
||||||
() => {
|
() => {
|
||||||
if (cached()) return
|
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 current = createMemo(() => selectSessionLineage(params.id, cached(), resolved()))
|
||||||
const directory = createMemo(() => current()?.session.directory)
|
const directory = createMemo(() => current()?.session.directory)
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,27 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
||||||
navigate(href)
|
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 = {
|
const actions = {
|
||||||
addSessionTab: (tab: Omit<SessionTab, "type">) => {
|
addSessionTab: (tab: Omit<SessionTab, "type">) => {
|
||||||
const next = { type: "session" as const, ...tab }
|
const next = { type: "session" as const, ...tab }
|
||||||
|
|
@ -175,25 +196,12 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
||||||
memory.remove(`draft:${draftID}`)
|
memory.remove(`draft:${draftID}`)
|
||||||
removeDraftPersisted(draftID)
|
removeDraftPersisted(draftID)
|
||||||
},
|
},
|
||||||
removeTab: (index: number) => {
|
removeTab,
|
||||||
const tab = store[index]
|
removeSessionTab(input: Omit<SessionTab, "type">) {
|
||||||
if (!tab) return
|
const index = store.findIndex(
|
||||||
const key = tabKey(tab)
|
(tab) => tab.type === "session" && tab.server === input.server && tab.sessionId === input.sessionId,
|
||||||
const draftID = tab.type === "draft" ? tab.draftID : undefined
|
)
|
||||||
const nextTab = store[index + 1] ?? store[index - 1]
|
if (index !== -1) removeTab(index)
|
||||||
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)
|
|
||||||
},
|
},
|
||||||
removeServer(key: ServerConnection.Key) {
|
removeServer(key: ServerConnection.Key) {
|
||||||
const drafts = store.flatMap((tab) => (tab.type === "draft" && tab.server === key ? [tab.draftID] : []))
|
const drafts = store.flatMap((tab) => (tab.type === "draft" && tab.server === key ? [tab.draftID] : []))
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import type { SessionNotFoundError } from "@opencode-ai/sdk/v2/client"
|
||||||
import type { ConfigInvalidError, ProviderModelNotFoundError } from "./server-errors"
|
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>) {
|
function fill(text: string, vars?: Record<string, string | number>) {
|
||||||
if (!vars) return text
|
if (!vars) return text
|
||||||
|
|
@ -142,3 +143,33 @@ describe("formatServerError", () => {
|
||||||
expect(formatServerError(wrapped, language.t)).toBe("Arquivo de config em config invalido: Missing host")
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,13 @@ function unwrapNamedError(error: unknown): unknown {
|
||||||
return error
|
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 {
|
function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError {
|
||||||
if (typeof error !== "object" || error === null) return false
|
if (typeof error !== "object" || error === null) return false
|
||||||
const o = error as Record<string, unknown>
|
const o = error as Record<string, unknown>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue