.kilo/plans/agent-manager-multi-project-runtime.md
UI architecture update: the active-body plus background-summary model in this document is superseded by
agent-manager-multi-project-uniform-ui.md. The runtime findings remain useful, but the final UI renders one permanent real interactive body for every expanded project and requires strict project-qualified routing before background actions are enabled.
Status: Runtime foundation implemented (this worktree); accordion UI pending
Landed in this worktree, all covered by unit tests (tests/unit/agent-project-*.test.ts):
src/agent-manager/project-paths.ts — canonical root resolution (realpath + normalizePath), case-aware samePath, deterministic projectIdFor(root) (sha1, stable across restarts), resolveGitRoot.src/agent-manager/project-registry.ts — versioned global catalog of additional projects (storage-injected, corrupt-safe, dedupe by id, trust/label/order). The pinned workspace project is never persisted.src/agent-manager/project-context.ts — immutable ProjectContext (owns WorktreeStateManager/WorktreeManager/SetupScriptService/stale set per canonical root, lazy creation, peek accessors) and ProjectContexts coordinator (pinned derivation, active/expanded lifecycle, trust+flag gating, syncPinned() for workspace changes, webview ProjectSnapshots).src/agent-manager/project-messages.ts — vscode-free handlers: requestProjects, addProject (folder picker → git root resolve → dedupe), removeProject, selectProject, setProjectExpanded, trustProject. All fail closed when the flag is off.src/agent-manager/project-wiring.ts — factory assembling registry/contexts/deps/host listeners (extracted to keep AgentManagerProvider.ts under its 2000-line cap).AgentManagerProvider — fields state/worktrees/setupScript/staleWorktreeIds replaced by context-backed getters; every existing handler now operates on the active project with zero per-handler changes. onMessage consumes project messages first and drops messages whose projectId mismatches the active project (stale-pane protection). activateProject re-runs initializeState for the new context; pollers/importer/run/terminal follow the active context through the existing getters. agentManager.state now carries projectId.agentManager.projects out-message (multiProject flag + ProjectSnapshot[]); new in-messages listed above; projectId? on agentManager.state and createWorktree. Webview type mirrors updated (webview-ui/src/types/messages/{extension,webview}-messages.ts).pickFolder, multiProject, readProjects/writeProjects (globalState key agentManager.projects), onDidChangeWorkspaceFolders, onDidChangeMultiProject.kilo-code.new.agentManager.multiProject (boolean, default false, application scope)..changeset/agent-manager-multi-project.md.Behavior with flag off: only the pinned workspace project exists; snapshots contain exactly one project; all registry data is preserved but hidden; no UI change.
agentManager.projects: header-only rows for collapsed/uninitialized projects (registry metadata only), full existing Local/worktree body for the active project, expanded non-active bodies from per-context state (extension must push per-context state with that context's projectId — currently pushState only pushes the active context).agentManager.trustProject then expand/select), remove/rename in project menu.selectProject before any repo action in a non-active project; send projectId on createWorktree (extension drops mismatches).pushState for expanded non-active projects; per-project git stats/PR pollers (ProjectPollers in project-pollers.ts) so every expanded accordion receives live stats/PR data tagged with its projectId; webview project-live.ts store routing per-project payloads into per-project summary stores; activate() keeps the previously active project expanded.KiloProvider.projectID and session refresh remain single-project; cross-project SSE filtering by session→directory owner is a follow-up (see "Verified findings" below).Build the project-aware Agent Manager runtime before changing the UI. The eventual UI is a pinned current-project view plus lazily initialized additional project accordions, with one shared tab/detail area. The first backend slice must be mergeable with the flag disabled and must route today's single project through the same project-aware contracts that secondary projects will use.
The implementation must preserve these invariants:
.kilo/agent-manager.json..kilo/agent-manager.json read, Git mutation, setup/run initialization, terminal creation, or poller subscription.AgentManagerProvider currently owns the panel and all repository-bound resources in one 1,941-line singleton. Its root is always Host.workspacePath() and its state/managers are lazily cached against that root (packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts:56-212, 1654-1695). The architecture test caps this file at 2,000 lines and explicitly forbids raising the cap (packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts:793-854).ensureGitExclude, loads .kilo/agent-manager.json, discovers/restores worktrees, registers sessions, starts state-dependent behavior, and recovers prompts (AgentManagerProvider.ts:267-360). This whole sequence cannot run for a collapsed catalog entry.WorktreeStateManager is correctly repository-scoped by constructor root and writes only <root>/.kilo/agent-manager.json (WorktreeStateManager.ts:100-124, 632-656, 797-847). Its save queue and flush() are suitable for a context shutdown barrier, but it has no cancellation/dispose primitive (755-795).KiloProvider is one provider attached to the one panel/webview (agent-manager/vscode-host.ts:67-169). It already routes many SDK calls through a session-to-directory map, but local sessions are allowed to fall back to workspaceFolders[0] (KiloProvider.ts:4343-4378; kilo-provider-utils.ts:319-345). That fallback becomes a cross-project write risk.KiloProvider has singleton project state (projectID, currentSession, project-scoped caches, one active project directory) and current session refresh treats all extra directories as worktrees of one canonical project (KiloProvider.ts:344-359, 1984-2029; kilo-provider-utils.ts:230-295). It is not multi-project-correct without refactoring.directory on session share and unshare (packages/sdk/js/src/v2/gen/sdk.gen.ts:4733-4795). Sharing must therefore use the same project/session route as prompts and transcript reads.packages/opencode/src/id/id.ts:1-17, 23-33, 42-49). Raw IDs can remain the SDK/server identifier, but the UI/runtime identity still needs a project-qualified reference, and route registration must reject an observed cross-project collision.session.status currently discards the SSE directory before invoking branch naming (AgentManagerProvider.ts:204-220). The orchestration bridge subscribes globally and rejects requests outside its root (orchestration-bridge.ts:175-203). Creating one unchanged bridge per project would let the wrong bridge reject a valid request before the correct project handles it.SessionTerminalManager instances would each register the same global VS Code commands and terminal listeners (SessionTerminalManager.ts:35-81). It must remain panel/coordinator-owned, with project-qualified keys and explicit CWDs. Also, TerminalRouter currently falls back from an unknown worktree ID to the repository root (terminal-routing.ts:120-130), which must be removed in strict routing.VscodeHost.workspacePath() uses workspaceFolders[0] (vscode-host.ts:172-174; review-utils.ts:12-15). It loses URI scheme/authority, and openFolder recreates paths with Uri.file. A catalog containing only path strings would be unsafe across local, SSH, container, and other remote extension hosts.features.ts; webview-ui/src/types/messages/config.ts). A multi-project bootstrap flag cannot be project-scoped because choosing which project config to read is the behavior being gated.Add a VS Code-free ProjectRegistry backed by an injected storage and resolver interface. vscode-host.ts implements those interfaces using ExtensionContext.globalState, workspace/open-dialog URIs, filesystem canonicalization, and Git.
Use these core types in src/agent-manager/project.ts:
type ProjectId = string
interface ProjectLocation {
uri: string
scheme: string
authority: string
root: string
commonGitDir: string
}
interface ProjectDescriptor extends ProjectLocation {
id: ProjectId
name: string
pinned: boolean
collapsed: boolean
order: number
status: "unverified" | "ready" | "missing" | "notGit" | "wrongAuthority"
}
root is the canonical filesystem Git toplevel. Resolve it only when deriving the pinned project, adding a project, or expanding an unverified project:
git -C <fsPath> rev-parse --show-toplevel through the existing hidden-window process wrapper.fs.promises.realpath; normalize separators, trailing separators, and Windows case for identity comparison.git rev-parse --path-format=absolute --git-common-dir as commonGitDir.ProjectId deterministically as a versioned SHA-256/base64url hash of scheme + NUL + authority + NUL + normalized canonical root. Do not expose a path-concatenated ID on the wire.Different linked worktree roots have different project IDs, but the first iteration must reject adding a project whose canonical commonGitDir matches any project already exposed in the window. Two Agent Manager contexts mutating the same Git common directory would bypass the current root-keyed worktree lock and could manage the same branches twice. This is a product-valid restriction to distinct Git projects, not a path workaround. Also change WorktreeManager's write-lock key to canonical commonGitDir so future relaxation does not reintroduce index.lock races.
Persist one unsynced global-state value under kilo.agentManager.projects.v1:
interface StoredProjectCatalogV1 {
version: 1
projects: Array<{
id: ProjectId
uri: string
scheme: string
authority: string
root: string
commonGitDir: string
name?: string
collapsed: boolean
order: number
addedAt: string
}>
}
Do not call setKeysForSync; machine/remote paths must not sync to other machines. Serialize mutations in-process. Each mutation re-reads the latest value, validates it, applies add/remove/reorder/metadata changes, and performs one globalState.update.
Registry behavior is exact:
snapshot(flag) derives the pinned current project fresh. It never reads a persisted active project.pinned: true, and defaults to expanded. It is not written into the catalog.unverified.ProjectContext..kilo, worktrees, sessions, branches, or shares.For a multi-root VS Code workspace, preserve current behavior by deriving the pinned project from workspaceFolders[0], then canonicalize that folder to its Git toplevel. Do not silently select another workspace folder if the first is not a Git repository.
Turn AgentManagerProvider into the panel-level coordinator and extract repository behavior into project-context.ts. Do not instantiate one current AgentManagerProvider per project.
The coordinator owns:
KiloProvider/SessionProvider;ProjectRegistry, active ProjectId, exposed descriptors, and Map<ProjectId, LazyProjectContext>;Semaphore(3) for Git/network polling across all projects;AgentManagerVisiblePresence;SessionTerminalManager and one project-aware TerminalRouter;Each ProjectContext is constructed once with an immutable descriptor/root/common Git directory and owns only repository-bound resources:
WorktreeStateManager, WorktreeManager, SetupScriptService, and repository GitOps;WorktreeDiffController, WorktreeImporter, and RunController;GitStatsPoller, PRStatusBridge/poller, and BranchNamingController;AgentManagerProvider.It does not own a webview, KiloProvider, VS Code terminal command registrations, global connection subscriptions, or a global orchestration subscription.
Lifecycle:
cold: descriptor only. No state manager, manager, poller, watcher, or per-repo read exists.initializing: one shared promise validates the location, constructs resources, performs the existing ensureGitExclude -> state.load -> recoverWorktrees -> register routes sequence, and captures a context generation.ready/expanded: posts project-stamped state and permits project actions. Pollers run only when both the panel is visible and this project is expanded. Multiple expanded contexts may be ready concurrently.suspended/collapsed: if active, the coordinator first activates pinned Local (or another explicit target). Stop stats/PR/diff polling and watchers, stop accepting new project mutations, wait for the current mutation barrier, flush state, and detach project-specific event side effects such as automatic branch naming. Keep validated session routes/tracking so a running backend session can still surface status and permission/question events without a repository read. Do not abort agents merely because an accordion collapsed.disposed: increment generation first, stop/dispose resources, await terminal-router/run/mutation cleanup and state flush, detach routes, then remove the exact context object from the map. Late completions may neither post nor mutate a replacement context.Expansion of a suspended context resumes its existing in-memory state and pollers; expansion of a cold context initializes. Concurrent expansions share the same promise. Collapse during initialization records the desired collapsed state, allows non-cancellable Git/state work to finish, suppresses stale posts, then immediately suspends.
The coordinator routes global events by canonical event directory before invoking a context. session.status must retain its directory. A single orchestration ingress determines the one owning context and delegates there; it rejects an unknown directory once, globally. It must never create one rejecting bridge per project. A collapsed project does not auto-initialize in response to an unsolicited orchestration event; return a typed project_inactive result.
Panel close keeps existing semantics for sessions actually opened/created by that panel, but aggregates their project-qualified routes before aborting. Project collapse and flag-off transitions do not abort them. Removing a project is refused while one of its tracked sessions or Agent Manager run/mutation is busy, rather than orphaning an interaction prompt.
Use one embedded KiloProvider. Multiple providers are not safe or useful here:
Refactor its Agent Manager adapter around an injected project/session route service while leaving sidebar and standalone providers unchanged. The route service supplies:
interface ProjectSessionRef {
projectId: ProjectId
sessionId: string
}
interface SessionRoute extends ProjectSessionRef {
directory: string
generation: number
}
Internally key UI maps by projectId + NUL + sessionId; never use that concatenation on the wire. Keep the raw sessionId for SDK calls and backend presence. Maintain a reverse raw-ID index only to process backend events. If the same raw ID is observed in two projects, mark it ambiguous and reject/drop operations and events that cannot also be disambiguated by event directory.
Required routing changes:
TerminalRouter's fallback to root.ProjectSessionRef. In strict multi-project mode, an unknown session never falls back to workspaceFolders[0] or the currently active project.projectID from the first root and merge all projects into it.KiloProvider.projectID with the route table/directory ownership check. Keep existing project filtering for non-Agent-Manager provider instances.activateProject(projectId, root) path. It updates projectDirectory, invalidates/refetches project-scoped config, providers, agents, commands, skills, MCP, indexing, sandbox, and Git status for that root. Guard every async refresh with activation generation so project A cannot populate caches after switching to B.This picks up each secondary project's config naturally through the CLI's directory-scoped instance/config resolution. Do not copy provider/config/settings data into global storage.
Define an internal/wire envelope in agent-manager/types.ts and mirror it in webview-ui/src/types/messages/agent-manager.ts:
interface ProjectStamp {
projectId: ProjectId
generation: number
}
interface ProjectEnvelope<T> {
type: "agentManager.projectMessage"
project: ProjectStamp
payload: T
}
Use project-qualified references for sessions, worktrees, and drafts:
interface SessionRef { projectId: ProjectId; sessionId: string }
interface WorktreeRef { projectId: ProjectId; worktreeId: string }
interface DraftRef { projectId: ProjectId; draftId: string }
Raw IDs remain in each repository's existing state file. The repository/context supplies the project identity when state is loaded. Do not migrate session IDs into global state and do not permit moving an existing session record between projects. A future cross-project continuation must explicitly fork/create in the target project; relabeling a raw session is invalid.
Protocol rules:
requestId inside the payload. Return a typed route error (project_required, project_unknown, project_inactive, project_mismatch, session_unknown, or session_ambiguous) without forwarding the original message.Sharing is an operation on SessionRef, not on a bare session ID:
client.session.share({ sessionID, directory }); unshare uses the exact same resolved route.projectId on output.Contribute an application-scoped VS Code setting, default false, for example kilo-code.new.agentManager.multiProject, with scope: "application". Read it in the host/coordinator; do not put it in CLI experimental config.
No .kilo/agent-manager.json schema migration is needed. Existing state is loaded unchanged by the pinned context, and additional contexts load their own existing files only when expanded. An absent catalog means no additional projects. Invalid catalog entries are ignored in the snapshot and logged, but a successful write must preserve valid entries from other authorities. Never scan the filesystem to discover projects as migration.
On restored legacy panel/webview state, unqualified tabs are accepted only in compatibility single-project mode. When multi-project mode exposes a second project, send a protocol reset and rebuild tabs/session lists from project envelopes instead of guessing which project owns a restored raw session ID.
Remote behavior:
vscode.env.remoteName alone.fsPath; preserve the original URI for openFolder and picker operations.This is the narrow first PR. It produces no secondary-project UI, but the current pinned project runs through the durable identity/routing contracts and the registry is fully tested.
src/agent-manager/project.ts with IDs, locations, descriptors, stamps, envelopes, and ref types. Add src/agent-manager/project-registry.ts with injected storage/resolver, v1 schema validation, pinned merge, authority filtering, dedupe, and serialized mutations.Host in src/agent-manager/host.ts; implement global-state storage, canonical Git/URI resolution, pinned project resolution, and the application flag in src/agent-manager/vscode-host.ts. Preserve URIs in openFolder. Add the setting contribution in packages/kilo-vscode/package.json.src/agent-manager/project-session-router.ts. It must register Local and worktree routes, resolve refs/directories, inherit child routes, validate directory ownership, detect raw-ID ambiguity, and implement compatibility versus strict mode.src/agent-manager/types.ts and the mirrored webview type file. Add a coordinator adapter in AgentManagerProvider that derives the pinned project at panel attach, wraps all legacy inbound messages, stamps context output, and switches strictness based on exposed-project count. In this slice only the pinned context is activated, so rendered behavior remains unchanged.SessionProvider adapter (host.ts, vscode-host.ts) with project-aware setSessionRoute, trackSession, getSessionInfo, loadMessages, share, and unshare methods. Retain temporary legacy methods only as single-project adapters, not as a second implementation.KiloProvider, kilo-provider/options.ts, and the extracted helpers in kilo-provider-utils.ts so Agent Manager Local sessions also have explicit routes, unknown strict routes fail closed, child routes inherit project identity, and project activation invalidates project-scoped cache work by generation. Keep non-Agent-Manager provider behavior unchanged.extension.ts auto-approve/session-directory lookup to ask the router/coordinator for the active or unique SessionRef; do not merge raw ID maps and choose the first match.terminal-routing.ts, and inject the pinned project root into createTerminalHost instead of reading workspaceFolders[0] internally.Slice A is complete when the flag-disabled Agent Manager is visually and behaviorally unchanged, every current project session has an explicit project route, and unit tests prove a second registered route cannot be accidentally handled through pinned-root fallback. Do not expose Add Project until Slice B is complete.
src/agent-manager/project-context.ts and move root-bound state/managers/handlers out of AgentManagerProvider. Keep context code VS Code-free. Reduce the provider cap to the new rounded-up line count instead of raising it.AgentManagerProvider the coordinator described above. Add cold/initializing/ready/suspended/disposed state, generation guards, expansion/activation APIs, shared semaphore, and per-project message dispatch.WorktreeManager's mutation lock key to canonical common Git directory and reject adding another exposed context with the same common directory.requestProjects, addProject, removeProject, expandProject, collapseProject, activateProject) behind the flag. These are testable protocol endpoints but need not yet have visible controls.After A and B are green, implement project accordions and the project selector in the New Worktree modal. Keep the current markup/layout when only the pinned project exists. Use composite refs in webview stores/tabs, one shared tab bar/detail pane, and clearly show the project name on cross-project session tabs and share actions. This UI slice is intentionally outside the backend-first scope.
Add or update these focused tests under packages/kilo-vscode/tests/unit/:
agent-manager-project-registry.test.ts: pinned project is always first/fresh; persisted active cannot replace it; additional roots survive restart; pinned duplicate is suppressed; add canonicalizes subdirectories/symlinks; duplicate root/common Git dir and wrong authority are rejected; flag-off snapshot exposes only pinned without mutating catalog; catalog listing performs no resolver/filesystem calls; missing entries remain stored.agent-manager-project-router.test.ts: every Local/worktree session has a route; matching refs resolve; wrong project, unknown session, unknown worktree, missing project, and raw-ID collision fail closed; child inherits parent project/directory; compatibility inference works only for one exposed project; share and unshare use the same explicit directory.agent-manager-project-context.test.ts: collapsed cold projects construct/read nothing; concurrent expand initializes once; two expanded contexts use separate state roots and share the concurrency gate; collapse stops pollers and flushes; collapse during init suppresses posts; disposed-generation results cannot update a replacement context; remove is blocked while busy.kilo-provider-session-refresh.test.ts: lists per project instead of merging project roots; outputs project stamps; failed worktree listing preserves only that project's sessions; activation generation drops stale config/session results.kilo-provider-worktree-context.test.ts: strict unknown session/worktree has no workspace fallback; Local routes explicitly resolve to their own project root; a secondary Local session cannot resolve to pinned root.AgentManagerProvider.spec.ts: single-project legacy messages are wrapped pinned; two-project unqualified messages return project_required; mismatched session/project returns project_mismatch; status/tool events dispatch by directory; restart selects pinned Local.agent-manager-arch.test.ts: new domain modules do not import vscode; lower AgentManagerProvider.ts maxLines after extraction.Run from packages/kilo-vscode/ after each slice:
bun test tests/unit/agent-manager-project-registry.test.ts tests/unit/agent-manager-project-router.test.ts tests/unit/kilo-provider-session-refresh.test.ts tests/unit/kilo-provider-worktree-context.test.ts
bun run typecheck
bun run lint
bun run test:unit
bun run knip
bun run check-kilocode-change
For Slice B, add the context/provider/terminal tests to the focused first command. No CLI/SDK regeneration is required because this architecture uses existing explicit-directory SDK parameters and adds only extension/webview protocol types.
KiloProvider or a helper. Treat every Agent Manager method that accepts sessionID, worktreeId, draftID, requestID, or directory as part of the routing audit. Tests should fail if strict mode calls workspaceFolders[0] for an identified operation..kilo/agent-manager.json format remain intact, while the unused global catalog is preserved for a later re-enable.