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..0a7a60a02683 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( @@ -138,7 +139,13 @@ async function activateSelection(requested: SidebarTarget, deps: ProjectMessageD 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 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..610eb716f6c0 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,53 @@ 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("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)