From 220cdd20c8af4d4420ee0c4bf9bc7da8a0e82673 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 3 Aug 2026 17:10:43 +0200 Subject: [PATCH 01/67] fix(vscode): inset recent sessions in narrow sidebars --- .changeset/narrow-sidebar-recent.md | 5 +++++ packages/kilo-vscode/webview-ui/src/styles/chat-layout.css | 3 +++ 2 files changed, 8 insertions(+) create mode 100644 .changeset/narrow-sidebar-recent.md diff --git a/.changeset/narrow-sidebar-recent.md b/.changeset/narrow-sidebar-recent.md new file mode 100644 index 00000000000..fcd56b0f73f --- /dev/null +++ b/.changeset/narrow-sidebar-recent.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep recent sessions and the Show History action inset and usable in narrow VS Code sidebars. diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css index 3e475d3ff39..d071072d90e 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css @@ -115,7 +115,10 @@ flex-direction: column; align-items: center; justify-content: center; + box-sizing: border-box; height: 100%; + width: 100%; + min-width: 0; color: var(--vscode-descriptionForeground); text-align: center; padding: 20px; From 4057d7eb387455b58c4d46d83b81ff3bc61a79a5 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 3 Aug 2026 17:44:22 +0200 Subject: [PATCH 02/67] fix(vscode): reduce narrow recent view side margins --- packages/kilo-vscode/webview-ui/src/styles/chat-layout.css | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css index d071072d90e..decab4d0f1c 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css @@ -122,6 +122,7 @@ color: var(--vscode-descriptionForeground); text-align: center; padding: 20px; + padding-inline: 12px; gap: 20px; } From e26acef176992b922515f9cbec3ab48eaf237d54 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 4 Aug 2026 10:41:05 +0200 Subject: [PATCH 03/67] fix(vscode): keep feedback label on one line --- packages/kilo-vscode/webview-ui/src/styles/welcome.css | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kilo-vscode/webview-ui/src/styles/welcome.css b/packages/kilo-vscode/webview-ui/src/styles/welcome.css index e2814931437..a1e1eb08208 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/welcome.css +++ b/packages/kilo-vscode/webview-ui/src/styles/welcome.css @@ -210,6 +210,7 @@ font-family: inherit; font-size: var(--kilo-font-size-12); font-weight: 500; + white-space: nowrap; cursor: pointer; transition: background-color 0.15s, From bdc66f447f3eb31545fc1dfd9ad78c15ea7c0bb3 Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Tue, 4 Aug 2026 08:45:00 +0000 Subject: [PATCH 04/67] chore: update kilo-vscode visual regression baselines --- .../settings/work-style-onboarding-default-chromium-linux.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/work-style-onboarding-default-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/work-style-onboarding-default-chromium-linux.png index 2597a60c84d..13b431ad977 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/work-style-onboarding-default-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/work-style-onboarding-default-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3f86dabc39700b3a86508c1e6499a546eeabf720832b62c9ce12f2d97378ebd -size 34649 +oid sha256:00092414080e2f70fb9b3166e8d06a1692af93e33717d0ab8fea5fe96e6a6982 +size 34468 From 6efe6475991e54cb987ad408c87028dd9f08d583 Mon Sep 17 00:00:00 2001 From: Rakshith N Date: Thu, 6 Aug 2026 23:28:48 +0530 Subject: [PATCH 05/67] feat: avoid long skill paths/URLs tooltip to overflow --- .../src/components/settings/AgentBehaviourTab.tsx | 4 ++-- packages/kilo-vscode/webview-ui/src/styles/settings.css | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx index 3a39b7da5dc..e97b1cff6df 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx @@ -903,7 +903,7 @@ const AgentBehaviourTab: Component = () => { "border-bottom": index() < skillPaths().length - 1 ? "1px solid var(--border-weak-base)" : "none", }} > - + { "border-bottom": index() < skillUrls().length - 1 ? "1px solid var(--border-weak-base)" : "none", }} > - + Date: Fri, 7 Aug 2026 00:31:01 +0530 Subject: [PATCH 06/67] test: add viewport bound checks --- .../tests/skills-settings-responsive.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/kilo-vscode/tests/skills-settings-responsive.spec.ts b/packages/kilo-vscode/tests/skills-settings-responsive.spec.ts index 5665ea93a3a..6403880ced9 100644 --- a/packages/kilo-vscode/tests/skills-settings-responsive.spec.ts +++ b/packages/kilo-vscode/tests/skills-settings-responsive.spec.ts @@ -73,6 +73,12 @@ test.describe("skills settings responsive layout", () => { await trigger.hover() const content = page.locator('[data-component="tooltip"]').filter({ hasText: seeded }) await expect(content, `Kilo Tooltip exposes full path on hover: ${seeded}`).toBeVisible() + const tipBox = await content.boundingBox() + const viewport = page.viewportSize()! + expect(tipBox, "path tooltip bounding box").not.toBeNull() + expect(tipBox!.x + tipBox!.width, `path tooltip stays inside viewport: ${seeded}`).toBeLessThanOrEqual( + viewport.width + 1, + ) } for (const seeded of [SEEDED_URL, SEEDED_URL_2]) { @@ -99,6 +105,12 @@ test.describe("skills settings responsive layout", () => { await trigger.hover() const content = page.locator('[data-component="tooltip"]').filter({ hasText: seeded }) await expect(content, `Kilo Tooltip exposes full URL on hover: ${seeded}`).toBeVisible() + const tipBox = await content.boundingBox() + const viewport = page.viewportSize()! + expect(tipBox, "URL tooltip bounding box").not.toBeNull() + expect(tipBox!.x + tipBox!.width, `URL tooltip stays inside viewport: ${seeded}`).toBeLessThanOrEqual( + viewport.width + 1, + ) } for (const [label, card] of [ From e2db06453915fe25a42122a64348285aee2c8f60 Mon Sep 17 00:00:00 2001 From: Rakshith N Date: Fri, 7 Aug 2026 11:24:43 +0530 Subject: [PATCH 07/67] (chore): add user readable changeset --- .changeset/vscode-skills-tooltip-wrap.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/vscode-skills-tooltip-wrap.md diff --git a/.changeset/vscode-skills-tooltip-wrap.md b/.changeset/vscode-skills-tooltip-wrap.md new file mode 100644 index 00000000000..a4a5ecec038 --- /dev/null +++ b/.changeset/vscode-skills-tooltip-wrap.md @@ -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. From 56e981b589c3c772fc388e6a0d349da6d5543b17 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 7 Aug 2026 09:13:57 +0200 Subject: [PATCH 08/67] fix(agent-manager): align multi-project header actions --- .changeset/align-agent-manager-project-actions.md | 5 +++++ .../kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx | 2 +- .../kilo-vscode/webview-ui/agent-manager/agent-manager.css | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .changeset/align-agent-manager-project-actions.md diff --git a/.changeset/align-agent-manager-project-actions.md b/.changeset/align-agent-manager-project-actions.md new file mode 100644 index 00000000000..b2d36e56cea --- /dev/null +++ b/.changeset/align-agent-manager-project-actions.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Align multi-project Agent Manager header actions with the worktree controls. diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx index 1e2dcb8fb8a..36359ab5f20 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx @@ -36,7 +36,6 @@ export const ProjectsSection: Component = (props) => ( label={} actions={
- {props.tools} = (props) => ( label={props.t("agentManager.project.add")} onClick={props.onAdd} /> + {props.tools}
} /> diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index fc1b0505301..fabf666ad83 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -287,6 +287,9 @@ html[data-theme="kilo-vscode"] land on the same glyph line as the rows inside the list. */ .am-projects > .am-section-header { padding-left: 2px; + /* The project toolbar has one more action than the worktree toolbar. Extend + its row so both toolbars start on the same leading action column. */ + margin-right: -14px; } .am-section-label { From dec5bef7ccd72728fd4810235293fe11f5c0805b Mon Sep 17 00:00:00 2001 From: Rakshith N Date: Fri, 7 Aug 2026 13:37:37 +0530 Subject: [PATCH 09/67] (chore): address bot review comments --- .../tests/skills-settings-responsive.spec.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/kilo-vscode/tests/skills-settings-responsive.spec.ts b/packages/kilo-vscode/tests/skills-settings-responsive.spec.ts index 6403880ced9..5d0330e207e 100644 --- a/packages/kilo-vscode/tests/skills-settings-responsive.spec.ts +++ b/packages/kilo-vscode/tests/skills-settings-responsive.spec.ts @@ -32,6 +32,16 @@ async function assertRowContained(row: Locator, card: Locator, label: string) { ) } +async function assertTooltipFitsViewport(content: Locator, label: string, page: Page) { + const tipBox = await content.boundingBox() + const viewport = page.viewportSize()! + expect(tipBox, `${label}: tooltip bounding box`).not.toBeNull() + expect(tipBox!.x, `${label}: tooltip left edge inside viewport`).toBeGreaterThanOrEqual(-1) + expect(tipBox!.x + tipBox!.width, `${label}: tooltip right edge inside viewport`).toBeLessThanOrEqual( + viewport.width + 1, + ) +} + test.describe("skills settings responsive layout", () => { test("folder-path and URL rows stay contained and the × button remains visible at a narrow viewport", async ({ page, @@ -73,12 +83,7 @@ test.describe("skills settings responsive layout", () => { await trigger.hover() const content = page.locator('[data-component="tooltip"]').filter({ hasText: seeded }) await expect(content, `Kilo Tooltip exposes full path on hover: ${seeded}`).toBeVisible() - const tipBox = await content.boundingBox() - const viewport = page.viewportSize()! - expect(tipBox, "path tooltip bounding box").not.toBeNull() - expect(tipBox!.x + tipBox!.width, `path tooltip stays inside viewport: ${seeded}`).toBeLessThanOrEqual( - viewport.width + 1, - ) + await assertTooltipFitsViewport(content, `path tooltip "${seeded}"`, page) } for (const seeded of [SEEDED_URL, SEEDED_URL_2]) { @@ -105,12 +110,7 @@ test.describe("skills settings responsive layout", () => { await trigger.hover() const content = page.locator('[data-component="tooltip"]').filter({ hasText: seeded }) await expect(content, `Kilo Tooltip exposes full URL on hover: ${seeded}`).toBeVisible() - const tipBox = await content.boundingBox() - const viewport = page.viewportSize()! - expect(tipBox, "URL tooltip bounding box").not.toBeNull() - expect(tipBox!.x + tipBox!.width, `URL tooltip stays inside viewport: ${seeded}`).toBeLessThanOrEqual( - viewport.width + 1, - ) + await assertTooltipFitsViewport(content, `URL tooltip "${seeded}"`, page) } for (const [label, card] of [ From 1406d719e36afd6d98d60b9fe54fa79b51e6b294 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Fri, 7 Aug 2026 10:24:46 +0200 Subject: [PATCH 10/67] fix(cli): remove unsupported kilo web command --- .changeset/remove-web-command.md | 5 ++ .../markdoc/partials/cli-commands-table.md | 1 - .../code-with-ai/platforms/cli-reference.md | 15 ----- packages/opencode/src/cli/cmd/web.ts | 61 ------------------- packages/opencode/src/index.ts | 4 +- packages/opencode/src/kilocode/commands.ts | 2 - .../__snapshots__/help-snapshots.test.ts.snap | 21 ------- .../test/cli/help/help-snapshots.test.ts | 1 - packages/opencode/test/kilocode/help.test.ts | 2 - 9 files changed, 7 insertions(+), 105 deletions(-) create mode 100644 .changeset/remove-web-command.md delete mode 100644 packages/opencode/src/cli/cmd/web.ts diff --git a/.changeset/remove-web-command.md b/.changeset/remove-web-command.md new file mode 100644 index 00000000000..8dd97b02feb --- /dev/null +++ b/.changeset/remove-web-command.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Remove unsupported `kilo web` CLI command. diff --git a/packages/kilo-docs/markdoc/partials/cli-commands-table.md b/packages/kilo-docs/markdoc/partials/cli-commands-table.md index 0ac95fd5548..d8653a898a6 100644 --- a/packages/kilo-docs/markdoc/partials/cli-commands-table.md +++ b/packages/kilo-docs/markdoc/partials/cli-commands-table.md @@ -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 ` | batch-test text models matching a filter for connectivity and latency | | `kilo profile` | show Kilo account profile | diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md b/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md index 0047db658f8..9a4ec19735b 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md @@ -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 ``` diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts deleted file mode 100644 index 36df40c8c93..00000000000 --- a/packages/opencode/src/cli/cmd/web.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Effect } from "effect" -import { UI } from "../ui" -import { effectCmd } from "../effect-cmd" -import { withNetworkOptions, resolveNetworkOptions } from "../network" -import { Flag } from "@opencode-ai/core/flag/flag" -import open from "open" - -export const WebCommand = effectCmd({ - command: "web", - builder: (yargs) => withNetworkOptions(yargs), - describe: "start kilo server and open web interface", - // Server loads instances per-request via x-kilo-directory header — no - // ambient project InstanceContext needed at startup. - instance: false, // kilocode_change - handler: Effect.fn("Cli.web")(function* (args) { - const { Server } = yield* Effect.promise(() => import("../../server/server")) - if (!Flag.KILO_SERVER_PASSWORD) { - UI.println(UI.Style.TEXT_WARNING_BOLD + "! KILO_SERVER_PASSWORD is not set; server is unsecured.") - } - const opts = yield* resolveNetworkOptions(args) - const server = yield* Effect.promise(() => Server.listen(opts)) - UI.empty() - UI.println(UI.logo(" ")) - UI.empty() - - // kilocode_change start - const urls = server.urls - - UI.println(UI.Style.TEXT_INFO_BOLD + " Local: ", UI.Style.TEXT_NORMAL, urls.local) - if (urls.network) { - UI.println(UI.Style.TEXT_INFO_BOLD + " Network: ", UI.Style.TEXT_NORMAL, urls.network) - } - - if (opts.mdns) { - UI.println(UI.Style.TEXT_INFO_BOLD + " mDNS: ", UI.Style.TEXT_NORMAL, `${opts.mdnsDomain}:${server.port}`) - } - - open(urls.local).catch(() => {}) - // kilocode_change end - - // kilocode_change start - graceful signal shutdown - const { InstanceRuntime } = yield* Effect.promise(() => import("../../project/instance-runtime")) - yield* Effect.promise( - () => - new Promise((resolve) => { - const shutdown = async () => { - try { - await InstanceRuntime.disposeAllInstances() - await server.stop(true) - } finally { - resolve() - } - } - process.once("SIGTERM", shutdown) - process.once("SIGINT", shutdown) - process.once("SIGHUP", shutdown) - }), - ) - // kilocode_change end - }), -}) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 779f51423af..f23744f03fb 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -22,7 +22,7 @@ import { AttachCommand } from "./cli/cmd/attach" import { TuiThreadCommand } from "./cli/cmd/tui" import { AcpCommand } from "./cli/cmd/acp" import { EOL } from "os" -import { WebCommand } from "./cli/cmd/web" +// kilocode_change - upstream web command intentionally omitted; Kilo does not ship an embedded web UI import { PrCommand } from "./cli/cmd/pr" import { SessionCommand } from "./cli/cmd/session" import { DbCommand } from "./cli/cmd/db" @@ -106,7 +106,7 @@ let cli = yargs(args) // kilocode_change .command(UpgradeCommand) .command(UninstallCommand) .command(ServeCommand) - .command(WebCommand) + // kilocode_change - upstream web command intentionally omitted .command(ModelsCommand) .command(StatsCommand) .command(ExportCommand) diff --git a/packages/opencode/src/kilocode/commands.ts b/packages/opencode/src/kilocode/commands.ts index 27f6316c5e0..fff41dc4647 100644 --- a/packages/opencode/src/kilocode/commands.ts +++ b/packages/opencode/src/kilocode/commands.ts @@ -13,7 +13,6 @@ import { AgentCommand } from "../cli/cmd/agent" import { UpgradeCommand } from "../cli/cmd/upgrade" import { UninstallCommand } from "../cli/cmd/uninstall" import { ServeCommand } from "../cli/cmd/serve" -import { WebCommand } from "../cli/cmd/web" import { ModelsCommand } from "../cli/cmd/models" import { StatsCommand } from "../cli/cmd/stats" import { ExportCommand } from "../cli/cmd/export" @@ -61,7 +60,6 @@ export const commands = [ UpgradeCommand, UninstallCommand, ServeCommand, - WebCommand, ModelsCommand, RollCallCommand, ProfileCommand, diff --git a/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap b/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap index 7a5c5b19cdd..241f8bd376a 100644 --- a/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap +++ b/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap @@ -235,27 +235,6 @@ Options: " `; -exports[`Kilo CLI help-text snapshots every documented command emits stable help text: kilo web --help 1`] = ` -"kilo web - -start kilo server and open web interface - -Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [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: []] -" -`; - exports[`Kilo CLI help-text snapshots every documented command emits stable help text: kilo models --help 1`] = ` "kilo models [provider] diff --git a/packages/opencode/test/cli/help/help-snapshots.test.ts b/packages/opencode/test/cli/help/help-snapshots.test.ts index 2f28861d4ef..f870c1abea8 100644 --- a/packages/opencode/test/cli/help/help-snapshots.test.ts +++ b/packages/opencode/test/cli/help/help-snapshots.test.ts @@ -66,7 +66,6 @@ const TOP_LEVEL = [ "upgrade", "uninstall", "serve", - "web", "models", "stats", "export", diff --git a/packages/opencode/test/kilocode/help.test.ts b/packages/opencode/test/kilocode/help.test.ts index bb1179b38af..264fbb8984f 100644 --- a/packages/opencode/test/kilocode/help.test.ts +++ b/packages/opencode/test/kilocode/help.test.ts @@ -12,7 +12,6 @@ import { AgentCommand } from "../../src/cli/cmd/agent" import { UpgradeCommand } from "../../src/cli/cmd/upgrade" import { UninstallCommand } from "../../src/cli/cmd/uninstall" import { ServeCommand } from "../../src/cli/cmd/serve" -import { WebCommand } from "../../src/cli/cmd/web" import { ModelsCommand } from "../../src/cli/cmd/models" import { StatsCommand } from "../../src/cli/cmd/stats" import { ExportCommand } from "../../src/cli/cmd/export" @@ -64,7 +63,6 @@ const commands = [ UpgradeCommand, UninstallCommand, ServeCommand, - WebCommand, ModelsCommand, StatsCommand, ExportCommand, From 0485e361c6fcde687923daf2d42a1761bd315366 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 7 Aug 2026 10:27:53 +0200 Subject: [PATCH 11/67] perf(agent-manager): speed up worktree session startup --- .changeset/faster-agent-manager-worktrees.md | 5 + packages/kilo-vscode/package.json | 6 + .../src/agent-manager/WorktreeManager.ts | 34 +++-- .../agent-manager/provider-multi-version.ts | 130 +++++++++++------- .../src/agent-manager/worktree-importer.ts | 3 + .../tests/unit/agent-manager-arch.test.ts | 2 +- .../tests/unit/provider-multi-version.test.ts | 67 +++++++++ .../tests/unit/sandbox-bootstrap.test.ts | 8 +- 8 files changed, 189 insertions(+), 66 deletions(-) create mode 100644 .changeset/faster-agent-manager-worktrees.md create mode 100644 packages/kilo-vscode/tests/unit/provider-multi-version.test.ts diff --git a/.changeset/faster-agent-manager-worktrees.md b/.changeset/faster-agent-manager-worktrees.md new file mode 100644 index 00000000000..259a5413f73 --- /dev/null +++ b/.changeset/faster-agent-manager-worktrees.md @@ -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. diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index 1ce8210b661..acca2e4b35f 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -59,6 +59,12 @@ ], "main": "./dist/extension.js", "contributes": { + "configurationDefaults": { + "files.watcherExclude": { + "**/.kilo/worktrees/**": true, + "**/.kilocode/worktrees/**": true + } + }, "taskDefinitions": [ { "type": "kilo-worktree-setup", diff --git a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts index 4879b32af35..763484584ed 100644 --- a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts +++ b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts @@ -150,6 +150,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 { + await this.ensureMigrated() + const base = branch || (await this.defaultBranch()) + await this.withGitLock(() => this.refreshBase(base)) + } + async renameBranch(worktreePath: string, current: string, requested: string): Promise { await this.ensureMigrated() return this.withGitLock(() => this.renameBranchImpl(worktreePath, current, requested)) @@ -762,20 +769,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 +831,23 @@ export class WorktreeManager { throw new Error(`Could not resolve start point for branch "${branch}"`) } + private async refreshBase(branch: string, requested?: string): Promise { + 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". diff --git a/packages/kilo-vscode/src/agent-manager/provider-multi-version.ts b/packages/kilo-vscode/src/agent-manager/provider-multi-version.ts index 41cc02049eb..59b6aba1afd 100644 --- a/packages/kilo-vscode/src/agent-manager/provider-multi-version.ts +++ b/packages/kilo-vscode/src/agent-manager/provider-multi-version.ts @@ -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 { +interface PreparedVersion { + spec: VersionSpec + wt: NonNullable>> +} + +/** Create one version's worktree while the shared-repository Git barrier is active. */ +async function prepareVersion(host: MultiVersionHost, spec: VersionSpec): Promise { 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 { + 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["files"] }, -): Promise { - 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 }) } diff --git a/packages/kilo-vscode/src/agent-manager/worktree-importer.ts b/packages/kilo-vscode/src/agent-manager/worktree-importer.ts index c44d7d9cf95..c1a55a8ba8c 100644 --- a/packages/kilo-vscode/src/agent-manager/worktree-importer.ts +++ b/packages/kilo-vscode/src/agent-manager/worktree-importer.ts @@ -30,6 +30,9 @@ export class WorktreeImporter { try { const result = await manager.listBranches() + void manager + .prefetchBase(result.defaultBranch) + .catch((err) => this.host.log("Failed to prefetch base branch:", err)) const checked = await manager.checkedOutBranches() const branches = result.branches.map((branch) => ({ ...branch, diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index a9c2f4b3552..7d0b8b1c132 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -461,7 +461,7 @@ describe("Agent Manager Provider — onMessage routing", () => { const lifecycle = source.getProject().addSourceFileAtPath(path.join(ROOT, "src/agent-manager", module)) const fn = lifecycle.getFunction(delegated[1]!) expect(fn, `delegated function ${delegated[1]} not found in ${module}`).toBeTruthy() - // The multi-version flow spans phase helpers (createVersion, sendInitialPrompts), + // The multi-version flow spans prepare, provision, and initial-prompt helpers, // so ordering assertions need the whole module, not just the orchestrator. if (delegated[1] === "createMultiVersion") return lifecycle.getText() return fn!.getText() diff --git a/packages/kilo-vscode/tests/unit/provider-multi-version.test.ts b/packages/kilo-vscode/tests/unit/provider-multi-version.test.ts new file mode 100644 index 00000000000..df0ec2d53b9 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/provider-multi-version.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it, mock } from "bun:test" +import type { Session } from "@kilocode/sdk/v2/client" +import { createMultiVersion, type MultiVersionHost } from "../../src/agent-manager/provider-multi-version" +import type { ProjectContext } from "../../src/agent-manager/project/context" +import type { CreateWorktreeOnDiskResult } from "../../src/agent-manager/worktree-create" + +describe("multi-version provisioning", () => { + it("finishes git creation before provisioning at bounded concurrency", async () => { + const flow: string[] = [] + const gates = [Promise.withResolvers(), Promise.withResolvers()] + const entered = [Promise.withResolvers(), Promise.withResolvers()] + const state = { addSession: mock(() => {}), armAutoName: mock(() => {}) } + const ctx = { + id: "project-1", + stateManager: () => state, + peekState: () => state, + worktreeManager: () => ({ removeWorktree: mock(async () => {}) }), + } as unknown as ProjectContext + const host = { + log: mock(() => {}), + post: mock((msg: { type: string; sessionId?: string }) => { + if (msg.type === "agentManager.sendInitialMessage") flow.push(`prompt:${msg.sessionId}`) + }), + createOnDisk: mock(async (opts: { branchName?: string }) => { + const index = opts.branchName?.endsWith("_v2") ? 1 : opts.branchName?.endsWith("_v3") ? 2 : 0 + flow.push(`git:${index}`) + return { + worktree: { id: `wt-${index}` }, + result: { path: `/repo/wt-${index}`, branch: `branch-${index}`, parentBranch: "main" }, + } as CreateWorktreeOnDiskResult + }), + runSetup: mock(async (dir: string) => { + const index = Number(dir.at(-1)!) + flow.push(`setup:${index}`) + if (index < 2) { + entered[index]?.resolve() + await gates[index]?.promise + } + }), + createSession: mock(async (dir: string) => ({ id: `session-${dir.at(-1)!}` }) as Session), + autoName: () => ({ enabled: false }), + register: mock(() => {}), + notifyReady: mock(() => {}), + sessions: { register: mock(() => {}) }, + promptName: mock(() => {}), + capture: mock(() => {}), + error: mock(() => {}), + } as unknown as MultiVersionHost + + const pending = createMultiVersion(ctx, host, { + type: "agentManager.createMultiVersion", + text: "Fix it", + branchName: "fix-it", + versions: 3, + }) + await Promise.all(entered.map((entry) => entry.promise)) + + expect(flow.slice(0, 5)).toEqual(["git:0", "git:1", "git:2", "setup:0", "setup:1"]) + expect(flow).not.toContain("setup:2") + + gates.forEach((gate) => gate.resolve()) + await pending + + expect(flow).toContain("setup:2") + expect(flow.filter((event) => event.startsWith("prompt:"))).toHaveLength(3) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/sandbox-bootstrap.test.ts b/packages/kilo-vscode/tests/unit/sandbox-bootstrap.test.ts index 445192eeff1..7d7a02172d8 100644 --- a/packages/kilo-vscode/tests/unit/sandbox-bootstrap.test.ts +++ b/packages/kilo-vscode/tests/unit/sandbox-bootstrap.test.ts @@ -98,8 +98,8 @@ describe("Agent Manager sandbox startup", () => { ) test("reconciles before exposing or prompting the session", () => { - // In createVersion the sandbox gate runs before the session is exposed. - const start = flow.indexOf("async function createVersion") + // In provisionVersion the sandbox gate runs before the session is exposed. + const start = flow.indexOf("async function provisionVersion") const version = flow.slice(start, flow.indexOf("\n/**", start + 1)) const gate = version.indexOf("await reconcileSandbox") const register = version.indexOf("host.register", gate) @@ -122,8 +122,8 @@ describe("Agent Manager sandbox startup", () => { expect(abort).toBeGreaterThan(discard) // The created sessions feed the initial prompt phase. - const prompts = flow.slice(flow.indexOf("async function sendInitialPrompts")) - expect(prompts).toContain("buildInitialMessages(created") + const prompts = flow.slice(flow.indexOf("function sendInitialPrompt")) + expect(prompts).toContain("buildInitialMessages([created]") expect(prompts).toContain('type: "agentManager.sendInitialMessage"') }) From 620bb927be08cb1e6930fe6f4227bc26dc9b4d75 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 7 Aug 2026 10:30:33 +0200 Subject: [PATCH 12/67] fix(agent-manager): keep header actions resize safe --- .../webview-ui/agent-manager/agent-manager.css | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index fabf666ad83..88adf062704 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -281,15 +281,14 @@ html[data-theme="kilo-vscode"] .am-project-body .am-section-header { padding-left: 6px; + padding-right: 6px; } /* This heading sits outside the projects list, so it needs its own inset to land on the same glyph line as the rows inside the list. */ .am-projects > .am-section-header { padding-left: 2px; - /* The project toolbar has one more action than the worktree toolbar. Extend - its row so both toolbars start on the same leading action column. */ - margin-right: -14px; + padding-right: 2px; } .am-section-label { @@ -331,7 +330,11 @@ html[data-theme="kilo-vscode"] .am-project-actions { display: flex; align-items: center; - gap: 2px; + justify-content: space-between; + /* The project row needs 20px + 24px + 20px; the worktree row uses the + remaining space for its split-button and settings gap. */ + width: 64px; + gap: 0; } .am-project { From db64768d5bcc5e25fdc0d968c35febe2289492e1 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 7 Aug 2026 10:42:52 +0200 Subject: [PATCH 13/67] fix(agent-manager): prefetch configured base branch --- .../kilo-vscode/src/agent-manager/worktree-importer.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/kilo-vscode/src/agent-manager/worktree-importer.ts b/packages/kilo-vscode/src/agent-manager/worktree-importer.ts index c1a55a8ba8c..59cad840556 100644 --- a/packages/kilo-vscode/src/agent-manager/worktree-importer.ts +++ b/packages/kilo-vscode/src/agent-manager/worktree-importer.ts @@ -30,8 +30,11 @@ 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(result.defaultBranch) + .prefetchBase(base) .catch((err) => this.host.log("Failed to prefetch base branch:", err)) const checked = await manager.checkedOutBranches() const branches = result.branches.map((branch) => ({ @@ -39,8 +42,6 @@ export class WorktreeImporter { 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) From cfaa224f3a63d6270692cbd8facb1b269137fdf8 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 7 Aug 2026 10:51:10 +0200 Subject: [PATCH 14/67] style(agent-manager): format base branch prefetch --- packages/kilo-vscode/src/agent-manager/worktree-importer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/src/agent-manager/worktree-importer.ts b/packages/kilo-vscode/src/agent-manager/worktree-importer.ts index 59cad840556..439bc8c3e6d 100644 --- a/packages/kilo-vscode/src/agent-manager/worktree-importer.ts +++ b/packages/kilo-vscode/src/agent-manager/worktree-importer.ts @@ -32,7 +32,8 @@ export class WorktreeImporter { 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 + 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)) From cccc9a52c647d904b37acff4ec5adbacb8269a7f Mon Sep 17 00:00:00 2001 From: Rakshith N Date: Fri, 7 Aug 2026 14:28:35 +0530 Subject: [PATCH 15/67] fix(vscode): allow 8px viewport slack for Kobalte tooltip overflowPadding --- .../kilo-vscode/tests/skills-settings-responsive.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/kilo-vscode/tests/skills-settings-responsive.spec.ts b/packages/kilo-vscode/tests/skills-settings-responsive.spec.ts index 5d0330e207e..4c66eb9fd6e 100644 --- a/packages/kilo-vscode/tests/skills-settings-responsive.spec.ts +++ b/packages/kilo-vscode/tests/skills-settings-responsive.spec.ts @@ -36,9 +36,12 @@ async function assertTooltipFitsViewport(content: Locator, label: string, page: const tipBox = await content.boundingBox() const viewport = page.viewportSize()! expect(tipBox, `${label}: tooltip bounding box`).not.toBeNull() - expect(tipBox!.x, `${label}: tooltip left edge inside viewport`).toBeGreaterThanOrEqual(-1) + // Kobalte's PopperRoot defaults to overflowPadding: 8, so the floating + // tooltip is allowed to extend up to 8px past each viewport edge before + // the shift middleware stops nudging it. + expect(tipBox!.x, `${label}: tooltip left edge inside viewport`).toBeGreaterThanOrEqual(-9) expect(tipBox!.x + tipBox!.width, `${label}: tooltip right edge inside viewport`).toBeLessThanOrEqual( - viewport.width + 1, + viewport.width + 9, ) } From 83d8e039d76046b5e60c0d276e561af0221dc04e Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 7 Aug 2026 11:07:40 +0200 Subject: [PATCH 16/67] perf(agent-manager): cache git preflight checks and format base prefetch --- packages/kilo-vscode/src/agent-manager/WorktreeManager.ts | 8 ++++++++ .../kilo-vscode/src/agent-manager/worktree-importer.ts | 4 +--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts index 763484584ed..a28e157a4ea 100644 --- a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts +++ b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts @@ -121,6 +121,8 @@ export class WorktreeManager { // Key: `${root}:${remote}:${branch}`, Value: timestamp when fetch was done private static fetchCache = new Map() private static readonly FETCH_CACHE_TTL = 60_000 // 1 minute + private static gitAvailable = false + private static lfsAvailable: boolean | undefined private withGitLock(fn: () => Promise): Promise { const key = this.root @@ -183,9 +185,12 @@ export class WorktreeManager { } private async ensureGitAvailable(): Promise { + 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.", @@ -912,10 +917,13 @@ export class WorktreeManager { } async checkLfsAvailable(): Promise { + 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 } diff --git a/packages/kilo-vscode/src/agent-manager/worktree-importer.ts b/packages/kilo-vscode/src/agent-manager/worktree-importer.ts index 439bc8c3e6d..0324ce6e1bd 100644 --- a/packages/kilo-vscode/src/agent-manager/worktree-importer.ts +++ b/packages/kilo-vscode/src/agent-manager/worktree-importer.ts @@ -34,9 +34,7 @@ export class WorktreeImporter { 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)) + 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, From 454cb934a04c2b62d2cbe38715e4f8f2a2c967d0 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 7 Aug 2026 11:24:51 +0200 Subject: [PATCH 17/67] feat(vscode): sync model selector with slash command overrides --- .changeset/command-model-selector-sync.md | 5 +++ .../kilo-vscode/src/kilo-provider/commands.ts | 18 ++++++--- .../tests/unit/prompt-send-contract.test.ts | 9 +++++ .../tests/unit/use-slash-command.test.ts | 27 +++++++++++++ .../src/components/chat/PromptInput.tsx | 5 +++ .../webview-ui/src/context/session.tsx | 40 ++++++++++++++----- .../webview-ui/src/types/messages/agents.ts | 3 ++ 7 files changed, 90 insertions(+), 17 deletions(-) create mode 100644 .changeset/command-model-selector-sync.md diff --git a/.changeset/command-model-selector-sync.md b/.changeset/command-model-selector-sync.md new file mode 100644 index 00000000000..cd64ab6f90e --- /dev/null +++ b/.changeset/command-model-selector-sync.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Update the active model, mode, and thinking selectors when executing a slash command with configured overrides. diff --git a/packages/kilo-vscode/src/kilo-provider/commands.ts b/packages/kilo-vscode/src/kilo-provider/commands.ts index d641fff1f38..822ef45ba2e 100644 --- a/packages/kilo-vscode/src/kilo-provider/commands.ts +++ b/packages/kilo-vscode/src/kilo-provider/commands.ts @@ -13,12 +13,18 @@ export async function loadCommands(client: KiloClient, dir: string): Promise 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) diff --git a/packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts b/packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts index 8cce4945257..f94736a9603 100644 --- a/packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts +++ b/packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts @@ -78,6 +78,15 @@ describe("sendCommand dismisses pending tool requests", () => { it("rejects questions before sending", () => { expect(body).toContain("dismissQuestion") }) + + it("applies model, agent, and variant overrides when provided by a command", () => { + expect(body).toContain("if (overrides?.agent)") + expect(body).toContain("selectAgent(overrides.agent, scope)") + expect(body).toContain("if (overrides?.model)") + expect(body).toContain("selectModel(parsed.providerID, parsed.modelID, scope)") + expect(body).toContain("if (overrides?.variant)") + expect(body).toContain("selectVariant(overrides.variant, scope)") + }) }) describe("static command completion contract", () => { diff --git a/packages/kilo-vscode/tests/unit/use-slash-command.test.ts b/packages/kilo-vscode/tests/unit/use-slash-command.test.ts index 710ca744bef..03862380fe9 100644 --- a/packages/kilo-vscode/tests/unit/use-slash-command.test.ts +++ b/packages/kilo-vscode/tests/unit/use-slash-command.test.ts @@ -188,4 +188,31 @@ describe("useSlashCommand sandbox action", () => { expect(ctx.slash.results()[0]?.description).toBe("Toggle sandbox") ctx.dispose() }) + + it("preserves model, agent, and variant metadata on loaded server commands", () => { + const ctx = setup(() => {}) + + ctx.fire({ + type: "commandsLoaded", + commands: [ + { + name: "ship", + description: "Ship PR", + agent: "code", + model: "openai/gpt-5.6-luna-fast", + variant: "xhigh", + hints: ["deploy"], + }, + ], + }) + + ctx.slash.onInput("/ship", 5) + const matches = ctx.slash.results() + expect(matches).toHaveLength(1) + expect(matches[0]?.name).toBe("ship") + expect(matches[0]?.agent).toBe("code") + expect(matches[0]?.model).toBe("openai/gpt-5.6-luna-fast") + expect(matches[0]?.variant).toBe("xhigh") + ctx.dispose() + }) }) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index 075b925b73b..da9e7789a8f 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -1239,6 +1239,11 @@ export const PromptInput: Component = (props) => { pendingId, context, origin ?? null, + { + agent: matched.agent, + model: matched.model, + variant: matched.variant, + }, ) } else { session.sendMessage(message, sel?.providerID, sel?.modelID, attachments, pendingId, context, data, origin ?? null) diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 1e09519fa5d..8831d7edfb0 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -283,6 +283,7 @@ interface SessionContextValue { draftID?: string, context?: string, origin?: string | null, + overrides?: { agent?: string; model?: string; variant?: string }, ) => void abort: () => void compact: () => void @@ -2352,31 +2353,51 @@ export const SessionProvider: ParentComponent = (props) => { draftID?: string, context?: string, origin?: string | null, + overrides?: { agent?: string; model?: string; variant?: string }, ) { if (!server.isConnected()) { console.warn("[Kilo New] Cannot send command: not connected") return } - // Cloud previews need import-then-command; post importAndSend with command metadata const sid = origin === undefined ? currentSessionID() : (origin ?? undefined) - const selection = providerID && modelID ? { providerID, modelID } : selected(sid) - recordModelUsage(selection?.providerID, selection?.modelID) + const effectiveDraftID = !sid && !draftID ? crypto.randomUUID() : draftID + const scope = effectiveDraftID ?? sid + if (!sid && !draftID && effectiveDraftID) agentDrafts.seed(effectiveDraftID) + + if (overrides?.agent) { + selectAgent(overrides.agent, scope) + } + if (overrides?.model) { + const parsed = parseModelString(overrides.model) + if (parsed) { + selectModel(parsed.providerID, parsed.modelID, scope) + } + } + if (overrides?.variant) { + selectVariant(overrides.variant, scope) + } + + const effectiveSelection = selected(scope) + const effectiveProvider = effectiveSelection?.providerID ?? providerID + const effectiveModel = effectiveSelection?.modelID ?? modelID + recordModelUsage(effectiveProvider, effectiveModel) + + // Cloud previews need import-then-command; post importAndSend with command metadata const preview = sid?.startsWith("cloud:") ? sid.slice("cloud:".length) : origin === undefined ? cloudPreviewId() : null if (preview) { - const scope = draftID ?? sid const agent = promptAgent(scope) vscode.postMessage({ type: "importAndSend", cloudSessionId: preview, text: `/${command} ${args}`.trim(), messageID: Identifier.ascending("message"), - providerID, - modelID, + providerID: effectiveProvider, + modelID: effectiveModel, agent, variant: currentVariant(scope), files, @@ -2393,9 +2414,6 @@ export const SessionProvider: ParentComponent = (props) => { dismissQuestion(q.id) } - const effectiveDraftID = !sid && !draftID ? crypto.randomUUID() : draftID - const scope = effectiveDraftID ?? sid - if (!sid && !draftID && effectiveDraftID) agentDrafts.seed(effectiveDraftID) if (scope) { clearClose(scope) addOptimistic(scope, messageID, `/${command} ${args}`.trim(), files) @@ -2414,8 +2432,8 @@ export const SessionProvider: ParentComponent = (props) => { messageID, sessionID: sid, draftID: effectiveDraftID, - providerID, - modelID, + providerID: effectiveProvider, + modelID: effectiveModel, agent, variant: currentVariant(scope), files, diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/agents.ts b/packages/kilo-vscode/webview-ui/src/types/messages/agents.ts index 4079413853d..90a69408180 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/agents.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/agents.ts @@ -11,6 +11,9 @@ export interface SkillInfo { export interface SlashCommandInfo { name: string description?: string + agent?: string + model?: string + variant?: string source?: "command" | "mcp" | "skill" hints: string[] } From 40121d832591d743cd360482e4f19e21a353310a Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 7 Aug 2026 11:51:34 +0200 Subject: [PATCH 18/67] fix(vscode): compress speech-to-text audio to AAC to prevent payload errors --- .changeset/compressed-speech-to-text-audio.md | 5 ++ .../kilo-vscode/src/speech-to-text/capture.ts | 72 ++++++++++--------- .../tests/unit/speech-to-text-capture.test.ts | 71 ++++++++++++++++-- 3 files changed, 109 insertions(+), 39 deletions(-) create mode 100644 .changeset/compressed-speech-to-text-audio.md diff --git a/.changeset/compressed-speech-to-text-audio.md b/.changeset/compressed-speech-to-text-audio.md new file mode 100644 index 00000000000..fee2a9166d1 --- /dev/null +++ b/.changeset/compressed-speech-to-text-audio.md @@ -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. diff --git a/packages/kilo-vscode/src/speech-to-text/capture.ts b/packages/kilo-vscode/src/speech-to-text/capture.ts index cbf5c54848a..e8b61251072 100644 --- a/packages/kilo-vscode/src/speech-to-text/capture.ts +++ b/packages/kilo-vscode/src/speech-to-text/capture.ts @@ -24,7 +24,7 @@ type Recording = Input & { type Audio = { data: string - format: "wav" + format: "m4a" model: string language?: string } @@ -39,12 +39,10 @@ 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) @@ -68,7 +66,7 @@ export async function startSpeechCapture(input: Input): Promise { 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 +103,7 @@ export async function stopSpeechCapture(requestId: string): Promise