From 77b2e0ed1a94facb8fe78ea251f8dd0652c98ab2 Mon Sep 17 00:00:00 2001 From: Ronny Date: Fri, 11 Sep 2026 17:34:08 +0200 Subject: [PATCH] fix(app): handle session.error events in global sync --- .../context/global-sync/event-reducer.test.ts | 42 +++++++++++++++++++ .../src/context/global-sync/event-reducer.ts | 25 +++++++++++ packages/app/src/context/server-sync.tsx | 6 +++ 3 files changed, 73 insertions(+) diff --git a/packages/app/src/context/global-sync/event-reducer.test.ts b/packages/app/src/context/global-sync/event-reducer.test.ts index 06d536618e05..60f12ee759fb 100644 --- a/packages/app/src/context/global-sync/event-reducer.test.ts +++ b/packages/app/src/context/global-sync/event-reducer.test.ts @@ -613,4 +613,46 @@ describe("applyDirectoryEvent", () => { expect(pushes).toEqual(["/tmp"]) expect(lspLoads).toBe(1) }) + + test("handles session.error by setting session status to idle and updating assistant error", () => { + const errors: Array = [] + const assistantMsg = { + id: "msg_2", + sessionID: "s1", + role: "assistant", + time: { created: 2 }, + modelID: "gpt", + providerID: "openai", + } as Message + const [store, setStore] = createStore( + baseState({ + session_status: { s1: { type: "busy" } }, + message: { s1: [userMessage("msg_1", "s1"), assistantMsg] }, + }), + ) + + applyDirectoryEvent({ + event: { + type: "session.error", + properties: { + sessionID: "s1", + error: { name: "ProviderAuthError", data: { message: "Invalid API key" } }, + }, + }, + store, + setStore, + push: () => undefined, + directory: "/tmp", + loadLsp: () => undefined, + notifyError: (message) => errors.push(message), + }) + + expect(store.session_status.s1).toEqual({ type: "idle" }) + const lastMsg = store.message.s1[1] + expect(lastMsg?.role === "assistant" ? lastMsg.error : undefined).toEqual({ + name: "UnknownError", + data: { message: "Invalid API key" }, + }) + expect(errors).toEqual(["Invalid API key"]) + }) }) diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index b84d5201cf55..1e2fd13980d3 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -119,6 +119,7 @@ export function applyDirectoryEvent(input: { retainedLimit?: number sessionContent?: boolean permission?: State["permission"] + notifyError?: (message?: string) => void }) { const event = input.event if (input.sessionContent === false && SESSION_CONTENT_EVENTS.has(event.type)) return @@ -467,6 +468,30 @@ export function applyDirectoryEvent(input: { ) break } + case "session.error": { + const props = event.properties as { + sessionID?: string + error?: { name?: string; data?: { message?: string }; message?: string } + } + const sessionID = props.sessionID + if (!sessionID) break + input.setStore("session_status", sessionID, { type: "idle" }) + const errorMsg = + props.error?.data?.message ?? props.error?.message ?? (typeof props.error === "string" ? props.error : undefined) + const messages = input.store.message[sessionID] + if (messages && messages.length > 0) { + const lastMsg = messages.at(-1) + if (lastMsg && lastMsg.role === "assistant") { + input.setStore("message", sessionID, messages.length - 1, (msg) => ({ + ...msg, + error: { name: "UnknownError" as const, data: { message: errorMsg ?? "" } }, + finish: "error", + })) + } + } + input.notifyError?.(errorMsg) + break + } case "lsp.updated": { input.loadLsp() break diff --git a/packages/app/src/context/server-sync.tsx b/packages/app/src/context/server-sync.tsx index 13a0b74bc6f3..901e5914c39d 100644 --- a/packages/app/src/context/server-sync.tsx +++ b/packages/app/src/context/server-sync.tsx @@ -608,6 +608,12 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { retainedLimit: sessionMeta.get(key)?.limit, sessionContent: false, permission: session.data.permission, + notifyError: (message) => + showToast({ + variant: "error", + title: language.t("notification.session.error.title"), + description: message ?? language.t("notification.session.error.fallbackDescription"), + }), vcsCache: children.vcsCache.get(key), loadLsp: () => { if (!children.active(key)) return