Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/quiet-agents-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---

Expose a read-only Agent Manager overview so agents can inspect active worktrees, sessions, paths, status, PR badges, and git counts before orchestrating more work.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { WorktreeImporter } from "./worktree-importer"
import { restoreWorktrees } from "./state-recovery"
import { diffSummary as localDiffSummary, diffFile as localDiffFile } from "./local-diff"
import { parseToolRequest, startFromTool, type ToolRequest } from "./tool-start"
import { writeOverviewSnapshot, type AgentManagerOverviewSnapshot } from "./overview-snapshot"

import { buildKeybindingMap } from "./format-keybinding"
import { resolveVersionModels, buildInitialMessages, type CreatedVersion } from "./multi-version"
Expand Down Expand Up @@ -453,6 +454,12 @@ export class AgentManagerProvider implements Disposable {
this.connectionService.registerOpen("agent-manager", m.sessionIDs)
return null
}
if (m.type === "agentManager.overviewSnapshot") {
void writeOverviewSnapshot(this.getRoot(), m.snapshot as unknown as AgentManagerOverviewSnapshot, (msg) =>
this.log(msg),
)
return null
}
}

private onUiMessage(
Expand Down Expand Up @@ -678,6 +685,7 @@ export class AgentManagerProvider implements Disposable {
existingBranch?: string
name?: string
label?: string
requestId?: string
}): Promise<{
worktree: ReturnType<WorktreeStateManager["addWorktree"]>
result: CreateWorktreeResult
Expand Down Expand Up @@ -732,6 +740,7 @@ export class AgentManagerProvider implements Disposable {
remote: result.remote,
groupId: opts?.groupId,
label: opts?.label,
requestId: opts?.requestId,
})

// Push state immediately so the sidebar shows the new worktree with a loading indicator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export interface Worktree {
originalBranch?: string
/** Section this worktree belongs to, or undefined for ungrouped. */
sectionId?: string
/** Agent Manager tool request that created this worktree, if any. */
requestId?: string
}

export interface Section {
Expand All @@ -62,6 +64,8 @@ export interface ManagedSession {
id: string
worktreeId: string | null
createdAt: string
/** Agent Manager tool request that created this session, if any. */
requestId?: string
}

interface StateFile {
Expand Down Expand Up @@ -174,6 +178,7 @@ export class WorktreeStateManager {
remote?: string
groupId?: string
label?: string
requestId?: string
}): Worktree {
const id = generateId("wt")
const wt: Worktree = {
Expand All @@ -186,6 +191,7 @@ export class WorktreeStateManager {
if (params.remote) wt.remote = params.remote
if (params.groupId) wt.groupId = params.groupId
if (params.label) wt.label = params.label
if (params.requestId) wt.requestId = params.requestId
this.worktrees.set(id, wt)
this.setNormalizedWorktreeOrder(this.worktreeOrder)
this.log(
Expand Down Expand Up @@ -271,8 +277,9 @@ export class WorktreeStateManager {
return orphaned
}

addSession(sessionId: string, worktreeId: string | null): ManagedSession {
addSession(sessionId: string, worktreeId: string | null, requestId?: string): ManagedSession {
const session: ManagedSession = { id: sessionId, worktreeId, createdAt: new Date().toISOString() }
if (requestId) session.requestId = requestId
this.sessions.set(sessionId, session)
this.log(`Added session ${sessionId} to worktree ${worktreeId ?? "local"}`)
void this.save()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ function createMockHost(): Host {
createOutput: () => ({ appendLine: vi.fn(), dispose: vi.fn() }) as OutputHandle,
extensionKeybindings: () => [],
serverPort: () => undefined,
copyToClipboard: vi.fn(),
capture: vi.fn(),
openExternal: vi.fn(),
refreshGit: vi.fn(),
dispose: vi.fn(),
}
}
Expand Down
165 changes: 165 additions & 0 deletions packages/kilo-vscode/src/agent-manager/overview-snapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import * as fs from "node:fs"
import * as path from "node:path"

export interface AgentManagerOverviewSnapshot {
version: number
generatedAt: string
root: string
active: {
tabId?: string
sessionId?: string
worktreeId?: string | null
}
summary: {
total: number
running: number
waiting: number
idle: number
done: number
failed: number
stale: number
worktrees: number
localTabs: number
}
requests: Array<{
id: string
source?: string
mode?: string
versions?: boolean
createdAt?: string
sessionIds: string[]
worktreeIds: string[]
status: "running" | "waiting" | "idle" | "done" | "failed" | "unknown"
summary: Record<string, number>
}>
sections: Array<{
id: string
name: string
selected?: boolean
collapsed?: boolean
tabIds: string[]
worktreeIds: string[]
sessionIds: string[]
summary: Record<string, number>
}>
tabs: Array<{
id: string
kind: "local" | "worktree"
selected: boolean
section: string
name: string
cwd: string
worktreeId: string | null
sessionId?: string
status: "running" | "waiting" | "idle" | "done" | "failed" | "unknown"
lastActivityAt?: string
stale: boolean
staleReason?: string
}>
worktrees: Array<{
id: string
section: string
name: string
path: string
branch: string
base?: string
selected: boolean
status: "running" | "waiting" | "idle" | "done" | "failed" | "unknown" | "inactive"
sessionIds: string[]
tabIds: string[]
requestId?: string
git?: {
changes?: number
files?: number
additions?: number
deletions?: number
ahead?: number
behind?: number
conflicts?: number
hasPr?: boolean
}
pr?: {
attached: boolean
number?: number
url?: string
state?: string
review?: string | null
checks?: string
}
runState?: "running" | "stopping" | "stopped" | "unknown"
lastActivityAt?: string
stale: boolean
staleReason?: string
}>
local?: {
selected: boolean
cwd: string
branch?: string
status: "running" | "waiting" | "idle" | "done" | "failed" | "unknown" | "inactive"
sessionIds: string[]
tabIds: string[]
stats?: {
changes?: number
files?: number
additions?: number
deletions?: number
ahead?: number
behind?: number
conflicts?: number
hasPr?: boolean
}
runState?: "running" | "stopping" | "stopped" | "unknown"
lastActivityAt?: string
stale: boolean
staleReason?: string
}
sessions: Array<{
id: string
tabId: string
worktreeId: string | null
requestId?: string
kind: "local" | "worktree"
section: string
name: string
cwd: string
status: "running" | "waiting" | "idle" | "done" | "failed" | "unknown"
selected: boolean
agent?: string
model?: string
startedAt?: string
lastActivityAt?: string
stale: boolean
staleReason?: string
attention?: "input" | "permission" | "error" | "none"
}>
}

const FILE = "agent-manager-overview.json"

function managerRoot(root: string): string {
const parts = path.resolve(root).split(path.sep)
const found = parts
.map((part, i) => ({ part, i }))
.filter((item) => (item.part === ".kilo" || item.part === ".kilocode") && parts[item.i + 1] === "worktrees")
.at(-1)
const index = found?.i ?? -1
if (index === -1) return root
return parts.slice(0, index).join(path.sep) || path.sep
}

export async function writeOverviewSnapshot(
root: string | undefined,
snapshot: AgentManagerOverviewSnapshot,
log: (message: string) => void,
): Promise<void> {
if (!root) return
const base = managerRoot(root)
const dir = path.join(base, ".kilo")
const file = path.join(dir, FILE)
try {
await fs.promises.mkdir(dir, { recursive: true })
await fs.promises.writeFile(file, `${JSON.stringify({ ...snapshot, root: base }, null, 2)}\n`, "utf-8")
} catch (error) {
log(`Failed to write Agent Manager overview snapshot: ${error}`)
}
}
15 changes: 10 additions & 5 deletions packages/kilo-vscode/src/agent-manager/tool-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface ToolDeps {
branchName?: string
name?: string
label?: string
requestId?: string
}) => Promise<WorktreeCreated | null>
cleanupWorktree: (wid: string, dir: string) => Promise<void>
setup: (dir: string, branch?: string, id?: string) => Promise<void>
Expand Down Expand Up @@ -108,7 +109,7 @@ async function prompt(client: KiloClient, sid: string, dir: string, task: ToolTa
)
}

async function local(deps: ToolDeps, client: KiloClient, task: ToolTask, directory?: string) {
async function local(deps: ToolDeps, client: KiloClient, task: ToolTask, requestId: string, directory?: string) {
const root = deps.getRoot()
const state = deps.getState()
if (!root || !state) return false
Expand All @@ -126,7 +127,7 @@ async function local(deps: ToolDeps, client: KiloClient, task: ToolTask, directo
const target = wt?.path ?? root
const { data } = await client.session.create({ directory: target, platform: PLATFORM }, { throwOnError: true })
const session = data
state.addSession(session.id, wt?.id ?? null)
state.addSession(session.id, wt?.id ?? null, requestId)
if (wt) deps.registerWorktreeSession(session.id, wt.path)
deps.push()
deps.getPanel()?.sessions.registerSession(session)
Expand All @@ -138,6 +139,7 @@ async function local(deps: ToolDeps, client: KiloClient, task: ToolTask, directo
tool: true,
mode: "local",
worktreeId: wt?.id,
requestId,
})
return true
}
Expand All @@ -148,6 +150,7 @@ async function worktree(
task: ToolTask,
index: number,
total: number,
requestId: string,
groupId?: string,
versions?: boolean,
) {
Expand All @@ -159,6 +162,7 @@ async function worktree(
branchName: version.branch,
name: version.branch,
label: versionedLabel(baseLabel, versions ? index : 0, versions ? total : 1),
requestId,
})
if (!created) return false

Expand All @@ -174,7 +178,7 @@ async function worktree(
await deps.cleanupWorktree(created.worktree.id, created.result.path)
return false
}
state.addSession(session.id, created.worktree.id)
state.addSession(session.id, created.worktree.id, requestId)
deps.registerWorktreeSession(session.id, created.result.path)
deps.notifyReady(session.id, created.result, created.worktree.id)
deps.getPanel()?.sessions.registerSession(session)
Expand All @@ -184,6 +188,7 @@ async function worktree(
sessionId: session.id,
worktreeId: created.worktree.id,
branch: created.result.branch,
requestId,
tool: true,
})
return true
Expand All @@ -205,8 +210,8 @@ export async function startFromTool(deps: ToolDeps, req: ToolRequest): Promise<v
try {
const done =
req.mode === "local"
? await local(deps, client, task, req.directory)
: await worktree(deps, client, task, i, total, groupId, versions)
? await local(deps, client, task, req.requestID, req.directory)
: await worktree(deps, client, task, i, total, req.requestID, groupId, versions)
if (done) state.ok++
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
Expand Down
6 changes: 6 additions & 0 deletions packages/kilo-vscode/src/agent-manager/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,11 @@ interface SetDefaultBaseBranchIn {
branch?: string
}

interface OverviewSnapshotIn {
type: "agentManager.overviewSnapshot"
snapshot: Record<string, unknown>
}

interface RequestExternalWorktreesIn {
type: "agentManager.requestExternalWorktrees"
}
Expand Down Expand Up @@ -739,6 +744,7 @@ export type AgentManagerInMessage =
| SetReviewDiffStyleIn
| SetReviewMarkdownRenderIn
| SetDefaultBaseBranchIn
| OverviewSnapshotIn
| RequestExternalWorktreesIn
| ImportFromBranchIn
| ImportFromPRIn
Expand Down
Loading
Loading