mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 11:05:31 +08:00
fix(agent-manager): restore multi-project progress indicators
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix Agent Manager progress indicators when multiple projects are expanded.
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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<string, string>
|
||||
t: LanguageContextValue["t"]
|
||||
onSearchRef: (ref: SidebarSearchMenuRef) => void
|
||||
@@ -211,6 +213,9 @@ export const ProjectList: Component<Props> = (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]}
|
||||
|
||||
@@ -36,6 +36,8 @@ interface Props {
|
||||
state?: AgentManagerStateMessage
|
||||
store?: ProjectStore
|
||||
busy?: (id: string) => boolean
|
||||
working?: (id: string) => boolean
|
||||
localBusy?: () => boolean
|
||||
stats?: Record<string, WorktreeGitStats>
|
||||
local?: LocalGitStats
|
||||
prs?: Record<string, PRStatus | null>
|
||||
@@ -219,7 +221,7 @@ export const ProjectSidebarBody: Component<Props> = (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> = (props) => {
|
||||
data-sidebar-id={`${props.project.id}:local`}
|
||||
onClick={() => props.onSelectLocal(props.project.id)}
|
||||
>
|
||||
<svg class="am-local-icon" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2.5" y="3.5" width="15" height="10" rx="1" stroke="currentColor" />
|
||||
<path d="M6 16.5H14" stroke="currentColor" stroke-linecap="square" />
|
||||
<path d="M10 13.5V16.5" stroke="currentColor" />
|
||||
</svg>
|
||||
<Show when={!props.localBusy?.()} fallback={<Spinner class="am-worktree-spinner" />}>
|
||||
<svg class="am-local-icon" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2.5" y="3.5" width="15" height="10" rx="1" stroke="currentColor" />
|
||||
<path d="M6 16.5H14" stroke="currentColor" stroke-linecap="square" />
|
||||
<path d="M10 13.5V16.5" stroke="currentColor" />
|
||||
</svg>
|
||||
</Show>
|
||||
<div class="am-local-text">
|
||||
<span class="am-local-label">{props.t("agentManager.local")}</span>
|
||||
<Show when={props.local?.branch}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }]]))
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -23,6 +23,7 @@ import type { MemoryShowMessage, MemoryOperationMessage, RequestMemoryMessage }
|
||||
|
||||
export interface SendMessageRequest {
|
||||
type: "sendMessage"
|
||||
projectId?: string
|
||||
text: string
|
||||
messageID?: string
|
||||
sessionID?: string
|
||||
|
||||
Reference in New Issue
Block a user