mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-30 19:32:39 +08:00
Merge branch 'main' into fix/mermaid-copy-png-svg-clipboard
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep recent sessions and the Show History action inset and usable in narrow VS Code sidebars.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
"@kilocode/cli": minor
|
||||
---
|
||||
|
||||
Add nested slash command suggestions for `/review` in VS Code and support `staged`, `unpushed`, and `quick` review modes.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Remove unsupported `kilo web` CLI command.
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -840,6 +840,7 @@ export function make(options: ClientOptions) {
|
||||
cwd: input?.["cwd"],
|
||||
title: input?.["title"],
|
||||
env: input?.["env"],
|
||||
size: input?.["size"],
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
|
||||
@@ -2632,6 +2632,7 @@ export type PtysCreateInput = {
|
||||
readonly cwd?: string
|
||||
readonly title?: string
|
||||
readonly env?: { readonly [x: string]: string }
|
||||
readonly size?: { readonly rows: number; readonly cols: number }
|
||||
}["command"]
|
||||
readonly args?: {
|
||||
readonly command?: string
|
||||
@@ -2639,6 +2640,7 @@ export type PtysCreateInput = {
|
||||
readonly cwd?: string
|
||||
readonly title?: string
|
||||
readonly env?: { readonly [x: string]: string }
|
||||
readonly size?: { readonly rows: number; readonly cols: number }
|
||||
}["args"]
|
||||
readonly cwd?: {
|
||||
readonly command?: string
|
||||
@@ -2646,6 +2648,7 @@ export type PtysCreateInput = {
|
||||
readonly cwd?: string
|
||||
readonly title?: string
|
||||
readonly env?: { readonly [x: string]: string }
|
||||
readonly size?: { readonly rows: number; readonly cols: number }
|
||||
}["cwd"]
|
||||
readonly title?: {
|
||||
readonly command?: string
|
||||
@@ -2653,6 +2656,7 @@ export type PtysCreateInput = {
|
||||
readonly cwd?: string
|
||||
readonly title?: string
|
||||
readonly env?: { readonly [x: string]: string }
|
||||
readonly size?: { readonly rows: number; readonly cols: number }
|
||||
}["title"]
|
||||
readonly env?: {
|
||||
readonly command?: string
|
||||
@@ -2660,7 +2664,16 @@ export type PtysCreateInput = {
|
||||
readonly cwd?: string
|
||||
readonly title?: string
|
||||
readonly env?: { readonly [x: string]: string }
|
||||
readonly size?: { readonly rows: number; readonly cols: number }
|
||||
}["env"]
|
||||
readonly size?: {
|
||||
readonly command?: string
|
||||
readonly args?: ReadonlyArray<string>
|
||||
readonly cwd?: string
|
||||
readonly title?: string
|
||||
readonly env?: { readonly [x: string]: string }
|
||||
readonly size?: { readonly rows: number; readonly cols: number }
|
||||
}["size"]
|
||||
}
|
||||
|
||||
export type PtysCreateOutput = {
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+5
-3
@@ -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 |
|
||||
|
||||
|
||||
Generated
+2786
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
allowBuilds:
|
||||
core-js: true
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1bc2a28b80d8dc5aea4c03609efc13a23a2c88103e56df741155d9e794fc96f7
|
||||
size 2422
|
||||
oid sha256:d49870048a42ea9c0166143fbb364b0d9ee45d786e3b1346fcd0eda76c533bdb
|
||||
size 2414
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e3f86dabc39700b3a86508c1e6499a546eeabf720832b62c9ce12f2d97378ebd
|
||||
size 34649
|
||||
oid sha256:00092414080e2f70fb9b3166e8d06a1692af93e33717d0ab8fea5fe96e6a6982
|
||||
size 34468
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user