mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge remote-tracking branch 'origin/main' into puddle-barometer
# Conflicts: # packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImplTest.kt
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix the Agent Manager sidebar keyboard shortcut badge so it appears on the right edge of local rows and only while hovered or holding the jump modifier, give worktree titles more room by no longer reserving space for hidden row actions, align project names and section headings with the row icons below them, and show which worktree a session belongs to in the search palette.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Improve Agent Manager terminal focus and keyboard navigation. `Cmd+/` now focuses a visible embedded terminal before hiding it, `Cmd+Shift+T` creates a side terminal only while that terminal area has focus, and `Cmd+Shift+[` / `]` switch terminal tabs. `Cmd+Shift+M` focuses the Agent Manager prompt instead of opening VS Code Problems. `Cmd+W` hides the last side terminal instead of stopping its shell.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Align multi-project Agent Manager header actions with the worktree controls.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Improve model search relevance with provider-aware results and personalized usage suggestions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Let Agent Manager users choose the repository when creating or importing a worktree in multi-project mode.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Remember Agent Manager worktree dialog model, variant, mode, and sandbox selections when reopened.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Reduce Agent Manager background Git polling load across large worktree sets.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Prevent built-in skill documentation examples from triggering shell permission prompts.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Update the active model, mode, and thinking selectors when executing a slash command with configured overrides.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
---
|
||||
|
||||
Add kilocode command-file endpoints so clients can list editable command/workflow files, inspect model and reasoning variant metadata, and remove them.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Compress speech-to-text audio input to AAC format across macOS, Linux, and Windows to prevent payload size errors on long recordings.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Start Agent Manager worktree sessions faster by prefetching base branches, reducing workspace file-watcher load, and overlapping independent multi-session setup.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix the Agent Manager terminal toolbar button to toggle panel visibility directly.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Prevent extension-managed GitHub CLI commands from opening transient Windows Terminal windows.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Restore keyboard input for interactive terminal prompts when the CLI session uses a workspace.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Route Agent Manager tool-launched sessions to the project that owns the tool event directory, keep sandboxed worktree sessions inside their active worktree, and wait for busy managed sessions before prompting them.
|
||||
@@ -2,4 +2,4 @@
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Scope Agent Manager session events and Git status to the active project, including edits inside nested repositories.
|
||||
Speed up local VS Code snapshot installs and scope Agent Manager session events and Git status to the active project, including edits inside nested repositories.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix the sidebar navigation bar (New Task, History, Agent Manager, KiloClaw, Marketplace, Profile, Settings) disappearing in Cursor when the Kilo Code view is docked in the Secondary Side Bar. Cursor now renders the navigation inside the webview itself so it stays visible regardless of dock location. VS Code is unaffected — it continues to use its native title bar toolbar, which already worked correctly everywhere.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@kilocode/kilo-ui": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix flickering and sticky scrolling when scrolling up in Agent Manager and chat sessions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Move the "why was this tool call approved" line to after the tool output instead of between the header and body, add an icon to it, and add a Display setting to hide it.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Allow subagent tasks to be resumed after their parent session is forked.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Support the Agent Manager tool with llama.cpp servers that reject prefix-only JSON Schema patterns.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Add bounded, context-aware grep controls without leaving agents waiting on completed searches.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve JetBrains session transcript layout, icons, reverted-change summaries, and multi-hunk diff rendering.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve slash command completion to match separators, camel-case humps, and contained command names.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep Agent Manager terminals aligned and correctly wrapped when the terminal panel is narrow.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep recent sessions and the Show History action inset and usable in narrow VS Code sidebars.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
"@kilocode/cli": minor
|
||||
---
|
||||
|
||||
Add nested slash command suggestions for `/review` in VS Code and support `staged`, `unpushed`, and `quick` review modes.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Preserve the selected reasoning effort when switching to a model that supports the same or nearest available variant.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Reduce Agent Manager Git polling overhead by reusing the validated Git executable and bypassing the macOS developer-tool launcher when safe.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Use a compatible Kilo wordmark in embedded Agent Manager terminals.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Prevent TUI config reload logs from corrupting the interactive terminal.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Avoid printing an error when closing the TUI cancels in-flight startup refreshes.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Remove unsupported `kilo web` CLI command.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Prevent concurrent Kilo startups from rewriting unchanged credentials, retry transient database locks, and redact bound values from database errors.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Apply saved sandbox settings to existing sessions and use the latest settings when enabling sandboxing
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Surface process exit signals and structured spawn failure details in server startup diagnostics
|
||||
@@ -2,4 +2,4 @@
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Persist the Agent Manager inspector width and share it between the terminal and diff viewer.
|
||||
Persist the Agent Manager inspector width, share it between the terminal and diff viewer, and keep resizing responsive.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Translate the Agent Manager terminal focus shortcut label in all supported locales.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Long skill folder paths and URLs shown in the tooltip on the Skills settings page now wrap inside the viewport instead of overflowing on a single line.
|
||||
@@ -41,6 +41,9 @@ jobs:
|
||||
- name: Check Effect Promise facade allowlist
|
||||
run: bun run script/check-opencode-promise-facades.ts
|
||||
|
||||
- name: Check domain architecture boundaries and ratchets
|
||||
run: bun run script/check-architecture.ts
|
||||
|
||||
- name: Check model tool network boundary
|
||||
run: bun run script/check-model-tool-network.ts
|
||||
|
||||
|
||||
@@ -1,459 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,210 +0,0 @@
|
||||
# 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.
|
||||
|
||||
- Backend half is done: `expected` is optional in the overlay schema, writer, and
|
||||
handler, so a client without a binding writes unconditionally again instead of
|
||||
getting a 400. The webview half (draft retention on `configUpdateFailed`, and the
|
||||
audit of every `updateConfig` sender) is still open.
|
||||
- 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
|
||||
|
||||
Partially addressed: untrusted projects are now filtered out of the consent list
|
||||
(P0-4), and config scope switching plus project-scoped `indexing.enabled` writes
|
||||
were restored after the rework had hardcoded the tab to global scope (the earlier
|
||||
P1-8 gap). The remaining blocker is the read path below.
|
||||
|
||||
|
||||
`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
|
||||
|
||||
Tracked as #12685 (section parity) and #12686 (sidebar drag-and-drop).
|
||||
|
||||
`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,437 @@
|
||||
# Plan: Agent Manager sidebar — shortcut badge fix + reclaim title width
|
||||
|
||||
Worktree: `/Users/marius/Documents/git/kilocode/.kilo/worktrees/mewing-profit`
|
||||
All paths below are relative to `packages/kilo-vscode/`.
|
||||
|
||||
Rules:
|
||||
|
||||
- Do exactly these edits. Do not refactor anything else.
|
||||
- Solid.js, not React: `class=`, not `className=`.
|
||||
- Do **not** add `kilocode_change` markers. This package is Kilo-owned and CI fails if you do.
|
||||
- Do **not** add new i18n strings. None are needed.
|
||||
- Out of scope: the per-project `SESSIONS` list in the tree (tracked in
|
||||
https://github.com/Kilo-Org/kilocode/issues/12928). Do not touch `UnassignedSessionsSection.tsx`.
|
||||
|
||||
Do task A, verify, then task B, verify, then task C.
|
||||
|
||||
## Task A — Shortcut badge on the right, hidden until hover or ⌘ held
|
||||
|
||||
The `⌘1` badge on "local" rows is always visible, and in multi-project mode it renders on the
|
||||
*left*, between the icon and the label. Worktree rows already behave correctly. Cause:
|
||||
`.am-shortcut-badge` has no default `opacity: 0` — worktree badges are only hidden because their
|
||||
container `.am-wt-hover-actions` is hidden, and local rows never got that container.
|
||||
|
||||
### A1 — `webview-ui/agent-manager/ProjectSidebarBody.tsx`
|
||||
|
||||
Replace the whole `<button class="am-local-item">` block (lines **301-348**) with this. The badge
|
||||
`<Show>` moves from before `am-local-text` to the end, and stats + badge get wrapped:
|
||||
|
||||
```tsx
|
||||
<button
|
||||
class="am-local-item"
|
||||
classList={{ "am-local-item-active": active() && props.selection === "local" }}
|
||||
data-sidebar-id={`${props.project.id}:local`}
|
||||
onClick={() => props.onSelectLocal(props.project.id)}
|
||||
>
|
||||
<Show when={!props.localBusy?.()} fallback={<Spinner class="am-worktree-spinner" />}>
|
||||
<svg class="am-local-icon" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2.5" y="3.5" width="15" height="10" rx="1" stroke="currentColor" />
|
||||
<path d="M6 16.5H14" stroke="currentColor" stroke-linecap="square" />
|
||||
<path d="M10 13.5V16.5" stroke="currentColor" />
|
||||
</svg>
|
||||
</Show>
|
||||
<div class="am-local-text">
|
||||
<span class="am-local-label">{props.t("agentManager.local")}</span>
|
||||
<Show when={props.local?.branch}>
|
||||
<span class="am-local-branch">{props.local!.branch}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="am-wt-actions-cell">
|
||||
<Show
|
||||
when={
|
||||
props.local &&
|
||||
(props.local.additions || props.local.deletions || props.local.ahead || props.local.behind)
|
||||
}
|
||||
>
|
||||
<div class="am-worktree-stats">
|
||||
<Show when={props.local!.behind}>
|
||||
<span class="am-worktree-behind">↓{props.local!.behind}</span>
|
||||
</Show>
|
||||
<Show when={props.local!.ahead}>
|
||||
<span class="am-worktree-commits">↑{props.local!.ahead}</span>
|
||||
</Show>
|
||||
<Show when={props.local!.additions}>
|
||||
<span class="am-stat-additions">+{props.local!.additions}</span>
|
||||
</Show>
|
||||
<Show when={props.local!.deletions}>
|
||||
<span class="am-stat-deletions">−{props.local!.deletions}</span>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="am-wt-hover-actions">
|
||||
<Show when={props.shortcutMap?.().get(`${props.project.id}:local`)}>
|
||||
{(shortcut) => (
|
||||
<span class="am-shortcut-badge">
|
||||
{isMac ? "⌘" : "Ctrl+"}
|
||||
{shortcut()}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
```
|
||||
|
||||
The `−` in `am-stat-deletions` is U+2212 MINUS SIGN, not a hyphen. Copy it verbatim.
|
||||
|
||||
### A2 — `webview-ui/agent-manager/SidebarBody.tsx`
|
||||
|
||||
Same bug in legacy single-project mode, but the badge is already last, so only the wrapper is
|
||||
missing.
|
||||
|
||||
Insert **before** line **127** (`<Show when={props.localStats() === undefined}>`):
|
||||
|
||||
```tsx
|
||||
<div class="am-wt-actions-cell">
|
||||
```
|
||||
|
||||
Replace line **182**:
|
||||
|
||||
```tsx
|
||||
<span class="am-shortcut-badge">{isMac ? "⌘" : "Ctrl+"}1</span>
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```tsx
|
||||
<div class="am-wt-hover-actions">
|
||||
<span class="am-shortcut-badge">{isMac ? "⌘" : "Ctrl+"}1</span>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Result: one `am-wt-actions-cell` div containing the skeleton `<Show>`, the stats `<Show>`, and the
|
||||
hover-actions div, closing before `</button>`. Prettier fixes indentation in task D.
|
||||
|
||||
### A3 — `webview-ui/agent-manager/agent-manager.css`
|
||||
|
||||
Delete lines **569-575** and replace with the rule that actually works:
|
||||
|
||||
```css
|
||||
.am-local-item .am-shortcut-badge {
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.am-local-item:hover .am-shortcut-badge {
|
||||
opacity: 1;
|
||||
}
|
||||
```
|
||||
|
||||
becomes:
|
||||
|
||||
```css
|
||||
.am-local-item:hover .am-wt-hover-actions {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
```
|
||||
|
||||
(`right: 8px` never applied — the badge is `position: static`. The `opacity: 1` never did anything
|
||||
because nothing set `opacity: 0` first.)
|
||||
|
||||
### A4 — same file, delete lines **609-611** entirely, add nothing:
|
||||
|
||||
```css
|
||||
.am-show-shortcuts .am-local-item .am-shortcut-badge {
|
||||
opacity: 1;
|
||||
}
|
||||
```
|
||||
|
||||
The rule at lines 597-600 is not scoped to worktree items, so after A1/A2 it already covers local
|
||||
rows.
|
||||
|
||||
### A5 — same file, lines **986-989**, add `visibility: hidden` to match worktree behaviour:
|
||||
|
||||
```css
|
||||
.am-local-item:hover .am-worktree-stats,
|
||||
.am-local-item:hover .am-worktree-stats-skeleton {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
```
|
||||
|
||||
### Verify A
|
||||
|
||||
```bash
|
||||
cd packages/kilo-vscode && bun run typecheck && bun run lint
|
||||
```
|
||||
|
||||
## Task B — Stop reserving width for invisible content
|
||||
|
||||
`.am-wt-actions-cell` is a grid whose children all stack in cell 1/1, so the cell is permanently as
|
||||
wide as its widest child. `visibility: hidden` does not remove layout, so the invisible hover
|
||||
actions (~42px) and the loading skeleton (~48px) reserve width on every row forever. That is why
|
||||
titles truncate ~40px before the right edge.
|
||||
|
||||
All edits in `webview-ui/agent-manager/agent-manager.css`.
|
||||
|
||||
### B1 — Take hover actions out of grid flow
|
||||
|
||||
Lines **475-482**. Add the four `position` lines at the top, keep everything else:
|
||||
|
||||
```css
|
||||
.am-wt-hover-actions {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 2px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
```
|
||||
|
||||
Absolutely positioned children never size grid tracks. `.am-wt-actions-cell` is already
|
||||
`position: relative` (line 464). Do not edit the `.am-wt-actions-cell > *` rule at lines 469-472.
|
||||
|
||||
### B2 — Same for the loading skeleton
|
||||
|
||||
Add immediately after the rule from B1:
|
||||
|
||||
```css
|
||||
/* Skeleton is a placeholder, not real content — it must not reserve title width. */
|
||||
.am-wt-actions-cell > .am-worktree-stats-skeleton {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
```
|
||||
|
||||
Do not edit the base `.am-worktree-stats-skeleton` rule (lines 744-748) or `.am-pr-badge-skeleton`.
|
||||
|
||||
After B1+B2 the cell is sized only by `.am-worktree-stats` and `.am-worktree-delete-hint`, which
|
||||
are the only things that should reserve space.
|
||||
|
||||
### B3 — Fade the title under the overlaying actions
|
||||
|
||||
The actions now overlay the row instead of sitting in reserved space, so a long title would render
|
||||
behind them. Same fix the codebase already uses for the local branch at lines 577-580. Add after
|
||||
B2:
|
||||
|
||||
```css
|
||||
.am-worktree-item:hover .am-worktree-branch {
|
||||
mask-image: linear-gradient(to right, black calc(100% - 48px), transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to right, black calc(100% - 48px), transparent 100%);
|
||||
}
|
||||
```
|
||||
|
||||
### B4 — Remove one layer of nested padding
|
||||
|
||||
Lines **365-370**, change `padding: 0 6px;` to `padding: 0;`:
|
||||
|
||||
```css
|
||||
.am-project-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
```
|
||||
|
||||
Gains 6px per side on every row in a project. Rows keep their own 10px inset
|
||||
(`.am-local-item` line 106, `.am-worktree-item` line 425) so they stay indented under the project
|
||||
header. Do not change those, and leave `.am-project-body .am-section-header` (lines 273-275) alone.
|
||||
|
||||
### B5 — Remove the remaining outer list gutters in multi-project mode
|
||||
|
||||
The sidebar keeps 8px horizontal padding for the header controls, but the project list should use
|
||||
the full width up to its scrollbar. In the same CSS file, extend `.am-projects-list` with:
|
||||
|
||||
```css
|
||||
.am-projects-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
margin-inline: -8px;
|
||||
}
|
||||
```
|
||||
|
||||
Do not remove padding from `.am-sidebar` itself. This makes project cards and rows reach the
|
||||
scrollbar without moving the `PROJECTS` header and its controls to the edge.
|
||||
|
||||
### Verify B
|
||||
|
||||
```bash
|
||||
cd packages/kilo-vscode && bun run typecheck && bun run lint
|
||||
```
|
||||
|
||||
## Task C — Show where a palette session lives
|
||||
|
||||
In the ⌘F search palette, multi-project session results show only the project name, so you cannot
|
||||
tell whether a session is in a worktree or at the project root.
|
||||
|
||||
File: `webview-ui/agent-manager/ProjectList.tsx`. In the session loop (lines **90-106**), add the
|
||||
two `const` lines and change `meta` and `search`:
|
||||
|
||||
```tsx
|
||||
for (const session of props.sessions[project.id] ?? []) {
|
||||
const wt = session.worktreeId ? state.worktrees.find((item) => item.id === session.worktreeId) : undefined
|
||||
const where = wt ? wt.label || wt.branch : props.t("agentManager.local")
|
||||
items.push({
|
||||
key: `${project.id}:session:${session.id}`,
|
||||
projectId: project.id,
|
||||
kind: "session",
|
||||
group: "sessions",
|
||||
title: session.title || props.t("agentManager.session.untitled"),
|
||||
meta: [project.label, where],
|
||||
search: [project.label, where, wt?.branch, session.title, session.id].filter(Boolean).join(" "),
|
||||
updatedAt: session.updatedAt,
|
||||
state: "idle",
|
||||
visible: project.expanded,
|
||||
sessionId: session.id,
|
||||
location: session.worktreeId ? "worktree" : "local",
|
||||
worktreeId: session.worktreeId ?? undefined,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
`state` is already in scope (line 55) and already null-checked (line 56). `meta` is joined with
|
||||
` · ` by the renderer (`SidebarSearchMenu.tsx:143`), so no separator work is needed.
|
||||
|
||||
## Task E — Align the project heading with the row icons
|
||||
|
||||
Every row in the projects tree used a different left inset, which made the sidebar look busy and
|
||||
indented for no reason. Measured against a full-bleed `.am-projects-list` (starting at x=0):
|
||||
|
||||
| Element | Before | After |
|
||||
|---|---|---|
|
||||
| `PROJECTS` label | 16px | 10px |
|
||||
| Project chevron | 12px | 10px |
|
||||
| `WORKTREES` / `SESSIONS` label | 6px | 10px |
|
||||
| `.am-local-item` icon | 10px | 10px |
|
||||
| `.am-worktree-item` icon | 10px | 10px |
|
||||
|
||||
Changes in `agent-manager.css`:
|
||||
|
||||
- `.am-local-item` padding `8px 10px` → `8px 6px`
|
||||
- `.am-worktree-item` padding `6px 10px` → `6px 6px`
|
||||
- `.am-project-item` padding `6px 8px 6px 12px` → `6px 6px`
|
||||
- `.am-project-body .am-section-header` padding-left `6px` → unchanged at `6px`
|
||||
- `.am-projects > .am-section-header` gets `padding-left: 2px`, because that heading sits inside
|
||||
`.am-sidebar`'s own 8px padding rather than in the pulled-out list
|
||||
|
||||
The leading columns carry no padding of their own (`.am-sidebar-header-toggle` and
|
||||
`.am-sidebar-header-chevron` are bare 16px boxes), so row padding is the only lever.
|
||||
|
||||
### Symmetric gutter
|
||||
|
||||
`.am-projects-list` uses `margin-left: -4px; margin-right: -4px`, pulling out of `.am-sidebar`'s
|
||||
8px padding to an even 4px gutter. An earlier attempt used `-8px` on the left for true full bleed,
|
||||
but that is wrong twice over: the resize handle's inner half then covered every card, so card
|
||||
clicks started a resize, and a selected row's `border-radius: var(--radius-sm)` background clipped
|
||||
flat against the window edge while still being inset on the right, which read as a bar bleeding off
|
||||
the sidebar rather than a card.
|
||||
|
||||
4px is also the most reclaimable on the right. The handle's hit area is 8px wide centered on the
|
||||
border, reaching 5px back into the content area (255-263 in a 260px sidebar). At this gutter the
|
||||
row's hover actions end at 249, leaving 6px of clearance; anything tighter puts row buttons under
|
||||
the handle and turns clicks into resize drags.
|
||||
|
||||
### Two tab stops
|
||||
|
||||
Reducing the insets exposed that labels sat at four different offsets: the non-collapsible
|
||||
`WORKTREES` heading at 6px (it passes no `onToggle`, so `SidebarSectionHeader` renders no chevron),
|
||||
collapsible headings at 28px, worktree card text at 30px, and local card text at 32px because
|
||||
`.am-local-icon` was 18px wide while `.am-wt-icon` held a 16px glyph.
|
||||
|
||||
Normalized to one 16px leading column with an 8px gap, giving exactly two tab stops:
|
||||
|
||||
- `.am-sidebar-header-main` gap `6px` → `8px`, matching the card icon gap
|
||||
- new `.am-project-body .am-sidebar-header:not(.am-sidebar-header-toggleable) .am-sidebar-header-main { padding-left: 24px }`
|
||||
so a heading with no chevron still reserves the column
|
||||
- `.am-local-icon` `18px` → `16px`
|
||||
- `.am-wt-icon` gains `width: 16px` and `justify-content: center`
|
||||
|
||||
Measured result at 260px:
|
||||
|
||||
| Tab stop | Elements |
|
||||
|---|---|
|
||||
| 10px | `PROJECTS` label, project chevron, local icon, worktree icon, `SESSIONS` chevron |
|
||||
| 34px | project name, `WORKTREES` label, `SESSIONS` label, local text, worktree text |
|
||||
|
||||
```
|
||||
cardLeftGap=4 | cardRightGap=4 | actionsRight=249 | handleZoneFrom=255
|
||||
```
|
||||
|
||||
`PROJECTS` intentionally stays at the 10px glyph stop rather than being pushed to 34px: it is the
|
||||
root heading, so indenting it further than the projects beneath it would invert the hierarchy.
|
||||
|
||||
### Rejected: a per-project icon in the heading
|
||||
|
||||
Superset shows a GitHub **owner** avatar per project (`https://github.com/{owner}.png?size=64`,
|
||||
falling back to the project's initial). Our webview CSP already allows `https:` images, so it was
|
||||
feasible, but it does not fit this repo set: of the local projects, ~17 are `Kilo-Org` remotes and
|
||||
would share one identical Kilo logo, and ~20 have no git remote at all and would show nothing.
|
||||
An owner avatar answers "who owns this", which is not the question the sidebar needs answered.
|
||||
Superset's repo-file scanner (`favicon-discovery.ts`) is dead code there, so it was not an option
|
||||
worth copying either. Left out entirely as out of scope.
|
||||
|
||||
## Task D — Final checks
|
||||
|
||||
From `packages/kilo-vscode`:
|
||||
|
||||
```bash
|
||||
bun run format
|
||||
bun run typecheck
|
||||
bun run lint
|
||||
bun run test:unit
|
||||
bun run check-kilocode-change
|
||||
bun run compile
|
||||
```
|
||||
|
||||
Then create `.changeset/agent-manager-sidebar-density.md`:
|
||||
|
||||
```md
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix the Agent Manager sidebar keyboard shortcut badge so it appears on the right edge of local rows and only while hovered or holding the jump modifier, give worktree titles more room by no longer reserving space for hidden row actions, and show which worktree a session belongs to in the search palette.
|
||||
```
|
||||
|
||||
## Visual regression baselines
|
||||
|
||||
Task A and B change existing snapshots: `WorktreeItemDefault`, `WorktreeItemActive`,
|
||||
`WorktreeItemPendingDelete`, `WorktreeItemStale`, `WorktreeItemWithStats`, `WorktreeItemGrouped`,
|
||||
`SidebarSearchOpen`, `MultiProjectSidebar` in `webview-ui/src/stories/agent-manager.stories.tsx`.
|
||||
|
||||
**Do not run or update visual regression tests.** `tests/visual-regression.spec.ts` is skipped on
|
||||
macOS, so you cannot produce valid Linux baselines locally. CI regenerates them and may push a
|
||||
baseline commit. If a push is then rejected, do **not** `git pull --rebase`; run
|
||||
`git fetch && git push --force-with-lease`.
|
||||
|
||||
## Manual test
|
||||
|
||||
Enable `kilo-code.new.experimental.multiProject` in Kilo Settings → Experimental, add a second
|
||||
project, open Agent Manager (`Cmd+Shift+M`).
|
||||
|
||||
1. Nothing hovered: no `⌘N` badge visible anywhere.
|
||||
2. Hover a `local` row: badge appears at the **right** edge, git stats fade out.
|
||||
3. Hold ⌘: badges appear on all local and worktree rows, all right-aligned. Release: all gone.
|
||||
4. Worktree rows with no git changes show noticeably longer titles than before. Hover one with a
|
||||
long title: it fades under the badge and trash button instead of colliding.
|
||||
5. ⌘F: session results read `<project> · <worktree or local>`.
|
||||
6. Turn the experimental setting off: the `local` row's `⌘1` badge is hidden until hover.
|
||||
|
||||
## Known issue, not fixed here
|
||||
|
||||
`⌘1`–`⌘9` index the full nav order, which includes session rows that render no badge
|
||||
(`navigate.ts:214-218` + `section-helpers.ts:147-153`). So with 4 worktrees you see `⌘1`–`⌘5` and
|
||||
`⌘6`–`⌘9` silently land on unlabelled session rows. Deliberately left alone: fixing it means
|
||||
changing jump semantics and rewriting `tests/unit/navigate.test.ts:745-757`, and it becomes moot
|
||||
once #12928 moves sessions out of the tree.
|
||||
@@ -0,0 +1,834 @@
|
||||
# Agent Manager — New Worktree Project Selector
|
||||
|
||||
Status: implemented 2026-08-06. The implementation is uncommitted.
|
||||
|
||||
This is Slice 4 item 6 ("Add project-aware New Worktree targeting") from
|
||||
`agent-manager-multi-project-uniform-ui.md`, the last unimplemented item of that slice.
|
||||
`agent-manager-multi-project-runtime.md:29` deferred it out of the backend-first scope.
|
||||
Everything the extension side needs already exists; this is almost entirely a webview
|
||||
change.
|
||||
|
||||
Implementation notes:
|
||||
|
||||
- The project catalog is passed into the dialog as an accessor so the picker reflects
|
||||
registry changes while it remains open.
|
||||
- The per-project default-base resolver returns `undefined` when the project has no
|
||||
configured/local branch, allowing the backend-detected branch response to remain the
|
||||
fallback instead of being replaced by a hardcoded `main`.
|
||||
- Branch, import-result, and worktree-ready messages carry `projectId` in multi-project
|
||||
mode, which makes fast project changes and cross-project creation activation safe.
|
||||
- The slash-command hook accepts optional caller-owned commands; `/project` is scoped to
|
||||
this dialog and is hidden when multi-project mode is unavailable.
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
With `kilo-code.new.experimental.multiProject` enabled, the New Worktree dialog has no
|
||||
notion of which repository it targets, and the user cannot see or change it.
|
||||
|
||||
`Cmd+N` opens the dialog with no project at all:
|
||||
|
||||
```tsx
|
||||
// AgentManagerApp.tsx:1870-1876
|
||||
const showNewWorktreeDialog = () => {
|
||||
if (!loaded()) return
|
||||
expandSidebar()
|
||||
dialog.show(() => (
|
||||
<NewWorktreeDialog mode={mode} onClose={() => dialog.close()} defaultBaseBranch={repoDefaultBranch()} />
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
`projectId` is `undefined`, so every message the dialog sends omits it
|
||||
(`agentManager.requestBranches` at `NewWorktreeDialog.tsx:321`,
|
||||
`agentManager.createMultiVersion` at `:373`, `agentManager.importFromPR` at `:566`,
|
||||
`agentManager.importFromBranch` at `:575`). The extension then silently falls back to the
|
||||
active project in `messageProject()` (`AgentManagerProvider.ts:474`) before running the
|
||||
message inside `ProjectScope`.
|
||||
|
||||
The result is correct but opaque:
|
||||
|
||||
- The dialog never shows which repository the worktree lands in.
|
||||
- The only way to target a specific project is the per-project `+` button
|
||||
(`ProjectList.tsx:136-146`), which does pass `projectId` explicitly.
|
||||
- `defaultBaseBranch` is resolved from the *active* project
|
||||
(`AgentManagerApp.tsx:261,272`), so even Advanced options' base-branch list and default
|
||||
badge are implicitly single-project.
|
||||
|
||||
The desired behavior, per the original request: the dialog should show the assigned
|
||||
project and let the user change it. Defaulting to the last selected project is fine as a
|
||||
default; it just must be visible and overridable.
|
||||
|
||||
---
|
||||
|
||||
## Design decisions
|
||||
|
||||
### Placement: inline with the tab switcher
|
||||
|
||||
The selector renders inside the New/Import pill row (`NewWorktreeDialog.tsx:620-699`),
|
||||
right-aligned with a constrained width, so it is shared by both tabs without adding a
|
||||
full-width form row.
|
||||
|
||||
```
|
||||
┌─ New Worktree ──────────────────────────────┐
|
||||
│ [ New ] [ Import ] [ folder kilocode ▾ ] │ ← inline, multiProject only
|
||||
│ [ Worktree name (optional) ] │
|
||||
│ [ prompt … ] Code ▾ GPT-5.6 ▾ None ▾ │
|
||||
│ › Advanced options │
|
||||
│ VERSIONS 1 2 3 4 ⧉ Compare Models │
|
||||
│ [ Create Worktree ] │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Rejected alternatives and why:
|
||||
|
||||
- **Inside Advanced options.** Wrong category. Advanced options holds refinements of a
|
||||
known target (branch name, base branch). The project *is* the target: changing it
|
||||
invalidates the branch list, base branch, default-branch badge, and setup scripts.
|
||||
Hiding it also fails the stated requirement of seeing which project is assigned.
|
||||
- **A full-width row directly beneath the tabs.** It covered both tabs, but consumed
|
||||
unnecessary vertical space and made the project control look like a primary form field.
|
||||
- **In the dialog title** (`New Worktree in [kilocode ▾]`). Reads nicely but requires
|
||||
widening `Dialog`'s `title` prop to accept JSX, which touches kilo-ui for one caller.
|
||||
|
||||
### Visibility
|
||||
|
||||
Render the row only when `multiProject` is true. With the flag off (the default) the
|
||||
dialog stays byte-identical to today, so there is no regression surface for the
|
||||
all-user path. When the flag is on but only the pinned project exists, still render it:
|
||||
showing the target is informative and the requirement is explicitly about seeing the
|
||||
assignment.
|
||||
|
||||
### Default value
|
||||
|
||||
`props.projectId ?? activeProjectId()`. `activeProjectId()` already exists at
|
||||
`AgentManagerApp.tsx:268` (`projectList().find((p) => p.active)?.id ?? currentProjectId()`).
|
||||
|
||||
No new persistence. The active project is already the durable "last selected" state
|
||||
(persisted per project as `activeTarget` in each repo's `.kilo/agent-manager.json`, plus
|
||||
the registry's ordering). Adding a separate "last dialog project" key would create a
|
||||
second source of truth that can disagree with the sidebar.
|
||||
|
||||
### Reuse, no new CSS
|
||||
|
||||
The row uses the existing `am-advanced-field` + `am-nv-config-label` +
|
||||
`am-selector-wrapper` + `am-selector-trigger` markup, i.e. exactly the structure the base
|
||||
branch selector already uses at `NewWorktreeDialog.tsx:813-905`, with `DeferredPopover`
|
||||
(already imported) instead of `BranchSelectPopover`.
|
||||
|
||||
### Component extraction
|
||||
|
||||
`NewWorktreeDialog.tsx` is already 1136 lines. The selector and its popover list go into a
|
||||
new `webview-ui/agent-manager/ProjectSelect.tsx` (roughly `BranchSelect.tsx`'s role):
|
||||
presentational, takes `projects`, `value`, `onSelect`, and labels, and owns nothing but
|
||||
its own list rendering. The dialog keeps only the signal, the popover trigger, and the
|
||||
effects.
|
||||
|
||||
Note: `webview-ui/agent-manager/NewWorktreeDialog.tsx` is not under a `maxLines` cap
|
||||
(`tests/unit/agent-manager-arch.test.ts` caps `src/agent-manager/*.ts` only), but the file
|
||||
is on the arch test's watched list and the caps exist to discourage exactly this kind of
|
||||
growth.
|
||||
|
||||
---
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. `webview-ui/agent-manager/ProjectSelect.tsx` (new)
|
||||
|
||||
Presentational popover body listing projects:
|
||||
|
||||
- Row = project label + dimmed root path (tooltip on the full root, matching
|
||||
`ProjectsSection.tsx:60`).
|
||||
- Check mark on the selected project.
|
||||
- Untrusted and missing projects are disabled and carry the same affordances the accordion
|
||||
uses: `lock` icon + trust hint, `warning` icon + missing hint
|
||||
(`ProjectsSection.tsx:67-75`). Selecting them is not possible; trust happens in the
|
||||
sidebar, not in this dialog. Keeps the dialog free of trust-flow branching.
|
||||
- There is intentionally no Add project action in this picker. Project registration and
|
||||
trust management stay in the Agent Manager Projects toolbar.
|
||||
|
||||
### 2. `webview-ui/agent-manager/NewWorktreeDialog.tsx`
|
||||
|
||||
Props change:
|
||||
|
||||
```ts
|
||||
export const NewWorktreeDialog: Component<{
|
||||
onClose: () => void
|
||||
projectId?: string // now: initial value, not fixed target
|
||||
projects?: AgentProjectSnapshot[] // omitted / empty => single-project, row hidden
|
||||
activeProjectId?: string
|
||||
defaultBase?: (projectId: string) => string // replaces defaultBaseBranch?: string
|
||||
mode: ModeRouter
|
||||
}>
|
||||
```
|
||||
|
||||
`defaultBaseBranch?: string` must become a per-project lookup because each project has its
|
||||
own configured default and its own local branch. `ProjectList.tsx:142` already computes
|
||||
that expression (`state?.defaultBaseBranch ?? props.local[projectId]?.branch`); hoist it
|
||||
into the callback so both call sites share it.
|
||||
|
||||
New state and derived values:
|
||||
|
||||
```ts
|
||||
const [project, setProject] = createSignal(props.projectId ?? props.activeProjectId)
|
||||
const [projectOpen, setProjectOpen] = createSignal(false)
|
||||
const selectable = () => (props.projects ?? []).filter((p) => p.trusted && !p.missing)
|
||||
const showProject = () => (props.projects?.length ?? 0) > 0
|
||||
```
|
||||
|
||||
Every outbound message switches from `props.projectId` to `project()`:
|
||||
`:321` `requestBranches`, `:373` `createMultiVersion`, `:566` `importFromPR`,
|
||||
`:575` `importFromBranch`.
|
||||
|
||||
Reload branch data on project change, replacing the one-shot `onMount` request at `:319-321`:
|
||||
|
||||
```ts
|
||||
createEffect(
|
||||
on(project, (id) => {
|
||||
setBranches([])
|
||||
setBranchSearch("")
|
||||
setHighlightedIndex(0)
|
||||
setBaseBranch(null) // custom base is project-specific
|
||||
setDefaultBranch(props.defaultBase?.(id) ?? "main")
|
||||
setBranchesLoading(true)
|
||||
vscode.postMessage({ type: "agentManager.requestBranches", projectId: id })
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
Drop stale branch replies in the `agentManager.branches` handler (`:520-525`).
|
||||
`AgentManagerBranchesMessage` **already declares an optional `projectId`**
|
||||
(`extension-messages.ts:939-945`, `src/agent-manager/types.ts:293-298`); the field is
|
||||
simply never populated or read today. Without this guard, switching projects twice quickly
|
||||
can race a wrong branch list into the base-branch popover:
|
||||
|
||||
```ts
|
||||
if (ev.projectId && ev.projectId !== project()) return
|
||||
```
|
||||
|
||||
Also replace the `if (!props.defaultBaseBranch) setDefaultBranch(ev.defaultBranch)` guard
|
||||
at `:523` — with a per-project lookup, the guard must consult
|
||||
`props.defaultBase?.(project())` instead of a fixed prop.
|
||||
|
||||
Preserved across a project change (all project-agnostic): prompt text and its
|
||||
`advancedDialogPrompt` persistence, images, name, agent, model, variant, versions, compare
|
||||
allocations, sandbox override.
|
||||
|
||||
Keyboard: add `project` to `WORKTREE_PROMPT_COMMANDS` so `/project` opens the popover,
|
||||
consistent with mode/model/variant/sandbox already being reachable from the dialog's slash
|
||||
menu (`:302-314`). Hide it from the list when `showProject()` is false, using the same
|
||||
`hidden` set mechanism already used for `agents` / `variant` / `sandbox`.
|
||||
|
||||
### 3. `webview-ui/agent-manager/AgentManagerApp.tsx`
|
||||
|
||||
`showNewWorktreeDialog` (`:1870-1876`) passes the catalog and a per-project default
|
||||
resolver instead of a single branch string:
|
||||
|
||||
```tsx
|
||||
<NewWorktreeDialog
|
||||
mode={mode}
|
||||
onClose={() => dialog.close()}
|
||||
projects={multiProject() ? projectList() : undefined}
|
||||
activeProjectId={activeProjectId()}
|
||||
defaultBase={defaultBase}
|
||||
/>
|
||||
```
|
||||
|
||||
where `defaultBase(id)` reads `registry.ensure(id).defaultBaseBranch() ?? registry.ensure(id).localStats()?.branch ?? repoDetectedBranch() ?? "main"`.
|
||||
`registry.ensure` and both store fields already exist
|
||||
(`project/registry.ts:39`, `project/store.ts:62,67,113,123`).
|
||||
|
||||
### 4. `webview-ui/agent-manager/ProjectList.tsx`
|
||||
|
||||
`newWorktree(projectId)` (`:136-146`) passes the same `projects` / `activeProjectId` /
|
||||
`defaultBase` props with `projectId` as the initial value, so the per-project `+` button
|
||||
opens the dialog pre-scoped but still switchable. Its current inline
|
||||
`state?.defaultBaseBranch ?? props.local[projectId]?.branch` expression is replaced by the
|
||||
shared resolver passed down from `AgentManagerApp`.
|
||||
|
||||
### 5. `src/agent-manager/worktree-importer.ts`
|
||||
|
||||
Stamp `projectId` on all three `agentManager.branches` posts (`:27`, `:48`, `:54`). The
|
||||
field is already in the type; the value is available from the ambient `ProjectScope`
|
||||
context the message runs in (`AgentManagerProvider.ts:479`). Without this the stale-reply
|
||||
guard in the webview is inert.
|
||||
|
||||
### 6. Activate the created worktree when the project differs
|
||||
|
||||
Creating in a non-active project currently leaves the sidebar where it is:
|
||||
`createMultiVersion` never activates (no activation call in `provider-multi-version.ts`),
|
||||
and the new worktree just appears in that project's accordion. That is right for the
|
||||
per-project `+` button, but for `Cmd+N` where the user deliberately switched projects,
|
||||
landing in the new worktree is what the flow implies.
|
||||
|
||||
Post an `agentManager.activateSelection` for the first created worktree when the chosen
|
||||
project differs from the active one. `activateSelection` already handles readiness, trust,
|
||||
and stale-target fallback (`project/messages.ts:76-99`), so this is one message, not new
|
||||
machinery. Hook it to the existing `agentManager.worktreeSetup` / `multiVersionProgress`
|
||||
handling in `AgentManagerApp.tsx:1453-1471`, which already carries `projectId`.
|
||||
|
||||
### 7. i18n
|
||||
|
||||
New keys in `webview-ui/agent-manager/i18n/en.ts` (near the existing
|
||||
`agentManager.dialog.*` block at `:130`):
|
||||
|
||||
- `agentManager.dialog.project.select` — "Select project"
|
||||
- `agentManager.dialog.project.untrusted` — "Trust this project in the sidebar first"
|
||||
- `agentManager.dialog.project.missing` — "Repository not found"
|
||||
|
||||
Then translate the four keys into the other 20 locale files in that directory via the
|
||||
`translator` subagent.
|
||||
|
||||
---
|
||||
|
||||
## Implementation order
|
||||
|
||||
1. `ProjectSelect.tsx` with the presentational list, plus i18n keys in `en.ts`.
|
||||
2. Dialog: `project` signal, prop rename to `defaultBase`, route all four outbound
|
||||
messages through `project()`, render the row behind `showProject()`.
|
||||
3. Dialog: `createEffect(on(project, …))` branch reload, base-branch reset, stale-reply
|
||||
guard.
|
||||
4. Call-site updates in `AgentManagerApp.tsx` and `ProjectList.tsx`, shared `defaultBase`
|
||||
resolver.
|
||||
5. `projectId` stamp in `worktree-importer.ts`.
|
||||
6. `/project` slash command.
|
||||
7. Post-create activation when the target project differs.
|
||||
8. Locale fan-out.
|
||||
9. Changeset (`minor`, user-facing): worktree creation targets an explicit project.
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
Existing source-text unit tests already assert against this dialog and will need to stay
|
||||
green: `tests/unit/new-worktree-dialog-sandbox.test.ts`,
|
||||
`tests/unit/prompt-input-bidirectional.test.ts`, and the dialog entry in
|
||||
`tests/unit/agent-manager-arch.test.ts`.
|
||||
|
||||
New coverage:
|
||||
|
||||
- The dialog posts `createMultiVersion` / `requestBranches` / `importFromBranch` /
|
||||
`importFromPR` with the *selected* project id, not the prop, after a project change.
|
||||
- A `agentManager.branches` reply carrying a non-current `projectId` does not mutate the
|
||||
branch list (the race guard).
|
||||
- Changing project clears `baseBranch` and re-derives `defaultBranch` from `defaultBase`.
|
||||
- Prompt text survives a project change (no accidental reset through the shared
|
||||
`advancedDialogPrompt` cache).
|
||||
- The row does not render when `projects` is empty, so the single-project dialog is
|
||||
unchanged.
|
||||
- Untrusted and missing projects are not selectable.
|
||||
|
||||
Checks to run before declaring done, from `packages/kilo-vscode/`:
|
||||
`bun run typecheck`, `bun run lint`, `bun run test:unit`, `bun run knip`.
|
||||
|
||||
---
|
||||
|
||||
## Manual verification
|
||||
|
||||
In the isolated harness (`bun run extension:isolated`) with
|
||||
`kilo-code.new.experimental.multiProject` enabled and two repositories registered:
|
||||
|
||||
1. `Cmd+N` from project A shows "Project: A". Switch to B, create, and confirm the
|
||||
worktree lands in B's accordion and the sidebar activates it.
|
||||
2. Switch project with Advanced options open and confirm the base-branch list and default
|
||||
badge follow the new project rather than showing A's branches.
|
||||
3. Switch project rapidly back and forth and confirm the branch list matches the selected
|
||||
project (the race guard).
|
||||
4. Type a prompt, switch project, confirm the prompt is retained.
|
||||
5. Use the Import tab after switching project and confirm branches and PR import target
|
||||
the selected repository.
|
||||
6. Turn the flag off and confirm the dialog is visually identical to today.
|
||||
|
||||
---
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Trusting or removing a project from inside the dialog. Trust stays in the sidebar; the
|
||||
dialog only disables untrusted entries.
|
||||
- Any change to how the active project is persisted.
|
||||
- The quick-create path (`Cmd+Shift+N` → `agentManager.createWorktree`,
|
||||
`AgentManagerApp.tsx:1863-1867`). It has no dialog, so it keeps targeting the active
|
||||
project. Worth revisiting only if the explicit-target rule should apply there too.
|
||||
- Per-project setup-script or agent selection in the dialog.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Stale branch data** is the only real correctness risk, and it is why the `projectId`
|
||||
stamp plus the reply guard are mandatory rather than optional polish.
|
||||
- **Prop signature change** (`defaultBaseBranch: string` → `defaultBase: (id) => string`)
|
||||
touches both call sites; a partial migration would silently show one project's default
|
||||
branch while creating in another.
|
||||
- **Dialog file growth**; mitigated by extracting `ProjectSelect.tsx`.
|
||||
|
||||
---
|
||||
|
||||
# Appendix: exact UI and styling specification
|
||||
|
||||
Everything below is copy-paste ready. Class names, tokens, and icon names are all verified
|
||||
against the current tree. Do not invent new tokens or new class names beyond the ones
|
||||
listed here.
|
||||
|
||||
## A. Visual layout
|
||||
|
||||
```
|
||||
┌─ New Worktree ─────────────────────────────────────────── X ─┐
|
||||
│ │
|
||||
│ [ New ][ Import ] [ 📁 kilocode ⌃⌄ ] │
|
||||
│ └────────────────────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ Worktree name (optional) │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ prompt … │ │
|
||||
│ │ Code ▾ OpenAI / GPT-5.6 ▾ None ▾ ✨ 🔒 🎤 │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
│ › Advanced options │
|
||||
│ VERSIONS [1][2][3][4] [⧉ Compare Models] │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ Create Worktree │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Open dropdown (anchored under the trigger, same width as the trigger):
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────┐
|
||||
│ 📁 kilocode ~/Documents/git/kilocode ✓│ ← .am-project-option-active
|
||||
│ 📁 cloud ~/Documents/git/cloud │
|
||||
│ 🔒 sample-app ~/dev/sample-app │ ← disabled, 50% opacity
|
||||
│ ⚠ old-repo ~/dev/old-repo │ ← disabled, 50% opacity
|
||||
└────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- The selector is inline with the New/Import buttons inside the tab-switcher flex row, so
|
||||
it applies to New and Import alike.
|
||||
- The project name and folder icon identify the scope without a separate visible label.
|
||||
- The trigger is the same control as the Advanced options base-branch trigger
|
||||
(`.am-selector-trigger`), so the dialog has one visual language for "pick a thing".
|
||||
- The row is **not rendered at all** when `props.projects` is empty or undefined. That is
|
||||
the single-project / flag-off case, which must stay pixel-identical to today.
|
||||
|
||||
## B. Exact icon names
|
||||
|
||||
Only these, from `packages/ui/src/components/icon.tsx`:
|
||||
|
||||
| Where | `Icon name` | Notes |
|
||||
|---|---|---|
|
||||
| Trigger left | `folder` | Always, regardless of project state. |
|
||||
| Trigger right | `selector` | Same as every other `.am-selector-trigger`. |
|
||||
| Option row, normal | `folder` | |
|
||||
| Option row, untrusted | `lock` | Matches the sidebar accordion affordance. |
|
||||
| Option row, missing | `warning` | Matches the sidebar accordion affordance. |
|
||||
| Option row, selected | `check-small` | Right-aligned. |
|
||||
|
||||
All at `size="small"`. Do not use `folder-add-left`, `check`, or `plus`.
|
||||
|
||||
## C. New file: `webview-ui/agent-manager/ProjectSelect.tsx`
|
||||
|
||||
```tsx
|
||||
// Project picker list for the New Worktree dialog
|
||||
|
||||
/** @jsxImportSource solid-js */
|
||||
|
||||
import { For, Show, type Component } from "solid-js"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import type { AgentProjectSnapshot } from "../src/types/messages"
|
||||
|
||||
interface ProjectSelectProps {
|
||||
projects: AgentProjectSnapshot[]
|
||||
selected?: string
|
||||
onSelect: (id: string) => void
|
||||
labels: { untrusted: string; missing: string }
|
||||
}
|
||||
|
||||
export const ProjectSelect: Component<ProjectSelectProps> = (props) => (
|
||||
<div class="am-dropdown-list">
|
||||
<For each={props.projects}>
|
||||
{(project) => {
|
||||
const blocked = () => !project.trusted || project.missing
|
||||
const hint = () => {
|
||||
if (project.missing) return props.labels.missing
|
||||
if (!project.trusted) return props.labels.untrusted
|
||||
return project.root
|
||||
}
|
||||
const icon = () => {
|
||||
if (project.missing) return "warning" as const
|
||||
if (!project.trusted) return "lock" as const
|
||||
return "folder" as const
|
||||
}
|
||||
return (
|
||||
<button
|
||||
class="am-project-option"
|
||||
classList={{ "am-project-option-active": props.selected === project.id }}
|
||||
disabled={blocked()}
|
||||
title={hint()}
|
||||
onClick={() => props.onSelect(project.id)}
|
||||
type="button"
|
||||
>
|
||||
<span class="am-project-option-left">
|
||||
<Icon name={icon()} size="small" />
|
||||
<span class="am-project-option-name">{project.label}</span>
|
||||
<span class="am-project-option-root">{project.root}</span>
|
||||
</span>
|
||||
<Show when={props.selected === project.id}>
|
||||
<Icon name="check-small" size="small" />
|
||||
</Show>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
Notes for the implementer:
|
||||
|
||||
- Wrap in `.am-dropdown-list`, not a bare fragment: that class supplies the scroll cap and
|
||||
4px padding, and `.am-dropdown [data-slot="popover-body"]` zeroes the popover padding.
|
||||
- No search input. A project list is short; adding one would need keyboard nav plumbing for
|
||||
no benefit.
|
||||
- `props.labels` is passed in rather than calling `useLanguage()` here, matching how
|
||||
`BranchSelect` and `SidebarSearchMenu` take label props.
|
||||
|
||||
## D. Exact JSX inserted into `NewWorktreeDialog.tsx`
|
||||
|
||||
### D.1 Imports
|
||||
|
||||
Add to the existing type import block at lines 6-12:
|
||||
|
||||
```ts
|
||||
AgentProjectSnapshot,
|
||||
```
|
||||
|
||||
Add after line 48 (`import { BranchSelect, BranchSelectPopover } …`):
|
||||
|
||||
```ts
|
||||
import { ProjectSelect } from "./ProjectSelect"
|
||||
```
|
||||
|
||||
`Icon`, `Show`, `DeferredPopover`, `createSignal`, `createEffect` are already imported.
|
||||
`on` from `solid-js` must be added to the line 5 import list.
|
||||
|
||||
### D.2 Props
|
||||
|
||||
Replace the component signature at lines 84-89 with:
|
||||
|
||||
```tsx
|
||||
export const NewWorktreeDialog: Component<{
|
||||
onClose: () => void
|
||||
/** Resolves the default base branch for one project. */
|
||||
defaultBase?: (projectId: string) => string | undefined
|
||||
/** Initial target project. The user can change it while the dialog is open. */
|
||||
projectId?: string
|
||||
/** Full project catalog. Empty or undefined hides the project row entirely. */
|
||||
projects?: () => AgentProjectSnapshot[]
|
||||
/** Project the sidebar currently has active; used as the default target. */
|
||||
activeProjectId?: string
|
||||
mode: ModeRouter
|
||||
}> = (props) => {
|
||||
```
|
||||
|
||||
### D.3 State
|
||||
|
||||
Immediately after line 101 (`const [tab, setTab] = createSignal<DialogTab>("new")`):
|
||||
|
||||
```tsx
|
||||
const [project, setProject] = createSignal(props.projectId ?? props.activeProjectId)
|
||||
const [projectOpen, setProjectOpen] = createSignal(false)
|
||||
const projects = () => props.projects?.() ?? []
|
||||
const showProject = () => projects().length > 0
|
||||
const projectLabel = () => projects().find((p) => p.id === project())?.label ?? ""
|
||||
```
|
||||
|
||||
`defaultBranch` (line 106) changes from `props.defaultBaseBranch ?? "main"` to:
|
||||
|
||||
```tsx
|
||||
const [defaultBranch, setDefaultBranch] = createSignal(
|
||||
(project() && props.defaultBase?.(project()!)) || "main",
|
||||
)
|
||||
```
|
||||
|
||||
### D.4 The inline selector
|
||||
|
||||
Insert inside the tab switcher after the Import button:
|
||||
|
||||
```tsx
|
||||
{/* Project scope — applies to both tabs. Hidden unless multi-project is on. */}
|
||||
<Show when={showProject()}>
|
||||
<div class="am-nv-project-inline">
|
||||
<div class="am-selector-wrapper">
|
||||
<DeferredPopover
|
||||
open={projectOpen()}
|
||||
onOpenChange={setProjectOpen}
|
||||
placement="bottom-start"
|
||||
flip={false}
|
||||
sameWidth
|
||||
portal={false}
|
||||
deferDismiss
|
||||
class="am-dropdown"
|
||||
trigger={
|
||||
<button class="am-selector-trigger" type="button" disabled={starting() || isPending()}>
|
||||
<span class="am-selector-left">
|
||||
<Icon name="folder" size="small" />
|
||||
<Show
|
||||
when={projectLabel()}
|
||||
fallback={
|
||||
<span class="am-selector-value am-selector-placeholder">
|
||||
{t("agentManager.dialog.project.select")}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<span class="am-selector-value">{projectLabel()}</span>
|
||||
</Show>
|
||||
</span>
|
||||
<span class="am-selector-right">
|
||||
<Icon name="selector" size="small" />
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<ProjectSelect
|
||||
projects={projects()}
|
||||
selected={project()}
|
||||
onSelect={(id) => {
|
||||
track("project_select", { changed: id !== props.activeProjectId })
|
||||
setProject(id)
|
||||
setProjectOpen(false)
|
||||
}}
|
||||
labels={{
|
||||
untrusted: t("agentManager.dialog.project.untrusted"),
|
||||
missing: t("agentManager.dialog.project.missing"),
|
||||
}}
|
||||
/>
|
||||
</DeferredPopover>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
```
|
||||
|
||||
Critical details, in order of how easily they get wrong:
|
||||
|
||||
1. `placement="bottom-start"`, **not** `top-start`. The rest of this dialog uses
|
||||
`top-start` because those triggers sit near the bottom of the panel. This one sits at
|
||||
the top, so it must open downward.
|
||||
2. `portal={false}` plus the escape CSS in section E.3. Do not switch to a portal unless
|
||||
the clipping fallback in E.3 is needed.
|
||||
3. `sameWidth` so the dropdown matches the trigger width, consistent with the base-branch
|
||||
and compare-models popovers.
|
||||
4. Never send the project label, root, or id as a telemetry property. `track` takes only
|
||||
the boolean shown above.
|
||||
|
||||
### D.5 Reactive reload on project change
|
||||
|
||||
Delete the one-shot request at lines 319-321 inside `onMount` and replace it with an effect
|
||||
placed next to the other `createEffect` calls:
|
||||
|
||||
```tsx
|
||||
// Project scope owns the branch data, the base branch, and the default badge.
|
||||
// Prompt, name, model, agent, versions and attachments are project-agnostic and survive.
|
||||
createEffect(
|
||||
on(project, (id) => {
|
||||
if (!id) return
|
||||
setBranches([])
|
||||
setBranchSearch("")
|
||||
setHighlightedIndex(0)
|
||||
setBaseBranch(null)
|
||||
setDefaultBranch(props.defaultBase?.(id) ?? "main")
|
||||
setBranchesLoading(true)
|
||||
vscode.postMessage({ type: "agentManager.requestBranches", projectId: id })
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
`on(project, …)` without `{ defer: true }` runs immediately, which replaces the removed
|
||||
`onMount` request. Keep the textarea focus logic in `onMount` untouched.
|
||||
|
||||
In the `agentManager.branches` handler (lines 520-525), replace the body with:
|
||||
|
||||
```tsx
|
||||
if (msg.type === "agentManager.branches") {
|
||||
const ev = msg as AgentManagerBranchesMessage
|
||||
if (ev.projectId && ev.projectId !== project()) return
|
||||
setBranches(ev.branches)
|
||||
const id = project()
|
||||
if (!id || !props.defaultBase?.(id)) setDefaultBranch(ev.defaultBranch)
|
||||
setBranchesLoading(false)
|
||||
}
|
||||
```
|
||||
|
||||
### D.6 Outbound project id
|
||||
|
||||
Four call sites change from `props.projectId` to `project()`:
|
||||
|
||||
| Line | Message |
|
||||
|---|---|
|
||||
| 321 (now inside the effect) | `agentManager.requestBranches` |
|
||||
| 373 | `agentManager.createMultiVersion` |
|
||||
| 566 | `agentManager.importFromPR` |
|
||||
| 575 | `agentManager.importFromBranch` |
|
||||
|
||||
Grep afterwards: `props.projectId` must appear exactly once in the file, in the `project`
|
||||
signal initializer.
|
||||
|
||||
## E. Exact CSS
|
||||
|
||||
All of it goes into `webview-ui/agent-manager/agent-manager.css`. No changes to kilo-ui.
|
||||
|
||||
### E.1 The inline selector
|
||||
|
||||
Insert directly after the `.am-tab-switcher-pill-active` rule (agent-manager.css:3614-3617),
|
||||
before the `/* Import tab layout */` comment at line 3619:
|
||||
|
||||
```css
|
||||
/* Project scope selector — inline with the New/Import tabs */
|
||||
|
||||
.am-nv-project-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
flex: 0 1 260px;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.am-nv-project-inline .am-selector-wrapper {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
```
|
||||
|
||||
The `flex: 0 1 260px` cap keeps the project control compact while allowing long project
|
||||
names to truncate. `margin-left: auto` keeps it aligned to the right of the New/Import
|
||||
buttons.
|
||||
|
||||
### E.2 The dropdown rows
|
||||
|
||||
Insert after the `.am-dropdown-empty` rule (agent-manager.css:3863-3868), before the
|
||||
`/* Import empty state */` comment at line 3870:
|
||||
|
||||
```css
|
||||
/* Project option rows in the New Worktree project dropdown.
|
||||
Deliberately distinct from .am-project-item, which styles the sidebar accordion. */
|
||||
|
||||
.am-project-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: none;
|
||||
color: var(--text-base);
|
||||
font-size: var(--font-size-base);
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.am-project-option:hover:not(:disabled) {
|
||||
background: var(--surface-inset-base-hover);
|
||||
}
|
||||
|
||||
.am-project-option-active {
|
||||
background: var(--surface-inset-base);
|
||||
}
|
||||
|
||||
.am-project-option:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.am-project-option-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.am-project-option-left [data-component="icon"] {
|
||||
color: var(--text-weaker);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.am-project-option-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
max-width: 45%;
|
||||
}
|
||||
|
||||
.am-project-option-root {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
font-size: var(--kilo-font-size-11);
|
||||
color: var(--text-weaker);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Why not reuse `.am-branch-item`: it is defined twice (lines 3211 and 3810) and the earlier
|
||||
definition sets `font-family: var(--font-mono, monospace)` on `.am-branch-item-name`, which
|
||||
would render project labels in monospace. Reusing it also couples project rows to future
|
||||
branch-row changes. `.am-project-item` is likewise off limits: it already styles the
|
||||
sidebar project accordion header (line 325).
|
||||
|
||||
### E.3 Popover clipping escape
|
||||
|
||||
`[data-slot="dialog-body"]` is `overflow: hidden` in `packages/ui/src/components/dialog.css:99-105`,
|
||||
and `[data-slot="dialog-content"]` is `overflow: auto` (line 38). The existing escape rules
|
||||
at agent-manager.css:2822-2828 only match popovers **inside** `.am-nv-dialog`, and this row
|
||||
is deliberately outside it. Without the following, the dropdown is clipped by the dialog.
|
||||
|
||||
Add to that same rule group (extend the existing selector list rather than duplicating the
|
||||
declaration):
|
||||
|
||||
```css
|
||||
[data-component="dialog"]:has(.am-nv-project-inline [data-component="popover-content"]) [data-slot="dialog-content"],
|
||||
[data-component="dialog"]:has(.am-nv-project-inline [data-component="popover-content"]) [data-slot="dialog-body"] {
|
||||
overflow: visible;
|
||||
}
|
||||
```
|
||||
|
||||
Verification step, not optional: open the dropdown with four or more projects registered
|
||||
and confirm no row is cut off and no inner scrollbar appears on the dialog. If it still
|
||||
clips, the documented fallback is to drop `portal={false}` from the `DeferredPopover` in
|
||||
D.4 and delete this rule; the dialog already sets `overflow: visible` on
|
||||
`[data-slot="dialog-content"]` for portal-based dropdowns (agent-manager.css:2755-2760).
|
||||
|
||||
## F. i18n
|
||||
|
||||
Add to `webview-ui/agent-manager/i18n/en.ts`, immediately after
|
||||
`"agentManager.dialog.namePlaceholder"`:
|
||||
|
||||
```ts
|
||||
"agentManager.dialog.project.select": "Select project",
|
||||
"agentManager.dialog.project.untrusted": "Trust this project in the sidebar first",
|
||||
"agentManager.dialog.project.missing": "Repository not found",
|
||||
```
|
||||
|
||||
Then add the same three keys to all 20 sibling locale files in that directory (`ar bs br da
|
||||
de es fa fr it ja ko nl no pl ru th tr uk zh zht`) via the `translator` subagent.
|
||||
|
||||
## G. What must not change
|
||||
|
||||
- No new CSS variables or tokens. Only the ones listed above, all already in use in this
|
||||
file.
|
||||
- No edits to `packages/kilo-ui/` or `packages/ui/`.
|
||||
- No change to `.am-project-item`, `.am-branch-item`, `.am-selector-trigger`,
|
||||
`.am-nv-config-label`, or any other existing rule. The only existing rule touched is the
|
||||
`overflow: visible` selector group in E.3, and only by adding selectors to it.
|
||||
- No new message types. `agentManager.requestBranches`, `agentManager.createMultiVersion`,
|
||||
`agentManager.importFromBranch`, and `agentManager.importFromPR` all already exist and
|
||||
already accept what is needed.
|
||||
- With `props.projects` empty, the rendered dialog markup must be identical to before the
|
||||
change. Verify by toggling `kilo-code.new.experimental.multiProject` off.
|
||||
@@ -345,7 +345,7 @@
|
||||
},
|
||||
"packages/kilo-jetbrains": {
|
||||
"name": "@kilocode/kilo-jetbrains",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.20",
|
||||
},
|
||||
"packages/kilo-memory": {
|
||||
"name": "@kilocode/kilo-memory",
|
||||
@@ -444,6 +444,7 @@
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@thisbeyond/solid-dnd": "0.7.5",
|
||||
"@vscode/codicons": "^0.0.44",
|
||||
"@xterm/addon-clipboard": "0.2.0",
|
||||
"@xterm/addon-fit": "0.11.0",
|
||||
"@xterm/addon-unicode-graphemes": "0.4.0",
|
||||
@@ -469,6 +470,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "4.11.3",
|
||||
"@babel/core": "^7.28.4",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@storybook/addon-a11y": "10.2.10",
|
||||
"@storybook/addon-docs": "10.2.10",
|
||||
@@ -480,6 +483,7 @@
|
||||
"@vscode/test-cli": "^0.0.12",
|
||||
"@vscode/test-electron": "^2.5.2",
|
||||
"@vscode/vsce": "^3.7.1",
|
||||
"babel-preset-solid": "^1.9.9",
|
||||
"esbuild": "^0.27.2",
|
||||
"esbuild-plugin-solid": "^0.6.0",
|
||||
"eslint": "^9.39.2",
|
||||
@@ -983,23 +987,23 @@
|
||||
},
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"esbuild",
|
||||
"protobufjs",
|
||||
"web-tree-sitter",
|
||||
"esbuild",
|
||||
"tree-sitter-bash",
|
||||
"protobufjs",
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
|
||||
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch",
|
||||
"@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch",
|
||||
"pacote@21.5.1": "patches/pacote@21.5.1.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||
"pacote@21.5.1": "patches/pacote@21.5.1.patch",
|
||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
|
||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch",
|
||||
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@effect/platform-node-shared": "4.0.0-beta.74",
|
||||
@@ -5442,10 +5446,6 @@
|
||||
|
||||
"@solid-primitives/resize-observer/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="],
|
||||
|
||||
"@standard-community/standard-json/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="],
|
||||
|
||||
"@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="],
|
||||
|
||||
"@storybook/addon-links/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="],
|
||||
|
||||
"@storybook/addon-onboarding/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="],
|
||||
@@ -6152,14 +6152,6 @@
|
||||
|
||||
"@smithy/util-stream/@smithy/core/@smithy/types": ["@smithy/types@4.14.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ=="],
|
||||
|
||||
"@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@standard-community/standard-json/effect/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||
|
||||
"@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@standard-community/standard-openapi/effect/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||
|
||||
"@storybook/addon-links/storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
|
||||
|
||||
"@storybook/addon-links/storybook/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-XCtBeP2R+Tx2S4LEXn1RRZWoquOUdIFB+8O58n2krHI=",
|
||||
"aarch64-linux": "sha256-gnf+k+mI7JaqoCrVzFeEVBmKELUfZ9D6XpqzlBXKYzI=",
|
||||
"aarch64-darwin": "sha256-j9nQgeurmHs00dGOIuxGoV1W/tQ/DJnWrfllr66y9yg=",
|
||||
"x86_64-darwin": "sha256-+arUGf3HZvSRumkfX082dM0wErzzDexOz3zZcVDMlSI="
|
||||
"x86_64-linux": "sha256-y6PZR6BsVZcsK/qJ0XvJBevukYzVJ8NRLHRbNSuSzb8=",
|
||||
"aarch64-linux": "sha256-0ozGLgGTHajlHqofixtjJv/dggq25MhWYIqmfmb+6Z0=",
|
||||
"aarch64-darwin": "sha256-IEYJothLBDT20BmLamBqM+pciFMQtWy8Um1YjFjbKz4=",
|
||||
"x86_64-darwin": "sha256-k2bQGKTkvIZmB2+I2Fy3TPm/NZ0BbGjxp9esxtGuUbI="
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -6,10 +6,11 @@
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.14",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=node src/index.ts",
|
||||
"dev": "KILO_CLIENT=cli bun run --cwd packages/opencode --conditions=node src/index.ts",
|
||||
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
"lint": "oxlint",
|
||||
"check:architecture": "bun run script/check-architecture.ts",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
||||
"postinstall": "bun run --cwd packages/core fix-node-pty && bun run script/setup-git.ts",
|
||||
|
||||
@@ -531,6 +531,7 @@ type Endpoint14_1Input = {
|
||||
readonly cwd?: Endpoint14_1Request["payload"]["cwd"]
|
||||
readonly title?: Endpoint14_1Request["payload"]["title"]
|
||||
readonly env?: Endpoint14_1Request["payload"]["env"]
|
||||
readonly size?: Endpoint14_1Request["payload"]["size"]
|
||||
}
|
||||
const Endpoint14_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_1Input) =>
|
||||
raw["pty.create"]({
|
||||
@@ -541,6 +542,7 @@ const Endpoint14_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_1Inpu
|
||||
cwd: input?.["cwd"],
|
||||
title: input?.["title"],
|
||||
env: input?.["env"],
|
||||
size: input?.["size"],
|
||||
},
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
|
||||
@@ -840,6 +840,7 @@ export function make(options: ClientOptions) {
|
||||
cwd: input?.["cwd"],
|
||||
title: input?.["title"],
|
||||
env: input?.["env"],
|
||||
size: input?.["size"],
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
|
||||
@@ -2632,6 +2632,7 @@ export type PtysCreateInput = {
|
||||
readonly cwd?: string
|
||||
readonly title?: string
|
||||
readonly env?: { readonly [x: string]: string }
|
||||
readonly size?: { readonly rows: number; readonly cols: number }
|
||||
}["command"]
|
||||
readonly args?: {
|
||||
readonly command?: string
|
||||
@@ -2639,6 +2640,7 @@ export type PtysCreateInput = {
|
||||
readonly cwd?: string
|
||||
readonly title?: string
|
||||
readonly env?: { readonly [x: string]: string }
|
||||
readonly size?: { readonly rows: number; readonly cols: number }
|
||||
}["args"]
|
||||
readonly cwd?: {
|
||||
readonly command?: string
|
||||
@@ -2646,6 +2648,7 @@ export type PtysCreateInput = {
|
||||
readonly cwd?: string
|
||||
readonly title?: string
|
||||
readonly env?: { readonly [x: string]: string }
|
||||
readonly size?: { readonly rows: number; readonly cols: number }
|
||||
}["cwd"]
|
||||
readonly title?: {
|
||||
readonly command?: string
|
||||
@@ -2653,6 +2656,7 @@ export type PtysCreateInput = {
|
||||
readonly cwd?: string
|
||||
readonly title?: string
|
||||
readonly env?: { readonly [x: string]: string }
|
||||
readonly size?: { readonly rows: number; readonly cols: number }
|
||||
}["title"]
|
||||
readonly env?: {
|
||||
readonly command?: string
|
||||
@@ -2660,7 +2664,16 @@ export type PtysCreateInput = {
|
||||
readonly cwd?: string
|
||||
readonly title?: string
|
||||
readonly env?: { readonly [x: string]: string }
|
||||
readonly size?: { readonly rows: number; readonly cols: number }
|
||||
}["env"]
|
||||
readonly size?: {
|
||||
readonly command?: string
|
||||
readonly args?: ReadonlyArray<string>
|
||||
readonly cwd?: string
|
||||
readonly title?: string
|
||||
readonly env?: { readonly [x: string]: string }
|
||||
readonly size?: { readonly rows: number; readonly cols: number }
|
||||
}["size"]
|
||||
}
|
||||
|
||||
export type PtysCreateOutput = {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Global } from "./global"
|
||||
import { DataMigrationTable } from "./data-migration.sql"
|
||||
import path from "path"
|
||||
import { parse as parseKiloAccounts } from "./kilocode/credential-migration"
|
||||
import { isBusy } from "./kilocode/sqlite-error"
|
||||
import { NonNegativeInt } from "./schema"
|
||||
// kilocode_change end
|
||||
|
||||
@@ -170,6 +171,17 @@ export const legacyImportLayer = Layer.effectDiscard(
|
||||
const integration = Integration.ID.make(integrationID.replace(/\/+$/, ""))
|
||||
return [{ integration, value: legacyValue(integration, decoded.value) }]
|
||||
})
|
||||
const migrated = yield* db.select().from(DataMigrationTable).where(eq(DataMigrationTable.name, name)).get()
|
||||
const existing = yield* db.select().from(CredentialTable).orderBy(desc(CredentialTable.time_created)).all()
|
||||
const same = (left: Value, right: Value) => JSON.stringify(left) === JSON.stringify(right)
|
||||
if (
|
||||
migrated &&
|
||||
values.every((item) => {
|
||||
const current = existing.find((row) => row.integration_id === item.integration)
|
||||
return current !== undefined && same(current.value, item.value)
|
||||
})
|
||||
)
|
||||
return
|
||||
yield* db.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
for (const item of values) {
|
||||
@@ -181,7 +193,12 @@ export const legacyImportLayer = Layer.effectDiscard(
|
||||
.orderBy(desc(CredentialTable.time_created)) // kilocode_change - reconcile the active imported account
|
||||
.get()
|
||||
if (current) {
|
||||
yield* tx.update(CredentialTable).set({ value: item.value }).where(eq(CredentialTable.id, current.id)).run()
|
||||
if (!same(current.value, item.value))
|
||||
yield* tx
|
||||
.update(CredentialTable)
|
||||
.set({ value: item.value })
|
||||
.where(eq(CredentialTable.id, current.id))
|
||||
.run()
|
||||
continue
|
||||
}
|
||||
yield* tx.insert(CredentialTable).values({
|
||||
@@ -194,7 +211,15 @@ export const legacyImportLayer = Layer.effectDiscard(
|
||||
yield* tx.insert(DataMigrationTable).values({ name, time_completed: Date.now() }).onConflictDoNothing().run()
|
||||
}),
|
||||
)
|
||||
}).pipe(Effect.orDie),
|
||||
}).pipe(
|
||||
Effect.retry({ while: isBusy, times: 2 }),
|
||||
Effect.catch((error) =>
|
||||
isBusy(error)
|
||||
? Effect.logWarning("legacy credential reconciliation deferred because the database is busy")
|
||||
: Effect.fail(error),
|
||||
),
|
||||
Effect.orDie,
|
||||
),
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NodeFileSystem, NodeSink, NodeStream } from "@effect/platform-node"
|
||||
import * as NodePath from "@effect/platform-node/NodePath"
|
||||
import { prepareCommand as prepareSandbox } from "@kilocode/sandbox" // kilocode_change
|
||||
import { tap as tapStdio, tapped } from "./kilocode/stdio-tap" // kilocode_change - Bun drops buffered stdio on close
|
||||
import * as SpawnExit from "./kilocode/spawn-exit" // kilocode_change
|
||||
import * as SpawnValidation from "./kilocode/spawn-validation" // kilocode_change
|
||||
import { settle } from "./kilocode/exit-code" // kilocode_change - settle signal termination as 128 + signum
|
||||
import * as Deferred from "effect/Deferred"
|
||||
@@ -268,7 +269,11 @@ export const make = Effect.gen(function* () {
|
||||
return { stdout, stderr, all: Stream.merge(stdout, stderr) }
|
||||
}
|
||||
|
||||
const spawn = (command: ChildProcess.StandardCommand, opts: NodeChildProcess.SpawnOptions) =>
|
||||
const spawn = (
|
||||
command: ChildProcess.StandardCommand,
|
||||
opts: NodeChildProcess.SpawnOptions,
|
||||
direct: boolean, // kilocode_change - avoid shadowing settle
|
||||
) =>
|
||||
Effect.callback<readonly [NodeChildProcess.ChildProcess, ExitSignal], PlatformError.PlatformError>((resume) => {
|
||||
const signal = Deferred.makeUnsafe<readonly [code: number | null, signal: NodeJS.Signals | null]>()
|
||||
const proc = launch(command.command, command.args, opts)
|
||||
@@ -280,6 +285,7 @@ export const make = Effect.gen(function* () {
|
||||
})
|
||||
proc.on("exit", (...args) => {
|
||||
exit = args
|
||||
if (direct) Deferred.doneUnsafe(signal, Exit.succeed(args)) // kilocode_change - bounded grep must not await inherited pipes
|
||||
})
|
||||
proc.on("close", (...args) => {
|
||||
if (end) return
|
||||
@@ -326,6 +332,18 @@ export const make = Effect.gen(function* () {
|
||||
return Effect.fail(toPlatformError("kill", new Error("Failed to kill child process"), command))
|
||||
})
|
||||
|
||||
// kilocode_change start - inspect descendants owned by commands that settle on direct exit
|
||||
const groupAlive = (proc: NodeChildProcess.ChildProcess) => {
|
||||
if (process.platform === "win32") return false
|
||||
try {
|
||||
process.kill(-proc.pid!, 0)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
const timeout =
|
||||
(
|
||||
proc: NodeChildProcess.ChildProcess,
|
||||
@@ -370,6 +388,7 @@ export const make = Effect.gen(function* () {
|
||||
switch (command._tag) {
|
||||
case "StandardCommand": {
|
||||
const validation = SpawnValidation.take(command) // kilocode_change - retain target validation through preparation
|
||||
const direct = SpawnExit.take(command) // kilocode_change - opt selected commands into direct-exit settlement
|
||||
const dir = yield* cwd(command.options)
|
||||
// kilocode_change start - prepare agent-scoped commands through the selected sandbox backend
|
||||
const target = yield* prepareSandbox(command, dir, env(command.options))
|
||||
@@ -396,21 +415,33 @@ export const make = Effect.gen(function* () {
|
||||
|
||||
const [proc, signal] = yield* Effect.acquireRelease(
|
||||
// kilocode_change start - spawn the prepared command and options
|
||||
spawn(target, {
|
||||
cwd: dir,
|
||||
env: env(target.options),
|
||||
stdio: stdios(sin, sout, serr, extra),
|
||||
detached: target.options.detached ?? process.platform !== "win32",
|
||||
shell: target.options.shell,
|
||||
// kilocode_change end
|
||||
windowsHide: process.platform === "win32",
|
||||
}),
|
||||
spawn(
|
||||
target,
|
||||
{
|
||||
cwd: dir,
|
||||
env: env(target.options),
|
||||
stdio: stdios(sin, sout, serr, extra),
|
||||
detached: target.options.detached ?? process.platform !== "win32",
|
||||
shell: target.options.shell,
|
||||
// kilocode_change end
|
||||
windowsHide: process.platform === "win32",
|
||||
},
|
||||
direct, // kilocode_change
|
||||
),
|
||||
Effect.fnUntraced(function* ([proc, signal]) {
|
||||
const done = yield* Deferred.isDone(signal)
|
||||
const kill = timeout(proc, command, target.options) // kilocode_change
|
||||
if (done) {
|
||||
const [code] = yield* Deferred.await(signal)
|
||||
if (process.platform === "win32") return yield* Effect.void
|
||||
// kilocode_change start - clean up only descendants owned by direct-settling commands
|
||||
if (direct && groupAlive(proc)) {
|
||||
yield* Effect.ignore(killGroup(command, proc, target.options.killSignal ?? "SIGTERM"))
|
||||
yield* Effect.sleep("100 millis")
|
||||
if (groupAlive(proc)) yield* Effect.ignore(killGroup(command, proc, "SIGKILL"))
|
||||
return yield* Effect.void
|
||||
}
|
||||
// kilocode_change end
|
||||
if (code !== 0 && Predicate.isNotNull(code)) return yield* Effect.ignore(kill(killGroup))
|
||||
return yield* Effect.void
|
||||
}
|
||||
|
||||
@@ -4,19 +4,29 @@ import type { Database } from "../database/database"
|
||||
type Db = Database.Interface["db"]
|
||||
|
||||
export function ensure(db: Db) {
|
||||
return db.transaction(
|
||||
(tx) =>
|
||||
Effect.gen(function* () {
|
||||
const rows = yield* tx.all<{ name: string }>("PRAGMA table_info('session_context_epoch')")
|
||||
const names = new Set(rows.map((row) => row.name))
|
||||
const load = db.all<{ name: string }>("PRAGMA table_info('session_context_epoch')")
|
||||
const ready = (rows: { name: string }[]) => {
|
||||
const names = new Set(rows.map((row) => row.name))
|
||||
return ["agent", "replacement_seq", "revision"].every((name) => names.has(name))
|
||||
}
|
||||
return load.pipe(
|
||||
Effect.flatMap((rows) => {
|
||||
if (ready(rows)) return Effect.void
|
||||
return db.transaction(
|
||||
(tx) =>
|
||||
Effect.gen(function* () {
|
||||
const current = yield* tx.all<{ name: string }>("PRAGMA table_info('session_context_epoch')")
|
||||
const names = new Set(current.map((row) => row.name))
|
||||
|
||||
if (!names.has("agent"))
|
||||
yield* tx.run("ALTER TABLE `session_context_epoch` ADD `agent` text DEFAULT 'build' NOT NULL")
|
||||
if (!names.has("replacement_seq"))
|
||||
yield* tx.run("ALTER TABLE `session_context_epoch` ADD `replacement_seq` integer")
|
||||
if (!names.has("revision"))
|
||||
yield* tx.run("ALTER TABLE `session_context_epoch` ADD `revision` integer DEFAULT 0 NOT NULL")
|
||||
}),
|
||||
{ behavior: "immediate" },
|
||||
if (!names.has("agent"))
|
||||
yield* tx.run("ALTER TABLE `session_context_epoch` ADD `agent` text DEFAULT 'build' NOT NULL")
|
||||
if (!names.has("replacement_seq"))
|
||||
yield* tx.run("ALTER TABLE `session_context_epoch` ADD `replacement_seq` integer")
|
||||
if (!names.has("revision"))
|
||||
yield* tx.run("ALTER TABLE `session_context_epoch` ADD `revision` integer DEFAULT 0 NOT NULL")
|
||||
}),
|
||||
{ behavior: "immediate" },
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Match } from "@opencode-ai/schema/filesystem"
|
||||
|
||||
export interface Options {
|
||||
readonly context?: number
|
||||
readonly literal?: boolean
|
||||
readonly ignoreCase?: boolean
|
||||
}
|
||||
|
||||
export type GrepMatch = Match & {
|
||||
readonly context: boolean
|
||||
readonly textTruncated: boolean
|
||||
}
|
||||
|
||||
export const flags = (input: Options) => [
|
||||
...(input.literal ? ["--fixed-strings"] : []),
|
||||
...(input.ignoreCase ? ["--ignore-case"] : []),
|
||||
...(input.context ? [`--context=${input.context}`] : []),
|
||||
]
|
||||
|
||||
export const stop = (limit: number) => {
|
||||
let matches = 0
|
||||
return (row: { readonly context: boolean }) => !row.context && ++matches > limit
|
||||
}
|
||||
|
||||
export const select = <
|
||||
A extends {
|
||||
readonly context: boolean
|
||||
readonly path: { readonly text: string }
|
||||
readonly line_number: number
|
||||
},
|
||||
>(
|
||||
input: { readonly limit: number; readonly context?: number },
|
||||
items: readonly A[],
|
||||
) => {
|
||||
let count = 0
|
||||
const overflow = items.findIndex((row) => !row.context && ++count > input.limit)
|
||||
const selected = items.slice(0, overflow === -1 ? items.length : overflow)
|
||||
const matches = selected.filter((row) => !row.context)
|
||||
return selected.filter(
|
||||
(row) =>
|
||||
!row.context ||
|
||||
matches.some(
|
||||
(match) =>
|
||||
match.path.text === row.path.text && Math.abs(match.line_number - row.line_number) <= (input.context ?? 0),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const decorate = (match: Match, context: boolean, textTruncated: boolean): GrepMatch =>
|
||||
Object.assign(match, { context, textTruncated })
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { ChildProcess } from "effect/unstable/process"
|
||||
|
||||
const commands = new WeakSet<object>()
|
||||
|
||||
export function attach(command: ChildProcess.StandardCommand) {
|
||||
commands.add(command)
|
||||
return command
|
||||
}
|
||||
|
||||
export function take(command: ChildProcess.StandardCommand) {
|
||||
const found = commands.has(command)
|
||||
commands.delete(command)
|
||||
return found
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Cause, Option } from "effect"
|
||||
import { isSqlError } from "effect/unstable/sql/SqlError"
|
||||
|
||||
export function isBusy(error: unknown): boolean {
|
||||
if (isSqlError(error)) return error.reason._tag === "LockTimeoutError"
|
||||
if (typeof error !== "object" || error === null || !("cause" in error) || error.cause === error) return false
|
||||
if (!Cause.isCause(error.cause)) return isBusy(error.cause)
|
||||
const failure = Cause.findErrorOption(error.cause)
|
||||
return Option.isSome(failure) && isBusy(failure.value)
|
||||
}
|
||||
@@ -214,7 +214,17 @@ const layer = Layer.effect(
|
||||
}
|
||||
yield* Effect.logInfo("creating session", { id, cmd: command, args, cwd })
|
||||
const { spawn } = yield* Effect.promise(() => pty())
|
||||
const proc = yield* Effect.sync(() => spawn(command, args, { name: "xterm-256color", cwd, env }))
|
||||
// kilocode_change start - spawn with initial terminal dimensions
|
||||
const proc = yield* Effect.sync(() =>
|
||||
spawn(command, args, {
|
||||
name: "xterm-256color",
|
||||
cwd,
|
||||
env,
|
||||
cols: input.size?.cols,
|
||||
rows: input.size?.rows,
|
||||
}),
|
||||
)
|
||||
// kilocode_change end
|
||||
const info: Info = {
|
||||
id,
|
||||
title: input.title || `Terminal ${id.slice(-4)}`,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
export * as Ripgrep from "./ripgrep"
|
||||
|
||||
import { Context, Effect, Fiber, Layer, Schema, Stream } from "effect"
|
||||
import { Context, Duration, Effect, Fiber, Layer, Schema, Stream } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { makeGlobalNode } from "./effect/app-node"
|
||||
import { Entry, Match } from "@opencode-ai/schema/filesystem"
|
||||
import * as KiloGrep from "./kilocode/ripgrep-grep" // kilocode_change
|
||||
import * as SpawnExit from "./kilocode/spawn-exit" // kilocode_change
|
||||
import * as SpawnValidation from "./kilocode/spawn-validation" // kilocode_change
|
||||
import { AppProcess, collectStream, waitForAbort } from "./process"
|
||||
import { NonNegativeInt, PositiveInt, RelativePath } from "./schema"
|
||||
@@ -21,7 +23,7 @@ const MAX_RECORD_BYTES = 64 * 1024
|
||||
const MAX_SUBMATCHES = 100
|
||||
|
||||
const RawMatch = Schema.Struct({
|
||||
type: Schema.Literal("match"),
|
||||
type: Schema.Literals(["match", "context"]), // kilocode_change - retain requested context records
|
||||
data: Schema.Struct({
|
||||
path: Schema.Struct({ text: Schema.String }),
|
||||
lines: Schema.Struct({ text: Schema.String }),
|
||||
@@ -37,7 +39,7 @@ const RawMatch = Schema.Struct({
|
||||
}),
|
||||
})
|
||||
|
||||
type RawMatchData = (typeof RawMatch.Type)["data"]
|
||||
type RawMatchData = (typeof RawMatch.Type)["data"] & { readonly context: boolean } // kilocode_change
|
||||
|
||||
export class Error extends Schema.TaggedErrorClass<Error>()("Ripgrep.Error", {
|
||||
message: Schema.String,
|
||||
@@ -69,7 +71,8 @@ export interface GlobInput {
|
||||
readonly validate?: Effect.Effect<void, unknown> // kilocode_change - bind approved searches at spawn
|
||||
}
|
||||
|
||||
export interface GrepInput {
|
||||
export interface GrepInput extends KiloGrep.Options {
|
||||
// kilocode_change
|
||||
readonly cwd: string
|
||||
readonly pattern: string
|
||||
readonly file?: string
|
||||
@@ -82,7 +85,7 @@ export interface GrepInput {
|
||||
export interface Interface {
|
||||
readonly find: (input: FindInput) => Effect.Effect<readonly Entry[], Error>
|
||||
readonly glob: (input: GlobInput) => Effect.Effect<SearchResult<Entry>, Error> // kilocode_change
|
||||
readonly grep: (input: GrepInput) => Effect.Effect<SearchResult<Match>, Error | InvalidPatternError> // kilocode_change
|
||||
readonly grep: (input: GrepInput) => Effect.Effect<SearchResult<KiloGrep.GrepMatch>, Error | InvalidPatternError> // kilocode_change
|
||||
}
|
||||
|
||||
// kilocode_change start - retain truncation state through model-facing tools
|
||||
@@ -114,6 +117,7 @@ const layer = Layer.effect(
|
||||
readonly parse: (line: string) => Effect.Effect<A | undefined, Error>
|
||||
readonly pattern?: string
|
||||
readonly onItem?: (item: A) => Effect.Effect<void>
|
||||
readonly stop?: (item: A) => boolean // kilocode_change - stop bounded searches at the overflow match
|
||||
readonly validate?: Effect.Effect<void, unknown> // kilocode_change - spawn-bound target validation
|
||||
}) => {
|
||||
const program = Effect.scoped(
|
||||
@@ -124,16 +128,24 @@ const layer = Layer.effect(
|
||||
cwd: input.cwd,
|
||||
extendEnv: true,
|
||||
stdin: "ignore",
|
||||
forceKillAfter: input.stop ? Duration.seconds(1) : undefined, // kilocode_change - bound grep interruption
|
||||
})
|
||||
const handle = yield* process.spawn(
|
||||
input.validate ? SpawnValidation.attach(command, input.validate) : command,
|
||||
)
|
||||
const validated = input.validate ? SpawnValidation.attach(command, input.validate) : command
|
||||
const spawned = input.stop ? SpawnExit.attach(validated) : validated // kilocode_change
|
||||
const handle = yield* process.spawn(spawned)
|
||||
// kilocode_change end
|
||||
const stderrFiber = yield* collectStream(handle.stderr, ERROR_BYTES).pipe(
|
||||
Effect.map((output) => output.buffer.toString("utf8")),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
let observed = 0
|
||||
let stopped = false // kilocode_change
|
||||
const take = input.stop // kilocode_change start
|
||||
? Stream.takeUntil<A>((row) => {
|
||||
stopped = input.stop?.(row) ?? false
|
||||
return stopped
|
||||
})
|
||||
: Stream.take(input.limit + 1) // kilocode_change end
|
||||
const rows = yield* Stream.decodeText(handle.stdout).pipe(
|
||||
Stream.splitLines,
|
||||
Stream.filter((line) => line.length > 0),
|
||||
@@ -143,11 +155,12 @@ const layer = Layer.effect(
|
||||
if (!input.onItem || observed++ >= input.limit) return Effect.void
|
||||
return input.onItem(row)
|
||||
}),
|
||||
Stream.take(input.limit + 1),
|
||||
take, // kilocode_change
|
||||
Stream.runCollect,
|
||||
Effect.map((chunk) => [...chunk]),
|
||||
)
|
||||
const truncated = rows.length > input.limit
|
||||
if (stopped) return { items: rows, truncated: true, partial: false } // kilocode_change
|
||||
const truncated = input.stop ? false : rows.length > input.limit // kilocode_change - custom stop owns truncation
|
||||
if (truncated) return { items: rows.slice(0, input.limit), truncated, partial: false }
|
||||
|
||||
const code = yield* handle.exitCode
|
||||
@@ -244,11 +257,13 @@ const layer = Layer.effect(
|
||||
grep: (input) =>
|
||||
run<RawMatchData>({
|
||||
...input,
|
||||
stop: KiloGrep.stop(input.limit), // kilocode_change
|
||||
args: [
|
||||
"--no-config",
|
||||
"--json",
|
||||
"--hidden",
|
||||
"--no-messages",
|
||||
...KiloGrep.flags(input), // kilocode_change
|
||||
...(input.include ? [`--glob=${input.include}`] : []),
|
||||
"--glob=!**/.git/**",
|
||||
"--",
|
||||
@@ -264,13 +279,19 @@ const layer = Layer.effect(
|
||||
})
|
||||
).pipe(
|
||||
Effect.flatMap((json) => {
|
||||
if (!json || typeof json !== "object" || !("type" in json) || json.type !== "match")
|
||||
if (
|
||||
!json ||
|
||||
typeof json !== "object" ||
|
||||
!("type" in json) ||
|
||||
(json.type !== "match" && json.type !== "context") // kilocode_change
|
||||
)
|
||||
return Effect.succeed(undefined)
|
||||
return Schema.decodeUnknownEffect(RawMatch)(json).pipe(
|
||||
Effect.map((match) => ({
|
||||
...match.data,
|
||||
path: { text: match.data.path.text.replace(/^\.[\\/]/, "") },
|
||||
submatches: match.data.submatches.slice(0, MAX_SUBMATCHES),
|
||||
context: match.type === "context", // kilocode_change
|
||||
})),
|
||||
Effect.mapError((cause) => failure("Invalid ripgrep match output", cause)),
|
||||
)
|
||||
@@ -280,12 +301,12 @@ const layer = Layer.effect(
|
||||
// kilocode_change start - retain spawn metadata after mapping matches
|
||||
Effect.map((result) => ({
|
||||
...result,
|
||||
items: result.items.map((match) => {
|
||||
items: KiloGrep.select(input, result.items).map((match) => {
|
||||
const relative = match.path.text
|
||||
.replace(/^(?:\.[\\/])+/u, "")
|
||||
.replace(/^[\\/]+/u, "")
|
||||
.replaceAll("\\", "/")
|
||||
return Match.make({
|
||||
const item = Match.make({
|
||||
entry: Entry.make({
|
||||
path: RelativePath.make(relative),
|
||||
type: "file",
|
||||
@@ -299,6 +320,7 @@ const layer = Layer.effect(
|
||||
end: submatch.end,
|
||||
})),
|
||||
})
|
||||
return KiloGrep.decorate(item, match.context, match.lines.text.length > 2_000)
|
||||
}),
|
||||
})),
|
||||
// kilocode_change end
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import path from "path"
|
||||
import { Database as SQLite } from "bun:sqlite" // kilocode_change
|
||||
import { describe, expect } from "bun:test"
|
||||
import { eq } from "drizzle-orm" // kilocode_change
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
import { CredentialTable } from "@opencode-ai/core/credential/sql" // kilocode_change
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Integration } from "@opencode-ai/core/integration"
|
||||
// kilocode_change start
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
// kilocode_change end
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
@@ -20,6 +24,16 @@ function localLayer(directory: string) {
|
||||
)
|
||||
}
|
||||
|
||||
// kilocode_change start
|
||||
function importer(dir: string, store: Database.Interface) {
|
||||
return Credential.legacyImportLayer.pipe(
|
||||
Layer.provide(Layer.succeed(Database.Service, store)),
|
||||
Layer.provide(FSUtil.defaultLayer),
|
||||
Layer.provide(Global.layerWith({ data: dir })),
|
||||
)
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
describe("Credential", () => {
|
||||
it.live("stores, updates, lists, and removes credentials", () =>
|
||||
Effect.acquireUseRelease(
|
||||
@@ -196,6 +210,70 @@ describe("Credential", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.live("skips unchanged legacy writes and defers locked reconciliation", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => {
|
||||
const file = path.join(tmp.path, "credential.db")
|
||||
const auth = path.join(tmp.path, "auth.json")
|
||||
const write = (key: string) =>
|
||||
Effect.promise(() => Bun.write(auth, JSON.stringify({ kilo: { type: "api", key } })))
|
||||
return Effect.gen(function* () {
|
||||
yield* write("first")
|
||||
const store = yield* Database.Service
|
||||
const layer = importer(tmp.path, store)
|
||||
yield* Layer.build(Layer.fresh(layer))
|
||||
|
||||
const before = yield* store.db
|
||||
.select()
|
||||
.from(CredentialTable)
|
||||
.where(eq(CredentialTable.integration_id, Integration.ID.make("kilo")))
|
||||
.get()
|
||||
yield* Layer.build(Layer.fresh(layer))
|
||||
const unchanged = yield* store.db
|
||||
.select()
|
||||
.from(CredentialTable)
|
||||
.where(eq(CredentialTable.integration_id, Integration.ID.make("kilo")))
|
||||
.get()
|
||||
expect(unchanged?.time_updated).toBe(before?.time_updated)
|
||||
|
||||
yield* write("second")
|
||||
yield* store.db.run("PRAGMA busy_timeout = 0")
|
||||
yield* Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const holder = new SQLite(file)
|
||||
holder.run("PRAGMA busy_timeout = 0")
|
||||
holder.run("BEGIN IMMEDIATE")
|
||||
return holder
|
||||
}),
|
||||
() => Layer.build(Layer.fresh(layer)),
|
||||
(holder) =>
|
||||
Effect.sync(() => {
|
||||
if (holder.inTransaction) holder.run("ROLLBACK")
|
||||
holder.close()
|
||||
}),
|
||||
)
|
||||
|
||||
const stale = yield* store.db
|
||||
.select()
|
||||
.from(CredentialTable)
|
||||
.where(eq(CredentialTable.integration_id, Integration.ID.make("kilo")))
|
||||
.get()
|
||||
expect(stale?.value).toMatchObject({ type: "key", key: "first" })
|
||||
|
||||
yield* Layer.build(Layer.fresh(layer))
|
||||
const reconciled = yield* store.db
|
||||
.select()
|
||||
.from(CredentialTable)
|
||||
.where(eq(CredentialTable.integration_id, Integration.ID.make("kilo")))
|
||||
.get()
|
||||
expect(reconciled?.value).toMatchObject({ type: "key", key: "second" })
|
||||
}).pipe(Effect.provide(Database.layerFromPath(file)), Effect.scoped)
|
||||
},
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("dual-writes stored credentials for released auth.json readers", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Database as SQLite } from "bun:sqlite"
|
||||
import { SqliteClient } from "@effect/sql-sqlite-bun"
|
||||
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
||||
import { DatabaseMigration } from "@opencode-ai/core/database/migration"
|
||||
@@ -159,6 +160,22 @@ describe("database migration compatibility", () => {
|
||||
sql`SELECT agent, replacement_seq AS replacementSeq, revision FROM session_context_epoch WHERE session_id = 'session'`,
|
||||
),
|
||||
).toEqual({ agent: "build", replacementSeq: 4, revision: 1 })
|
||||
|
||||
yield* db.run("PRAGMA busy_timeout = 0")
|
||||
yield* Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const holder = new SQLite(filename)
|
||||
holder.run("PRAGMA busy_timeout = 0")
|
||||
holder.run("BEGIN IMMEDIATE")
|
||||
return holder
|
||||
}),
|
||||
() => ensure(db),
|
||||
(holder) =>
|
||||
Effect.sync(() => {
|
||||
if (holder.inTransaction) holder.run("ROLLBACK")
|
||||
holder.close()
|
||||
}),
|
||||
)
|
||||
}),
|
||||
).pipe(Effect.provide(Database.layerFromPath(filename)), Effect.scoped),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import fs from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { Effect, Fiber, Layer } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||
import { RipgrepBinary } from "@opencode-ai/core/ripgrep/binary"
|
||||
import { tmpdir } from "../fixture/tmpdir"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
const record = (type: "match" | "context", line: number, text: string) =>
|
||||
JSON.stringify({
|
||||
type,
|
||||
data: {
|
||||
path: { text: "fixture.ts" },
|
||||
lines: { text: `${text}\n` },
|
||||
line_number: line,
|
||||
absolute_offset: line * 10,
|
||||
submatches: type === "match" ? [{ match: { text: "NEEDLE.*" }, start: 0, end: 8 }] : [],
|
||||
},
|
||||
})
|
||||
|
||||
const alive = (pid: number) => {
|
||||
try {
|
||||
process.kill(pid, 0)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const read = async (file: string) => {
|
||||
const deadline = Date.now() + 1_000
|
||||
while (Date.now() < deadline) {
|
||||
const value = await fs.readFile(file, "utf8").catch(() => undefined)
|
||||
if (value) return value
|
||||
await Bun.sleep(10)
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${file}`)
|
||||
}
|
||||
|
||||
const cleanup = async (file: string) => {
|
||||
const pid = Number(await fs.readFile(file, "utf8").catch(() => ""))
|
||||
if (!pid || !alive(pid)) return
|
||||
try {
|
||||
process.kill(pid, "SIGKILL")
|
||||
} catch (err) {
|
||||
if (alive(pid)) throw err
|
||||
}
|
||||
}
|
||||
|
||||
const gone = async (pid: number) => {
|
||||
const deadline = Date.now() + 1_000
|
||||
while (Date.now() < deadline) {
|
||||
if (!alive(pid)) return true
|
||||
await Bun.sleep(10)
|
||||
}
|
||||
return !alive(pid)
|
||||
}
|
||||
|
||||
const fixture = async (dir: string, source: string) => {
|
||||
if (process.platform !== "win32") {
|
||||
const binary = path.join(dir, "rg")
|
||||
await fs.writeFile(binary, `#!${process.execPath}\n${source}`, { mode: 0o755 })
|
||||
return binary
|
||||
}
|
||||
|
||||
const script = path.join(dir, "fake-rg.cjs")
|
||||
const binary = path.join(dir, "rg.cmd")
|
||||
await fs.writeFile(script, source)
|
||||
await fs.writeFile(binary, `@echo off\r\n"${process.execPath}" "%~dp0fake-rg.cjs" %*\r\n`)
|
||||
return binary
|
||||
}
|
||||
|
||||
const layer = (binary: string) =>
|
||||
LayerNode.compile(Ripgrep.node, [
|
||||
[
|
||||
RipgrepBinary.node,
|
||||
Layer.succeed(RipgrepBinary.Service, RipgrepBinary.Service.of({ filepath: Effect.succeed(binary) })),
|
||||
],
|
||||
] as const)
|
||||
|
||||
describe("Kilo ripgrep settlement", () => {
|
||||
it.live(
|
||||
"settles a bounded parameterized grep when inherited output stays open",
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) =>
|
||||
Effect.gen(function* () {
|
||||
const retained = path.join(tmp.path, "retained.pid")
|
||||
const owned = path.join(tmp.path, "owned.pid")
|
||||
const args = path.join(tmp.path, "args.json")
|
||||
const output =
|
||||
[
|
||||
record("context", 1, "before"),
|
||||
record("match", 2, "NEEDLE.*"),
|
||||
record("context", 3, "after"),
|
||||
record("context", 5, "later before"),
|
||||
record("match", 6, "NEEDLE.*"),
|
||||
].join("\n") + "\n"
|
||||
const source = `const { spawn } = require("node:child_process")
|
||||
const { writeFileSync, writeSync } = require("node:fs")
|
||||
const retained = spawn(process.execPath, ["-e", "setTimeout(() => process.exit(0), 30_000)"], {
|
||||
detached: true,
|
||||
stdio: ["ignore", "inherit", "inherit"],
|
||||
})
|
||||
retained.unref()
|
||||
${
|
||||
process.platform === "win32"
|
||||
? ""
|
||||
: `const owned = spawn(process.execPath, ["-e", "setTimeout(() => process.exit(0), 30_000)"], {
|
||||
stdio: "ignore",
|
||||
})
|
||||
owned.unref()`
|
||||
}
|
||||
writeFileSync(${JSON.stringify(retained)}, String(retained.pid))
|
||||
${process.platform === "win32" ? "" : `writeFileSync(${JSON.stringify(owned)}, String(owned.pid))`}
|
||||
writeFileSync(${JSON.stringify(args)}, JSON.stringify(process.argv.slice(2)))
|
||||
writeSync(1, ${JSON.stringify(output)})
|
||||
`
|
||||
const binary = yield* Effect.promise(() => fixture(tmp.path, source))
|
||||
|
||||
const result = yield* Ripgrep.Service.pipe(
|
||||
Effect.flatMap((ripgrep) =>
|
||||
ripgrep.grep({
|
||||
cwd: tmp.path,
|
||||
pattern: "NEEDLE.*",
|
||||
include: "*.ts",
|
||||
context: 1,
|
||||
limit: 1,
|
||||
literal: true,
|
||||
ignoreCase: true,
|
||||
}),
|
||||
),
|
||||
Effect.provide(layer(binary)),
|
||||
Effect.timeout("5 seconds"),
|
||||
)
|
||||
const pid = Number(yield* Effect.promise(() => read(retained)))
|
||||
const passed: unknown = JSON.parse(yield* Effect.promise(() => read(args)))
|
||||
if (!Array.isArray(passed) || !passed.every((arg) => typeof arg === "string")) {
|
||||
throw new Error("Fake ripgrep did not capture string arguments")
|
||||
}
|
||||
|
||||
expect(result.truncated).toBe(true)
|
||||
expect(result.items.map((item) => [item.context, item.line, item.text.trim()])).toEqual([
|
||||
[true, 1, "before"],
|
||||
[false, 2, "NEEDLE.*"],
|
||||
[true, 3, "after"],
|
||||
])
|
||||
expect(passed).toContain("--fixed-strings")
|
||||
expect(passed).toContain("--ignore-case")
|
||||
expect(passed).toContain("--context=1")
|
||||
expect(passed).toContain("--glob=*.ts")
|
||||
expect(alive(pid)).toBe(true)
|
||||
if (process.platform !== "win32") {
|
||||
const child = Number(yield* Effect.promise(() => read(owned)))
|
||||
expect(yield* Effect.promise(() => gone(child))).toBe(true)
|
||||
}
|
||||
}),
|
||||
(tmp) =>
|
||||
Effect.promise(async () => {
|
||||
await cleanup(path.join(tmp.path, "retained.pid"))
|
||||
await cleanup(path.join(tmp.path, "owned.pid"))
|
||||
await tmp[Symbol.asyncDispose]()
|
||||
}),
|
||||
),
|
||||
10_000,
|
||||
)
|
||||
|
||||
it.live(
|
||||
"force kills a bounded grep that does not exit after cancellation",
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) =>
|
||||
Effect.gen(function* () {
|
||||
const ready = path.join(tmp.path, "ready.pid")
|
||||
const source = `const { writeFileSync } = require("node:fs")
|
||||
if (process.platform !== "win32") process.on("SIGTERM", () => {})
|
||||
writeFileSync(${JSON.stringify(ready)}, String(process.pid))
|
||||
setInterval(() => {}, 10_000)
|
||||
`
|
||||
const binary = yield* Effect.promise(() => fixture(tmp.path, source))
|
||||
|
||||
const controller = new AbortController()
|
||||
const fiber = yield* Ripgrep.Service.pipe(
|
||||
Effect.flatMap((ripgrep) =>
|
||||
ripgrep.grep({ cwd: tmp.path, pattern: "needle", context: 1, limit: 1, signal: controller.signal }),
|
||||
),
|
||||
Effect.provide(layer(binary)),
|
||||
Effect.exit,
|
||||
Effect.forkScoped,
|
||||
)
|
||||
const pid = Number(yield* Effect.promise(() => read(ready)))
|
||||
controller.abort()
|
||||
const exit = yield* Fiber.join(fiber).pipe(Effect.timeout("5 seconds"))
|
||||
|
||||
expect(exit._tag).toBe("Failure")
|
||||
expect(alive(pid)).toBe(false)
|
||||
}),
|
||||
(tmp) =>
|
||||
Effect.promise(async () => {
|
||||
await cleanup(path.join(tmp.path, "ready.pid"))
|
||||
await tmp[Symbol.asyncDispose]()
|
||||
}),
|
||||
),
|
||||
10_000,
|
||||
)
|
||||
})
|
||||
@@ -279,7 +279,13 @@ export class SQLiteEffectPreparedQuery<
|
||||
assertUnreachable(cacheStrat)
|
||||
}).pipe(
|
||||
Effect.catch((e) => {
|
||||
return Effect.fail(new EffectDrizzleQueryError({ query: queryString, params, cause: Cause.fail(e) }))
|
||||
return Effect.fail(
|
||||
new EffectDrizzleQueryError({
|
||||
query: queryString,
|
||||
params: params.map(() => "<redacted>"), // kilocode_change - bound values may contain credentials
|
||||
cause: Cause.fail(e),
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -130,6 +130,24 @@ test("preserves failed transaction begin errors", async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// kilocode_change start - query errors must never expose bound credential values
|
||||
test("redacts bound values from query errors", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDb
|
||||
const secret = "must-not-leak"
|
||||
yield* db.insert(users).values({ id: 1, name: "Ada" })
|
||||
|
||||
const error = yield* db.insert(users).values({ id: 1, name: secret }).pipe(Effect.flip)
|
||||
|
||||
expect(error.message).not.toContain(secret)
|
||||
expect(error.params).not.toContain(secret)
|
||||
expect(error.params.every((param) => param === "<redacted>")).toBe(true)
|
||||
}),
|
||||
)
|
||||
})
|
||||
// kilocode_change end
|
||||
|
||||
test("supports returning and rejects empty update sets", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
| `kilo upgrade [target]` | upgrade kilo to the latest or a specific version |
|
||||
| `kilo uninstall` | uninstall kilo and remove all related files |
|
||||
| `kilo serve` | starts a headless kilo server |
|
||||
| `kilo web` | start kilo server and open web interface |
|
||||
| `kilo models [provider]` | list all available models |
|
||||
| `kilo roll-call <filter>` | batch-test text models matching a filter for connectivity and latency |
|
||||
| `kilo profile` | show Kilo account profile |
|
||||
|
||||
@@ -14,6 +14,31 @@ The JetBrains plugin provides the best native JetBrains UX for working with an A
|
||||
|
||||
{% image src="/docs/img/jetbrains/plugin-auto-updates.png" alt="JetBrains Updates settings with Update plugins automatically enabled" width="900" caption="Enable automatic plugin updates to receive Kilo Code fixes and improvements." /%}
|
||||
|
||||
### Install with bundled Kilo Core
|
||||
|
||||
The Marketplace build is best for most users. Use the bundled Kilo Core build when your IDE cannot download the Kilo Core runtime after installation, such as on locked-down corporate networks, behind strict proxy or TLS inspection, in offline development environments, or where corporate policy blocks applications from downloading executables at runtime.
|
||||
|
||||
The bundled build ships the JetBrains plugin with Kilo Core included. The install is larger, but first launch does not need a separate runtime download.
|
||||
|
||||
1. Open **Settings → Plugins**
|
||||
2. Click the gear icon and choose **Manage Plugin Repositories...**
|
||||
3. Click **+** and add the Kilo Code repository URL:
|
||||
|
||||
```text
|
||||
https://kilo-org.github.io/kilocode/jetbrains/updatePlugins.xml
|
||||
```
|
||||
|
||||
4. Click **OK**, then install or update **Kilo Code** from **Settings → Plugins**
|
||||
5. Restart the IDE if prompted
|
||||
|
||||
{% image src="/docs/img/jetbrains/plugin-custom-repository-menu.png" alt="JetBrains Plugins settings with Manage Plugin Repositories selected from the gear menu" width="900" caption="Open Manage Plugin Repositories from the Plugins settings gear menu." /%}
|
||||
|
||||
{% image src="/docs/img/jetbrains/plugin-custom-repository-url.png" alt="JetBrains Custom Plugin Repositories dialog with the Kilo Code repository URL added" width="700" caption="Add the Kilo Code custom plugin repository URL." /%}
|
||||
|
||||
After restart, open the **Kilo Code** tool window and choose **... → Core**. The menu footer should show **Bundled Core** with the version and architecture.
|
||||
|
||||
{% image src="/docs/img/jetbrains/plugin-bundled-core.png" alt="Kilo Code tool window Core menu showing Bundled Core and the current architecture" width="900" caption="Confirm that the plugin is using Bundled Core." /%}
|
||||
|
||||
### If you used the v7 EAP {% #jetbrains-early-access %}
|
||||
|
||||
{% callout type="info" %}
|
||||
|
||||
@@ -232,7 +232,7 @@ See [Agent Manager Workflows](/docs/automate/agent-manager-workflows#merging-wor
|
||||
|
||||
## Terminals
|
||||
|
||||
Each session has a dedicated terminal rooted in the session's worktree directory. Press `Cmd+/` (macOS) / `Ctrl+/` (Windows/Linux) to focus the terminal for the active session.
|
||||
Each session has a dedicated terminal rooted in the session's worktree directory. Press `Cmd+/` (macOS) / `Ctrl+/` (Windows/Linux) to focus the terminal for the active session. If the embedded terminal is already visible but the prompt has focus, the same shortcut focuses the terminal without hiding it. Press it again while the terminal has focus to hide the panel.
|
||||
|
||||
### Choosing the Terminal Destination
|
||||
|
||||
@@ -243,11 +243,18 @@ The toolbar's terminal button is a split button: click it to open a terminal, or
|
||||
|
||||
The dropdown choice is remembered per panel and becomes the default for new panels. You can also set the default directly with the `kilo-code.new.agentManager.terminalButtonDestination` setting (`vscode` or `agentManager`). The `Cmd+/` (macOS) / `Ctrl+/` (Windows/Linux) shortcut follows the same destination.
|
||||
|
||||
With the **Agent Manager panel** destination, the terminal works like the diff panel: press `Cmd+/` to reveal it and press again to hide it. Hiding never stops the terminal — scrollback and running processes continue in the background, and focus returns to the chat input. A terminal stops only when you close its tab in the panel.
|
||||
With the **Agent Manager panel** destination, the terminal works like the diff panel: press `Cmd+/` to reveal and focus it, press it while the panel is visible but another control has focus to move focus into the terminal, and press it again from the terminal to hide it. Hiding never stops the terminal — scrollback and running processes continue in the background, and focus returns to the chat input. A terminal stops only when you click its close button or type `exit` in the shell.
|
||||
|
||||
### Multiple Terminals
|
||||
|
||||
The side panel hosts multiple terminals per context (the local workspace or a worktree). The panel header is a tab strip: click a tab to switch, click **+** to open another terminal, and click **X** (or middle-click) to close a single terminal. Drag tabs to reorder them. Closing a terminal no longer hides the panel — closing the last one lands on the empty state. Pressing `Cmd+W` (macOS) / `Ctrl+W` (Windows/Linux) with a focused side terminal closes exactly that terminal.
|
||||
Agent Manager has two separate terminal tab strips:
|
||||
|
||||
- **Main terminal tabs** appear alongside the agent session tabs. With the prompt or a main terminal focused, press `Cmd+Shift+T` / `Ctrl+Shift+T` to create another main terminal tab.
|
||||
- **Side terminal tabs** appear in the terminal panel. Focus a side terminal, then press `Cmd+Shift+T` / `Ctrl+Shift+T` to create another side terminal. You can also click **+** in the side-terminal strip.
|
||||
|
||||
The shortcut follows terminal focus, not panel visibility. A visible side panel with the prompt focused still creates a main terminal tab. Press `Cmd+Shift+[` / `Ctrl+Shift+[` for the previous terminal or `Cmd+Shift+]` / `Ctrl+Shift+]` for the next terminal in the focused terminal strip. Drag tabs to reorder them. Pressing `Cmd+W` / `Ctrl+W` with a focused side terminal closes that terminal when other terminals remain. On the last side terminal, it hides the panel and keeps the shell alive; use its close button or type `exit` to stop it.
|
||||
|
||||
`Cmd+T` / `Ctrl+T` always creates a new agent session tab. It never creates a terminal.
|
||||
|
||||
New terminals are named "Terminal N" using the lowest free number, and tabs pick up the live title from the shell or running program, so a dev server or editor names its own tab.
|
||||
|
||||
@@ -256,7 +263,7 @@ New terminals are named "Terminal N" using the lowest free number, and tabs pick
|
||||
A common workflow is letting the agent work, then switching to the terminal to run tests or inspect the worktree, then switching back to control the agent:
|
||||
|
||||
1. **Agent Manager → Terminal:** Press `Cmd+/` (macOS) / `Ctrl+/` (Windows/Linux) to open and focus the terminal for the current session. The terminal runs inside the session's worktree, so commands like `npm test` or `git status` operate on the agent's isolated branch.
|
||||
2. **Terminal → Agent Manager:** Press `Cmd+Shift+M` (macOS) / `Ctrl+Shift+M` (Windows/Linux) to bring focus back to the Agent Manager panel and its prompt input. This works from anywhere in VS Code — the terminal, another editor tab, or the sidebar.
|
||||
2. **Terminal → Agent Manager:** Press `Cmd+Shift+M` (macOS) / `Ctrl+Shift+M` (Windows/Linux) to bring focus back to the Agent Manager panel and its prompt input. This explicit shortcut always targets the prompt and works from anywhere in VS Code — the terminal, another editor tab, or the sidebar. Returning to the panel by clicking its editor tab or switching windows restores the last focused control instead.
|
||||
|
||||
## Setup Scripts
|
||||
|
||||
@@ -392,11 +399,13 @@ Closing a managed worktree removes it from Agent Manager, deletes its `.kilo/wor
|
||||
| `Cmd+Shift+N` | `Ctrl+Shift+N` | Create a new worktree immediately |
|
||||
| `Cmd+Shift+O` | `Ctrl+Shift+O` | Import/open worktree |
|
||||
| `Cmd+Shift+W` | `Ctrl+Shift+W` | Close current worktree |
|
||||
| `Cmd+T` | `Ctrl+T` | New tab (session) in worktree |
|
||||
| `Cmd+W` | `Ctrl+W` | Close current tab |
|
||||
| `Cmd+T` | `Ctrl+T` | New agent session tab in worktree |
|
||||
| `Cmd+W` | `Ctrl+W` | Close the focused tab or terminal; the last side terminal hides instead of stopping |
|
||||
| `Cmd+Alt+Up` / `Down` | `Ctrl+Alt+Up` / `Down` | Previous / next worktree |
|
||||
| `Cmd+Alt+Left` / `Right` | `Ctrl+Alt+Left` / `Right` | Previous / next tab in worktree |
|
||||
| `Cmd+/` | `Ctrl+/` | Focus terminal for current session |
|
||||
| `Cmd+/` | `Ctrl+/` | Focus terminal, or hide it when it already has focus |
|
||||
| `Cmd+Shift+T` | `Ctrl+Shift+T` | New side terminal when a side terminal is focused; otherwise new main terminal tab |
|
||||
| `Cmd+Shift+[` / `]` | `Ctrl+Shift+[` / `]` | Previous / next terminal |
|
||||
| `Cmd+D` | `Ctrl+D` | Toggle diff panel |
|
||||
| `Cmd+E` | `Ctrl+E` | Run / stop run script |
|
||||
| `Cmd+Shift+/` | `Ctrl+Shift+/` | Show keyboard shortcuts |
|
||||
|
||||
@@ -10,7 +10,6 @@ Auto Model is a smart routing system that selects an underlying model for each r
|
||||
| Tier | Best For | Pricing |
|
||||
|---|---|---|
|
||||
| `kilo-auto/frontier` | Maximum capability with the best available models | Paid |
|
||||
| `kilo-auto/balanced` | Strong performance at a lower cost | Paid |
|
||||
| `kilo-auto/efficient` | Lowest cost per task, with capability matched to difficulty | Paid |
|
||||
| `kilo-auto/free` | The best free models available | Free |
|
||||
|
||||
@@ -31,8 +30,7 @@ The underlying models behind each Auto Model tier are updated server-side as bet
|
||||
## Tiers
|
||||
|
||||
- **Frontier** — Routes to the latest and most capable paid models. Uses different models for reasoning-heavy tasks (planning, architecture, debugging) versus implementation tasks (coding, building, exploring), pairing the right capability to each type of work.
|
||||
- **Balanced** — Routes to a cost-effective model for all modes. The specific model is selected based on the API interface in use, but does not vary by mode. A good default for most developers who want strong AI assistance without paying frontier prices.
|
||||
- **Efficient** — Session-aware routing that classifies the difficulty of each request in real time and routes it to the cheapest model proven accurate enough for that task, based on Kilo's continuously-run benchmarks. Routine work stays lean while harder tasks get a more capable model. Because it watches your session in context, it keeps using a model across related turns and only switches when a cheaper option is clearly worth it. If a routing decision can't be made, it falls back to the Balanced tier, so quality never drops below Balanced.
|
||||
- **Efficient** — Session-aware routing that classifies the difficulty of each request in real time and routes it to the cheapest model proven accurate enough for that task, based on Kilo's continuously-run benchmarks. Routine work stays lean while harder tasks get a more capable model. Because it watches your session in context, it keeps using a model across related turns and only switches when a cheaper option is clearly worth it. If a routing decision can't be made, it falls back to a fixed, cost-effective baseline model, so quality never drops below that baseline. A good default for most developers who want strong AI assistance without paying frontier prices.
|
||||
- **Free** — Routes to the best available free models on OpenRouter, splitting traffic across them. Because free model availability shifts over time as providers change promotional periods, the mapping is updated server-side — you always get the best free option without having to track what's currently available. Quality will be lower than paid tiers, and the models may change over time.
|
||||
|
||||
### How Auto Efficient routing works
|
||||
@@ -61,7 +59,7 @@ New entries are benchmarked on demand before they can serve traffic. Each entry
|
||||
| Failed | Benchmarking failed — retry the entry |
|
||||
| Unavailable | The model or variant is no longer in your catalog — remove the entry |
|
||||
|
||||
Routing decides only among ready entries. If no pool entry can serve a request, the request falls back to the Balanced tier, so quality never drops below Balanced.
|
||||
Routing decides only among ready entries. If no pool entry can serve a request, the request falls back to a fixed, cost-effective baseline model, so quality never drops below that baseline.
|
||||
|
||||
{% callout type="note" %}
|
||||
You can benchmark up to 10 new or retried pairs per owner per rolling 24 hours. Entries that are already ready or benchmarking don't count against this limit.
|
||||
@@ -85,7 +83,7 @@ No need to manually switch models when changing modes. Auto Model handles routin
|
||||
|
||||
### Flexible Cost Control
|
||||
|
||||
Pick the tier that fits your budget. Frontier gives you the best models for demanding work; Balanced offers capable models at a fraction of the cost; Efficient minimizes cost per task by matching model capability to task difficulty; Free costs nothing.
|
||||
Pick the tier that fits your budget. Frontier gives you the best models for demanding work; Efficient minimizes cost per task by matching model capability to task difficulty, at a fraction of Frontier's cost; Free costs nothing.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -101,7 +99,7 @@ Select an Auto Model tier from the model dropdown in the Kilo Code chat interfac
|
||||
|
||||
1. Open Kilo Code in VS Code or JetBrains
|
||||
2. Click the model selector dropdown
|
||||
3. Choose an Auto Model such as `kilo-auto/frontier` or `kilo-auto/balanced`
|
||||
3. Choose an Auto Model such as `kilo-auto/frontier` or `kilo-auto/efficient`
|
||||
4. Start chatting - the right model is selected automatically based on your current mode
|
||||
|
||||
## When to Use Auto Model
|
||||
|
||||
@@ -5,7 +5,7 @@ description: "Autonomous AI agent orchestration for your codebase"
|
||||
|
||||
# {% $markdoc.frontmatter.title %}
|
||||
|
||||
Gastown by Kilo is an autonomous agent orchestration platform that manages teams of AI agents working on your codebase. Built on [Gastown](https://gastown.dev) — the open protocol for agent orchestration — Kilo's implementation coordinates coding agents, a code review agent, and a conversational coordinator to ship features, fix bugs, and maintain your projects with minimal human intervention.
|
||||
Gastown by Kilo is an autonomous agent orchestration platform that manages teams of AI agents working on your codebase. Built on [Gastown](https://github.com/gastownhall/gastown) — the open protocol for agent orchestration — Kilo's implementation coordinates coding agents, a code review agent, and a conversational coordinator to ship features, fix bugs, and maintain your projects with minimal human intervention.
|
||||
|
||||
You describe the work. Agents figure out how to do it, write the code, review each other's output, and land clean PRs — while you stay in control of what ships.
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ The primary model used by all agents (polecats, refinery, mayor). This affects q
|
||||
|
||||
Popular choices:
|
||||
- **Kilo Auto Frontier** — highest quality models, best results (recommended)
|
||||
- **Kilo Auto Balanced** — good balance of quality and cost (minimum for Gas Town)
|
||||
- **Kilo Auto Efficient** — cheapest model proven accurate enough for each task, with capability matched to difficulty (minimum for Gas Town)
|
||||
|
||||
### Role-Specific Models
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ Beads automatically escalate after 3 failed review cycles. If a bead is genuinel
|
||||
**Fix:**
|
||||
1. Review failed bead descriptions — make them more specific
|
||||
2. Ensure the repo builds cleanly (agents struggle with pre-existing broken builds)
|
||||
3. Consider upgrading the model (Auto Balanced → Auto Frontier for complex work)
|
||||
3. Consider upgrading the model (Auto Efficient → Auto Frontier for complex work)
|
||||
4. Add custom instructions to guide agents: test commands, build steps, conventions
|
||||
|
||||
## Getting Help
|
||||
|
||||
@@ -659,21 +659,6 @@ Options:
|
||||
--cors additional domains to allow for CORS [array] [default: []]
|
||||
```
|
||||
|
||||
## kilo web
|
||||
|
||||
```
|
||||
start kilo server and open web interface
|
||||
|
||||
Options:
|
||||
--help Show help [boolean]
|
||||
--version Show version number [boolean]
|
||||
--port port to listen on [number] [default: 0]
|
||||
--hostname hostname to listen on [string] [default: "127.0.0.1"]
|
||||
--mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) [boolean] [default: false]
|
||||
--mdns-domain custom domain name for mDNS service (default: kilo.local) [string] [default: "kilo.local"]
|
||||
--cors additional domains to allow for CORS [array] [default: []]
|
||||
```
|
||||
|
||||
## kilo models
|
||||
|
||||
```
|
||||
|
||||
@@ -110,7 +110,19 @@ Before using Kilo for Slack:
|
||||
|
||||
## Setup
|
||||
|
||||
To install Kilo for Slack, go to the Integrations menu in the sidebar at [app.kilo.ai](https://app.kilo.ai) and set up the Slack integration.
|
||||
To install Kilo for Slack, go to the Integrations menu in the sidebar at [app.kilo.ai](https://app.kilo.ai) and set up the Slack integration:
|
||||
|
||||
{% image src="/docs/img/connect/slack/slackbot-integrations.webp" alt="Kilo Integrations page where you install the Slack integration" width="800" /%}
|
||||
|
||||
Then hit the Configure button, which will take you to this page:
|
||||
{% image src="/docs/img/connect/slack/slackbot-integrations-2.webp" alt="Kilo Integrations page where you install the Slack integration, step 2" width="800" /%}
|
||||
|
||||
Once you press "Connect with Slack", the Slack OAuth authorization page appears:
|
||||
{% image src="/docs/img/connect/slack/slackbot-slack-authorize.webp" alt="Slack OAuth authorization page" width="800" /%}
|
||||
|
||||
Once this is completed you're good to go!
|
||||
|
||||
To make proper use of all of the Kilo Slack bot's capabilities, it's important that you also connect your source control provider with Kilo:
|
||||
|
||||
| Platform | Integration Type | Details |
|
||||
|---|---|---|
|
||||
|
||||
@@ -243,9 +243,9 @@ General settings include:
|
||||
|---|---|---|
|
||||
| Security Agent enabled | Off until you turn it on | Turning it on queues an initial sync for the selected repository scope. |
|
||||
| Repository selection | Selected repositories during setup | Choose all accessible repositories or selected repositories. |
|
||||
| Triage model | Kilo Balanced | Used for initial triage and exploitability recommendations. |
|
||||
| Analysis model | Kilo Balanced | Used for sandbox analysis and result extraction. |
|
||||
| Remediation model | Kilo Balanced | Used by Cloud Agent for remediation PR work. |
|
||||
| Triage model | Kilo Efficient | Used for initial triage and exploitability recommendations. |
|
||||
| Analysis model | Kilo Efficient | Used for sandbox analysis and result extraction. |
|
||||
| Remediation model | Kilo Efficient | Used by Cloud Agent for remediation PR work. |
|
||||
| Analysis mode | Auto | Auto, Shallow, or Deep. |
|
||||
|
||||
#### Turn Security Agent on or off
|
||||
@@ -271,7 +271,7 @@ Security Agent uses a separate model for each stage:
|
||||
- The Analysis model runs sandbox analysis and extracts the result.
|
||||
- The Remediation model is used by Cloud Agent to prepare remediation pull requests.
|
||||
|
||||
Kilo Balanced is the default for all three stages. You can change each model independently. The model recorded in finding details is the model used when that analysis or remediation attempt ran. AI triage, sandbox analysis, and remediation consume Kilo Code credits.
|
||||
Kilo Efficient is the default for all three stages. You can change each model independently. The model recorded in finding details is the model used when that analysis or remediation attempt ran. AI triage, sandbox analysis, and remediation consume Kilo Code credits.
|
||||
|
||||
#### Choose an analysis mode
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ For NVIDIA free endpoints (Super/Ultra/etc): Trial use only - do not submit pers
|
||||
|
||||
## Auto models
|
||||
|
||||
Auto virtual models select an underlying model using tier-specific routing. Frontier uses the `x-kilocode-mode` request header. Balanced uses the API interface, Free uses deterministic affinity across available candidates, and Small uses account balance.
|
||||
Auto virtual models select an underlying model using tier-specific routing. Frontier uses the `x-kilocode-mode` request header. Efficient classifies task difficulty in session context and falls back to the API interface for its baseline model, Free uses deterministic affinity across available candidates, and Small uses account balance.
|
||||
|
||||
{% callout type="info" title="Underlying models can change" %}
|
||||
The mappings below reflect the current routing. The underlying models behind each `kilo-auto/*` tier are updated server-side as better options become available or as providers change pricing and availability — the tier IDs themselves remain stable.
|
||||
@@ -89,9 +89,9 @@ Highest performance and capability for any task. Frontier requests are sent with
|
||||
| `build`, `explore`, `code` | `anthropic/claude-sonnet-4.6` |
|
||||
| Default (no / unknown mode) | `anthropic/claude-sonnet-4.6` |
|
||||
|
||||
### `kilo-auto/balanced`
|
||||
### `kilo-auto/efficient`
|
||||
|
||||
Great balance of price and capability. The resolved model depends on the API interface used by the client.
|
||||
Session-aware routing that classifies each request by difficulty and routes to the cheapest model proven accurate enough for the task. When no confident routing decision can be made, requests fall back to a baseline model resolved by the API interface used by the client.
|
||||
|
||||
| API interface | Resolved Model | Reasoning effort |
|
||||
|---|---|---|
|
||||
@@ -132,5 +132,5 @@ curl -X POST "https://api.kilo.ai/api/gateway/chat/completions" \
|
||||
-H "Authorization: Bearer $KILO_API_KEY" \
|
||||
-H "x-kilocode-mode: plan" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model": "kilo-auto/balanced", "messages": [{"role": "user", "content": "Design a database schema"}]}'
|
||||
-d '{"model": "kilo-auto/efficient", "messages": [{"role": "user", "content": "Design a database schema"}]}'
|
||||
```
|
||||
|
||||
@@ -14,7 +14,6 @@ Auto Model is Kilo's smart routing system. Instead of selecting a specific provi
|
||||
| Tier | Name | Best For | Cost |
|
||||
|---|---|---|---|
|
||||
| `kilo-auto/frontier` | Auto Frontier | Maximum capability — routes to top-tier models for planning/architect/debug and high-quality models for coding | Paid (highest) |
|
||||
| `kilo-auto/balanced` | Auto Balanced | Strong performance at a predictably lower cost — routes every request to one fixed high-quality model | Paid |
|
||||
| `kilo-auto/efficient` | Auto Efficient | Lowest cost per task — classifies each request by difficulty and routes to the cheapest benchmark-proven model for that task | Paid (lowest) |
|
||||
| `kilo-auto/free` | Auto Free | No credits required — rotates through available free models | Free |
|
||||
|
||||
@@ -22,20 +21,14 @@ Auto Model is Kilo's smart routing system. Instead of selecting a specific provi
|
||||
The underlying models behind each tier are updated server-side as better options become available or as providers change pricing. See [kilo.ai/models](https://kilo.ai/models) for current model assignments and live pricing.
|
||||
{% /callout %}
|
||||
|
||||
## Balanced vs Efficient — What's the Difference?
|
||||
## What Makes Auto Efficient Efficient?
|
||||
|
||||
Both tiers are paid, but they optimize for different things.
|
||||
**Auto Efficient** observes your coding session in context, classifies the difficulty of each request in real time, and routes it to the *cheapest model proven accurate enough* for that specific task, based on Kilo's continuously running benchmarks. Routine tasks (small edits, lookups, quick explanations) are handled by leaner models; harder tasks (architecture, debugging, complex refactors) automatically get a more capable model.
|
||||
|
||||
**Auto Balanced** routes every request to a single, fixed high-quality model. You get consistent, strong results with predictable cost — a reliable default for most developers.
|
||||
|
||||
**Auto Efficient** goes further. It observes your coding session in context, classifies the difficulty of each request in real time, and routes it to the *cheapest model proven accurate enough* for that specific task, based on Kilo's continuously running benchmarks. Routine tasks (small edits, lookups, quick explanations) are handled by leaner models; harder tasks (architecture, debugging, complex refactors) automatically get a more capable model.
|
||||
|
||||
Efficient is also session-aware: it stays with a model across related turns and only switches when a cheaper option is clearly worth it. If it cannot make a routing decision with confidence, it falls back to Balanced — so quality never drops below Balanced.
|
||||
|
||||
Think of Efficient as Balanced with an intelligent cost optimizer layered on top.
|
||||
Efficient is also session-aware: it stays with a model across related turns and only switches when a cheaper option is clearly worth it. If it cannot make a routing decision with confidence, it falls back to a fixed, high-quality baseline model — so quality never drops below that baseline. You get consistent, strong results with predictable cost, plus an intelligent cost optimizer layered on top.
|
||||
|
||||
{% callout type="tip" %}
|
||||
For everyday coding tasks, start with **Auto Efficient** or **Auto Balanced**. Switch to **Auto Frontier** for complex architecture sessions or deep debugging where maximum capability matters.
|
||||
For everyday coding tasks, start with **Auto Efficient**. Switch to **Auto Frontier** for complex architecture sessions or deep debugging where maximum capability matters.
|
||||
{% /callout %}
|
||||
|
||||
## How to Switch Auto Models
|
||||
|
||||
@@ -161,9 +161,9 @@ A configured destination is an egress route, not tenant, organization, repositor
|
||||
|
||||
The config setting supplies the initial default for new sessions that do not have a saved preference. Use the lock button in the VS Code prompt or `/sandbox` in the CLI to change the current session. Your latest choice is saved as the default for future sessions in that project, takes precedence over the config default, and persists across restarts.
|
||||
|
||||
Each initialized session snapshots its network mode, allowed destinations, and additional writable paths. Changing config affects new sessions. The prompt control or `/sandbox` can change the current session's enabled state, but it cannot change these authority lists, and they never expand during an active session.
|
||||
Each session preserves its enabled or disabled choice. Saving changes through Kilo settings to network mode, allowed destinations, or additional writable paths refreshes existing session policies before their next tool execution. Enabling sandboxing also reads the latest settings. A tool that is already running keeps the policy it started with.
|
||||
|
||||
Forked sessions retain the source session's confinement. Subagents inherit the stricter combination of parent and child settings: sandboxing remains enabled if either requires it, deny-all wins over destination exceptions, destination lists intersect, and additional writable paths intersect.
|
||||
At creation, forked sessions retain the source session's confinement and subagents inherit the stricter combination of parent and child settings: sandboxing remains enabled if either requires it, deny-all wins over destination exceptions, destination lists intersect, and additional writable paths intersect. Later trusted sandbox settings replace those network and writable-path limits before the affected session's next tool execution.
|
||||
|
||||
Cloud sessions do not expose the local sandbox control because their tools do not run in your local sandbox.
|
||||
|
||||
|
||||
@@ -158,6 +158,6 @@ Or ask your Claw to build a custom skill from scratch — it has a built-in skil
|
||||
|
||||
## Manage inference
|
||||
|
||||
**Model picker:** Balanced is a good starting point. Frontier is more capable but significantly more expensive.
|
||||
**Model picker:** Efficient is a good starting point. Frontier is more capable but significantly more expensive.
|
||||
|
||||
You can also use your [Kilo Pass](https://kilo.ai/pricing/kilo-pass) credits — find this under **Profile** in the dashboard.
|
||||
|
||||
@@ -37,7 +37,7 @@ Depending on your setup, you can also use:
|
||||
{% image src="/docs/img/kiloclaw/profile-claw-nav.png" alt="Profile page showing Claw navigation" width="400" caption="Claw navigation in profile sidebar" /%}
|
||||
|
||||
3. Click **Create Instance**
|
||||
4. Your instance will use **Kilo Auto Balanced** as the default model. You can optionally select a different model from the dropdown — see all available models at the [Kilo Leaderboard](https://kilo.ai/leaderboard#all-models).
|
||||
4. Your instance will use **Kilo Auto Efficient** as the default model. You can optionally select a different model from the dropdown — see all available models at the [Kilo Leaderboard](https://kilo.ai/leaderboard#all-models).
|
||||
|
||||
{% image src="/docs/img/kiloclaw/create-instance.png" alt="Create instance modal with model selection" width="600" caption="Model selection during instance creation" /%}
|
||||
|
||||
|
||||
Generated
+2786
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
allowBuilds:
|
||||
core-js: true
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 821 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e9241e89fae82f69f1fab0371423ab45c905b54ff0c60ec1408b54d60f6c4c6c
|
||||
size 19636
|
||||
oid sha256:785cb43d5208977a69f03056bf16e0bbc32b29aeba1d14257848a32d19590bd7
|
||||
size 19702
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a96dd287b6ffdbdc0360baa9af99d09c3b709a5adaef18f5e7b7e43a83e44746
|
||||
size 3134
|
||||
oid sha256:950cf0a3053a283651b2232b4a4e2cfdced3c6feb2f053b0a5976f8d169cfcf1
|
||||
size 3193
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:60daae227133cedd35f93753128453177400f83233268ae19af984bc9a72ad4b
|
||||
size 5203
|
||||
oid sha256:c02aed8f52a7b22d14d2068e1f87b65bb42ed6737115dadf89615ff5a4c870e5
|
||||
size 5214
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4cda12827fa0de0590119ab707f6403fc98291843701ab0d4300e493b4b284aa
|
||||
size 14965
|
||||
oid sha256:faae43fcbe8c42aef80184b4a815bbe6a024e0345243b5b8eb1be04d62b8611f
|
||||
size 15184
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8099695f3cb2f79a54f6ada28eaf30a4e31afc5e660e293853d1ff34a2afaed1
|
||||
size 1605
|
||||
oid sha256:82ee97e40bd03a7c126e154c1644d878e374f3a573d81b22daf602a8e74ed840
|
||||
size 1596
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0bee9b58fb35c2484a9da4bffcae07f98a942c1097df33b549e13de781790824
|
||||
size 8026
|
||||
oid sha256:4d7fa00cb51b6ab591a6f25f6f863d17b78cfcd8166cf09d179ab485e394a084
|
||||
size 8149
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bf6056375e5bf9d9ad78487d58a7fc1fb2d570333d057d412f1d70893a73e5df
|
||||
size 6176
|
||||
oid sha256:1d841a283f99962a7aa3e0ecc1b24aba5a5351534e74ebe3a6794f79e6d9f374
|
||||
size 6204
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1ebd138c8d5e59bc09ce0d8d571d5598622fb61631cf5a698c80380872bef0a4
|
||||
size 7031
|
||||
oid sha256:adb4fa97f6e2dfbf1d5dd7aa58181d548c99cc0ca170f8618af72d3e6f252974
|
||||
size 7008
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b629ad25ef4f3da8bd8a9e59c64cc05279dfc262db2d8d9d25c3697f48ef8393
|
||||
size 12834
|
||||
oid sha256:8caa6d6b2d7e1b0bbab8e47a486e27bcbfd7716ef84ad8771980a658c8cc41ab
|
||||
size 13002
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b78544f9c306dbcb0278502b1f929f0af5389a90b73362abbea6f76612c5def9
|
||||
size 7761
|
||||
oid sha256:089e30f789f0f592e0ee3794dfe736fc503290d05e300830fa720a5b75046894
|
||||
size 7768
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4785426a34c06b0c74fe62c117528bb16ea579b2d677902c839336f6a29a380e
|
||||
size 5926
|
||||
oid sha256:3792bcfd82e37fd2ae9664f0abfbd50a6823be2da55b96640557d3696b490ab9
|
||||
size 6018
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e2b861de27fb8e6d64becd50bb4b3b05d748fba74b98aafaf5cb2a182ff74655
|
||||
size 26259
|
||||
oid sha256:f65d353dac1fd4bf4ddaee9f7171e348b19bb279cb220b7f745651c3d65ad821
|
||||
size 26237
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:afa789c231b370c2ba874029a8a0597b5ca7ffff5624ddd343fcd54139a94012
|
||||
size 6132
|
||||
oid sha256:4a133a9d0b13ba7effcd1dab38a1843526b7aac99a9be5df9647d1ba7fb8d858
|
||||
size 6140
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user