feat(vscode): multi-project Agent Manager with per-project state, sessions, and lifecycle

Add an experimental multi-project mode to the Agent Manager sidebar behind
kilo-code.new.experimental.multiProject (default off). A persistent project
registry catalogs additional git repositories across restarts, while the
workspace repository stays the pinned default project.

Extension:
- Immutable per-project contexts own all repository-bound services (state,
  worktrees, setup scripts, stale tracking, pollers) with generation-based
  invalidation and fail-closed trust checks.
- ProjectContexts manage activation, expansion, and fast switching; session
  routes resolve directories exactly per project via a shared route service.
- pushProjectSessions caches each project's session list and re-posts it on
  fresh skips; session.created/updated/deleted SSE events upsert into the
  owning project's cache so externally created sessions appear immediately.
- Selection restore persists the active target per project and falls back to
  the local context silently when the remembered target is gone.
- Worktree lifecycle handlers extracted into provider-lifecycle.ts with an
  explicit deps object instead of ambient project scope.
- initializeState and onRequestState always refresh sessions: with zero
  managed sessions the listing never ran and the sidebar skeletons forever.
- Log instead of dropping silently when a state-gated message is not ready.

Webview:
- ProjectList accordion with per-project sidebar body, search, actions, and
  default-branch dialog; selecting a project header restores its target.
- Local session tabs and terminal contexts are bucketed per project so open
  tabs never leak across projects sharing the LOCAL context.
- The active project's session list overlays the live session store so new
  sessions show without waiting for a backend re-list.
- SectionHeader requires a DragDropProvider ancestor; the multi-project body
  now provides one (its absence crashed the whole webview render).
- SidebarBody and TabBar extracted out of AgentManagerApp (3215 to 2748
  lines); AgentManagerProvider down to 1887 with caps lowered accordingly.

Also includes the config write revision bindings and per-project indexing
consent groundwork that rode along on this branch.
This commit is contained in:
marius-kilocode
2026-07-27 16:44:57 +02:00
parent 2da8949813
commit 46b7e55f8d
141 changed files with 12752 additions and 1674 deletions
@@ -0,0 +1,262 @@
# Agent Manager multi-project configuration architecture
**Status:** Blocking architecture for multi-project release
**Date:** 2026-07-22
This document is the canonical configuration specification for Agent Manager multi-project support. The main UI/runtime plan references it and must not duplicate or contradict it.
The executable sequence from the current branch is [`agent-manager-multi-project-implementation-handoff.md`](./agent-manager-multi-project-implementation-handoff.md).
## Decision
Keep the useful current split between user and project settings, but make every Settings read and write target explicit, immutable, revisioned, and independent from Agent Manager activation.
Multi-project must not ship broadly while Settings can load a draft for project A and resolve its save target from the mutable active project B.
## Current behavior
Kilo has four configuration stores:
| Store | Example | Owner |
|---|---|---|
| VS Code preferences | VS Code `settings.json` | This VS Code user/installation |
| Kilo user config | `~/.config/kilo/kilo.json` | User defaults across projects and Kilo clients |
| Kilo project config | `<repo>/.kilo/kilo.jsonc` | Repository behavior and overrides |
| Runtime/session state | In-memory, directory-qualified | One project, worktree, or session |
The shared Settings save currently calls `splitConfigByScope()`:
- `commit_message` is written to project config;
- `indexing.enabled` is written to project config;
- all other generic Settings fields are written to user config.
The Indexing tab also has an explicit Global/Project selector and can write the entire `indexing` object to either layer. This is too broad because project config can receive provider/model/vector-store credentials and infrastructure settings.
Several controls are VS Code preferences and bypass Kilo config entirely, including autocomplete UI, browser automation, notifications, max auto-approve cost, commit-message output language, and indexing button visibility.
## Confirmed blocking failure
The current protocol does not bind a draft to the config target it was loaded from:
1. `KiloProvider.fetchAndSendConfig()` resolves a mutable current directory and sends unqualified `configLoaded` state.
2. The webview owns one global/project/effective draft.
3. Agent Manager changes the active project from A to B.
4. The webview sends an unqualified `updateConfig`.
5. `KiloProvider.handleUpdateConfig()` resolves the current directory again at save time and may write A's draft to B.
Reads are directory-scoped, but writes are not bound to the read target. This is the release blocker.
The backend also lacks an expected target/revision precondition, so external editors or another window can overwrite a config between read and save.
## Ownership policy
### VS Code preferences
These remain in VS Code settings and do not participate in project config:
- extension language and presentation preferences;
- autocomplete enablement, keybindings, provider, and model;
- browser automation enablement/system Chrome/headless mode;
- notification enablement and sound;
- maximum automatic approval cost;
- commit-message output language;
- indexing button visibility while indexing is disabled;
- multi-project feature enablement.
### Kilo user config
These are personal defaults or security policy and should be edited in User scope:
- default, small, and subagent models and variants;
- provider enablement, custom providers, credentials, and endpoints;
- user/global agents and default agent;
- permission defaults and user tool defaults;
- sandbox policy, network access, writable paths, and allowed hosts;
- compaction, checkpoint/snapshot, and tool-output defaults;
- username and display behavior;
- sharing, remote control, telemetry, and experimental features;
- user/global formatter, LSP, MCP, skills, instructions, commands, and workflows;
- indexing provider, model, credentials, vector storage, and global tuning defaults.
A trusted project may override many of these at runtime, but editing User scope never writes those overrides.
### Kilo project config
These describe repository behavior and are valid project settings:
- commit-message prompt;
- repository indexing file extensions, include/ignore rules, and deliberate project tuning overrides;
- repository instructions;
- repository skill paths;
- repository commands/workflows;
- trusted project agents;
- trusted project MCP servers;
- repository formatter/LSP overrides;
- repository watcher ignores;
- repository-specific tool restrictions and permission requests.
Project configuration may override user model/agent/tool defaults, but provider credentials and security-policy weakening must not be silently authored into a repository.
### Machine-local project consent
Indexing enablement is privacy consent, not repository configuration. Store it outside the repository, keyed by canonical `ProjectId` in machine-local extension state:
- newly observed projects default to indexing disabled;
- users explicitly enable indexing for one project on this machine;
- repository config cannot enable indexing;
- repository config may describe what to index, but consent gates whether indexing starts;
- canonical project identity prevents a symlink or alternate path from bypassing consent.
Effective indexing requires both valid user-global indexing configuration and machine-local consent for that project.
### Current tabs
| Settings tab | Correct editable target |
|---|---|
| Models | User by default; explicit Project scope may override models/agents |
| Providers | User only for credentials/endpoints; project-provided entries are source-labelled |
| Agent Behaviour | User or explicit trusted Project scope |
| Auto Approve | User Kilo config; max cost remains VS Code preference |
| Browser | VS Code preferences |
| Checkpoints | User default or explicit Project override |
| Display | User config |
| Autocomplete | VS Code preferences |
| Notifications | VS Code preferences |
| Context | User defaults; repository watcher/instruction rules in explicit Project scope |
| Commit Message | Project scope for prompt; language remains VS Code preference |
| Indexing | User for provider/model/credentials/storage; machine-local project consent for enablement; Project for repository rules |
| Experimental | User config; multi-project also mirrors to VS Code preference |
| Sandboxing | User config only |
| Language | VS Code preference |
| MCP/Commands/Skills | User defaults or explicit trusted Project scope |
No field silently chooses a file during save. The UI must display its scope.
## Settings UX
Use explicit scope and project controls:
```text
Scope: User | Project
Project: backend
Target: /projects/backend/.kilo/kilo.jsonc
```
- User scope always targets user config.
- Project scope requires an explicit trusted project selector.
- The Settings selector is separate from Agent Manager's active project.
- Opening Settings may initialize the project selector once from the current project, but later Agent Manager switches never change it.
- A dirty project draft cannot move to another project. Selector changes require Save, Discard, or Stay.
- Inherited values show source badges such as User, Project: backend, and Managed.
- Project scope offers Override and Reset to inherited.
- Project-sourced providers cannot be silently deleted from project config through User scope.
Runtime config still follows the exact session directory. Settings Project scope targets the registered project root, not the active worktree. Worktree-config editing is a separate future feature requiring an explicit `WorktreeRef`.
## Immutable binding contract
A Settings read returns an opaque binding:
```ts
interface SettingsBinding {
id: string
connectionGeneration: number
scope: "global" | "project"
project?: {
projectId: string
root: string
generation: number
}
directory: string
target: {
scope: "global" | "project"
path: string
revision: string
exists: boolean
writable: boolean
}
}
```
The write contains only the opaque binding and patch:
```ts
interface WriteSettingsConfig {
type: "settingsConfig.write"
requestId: string
bindingId: string
set: Record<string, unknown>
unset: string[][]
}
```
The extension stores the authoritative binding. On write it must:
1. reject unknown/expired bindings;
2. verify project existence, generation, and trust;
3. capture the binding before the first await;
4. use the binding's stored directory and scope;
5. never call `getWorkspaceDirectory()`, `contexts.active()`, or use a worktree/session fallback;
6. clear a draft only from the matching `{ requestId, bindingId }` response.
Bindings expire after save, reconnect, trust revocation, project removal, or context generation change.
## Backend revision contract
`GET /config/overlay` must return the exact global/project target path, parsed raw target config, effective config/source metadata, and a revision.
The revision is a SHA-256 fingerprint of canonical target path, existence marker, and exact file bytes. This catches content changes, JSONC comment-only edits, and target changes.
`PATCH /config/overlay` accepts one scope and requires:
```ts
{
scope: "global" | "project"
set: Record<string, unknown>
unset: string[][]
expected: {
path: string
revision: string
}
}
```
The backend must re-resolve the authoritative target, verify path/revision under a target lock, patch the raw target layer, validate it, atomically replace the file, and return a fresh snapshot. It never accepts an arbitrary client path.
Expected failures include expired binding, unknown/untrusted project, changed target, revision conflict, invalid config, non-writable target, and I/O failure. Every failure preserves the draft.
## Required implementation
1. Add revisioned target descriptors and compare-and-swap writes to the Kilo config overlay API.
2. Split activation-bound runtime config state from binding-keyed Settings editor state.
3. Replace unqualified `configLoaded`/`updateConfig` with settings read/write messages carrying request and binding IDs.
4. Replace hidden `splitConfigByScope` saves with explicit scope on every editable control.
5. Restrict Indexing Project scope to repository rules; keep provider/model/credentials/storage in User scope.
6. Move indexing enablement from project config to machine-local consent keyed by canonical `ProjectId`, default off.
7. Audit direct config mutators outside the save bar, especially provider disconnect, imports/resets, custom providers, work styles, permission rules, and indexing actions.
8. Make Open Project Config take a `ProjectRef`, resolve the immutable registered root, and verify trust.
9. Partition config caches/events by scope, directory, target, revision, and activation generation.
## Blocking tests
- Load Settings for A, switch Agent Manager to B, save: only A's bound target changes.
- Same test while selecting A/B worktrees and sessions.
- User-scope save always changes only user config.
- Project-scope save requires the explicit trusted project and changes only its registered root config.
- Dirty drafts survive Agent Manager switches and cannot migrate between Settings projects.
- Out-of-order reads and writes update only the matching request/binding.
- External file change causes a revision conflict without losing the draft.
- A changed config target path causes a target conflict.
- Project removal, generation change, or trust revocation expires its binding.
- Indexing provider/model/credentials/storage never enter project config through the form.
- A repository file containing `indexing.enabled: true` cannot grant indexing consent.
- New projects default to indexing disabled until explicitly enabled on this machine.
- Consent follows canonical project identity across symlink/path aliases and never leaks to another project.
- `commit_message.prompt` and repository indexing rules still support explicit project writes.
- Runtime worktree config uses the worktree directory while Project Settings remains bound to the registered project root.
## Release gate
Keep multi-project disabled by default until the immutable binding/revision contract and the blocking tests above are implemented. The useful existing project-local behavior should be preserved, not removed; its write target must become explicit and immutable.
@@ -0,0 +1,459 @@
# Agent Manager multi-project implementation handoff
**Status:** Implementation-ready handoff from the current branch
**Date:** 2026-07-22
This document tells the next implementation model exactly how to continue from branch `abalone-bactrosaurus` at commit `8f48da2278` plus the uncommitted plan files. It is the execution checklist. Detailed decisions live in:
- [`agent-manager-multi-project-configuration.md`](./agent-manager-multi-project-configuration.md) for configuration ownership, bindings, revisions, indexing consent, and settings tests;
- [`agent-manager-multi-project-uniform-ui.md`](./agent-manager-multi-project-uniform-ui.md) for runtime, routing, lifecycle, uniform sidebar UI, and acceptance criteria.
If this handoff conflicts with either architecture document, stop and resolve the contradiction before coding.
## Goal
Finish the current prototype into a safe, complete multi-project Agent Manager behind a default-off VS Code experimental flag.
The feature is done only when:
- multiple expanded projects show the same full Agent Manager sidebar behavior;
- every action uses explicit project/worktree/session ownership;
- Settings cannot write the wrong project's config;
- indexing requires machine-local consent per canonical project;
- project identity is canonical, Git-root-aware, common-dir-safe, and remote-authority-aware;
- single-project behavior remains unchanged when the flag is off.
## Current branch baseline
### Implemented and worth keeping
- persistent additional-project registry with serialized/re-read mutations;
- separate `ProjectContext` objects and per-project Agent Manager state files;
- project-stamped state, stats, PR, and session payloads;
- canonical worktree presence comparison for symlink/`/tmp` aliases;
- per-project session listing from project root and worktree directories;
- partial production `ProjectRouteService` integration and raw-session ambiguity rejection;
- project-qualified sidebar DOM IDs and cross-project previous/next navigation;
- atomic selection validation and same-project activation fast path;
- flag-off return to pinned project;
- Experimental Settings toggle and translations;
- focused unit tests for registry, contexts, paths, pollers, routing, sessions, selection, and navigation.
### Not implemented or incomplete
- immutable/revisioned Settings bindings;
- machine-local indexing consent;
- canonical pinned Git toplevel, `commonGitDir`, URI scheme/authority identity;
- strict project envelopes for all Agent Manager operations;
- complete route-service use for every session/terminal/diff/share/permission operation;
- one full sidebar body shared by single- and multi-project modes;
- uniform per-context poller and stale-state ownership;
- all repository mutations through `ProjectContext.run()`;
- complete real two-project E2E verification.
### Known diff cleanup
- remove unrelated `packages/opencode/package.json` ordering/dependency drift;
- restore unrelated `bun.lock` ordering/version drift unless required by a retained change;
- remove `experimental.multi_project` from the CLI config schema and generated SDK once the VS Code flag is the sole source of truth;
- rewrite the changeset after final behavior is complete.
Do not reset or discard unrelated user changes. Inspect every cleanup diff before applying it.
## Non-negotiable invariants
1. Agent Manager activation is not a Settings write target.
2. No identified operation falls back to the active project or workspace root in multi-project mode.
3. A project config write targets the explicitly selected registered project root, never an active worktree.
4. Runtime config remains directory-correct: project Local uses root, worktree sessions use exact worktree directory.
5. Repository config cannot grant indexing consent.
6. New projects default to indexing disabled on this machine.
7. Provider/API credentials and indexing infrastructure never enter project config through Settings.
8. Raw session/worktree IDs are never sufficient UI/runtime identity when multiple projects are exposed.
9. The full existing sidebar behavior is reused; do not maintain a simplified multi-project copy.
10. Do not raise `AgentManagerProvider.ts` or `AgentManagerApp.tsx` line caps.
## Work order
Complete slices in order. Do not start the next slice while the current slice's tests or checks fail.
## Slice 0A: consolidate the feature flag
### Required result
Use one source of truth: VS Code application setting `kilo-code.new.experimental.multiProject`.
### Required changes
1. Remove `experimental.multi_project` from:
- `packages/core/src/v1/config/config.ts`;
- `packages/sdk/js/src/v2/gen/types.gen.ts` by regenerating the SDK after schema removal;
- `packages/kilo-vscode/webview-ui/src/types/messages/config.ts`.
2. Do not hand-edit generated SDK files. Run `./script/generate.ts` from repository root after endpoint/schema changes.
3. Change `ExperimentalTab.tsx` so the toggle reads the VS Code setting delivered in `configLoaded.settings`, not `config().experimental`.
4. Add `multiProject` to the extension settings payload sent by `KiloProvider.fetchAndSendConfig()`.
5. Toggling it sends only `updateSetting("experimental.multiProject", checked)`.
6. Keep `VscodeHost.multiProject()` and its change listener reading the same VS Code setting.
### Tests
- toggle initial state reflects VS Code setting when CLI config disagrees;
- toggling updates the VS Code setting and Agent Manager reacts;
- no CLI/project config file gains `experimental.multi_project`.
### Stop gate
Run from `packages/kilo-vscode/`:
```sh
bun run format
bun run format:check
bun run typecheck
bun run lint
bun run test:unit
```
Do not continue if any command fails.
## Slice 0B: machine-local indexing consent
### Required result
Indexing enablement is explicit machine-local consent keyed by canonical `ProjectId`, default false. Project config controls indexing rules but cannot enable indexing.
### Required changes
1. Add a versioned machine-local consent store in the extension, not repository config and not synced across machines.
2. Use canonical project identity as the key. The pinned project and registered projects use the same identity resolver.
3. Remove hidden routing of `indexing.enabled` to project config from `webview-ui/src/utils/config-scope.ts`.
4. Remove project/global inheritance logic for `indexing.enabled` from `indexing-tab-state.ts`.
5. The Indexing UI requires an explicit project selector for enablement and reads/writes consent through dedicated messages.
6. Keep these in User config only:
- provider;
- model and dimension;
- credentials/API keys/base URLs;
- vector-store type and connection/storage settings;
- machine/infrastructure tuning defaults.
7. Keep these available in explicit Project scope:
- file extensions;
- repository include/ignore rules;
- deliberate repository chunking/tuning overrides.
8. Indexing startup/status must require both valid User indexing configuration and consent for the routed project.
9. A repository containing `indexing.enabled: true` must not enable indexing.
### Suggested files
- new VS Code-free consent store under `src/indexing/` or `src/agent-manager/`;
- `src/KiloProvider.ts` only as a thin protocol adapter;
- `webview-ui/src/components/settings/IndexingTab.tsx`;
- `webview-ui/src/components/settings/indexing-tab-state.ts`;
- `webview-ui/src/context/config.tsx` only if necessary;
- focused tests beside existing indexing tests.
### Tests
- unknown project defaults off;
- A enabled does not enable B;
- symlink/path alias resolves to the same consent;
- repository config cannot grant consent;
- removing/re-adding behavior follows the documented store policy;
- no credentials/storage settings are emitted in a Project-scope patch.
## Slice 0C: revisioned config overlay backend
### Required result
Config reads return authoritative target descriptors and revisions. Writes use compare-and-swap and cannot overwrite external changes or a changed target.
### Required changes
1. Extend Kilo-owned config overlay code:
- `packages/opencode/src/kilocode/config/overlay.ts`;
- a new Kilo-owned writer under `packages/opencode/src/kilocode/config/`;
- `packages/opencode/src/kilocode/server/httpapi/groups/config-console.ts`;
- `packages/opencode/src/kilocode/server/httpapi/handlers/config-console.ts`.
2. Read response includes for global and project targets:
- canonical path;
- scope;
- exists/writable;
- SHA-256 revision of canonical path, existence marker, and exact bytes;
- raw parsed target layer;
- effective config and source metadata.
3. Write request accepts exactly one scope, `set`, `unset`, and expected path/revision.
4. Server re-resolves the authoritative target. Never trust client path as a destination.
5. Lock by canonical target path, re-read inside lock, compare revision, patch raw JSONC, validate, and atomically replace.
6. Return a fresh authoritative snapshot.
7. Return typed 409 conflicts for target/revision changes; preserve drafts client-side.
8. Keep Kilo logic in `packages/opencode/src/kilocode/`. Shared upstream files get minimal marked delegation only when unavoidable.
9. Regenerate the SDK after endpoint changes.
### Tests
Add/extend:
- `packages/opencode/test/kilocode/server/config-overlay.test.ts`;
- `packages/opencode/test/kilocode/project-config-update.test.ts`.
Cover:
- exact raw target layer;
- comment-only external edit conflict;
- missing-file target revision;
- newly created higher-priority target conflict;
- concurrent writers: one success, one 409;
- symlink escape rejection;
- global and project writes remain separate;
- atomic write failure never exposes partial content.
### Stop gate
From `packages/opencode/`:
```sh
bun run typecheck
bun test test/kilocode/server/config-overlay.test.ts
bun test test/kilocode/project-config-update.test.ts
```
From repository root:
```sh
./script/generate.ts
bun run script/check-opencode-annotations.ts
bun run script/check-opencode-promise-facades.ts
```
## Slice 0D: immutable Settings bindings
### Required result
Settings has explicit `User | Project` scope. Project scope requires an explicit trusted project selector. Drafts and saves never follow Agent Manager activation.
### Required changes
1. Split runtime-effective config from Settings editor config.
2. Add a binding controller under `packages/kilo-vscode/src/kilo-provider/`; keep `KiloProvider.ts` as a thin adapter.
3. Replace unqualified config protocol with:
- read request carrying request ID, scope, and optional project ID;
- snapshot carrying opaque binding ID and source/target metadata;
- write request carrying request ID, binding ID, set, and unset.
4. The extension stores authoritative bindings. The webview never supplies a writable path.
5. Project binding resolution:
- resolve registered `ProjectContext.root`;
- require project existence, matching generation, and trust;
- never use active worktree/session or `getWorkspaceDirectory()` after binding creation.
6. Bindings expire on successful save, reconnect, project removal, generation change, or trust revocation.
7. Separate drafts by scope/project binding.
8. Dirty selector changes prompt Save, Discard, or Stay.
9. Out-of-order reads/writes update only matching request/binding.
10. External config events refresh clean drafts and mark dirty drafts stale without overwriting them.
11. Remove `splitConfigByScope()` after all controls declare scope explicitly.
12. Audit direct config mutators outside the save bar, including provider disconnect, imports/resets, custom providers, work styles, Permission Dock, MCP actions, and indexing.
13. Open Project Config takes explicit `ProjectRef`, checks trust, and resolves the registered root.
### UI scope policy
Use the table in `agent-manager-multi-project-configuration.md`. Do not invent another policy.
### Blocking tests
- load A, activate B, save A: only A target changes;
- same while selecting worktrees/sessions;
- User save changes only user config;
- Project save changes only selected trusted project's root config;
- dirty draft cannot migrate to another project;
- project removal/trust revocation expires binding;
- external edit returns conflict and preserves draft;
- no save-time active-directory lookup occurs.
## Slice 1: canonical project identity
### Required result
Every project is a canonical Git repository root with URI authority and common Git directory identity.
### Required changes
1. Replace path-only project descriptors with:
```ts
{
id,
uri,
scheme,
authority,
root,
commonGitDir,
label,
order,
trusted,
addedAt
}
```
2. Pinned and added projects use the same resolver:
- validate URI host scope;
- `git rev-parse --show-toplevel`;
- `git rev-parse --path-format=absolute --git-common-dir`;
- realpath/normalize both;
- derive ID from scheme, authority, and canonical root.
3. Opening VS Code in a repository subdirectory still uses the Git toplevel and root state file.
4. Reject another exposed project sharing canonical `commonGitDir`.
5. Key Git mutation locks by common Git directory.
6. Preserve remote URI scheme/authority in picker, storage, and open-folder operations.
7. Migrate or safely read the current registry version without dropping valid entries.
### Tests
- workspace subdirectory resolves to repository root;
- symlink aliases dedupe;
- linked worktree/common-dir duplicate rejected;
- other remote authority hidden but preserved;
- missing/non-Git entry remains removable and never falls back to pinned root.
## Slice 2: strict routing and lifecycle
### Required result
Every repository-bound operation is explicitly project-qualified once multiple projects are exposed.
### Required changes
1. Introduce one project envelope at the Agent Manager boundary with project ID and generation.
2. Compatibility adapter injects pinned identity only in single-project mode.
3. Multi-project mode rejects missing/mismatched identity with typed errors.
4. Wire `ProjectRouteService` into every existing-session operation:
- transcript/messages;
- prompt/abort;
- share/unshare;
- fork/continuation;
- permission/question responses;
- terminal;
- file/context operations;
- diff/apply/revert.
5. Route global SSE events once by exact directory/session ownership.
6. Preserve non-Agent-Manager KiloProvider behavior.
7. Execute repository mutations through `ctx.run()` and generation-check commits.
8. Removal waits for mutation queue, stops pollers/watchers, flushes state, detaches routes, then removes registry entry.
9. Flag-off activates pinned Local and suspends secondaries without aborting sessions.
### Tests
- same raw session ID across projects is ambiguous without qualifier;
- B Local/share/unshare/permission never routes through A;
- unknown IDs produce no terminal/file/diff operation;
- project switch during mutation cannot change operation ownership;
- late completion after removal cannot mutate replacement context.
## Slice 3: uniform context-owned state and polling
### Required result
Every initialized expanded project owns the same live-state services. Active selection changes cadence/detail only, not ownership.
### Required changes
1. Move stats and PR pollers into `ProjectContext`; remove active singleton/background split.
2. Each context owns stale/presence state, cached stats, local stats, PR state, run/setup state, and generation.
3. Background presence updates project stale state rather than emitting empty arrays.
4. All project outputs include project ID and generation; webview discards stale generations.
5. Session-created/deleted/updated events refresh the owning project store without active-project filtering.
6. Panel visibility and expansion uniformly control polling.
### Tests
- two expanded projects both receive stats/presence/PR updates;
- collapse stops only that project's pollers;
- stale late poll result is dropped;
- background session deletion updates only its project.
## Slice 4: one full sidebar body
### Required result
Single- and multi-project modes render the same full `ProjectSidebarBody` implementation.
### Required changes
1. Extract the current full `renderBody()` from `AgentManagerApp.tsx` intact.
2. Parameterize it with one project store and project-qualified actions.
3. Preserve:
- busy state;
- drag/drop and grouping;
- section actions;
- search and keyboard hints;
- run/setup state;
- New Worktree dialog;
- worktree menus and PR actions;
- complete managed/unassigned session behavior.
4. Delete the simplified duplicate body path.
5. Render project rows keyed by stable project ID and keep bodies mounted across active changes.
6. New Worktree receives explicit target project, defaulting to Settings/detail selection policy as specified.
### Tests
- single-project behavior unchanged with flag off;
- two expanded projects render two full bodies;
- active selection changes no body mount identity;
- drag/drop, sections, worktree actions, sessions, and keyboard navigation work across projects.
## Slice 5: cleanup and release validation
1. Remove unrelated manifest/lockfile drift.
2. Update changeset to final user-visible behavior.
3. Run all extension and affected CLI checks.
4. Build/package the extension.
5. Use the VS Code self-test fixture with two repositories and multiple worktrees/sessions.
6. Keep feature default off until every blocking test passes.
### Required extension checks
From `packages/kilo-vscode/`:
```sh
bun run format
bun run format:check
bun run typecheck
bun run lint
bun run test:unit
bun run knip
bun run check-kilocode-change
bun run compile
```
From repository root:
```sh
bun run script/check-opencode-annotations.ts
bun run script/check-opencode-promise-facades.ts
bun run script/check-md-table-padding.ts
```
### Manual scenarios
1. Flag off: current single-project Agent Manager is unchanged.
2. Enable flag in Experimental Settings; restart; value persists without CLI/project config changes.
3. Add/trust/expand two repositories; restart; pinned remains first.
4. Both projects show Local, worktrees, sessions, stats, PR state, and full actions.
5. Navigate previous/next across projects.
6. Create/delete/rename worktrees in both projects.
7. Send prompts and answer permission/question requests in both projects.
8. Share/unshare B while A is active; exact B directory is used.
9. Load Project Settings A, activate B, save A; B is unchanged.
10. Modify A config externally while draft is dirty; conflict preserves draft.
11. Indexing defaults off for a new project; enabling A does not enable B; repo config cannot enable either.
12. Disable flag while B is active; pinned Local becomes active and B remains registered/suspended.
13. Repeat key routing and identity checks in a remote VS Code window.
## Rules for the implementation model
- Work on one slice at a time.
- Read the referenced architecture section before editing.
- Add focused tests before or with behavior changes.
- Use `apply_patch` for manual edits.
- Do not hand-edit generated SDK files.
- Do not raise file-size caps.
- Do not add fallback compatibility code unless the plan explicitly requires the single-project adapter.
- Do not claim a slice complete until its stop gate passes.
- If a required API or ownership decision is missing, stop and update this plan instead of guessing.
@@ -0,0 +1,351 @@
# Agent Manager multi-project runtime
> 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
## Implementation status (2026-07-20)
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 `ProjectSnapshot`s).
- `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`.
- Protocol — `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`).
- Host — `pickFolder`, `multiProject`, `readProjects`/`writeProjects` (globalState key `agentManager.projects`), `onDidChangeWorkspaceFolders`, `onDidChangeMultiProject`.
- Setting — `kilo-code.new.agentManager.multiProject` (boolean, default false, application scope).
- Changeset — `.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.
## Remaining for the UI session (accordion)
- Project accordion rendering from `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).
- Add-project entry point, trust CTA (sends `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).
- New Worktree dialog project selector (defaults to active project).
- Extension follow-ups: hidden-cadence polling for expanded projects; session-owner index so Local sessions of project B open/share correctly even while A is active (currently Local sessions resolve through the active root — acceptable only while the UI activates before opening).
- Implemented: per-context `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.
- Known boundary: `KiloProvider.projectID` and session refresh remain single-project; cross-project SSE filtering by session→directory owner is a follow-up (see "Verified findings" below).
## Objective (original)
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:
- The first project is always the canonical Git root of the first VS Code workspace folder in the current window. It is derived at runtime, is always first, and cannot be removed or replaced by persisted state.
- Global extension storage contains only the catalog of additional projects and project-list UI metadata. Each repository remains authoritative for its own `.kilo/agent-manager.json`.
- A collapsed, never-opened additional project causes no `.kilo/agent-manager.json` read, Git mutation, setup/run initialization, terminal creation, or poller subscription.
- Every session/worktree/draft operation has a project identity internally. Once secondary-project activation is enabled and more than one project is exposed, missing or mismatched identity is rejected rather than routed to the current workspace.
- The feature flag gates catalog exposure, secondary activation, and multi-project wire/UI behavior. It does not select a separate legacy implementation.
## Verified findings
- `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`).
- Opening the panel immediately initializes the current repository, performs `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`).
- The embedded Agent Manager `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.
- The shared backend and generated SDK support an explicit `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.
- Backend session IDs are high-entropy process-global IDs (`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.
- Multiple `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.
- Existing Kilo experimental config is CLI/project config, not an application feature-flag service (`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.
## Architecture decisions
### 1. Project identity and registry semantics
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`:
```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:
1. Start from the selected workspace/folder URI.
2. Reject a URI whose scheme/authority is not the current extension-host scope.
3. Run `git -C <fsPath> rev-parse --show-toplevel` through the existing hidden-window process wrapper.
4. Resolve the result with `fs.promises.realpath`; normalize separators, trailing separators, and Windows case for identity comparison.
5. Resolve and canonicalize `git rev-parse --path-format=absolute --git-common-dir` as `commonGitDir`.
6. Derive `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`:
```ts
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.
- The pinned descriptor is first, has `pinned: true`, and defaults to expanded. It is not written into the catalog.
- Persisted entries matching the pinned project ID are suppressed, not deleted. They reappear as additional projects when another window has a different pinned project.
- Additional entries are filtered to the current URI scheme/authority before exposure. Entries for other authorities remain untouched in storage.
- Reading the catalog does not stat, realpath, run Git, or open per-repository state. Persisted additional entries start as `unverified`.
- Adding validates/canonicalizes the chosen folder, rejects the pinned project, duplicate IDs, duplicate common Git directories, and authority mismatches, then persists it. A newly added project may be marked expanded and explicitly activated by the caller; listing alone never initializes it.
- A missing or no-longer-Git root is marked unavailable only when expansion validates it. Keep the entry and its metadata. Do not prune it automatically and do not create a `ProjectContext`.
- Removing is allowed only for non-pinned projects. It removes only the catalog entry after the context is idle/disposed; it never deletes `.kilo`, worktrees, sessions, branches, or shares.
- On every extension/panel restart, the active selection is the pinned project's Local context. Persisted additional projects and collapse/order/name metadata survive, but a stale secondary active session does not override the current VS Code window.
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.
### 2. Coordinator and immutable lazy ProjectContext
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:
- the one panel and embedded `KiloProvider`/`SessionProvider`;
- `ProjectRegistry`, active `ProjectId`, exposed descriptors, and `Map<ProjectId, LazyProjectContext>`;
- a shared `Semaphore(3)` for Git/network polling across all projects;
- the one project/session router;
- one project-aware `AgentManagerVisiblePresence`;
- one `SessionTerminalManager` and one project-aware `TerminalRouter`;
- one global orchestration ingress and the global SSE/tool/status subscriptions;
- project and activation generations used to discard stale async results.
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`;
- local diff functions, `WorktreeDiffController`, `WorktreeImporter`, and `RunController`;
- `GitStatsPoller`, `PRStatusBridge`/poller, and `BranchNamingController`;
- per-project cached state/stats/stale IDs, managed/open session refs, and initialization promise;
- project-specific message handlers currently embedded in `AgentManagerProvider`.
It does not own a webview, `KiloProvider`, VS Code terminal command registrations, global connection subscriptions, or a global orchestration subscription.
Lifecycle:
1. `cold`: descriptor only. No state manager, manager, poller, watcher, or per-repo read exists.
2. `initializing`: one shared promise validates the location, constructs resources, performs the existing `ensureGitExclude -> state.load -> recoverWorktrees -> register routes` sequence, and captures a context generation.
3. `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.
4. `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.
5. `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.
### 3. One embedded KiloProvider, made project-aware
Use one embedded `KiloProvider`. Multiple providers are not safe or useful here:
- there is one webview and one message handler;
- each provider would own conflicting current-session/cached UI state and duplicate SSE handling;
- multiple terminal/visibility registrations would be ambiguous;
- it would not solve session identity at the shared webview boundary.
Refactor its Agent Manager adapter around an injected project/session route service while leaving sidebar and standalone providers unchanged. The route service supplies:
```ts
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:
- Register an explicit route for every Agent Manager session, including Local sessions at a project root. Do not omit a map entry merely because it equals the first workspace root.
- New/draft operations resolve from the outer project envelope and an explicit Local/worktree context. Unknown worktree IDs are errors; remove `TerminalRouter`'s fallback to root.
- Existing session operations resolve only from `ProjectSessionRef`. In strict multi-project mode, an unknown session never falls back to `workspaceFolders[0]` or the currently active project.
- Child/subagent sessions inherit the parent's project and directory. A directory-bearing event must also pass the owning-context check before registration.
- Session list refresh runs per project root plus that project's worktree directories. It emits sessions stamped with that project; it does not infer one canonical `projectID` from the first root and merge all projects into it.
- Replace singleton Agent Manager use of `KiloProvider.projectID` with the route table/directory ownership check. Keep existing project filtering for non-Agent-Manager provider instances.
- On active-project/session change, call one `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.
- Every session create, continuation, prompt, permission/question response, terminal operation, diff operation, file/context operation, and share/unshare SDK call receives the resolved explicit directory.
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.
### 4. Project-qualified protocol and share semantics
Define an internal/wire envelope in `agent-manager/types.ts` and mirror it in `webview-ui/src/types/messages/agent-manager.ts`:
```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:
```ts
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:
- The coordinator is the only component that unwraps inbound envelopes and wraps context output.
- Global catalog/route errors are global messages. Repository state, stats, session metadata, diff, run, PR, terminal, and chat messages are project envelopes.
- Preserve `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.
- Validate the envelope project against the context, persisted session/worktree ownership, session route, and event/request directory. A valid raw ID in the wrong envelope is still rejected.
- Outbound generation lets the webview/coordinator discard messages from a collapsed/disposed/replaced context.
- While the flag is off, or while only the pinned project is exposed, a compatibility adapter wraps today's unqualified webview messages with the pinned project and unwraps pinned output to the existing shape. The core handlers still receive envelopes.
- Once the flag is on and a second project is exposed, unqualified project/session/worktree messages fail closed. The multi-project UI must send/consume envelopes and use composite refs as tab/store keys.
Sharing is an operation on `SessionRef`, not on a bare session ID:
- Manual share calls `client.session.share({ sessionID, directory })`; unshare uses the exact same resolved route.
- Automatic sharing remains a project config behavior because session creation is sent with the target project's directory.
- Store no share URL or share state in the project catalog. Session list/get/update data remains authoritative and is enriched with `projectId` on output.
- Removing/collapsing a project never unshares its sessions. A share URL is externally usable, but reopening or mutating the local session still requires its local project route.
- A mismatched or missing project cannot share/unshare, even if the raw session ID happens to resolve elsewhere.
### 5. Feature flag, restart, and migration behavior
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.
- Flag off: derive and initialize only pinned current project, use the compatibility wire adapter, and preserve the exact current view. Read but do not expose, validate, initialize, rewrite, or delete additional catalog entries.
- Flag on: publish lightweight descriptors. Only explicit expansion initializes an additional project. Selecting Local/worktree also expands and initializes its owner before activation.
- Runtime on -> off: increment activation generation, activate pinned Local, suspend all secondary contexts, switch to compatibility protocol, and preserve catalog/per-repo state. Do not abort backend sessions.
- Runtime off -> on: expose stored descriptors as cold/unverified. Do not eagerly restore a persisted secondary active project.
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:
- Scope catalog exposure and identity by full URI scheme + authority, not `vscode.env.remoteName` alone.
- Run Git and filesystem operations in the current extension host against `fsPath`; preserve the original URI for `openFolder` and picker operations.
- Reject selection from another authority. Keep such stored entries for their matching window/host.
- Unsupported virtual filesystems, missing roots, permission failures, and non-Git directories become unavailable descriptors after explicit validation. They do not fall back to the pinned root.
## Ordered implementation plan
### Slice A: mergeable routing foundation before multi-project UI
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.
1. Add `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.
2. Extend `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`.
3. Add `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.
4. Add envelope/route errors to `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.
5. Extend the `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.
6. Modify `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.
7. Update `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.
8. Remove unknown-worktree-to-root fallback in `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.
### Slice B: lazy multi-context runtime, still before UI rendering
1. Add `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.
2. Make `AgentManagerProvider` the coordinator described above. Add cold/initializing/ready/suspended/disposed state, generation guards, expansion/activation APIs, shared semaphore, and per-project message dispatch.
3. Refactor status/tool/orchestration subscriptions so the coordinator routes once by project/directory. Do not retain per-context global subscriptions that can reject each other's requests.
4. Make terminal management panel-global and key mappings by composite session/worktree refs. A context supplies only an already-validated CWD. Ensure context collapse cannot switch to another project's terminal by raw ID.
5. Refactor session refresh to accept one project at a time and emit project-stamped results. Register routes for discovered Local sessions before they can be opened. Preserve failed-directory session IDs only within the affected project.
6. Change `WorktreeManager`'s mutation lock key to canonical common Git directory and reject adding another exposed context with the same common directory.
7. Add backend catalog commands (`requestProjects`, `addProject`, `removeProject`, `expandProject`, `collapseProject`, `activateProject`) behind the flag. These are testable protocol endpoints but need not yet have visible controls.
### Slice C: first usable flagged UI
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.
## Exact tests
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.
- Extend `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.
- Extend `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.
- Extend `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.
- Add terminal routing coverage: unknown project/worktree produces an error and no PTY/VS Code terminal; identical raw session IDs in different projects cannot select the wrong terminal.
- Extend `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:
```sh
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.
## Manual validation after the UI slice
1. With the flag off and a non-empty stored catalog, restart VS Code. Agent Manager must look and behave exactly as today and use only the current window's Git root.
2. Turn the flag on, add a second repository, restart, and verify its header persists while the pinned current project remains first and active. Confirm the collapsed second project creates no output/state access/poll traffic until expanded.
3. Expand both projects, open Local and worktree sessions from each, switch rapidly, send prompts, answer permission/question requests, open terminals/diffs, and share then unshare. Verify every operation affects the displayed project's repository and config.
4. Disable the flag while a secondary project is active. Verify pinned Local becomes active, secondary state remains on disk/catalog, and re-enabling restores the descriptor without eager initialization.
5. Rename/remove a cataloged directory and restart. Verify the project remains as unavailable, cannot route actions to pinned root, and can be removed without touching its repository state.
6. Repeat in a remote window. Verify local/other-authority catalog entries are hidden but preserved and folder opening retains the remote URI authority.
## Risks and rollback
- The largest correctness risk is a hidden workspace-root fallback in `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.
- Project-scoped KiloProvider caches can show stale settings after a rapid project switch. Activation generation checks are required before posting and before replacing caches; merely cancelling transcript load is insufficient.
- Global event consumers can race. There must be exactly one ownership decision for orchestration/tool events, and directory must be retained for status/permission/question handling.
- Multiple contexts increase polling/process pressure. Share the concurrency semaphore, gate all pollers by panel visibility and expansion, and suspend on collapse/flag-off.
- Global-state updates from separate VS Code windows do not provide a transactional CAS. The first iteration should re-read before each serialized mutation and tolerate another window appearing after reload; do not build a filesystem lock or custom database for this feature.
- Rollback is safe: turn off the application flag. The pinned path and existing `.kilo/agent-manager.json` format remain intact, while the unused global catalog is preserved for a later re-enable.
@@ -0,0 +1,201 @@
# Agent Manager Multi-Project — Shipping Gaps
Status as of 2026-07-27. The multi-project sidebar renders and works end to end in the
self-test harness (both projects list worktrees/sessions/sections, section creation
persists, worktree delete works, project switching restores selection, live session
upsert verified). Full unit suite green (3444 pass / 0 fail), typecheck, lint, knip,
arch caps all pass. Everything is uncommitted in the worktree.
This document lists what is still missing, ranked by whether it should block shipping.
Two audiences matter for the blocking decision:
- **All users**: the branch changes shared surfaces (legacy sidebar refactor, config
write path, indexing consent, pollers). Regressions here ship to everyone, flag off
or not.
- **Experimental users**: multi-project mode is gated behind
`kilo-code.new.experimental.multiProject`, default off. Rough edges here are
acceptable if they cannot corrupt data.
---
## 1. Blocks shipping (all-user surfaces)
### 1.0 Empty-state skeleton regression (fixed, must stay fixed)
`initializeState` and `onRequestState` only called `refreshSessions()` when the
managed state contained at least one session. With zero managed sessions the
backend listing never ran, `sessionsLoaded` never reached the webview, and both
the WORKTREES and SESSIONS sections stayed on skeletons forever (the worktree
gate requires `worktreesLoaded() && sessionsLoaded()`). Any user whose state file
lost its sessions — exactly what the earlier session-persist bugs caused — hit a
permanently empty-looking sidebar. Reproduced in the harness on a worktree-root
workspace and fixed by making both refreshes unconditional. Root-caused via
stage-by-stage init logging on 2026-07-27; do not reintroduce a content-based
guard here.
### 1.1 Config write path can fail or wipe drafts for existing callers — P0-6 / P1-7
The config binding rework requires every `updateConfig` sender to carry a binding id,
and `configUpdateFailed` currently wipes the draft for the failed scope. Any existing
sender without a binding (permission dock, model picker, auto-approve, onboarding)
now fails or loses the user's unsaved edits.
- Fix: absent binding id falls back to the legacy revision-less write; keep revision
enforcement only where a binding was actually supplied. Keep the draft for scopes
that did not complete.
- Work: the code change is small; the real work is auditing every webview
`updateConfig` sender and classifying it.
- Also: this change is orthogonal to multi-project. Split it into its own commit
(`feat(vscode): config write revision bindings`) so it can be reverted alone.
### 1.2 Indexing status read silently revokes consent — P0-3 / P0-4
`fetchAndSendIndexingStatus` issues `PUT /indexing/consent` (a write) on a plain
status refresh, defaulting to `enabled: false` for any project not in local
`globalState`. On a fresh machine/profile, the first status read turns indexing off
for users who had it on. A refresh can also target the wrong project
(`requestIndexingStatus` resolves from the current session's directory). Separately,
the Indexing tab lists untrusted projects and would let a user enable indexing for a
repo the trust system has not approved.
- Fix: read status with a GET; PUT only from the explicit setter; seed consent from
the effective config on first read instead of defaulting false; require an explicit
project on refresh; filter the project list to trusted projects.
- Dependency: needs a read-only status endpoint. If none exists, this escapes into
`packages/opencode` (shared upstream code, needs `kilocode_change` markers) or the
cloud repo.
- Also orthogonal to multi-project; split into its own commit
(`feat(vscode): per-project indexing consent`).
### 1.3 Land the current work as separate, revertable commits
Everything currently sits uncommitted in one worktree. The review's recommendation
stands: `git reset --soft origin/main` and stage by path into three commits —
multi-project, config bindings, indexing consent. The multi-project commit message
must not promise per-project config/indexing behavior that lands in the other two.
---
## 2. Should fix soon after (experimental surface, data-integrity relevant)
### 2.1 Same repo can register twice — P0-5
`projectIdFor` hashes the canonical path verbatim while `samePath` folds case, so two
casings of one repo produce two project ids, two contexts, and two state managers
racing to write the same `.kilo/agent-manager.json` (last write wins, worktrees and
sessions vanish).
- Fix: fold case inside `projectIdFor` on darwin/win32; reject `addProject` when
`samePath` matches any registered root.
- Migration: existing registry keys were built from the old hash, so re-key them on
load or dual-lookup on read. Note `canonicalizePath` already realpaths existing
paths; the fallback branch must fold case too.
- Why not blocking: requires an unusual casing mismatch at registration time, and the
feature is flag-gated.
### 2.2 Route service is shared across panels but versioned per context — P0-7
Two VS Code windows each have their own `ProjectContexts` with independent generation
counters feeding one shared `ProjectRouteService`. Panel B registering a project at
generation 0 while panel A is at 2 unregisters panel A's routes; closing one panel
drops routes the other still needs.
- Fix: panel-qualified keys (`panelId + projectId + sessionId`), generations issued
by the service, ambiguity computed across all panels.
- Why not blocking: needs two windows running Agent Manager against the same repos.
The fallback already refuses ambiguous raw ids instead of resolving them wrong.
- Note: no two-panel test harness exists today; this fix should create one.
### 2.3 `gh pr` noise in repos without remotes — Bug 5
`gh pr view` fails every 15s per worktree forever, logs before the dedupe, and
`pollOnce` rejections are unhandled.
- Fix: log after the `lastHash` dedupe; `void this.pollOnce().catch(log)` in
`schedule`; per-root remote probe with a single error emission; skip PR pollers for
remote-less projects in `ProjectPollers.sync`.
---
## 3. UX papercuts in experimental mode (fix opportunistically)
- **Legacy tabs orphaned on upgrade** (P1-1): `createLocalTabs` migrates persisted
`localSessionIDs` into a `single` bucket that `tabKey()` never reads once the
catalog arrives. Migrate `single` to the pinned project id on first state apply.
- **`restoreProjectTarget` skips tab bookkeeping** (P1-3): a restored session has no
tab. Call `selectLocal()` first, add the tab, then select.
- **`ensurePendingTab` runs before restore** (P1-4): switch adds a "New Session"
draft that restore may contradict. Move it after restore.
- **Per-keystroke state saves**: `setActiveTarget` writes
`.kilo/agent-manager.json` on every selection change. Debounce or persist on
deactivation/dispose only.
- **Untranslated Indexing tab strings** (P1-9), **unused-ish composite id schemes**
(P1-12), **stats messages tagged at emit time** (P1-14), **no presence sync for
background projects** (P1-15), **registry read cache** (P1-16/17), **realpath
syscall churn** (P1-18), **`resolveProjectRoot` process spawning** (P1-19).
---
## 4. Structural debt (schedule, don't block)
### 4.1 Two sidebar implementations — Arch 1.1
`AgentManagerApp` keeps the legacy `renderBody` (now `SidebarBody.tsx`) and
`ProjectSidebarBody.tsx` as a reduced reimplementation. The reimplementation already
caused one full outage of multi-project mode (missing `DragDropProvider` crashed the
webview render). Missing versus legacy: worktree ordering, drag-and-drop reorder and
move-to-section, grouping, busy/navHint/shortcut badges, section auto-rename, stats
skeletons.
- Direction: single-project becomes the degenerate case of multi-project (one
implicit pinned project, header row hidden), `SidebarBody.tsx` is deleted.
- Do this last: it churns the same message-stamping code as 2.2, needs per-project
DnD state, and must keep legacy pixel-identical for the default-off population.
Use the visual-regression skill to cover both modes before merging it.
### 4.2 Ambient project scope via AsyncLocalStorage — Arch 1.3
`ProjectScope` plus the `this.state`/`this.context` getters make the target project
invisible at call sites; any continuation escaping ALS silently falls back to the
active project. `provider-lifecycle.ts` shows the intended end state (explicit deps).
Keep threading `ctx` explicitly into the remaining handler groups; add a dev-mode log
in the `context` getter when a project-stamped message resolves without a scope.
### 4.3 Per-project webview store — Arch 1.2
`worktrees()`, `managedSessions()`, `selection()` etc. are single-valued with
`memKey()`/`tabKey()` and two "current project" accessors that disagree during the
switch window. Long-term: one `createProjectStore(projectId)` per project. For now
the gating added to the remember effect contains the known race.
---
## 5. Verification gaps to close before the PR
- **SSE session upsert** is unit-tested (`upsertSession`, `byDirectory`, fresh-skip
re-post) but not E2E-verified: the harness backend's basic-auth credentials could
not be re-extracted after a window reload, so external session creation was not
exercised live. Verify manually: `kilo` a new session in a registered repo from a
terminal and watch it appear in that project's sidebar.
- **Tab isolation across projects** (per-project buckets) is unit-tested but not
E2E-verified with real sessions open in two projects.
- **Legacy mode parity**: legacy sidebar and tab bar were smoke-tested (render,
select, section create + inline rename), but not the full matrix (delete, DnD
reorder, move-to-section, review tab, terminals). The extraction moved ~700 lines
of JSX; a visual-regression pass over `SidebarBody`/`TabBar` stories is the
cheapest safety net.
- **Harness instability**: the isolated VS Code window crashed repeatedly during this
work. If it keeps failing on the next pass, say so in the PR rather than claiming
coverage that does not exist.
---
## Proposed landing sequence
1. Quick independent fixes: 2.3 (gh noise), P0-4 (trust filter), 1.1's audit + 1.2's
fallback (config), 1.2's indexing read/write split if the GET endpoint exists.
2. 2.1 (case-fold + registry migration).
3. 2.2 (route service, with a two-panel test harness).
4. 1.1's config/indexing commits split out and merged separately.
5. Arch 1.1 (sidebar collapse) with visual-regression coverage; then the P1 batch.
@@ -0,0 +1,740 @@
# Agent Manager multi-project uniform UI architecture
**Status:** Ready, corrected architecture; implementation pending
**Date:** 2026-07-22
This plan supersedes the active-body plus background-summary UI described in `agent-manager-multi-project-runtime.md`. The existing project registry, immutable project-root concept, per-project state payloads, and project-tagged stats work are useful foundations. The current read-only `ProjectSummary` and active-project dynamic getter routing are not the target architecture.
Configuration reads, writes, scopes, drafts, trust, revisions, and release-blocking tests are specified canonically in [`agent-manager-multi-project-configuration.md`](./agent-manager-multi-project-configuration.md). If this plan and that document differ on configuration behavior, the configuration document wins.
The step-by-step implementation sequence from the current branch is [`agent-manager-multi-project-implementation-handoff.md`](./agent-manager-multi-project-implementation-handoff.md). Implementers should follow that handoff slice by slice rather than treating this architecture document as an unordered task list.
## Product requirement
Every expanded project permanently renders the real existing Agent Manager sidebar UI:
- the real Local card;
- the real worktree cards, including their current menus and actions;
- the real session list, which may initially use a simplified layout but must contain real live session data;
- live git stats, PR state, run/setup state, stale state, and session state owned by that project.
Selecting or interacting with a project may change the shared detail pane and top toolbar. It must not remove, replace, downgrade, or remount any expanded project body. There is no active-body versus background-summary rendering mode.
The shared detail pane remains singular. The sidebar displays all expanded projects concurrently; the detail pane displays one selected project/local/worktree/session target at a time.
## Architecture verdict on the current worktree
The current implementation is a valid partial runtime foundation, but it is not safe to extend directly into fully interactive background cards.
Keep and evolve:
- `ProjectRegistry` and the pinned-workspace project concept;
- immutable root ownership in `ProjectContext`;
- `initContextState` extraction;
- project-stamped state/stats/PR messages;
- per-project webview stores;
- reusable `WorktreeItem`, `SectionHeader`, `WorktreeSectionActions`, and `UnassignedSessionsSection` components;
- the application-scoped feature flag.
Replace or refactor before exposing interactive multi-project UI:
- delete `ProjectSummary`; do not add more behavior to it;
- remove the rule that only `project.active` renders `renderBody()`;
- stop routing normal actions through `contexts.active()` and optional `projectId`;
- replace raw session/worktree/local UI keys with project-qualified references;
- replace the active singleton poller plus background poller split with uniform context-owned polling;
- make context initialization, mutation, suspension, and disposal explicit and generation-guarded;
- make the one embedded `KiloProvider` project/session-route aware.
- separate activation-bound runtime config from the Settings editor's immutable read/write binding.
## Core model
### Project identity
A project is one canonical Git repository root in the current extension host.
```ts
type ProjectId = string
interface ProjectRef {
projectId: ProjectId
}
interface WorktreeRef extends ProjectRef {
worktreeId: string
}
interface SessionRef extends ProjectRef {
sessionId: string
}
type SidebarTarget =
| { projectId: ProjectId; kind: "local" }
| { projectId: ProjectId; kind: "worktree"; worktreeId: string }
| { projectId: ProjectId; kind: "session"; sessionId: string }
```
Raw backend and repository-local IDs remain unchanged. Composite references are required at every panel/runtime boundary so identical local sentinels, worktree IDs, section IDs, or session IDs cannot cross projects.
Project identity must include the extension-host URI scheme and authority in addition to the canonical root. Adding a project must also resolve canonical `git-common-dir` and reject another exposed project sharing that common Git directory. Linked worktrees of one repository cannot be registered as independent projects in the first release.
### Active project means detail selection only
`activeProjectId` is not a rendering mode and not an implicit mutation target.
It means:
- which project owns the current shared detail/chat/diff/terminal pane;
- which project the global top toolbar and global New Session/Run buttons target;
- which project/worktree directory supplies runtime-effective Kilo configuration in the one embedded `KiloProvider`.
It never selects a Settings write target. Settings bindings are explicit and remain unchanged when detail selection changes.
Project expansion controls rendering. Every `project.expanded` project renders one stable `ProjectSidebarBody` keyed by `projectId`, regardless of active status.
### Atomic selection
Do not send `selectProject` followed by an unqualified worktree/session action. VS Code message handlers are asynchronous and are not a transaction queue.
Use one atomic selection intent:
```ts
interface ActivateSelectionMessage {
type: "agentManager.activateSelection"
target: SidebarTarget
}
```
The coordinator validates the project and target, ensures the context is ready, activates the project-specific Kilo route, then emits one project-qualified activation result. The sidebar body stays mounted; only shared detail state changes.
Mutations that do not need the detail pane, such as rename, delete, section edits, run, setup, open window, PR actions, or worktree creation, execute directly against their explicit `ProjectRef` or `WorktreeRef`. They do not activate another project as a side effect.
## Configuration architecture
See [`agent-manager-multi-project-configuration.md`](./agent-manager-multi-project-configuration.md) for the complete contract.
### Verified blocking failure
The current config protocol is unsafe for multi-project use:
- `KiloProvider.fetchAndSendConfig()` resolves a mutable current directory and emits one unqualified `configLoaded` value (`packages/kilo-vscode/src/KiloProvider.ts:2513`).
- `ConfigProvider` owns one global/project/effective draft and sends an unqualified `updateConfig` (`packages/kilo-vscode/webview-ui/src/context/config.tsx:54`, `:243`).
- `KiloProvider.handleUpdateConfig()` resolves `getWorkspaceDirectory()` again at save time (`packages/kilo-vscode/src/KiloProvider.ts:2982`). A draft loaded for project A can therefore be written to project B after detail activation changes.
- The backend derives the project target from request directory and has no target or revision precondition (`packages/opencode/src/config/config.ts:944`, `packages/opencode/src/kilocode/config/config.ts:67`). Concurrent editors can overwrite one another, and a newly created higher-priority config file can redirect a pending write.
- Hidden scope splitting currently writes `commit_message` and `indexing.enabled` to the project while most fields go global (`packages/kilo-vscode/webview-ui/src/utils/config-scope.ts:3`). The UI does not make this target choice explicit.
- Project config writes also exist outside the save bar. In particular, provider disconnect can call `saveProject()` using the mutable provider workspace directory (`packages/kilo-vscode/src/provider-actions.ts:252`). Disabling only `handleUpdateConfig()` is insufficient.
This is a release blocker, not a follow-up cleanup.
### Product decision and staged order
Preserve useful project-local editing such as `commit_message.prompt` and repository indexing rules. Indexing enablement is machine-local project consent, default off, and cannot be granted by repository config. Replace hidden active-directory targeting with explicit `User | Project` scope, a required Settings project selector for Project scope, an opaque immutable binding, and optimistic concurrency by target revision.
Do not maintain one write model for flag-off mode and another for multi-project mode. Single-project mode uses the same explicit protocol, with a narrow adapter that injects the pinned `ProjectRef` for reads and open-file actions.
### Separate runtime config from Settings editor config
The current `ConfigProvider` conflates two different state machines. Split them:
- **Runtime config** is read-only UI state for the selected detail/session route. It is keyed by `{ projectId, directory, activationGeneration }`, follows atomic detail activation, and drives chat, providers, agents, features, display, MCP, indexing, and sandbox behavior.
- **Settings editor config** is a scoped layer editor keyed by an immutable binding. It never follows detail activation and its draft is never applied optimistically to runtime config.
Use distinct messages. Names may follow repository conventions, but the fields and invariants are mandatory:
```ts
interface RuntimeConfigLoaded {
type: "runtimeConfigLoaded"
route: {
projectId: ProjectId
directory: string
activationGeneration: number
}
config: Config
features: FeatureFlags
}
type ConfigScope = "global" | "project"
interface ConfigTarget {
scope: ConfigScope
path: string
revision: string
exists: boolean
writable: boolean
}
interface SettingsBinding {
id: string
connectionGeneration: number
scope: ConfigScope
project?: {
projectId: ProjectId
root: string
generation: number
}
directory: string
target: ConfigTarget
}
interface ReadSettingsConfig {
type: "settingsConfig.read"
requestId: string
scope: ConfigScope
projectId?: ProjectId
}
interface SettingsConfigSnapshot {
type: "settingsConfig.snapshot"
requestId: string
binding: SettingsBinding
preview?: {
projectId: ProjectId
root: string
generation: number
directory: string
}
targetConfig: Config
effective: Config
global: Config
project: Config
fields: Record<string, ResolvedConfigField>
collections: Record<string, ResolvedConfigField[]>
}
interface WriteSettingsConfig {
type: "settingsConfig.write"
requestId: string
bindingId: string
set: Partial<Config>
unset: string[][]
}
```
For a global read, `projectId` is optional preview context used only to explain project shadowing. It never changes the global target. A project read requires `projectId` and always resolves the immutable `ProjectContext.root`, not the active worktree or session.
For User scope, preview identity is presentation state, not part of the editable target identity. The controller keeps the global target binding and draft separate from the latest project preview. A clean preview change may replace both from one fresh snapshot. A dirty preview change updates only `preview`, effective/source metadata, and project raw data. It retains the original global target revision; if the fresh read reports a different global path or revision, mark the draft stale instead of silently rebasing it.
`targetConfig` is the parsed raw content of the one file the API will patch. It is not the aggregate `global` or `project` layer. The form edits this raw target value so it never copies values inherited from another file into the target. The aggregate values remain in the snapshot solely for source, inheritance, and effective-preview UI. Existing JSONC comments remain server-side raw text and are preserved by patching; the webview does not round-trip serialized file contents.
Parse `targetConfig` from raw JSONC without expanding `{env:}` or `{file:}` variables, so the Settings protocol never turns placeholders into secret values. If the exact target is syntactically or structurally invalid, return diagnostics and `writable: false` for form editing rather than treating it as `{}` and overwriting it. The recovery action is **Open file**. Source metadata must cover every path rendered by Settings, not only the current hand-maintained overlay field list.
The extension owns an in-memory binding registry. Binding IDs are opaque, unpredictable, and scoped to one provider/webview lifecycle. The webview returns only `bindingId`, not a client-authoritative path or project envelope. On write the extension must:
1. look up `bindingId` and reject unknown or expired bindings;
2. verify that the project still exists, has the same context generation, and remains trusted;
3. capture the stored binding before the first `await`;
4. send the stored directory and expected target to the backend without calling `getWorkspaceDirectory()`, `contexts.active()`, or any fallback;
5. route success or failure by `{ requestId, binding.id }` only.
A binding expires after a successful save, backend reconnect, trust revocation, project removal, or context generation change. Success returns a new snapshot and binding. Cached snapshots are keyed by binding/scope/project identity; there is no singleton `cachedConfigMessage` for Settings.
### Backend read/write and optimistic concurrency contract
Evolve the Kilo-owned `/config/overlay` API rather than adding project identity to generic `Config.update`. The backend does not know Agent Manager `projectId`; it enforces the routed directory and filesystem target while the extension enforces project identity.
`GET /config/overlay` continues to take explicit `directory` and `scope`, but returns:
```ts
interface ConfigOverlaySnapshot {
context: { directory: string; worktree?: string }
scope: ConfigScope
targetConfig: Config
effective: Config
global: Config
project: Config
sources: ConfigSource[]
targets: {
global: ConfigTarget
project: ConfigTarget
active: ConfigTarget
}
fields: Record<string, ResolvedConfigField>
collections: Record<string, ResolvedConfigField[]>
}
```
The revision is a SHA-256 fingerprint of scope, canonical target path, existence marker, and exact file bytes. It is not an mtime. Exact bytes are required so comment-only JSONC edits and delete/recreate cycles with different content cause conflicts. The missing-file revision includes the intended canonical path. An ABA delete/recreate with identical bytes may retain the same revision; that state is content-equivalent and does not lose a user edit.
`PATCH /config/overlay?directory=...` accepts exactly one scope per request:
```ts
interface ConfigOverlayWrite {
scope: ConfigScope
set?: Record<string, unknown>
unset?: string[][]
expected: {
path: string
revision: string
}
}
interface ConfigOverlayWriteResult {
outcome: "applied" | "applied_but_overridden"
changed: boolean
overriddenPaths: string[][]
snapshot: ConfigOverlaySnapshot
}
```
The server performs the following transaction:
1. Resolve the authoritative target from the routed instance directory and requested scope. Never accept `expected.path` as an arbitrary destination.
2. Canonicalize and compare the resolved and expected paths. Reject a changed target, including a newly created higher-priority config file.
3. Acquire a cross-process lock keyed by canonical target path. Project writes must use the same locking discipline already used for global config.
4. Inside the lock, re-resolve the target, read exact bytes, recompute the revision, and reject a mismatch.
5. Apply `set`/`unset` to the raw target layer, preserve JSONC comments where supported, validate the result, and atomically replace the target using a temporary file in the same directory. Do not expose a partially written config.
6. Invalidate the affected config instances, reload the overlay, and return the authoritative post-write snapshot. Do not fabricate optimistic config if refresh fails.
7. Emit a config event containing `scope`, routed `directory`, canonical `target`, and new `revision`. Global changes invalidate all runtime config caches; project changes invalidate only matching directory/project caches.
`applied_but_overridden` is success. For each `set` leaf, the refreshed overlay identifies whether a higher layer still wins, such as project config shadowing a user value or managed/runtime config shadowing a project value. The UI keeps the saved value and explains why effective behavior did not change.
Do not send global and project patches in one webview message or pretend that two files save atomically. User and Project scope maintain independent drafts and save independently. VS Code extension preferences such as autocomplete settings are a third, visibly separate store with their own request acknowledgements.
Domain actions outside Settings must not perform client-side read-modify-write against a mutable effective config. Provider/custom-provider/work-style actions are User-global operations and should call either the same revision-aware User endpoint or a narrow backend mutation that merges named fields under the target lock. Permission Dock "always" rules remain a narrow global rule mutation tied to the exact pending request/session route; that session directory validates the request but does not select a project config target. All such mutations emit the revisioned config event, so a dirty Settings editor becomes stale rather than being overwritten.
Expected typed failures are:
- `unknown_project` or `stale_project_generation` from the extension;
- `untrusted_project` or `workspace_untrusted` from the extension;
- `binding_mismatch` or `binding_expired` from the extension;
- HTTP 409 `config_target_changed` with the current target descriptor;
- HTTP 409 `config_revision_conflict` with the current target descriptor and fresh snapshot when available;
- HTTP 403 `config_target_not_writable` or project target escaping its trusted root;
- HTTP 422 `config_invalid` with schema/parse details;
- an I/O failure with no success acknowledgement.
Every failure preserves the draft and names the scope/project/path. A config event is never treated as confirmation of the user's save; only a matching write response may clear that draft.
### Draft and project-switch behavior
- Detail activation from project A to B does not change the Settings selector, binding, values, draft, save state, or target.
- A User Settings draft is global and survives all detail-project switches.
- Project scope uses an explicit trusted project selector that never follows detail activation automatically.
- On first open, the selector may initialize once from the pinned/selected project for convenience, but that choice is immediately captured as Settings state.
- Choosing another Settings project with a dirty draft prompts `Save`, `Discard`, or `Stay`. Drafts are never carried to a different project.
- Clean selector changes issue a new request and ignore out-of-order responses by `requestId`.
- A save captures one binding. Switching visible settings while it is in flight does not redirect it; the result updates only the original binding store and identifies that target in its notification.
- External config events refresh a clean binding. For a dirty or saving binding they mark it stale and show a changed-on-disk banner without replacing local edits.
- A 409 preserves the draft and disables blind retry. The first version offers `Reload`, `Open file`, and `Discard`; it does not auto-merge or force overwrite. A future three-way merge may use the original snapshot, fresh snapshot, and draft.
### Scope UX
The first release presents explicit `User | Project` scope, a required selector in Project scope, source badges, and separate binding-keyed drafts. The selected scope applies to the entire operation. Remove hidden `splitConfigByScope`; no setting silently chooses a different file. Inherited user values remain visible in Project scope with `Override`/`Reset to inherited` affordances backed by `set`/`unset`.
### Project root, worktree, and trust semantics
- A Project Settings binding targets the registered checkout root held by `ProjectContext.root`. It never targets the last selected Local/worktree/session card.
- Runtime config remains directory-correct: Local uses the project root, and a worktree session uses its exact routed worktree directory. Branch-specific config in a managed worktree can therefore differ at runtime.
- Editing a project's root config does not claim to update already-created worktree checkouts. Editing a worktree config from Settings is out of scope for the first Project Settings release. A future worktree editor must use an explicit `WorktreeRef` and its own immutable binding.
- The backend resolver may choose an existing root or `.kilo`/`.kilocode` config according to current precedence, but the expected target must remain within the trusted project checkout after canonical/symlink resolution. A symlink escape is read-only and must not be patched through the API.
- For an existing target, canonicalize the file with `realpath`. For a missing target, canonicalize its nearest existing ancestor and append the remaining path components before checking containment. Global writes must similarly remain under the canonical `Global.Path.config` root. Do not rely on a lexical prefix check.
- Additional Agent Manager projects must be registry-trusted before project overlay evaluation, opening/creating project config, or project writes. Global User Settings remain editable without trusting a project.
- The pinned project is trusted for config evaluation only when the VS Code workspace is trusted. Do not treat `pinned` as an unconditional trust grant.
- Trust is stored outside the repository in the extension registry/global state. A project config file cannot grant its own trust. Revoking trust invalidates all bindings and project runtime caches.
### Concrete implementation sequence for config safety
1. In `packages/opencode/src/kilocode/config/overlay.ts`, replace path-only targets with revisioned target descriptors and add generic changed-path source resolution.
2. Add a Kilo-owned compare-and-swap writer under `packages/opencode/src/kilocode/config/` that resolves, confines, locks, fingerprints, patches, validates, and atomically replaces one target. Keep shared `packages/opencode/src/config/config.ts` changes to minimal marked delegation/invalidation hooks.
3. Update `packages/opencode/src/kilocode/server/httpapi/groups/config-console.ts` and `handlers/config-console.ts` with the write precondition, result, and typed error contracts. Regenerate `packages/sdk/js/`.
4. Extract a config binding controller under `packages/kilo-vscode/src/kilo-provider/` and have `KiloProvider.ts` delegate reads/writes to it. The controller receives explicit project-route resolution; it never reads mutable active workspace state during a bound operation.
5. Replace `configLoaded`, `configUpdated`, and unqualified `updateConfig` in `webview-ui/src/types/messages/` with runtime and Settings message families. Keep a temporary single-project adapter only at the protocol edge.
6. Split `webview-ui/src/context/config.tsx` into activation-bound runtime state and binding-keyed Settings editor state. Settings components consume the editor context; non-settings consumers continue to use runtime-effective config.
7. Remove `webview-ui/src/utils/config-scope.ts` after migrating every control to explicit scope. Preserve explicit project editing for commit messages and repository indexing rules; move indexing enablement to machine-local project consent.
8. Audit all extension config mutators and route user-config imports, resets, custom providers, work-style writes, project writes, and save-bar writes through a revision-aware endpoint or backend atomic field-level mutation. A project-sourced item must never fall back to an active directory write.
9. Make open-file actions take `ProjectRef`, resolve `ProjectContext.root`, verify trust, and show the exact path. For a missing target, open an unsaved document at that explicit path or require an explicit create action; never create it merely by visiting Settings.
10. Update config events and cache invalidation so scope/directory/revision are retained end to end.
Focused tests belong in:
- `packages/opencode/test/kilocode/server/config-overlay.test.ts` for raw target snapshots, target changes, 409 revisions, source shadowing, locks, symlink confinement, and response outcomes;
- `packages/opencode/test/kilocode/project-config-update.test.ts` for target selection and atomic writes from repository roots/nested directories;
- a new Kilo-owned backend unit test beside those files only if the compare-and-swap writer needs direct fault-injection coverage;
- `packages/kilo-vscode/tests/unit/config-utils.test.ts`, replacing singleton `ConfigState` assumptions with binding/request-aware draft tests;
- `packages/kilo-vscode/tests/unit/config-scope.test.ts`, deleted when `splitConfigByScope` is removed and replaced by tests proving one explicit scope per save;
- Agent Manager route/context tests for immutable project-root resolution, trust revocation, removal, and generation changes;
- KiloProvider protocol tests for A-to-B activation during reads and writes, out-of-order responses, cache partitioning, and auditing non-save-bar mutators.
## One `.kilo/agent-manager.json` per project
Yes, every project has its own repository-local state file:
```text
project-a/.kilo/agent-manager.json
project-b/.kilo/agent-manager.json
project-c/.kilo/agent-manager.json
```
This is already structurally supported because `WorktreeStateManager` derives its file from its immutable constructor root. The final architecture makes this ownership explicit and safe.
### Ownership rules
Each `ProjectContext` owns exactly one:
- canonical repository root and canonical Git common directory;
- `WorktreeStateManager(root)` writing only `<root>/.kilo/agent-manager.json`;
- `WorktreeManager(root)`;
- `SetupScriptService(root)` and project run service;
- diff/import/branch-naming services;
- git stats and PR pollers;
- worktree/session/section/order/run/stale caches;
- mutation queue and initialization promise.
The global project registry stores descriptors only. It never stores worktrees, sections, sessions, tab order, or repository behavior. The pinned project is derived from the current workspace and remains outside the registry.
### Initialization
The first explicit expansion of a trusted project calls one single-flight `ensureReady()` promise:
1. validate root and Git common directory;
2. construct project-owned services;
3. update that repository's local Git exclude;
4. load only that repository's `.kilo/agent-manager.json`;
5. discover and reconcile only that repository's worktrees;
6. register Local and worktree session routes for that project;
7. start context-owned polling if the project is expanded and the panel is visible;
8. post one generation-stamped project snapshot.
Repeated expansion must reuse the same initialization promise. Collapse/removal during initialization records the desired lifecycle transition and prevents late completion from posting or mutating a replacement context.
### Writes and disposal
All mutations for one project execute through that context's serialized mutation queue. An operation captures the context and generation before its first await and never re-resolves through `contexts.active()` afterward.
Collapse:
- leaves the project registered;
- stops pollers/watchers after current operations reach a safe boundary;
- flushes its own state file;
- retains session routes required for already-running backend sessions;
- never aborts a session merely because the UI collapsed.
Removal:
- never deletes `.kilo/agent-manager.json`, worktrees, branches, or sessions;
- refuses removal while a project mutation/run requires ownership, unless an explicit coordinated shutdown is implemented;
- awaits context disposal and state flush before deleting the registry descriptor;
- invalidates context generation before awaits so late callbacks are ignored.
Panel disposal awaits all initialized contexts before shared services are disposed.
Two VS Code windows writing the same repository state file remain the same pre-existing concurrency boundary as today's single-project Agent Manager. This plan prevents one panel from creating two contexts for the same Git common directory; cross-window file coordination is not expanded in this feature.
## ProjectContext lifecycle
`ProjectContext` becomes a lifecycle owner rather than only a lazy service container.
```ts
type ProjectLifecycle =
| "cold"
| "initializing"
| "ready"
| "suspended"
| "disposing"
| "disposed"
interface ProjectContext {
readonly id: ProjectId
readonly root: string
readonly commonGitDir: string
readonly generation: number
ensureReady(): Promise<ReadyProjectContext>
run<T>(operation: (ctx: ReadyProjectContext) => Promise<T>): Promise<T>
suspend(): Promise<void>
dispose(): Promise<void>
}
```
Every async init, poll, mutation, diff, run, import, setup, PR update, branch naming, and session operation checks context identity and generation before committing state or posting to the webview.
## Strict project-qualified protocol
When multi-project mode exposes more than one project, every repository operation requires project identity.
Required project qualification includes:
- worktree create/delete/rename/import/order/section/open/PR actions;
- Local and worktree session create/open/close/fork/promote/share/unshare actions;
- prompt, abort, permission, question, command, sandbox, and context actions;
- terminal create/input/resize/close/show actions;
- diff/watch/apply/revert actions;
- run/setup/configuration actions;
- all state/session/diff/run/terminal/setup/PR output messages.
Missing, unknown, mismatched, inactive-when-required, or ambiguous identities return a typed route error. They never fall back to the current project or `workspaceFolders[0]`.
Single-project mode retains a narrow compatibility adapter that injects the pinned project identity. Core handlers always operate on explicit references.
## One KiloProvider with a project/session route service
Keep one embedded `KiloProvider`. Multiple providers would duplicate event handling, terminal registrations, caches, and current-session state for one webview.
Add an Agent Manager route service:
```ts
interface SessionRoute extends SessionRef {
directory: string
generation: number
}
```
It must:
- register explicit routes for every Local session at its project root;
- register every worktree session at its worktree path;
- let child sessions inherit the parent route;
- validate directory ownership;
- detect ambiguous raw session IDs;
- supply the exact directory to every SDK operation, including share/unshare;
- remove current-root and workspace-root fallback for identified Agent Manager sessions;
- refresh sessions independently for every initialized project and emit real project-stamped `SessionInfo` records.
The shared detail pane consumes the selected `SessionRef`. Every sidebar body consumes its project's real session store. Background session UI may initially be visually simpler, but it must not display fabricated titles or placeholder session objects.
Project activation in `KiloProvider` must invalidate and refresh project-scoped config, providers, agents, commands, skills, MCP state, indexing, sandbox state, and Git status under an activation generation guard.
## Global services with composite keys
Some services remain panel-global to avoid duplicate VS Code registrations, but all associations use composite references.
- terminal mappings: `ProjectId + SessionId` or `ProjectId + WorktreeId`;
- run state: keyed by `WorktreeRef`, including project Local;
- visible/presence state: keyed by `SessionRef`;
- tabs and sidebar selection: keyed by `SidebarTarget`;
- diff/apply/revert state: keyed by `ProjectRef` and target;
- pending setup/create/delete state: keyed by project-qualified resource refs.
Remove unknown-worktree-to-current-root terminal fallback. A context supplies a validated CWD to the global terminal manager.
Global backend events are routed once by event directory and registered session route. Status, tool, orchestration, permission, and question events retain their directory and enter exactly one owning project context. A cold/collapsed context is not initialized by an unsolicited event.
## Uniform project sidebar UI
### One body implementation
Extract the existing real `renderBody()` from `AgentManagerApp.tsx` into `ProjectSidebarBody`. Do not maintain active and background implementations.
`ProjectSidebarBody` receives a stable project-scoped store and project-qualified action callbacks. It reuses:
- the existing Local card markup;
- `WorktreeSectionActions`;
- `WorktreeItem`;
- `SectionHeader` and current drag/drop behavior;
- `UnassignedSessionsSection`;
- existing rename/delete/open/PR/run/setup/session affordances.
Render it for every expanded project:
```tsx
<For each={projects()}>
{(project) => (
<ProjectAccordion project={project}>
<Show when={project.expanded}>
<ProjectSidebarBody projectId={project.id} />
</Show>
</ProjectAccordion>
)}
</For>
```
The body is keyed by `projectId`, not active status. Changing detail selection cannot remount it.
### Per-project webview stores
Replace active-only shared sidebar signals with `Map<ProjectId, ProjectSidebarStore>` or an equivalent Solid store registry.
Each store owns:
- state/worktrees/sections/order/stale state;
- real `SessionInfo` and managed-session state;
- git stats and local stats;
- PR and run/setup state;
- pending delete/rename/create state;
- sidebar search/order/collapse state where repository-owned;
- generation for dropping stale output.
Only shared detail/chat/terminal state remains global. The top toolbar reads the selected detail target's project store.
`project-live.ts` may be evolved into this complete store registry. It must stop mirroring active payloads into a separate sidebar signal set.
### Scrolling and layout
The project list becomes the sidebar scroll container. Individual project bodies must not use a global `50vh` cap that makes two projects fight for viewport height. Section and project collapse state controls density; normal browser scrolling exposes all expanded worktrees.
### UI behavior
- Clicking a Local/worktree/session card emits one atomic project-qualified selection.
- Rename/delete/run/open/section/PR actions execute against the clicked card's project without changing sidebar structure.
- The active project may receive an emphasis style, but no different markup.
- Active project chevrons are not disabled; expansion and detail selection are separate concepts.
- Global New Session/Run buttons target the last selected detail project.
- New Worktree accepts an explicit target project, defaulting to the selected detail project.
## Uniform polling and live state
Every ready expanded context owns the same poller set. There is no active singleton poller plus background poller split.
Pollers emit `{ projectId, generation }` and update only their owning context/store. Stop/suspend increments generation before clearing timers so in-flight Git/PR results are dropped.
Context state is pushed after every project mutation, not only initial expansion or active-project changes. This includes worktree/session/section/order/run/setup changes in background projects.
Panel visibility changes poll cadence uniformly across all expanded contexts.
## Registry correctness
Registry mutations use one in-process queue. Each mutation re-reads persisted storage, validates it, applies one mutation, writes once, then updates memory. Trust/remove persistence failures are user-visible and do not leave UI state ahead of storage.
Registry descriptors include canonical root, canonical Git common directory, URI scheme/authority, order, label, trust, and added time. They do not contain Agent Manager repository state.
## Implementation order
### Slice 0: configuration safety blocker
1. Introduce revisioned `/config/overlay` reads and compare-and-swap writes, first for User scope and with the same generic implementation covered for Project scope.
2. Separate runtime-effective config messages/state from immutable Settings bindings and drafts.
3. Add explicit `User | Project` Settings scope and an immutable trusted project selector/binding for Project writes.
4. Remove hidden `splitConfigByScope` persistence and audit every direct/indirect config mutation, including provider disconnect, import/reset, custom providers, work-style presets, Permission Dock, and indexing.
5. Make config events scope/directory/target/revision aware and generation-guard runtime activation refreshes.
6. Add target-switch, stale-binding, revision-conflict, trust, and draft-preservation tests.
Do not enable multi-project broadly before this slice passes.
### Slice 1: routing and identity blockers
1. Add project-qualified refs and strict protocol types.
2. Add the project/session route service and explicit Local routes.
3. Remove Agent Manager current/workspace-root fallback for identified operations.
4. Add atomic project-qualified selection.
5. Convert panel-global maps to composite keys.
6. Add strict routing and raw-ID collision tests.
Do not expose interactive background controls before this slice passes.
### Slice 2: context lifecycle and service ownership
1. Add single-flight initialization, lifecycle state, generation, and mutation queue to `ProjectContext`.
2. Move root-bound diff/import/run/setup/branch-naming/poller ownership behind context APIs.
3. Replace dynamic active getters inside async mutations with captured contexts.
4. Add suspend/remove/dispose coordination.
5. Reject duplicate Git common directories and serialize registry writes.
6. Route global backend events by directory.
### Slice 3: complete per-project stores
1. Refresh and store real sessions per project.
2. Emit all relevant output with project and generation.
3. Evolve `project-live.ts` into a complete project sidebar store registry.
4. Make polling uniform and generation-safe.
5. Add background mutation/state propagation tests.
### Slice 4: uniform real UI
1. Extract the current `renderBody()` intact into `ProjectSidebarBody`.
2. Parameterize it by one project store and project-qualified callbacks.
3. Render one body per expanded project.
4. Delete `ProjectSummary` and its CSS.
5. Separate project expansion from detail activation.
6. Add project-aware New Worktree targeting.
7. Add component/visual tests proving no body remount or structural change on selection.
### Slice 5: validation and release gating
1. Run focused lifecycle/router/context tests after each slice.
2. From `packages/opencode/`, run the focused config overlay/project-update tests and CLI typecheck. Run the opencode annotation and Promise-facade guards for shared-file hooks.
3. Regenerate the JS SDK after endpoint schema changes and verify generated consumers compile; never hand-edit generated sources.
4. From `packages/kilo-vscode/`, run typecheck, lint, focused/unit tests, knip, compile/package, and architecture guards.
5. Manually exercise two repositories with multiple worktrees and sessions in both, including a dirty User draft during project activation, an external config edit conflict, project trust revocation, project removal, and project-config open targeting.
6. Keep the feature flag default off until all acceptance criteria pass.
## Blocking tests
The feature is not complete until tests prove:
1. Two concurrent expands initialize one context once.
2. Collapse/remove during initialization produces no late state or poller posts.
3. A project switch during create/import/setup/session/run/diff cannot change operation ownership.
4. Project B Local sessions never resolve through project A.
5. Missing/mismatched project references fail closed.
6. Poller/PR late results after stop or generation change are dropped.
7. Same raw worktree/session/local IDs in two projects do not collide in runtime or UI stores.
8. Two expanded projects render two permanent full `ProjectSidebarBody` instances.
9. Selecting a card changes only shared detail state and active emphasis, not body structure or mount identity.
10. Every worktree/session/terminal/diff/run/setup/section/PR action targets the clicked project's context.
11. Registry writes preserve concurrent add/remove/trust changes.
12. Roots sharing one Git common directory cannot both register.
13. Panel shutdown awaits all contexts and stops background activity.
14. Flag-off behavior remains the current single-project UI and state file.
15. A Settings draft loaded for project A cannot write project B after any activation, expansion, removal, trust, or worktree switch.
16. User Settings writes target the revisioned user file; explicit Project Settings writes target only the selected trusted project's revisioned root config. No key is silently rerouted.
17. A clean external config change refreshes Settings; a dirty or saving binding is marked stale without losing its draft.
18. Concurrent editors using the same revision produce one success and one 409 conflict, with no lost update or partial file.
19. A comment-only JSONC edit, file deletion/recreation with changed bytes, or newly created higher-priority config changes the revision/target and rejects the stale write.
20. A save response clears only the matching `{ requestId, binding.id }`; an SSE config event or out-of-order read cannot confirm a save.
21. Global and project writes are independently acknowledged and never presented as one atomic transaction.
22. Project config open/read/write actions reject unknown, untrusted, removed, generation-stale, symlink-escaping, or mismatched project targets.
23. Runtime config for a selected managed worktree uses that worktree directory, while Project Settings/open-file targets the immutable registered project root.
24. Project-sourced providers and other config collections cannot trigger an implicit active-project write from Settings actions.
## Acceptance criteria
- No `ProjectSummary` or alternate fake-card body exists.
- Every expanded project displays real live Local, worktree, and session UI concurrently.
- Every visible worktree/session action is usable and routes by explicit project identity.
- Switching detail selection causes no sidebar body remount, replacement, collapse, or data reset.
- Each project reads and writes only its own `.kilo/agent-manager.json`.
- Expanded projects remain live independently; collapsed projects suspend safely.
- No identified operation falls back to active/current/workspace root.
- Single-project behavior and persisted state remain backward-compatible when the flag is off.
- Shared Settings exposes explicit User and Project scope; Project writes require a trusted project selector and immutable binding.
- Runtime project activation and Settings editor binding are separate state machines.
- Every Settings config write has an immutable scope/target binding and revision precondition; narrow domain mutations merge named fields under the same target lock. No config mutation uses a save-time active-directory lookup.
- Dirty drafts survive external updates and conflicts and can never migrate between projects.
- Project Settings targets the explicit registered root, never the active Agent Manager project or active worktree.
## Planned UI
```text
┌─ Sidebar ─────────────────────────────────┬─ Shared detail pane ──────────┐
│ ← selected project search calendar gear │ │
│ │ Chat / diff / terminal │
│ PROJECTS + │ for the selected │
│ ▾ abalone-bactrosaurus │ project/worktree/session │
│ ┌─────────────────────────────────┐ │ │
│ │ Local main +3295 -548 ↓54 │ │ │
│ └─────────────────────────────────┘ │ │
│ WORKTREES actions │ │
│ ┌─────────────────────────────────┐ │ │
│ │ feature-x #1234 +120 -8 ↓2 │ │ │
│ │ real menus, rename, run, open │ │ │
│ └─────────────────────────────────┘ │ │
│ ┌─────────────────────────────────┐ │ │
│ │ bugfix-y +45 -12 │ │ │
│ └─────────────────────────────────┘ │ │
│ SESSIONS │ │
│ • Fix login redirect running │ │
│ • Refactor auth │ │
│ │ │
│ ▾ kilo-pi-provider │ │
│ ┌─────────────────────────────────┐ │ │
│ │ Local master +16 -6 ↓1 │ │ │
│ └─────────────────────────────────┘ │ │
│ WORKTREES actions │ │
│ ┌─────────────────────────────────┐ │ │
│ │ provider-z #88 +88 -3 │ │ │
│ │ real menus, rename, run, open │ │ │
│ └─────────────────────────────────┘ │ │
│ SESSIONS │ │
│ • Add provider tests │ │
│ │ │
│ ▸ docs-repo collapsed │ │
│ │ │
│ [New Session] [Run] │ │
└───────────────────────────────────────────┴──────────────────────────────┘
Selecting any card:
- keeps every project body mounted and pixel-stable;
- changes only the selected styling, top toolbar target, and shared detail pane;
- never swaps a real body for a summary or vice versa.
```