diff --git a/packages/kilo-vscode/src/kilo-provider-utils.ts b/packages/kilo-vscode/src/kilo-provider-utils.ts index 1b5d5812a5..1f9bf18fd3 100644 --- a/packages/kilo-vscode/src/kilo-provider-utils.ts +++ b/packages/kilo-vscode/src/kilo-provider-utils.ts @@ -195,10 +195,12 @@ export async function loadSessions(ctx: SessionRefreshContext): Promise() const extra = await Promise.all( [...worktreeDirs].map((dir) => list(dir).catch((err: unknown) => { console.error(`[Kilo] Failed to list sessions for ${dir}:`, err) + failed.add(dir) return [] as Session[] }), ), @@ -212,9 +214,19 @@ export async function loadSessions(ctx: SessionRefreshContext): Promise sessionToWebview(s)), + ...(preserve.length ? { preserveSessionIds: preserve } : {}), }) return projectID diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-session-refresh.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-session-refresh.test.ts index 7747f980ad..2dd31cf7b9 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-session-refresh.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-session-refresh.test.ts @@ -157,6 +157,86 @@ describe("KiloProvider pending session refresh", () => { expect((sent[0] as { sessions: { id: string }[] }).sessions.map((s) => s.id)).toEqual(["ses_worktree"]) }) + it("preserves session ids when worktree directory listing fails", async () => { + const sent: unknown[] = [] + const ctx = createContext({ + connectionState: "connected", + sessionDirectories: new Map([ + ["ses_wt1", "/worktree1"], + ["ses_wt2", "/worktree2"], + ]), + listSessions: async (dir) => { + if (dir === "/repo") { + return [ + { + id: "ses_root", + projectID: "project", + title: "root", + directory: "/repo", + time: { created: 1, updated: 1 }, + }, + ] as never + } + if (dir === "/worktree1") throw new Error("backend not ready") + return [ + { + id: "ses_wt2", + projectID: "project", + title: "wt2", + directory: "/worktree2", + time: { created: 2, updated: 2 }, + }, + ] as never + }, + postMessage: (msg) => sent.push(msg), + }) + + await loadSessions(ctx) + + expect(sent).toHaveLength(1) + const msg = sent[0] as { sessions: { id: string }[]; preserveSessionIds?: string[] } + expect(msg.sessions.map((s) => s.id)).toEqual(["ses_root", "ses_wt2"]) + expect(msg.preserveSessionIds).toEqual(["ses_wt1"]) + }) + + it("omits preserveSessionIds when all directories succeed", async () => { + const sent: unknown[] = [] + const ctx = createContext({ + connectionState: "connected", + sessionDirectories: new Map([["ses_wt", "/worktree"]]), + listSessions: async (dir) => { + if (dir === "/repo") { + return [ + { + id: "ses_root", + projectID: "project", + title: "root", + directory: "/repo", + time: { created: 1, updated: 1 }, + }, + ] as never + } + return [ + { + id: "ses_wt", + projectID: "project", + title: "wt", + directory: "/worktree", + time: { created: 2, updated: 2 }, + }, + ] as never + }, + postMessage: (msg) => sent.push(msg), + }) + + await loadSessions(ctx) + + expect(sent).toHaveLength(1) + const msg = sent[0] as { sessions: { id: string }[]; preserveSessionIds?: string[] } + expect(msg.sessions.map((s) => s.id)).toEqual(["ses_root", "ses_wt"]) + expect(msg.preserveSessionIds).toBeUndefined() + }) + it("flushes deferred refresh via flushPendingSessionRefresh", async () => { const { calls, fn } = createListSessions() const ctx = createContext() diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 323cd50959..ed1c38b7ef 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -687,7 +687,7 @@ export const SessionProvider: ParentComponent = (props) => { break case "sessionsLoaded": - handleSessionsLoaded(message.sessions) + handleSessionsLoaded(message.sessions, message.preserveSessionIds) break case "sessionUpdated": @@ -1120,16 +1120,20 @@ export const SessionProvider: ParentComponent = (props) => { setStore("todos", sessionID, items) } - function handleSessionsLoaded(loaded: SessionInfo[]) { + function handleSessionsLoaded(loaded: SessionInfo[], preserve?: string[]) { + const kept = preserve?.length ? new Set(preserve) : undefined batch(() => { // Reconcile: remove sessions not in the loaded list to prevent stale // entries from other projects accumulating in the store. + // Sessions whose worktree directories failed to list are preserved — + // their absence is transient, not a real deletion. const ids = new Set(loaded.map((s) => s.id)) setStore( "sessions", produce((sessions) => { for (const id of Object.keys(sessions)) { if (id.startsWith("cloud:")) continue + if (kept?.has(id)) continue if (!ids.has(id)) delete sessions[id] } }), diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index e14f5c0184..e2b194a4a0 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -580,6 +580,7 @@ export interface MessageCreatedMessage { export interface SessionsLoadedMessage { type: "sessionsLoaded" sessions: SessionInfo[] + preserveSessionIds?: string[] } export interface CloudSessionsLoadedMessage {