From fa986a115d82ae3efb02f5cae4600afc1743963f Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 9 Sep 2026 15:40:27 +0200 Subject: [PATCH 1/2] fix(vscode): preserve plan opens across worktree switches --- .changeset/defer-inactive-plan-opens.md | 5 ++ .../kilo-vscode/tests/unit/open-plan.test.ts | 40 +++++++++++++- packages/kilo-vscode/webview-ui/src/App.tsx | 16 +++--- .../webview-ui/src/utils/open-plan.ts | 52 ++++++++++++++++++- 4 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 .changeset/defer-inactive-plan-opens.md diff --git a/.changeset/defer-inactive-plan-opens.md b/.changeset/defer-inactive-plan-opens.md new file mode 100644 index 000000000000..d7e2b63afe22 --- /dev/null +++ b/.changeset/defer-inactive-plan-opens.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Defer automatic plan previews until their agent session is active. diff --git a/packages/kilo-vscode/tests/unit/open-plan.test.ts b/packages/kilo-vscode/tests/unit/open-plan.test.ts index 9e3c68523a52..a7775131ee75 100644 --- a/packages/kilo-vscode/tests/unit/open-plan.test.ts +++ b/packages/kilo-vscode/tests/unit/open-plan.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "bun:test" import type { ExtensionMessage, Part } from "../../webview-ui/src/types/messages" -import { planOpens } from "../../webview-ui/src/utils/open-plan" +import { createPlanOpener, planOpens } from "../../webview-ui/src/utils/open-plan" const done = (id = "part-1") => ({ @@ -64,4 +64,42 @@ describe("planOpens", () => { expect(planOpens(update(running), "session-1")).toEqual([]) expect(planOpens(update(unmarked), "session-1")).toEqual([]) }) + + it("defers inactive plans and replays them when the session becomes active", async () => { + let active = "session-2" + const opened: string[] = [] + const opener = createPlanOpener( + () => active, + (plan) => opened.push(`${plan.sessionID}:${plan.id}`), + ) + const plan = update(done("part-deferred"), "session-1") + + opener.accept(plan) + expect(opened).toEqual([]) + + active = "session-1" + opener.flush(active) + await Promise.resolve() + expect(opened).toEqual(["session-1:part-deferred"]) + }) + + it("requeues a plan if the active session changes before dispatch", async () => { + let active = "session-1" + const opened: string[] = [] + const opener = createPlanOpener( + () => active, + (plan) => opened.push(`${plan.sessionID}:${plan.id}`), + ) + const plan = update(done("part-race"), "session-1") + + opener.accept(plan) + active = "session-2" + await Promise.resolve() + expect(opened).toEqual([]) + + active = "session-1" + opener.flush(active) + await Promise.resolve() + expect(opened).toEqual(["session-1:part-race"]) + }) }) diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index 10f1b731c496..8c148bada417 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -20,7 +20,7 @@ import { useWorktreeMode } from "./context/worktree-mode" import { useDiffStyle } from "./context/diff-style" import { dispatchAgentManagerEditPreview } from "./utils/agent-manager-events" import { strongest } from "./utils/session-activity" -import { planOpens } from "./utils/open-plan" +import { createPlanOpener } from "./utils/open-plan" import type { PermissionFileDiff } from "./types/messages" // Override the upstream "task" tool renderer with the fully-expanded version @@ -36,7 +36,6 @@ import "./styles/chat.css" type ViewType = "newTask" | "history" | "profile" | "settings" | "subAgentViewer" const VALID_VIEWS = new Set(["newTask", "history", "profile", "settings", "subAgentViewer"]) -const opened = new Set() /** * Bridge our session store to the DataProvider's expected Data shape. @@ -151,14 +150,11 @@ export const DataBridge: Component<{ children: any }> = (props) => { vscode.postMessage({ type: "openFile", filePath, line, column, sessionID }) } - const unsubscribePlans = vscode.onMessage((message) => { - for (const plan of planOpens(message, session.currentSessionID())) { - const id = `${plan.sessionID}:${plan.id}` - if (opened.has(id)) continue - opened.add(id) - queueMicrotask(() => open(plan.path, undefined, undefined, plan.sessionID)) - } - }) + const opener = createPlanOpener(session.currentSessionID, (plan) => + open(plan.path, undefined, undefined, plan.sessionID), + ) + const unsubscribePlans = vscode.onMessage(opener.accept) + createEffect(() => opener.flush(session.currentSessionID())) onCleanup(unsubscribePlans) const openDiff = (diff: PermissionFileDiff) => { diff --git a/packages/kilo-vscode/webview-ui/src/utils/open-plan.ts b/packages/kilo-vscode/webview-ui/src/utils/open-plan.ts index 84c9fa970c43..1798e9de15c4 100644 --- a/packages/kilo-vscode/webview-ui/src/utils/open-plan.ts +++ b/packages/kilo-vscode/webview-ui/src/utils/open-plan.ts @@ -10,7 +10,10 @@ export type PlanOpen = { sessionID: string } -export function planOpens(message: ExtensionMessage, activeSessionID: string | undefined): PlanOpen[] { +const opened = new Set() +const pending = new Map() + +function read(message: ExtensionMessage): PlanOpen[] { const updates: Update[] = message.type === "partUpdated" ? [message] : message.type === "partsUpdated" ? message.updates : [] @@ -19,7 +22,52 @@ export function planOpens(message: ExtensionMessage, activeSessionID: string | u if (part.type !== "tool" || part.tool !== "open_plan" || part.state.status !== "completed") return [] if (part.state.metadata?.open !== true) return [] const path = part.state.metadata.plan - if (typeof path !== "string" || !path || update.sessionID !== activeSessionID) return [] + if (typeof path !== "string" || !path || !update.sessionID) return [] return [{ id: part.id, path, sessionID: update.sessionID }] }) } + +export function planOpens(message: ExtensionMessage, activeSessionID: string | undefined): PlanOpen[] { + return read(message).filter((plan) => plan.sessionID === activeSessionID) +} + +/** Defer plans from inactive sessions until their session becomes active. */ +export function createPlanOpener(active: () => string | undefined, open: (plan: PlanOpen) => void) { + const id = (plan: PlanOpen) => `${plan.sessionID}:${plan.id}` + const schedule = (plan: PlanOpen) => { + const key = id(plan) + if (opened.has(key)) { + pending.delete(key) + return + } + pending.delete(key) + queueMicrotask(() => { + if (active() !== plan.sessionID) { + pending.set(key, plan) + return + } + if (opened.has(key)) return + opened.add(key) + open(plan) + }) + } + const accept = (message: ExtensionMessage) => { + const current = active() + for (const plan of read(message)) { + const key = id(plan) + if (opened.has(key)) continue + if (plan.sessionID !== current) { + pending.set(key, plan) + continue + } + schedule(plan) + } + } + const flush = (sessionID: string | undefined) => { + if (!sessionID) return + for (const plan of pending.values()) { + if (plan.sessionID === sessionID) schedule(plan) + } + } + return { accept, flush } +} From 19f83f01b5109d6c03a5587fc6fa73cf34979491 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 9 Sep 2026 16:25:48 +0200 Subject: [PATCH 2/2] fix(vscode): simplify plan opener cleanup --- packages/kilo-vscode/webview-ui/src/utils/open-plan.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/utils/open-plan.ts b/packages/kilo-vscode/webview-ui/src/utils/open-plan.ts index 1798e9de15c4..2ba97cf48fe8 100644 --- a/packages/kilo-vscode/webview-ui/src/utils/open-plan.ts +++ b/packages/kilo-vscode/webview-ui/src/utils/open-plan.ts @@ -36,11 +36,8 @@ export function createPlanOpener(active: () => string | undefined, open: (plan: const id = (plan: PlanOpen) => `${plan.sessionID}:${plan.id}` const schedule = (plan: PlanOpen) => { const key = id(plan) - if (opened.has(key)) { - pending.delete(key) - return - } pending.delete(key) + if (opened.has(key)) return queueMicrotask(() => { if (active() !== plan.sessionID) { pending.set(key, plan)