Merge branch 'main' into fix/mermaid-copy-png-svg-clipboard

This commit is contained in:
fxnie
2026-08-10 09:56:33 +08:00
committed by GitHub
210 changed files with 7097 additions and 1026 deletions
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Align multi-project Agent Manager header actions with the worktree controls.
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Update the active model, mode, and thinking selectors when executing a slash command with configured overrides.
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Compress speech-to-text audio input to AAC format across macOS, Linux, and Windows to prevent payload size errors on long recordings.
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Start Agent Manager worktree sessions faster by prefetching base branches, reducing workspace file-watcher load, and overlapping independent multi-session setup.
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Fix the Agent Manager terminal toolbar button to toggle panel visibility directly.
@@ -0,0 +1,6 @@
---
"kilo-code": patch
"@kilocode/cli": patch
---
Route Agent Manager tool-launched sessions to the project that owns the tool event directory, keep sandboxed worktree sessions inside their active worktree, and wait for busy managed sessions before prompting them.
+1 -1
View File
@@ -2,4 +2,4 @@
"kilo-code": patch
---
Fix the sidebar navigation bar (New Task, History, Agent Manager, KiloClaw, Marketplace, Profile, Settings) disappearing when the Kilo Code view is docked in the Secondary Side Bar. The navigation is now rendered inside the webview itself, so it stays visible regardless of where the view is docked.
Fix the sidebar navigation bar (New Task, History, Agent Manager, KiloClaw, Marketplace, Profile, Settings) disappearing in Cursor when the Kilo Code view is docked in the Secondary Side Bar. Cursor now renders the navigation inside the webview itself so it stays visible regardless of dock location. VS Code is unaffected — it continues to use its native title bar toolbar, which already worked correctly everywhere.
+6
View File
@@ -0,0 +1,6 @@
---
"@kilocode/kilo-ui": patch
"kilo-code": patch
---
Fix flickering and sticky scrolling when scrolling up in Agent Manager and chat sessions.
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Move the "why was this tool call approved" line to after the tool output instead of between the header and body, add an icon to it, and add a Display setting to hide it.
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep Agent Manager terminals aligned and correctly wrapped when the terminal panel is narrow.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep recent sessions and the Show History action inset and usable in narrow VS Code sidebars.
@@ -0,0 +1,6 @@
---
"kilo-code": minor
"@kilocode/cli": minor
---
Add nested slash command suggestions for `/review` in VS Code and support `staged`, `unpushed`, and `quick` review modes.
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Remove unsupported `kilo web` CLI command.
+6
View File
@@ -0,0 +1,6 @@
---
"kilo-code": patch
"@kilocode/cli": patch
---
Apply saved sandbox settings to existing sessions and use the latest settings when enabling sandboxing
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Show the permission approval reason for reads and writes outside the workspace, matching other tools
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Surface process exit signals and structured spawn failure details in server startup diagnostics
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Long skill folder paths and URLs shown in the tooltip on the Skills settings page now wrap inside the viewport instead of overflowing on a single line.
@@ -41,6 +41,9 @@ jobs:
- name: Check Effect Promise facade allowlist
run: bun run script/check-opencode-promise-facades.ts
- name: Check domain architecture boundaries and ratchets
run: bun run script/check-architecture.ts
- name: Check model tool network boundary
run: bun run script/check-model-tool-network.ts
+12 -21
View File
@@ -470,6 +470,8 @@
},
"devDependencies": {
"@axe-core/playwright": "4.11.3",
"@babel/core": "^7.28.4",
"@babel/preset-typescript": "^7.27.1",
"@playwright/test": "1.57.0",
"@storybook/addon-a11y": "10.2.10",
"@storybook/addon-docs": "10.2.10",
@@ -481,6 +483,7 @@
"@vscode/test-cli": "^0.0.12",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.7.1",
"babel-preset-solid": "^1.9.9",
"esbuild": "^0.27.2",
"esbuild-plugin-solid": "^0.6.0",
"eslint": "^9.39.2",
@@ -984,23 +987,23 @@
},
},
"trustedDependencies": [
"esbuild",
"protobufjs",
"web-tree-sitter",
"esbuild",
"tree-sitter-bash",
"protobufjs",
],
"patchedDependencies": {
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch",
"@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch",
"pacote@21.5.1": "patches/pacote@21.5.1.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"pacote@21.5.1": "patches/pacote@21.5.1.patch",
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch",
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
},
"overrides": {
"@effect/platform-node-shared": "4.0.0-beta.74",
@@ -5443,10 +5446,6 @@
"@solid-primitives/resize-observer/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="],
"@standard-community/standard-json/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="],
"@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="],
"@storybook/addon-links/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="],
"@storybook/addon-onboarding/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="],
@@ -6153,14 +6152,6 @@
"@smithy/util-stream/@smithy/core/@smithy/types": ["@smithy/types@4.14.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ=="],
"@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-community/standard-json/effect/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-community/standard-openapi/effect/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"@storybook/addon-links/storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
"@storybook/addon-links/storybook/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-epitmtKUd9fAucKdH/sDdqv5WmwfpiPH+h/PNt55gd4=",
"aarch64-linux": "sha256-FHVsi2iho+U5aj6Z9lg2GTIu1ViUuqI2c/nZuwjmPH8=",
"aarch64-darwin": "sha256-QDPVLcbTyaZRZvSnqvWNR/mIkS1TrZLX0HjUlg6xwwI=",
"x86_64-darwin": "sha256-SSPc9b3WwcYCnuywcyMRJjVlugoXuo0UoPJ+qzPGajk="
"x86_64-linux": "sha256-y6PZR6BsVZcsK/qJ0XvJBevukYzVJ8NRLHRbNSuSzb8=",
"aarch64-linux": "sha256-0ozGLgGTHajlHqofixtjJv/dggq25MhWYIqmfmb+6Z0=",
"aarch64-darwin": "sha256-IEYJothLBDT20BmLamBqM+pciFMQtWy8Um1YjFjbKz4=",
"x86_64-darwin": "sha256-k2bQGKTkvIZmB2+I2Fy3TPm/NZ0BbGjxp9esxtGuUbI="
}
}
+1
View File
@@ -10,6 +10,7 @@
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
"dev:storybook": "bun --cwd packages/storybook storybook",
"lint": "oxlint",
"check:architecture": "bun run script/check-architecture.ts",
"typecheck": "bun turbo typecheck",
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
"postinstall": "bun run --cwd packages/core fix-node-pty && bun run script/setup-git.ts",
@@ -531,6 +531,7 @@ type Endpoint14_1Input = {
readonly cwd?: Endpoint14_1Request["payload"]["cwd"]
readonly title?: Endpoint14_1Request["payload"]["title"]
readonly env?: Endpoint14_1Request["payload"]["env"]
readonly size?: Endpoint14_1Request["payload"]["size"]
}
const Endpoint14_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_1Input) =>
raw["pty.create"]({
@@ -541,6 +542,7 @@ const Endpoint14_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_1Inpu
cwd: input?.["cwd"],
title: input?.["title"],
env: input?.["env"],
size: input?.["size"],
},
}).pipe(Effect.mapError(mapClientError))
+1
View File
@@ -840,6 +840,7 @@ export function make(options: ClientOptions) {
cwd: input?.["cwd"],
title: input?.["title"],
env: input?.["env"],
size: input?.["size"],
},
successStatus: 200,
declaredStatuses: [400, 401],
+13
View File
@@ -2632,6 +2632,7 @@ export type PtysCreateInput = {
readonly cwd?: string
readonly title?: string
readonly env?: { readonly [x: string]: string }
readonly size?: { readonly rows: number; readonly cols: number }
}["command"]
readonly args?: {
readonly command?: string
@@ -2639,6 +2640,7 @@ export type PtysCreateInput = {
readonly cwd?: string
readonly title?: string
readonly env?: { readonly [x: string]: string }
readonly size?: { readonly rows: number; readonly cols: number }
}["args"]
readonly cwd?: {
readonly command?: string
@@ -2646,6 +2648,7 @@ export type PtysCreateInput = {
readonly cwd?: string
readonly title?: string
readonly env?: { readonly [x: string]: string }
readonly size?: { readonly rows: number; readonly cols: number }
}["cwd"]
readonly title?: {
readonly command?: string
@@ -2653,6 +2656,7 @@ export type PtysCreateInput = {
readonly cwd?: string
readonly title?: string
readonly env?: { readonly [x: string]: string }
readonly size?: { readonly rows: number; readonly cols: number }
}["title"]
readonly env?: {
readonly command?: string
@@ -2660,7 +2664,16 @@ export type PtysCreateInput = {
readonly cwd?: string
readonly title?: string
readonly env?: { readonly [x: string]: string }
readonly size?: { readonly rows: number; readonly cols: number }
}["env"]
readonly size?: {
readonly command?: string
readonly args?: ReadonlyArray<string>
readonly cwd?: string
readonly title?: string
readonly env?: { readonly [x: string]: string }
readonly size?: { readonly rows: number; readonly cols: number }
}["size"]
}
export type PtysCreateOutput = {
+11 -1
View File
@@ -214,7 +214,17 @@ const layer = Layer.effect(
}
yield* Effect.logInfo("creating session", { id, cmd: command, args, cwd })
const { spawn } = yield* Effect.promise(() => pty())
const proc = yield* Effect.sync(() => spawn(command, args, { name: "xterm-256color", cwd, env }))
// kilocode_change start - spawn with initial terminal dimensions
const proc = yield* Effect.sync(() =>
spawn(command, args, {
name: "xterm-256color",
cwd,
env,
cols: input.size?.cols,
rows: input.size?.rows,
}),
)
// kilocode_change end
const info: Info = {
id,
title: input.title || `Terminal ${id.slice(-4)}`,
@@ -64,6 +64,10 @@ export const CodeWithAiNav: NavSection[] = [
href: "/code-with-ai/agents/chat-interface",
children: "Chat Interface",
},
{
href: "/code-with-ai/agents/session-history",
children: "Session History and Search",
},
{
href: "/code-with-ai/agents/context-mentions",
children: "Context & Mentions",
@@ -13,7 +13,6 @@
| `kilo upgrade [target]` | upgrade kilo to the latest or a specific version |
| `kilo uninstall` | uninstall kilo and remove all related files |
| `kilo serve` | starts a headless kilo server |
| `kilo web` | start kilo server and open web interface |
| `kilo models [provider]` | list all available models |
| `kilo roll-call <filter>` | batch-test text models matching a filter for connectivity and latency |
| `kilo profile` | show Kilo account profile |
@@ -63,7 +63,7 @@ Open your terminal and run `kilo` to launch the interactive terminal interface (
- **Input prompt** - Type your requests and press Enter to send
- **Action buttons** - Approve or reject proposed changes, answer questions
- **Agent dropdown** - Switch between agents (e.g. Code, Ask, Plan) from the sidebar
- **Session management** - Start new sessions or resume previous ones
- **Session management** - Start new sessions or [search and resume previous ones](/docs/code-with-ai/agents/session-history)
**Providing context:**
@@ -89,7 +89,7 @@ You can also rename local sessions from **History** using the edit button or the
- **Input prompt** - Type your requests and press Enter to send
- **Action buttons** - Approve or reject proposed changes, answer questions
- **Agent cycling** - Switch between agents using keybinds or slash commands
- **Session management** - Start new sessions or resume previous ones
- **Session management** - Start new sessions or [search and resume previous ones](/docs/code-with-ai/agents/session-history)
- **New task** - Start a new task, available using the `+` button at the top or `New Task` button above the chat input
- **Worktree** - Continue the current task with it's git state and session history in the Agent Manager in an isolated worktree
- **File changes** - Shows the number of lines changed and opens a diff view
@@ -25,15 +25,15 @@ Type `@` in the chat input to get autocomplete suggestions. You can mention:
| **File** | Attach a file's contents to your message | `@src/utils.ts` |
| **Terminal** | Include your active VS Code terminal output | `@terminal` |
| **Git Changes** | Attach uncommitted working-tree diffs and new files | `@git-changes` |
| **Past chats** | Attach a previous session's transcript as context | `@` → **Past chats** → pick a session |
| **Past chats** | Add a previous session's chat history as context | `@` → **Past chats** → pick a session |
Selecting a suggestion inserts the mention and highlights it in the input. File contents, terminal output, and git changes are attached as context when you send the message.
### Referencing Past Chats
Choosing **Past chats** from the `@` menu opens a searchable picker of your previous sessions in the current workspace, ordered by recency. Selecting a session inserts a highlighted mention token; when you send the message, that session's current transcript is attached as context so the agent can build on the earlier conversation. Clicking the mention token opens the referenced session.
Choosing **Past chats** from the `@` menu opens a searchable picker of sessions in the current workspace and related Git worktrees, including Agent Manager worktrees, ordered by recency. Search matches session titles and worktree names. Selecting a session inserts a highlighted mention token; when you send the message, that session's current chat history is added as context so the agent can build on the earlier conversation. Clicking the mention token opens the referenced session.
Very long transcripts are truncated, keeping the beginning and end, so a single mention cannot fill the context window.
Very long chat histories are truncated, keeping the beginning and end, so a single mention cannot fill the context window.
### Drag and Drop
@@ -77,7 +77,7 @@ This means the agent can explore your entire project as needed, rather than bein
| **Mention files when helpful** | If you know the exact file, mention its path to save the agent a search step |
| **Keep editor tabs relevant** | Open tabs are passed as context, so keep relevant files open |
| **Trust the agent's tools** | The agent can search, read, and explore your codebase — let it do the discovery work |
| **Reference a past chat** | Type `@` and choose **Past chats** to open a searchable picker of previous sessions in the current workspace. Selecting a session attaches its current transcript as context when you send the message. |
| **Reference a past chat** | Type `@` and choose **Past chats** to search the current workspace and related Git worktrees, including Agent Manager worktrees. Selecting a session adds its current chat history as context when you send the message. |
{% /tab %}
{% tab label="CLI" %}
@@ -93,6 +93,7 @@ When you describe a task, the agent uses its tools — `read`, `grep`, `glob`, a
In the terminal-based TUI, you can provide context in several ways:
- **Type `@` for file autocomplete** — In the TUI, type `@` followed by a filename to get autocomplete suggestions. Selecting a file attaches its contents to your message. You can limit how much is included by appending a line range, e.g. `@src/utils.ts#10-50`.
- **Reference a past chat** — Type `@`, choose **Past chats**, then search for and select a session. Kilo adds that chat history as context when you send the message without switching sessions.
- **Mention file paths in your message** — Simply refer to files by path in your conversation text (e.g., "look at src/utils.ts") and the agent will read them.
- **Use `kilo run -f`** — When using the non-interactive `kilo run` command, pass `-f path/to/file.ts` to explicitly include a file's contents in the context.
- **Let the agent find files itself** — The agent has access to `glob` (find files by pattern), `grep` (search file contents), and `read` (read file contents) tools. Describe what you're looking for and it will locate the relevant code.
@@ -116,6 +117,7 @@ This means the agent can explore your entire project as needed, rather than bein
|---|---|
| **Describe the task clearly** | The agent finds context on its own — focus on _what_ you want done rather than _where_ the code is |
| **Mention files when helpful** | If you know the exact file, mention its path to save the agent a search step |
| **Reference a past chat** | Type `@` and choose **Past chats** to add another session's chat history as context without switching sessions. |
| **Use `kilo run -f`** | Pass key files with `-f` when using `kilo run` for immediate context |
| **Trust the agent's tools** | The agent can search, read, and explore your codebase — let it do the discovery work |
@@ -0,0 +1,288 @@
---
title: "Session History and Search"
description: "Find, search, inspect, and resume local Kilo Code sessions"
---
# Session history and search
Kilo Code keeps local session metadata and chat history so you can resume earlier work. You can search session titles in the history UI or CLI, ask the agent to search chat content, or inspect the local SQLite database directly.
{% callout type="info" %}
Local sessions use SQLite, not PostgreSQL. Use `kilo db` or `sqlite3`, not `psql`, to inspect the local database.
{% /callout %}
## Choose a search method
Different search surfaces cover different content and scopes:
| Method | Searches | Scope |
|---|---|---|
| [VS Code History](#search-session-history-in-vs-code) | Session titles | Sessions loaded in the selected Local or Cloud history view |
| [JetBrains History](#search-session-history-in-jetbrains) | Session titles | Sessions loaded in the selected Local or Cloud history view |
| [CLI Past chats](#reference-a-past-chat-in-the-cli-tui) | Session titles | Current workspace |
| [`kilo session list --search`](#filter-session-titles) | Session titles | Current workspace, or every local workspace with `--all` |
| [Ask Kilo to recall past chats](#ask-kilo-to-search-past-chats) | Titles and high-signal chat content | Current workspace and related Git worktrees |
| [VS Code chat search](#search-the-open-chat-session) | Rendered content in the open session | Current chat session |
| [VS Code Past chats](#add-a-past-chat-to-the-current-session) | Session titles and worktree names | Current workspace and related Git worktrees, including Agent Manager worktrees |
| [SQLite query](#query-sqlite-directly) | Any stored field you select | Entire selected local database |
History and CLI list searches do **not** search message content. To find a string inside prompts or replies, ask Kilo to search past chats or use a direct SQLite query.
## Search in the UI
{% tabs %}
{% tab label="VS Code" %}
### Search session history in VS Code
1. Open the Kilo Code sidebar.
2. Select the **History** button in the sidebar title bar. You can also select **Show History** below the recent sessions on a new chat.
3. Select **Local** or **Cloud**.
4. Enter part of a session title in **Search sessions**.
5. Select a result to reopen it.
Local history actions also let you rename, export, or delete a session. Cloud history can be filtered to **Only this repository**.
The History search is a fuzzy title search over the sessions currently loaded into the view. It does not search prompts or agent replies.
### Search the open chat session
Open a session and select the magnifying-glass button in its header. Chat search supports:
- Previous and next match navigation
- Match case
- Whole word
- Regular expressions
Kilo loads older messages while the search is active so the search covers the full local chat session, not only the messages initially visible.
### Add a past chat to the current session
Type `@` in the chat input, select **Past chats**, and search by session title or worktree name. Selecting a result adds that past chat's content as context when you send the message from your current session. It does not switch or reopen sessions. Past chats includes sessions in the current workspace and related Git worktrees, including Agent Manager worktrees.
{% /tab %}
{% tab label="JetBrains" %}
### Search session history in JetBrains
1. Open the Kilo Code tool window.
2. Select **History** in the tool window toolbar.
3. Select **Local** or **Cloud**.
4. Enter part of a session title in **Search sessions**.
5. Select a result to reopen it.
You can rename or delete local sessions. In Cloud history, enable **Only this repository** to narrow the list.
The JetBrains History search matches session titles, not chat content.
{% /tab %}
{% tab label="CLI TUI" %}
### Reference a past chat in the CLI TUI
1. Run `kilo`.
2. Type `@` in the prompt and select **Past chats**.
3. Search for a session by title.
4. Select a session, finish your message, and send it.
Kilo adds the selected chat history as context for your message. This does not switch the TUI to the selected session. Use `/sessions`, `/resume`, or `/continue` when you want to load a different session instead.
{% /tab %}
{% /tabs %}
## Ask Kilo to search past chats
The simplest way to search message content across past local sessions is to ask Kilo directly:
```text
Search my local sessions for "database disk image is malformed" and summarize the matching conversations.
```
Kilo's local recall search covers session titles, user and assistant text, file references, and failed tool errors in the current workspace and related Git worktrees. Every query term must occur somewhere in a matching session; exact phrases and user-authored matches rank higher.
The search includes archived and child sessions. It excludes reasoning, synthetic or ignored text, successful tool output, file contents, and other metadata. Kilo can then read the full chat history for a selected result.
## Search with CLI commands
### Filter session titles
List sessions in the current workspace:
```bash
kilo session list --search "database migration"
```
Search titles across every local workspace and return JSON:
```bash
kilo session list --all --search "database migration" --format json
```
Limit the number of results:
```bash
kilo session list --all --search "migration" --max-count 25
```
`--search` performs a SQLite `LIKE` substring search on the title. It does not inspect messages. Use the returned session ID to resume or export a session:
```bash
kilo --session ses_123
kilo run --session ses_123 "Summarize where we stopped"
kilo export ses_123 > session.json
```
### Find the local database
Print the database selected by the current CLI environment:
```bash
kilo db path
```
Stable installations normally use these paths:
| Environment | Default path |
|---|---|
| Windows | `%USERPROFILE%\.local\share\kilo\kilo.db` |
| macOS | `~/.local/share/kilo/kilo.db` |
| Linux | `~/.local/share/kilo/kilo.db` |
| VS Code Remote SSH | `~/.local/share/kilo/kilo.db` on the remote machine |
The actual path can differ when `KILO_DB`, `XDG_DATA_HOME`, a development channel, or an isolated development environment is active. Prefer `kilo db path` over assuming a path.
## Query SQLite directly
{% callout type="warning" %}
The `kilo db` query command accepts arbitrary SQL, including writes. Use read-only queries, and use `sqlite3 -readonly` for safer interactive inspection. Stop Kilo before copying, replacing, or restoring the database because active transactions may also use `kilo.db-wal` and `kilo.db-shm`.
{% /callout %}
### Use the built-in database command
List recently updated sessions:
```bash
kilo db "
SELECT
id,
title,
directory,
datetime(time_updated / 1000, 'unixepoch') AS updated
FROM session
ORDER BY time_updated DESC
LIMIT 25
" --format json
```
Search non-synthetic text parts for a case-insensitive substring:
```bash
kilo db "
SELECT
s.id AS session_id,
s.title,
json_extract(m.data, '$.role') AS role,
json_extract(p.data, '$.text') AS text
FROM session AS s
JOIN message AS m ON m.session_id = s.id
JOIN part AS p ON p.message_id = m.id
WHERE json_valid(p.data)
AND json_extract(p.data, '$.type') = 'text'
AND coalesce(json_extract(p.data, '$.synthetic'), 0) = 0
AND coalesce(json_extract(p.data, '$.ignored'), 0) = 0
AND instr(
lower(json_extract(p.data, '$.text')),
lower('database disk image is malformed')
) > 0
ORDER BY s.time_updated DESC, m.time_created, p.id
" --format json
```
Replace the search string in the second `lower(...)` expression. This query searches ordinary user and assistant text. It intentionally does not reproduce Kilo's recall ranking, workspace scope, file-reference matching, or tool-error matching.
### Use the SQLite shell
Install the `sqlite3` command-line tool, then open the selected database in read-only mode:
```bash
sqlite3 -readonly "$(kilo db path)"
```
Useful commands inside the shell include:
```sql
.tables
.schema session
.schema message
.schema part
```
Run the SQL examples above without the surrounding `kilo db` command. Exit with `.quit`.
`psql` cannot open this file because `psql` speaks the PostgreSQL client protocol, while local Kilo sessions are stored in an embedded SQLite database.
## Search through the API or SDK
Run a local server with authentication before exposing its API:
```bash
export KILO_SERVER_PASSWORD='replace-with-a-strong-password'
kilo serve --port 4096
```
Search session titles in the current directory:
```bash
curl -u "kilo:$KILO_SERVER_PASSWORD" \
--get "http://127.0.0.1:4096/session" \
--data-urlencode "directory=$PWD" \
--data-urlencode "roots=true" \
--data-urlencode "search=migration" \
--data-urlencode "limit=50"
```
Read the complete chat history for one result:
```bash
curl -u "kilo:$KILO_SERVER_PASSWORD" \
--get "http://127.0.0.1:4096/session/ses_123/message" \
--data-urlencode "directory=$PWD"
```
The list endpoint's `search` parameter searches titles only. To search chat content programmatically, list candidate sessions, read their messages, and inspect text parts in your application.
The JavaScript SDK exposes the same endpoints:
```ts
import { createKiloClient } from "@kilocode/sdk/v2/client"
const auth = Buffer.from(`kilo:${process.env.KILO_SERVER_PASSWORD}`).toString("base64")
const client = createKiloClient({
baseUrl: "http://127.0.0.1:4096",
directory: process.cwd(),
headers: { Authorization: `Basic ${auth}` },
})
const result = await client.session.list(
{ roots: true, search: "migration", limit: 50 },
{ throwOnError: true },
)
for (const session of result.data) {
const history = await client.session.messages(
{ sessionID: session.id, limit: 0 },
{ throwOnError: true },
)
// Inspect history.data[*].parts for the content your application needs.
console.log(session.title, history.data.length)
}
```
`limit: 0` requests the complete chat history from the compatibility API. For large sessions, use a positive `limit` and follow the pagination cursor instead.
## Local and cloud sessions
Local sessions live in the SQLite database on the machine where Kilo runs. With VS Code Remote SSH, that means the remote host. Cloud sessions and shared-session copies are separate and are not available through local SQLite queries unless you import them locally.
For database corruption or reset procedures, see [Troubleshooting IDE Extensions](/docs/getting-started/troubleshooting/troubleshooting-extension). Resetting the database removes local session history, so back it up before recovery work.
@@ -659,21 +659,6 @@ Options:
--cors additional domains to allow for CORS [array] [default: []]
```
## kilo web
```
start kilo server and open web interface
Options:
--help Show help [boolean]
--version Show version number [boolean]
--port port to listen on [number] [default: 0]
--hostname hostname to listen on [string] [default: "127.0.0.1"]
--mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) [boolean] [default: false]
--mdns-domain custom domain name for mDNS service (default: kilo.local) [string] [default: "kilo.local"]
--cors additional domains to allow for CORS [array] [default: []]
```
## kilo models
```
@@ -110,7 +110,19 @@ Before using Kilo for Slack:
## Setup
To install Kilo for Slack, go to the Integrations menu in the sidebar at [app.kilo.ai](https://app.kilo.ai) and set up the Slack integration.
To install Kilo for Slack, go to the Integrations menu in the sidebar at [app.kilo.ai](https://app.kilo.ai) and set up the Slack integration:
{% image src="/docs/img/connect/slack/slackbot-integrations.webp" alt="Kilo Integrations page where you install the Slack integration" width="800" /%}
Then hit the Configure button, which will take you to this page:
{% image src="/docs/img/connect/slack/slackbot-integrations-2.webp" alt="Kilo Integrations page where you install the Slack integration, step 2" width="800" /%}
Once you press "Connect with Slack", the Slack OAuth authorization page appears:
{% image src="/docs/img/connect/slack/slackbot-slack-authorize.webp" alt="Slack OAuth authorization page" width="800" /%}
Once this is completed you're good to go!
To make proper use of all of the Kilo Slack bot's capabilities, it's important that you also connect your source control provider with Kilo:
| Platform | Integration Type | Details |
|---|---|---|
@@ -5,7 +5,7 @@ description: "Share and collaborate on Kilo Code sessions"
# Sessions & Sharing
A session is your platform-agnostic interaction with Kilo. It remembers your repository, your task, and the conversation so you can pause and resume work without losing context. Sessions are private to your account by default; you can optionally share a link with others who can read or fork your session.
A session is your platform-agnostic interaction with Kilo. It remembers your repository, your task, and the conversation so you can pause and resume work without losing context. Local sessions stay on the machine where Kilo runs. Account-backed cloud sessions are private to your account by default. You can optionally share a session link with others who can read or fork it.
## What a session keeps for you
@@ -49,7 +49,9 @@ Forking creates a new session in your account, with its own ID, and copies over
## Where your session data lives
To keep sessions fast and resumable, Kilo stores small JSON blobs associated with your session. These include your conversation history and task metadata. If you share a session, Kilo keeps a public copy used by the share link while your private session remains under your account.
Kilo stores local session history and metadata in an SQLite database on the machine where Kilo runs. Cloud sessions and shared-session copies use Kilo's cloud services and are separate from the local database. If you share a session, Kilo keeps a public copy for the share link while the original session remains private.
See [Session History and Search](/docs/code-with-ai/agents/session-history) to find the local database, search session titles or transcripts, and inspect sessions with the CLI or SQLite.
Good practice:
@@ -161,9 +161,9 @@ A configured destination is an egress route, not tenant, organization, repositor
The config setting supplies the initial default for new sessions that do not have a saved preference. Use the lock button in the VS Code prompt or `/sandbox` in the CLI to change the current session. Your latest choice is saved as the default for future sessions in that project, takes precedence over the config default, and persists across restarts.
Each initialized session snapshots its network mode, allowed destinations, and additional writable paths. Changing config affects new sessions. The prompt control or `/sandbox` can change the current session's enabled state, but it cannot change these authority lists, and they never expand during an active session.
Each session preserves its enabled or disabled choice. Saving changes through Kilo settings to network mode, allowed destinations, or additional writable paths refreshes existing session policies before their next tool execution. Enabling sandboxing also reads the latest settings. A tool that is already running keeps the policy it started with.
Forked sessions retain the source session's confinement. Subagents inherit the stricter combination of parent and child settings: sandboxing remains enabled if either requires it, deny-all wins over destination exceptions, destination lists intersect, and additional writable paths intersect.
At creation, forked sessions retain the source session's confinement and subagents inherit the stricter combination of parent and child settings: sandboxing remains enabled if either requires it, deny-all wins over destination exceptions, destination lists intersect, and additional writable paths intersect. Later trusted sandbox settings replace those network and writable-path limits before the affected session's next tool execution.
Cloud sessions do not expose the local sandbox control because their tools do not run in your local sandbox.
@@ -27,12 +27,14 @@ If every prompt fails with `SQLiteError: database disk image is malformed`, Kilo
### Find the database
The database location depends on where Kilo Code is running:
When the kilo CLI uses the same environment as the affected installation, run `kilo db path` to print the selected database. See [Session History and Search](/docs/code-with-ai/agents/session-history) for normal database inspection and search workflows.
The default database location depends on where Kilo Code is running:
| Environment | Database path |
|---|---|
| Windows | `%LOCALAPPDATA%\kilo\kilo.db` |
| macOS | `~/Library/Application Support/kilo/kilo.db` |
| Windows | `%USERPROFILE%\.local\share\kilo\kilo.db` |
| macOS | `~/.local/share/kilo/kilo.db` |
| Linux | `~/.local/share/kilo/kilo.db` |
| VS Code Remote SSH | `~/.local/share/kilo/kilo.db` on the remote machine |
+2786
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
allowBuilds:
core-js: true
Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1bc2a28b80d8dc5aea4c03609efc13a23a2c88103e56df741155d9e794fc96f7
size 2422
oid sha256:d49870048a42ea9c0166143fbb364b0d9ee45d786e3b1346fcd0eda76c533bdb
size 2414
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e3f86dabc39700b3a86508c1e6499a546eeabf720832b62c9ce12f2d97378ebd
size 34649
oid sha256:00092414080e2f70fb9b3166e8d06a1692af93e33717d0ab8fea5fe96e6a6982
size 34468
+2
View File
@@ -72,6 +72,8 @@
<!-- packages/kilo-vscode/src/agent-manager/WorktreeManager.ts -->
- <https://github.com>
<!-- packages/opencode/src/kilocode/security/github.ts -->
- <https://github.com/anthropics/claude-code/issues/31375>
<!-- packages/kilo-vscode/src/utils.ts -->
- <https://github.com/apps/kiloconnect>
<!-- packages/opencode/src/cli/cmd/github.handler.ts -->
- <https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-23-25.ts>
+10 -2
View File
@@ -507,17 +507,25 @@ html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-ty
}
}
/* "why was this allowed" line inside a tool's expanded body */
/* "why was this allowed" line inside a tool's expanded body. Styled like
[data-component="tool-hint"] (muted) so it reads as ambient context rather
than a call to action, and recedes the way reasoning text does. */
[data-slot="tool-approval-line"] {
display: flex;
flex-wrap: wrap;
align-items: baseline;
align-items: center;
gap: 4px;
padding: 4px 0 6px;
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
line-height: var(--line-height-normal);
color: var(--text-weak);
opacity: 0.9;
svg {
flex-shrink: 0;
color: var(--text-weak);
}
[data-slot="tool-approval-decision"] {
font-weight: var(--font-weight-medium);
+13 -7
View File
@@ -44,18 +44,24 @@ export function BasicTool(props: BasicToolProps) {
writeToolOpen(key(), open)
props.onOpenChange?.(open)
}
// Renders after the body/tool list, not before — it's context about what
// happened, not part of the header.
const details = () => (
<div data-slot="basic-tool-details">
<Show when={inBody() && approval()}>{(value) => <ToolApprovalLine display={value()} />}</Show>
{props.children}
<Show when={inBody() && approval()}>{(value) => <ToolApprovalLine display={value()} />}</Show>
</div>
)
if (!("children" in props) && !inBody()) {
return <Base {...props} defaultOpen={initial()} retainDetails={props.defer} onOpenChange={change} />
}
// A <Show>, not a plain `if`: inBody() tracks the visibility toggle, which can
// flip after mount (Settings), so the branch must stay reactive.
return (
<Base {...props} defaultOpen={initial()} retainDetails={props.defer} onOpenChange={change} hasDetails={inBody()}>
{details()}
</Base>
<Show
when={"children" in props || inBody()}
fallback={<Base {...props} defaultOpen={initial()} retainDetails={props.defer} onOpenChange={change} />}
>
<Base {...props} defaultOpen={initial()} retainDetails={props.defer} onOpenChange={change} hasDetails={inBody()}>
{details()}
</Base>
</Show>
)
}
@@ -48,8 +48,8 @@ import { checksum } from "@opencode-ai/core/util/encode"
import { Tooltip } from "./tooltip"
import { IconButton } from "./icon-button"
import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
import { ToolApprovalProvider, resolveToolApproval } from "./tool-approval"
export { ToolApprovalProvider, resolveToolApproval } from "./tool-approval"
import { ToolApprovalProvider, resolveToolApproval, useToolApproval } from "./tool-approval"
export { ToolApprovalProvider, resolveToolApproval, ToolApprovalVisibilityProvider } from "./tool-approval"
import { GrowBox } from "./grow-box"
import { COLLAPSIBLE_SPRING } from "./motion"
import { busy, createThrottledValue, useToolFade, useContextToolPending } from "./tool-utils"
@@ -1888,10 +1888,13 @@ ToolRegistry.register({
const pending = createMemo(() => busy(props.status))
const images = createMemo(() => (props.attachments ?? []).filter((f) => f.mime.startsWith("image/") && f.url))
const preview = (url: string, alt?: string) => dialog.show(() => <ImagePreview src={url} alt={alt} />)
// Read is high-frequency and low-risk, so details stay hidden unless the target was outside
// the workspace, in which case the approval reason explains what looks like an "agent escape".
const approval = useToolApproval()
return (
<>
<BasicTool
hideDetails
hideDetails={!approval()?.approval.outsideWorkspace}
{...props}
icon="glasses"
onSubtitleClick={
@@ -54,4 +54,28 @@ describe("resolveToolApproval", () => {
expect(out?.source).toBe("ui.approval.source.agent(agent=code)")
expect(out?.rule).toBeUndefined()
})
test("adds the outsideWorkspace text with just the filename when a path is known", () => {
const approval = {
source: "agent" as const,
agent: "code",
outsideWorkspace: true,
outsideWorkspacePath: "/etc/secrets/hello.txt",
}
const out = resolveToolApproval({ approval }, t)
expect(out?.outsideWorkspace).toBe("ui.approval.outsideWorkspace(file=hello.txt)")
})
test("omits the outsideWorkspace text for an ordinary in-workspace approval", () => {
const approval = { source: "agent" as const, agent: "code" }
const out = resolveToolApproval({ approval }, t)
expect(out?.outsideWorkspace).toBeUndefined()
})
test("omits the outsideWorkspace text when outsideWorkspace is set but no path is known", () => {
// e.g. a bash command scanning multiple external directories has no single filepath to show.
const approval = { source: "agent" as const, agent: "code", outsideWorkspace: true }
const out = resolveToolApproval({ approval }, t)
expect(out?.outsideWorkspace).toBeUndefined()
})
})
@@ -1,4 +1,6 @@
import { createContext, useContext, Show, type Accessor, type ParentProps } from "solid-js"
import { getFilename } from "@opencode-ai/core/util/path"
import { Icon } from "./icon"
/**
* Explains why a tool call was auto-approved, inside the expanded tool row.
@@ -11,6 +13,10 @@ export type ToolApproval = {
source: "agent" | "global" | "project" | "yolo" | "session" | "manual" | "default"
agent?: string
rule?: { permission: string; pattern: string; action: string }
/** True when the tool call's target path was outside the workspace/worktree. */
outsideWorkspace?: boolean
/** The target file path, when known, for display as a filename next to the note above. */
outsideWorkspacePath?: string
}
/** Pre-resolved, localized text plus the raw approval, supplied by the caller. */
@@ -19,6 +25,7 @@ export type ToolApprovalDisplay = {
decision: string
source?: string
rule?: string
outsideWorkspace?: string
}
const SOURCE_KEYS = ["agent", "global", "project", "yolo", "session", "manual", "default"] as const
@@ -30,8 +37,23 @@ export function ToolApprovalProvider(props: ParentProps<{ value: Accessor<ToolAp
return <Context.Provider value={props.value}>{props.children}</Context.Provider>
}
/**
* Whether the approval line should render at all. Hosts that expose a "hide
* auto-approval reason" display setting wrap their tree in
* `ToolApprovalVisibilityProvider`; without one, the line stays visible.
*/
const VisibilityContext = createContext<Accessor<boolean>>(() => true)
export function ToolApprovalVisibilityProvider(props: ParentProps<{ value: Accessor<boolean> }>) {
return <VisibilityContext.Provider value={props.value}>{props.children}</VisibilityContext.Provider>
}
/** Read the approval for the tool row below, gated by the visibility toggle
* here (not per call site) so a new `ToolApprovalProvider` usage can't forget it. */
export function useToolApproval() {
return useContext(Context)
const value = useContext(Context)
const visible = useContext(VisibilityContext)
return () => (visible() ? value() : undefined)
}
/** Read the raw approval payload off a tool part's metadata, if present. */
@@ -66,11 +88,16 @@ export function resolveToolApproval(
rule && !(rule.permission === "*" && rule.pattern === "*")
? t("ui.approval.rule", { permission: rule.permission, pattern: rule.pattern })
: undefined
// Only worth calling out when we know which file it was; a bare "outside your workspace" note
// without a filename (e.g. a bash command touching several directories) isn't actionable.
const filename = approval.outsideWorkspacePath ? getFilename(approval.outsideWorkspacePath) : undefined
return {
approval,
decision: approval.source === "manual" ? t("ui.approval.manual") : t("ui.approval.auto"),
source: sourceText(),
rule: ruleText,
outsideWorkspace:
approval.outsideWorkspace && filename ? t("ui.approval.outsideWorkspace", { file: filename }) : undefined,
}
}
@@ -79,11 +106,15 @@ export function ToolApprovalLine(props: { display: ToolApprovalDisplay }) {
const manual = () => props.display.approval.source === "manual"
return (
<div data-slot="tool-approval-line" data-source={props.display.approval.source}>
<Icon name="shield" size="small" />
<span data-slot="tool-approval-decision">{props.display.decision}</span>
<Show when={!manual()}>
<Show when={props.display.source}>{(text) => <span data-slot="tool-approval-source">{text()}</span>}</Show>
<Show when={props.display.rule}>{(text) => <span data-slot="tool-approval-rule">{text()}</span>}</Show>
</Show>
<Show when={props.display.outsideWorkspace}>
{(text) => <span data-slot="tool-approval-outside-workspace">{text()}</span>}
</Show>
</div>
)
}
@@ -216,4 +216,50 @@ describe("createAutoScroll non-scrollable layouts", () => {
expect(ctx.el.scrollTop).toBe(300)
ctx.dispose()
})
test("does not snap to bottom on content resize after user scrolls up while idle", () => {
const ctx = setup({ working: false })
ctx.el.scrollHeight = 1000
ctx.el.clientHeight = 200
ctx.el.scrollTop = 800 // at bottom
// User wheels up
const event = new FakeWheelEvent(-50, ctx.el)
ctx.el.fire("wheel", event as unknown as Event)
ctx.el.scrollTop = 750
ctx.scroll.handleScroll()
expect(ctx.scroll.userScrolled()).toBe(true)
// Virtual list re-measures / resizes content
ctx.el.scrollHeight = 1100
ctx.resize()
// Must NOT snap to bottom (1100), must remain at user scroll position (750)
expect(ctx.scroll.userScrolled()).toBe(true)
expect(ctx.el.scrollTop).toBe(750)
ctx.dispose()
})
test("does not snap to bottom when dragging scrollbar up while idle", () => {
const ctx = setup({ working: false })
ctx.el.scrollHeight = 1000
ctx.el.clientHeight = 200
ctx.el.scrollTop = 800 // at bottom
// User presses pointerdown on scrollbar and drags up
ctx.el.fire("pointerdown", new Event("pointerdown"))
ctx.el.scrollTop = 600
ctx.scroll.handleScroll()
expect(ctx.scroll.userScrolled()).toBe(true)
// Content resize during drag
ctx.el.scrollHeight = 1050
ctx.resize()
expect(ctx.scroll.userScrolled()).toBe(true)
expect(ctx.el.scrollTop).toBe(600)
ctx.dispose()
})
})
@@ -4,7 +4,6 @@ import { createResizeObserver } from "@solid-primitives/resize-observer"
import { canScroll, distanceFromBottom } from "./auto-scroll"
import { createUserActivity } from "./scroll-user-activity"
const DEBOUNCE_MS = 100
// Grace window after a real pointer/key/touch interaction during which a
// ResizeObserver or non-user scroll event must not snap the view back to the
// bottom. Upward wheel intent pauses immediately in its capture handler.
@@ -14,6 +13,7 @@ export interface AutoScrollOptions {
working: () => boolean
onUserInteracted?: () => void
bottomThreshold?: number
overflowAnchor?: "none" | "auto" | "dynamic"
}
export function createAutoScroll(options: AutoScrollOptions) {
@@ -24,7 +24,6 @@ export function createAutoScroll(options: AutoScrollOptions) {
let scroll: HTMLElement | undefined
let settling = false
let settleTimer: ReturnType<typeof setTimeout> | undefined
let stopTimer: ReturnType<typeof setTimeout> | undefined
let cleanup: (() => void) | undefined
const [store, setStore] = createStore({
@@ -100,7 +99,7 @@ export function createAutoScroll(options: AutoScrollOptions) {
const handleScroll = () => {
if (!scroll) return
const input = userActivity.consumeScroll()
userActivity.consumeScroll()
const distance = distanceFromBottom(scroll)
if (!canScroll(scroll)) return
@@ -110,52 +109,25 @@ export function createAutoScroll(options: AutoScrollOptions) {
return
}
if (!store.userScrolled && !input) {
// Only explicit user input can pause following. Treat unclassified
// scroll events from virtualization or layout changes as programmatic.
if (userActivity.isRecent()) {
stop()
} else {
bottom()
}
return
}
// Debounce to avoid layout-induced scroll shifts (e.g. images loading,
// virtual-list reflows) from incorrectly breaking auto-follow.
if (stopTimer) clearTimeout(stopTimer)
stopTimer = setTimeout(() => {
stopTimer = undefined
if (!scroll) return
if (distanceFromBottom(scroll) < threshold()) return
stop()
}, DEBOUNCE_MS)
stop()
}
const onContentResize = () => {
if (scroll && !canScroll(scroll)) return
if (!active()) {
if (!store.userScrolled && scroll && distanceFromBottom(scroll) > threshold()) {
bottom()
return
}
return
}
if (store.userScrolled) {
return
}
// Virtualized lists (virtua) re-measure items during user scroll, firing
// resize events that race ahead of handleScroll's DEBOUNCE_MS window.
// If the user just interacted with the scroller and is no longer near
// the bottom, treat the resize as a layout reflow on top of their
// scroll — pause auto-follow instead of snapping back to the bottom.
if (scroll && userActivity.isRecent() && distanceFromBottom(scroll) > threshold()) {
if (!scroll || !canScroll(scroll)) return
if (store.userScrolled) return
if (userActivity.isRecent() && distanceFromBottom(scroll) > threshold()) {
stop()
return
}
// ResizeObserver fires after layout, before paint.
// Keep the bottom locked in the same frame to avoid visible
// "jump up then catch up" artifacts while streaming content.
if (!active()) {
if (!userActivity.isRecent() && distanceFromBottom(scroll) > threshold()) {
bottom()
}
return
}
follow()
}
@@ -173,6 +145,15 @@ export function createAutoScroll(options: AutoScrollOptions) {
createResizeObserver(() => store.contentRef, onContentResize)
createResizeObserver(() => store.scrollRef, onViewportResize)
createEffect(
on(
() => store.userScrolled,
() => {
if (scroll) updateOverflowAnchor(scroll)
},
),
)
createEffect(
on(options.working, (working: boolean) => {
settling = false
@@ -195,6 +176,19 @@ export function createAutoScroll(options: AutoScrollOptions) {
// Lifecycle
// ---------------------------------------------------------------------------
const updateOverflowAnchor = (el: HTMLElement) => {
const mode = options.overflowAnchor ?? "none"
if (mode === "none") {
el.style.overflowAnchor = "none"
return
}
if (mode === "auto") {
el.style.overflowAnchor = "auto"
return
}
el.style.overflowAnchor = store.userScrolled ? "auto" : "none"
}
const setScroll = (el: HTMLElement | undefined) => {
if (cleanup) {
cleanup()
@@ -206,13 +200,12 @@ export function createAutoScroll(options: AutoScrollOptions) {
if (!el) return
el.style.overflowAnchor = "auto"
updateOverflowAnchor(el)
cleanup = userActivity.listen(el)
}
onCleanup(() => {
if (settleTimer) clearTimeout(settleTimer)
if (stopTimer) clearTimeout(stopTimer)
if (cleanup) cleanup()
})
@@ -18,13 +18,16 @@ export const createUserActivity = (options: UserActivityOptions) => {
// do not get mistaken for the user leaving auto-follow mode.
const mark = (event: Event) => {
if (!isPotentialScrollInput(event)) return
if (scroll && scroll.scrollHeight - scroll.clientHeight <= 1) return
marked = true
time = performance.now()
}
const handleWheel = (event: WheelEvent) => {
if (event.deltaY >= 0 || !scroll || scroll.scrollTop <= 0) return
time = performance.now()
if (!isPotentialScrollInput(event)) return
if (!scroll || scroll.scrollHeight - scroll.clientHeight <= 1) return
mark(event)
if (event.deltaY >= 0 || scroll.scrollTop <= 0) return
options.onWheelUp()
}
+1 -1
View File
@@ -3,4 +3,4 @@
* Only add icons here that are actually used — esbuild/Vite will
* tree-shake unused exports but explicit re-exports keep the API small.
*/
export { WandSparkles } from "lucide-solid"
export { default as WandSparkles } from "lucide-solid/icons/wand-sparkles"
+172 -114
View File
@@ -1,10 +1,96 @@
const esbuild = require("esbuild")
const path = require("path")
const { solidPlugin } = require("esbuild-plugin-solid")
const fs = require("fs")
const crypto = require("crypto")
const core = require("@babel/core")
const solid = require("babel-preset-solid")
const ts = require("@babel/preset-typescript")
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
/**
* Cache transformed Solid JSX files in memory and on disk to avoid
* re-parsing and re-transforming unchanged files across builds and webviews.
*/
const solidCacheDir = path.join(__dirname, "node_modules", ".cache", "esbuild-solid")
const solidMemCache = new Map()
const buildScriptHash = crypto
.createHash("sha256")
.update(fs.readFileSync(__filename, "utf8"))
.update(require("babel-preset-solid/package.json").version || "")
.update(require("@babel/preset-typescript/package.json").version || "")
.digest("hex")
.slice(0, 8)
if (!fs.existsSync(solidCacheDir)) {
try {
fs.mkdirSync(solidCacheDir, { recursive: true })
} catch (err) {
console.warn("[esbuild] could not create solid cache directory", err)
}
}
const cachedSolidPlugin = {
name: "esbuild:solid-cached",
setup(build) {
build.onLoad({ filter: /\.(t|j)sx$/ }, async (args) => {
let mtime = 0
let size = 0
try {
const st = fs.statSync(args.path)
mtime = st.mtimeMs
size = st.size
} catch (err) {
console.warn("[esbuild] could not stat source file for cache key", args.path, err)
}
const cacheKey = `${args.path}:${mtime}:${size}:${buildScriptHash}`
const memHit = solidMemCache.get(cacheKey)
if (memHit) return { contents: memHit, loader: "js" }
const diskKey = crypto.createHash("sha256").update(cacheKey).digest("hex") + ".js"
const diskPath = path.join(solidCacheDir, diskKey)
if (fs.existsSync(diskPath)) {
try {
const diskCode = fs.readFileSync(diskPath, "utf8")
solidMemCache.set(cacheKey, diskCode)
return { contents: diskCode, loader: "js" }
} catch (err) {
console.warn("[esbuild] cache read failed, rebuilding", diskPath, err)
}
}
const source = fs.readFileSync(args.path, "utf8")
const { name, ext } = path.parse(args.path)
const filename = name + ext
const result = await core.transformAsync(source, {
presets: [
[solid, {}],
[ts, {}],
],
filename,
sourceMaps: "inline",
})
if (result?.code === void 0 || result.code === null) {
throw new Error("No result was provided from Babel")
}
if (solidMemCache.size > 2000) solidMemCache.clear()
solidMemCache.set(cacheKey, result.code)
try {
fs.writeFileSync(diskPath, result.code)
} catch (err) {
console.warn("[esbuild] cache write failed", diskPath, err)
}
return { contents: result.code, loader: "js" }
})
},
}
/**
* Force all solid-js imports (from kilo-ui and the webview) to resolve to
* the **same** copy so SolidJS contexts are shared across packages.
@@ -123,7 +209,7 @@ const svgSpritePlugin = {
name: "svg-sprite-inline",
setup(build) {
build.onLoad({ filter: /sprite\.svg$/ }, (args) => {
const content = require("fs").readFileSync(args.path, "utf8")
const content = fs.readFileSync(args.path, "utf8")
return {
contents: `
const svg = ${JSON.stringify(content)};
@@ -160,69 +246,8 @@ const cssPackageResolvePlugin = {
},
}
function createBrowserWebviewContext(entryPoint, outfile) {
return esbuild.context({
entryPoints: [entryPoint],
bundle: true,
format: "iife",
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: "browser",
outfile,
logLevel: "silent",
loader: {
".woff": "file",
".woff2": "file",
".ttf": "file",
},
plugins: [
solidDedupePlugin,
pierreWorkerAliasPlugin,
markdownWorkerUrlPlugin,
svgSpritePlugin,
cssPackageResolvePlugin,
solidPlugin(),
esbuildProblemMatcherPlugin,
],
})
}
// Bundle Pierre's Shiki worker into a single self-contained asset that the
// webviews load off the main thread for syntax highlighting.
function createShikiWorkerContext() {
return esbuild.context({
entryPoints: ["kilo-shiki-worker"],
bundle: true,
format: "iife",
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: "browser",
outfile: "dist/shiki-worker.js",
logLevel: "silent",
plugins: [shikiWorkerEntryPlugin, esbuildProblemMatcherPlugin],
})
}
function createMarkdownShikiWorkerContext() {
return esbuild.context({
entryPoints: [path.join(__dirname, "..", "ui", "src", "components", "markdown-shiki.worker.ts")],
bundle: true,
format: "esm",
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: "browser",
outfile: "dist/markdown-shiki-worker.js",
logLevel: "silent",
plugins: [esbuildProblemMatcherPlugin],
})
}
async function main() {
// Build extension
const extensionCtx = await esbuild.context({
function getExtensionConfig() {
return {
entryPoints: ["src/extension.ts"],
bundle: true,
format: "cjs",
@@ -239,68 +264,101 @@ async function main() {
outfile: "dist/extension.js",
external: ["vscode"],
logLevel: "silent",
plugins: [esbuildProblemMatcherPlugin],
})
plugins: watch ? [esbuildProblemMatcherPlugin] : [],
}
}
// Build Agent Manager webview (SolidJS, shares components with sidebar)
const agentManagerCtx = await createBrowserWebviewContext(
"webview-ui/agent-manager/index.tsx",
"dist/agent-manager.js",
)
function getWebviewsConfig() {
return {
entryPoints: {
"agent-manager": "webview-ui/agent-manager/index.tsx",
kiloclaw: "webview-ui/kiloclaw/index.tsx",
marketplace: "webview-ui/marketplace/index.tsx",
"diff-viewer": "webview-ui/diff-viewer/index.tsx",
"diff-virtual": "webview-ui/diff-virtual/index.tsx",
webview: "webview-ui/src/index.tsx",
},
outdir: "dist",
bundle: true,
format: "iife",
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: "browser",
logLevel: "silent",
loader: {
".woff": "file",
".woff2": "file",
".ttf": "file",
},
plugins: [
solidDedupePlugin,
pierreWorkerAliasPlugin,
markdownWorkerUrlPlugin,
svgSpritePlugin,
cssPackageResolvePlugin,
cachedSolidPlugin,
...(watch ? [esbuildProblemMatcherPlugin] : []),
],
}
}
// Build KiloClaw webview (SolidJS, standalone chat panel)
const kiloClawCtx = await createBrowserWebviewContext("webview-ui/kiloclaw/index.tsx", "dist/kiloclaw.js")
function getShikiWorkerConfig() {
return {
entryPoints: ["kilo-shiki-worker"],
bundle: true,
format: "iife",
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: "browser",
outfile: "dist/shiki-worker.js",
logLevel: "silent",
plugins: [shikiWorkerEntryPlugin, ...(watch ? [esbuildProblemMatcherPlugin] : [])],
}
}
// Build Marketplace webview (SolidJS, standalone catalog panel)
const marketplaceCtx = await createBrowserWebviewContext("webview-ui/marketplace/index.tsx", "dist/marketplace.js")
function getMarkdownShikiWorkerConfig() {
return {
entryPoints: [path.join(__dirname, "..", "ui", "src", "components", "markdown-shiki.worker.ts")],
bundle: true,
format: "esm",
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: "browser",
outfile: "dist/markdown-shiki-worker.js",
logLevel: "silent",
plugins: watch ? [esbuildProblemMatcherPlugin] : [],
}
}
// Build Diff Viewer webview (SolidJS, reuses Agent Manager diff components)
const diffViewerCtx = await createBrowserWebviewContext("webview-ui/diff-viewer/index.tsx", "dist/diff-viewer.js")
// Build Diff Virtual webview (lightweight single-file diff for permission approval)
const diffVirtualCtx = await createBrowserWebviewContext("webview-ui/diff-virtual/index.tsx", "dist/diff-virtual.js")
// Build webview
const webviewCtx = await createBrowserWebviewContext("webview-ui/src/index.tsx", "dist/webview.js")
// Build the shared Shiki highlighting worker asset
const shikiWorkerCtx = await createShikiWorkerContext()
const markdownShikiWorkerCtx = await createMarkdownShikiWorkerContext()
async function main() {
const extensionConfig = getExtensionConfig()
const webviewsConfig = getWebviewsConfig()
const shikiWorkerConfig = getShikiWorkerConfig()
const markdownShikiWorkerConfig = getMarkdownShikiWorkerConfig()
if (watch) {
const [extensionCtx, webviewsCtx, shikiWorkerCtx, markdownShikiWorkerCtx] = await Promise.all([
esbuild.context(extensionConfig),
esbuild.context(webviewsConfig),
esbuild.context(shikiWorkerConfig),
esbuild.context(markdownShikiWorkerConfig),
])
await Promise.all([
extensionCtx.watch(),
webviewCtx.watch(),
agentManagerCtx.watch(),
diffViewerCtx.watch(),
diffVirtualCtx.watch(),
kiloClawCtx.watch(),
marketplaceCtx.watch(),
webviewsCtx.watch(),
shikiWorkerCtx.watch(),
markdownShikiWorkerCtx.watch(),
])
} else {
await Promise.all([
extensionCtx.rebuild(),
webviewCtx.rebuild(),
agentManagerCtx.rebuild(),
kiloClawCtx.rebuild(),
marketplaceCtx.rebuild(),
diffViewerCtx.rebuild(),
diffVirtualCtx.rebuild(),
shikiWorkerCtx.rebuild(),
markdownShikiWorkerCtx.rebuild(),
])
await Promise.all([
extensionCtx.dispose(),
webviewCtx.dispose(),
agentManagerCtx.dispose(),
diffViewerCtx.dispose(),
diffVirtualCtx.dispose(),
kiloClawCtx.dispose(),
marketplaceCtx.dispose(),
shikiWorkerCtx.dispose(),
markdownShikiWorkerCtx.dispose(),
esbuild.build(extensionConfig),
esbuild.build(webviewsConfig),
esbuild.build(shikiWorkerConfig),
esbuild.build(markdownShikiWorkerConfig),
])
}
}
+137 -4
View File
@@ -59,6 +59,12 @@
],
"main": "./dist/extension.js",
"contributes": {
"configurationDefaults": {
"files.watcherExclude": {
"**/.kilo/worktrees/**": true,
"**/.kilocode/worktrees/**": true
}
},
"taskDefinitions": [
{
"type": "kilo-worktree-setup",
@@ -141,6 +147,41 @@
"category": "Kilo Code",
"icon": "$(settings-gear)"
},
{
"command": "kilo-code.new.sidebarTitle.plusButtonClicked",
"title": "New Task",
"icon": "$(add)"
},
{
"command": "kilo-code.new.sidebarTitle.agentManagerOpen",
"title": "Agent Manager",
"icon": "$(organization)"
},
{
"command": "kilo-code.new.sidebarTitle.kiloClawOpen",
"title": "KiloClaw",
"icon": "$(comment-discussion)"
},
{
"command": "kilo-code.new.sidebarTitle.marketplaceButtonClicked",
"title": "Marketplace",
"icon": "$(extensions)"
},
{
"command": "kilo-code.new.sidebarTitle.historyButtonClicked",
"title": "History",
"icon": "$(history)"
},
{
"command": "kilo-code.new.sidebarTitle.profileButtonClicked",
"title": "Profile",
"icon": "$(account)"
},
{
"command": "kilo-code.new.sidebarTitle.settingsButtonClicked",
"title": "Settings",
"icon": "$(settings-gear)"
},
{
"command": "kilo-code.new.openInTab",
"title": "Open in Tab",
@@ -436,12 +477,76 @@
],
"menus": {
"commandPalette": [
{
"command": "kilo-code.new.sidebarTitle.plusButtonClicked",
"when": "false"
},
{
"command": "kilo-code.new.sidebarTitle.historyButtonClicked",
"when": "false"
},
{
"command": "kilo-code.new.sidebarTitle.agentManagerOpen",
"when": "false"
},
{
"command": "kilo-code.new.sidebarTitle.kiloClawOpen",
"when": "false"
},
{
"command": "kilo-code.new.agentManager.advancedWorktree",
"when": "false"
},
{
"command": "kilo-code.new.sidebarTitle.marketplaceButtonClicked",
"when": "false"
},
{
"command": "kilo-code.new.sidebarTitle.profileButtonClicked",
"when": "false"
},
{
"command": "kilo-code.new.sidebarTitle.settingsButtonClicked",
"when": "false"
}
],
"view/title": [
{
"command": "kilo-code.new.sidebarTitle.plusButtonClicked",
"group": "navigation@0",
"when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.sidebarTitle.historyButtonClicked",
"group": "navigation@1",
"when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.sidebarTitle.agentManagerOpen",
"group": "navigation@2",
"when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.sidebarTitle.kiloClawOpen",
"group": "navigation@3",
"when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.sidebarTitle.marketplaceButtonClicked",
"group": "navigation@4",
"when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.sidebarTitle.profileButtonClicked",
"group": "navigation@5",
"when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.sidebarTitle.settingsButtonClicked",
"group": "navigation@6",
"when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor"
}
],
"view/title": [],
"scm/title": [
{
"command": "kilo-code.new.generateCommitMessage",
@@ -461,6 +566,26 @@
"command": "kilo-code.new.openInTab",
"group": "navigation",
"when": "true"
},
{
"command": "kilo-code.new.plusButtonClicked",
"group": "navigation@0",
"when": "activeWebviewPanelId == kilo-code.new.TabPanel && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.historyButtonClicked",
"group": "navigation@1",
"when": "activeWebviewPanelId == kilo-code.new.TabPanel && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.profileButtonClicked",
"group": "navigation@2",
"when": "activeWebviewPanelId == kilo-code.new.TabPanel && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.settingsButtonClicked",
"group": "navigation@3",
"when": "activeWebviewPanelId == kilo-code.new.TabPanel && !kilo-code.new.isCursor"
}
],
"editor/context": [
@@ -1066,6 +1191,11 @@
"default": false,
"description": "Show tokens-per-second (prompt-processing / text-generation) badges on assistant messages and the task header"
},
"kilo-code.new.showAutoApprovalReason": {
"type": "boolean",
"default": true,
"description": "Show a line on tool calls explaining why they were auto-approved (matched rule, agent default, YOLO mode, etc.)"
},
"kilo-code.new.chat.shiftTabCyclesVariant": {
"type": "boolean",
"default": true,
@@ -1101,12 +1231,12 @@
"prepare:cli-binary": "bun script/local-bin.ts",
"prepare:sdk": "bun script/prepare-sdk.ts",
"build:launch": "bun run prepare:cli-binary && bun run prepare:sdk && bun run build:check:production",
"compile": "bun run prepare:cli-binary -- --force && bun run rebuild-sdk && bun run build:check",
"watch": "bun run rebuild-sdk && bun run --parallel watch:esbuild watch:tsc",
"compile": "bun run prepare:cli-binary && bun run prepare:sdk && bun run build:check",
"watch": "bun run prepare:sdk && bun run --parallel watch:esbuild watch:tsc",
"watch:esbuild": "bun run prepare:cli-binary && node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"watch:cli": "bun script/watch-cli.ts",
"package": "bun run prepare:cli-binary && bun run rebuild-sdk && bun run build:check:production",
"package": "bun run prepare:cli-binary && bun run prepare:sdk && bun run build:check:production",
"build:check": "bun run --parallel check-types check-types:webview lint bundle",
"build:check:production": "bun run --parallel check-types check-types:webview lint bundle:production",
"bundle": "bun esbuild.js",
@@ -1138,6 +1268,8 @@
},
"devDependencies": {
"@axe-core/playwright": "4.11.3",
"@babel/core": "^7.28.4",
"@babel/preset-typescript": "^7.27.1",
"@playwright/test": "1.57.0",
"@storybook/addon-a11y": "10.2.10",
"@storybook/addon-docs": "10.2.10",
@@ -1149,6 +1281,7 @@
"@vscode/test-cli": "^0.0.12",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.7.1",
"babel-preset-solid": "^1.9.9",
"esbuild": "^0.27.2",
"esbuild-plugin-solid": "^0.6.0",
"eslint": "^9.39.2",
+7 -5
View File
@@ -252,13 +252,15 @@ async function ensureBuiltBinary(): Promise<string> {
)
}
// Use the repository-pinned Bun version throughout. Newer canaries can fail compilation
// and must not cause packaged snapshots to fall back to the browser-mode source wrapper.
const pkg = await Bun.file(join(repoDir, "package.json")).json()
const bun = String(pkg.packageManager)
log("Installing dependencies in opencode package...")
await $`bunx ${bun} install --frozen-lockfile`.cwd(opencodeDir)
await $`bunx ${bun} run build --single --skip-install`.cwd(opencodeDir)
log("Building CLI binary...")
try {
await $`bunx ${bun} run build --single --skip-install`.cwd(opencodeDir)
} catch (err) {
log(`Pinned bunx build failed (${err}), running via active bun runtime...`)
await $`bun run script/build.ts --single --skip-install`.cwd(opencodeDir)
}
const built = await findKiloBinaryInOpencodeDist()
if (!built) {
+15 -2
View File
@@ -17,7 +17,7 @@ import type { EditorContext, IndexingStatus } from "./services/cli-backend/types
import { FileIgnoreController } from "./services/autocomplete/shims/FileIgnoreController"
import { ChatTextAreaAutocomplete } from "./services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete"
import { notebookUri } from "./services/autocomplete/continuedev/core/autocomplete/notebook"
import { buildWebviewHtml, getWebviewFontSize } from "./utils"
import { buildWebviewHtml, getWebviewFontSize, isCursorHost } from "./utils"
import { saveImage } from "./kilo-provider/save-image"
import { handleEditorAction } from "./kilo-provider/editor-actions"
import { exportTranscript } from "./kilo-provider/export-transcript"
@@ -186,6 +186,10 @@ import {
import { canonicalizePath, projectIdFor, samePath } from "./agent-manager/project/paths"
import { validChatSetting, watchChatConfig } from "./kilo-provider/chat-settings"
import { buildThroughputSettingMessage, watchThroughputConfig } from "./kilo-provider/throughput-settings"
import {
buildAutoApprovalReasonSettingMessage,
watchAutoApprovalReasonConfig,
} from "./kilo-provider/auto-approval-reason-settings"
let maxCost = 0
@@ -419,6 +423,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private indexingConfigDisposable: vscode.Disposable | null = null
private chatConfigDisposable: vscode.Disposable | null = null
private throughputConfigDisposable: vscode.Disposable | null = null
private autoApprovalReasonConfigDisposable: vscode.Disposable | null = null
private telemetryStateDisposable: vscode.Disposable | null = null
private viewStateDisposable: vscode.Disposable | null = null
private visibilityDisposable: vscode.Disposable | null = null
@@ -1002,6 +1007,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.chatConfigDisposable = watchChatConfig((msg) => this.postMessage(msg))
this.throughputConfigDisposable?.dispose()
this.throughputConfigDisposable = watchThroughputConfig((msg) => this.postMessage(msg))
this.autoApprovalReasonConfigDisposable?.dispose()
this.autoApprovalReasonConfigDisposable = watchAutoApprovalReasonConfig((msg) => this.postMessage(msg))
this.telemetryStateDisposable?.dispose()
this.telemetryStateDisposable = watchTelemetryState((msg) => this.postMessage(msg))
this.webviewMessageDisposable = webview.onDidReceiveMessage(async (message) => {
@@ -1832,6 +1839,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.sendNotificationSettings()
this.sendTimelineSetting()
this.postMessage(buildThroughputSettingMessage())
this.postMessage(buildAutoApprovalReasonSettingMessage())
this.postMessage({ type: "extensionDataReady" })
console.log("[Kilo New] KiloProvider: ✅ initializeConnection completed successfully")
@@ -4072,6 +4080,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.sendNotificationSettings()
this.sendTimelineSetting()
this.postMessage(buildThroughputSettingMessage())
this.postMessage(buildAutoApprovalReasonSettingMessage())
this.sendWorkStyle()
await ModelState.reset(this.client, (msg) => this.postMessage(msg))
@@ -4909,7 +4918,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
title: "Kilo Code",
port: this.connectionService.getServerInfo()?.port,
extraStyles: `.container { height: 100vh; }`,
topBar: this.opts.hideTopBar !== true,
// Dedicated single-purpose panels (Settings, Profile, Sub-Agent Viewer)
// never show the bar. Sidebar and "Open in Tab" only need it in Cursor —
// VS Code's native toolbar (restored in package.json) works everywhere.
topBar: this.opts.hideTopBar !== true && isCursorHost(),
topBarSurface: this.opts.topBarSurface === "tab" ? "tab_title" : "sidebar_title",
})
}
@@ -4998,6 +5010,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.indexingConfigDisposable?.dispose()
this.chatConfigDisposable?.dispose()
this.throughputConfigDisposable?.dispose()
this.autoApprovalReasonConfigDisposable?.dispose()
this.telemetryStateDisposable?.dispose()
this.autoApproveBridge?.dispose()
this.visibleTaskStreams.clear()
@@ -6,7 +6,7 @@ import { watchFontSizeConfig } from "./kilo-provider/font-size"
import { mapSSEEventToWebviewMessage } from "./kilo-provider-utils"
import { resolvePanelProjectDirectory } from "./project-directory"
import { seedSessionStatuses } from "./session-status"
import type { KiloConnectionService } from "./services/cli-backend"
import { type KiloConnectionService, ServerStartupError } from "./services/cli-backend"
import { MarketplaceService } from "./services/marketplace"
import {
fetchMarketplaceData,
@@ -123,7 +123,15 @@ export class MarketplacePanelProvider implements vscode.Disposable {
)
this.subscriptions.push(
this.connection.onStateChange((state, err) => {
this.post({ type: "connectionState", state, ...(err ? { error: err.message } : {}) })
this.post({
type: "connectionState",
state,
...(err ? { error: err.message } : {}),
...(err instanceof ServerStartupError && {
userMessage: err.userMessage,
userDetails: err.userDetails,
}),
})
if (state === "connected") void this.sync(false)
}),
this.connection.onLanguageChanged((locale) => this.post({ type: "languageChanged", locale })),
@@ -155,7 +163,15 @@ export class MarketplacePanelProvider implements vscode.Disposable {
await this.connection.connect(this.directory())
await this.sync(this.statuses.size === 0)
} catch (err) {
this.post({ type: "connectionState", state: "error", error: err instanceof Error ? err.message : String(err) })
this.post({
type: "connectionState",
state: "error",
error: err instanceof Error ? err.message : String(err),
...(err instanceof ServerStartupError && {
userMessage: err.userMessage,
userDetails: err.userDetails,
}),
})
}
}
@@ -53,8 +53,10 @@ import {
import { initContextState, pushProjectSessions, reactivateProject, registerProjectSessions } from "./project/init"
import { createLocalDiff } from "./local-diff"
import { parseToolRequest, startFromTool, type ToolRequest } from "./tool-start"
import { handleToolEvent } from "./tool-project"
import { sandboxSessionMetadata } from "../shared/sandbox-session"
import { AgentManagerOrchestrationBridge } from "./orchestration-bridge"
import { createOrchestrationBridge } from "./orchestration-setup"
import type { AgentManagerOrchestrationBridge } from "./orchestration-bridge"
import { pruneSubagents } from "./prune-subagents"
import { startSession } from "./mcp-warmup"
import { readTerminalFont, watchTerminalFont } from "./terminal-font"
@@ -253,22 +255,20 @@ export class AgentManagerProvider implements Disposable {
this.statsPoller = pollers.stats
this.prBridge = pollers.pr
this.projectPollers = pollers.projects
this.orchestration = new AgentManagerOrchestrationBridge(this.connectionService, {
root: () => this.getRoot(),
state: () => this.state,
ready: async () => {
this.stateReady ??= this.initializeState()
await this.stateReady
return this.state
},
stats: () => this.statsPoller.snapshot(),
prs: () => this.prBridge.snapshot(),
push: () => this.pushState(),
managed: (id) => this.panelSessions.has(id) || !!this.state?.getSession(id),
close: async (id) => {
await this.onCloseSession(id)
this.postToWebview({ type: "agentManager.sessionClosed", sessionId: id })
},
this.orchestration = createOrchestrationBridge({
connectionService: this.connectionService,
contexts: this.contexts,
projectScope: this.projectScope,
getRoot: () => this.getRoot(),
getState: () => this.state,
getStateReady: () => this.stateReady,
initStateReady: () => (this.stateReady = this.initializeState()),
getStats: () => this.statsPoller.snapshot(),
getPrs: () => this.prBridge.snapshot(),
pushState: (ctx) => this.pushState(ctx),
hasPanelSession: (id) => this.panelSessions.has(id),
closeSession: (id) => this.onCloseSession(id),
postSessionClosed: (id) => this.postToWebview({ type: "agentManager.sessionClosed", sessionId: id }),
log: (...args) => this.log(...args),
})
this.unsubTool = this.connectionService.onEventFiltered(
@@ -1083,14 +1083,16 @@ export class AgentManagerProvider implements Disposable {
}
private onToolEvent(event: unknown, directory?: string): void {
const properties = (event as { properties?: unknown }).properties
const req = parseToolRequest(properties)
if (!req) return
if (directory) {
req.directory = directory
req.projectId ??= this.contexts.byDirectory(directory)?.id
}
void this.startToolRequest(req)
handleToolEvent(
event,
directory,
{
byDirectory: (value) => this.contexts.byDirectory(value),
usable: (id) => this.contexts.usable(id),
},
this.projectScope,
(req) => this.startToolRequest(req),
)
}
private async startToolRequest(req: ToolRequest): Promise<void> {
@@ -1829,19 +1831,15 @@ export class AgentManagerProvider implements Disposable {
public async createFromSidebar(baseBranch?: string, branchName?: string): Promise<void> {
this.openPanel()
const panel = this.panel
if (!panel) return
if (!(await this.waitForPanelReady(panel))) return
if (!this.panel || !(await this.waitForPanelReady(this.panel))) return
await this.waitForStateReady("createFromSidebar")
await this.onCreateWorktree(baseBranch, branchName)
}
public async openAdvancedWorktree(): Promise<void> {
this.openPanel()
const panel = this.panel
if (!panel) return
if (!(await this.waitForPanelActive(panel))) return
if (!(await this.waitForPanelReady(panel))) return
if (!this.panel || !(await this.waitForPanelActive(this.panel)) || !(await this.waitForPanelReady(this.panel)))
return
await this.waitForStateReady("openAdvancedWorktree")
queueMicrotask(() => this.postToWebview({ type: "action", action: "advancedWorktree" }))
}
@@ -121,6 +121,8 @@ export class WorktreeManager {
// Key: `${root}:${remote}:${branch}`, Value: timestamp when fetch was done
private static fetchCache = new Map<string, number>()
private static readonly FETCH_CACHE_TTL = 60_000 // 1 minute
private static gitAvailable = false
private static lfsAvailable: boolean | undefined
private withGitLock<T>(fn: () => Promise<T>): Promise<T> {
const key = this.root
@@ -150,6 +152,13 @@ export class WorktreeManager {
return this.withGitLock(() => this.createWorktreeImpl(params))
}
/** Start the remote base refresh before creation reaches the git mutex. */
async prefetchBase(branch?: string): Promise<void> {
await this.ensureMigrated()
const base = branch || (await this.defaultBranch())
await this.withGitLock(() => this.refreshBase(base))
}
async renameBranch(worktreePath: string, current: string, requested: string): Promise<string> {
await this.ensureMigrated()
return this.withGitLock(() => this.renameBranchImpl(worktreePath, current, requested))
@@ -176,9 +185,12 @@ export class WorktreeManager {
}
private async ensureGitAvailable(): Promise<void> {
if (WorktreeManager.gitAvailable) return
try {
await execWithShellEnv("git", ["--version"])
WorktreeManager.gitAvailable = true
} catch (error) {
WorktreeManager.gitAvailable = false
if (error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") {
throw new Error(
"Git is not installed or not found in PATH. Please install Git (https://git-scm.com) and restart VS Code.",
@@ -762,20 +774,14 @@ export class WorktreeManager {
source: "remote",
}
}
WorktreeManager.fetchCache.delete(cacheKey)
}
// Either not cached or cache is stale - do the fetch.
// Use non-interactive env to prevent SSH passphrase popups.
onProgress?.("fetching", `Fetching ${remote}/${branch}...`)
try {
// Only opt into simple-git's allowUnsafeSshCommand when the SSH command
// is the fixed value Kilo injects — never for an inherited one, which
// could be attacker-controlled.
const env = nonInteractiveEnv()
await simpleGit(this.root, { unsafe: { allowUnsafeSshCommand: isKiloOwnedSshCommand(env) } })
.env(env)
.fetch(remote, branch, { "--quiet": null, "--no-tags": null })
WorktreeManager.fetchCache.set(cacheKey, Date.now())
await this.refreshBase(branch, remote)
if (await this.refExistsLocally(`${remote}/${branch}`)) {
return {
ref: `${remote}/${branch}`,
@@ -830,6 +836,23 @@ export class WorktreeManager {
throw new Error(`Could not resolve start point for branch "${branch}"`)
}
private async refreshBase(branch: string, requested?: string): Promise<void> {
const remote = requested ?? (await this.resolveRemote())
if (!remote) return
const key = `${this.root}:${remote}:${branch}`
const cached = WorktreeManager.fetchCache.get(key)
if (cached && Date.now() - cached < WorktreeManager.FETCH_CACHE_TTL) return
// Only opt into simple-git's allowUnsafeSshCommand when the SSH command
// is the fixed value Kilo injects — never for an inherited one, which
// could be attacker-controlled.
const env = nonInteractiveEnv()
await simpleGit(this.root, { unsafe: { allowUnsafeSshCommand: isKiloOwnedSshCommand(env) } })
.env(env)
.fetch(remote, branch, { "--quiet": null, "--no-tags": null })
WorktreeManager.fetchCache.set(key, Date.now())
}
/**
* Resolve the primary remote name for this repo.
* Uses `GitOps.resolveRemote` when available, otherwise checks for "origin".
@@ -894,10 +917,13 @@ export class WorktreeManager {
}
async checkLfsAvailable(): Promise<boolean> {
if (WorktreeManager.lfsAvailable) return true
try {
await execWithShellEnv("git", ["lfs", "version"], { cwd: this.root, timeout: 5000 })
WorktreeManager.lfsAvailable = true
return true
} catch {
WorktreeManager.lfsAvailable = false
// git-lfs not installed
return false
}
@@ -41,14 +41,15 @@ interface Failure {
}
interface Options {
root(): string | undefined
ready(): Promise<WorktreeStateManager | undefined>
state(): WorktreeStateManager | undefined
stats(): Promise<{ worktrees: WorktreeStats[]; local?: LocalStats }>
prs(): Map<string, PRStatus>
push(): void
managed(sessionID: string): boolean
close(sessionID: string): Promise<void>
root(directory?: string): string | undefined
ready(directory?: string): Promise<WorktreeStateManager | undefined>
state(directory?: string): WorktreeStateManager | undefined
stats(directory?: string): Promise<{ worktrees: WorktreeStats[]; local?: LocalStats }>
prs(directory?: string): Map<string, PRStatus>
push(directory?: string): void
managed(sessionID: string, directory?: string): boolean
close(sessionID: string, directory?: string): Promise<void>
directories?(): string[]
log(...args: unknown[]): void
}
@@ -108,6 +109,7 @@ export class AgentManagerOrchestrationBridge {
})
})
this.unsubscribeDirectories = connection.registerDirectoryProvider(() => {
if (this.options.directories) return this.options.directories()
const root = this.options.root()
const dirs =
this.options
@@ -181,8 +183,8 @@ export class AgentManagerOrchestrationBridge {
}
private async admit(request: Request, directory: string): Promise<void> {
const state = await this.options.ready()
const root = this.options.root()
const state = await this.options.ready(directory)
const root = this.options.root(directory)
if (this.disposed || this.settled.has(request.id)) return
if (!state || !root) {
const accepted = await this.reject(request.id, directory, {
@@ -232,7 +234,7 @@ export class AgentManagerOrchestrationBridge {
private async run(request: Request, origin: Origin, active: Active): Promise<void> {
try {
const outcome = this.outcomes.get(request.id) ?? (await this.execute(request, active))
const outcome = this.outcomes.get(request.id) ?? (await this.execute(request, origin, active))
if (!outcome || this.disposed || active.cancelled) return
this.rememberOutcome(request.id, outcome)
const accepted =
@@ -248,10 +250,10 @@ export class AgentManagerOrchestrationBridge {
}
}
private async execute(request: Request, active: Active): Promise<Outcome | undefined> {
private async execute(request: Request, origin: Origin, active: Active): Promise<Outcome | undefined> {
try {
const state = await this.options.ready()
const root = this.options.root()
const state = await this.options.ready(origin.directory)
const root = this.options.root(origin.directory)
if (!state || !root)
throw new OrchestrationError("workspace_unavailable", "Agent Manager requires an open workspace")
if (this.disposed || active.cancelled) return
@@ -260,7 +262,7 @@ export class AgentManagerOrchestrationBridge {
// Git stats are refreshed by the poller independently. A forced refresh
// here can spawn one diff/ahead-behind pair per worktree and exceed the
// host request timeout before the overview can return its IDs.
const stats = await this.options.stats()
const stats = await this.options.stats(origin.directory)
if (this.disposed || active.cancelled) return
const result = await overview({
client,
@@ -269,7 +271,7 @@ export class AgentManagerOrchestrationBridge {
titles: this.titles,
filter: request.filter,
stats,
prs: this.options.prs(),
prs: this.options.prs(origin.directory),
})
return { result: { operation: "overview", overview: result } }
}
@@ -288,7 +290,7 @@ export class AgentManagerOrchestrationBridge {
}
if (request.operation === "move") {
move({ state, sessionID: request.targetSessionID, sectionID: request.sectionID })
this.options.push()
this.options.push(origin.directory)
if (this.disposed || active.cancelled) return
return {
result: {
@@ -299,10 +301,10 @@ export class AgentManagerOrchestrationBridge {
},
}
}
if (!this.options.managed(request.targetSessionID)) {
if (!this.options.managed(request.targetSessionID, origin.directory)) {
throw new OrchestrationError("unknown_session", "The session is not managed by this Agent Manager workspace")
}
await this.options.close(request.targetSessionID)
await this.options.close(request.targetSessionID, origin.directory)
if (this.disposed || active.cancelled) return
return { result: { operation: "stop", sessionID: request.targetSessionID, stopped: true } }
} catch (error) {
@@ -319,6 +319,7 @@ export async function prompt(input: {
text: string
messageID: string
signal?: AbortSignal
idleTimeoutMs?: number
}): Promise<void> {
if (input.signal?.aborted) return
const managed = input.state.getSession(input.sessionID)
@@ -342,15 +343,7 @@ export async function prompt(input: {
if (!(await sameManagedDirectory(response.data.directory, dir))) {
throw new OrchestrationError("cross_workspace", "The managed session belongs to a different workspace directory")
}
const status = await input.client.session.status({ directory: dir })
if (status.error) throw new OrchestrationError("host_error", "The managed session status could not be read")
const activity = status.data?.[input.sessionID]?.type ?? "idle"
if (activity !== "idle") {
throw new OrchestrationError(
"unavailable_session",
`The managed session is ${activity}; only idle sessions can be prompted`,
)
}
await waitForIdle(input.client, dir, input.sessionID, input.signal, input.idleTimeoutMs ?? 30_000)
if (input.signal?.aborted) return
await input.client.session.promptAsync(
{
@@ -364,6 +357,29 @@ export async function prompt(input: {
)
}
async function waitForIdle(
client: KiloClient,
directory: string,
sessionID: string,
signal: AbortSignal | undefined,
timeout: number,
start = Date.now(),
): Promise<void> {
if (signal?.aborted) return
const status = await client.session.status({ directory })
if (status.error) throw new OrchestrationError("host_error", "The managed session status could not be read")
const activity = status.data?.[sessionID]?.type ?? "idle"
if (activity === "idle") return
if (Date.now() - start >= timeout) {
throw new OrchestrationError(
"unavailable_session",
`The managed session is still ${activity}; only idle sessions can be prompted`,
)
}
await new Promise<void>((resolve) => setTimeout(resolve, 250))
return waitForIdle(client, directory, sessionID, signal, timeout, start)
}
export function move(input: { state: WorktreeStateManager; sessionID: string; sectionID: string | null }): void {
const session = input.state.getSession(input.sessionID)
if (!session)
@@ -0,0 +1,78 @@
import type { KiloConnectionService } from "../services/cli-backend/connection-service"
import { AgentManagerOrchestrationBridge } from "./orchestration-bridge"
import type { ProjectContexts } from "./project/contexts"
import type { ProjectContext } from "./project/context"
import type { ProjectScope } from "./project/scope"
import type { WorktreeStateManager } from "./WorktreeStateManager"
import type { WorktreeStats, LocalStats } from "./GitStatsPoller"
import type { PRStatus } from "./types"
import { initContextState } from "./project/init"
export interface OrchestrationBridgeDeps {
connectionService: KiloConnectionService
contexts: ProjectContexts
projectScope: ProjectScope
getRoot: () => string | undefined
getState: () => WorktreeStateManager | undefined
getStateReady: () => Promise<void> | undefined
initStateReady: () => Promise<void>
getStats: () => Promise<{ worktrees: WorktreeStats[]; local?: LocalStats }>
getPrs: () => Map<string, PRStatus>
pushState: (ctx?: ProjectContext) => void
hasPanelSession: (id: string) => boolean
closeSession: (id: string) => Promise<unknown>
postSessionClosed: (id: string) => void
log: (...args: unknown[]) => void
}
export function createOrchestrationBridge(deps: OrchestrationBridgeDeps): AgentManagerOrchestrationBridge {
return new AgentManagerOrchestrationBridge(deps.connectionService, {
root: (dir) => (dir ? deps.contexts.byDirectory(dir)?.root : undefined) ?? deps.getRoot(),
state: (dir) => (dir ? deps.contexts.byDirectory(dir)?.peekState() : undefined) ?? deps.getState(),
ready: async (dir) => {
const ctx = dir ? deps.contexts.byDirectory(dir) : undefined
if (ctx && ctx.id !== deps.contexts.active()?.id) {
await initContextState(ctx, (...args) => deps.log(...args))
return ctx.stateManager()
}
const ready = deps.getStateReady() ?? deps.initStateReady()
await ready
return deps.getState()
},
stats: () => deps.getStats(),
prs: () => deps.getPrs(),
push: (dir) => {
const ctx = dir ? deps.contexts.byDirectory(dir) : undefined
deps.pushState(ctx)
},
managed: (id, dir) => {
const ctx = dir ? deps.contexts.byDirectory(dir) : undefined
if (ctx) return ctx.hasLiveSession(id) || !!ctx.peekState()?.getSession(id)
return deps.hasPanelSession(id) || !!deps.getState()?.getSession(id)
},
close: async (id, dir) => {
const ctx = dir ? deps.contexts.byDirectory(dir) : undefined
if (ctx) {
await deps.projectScope.run(ctx, () => deps.closeSession(id))
} else {
await deps.closeSession(id)
}
deps.postSessionClosed(id)
},
directories: () => {
const all: string[] = []
for (const ctx of deps.contexts.values()) {
all.push(ctx.root)
for (const wt of ctx.peekState()?.getWorktrees() ?? []) {
if (wt.path) all.push(wt.path)
}
}
if (all.length === 0) {
const root = deps.getRoot()
if (root) all.push(root)
}
return all
},
log: (...args) => deps.log(...args),
})
}
@@ -100,6 +100,10 @@ export class ProjectContexts {
return this.contexts.get(id)
}
values(): IterableIterator<ProjectContext> {
return this.contexts.values()
}
/** The context that owns a directory: its root or one of its worktree paths. */
byDirectory(dir: string): ProjectContext | undefined {
for (const ctx of this.contexts.values()) {
@@ -6,6 +6,9 @@ import { versionedName } from "./branch-name"
import { resolveVersionModels, buildInitialMessages, type CreatedVersion } from "./multi-version"
import { ensureSandbox } from "./sandbox-bootstrap"
import type { LifecycleHost } from "./provider-lifecycle"
import { Semaphore } from "./semaphore"
const PROVISION_CONCURRENCY = 2
/**
* Multi-version creation needs the lifecycle capabilities plus three provider
@@ -19,8 +22,8 @@ export interface MultiVersionHost extends LifecycleHost {
}
/**
* Create N worktrees with one session each (optionally one model per version),
* then fan the initial prompt out to every created session. State is reached
* Create N worktrees, provision their sessions with bounded concurrency, then
* send each initial prompt as soon as that session is ready. State is reached
* through the project context; everything else goes through the host.
*/
export async function createMultiVersion(
@@ -57,26 +60,49 @@ export async function createMultiVersion(
groupId,
})
// Phase 1: Create all worktrees + sessions first
// Phase 1: finish every shared-repository Git mutation before setup scripts
// or agents can run their own Git commands in the new worktrees.
const created: CreatedVersion[] = []
for (let i = 0; i < versions; i++) {
const version = await createVersion(ctx, host, {
index: i,
versions,
groupId,
baseBranch,
branchName,
worktreeName,
const specs = Array.from({ length: versions }, (_, index) => ({
index,
versions,
groupId,
baseBranch,
branchName,
worktreeName,
models,
providerID,
modelID,
sandbox: msg.sandbox,
}))
const prepared: PreparedVersion[] = []
for (const spec of specs) {
const version = await prepareVersion(host, spec)
if (version) prepared.push(version)
}
// Phase 2: Git creation is complete, so independent setup/session pipelines
// can overlap without racing the shared worktree metadata mutation.
const provision = async (version: PreparedVersion) => {
const ready = await provisionVersion(ctx, host, version)
if (!ready) return
created.push(ready)
sendInitialPrompt(
host,
ctx.id,
ready,
models,
providerID,
modelID,
sandbox: msg.sandbox,
})
if (!version) continue
created.push(version)
{ providerID, modelID },
{
text,
agent,
variant: msg.variant,
files,
},
)
// Update progress
host.post({
type: "agentManager.multiVersionProgress",
projectId: ctx.id,
@@ -87,15 +113,8 @@ export async function createMultiVersion(
})
}
// Phase 2: Send the initial prompt to all sessions, or clear busy state if no text.
await sendInitialPrompts(
host,
ctx.id,
created,
models,
{ providerID, modelID },
{ text, agent, variant: msg.variant, files },
)
const gate = new Semaphore(PROVISION_CONCURRENCY)
await Promise.all(prepared.map((version) => gate.run(() => provision(version))))
// Notify completion
host.post({
@@ -128,12 +147,13 @@ interface VersionSpec {
sandbox: boolean | undefined
}
/** Create one version's worktree + session and wire it into state and the webview. */
async function createVersion(
ctx: ProjectContext,
host: MultiVersionHost,
spec: VersionSpec,
): Promise<CreatedVersion | null> {
interface PreparedVersion {
spec: VersionSpec
wt: NonNullable<Awaited<ReturnType<MultiVersionHost["createOnDisk"]>>>
}
/** Create one version's worktree while the shared-repository Git barrier is active. */
async function prepareVersion(host: MultiVersionHost, spec: VersionSpec): Promise<PreparedVersion | null> {
host.log(`Creating worktree ${spec.index + 1}/${spec.versions}`)
const version = versionedName(spec.branchName || spec.worktreeName, spec.index, spec.versions)
@@ -148,6 +168,16 @@ async function createVersion(
host.log(`Failed to create worktree for version ${spec.index + 1}`)
return null
}
return { spec, wt }
}
/** Set up one prepared worktree, create its session, and expose it to the UI. */
async function provisionVersion(
ctx: ProjectContext,
host: MultiVersionHost,
prepared: PreparedVersion,
): Promise<CreatedVersion | null> {
const { spec, wt } = prepared
await host.runSetup(wt.result.path, wt.result.branch, wt.worktree.id)
@@ -240,11 +270,11 @@ async function reconcileSandbox(
}
}
/** Fan the initial prompt out to every created session, throttled between sends. */
async function sendInitialPrompts(
/** Send one version's initial prompt as soon as its session is ready. */
function sendInitialPrompt(
host: MultiVersionHost,
projectId: string,
created: CreatedVersion[],
created: CreatedVersion,
models: VersionSpec["models"],
resolved: { providerID: string | undefined; modelID: string | undefined },
input: {
@@ -253,22 +283,16 @@ async function sendInitialPrompts(
variant: string | undefined
files: Extract<AgentManagerInMessage, { type: "agentManager.createMultiVersion" }>["files"]
},
): Promise<void> {
const messages = buildInitialMessages(created, models, resolved, input.text, input.agent, input.variant, input.files)
for (let i = 0; i < messages.length; i++) {
const msg = messages[i]!
if (input.text) {
host.log(`Sending initial message to version ${i + 1} (session=${msg.sessionId})`)
host.promptName({
sessionID: msg.sessionId,
text: input.text,
providerID: msg.providerID,
modelID: msg.modelID,
})
}
host.post({ type: "agentManager.sendInitialMessage", projectId, ...msg })
if (input.text && i < messages.length - 1) {
await new Promise((resolve) => setTimeout(resolve, 300))
}
): void {
const msg = buildInitialMessages([created], models, resolved, input.text, input.agent, input.variant, input.files)[0]!
if (input.text) {
host.log(`Sending initial message to version ${created.versionIndex + 1} (session=${msg.sessionId})`)
host.promptName({
sessionID: msg.sessionId,
text: input.text,
providerID: msg.providerID,
modelID: msg.modelID,
})
}
host.post({ type: "agentManager.sendInitialMessage", projectId, ...msg })
}
@@ -51,6 +51,7 @@ interface Entry {
export class TerminalManager {
private readonly entries = new Map<string, Entry>()
private readonly restarts = new Map<string, Promise<void>>()
private readonly pending = new Map<string, { cols: number; rows: number }>()
constructor(private readonly deps: TerminalManagerDeps) {}
@@ -67,7 +68,14 @@ export class TerminalManager {
worktreeId: string | null
cwd: string
title: string
cols?: number
rows?: number
}): Promise<{ terminalId: string; worktreeId: string | null; title: string; wsUrl: string }> {
const initial =
this.pending.get(params.terminalId) ??
(params.cols !== undefined && params.rows !== undefined ? { cols: params.cols, rows: params.rows } : undefined)
this.pending.delete(params.terminalId)
const client = this.deps.getClient()
const { data, error } = await client.pty.create({
directory: params.cwd,
@@ -76,6 +84,7 @@ export class TerminalManager {
// xterm's DOM renderer cannot draw the Unicode sextant glyphs used by
// Kilo's modern wordmark, so use the compatible logo in embedded tabs.
env,
size: initial,
})
if (error || !data) {
const err = error instanceof Error ? error.message : String(error ?? "unknown error")
@@ -89,15 +98,39 @@ export class TerminalManager {
title: data.title ?? params.title,
}
this.entries.set(params.terminalId, entry)
// If a resize arrived while pty.create was in flight that differed from `initial`, apply it now.
const latest = this.pending.get(params.terminalId)
if (latest && (latest.cols !== initial?.cols || latest.rows !== initial?.rows)) {
this.pending.delete(params.terminalId)
const { error: resizeErr } = await client.pty.update({
directory: entry.cwd,
ptyID: entry.ptyID,
size: latest,
})
if (resizeErr) {
const err = resizeErr instanceof Error ? resizeErr.message : String(resizeErr)
this.deps.log(`Initial terminal resize failed (${params.terminalId}): ${err}`)
}
}
const wsUrl = this.deps.buildWsUrl(entry.ptyID, entry.cwd)
this.deps.log(`Terminal created: ${params.terminalId} -> pty ${entry.ptyID} cwd=${entry.cwd}`)
return { terminalId: params.terminalId, worktreeId: entry.worktreeId, title: entry.title, wsUrl }
}
/** Forward a resize event to the backend PTY. Missing terminals are a no-op. */
/**
* Forward a resize event to the backend PTY.
*
* If the terminal creation is still in flight, dimensions are queued into
* `pending` and applied during PTY initialization before the WebSocket
* URL is returned.
*/
async resize(terminalId: string, cols: number, rows: number): Promise<void> {
const entry = this.entries.get(terminalId)
if (!entry) return
if (!entry) {
this.pending.set(terminalId, { cols, rows })
return
}
this.pending.delete(terminalId)
const client = this.deps.getClient()
const { error } = await client.pty.update({
directory: entry.cwd,
@@ -126,6 +159,7 @@ export class TerminalManager {
* failed delete would be silently logged as a successful close and
* the server-side PTY would linger until `kilo serve` exits. */
async close(terminalId: string): Promise<void> {
this.pending.delete(terminalId)
const entry = this.entries.get(terminalId)
if (!entry) return
this.entries.delete(terminalId)
@@ -187,6 +221,7 @@ export class TerminalManager {
* is sampled mid-shutdown.
*/
async dispose(): Promise<void> {
this.pending.clear()
const snapshot = [...this.entries.values()]
if (snapshot.length === 0) {
this.entries.clear()
@@ -88,7 +88,7 @@ export class TerminalRouter {
handle(m: AgentManagerInMessage): boolean {
if (!isTerminalMessage(m)) return false
if (m.type === "agentManager.terminal.create") {
void this.handleCreate(m.createId, m.placement, m.worktreeId)
void this.handleCreate(m.createId, m.placement, m.worktreeId, m.cols, m.rows)
return true
}
if (m.type === "agentManager.terminal.close") {
@@ -132,7 +132,13 @@ export class TerminalRouter {
return manager.dispose()
}
private async handleCreate(createId: string, placement: TerminalPlacement, worktreeId: string | null): Promise<void> {
private async handleCreate(
createId: string,
placement: TerminalPlacement,
worktreeId: string | null,
cols?: number,
rows?: number,
): Promise<void> {
const generation = this.generation
const manager = this.manager
const cwd = this.resolveCwd(worktreeId)
@@ -155,7 +161,7 @@ export class TerminalRouter {
// Join the shared backend connection instead of racing its synchronous
// client accessor when this is the first Kilo action in the window.
await this.deps.getClientAsync()
const created = await manager.create({ terminalId: createId, worktreeId, cwd, title })
const created = await manager.create({ terminalId: createId, worktreeId, cwd, title, cols, rows })
if (generation !== this.generation) {
await manager.close(created.terminalId)
return
@@ -0,0 +1,32 @@
import type { ToolRequest } from "./tool-start"
import { parseToolRequest } from "./tool-start"
export function routeToolRequest<T extends { projectId?: string; directory?: string }, C extends { id: string }>(
input: T,
directory: string | undefined,
deps: { byDirectory: (value: string) => C | undefined; usable: (id: string) => C | undefined },
): { request: T; owner?: C } {
const request = directory ? { ...input, directory } : input
const owner =
(directory && deps.byDirectory(directory)) ?? (request.projectId ? deps.usable(request.projectId) : undefined)
if (!owner) return { request }
return { request: { ...request, projectId: owner.id }, owner }
}
export function handleToolEvent<C extends { id: string }>(
event: unknown,
directory: string | undefined,
contexts: { byDirectory: (value: string) => C | undefined; usable: (id: string) => C | undefined },
scope: { run: <T>(owner: C, fn: () => Promise<T>) => Promise<T> },
start: (req: ToolRequest) => Promise<void>,
): void {
const properties = (event as { properties?: unknown }).properties
const req = parseToolRequest(properties)
if (!req) return
const routed = routeToolRequest(req, directory, contexts)
if (routed.owner) {
void scope.run(routed.owner, () => start(routed.request))
return
}
void start(routed.request)
}
@@ -946,6 +946,8 @@ interface TerminalCreateIn {
placement: TerminalPlacement
/** null for LOCAL, worktree id otherwise */
worktreeId: string | null
cols?: number
rows?: number
}
interface TerminalCloseIn {
@@ -30,14 +30,17 @@ export class WorktreeImporter {
try {
const result = await manager.listBranches()
const state = this.host.state()
const configured = state?.getDefaultBaseBranch()
const base =
configured && result.branches.some((branch) => branch.name === configured) ? configured : result.defaultBranch
void manager.prefetchBase(base).catch((err) => this.host.log("Failed to prefetch base branch:", err))
const checked = await manager.checkedOutBranches()
const branches = result.branches.map((branch) => ({
...branch,
isCheckedOut: checked.has(branch.name),
}))
const state = this.host.state()
const configured = state?.getDefaultBaseBranch()
if (state && configured && !branches.some((branch) => branch.name === configured)) {
this.host.log(`Default base branch "${configured}" no longer exists, clearing`)
state.setDefaultBaseBranch(undefined)
+37 -1
View File
@@ -17,7 +17,7 @@ import { ensureBackendForAutocomplete } from "./services/autocomplete/ensure-bac
import { AutocompleteServiceManager } from "./services/autocomplete/AutocompleteServiceManager"
import { AttentionService } from "./services/attention"
import { BrowserAutomationService } from "./services/browser-automation"
import { TelemetryProxy } from "./services/telemetry"
import { TelemetryEventName, TelemetryProxy } from "./services/telemetry"
import { registerCommitMessageService } from "./services/commit-message"
import { registerCodeActions, registerTerminalActions, KiloCodeActionProvider } from "./services/code-actions"
import { registerToggleAutoApprove } from "./commands/toggle-auto-approve"
@@ -26,6 +26,7 @@ import { RemoteStatusService } from "./services/RemoteStatusService"
import { markWorkspace } from "./util/spotlight"
import { createNotebookBridge } from "./services/notebook"
import { createGitExecutable } from "./util/git-executable"
import { isCursorHost } from "./utils"
let agentManager: AgentManagerProvider | undefined
let shuttingDown = false
@@ -48,6 +49,10 @@ export function activate(context: vscode.ExtensionContext) {
console.log("Kilo Code extension is now active")
shuttingDown = false
// Drives the "!kilo-code.new.isCursor" guards on the native view/title and
// editor/title menu contributions — see isCursorHost() for why.
void vscode.commands.executeCommand("setContext", "kilo-code.new.isCursor", isCursorHost())
const telemetry = TelemetryProxy.getInstance()
// Create shared connection service (one server for all webviews)
@@ -339,8 +344,39 @@ export function activate(context: vscode.ExtensionContext) {
}),
)
// Sidebar menus use wrapper commands so this event measures real title button presses,
// not programmatic opens, shortcuts, or editor title commands.
const track = (button: string, command: string) => {
TelemetryProxy.capture(TelemetryEventName.TITLE_BUTTON_CLICKED, {
button,
surface: "sidebar_title",
})
void vscode.commands.executeCommand(command)
}
// Register toolbar button command handlers
context.subscriptions.push(
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.plusButtonClicked", () => {
track("new_task", "kilo-code.new.plusButtonClicked")
}),
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.historyButtonClicked", () => {
track("history", "kilo-code.new.historyButtonClicked")
}),
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.agentManagerOpen", () => {
track("agent_manager", "kilo-code.new.agentManagerOpen")
}),
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.kiloClawOpen", () => {
track("kiloclaw", "kilo-code.new.kiloClawOpen")
}),
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.marketplaceButtonClicked", () => {
track("marketplace", "kilo-code.new.marketplaceButtonClicked")
}),
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.profileButtonClicked", () => {
track("profile", "kilo-code.new.profileButtonClicked")
}),
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.settingsButtonClicked", () => {
track("settings", "kilo-code.new.settingsButtonClicked")
}),
vscode.commands.registerCommand("kilo-code.new.plusButtonClicked", () => {
const tab = activeTabProvider()
if (tab) tab.postMessage({ type: "action", action: "plusButtonClicked" })
@@ -0,0 +1,19 @@
import * as vscode from "vscode"
type Post = (msg: unknown) => void
export function buildAutoApprovalReasonSettingMessage() {
const config = vscode.workspace.getConfiguration("kilo-code.new")
return {
type: "autoApprovalReasonSettingLoaded" as const,
visible: config.get<boolean>("showAutoApprovalReason", true),
}
}
export function watchAutoApprovalReasonConfig(post: Post): vscode.Disposable {
return vscode.workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration("kilo-code.new.showAutoApprovalReason")) {
post(buildAutoApprovalReasonSettingMessage())
}
})
}
@@ -13,12 +13,18 @@ export async function loadCommands(client: KiloClient, dir: string): Promise<unk
const promise = retry(() => client.command.list({ directory: dir }, { throwOnError: true })).then(({ data }) => ({
type: "commandsLoaded",
commands: data.map((cmd) => ({
name: cmd.name,
description: cmd.description,
source: cmd.source,
hints: cmd.hints,
})),
commands: data.map((cmd) => {
const item = cmd as typeof cmd & { variant?: string }
return {
name: item.name,
description: item.description,
agent: item.agent,
model: item.model,
variant: item.variant,
source: item.source,
hints: item.hints,
}
}),
}))
promises.set(dir, promise)
@@ -6,6 +6,7 @@ import type { SuggestionContext } from "./handlers/suggestion"
import type { KiloClient } from "@kilocode/sdk/v2/client"
import { buildChatSettingsMessage } from "./chat-settings"
import { buildThroughputSettingMessage } from "./throughput-settings"
import { buildAutoApprovalReasonSettingMessage } from "./auto-approval-reason-settings"
import { handleModelUsageMessage, type ModelUsageMessage } from "./model-usage"
type Ctx = {
@@ -71,6 +72,10 @@ export async function routeEarlyMessage(
ctx.post(buildThroughputSettingMessage())
return true
}
if (message.type === "requestAutoApprovalReasonSetting") {
ctx.post(buildAutoApprovalReasonSettingMessage())
return true
}
if (message.type === "requestSpeechToTextModels") {
await ctx.speechToTextModels()
return true
@@ -73,6 +73,7 @@ function slimEdit(state: Record<string, unknown>): Record<string, unknown> {
}
}
if (meta.diagnostics) result.diagnostics = meta.diagnostics
if (meta.approval) result.approval = meta.approval
next.metadata = result
return next
}
@@ -84,6 +85,7 @@ function slimPatch(state: Record<string, unknown>): Record<string, unknown> {
if (isObj(meta)) {
const slim: Record<string, unknown> = {}
if (meta.diagnostics) slim.diagnostics = meta.diagnostics
if (meta.approval) slim.approval = meta.approval
if (Array.isArray(meta.files)) {
slim.files = (meta.files as Record<string, unknown>[]).map((f) => {
const diff = patch(f.patch) ?? patch(f.diff)
@@ -115,6 +117,7 @@ function slimMultiedit(state: Record<string, unknown>): Record<string, unknown>
if (isObj(meta)) {
const slim: Record<string, unknown> = {}
if (meta.diagnostics) slim.diagnostics = meta.diagnostics
if (meta.approval) slim.approval = meta.approval
if (Array.isArray(meta.results)) {
slim.results = (meta.results as Record<string, unknown>[]).map((r) => {
const rs: Record<string, unknown> = {}
@@ -149,6 +152,7 @@ function slimWrite(state: Record<string, unknown>): Record<string, unknown> {
if (meta.filepath) slim.filepath = meta.filepath
if (meta.exists !== undefined) slim.exists = meta.exists
if (meta.diagnostics) slim.diagnostics = meta.diagnostics
if (meta.approval) slim.approval = meta.approval
const fd = meta.filediff
if (isObj(fd)) {
slim.filediff = {
@@ -282,3 +282,25 @@ describe("KiloConnectionService drainPendingPrompts", () => {
expect(cleared).toBe(0)
})
})
describe("KiloConnectionService server exit handling", () => {
test("reports signal name when process is killed by signal", () => {
const service = new KiloConnectionService({} as any)
let stateErr: Error | undefined
service.onStateChange((state, err) => {
if (state === "error") stateErr = err
})
;(service as any).handleServerExit(null, "SIGSEGV")
expect(stateErr?.message).toBe("CLI background process exited with signal SIGSEGV. Retry to reconnect.")
})
test("reports exit code when process exits normally with code", () => {
const service = new KiloConnectionService({} as any)
let stateErr: Error | undefined
service.onStateChange((state, err) => {
if (state === "error") stateErr = err
})
;(service as any).handleServerExit(1, null)
expect(stateErr?.message).toBe("CLI background process exited with code 1. Retry to reconnect.")
})
})
@@ -138,7 +138,7 @@ export class KiloConnectionService {
update: async () => undefined,
} satisfies Pick<vscode.Memento, "get" | "update">)
this.sandboxPreference = new SandboxPreference(state)
this.serverManager = new ServerManager(context, (code) => this.handleServerExit(code))
this.serverManager = new ServerManager(context, (code, signal) => this.handleServerExit(code, signal))
this.active = vscode.window.state.focused
this.windowStateDisposable = vscode.window.onDidChangeWindowState((ws) => {
this.active = ws.focused
@@ -790,13 +790,11 @@ export class KiloConnectionService {
this.questionRevision += 1
}
private handleServerExit(code: number | null): void {
console.warn("[Kilo New] ConnectionService: CLI background process exited:", code)
private handleServerExit(code: number | null, signal: NodeJS.Signals | null): void {
const reason = signal ? `signal ${signal}` : `code ${code ?? "unknown"}`
console.warn(`[Kilo New] ConnectionService: CLI background process exited with ${reason}`)
this.resetConnection()
this.setState(
"error",
new Error(`CLI background process exited with code ${code ?? "unknown"}. Retry to reconnect.`),
)
this.setState("error", new Error(`CLI background process exited with ${reason}. Retry to reconnect.`))
}
private async doConnect(workspaceDir: string): Promise<void> {
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "انتهت عملية CLI بالرمز {{code}} قبل بدء الخادم",
"server.startupTimeout": "انتهت مهلة بدء تشغيل الخادم بعد {{seconds}} ثانية",
"server.processExited": "تم إنهاء عملية CLI بالرمز {{code}} قبل بدء الخادم",
"server.processSignaled": "تم إنهاء عملية CLI بواسطة الإشارة {{signal}} قبل بدء الخادم",
"server.spawnFailed": "فشل في تشغيل الملف الثنائي لـ CLI ({{code}})",
"server.startupTimeout": "انتهت مهلة بدء تشغيل الخادم بعد {{seconds}} ثوانٍ",
"remote.connected": "Kilo Remote: متصل",
"remote.connecting": "Kilo Remote: جارٍ الاتصال\u2026",
} as const
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "O processo da CLI foi encerrado com o código {{code}} antes que o servidor fosse iniciado",
"server.startupTimeout": "Tempo limite de inicialização do servidor esgotado após {{seconds}} segundos",
"server.processExited": "O processo da CLI foi encerrado com o código {{code}} antes da inicialização do servidor",
"server.processSignaled": "O processo da CLI foi finalizado pelo sinal {{signal}} antes da inicialização do servidor",
"server.spawnFailed": "Falha ao gerar o binário da CLI ({{code}})",
"server.startupTimeout": "Tempo limite de inicialização do servidor atingido após {{seconds}} segundos",
"remote.connected": "Kilo Remote: Conectado",
"remote.connecting": "Kilo Remote: Conectando\u2026",
} as const
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "CLI proces je izašao sa kodom {{code}} prije nego što se server pokrenuo",
"server.startupTimeout": "Vrijeme pokretanja servera je isteklo nakon {{seconds}} sekundi",
"server.processExited": "CLI proces je izašao s kodom {{code}} prije nego što je server pokrenut",
"server.processSignaled": "CLI proces je prekinut signalom {{signal}} prije pokretanja servera",
"server.spawnFailed": "Neuspjelo pokretanje CLI binarne datoteke ({{code}})",
"server.startupTimeout": "Isteklo je vrijeme za pokretanje servera nakon {{seconds}} sekundi",
"remote.connected": "Kilo Remote: Povezano",
"remote.connecting": "Kilo Remote: Povezivanje\u2026",
} as const
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "CLI-processen afsluttede med kode {{code}} før serveren startede",
"server.startupTimeout": "Serverens opstartstid udløb efter {{seconds}} sekunder",
"server.processExited": "CLI-processen afsluttedes med kode {{code}}, før serveren startede",
"server.processSignaled": "CLI-processen blev afbrudt af signal {{signal}}, før serveren startede",
"server.spawnFailed": "Kunne ikke starte CLI-binærfil ({{code}})",
"server.startupTimeout": "Timeout for serverstart efter {{seconds}} sekunder",
"remote.connected": "Kilo Remote: Forbundet",
"remote.connecting": "Kilo Remote: Forbinder\u2026",
} as const
@@ -1,5 +1,8 @@
export const dict = {
"server.processExited": "Der CLI-Prozess wurde mit dem Code {{code}} beendet, bevor der Server gestartet wurde",
"server.processSignaled":
"Der CLI-Prozess wurde durch das Signal {{signal}} beendet, bevor der Server gestartet wurde",
"server.spawnFailed": "Fehler beim Starten der CLI-Binärdatei ({{code}})",
"server.startupTimeout": "Zeitüberschreitung beim Serverstart nach {{seconds}} Sekunden",
"remote.connected": "Kilo Remote: Verbunden",
"remote.connecting": "Kilo Remote: Verbindung wird hergestellt\u2026",
@@ -1,5 +1,7 @@
export const dict = {
"server.processExited": "CLI process exited with code {{code}} before server started",
"server.processSignaled": "CLI process terminated by signal {{signal}} before server started",
"server.spawnFailed": "Failed to spawn CLI binary ({{code}})",
"server.startupTimeout": "Server startup timeout after {{seconds}} seconds",
"remote.connected": "Kilo Remote: Connected",
"remote.connecting": "Kilo Remote: Connecting\u2026",
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "El proceso de la CLI finalizó con el código {{code}} antes de que se iniciara el servidor",
"server.startupTimeout": "Tiempo de espera de inicio del servidor agotado después de {{seconds}} segundos",
"server.processExited": "El proceso CLI finalizó con el código {{code}} antes de que se iniciara el servidor",
"server.processSignaled": "El proceso CLI fue terminado por la señal {{signal}} antes de que se iniciara el servidor",
"server.spawnFailed": "Error al generar el binario CLI ({{code}})",
"server.startupTimeout": "Tiempo de espera agotado para el inicio del servidor después de {{seconds}} segundos",
"remote.connected": "Kilo Remote: Conectado",
"remote.connecting": "Kilo Remote: Conectando\u2026",
} as const
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "فرآیند CLI با کد {{code}} قبل از راه‌اندازی سرور خاتمه یافت",
"server.processExited": "فرایند CLI قبل از شروع سرور با کد {{code}} خارج شد",
"server.processSignaled": "فرایند CLI با سیگنال {{signal}} قبل از شروع سرور متوقف شد",
"server.spawnFailed": "اجرای فایل باینری CLI ناموفق بود ({{code}})",
"server.startupTimeout": "زمان راه‌اندازی سرور پس از {{seconds}} ثانیه به پایان رسید",
"remote.connected": "Kilo Remote: متصل شد",
"remote.connecting": "Kilo Remote: در حال اتصال…",
"remote.connecting": "Kilo Remote: در حال اتصال\u2026",
} as const
+6 -4
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "Le processus CLI s'est terminé avec le code {{code}} avant le démarrage du serveur",
"server.startupTimeout": "Délai de démarrage du serveur dépassé après {{seconds}} secondes",
"remote.connected": "Kilo Remote\u00a0: Connecté",
"remote.connecting": "Kilo Remote\u00a0: Connexion\u2026",
"server.processExited": "Le processus CLI a quitté avec le code {{code}} avant le démarrage du serveur",
"server.processSignaled": "Le processus CLI a été arrêté par le signal {{signal}} avant le démarrage du serveur",
"server.spawnFailed": "Échec du lancement du binaire CLI ({{code}})",
"server.startupTimeout": "Délai d'attente du démarrage du serveur dépassé après {{seconds}} secondes",
"remote.connected": "Kilo Remote : Connecté",
"remote.connecting": "Kilo Remote : Connexion en cours\u2026",
} as const
+5 -3
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "Il processo CLI è uscito con codice {{code}} prima dell'avvio del server",
"server.processExited": "Il processo CLI è terminato con il codice {{code}} prima dell'avvio del server",
"server.processSignaled": "Il processo CLI è stato terminato dal segnale {{signal}} prima dell'avvio del server",
"server.spawnFailed": "Impossibile avviare il binario CLI ({{code}})",
"server.startupTimeout": "Timeout di avvio del server dopo {{seconds}} secondi",
"remote.connected": "Kilo Remote: connesso",
"remote.connecting": "Kilo Remote: connessione...",
"remote.connected": "Kilo Remote: Connesso",
"remote.connecting": "Kilo Remote: Connessione in corso\u2026",
} as const
@@ -1,5 +1,7 @@
export const dict = {
"server.processExited": "サーバーが起動する前に、CLI プロセスがコード {{code}} で終了しました",
"server.processSignaled": "サーバーが起動する前に、CLI プロセスがシグナル {{signal}} で終了しました",
"server.spawnFailed": "CLI バイナリの起動に失敗しました ({{code}})",
"server.startupTimeout": "サーバーの起動が {{seconds}} 秒後にタイムアウトしました",
"remote.connected": "Kilo Remote: 接続済み",
"remote.connecting": "Kilo Remote: 接続中\u2026",
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "서버가 시작되기 전에 CLI 프로세스가 코드 {{code}}로 종료되었습니다",
"server.startupTimeout": "{{seconds}}초 후 서버 시작 시간이 초과되었습니다",
"server.processExited": "서버가 시작되기 전에 CLI 프로세스가 코드 {{code}}(으)로 종료되었습니다",
"server.processSignaled": "서버가 시작되기 전에 CLI 프로세스가 신호 {{signal}}(으)로 종료되었습니다",
"server.spawnFailed": "CLI 바이너리를 생성하지 못했습니다 ({{code}})",
"server.startupTimeout": "{{seconds}}초 후 서버 시작 시간 초과",
"remote.connected": "Kilo Remote: 연결됨",
"remote.connecting": "Kilo Remote: 연결 중\u2026",
} as const
@@ -1,5 +1,7 @@
export const dict = {
"server.processExited": "CLI-proces is afgesloten met code {{code}} voordat de server is gestart",
"server.processSignaled": "CLI-proces is beëindigd door signaal {{signal}} voordat de server is gestart",
"server.spawnFailed": "Kan CLI-binair bestand niet starten ({{code}})",
"server.startupTimeout": "Time-out bij opstarten van server na {{seconds}} seconden",
"remote.connected": "Kilo Remote: Verbonden",
"remote.connecting": "Kilo Remote: Verbinden\u2026",
@@ -1,5 +1,7 @@
export const dict = {
"server.processExited": "CLI-prosessen avsluttet med kode {{code}} før serveren startet",
"server.processSignaled": "CLI-prosessen ble avsluttet av signal {{signal}} før serveren startet",
"server.spawnFailed": "Kunne ikke starte CLI-binærfil ({{code}})",
"server.startupTimeout": "Tidsavbrudd for serveroppstart etter {{seconds}} sekunder",
"remote.connected": "Kilo Remote: Tilkoblet",
"remote.connecting": "Kilo Remote: Kobler til\u2026",
+3 -1
View File
@@ -1,5 +1,7 @@
export const dict = {
"server.processExited": "Proces CLI zakończył się z kodem {{code}} przed uruchomieniem serwera",
"server.processExited": "Proces CLI zakończył działanie z kodem {{code}} przed uruchomieniem serwera",
"server.processSignaled": "Proces CLI został przerwany przez sygnał {{signal}} przed uruchomieniem serwera",
"server.spawnFailed": "Nie udało się uruchomić pliku binarnego CLI ({{code}})",
"server.startupTimeout": "Przekroczono limit czasu uruchamiania serwera po {{seconds}} sekundach",
"remote.connected": "Kilo Remote: Połączono",
"remote.connecting": "Kilo Remote: Łączenie\u2026",
@@ -1,5 +1,7 @@
export const dict = {
"server.processExited": "Процесс CLI завершился с кодом {{code}} до запуска сервера",
"server.processSignaled": "Процесс CLI был завершен сигналом {{signal}} до запуска сервера",
"server.spawnFailed": "Не удалось запустить исполняемый файл CLI ({{code}})",
"server.startupTimeout": "Время ожидания запуска сервера истекло через {{seconds}} секунд",
"remote.connected": "Kilo Remote: Подключено",
"remote.connecting": "Kilo Remote: Подключение\u2026",
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "กระบวนการ CLI ออกด้วยรหัส {{code}} ก่อนที่เซิร์ฟเวอร์จะเริ่มทำงาน",
"server.startupTimeout": "หมดเวลาการเริ่มต้นเซิร์ฟเวอร์หลังจาก {{seconds}} วินาที",
"server.processExited": "กระบวนการ CLI ออกด้วยรหัส {{code}} ก่อนที่เซิร์ฟเวอร์จะเริ่มต้น",
"server.processSignaled": "กระบวนการ CLI ถูกยุติโดยสัญญาณ {{signal}} ก่อนที่เซิร์ฟเวอร์จะเริ่มต้น",
"server.spawnFailed": "ไม่สามารถเริ่มต้นไบนารี CLI ({{code}})",
"server.startupTimeout": "หมดเวลาเริ่มต้นเซิร์ฟเวอร์หลังจาก {{seconds}} วินาที",
"remote.connected": "Kilo Remote: เชื่อมต่อแล้ว",
"remote.connecting": "Kilo Remote: กำลังเชื่อมต่อ\u2026",
} as const
+3 -1
View File
@@ -1,5 +1,7 @@
export const dict = {
"server.processExited": "CLI işlemi sunucu başlamadan önce {{code}} koduyla çıktı",
"server.processExited": "CLI işlemi sunucu başlatılmadan önce {{code}} koduyla çıktı",
"server.processSignaled": "CLI işlemi sunucu başlatılmadan önce {{signal}} sinyali ile sonlandırıldı",
"server.spawnFailed": "CLI ikili dosyası başlatılamadı ({{code}})",
"server.startupTimeout": "{{seconds}} saniye sonra sunucu başlatma zaman aşımı",
"remote.connected": "Kilo Remote: Bağlandı",
"remote.connecting": "Kilo Remote: Bağlanıyor\u2026",
+3 -1
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "Процес CLI завершився з кодом {{code}} до запуску сервера",
"server.startupTimeout": "Час очікування запуску сервера вичерпано після {{seconds}} секунд",
"server.processSignaled": "Процес CLI був завершений сигналом {{signal}} до запуску сервера",
"server.spawnFailed": "Не вдалося запустити двійковий файл CLI ({{code}})",
"server.startupTimeout": "Час очікування запуску сервера минув через {{seconds}} секунд",
"remote.connected": "Kilo Remote: Підключено",
"remote.connecting": "Kilo Remote: Підключення\u2026",
} as const
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "在服务器启动之前,CLI 进程已退出,代码为 {{code}}",
"server.startupTimeout": "服务器启动在 {{seconds}} 秒后超时",
"server.processExited": "CLI 进程在服务器启动前以代码 {{code}} 退出",
"server.processSignaled": "CLI 进程在服务器启动前被信号 {{signal}} 终止",
"server.spawnFailed": "生成 CLI 二进制文件失败 ({{code}})",
"server.startupTimeout": "{{seconds}} 秒后服务器启动超时",
"remote.connected": "Kilo Remote: 已连接",
"remote.connecting": "Kilo Remote: 正在连接\u2026",
} as const
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "在伺服器啟動之前,CLI 處理程序已退出,代碼為 {{code}}",
"server.startupTimeout": "伺服器啟動在 {{seconds}} 秒後逾時",
"server.processExited": "CLI 程序在伺服器啟動前以代碼 {{code}} 退出",
"server.processSignaled": "CLI 程序在伺服器啟動前被信號 {{signal}} 終止",
"server.spawnFailed": "生成 CLI 二進位檔案失敗 ({{code}})",
"server.startupTimeout": "{{seconds}} 秒後伺服器啟動逾時",
"remote.connected": "Kilo Remote: 已連線",
"remote.connecting": "Kilo Remote: 正在連線\u2026",
} as const
@@ -17,7 +17,7 @@ export interface ServerInstance {
const STARTUP_TIMEOUT_SECONDS = 30
type WorkspaceFolderLike = { uri: { fsPath: string } }
type ServerExitListener = (code: number | null) => void
type ServerExitListener = (code: number | null, signal: NodeJS.Signals | null) => void
export function resolveServerCwd(folders: readonly WorkspaceFolderLike[] | undefined, storage: string): string {
return folders?.[0]?.uri.fsPath ?? storage
@@ -182,25 +182,36 @@ export class ServerManager {
stderrLines.push(errorOutput)
})
serverProcess.on("error", (error) => {
console.error("[Kilo New] ServerManager: ❌ Process error:", error)
serverProcess.on("error", (err: NodeJS.ErrnoException) => {
console.error("[Kilo New] ServerManager: ❌ Process error:", err)
if (!resolved) {
reject(error)
const spawnErr = err as NodeJS.ErrnoException & { spawnargs?: string[] }
const code = err.code || err.name || "UNKNOWN"
const header = t("server.spawnFailed", { code })
const lines = [
`Error: ${err.message}`,
...(err.code ? [`Code: ${err.code}`] : []),
...(err.errno !== undefined ? [`Errno: ${err.errno}`] : []),
...(err.syscall ? [`Syscall: ${err.syscall}`] : []),
...(err.path ? [`Path: ${err.path}`] : []),
...(Array.isArray(spawnErr.spawnargs) ? [`Spawn args: ${JSON.stringify(spawnErr.spawnargs)}`] : []),
]
const { userMessage, userDetails } = toErrorMessage(header, [...lines, ...stderrLines], cliPath)
reject(new ServerStartupError(userMessage, userDetails))
}
})
serverProcess.on("exit", (code) => {
console.log("[Kilo New] ServerManager: 🛑 Process exited with code:", code)
serverProcess.on("exit", (code, signal) => {
console.warn("[Kilo New] ServerManager: 🛑 Process exited:", { code, signal })
if (this.instance?.process === serverProcess) {
this.instance = null
this.onExit?.(code)
this.onExit?.(code, signal)
}
if (!resolved) {
const { userMessage, userDetails } = toErrorMessage(
t("server.processExited", { code: code ?? "null" }),
stderrLines,
cliPath,
)
const msg = signal
? t("server.processSignaled", { signal })
: t("server.processExited", { code: code ?? "unknown" })
const { userMessage, userDetails } = toErrorMessage(msg, stderrLines, cliPath)
reject(new ServerStartupError(userMessage, userDetails))
}
})
@@ -24,7 +24,7 @@ type Recording = Input & {
type Audio = {
data: string
format: "wav"
format: "m4a"
model: string
language?: string
}
@@ -39,16 +39,17 @@ ObjC.import("AVFoundation")
ObjC.import("Foundation")
function run(args) {
const settings = $.NSMutableDictionary.alloc.init
settings.setObjectForKey($.NSNumber.numberWithUnsignedInt(1819304813), $.AVFormatIDKey)
settings.setObjectForKey($.NSNumber.numberWithUnsignedInt(1633772320), $.AVFormatIDKey)
settings.setObjectForKey($.NSNumber.numberWithDouble(16000), $.AVSampleRateKey)
settings.setObjectForKey($.NSNumber.numberWithInt(1), $.AVNumberOfChannelsKey)
settings.setObjectForKey($.NSNumber.numberWithInt(16), $.AVLinearPCMBitDepthKey)
settings.setObjectForKey($.NSNumber.numberWithBool(false), $.AVLinearPCMIsFloatKey)
settings.setObjectForKey($.NSNumber.numberWithBool(false), $.AVLinearPCMIsBigEndianKey)
settings.setObjectForKey($.NSNumber.numberWithInt(24000), $.AVEncoderBitRateKey)
const error = Ref()
const url = $.NSURL.fileURLWithPath(args[0])
const recorder = $.AVAudioRecorder.alloc.initWithURLSettingsError(url, settings, error)
if (!recorder || !recorder.prepareToRecord || !recorder.record) throw new Error("Could not start recording")
if (!recorder || !recorder.prepareToRecord || !recorder.record) {
const description = error[0] && error[0].localizedDescription
throw new Error(description ? description.js : "Could not start recording")
}
console.log("ready")
$.NSFileHandle.fileHandleWithStandardInput.readDataToEndOfFile
recorder.stop
@@ -68,7 +69,7 @@ export async function startSpeechCapture(input: Input): Promise<boolean> {
starting = input.requestId
try {
const file = path.join(os.tmpdir(), `kilo-stt-${process.pid}-${Date.now()}.wav`)
const file = path.join(os.tmpdir(), `kilo-stt-${process.pid}-${Date.now()}.m4a`)
if (useMacCapture(process.platform, process.env)) {
const result = await startMac(file, input).catch((err: unknown) => {
console.warn("[Kilo New] Native macOS speech capture failed, falling back to FFmpeg", err)
@@ -105,7 +106,7 @@ export async function stopSpeechCapture(requestId: string): Promise<Audio> {
const file = await readFile(state.file)
await removeFile(state.file)
return { data: file.toString("base64"), format: "wav", model: state.model, language: state.language }
return { data: file.toString("base64"), format: "m4a", model: state.model, language: state.language }
}
export async function cancelSpeechCapture(requestId: string): Promise<void> {
@@ -168,6 +169,35 @@ export function macCaptureArgs(file: string): string[] {
return ["-l", "JavaScript", "-e", macScript, file]
}
export function ffmpegCaptureArgs(input: string[], file: string): string[] {
return ["-y", ...input, "-c:a", "aac", "-b:a", "24k", "-ar", "16000", "-ac", "1", "-movflags", "+faststart", file]
}
export function ffmpegPipeArgs(file: string): string[] {
return [
"-y",
"-f",
"s16le",
"-ar",
"16000",
"-ac",
"1",
"-i",
"pipe:0",
"-c:a",
"aac",
"-b:a",
"24k",
"-ar",
"16000",
"-ac",
"1",
"-movflags",
"+faststart",
file,
]
}
export function useMacCapture(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): boolean {
return platform === "darwin" && !env.KILO_FFMPEG_PATH && !env.FFMPEG_PATH
}
@@ -178,7 +208,7 @@ async function startWithArgs(bin: string, file: string, input: Input, args: Args
const proc = first.pipe
? pipeProcess(first.pipe, bin, file)
: spawn(bin, ["-y", ...first.input, "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1", "-f", "wav", file], {
: spawn(bin, ffmpegCaptureArgs(first.input, file), {
stdio: ["pipe", "ignore", "pipe"],
})
const state = createState(input, file, proc)
@@ -212,32 +242,9 @@ function createState(input: Input, file: string, proc: ChildProcess): Recording
function pipeProcess(pipe: string[], bin: string, file: string): ChildProcess {
const source = spawn("pw-record", pipe, { stdio: ["ignore", "pipe", "pipe"] })
const proc = spawn(
bin,
[
"-y",
"-f",
"s16le",
"-ar",
"16000",
"-ac",
"1",
"-i",
"pipe:0",
"-acodec",
"pcm_s16le",
"-ar",
"16000",
"-ac",
"1",
"-f",
"wav",
file,
],
{
stdio: ["pipe", "ignore", "pipe"],
},
)
const proc = spawn(bin, ffmpegPipeArgs(file), {
stdio: ["pipe", "ignore", "pipe"],
})
if (source.stdout && proc.stdin) source.stdout.pipe(proc.stdin)
source.on("error", (err) => proc.emit("error", err))
+21
View File
@@ -18,6 +18,27 @@ export function getWebviewFontSize(): number {
return clamp(raw)
}
/**
* True when running inside Cursor rather than real VS Code (or another
* fork). Cursor's Secondary Side Bar support is known to be unreliable for
* extension-contributed `view/title` toolbars (see
* https://github.com/anthropics/claude-code/issues/31375 for the same class
* of bug in a different extension), so Cursor falls back to an in-webview
* navigation bar instead of the native toolbar that VS Code renders fine
* everywhere.
*
* This is a per-host choice, not a per-dock-location one: `WebviewView` (and
* the rest of the public API, checked against @types/vscode) exposes no way
* to ask "is my view currently in the primary or secondary side bar", so the
* webview fallback bar is Cursor's only option in both locations. Accepted
* trade-off: Cursor's primary side bar loses the single-line native look it
* had before this existed, in exchange for the Secondary Side Bar actually
* working, with zero guessing about dock position anywhere.
*/
export function isCursorHost(): boolean {
return vscode.env.appName.toLowerCase().includes("cursor")
}
function fontStyle(): string {
const base = getWebviewFontSize()
const vars = SIZES.map((size) => `--kilo-font-size-${size}: ${(base * size) / 13}px;`).join("\n ")

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