chore: merge main into browser worktree

This commit is contained in:
marius-kilocode
2026-08-28 19:31:44 +02:00
194 changed files with 3462 additions and 652 deletions
@@ -1,6 +0,0 @@
---
"kilo-code": minor
"@opencode-ai/ui": patch
---
Reference other Agent Manager worktrees with a searchable @ picker that prioritizes recently opened worktrees. Share paths, branches, and session IDs without attaching diffs or chat history. Keep picker results in sync when clearing search.
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Keep Agent Manager worktree spinners active while background agents are running.
@@ -1,6 +0,0 @@
---
"kilo-code": patch
"@kilocode/kilo-ui": patch
---
Load Agent Manager changes faster and preserve cached diffs when switching sessions.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Place the prompt navigator on the outer sidebar edge and delay hover previews to avoid accidental popups. Keep the navigator on the right in Agent Manager and editor tabs.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/cli": patch
---
Clear a failed turn that produced no output from the conversation when the next message is sent, so an "An error occurred" placeholder no longer lingers in history. A turn that wrote text or ran a tool before failing is kept, since its record explains changes already made.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Click a diagram in chat to open it in a resizable viewer window with zoom controls, trackpad pinch zoom, drag to pan, double click to fit and scrollbars. The diagram editor tab uses the same viewer. Copying a rendered diagram, from the viewer or from chat, now puts the picture on the clipboard.
+6
View File
@@ -0,0 +1,6 @@
---
"kilo-code": minor
"@kilocode/cli": patch
---
Edit queued messages in VS Code before they are sent, while preserving their text and attachments.
@@ -1,6 +0,0 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---
Avoid checkpoint cleanup errors when deleting worktrees under protected parent folders.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Keep queued prompt text visible while attachment context loads, and hide empty queued placeholders and internal messages.
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Keep chat scroll position during streaming, including small upward gestures, direction changes, and scrolling over message controls.
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Prevent intermittent server connection failures during VS Code startup.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Use the Git executable configured in VS Code when creating worktrees on Windows.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/sdk": patch
---
Fix a prototype pollution vulnerability in the generated SDK client request parameters (CVE-2026-48819).
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": minor
---
Show a branch-aware tip on the empty session screen: suggest running work in a worktree with a link when you're on a plain checkout, and confirm isolation when you're already in one.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Show why a turn failed instead of letting the session stop with no visible reason. The reason is written once, on the turn that failed, and Retry sits below it whenever that turn can be continued. Failures the conversation has already moved past no longer leave cards behind mid-transcript, and failed sessions are flagged in history, worktree rows, and their editor tab the same way an error or a pending question is.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Show a warning when a provider ends a response without signalling that it finished, instead of quietly returning the session to idle. The warning also appears when reopening the session.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": minor
---
Stop treating a manually stopped session as a failure, and add a Retry action to failed turns. Pressing Stop now shows a short "Stopped" note instead of an error badge and attention dot. Retry continues the failed turn where it stopped, keeping the conversation and any file changes it already made, and runs with the model and effort selected at that moment — so switching away from an unavailable provider and pressing Retry picks the new one up. This includes failures that never produced a reply, such as missing provider credentials.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Label the Kilo tool window's create buttons as + Session and + Worktree with a compact plus icon.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Detect a worktree's pull request reliably in Agent Manager. Imported PRs — including PRs from forks — hand-made worktrees, and locally renamed branches now show their PR badge, the current repository row gets one too, and a freshly imported PR no longer waits out the status poll. Imported PR branches also get proper git tracking, so `git push` and `git pull` work in the new worktree.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": minor
---
Add a toggle to show or hide the session list in a worktree editor tab. The choice is remembered per worktree, and while the list is hidden the toggle shows the session count or flags a session that needs your attention.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Stop the New Worktree dialog from flashing the previous tab's content when switching between New, From PR, and From Branch.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": minor
---
Render Mermaid code fences as inline diagrams in JetBrains chat markdown, and open any diagram in its own editor tab with Diagram and Source views.
-7
View File
@@ -1,7 +0,0 @@
---
"@kilocode/cli": patch
"@opencode-ai/core": patch
"@kilocode/kilo-indexing": patch
---
Fix ReDoS vulnerabilities in glob matching by upgrading minimatch to 10.2.6.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-docs": patch
---
Update nanoid to 3.3.18 to fix the infinite loop vulnerability flagged by Dependabot.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Open files by clicking their names in read tool headers.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/cli": patch
---
Deduplicate the plan/ask/architect permission ruleset so denial messages and permission payloads no longer show stacked copies of the same rule block.
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Keep focus in active text fields when questions appear or refresh.
@@ -1,6 +0,0 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---
Remove deleted worktree checkpoints without losing conversation history and stop showing activity for deleted sessions.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": minor
---
Choose a separate model for conversation compaction in Context settings.
-7
View File
@@ -1,7 +0,0 @@
---
"kilo-code": patch
"@kilocode/kilo-ui": patch
"@opencode-ai/ui": patch
---
Stop offscreen loading animations while preserving their original appearance, and release obsolete transcript pages from memory.
@@ -1,6 +0,0 @@
---
"kilo-code": patch
"@kilocode/cli": patch
---
Search the latest 5,000 chats across the worktree family and display the best 50 matches in the sidebar and Agent Manager. Skip inaccessible folders from unrelated projects when finding past chats.
@@ -1,6 +0,0 @@
---
"@kilocode/cli": minor
"kilo-code": minor
---
Review all committed and uncommitted Agent Manager worktree changes with `/review worktree`.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/cli": patch
---
Prevent automatic indexing of home directories and filesystem roots, and show a warning to open a project folder instead.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Show consistent running, completion, and input-required indicators across session tabs and Agent Manager worktrees.
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Recognize sessions discovered in managed Agent Manager worktrees during orchestration actions.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/cli": patch
---
Resume pending requests after automatic compaction without replaying requests that already completed.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/cli": patch
---
Fix the task tool intermittently returning an empty result. Subagents that ran with memory context had a synthetic marker part appended after their answer, which was picked up as the final text part and surfaced as an empty `<task_result>` to the parent agent. The task tool now ignores synthetic, ignored, and empty text parts, and background jobs no longer let an empty run overwrite an earlier successful result, so resumed tasks keep their real output.
+1 -1
View File
@@ -13,7 +13,7 @@ on:
jobs:
build:
name: Build docs site
runs-on: blacksmith-4vcpu-ubuntu-2404 # kilocode_change
runs-on: ${{ github.repository == 'Kilo-Org/kilocode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} # kilocode_change
steps:
- name: Checkout repository
uses: actions/checkout@v6 # kilocode_change
+3 -3
View File
@@ -16,7 +16,7 @@ env:
jobs:
changes:
name: detect JetBrains changes
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'Kilo-Org/kilocode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} # kilocode_change
outputs:
jetbrains: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.filter.outputs.jetbrains }}
steps:
@@ -43,7 +43,7 @@ jobs:
name: jetbrains
needs: changes
if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.jetbrains == 'true'
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'Kilo-Org/kilocode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} # kilocode_change
defaults:
run:
shell: bash
@@ -104,7 +104,7 @@ jobs:
- changes
- unit
if: always()
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'Kilo-Org/kilocode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} # kilocode_change
steps:
- name: Verify JetBrains jobs passed
run: |
+1 -1
View File
@@ -21,7 +21,7 @@ on:
jobs:
unit:
name: unit tests
runs-on: blacksmith-4vcpu-ubuntu-2404 # kilocode_change
runs-on: ${{ github.repository == 'Kilo-Org/kilocode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} # kilocode_change
defaults:
run:
shell: bash
+4 -4
View File
@@ -11,7 +11,7 @@ jobs:
# kilocode_change start
changes:
name: detect typecheck changes
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'Kilo-Org/kilocode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} # kilocode_change
outputs:
javascript: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.filter.outputs.javascript }}
jetbrains: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.filter.outputs.jetbrains }}
@@ -45,7 +45,7 @@ jobs:
name: typecheck-js
needs: changes # kilocode_change
if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.javascript == 'true' # kilocode_change
runs-on: blacksmith-4vcpu-ubuntu-2404 # kilocode_change
runs-on: ${{ github.repository == 'Kilo-Org/kilocode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} # kilocode_change
steps:
- name: Checkout repository
uses: actions/checkout@v6 # kilocode_change
@@ -66,7 +66,7 @@ jobs:
name: typecheck-jetbrains
needs: changes
if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.jetbrains == 'true'
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'Kilo-Org/kilocode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} # kilocode_change
steps:
- name: Checkout repository
uses: actions/checkout@v6
@@ -91,7 +91,7 @@ jobs:
required:
name: typecheck
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'Kilo-Org/kilocode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} # kilocode_change
needs:
- typecheck-js
- changes
+2 -2
View File
@@ -59,7 +59,7 @@ jobs:
needs: check-paths
if: needs.check-paths.outputs.matched == 'true'
name: Visual Regression (kilo-ui) # kilocode_change
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'Kilo-Org/kilocode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} # kilocode_change
timeout-minutes: 15
steps:
@@ -221,7 +221,7 @@ jobs:
needs: check-paths
if: needs.check-paths.outputs.matched == 'true'
name: Visual Regression (kilo-vscode webview) # kilocode_change
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'Kilo-Org/kilocode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} # kilocode_change
timeout-minutes: 15
env:
NODE_OPTIONS: --max-old-space-size=4096
+1 -1
View File
@@ -21,5 +21,5 @@
"@types/react-dom": "^19.2.3",
"typescript": "^5.8.2"
},
"version": "7.5.5"
"version": "7.5.6"
}
+33 -33
View File
@@ -32,7 +32,7 @@
},
"packages/client": {
"name": "@opencode-ai/client",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@opencode-ai/protocol": "workspace:*",
"@opencode-ai/schema": "workspace:*",
@@ -56,7 +56,7 @@
},
"packages/codemode": {
"name": "@opencode-ai/codemode",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -70,7 +70,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "7.5.5",
"version": "7.5.6",
"bin": {
"opencode": "./bin/opencode",
},
@@ -168,7 +168,7 @@
},
"packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -182,7 +182,7 @@
},
"packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"effect": "catalog:",
},
@@ -194,7 +194,7 @@
},
"packages/http-recorder": {
"name": "@opencode-ai/http-recorder",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@effect/platform-node": "4.0.0-beta.83",
"@effect/platform-node-shared": "4.0.0-beta.83",
@@ -215,7 +215,7 @@
},
"packages/httpapi-codegen": {
"name": "@opencode-ai/httpapi-codegen",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"effect": "catalog:",
"prettier": "3.6.2",
@@ -228,7 +228,7 @@
},
"packages/kilo-console": {
"name": "@kilocode/kilo-console",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@kilocode/kilo-indexing": "workspace:*",
"@kilocode/kilo-web-ui": "workspace:*",
@@ -251,7 +251,7 @@
},
"packages/kilo-docs": {
"name": "@kilocode/kilo-docs",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@docsearch/css": "^4",
"@docsearch/js": "^4",
@@ -281,7 +281,7 @@
},
"packages/kilo-gateway": {
"name": "@kilocode/kilo-gateway",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/openai": "3.0.88",
@@ -315,7 +315,7 @@
},
"packages/kilo-i18n": {
"name": "@kilocode/kilo-i18n",
"version": "7.5.5",
"version": "7.5.6",
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
@@ -325,7 +325,7 @@
},
"packages/kilo-indexing": {
"name": "@kilocode/kilo-indexing",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@aws-sdk/client-bedrock-runtime": "3.1005.0",
"@aws-sdk/credential-provider-ini": "3.972.31",
@@ -357,11 +357,11 @@
},
"packages/kilo-jetbrains": {
"name": "@kilocode/kilo-jetbrains",
"version": "7.4.23",
"version": "7.5.5",
},
"packages/kilo-memory": {
"name": "@kilocode/kilo-memory",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"effect": "catalog:",
"zod": "catalog:",
@@ -375,7 +375,7 @@
},
"packages/kilo-sandbox": {
"name": "@kilocode/sandbox",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@anthropic-ai/sandbox-runtime": "catalog:",
"effect": "catalog:",
@@ -390,7 +390,7 @@
},
"packages/kilo-telemetry": {
"name": "@kilocode/kilo-telemetry",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@kilocode/kilo-gateway": "workspace:*",
"posthog-node": "4.4.0",
@@ -404,7 +404,7 @@
},
"packages/kilo-ui": {
"name": "@kilocode/kilo-ui",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "0.13.11",
@@ -442,7 +442,7 @@
},
"packages/kilo-vscode": {
"name": "kilo-code",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@kilocode/kilo-gateway": "workspace:*",
@@ -519,7 +519,7 @@
},
"packages/kilo-web-ui": {
"name": "@kilocode/kilo-web-ui",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@kilocode/kilo-ui": "workspace:*",
"@kobalte/core": "catalog:",
@@ -536,7 +536,7 @@
},
"packages/llm": {
"name": "@opencode-ai/llm",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14",
@@ -555,7 +555,7 @@
},
"packages/opencode": {
"name": "@kilocode/cli",
"version": "7.5.5",
"version": "7.5.6",
"bin": {
"kilo": "./bin/kilo",
"kilocode": "./bin/kilo",
@@ -723,7 +723,7 @@
},
"packages/plugin": {
"name": "@kilocode/plugin",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@kilocode/sdk": "workspace:*",
@@ -752,7 +752,7 @@
},
"packages/plugin-atomic-chat": {
"name": "@kilocode/plugin-atomic-chat",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@kilocode/plugin": "workspace:*",
},
@@ -766,7 +766,7 @@
},
"packages/protocol": {
"name": "@opencode-ai/protocol",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"effect": "catalog:",
@@ -779,7 +779,7 @@
},
"packages/schema": {
"name": "@opencode-ai/schema",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"effect": "catalog:",
},
@@ -791,7 +791,7 @@
},
"packages/script": {
"name": "@opencode-ai/script",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"semver": "^7.6.3",
},
@@ -802,7 +802,7 @@
},
"packages/sdk-next": {
"name": "@opencode-ai/sdk-next",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@opencode-ai/client": "workspace:*",
"@opencode-ai/core": "workspace:*",
@@ -817,7 +817,7 @@
},
"packages/sdk/js": {
"name": "@kilocode/sdk",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -832,7 +832,7 @@
},
"packages/server": {
"name": "@opencode-ai/server",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
@@ -847,7 +847,7 @@
},
"packages/session-ui": {
"name": "@opencode-ai/session-ui",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "catalog:",
@@ -892,7 +892,7 @@
},
"packages/storybook": {
"name": "@opencode-ai/storybook",
"version": "7.5.5",
"version": "7.5.6",
"devDependencies": {
"@opencode-ai/session-ui": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -917,7 +917,7 @@
},
"packages/tui": {
"name": "@opencode-ai/tui",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@kilocode/plugin": "workspace:*",
"@kilocode/sdk": "workspace:*",
@@ -943,7 +943,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "catalog:",
+1 -1
View File
@@ -177,6 +177,6 @@
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
"solid-js@1.9.12": "patches/solid-js@1.9.12.patch"
},
"version": "7.5.5",
"version": "7.5.6",
"peerDependencies": {}
}
+1 -1
View File
@@ -38,5 +38,5 @@
"@typescript/native-preview": "catalog:",
"effect": "catalog:"
},
"version": "7.5.5"
"version": "7.5.6"
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/codemode",
"version": "7.5.5",
"version": "7.5.6",
"description": "Effect-native confined code execution over schema-described tools",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.5.5",
"version": "7.5.6",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.5.5",
"version": "7.5.6",
"name": "@opencode-ai/effect-drizzle-sqlite",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.5.5",
"version": "7.5.6",
"name": "@opencode-ai/effect-sqlite-node",
"type": "module",
"license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "kilo"
name = "Kilo"
description = "The open source coding agent."
version = "7.5.5"
version = "7.5.6"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/Kilo-Org/kilocode"
@@ -11,26 +11,26 @@ name = "Kilo"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.5.5/opencode-darwin-arm64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.5.6/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.5.5/opencode-darwin-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.5.6/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.5.5/opencode-linux-arm64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.5.6/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.5.5/opencode-linux-x64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.5.6/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.5.5/opencode-windows-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.5.6/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.5.5",
"version": "7.5.6",
"name": "@opencode-ai/http-recorder",
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
"type": "module",
+1 -1
View File
@@ -20,5 +20,5 @@
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:"
},
"version": "7.5.5"
"version": "7.5.6"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-console",
"version": "7.5.5",
"version": "7.5.6",
"private": true,
"type": "module",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-docs",
"version": "7.5.5",
"version": "7.5.6",
"private": true,
"scripts": {
"dev": "next dev --webpack --port 3002",
@@ -220,6 +220,8 @@ The companion `agent_manager_models` tool searches models and their supported re
The same tool also manages existing sessions. It can return an overview of sections, worktrees, and local sessions, send a prompt to one managed session, stop a managed session, or move a session's worktree into a section. The overview includes section IDs, each section's assigned worktrees, worktree IDs, and session IDs. Use those exact IDs for a subsequent move. Moving accepts a section ID from the overview, or `null` to ungroup the worktree. Moving a session moves its whole worktree, including multi-version siblings. Local sessions cannot be assigned to a section. Stopping aborts the session's active work and removes it from the panel, just like closing the session tab.
Prompts to busy or retrying sessions enter the same queue as follow-up messages sent from chat. The tool returns when the prompt is accepted, without waiting for it to run or finish. Sessions with pending questions or permission requests still refuse prompts. Answer the question with `action: "answer"`, or resolve the permission request in Agent Manager, before prompting again.
The tool uses the `agent_manager` permission. Approval prompts are scoped to the requested capability, so approving `worktree` does not automatically approve `local`, an overview, or a targeted prompt. Prompting an existing managed session requires an explicit `prompt` approval the first time, even if Agent Manager session creation was previously approved broadly. Stopping a session likewise requires an explicit `stop` approval, and moving a worktree requires an explicit `move` approval.
## Sections
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a356afcc1d81da64ec5b498840abeadc77c44bb2da9cebbb3ffff99dd7183d6
size 9020
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6c802176dc42720d51bb0cccaea93c863c98b2886c46600daa98cd13ff121ee6
size 3827
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e4b01ca8648de21f8847bdfd8d166f028d1924e9534ac50bb64f520060a92ecf
size 6391
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ebd62d9dda76da0d8e430ed7c81e5cb66c3233bc6193f4aa077bd1d6d53fbef6
size 5370
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-gateway",
"version": "7.5.5",
"version": "7.5.6",
"type": "module",
"license": "MIT",
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-i18n",
"version": "7.5.5",
"version": "7.5.6",
"type": "module",
"license": "MIT",
"description": "Kilo-specific i18n translations and overrides",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-indexing",
"version": "7.5.5",
"version": "7.5.6",
"type": "module",
"license": "MIT",
"description": "Standalone indexing engine and host helpers for Kilo Code",
+49
View File
@@ -1,5 +1,33 @@
# Changelog
## 7.6.0
### Minor Changes
- [#13521](https://github.com/Kilo-Org/kilocode/pull/13521) [`a453e2d`](https://github.com/Kilo-Org/kilocode/commit/a453e2d231fd2275d6a0ac84bb17e3e888e29abb) - Show a branch-aware tip on the empty session screen: suggest running work in a worktree with a link when you're on a plain checkout, and confirm isolation when you're already in one.
- [#13482](https://github.com/Kilo-Org/kilocode/pull/13482) [`ec94fdc`](https://github.com/Kilo-Org/kilocode/commit/ec94fdc791ea52fb1faee1ec21536fc58a2f29bf) - Stop treating a manually stopped session as a failure, and add a Retry action to failed turns. Pressing Stop now shows a short "Stopped" note instead of an error badge and attention dot. Retry continues the failed turn where it stopped, keeping the conversation and any file changes it already made, and runs with the model and effort selected at that moment — so switching away from an unavailable provider and pressing Retry picks the new one up. This includes failures that never produced a reply, such as missing provider credentials.
- [#13521](https://github.com/Kilo-Org/kilocode/pull/13521) [`dfaa30b`](https://github.com/Kilo-Org/kilocode/commit/dfaa30b0b0fc79d2d048da6b8c94039c9d94476f) - Add a toggle to show or hide the session list in a worktree editor tab. The choice is remembered per worktree, and while the list is hidden the toggle shows the session count or flags a session that needs your attention.
- [#13396](https://github.com/Kilo-Org/kilocode/pull/13396) [`b9d4d87`](https://github.com/Kilo-Org/kilocode/commit/b9d4d874f76c034626e628a29f50a82b028901d0) - Render Mermaid code fences as inline diagrams in JetBrains chat markdown, and open any diagram in its own editor tab with Diagram and Source views.
### Patch Changes
- [#13396](https://github.com/Kilo-Org/kilocode/pull/13396) [`54b7225`](https://github.com/Kilo-Org/kilocode/commit/54b7225c4d30775cd5918aa77e410fc55889221f) - Click a diagram in chat to open it in a resizable viewer window with zoom controls, trackpad pinch zoom, drag to pan, double click to fit and scrollbars. The diagram editor tab uses the same viewer. Copying a rendered diagram, from the viewer or from chat, now puts the picture on the clipboard.
- [#13520](https://github.com/Kilo-Org/kilocode/pull/13520) [`bac3e7a`](https://github.com/Kilo-Org/kilocode/commit/bac3e7a7a83bdcc0ec8b93ede2fa49adb88fd133) - Show why a turn failed instead of letting the session stop with no visible reason. The reason is written once, on the turn that failed, and Retry sits below it whenever that turn can be continued. Failures the conversation has already moved past no longer leave cards behind mid-transcript, and failed sessions are flagged in history, worktree rows, and their editor tab the same way an error or a pending question is.
- [#13520](https://github.com/Kilo-Org/kilocode/pull/13520) [`b3f68e0`](https://github.com/Kilo-Org/kilocode/commit/b3f68e0a2ac9ecf44e3dadfec54e41f6dd1f8273) - Show a warning when a provider ends a response without signalling that it finished, instead of quietly returning the session to idle. The warning also appears when reopening the session.
- [#13521](https://github.com/Kilo-Org/kilocode/pull/13521) [`d96b1b3`](https://github.com/Kilo-Org/kilocode/commit/d96b1b3c3d2a5424b0d5a5ecb299731cd1e88a37) - Label the Kilo tool window's create buttons as + Session and + Worktree with a compact plus icon.
- [#13440](https://github.com/Kilo-Org/kilocode/pull/13440) [`85e13e5`](https://github.com/Kilo-Org/kilocode/commit/85e13e59fcb7e9cdc4d717b50e9ef92ec3e54471) - Detect a worktree's pull request reliably in Agent Manager. Imported PRs — including PRs from forks — hand-made worktrees, and locally renamed branches now show their PR badge, the current repository row gets one too, and a freshly imported PR no longer waits out the status poll. Imported PR branches also get proper git tracking, so `git push` and `git pull` work in the new worktree.
- [#13440](https://github.com/Kilo-Org/kilocode/pull/13440) [`c3066e9`](https://github.com/Kilo-Org/kilocode/commit/c3066e988b0d8a0b56289f410f0f2a8de640fe3d) - Stop the New Worktree dialog from flashing the previous tab's content when switching between New, From PR, and From Branch.
- [#13525](https://github.com/Kilo-Org/kilocode/pull/13525) [`66e6871`](https://github.com/Kilo-Org/kilocode/commit/66e687169973e917e415a6401586a24cdcf6a79a) - Stop showing a false "Git is not installed" warning for worktrees that were deleted from disk
## 7.4.24
### Patch Changes
@@ -316,6 +344,27 @@
## [Unreleased]
## [7.1.1] - 2026-08-28
### Added
- Render Mermaid diagrams directly in JetBrains chat, with editor-tab viewing, zoom, pan, fit-to-view, source view, and PNG copy support.
- Add clearer JetBrains worktree session empty states, labeled create actions, and a persisted toggle for showing or hiding the worktree session list.
### Fixed
- Retry failed JetBrains turns by continuing the original request, keeping transcript and workspace context intact instead of rolling back or appending a placeholder prompt.
- Surface JetBrains failed-turn errors more clearly and hide the failure card as soon as Retry starts.
- Prevent compaction recovery from replaying requests incorrectly, looping, or dropping pending tool progress in Kilo Core.
- Avoid unsafe indexing and checkpoint cleanup behavior in filesystem roots, home directories, and protected parent directories.
- Return real subagent results instead of empty task responses in Kilo Core.
- Avoid duplicated plan-mode permission rule stacking.
### Changed
- Update the pinned Kilo Core CLI used by JetBrains from 7.4.23 to 7.5.5.
- Improve JetBrains worktree session guidance with branch-aware copy and localized empty-state text.
## [7.1.1-rc.1] - 2026-08-27
### Added
@@ -152,13 +152,43 @@ class KiloWorktreeRpcApiImpl : KiloWorktreeRpcApi {
override suspend fun stats(directory: String): WorktreeStatsListDto = withContext(Dispatchers.IO) {
val root = Path.of(directory).normalize()
val res = runGit(root, "worktree", "list", "--porcelain")
if (!res.ok) return@withContext WorktreeStatsListDto()
val items = managedWorktrees(parseWorktreeList(res.stdout))
val items = sync(root) ?: return@withContext WorktreeStatsListDto()
val fallback = baseBranch(items) ?: "HEAD"
WorktreeStatsListDto(parallel(items.filter { !it.main }) { item -> stats(item, fallback) })
}
/**
* Lists the managed worktrees of [root] after reconciling git's metadata with the disk, so
* callers never probe a directory that no longer exists. Returns null when [root] itself is gone
* or git cannot list.
*
* Dropping gone entries from the result is what makes probing safe; the prune is only metadata
* hygiene. So the prune runs exclusively when a Kilo-managed worktree is the stale one, and a
* mis-parse can at worst skip it — git re-checks every entry on disk and only ever removes
* `$GIT_DIR/worktrees` bookkeeping for a checkout it finds missing, never any files, and never a
* locked worktree (the documented guard for worktrees on unmounted volumes).
*/
private fun sync(root: Path): List<WorktreeDto>? {
if (!Files.isDirectory(root)) {
LOG.info("worktree sync skipped, directory does not exist: $root")
return null
}
val res = runGit(root, "worktree", "list", "--porcelain")
if (!res.ok) return null
val raw = parseWorktreeList(res.stdout)
val stale = staleWorktrees(raw)
val synced = if (stale.isEmpty()) managedWorktrees(raw) else {
LOG.info("worktree sync pruning stale managed worktrees: ${stale.joinToString(", ") { it.path }}")
val prune = runGit(root, "worktree", "prune", "-v")
if (!prune.ok) LOG.warn("worktree prune during sync failed: exit=${prune.exit} stderr=${snippet(prune.stderr)}")
if (prune.ok && prune.stdout.isNotBlank()) LOG.info("worktree sync pruned: ${snippet(prune.stdout)}")
val again = runGit(root, "worktree", "list", "--porcelain")
if (!again.ok) return null
managedWorktrees(parseWorktreeList(again.stdout))
}
return synced.filter { Files.isDirectory(Path.of(it.path)) }
}
override suspend fun ghStatus(directory: String): GhAvailability = withContext(Dispatchers.IO) {
probeGh(Path.of(directory).normalize(), "rpc")
}
@@ -167,11 +197,17 @@ class KiloWorktreeRpcApiImpl : KiloWorktreeRpcApi {
val now = System.currentTimeMillis()
prs[directory]?.takeIf { now - it.time < PR_TTL }?.let { return@withContext it.value }
val root = Path.of(directory).normalize()
// A gone directory reports nothing and is not cached, so a real availability problem found
// from a live directory still reaches the UI.
if (!Files.isDirectory(root)) {
LOG.info("pr status skipped, directory does not exist: $root")
return@withContext WorktreePrListDto()
}
val available = ghAvailable(root)
if (available != GhAvailability.OK) return@withContext WorktreePrListDto(available).also { prs[directory] = Timed(now, it) }
val res = runGit(root, "worktree", "list", "--porcelain")
if (!res.ok) return@withContext WorktreePrListDto().also { prs[directory] = Timed(now, it) }
val all = managedWorktrees(parseWorktreeList(res.stdout))
// Sync the worktree list before the per-worktree lookups so a worktree that was added and
// then deleted on disk is pruned instead of resolved from a directory that no longer exists.
val all = sync(root) ?: return@withContext WorktreePrListDto().also { prs[directory] = Timed(now, it) }
val items = prTargets(all)
val base = baseBranch(all)
var status = GhAvailability.OK
@@ -192,6 +228,10 @@ class KiloWorktreeRpcApiImpl : KiloWorktreeRpcApi {
val now = System.currentTimeMillis()
branches[directory]?.takeIf { now - it.time < PR_TTL }?.let { return@withContext it.value }
val root = Path.of(directory).normalize()
if (!Files.isDirectory(root)) {
LOG.info("branch status skipped, directory does not exist: $root")
return@withContext BranchStatusDto()
}
val branch = runGit(root, "branch", "--show-current").stdout.trim()
val worktree = isLinkedWorktree(root)
val availability = ghAvailable(root)
@@ -632,6 +672,10 @@ class KiloWorktreeRpcApiImpl : KiloWorktreeRpcApi {
}
private fun ghAvailable(root: Path): GhAvailability {
if (!Files.isDirectory(root)) {
LOG.info("gh availability skipped dir=$root missing=true")
return GhAvailability.OK
}
val status = probeGh(root, "availability")
if (status != GhAvailability.MISSING) return status
val now = System.currentTimeMillis()
@@ -643,6 +687,13 @@ class KiloWorktreeRpcApiImpl : KiloWorktreeRpcApi {
}
private fun probeGh(root: Path, reason: String): GhAvailability = synchronized(ghLock) {
// A stale/removed worktree directory makes the process spawn fail, which would be
// misreported as GIT_MISSING. Treat a missing directory as "nothing to report" and
// don't cache it, so the next probe on a real directory still runs.
if (!Files.isDirectory(root)) {
LOG.info("gh probe skipped reason=$reason dir=$root missing=true")
return@synchronized GhAvailability.OK
}
val now = System.currentTimeMillis()
ghCache?.takeIf { now - it.time < GH_STATUS_TTL }?.let {
LOG.info("gh probe cache hit reason=$reason value=${it.value}")
@@ -652,6 +703,12 @@ class KiloWorktreeRpcApiImpl : KiloWorktreeRpcApi {
LOG.info("gh probe start reason=$reason dir=$root")
val git = runGit(root, "--version")
if (!git.ok) {
// The directory can disappear between the check above and the spawn; a failed working
// directory is not evidence that git is uninstalled, so report nothing in that case.
if (badDir(git.stderr)) {
LOG.info("gh probe skipped reason=$reason dir=$root badDir=true stderr=${snippet(git.stderr)}")
return@synchronized GhAvailability.OK
}
val value = GhAvailability.GIT_MISSING
ghCache = Timed(System.currentTimeMillis(), value)
LOG.info("gh probe result reason=$reason value=$value exit=${git.exit} ms=${System.currentTimeMillis() - start} stderr=${snippet(git.stderr)}")
@@ -670,6 +727,12 @@ class KiloWorktreeRpcApiImpl : KiloWorktreeRpcApi {
}
/** True when a process failed because its working directory is gone, not because the tool is absent. */
internal fun badDir(text: String): Boolean {
val msg = text.lowercase()
return msg.contains("working directory") && (msg.contains("does not exist") || msg.contains("not a directory"))
}
internal fun classifyGhError(text: String): GhAvailability {
val msg = text.lowercase()
if (msg.contains("not logged") || msg.contains("gh auth login") || msg.contains("authentication")) return GhAvailability.UNAUTH
@@ -841,6 +904,21 @@ internal fun managedWorktrees(items: List<WorktreeDto>): List<WorktreeDto> {
}
}
/**
* Kilo-managed worktrees under `.kilo/worktrees/` whose checkout is gone. This is the only reason
* [KiloWorktreeRpcApiImpl.sync] runs a prune, so a stale worktree the user keeps somewhere else is
* never a reason for the plugin to touch git's administrative files on a polling loop.
*/
internal fun staleWorktrees(items: List<WorktreeDto>): List<WorktreeDto> {
val main = items.firstOrNull { it.main } ?: return emptyList()
val storage = Path.of(main.path).normalize().resolve(".kilo").resolve("worktrees").normalize()
return items.filter { item ->
if (item.main) return@filter false
if (Path.of(item.path).normalize().parent != storage) return@filter false
item.prunable || !Files.isDirectory(Path.of(item.path))
}
}
/**
* Worktrees eligible for a PR lookup. The main working tree is included — it can sit on a PR branch
* just like a linked worktree — while detached heads have no branch to resolve and prunable entries
@@ -37,6 +37,77 @@ class KiloWorktreeRpcApiImplTest {
assertFalse(api.open(repo.resolve("missing").toString()))
}
@Test
fun `ghStatus does not report git missing for a removed directory`() = runBlocking {
assertEquals(GhAvailability.OK, api.ghStatus(repo.resolve("missing").toString()))
}
@Test
fun `prStatus does not report git missing for a removed directory`() = runBlocking {
assertEquals(GhAvailability.OK, api.prStatus(repo.resolve("missing").toString()).availability)
}
@Test
fun `branch status skips a missing directory without caching the empty result`() = runBlocking {
initRepo()
val dir = repo.resolve(".kilo").resolve("worktrees").resolve("late")
assertEquals("", api.branchStatus(dir.toString()).branch)
git(repo, "worktree", "add", "-b", "feature/x", dir.toString())
assertEquals("feature/x", api.branchStatus(dir.toString()).branch)
}
@Test
fun `stats syncs away a worktree deleted from disk`() = runBlocking {
initRepo()
val created = assertNotNull(api.create(repo.toString(), CreateWorktreeRequestDto("feature/x")).worktree)
assertTrue(api.stats(repo.toString()).items.any { it.path == created.path })
delete(Path.of(created.path))
assertTrue(api.stats(repo.toString()).items.none { it.path == created.path })
// The stale entry is pruned from git metadata, so later probes never target the gone directory.
val listed = output(repo, "worktree", "list", "--porcelain")
assertFalse(listed.contains(created.path), "stale worktree should be pruned during sync: $listed")
}
@Test
fun `stats leaves a gone worktree outside the kilo storage registered`() = runBlocking {
initRepo()
val outside = remote.resolve("elsewhere")
git(repo, "worktree", "add", "-b", "feature/outside", outside.toString())
delete(outside)
// The gone entry is excluded from the probe targets, but pruning someone else's worktree
// metadata is not the plugin's business, so git's bookkeeping is left untouched.
assertTrue(api.stats(repo.toString()).items.none { it.path == outside.toString() })
val listed = output(repo, "worktree", "list", "--porcelain")
assertTrue(listed.contains(outside.toString()), "unmanaged worktree must not be pruned: $listed")
}
@Test
fun `staleWorktrees only reports gone worktrees inside the kilo storage`() {
val main = WorktreeDto("/repo", "repo", "main", "/repo", main = true)
val managed = WorktreeDto(
"/repo/.kilo/worktrees/gone",
"gone",
"feature/gone",
"/repo/.kilo/worktrees/gone",
prunable = true,
)
val outside = WorktreeDto("/elsewhere/gone", "gone", "feature/other", "/elsewhere/gone", prunable = true)
assertEquals(listOf(managed.path), staleWorktrees(listOf(main, managed, outside)).map { it.path })
assertTrue(staleWorktrees(listOf(main, outside)).isEmpty())
}
@Test
fun `badDir detects a missing working directory spawn failure`() {
assertTrue(badDir("Cannot start a process, the working directory '/tmp/gone' does not exist"))
assertFalse(badDir("Cannot run program \"git\": error=2, No such file or directory"))
}
@Test
fun `parseWorktreeList reads porcelain output and flags the main tree`() {
val raw = """
+1 -1
View File
@@ -1,5 +1,5 @@
kotlin.stdlib.default.dependency=false
kilo.jetbrains.version=7.1.1-rc.1
kilo.jetbrains.version=7.1.1
# When true (default) the JetBrains plugin uses the pinned CLI release from package.json.
# Set to false ONLY for local dev: generate the client from local source + bundle the local binary.
# false is NOT releasable -- production builds fail unless this is true.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-memory",
"version": "7.5.5",
"version": "7.5.6",
"type": "module",
"license": "MIT",
"description": "Project memory storage, indexing, recall, and command helpers for Kilo Code",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/sandbox",
"version": "7.5.5",
"version": "7.5.6",
"type": "module",
"license": "MIT",
"private": true,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-telemetry",
"version": "7.5.5",
"version": "7.5.6",
"type": "module",
"license": "MIT",
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-ui",
"version": "7.5.5",
"version": "7.5.6",
"type": "module",
"license": "MIT",
"exports": {
@@ -752,6 +752,8 @@ export function UserMessageDisplay(props: {
text?: string
copyText?: string
header?: JSX.Element
edit?: { label: string; onClick: () => void; disabled?: boolean }
queuedDisabled?: boolean
onDelete?: () => void
onFork?: () => void
onRevert?: () => void
@@ -835,6 +837,7 @@ export function UserMessageDisplay(props: {
icon="close-small"
size="normal"
variant="ghost"
disabled={props.queuedDisabled}
onMouseDown={(e) => e.preventDefault()}
onClick={(event) => {
event.stopPropagation()
@@ -846,6 +849,28 @@ export function UserMessageDisplay(props: {
</Show>
)
const Edit = () => (
<Show when={props.edit}>
{(edit) => (
<Tooltip value={edit().label} placement="right" gutter={4}>
<IconButton
data-slot="user-message-edit"
icon="edit"
size="small"
variant="ghost"
disabled={props.queuedDisabled || edit().disabled}
onMouseDown={(event) => event.preventDefault()}
onClick={(event) => {
event.stopPropagation()
edit().onClick()
}}
aria-label={edit().label}
/>
</Tooltip>
)}
</Show>
)
return (
<GrowBox animate={!!props.animate} fade class="w-full min-w-0 self-stretch max-w-full">
<div data-component="user-message" data-interrupted={props.interrupted ? "" : undefined}>
@@ -885,6 +910,7 @@ export function UserMessageDisplay(props: {
<Show when={!text() && !props.header && props.queued}>
<div data-slot="user-message-queued-indicator">
<TextShimmer text={i18n.t("ui.message.queued")} />
<Edit />
<Delete />
</div>
</Show>
@@ -900,6 +926,7 @@ export function UserMessageDisplay(props: {
<GrowBox animate={!!props.animate} open={!!props.queued}>
<div data-slot="user-message-queued-indicator">
<TextShimmer text={i18n.t("ui.message.queued")} />
<Edit />
<Delete />
</div>
</GrowBox>
+33 -19
View File
@@ -2,6 +2,18 @@
This file provides guidance to agents when working with code in this repository.
## Start Here
Paths below are relative to this package. Start with the relevant implementation and nearby tests; the sections below retain the architecture and safety details.
| Area | Entry points |
|---|---|
| CLI connection and process lifecycle | [connection-service.ts](src/services/cli-backend/connection-service.ts), [server-manager.ts](src/services/cli-backend/server-manager.ts) |
| Host/webview messages | [host handlers](src/kilo-provider/), [message contracts](webview-ui/src/types/messages/) |
| Agent Manager and project routing | [host](src/agent-manager/), [project routing/settings](src/agent-manager/project/), [webview](webview-ui/agent-manager/) |
| Webview composition and styles | [provider-shell.tsx](webview-ui/src/context/provider-shell.tsx), [styles](webview-ui/src/styles/) |
| Tests | [unit tests](tests/unit/), [visual regression](tests/visual-regression.spec.ts) |
## Product Context
Kilo Code is an open source AI coding agent platform. It ships as a CLI and editor clients that all build on the same backend. This package (`packages/kilo-vscode/`) is the **VS Code extension**.
@@ -10,14 +22,14 @@ Kilo Code is an open source AI coding agent platform. It ships as a CLI and edit
All products are thin clients over the **CLI** (`packages/opencode/`, published as `@kilocode/cli`). The CLI is a fork of upstream [OpenCode](https://github.com/anomalyco/opencode) with Kilo-specific additions (gateway auth, telemetry, migration, code review, branding). It contains the full AI agent runtime, tool execution, session management, provider integrations (500+ models), and an HTTP API server.
Every client spawns or connects to a `kilo serve` process and communicates via HTTP REST + SSE using the auto-generated `@kilocode/sdk`.
The VS Code extension spawns or connects to a `kilo serve` process and communicates via HTTP REST + SSE using the auto-generated `@kilocode/sdk`. CLI clients can also use in-process transports, as shown below.
```
@kilocode/cli (packages/opencode/)
┌────────────────────────────────┐
│ AI agents, tools, sessions, │
│ providers, config, MCP, LSP │
│ Hono HTTP server + SSE │
│ HTTP API server + SSE
└──┬──────────┬──────────────────┘
│ │
┌───────┴──┐ ┌────┴────┐
@@ -48,9 +60,9 @@ Every client spawns or connects to a `kilo serve` process and communicates via H
| Package | Name | Role |
|---|---|---|
| `packages/opencode/` | `@kilocode/cli` | Core CLI — forked from upstream OpenCode. AI agents, tools, sessions, server. |
| `packages/sdk/js/` | `@kilocode/sdk` | Auto-generated TypeScript SDK client for the server API. Do not edit `src/gen/` by hand. |
| `packages/sdk/js/` | `@kilocode/sdk` | Auto-generated TypeScript SDK client for the server API. Do not edit `src/gen/` or `src/v2/gen/` by hand. |
| `packages/ui/` | `@opencode-ai/ui` | Shared UI primitives |
| `packages/util/` | `@opencode-ai/util` | Shared utilities (error, path, retry, slug) |
| `packages/core/` | `@opencode-ai/core` | Shared runtime and utilities (`src/util/`) |
| `packages/plugin/` | `@kilocode/plugin` | Plugin/tool interface definitions |
## Commands
@@ -68,7 +80,9 @@ bun run format # Run formatter (do this before committing to
The `extension` commands also work from the repo root. When a user asks to run an isolated VS Code/Kilo environment, prefer the CLI scripts: `bun run extension:isolated` reuses `.kilo-dev/`, while `bun run extension:isolated:clean` clears `.kilo-dev/` before launching. Pass an optional workspace path after `--`, for example `bun run extension:isolated -- ../sample-project`. Pass `--insiders` to prefer VS Code Insiders, `--workspace PATH` to open a different folder, `--clean` to wipe cached state, or `--wait` to block until VS Code closes. VS Code is auto-detected on macOS, Linux, and Windows; override with `--app-path` or `VSCODE_EXEC_PATH`.
Single test: `bun run test -- --grep "test name"`
From this package: `bun run typecheck` checks host and webview types; `bun run test:unit` runs Bun unit tests. For a focused Agent Manager example, use `bun test tests/unit/agent-manager-arch.test.ts`.
Single VS Code integration test: `bun run test -- --grep "test name"`
## CLI Binary
@@ -96,7 +110,7 @@ The extension is a client of the CLI. Activation creates one shared `KiloConnect
Extension (Node.js) CLI Backend (child process)
┌──────────────────────────┐ ┌──────────────────────┐
│ KiloConnectionService │── HTTP/SSE ──> │ kilo serve --port 0 │
│ ├── ServerManager │ │ Hono REST API │
│ ├── ServerManager │ │ HTTP REST API │
│ ├── HttpClient │ │ SSE event stream │
│ └── SSEClient │ │ Session management │
│ │ │ AI agent runtime │
@@ -123,11 +137,11 @@ Two separate esbuild builds in [`esbuild.js`](esbuild.js):
- Webview uses **Solid.js** (not React) — JSX compiles via `esbuild-plugin-solid`
- Extension code in `src/`, webview code in `webview-ui/src/` with separate tsconfig
- Tests compile to `out/` via `compile-tests`, not `dist/`
- CSP requires nonce for scripts and `font-src` for bundled fonts — see [`KiloProvider.ts`](src/KiloProvider.ts:777)
- CSP requires nonce for scripts and `font-src` for bundled fonts — see [`KiloProvider.ts`](src/KiloProvider.ts)
- HTML root has `data-theme="kilo-vscode"` to activate kilo-ui's VS Code theme bridge
- Extension and webview have no shared state — communicate via `vscode.Webview.postMessage()`
- For editor panels, use [`AgentManagerProvider`](src/agent-manager/AgentManagerProvider.ts) pattern with `retainContextWhenHidden: true`
- esbuild webview build includes [`cssPackageResolvePlugin`](esbuild.js:29) for CSS `@import` resolution and font loaders (`.woff`, `.woff2`, `.ttf`)
- esbuild webview build includes [`cssPackageResolvePlugin`](esbuild.js) for CSS `@import` resolution and font loaders (`.woff`, `.woff2`, `.ttf`)
- Avoid `setTimeout` for sequencing VS Code operations — use deterministic event-based waits (e.g. `waitForWebviewPanelToBeActive()`)
## Extension ↔ Webview Feature Pattern
@@ -135,9 +149,9 @@ Two separate esbuild builds in [`esbuild.js`](esbuild.js):
When adding a new feature that requires data from the CLI backend to be displayed in the webview:
1. **Types** (`src/services/cli-backend/types.ts`): Add response types for the backend data
2. **HTTP Client** (`src/services/cli-backend/http-client.ts`): Add a fetch method to retrieve the data
3. **KiloProvider** (`src/KiloProvider.ts`): Add a `fetchAndSend*()` method using the cached message pattern, and handle the corresponding `request*` message from the webview in `handleWebviewMessage()`
4. **Message Types** (`webview-ui/src/types/messages.ts`): Add `*LoadedMessage` (extension→webview) and `Request*Message` (webview→extension) types to the `ExtensionMessage` / `WebviewMessage` unions
2. **SDK Client** (`src/services/cli-backend/connection-service.ts`): Use the existing SDK client to retrieve the data
3. **Host Handler** (`src/kilo-provider/` or the existing handler in `src/KiloProvider.ts`): Handle the corresponding webview request using the existing cached message pattern
4. **Message Types** (`webview-ui/src/types/messages/`): Add `*LoadedMessage` (extension→webview) and `Request*Message` (webview→extension) types to the `ExtensionMessage` / `WebviewMessage` unions
5. **Context** (`webview-ui/src/context/`): Subscribe to the loaded message **outside** `onMount` (to catch early pushes before mount), add retry logic for the request message, expose state via context
6. **Component** (`webview-ui/src/components/`): Consume context, render UI
@@ -172,7 +186,7 @@ Extension-side code lives in `src/agent-manager/`, webview code in `webview-ui/a
Multi-project Agent Manager is an incremental migration behind the application-scoped `kilo-code.new.experimental.multiProject` flag (default `false`); flag-off behavior must remain unchanged. The project registry/contexts, per-project state and session routing, project sidebar, sections and drag-and-drop, progress/persistence, and project-targeted worktree creation are implemented.
It is not yet a complete convergence: audit every operation for explicit project/worktree/session routing, finish immutable project-bound Settings and machine-local indexing consent, harden canonical Git identity and multi-window route ownership, and replace the duplicate `SidebarBody`/`ProjectSidebarBody` implementations with one shared body. Full two-project E2E and legacy-parity coverage is still incomplete.
Current implementations are in [project/](src/agent-manager/project/) and [indexing-consent.ts](src/indexing-consent.ts). Check the code and tests before treating migration items as unfinished work. Review areas remain explicit project/worktree/session routing, immutable project-bound Settings, machine-local indexing consent, canonical Git identity, multi-window route ownership, shared sidebar convergence, and full two-project E2E/legacy-parity coverage.
## Webview UI (kilo-ui)
@@ -180,14 +194,14 @@ New webview features must use **`@kilocode/kilo-ui`** components instead of raw
- Import via deep subpaths: `import { Button } from "@kilocode/kilo-ui/button"`
- Available components include `Button`, `IconButton`, `Dialog`, `Spinner`, `Card`, `Tabs`, `Tooltip`, `Toast`, `Code`, `Markdown`, and more
- Provider hierarchy in [`App.tsx`](webview-ui/src/App.tsx:113): `ThemeProvider → I18nProvider → DialogProvider → MarkedProvider → VSCodeProvider → ServerProvider → ProviderProvider → SessionProvider`
- Global styles imported via `import "@kilocode/kilo-ui/styles"` in [`index.tsx`](webview-ui/src/index.tsx:2)
- [`chat.css`](webview-ui/src/styles/chat.css) is being progressively migrated — when replacing a component with kilo-ui, remove the corresponding CSS rules from it
- New CSS for components not yet in kilo-ui goes into `chat.css` grouped by comment-delimited sections (`/* Component Name */`). Once a kilo-ui equivalent exists, remove the section.
- Provider composition is defined by `ProviderShell.Root`, `.Session`, and `.Chat` in [provider-shell.tsx](webview-ui/src/context/provider-shell.tsx); follow that implementation rather than a copied provider order.
- Global styles imported via `import "@kilocode/kilo-ui/styles"` in [`index.tsx`](webview-ui/src/index.tsx)
- [`chat.css`](webview-ui/src/styles/chat.css) imports the focused stylesheets in [styles/](webview-ui/src/styles/). When replacing a component with kilo-ui, remove obsolete rules from the owning stylesheet.
- Extension-specific CSS stays with its feature: chat styles under `webview-ui/src/styles/`, Agent Manager styles under `webview-ui/agent-manager/`. Reusable component styles belong in `packages/kilo-ui/`.
- **Check existing webview usages first**: `webview-ui/src/` and `packages/kilo-ui/src/stories/` show how kilo-ui components are composed. Do not rely only on the component API in isolation.
- **`data-component` and `data-slot` attributes carry CSS styling** — kilo-ui uses `[data-component]` and `[data-slot]` attribute selectors, not class names. Reuse existing component slots where available so shared styles apply consistently.
- **Prefer kilo-ui styles**: Always reuse existing kilo-ui CSS variables, tokens, and component styles instead of writing custom CSS. If a style doesn't exist in kilo-ui yet, add it there and reuse it rather than inlining or duplicating styles in the webview.
- **Icons**: kilo-ui has 75+ custom SVG icons in [`packages/ui/src/components/icon.tsx`](../../packages/ui/src/components/icon.tsx). To list all available icon names: `node -e "const c=require('fs').readFileSync('../../packages/ui/src/components/icon.tsx','utf8');[...c.matchAll(/^\\s{2}[\"']?([\\w-]+)[\"']?:\\s*\x60/gm)].map(m=>m[1]).sort().forEach(n=>console.log(n))"`. Icon names use both hyphenated (`arrow-left`) and bare-word (`brain`, `console`, `providers`) keys.
- **Prefer kilo-ui styles**: Reuse existing kilo-ui CSS variables, tokens, and component styles. Add missing reusable styles there; keep extension-specific rules in their owning feature stylesheet rather than inlining or duplicating styles.
- **Icons**: Kilo-only icons are in [kilo-ui's registry](../kilo-ui/src/components/icon.tsx), which falls back to [the upstream registry](../ui/src/components/icon.tsx). To list upstream icon names: `node -e "const c=require('fs').readFileSync('../../packages/ui/src/components/icon.tsx','utf8');[...c.matchAll(/^\\s{2}[\"']?([\\w-]+)[\"']?:\\s*\x60/gm)].map(m=>m[1]).sort().forEach(n=>console.log(n))"`. Icon names use both hyphenated (`arrow-left`) and bare-word (`brain`, `console`, `providers`) keys.
### Diff Rendering Performance
@@ -236,7 +250,7 @@ Follow monorepo root AGENTS.md style guide:
- Prefer `const` over `let`, early returns over `else`
- Single-word variable names when possible
- Avoid `try`/`catch`, avoid `any` type
- ESLint enforces: curly braces, strict equality, semicolons, camelCase/PascalCase imports
- ESLint rules live in [eslint.config.mjs](eslint.config.mjs); formatting follows the repository's Prettier configuration.
## File Size Caps (maxLines)
+69
View File
@@ -1,5 +1,74 @@
# kilo-code
## 7.5.6
### Minor Changes
- [#13512](https://github.com/Kilo-Org/kilocode/pull/13512) [`13a9673`](https://github.com/Kilo-Org/kilocode/commit/13a9673d08cfc69eebb89898861a1ee80278f226) - Reference other Agent Manager worktrees with a searchable @ picker that prioritizes recently opened worktrees. Share paths, branches, and session IDs without attaching diffs or chat history. Keep picker results in sync when clearing search.
- [#13533](https://github.com/Kilo-Org/kilocode/pull/13533) [`6d15d18`](https://github.com/Kilo-Org/kilocode/commit/6d15d18fa766aaf6a478b63aa98c10ffd23e3d4c) - Choose a separate model for conversation compaction in Context settings.
- [#13498](https://github.com/Kilo-Org/kilocode/pull/13498) [`46bd29d`](https://github.com/Kilo-Org/kilocode/commit/46bd29d733d69545de60a5100997512756ad61b3) - Review all committed and uncommitted Agent Manager worktree changes with `/review worktree`.
### Patch Changes
- [#13502](https://github.com/Kilo-Org/kilocode/pull/13502) [`094ca19`](https://github.com/Kilo-Org/kilocode/commit/094ca1987e0516dcac6df47c52944b737b87d70e) - Keep Agent Manager worktree spinners active while background agents are running.
- [#13505](https://github.com/Kilo-Org/kilocode/pull/13505) [`b382a3b`](https://github.com/Kilo-Org/kilocode/commit/b382a3b838e317acc284442361b9bae2f39c992e) - Load Agent Manager changes faster and preserve cached diffs when switching sessions.
- [#13499](https://github.com/Kilo-Org/kilocode/pull/13499) [`8c0a570`](https://github.com/Kilo-Org/kilocode/commit/8c0a570222fcc51e23041c63f69db7118d5f4c7b) - Place the prompt navigator on the outer sidebar edge and delay hover previews to avoid accidental popups. Keep the navigator on the right in Agent Manager and editor tabs.
- [#13547](https://github.com/Kilo-Org/kilocode/pull/13547) [`2fd53b0`](https://github.com/Kilo-Org/kilocode/commit/2fd53b027b1dee21a82967180e436524dd8e46b8) - Prevent cleared questions from reappearing after saving settings.
- [#13545](https://github.com/Kilo-Org/kilocode/pull/13545) [`b8058a3`](https://github.com/Kilo-Org/kilocode/commit/b8058a30be6211cf9db234c53aed9db13320ca59) - Clear false failure banners after successful context overflow recovery while preserving terminal errors.
- [#13544](https://github.com/Kilo-Org/kilocode/pull/13544) [`f5a7a1d`](https://github.com/Kilo-Org/kilocode/commit/f5a7a1d61bf7eaaa7bc307a976f6f56150fa6264) - Resume interrupted tasks from the empty send button without adding a chat message.
- [#13546](https://github.com/Kilo-Org/kilocode/pull/13546) [`31e76dd`](https://github.com/Kilo-Org/kilocode/commit/31e76dd11db8eb368216faa175663d8c648df187) - Use configured agent reasoning in new sessions instead of outdated saved defaults, and preserve explicit Default selections.
- [#13540](https://github.com/Kilo-Org/kilocode/pull/13540) [`bac3043`](https://github.com/Kilo-Org/kilocode/commit/bac3043143dec08557c3b9013f1cec3a7193924c) - Avoid checkpoint cleanup errors when deleting worktrees under protected parent folders.
- [#13528](https://github.com/Kilo-Org/kilocode/pull/13528) [`de9e1ed`](https://github.com/Kilo-Org/kilocode/commit/de9e1edcfb87f5650a265f96cfcc092f015c4d77) - Keep queued prompt text visible while attachment context loads, and hide empty queued placeholders and internal messages.
- [#13543](https://github.com/Kilo-Org/kilocode/pull/13543) [`67b299f`](https://github.com/Kilo-Org/kilocode/commit/67b299f66e62a76967f58a0f2da371cc3e699261) - Clear stale failures when an agent resumes, keep optional review suggestions from triggering attention, and explain Agent Manager status icons on hover. Keep progress spinners separate from attention warnings during worktree operations.
- [#13500](https://github.com/Kilo-Org/kilocode/pull/13500) [`c8df9c4`](https://github.com/Kilo-Org/kilocode/commit/c8df9c4a7dc871fcd616accbf1e917241e48f777) - Keep chat scroll position during streaming, including small upward gestures, direction changes, and scrolling over message controls.
- [#13466](https://github.com/Kilo-Org/kilocode/pull/13466) [`64c75a5`](https://github.com/Kilo-Org/kilocode/commit/64c75a53ab29caf408a0fad1f8ae8c8d19565a17) - Prevent intermittent server connection failures during VS Code startup.
- [#13475](https://github.com/Kilo-Org/kilocode/pull/13475) [`be2ec51`](https://github.com/Kilo-Org/kilocode/commit/be2ec51159fdbeeee852c1e25682f3b1cc46036e) - Use the Git executable configured in VS Code when creating worktrees on Windows.
- [#13542](https://github.com/Kilo-Org/kilocode/pull/13542) [`47dc16b`](https://github.com/Kilo-Org/kilocode/commit/47dc16b153829c0aa238c9282ae79ebe7bcbcb3a) - Open running background agents directly from the header and show an overflow count for agents that do not fit.
- [#12830](https://github.com/Kilo-Org/kilocode/pull/12830) [`bb20d34`](https://github.com/Kilo-Org/kilocode/commit/bb20d34d308af96dc0184bfd5f0cd2313f60950d) Thanks [@thomasboom](https://github.com/thomasboom)! - Open files by clicking their names in read tool headers.
- [#13560](https://github.com/Kilo-Org/kilocode/pull/13560) [`454b101`](https://github.com/Kilo-Org/kilocode/commit/454b1013e8673bc2cab87e87b5c2ca69d40a62d0) - Keep dismissed background agents hidden when switching sessions or returning from History and empty chats.
- [#13531](https://github.com/Kilo-Org/kilocode/pull/13531) [`c1d1e9b`](https://github.com/Kilo-Org/kilocode/commit/c1d1e9bcd7e63c52af888fa8f3baf079ff2c6af8) - Keep focus in active text fields when questions appear or refresh.
- [#13476](https://github.com/Kilo-Org/kilocode/pull/13476) [`45202c0`](https://github.com/Kilo-Org/kilocode/commit/45202c0764a2b8946a783f3376b2a1bad75a17ff) - Remove deleted worktree checkpoints without losing conversation history and stop showing activity for deleted sessions.
- [#13548](https://github.com/Kilo-Org/kilocode/pull/13548) [`039a235`](https://github.com/Kilo-Org/kilocode/commit/039a235b6ac492d08c079a035a04a49a01cc175d) - Queue Agent Manager follow-up prompts when the target session is busy instead of rejecting them.
- [#13529](https://github.com/Kilo-Org/kilocode/pull/13529) [`8cb1931`](https://github.com/Kilo-Org/kilocode/commit/8cb1931275b1df6d145b8a283cef550ae2851e29) - Stop offscreen loading animations while preserving their original appearance, and release obsolete transcript pages from memory.
- [#13530](https://github.com/Kilo-Org/kilocode/pull/13530) [`5d1313f`](https://github.com/Kilo-Org/kilocode/commit/5d1313f8a23dc5f58ae66fed35c4756bf81913d5) - Search the latest 5,000 chats across the worktree family and display the best 50 matches in the sidebar and Agent Manager. Skip inaccessible folders from unrelated projects when finding past chats.
- [#13504](https://github.com/Kilo-Org/kilocode/pull/13504) [`f293ecb`](https://github.com/Kilo-Org/kilocode/commit/f293ecbba849384ae8b10bc7735dc79791c62cdf) - Show consistent running, completion, and input-required indicators across session tabs and Agent Manager worktrees.
- [#13553](https://github.com/Kilo-Org/kilocode/pull/13553) [`1e13220`](https://github.com/Kilo-Org/kilocode/commit/1e132201958eb14522968cc7de29ab94d7504c66) - Keep session and worktree activity indicators stable during routine server reconnects without hiding connection failures.
- [#13422](https://github.com/Kilo-Org/kilocode/pull/13422) [`83e2772`](https://github.com/Kilo-Org/kilocode/commit/83e27728e56d461d8c749ba60722836c4bcad1f6) - Recognize sessions discovered in managed Agent Manager worktrees during orchestration actions.
- Updated dependencies [[`13a9673`](https://github.com/Kilo-Org/kilocode/commit/13a9673d08cfc69eebb89898861a1ee80278f226), [`b382a3b`](https://github.com/Kilo-Org/kilocode/commit/b382a3b838e317acc284442361b9bae2f39c992e), [`f5a7a1d`](https://github.com/Kilo-Org/kilocode/commit/f5a7a1d61bf7eaaa7bc307a976f6f56150fa6264), [`f9ddb78`](https://github.com/Kilo-Org/kilocode/commit/f9ddb78b17714075ab4f5d1ccb26f2cdbcd644bf), [`34b10a6`](https://github.com/Kilo-Org/kilocode/commit/34b10a672b3048ed53477a1019f08832b522db2d), [`8cb1931`](https://github.com/Kilo-Org/kilocode/commit/8cb1931275b1df6d145b8a283cef550ae2851e29)]:
- @opencode-ai/ui@7.5.6
- @kilocode/kilo-ui@7.5.6
- @kilocode/sdk@7.5.6
- @opencode-ai/core@7.5.6
- @kilocode/kilo-indexing@7.5.6
- @kilocode/plugin@7.5.6
- @kilocode/kilo-gateway@7.5.6
## 7.5.5
### Patch Changes
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "kilo-code",
"displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete",
"description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.",
"version": "7.5.5",
"version": "7.5.6",
"icon": "assets/icons/logo-outline-black.png",
"galleryBanner": {
"color": "#FFFFFF",
+2 -1
View File
@@ -236,11 +236,12 @@ function newest(paths: string[]) {
// ---------------------------------------------------------------------------
async function compile() {
if (!shouldBuild) {
if (!shouldBuild && existsSync(join(root, "dist", "extension.js"))) {
console.log("[launch] Skipping build (--no-build)")
return
}
if (!shouldBuild) console.log("[launch] dist/extension.js is missing in this worktree, building first...")
await ensureDependencies()
console.log("[launch] Building extension...")
await $`bun run build:launch`.cwd(root).env(cleanEnv(process.env))
+37 -5
View File
@@ -1070,6 +1070,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
post: (msg) => this.postMessage(msg),
browserSettings: () => this.sendBrowserSettings(),
exportTranscript: (sessionID) => this.handleExportSessionTranscript(sessionID),
resume: (sessionID, messageID, requestID) => this.handleResumeSession(sessionID, messageID, requestID),
copy: (text) => vscode.env.clipboard.writeText(text),
openSessions: (ids) => this.trackOpenSessions(ids),
activity: (state) => {
@@ -1172,7 +1173,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.checkpoint(message.sessionID, () => this.handleUnrevertSession(message.sessionID))
break
case "deleteMessage":
await this.handleDeleteMessage(message.sessionID, message.messageID)
await this.handleDeleteMessage(message.sessionID, message.messageID, message.requestID)
break
case "permissionResponse":
await handlePermissionResponse(
@@ -2455,17 +2456,26 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}
private async handleDeleteMessage(sessionID: string, messageID: string): Promise<void> {
if (!this.client) {
private async handleDeleteMessage(sessionID: string, messageID: string, requestID?: string): Promise<void> {
const result = {
type: "deleteMessageResult" as const,
sessionID,
messageID,
...(requestID !== undefined ? { requestID } : {}),
}
const client = this.client
if (!client) {
this.postMessage({ type: "error", message: "Not connected to CLI backend", sessionID })
this.postMessage({ ...result, success: false })
return
}
try {
await this.client.session.deleteMessage(
{ sessionID, messageID, directory: this.getWorkspaceDirectory(sessionID) },
const response = await client.session.deleteMessage(
{ sessionID, messageID, directory: this.getWorkspaceDirectory(sessionID), queued: true },
{ throwOnError: true },
)
this.postMessage({ ...result, success: response.data === true })
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to delete message:", error)
this.postMessage({
@@ -2473,6 +2483,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
message: getErrorMessage(error) || "Failed to delete message",
sessionID,
})
this.postMessage({ ...result, success: false })
}
}
@@ -4244,6 +4255,27 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
)
}
private async handleResumeSession(sessionID: string, messageID: string, requestID: string): Promise<void> {
try {
if (!this.client) throw new Error("Not connected to CLI backend")
const directory = this.getWorkspaceDirectory(sessionID)
await this.checkpoints.get(sessionID)
await this.client.kilocode.resumeSession(
{ sessionID, messageID, directory, snapshotInitialization: this.opts.snapshotInitialization },
{ throwOnError: true },
)
this.postMessage({ type: "sessionResumeResult", sessionID, requestID })
} catch (error) {
console.error("[Kilo New] Failed to resume session:", error)
this.postMessage({
type: "sessionResumeResult",
sessionID,
requestID,
error: getErrorMessage(error) || "Failed to resume session",
})
}
}
private async handleAbort(sessionID?: string): Promise<void> {
const sid = sessionID || this.currentSession?.id
if (!sid || !(await this.stopSession(sid))) return
@@ -355,10 +355,6 @@ function truncate(value: string, max: number): string {
return value.length <= max ? value : `${value.slice(0, max - 1)}`
}
// Name what keeps the target from accepting a prompt. A session blocked on a question
// never becomes idle on its own, so naming the blocker here lets an orchestrating agent
// answer it instead of waiting out the idle timeout. The message echoes the exact session
// and question IDs so a follow-up answer call can copy them without guessing.
async function blocked(input: Target, dir: string, name: string): Promise<string | undefined> {
const [perms, qs] = await Promise.all([
input.client.permission.list({ directory: dir }),
@@ -398,14 +394,12 @@ export async function prompt(input: {
text: string
messageID: string
signal?: AbortSignal
idleTimeoutMs?: number
managed?: ManagedSession
}): Promise<void> {
if (input.signal?.aborted) return
const target = await locate(input)
const blocker = await blocked(input, target.dir, target.name)
if (blocker) throw new OrchestrationError("unavailable_session", blocker)
await waitForIdle(input.client, target.dir, input.sessionID, input.signal, input.idleTimeoutMs ?? 30_000)
if (input.signal?.aborted) return
await input.client.session.promptAsync(
{
@@ -467,29 +461,6 @@ export async function answer(input: {
return { questionID: target.id }
}
async function waitForIdle(
client: KiloClient,
directory: string,
sessionID: string,
signal: AbortSignal | undefined,
timeout: number,
start = Date.now(),
): Promise<void> {
if (signal?.aborted) return
const status = await client.session.status({ directory })
if (status.error) throw new OrchestrationError("host_error", "The managed session status could not be read")
const activity = status.data?.[sessionID]?.type ?? "idle"
if (activity === "idle") return
if (Date.now() - start >= timeout) {
throw new OrchestrationError(
"unavailable_session",
`The managed session is still ${activity}; only idle sessions can be prompted`,
)
}
await new Promise<void>((resolve) => setTimeout(resolve, 250))
return waitForIdle(client, directory, sessionID, signal, timeout, start)
}
export function move(input: {
state: WorktreeStateManager
sessionID: string
@@ -147,6 +147,14 @@ export class WorktreeActivity {
this.publish()
}
pause(): void {
if (this.dead) return
for (const state of this.states) {
state.request = undefined
state.loaded = false
}
}
clear(): void {
if (this.dead) return
for (const state of this.states) this.invalidate(state)
@@ -385,6 +393,7 @@ export function createWorktreeActivity(opts: FactoryOptions) {
return activity.sync(force)
}
const unsubState = opts.connection.onStateChange((state) => {
if (state === "connecting") return activity.pause()
if (state !== "connected") return activity.clear()
void sync(true)
})
@@ -17,6 +17,7 @@ type Ctx = {
post: (msg: unknown) => void
browserSettings: () => void
exportTranscript: (sessionID: string) => Promise<void>
resume: (sessionID: string, messageID: string, requestID: string) => Promise<void>
copy: (text: string) => PromiseLike<void>
openSessions: (ids: string[]) => void
activity: (state: unknown) => void
@@ -56,10 +57,27 @@ async function routeBackgroundMessage(
return undefined
}
function isResume(input: { sessionID?: unknown; messageID?: unknown; requestID?: unknown }): input is {
sessionID: string
messageID: string
requestID: string
} {
return (
typeof input.sessionID === "string" && typeof input.messageID === "string" && typeof input.requestID === "string"
)
}
export async function routeEarlyMessage(
message: { type: string; id?: unknown; text?: unknown; state?: unknown },
ctx: Ctx,
): Promise<boolean> {
if (message.type === "resumeSession") {
const input = message as { sessionID?: unknown; messageID?: unknown; requestID?: unknown }
if (isResume(input)) {
await ctx.resume(input.sessionID, input.messageID, input.requestID)
}
return true
}
if (message.type === "copyToClipboard") {
if (typeof message.id !== "string") return true
if (typeof message.text !== "string") {
@@ -595,6 +595,7 @@ export class KiloConnectionService {
for (const q of qs) {
const { error } = await client.question.reject({ requestID: q.id, directory: dir })
if (error && !isNotFound(error)) throw new Error(`Failed to reject question ${q.id}: ${String(error)}`)
this.clearQuestionDirectory(q.id)
}
}
})
@@ -53,6 +53,26 @@ test.describe("webview accessibility ratchet", () => {
})
}
test("Background agent summary and visible agents remain pointer-accessible", async ({ page }) => {
await page.setViewportSize({ width: 200, height: 720 })
await open(page, "chat--task-header-background-agents-200")
const agents = page.locator('[data-component="task-header-agents"]')
const summary = agents.locator('[data-slot="task-header-agents-summary"]')
const list = agents.locator('[data-slot="task-header-todos-list"]')
await expect(summary).toHaveAttribute("aria-hidden", "false")
await summary.click()
await expect(list).toBeVisible()
await summary.click()
await expect(list).toBeHidden()
await page.setViewportSize({ width: 1280, height: 720 })
const item = agents.locator('[data-slot="task-header-agents-item"]').first()
await expect(item).toHaveAttribute("aria-hidden", "false")
await item.click()
await expect(list).toBeHidden()
})
test("Agent Manager keeps virtualized transcript fragments laid out", async ({ page }) => {
await open(page, "agentmanager--sidebar-search-open")
@@ -33,7 +33,8 @@ Object.assign(globalThis, {
})
const { render } = await import("solid-js/web")
const { For } = await import("solid-js")
const { For, createSignal } = await import("solid-js")
const { WorktreeItem } = await import("../../webview-ui/agent-manager/WorktreeItem")
const { DragDropProvider, SortableProvider } = await import("@thisbeyond/solid-dnd")
const { renderTab } = await import("../../webview-ui/agent-manager/tab-rendering")
const { VSCodeProvider } = await import("../../webview-ui/src/context/vscode")
@@ -43,6 +44,7 @@ const { LanguageContext } = await import("../../webview-ui/src/context/language"
const { ProviderContext } = await import("../../webview-ui/src/context/provider")
const { SessionProvider, useSession } = await import("../../webview-ui/src/context/session")
const { post } = await import("../../webview-ui/src/utils/webview-message")
const { terminal } = await import("../../webview-ui/src/context/session-outcome")
const provider = {
providers: () => ({}),
@@ -83,6 +85,8 @@ const language = {
}
const ref = { value: undefined as ReturnType<typeof useSession> | undefined }
const [operation, setOperation] = createSignal(false)
const [run, setRun] = createSignal(false)
const Probe = () => {
const session = useSession()
ref.value = session
@@ -107,6 +111,40 @@ const Probe = () => {
<SortableProvider ids={ids}>
<For each={ids}>{(id) => renderTab(id, deps)}</For>
</SortableProvider>
<WorktreeItem
worktree={{
id: "worktree",
path: "/test/worktree",
branch: "test",
parentBranch: "main",
createdAt: "2026-01-01T00:00:00.000Z",
}}
label="Recovery test"
active
pendingDelete={false}
busy={operation()}
activity={session.activityFor("root")}
runStatus={run() ? { worktreeId: "worktree", state: "running" } : undefined}
stale={false}
sessions={1}
grouped={false}
groupStart={false}
groupEnd={false}
groupSize={0}
renaming={false}
renameValue=""
closeKeybind=""
openKeybind=""
onClick={() => {}}
onDelete={() => {}}
onStartRename={() => {}}
onRenameInput={() => {}}
onCommitRename={() => {}}
onCancelRename={() => {}}
onRemoveStale={() => {}}
onCopyPath={() => {}}
onOpen={() => {}}
/>
</DragDropProvider>
)
}
@@ -147,12 +185,19 @@ const state = (id: string) => {
assert(value)
return value.activityFor(id)
}
const card = (expected: string) => {
const icon = host.querySelector('[data-sidebar-id="worktree"] .am-wt-icon')
assert(icon)
assert.equal(icon.getAttribute("data-activity"), expected)
assert.equal(!!icon.querySelector('[data-component="spinner"]'), expected === "busy" || expected === "retry")
}
const check = async (id: string, expected: string) => {
await settle()
step.value += 1
const value = ref.value
assert(value)
const actual = state(id)
if (id === "root") card(expected)
const tab = host.querySelector(`[data-tab-id="${id}"] [data-activity]`)
if (id === "root" || id === "background") assert(tab, `Missing rendered tab for ${id}`)
if (tab && tab.getAttribute("data-activity") !== expected) {
@@ -201,6 +246,14 @@ try {
value.setCurrentSessionID("root")
await check("root", "idle")
await check("background", "idle")
for (const update of [setOperation, setRun]) {
update(true)
await settle()
card("busy")
update(false)
await settle()
card("idle")
}
await emit({ type: "sessionStatus", sessionID: "background", status: "busy" })
await check("background", "busy")
@@ -242,6 +295,12 @@ try {
await check("root", "waiting")
await check("task-child", "waiting")
await check("task-grand", "waiting")
for (const update of [setOperation, setRun]) {
update(true)
await settle()
card("waiting")
update(false)
}
await emit({ type: "permissionError", permissionID: "permission", stale: true })
await check("root", "idle")
assert.equal(value.permissions().length, 0)
@@ -292,7 +351,9 @@ try {
type: "suggestionRequest",
suggestion: { id: "suggestion", sessionID: "task-grand", text: "Try this", actions: [] },
})
await check("root", "waiting")
await check("root", "idle")
await check("task-grand", "idle")
assert.equal(value.scopedSuggestions("root").length, 1)
await emit({ type: "suggestionResolved", requestID: "suggestion" })
await check("root", "idle")
assert.equal(value.suggestions().length, 0)
@@ -309,11 +370,17 @@ try {
await check("root", "idle")
await emit({ type: "sessionTurnClosed", sessionID: "root", reason: "completed" })
await emit({
type: "suggestionRequest",
suggestion: { id: "review", sessionID: "root", text: "Review the changes", actions: [] },
})
await check("root", "done")
assert.equal(value.suggestions().length, 1)
await emit({ type: "sessionStatus", sessionID: "root", status: "busy" })
await check("root", "busy")
await emit({ type: "sessionStatus", sessionID: "root", status: "idle" })
await check("root", "idle")
await emit({ type: "suggestionResolved", requestID: "review" })
await emit({ type: "sessionTurnClosed", sessionID: "root", reason: "completed" })
await check("root", "done")
@@ -338,6 +405,10 @@ try {
value.setCurrentSessionID("background")
await emit({ type: "sessionStatus", sessionID: "background", status: "busy" })
await check("background", "busy")
await emit({ type: "connectionState", state: "connecting" })
await check("background", "busy")
await emit({ type: "connectionState", state: "connected" })
await check("background", "busy")
await emit({ type: "sessionError", eventID: "background-aborted", error: { name: "MessageAbortedError" } })
await check("background", "busy")
await emit({ type: "connectionState", state: "disconnected", error: "offline" })
@@ -356,6 +427,10 @@ try {
},
})
await check("background", "waiting")
await emit({ type: "connectionState", state: "connecting" })
await check("background", "waiting")
await emit({ type: "connectionState", state: "connected" })
await check("background", "waiting")
await emit({ type: "connectionState", state: "error", error: "failed" })
await check("background", "error")
await emit({ type: "connectionState", state: "connecting" })
@@ -372,15 +447,114 @@ try {
await check("root", "idle")
await emit({ type: "sessionError", eventID: "root-error", error: { name: "ProviderError" } })
await check("root", "error")
for (const update of [setOperation, setRun]) {
update(true)
await settle()
card("error")
update(false)
}
assert.equal(value.inUseFor("root"), false)
await emit({ type: "sessionError", eventID: "later-overflow", error: { name: "ContextOverflowError" } })
await emit({ type: "sessionTurnClosed", sessionID: "root", reason: "completed" })
await check("root", "error")
await emit({ type: "sessionStatus", sessionID: "root", status: "busy" })
await check("root", "busy")
for (const status of ["busy", "retry"] as const) {
await emit({ type: "sessionStatus", sessionID: "root", status })
await emit({
type: "sessionError",
sessionID: "root",
eventID: `root-${status}-error`,
error: { name: "ProviderError", message: "Request Entity Too Large" },
})
await check("root", "error")
await emit({ type: "sessionStatus", sessionID: "root", status })
await check("root", status)
assert.equal(value.closeReason(), undefined)
assert(value.messages().some((message) => message.sessionErrorID === `root-${status}-error`))
}
await emit({ type: "sessionTurnClosed", sessionID: "root", reason: "completed" })
await emit({ type: "sessionStatus", sessionID: "root", status: "idle" })
await check("root", "idle")
await check("root", "done")
await emit({ type: "sessionStatus", sessionID: "root", status: "busy" })
await emit({ type: "sessionError", sessionID: "root", eventID: "terminal-error", error: { name: "ProviderError" } })
await emit({ type: "sessionStatus", sessionID: "root", status: "idle" })
await check("root", "error")
for (const order of ["before", "after"]) {
await emit({ type: "sessionStatus", sessionID: "root", status: "busy" })
await emit({
type: "sessionError",
sessionID: "root",
eventID: `overflow-${order}`,
error: { name: "ContextOverflowError", data: { message: "Request Entity Too Large" } },
})
await check("root", "busy")
await emit({
type: "sessionError",
sessionID: "root",
eventID: `retry-${order}`,
error: { name: "ContextOverflowError", data: { message: "Request Entity Too Large" } },
})
await emit({
type: "messageCreated",
message: {
id: `recovered-${order}`,
sessionID: "root",
role: "assistant",
createdAt: new Date().toISOString(),
finish: "stop",
},
})
if (order === "before") await emit({ type: "sessionStatus", sessionID: "root", status: "idle" })
await emit({ type: "sessionTurnClosed", sessionID: "root", reason: "completed" })
if (order === "after") await emit({ type: "sessionStatus", sessionID: "root", status: "idle" })
await check("root", "done")
assert.equal(value.closeReason(), "completed")
assert.equal(
value.messages().some((message) => message.sessionErrorID === `overflow-${order}`),
false,
)
assert.equal(
value.messages().some((message) => message.sessionErrorID === `retry-${order}`),
false,
)
assert.equal(terminal({ reason: value.closeReason(), messages: value.visibleMessages(), todos: [] }), undefined)
assert.equal(
value.messages().some((message) => message.sessionErrorID === "root-error"),
true,
)
assert.equal(
value.messages().some((message) => message.sessionErrorID === "later-overflow"),
true,
)
}
for (const reason of ["error", "completed"]) {
await emit({ type: "sessionStatus", sessionID: "root", status: "busy" })
await emit({
type: "sessionError",
sessionID: "root",
eventID: `unrecovered-${reason}`,
error: { name: "ContextOverflowError", data: { message: "Context limit reached" } },
})
if (reason === "completed") {
await emit({ type: "sessionError", sessionID: "root", eventID: "terminal-error", error: { name: "APIError" } })
}
await emit({ type: "sessionTurnClosed", sessionID: "root", reason })
await emit({ type: "sessionStatus", sessionID: "root", status: "idle" })
await emit({ type: "sessionTurnClosed", sessionID: "root", reason: "completed" })
await check("root", "error")
assert.equal(value.closeReason(), "error")
assert.equal(
value.messages().some((message) => message.sessionErrorID === `unrecovered-${reason}`),
true,
)
}
await emit({ type: "sessionStatus", sessionID: "root", status: "busy" })
await emit({ type: "sessionStatus", sessionID: "root", status: "idle" })
await emit({
type: "questionRequest",
question: {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"type": "module",
"version": "7.5.5",
"version": "7.5.6",
"dependencies": {},
"devDependencies": {},
"peerDependencies": {}
@@ -73,6 +73,7 @@ const TSX_FILES = [
path.join(ROOT, "webview-ui/diff-virtual/DiffVirtualApp.tsx"),
// Shared components that consume agent-manager CSS classes (e.g. am-dropdown,
// am-branch-item) used by both the agent manager and the diff viewer.
path.join(ROOT, "webview-ui/src/components/shared/ActivityIcon.tsx"),
path.join(ROOT, "webview-ui/src/components/shared/BranchSelect.tsx"),
path.join(ROOT, "webview-ui/src/components/chat/TabDnd.tsx"),
path.join(ROOT, "webview-ui/diff-viewer/BaseBranchPicker.tsx"),
@@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test"
import { initialMessage, initialVariant, seedInitialVariant } from "../../webview-ui/agent-manager/initial-message"
describe("Agent Manager initial message", () => {
it("forwards the selected variant to sendMessage", () => {
it.each(["high", ""])("forwards the selected variant %s to sendMessage", (variant) => {
const msg = initialMessage({
type: "agentManager.sendInitialMessage",
projectId: "project-a",
@@ -12,7 +12,7 @@ describe("Agent Manager initial message", () => {
providerID: "anthropic",
modelID: "claude-sonnet-4",
agent: "code",
variant: "high",
variant,
})
expect(msg).toEqual({
@@ -23,7 +23,7 @@ describe("Agent Manager initial message", () => {
providerID: "anthropic",
modelID: "claude-sonnet-4",
agent: "code",
variant: "high",
variant,
files: undefined,
})
})
@@ -38,7 +38,7 @@ describe("Agent Manager initial message", () => {
).toBeUndefined()
})
it("builds the initial session variant state", () => {
it.each(["medium", ""])("builds the initial session variant state for %s", (variant) => {
const state = initialVariant(
{
type: "agentManager.sendInitialMessage",
@@ -46,7 +46,7 @@ describe("Agent Manager initial message", () => {
worktreeId: "wt-a",
providerID: "anthropic",
modelID: "claude-sonnet-4",
variant: "medium",
variant,
},
"code",
)
@@ -56,7 +56,7 @@ describe("Agent Manager initial message", () => {
providerID: "anthropic",
modelID: "claude-sonnet-4",
agent: "code",
value: "medium",
value: variant,
})
})
@@ -75,7 +75,7 @@ describe("Agent Manager initial message", () => {
).toBeUndefined()
})
it("seeds initial variant state into the session store", () => {
it.each(["medium", ""])("seeds initial variant %s into the session store", (variant) => {
const calls: unknown[] = []
seedInitialVariant(
@@ -89,10 +89,10 @@ describe("Agent Manager initial message", () => {
worktreeId: "wt-a",
providerID: "anthropic",
modelID: "claude-sonnet-4",
variant: "medium",
variant,
},
)
expect(calls).toEqual([["session-a", "anthropic", "claude-sonnet-4", "medium", "code"]])
expect(calls).toEqual([["session-a", "anthropic", "claude-sonnet-4", variant, "code"]])
})
})
@@ -153,8 +153,9 @@ describe("AgentManagerOrchestrationBridge", () => {
prompt: "Continue",
}
it("deduplicates prompt delivery and retries only the failed acknowledgement", async () => {
it("deduplicates busy-session prompt submission and retries only the failed acknowledgement", async () => {
const test = harness()
test.client.session.status.mockImplementation(async () => ({ data: { ses_target: { type: "busy" } } }))
test.status.failReply = true
test.request(request)
@@ -163,6 +164,8 @@ describe("AgentManagerOrchestrationBridge", () => {
test.request(request)
await waitFor(() => test.replies.length === 2)
expect(test.client.session.status).not.toHaveBeenCalled()
expect(test.rejections).toEqual([])
expect(test.promptAsync).toHaveBeenCalledTimes(1)
expect(test.promptAsync).toHaveBeenCalledWith(
expect.objectContaining({
@@ -176,7 +176,7 @@ describe("Agent Manager orchestration domain", () => {
expect(dirs.size).toBe(4)
})
it("delivers only to an idle managed session in its authoritative directory", async () => {
it("delivers to a managed session in its authoritative directory", async () => {
const managed = state.addWorktree({ branch: "fix/prompt", path: worktree, parentBranch: "main" })
state.addSession("ses_target", managed.id)
const get = mock(async () => ({
@@ -302,32 +302,92 @@ describe("Agent Manager orchestration domain", () => {
expect(delivered).toHaveBeenCalledWith(expect.objectContaining({ directory: worktree }), { throwOnError: true })
})
it("waits for a busy managed session to become idle before prompting", async () => {
const managed = state.addWorktree({ branch: "fix/wait", path: worktree, parentBranch: "main" })
state.addSession("ses_wait", managed.id)
let calls = 0
const promptAsync = mock(async () => ({ data: undefined }))
it.each(["busy", "retry"] as const)("submits prompts to a %s session without waiting for idle", async (activity) => {
const managed = state.addWorktree({ branch: "fix/queue", path: worktree, parentBranch: "main" })
state.addSession("ses_queue", managed.id)
const delivered = mock(async () => ({ data: undefined }))
const client = {
session: {
get: mock(async () => ({ data: { id: "ses_wait", directory: worktree, title: "Wait" } as Session })),
status: mock(async () => ({ data: calls++ === 0 ? { ses_wait: { type: "busy" } } : {} })),
promptAsync,
get: mock(async () => ({ data: { id: "ses_queue", directory: worktree, title: "Queue" } as Session })),
status: mock(async () => ({ data: { ses_queue: { type: activity } } })),
promptAsync: delivered,
abort: mock(async () => ({ data: true })),
},
permission: {
list: mock(async () => ({ data: [] })),
permission: { list: mock(async () => ({ data: [] })) },
question: { list: mock(async () => ({ data: noQuestions })) },
} as unknown as KiloClient
await prompt({ client, root, state, sessionID: "ses_queue", text: "Continue", messageID: "amr_queue" })
expect(client.session.status).not.toHaveBeenCalled()
expect(client.session.abort).not.toHaveBeenCalled()
expect(delivered).toHaveBeenCalledTimes(1)
expect(delivered).toHaveBeenCalledWith(
{
sessionID: "ses_queue",
directory: worktree,
messageID: "msg_agent_manager_amr_queue",
parts: [{ type: "text", text: "Continue" }],
snapshotInitialization: "wait",
},
{ throwOnError: true },
)
})
it("rejects prompts to a busy session with a pending permission", async () => {
state.addSession("ses_blocked", null)
const delivered = mock(async () => ({ data: undefined }))
const client = {
session: {
get: mock(async () => ({ data: { id: "ses_blocked", directory: root, title: "Blocked" } as Session })),
status: mock(async () => ({ data: { ses_blocked: { type: "busy" } } })),
promptAsync: delivered,
},
permission: { list: mock(async () => ({ data: [{ id: "perm_1", sessionID: "ses_blocked" }] })) },
question: { list: mock(async () => ({ data: noQuestions })) },
} as unknown as KiloClient
await expect(
prompt({ client, root, state, sessionID: "ses_blocked", text: "Continue", messageID: "amr_permission" }),
).rejects.toMatchObject({
code: "unavailable_session",
message: expect.stringContaining("pending permission request"),
})
expect(delivered).not.toHaveBeenCalled()
})
it("does not submit a prompt cancelled during validation", async () => {
state.addSession("ses_cancelled", null)
const controller = new AbortController()
const delivered = mock(async () => ({ data: undefined }))
const client = {
session: {
get: mock(async () => ({ data: { id: "ses_cancelled", directory: root, title: "Cancelled" } as Session })),
promptAsync: delivered,
},
permission: { list: mock(async () => ({ data: [] })) },
question: {
list: mock(async () => ({ data: noQuestions })),
list: mock(async () => {
controller.abort()
return { data: noQuestions }
}),
},
} as unknown as KiloClient
await prompt({ client, root, state, sessionID: "ses_wait", text: "Continue", messageID: "amr_wait" })
await prompt({
client,
root,
state,
sessionID: "ses_cancelled",
text: "Continue",
messageID: "amr_cancelled",
signal: controller.signal,
})
expect(client.session.status).toHaveBeenCalledTimes(2)
expect(promptAsync).toHaveBeenCalledTimes(1)
expect(delivered).not.toHaveBeenCalled()
})
it("fails fast with the pending question named instead of waiting out the idle timeout", async () => {
it("rejects prompts with the pending question and answer options named", async () => {
const managed = state.addWorktree({ branch: "fix/blocked", path: worktree, parentBranch: "main" })
state.addSession("ses_blocked", managed.id)
const promptAsync = mock(async () => ({ data: undefined }))
@@ -420,7 +480,7 @@ describe("Agent Manager orchestration domain", () => {
} satisfies Partial<OrchestrationError>)
})
it("rejects unknown, stale, cross-workspace, and busy targets", async () => {
it("rejects unknown, stale, and cross-workspace targets", async () => {
const managed = state.addWorktree({ branch: "fix/errors", path: worktree, parentBranch: "main" })
state.addSession("ses_target", managed.id)
const promptAsync = mock(async () => ({ data: undefined }))
@@ -430,7 +490,6 @@ describe("Agent Manager orchestration domain", () => {
status: mock(async () => ({ data: {} })),
promptAsync,
},
// Permission replies remain out of scope. This empty read keeps the test focused on question/idle handling.
permission: {
list: mock(async () => ({ data: [] })),
},
@@ -471,25 +530,6 @@ describe("Agent Manager orchestration domain", () => {
).rejects.toMatchObject({
code: "cross_workspace",
} satisfies Partial<OrchestrationError>)
;(client.session.get as ReturnType<typeof mock>).mockImplementation(async () => ({
data: { id: "ses_target", directory: worktree, title: "Target" } as Session,
}))
;(client.session.status as ReturnType<typeof mock>).mockImplementation(async () => ({
data: { ses_target: { type: "busy" } },
}))
await expect(
prompt({
client,
root,
state,
sessionID: "ses_target",
text: "Continue",
messageID: "amr_busy",
idleTimeoutMs: 0,
}),
).rejects.toMatchObject({
code: "unavailable_session",
} satisfies Partial<OrchestrationError>)
fs.rmSync(worktree, { recursive: true, force: true })
await expect(
@@ -2,6 +2,7 @@ import { describe, expect, it } from "bun:test"
import {
backgroundAgents,
backgroundJobAgents,
fitBackgroundAgents,
showBackgroundAgent,
} from "../../webview-ui/src/components/chat/background-agents"
import { childForeground, showChildPromotion } from "../../webview-ui/src/components/chat/task-tool-state"
@@ -49,6 +50,29 @@ function taskPart(opts: TaskOptions = {}): ToolPart {
const busy: SessionStatusInfo = { type: "busy" }
const idle: SessionStatusInfo = { type: "idle" }
describe("fitBackgroundAgents", () => {
it("uses the full width when all agents fit without an overflow button", () => {
expect(fitBackgroundAgents([30, 30], 66, 80, 6)).toBe(2)
})
it("reserves the overflow button and spacing while fitting a prefix", () => {
expect(fitBackgroundAgents([100, 120, 80], 285, 50, 6)).toBe(2)
expect(fitBackgroundAgents([100, 120, 80], 281, 50, 6)).toBe(1)
})
it("falls back to the summary when no agent fits with the overflow button", () => {
expect(fitBackgroundAgents([100, 120], 155, 50, 6)).toBe(0)
expect(fitBackgroundAgents([100, 120], 156, 50, 6)).toBe(1)
})
it("handles single agents, empty lists, and hidden containers", () => {
expect(fitBackgroundAgents([100], 100, 50, 6)).toBe(1)
expect(fitBackgroundAgents([100], 99, 50, 6)).toBe(0)
expect(fitBackgroundAgents([], 100, 50, 6)).toBe(0)
expect(fitBackgroundAgents([100, 120], 0, 50, 6)).toBe(0)
})
})
describe("backgroundAgents", () => {
it("lists a running background agent from tool state metadata", () => {
const tools = [taskPart({ child: "ses_child", background: true, description: "Audit deps", agent: "explore" })]
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
import { KiloConnectionService } from "../../src/services/cli-backend/connection-service"
describe("KiloConnectionService question routing", () => {
test("ignores stale NotFoundError rejects while draining questions", async () => {
test.each([undefined, { _tag: "NotFound" }])("invalidates recovery after draining (%j)", async (error) => {
const service = new KiloConnectionService({} as any)
const client = {
permission: {
@@ -10,7 +10,7 @@ describe("KiloConnectionService question routing", () => {
},
question: {
list: async () => ({ data: [{ id: "que_test" }] }),
reject: async () => ({ error: { _tag: "NotFound" } }),
reject: async () => ({ error }),
},
suggestion: {
list: async () => ({ data: [] }),
@@ -22,8 +22,14 @@ describe("KiloConnectionService question routing", () => {
;(service as any).client = client
;(service as any).directoryProviders.add(() => ["/tmp/workspace"])
service.recordQuestionDirectory("que_test", "/tmp/workspace")
const revisions: number[] = []
service.onClearPendingPrompts(() => revisions.push(service.getQuestionRevision()))
await expect(service.drainPendingPrompts()).resolves.toBeUndefined()
expect(service.getQuestionDirectory("que_test")).toBeUndefined()
expect(revisions).toEqual([1])
})
test("records and clears request origins from SSE events", () => {
@@ -43,6 +43,22 @@ describe("routeEarlyMessage clipboard handling", () => {
})
})
describe("routeEarlyMessage resume", () => {
it("forwards the original session, assistant, and request IDs without sending text", async () => {
const calls: string[][] = []
const ctx = {
resume: async (...ids: string[]) => {
calls.push(ids)
},
} as Ctx
const message = { type: "resumeSession", sessionID: "ses_1", messageID: "msg_1", requestID: "request-1" }
expect(await routeEarlyMessage(message, ctx)).toBe(true)
expect(calls).toEqual([["ses_1", "msg_1", "request-1"]])
expect(await routeEarlyMessage({ ...message, messageID: undefined }, ctx)).toBe(true)
expect(calls).toHaveLength(1)
})
})
describe("routeEarlyMessage activity", () => {
it("forwards authoritative webview presentation state without interpreting session events", async () => {
const calls: unknown[] = []
@@ -70,6 +70,8 @@ function createClient(options?: {
createDeferred?: Deferred<{ data: ReturnType<typeof mkCreatedSession> }>
abortFailures?: string[]
abortDeferred?: Deferred<void>
deleteResult?: unknown
deleteError?: boolean
supportDeferred?: Deferred<{ data: { available: boolean; reason?: string } }>
sandboxDeferred?: Deferred<{ data: unknown }>
sandboxStarted?: Deferred<void>
@@ -79,6 +81,12 @@ function createClient(options?: {
const stopped: { sessionID: string; directory?: string }[] = []
const aborted: { sessionID: string; directory?: string }[] = []
const deleted: { sessionID: string; directory?: string }[] = []
const deletedMessages: Array<{
sessionID: string
messageID: string
directory?: string
queued?: boolean
}> = []
const prompted: Array<Record<string, unknown>> = []
const reverted: Array<Record<string, unknown>> = []
const created: Array<Record<string, unknown>> = []
@@ -90,6 +98,7 @@ function createClient(options?: {
stopped,
aborted,
deleted,
deletedMessages,
prompted,
reverted,
created,
@@ -133,6 +142,11 @@ function createClient(options?: {
if (options?.deleteDeferred) return options.deleteDeferred.promise
return { data: {} }
},
deleteMessage: async (params: { sessionID: string; messageID: string; directory?: string; queued?: boolean }) => {
deletedMessages.push(params)
if (options?.deleteError) throw new Error("delete failed")
return { data: options?.deleteResult }
},
},
sandbox: {
support: async (params: Record<string, unknown>) => {
@@ -171,7 +185,7 @@ function createClient(options?: {
}
}
function createConnection(client: ReturnType<typeof createClient>) {
function createConnection(client: ReturnType<typeof createClient> | null) {
const state = { value: undefined as boolean | undefined, revision: 0, pending: Promise.resolve() }
return {
sandboxPreference: {
@@ -251,13 +265,14 @@ type ProviderInternals = {
refreshGitStatus: (directory?: string, sessionID?: string) => Promise<void>
handleLoadMessages: (sid: string, opts?: { mode?: string; before?: string; limit?: number }) => Promise<void>
handleDeleteSession: (sid: string) => Promise<void>
handleDeleteMessage: (sid: string, mid: string, rid?: string) => Promise<void>
}
function makeProvider(client: ReturnType<typeof createClient>) {
function makeProvider(client: ReturnType<typeof createClient> | null) {
const connection = createConnection(client)
const provider = new KiloProvider({} as never, connection as never)
const internal = provider as unknown as ProviderInternals
internal.connectionState = "connected"
internal.connectionState = client ? "connected" : "disconnected"
const sent: unknown[] = []
internal.webview = {
postMessage: async (message: unknown) => {
@@ -1159,6 +1174,31 @@ describe("KiloProvider.handleDeleteSession / background processes", () => {
})
})
describe("KiloProvider.handleDeleteMessage", () => {
const ids = { sessionID: "s1", messageID: "m1", requestID: "r1" }
it.each([true, false, undefined, "true"])("confirms only a true queued deletion result: %p", async (result) => {
const client = createClient({ deleteResult: result })
const { internal, sent } = makeProvider(client)
await internal.handleDeleteMessage(ids.sessionID, ids.messageID, ids.requestID)
expect(client.deletedMessages).toEqual([{ sessionID: "s1", messageID: "m1", directory: "/repo", queued: true }])
expect(sent).toContainEqual({ type: "deleteMessageResult", ...ids, success: result === true })
})
it.each([true, false])("confirms removal failures when connected=%p", async (connected) => {
const error = spyOn(console, "error").mockImplementation(() => {})
const { internal, sent } = makeProvider(connected ? createClient({ deleteError: true }) : null)
await internal.handleDeleteMessage(ids.sessionID, ids.messageID, ids.requestID)
expect(sent).toContainEqual({
type: "error",
message: connected ? "delete failed" : "Not connected to CLI backend",
sessionID: ids.sessionID,
})
expect(sent).toContainEqual({ type: "deleteMessageResult", ...ids, success: false })
error.mockRestore()
})
})
describe("KiloProvider.handleLoadMessages / slim payload", () => {
it("shows a cost alert even when cost arrives after the session is idle", () => {
const client = createClient()
@@ -0,0 +1,87 @@
import { afterEach, describe, expect, test } from "bun:test"
import { cpSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync } from "node:fs"
import { tmpdir } from "node:os"
import { delimiter, dirname, join, resolve } from "node:path"
const source = resolve(import.meta.dir, "../../../..")
const dirs: string[] = []
afterEach(() => {
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true })
})
async function fixture() {
const repo = realpathSync(mkdtempSync(join(tmpdir(), "kilo-launch-")))
dirs.push(repo)
for (const path of ["", "packages/kilo-vscode", "packages/opencode", "packages/sdk/js"]) {
const dir = join(repo, path)
mkdirSync(dir, { recursive: true })
symlinkSync(join(source, path, "node_modules"), join(dir, "node_modules"), "junction")
}
const root = join(repo, "packages/kilo-vscode")
mkdirSync(join(root, "script"))
cpSync(join(source, "packages/kilo-vscode/script/launch.ts"), join(root, "script/launch.ts"))
await Bun.write(join(root, "package.json"), JSON.stringify({ scripts: { "build:launch": "bun build.ts" } }))
await Bun.write(join(root, "build.ts"), 'await Bun.write("dist/extension.js", "built")')
const workspace = join(repo, "workspace")
await Bun.write(join(workspace, "package.json"), JSON.stringify({ main: "index.ts" }))
await Bun.write(join(workspace, "index.ts"), 'await Bun.write("opened.json", JSON.stringify(process.argv.slice(2)))')
return {
root,
workspace,
run: (args: string[] = ["--no-build"]) =>
Bun.spawnSync(
[
process.execPath,
join(root, "script/launch.ts"),
"--isolated",
"--wait",
"--app-path",
process.execPath,
"--workspace",
workspace,
...args,
],
{
cwd: repo,
env: { ...process.env, PATH: [dirname(process.execPath), process.env.PATH].join(delimiter) },
stdout: "pipe",
stderr: "pipe",
},
),
}
}
describe("extension launch build", () => {
test("builds a fresh worktree before launching even with --no-build", async () => {
const item = await fixture()
const result = item.run()
expect(result.exitCode).toBe(0)
expect(await Bun.file(join(item.root, "dist/extension.js")).text()).toBe("built")
expect(await Bun.file(join(item.workspace, "opened.json")).json()).toContain(
`--extensionDevelopmentPath=${item.root}`,
)
})
test("reuses an existing bundle with --no-build", async () => {
const item = await fixture()
await Bun.write(join(item.root, "dist/extension.js"), "previous")
expect(item.run().exitCode).toBe(0)
expect(await Bun.file(join(item.root, "dist/extension.js")).text()).toBe("previous")
expect(await Bun.file(join(item.workspace, "opened.json")).exists()).toBe(true)
})
test("rebuilds an existing bundle by default", async () => {
const item = await fixture()
await Bun.write(join(item.root, "dist/extension.js"), "previous")
expect(item.run([]).exitCode).toBe(0)
expect(await Bun.file(join(item.root, "dist/extension.js")).text()).toBe("built")
})
test("does not launch when the required build fails", async () => {
const item = await fixture()
await Bun.write(join(item.root, "build.ts"), "process.exit(1)")
expect(item.run().exitCode).toBe(1)
expect(await Bun.file(join(item.workspace, "opened.json")).exists()).toBe(false)
})
})
@@ -0,0 +1,64 @@
import { describe, expect, it } from "bun:test"
import { continuation } from "../../webview-ui/src/context/session-continuation"
import type { Message, Part } from "../../webview-ui/src/types/messages"
const user: Message = { id: "user", sessionID: "session", role: "user", createdAt: "2026-01-01" }
const assistant: Message = {
id: "assistant",
sessionID: "session",
role: "assistant",
parentID: user.id,
createdAt: "2026-01-01",
error: { name: "MessageAbortedError" },
}
const input = {
id: "session",
status: "idle",
messages: [user, assistant],
parts: (_: string): Part[] => [],
submitting: false,
blocked: false,
loading: false,
reverted: false,
}
describe("empty prompt continuation", () => {
it("targets the stopped assistant rather than creating a new user turn", () => {
expect(continuation(input)).toBe(assistant.id)
expect(continuation({ ...input, messages: [user, { ...assistant, finish: "stop" }] })).toBe(assistant.id)
})
it("accepts unfinished assistant and tool-call turns", () => {
for (const finish of [undefined, "tool-calls"]) {
expect(continuation({ ...input, messages: [user, { ...assistant, error: undefined, finish }] })).toBe(
assistant.id,
)
}
})
it("does not resume completed, filtered, limited, or failed responses", () => {
for (const finish of ["stop", "length", "unknown", "content-filter", "error"]) {
expect(continuation({ ...input, messages: [user, { ...assistant, error: undefined, finish }] })).toBeUndefined()
}
expect(continuation({ ...input, messages: [user, { ...assistant, error: { name: "APIError" } }] })).toBeUndefined()
})
it("does not resume empty chats, previews, or a different user turn", () => {
expect(continuation({ ...input, id: undefined })).toBeUndefined()
expect(continuation({ ...input, id: "cloud:preview" })).toBeUndefined()
expect(continuation({ ...input, messages: [] })).toBeUndefined()
expect(continuation({ ...input, messages: [user] })).toBeUndefined()
expect(continuation({ ...input, messages: [user, { ...assistant, parentID: "older-user" }] })).toBeUndefined()
expect(continuation({ ...input, messages: [user, assistant, { ...user, id: "next-user" }] })).toBeUndefined()
expect(continuation({ ...input, messages: [user, { ...assistant, summary: true }] })).toBeUndefined()
})
it("keeps busy, pending, blocked, loading, and reverted sessions unavailable", () => {
for (const status of ["busy", "retry", "offline"]) {
expect(continuation({ ...input, status })).toBeUndefined()
}
for (const key of ["submitting", "blocked", "loading", "reverted"] as const) {
expect(continuation({ ...input, [key]: true })).toBeUndefined()
}
})
})
@@ -7,6 +7,7 @@ import {
deleteDraftsForSession,
drafts,
imageDrafts,
mentionDrafts,
reviewDrafts,
} from "../../webview-ui/src/utils/draft-store"
import {
@@ -23,6 +24,7 @@ beforeEach(() => {
browserDrafts.clear()
reviewDrafts.clear()
imageDrafts.clear()
mentionDrafts.clear()
})
describe("failedPrompt", () => {
@@ -80,6 +82,7 @@ describe("deleteDraftsForSession", () => {
])
reviewDrafts.set("prompt:default:session:a", [])
imageDrafts.set("prompt:default:session:a", [])
mentionDrafts.set("prompt:default:session:a", { paths: ["file with spaces.ts"], sessions: [] })
deleteDraftsForSession("a")
@@ -90,6 +93,7 @@ describe("deleteDraftsForSession", () => {
expect(browserDrafts.get("prompt:default:session:b")?.[0]?.selector).toBe("#other")
expect(reviewDrafts.has("prompt:default:session:a")).toBe(false)
expect(imageDrafts.has("prompt:default:session:a")).toBe(false)
expect(mentionDrafts.has("prompt:default:session:a")).toBe(false)
})
it("is a no-op when given an empty id", () => {
@@ -121,6 +121,7 @@ describe("terminal", () => {
remaining: 1,
})
expect(terminal({ reason: "error", messages: [message("error")], todos: [] })?.kind).toBe("error")
expect(terminal({ reason: "error", messages: [message("stop")], todos: [] })?.kind).toBe("error")
})
it("does not duplicate a concrete rendered failure", () => {
@@ -39,6 +39,18 @@ describe("session preference recovery", () => {
})
})
it.each([undefined, ""])("restores model default %s instead of an older effort", (variant) => {
const prefs = resolveMessagePrefs(
[
msg({ model: { providerID: "anthropic", modelID: "claude-sonnet-4", variant: "high" } }),
msg({ model: { providerID: "anthropic", modelID: "claude-sonnet-4", variant } }),
],
agents,
)
expect(prefs.variant).toBe("")
})
it("ignores assistant-only model data and invalid agents", () => {
const prefs = resolveMessagePrefs(
[
@@ -66,7 +78,7 @@ describe("session preference recovery", () => {
expect(prefs).toEqual({
agent: "code",
model: { providerID: "anthropic", modelID: "claude-sonnet-4" },
variant: undefined,
variant: "",
})
})
})
@@ -4,12 +4,19 @@ import {
messageTurns,
partitionTurns,
queuedUserMessageIDs,
removeQueuedMessage,
stableMessageTurns,
visibleMessages,
visibleParts,
type RevertBoundary,
} from "../../webview-ui/src/context/session-queue"
import type { Message, Part, SessionStatusInfo } from "../../webview-ui/src/types/messages"
import type {
ExtensionMessage,
Message,
Part,
SessionStatusInfo,
WebviewMessage,
} from "../../webview-ui/src/types/messages"
const base = {
sessionID: "session",
@@ -634,3 +641,57 @@ describe("activeUserMessageID", () => {
expect(activeUserMessageID(messages, { type: "busy" })).toBe("message_3")
})
})
describe("queued deletion cleanup", () => {
const setup = (timeout = 10_000) => {
const listeners = new Set<(message: ExtensionMessage) => void>()
const sent: WebviewMessage[] = []
const promise = removeQueuedMessage(
{
onMessage: (handler) => {
listeners.add(handler)
return () => {
listeners.delete(handler)
}
},
postMessage: (message) => {
sent.push(message)
},
},
"session",
"message",
timeout,
)
return {
promise,
listeners,
sent,
emit: (message: ExtensionMessage) => listeners.forEach((handler) => handler(message)),
}
}
it.each([true, false])("settles only the matching acknowledgment: %p", async (success) => {
const state = setup()
const request = state.sent.at(0)
if (request?.type !== "deleteMessage") throw new Error("Missing deletion request")
state.emit({ type: "connectionState", state: "connecting" })
state.emit({ ...request, type: "deleteMessageResult", requestID: "unrelated", success: true })
expect(state.listeners.size).toBe(1)
state.emit({ ...request, type: "deleteMessageResult", success })
expect(await state.promise).toBe(success)
expect(state.listeners.size).toBe(0)
})
const interruptions: Array<ExtensionMessage | undefined> = [
undefined,
{ type: "connectionState", state: "disconnected" },
{ type: "connectionState", state: "error" },
{ type: "sessionDeleted", sessionID: "session" },
]
it.each(interruptions)("releases the waiter and listener after timeout or interruption: %p", async (message) => {
const state = setup(1)
if (message) state.emit(message)
expect(await state.promise).toBe(false)
expect(state.listeners.size).toBe(0)
})
})
@@ -52,6 +52,31 @@ describe("per-session variant selection", () => {
expect(getAgentVariant(store, model, { variants: { low: {}, high: {} } }, "ask")).toBe("high")
})
it("falls back to the configured mode variant", () => {
expect(getAgentVariant({}, model, { variants: { high: {}, max: {} } }, "code", "max")).toBe("max")
})
it.each(["anthropic/claude-sonnet-4", variantKey(model, "code")])(
"prefers the configured variant over the remembered preference %s",
(key) => {
const store = { [key]: "high" }
expect(getVariant(store, model, ["high", "max"], "code", "pending-new", "max")).toBe("max")
expect(getAgentVariant(store, model, { variants: { high: {}, max: {} } }, "code", "max")).toBe("max")
},
)
it.each(["low", ""])("preserves a session choice %s above configured and remembered variants", (value) => {
const store = {
[variantKey(model, "code")]: "high",
[variantKey(model, "code", "session-a")]: value,
}
expect(getVariant(store, model, ["low", "high", "max"], "code", "session-a", "max")).toBe(value || undefined)
})
it("ignores a configured variant that the model does not support", () => {
expect(getAgentVariant({}, model, { variants: { low: {}, high: {} } }, "code", "max")).toBeUndefined()
})
it("uses the model default when no variant is selected", () => {
expect(getVariant({}, model, variants, "code")).toBeUndefined()
expect(getVariant({ [variantKey(model, "code")]: "" }, model, variants, "code")).toBeUndefined()
@@ -4,7 +4,8 @@ import type { ExtensionMessage, ModelSelection } from "../../webview-ui/src/type
const model: ModelSelection = { providerID: "anthropic", modelID: "claude-sonnet-4" }
function setup(session?: string) {
function setup(session?: string, configured?: string) {
const config = { model: "anthropic/claude-sonnet-4", variant: configured }
const selections: Record<string, string> = {}
const messages: Array<{ type: string; key?: string; value?: string }> = []
const order: string[] = []
@@ -17,7 +18,8 @@ function setup(session?: string) {
selected: () => model,
session: () => session,
agent: () => "code",
find: () => ({ variants: { low: {}, high: {} } }),
config: () => config,
find: () => ({ variants: { low: {}, high: {}, max: {} } }),
post: (message) => {
order.push("post")
messages.push(message)
@@ -28,7 +30,7 @@ function setup(session?: string) {
return () => order.push("unsub")
},
})
return { variants, selections, messages, order, dispatch: (message: ExtensionMessage) => handler?.(message) }
return { variants, config, selections, messages, order, dispatch: (message: ExtensionMessage) => handler?.(message) }
}
describe("session variants", () => {
@@ -51,6 +53,48 @@ describe("session variants", () => {
expect(state.selections).toEqual({ "agent/code/anthropic/claude-sonnet-4": "high" })
})
it("uses the configured agent variant when no picker selection exists", () => {
const state = setup(undefined, "max")
expect(state.variants.agent("code", model)).toBe("max")
expect(state.variants.current()).toBe("max")
expect(state.variants.request()).toBe("max")
})
it("uses updated configuration ahead of remembered defaults for new tabs", () => {
const state = setup("pending-new", "high")
state.selections["agent/code/anthropic/claude-sonnet-4"] = "low"
expect(state.variants.current()).toBe("high")
state.config.variant = "max"
expect(state.variants.current()).toBe("max")
expect(state.variants.request()).toBe("max")
expect(state.variants.agent("code", model)).toBe("max")
})
it("does not apply a configured variant to another model", () => {
const state = setup("pending-new", "max")
state.config.model = "anthropic/another-model"
expect(state.variants.current()).toBeUndefined()
expect(state.variants.agent("code", model)).toBeUndefined()
})
it("sends an explicit model default instead of inheriting the configured agent variant", () => {
const state = setup("session-a", "max")
state.variants.select(undefined)
expect(state.variants.current()).toBeUndefined()
expect(state.variants.request()).toBe("")
expect(state.variants.current("session-b")).toBe("max")
expect(state.variants.request("session-b")).toBe("max")
})
it.each(["sidebar-pending:new", "pending:new"])("keeps a pre-submit Default choice scoped to %s", (id) => {
const state = setup(undefined, "max")
state.variants.select(undefined, id)
expect(state.variants.current(id)).toBeUndefined()
expect(state.variants.request(id)).toBe("")
expect(state.variants.current("another-draft")).toBe("max")
expect(state.messages).toEqual([])
})
it("persists global selections but keeps session selections local", () => {
const global = setup()
global.variants.select("high")

Some files were not shown because too many files have changed in this diff Show More