From 5b336a90abdb82b82e5dcc2c84cc92842f3117a3 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 30 Sep 2026 09:57:51 +0200 Subject: [PATCH 1/2] fix(agent-manager): keep latest project selection A delayed project readiness completion could activate an older sidebar selection and override a newer click. Ignore a selection when a newer one has started. --- .changeset/agent-manager-latest-selection.md | 5 ++++ .../src/agent-manager/project/messages.ts | 5 ++++ .../unit/agent-project-selection.test.ts | 24 +++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 .changeset/agent-manager-latest-selection.md diff --git a/.changeset/agent-manager-latest-selection.md b/.changeset/agent-manager-latest-selection.md new file mode 100644 index 000000000000..c5f144ad2618 --- /dev/null +++ b/.changeset/agent-manager-latest-selection.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep the latest Agent Manager selection when another project finishes loading. diff --git a/packages/kilo-vscode/src/agent-manager/project/messages.ts b/packages/kilo-vscode/src/agent-manager/project/messages.ts index 34c798a4bdeb..ea784908351a 100644 --- a/packages/kilo-vscode/src/agent-manager/project/messages.ts +++ b/packages/kilo-vscode/src/agent-manager/project/messages.ts @@ -18,6 +18,7 @@ import { cloneProject, createProject, defaultParent, onboard, type Onboarding } import { runner } from "./prepare" const pending = new WeakSet() +const selections = new WeakMap() /** Route one session to a directory inside a project via the shared session provider. */ export function routeProjectSession( @@ -133,12 +134,16 @@ export async function handleProjectMessage(m: AgentManagerInMessage, deps: Proje async function activateSelection(requested: SidebarTarget, deps: ProjectMessageDeps, restore = false): Promise { if (disabled(deps)) return + const token = Symbol() + selections.set(deps, token) const ctx = deps.contexts.resolve(requested.projectId) if (!ctx || !deps.contexts.usable(requested.projectId)) { deps.error("The project is unavailable. Check that the repository still exists.") return } const result = await deps.ready(ctx, { warm: true }) + // A newer click can finish while this project's readiness is pending. + if (selections.get(deps) !== token || !deps.enabled()) return if (!result.current || !result.ok) { deps.error("The project is not ready yet. Expand it before selecting a worktree or session.") deps.push() diff --git a/packages/kilo-vscode/tests/unit/agent-project-selection.test.ts b/packages/kilo-vscode/tests/unit/agent-project-selection.test.ts index 70fa285b5b36..de47b316b65b 100644 --- a/packages/kilo-vscode/tests/unit/agent-project-selection.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-project-selection.test.ts @@ -131,6 +131,30 @@ describe("activateSelection — cross-project selection", () => { expect(calls.selected).toEqual([extra]) }) + it.each([true, false])("ignores a superseded selection when readiness completes with ok=%s", async (ok) => { + const gate = Promise.withResolvers() + const { contexts, deps, calls, extra } = setup({ + ready: (ctx) => + ctx.ensureReady(async () => { + ctx.stateManager() + if (!ctx.pinned) await gate.promise + return { ok: ctx.pinned || ok, refsFixed: 0 } + }), + }) + + const pending = handleProjectMessage(activateMsg(extra), deps) + await handleProjectMessage(activateMsg(PINNED, { kind: "worktree", worktreeId: "wt1" }), deps) + expect(calls.selected).toEqual(["worktree"]) + + gate.resolve() + await pending + + expect(contexts.active()?.id).toBe(PINNED) + expect(calls.selected).toEqual(["worktree"]) + expect(contexts.get(extra)?.peekState()?.getActiveTarget()).toBeUndefined() + expect(calls.error).toEqual([]) + }) + it("falls back to local without an error toast when the target is gone", async () => { const { contexts, deps, calls, extra } = setup() contexts.activate(PINNED) From b1ef1c9c43f43b545019296c000be76642f2d609 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 30 Sep 2026 10:06:48 +0200 Subject: [PATCH 2/2] fix(agent-manager): ignore unavailable selection for latest-click token Record the latest-selection token only for an actionable target so a click on an unavailable project cannot discard a pending valid selection. --- .../src/agent-manager/project/messages.ts | 6 +++-- .../unit/agent-project-selection.test.ts | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/kilo-vscode/src/agent-manager/project/messages.ts b/packages/kilo-vscode/src/agent-manager/project/messages.ts index ea784908351a..0a7a60a02683 100644 --- a/packages/kilo-vscode/src/agent-manager/project/messages.ts +++ b/packages/kilo-vscode/src/agent-manager/project/messages.ts @@ -134,13 +134,15 @@ export async function handleProjectMessage(m: AgentManagerInMessage, deps: Proje async function activateSelection(requested: SidebarTarget, deps: ProjectMessageDeps, restore = false): Promise { if (disabled(deps)) return - const token = Symbol() - selections.set(deps, token) const ctx = deps.contexts.resolve(requested.projectId) if (!ctx || !deps.contexts.usable(requested.projectId)) { deps.error("The project is unavailable. Check that the repository still exists.") return } + // Record the token only for an actionable target, so a click on an + // unavailable project cannot discard a pending valid selection. + const token = Symbol() + selections.set(deps, token) const result = await deps.ready(ctx, { warm: true }) // A newer click can finish while this project's readiness is pending. if (selections.get(deps) !== token || !deps.enabled()) return diff --git a/packages/kilo-vscode/tests/unit/agent-project-selection.test.ts b/packages/kilo-vscode/tests/unit/agent-project-selection.test.ts index de47b316b65b..610eb716f6c0 100644 --- a/packages/kilo-vscode/tests/unit/agent-project-selection.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-project-selection.test.ts @@ -155,6 +155,29 @@ describe("activateSelection — cross-project selection", () => { expect(calls.error).toEqual([]) }) + it("keeps a pending selection when a newer click targets an unavailable project", async () => { + const gate = Promise.withResolvers() + const { contexts, deps, calls, extra } = setup({ + ready: (ctx) => + ctx.ensureReady(async () => { + ctx.stateManager() + if (!ctx.pinned) await gate.promise + return { ok: true, refsFixed: 0 } + }), + }) + + const pending = handleProjectMessage(activateMsg(extra), deps) + await handleProjectMessage(activateMsg("prj-missing"), deps) + expect(calls.selected).toEqual([]) + expect(calls.error.length).toBe(1) + + gate.resolve() + await pending + + expect(contexts.active()?.id).toBe(extra) + expect(calls.selected).toEqual([extra]) + }) + it("falls back to local without an error toast when the target is gone", async () => { const { contexts, deps, calls, extra } = setup() contexts.activate(PINNED)