From 561d1782aedd2c8b98f6e2cd6d88937b9c37fd5b Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 4 Aug 2026 12:16:22 +0200 Subject: [PATCH] fix(agent-manager): restore multi-project progress indicators --- .changeset/fix-multi-project-progress.md | 5 +++ .../agent-manager/provider-multi-version.ts | 16 ++++++- .../kilo-vscode/src/agent-manager/types.ts | 7 +++ .../agent-manager-initial-message.test.ts | 2 + .../tests/unit/agent-project-progress.test.ts | 45 +++++++++++++++++++ .../tests/unit/project-store.test.ts | 12 +++++ .../agent-manager/AgentManagerApp.tsx | 33 ++++++++------ .../webview-ui/agent-manager/ProjectList.tsx | 7 ++- .../agent-manager/ProjectSidebarBody.tsx | 16 ++++--- .../agent-manager/initial-message.ts | 1 + .../agent-manager/project/progress.ts | 23 ++++++++++ .../src/types/messages/extension-messages.ts | 6 +++ .../src/types/messages/webview-messages.ts | 1 + 13 files changed, 152 insertions(+), 22 deletions(-) create mode 100644 .changeset/fix-multi-project-progress.md create mode 100644 packages/kilo-vscode/tests/unit/agent-project-progress.test.ts create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/project/progress.ts diff --git a/.changeset/fix-multi-project-progress.md b/.changeset/fix-multi-project-progress.md new file mode 100644 index 0000000000..53ccaf5730 --- /dev/null +++ b/.changeset/fix-multi-project-progress.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix Agent Manager progress indicators when multiple projects are expanded. diff --git a/packages/kilo-vscode/src/agent-manager/provider-multi-version.ts b/packages/kilo-vscode/src/agent-manager/provider-multi-version.ts index 37a9fe93a1..41cc02049e 100644 --- a/packages/kilo-vscode/src/agent-manager/provider-multi-version.ts +++ b/packages/kilo-vscode/src/agent-manager/provider-multi-version.ts @@ -50,6 +50,7 @@ export async function createMultiVersion( // Notify webview that multi-version creation has started host.post({ type: "agentManager.multiVersionProgress", + projectId: ctx.id, status: "creating", total: versions, completed: 0, @@ -78,6 +79,7 @@ export async function createMultiVersion( // Update progress host.post({ type: "agentManager.multiVersionProgress", + projectId: ctx.id, status: "creating", total: versions, completed: created.length, @@ -86,11 +88,19 @@ export async function createMultiVersion( } // Phase 2: Send the initial prompt to all sessions, or clear busy state if no text. - await sendInitialPrompts(host, created, models, { providerID, modelID }, { text, agent, variant: msg.variant, files }) + await sendInitialPrompts( + host, + ctx.id, + created, + models, + { providerID, modelID }, + { text, agent, variant: msg.variant, files }, + ) // Notify completion host.post({ type: "agentManager.multiVersionProgress", + projectId: ctx.id, status: "done", total: versions, completed: created.length, @@ -172,6 +182,7 @@ async function createVersion( if (earlyProviderID && earlyModelID) { host.post({ type: "agentManager.setSessionModel", + projectId: ctx.id, sessionId: session.id, providerID: earlyProviderID, modelID: earlyModelID, @@ -232,6 +243,7 @@ async function reconcileSandbox( /** Fan the initial prompt out to every created session, throttled between sends. */ async function sendInitialPrompts( host: MultiVersionHost, + projectId: string, created: CreatedVersion[], models: VersionSpec["models"], resolved: { providerID: string | undefined; modelID: string | undefined }, @@ -254,7 +266,7 @@ async function sendInitialPrompts( modelID: msg.modelID, }) } - host.post({ type: "agentManager.sendInitialMessage", ...msg }) + host.post({ type: "agentManager.sendInitialMessage", projectId, ...msg }) if (input.text && i < messages.length - 1) { await new Promise((resolve) => setTimeout(resolve, 300)) } diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 3a2b555e35..5f08325a9f 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -258,6 +258,8 @@ interface SessionClosedMessage { interface MultiVersionProgressMessage { type: "agentManager.multiVersionProgress" + /** Owning project; absent in single-project mode. */ + projectId?: string status: "creating" | "done" total: number completed: number @@ -266,6 +268,8 @@ interface MultiVersionProgressMessage { interface SetSessionModelMessage { type: "agentManager.setSessionModel" + /** Owning project; absent in single-project mode. */ + projectId?: string sessionId: string providerID: string modelID: string @@ -273,6 +277,8 @@ interface SetSessionModelMessage { interface SendInitialMessage { type: "agentManager.sendInitialMessage" + /** Owning project; absent in single-project mode. */ + projectId?: string sessionId: string worktreeId: string text?: string @@ -791,6 +797,7 @@ interface FileSourceIn { interface SendMessageIn { type: "sendMessage" + projectId?: string text: string messageID?: string sessionID?: string diff --git a/packages/kilo-vscode/tests/unit/agent-manager-initial-message.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-initial-message.test.ts index cba8460789..87714a5cac 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-initial-message.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-initial-message.test.ts @@ -5,6 +5,7 @@ describe("Agent Manager initial message", () => { it("forwards the selected variant to sendMessage", () => { const msg = initialMessage({ type: "agentManager.sendInitialMessage", + projectId: "project-a", sessionId: "session-a", worktreeId: "wt-a", text: "Fix it", @@ -16,6 +17,7 @@ describe("Agent Manager initial message", () => { expect(msg).toEqual({ type: "sendMessage", + projectId: "project-a", text: "Fix it", sessionID: "session-a", providerID: "anthropic", diff --git a/packages/kilo-vscode/tests/unit/agent-project-progress.test.ts b/packages/kilo-vscode/tests/unit/agent-project-progress.test.ts new file mode 100644 index 0000000000..21965da952 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/agent-project-progress.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "bun:test" +import { createProjectStore } from "../../webview-ui/agent-manager/project/store" +import { clearMultiVersionBusy, markMultiVersionBusy } from "../../webview-ui/agent-manager/project/progress" + +const state = (projectId: string) => ({ + type: "agentManager.state" as const, + projectId, + worktrees: [ + { + id: "same", + branch: `${projectId}-same`, + path: `/repo/${projectId}/same`, + parentBranch: "main", + createdAt: "2026-01-01", + groupId: "group", + }, + ], + sessions: [{ id: `${projectId}-session`, worktreeId: "same", createdAt: "2026-01-01" }], + sections: [], +}) + +describe("multi-project progress state", () => { + it("updates only the owning project's grouped worktrees", () => { + const first = createProjectStore("a") + const second = createProjectStore("b") + first.applyState(state("a")) + second.applyState(state("b")) + first.setBusy(new Map([["same", { reason: "setting-up" as const }]])) + second.setBusy(new Map([["same", { reason: "setting-up" as const }]])) + + clearMultiVersionBusy(second, "group") + + expect(first.busy().has("same")).toBe(true) + expect(second.busy().has("same")).toBe(false) + }) + + it("marks a newly created grouped worktree as busy in its project store", () => { + const store = createProjectStore("a") + store.applyState(state("a")) + + markMultiVersionBusy(store, "a-session") + + expect(store.busy().get("same")?.reason).toBe("setting-up") + }) +}) diff --git a/packages/kilo-vscode/tests/unit/project-store.test.ts b/packages/kilo-vscode/tests/unit/project-store.test.ts index 84dbe9e710..7f0c7d070e 100644 --- a/packages/kilo-vscode/tests/unit/project-store.test.ts +++ b/packages/kilo-vscode/tests/unit/project-store.test.ts @@ -42,4 +42,16 @@ describe("project stores", () => { same: { worktreeId: "same", state: "running" }, }) }) + + it("keeps busy worktrees isolated between projects", () => { + const first = createProjectStore("a") + const second = createProjectStore("b") + first.applyState(state("a", ["same"])) + second.applyState(state("b", ["same"])) + + first.setBusy(new Map([["same", { reason: "setting-up" as const }]])) + + expect(first.busy().has("same")).toBe(true) + expect(second.busy().has("same")).toBe(false) + }) }) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 939a8e505b..e4649be8f7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -89,6 +89,7 @@ import type { WorktreeBusyState } from "./project/store" import { rememberTarget, restoreProjectTarget } from "./project/restore" import { createProjectStateRouter } from "./project/state" import { applyRunStatus } from "./project/run-status" +import { clearMultiVersionBusy, markMultiVersionBusy } from "./project/progress" import { selectLocalAction, selectWorktreeAction } from "./selection-actions" import { DataBridge } from "../src/App" import { LanguageBridge } from "../src/context/language-bridge" @@ -865,6 +866,16 @@ const AgentManagerContent: Component = () => { /** True when a local session is actively working. */ const isLocalBusy = (): boolean => isAnySessionBusy(localSessionIDs()) + const projectBusy = (projectId: string, worktreeId: string | null): boolean => { + if (projectId === activeProjectId()) { + return worktreeId === null ? isLocalBusy() : isAgentBusy(worktreeId) + } + const ids = (projectSessionsLive()[projectId] ?? []) + .filter((item) => item.worktreeId === worktreeId) + .map((item) => item.id) + return isAnySessionBusy(ids) + } + const isSessionBusy = (id: string): boolean => isAnySessionBusy([id]) /** Worktrees sorted so that grouped items are always adjacent, respecting custom order if set. */ @@ -1447,13 +1458,8 @@ const AgentManagerContent: Component = () => { const ev = msg as unknown as AgentManagerMultiVersionProgressMessage if (ev.status === "done" && ev.groupId) { // Clear busy state for all worktrees in this group - setBusyWorktrees((prev) => { - const next = new Map(prev) - for (const wt of worktrees()) { - if (wt.groupId === ev.groupId) next.delete(wt.id) - } - return next - }) + const store = ev.projectId ? registry.ensure(ev.projectId) : registry.active() + clearMultiVersionBusy(store, ev.groupId) } } @@ -1462,11 +1468,8 @@ const AgentManagerContent: Component = () => { if (msg.type === "agentManager.worktreeSetup") { const ev = msg as AgentManagerWorktreeSetupMessage if (ev.status === "ready" && ev.sessionId) { - const ms = managedSessions().find((s) => s.id === ev.sessionId) - const wt = ms?.worktreeId ? worktrees().find((w) => w.id === ms.worktreeId) : undefined - if (wt?.groupId) { - setBusyWorktrees((prev) => new Map([...prev, [wt.id, { reason: "setting-up" as const }]])) - } + const store = ev.projectId ? registry.ensure(ev.projectId) : registry.active() + markMultiVersionBusy(store, ev.sessionId) } } @@ -1503,7 +1506,8 @@ const AgentManagerContent: Component = () => { // Clear busy state — use worktreeId from the message directly // to avoid race condition where managedSessions() hasn't updated yet if (ev.worktreeId) { - setBusyWorktrees((prev) => { + const store = ev.projectId ? registry.ensure(ev.projectId) : registry.active() + store.setBusy((prev) => { const next = new Map(prev) next.delete(ev.worktreeId) return next @@ -2335,6 +2339,9 @@ const AgentManagerContent: Component = () => { projects={projectList()} states={projectStates()} store={(id) => registry.ensure(id)} + busy={(projectId, id) => registry.ensure(projectId).busy().has(id)} + working={(projectId, id) => projectBusy(projectId, id)} + localBusy={(projectId) => projectBusy(projectId, null)} stats={projectLive.stats()} local={projectLive.local()} prs={projectLive.prs()} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx index 3fb1220c16..cbfc6c08b7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx @@ -34,7 +34,9 @@ interface Props { selection?: string currentSessionID?: () => string | undefined mode: ModeRouter - busy?: (id: string) => boolean + busy?: (projectId: string, id: string) => boolean + working?: (projectId: string, id: string) => boolean + localBusy?: (projectId: string) => boolean bindings: Record t: LanguageContextValue["t"] onSearchRef: (ref: SidebarSearchMenuRef) => void @@ -211,6 +213,9 @@ export const ProjectList: Component = (props) => { project={project} state={props.states[project.id]} store={props.store?.(project.id)} + busy={(id) => props.busy?.(project.id, id) ?? false} + working={(id) => props.working?.(project.id, id) ?? false} + localBusy={() => props.localBusy?.(project.id) ?? false} stats={props.stats[project.id]} local={props.local[project.id]} prs={props.prs[project.id]} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx index 40b3cd82cb..77a479db17 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx @@ -36,6 +36,8 @@ interface Props { state?: AgentManagerStateMessage store?: ProjectStore busy?: (id: string) => boolean + working?: (id: string) => boolean + localBusy?: () => boolean stats?: Record local?: LocalGitStats prs?: Record @@ -219,7 +221,7 @@ export const ProjectSidebarBody: Component = (props) => { active={active() && props.selection === worktree.id} pendingDelete={pending() === worktree.id} busy={props.busy?.(worktree.id) ?? false} - working={runs()[worktree.id]?.state === "running"} + working={props.working?.(worktree.id) || runs()[worktree.id]?.state === "running"} stale={state()?.staleWorktreeIds?.includes(worktree.id) === true} stats={props.stats?.[worktree.id]} sessions={sessions(worktree.id).length} @@ -279,11 +281,13 @@ export const ProjectSidebarBody: Component = (props) => { data-sidebar-id={`${props.project.id}:local`} onClick={() => props.onSelectLocal(props.project.id)} > - - - - - + }> + + + + + +
{props.t("agentManager.local")} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/initial-message.ts b/packages/kilo-vscode/webview-ui/agent-manager/initial-message.ts index 15049eec69..7f210d4a56 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/initial-message.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/initial-message.ts @@ -9,6 +9,7 @@ export function initialMessage(ev: AgentManagerSendInitialMessage): SendMessageR if (!ev.text) return undefined return { type: "sendMessage", + ...(ev.projectId ? { projectId: ev.projectId } : {}), text: ev.text, sessionID: ev.sessionId, providerID: ev.providerID, diff --git a/packages/kilo-vscode/webview-ui/agent-manager/project/progress.ts b/packages/kilo-vscode/webview-ui/agent-manager/project/progress.ts new file mode 100644 index 0000000000..807c7b2be9 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/project/progress.ts @@ -0,0 +1,23 @@ +import type { ProjectStore } from "./store" + +/** Clear the loading indicators for every worktree in one multi-version group. */ +export function clearMultiVersionBusy(store: ProjectStore, groupId: string): void { + const ids = new Set( + store + .worktrees() + .filter((wt) => wt.groupId === groupId) + .map((wt) => wt.id), + ) + if (ids.size === 0) return + store.setBusy((prev) => new Map([...prev].filter(([id]) => !ids.has(id)))) +} + +/** Keep a newly created grouped worktree showing progress until its prompt starts. */ +export function markMultiVersionBusy(store: ProjectStore, sessionId: string): void { + const session = store.managedSessions().find((item) => item.id === sessionId) + const id = session?.worktreeId + if (!id) return + const worktree = store.worktrees().find((item) => item.id === id) + if (!worktree?.groupId) return + store.setBusy((prev) => new Map([...prev, [id, { reason: "setting-up" as const }]])) +} diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts index d7000fd4d0..7e7dc8f4f4 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts @@ -894,6 +894,8 @@ export interface SandboxStatusErrorMessage { // Multi-version creation progress (extension → webview) export interface AgentManagerMultiVersionProgressMessage { type: "agentManager.multiVersionProgress" + /** Owning project; absent in single-project mode. */ + projectId?: string status: "creating" | "done" total: number completed: number @@ -1037,6 +1039,8 @@ export interface WorktreeStatsLoadedMessage { // Set the model for a session (extension → webview, used during multi-version creation) export interface AgentManagerSetSessionModelMessage { type: "agentManager.setSessionModel" + /** Owning project; absent in single-project mode. */ + projectId?: string sessionId: string providerID: string modelID: string @@ -1045,6 +1049,8 @@ export interface AgentManagerSetSessionModelMessage { // Request webview to send initial prompt to a newly created session (extension → webview) export interface AgentManagerSendInitialMessage { type: "agentManager.sendInitialMessage" + /** Owning project; absent in single-project mode. */ + projectId?: string sessionId: string worktreeId: string text?: string diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts index 4f11b99d7c..2e4692a0be 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts @@ -23,6 +23,7 @@ import type { MemoryShowMessage, MemoryOperationMessage, RequestMemoryMessage } export interface SendMessageRequest { type: "sendMessage" + projectId?: string text: string messageID?: string sessionID?: string