From 76d0b2103ee855c66ac6568f052f077febd4f30f Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 30 Jun 2026 16:25:06 +0200 Subject: [PATCH] fix(agent-manager): keep chat in sync with session selection while offline selectSession bailed out entirely when the webview's backend connection was momentarily unavailable. In Agent Manager the side diff is resolved from the worktree selection independently of currentSessionID, so a switch during a transient disconnect moved the diff while the chat stayed frozen on the previous session (the 'switching only changes the sidebar diff' report). Update currentSessionID/draft synchronously regardless of connection so the chat always follows the selection, and gate only the message fetch on the connection. A fetch deferred while offline is replayed once the backend reconnects, scoped to the still-current unloaded session so the normal connected path never double-fetches. --- ...ix-agent-manager-session-switch-offline.md | 5 ++ .../unit/session-select-connection.test.ts | 61 +++++++++++++++++++ .../webview-ui/src/context/session.tsx | 43 ++++++++++--- 3 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 .changeset/fix-agent-manager-session-switch-offline.md create mode 100644 packages/kilo-vscode/tests/unit/session-select-connection.test.ts diff --git a/.changeset/fix-agent-manager-session-switch-offline.md b/.changeset/fix-agent-manager-session-switch-offline.md new file mode 100644 index 0000000000..c59b4f3d2a --- /dev/null +++ b/.changeset/fix-agent-manager-session-switch-offline.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep the chat in sync with the selected Agent Manager session when the backend connection is briefly unavailable, so switching sessions no longer updates only the side diff while the conversation stays on the previous session. diff --git a/packages/kilo-vscode/tests/unit/session-select-connection.test.ts b/packages/kilo-vscode/tests/unit/session-select-connection.test.ts new file mode 100644 index 0000000000..f31152f54b --- /dev/null +++ b/packages/kilo-vscode/tests/unit/session-select-connection.test.ts @@ -0,0 +1,61 @@ +/** + * Source contract test for selectSession's connection handling. + * + * Static analysis — reads session.tsx and verifies that selectSession updates + * the current session id BEFORE (and independently of) the backend connection + * check, and only defers the message fetch when offline. + * + * Regression guard: previously selectSession bailed out entirely when + * `server.isConnected()` was false. In Agent Manager the side diff is resolved + * from the worktree selection independently of currentSessionID, so a switch + * during a transient disconnect moved the diff but left the chat frozen on the + * previous session ("switching only changes the sidebar diff"). The chat must + * always follow the selection; only the network fetch may wait for reconnect. + */ + +import { describe, it, expect } from "bun:test" +import fs from "node:fs" +import path from "node:path" + +const ROOT = path.resolve(import.meta.dir, "../..") +const SESSION_FILE = path.join(ROOT, "webview-ui/src/context/session.tsx") + +const source = fs.readFileSync(SESSION_FILE, "utf-8") + +describe("selectSession keeps the chat in sync with the selection while offline", () => { + const start = source.indexOf("function selectSession(") + const cloudGuard = source.indexOf('id.startsWith("cloud:")', start) + const setCurrent = source.indexOf("setCurrentSessionID(id)", start) + const offlineDefer = source.indexOf("if (!server.isConnected()) {", start) + + it("selectSession exists", () => { + expect(start).toBeGreaterThan(-1) + }) + + it("returns early for cloud preview ids before touching the current session", () => { + expect(cloudGuard).toBeGreaterThan(start) + expect(cloudGuard).toBeLessThan(setCurrent) + }) + + it("sets currentSessionID before checking the connection (chat follows selection offline)", () => { + expect(setCurrent).toBeGreaterThan(-1) + expect(offlineDefer).toBeGreaterThan(-1) + // The whole point of the fix: the local selection update must precede the + // connection guard, so a disconnected switch no longer freezes the chat. + expect(setCurrent).toBeLessThan(offlineDefer) + }) + + it("defers (does not drop) the fetch when offline", () => { + const body = source.slice(start, source.indexOf("\n function selectCloudSession(")) + expect(body).toContain("deferredFetch") + }) +}) + +describe("a deferred fetch is replayed on reconnect", () => { + it("watches the connection and replays the deferred session load", () => { + expect(source).toContain("on(server.isConnected") + const effect = source.slice(source.indexOf("on(server.isConnected")) + expect(effect).toContain("deferredFetch") + expect(effect).toMatch(/loadMessages.*mode: "replace"/s) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 921fff8f8e..820167e775 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -10,6 +10,7 @@ import { createSignal, createMemo, createEffect, + on, onMount, onCleanup, batch, @@ -2519,27 +2520,53 @@ export const SessionProvider: ParentComponent = (props) => { }) } + // Session whose message fetch was deferred because the backend was offline at + // selection time. Replayed by the reconnect effect below. + let deferredFetch: string | undefined + function selectSession(id: string) { - if (!server.isConnected()) { - console.warn("[Kilo New] Cannot select session: not connected") - return - } + // Cloud preview sessions use a separate keyed path (selectCloudSession). if (id.startsWith("cloud:")) { console.warn("[Kilo New] Cannot select cloud preview session via selectSession") return } const ready = loaded().has(id) + // Reflect the selection locally and synchronously so the chat always tracks + // the sidebar/tab selection. These are local signals and need no backend, so + // they update even while disconnected. Bailing out here when not connected + // froze the chat on the previous session while the side diff (resolved from + // the worktree selection) still moved — the reported "only the diff changes". setCurrentSessionID(id) setDraftSessionID(id) setLoading(!ready) - if (ready) { - vscode.postMessage({ type: "loadMessages", sessionID: id, mode: "focus" }) + if (!ready) patchPage(id, { loadingInitial: true, loadingOlder: false, before: undefined, hasMore: false }) + // Only the message fetch needs the backend. Defer it while offline and let + // the reconnect effect replay it; cached sessions already render from store. + if (!server.isConnected()) { + deferredFetch = ready ? undefined : id return } - patchPage(id, { loadingInitial: true, loadingOlder: false, before: undefined, hasMore: false }) - vscode.postMessage({ type: "loadMessages", sessionID: id, mode: "replace", limit: MESSAGE_PAGE_LIMIT }) + deferredFetch = undefined + vscode.postMessage( + ready + ? { type: "loadMessages", sessionID: id, mode: "focus" } + : { type: "loadMessages", sessionID: id, mode: "replace", limit: MESSAGE_PAGE_LIMIT }, + ) } + // Replay a fetch deferred while offline once the backend reconnects. Scoped to + // the still-current, still-unloaded session so the normal connected path never + // double-fetches. + createEffect( + on(server.isConnected, (connected) => { + if (!connected) return + const id = deferredFetch + deferredFetch = undefined + if (!id || id !== currentSessionID() || loaded().has(id)) return + vscode.postMessage({ type: "loadMessages", sessionID: id, mode: "replace", limit: MESSAGE_PAGE_LIMIT }) + }), + ) + function selectCloudSession(cloudSessionId: string) { if (!server.isConnected()) { console.warn("[Kilo New] Cannot select cloud session: not connected")