Merge remote-tracking branch 'origin/main' into johnnyeric/kilo-opencode-v1.17.4

# Conflicts:
#	packages/opencode/src/cli/cmd/tui.ts
#	packages/opencode/src/cli/tui/worker.ts
#	packages/opencode/test/kilocode/cli/tui/thread.test.ts
This commit is contained in:
Johnny Amancio
2026-07-17 11:50:24 +02:00
159 changed files with 5751 additions and 813 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@kilocode/cli": minor
"kilo-code": minor
---
Allow agents to stop and remove a targeted Agent Manager session.
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Improve CLI sidebar usage sections with cent-formatted costs, collapsible details, and aligned model totals.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Respect the Terminal Command Blocks display setting for background process tool cards.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-gateway": patch
---
Show the actual routed model and upstream cost for Kilo Gateway Anthropic Messages and OpenAI Responses requests, including requests served through OpenRouter.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Support bidirectional text in chat messages so right-to-left and mixed-language conversations render in the correct direction.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Open the Agent Manager new-worktree dialog from primary actions and keyboard shortcuts, with an explicit quick-create action in its dropdown.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/cli": patch
---
Speed up local conversation recall searches on large histories.
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep sessions usable after deprecated local review commands complete without a session status transition.
@@ -1,5 +0,0 @@
---
"@kilocode/cli": patch
---
Restore directory `@`-mentions by listing their entries without inlining child file contents. Untrusted external directory attachments remain denied.
-6
View File
@@ -1,6 +0,0 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---
Show newly submitted messages immediately after reverting a conversation.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-ui": patch
---
Keep chat auto-scroll detached after a small upward wheel scroll near the latest message.
@@ -1,5 +0,0 @@
---
"@kilocode/cli": patch
---
Fix Grok 4.5 reasoning variants not showing up in the model picker.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Support editing custom OpenAI-compatible providers from JetBrains settings and replace their Disconnect action with Edit and Delete. Added or edited providers stay selected, and the custom provider dialog now closes after a successful save.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Fix adding a Custom OpenAI-Compatible Provider silently failing. The dialog now requires at least one model and reports save errors inline so you can correct your input and retry without re-entering the form.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Match the model picker Close button styling to JetBrains dialog primary buttons.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Use a trash icon for provider delete and show provider edit/delete actions on selection, matching the other settings lists.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/cli": minor
---
Improve CLI project-memory controls, status, activity indicators, and optional recall details.
-6
View File
@@ -1,6 +0,0 @@
---
"@kilocode/kilo-memory": minor
"kilo-code": minor
---
Improve project-memory activity in VS Code with a task-header menu, quick actions, and optional verbose details.
-7
View File
@@ -1,7 +0,0 @@
---
"@kilocode/cli": minor
"@kilocode/sdk": minor
"kilo-code": minor
---
Support verbose project-memory settings and show recalled memory snippets in conversation markers when enabled.
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-indexing": patch
---
Allow remote embedder validation timeouts to retry before indexing reports a connection failure.
@@ -0,0 +1,5 @@
---
"@kilocode/cli": minor
---
Remote CLI: expose slash command discovery, execution, and `/new` session creation over the relay. `list_commands` and `send_command` (including the built-in `compact` flow) are scoped to the current session's directory. `create_session` creates a root session in that directory, attaches it to the relay heartbeat set, and returns the new session id only after the heartbeat completes so the mobile client can navigate immediately. Failures are sanitized; the command is not auto-retried and the user may retry manually after a transient relay failure.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/cli": patch
---
Retry incomplete model responses that end without final output or tool activity while preserving partial answers and completed tools.
+26 -25
View File
@@ -32,7 +32,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "7.4.9",
"version": "7.4.11",
"bin": {
"opencode": "./bin/opencode",
},
@@ -127,7 +127,7 @@
},
"packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -141,7 +141,7 @@
},
"packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"effect": "catalog:",
},
@@ -153,7 +153,7 @@
},
"packages/http-recorder": {
"name": "@opencode-ai/http-recorder",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"@effect/platform-node": "4.0.0-beta.74",
"@effect/platform-node-shared": "4.0.0-beta.74",
@@ -174,7 +174,7 @@
},
"packages/kilo-console": {
"name": "@kilocode/kilo-console",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"@kilocode/kilo-web-ui": "workspace:*",
"@kilocode/sdk": "workspace:*",
@@ -196,7 +196,7 @@
},
"packages/kilo-docs": {
"name": "@kilocode/kilo-docs",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"@docsearch/css": "^4",
"@docsearch/js": "^4",
@@ -226,7 +226,7 @@
},
"packages/kilo-gateway": {
"name": "@kilocode/kilo-gateway",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/anthropic": "3.0.71",
@@ -262,7 +262,7 @@
},
"packages/kilo-i18n": {
"name": "@kilocode/kilo-i18n",
"version": "7.4.9",
"version": "7.4.11",
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
@@ -272,7 +272,7 @@
},
"packages/kilo-indexing": {
"name": "@kilocode/kilo-indexing",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"@aws-sdk/client-bedrock-runtime": "3.1005.0",
"@aws-sdk/credential-provider-ini": "3.972.31",
@@ -304,11 +304,11 @@
},
"packages/kilo-jetbrains": {
"name": "@kilocode/kilo-jetbrains",
"version": "7.4.5",
"version": "7.4.9",
},
"packages/kilo-memory": {
"name": "@kilocode/kilo-memory",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"effect": "catalog:",
"zod": "catalog:",
@@ -322,7 +322,7 @@
},
"packages/kilo-sandbox": {
"name": "@kilocode/sandbox",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"@anthropic-ai/sandbox-runtime": "catalog:",
"effect": "catalog:",
@@ -337,7 +337,7 @@
},
"packages/kilo-telemetry": {
"name": "@kilocode/kilo-telemetry",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"@kilocode/kilo-gateway": "workspace:*",
"posthog-node": "4.4.0",
@@ -351,7 +351,7 @@
},
"packages/kilo-ui": {
"name": "@kilocode/kilo-ui",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "0.13.11",
@@ -388,7 +388,7 @@
},
"packages/kilo-vscode": {
"name": "kilo-code",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@kilocode/kilo-gateway": "workspace:*",
@@ -457,7 +457,7 @@
},
"packages/kilo-web-ui": {
"name": "@kilocode/kilo-web-ui",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"@kilocode/kilo-ui": "workspace:*",
"@kobalte/core": "catalog:",
@@ -474,7 +474,7 @@
},
"packages/llm": {
"name": "@opencode-ai/llm",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"@smithy/eventstream-codec": "4.2.14",
"@smithy/util-utf8": "4.2.2",
@@ -492,7 +492,7 @@
},
"packages/opencode": {
"name": "@kilocode/cli",
"version": "7.4.9",
"version": "7.4.11",
"bin": {
"kilo": "./bin/kilo",
"kilocode": "./bin/kilo",
@@ -659,7 +659,7 @@
},
"packages/plugin": {
"name": "@kilocode/plugin",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"effect": "catalog:",
@@ -687,7 +687,7 @@
},
"packages/plugin-atomic-chat": {
"name": "@kilocode/plugin-atomic-chat",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"@kilocode/plugin": "workspace:*",
},
@@ -701,7 +701,7 @@
},
"packages/script": {
"name": "@opencode-ai/script",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"semver": "^7.6.3",
},
@@ -712,7 +712,7 @@
},
"packages/sdk/js": {
"name": "@kilocode/sdk",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -727,7 +727,7 @@
},
"packages/server": {
"name": "@opencode-ai/server",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"drizzle-orm": "catalog:",
@@ -741,7 +741,7 @@
},
"packages/storybook": {
"name": "@opencode-ai/storybook",
"version": "7.4.9",
"version": "7.4.11",
"devDependencies": {
"@opencode-ai/ui": "workspace:*",
"@solidjs/meta": "catalog:",
@@ -791,7 +791,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "catalog:",
@@ -833,6 +833,7 @@
"@types/katex": "0.16.7",
"@types/luxon": "catalog:",
"@typescript/native-preview": "catalog:",
"happy-dom": "20.8.9",
"tailwindcss": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-yNHrsBXfCWeuX13frVTjIL7U4k71wSimDtdTjoBndoY=",
"aarch64-linux": "sha256-zbRBPcPduV1a2EH4BiZWPJ+LyZMLCEPkQ3s+U7tDxtk=",
"aarch64-darwin": "sha256-NLLlE2t3vs1E5fDwyiFGoneJAdTb6uXrFuVa+LwBtlY=",
"x86_64-darwin": "sha256-BH31+FPgkt1xXJixkj+DgXIveJ49fWk4AIc3Hx+4XzM="
"x86_64-linux": "sha256-SS/pjldHwh2DdsWAvKJ8NivbUWuzo1GOgOoPCuJEd08=",
"aarch64-linux": "sha256-57nGVTiyjyjl2/27kd1rHQLgbO3XfeZp6xXBhMXb12Q=",
"aarch64-darwin": "sha256-OTtzy5ytPFMVKTE1tqi55aKxcdvdPe1yyqthvEZrVTY=",
"x86_64-darwin": "sha256-4It5t+dcvwv6UMOFXX67Kxp2qxoDOrvHKeuWTGzYYX4="
}
}
+1 -1
View File
@@ -165,6 +165,6 @@
"pacote@21.5.1": "patches/pacote@21.5.1.patch",
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch"
},
"version": "7.4.9",
"version": "7.4.11",
"peerDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.4.9",
"version": "7.4.11",
"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.4.9",
"version": "7.4.11",
"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.4.9",
"version": "7.4.11",
"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.4.9"
version = "7.4.11"
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.4.9/opencode-darwin-arm64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/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.4.9/opencode-darwin-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.9/opencode-linux-arm64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/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.4.9/opencode-linux-x64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/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.4.9/opencode-windows-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/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.4.9",
"version": "7.4.11",
"name": "@opencode-ai/http-recorder",
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-console",
"version": "7.4.9",
"version": "7.4.11",
"private": true,
"type": "module",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-docs",
"version": "7.4.9",
"version": "7.4.11",
"private": true,
"scripts": {
"dev": "next dev --webpack --port 3002",
@@ -35,7 +35,7 @@ Parallel work pays off when sessions are **independent** — neither one's outpu
Every productive worktree session follows the same rhythm:
1. **Create a worktree** (`Cmd+N` / `Ctrl+N`) and describe the task.
1. **Open a new worktree dialog** (`Cmd+N` / `Ctrl+N`), then describe the task.
2. **Let the agent run.** Switch to another worktree, another session, or step away.
3. **Verify manually.** Before you trust "all tests pass", run the app with the run script (`Cmd+E` / `Ctrl+E`) or open the worktree's terminal (`Cmd+/` / `Ctrl+/`) and run the tests yourself.
4. **Review the diff** (`Cmd+D` / `Ctrl+D`). Drop inline comments, then **Send to chat** to feed them back to the agent.
@@ -48,7 +48,7 @@ The single biggest lever on this loop is **keeping each worktree's scope small**
### 1. Side quest
Something unrelated came up while you are mid-task. Create a new worktree for it (`Cmd+N`), let the agent work, review when it is done. Your main work is unaffected.
Something unrelated came up while you are mid-task. Open a new worktree dialog for it (`Cmd+N`), let the agent work, review when it is done. Your main work is unaffected.
### 2. Build a skeleton, then split the work
@@ -64,7 +64,7 @@ This mirrors how a human team works: agree the API contract first, then split ba
For genuinely hard tasks where you do not know which approach will work:
1. Open the advanced new-worktree dialog (`Cmd+Shift+N` / `Ctrl+Shift+N`) and pick 24 versions.
1. Open the new-worktree dialog (`Cmd+N` / `Ctrl+N`) and pick 24 versions.
2. Optionally assign a different model to each.
3. Review the diffs side by side, pick the winner, apply it, discard the rest.
@@ -76,7 +76,7 @@ A related pattern: use the sidebar as an investigation surface. Start two or thr
### 5. A worktree per bug
For a day of small fixes: one worktree per bug (`Cmd+N`), one branch per fix, merge each quickly so none drift. Close the worktree when the fix lands.
For a day of small fixes: one worktree per bug. Use `Cmd+N` to configure each worktree or `Cmd+Shift+N` to create one immediately from the default branch, merge each quickly so none drift. Close the worktree when the fix lands.
### 6. Multiple sessions on one branch
@@ -225,8 +225,9 @@ Merge the most foundational one first. Then, in each remaining worktree, ask the
| Situation | Where |
|---|---|
| Small, interactive task | Sidebar |
| Long task, want to do something else meanwhile | New worktree (`Cmd+N`) |
| Two or three approaches, pick the winner | Multi-version (`Cmd+Shift+N`) |
| Long task, want to do something else meanwhile | Configure a new worktree (`Cmd+N`) |
| Quick task on the default base branch | Create a new worktree (`Cmd+Shift+N`) |
| Two or three approaches, pick the winner | Configure multi-version worktrees (`Cmd+N`) |
| Sidebar task outgrew the sidebar | Continue in Worktree |
| Separate conversation on the same branch | New tab (`Cmd+T`) |
| Long conversation, want a fresh context on same branch | New tab, summarize |
@@ -69,7 +69,7 @@ The extension uses `gh` to automatically discover PRs for each worktree branch.
2. **Branch name**`gh pr view <branch>` matches same-repo branches pushed to origin
3. **HEAD commit SHA**`gh pr list --search "<sha>"` as a last resort, matching PRs whose head ref points to the exact same commit
You can also import a PR directly from the advanced new worktree dialog: open the **New Worktree** dropdown and select **Advanced**, or press `Cmd+Shift+N` (macOS) / `Ctrl+Shift+N` (Windows/Linux), switch to the **Import** tab, then paste the GitHub PR URL. The branch is checked out and the badge appears automatically.
You can also import a PR directly from the new worktree dialog: click **New Worktree** or press `Cmd+N` (macOS) / `Ctrl+N` (Windows/Linux), switch to the **Import** tab, then paste the GitHub PR URL. The branch is checked out and the badge appears automatically.
#### Badge colors
@@ -106,12 +106,14 @@ PR badges update automatically in the background. The active worktree refreshes
### Creating a New Worktree Session
1. Click **New Worktree** or press `Cmd+N` (macOS) / `Ctrl+N` (Windows/Linux) to create a new worktree
1. Click **New Worktree** or press `Cmd+N` (macOS) / `Ctrl+N` (Windows/Linux) to open the new worktree dialog
2. Enter a branch name (or let Kilo generate one)
3. Type your first message to start the agent
3. Type your first message, then create the worktree
A new git worktree is created from your current branch. The agent works in isolation — your main branch is unaffected.
To create a worktree immediately from the default base branch, press `Cmd+Shift+N` (macOS) / `Ctrl+Shift+N` (Windows/Linux).
### Multi-Version Mode
You can run up to 4 parallel implementations of the same prompt across separate worktrees:
@@ -352,8 +354,8 @@ Closing a managed worktree removes it from Agent Manager, deletes its `.kilo/wor
| Shortcut (macOS) | Shortcut (Windows/Linux) | Action |
|---|---|---|
| `Cmd+Shift+M` | `Ctrl+Shift+M` | Open / focus Agent Manager (works from anywhere) |
| `Cmd+N` | `Ctrl+N` | New worktree |
| `Cmd+Shift+N` | `Ctrl+Shift+N` | New worktree (advanced options) |
| `Cmd+N` | `Ctrl+N` | Configure a new worktree |
| `Cmd+Shift+N` | `Ctrl+Shift+N` | Create a new worktree immediately |
| `Cmd+Shift+O` | `Ctrl+Shift+O` | Import/open worktree |
| `Cmd+Shift+W` | `Ctrl+Shift+W` | Close current worktree |
| `Cmd+T` | `Ctrl+T` | New tab (session) in worktree |
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6a89d1a9a31f7de1780d772b7caf71d96f0b4b895df954c77dcd508a7febe7c9
size 1950
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9a864b6a5c0e88bc00292f8a38685acdf2ecdd9eb5d880a663ba830152b7280b
size 3523
oid sha256:431a0c12cb844b5d4e535cc4c7bf5546fd9e66a178b8023b00978d07c291a531
size 3919
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08f1498ef61670dfec83a2de45f1b9d7008c6c9987705a39175eb990182ee5c2
size 6156
oid sha256:f68544cdf994abca3004e652911aa8edd77cf2bb3f91678e5dc6210022dff2f9
size 6188
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-gateway",
"version": "7.4.9",
"version": "7.4.11",
"type": "module",
"license": "MIT",
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
@@ -54,7 +54,7 @@ type Data = z.infer<typeof dataSchema>
function routed(meta: Gateway) {
const route = meta.routing
if (!route) return
const hit = route.modelAttempts?.findLast((item) => item.success === true)
const hit = route.modelAttempts?.slice().reverse().find((item) => item.success === true)
const id = hit?.canonicalSlug ?? route.canonicalSlug
if (!id) return
const value = id.trim()
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-i18n",
"version": "7.4.9",
"version": "7.4.11",
"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.4.9",
"version": "7.4.11",
"type": "module",
"license": "MIT",
"description": "Standalone indexing engine and host helpers for Kilo Code",
@@ -16,7 +16,7 @@ import { VoyageEmbedder } from "./embedders/voyage"
import { QdrantVectorStore } from "./vector-store/qdrant-client"
import { LanceDBVectorStore } from "./vector-store/lancedb-vector-store"
import { codeParser, DirectoryScanner, FileWatcher } from "./processors"
import type { ICodeParser, IEmbedder, IFileWatcher, IVectorStore } from "./interfaces"
import type { AvailableEmbedders, ICodeParser, IEmbedder, IFileWatcher, IVectorStore } from "./interfaces"
import type { CodeIndexConfigManager } from "./config-manager"
import type { CacheManager } from "./cache-manager"
import type { IndexingTelemetryMeta, IndexingTelemetryReporter } from "./interfaces/telemetry"
@@ -31,10 +31,28 @@ import type { IgnoreMatcher } from "./shared/load-ignore"
const log = Log.create({ service: "indexing-factory" })
function timeout(provider: string): number {
if (provider === "ollama") return OLLAMA_EMBEDDER_REQUEST_TIMEOUT_MS
return REMOTE_EMBEDDER_VALIDATION_TIMEOUT_MS
}
// RATIONALE: The OpenAI SDK applies the per-attempt timeout and retries internally.
const policy = {
openai: undefined,
openrouter: undefined,
"openai-compatible": undefined,
kilo: undefined,
gemini: undefined,
mistral: undefined,
"vercel-ai-gateway": undefined,
ollama: {
timeout: OLLAMA_EMBEDDER_REQUEST_TIMEOUT_MS,
error: "Connection to embedding service failed (timeout)",
},
voyage: {
timeout: REMOTE_EMBEDDER_VALIDATION_TIMEOUT_MS,
error: "Connection failed. Please check the endpoint URL and network connectivity.",
},
bedrock: {
timeout: REMOTE_EMBEDDER_VALIDATION_TIMEOUT_MS,
error: "Connection failed. Please check the endpoint URL and network connectivity.",
},
} satisfies Record<AvailableEmbedders, { timeout: number; error: string } | undefined>
/**
* Factory class responsible for creating and configuring code indexing service dependencies.
@@ -128,26 +146,26 @@ export class CodeIndexServiceFactory {
}
public async validateEmbedder(embedder: IEmbedder): Promise<{ valid: boolean; error?: string }> {
const ms = timeout(embedder.embedderInfo.name)
const deadline = policy[embedder.embedderInfo.name]
let timer: ReturnType<typeof setTimeout> | undefined
const wait = embedder.validateConfiguration()
const fail = new Promise<{ valid: boolean; error?: string }>((resolve) => {
timer = setTimeout(
() =>
resolve({
valid: false,
error:
embedder.embedderInfo.name === "ollama"
? "Connection to embedding service failed (timeout)"
: "Connection failed. Please check the endpoint URL and network connectivity.",
}),
ms,
)
})
const fail =
deadline === undefined
? undefined
: new Promise<{ valid: boolean; error?: string }>((resolve) => {
timer = setTimeout(
() =>
resolve({
valid: false,
error: deadline.error,
}),
deadline.timeout,
)
})
try {
log.info("validating embedder", { provider: embedder.embedderInfo.name })
const result = await Promise.race([wait, fail])
const result = fail ? await Promise.race([wait, fail]) : await wait
if (result.valid) {
log.info("embedder validation succeeded", { provider: embedder.embedderInfo.name })
}
@@ -1,6 +1,11 @@
import { describe, expect, test, mock, beforeEach } from "bun:test"
import path from "path"
import { mockEmbeddingsCreate, openAIMockFactory, setOpenAIConstructorHook } from "./embedders/__helpers__/openai-mock"
import {
OLLAMA_EMBEDDER_REQUEST_TIMEOUT_MS,
REMOTE_EMBEDDER_VALIDATION_TIMEOUT_MS,
} from "../../../src/indexing/constants"
import type { AvailableEmbedders, IEmbedder } from "../../../src/indexing/interfaces/embedder"
mock.module("openai", openAIMockFactory)
import { CodeIndexServiceFactory } from "../../../src/indexing/service-factory"
@@ -23,6 +28,20 @@ function createFactory(input?: Partial<ConstructorParameters<typeof CodeIndexCon
return new CodeIndexServiceFactory(cfg, workspacePath, cache, cacheDirectory)
}
function createEmbedder(name: AvailableEmbedders): IEmbedder {
return {
async createEmbeddings() {
return { embeddings: [] }
},
async validateConfiguration() {
return { valid: true }
},
get embedderInfo() {
return { name }
},
}
}
describe("CodeIndexServiceFactory", () => {
beforeEach(() => {
mockEmbeddingsCreate.mockReset()
@@ -39,6 +58,55 @@ describe("CodeIndexServiceFactory", () => {
expect(factory.createEmbedder().embedderInfo).toEqual({ name: "openai-compatible" })
})
test("lets SDK-backed embedders own validation timeouts", async () => {
const original = globalThis.setTimeout
const timer = mock((...args: Parameters<typeof setTimeout>) => original(...args))
globalThis.setTimeout = timer
try {
const factory = createFactory()
const providers = [
"openai",
"openrouter",
"openai-compatible",
"kilo",
"gemini",
"mistral",
"vercel-ai-gateway",
] satisfies AvailableEmbedders[]
for (const provider of providers) {
await expect(factory.validateEmbedder(createEmbedder(provider))).resolves.toEqual({ valid: true })
}
expect(timer).not.toHaveBeenCalled()
} finally {
globalThis.setTimeout = original
}
})
test("retains factory deadlines for non-SDK embedders", async () => {
const original = globalThis.setTimeout
const timer = mock((...args: Parameters<typeof setTimeout>) => original(...args))
globalThis.setTimeout = timer
try {
const factory = createFactory()
await factory.validateEmbedder(createEmbedder("voyage"))
await factory.validateEmbedder(createEmbedder("bedrock"))
await factory.validateEmbedder(createEmbedder("ollama"))
expect(timer.mock.calls.map((call) => call[1])).toEqual([
REMOTE_EMBEDDER_VALIDATION_TIMEOUT_MS,
REMOTE_EMBEDDER_VALIDATION_TIMEOUT_MS,
OLLAMA_EMBEDDER_REQUEST_TIMEOUT_MS,
])
} finally {
globalThis.setTimeout = original
}
})
test("uses default LanceDB directory when config is unset", () => {
const factory = createFactory({ vectorStoreProvider: "lancedb", lancedbVectorStoreDirectory: undefined })
+12
View File
@@ -1,5 +1,17 @@
# Changelog
## 7.4.10
### Patch Changes
- [#12217](https://github.com/Kilo-Org/kilocode/pull/12217) [`d6b36a0`](https://github.com/Kilo-Org/kilocode/commit/d6b36a028cc0a4b7bfd158d75e287c110e2838f7) - Support editing custom OpenAI-compatible providers from JetBrains settings and replace their Disconnect action with Edit and Delete. Added or edited providers stay selected, and the custom provider dialog now closes after a successful save.
- [#12217](https://github.com/Kilo-Org/kilocode/pull/12217) [`6077c1c`](https://github.com/Kilo-Org/kilocode/commit/6077c1c3b36d4c5cd68f206fc146ca472d841c5e) - Fix adding a Custom OpenAI-Compatible Provider silently failing. The dialog now requires at least one model and reports save errors inline so you can correct your input and retry without re-entering the form.
- [#12217](https://github.com/Kilo-Org/kilocode/pull/12217) [`cae3270`](https://github.com/Kilo-Org/kilocode/commit/cae3270c9dacc4097a681539cf3e07cfadceaca7) - Match the model picker Close button styling to JetBrains dialog primary buttons.
- [#12217](https://github.com/Kilo-Org/kilocode/pull/12217) [`cae3270`](https://github.com/Kilo-Org/kilocode/commit/cae3270c9dacc4097a681539cf3e07cfadceaca7) - Use a trash icon for provider delete and show provider edit/delete actions on selection, matching the other settings lists.
## 7.4.6
### Patch Changes
@@ -264,58 +264,86 @@ class KiloBackendSessionManager(
// ------ mapping (generated API model → DTO) ------
private fun dto(s: ai.kilocode.jetbrains.api.model.Session) = SessionDto(
private fun dto(s: ai.kilocode.jetbrains.api.model.Session) = dto(
id = s.id,
projectID = s.projectID,
directory = s.directory,
parentID = s.parentID,
project = s.projectID,
dir = s.directory,
parent = s.parentID,
title = s.title,
version = s.version,
time = SessionTimeDto(
created = time(s.id, "created", s.time.created),
updated = time(s.id, "updated", s.time.updated),
archived = s.time.archived,
),
summary = s.summary?.let {
SessionSummaryDto(
additions = it.additions.safeInt(),
deletions = it.deletions.safeInt(),
files = it.files.safeInt(),
)
},
created = s.time.created,
updated = s.time.updated,
archived = s.time.archived,
summary = s.summary?.let { summary(it.additions, it.deletions, it.files) },
revert = revertDto(s.revert),
)
private fun dto(s: GlobalSession) = SessionDto(
private fun dto(s: ai.kilocode.jetbrains.api.model.Session1) = dto(
id = s.id,
projectID = s.projectID,
directory = s.directory,
parentID = s.parentID,
project = s.projectID,
dir = s.directory,
parent = s.parentID,
title = s.title,
version = s.version,
time = SessionTimeDto(
created = time(s.id, "created", s.time.created),
updated = time(s.id, "updated", s.time.updated),
archived = s.time.archived,
),
summary = s.summary?.let {
SessionSummaryDto(
additions = it.additions?.safeInt() ?: 0,
deletions = it.deletions?.safeInt() ?: 0,
files = it.files?.safeInt() ?: 0,
)
},
created = s.time.created,
updated = s.time.updated,
archived = s.time.archived,
summary = s.summary?.let { summary(it.additions, it.deletions, it.files) },
revert = revertDto(s.revert),
)
private fun dto(s: GlobalSession) = dto(
id = s.id,
project = s.projectID,
dir = s.directory,
parent = s.parentID,
title = s.title,
version = s.version,
created = s.time.created,
updated = s.time.updated,
archived = s.time.archived,
summary = s.summary?.let { summary(it.additions, it.deletions, it.files) },
revert = revertDto(s.revert),
)
private fun dto(
id: String,
project: String,
dir: String,
parent: String?,
title: String,
version: String,
created: Number?,
updated: Number?,
archived: Double?,
summary: SessionSummaryDto?,
revert: SessionRevertDto?,
) = SessionDto(
id = id,
projectID = project,
directory = dir,
parentID = parent,
title = title,
version = version,
time = SessionTimeDto(
created = time(id, "created", created),
updated = time(id, "updated", updated),
archived = archived,
),
summary = summary,
revert = revert,
)
private fun summary(add: Double?, del: Double?, files: Double?) = SessionSummaryDto(
additions = count(add),
deletions = count(del),
files = count(files),
)
private fun revertDto(s: ai.kilocode.jetbrains.api.model.SessionRevert?) = s?.let {
revertDto(it.messageID, it.partID, it.snapshot, it.diff)
}
private fun revertDto(s: ai.kilocode.jetbrains.api.model.GlobalSessionRevert?) = s?.let {
revertDto(it.messageID, it.partID, it.snapshot, it.diff)
}
private fun revertDto(message: String, part: String?, snapshot: String?, diff: String?) =
SessionRevertDto(
messageID = message,
@@ -334,6 +362,8 @@ class KiloBackendSessionManager(
private fun encode(value: String) = java.net.URLEncoder.encode(value, Charsets.UTF_8)
private fun count(value: Double?) = value?.safeInt() ?: 0
private fun time(id: String, field: String, value: Number?): Double {
if (value != null) return value.toDouble()
log.warn("Session $id missing $field timestamp; defaulting to 0.0")
@@ -124,16 +124,19 @@ internal object OpenApiSpecNormalizer {
as? JsonObject ?: return root
val props = schema["properties"] as? JsonObject ?: return root
val nullable = setOf("balance", "kiloPass", "currentOrgId")
val keys = setOf("balance", "kiloPass", "currentOrgId")
val fixed = JsonObject(props.mapValues { (key, value) ->
if (key !in nullable) return@mapValues value
val obj = value as? JsonObject ?: return@mapValues value
// Skip if already wrapped (has anyOf containing {type:null}).
val existing = obj["anyOf"] as? JsonArray
if (existing != null && existing.any {
(it as? JsonObject)?.get("type")?.let { t -> (t as? JsonPrimitive)?.content } == "null"
}) return@mapValues value
JsonObject(mapOf("anyOf" to JsonArray(listOf(obj, JsonObject(mapOf("type" to JsonPrimitive("null")))))))
if (key !in keys) return@mapValues value
val wrapped = nullable(value)
when (key) {
"balance" -> nullableProps(wrapped, setOf("balance"))
"kiloPass" -> nullableProps(wrapped, setOf(
"currentPeriodBaseCreditsUsd",
"currentPeriodUsageUsd",
"currentPeriodBonusCreditsUsd",
))
else -> wrapped
}
})
// Rebuild nested objects up to root.
@@ -152,6 +155,29 @@ internal object OpenApiSpecNormalizer {
return JsonObject(root + mapOf("paths" to newPaths))
}
private fun nullable(value: JsonElement): JsonElement {
val obj = value as? JsonObject ?: return value
val any = obj["anyOf"] as? JsonArray
if (any != null && any.any(::nullSchema)) return obj
return JsonObject(mapOf("anyOf" to JsonArray(listOf(obj, JsonObject(mapOf("type" to JsonPrimitive("null")))))))
}
private fun nullableProps(value: JsonElement, names: Set<String>): JsonElement {
val obj = value as? JsonObject ?: return value
val any = obj["anyOf"] as? JsonArray
if (any != null) {
return JsonObject(obj + mapOf("anyOf" to JsonArray(any.map { nullableProps(it, names) })))
}
val props = obj["properties"] as? JsonObject ?: return obj
val fixed = JsonObject(props.mapValues { (key, prop) ->
if (key in names) nullable(prop) else prop
})
return JsonObject(obj + mapOf("properties" to fixed))
}
private fun nullSchema(value: JsonElement) =
(value as? JsonObject)?.get("type")?.let { (it as? JsonPrimitive)?.content } == "null"
/**
* Deduplicate the root-level "tags" array by name — the spec validator
* rejects repeated tag names even when they describe different things.
@@ -148,6 +148,12 @@ class OpenApiSpecNormalizerTest {
val balanceTypes = balanceAnyOf.map { (it as? JsonObject)?.get("type").let { t -> (t as? JsonPrimitive)?.content } }
assert("null" in balanceTypes) { "balance anyOf should include null but got $balanceTypes" }
assert(balanceAnyOf.any { it is JsonObject && "properties" in it }) { "balance anyOf should include the object schema" }
val balanceObject = balanceAnyOf.filterIsInstance<JsonObject>().first { "properties" in it }
val balanceValue = obj(obj(balanceObject["properties"])["balance"])
val balanceValueTypes = arr(balanceValue["anyOf"]).map {
(it as? JsonObject)?.get("type").let { t -> (t as? JsonPrimitive)?.content }
}
assert("null" in balanceValueTypes) { "inner balance value should include null but got $balanceValueTypes" }
// kiloPass must be anyOf [object, null]
val pass = obj(props["kiloPass"])
@@ -156,6 +162,11 @@ class OpenApiSpecNormalizerTest {
val passTypes = passAnyOf.map { (it as? JsonObject)?.get("type").let { t -> (t as? JsonPrimitive)?.content } }
assert("null" in passTypes) { "kiloPass anyOf should include null but got $passTypes" }
assert(passAnyOf.any { it is JsonObject && "properties" in it }) { "kiloPass anyOf should include the object schema" }
val passObject = passAnyOf.filterIsInstance<JsonObject>().first { "properties" in it }
val passProps = obj(passObject["properties"])
val base = obj(passProps["currentPeriodBaseCreditsUsd"])
val baseTypes = arr(base["anyOf"]).map { (it as? JsonObject)?.get("type").let { t -> (t as? JsonPrimitive)?.content } }
assert("null" in baseTypes) { "inner kiloPass value should include null but got $baseTypes" }
// currentOrgId must be anyOf [string, null]
val orgId = obj(props["currentOrgId"])
+1 -1
View File
@@ -8,7 +8,7 @@
"test": "./gradlew test",
"test:ci": "bun script/test-ci.ts"
},
"version": "7.4.5",
"version": "7.4.9",
"dependencies": {},
"devDependencies": {},
"peerDependencies": {}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-memory",
"version": "7.4.9",
"version": "7.4.11",
"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.4.9",
"version": "7.4.11",
"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.4.9",
"version": "7.4.11",
"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.4.9",
"version": "7.4.11",
"type": "module",
"license": "MIT",
"exports": {
+4
View File
@@ -3,6 +3,10 @@ import { splitProps } from "solid-js"
const icons: Record<string, { path: string; viewBox: string }> = {
memory: {
viewBox: "0 0 24 24",
path: `<path d="M2 7h4M2 12h4M2 17h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><rect width="16" height="20" x="4" y="2" rx="2" stroke="currentColor" stroke-width="2"/>`,
},
database: {
viewBox: "0 0 24 24",
path: `<ellipse cx="12" cy="5" rx="7" ry="3" stroke="currentColor" stroke-width="2"/><path d="M5 5v7c0 1.66 3.13 3 7 3s7-1.34 7-3V5M5 12v7c0 1.66 3.13 3 7 3s7-1.34 7-3v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>`,
},
@@ -392,6 +392,9 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] {
}
[data-slot="user-message-text"] {
unicode-bidi: plaintext; /* Match the prompt input */
background-color: color-mix(in srgb, var(--text-base) 7%, transparent);
border: 1px solid transparent;
transition: opacity 0.3s ease;
&[data-queued] {
@@ -408,6 +411,11 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] {
}
}
body.vscode-high-contrast [data-component="user-message"] [data-slot="user-message-text"],
body.vscode-high-contrast-light [data-component="user-message"] [data-slot="user-message-text"] {
border-color: var(--border-weak-base);
}
[data-component="tool-output"] {
margin-bottom: 0px;
}
@@ -857,7 +857,7 @@ export function UserMessageDisplay(props: {
<div data-slot="user-message-body">
{props.header}
<Show when={text()}>
<div data-slot="user-message-text" data-queued={props.queued ? "" : undefined}>
<div data-slot="user-message-text" dir="auto" data-queued={props.queued ? "" : undefined}>
<HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
</div>
</Show>
+26
View File
@@ -1,5 +1,31 @@
# kilo-code
## 7.4.11
### Minor Changes
- [#12254](https://github.com/Kilo-Org/kilocode/pull/12254) [`30e7ec4`](https://github.com/Kilo-Org/kilocode/commit/30e7ec4ab45fac724b41ec0b4342e272e7f584d2) - Improve project-memory activity in VS Code with a task-header menu, quick actions, and optional verbose details.
- [#12250](https://github.com/Kilo-Org/kilocode/pull/12250) [`bd69158`](https://github.com/Kilo-Org/kilocode/commit/bd69158131aafdcc2f44aede22b573c2b0432f21) - Support verbose project-memory settings and show recalled memory snippets in conversation markers when enabled.
### Patch Changes
- [#12287](https://github.com/Kilo-Org/kilocode/pull/12287) [`bb75928`](https://github.com/Kilo-Org/kilocode/commit/bb759287495fabc1f3fa7e0fbecff13ab4d41cb2) - Use distinct icons for context, project memory, and code indexing, and group context controls to preserve task titles in narrow VS Code sidebars.
- [#12285](https://github.com/Kilo-Org/kilocode/pull/12285) [`9fa2e18`](https://github.com/Kilo-Org/kilocode/commit/9fa2e187047a4d2b6e76066af3c139c21851e854) - Prevent sessions from freezing after submitting or dismissing a question.
- [#12274](https://github.com/Kilo-Org/kilocode/pull/12274) [`5180c10`](https://github.com/Kilo-Org/kilocode/commit/5180c10c4f69500ce303437646371500a71dba46) - Show newly submitted messages immediately after reverting a conversation.
- Updated dependencies [[`319f159`](https://github.com/Kilo-Org/kilocode/commit/319f159ac333d18855a72ddb1fa61ed471ebf2d9), [`bb75928`](https://github.com/Kilo-Org/kilocode/commit/bb759287495fabc1f3fa7e0fbecff13ab4d41cb2), [`9fa8a13`](https://github.com/Kilo-Org/kilocode/commit/9fa8a13e26db552373149e1275597eb07861de91), [`30e7ec4`](https://github.com/Kilo-Org/kilocode/commit/30e7ec4ab45fac724b41ec0b4342e272e7f584d2), [`bd69158`](https://github.com/Kilo-Org/kilocode/commit/bd69158131aafdcc2f44aede22b573c2b0432f21)]:
- @kilocode/kilo-gateway@7.4.10
- @kilocode/kilo-ui@7.4.10
- @kilocode/kilo-memory@7.5.0
- @kilocode/sdk@7.5.0
- @opencode-ai/core@7.4.10
- @kilocode/kilo-indexing@7.4.10
- @kilocode/plugin@7.4.10
- @opencode-ai/ui@7.4.10
## 7.4.9
### Patch Changes
+18 -10
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.4.9",
"version": "7.4.11",
"icon": "assets/icons/logo-outline-black.png",
"galleryBanner": {
"color": "#FFFFFF",
@@ -291,6 +291,11 @@
},
{
"command": "kilo-code.new.agentManager.newWorktree",
"title": "Agent Manager: Configure New Worktree",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.agentManager.quickWorktree",
"title": "Agent Manager: New Worktree",
"category": "Kilo Code"
},
@@ -311,7 +316,7 @@
},
{
"command": "kilo-code.new.agentManager.advancedWorktree",
"title": "Agent Manager: Advanced New Worktree",
"title": "Agent Manager: Configure New Worktree",
"category": "Kilo Code"
},
{
@@ -472,6 +477,10 @@
"command": "kilo-code.new.sidebarTitle.kiloClawOpen",
"when": "false"
},
{
"command": "kilo-code.new.agentManager.advancedWorktree",
"when": "false"
},
{
"command": "kilo-code.new.sidebarTitle.marketplaceButtonClicked",
"when": "false"
@@ -713,6 +722,12 @@
"mac": "cmd+n",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.quickWorktree",
"key": "ctrl+shift+n",
"mac": "cmd+shift+n",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.openWorktree",
"key": "ctrl+shift+o",
@@ -731,12 +746,6 @@
"mac": "cmd+shift+w",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.advancedWorktree",
"key": "ctrl+shift+n",
"mac": "cmd+shift+n",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.jumpTo1",
"key": "ctrl+1",
@@ -1159,8 +1168,7 @@
"pretest": "bun run compile-tests && bun run compile && bun run lint",
"check-types": "tsc --noEmit",
"check-types:webview": "bun script/typecheck.ts --project webview-ui/tsconfig.json",
"typecheck": "bun run check-types:extension && bun run check-types:webview",
"check-types:extension": "bun script/typecheck.ts",
"typecheck": "bun run check-types && bun run check-types:webview",
"format": "prettier --write .",
"format:check": "prettier --check .",
"knip": "knip",
+4
View File
@@ -139,6 +139,7 @@ import {
import { fetchAndSendPendingSuggestions } from "./kilo-provider/handlers/suggestion"
import { nativeTitle } from "./kilo-provider/native-tab-title"
import { parseReview, reviewMetadata, type ReviewMessageData } from "./shared/review-comments"
import { completesWithoutStatus } from "./kilo-provider/command-completion"
import { KiloProviderMemory } from "./kilo-provider/memory"
import {
@@ -3413,6 +3414,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
messageID,
),
)
if (messageID && completesWithoutStatus(command)) {
this.postMessage({ type: "sessionCommandCompleted", messageID })
}
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to send command:", error)
this.postMessage({
@@ -194,6 +194,11 @@ export class AgentManagerProvider implements Disposable {
},
stats: (refresh) => this.statsPoller.snapshot(refresh),
prs: () => this.prBridge.snapshot(),
managed: (id) => this.panelSessions.has(id) || !!this.state?.getSession(id),
close: async (id) => {
await this.onCloseSession(id)
this.postToWebview({ type: "agentManager.sessionClosed", sessionId: id })
},
log: (...args) => this.log(...args),
})
this.unsubTool = this.connectionService.onEventFiltered(
@@ -25,10 +25,12 @@ interface RequestBase {
type Request =
| (RequestBase & { operation: "overview"; filter?: OverviewFilter })
| (RequestBase & { operation: "prompt"; targetSessionID: string; prompt: string })
| (RequestBase & { operation: "stop"; targetSessionID: string })
type Result =
| { operation: "overview"; overview: Overview }
| { operation: "prompt"; sessionID: string; delivered: true }
| { operation: "stop"; sessionID: string; stopped: true }
interface Failure {
code: FailureCode | "cancelled" | "disconnected" | "timeout"
@@ -41,6 +43,8 @@ interface Options {
state(): WorktreeStateManager | undefined
stats(refresh?: boolean): Promise<{ worktrees: WorktreeStats[]; local?: LocalStats }>
prs(): Map<string, PRStatus>
managed(sessionID: string): boolean
close(sessionID: string): Promise<void>
log(...args: unknown[]): void
}
@@ -262,17 +266,25 @@ export class AgentManagerOrchestrationBridge {
})
return { result: { operation: "overview", overview: result } }
}
await prompt({
client,
root,
state,
sessionID: request.targetSessionID,
text: request.prompt,
messageID: request.id,
signal: active.controller.signal,
})
if (request.operation === "prompt") {
await prompt({
client,
root,
state,
sessionID: request.targetSessionID,
text: request.prompt,
messageID: request.id,
signal: active.controller.signal,
})
if (this.disposed || active.cancelled) return
return { result: { operation: "prompt", sessionID: request.targetSessionID, delivered: true } }
}
if (!this.options.managed(request.targetSessionID)) {
throw new OrchestrationError("unknown_session", "The session is not managed by this Agent Manager workspace")
}
await this.options.close(request.targetSessionID)
if (this.disposed || active.cancelled) return
return { result: { operation: "prompt", sessionID: request.targetSessionID, delivered: true } }
return { result: { operation: "stop", sessionID: request.targetSessionID, stopped: true } }
} catch (error) {
if (this.disposed || active.cancelled) return
return { error: failure(error) }
@@ -190,6 +190,11 @@ interface SessionForkedMessage {
worktreeId?: string
}
interface SessionClosedMessage {
type: "agentManager.sessionClosed"
sessionId: string
}
interface MultiVersionProgressMessage {
type: "agentManager.multiVersionProgress"
status: "creating" | "done"
@@ -307,6 +312,7 @@ export type AgentManagerOutMessage =
| ErrorOutMessage
| SessionAddedMessage
| SessionForkedMessage
| SessionClosedMessage
| MultiVersionProgressMessage
| SetSessionModelMessage
| SendInitialMessage
+3
View File
@@ -498,6 +498,9 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand("kilo-code.new.agentManager.newWorktree", () => {
agentManagerProvider.postMessage({ type: "action", action: "newWorktree" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.quickWorktree", () => {
agentManagerProvider.postMessage({ type: "action", action: "quickWorktree" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.openWorktree", () => {
agentManagerProvider.postMessage({ type: "action", action: "openWorktree" })
}),
@@ -0,0 +1,3 @@
export function completesWithoutStatus(command: string): boolean {
return command === "local-review" || command === "local-review-uncommitted"
}
@@ -10,6 +10,7 @@ import type { CloudSessionData, EditorContext } from "../../services/cli-backend
import { getErrorMessage, sessionToWebview, mapCloudSessionMessageToWebviewMessage } from "../../kilo-provider-utils"
import type { MessageFile } from "../message-files"
import { reviewMetadata, type ReviewMessageData } from "../../shared/review-comments"
import { completesWithoutStatus } from "../command-completion"
const TIMEOUT = 30_000
@@ -232,6 +233,9 @@ export async function handleImportAndSend(
{ throwOnError: true },
)
})
if (messageID && command && completesWithoutStatus(command)) {
ctx.postMessage({ type: "sessionCommandCompleted", messageID })
}
} catch (err) {
console.error("[Kilo New] Failed to send message after cloud import:", err)
ctx.postMessage({
@@ -0,0 +1,74 @@
import { Window } from "happy-dom"
import type { QuestionRequest } from "../../webview-ui/src/types/messages"
const window = new Window()
Object.assign(globalThis, {
window,
document: window.document,
Node: window.Node,
Element: window.Element,
HTMLElement: window.HTMLElement,
SVGElement: window.SVGElement,
requestAnimationFrame: () => 0,
})
const { Show, createSignal } = await import("solid-js")
const { render } = await import("solid-js/web")
const { SessionContext } = await import("../../webview-ui/src/context/session")
const { LanguageContext } = await import("../../webview-ui/src/context/language")
const { QuestionDock } = await import("../../webview-ui/src/components/chat/QuestionDock")
const request: QuestionRequest = {
id: "question-1",
sessionID: "session-1",
questions: [
{
question: "Continue?",
header: "Confirm",
options: [{ label: "Yes", description: "Continue" }],
},
],
}
const [active, setActive] = createSignal<QuestionRequest | undefined>(request)
const calls: Array<{ id: string; answers: string[][] }> = []
const session = {
questionErrors: () => new Set<string>(),
selectedAgent: () => "code",
selectAgent: () => {},
replyToQuestion: (id: string, answers: string[][]) => {
calls.push({ id, answers })
setActive(undefined)
},
rejectQuestion: () => {},
closeQuestion: () => {},
}
const language = {
locale: () => "en",
setLocale: () => {},
userOverride: () => "",
t: (key: string) => key,
}
const root = document.createElement("div")
document.body.append(root)
const dispose = render(
() => (
<SessionContext.Provider value={session as never}>
<LanguageContext.Provider value={language as never}>
<Show when={active()}>{(item) => <QuestionDock request={item()} />}</Show>
</LanguageContext.Provider>
</SessionContext.Provider>
),
root,
)
const option = root.querySelector<HTMLButtonElement>('[data-slot="question-option"]')
const submit = root.querySelector<HTMLButtonElement>('[data-slot="question-footer-actions"] button')
if (!option || !submit) throw new Error("Question controls did not render")
option.click()
if (submit.disabled) throw new Error("Submit did not enable after selecting an answer")
submit.click()
if (calls.length !== 1 || calls[0]?.id !== request.id || calls[0]?.answers[0]?.[0] !== "Yes") {
throw new Error(`Unexpected question reply: ${JSON.stringify(calls)}`)
}
if (root.querySelector('[data-component="question-dock"]')) throw new Error("Question dock did not unmount")
dispose()
+1 -1
View File
@@ -1,6 +1,6 @@
{
"type": "module",
"version": "7.4.9",
"version": "7.4.11",
"dependencies": {},
"devDependencies": {},
"peerDependencies": {}
@@ -20,6 +20,7 @@ const CSS_FILES = [
]
const TSX_FILES = [
path.join(ROOT, "webview-ui/agent-manager/AgentManagerApp.tsx"),
path.join(ROOT, "webview-ui/agent-manager/UnassignedSessionsSection.tsx"),
path.join(ROOT, "webview-ui/agent-manager/NewWorktreeDialog.tsx"),
path.join(ROOT, "webview-ui/agent-manager/sortable-tab.tsx"),
path.join(ROOT, "webview-ui/agent-manager/DiffPanel.tsx"),
@@ -269,6 +270,89 @@ describe("Agent Manager Model Picker", () => {
})
})
describe("Agent Manager Worktree Actions", () => {
it("opens the configuration dialog from the primary plus action", () => {
const source = fs.readFileSync(path.join(ROOT, "webview-ui/agent-manager/WorktreeSectionActions.tsx"), "utf-8")
const start = source.indexOf('<div class="am-split-button">')
const end = source.indexOf("</div>", start)
const actions = source.slice(start, end)
expect(start).toBeGreaterThanOrEqual(0)
expect(end).toBeGreaterThan(start)
expect(actions).toContain("onClick={props.onNew}")
expect(actions).toContain('keybind={props.bindings.newWorktree ?? ""}')
expect(actions).toContain("<DropdownMenu.Item onSelect={props.onCreate}>")
expect(actions).toContain('props.t("sidebar.session.newWorktree.from")')
expect(actions).toContain('props.bindings.quickWorktree ?? ""')
expect(actions).not.toContain("onSelect={props.onNew}")
})
it("opens the configuration dialog from the new-worktree shortcut", () => {
const source = fs.readFileSync(TSX_FILE, "utf-8")
const start = source.indexOf('else if (msg.action === "newWorktree")')
const end = source.indexOf('else if (msg.action === "quickWorktree")', start)
const action = source.slice(start, end)
expect(start).toBeGreaterThanOrEqual(0)
expect(end).toBeGreaterThan(start)
expect(action).toContain("showNewWorktreeDialog()")
})
it("creates a worktree from the quick-worktree shortcut", () => {
const source = fs.readFileSync(TSX_FILE, "utf-8")
const start = source.indexOf('else if (msg.action === "quickWorktree")')
const end = source.indexOf('else if (msg.action === "openWorktree")', start)
const action = source.slice(start, end)
expect(start).toBeGreaterThanOrEqual(0)
expect(end).toBeGreaterThan(start)
expect(action).toContain("handleCreateWorktree()")
})
it("registers distinct dialog and quick-create worktree shortcuts", () => {
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf-8")) as {
contributes: { keybindings: { command: string; key?: string; mac?: string }[] }
}
const source = fs.readFileSync(TSX_FILE, "utf-8")
const dialog = manifest.contributes.keybindings.find(
(item) => item.command === "kilo-code.new.agentManager.newWorktree",
)
const quick = manifest.contributes.keybindings.find(
(item) => item.command === "kilo-code.new.agentManager.quickWorktree",
)
expect(dialog).toMatchObject({ key: "ctrl+n", mac: "cmd+n" })
expect(quick).toMatchObject({ key: "ctrl+shift+n", mac: "cmd+shift+n" })
expect(source).toContain('newWorktree: isMac ? "⌘N" : "Ctrl+N"')
expect(source).toContain('quickWorktree: isMac ? "⌘⇧N" : "Ctrl+Shift+N"')
})
it("forwards the quick-worktree command to immediate creation", () => {
const source = fs.readFileSync(path.join(ROOT, "src/extension.ts"), "utf-8")
const start = source.indexOf('vscode.commands.registerCommand("kilo-code.new.agentManager.quickWorktree"')
const end = source.indexOf('vscode.commands.registerCommand("kilo-code.new.agentManager.openWorktree"', start)
const command = source.slice(start, end)
expect(start).toBeGreaterThanOrEqual(0)
expect(end).toBeGreaterThan(start)
expect(command).toContain('agentManagerProvider.postMessage({ type: "action", action: "quickWorktree" })')
})
it("shows the same mapping in the keyboard shortcuts dialog", () => {
const source = fs.readFileSync(path.join(ROOT, "webview-ui/agent-manager/shortcuts.ts"), "utf-8")
expect(source).toContain('t("agentManager.shortcuts.advancedWorktree"), binding: bindings.newWorktree')
expect(source).toContain('t("agentManager.shortcuts.newWorktree"), binding: bindings.quickWorktree')
})
it("does not attribute the new-worktree shortcut to session promotion", () => {
const source = fs.readFileSync(path.join(ROOT, "webview-ui/agent-manager/UnassignedSessionsSection.tsx"), "utf-8")
expect(source).toContain('<Tooltip value={t("agentManager.session.openInWorktree")}')
expect(source).not.toContain("TooltipKeybind")
})
})
// ---------------------------------------------------------------------------
// Provider message routing — static-analysis regression tests
//
@@ -5,6 +5,7 @@ const { AgentManagerProvider } = await import("../../src/agent-manager/AgentMana
type Manager = {
connectionService: { getClient: () => unknown }
panel: {
postMessage: (message: unknown) => void
sessions: {
getSessionDirectories: () => ReadonlyMap<string, string>
clearSessionDirectory: (id: string) => void
@@ -24,6 +25,7 @@ function createManager(options?: { dir?: string; panelDir?: string; state?: bool
const aborted: string[][] = []
const cleared: string[] = []
const removed: string[] = []
const messages: unknown[] = []
const events: string[] = []
const client = {
backgroundProcess: {
@@ -44,6 +46,7 @@ function createManager(options?: { dir?: string; panelDir?: string; state?: bool
const manager = Object.create(AgentManagerProvider.prototype) as Manager
manager.connectionService = { getClient: () => client }
manager.panel = {
postMessage: (message) => messages.push(message),
sessions: {
getSessionDirectories: () => new Map(options?.panelDir ? [["s1", options.panelDir]] : []),
clearSessionDirectory: (id) => cleared.push(id),
@@ -59,12 +62,12 @@ function createManager(options?: { dir?: string; panelDir?: string; state?: bool
manager.pushState = mock(() => undefined)
manager.log = mock(() => undefined)
return { manager, stopped, aborted, cleared, removed, events }
return { manager, stopped, aborted, cleared, removed, messages, events }
}
describe("AgentManagerProvider closeSession", () => {
it("aborts the agent before stopping processes and removing its tab", async () => {
const { manager, stopped, aborted, cleared, removed, events } = createManager({ dir: "/repo/worktree" })
const { manager, stopped, aborted, cleared, removed, messages, events } = createManager({ dir: "/repo/worktree" })
await manager.onCloseSession("s1")
@@ -73,6 +76,7 @@ describe("AgentManagerProvider closeSession", () => {
expect(events).toEqual(["abort", "processes", "remove"])
expect(removed).toEqual(["s1"])
expect(cleared).toEqual(["s1"])
expect(messages).toEqual([])
expect(manager.panelSessions.has("s1")).toBe(false)
})
@@ -42,7 +42,9 @@ describe("AgentManagerOrchestrationBridge", () => {
state?: (state: "connecting" | "connected" | "disconnected" | "error") => void
} = {}
const status = { failList: "", failReply: false }
const managed = new Set(["ses_target"])
const promptAsync = mock(async () => ({ data: undefined }))
const close = mock(async () => undefined)
const client = {
session: {
get: mock(async () => ({
@@ -95,6 +97,8 @@ describe("AgentManagerOrchestrationBridge", () => {
state: () => state,
stats: async () => ({ worktrees: [] }),
prs: () => new Map(),
managed: (id) => managed.has(id),
close,
log: () => undefined,
})
const request = (value: AgentManagerRequest, directory = root) =>
@@ -102,7 +106,7 @@ describe("AgentManagerOrchestrationBridge", () => {
{ id: `event-${value.id}`, type: "kilocode.agent_manager.requested", properties: value } as SSEPayload,
directory,
)
return { bridge, client, handlers, lists, promptAsync, rejections, replies, request, status }
return { bridge, client, close, handlers, lists, managed, promptAsync, rejections, replies, request, status }
}
const request: AgentManagerRequest = {
@@ -148,6 +152,85 @@ describe("AgentManagerOrchestrationBridge", () => {
test.bridge.dispose()
})
it("stops a managed session through the same close operation as the UI", async () => {
const test = harness()
test.status.failReply = true
const stop: AgentManagerRequest = {
id: "amr_stop",
sessionID: "ses_caller",
operation: "stop",
targetSessionID: "ses_target",
}
test.request(stop)
await waitFor(() => test.replies.length === 1)
test.status.failReply = false
test.request(stop)
await waitFor(() => test.replies.length === 2)
expect(test.close).toHaveBeenCalledTimes(1)
expect(test.close).toHaveBeenCalledWith("ses_target")
expect(test.replies).toEqual([
{
requestID: "amr_stop",
directory: root,
result: { operation: "stop", sessionID: "ses_target", stopped: true },
},
{
requestID: "amr_stop",
directory: root,
result: { operation: "stop", sessionID: "ses_target", stopped: true },
},
])
test.bridge.dispose()
})
it("stops a live panel session before it is persisted", async () => {
const test = harness()
test.managed.add("ses_live")
test.request({
id: "amr_stop_live",
sessionID: "ses_caller",
operation: "stop",
targetSessionID: "ses_live",
})
await waitFor(() => test.replies.length === 1)
expect(state.getSession("ses_live")).toBeUndefined()
expect(test.close).toHaveBeenCalledWith("ses_live")
expect(test.replies[0]).toEqual({
requestID: "amr_stop_live",
directory: root,
result: { operation: "stop", sessionID: "ses_live", stopped: true },
})
test.bridge.dispose()
})
it("rejects stopping a session not managed by the current workspace", async () => {
const test = harness()
test.request({
id: "amr_stop_unknown",
sessionID: "ses_caller",
operation: "stop",
targetSessionID: "ses_unknown",
})
await waitFor(() => test.rejections.length === 1)
expect(test.close).not.toHaveBeenCalled()
expect(test.rejections).toEqual([
{
requestID: "amr_stop_unknown",
directory: root,
error: {
code: "unknown_session",
message: "The session is not managed by this Agent Manager workspace",
},
},
])
test.bridge.dispose()
})
it("rejects request origins outside the current Agent Manager workspace", async () => {
const test = harness()
@@ -0,0 +1,11 @@
import { describe, expect, it } from "bun:test"
import { completesWithoutStatus } from "../../src/kilo-provider/command-completion"
describe("completesWithoutStatus", () => {
it("matches only deprecated static review aliases", () => {
expect(completesWithoutStatus("local-review")).toBe(true)
expect(completesWithoutStatus("local-review-uncommitted")).toBe(true)
expect(completesWithoutStatus("review")).toBe(false)
expect(completesWithoutStatus("init")).toBe(false)
})
})
@@ -21,6 +21,7 @@ const CHATVIEW_FILE = path.join(ROOT, "webview-ui/src/components/chat/ChatView.t
const PROMPT_UTILS_FILE = path.join(ROOT, "webview-ui/src/components/chat/prompt-input-utils.ts")
const PROMPT_FILE = path.join(ROOT, "webview-ui/src/components/chat/PromptInput.tsx")
const KILOPROVIDER_FILE = path.join(ROOT, "src/KiloProvider.ts")
const CLOUD_SESSION_FILE = path.join(ROOT, "src/kilo-provider/handlers/cloud-session.ts")
const CONNECTION_SERVICE_FILE = path.join(ROOT, "src/services/cli-backend/connection-service.ts")
function readFile(filePath: string): string {
@@ -79,6 +80,32 @@ describe("sendCommand dismisses pending tool requests", () => {
})
})
describe("static command completion contract", () => {
const source = readFile(SESSION_FILE)
it("finishes only the acknowledged command submission", () => {
const body = extractFunctionBody(source, "handleCommandCompletion")
expect(body).toMatch(/message\.type === "sessionCommandCompleted"\) finishSubmission\(message\.messageID\)/)
})
it("acknowledges aliases after direct and cloud command confirmation", () => {
const provider = readFile(KILOPROVIDER_FILE)
const cloud = readFile(CLOUD_SESSION_FILE)
expect(provider).toMatch(
/await runWithMessageConfirmation[\s\S]*?if \(messageID && completesWithoutStatus\(command\)\)[\s\S]*?sessionCommandCompleted/,
)
expect(cloud).toMatch(
/await run\(messageID, "Cloud import send"[\s\S]*?if \(messageID && command && completesWithoutStatus\(command\)\)[\s\S]*?sessionCommandCompleted/,
)
})
it("does not clear every session submission or active abort", () => {
const body = extractFunctionBody(source, "handleCommandCompletion")
expect(body).not.toContain("confirmSubmissions")
expect(body).not.toContain("aborts.clear")
})
})
describe("ChatView prompt-block contract", () => {
const source = readFile(CHATVIEW_FILE)
@@ -0,0 +1,45 @@
import { describe, expect, it } from "bun:test"
import { unlinkSync } from "node:fs"
import path from "node:path"
import { build } from "esbuild"
import { solidPlugin } from "esbuild-plugin-solid"
const ROOT = path.resolve(import.meta.dir, "../..")
const WEBVIEW = path.join(ROOT, "webview-ui")
const FIXTURE = path.join(ROOT, "tests/fixtures/question-dock-disposal.tsx")
describe("QuestionDock disposal", () => {
it("does not read a stale callback-form Show accessor", async () => {
const solid = path.dirname(Bun.resolveSync("solid-js/package.json", WEBVIEW))
const aliases: Record<string, string> = {
"solid-js": path.join(solid, "dist/solid.js"),
"solid-js/web": path.join(solid, "web/dist/web.js"),
"solid-js/store": path.join(solid, "store/dist/store.js"),
}
const dedupe = {
name: "solid-dedupe",
setup(ctx: Parameters<NonNullable<Parameters<typeof build>[0]["plugins"]>[number]["setup"]>[0]) {
ctx.onResolve({ filter: /^solid-js(\/web|\/store)?$/ }, (args) => ({ path: aliases[args.path] }))
},
}
const result = await build({
entryPoints: [FIXTURE],
bundle: true,
conditions: ["browser"],
external: ["happy-dom"],
format: "esm",
logLevel: "silent",
platform: "node",
plugins: [dedupe, solidPlugin()],
target: "es2022",
write: false,
})
const file = path.join(ROOT, `.question-dock-disposal-${crypto.randomUUID()}.mjs`)
await Bun.write(file, result.outputFiles[0]!.contents)
const child = Bun.spawnSync(["bun", file], { cwd: WEBVIEW, stdout: "pipe", stderr: "pipe" })
unlinkSync(file)
const output = child.stdout.toString() + child.stderr.toString()
expect(child.exitCode, output).toBe(0)
})
})
@@ -0,0 +1,24 @@
import { describe, expect, it } from "bun:test"
import type { Part, ToolPart } from "@kilocode/sdk/v2"
import { toolDefaultOpen } from "../../webview-ui/src/components/chat/tool-default-open"
function tool(name: string) {
return { type: "tool", tool: name } as ToolPart
}
describe("toolDefaultOpen", () => {
it.each(["bash", "background_process"])("uses the terminal preference for %s", (name) => {
expect(toolDefaultOpen(tool(name), false, true)).toBe(false)
expect(toolDefaultOpen(tool(name), true, false)).toBe(true)
})
it.each(["edit", "write", "apply_patch"])("uses the code edit preference for %s", (name) => {
expect(toolDefaultOpen(tool(name), true, false)).toBe(false)
expect(toolDefaultOpen(tool(name), false, true)).toBe(true)
})
it("leaves unrelated parts unchanged", () => {
expect(toolDefaultOpen(tool("read"), true, true)).toBeUndefined()
expect(toolDefaultOpen({ type: "text" } as Part, true, true)).toBeUndefined()
})
})
@@ -56,7 +56,6 @@ import { ThemeProvider } from "@kilocode/kilo-ui/theme"
import { DialogProvider, useDialog } from "@kilocode/kilo-ui/context/dialog"
import { Dialog } from "@kilocode/kilo-ui/dialog"
import { DropdownMenu } from "@kilocode/kilo-ui/dropdown-menu"
import { ContextMenu } from "@kilocode/kilo-ui/context-menu"
import { MarkedProvider } from "@kilocode/kilo-ui/context/marked"
import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code"
import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff"
@@ -92,7 +91,6 @@ import { NewWorktreeDialog } from "./NewWorktreeDialog"
import { DataBridge, MermaidDownloadBridge } from "../src/App"
import { LanguageBridge } from "../src/context/language-bridge"
import { useLanguage } from "../src/context/language"
import { formatRelativeDate } from "../src/utils/date"
import { createTabFocus } from "../src/utils/tab-navigation"
import {
canOpenRootSession,
@@ -137,6 +135,7 @@ import { createSidebarSearch, type SidebarSearchItem } from "./sidebar-search"
import { WorktreeSectionActions } from "./WorktreeSectionActions"
import { BranchSelect } from "../src/components/shared/BranchSelect"
import { WorktreeItem } from "./WorktreeItem"
import { UnassignedSessionsSection } from "./UnassignedSessionsSection"
import SectionHeader from "./SectionHeader"
import { randomColor } from "./section-colors"
import { createNewTaskDrafts } from "./new-task-drafts"
@@ -204,7 +203,7 @@ const defaultBindings: Record<string, string> = {
newTab: isMac ? "⌘T" : "Ctrl+T",
closeTab: isMac ? "⌘W" : "Ctrl+W",
newWorktree: isMac ? "⌘N" : "Ctrl+N",
advancedWorktree: isMac ? "⌘⇧N" : "Ctrl+Shift+N",
quickWorktree: isMac ? "⌘⇧N" : "Ctrl+Shift+N",
closeWorktree: isMac ? "⌘⇧W" : "Ctrl+Shift+W",
openWorktree: isMac ? "⌘⇧O" : "Ctrl+Shift+O",
openPR: isMac ? "⌘⇧R" : "Ctrl+Shift+R",
@@ -256,6 +255,11 @@ const AgentManagerContent: Component = () => {
setLocalSessionIDs((prev) => (prev.includes(sid) ? prev.filter((id) => id !== sid) : prev))
const [sidebarWidth, setSidebarWidth] = createSignal(persisted?.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH)
const [sessionsCollapsed, setSessionsCollapsed] = createSignal(true)
const toggleSessions = () => {
const collapsed = !sessionsCollapsed()
setSessionsCollapsed(collapsed)
vscode.postMessage({ type: "agentManager.setSessionsCollapsed", collapsed })
}
const sidebar = createSidebarCollapse(vscode)
const sidebarCollapsed = sidebar.collapsed
const expandSidebar = sidebar.expand
@@ -925,15 +929,17 @@ const AgentManagerContent: Component = () => {
const scrollIntoView = (el: HTMLElement) => el.scrollIntoView({ block: "nearest", behavior: "smooth" })
const selectUnassigned = (id: string) => {
saveTabMemory()
setSelection(null)
setReviewActive(false)
session.selectSession(id)
}
const focusSidebarItem = (item: { type: string; id: string }) => {
if (item.type === "local") selectLocal()
else if (item.type === "wt") selectWorktree(item.id)
else {
saveTabMemory()
setSelection(null)
setReviewActive(false)
session.selectSession(item.id)
}
else selectUnassigned(item.id)
const el = document.querySelector(`[data-sidebar-id="${item.id}"]`)
if (el instanceof HTMLElement) scrollIntoView(el)
}
@@ -1100,11 +1106,12 @@ const AgentManagerContent: Component = () => {
} else setSidePanel((prev) => (prev === "diff" ? null : "diff"))
} else if (msg.action === "newTab") handleNewTabForCurrentSelection()
else if (msg.action === "closeTab") closeActiveTab()
else if (msg.action === "newWorktree") handleNewWorktreeOrPromote()
else if (msg.action === "newWorktree") showNewWorktreeDialog()
else if (msg.action === "quickWorktree") handleCreateWorktree()
else if (msg.action === "openWorktree") openWorktreeDirectory()
else if (msg.action === "openPR") openSelectedPR()
else if (msg.action === "runScript") runSelected()
else if (msg.action === "advancedWorktree") showAdvancedWorktreeDialog()
else if (msg.action === "advancedWorktree") showNewWorktreeDialog()
else if (msg.action === "closeWorktree") closeSelectedWorktree()
else if (msg.action === "showShortcuts") handleShowKeyboardShortcuts()
else if (msg.action === "focusInput") window.dispatchEvent(new Event("focusPrompt"))
@@ -1215,8 +1222,8 @@ const AgentManagerContent: Component = () => {
// Mark sessions loaded as soon as the session context receives data (even if empty)
const unsubSessions = vscode.onMessage((msg) => {
if (msg.type === "sessionsLoaded" && !sessionsLoaded()) setSessionsLoaded(true)
if (msg.type === "agentManager.sessionClosed") handleCloseTab(msg.sessionId, false)
})
const unsubRun = vscode.onMessage((msg) => {
if (msg.type !== "agentManager.runStatus") return
const ev = msg as RunStatus
@@ -1844,8 +1851,7 @@ const AgentManagerContent: Component = () => {
}
const createWorktree = metrics.click("new_worktree", "worktrees", handleCreateWorktree)
// Advanced worktree dialog — opens a full dialog with prompt, versions, model, mode
const showAdvancedWorktreeDialog = () => {
const showNewWorktreeDialog = () => {
if (!loaded()) return
expandSidebar()
dialog.show(() => <NewWorktreeDialog onClose={() => dialog.close()} defaultBaseBranch={repoDefaultBranch()} />)
@@ -1934,8 +1940,7 @@ const AgentManagerContent: Component = () => {
confirmDeleteWorktree(worktreeId)
}
const handlePromote = (sessionId: string, e: MouseEvent) => {
e.stopPropagation()
const promoteSession = (sessionId: string) => {
if (!loaded()) return
metrics.track("promote_session", "unassigned_session")
vscode.postMessage({ type: "agentManager.promoteSession", sessionId })
@@ -1953,6 +1958,11 @@ const AgentManagerContent: Component = () => {
vscode.postMessage({ type: "agentManager.openLocally", sessionId: sid })
}
const openUnassigned = (id: string) => {
metrics.track("open_session_locally", "unassigned_session_menu")
openLocally(id)
}
const handleAddSession = () => {
const sel = selection()
expandSidebar()
@@ -1969,7 +1979,7 @@ const AgentManagerContent: Component = () => {
if (!sel || sel === LOCAL) return vscode.postMessage(msg)
vscode.postMessage({ ...msg, worktreeId: sel })
}
const handleCloseTab = (sessionId: string) => {
const handleCloseTab = (sessionId: string, notify = true) => {
freezeTabs()
const pending = isPending(sessionId)
const isActive = pending ? sessionId === activePendingId() : session.currentSessionID() === sessionId
@@ -1999,7 +2009,7 @@ const AgentManagerContent: Component = () => {
if (session.isSubmitting(sessionId) || isPendingSend(sessionId)) discardPendingDraft(sessionId)
queueMicrotask(() => deletePendingDraft(sessionId))
}
vscode.postMessage({ type: "agentManager.closeSession", sessionId })
if (notify) vscode.postMessage({ type: "agentManager.closeSession", sessionId })
tabFocus.restore()
}
@@ -2174,18 +2184,6 @@ const AgentManagerContent: Component = () => {
}
}
// Cmd+N: if an unassigned session is selected, promote it; otherwise create a new worktree
const handleNewWorktreeOrPromote = () => {
if (!loaded()) return
const sel = selection()
const sid = session.currentSessionID()
if (sel === null && sid && !worktreeSessionIds().has(sid)) {
vscode.postMessage({ type: "agentManager.promoteSession", sessionId: sid })
return
}
handleCreateWorktree()
}
// Close the currently selected worktree with a confirmation dialog
const closeSelectedWorktree = () => {
const sel = selection()
@@ -2312,7 +2310,7 @@ const AgentManagerContent: Component = () => {
onRef={(value) => (sidebarSearchMenu = value)}
onSelect={focusSidebarSearchItem}
onCreate={createWorktree}
onAdvanced={showAdvancedWorktreeDialog}
onNew={showNewWorktreeDialog}
onSection={newSection}
onShortcuts={metrics.click("keyboard_shortcuts", "worktrees_header", handleShowKeyboardShortcuts)}
onSetup={setupScript}
@@ -2549,7 +2547,7 @@ const AgentManagerContent: Component = () => {
)
})()}
<Show when={worktrees().length === 0}>
<button class="am-worktree-create" onClick={createWorktree}>
<button class="am-worktree-create" onClick={showNewWorktreeDialog}>
<Icon name="plus" size="small" />
<span>{t("agentManager.worktree.new")}</span>
</button>
@@ -2559,102 +2557,16 @@ const AgentManagerContent: Component = () => {
</div>
</div>
{/* SESSIONS section (unassigned) — collapsible */}
<div class={`am-section ${sessionsCollapsed() ? "" : "am-section-grow"}`}>
<button
class="am-section-header am-section-toggle"
onClick={() => {
const next = !sessionsCollapsed()
setSessionsCollapsed(next)
vscode.postMessage({ type: "agentManager.setSessionsCollapsed", collapsed: next })
}}
>
<span class="am-section-label">
<Icon
name={sessionsCollapsed() ? "chevron-right" : "chevron-down"}
size="small"
class="am-section-chevron"
/>
{t("agentManager.section.sessions")}
</span>
</button>
<Show when={!sessionsCollapsed()}>
<div class="am-list">
<Show
when={sessionsLoaded()}
fallback={
<div class="am-skeleton-list">
<div class="am-skeleton-session">
<div class="am-skeleton-session-title" style={{ width: "70%" }} />
<div class="am-skeleton-session-time" />
</div>
<div class="am-skeleton-session">
<div class="am-skeleton-session-title" style={{ width: "55%" }} />
<div class="am-skeleton-session-time" />
</div>
<div class="am-skeleton-session">
<div class="am-skeleton-session-title" style={{ width: "65%" }} />
<div class="am-skeleton-session-time" />
</div>
</div>
}
>
<For each={unassignedSessions()}>
{(s) => (
<ContextMenu>
<ContextMenu.Trigger as="div" style={{ display: "contents" }}>
<button
class={`am-item ${s.id === session.currentSessionID() && selection() === null ? "am-item-active" : ""}`}
data-sidebar-id={s.id}
onClick={() => {
saveTabMemory()
setSelection(null)
setReviewActive(false)
session.selectSession(s.id)
}}
>
<span class="am-item-title">{s.title || t("agentManager.session.untitled")}</span>
<span class="am-item-time">{formatRelativeDate(s.updatedAt)}</span>
<div class="am-item-promote">
<TooltipKeybind
title={t("agentManager.session.openInWorktree")}
keybind={kb().newWorktree ?? ""}
placement="right"
>
<IconButton
icon="branch"
size="small"
variant="ghost"
label={t("agentManager.session.openInWorktree")}
onClick={(e: MouseEvent) => handlePromote(s.id, e)}
/>
</TooltipKeybind>
</div>
</button>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content class="am-ctx-menu">
<ContextMenu.Item onSelect={() => handlePromote(s.id, new MouseEvent("click"))}>
<Icon name="branch" size="small" />
<ContextMenu.ItemLabel>{t("agentManager.session.openInWorktree")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
<ContextMenu.Item
onSelect={metrics.click("open_session_locally", "unassigned_session_menu", () =>
openLocally(s.id),
)}
>
<Icon name="folder" size="small" />
<ContextMenu.ItemLabel>{t("agentManager.session.openLocally")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
)}
</For>
</Show>
</div>
</Show>
</div>
<UnassignedSessionsSection
sessions={unassignedSessions}
loaded={sessionsLoaded}
collapsed={sessionsCollapsed}
active={() => (selection() === null ? session.currentSessionID() : undefined)}
onToggle={toggleSessions}
onSelect={selectUnassigned}
onPromote={promoteSession}
onOpen={openUnassigned}
/>
</div>
<div class="am-detail">
@@ -0,0 +1,104 @@
import { For, Show, type Accessor, type Component } from "solid-js"
import { ContextMenu } from "@kilocode/kilo-ui/context-menu"
import { Icon } from "@kilocode/kilo-ui/icon"
import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
import type { SessionInfo } from "../src/types/messages"
import { useLanguage } from "../src/context/language"
import { formatRelativeDate } from "../src/utils/date"
interface Props {
sessions: Accessor<SessionInfo[]>
loaded: Accessor<boolean>
collapsed: Accessor<boolean>
active: Accessor<string | undefined>
onToggle: () => void
onSelect: (id: string) => void
onPromote: (id: string) => void
onOpen: (id: string) => void
}
export const UnassignedSessionsSection: Component<Props> = (props) => {
const { t } = useLanguage()
const promote = (id: string, event: MouseEvent) => {
event.stopPropagation()
props.onPromote(id)
}
return (
<div class={`am-section ${props.collapsed() ? "" : "am-section-grow"}`}>
<button class="am-section-header am-section-toggle" onClick={props.onToggle}>
<span class="am-section-label">
<Icon name={props.collapsed() ? "chevron-right" : "chevron-down"} size="small" class="am-section-chevron" />
{t("agentManager.section.sessions")}
</span>
</button>
<Show when={!props.collapsed()}>
<div class="am-list">
<Show
when={props.loaded()}
fallback={
<div class="am-skeleton-list">
<div class="am-skeleton-session">
<div class="am-skeleton-session-title" style={{ width: "70%" }} />
<div class="am-skeleton-session-time" />
</div>
<div class="am-skeleton-session">
<div class="am-skeleton-session-title" style={{ width: "55%" }} />
<div class="am-skeleton-session-time" />
</div>
<div class="am-skeleton-session">
<div class="am-skeleton-session-title" style={{ width: "65%" }} />
<div class="am-skeleton-session-time" />
</div>
</div>
}
>
<For each={props.sessions()}>
{(session) => (
<ContextMenu>
<ContextMenu.Trigger as="div" style={{ display: "contents" }}>
<button
class={`am-item ${session.id === props.active() ? "am-item-active" : ""}`}
data-sidebar-id={session.id}
onClick={() => props.onSelect(session.id)}
>
<span class="am-item-title" dir="auto">
{session.title || t("agentManager.session.untitled")}
</span>
<span class="am-item-time">{formatRelativeDate(session.updatedAt)}</span>
<div class="am-item-promote">
<Tooltip value={t("agentManager.session.openInWorktree")} placement="right">
<IconButton
icon="branch"
size="small"
variant="ghost"
label={t("agentManager.session.openInWorktree")}
onClick={(event: MouseEvent) => promote(session.id, event)}
/>
</Tooltip>
</div>
</button>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content class="am-ctx-menu">
<ContextMenu.Item onSelect={() => props.onPromote(session.id)}>
<Icon name="branch" size="small" />
<ContextMenu.ItemLabel>{t("agentManager.session.openInWorktree")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
<ContextMenu.Item onSelect={() => props.onOpen(session.id)}>
<Icon name="folder" size="small" />
<ContextMenu.ItemLabel>{t("agentManager.session.openLocally")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
)}
</For>
</Show>
</div>
</Show>
</div>
)
}
@@ -22,7 +22,7 @@ interface WorktreeSectionActionsProps {
onRef: (ref: SidebarSearchMenuRef) => void
onSelect: (item: SidebarSearchItem) => void
onCreate: () => void
onAdvanced: () => void
onNew: () => void
onSection: () => void
onShortcuts: () => void
onSetup: () => void
@@ -48,14 +48,19 @@ export const WorktreeSectionActions: Component<WorktreeSectionActionsProps> = (p
/>
<Show when={props.git}>
<div class="am-split-button">
<IconButton
icon="plus"
size="small"
variant="ghost"
label={props.t("agentManager.worktree.new")}
onClick={props.onCreate}
disabled={!props.loaded}
/>
<TooltipKeybind
title={props.t("agentManager.shortcuts.advancedWorktree")}
keybind={props.bindings.newWorktree ?? ""}
>
<IconButton
icon="plus"
size="small"
variant="ghost"
label={props.t("agentManager.worktree.new")}
onClick={props.onNew}
disabled={!props.loaded}
/>
</TooltipKeybind>
<DropdownMenu gutter={4} placement="bottom-end">
<DropdownMenu.Trigger
class="am-split-arrow"
@@ -76,16 +81,7 @@ export const WorktreeSectionActions: Component<WorktreeSectionActionsProps> = (p
</span>
</DropdownMenu.ItemLabel>
<span class="am-menu-shortcut">
{parseBindingTokens(props.bindings.newWorktree ?? "").map((token) => (
<kbd class="am-menu-key">{token}</kbd>
))}
</span>
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={props.onAdvanced}>
<Icon name="settings-gear" size="small" />
<DropdownMenu.ItemLabel>{props.t("agentManager.dialog.configureWorktree")}</DropdownMenu.ItemLabel>
<span class="am-menu-shortcut">
{parseBindingTokens(props.bindings.advancedWorktree ?? "").map((token) => (
{parseBindingTokens(props.bindings.quickWorktree ?? "").map((token) => (
<kbd class="am-menu-key">{token}</kbd>
))}
</span>
+1 -1
View File
@@ -78,9 +78,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "العنصر السابق",
"agentManager.shortcuts.nextItem": "العنصر التالي",
"agentManager.shortcuts.newWorktree": "Worktree جديد",
"agentManager.shortcuts.advancedWorktree": "تكوين worktree جديد",
"agentManager.shortcuts.openWorktree": "فتح Worktree",
"agentManager.shortcuts.openPR": "فتح طلب السحب",
"agentManager.shortcuts.advancedWorktree": "تكوين worktree جديد",
"agentManager.shortcuts.deleteWorktree": "حذف Worktree",
"agentManager.shortcuts.previousTab": "علامة التبويب السابقة",
"agentManager.shortcuts.nextTab": "علامة التبويب التالية",
+1 -1
View File
@@ -80,9 +80,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "Item anterior",
"agentManager.shortcuts.nextItem": "Próximo item",
"agentManager.shortcuts.newWorktree": "Novo Worktree",
"agentManager.shortcuts.advancedWorktree": "Configurar novo worktree",
"agentManager.shortcuts.openWorktree": "Abrir Worktree",
"agentManager.shortcuts.openPR": "Abrir PR",
"agentManager.shortcuts.advancedWorktree": "Configurar novo worktree",
"agentManager.shortcuts.deleteWorktree": "Excluir Worktree",
"agentManager.shortcuts.previousTab": "Aba anterior",
"agentManager.shortcuts.nextTab": "Próxima aba",
+1 -1
View File
@@ -80,9 +80,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "Prethodni element",
"agentManager.shortcuts.nextItem": "Sljedeći element",
"agentManager.shortcuts.newWorktree": "Novi Worktree",
"agentManager.shortcuts.advancedWorktree": "Konfiguriši novi worktree",
"agentManager.shortcuts.openWorktree": "Otvori worktree",
"agentManager.shortcuts.openPR": "Otvori PR",
"agentManager.shortcuts.advancedWorktree": "Konfiguriši novi worktree",
"agentManager.shortcuts.deleteWorktree": "Obriši Worktree",
"agentManager.shortcuts.previousTab": "Prethodna kartica",
"agentManager.shortcuts.nextTab": "Sljedeća kartica",
+1 -1
View File
@@ -81,9 +81,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "Forrige element",
"agentManager.shortcuts.nextItem": "Næste element",
"agentManager.shortcuts.newWorktree": "Nyt Worktree",
"agentManager.shortcuts.advancedWorktree": "Konfigurer nyt worktree",
"agentManager.shortcuts.openWorktree": "Åbn Worktree",
"agentManager.shortcuts.openPR": "Åbn PR",
"agentManager.shortcuts.advancedWorktree": "Konfigurer nyt worktree",
"agentManager.shortcuts.deleteWorktree": "Slet Worktree",
"agentManager.shortcuts.previousTab": "Forrige fane",
"agentManager.shortcuts.nextTab": "Næste fane",
@@ -81,9 +81,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "Vorheriges Element",
"agentManager.shortcuts.nextItem": "Nächstes Element",
"agentManager.shortcuts.newWorktree": "Neuer Worktree",
"agentManager.shortcuts.advancedWorktree": "Neuen Worktree konfigurieren",
"agentManager.shortcuts.openWorktree": "Worktree öffnen",
"agentManager.shortcuts.openPR": "Pull Request öffnen",
"agentManager.shortcuts.advancedWorktree": "Neuen Worktree konfigurieren",
"agentManager.shortcuts.deleteWorktree": "Worktree löschen",
"agentManager.shortcuts.previousTab": "Vorheriger Tab",
"agentManager.shortcuts.nextTab": "Nächster Tab",
@@ -84,9 +84,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "Previous item",
"agentManager.shortcuts.nextItem": "Next item",
"agentManager.shortcuts.newWorktree": "New worktree",
"agentManager.shortcuts.advancedWorktree": "Configure new worktree",
"agentManager.shortcuts.openWorktree": "Open worktree",
"agentManager.shortcuts.openPR": "Open pull request",
"agentManager.shortcuts.advancedWorktree": "Configure new worktree",
"agentManager.shortcuts.deleteWorktree": "Delete worktree",
"agentManager.shortcuts.previousTab": "Previous tab",
"agentManager.shortcuts.nextTab": "Next tab",
+1 -1
View File
@@ -80,9 +80,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "Elemento anterior",
"agentManager.shortcuts.nextItem": "Siguiente elemento",
"agentManager.shortcuts.newWorktree": "Nuevo Worktree",
"agentManager.shortcuts.advancedWorktree": "Configurar nuevo worktree",
"agentManager.shortcuts.openWorktree": "Abrir Worktree",
"agentManager.shortcuts.openPR": "Abrir pull request",
"agentManager.shortcuts.advancedWorktree": "Configurar nuevo worktree",
"agentManager.shortcuts.deleteWorktree": "Eliminar Worktree",
"agentManager.shortcuts.previousTab": "Pestaña anterior",
"agentManager.shortcuts.nextTab": "Siguiente pestaña",
+1 -1
View File
@@ -80,9 +80,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "Élément précédent",
"agentManager.shortcuts.nextItem": "Élément suivant",
"agentManager.shortcuts.newWorktree": "Nouveau Worktree",
"agentManager.shortcuts.advancedWorktree": "Configurer un nouveau worktree",
"agentManager.shortcuts.openWorktree": "Ouvrir le worktree",
"agentManager.shortcuts.openPR": "Ouvrir la pull request",
"agentManager.shortcuts.advancedWorktree": "Configurer un nouveau worktree",
"agentManager.shortcuts.deleteWorktree": "Supprimer le Worktree",
"agentManager.shortcuts.previousTab": "Onglet précédent",
"agentManager.shortcuts.nextTab": "Onglet suivant",
+1 -1
View File
@@ -85,9 +85,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "Elemento precedente",
"agentManager.shortcuts.nextItem": "Elemento successivo",
"agentManager.shortcuts.newWorktree": "Nuovo worktree",
"agentManager.shortcuts.advancedWorktree": "Configura nuovo worktree",
"agentManager.shortcuts.openWorktree": "Apri worktree",
"agentManager.shortcuts.openPR": "Apri pull request",
"agentManager.shortcuts.advancedWorktree": "Configura nuovo worktree",
"agentManager.shortcuts.deleteWorktree": "Elimina worktree",
"agentManager.shortcuts.previousTab": "Scheda precedente",
"agentManager.shortcuts.nextTab": "Scheda successiva",
+1 -1
View File
@@ -80,9 +80,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "前の項目",
"agentManager.shortcuts.nextItem": "次の項目",
"agentManager.shortcuts.newWorktree": "新しいWorktree",
"agentManager.shortcuts.advancedWorktree": "新規 worktree の構成",
"agentManager.shortcuts.openWorktree": "Worktreeを開く",
"agentManager.shortcuts.openPR": "PRを開く",
"agentManager.shortcuts.advancedWorktree": "新規 worktree の構成",
"agentManager.shortcuts.deleteWorktree": "Worktreeを削除",
"agentManager.shortcuts.previousTab": "前のタブ",
"agentManager.shortcuts.nextTab": "次のタブ",
+1 -1
View File
@@ -79,9 +79,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "이전 항목",
"agentManager.shortcuts.nextItem": "다음 항목",
"agentManager.shortcuts.newWorktree": "새 Worktree",
"agentManager.shortcuts.advancedWorktree": "새 worktree 구성",
"agentManager.shortcuts.openWorktree": "Worktree 열기",
"agentManager.shortcuts.openPR": "PR 열기",
"agentManager.shortcuts.advancedWorktree": "새 worktree 구성",
"agentManager.shortcuts.deleteWorktree": "Worktree 삭제",
"agentManager.shortcuts.previousTab": "이전 탭",
"agentManager.shortcuts.nextTab": "다음 탭",
+1 -1
View File
@@ -84,9 +84,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "Vorig item",
"agentManager.shortcuts.nextItem": "Volgend item",
"agentManager.shortcuts.newWorktree": "Nieuwe worktree",
"agentManager.shortcuts.advancedWorktree": "Nieuwe worktree configureren",
"agentManager.shortcuts.openWorktree": "Worktree openen",
"agentManager.shortcuts.openPR": "Pull request openen",
"agentManager.shortcuts.advancedWorktree": "Nieuwe worktree configureren",
"agentManager.shortcuts.deleteWorktree": "Worktree verwijderen",
"agentManager.shortcuts.previousTab": "Vorig tabblad",
"agentManager.shortcuts.nextTab": "Volgend tabblad",
+1 -1
View File
@@ -79,9 +79,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "Forrige element",
"agentManager.shortcuts.nextItem": "Neste element",
"agentManager.shortcuts.newWorktree": "Nytt Worktree",
"agentManager.shortcuts.advancedWorktree": "Konfigurer nytt worktree",
"agentManager.shortcuts.openWorktree": "Åpne Worktree",
"agentManager.shortcuts.openPR": "Åpne pull request",
"agentManager.shortcuts.advancedWorktree": "Konfigurer nytt worktree",
"agentManager.shortcuts.deleteWorktree": "Slett Worktree",
"agentManager.shortcuts.previousTab": "Forrige fane",
"agentManager.shortcuts.nextTab": "Neste fane",
+1 -1
View File
@@ -80,9 +80,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "Poprzedni element",
"agentManager.shortcuts.nextItem": "Następny element",
"agentManager.shortcuts.newWorktree": "Nowy Worktree",
"agentManager.shortcuts.advancedWorktree": "Skonfiguruj nowe worktree",
"agentManager.shortcuts.openWorktree": "Otwórz Worktree",
"agentManager.shortcuts.openPR": "Otwórz pull request",
"agentManager.shortcuts.advancedWorktree": "Skonfiguruj nowe worktree",
"agentManager.shortcuts.deleteWorktree": "Usuń Worktree",
"agentManager.shortcuts.previousTab": "Poprzednia karta",
"agentManager.shortcuts.nextTab": "Następna karta",
+1 -1
View File
@@ -80,9 +80,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "Предыдущий элемент",
"agentManager.shortcuts.nextItem": "Следующий элемент",
"agentManager.shortcuts.newWorktree": "Новый Worktree",
"agentManager.shortcuts.advancedWorktree": "Настроить новое worktree",
"agentManager.shortcuts.openWorktree": "Открыть Worktree",
"agentManager.shortcuts.openPR": "Открыть PR",
"agentManager.shortcuts.advancedWorktree": "Настроить новое worktree",
"agentManager.shortcuts.deleteWorktree": "Удалить Worktree",
"agentManager.shortcuts.previousTab": "Предыдущая вкладка",
"agentManager.shortcuts.nextTab": "Следующая вкладка",
+1 -1
View File
@@ -76,9 +76,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "รายการก่อนหน้า",
"agentManager.shortcuts.nextItem": "รายการถัดไป",
"agentManager.shortcuts.newWorktree": "Worktree ใหม่",
"agentManager.shortcuts.advancedWorktree": "กำหนดค่า worktree ใหม่",
"agentManager.shortcuts.openWorktree": "เปิด Worktree",
"agentManager.shortcuts.openPR": "เปิด Pull Request",
"agentManager.shortcuts.advancedWorktree": "กำหนดค่า worktree ใหม่",
"agentManager.shortcuts.deleteWorktree": "ลบ Worktree",
"agentManager.shortcuts.previousTab": "แท็บก่อนหน้า",
"agentManager.shortcuts.nextTab": "แท็บถัดไป",
+1 -1
View File
@@ -85,9 +85,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "Önceki öğe",
"agentManager.shortcuts.nextItem": "Sonraki öğe",
"agentManager.shortcuts.newWorktree": "Yeni worktree",
"agentManager.shortcuts.advancedWorktree": "Yeni worktree yapılandır",
"agentManager.shortcuts.openWorktree": "Worktree aç",
"agentManager.shortcuts.openPR": "Pull request'i aç",
"agentManager.shortcuts.advancedWorktree": "Yeni worktree yapılandır",
"agentManager.shortcuts.deleteWorktree": "Worktree'yi sil",
"agentManager.shortcuts.previousTab": "Önceki sekme",
"agentManager.shortcuts.nextTab": "Sonraki sekme",
+1 -1
View File
@@ -86,9 +86,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "Попередній елемент",
"agentManager.shortcuts.nextItem": "Наступний елемент",
"agentManager.shortcuts.newWorktree": "Нове робоче дерево",
"agentManager.shortcuts.advancedWorktree": "Налаштувати нове worktree",
"agentManager.shortcuts.openWorktree": "Відкрити робоче дерево",
"agentManager.shortcuts.openPR": "Відкрити PR",
"agentManager.shortcuts.advancedWorktree": "Налаштувати нове worktree",
"agentManager.shortcuts.deleteWorktree": "Видалити робоче дерево",
"agentManager.shortcuts.previousTab": "Попередня вкладка",
"agentManager.shortcuts.nextTab": "Наступна вкладка",
+1 -1
View File
@@ -76,9 +76,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "上一项",
"agentManager.shortcuts.nextItem": "下一项",
"agentManager.shortcuts.newWorktree": "新建 Worktree",
"agentManager.shortcuts.advancedWorktree": "配置新 worktree",
"agentManager.shortcuts.openWorktree": "打开 Worktree",
"agentManager.shortcuts.openPR": "打开 PR",
"agentManager.shortcuts.advancedWorktree": "配置新 worktree",
"agentManager.shortcuts.deleteWorktree": "删除 Worktree",
"agentManager.shortcuts.previousTab": "上一个标签页",
"agentManager.shortcuts.nextTab": "下一个标签页",
+1 -1
View File
@@ -76,9 +76,9 @@ export const dict = {
"agentManager.shortcuts.previousItem": "上一個項目",
"agentManager.shortcuts.nextItem": "下一個項目",
"agentManager.shortcuts.newWorktree": "新建 Worktree",
"agentManager.shortcuts.advancedWorktree": "配置新 worktree",
"agentManager.shortcuts.openWorktree": "開啟 Worktree",
"agentManager.shortcuts.openPR": "開啟 PR",
"agentManager.shortcuts.advancedWorktree": "配置新 worktree",
"agentManager.shortcuts.deleteWorktree": "刪除 Worktree",
"agentManager.shortcuts.previousTab": "上一個分頁",
"agentManager.shortcuts.nextTab": "下一個分頁",
@@ -32,8 +32,8 @@ export function buildShortcutCategories(
shortcuts: [
{ label: t("agentManager.shortcuts.previousItem"), binding: bindings.previousSession ?? "" },
{ label: t("agentManager.shortcuts.nextItem"), binding: bindings.nextSession ?? "" },
{ label: t("agentManager.shortcuts.newWorktree"), binding: bindings.newWorktree ?? "" },
{ label: t("agentManager.shortcuts.advancedWorktree"), binding: bindings.advancedWorktree ?? "" },
{ label: t("agentManager.shortcuts.advancedWorktree"), binding: bindings.newWorktree ?? "" },
{ label: t("agentManager.shortcuts.newWorktree"), binding: bindings.quickWorktree ?? "" },
{ label: t("agentManager.shortcuts.deleteWorktree"), binding: bindings.closeWorktree ?? "" },
{ label: t("agentManager.shortcuts.openWorktree"), binding: bindings.openWorktree ?? "" },
{ label: t("agentManager.shortcuts.openPR"), binding: bindings.openPR ?? "" },
@@ -33,14 +33,7 @@ import type { Part as TimelinePart } from "../../types/messages"
import type { TimelineHighlight } from "../../utils/timeline/highlight"
import { QuestionDock } from "./QuestionDock"
import { SuggestBar } from "./SuggestBar"
const EDIT_TOOLS = new Set(["edit", "write", "apply_patch"])
function editOpen(part: SDKPart, open: boolean) {
if (part.type !== "tool") return undefined
const tool = (part as unknown as ToolPart).tool
return EDIT_TOOLS.has(tool) ? open : undefined
}
import { toolDefaultOpen } from "./tool-default-open"
/** Extract plan path from a completed plan_exit tool part. */
function planExitInfo(part: SDKPart): { plan: string } | undefined {
@@ -296,7 +289,7 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
part={part}
message={props.message as SDKMessage}
showAssistantCopyPartID={props.showAssistantCopyPartID}
defaultOpen={editOpen(part, edit())}
defaultOpen={toolDefaultOpen(part, open(), edit())}
forceOpen={forceOpen()}
forceOpenFile={forceOpen() ? props.forceOpenFile : undefined}
reasoningAutoCollapse={display.reasoningAutoCollapse()}
@@ -26,6 +26,7 @@ import { isEnterKeyCommitNotIme } from "../../utils/ime-enter"
export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => {
const session = useSession()
const language = useLanguage()
const id = props.request.id
const questions = createMemo(() => props.request.questions)
const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
@@ -57,8 +58,8 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
// Chat search indexes only the mounted page's options, and there's no
// other signal exposing which page that is — publish it here so search
// stays in sync as the user navigates instead of always assuming page 0.
createEffect(() => setActiveQuestionTab(props.request.id, store.tab))
onCleanup(() => clearActiveQuestionTab(props.request.id))
createEffect(() => setActiveQuestionTab(id, store.tab))
onCleanup(() => clearActiveQuestionTab(id))
const question = createMemo(() => questions()[store.tab])
const confirm = createMemo(() => !single() && store.tab === questions().length)
@@ -122,13 +122,6 @@ export const TaskHeader: Component<TaskHeaderProps> = (props) => {
: []),
]
})
const activitySummary = createMemo(() => {
if (memory.error()) return memory.error()!
if (!memory.enabled()) return language.t("chat.memory.project.disabled")
const lines = activityLines()
if (lines.length === 0) return language.t("chat.memory.activity.idle")
return [language.t("chat.memory.status.active"), ...lines].join(" · ")
})
const activityItems = createMemo(() =>
activity()
.flatMap((item) => {
@@ -157,6 +150,8 @@ export const TaskHeader: Component<TaskHeaderProps> = (props) => {
)
const activityTooltip = () => (
<>
<div data-slot="task-header-context-tooltip-title">{language.t("settings.context.title")}</div>
<div data-slot="task-header-context-tooltip-status">{memoryStatus()}</div>
{activitySummaryView()}
<Show when={memoryVerbose() && activityItems().length > 0}>
<div data-slot="task-header-memory-activity-list">
@@ -292,7 +287,9 @@ export const TaskHeader: Component<TaskHeaderProps> = (props) => {
startRename()
}}
>
<span data-slot="task-header-title-label">{title()}</span>
<span data-slot="task-header-title-label" dir="auto">
{title()}
</span>
</span>
</Show>
</div>
@@ -318,107 +315,117 @@ export const TaskHeader: Component<TaskHeaderProps> = (props) => {
<DeferredPopover
placement="bottom-end"
portal={false}
class="task-header-memory-popover"
class="task-header-context-popover"
triggerAs="button"
triggerProps={{
type: "button",
get class() {
return `task-header-memory-trigger${memory.enabled() ? " task-header-memory-trigger--enabled" : ""}`
},
class: "task-header-context-trigger",
get ["aria-label"]() {
return activitySummary()
return language.t("settings.context.title")
},
}}
trigger={
<>
<Icon name="memory" size="small" />
<Icon name="server" size="small" />
<Show when={memoryActive()}>
<span data-slot="task-header-memory-dot" />
</Show>
</>
}
>
<div data-slot="task-header-memory-menu">
<div data-slot="task-header-memory-status">{memoryStatus()}</div>
{activitySummaryView()}
<Show
when={memory.enabled()}
fallback={
<>
<div data-slot="task-header-context-menu">
<div data-slot="task-header-context-title">{language.t("settings.context.title")}</div>
<Show when={!props.readonly}>
<section data-slot="task-header-context-section">
<div data-slot="task-header-context-section-title">
<Icon name="compress" size="small" />
<span>{language.t("settings.context.compaction.title")}</span>
</div>
<div data-slot="task-header-context-actions">
<button
data-slot="task-header-memory-action"
disabled={memory.pending()}
onClick={() => memory.enable()}
data-slot="task-header-context-action"
disabled={!canCompact()}
onClick={() => session.compact()}
>
{language.t("chat.memory.enable")}
{language.t("command.session.compact")}
</button>
</>
}
>
<div data-slot="task-header-memory-actions">
<button
data-slot="task-header-memory-action"
disabled={memory.loading() || memory.pending()}
onClick={() => memory.showMemory()}
>
{language.t("chat.memory.inspect")}
</button>
<button
data-slot="task-header-memory-action"
disabled={memory.pending()}
onClick={() => memory.remember()}
>
{language.t("chat.memory.remember")}
</button>
<button
data-slot="task-header-memory-action"
disabled={memory.pending()}
onClick={() => memory.forget()}
>
{language.t("chat.memory.forget")}
</button>
<button
data-slot="task-header-memory-action"
disabled={memory.pending()}
onClick={() => memory.rebuild()}
>
{language.t("chat.memory.rebuild")}
</button>
<button
data-slot="task-header-memory-action"
disabled={memory.pending()}
onClick={() => memory.disable()}
>
{language.t("chat.memory.disable")}
</button>
</div>
<div data-slot="task-header-memory-verbose">
<span>{language.t("chat.memory.verbose")}</span>
<Switch
checked={memoryVerbose()}
disabled={memory.pending()}
hideLabel
onChange={(next) => memory.verbose(next ? "on" : "off")}
>
{language.t("chat.memory.verbose")}
</Switch>
</div>
</div>
</section>
</Show>
<section data-slot="task-header-context-section">
<div data-slot="task-header-context-section-title">
<Icon name="memory" size="small" />
<span>{language.t("settings.context.memory.title")}</span>
</div>
<div data-slot="task-header-memory-status">{memoryStatus()}</div>
{activitySummaryView()}
<div data-slot="task-header-context-actions">
<Show
when={memory.enabled()}
fallback={
<button
data-slot="task-header-context-action"
disabled={memory.pending()}
onClick={() => memory.enable()}
>
{language.t("chat.memory.enable")}
</button>
}
>
<button
data-slot="task-header-context-action"
disabled={memory.loading() || memory.pending()}
onClick={() => memory.showMemory()}
>
{language.t("chat.memory.inspect")}
</button>
<button
data-slot="task-header-context-action"
disabled={memory.pending()}
onClick={() => memory.remember()}
>
{language.t("chat.memory.remember")}
</button>
<button
data-slot="task-header-context-action"
disabled={memory.pending()}
onClick={() => memory.forget()}
>
{language.t("chat.memory.forget")}
</button>
<button
data-slot="task-header-context-action"
disabled={memory.pending()}
onClick={() => memory.rebuild()}
>
{language.t("chat.memory.rebuild")}
</button>
<button
data-slot="task-header-context-action"
disabled={memory.pending()}
onClick={() => memory.disable()}
>
{language.t("chat.memory.disable")}
</button>
</Show>
</div>
<Show when={memory.enabled()}>
<div data-slot="task-header-memory-verbose">
<span>{language.t("chat.memory.verbose")}</span>
<Switch
checked={memoryVerbose()}
disabled={memory.pending()}
hideLabel
onChange={(next) => memory.verbose(next ? "on" : "off")}
>
{language.t("chat.memory.verbose")}
</Switch>
</div>
</Show>
</section>
</div>
</DeferredPopover>
</Tooltip>
<Show when={!props.readonly}>
<Tooltip value={language.t("command.session.compact")} placement="bottom">
<IconButton
icon="compress"
size="small"
variant="ghost"
disabled={!canCompact()}
onClick={() => session.compact()}
aria-label={language.t("command.session.compact")}
/>
</Tooltip>
</Show>
<Show when={hasMessages()}>
<Tooltip value={language.t("chat.search.toggle")} placement="bottom">
<IconButton

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