From 44f13738a30668483a2cc5c22c6ba82a718cdb90 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Sat, 18 Jul 2026 08:43:45 -0400 Subject: [PATCH 01/71] feat: configure web search availability for all providers Adds an experimental.websearch config flag that exposes the websearch tool to models from any provider, not only the Kilo gateway. The flag is editable from the VS Code Web Tools settings tab (renamed from Browser) and the Kilo Console Tools page, with project/global overlay inheritance and revert support. Environment flags KILO_ENABLE_EXA and KILO_ENABLE_PARALLEL keep working as before. --- .changeset/enable-websearch-config.md | 6 + packages/core/src/v1/config/config.ts | 3 + .../src/routes/config/ToolsRoute.tsx | 48 +++- .../src/components/settings/BrowserTab.tsx | 119 +++++--- .../src/components/settings/Settings.tsx | 6 +- .../kilo-vscode/webview-ui/src/i18n/en.ts | 8 + .../webview-ui/src/types/messages/config.ts | 1 + .../opencode/src/kilocode/config/overlay.ts | 1 + packages/opencode/src/tool/registry.ts | 2 + .../test/kilocode/config/config.test.ts | 6 + .../kilocode/server/config-overlay.test.ts | 36 +++ packages/sdk/js/src/v2/gen/types.gen.ts | 1 + packages/sdk/openapi.json | 258 +++++++++++++++++- 13 files changed, 458 insertions(+), 37 deletions(-) create mode 100644 .changeset/enable-websearch-config.md diff --git a/.changeset/enable-websearch-config.md b/.changeset/enable-websearch-config.md new file mode 100644 index 00000000000..ebbc4135f3b --- /dev/null +++ b/.changeset/enable-websearch-config.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Configure web search for models from all providers through Kilo configuration, VS Code settings, and Kilo Console settings. diff --git a/packages/core/src/v1/config/config.ts b/packages/core/src/v1/config/config.ts index 97eb6bba353..b8ac33fef4a 100644 --- a/packages/core/src/v1/config/config.ts +++ b/packages/core/src/v1/config/config.ts @@ -278,6 +278,9 @@ export const Info = Schema.Struct({ batch_tool: Schema.optional(Schema.Boolean).annotate({ description: "Enable the batch tool" }), // kilocode_change start codebase_search: Schema.optional(Schema.Boolean).annotate({ description: "Enable AI-powered codebase search" }), + websearch: Schema.optional(Schema.Boolean).annotate({ + description: "Enable web search for all model providers", + }), image_generation: Schema.optional(Schema.Boolean).annotate({ description: "Enable AI image generation" }), image_generation_model: Schema.optional(Schema.String).annotate({ description: "Model ID to use for image generation (default: openrouter/auto)", diff --git a/packages/kilo-console/src/routes/config/ToolsRoute.tsx b/packages/kilo-console/src/routes/config/ToolsRoute.tsx index 1aca8a16b84..5688e41084b 100644 --- a/packages/kilo-console/src/routes/config/ToolsRoute.tsx +++ b/packages/kilo-console/src/routes/config/ToolsRoute.tsx @@ -1,14 +1,18 @@ import { createMemo, createSignal, For, Show } from "solid-js" import { ConfigRow, SectionTitle, StatusTag } from "@kilocode/kilo-web-ui/console" +import { Button } from "@kilocode/kilo-web-ui/button" +import { Card } from "@kilocode/kilo-web-ui/card" import { SearchField } from "../../components/SearchField" import { useConfig } from "../../context/config" import { toolCapabilities, toolName } from "../../shared/utils" -import { ConfigCountTag as CountTag, ConfigPage } from "./ConfigPage" +import { ConfigCountTag as CountTag, ConfigPage, ConfigTag as Tag, SourceBadge } from "./ConfigPage" export function ToolsRoute() { const ctx = useConfig() const [search, setSearch] = createSignal("") const snap = () => ctx.data() + const websearch = createMemo(() => snap()?.overlay.fields["experimental.websearch"]) + const searchEnabled = createMemo(() => websearch()?.value === true) const rows = createMemo(() => { const data = snap() if (!data) return [] @@ -52,6 +56,48 @@ export function ToolsRoute() { } description="Built-in tools available to agents, including file access, terminal execution, search, fetch, and orchestration tools." > + +
+
+

Web search

+

Control web search availability for models from providers that do not enable it by default.

+
+ + + +
+
+ +
+
+ = (props) => ( +

+ {props.title} +

+) + const BrowserTab: Component = () => { const { postMessage, onMessage } = useVSCode() const { t } = useLanguage() + const { config, updateConfig } = useConfig() const [settings, setSettings] = createSignal({ enabled: false, @@ -33,6 +51,10 @@ const BrowserTab: Component = () => { postMessage({ type: "updateSetting", key: `browserAutomation.${key}`, value }) } + const updateWebsearch = (checked: boolean) => { + updateConfig({ experimental: { ...config().experimental, websearch: checked } }) + } + return (
{/* Info text */} @@ -52,43 +74,78 @@ const BrowserTab: Component = () => { "line-height": "1.5", }} > - {t("settings.browser.description")} + {t("settings.webTools.description")}

- - {/* Enable toggle */} - - update("enabled", checked)} hideLabel> - {t("settings.browser.enable.title")} - - - - {/* Use System Chrome */} - - update("useSystemChrome", checked)} - hideLabel +
+
+ + - {t("settings.browser.systemChrome.title")} - - + + {t("settings.experimental.websearch.title")} + + + +
- {/* Headless mode */} - +
+

- update("headless", checked)} hideLabel> - {t("settings.browser.headless.title")} - - - + {t("settings.browser.description")} +

+ + {/* Enable toggle */} + + update("enabled", checked)} hideLabel> + {t("settings.browser.enable.title")} + + + + {/* Use System Chrome */} + + update("useSystemChrome", checked)} + hideLabel + > + {t("settings.browser.systemChrome.title")} + + + + {/* Headless mode */} + + update("headless", checked)} + hideLabel + > + {t("settings.browser.headless.title")} + + + + ) } diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx index 7e67d9df601..8bd39f14a44 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx @@ -177,9 +177,9 @@ const Settings: Component = (props) => { {language.t("settings.autoApprove.title")} - + - {language.t("settings.browser.title")} + {language.t("settings.webTools.title")} @@ -249,7 +249,7 @@ const Settings: Component = (props) => { -

{language.t("settings.browser.title")}

+

{language.t("settings.webTools.title")}

diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index e1a53df79ea..abe0350075a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1177,6 +1177,7 @@ export const dict = { "settings.agentBehaviour.title": "Agent Behaviour", "settings.autoApprove.title": "Auto-Approve", "settings.browser.title": "Browser", + "settings.webTools.title": "Web Tools", "settings.checkpoints.title": "Checkpoints", "settings.display.title": "Display", "settings.autocomplete.title": "Autocomplete", @@ -1349,6 +1350,10 @@ export const dict = { "settings.browser.description": "When enabled, the AI agent can interact with web pages — navigating, clicking, typing, and taking screenshots. A Chrome window will open so you can watch the agent work.", + "settings.webTools.description": + "Configure web search and browser automation. Search requests connect directly to Exa or Parallel.", + "settings.webTools.websearchEnable": "Enable for All Providers", + "settings.webTools.browserAutomation": "Browser Automation", "settings.browser.enable.title": "Enable Browser Automation", "settings.browser.enable.description": "Register the Playwright MCP server with the CLI backend.", "settings.browser.systemChrome.title": "Use System Chrome", @@ -1409,6 +1414,9 @@ export const dict = { "settings.experimental.lsp.description": "Enable language server protocol integration", "settings.experimental.batch.title": "Batch Tool", "settings.experimental.batch.description": "Enable batching of multiple tool calls", + "settings.experimental.websearch.title": "Web Search", + "settings.experimental.websearch.description": + "Make web search available to models from all providers. Searches connect directly to Exa or Parallel.", "settings.experimental.codebaseSearch.title": "Codebase Search", "settings.experimental.codebaseSearch.description": "Enable AI-powered natural language search across your codebase", "settings.experimental.imageGeneration.title": "Image Generation", diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts index c10943cecc4..0dc2d05dc71 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts @@ -39,6 +39,7 @@ export interface WatcherConfig { export interface ExperimentalConfig { batch_tool?: boolean + websearch?: boolean codebase_search?: boolean image_generation?: boolean image_generation_model?: string diff --git a/packages/opencode/src/kilocode/config/overlay.ts b/packages/opencode/src/kilocode/config/overlay.ts index 2d576c3bcea..b0cd42f88c3 100644 --- a/packages/opencode/src/kilocode/config/overlay.ts +++ b/packages/opencode/src/kilocode/config/overlay.ts @@ -88,6 +88,7 @@ export namespace KilocodeConfigOverlay { ["disabled_providers"], ["watcher", "ignore"], ["instructions"], + ["experimental", "websearch"], ["indexing", "enabled"], ["indexing", "provider"], ["indexing", "model"], diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index bab5ff3c895..f8eb66469ab 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -373,9 +373,11 @@ export const layer: Layer.Layer< }) const tools: Interface["tools"] = Effect.fn("ToolRegistry.tools")(function* (input) { + const cfg = yield* config.get() // kilocode_change const filtered = (yield* all()).filter((tool) => { if (!KiloToolRegistry.available(tool, input.agent)) return false // kilocode_change if (tool.id === WebSearchTool.id) { + if (cfg.experimental?.websearch === true) return true // kilocode_change return webSearchEnabled(input.providerID, { exa: flags.enableExa, parallel: flags.enableParallel }) } diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts index dde6a6d7078..50529c043ff 100644 --- a/packages/opencode/test/kilocode/config/config.test.ts +++ b/packages/opencode/test/kilocode/config/config.test.ts @@ -152,6 +152,12 @@ describe("global config updates", () => { }) describe("kilocode indexing config", () => { + test("accepts the websearch availability setting", () => { + const config = Schema.decodeUnknownSync(Config.Info)({ experimental: { websearch: true } }) + + expect(config.experimental?.websearch).toBe(true) + }) + test("ignores retired semantic indexing flags in existing configs", async () => { await using tmp = await tmpdir({ git: true }) await writeConfig(tmp.path, { diff --git a/packages/opencode/test/kilocode/server/config-overlay.test.ts b/packages/opencode/test/kilocode/server/config-overlay.test.ts index d3351b0d509..e8a55f9ac7f 100644 --- a/packages/opencode/test/kilocode/server/config-overlay.test.ts +++ b/packages/opencode/test/kilocode/server/config-overlay.test.ts @@ -201,6 +201,42 @@ describe("config overlay routes", () => { }) }) + test.serial("resolves and reverts project websearch overrides", async () => { + await using global = await tmpdir() + await using project = await tmpdir() + await setGlobal(global.path, { experimental: { websearch: true } }) + + await json( + await req(project.path, "/config/overlay", { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ scope: "project", set: { experimental: { websearch: false } } }), + }), + ) + const overridden = await json(await req(project.path, "/config/overlay?scope=project")) + expect(overridden.fields["experimental.websearch"]).toMatchObject({ + source: "project", + inherited: false, + overridden: true, + value: false, + }) + + await json( + await req(project.path, "/config/overlay", { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ scope: "project", unset: [["experimental", "websearch"]] }), + }), + ) + const inherited = await json(await req(project.path, "/config/overlay?scope=project")) + expect(inherited.fields["experimental.websearch"]).toMatchObject({ + source: "global", + inherited: true, + overridden: false, + value: true, + }) + }) + test.serial("marks global indexing values inherited in project scope", async () => { await using global = await tmpdir() await using project = await tmpdir() diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9bdf89a86c3..0bd15820a76 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1699,6 +1699,7 @@ export type Config = { disable_paste_summary?: boolean batch_tool?: boolean codebase_search?: boolean + websearch?: boolean image_generation?: boolean image_generation_model?: string agent_requirements?: boolean diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 1c1f5943741..7ea6d2387db 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -25850,8 +25850,7 @@ "items": { "type": "string", "pattern": "^\\s*\\.?[A-Za-z0-9][A-Za-z0-9_+-]*\\s*$" - }, - "minItems": 1 + } } }, "additionalProperties": false @@ -27001,6 +27000,9 @@ "codebase_search": { "type": "boolean" }, + "websearch": { + "type": "boolean" + }, "image_generation": { "type": "boolean" }, @@ -33543,6 +33545,52 @@ } ] }, + "added": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "removed": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, "skippedCount": { "anyOf": [ { @@ -33840,6 +33888,52 @@ } ] }, + "added": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "removed": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, "skippedCount": { "anyOf": [ { @@ -34137,6 +34231,52 @@ } ] }, + "added": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "removed": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, "skippedCount": { "anyOf": [ { @@ -41274,6 +41414,44 @@ } ] }, + "added": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "removed": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, "skippedCount": { "anyOf": [ { @@ -41535,6 +41713,44 @@ } ] }, + "added": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "removed": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, "skippedCount": { "anyOf": [ { @@ -41796,6 +42012,44 @@ } ] }, + "added": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "removed": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, "skippedCount": { "anyOf": [ { From 91d87588bf88af5b0e75780aab9637d23142dc76 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Tue, 21 Jul 2026 09:11:20 -0400 Subject: [PATCH 02/71] fix: address web search review feedback --- packages/core/src/v1/config/config.ts | 6 +-- .../src/routes/config/ToolsRoute.tsx | 6 +-- .../tests/settings-accessibility.spec.ts | 2 +- .../src/components/settings/BrowserTab.tsx | 12 ++--- .../kilo-vscode/webview-ui/src/i18n/ar.ts | 8 ++- .../kilo-vscode/webview-ui/src/i18n/br.ts | 9 +++- .../kilo-vscode/webview-ui/src/i18n/bs.ts | 9 +++- .../kilo-vscode/webview-ui/src/i18n/da.ts | 9 +++- .../kilo-vscode/webview-ui/src/i18n/de.ts | 9 +++- .../kilo-vscode/webview-ui/src/i18n/en.ts | 15 +++--- .../kilo-vscode/webview-ui/src/i18n/es.ts | 9 +++- .../kilo-vscode/webview-ui/src/i18n/fr.ts | 9 +++- .../kilo-vscode/webview-ui/src/i18n/it.ts | 9 +++- .../kilo-vscode/webview-ui/src/i18n/ja.ts | 9 +++- .../kilo-vscode/webview-ui/src/i18n/ko.ts | 9 +++- .../kilo-vscode/webview-ui/src/i18n/nl.ts | 9 +++- .../kilo-vscode/webview-ui/src/i18n/no.ts | 9 +++- .../kilo-vscode/webview-ui/src/i18n/pl.ts | 9 +++- .../kilo-vscode/webview-ui/src/i18n/ru.ts | 9 +++- .../kilo-vscode/webview-ui/src/i18n/th.ts | 9 +++- .../kilo-vscode/webview-ui/src/i18n/tr.ts | 9 +++- .../kilo-vscode/webview-ui/src/i18n/uk.ts | 9 +++- .../kilo-vscode/webview-ui/src/i18n/zh.ts | 7 ++- .../kilo-vscode/webview-ui/src/i18n/zht.ts | 7 ++- .../webview-ui/src/types/messages/config.ts | 2 +- .../opencode/src/kilocode/config/overlay.ts | 2 +- packages/opencode/src/tool/registry.ts | 2 +- .../test/kilocode/config/config.test.ts | 8 +-- .../kilocode/server/config-overlay.test.ts | 10 ++-- packages/opencode/test/tool/registry.test.ts | 50 ++++++++++++++++++- packages/sdk/js/src/v2/gen/types.gen.ts | 2 +- packages/sdk/openapi.json | 6 +-- 32 files changed, 233 insertions(+), 56 deletions(-) diff --git a/packages/core/src/v1/config/config.ts b/packages/core/src/v1/config/config.ts index b8ac33fef4a..bd9baab123d 100644 --- a/packages/core/src/v1/config/config.ts +++ b/packages/core/src/v1/config/config.ts @@ -226,6 +226,9 @@ export const Info = Schema.Struct({ layout: Schema.optional(ConfigLayoutV1.Layout).annotate({ description: "@deprecated Always uses stretch layout." }), permission: Schema.optional(ConfigPermissionV1.Info), tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)), + web_search: Schema.optional(Schema.Boolean).annotate({ + description: "Make web search available to models from all providers", + }), // kilocode_change attachment: Schema.optional(ConfigAttachmentV1.Info).annotate({ description: "Attachment processing configuration, including image size limits and resizing behavior", }), @@ -278,9 +281,6 @@ export const Info = Schema.Struct({ batch_tool: Schema.optional(Schema.Boolean).annotate({ description: "Enable the batch tool" }), // kilocode_change start codebase_search: Schema.optional(Schema.Boolean).annotate({ description: "Enable AI-powered codebase search" }), - websearch: Schema.optional(Schema.Boolean).annotate({ - description: "Enable web search for all model providers", - }), image_generation: Schema.optional(Schema.Boolean).annotate({ description: "Enable AI image generation" }), image_generation_model: Schema.optional(Schema.String).annotate({ description: "Model ID to use for image generation (default: openrouter/auto)", diff --git a/packages/kilo-console/src/routes/config/ToolsRoute.tsx b/packages/kilo-console/src/routes/config/ToolsRoute.tsx index 5688e41084b..9dcb20b6e05 100644 --- a/packages/kilo-console/src/routes/config/ToolsRoute.tsx +++ b/packages/kilo-console/src/routes/config/ToolsRoute.tsx @@ -11,7 +11,7 @@ export function ToolsRoute() { const ctx = useConfig() const [search, setSearch] = createSignal("") const snap = () => ctx.data() - const websearch = createMemo(() => snap()?.overlay.fields["experimental.websearch"]) + const websearch = createMemo(() => snap()?.overlay.fields.web_search) const searchEnabled = createMemo(() => websearch()?.value === true) const rows = createMemo(() => { const data = snap() @@ -66,7 +66,7 @@ export function ToolsRoute() { @@ -79,7 +79,7 @@ export function ToolsRoute() { type="button" aria-pressed={searchEnabled()} disabled={Boolean(ctx.saving()) || websearch()?.editable === false} - onClick={() => ctx.save({ experimental: { websearch: !searchEnabled() } })} + onClick={() => ctx.save({ web_search: !searchEnabled() })} > Enable for all providers diff --git a/packages/kilo-vscode/tests/settings-accessibility.spec.ts b/packages/kilo-vscode/tests/settings-accessibility.spec.ts index 03073ea4c5a..5e2b62082fb 100644 --- a/packages/kilo-vscode/tests/settings-accessibility.spec.ts +++ b/packages/kilo-vscode/tests/settings-accessibility.spec.ts @@ -6,7 +6,7 @@ const NAMES = [ "Providers", "Agent Behaviour", "Auto-Approve", - "Browser", + "Web Tools", "Checkpoints", "Display", "Autocomplete", diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx index 1422752ff51..fabc7858b8a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx @@ -52,7 +52,7 @@ const BrowserTab: Component = () => { } const updateWebsearch = (checked: boolean) => { - updateConfig({ experimental: { ...config().experimental, websearch: checked } }) + updateConfig({ web_search: checked }) } return ( @@ -79,15 +79,15 @@ const BrowserTab: Component = () => {
-
+
- - {t("settings.experimental.websearch.title")} + + {t("settings.webTools.webSearch.title")} diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index f86fe517b07..02fc76303d4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1203,7 +1203,13 @@ export const dict = { "settings.section.configuration": "الإعدادات", "settings.agentBehaviour.title": "سلوك الوكيل", "settings.autoApprove.title": "الموافقة التلقائية", - "settings.browser.title": "المتصفح", + "settings.webTools.title": "أدوات الويب", + "settings.webTools.description": "اضبط البحث على الويب وأتمتة المتصفح. تتصل طلبات البحث مباشرةً بـ Exa أو Parallel.", + "settings.webTools.webSearch.enable": "تمكين لجميع المزوّدين", + "settings.webTools.browserAutomation": "أتمتة المتصفح", + "settings.webTools.webSearch.title": "البحث على الويب", + "settings.webTools.webSearch.description": + "اجعل البحث على الويب متاحًا لنماذج جميع المزوّدين. تتصل عمليات البحث مباشرةً بـ Exa أو Parallel.", "settings.checkpoints.title": "نقاط التحقق", "settings.display.title": "العرض", "settings.autocomplete.title": "الإكمال التلقائي", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 35a1450ea1f..902114dd85c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1224,7 +1224,14 @@ export const dict = { "settings.section.configuration": "Configuração", "settings.agentBehaviour.title": "Comportamento do Agente", "settings.autoApprove.title": "Aprovação Automática", - "settings.browser.title": "Navegador", + "settings.webTools.title": "Ferramentas da Web", + "settings.webTools.description": + "Configure a pesquisa na web e a automação do navegador. As solicitações de pesquisa se conectam diretamente ao Exa ou Parallel.", + "settings.webTools.webSearch.enable": "Ativar para todos os provedores", + "settings.webTools.browserAutomation": "Automação do navegador", + "settings.webTools.webSearch.title": "Pesquisa na Web", + "settings.webTools.webSearch.description": + "Disponibilize a pesquisa na web para modelos de todos os provedores. As pesquisas se conectam diretamente ao Exa ou Parallel.", "settings.checkpoints.title": "Pontos de Verificação", "settings.display.title": "Exibição", "settings.autocomplete.title": "Autocompletar", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index c19e73262cd..dbb2e7b9230 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1269,7 +1269,14 @@ export const dict = { "settings.section.configuration": "Konfiguracija", "settings.agentBehaviour.title": "Ponašanje agenta", "settings.autoApprove.title": "Automatsko odobravanje", - "settings.browser.title": "Preglednik", + "settings.webTools.title": "Web alati", + "settings.webTools.description": + "Konfigurišite web pretragu i automatizaciju preglednika. Zahtjevi za pretragu povezuju se direktno s Exa ili Parallel.", + "settings.webTools.webSearch.enable": "Omogući za sve pružaoce", + "settings.webTools.browserAutomation": "Automatizacija preglednika", + "settings.webTools.webSearch.title": "Web pretraga", + "settings.webTools.webSearch.description": + "Omogućite web pretragu modelima svih pružalaca. Pretrage se povezuju direktno s Exa ili Parallel.", "settings.checkpoints.title": "Kontrolne tačke", "settings.display.title": "Prikaz", "settings.autocomplete.title": "Automatsko dovršavanje", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 8e9a593dd37..028595f8599 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1262,7 +1262,14 @@ export const dict = { "settings.section.configuration": "Konfiguration", "settings.agentBehaviour.title": "Agentadfærd", "settings.autoApprove.title": "Automatisk godkendelse", - "settings.browser.title": "Browser", + "settings.webTools.title": "Webværktøjer", + "settings.webTools.description": + "Konfigurer websøgning og browserautomatisering. Søgeanmodninger sendes direkte til Exa eller Parallel.", + "settings.webTools.webSearch.enable": "Aktivér for alle udbydere", + "settings.webTools.browserAutomation": "Browserautomatisering", + "settings.webTools.webSearch.title": "Websøgning", + "settings.webTools.webSearch.description": + "Gør websøgning tilgængelig for modeller fra alle udbydere. Søgninger sendes direkte til Exa eller Parallel.", "settings.checkpoints.title": "Kontrolpunkter", "settings.display.title": "Visning", "settings.autocomplete.title": "Autofuldførelse", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 380e7d3db41..4adcf7b72f8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1283,7 +1283,14 @@ export const dict = { "settings.section.configuration": "Konfiguration", "settings.agentBehaviour.title": "Agentenverhalten", "settings.autoApprove.title": "Automatisch genehmigen", - "settings.browser.title": "Browser", + "settings.webTools.title": "Web-Tools", + "settings.webTools.description": + "Konfigurieren Sie Websuche und Browserautomatisierung. Suchanfragen werden direkt an Exa oder Parallel gesendet.", + "settings.webTools.webSearch.enable": "Für alle Anbieter aktivieren", + "settings.webTools.browserAutomation": "Browserautomatisierung", + "settings.webTools.webSearch.title": "Websuche", + "settings.webTools.webSearch.description": + "Machen Sie die Websuche für Modelle aller Anbieter verfügbar. Suchanfragen werden direkt an Exa oder Parallel gesendet.", "settings.checkpoints.title": "Prüfpunkte", "settings.display.title": "Anzeige", "settings.autocomplete.title": "Autovervollständigung", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index abe0350075a..e926699acc2 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1176,8 +1176,14 @@ export const dict = { "settings.section.configuration": "Configuration", "settings.agentBehaviour.title": "Agent Behaviour", "settings.autoApprove.title": "Auto-Approve", - "settings.browser.title": "Browser", "settings.webTools.title": "Web Tools", + "settings.webTools.description": + "Configure web search and browser automation. Search requests connect directly to Exa or Parallel.", + "settings.webTools.webSearch.enable": "Enable for All Providers", + "settings.webTools.browserAutomation": "Browser Automation", + "settings.webTools.webSearch.title": "Web Search", + "settings.webTools.webSearch.description": + "Make web search available to models from all providers. Searches connect directly to Exa or Parallel.", "settings.checkpoints.title": "Checkpoints", "settings.display.title": "Display", "settings.autocomplete.title": "Autocomplete", @@ -1350,10 +1356,6 @@ export const dict = { "settings.browser.description": "When enabled, the AI agent can interact with web pages — navigating, clicking, typing, and taking screenshots. A Chrome window will open so you can watch the agent work.", - "settings.webTools.description": - "Configure web search and browser automation. Search requests connect directly to Exa or Parallel.", - "settings.webTools.websearchEnable": "Enable for All Providers", - "settings.webTools.browserAutomation": "Browser Automation", "settings.browser.enable.title": "Enable Browser Automation", "settings.browser.enable.description": "Register the Playwright MCP server with the CLI backend.", "settings.browser.systemChrome.title": "Use System Chrome", @@ -1414,9 +1416,6 @@ export const dict = { "settings.experimental.lsp.description": "Enable language server protocol integration", "settings.experimental.batch.title": "Batch Tool", "settings.experimental.batch.description": "Enable batching of multiple tool calls", - "settings.experimental.websearch.title": "Web Search", - "settings.experimental.websearch.description": - "Make web search available to models from all providers. Searches connect directly to Exa or Parallel.", "settings.experimental.codebaseSearch.title": "Codebase Search", "settings.experimental.codebaseSearch.description": "Enable AI-powered natural language search across your codebase", "settings.experimental.imageGeneration.title": "Image Generation", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index e1cd4296143..d66f4bd86f6 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1276,7 +1276,14 @@ export const dict = { "settings.section.configuration": "Configuración", "settings.agentBehaviour.title": "Comportamiento del agente", "settings.autoApprove.title": "Aprobación automática", - "settings.browser.title": "Navegador", + "settings.webTools.title": "Herramientas web", + "settings.webTools.description": + "Configura la búsqueda web y la automatización del navegador. Las solicitudes de búsqueda se conectan directamente a Exa o Parallel.", + "settings.webTools.webSearch.enable": "Habilitar para todos los proveedores", + "settings.webTools.browserAutomation": "Automatización del navegador", + "settings.webTools.webSearch.title": "Búsqueda web", + "settings.webTools.webSearch.description": + "Permite que los modelos de todos los proveedores usen la búsqueda web. Las búsquedas se conectan directamente a Exa o Parallel.", "settings.checkpoints.title": "Puntos de control", "settings.display.title": "Pantalla", "settings.autocomplete.title": "Autocompletado", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 71055c0d653..1ed7c73e8bd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1287,7 +1287,14 @@ export const dict = { "settings.section.configuration": "Configuration", "settings.agentBehaviour.title": "Comportement de l'agent", "settings.autoApprove.title": "Approbation automatique", - "settings.browser.title": "Navigateur", + "settings.webTools.title": "Outils web", + "settings.webTools.description": + "Configurez la recherche web et l’automatisation du navigateur. Les requêtes de recherche se connectent directement à Exa ou Parallel.", + "settings.webTools.webSearch.enable": "Activer pour tous les fournisseurs", + "settings.webTools.browserAutomation": "Automatisation du navigateur", + "settings.webTools.webSearch.title": "Recherche web", + "settings.webTools.webSearch.description": + "Rendez la recherche web disponible pour les modèles de tous les fournisseurs. Les recherches se connectent directement à Exa ou Parallel.", "settings.checkpoints.title": "Points de contrôle", "settings.display.title": "Affichage", "settings.autocomplete.title": "Autocomplétion", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 034276bb9c4..b0c9a5f7038 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -1023,7 +1023,14 @@ export const dict = { "settings.section.configuration": "Configurazione", "settings.agentBehaviour.title": "Comportamento agente", "settings.autoApprove.title": "Approvazione automatica", - "settings.browser.title": "Browser", + "settings.webTools.title": "Strumenti web", + "settings.webTools.description": + "Configura la ricerca web e l'automazione del browser. Le richieste di ricerca si connettono direttamente a Exa o Parallel.", + "settings.webTools.webSearch.enable": "Abilita per tutti i provider", + "settings.webTools.browserAutomation": "Automazione del browser", + "settings.webTools.webSearch.title": "Ricerca web", + "settings.webTools.webSearch.description": + "Rendi disponibile la ricerca web ai modelli di tutti i provider. Le ricerche si connettono direttamente a Exa o Parallel.", "settings.checkpoints.title": "Checkpoint", "settings.display.title": "Visualizzazione", "settings.autocomplete.title": "Autocompletamento", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 2c6bd6c9e3c..fd60365ca9f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1257,7 +1257,14 @@ export const dict = { "settings.section.configuration": "設定", "settings.agentBehaviour.title": "エージェントの動作", "settings.autoApprove.title": "自動承認", - "settings.browser.title": "ブラウザ", + "settings.webTools.title": "ウェブツール", + "settings.webTools.description": + "ウェブ検索とブラウザ自動化を設定します。検索リクエストは Exa または Parallel に直接接続されます。", + "settings.webTools.webSearch.enable": "すべてのプロバイダーで有効化", + "settings.webTools.browserAutomation": "ブラウザ自動化", + "settings.webTools.webSearch.title": "ウェブ検索", + "settings.webTools.webSearch.description": + "すべてのプロバイダーのモデルでウェブ検索を利用できるようにします。検索は Exa または Parallel に直接接続されます。", "settings.checkpoints.title": "チェックポイント", "settings.display.title": "表示", "settings.autocomplete.title": "オートコンプリート", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 990edeb5eb3..9dd03144c25 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1210,7 +1210,14 @@ export const dict = { "settings.section.configuration": "구성", "settings.agentBehaviour.title": "에이전트 동작", "settings.autoApprove.title": "자동 승인", - "settings.browser.title": "브라우저", + "settings.webTools.title": "웹 도구", + "settings.webTools.description": + "웹 검색 및 브라우저 자동화를 구성합니다. 검색 요청은 Exa 또는 Parallel에 직접 연결됩니다.", + "settings.webTools.webSearch.enable": "모든 제공업체에 사용", + "settings.webTools.browserAutomation": "브라우저 자동화", + "settings.webTools.webSearch.title": "웹 검색", + "settings.webTools.webSearch.description": + "모든 제공업체의 모델에서 웹 검색을 사용할 수 있도록 합니다. 검색은 Exa 또는 Parallel에 직접 연결됩니다.", "settings.checkpoints.title": "체크포인트", "settings.display.title": "디스플레이", "settings.autocomplete.title": "자동 완성", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 75d20e4f178..8158160380a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1218,7 +1218,14 @@ export const dict = { "settings.section.configuration": "Configuratie", "settings.agentBehaviour.title": "Agent Gedrag", "settings.autoApprove.title": "Automatisch Goedkeuren", - "settings.browser.title": "Browser", + "settings.webTools.title": "Webtools", + "settings.webTools.description": + "Configureer zoeken op internet en browserautomatisering. Zoekopdrachten maken rechtstreeks verbinding met Exa of Parallel.", + "settings.webTools.webSearch.enable": "Inschakelen voor alle providers", + "settings.webTools.browserAutomation": "Browserautomatisering", + "settings.webTools.webSearch.title": "Zoeken op internet", + "settings.webTools.webSearch.description": + "Maak zoeken op internet beschikbaar voor modellen van alle providers. Zoekopdrachten maken rechtstreeks verbinding met Exa of Parallel.", "settings.checkpoints.title": "Controlepunten", "settings.display.title": "Weergave", "settings.autocomplete.title": "Automatisch Aanvullen", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 68c4af800b8..5e8b9faf1f4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1222,7 +1222,14 @@ export const dict = { "settings.section.configuration": "Konfigurasjon", "settings.agentBehaviour.title": "Agentoppførsel", "settings.autoApprove.title": "Automatisk godkjenning", - "settings.browser.title": "Nettleser", + "settings.webTools.title": "Nettverktøy", + "settings.webTools.description": + "Konfigurer nettsøk og nettleserautomatisering. Søk sendes direkte til Exa eller Parallel.", + "settings.webTools.webSearch.enable": "Aktiver for alle leverandører", + "settings.webTools.browserAutomation": "Nettleserautomatisering", + "settings.webTools.webSearch.title": "Nettsøk", + "settings.webTools.webSearch.description": + "Gjør nettsøk tilgjengelig for modeller fra alle leverandører. Søk sendes direkte til Exa eller Parallel.", "settings.checkpoints.title": "Kontrollpunkter", "settings.display.title": "Visning", "settings.autocomplete.title": "Autofullfør", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index f7238a444cb..aa920ac5db3 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1221,7 +1221,14 @@ export const dict = { "settings.section.configuration": "Konfiguracja", "settings.agentBehaviour.title": "Zachowanie agenta", "settings.autoApprove.title": "Automatyczne zatwierdzanie", - "settings.browser.title": "Przeglądarka", + "settings.webTools.title": "Narzędzia internetowe", + "settings.webTools.description": + "Skonfiguruj wyszukiwanie w sieci i automatyzację przeglądarki. Żądania wyszukiwania łączą się bezpośrednio z Exa lub Parallel.", + "settings.webTools.webSearch.enable": "Włącz dla wszystkich dostawców", + "settings.webTools.browserAutomation": "Automatyzacja przeglądarki", + "settings.webTools.webSearch.title": "Wyszukiwanie w sieci", + "settings.webTools.webSearch.description": + "Udostępnij wyszukiwanie w sieci modelom wszystkich dostawców. Wyszukiwania łączą się bezpośrednio z Exa lub Parallel.", "settings.checkpoints.title": "Punkty kontrolne", "settings.display.title": "Wyświetlanie", "settings.autocomplete.title": "Autouzupełnianie", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 47a24933c3a..68027826b86 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1268,7 +1268,14 @@ export const dict = { "settings.section.configuration": "Конфигурация", "settings.agentBehaviour.title": "Поведение агента", "settings.autoApprove.title": "Автоодобрение", - "settings.browser.title": "Браузер", + "settings.webTools.title": "Веб-инструменты", + "settings.webTools.description": + "Настройте веб-поиск и автоматизацию браузера. Поисковые запросы отправляются напрямую в Exa или Parallel.", + "settings.webTools.webSearch.enable": "Включить для всех провайдеров", + "settings.webTools.browserAutomation": "Автоматизация браузера", + "settings.webTools.webSearch.title": "Веб-поиск", + "settings.webTools.webSearch.description": + "Сделайте веб-поиск доступным для моделей всех провайдеров. Поисковые запросы отправляются напрямую в Exa или Parallel.", "settings.checkpoints.title": "Контрольные точки", "settings.display.title": "Отображение", "settings.autocomplete.title": "Автодополнение", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index cac0eea7a9b..c029e495b63 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1250,7 +1250,14 @@ export const dict = { "settings.section.configuration": "การกำหนดค่า", "settings.agentBehaviour.title": "พฤติกรรมของเอเจนต์", "settings.autoApprove.title": "อนุมัติอัตโนมัติ", - "settings.browser.title": "เบราว์เซอร์", + "settings.webTools.title": "เครื่องมือเว็บ", + "settings.webTools.description": + "กำหนดค่าการค้นหาเว็บและระบบอัตโนมัติของเบราว์เซอร์ คำขอค้นหาจะเชื่อมต่อโดยตรงกับ Exa หรือ Parallel", + "settings.webTools.webSearch.enable": "เปิดใช้สำหรับผู้ให้บริการทั้งหมด", + "settings.webTools.browserAutomation": "ระบบอัตโนมัติของเบราว์เซอร์", + "settings.webTools.webSearch.title": "ค้นหาเว็บ", + "settings.webTools.webSearch.description": + "ทำให้โมเดลจากผู้ให้บริการทั้งหมดใช้การค้นหาเว็บได้ การค้นหาจะเชื่อมต่อโดยตรงกับ Exa หรือ Parallel", "settings.checkpoints.title": "จุดตรวจสอบ", "settings.display.title": "การแสดงผล", "settings.autocomplete.title": "เติมข้อความอัตโนมัติ", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index e799d071417..a43c79880d2 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1215,7 +1215,14 @@ export const dict = { "settings.section.configuration": "Yapılandırma", "settings.agentBehaviour.title": "Ajan Davranışı", "settings.autoApprove.title": "Otomatik Onay", - "settings.browser.title": "Tarayıcı", + "settings.webTools.title": "Web Araçları", + "settings.webTools.description": + "Web aramasını ve tarayıcı otomasyonunu yapılandırın. Arama istekleri doğrudan Exa veya Parallel'e bağlanır.", + "settings.webTools.webSearch.enable": "Tüm Sağlayıcılar İçin Etkinleştir", + "settings.webTools.browserAutomation": "Tarayıcı Otomasyonu", + "settings.webTools.webSearch.title": "Web Araması", + "settings.webTools.webSearch.description": + "Web aramasını tüm sağlayıcıların modelleri için kullanılabilir hale getirin. Aramalar doğrudan Exa veya Parallel'e bağlanır.", "settings.checkpoints.title": "Kontrol Noktaları", "settings.display.title": "Görünüm", "settings.autocomplete.title": "Otomatik Tamamlama", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index dfc15c248db..75783bc9e42 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1213,7 +1213,14 @@ export const dict = { "settings.section.configuration": "Конфігурація", "settings.agentBehaviour.title": "Поведінка агента", "settings.autoApprove.title": "Автоматичне схвалення", - "settings.browser.title": "Браузер", + "settings.webTools.title": "Вебінструменти", + "settings.webTools.description": + "Налаштуйте вебпошук і автоматизацію браузера. Пошукові запити надсилаються безпосередньо до Exa або Parallel.", + "settings.webTools.webSearch.enable": "Увімкнути для всіх постачальників", + "settings.webTools.browserAutomation": "Автоматизація браузера", + "settings.webTools.webSearch.title": "Вебпошук", + "settings.webTools.webSearch.description": + "Зробіть вебпошук доступним для моделей усіх постачальників. Пошукові запити надсилаються безпосередньо до Exa або Parallel.", "settings.checkpoints.title": "Контрольні точки", "settings.display.title": "Відображення", "settings.autocomplete.title": "Автодоповнення", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 8432bbee901..9e1dd057295 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1229,7 +1229,12 @@ export const dict = { "settings.section.configuration": "配置", "settings.agentBehaviour.title": "智能体行为", "settings.autoApprove.title": "自动审批", - "settings.browser.title": "浏览器", + "settings.webTools.title": "网络工具", + "settings.webTools.description": "配置网页搜索和浏览器自动化。搜索请求会直接连接到 Exa 或 Parallel。", + "settings.webTools.webSearch.enable": "为所有提供商启用", + "settings.webTools.browserAutomation": "浏览器自动化", + "settings.webTools.webSearch.title": "网页搜索", + "settings.webTools.webSearch.description": "让所有提供商的模型都可使用网页搜索。搜索会直接连接到 Exa 或 Parallel。", "settings.checkpoints.title": "检查点", "settings.display.title": "显示", "settings.autocomplete.title": "自动补全", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index b69bb7054c1..aed2a971794 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1190,7 +1190,12 @@ export const dict = { "settings.section.configuration": "設定", "settings.agentBehaviour.title": "Agent 行為", "settings.autoApprove.title": "自動核准", - "settings.browser.title": "瀏覽器", + "settings.webTools.title": "網路工具", + "settings.webTools.description": "設定網頁搜尋和瀏覽器自動化。搜尋請求會直接連線至 Exa 或 Parallel。", + "settings.webTools.webSearch.enable": "為所有供應商啟用", + "settings.webTools.browserAutomation": "瀏覽器自動化", + "settings.webTools.webSearch.title": "網頁搜尋", + "settings.webTools.webSearch.description": "讓所有供應商的模型都可使用網頁搜尋。搜尋會直接連線至 Exa 或 Parallel。", "settings.checkpoints.title": "檢查點", "settings.display.title": "顯示", "settings.autocomplete.title": "自動完成", diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts index 0dc2d05dc71..89dcc0af832 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts @@ -39,7 +39,6 @@ export interface WatcherConfig { export interface ExperimentalConfig { batch_tool?: boolean - websearch?: boolean codebase_search?: boolean image_generation?: boolean image_generation_model?: string @@ -155,6 +154,7 @@ export interface Config { compaction?: CompactionConfig commit_message?: CommitMessageConfig tools?: Record + web_search?: boolean auto_collapse_reasoning?: boolean experimental?: ExperimentalConfig sandbox?: SandboxConfig diff --git a/packages/opencode/src/kilocode/config/overlay.ts b/packages/opencode/src/kilocode/config/overlay.ts index b0cd42f88c3..ec806bf053f 100644 --- a/packages/opencode/src/kilocode/config/overlay.ts +++ b/packages/opencode/src/kilocode/config/overlay.ts @@ -88,7 +88,7 @@ export namespace KilocodeConfigOverlay { ["disabled_providers"], ["watcher", "ignore"], ["instructions"], - ["experimental", "websearch"], + ["web_search"], ["indexing", "enabled"], ["indexing", "provider"], ["indexing", "model"], diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index f8eb66469ab..4df567dd6bf 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -377,7 +377,7 @@ export const layer: Layer.Layer< const filtered = (yield* all()).filter((tool) => { if (!KiloToolRegistry.available(tool, input.agent)) return false // kilocode_change if (tool.id === WebSearchTool.id) { - if (cfg.experimental?.websearch === true) return true // kilocode_change + if (cfg.web_search === true) return true // kilocode_change return webSearchEnabled(input.providerID, { exa: flags.enableExa, parallel: flags.enableParallel }) } diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts index 50529c043ff..51902e15142 100644 --- a/packages/opencode/test/kilocode/config/config.test.ts +++ b/packages/opencode/test/kilocode/config/config.test.ts @@ -151,13 +151,15 @@ describe("global config updates", () => { }) }) -describe("kilocode indexing config", () => { +describe("kilocode web search config", () => { test("accepts the websearch availability setting", () => { - const config = Schema.decodeUnknownSync(Config.Info)({ experimental: { websearch: true } }) + const config = Schema.decodeUnknownSync(Config.Info)({ web_search: true }) - expect(config.experimental?.websearch).toBe(true) + expect(config.web_search).toBe(true) }) +}) +describe("kilocode indexing config", () => { test("ignores retired semantic indexing flags in existing configs", async () => { await using tmp = await tmpdir({ git: true }) await writeConfig(tmp.path, { diff --git a/packages/opencode/test/kilocode/server/config-overlay.test.ts b/packages/opencode/test/kilocode/server/config-overlay.test.ts index e8a55f9ac7f..a21deb1e5db 100644 --- a/packages/opencode/test/kilocode/server/config-overlay.test.ts +++ b/packages/opencode/test/kilocode/server/config-overlay.test.ts @@ -204,17 +204,17 @@ describe("config overlay routes", () => { test.serial("resolves and reverts project websearch overrides", async () => { await using global = await tmpdir() await using project = await tmpdir() - await setGlobal(global.path, { experimental: { websearch: true } }) + await setGlobal(global.path, { web_search: true }) await json( await req(project.path, "/config/overlay", { method: "PATCH", headers: { "content-type": "application/json" }, - body: JSON.stringify({ scope: "project", set: { experimental: { websearch: false } } }), + body: JSON.stringify({ scope: "project", set: { web_search: false } }), }), ) const overridden = await json(await req(project.path, "/config/overlay?scope=project")) - expect(overridden.fields["experimental.websearch"]).toMatchObject({ + expect(overridden.fields.web_search).toMatchObject({ source: "project", inherited: false, overridden: true, @@ -225,11 +225,11 @@ describe("config overlay routes", () => { await req(project.path, "/config/overlay", { method: "PATCH", headers: { "content-type": "application/json" }, - body: JSON.stringify({ scope: "project", unset: [["experimental", "websearch"]] }), + body: JSON.stringify({ scope: "project", unset: [["web_search"]] }), }), ) const inherited = await json(await req(project.path, "/config/overlay?scope=project")) - expect(inherited.fields["experimental.websearch"]).toMatchObject({ + expect(inherited.fields.web_search).toMatchObject({ source: "global", inherited: true, overridden: false, diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index 393bea6f25e..97e4e6c51c1 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -52,12 +52,13 @@ const configLayer = TestConfig.layer({ type RegistryLayerOptions = { flags?: Partial plugin?: Layer.Layer + config?: Parameters[0] // kilocode_change } const registryLayer = (opts: RegistryLayerOptions = {}) => ToolRegistry.layer .pipe( - Layer.provide(configLayer), + Layer.provide(opts.config ? TestConfig.layer(opts.config) : configLayer), // kilocode_change Layer.provide(opts.plugin ?? Plugin.defaultLayer), Layer.provide(Question.defaultLayer), Layer.provide(Todo.defaultLayer), @@ -118,6 +119,21 @@ const withBrokenPlugin = testEffect( Layer.mergeAll(registryLayer({ plugin: brokenPluginLayer }), node, Agent.defaultLayer), ) // kilocode_change start +const websearch = testEffect( + Layer.mergeAll( + registryLayer({ + config: { + get: () => + Effect.succeed({ + web_search: true, + provider: { openai: { options: { apiKey: "test-openai-key" } } }, + }), + }, + }), + node, + Agent.defaultLayer, + ), +) const sandboxed = testEffect( Layer.mergeAll(registryLayer({ flags: { experimentalLspTool: true } }), node, Agent.defaultLayer), ) @@ -139,6 +155,38 @@ function sandboxProfile(): Profile { describe("tool.registry", () => { // kilocode_change start + it.instance("hides websearch for a third-party provider by default", () => + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const agent = yield* Agent.Service + const build = yield* agent.get("build") + if (!build) return yield* Effect.die(new Error("build agent not found")) + const tools = yield* registry.tools({ + providerID: ProviderV2.ID.openai, + modelID: ModelV2.ID.make("test"), + agent: build, + }) + + expect(tools.map((tool) => tool.id)).not.toContain("websearch") + }), + ) + + websearch.instance("shows websearch for a configured third-party provider when enabled", () => + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const agent = yield* Agent.Service + const build = yield* agent.get("build") + if (!build) return yield* Effect.die(new Error("build agent not found")) + const tools = yield* registry.tools({ + providerID: ProviderV2.ID.openai, + modelID: ModelV2.ID.make("test"), + agent: build, + }) + + expect(tools.map((tool) => tool.id)).toContain("websearch") + }), + ) + sandboxed.instance("preserves built-in network classification through production tool definition processing", () => Effect.gen(function* () { const registry = yield* ToolRegistry.Service diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 0bd15820a76..5f9963def06 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1673,6 +1673,7 @@ export type Config = { tools?: { [key: string]: boolean } + web_search?: boolean attachment?: AttachmentConfig enterprise?: { url?: string @@ -1699,7 +1700,6 @@ export type Config = { disable_paste_summary?: boolean batch_tool?: boolean codebase_search?: boolean - websearch?: boolean image_generation?: boolean image_generation_model?: string agent_requirements?: boolean diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 7ea6d2387db..1451c3a6b3f 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -26905,6 +26905,9 @@ "type": "boolean" } }, + "web_search": { + "type": "boolean" + }, "attachment": { "$ref": "#/components/schemas/AttachmentConfig" }, @@ -27000,9 +27003,6 @@ "codebase_search": { "type": "boolean" }, - "websearch": { - "type": "boolean" - }, "image_generation": { "type": "boolean" }, From 25dcfb6cff23abe840f3a728ebe48deef6af9355 Mon Sep 17 00:00:00 2001 From: Kelly Sun Date: Thu, 23 Jul 2026 14:28:32 -0400 Subject: [PATCH 03/71] docs: add Mixlayer provider page Adds an AI-providers docs page for Mixlayer (OpenAI-compatible inference for open models like GLM and Qwen) and a nav entry, documenting the OpenAI-Compatible setup for both VS Code and the CLI. --- packages/kilo-docs/lib/nav/ai-providers.ts | 1 + .../kilo-docs/pages/ai-providers/mixlayer.md | 97 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 packages/kilo-docs/pages/ai-providers/mixlayer.md diff --git a/packages/kilo-docs/lib/nav/ai-providers.ts b/packages/kilo-docs/lib/nav/ai-providers.ts index f604826c7cb..1a2dfe9ae35 100644 --- a/packages/kilo-docs/lib/nav/ai-providers.ts +++ b/packages/kilo-docs/lib/nav/ai-providers.ts @@ -46,6 +46,7 @@ export const AiProvidersNav: NavSection[] = [ { href: "/ai-providers/groq", children: "Groq" }, { href: "/ai-providers/cerebras", children: "Cerebras" }, { href: "/ai-providers/fireworks", children: "Fireworks AI" }, + { href: "/ai-providers/mixlayer", children: "Mixlayer" }, ], }, { diff --git a/packages/kilo-docs/pages/ai-providers/mixlayer.md b/packages/kilo-docs/pages/ai-providers/mixlayer.md new file mode 100644 index 00000000000..ac0221315a6 --- /dev/null +++ b/packages/kilo-docs/pages/ai-providers/mixlayer.md @@ -0,0 +1,97 @@ +--- +title: "Using Mixlayer with Kilo Code | Fast Open-Model Inference" +description: "Run open models like GLM and Qwen on Mixlayer's OpenAI-compatible API in Kilo Code. Setup guide for VS Code and the CLI." +--- + +# Using Mixlayer With Kilo Code + +Mixlayer is an inference platform for open models such as GLM and Qwen, with a serving stack built from scratch by core contributors to Candle. It exposes an OpenAI-compatible API, so you can use it in Kilo Code through the **OpenAI Compatible** provider. + +**Website:** [https://mixlayer.com/](https://mixlayer.com/) + +## Getting an API Key + +1. **Sign Up/Sign In:** Go to [Mixlayer](https://mixlayer.com/) and create an account or sign in. +2. **Navigate to API Keys:** Open the [Mixlayer console](https://console.mixlayer.com/) and go to the API Keys page. +3. **Create a Key:** Click **New Key**, give it a descriptive name (e.g., "Kilo Code"), and copy it. You will not be able to view it again. + +## Configuration in Kilo Code + +Mixlayer's API is OpenAI-compatible, with the base URL `https://models.mixlayer.ai/v1`. Configure it through Kilo Code's **OpenAI Compatible** provider. + +{% tabs %} +{% tab label="VSCode" %} + +1. Open **Settings** (gear icon) and go to the **Providers** tab. +2. Scroll to the bottom and click **Custom provider**. +3. Fill in the dialog: + - **Provider ID** — `mixlayer` + - **Display name** — `Mixlayer` + - **Provider API** — **OpenAI Compatible** + - **Base URL** — `https://models.mixlayer.ai/v1` + - **API key** — your Mixlayer API key +4. Kilo Code auto-fetches the available models from Mixlayer's `/v1/models` endpoint, so you can pick a model directly from the list. Click **Submit** to save. + +{% /tab %} +{% tab label="CLI" %} + +Set the API key as an environment variable and define an OpenAI-compatible provider in your `kilo.json` config file (`~/.config/kilo/kilo.json` or `./kilo.json`): + +**Environment variable:** + +```bash +export MIXLAYER_API_KEY="your-api-key" +``` + +**Config file:** + +```jsonc +{ + "provider": { + "mixlayer": { + "npm": "@ai-sdk/openai-compatible", + "env": ["MIXLAYER_API_KEY"], + "options": { + "baseURL": "https://models.mixlayer.ai/v1", + }, + "models": { + "z-ai/glm-5.2": { + "name": "GLM-5.2", + "limit": { "context": 262144, "output": 262144 }, + }, + "qwen/qwen3.5-397b-a17b": { + "name": "Qwen3.5 397B A17B", + "limit": { "context": 131072, "output": 131072 }, + }, + }, + }, + }, +} +``` + +Then set your default model using the `provider-id/model-id` format: + +```jsonc +{ + "model": "mixlayer/z-ai/glm-5.2", +} +``` + +{% /tab %} +{% /tabs %} + +## Models + +Mixlayer serves open models including: + +- `z-ai/glm-5.2` — 256K context +- `qwen/qwen3.5-397b-a17b` and the Qwen 3.5 / 3.6 line (vision-capable) +- `moonshotai/kimi-k2.7-code` + +Tool calling and reasoning are supported across the model line. See the [Mixlayer docs](https://docs.mixlayer.com) for the full, current model list and supported parameters. + +## Tips and Notes + +- **Model list:** Kilo Code auto-detects available models from Mixlayer's `/v1/models` endpoint, so the picker stays current with your account. +- **Pricing:** See the [Mixlayer console](https://console.mixlayer.com/) for current per-model pricing. +- **Reasoning:** Qwen models support a thinking mode; reasoning tokens count against the output budget, so allow enough `limit.output` when reasoning is enabled. From 34e787e5ac039b0d070ed186ee5979aba38c8576 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Mon, 27 Jul 2026 11:48:09 -0400 Subject: [PATCH 04/71] fix: enable web search by default --- packages/core/src/v1/config/config.ts | 3 +- .../src/routes/config/ToolsRoute.tsx | 2 +- .../src/components/settings/BrowserTab.tsx | 2 +- packages/opencode/src/tool/registry.ts | 2 +- .../test/kilocode/config/config.test.ts | 6 +-- packages/opencode/test/tool/registry.test.ts | 50 ++++++++++++------- 6 files changed, 40 insertions(+), 25 deletions(-) diff --git a/packages/core/src/v1/config/config.ts b/packages/core/src/v1/config/config.ts index bd9baab123d..0ef9d1ffc71 100644 --- a/packages/core/src/v1/config/config.ts +++ b/packages/core/src/v1/config/config.ts @@ -227,7 +227,8 @@ export const Info = Schema.Struct({ permission: Schema.optional(ConfigPermissionV1.Info), tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)), web_search: Schema.optional(Schema.Boolean).annotate({ - description: "Make web search available to models from all providers", + description: + "Make web search available to models from all providers (default: true). Set to false to limit it to managed providers.", }), // kilocode_change attachment: Schema.optional(ConfigAttachmentV1.Info).annotate({ description: "Attachment processing configuration, including image size limits and resizing behavior", diff --git a/packages/kilo-console/src/routes/config/ToolsRoute.tsx b/packages/kilo-console/src/routes/config/ToolsRoute.tsx index 9dcb20b6e05..fae54f6b8cb 100644 --- a/packages/kilo-console/src/routes/config/ToolsRoute.tsx +++ b/packages/kilo-console/src/routes/config/ToolsRoute.tsx @@ -12,7 +12,7 @@ export function ToolsRoute() { const [search, setSearch] = createSignal("") const snap = () => ctx.data() const websearch = createMemo(() => snap()?.overlay.fields.web_search) - const searchEnabled = createMemo(() => websearch()?.value === true) + const searchEnabled = createMemo(() => websearch()?.value !== false) const rows = createMemo(() => { const data = snap() if (!data) return [] diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx index fabc7858b8a..ca870fef21a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx @@ -86,7 +86,7 @@ const BrowserTab: Component = () => { description={t("settings.webTools.webSearch.description")} last > - + {t("settings.webTools.webSearch.title")} diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 4df567dd6bf..4f7c275cfa5 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -377,7 +377,7 @@ export const layer: Layer.Layer< const filtered = (yield* all()).filter((tool) => { if (!KiloToolRegistry.available(tool, input.agent)) return false // kilocode_change if (tool.id === WebSearchTool.id) { - if (cfg.web_search === true) return true // kilocode_change + if (cfg.web_search !== false) return true // kilocode_change return webSearchEnabled(input.providerID, { exa: flags.enableExa, parallel: flags.enableParallel }) } diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts index 51902e15142..39389503067 100644 --- a/packages/opencode/test/kilocode/config/config.test.ts +++ b/packages/opencode/test/kilocode/config/config.test.ts @@ -152,10 +152,10 @@ describe("global config updates", () => { }) describe("kilocode web search config", () => { - test("accepts the websearch availability setting", () => { - const config = Schema.decodeUnknownSync(Config.Info)({ web_search: true }) + test("accepts explicitly limiting web search to managed providers", () => { + const config = Schema.decodeUnknownSync(Config.Info)({ web_search: false }) - expect(config.web_search).toBe(true) + expect(config.web_search).toBe(false) }) }) diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index 97e4e6c51c1..2420506b9de 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -125,7 +125,21 @@ const websearch = testEffect( config: { get: () => Effect.succeed({ - web_search: true, + provider: { openai: { options: { apiKey: "test-openai-key" } } }, + }), + }, + }), + node, + Agent.defaultLayer, + ), +) +const websearchOff = testEffect( + Layer.mergeAll( + registryLayer({ + config: { + get: () => + Effect.succeed({ + web_search: false, provider: { openai: { options: { apiKey: "test-openai-key" } } }, }), }, @@ -155,23 +169,7 @@ function sandboxProfile(): Profile { describe("tool.registry", () => { // kilocode_change start - it.instance("hides websearch for a third-party provider by default", () => - Effect.gen(function* () { - const registry = yield* ToolRegistry.Service - const agent = yield* Agent.Service - const build = yield* agent.get("build") - if (!build) return yield* Effect.die(new Error("build agent not found")) - const tools = yield* registry.tools({ - providerID: ProviderV2.ID.openai, - modelID: ModelV2.ID.make("test"), - agent: build, - }) - - expect(tools.map((tool) => tool.id)).not.toContain("websearch") - }), - ) - - websearch.instance("shows websearch for a configured third-party provider when enabled", () => + websearch.instance("shows websearch by default for a configured third-party provider", () => Effect.gen(function* () { const registry = yield* ToolRegistry.Service const agent = yield* Agent.Service @@ -187,6 +185,22 @@ describe("tool.registry", () => { }), ) + websearchOff.instance("hides websearch for a configured third-party provider when disabled", () => + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const agent = yield* Agent.Service + const build = yield* agent.get("build") + if (!build) return yield* Effect.die(new Error("build agent not found")) + const tools = yield* registry.tools({ + providerID: ProviderV2.ID.openai, + modelID: ModelV2.ID.make("test"), + agent: build, + }) + + expect(tools.map((tool) => tool.id)).not.toContain("websearch") + }), + ) + sandboxed.instance("preserves built-in network classification through production tool definition processing", () => Effect.gen(function* () { const registry = yield* ToolRegistry.Service From c5d3032118aa87cdecb3ae40f1117344762efd2b Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Mon, 27 Jul 2026 14:47:11 -0400 Subject: [PATCH 05/71] fix: keep third-party web search opt-in --- .changeset/enable-websearch-config.md | 2 +- packages/core/src/v1/config/config.ts | 3 +- .../src/routes/config/ToolsRoute.tsx | 2 +- .../src/components/settings/BrowserTab.tsx | 2 +- packages/opencode/src/tool/registry.ts | 2 +- .../test/kilocode/config/config.test.ts | 6 +-- packages/opencode/test/tool/registry.test.ts | 50 +++++++------------ 7 files changed, 26 insertions(+), 41 deletions(-) diff --git a/.changeset/enable-websearch-config.md b/.changeset/enable-websearch-config.md index ebbc4135f3b..cd47a9e9fb5 100644 --- a/.changeset/enable-websearch-config.md +++ b/.changeset/enable-websearch-config.md @@ -3,4 +3,4 @@ "kilo-code": patch --- -Configure web search for models from all providers through Kilo configuration, VS Code settings, and Kilo Console settings. +Allow users to enable web search for models from all providers through Kilo configuration, VS Code settings, and Kilo Console settings. diff --git a/packages/core/src/v1/config/config.ts b/packages/core/src/v1/config/config.ts index 0ef9d1ffc71..fea9a7f5989 100644 --- a/packages/core/src/v1/config/config.ts +++ b/packages/core/src/v1/config/config.ts @@ -227,8 +227,7 @@ export const Info = Schema.Struct({ permission: Schema.optional(ConfigPermissionV1.Info), tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)), web_search: Schema.optional(Schema.Boolean).annotate({ - description: - "Make web search available to models from all providers (default: true). Set to false to limit it to managed providers.", + description: "Make web search available to models from all providers (default: false)", }), // kilocode_change attachment: Schema.optional(ConfigAttachmentV1.Info).annotate({ description: "Attachment processing configuration, including image size limits and resizing behavior", diff --git a/packages/kilo-console/src/routes/config/ToolsRoute.tsx b/packages/kilo-console/src/routes/config/ToolsRoute.tsx index fae54f6b8cb..9dcb20b6e05 100644 --- a/packages/kilo-console/src/routes/config/ToolsRoute.tsx +++ b/packages/kilo-console/src/routes/config/ToolsRoute.tsx @@ -12,7 +12,7 @@ export function ToolsRoute() { const [search, setSearch] = createSignal("") const snap = () => ctx.data() const websearch = createMemo(() => snap()?.overlay.fields.web_search) - const searchEnabled = createMemo(() => websearch()?.value !== false) + const searchEnabled = createMemo(() => websearch()?.value === true) const rows = createMemo(() => { const data = snap() if (!data) return [] diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx index ca870fef21a..fabc7858b8a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx @@ -86,7 +86,7 @@ const BrowserTab: Component = () => { description={t("settings.webTools.webSearch.description")} last > - + {t("settings.webTools.webSearch.title")} diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 4f7c275cfa5..4df567dd6bf 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -377,7 +377,7 @@ export const layer: Layer.Layer< const filtered = (yield* all()).filter((tool) => { if (!KiloToolRegistry.available(tool, input.agent)) return false // kilocode_change if (tool.id === WebSearchTool.id) { - if (cfg.web_search !== false) return true // kilocode_change + if (cfg.web_search === true) return true // kilocode_change return webSearchEnabled(input.providerID, { exa: flags.enableExa, parallel: flags.enableParallel }) } diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts index 39389503067..c384d76dff0 100644 --- a/packages/opencode/test/kilocode/config/config.test.ts +++ b/packages/opencode/test/kilocode/config/config.test.ts @@ -152,10 +152,10 @@ describe("global config updates", () => { }) describe("kilocode web search config", () => { - test("accepts explicitly limiting web search to managed providers", () => { - const config = Schema.decodeUnknownSync(Config.Info)({ web_search: false }) + test("accepts enabling web search for all providers", () => { + const config = Schema.decodeUnknownSync(Config.Info)({ web_search: true }) - expect(config.web_search).toBe(false) + expect(config.web_search).toBe(true) }) }) diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index 2420506b9de..97e4e6c51c1 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -125,21 +125,7 @@ const websearch = testEffect( config: { get: () => Effect.succeed({ - provider: { openai: { options: { apiKey: "test-openai-key" } } }, - }), - }, - }), - node, - Agent.defaultLayer, - ), -) -const websearchOff = testEffect( - Layer.mergeAll( - registryLayer({ - config: { - get: () => - Effect.succeed({ - web_search: false, + web_search: true, provider: { openai: { options: { apiKey: "test-openai-key" } } }, }), }, @@ -169,23 +155,7 @@ function sandboxProfile(): Profile { describe("tool.registry", () => { // kilocode_change start - websearch.instance("shows websearch by default for a configured third-party provider", () => - Effect.gen(function* () { - const registry = yield* ToolRegistry.Service - const agent = yield* Agent.Service - const build = yield* agent.get("build") - if (!build) return yield* Effect.die(new Error("build agent not found")) - const tools = yield* registry.tools({ - providerID: ProviderV2.ID.openai, - modelID: ModelV2.ID.make("test"), - agent: build, - }) - - expect(tools.map((tool) => tool.id)).toContain("websearch") - }), - ) - - websearchOff.instance("hides websearch for a configured third-party provider when disabled", () => + it.instance("hides websearch for a third-party provider by default", () => Effect.gen(function* () { const registry = yield* ToolRegistry.Service const agent = yield* Agent.Service @@ -201,6 +171,22 @@ describe("tool.registry", () => { }), ) + websearch.instance("shows websearch for a configured third-party provider when enabled", () => + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const agent = yield* Agent.Service + const build = yield* agent.get("build") + if (!build) return yield* Effect.die(new Error("build agent not found")) + const tools = yield* registry.tools({ + providerID: ProviderV2.ID.openai, + modelID: ModelV2.ID.make("test"), + agent: build, + }) + + expect(tools.map((tool) => tool.id)).toContain("websearch") + }), + ) + sandboxed.instance("preserves built-in network classification through production tool definition processing", () => Effect.gen(function* () { const registry = yield* ToolRegistry.Service From 5325cc711b268a27ede10f2430ca4d66d887a33e Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Mon, 27 Jul 2026 15:39:58 -0400 Subject: [PATCH 06/71] docs: add NVIDIA to BYOK providers Cloud now supports NVIDIA as a direct BYOK provider. Documents the `nvidia-byok` key ID, the required model prefix, tool-calling requirement, and NVIDIA's Developer Program endpoint restrictions. --- packages/kilo-docs/pages/gateway/authentication.md | 1 + packages/kilo-docs/pages/getting-started/byok.md | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/kilo-docs/pages/gateway/authentication.md b/packages/kilo-docs/pages/gateway/authentication.md index 30129525ed0..ba6c3a794f6 100644 --- a/packages/kilo-docs/pages/gateway/authentication.md +++ b/packages/kilo-docs/pages/gateway/authentication.md @@ -97,6 +97,7 @@ BYOK lets you use your own provider API keys with the Kilo AI Gateway. When a BY | Kimi Code | `kimi-coding` | | Martian | `martian` | | Neuralwatt | `neuralwatt` | +| NVIDIA | `nvidia-byok` | | Ollama Cloud | `ollama-cloud` | | OpenCode Go | `opencode-go` | | OrcaRouter | `orcarouter` | diff --git a/packages/kilo-docs/pages/getting-started/byok.md b/packages/kilo-docs/pages/getting-started/byok.md index adb426172dc..bf3d3ff842b 100644 --- a/packages/kilo-docs/pages/getting-started/byok.md +++ b/packages/kilo-docs/pages/getting-started/byok.md @@ -49,6 +49,7 @@ These providers offer coding-focused subscriptions or dedicated endpoints. Bring - Martian - Mistral Codestral - Neuralwatt +- NVIDIA - Ollama Cloud - OpenCode Go - OrcaRouter @@ -87,6 +88,16 @@ Your IAM user or role must have the following permissions: - `bedrock:InvokeModel` - `bedrock:InvokeModelWithResponseStream` +### NVIDIA configuration + +Create an API key in the [NVIDIA API Catalog](https://build.nvidia.com/settings/api-keys), then add it as the NVIDIA provider. + +NVIDIA models use their own `nvidia-byok/` prefix, so select a model such as `nvidia-byok/nvidia/nemotron-3-super-120b-a12b` rather than a Kilo Gateway model with the same name. Only NVIDIA-hosted models that support tool calling are available. + +{% callout type="warning" title="NVIDIA API Catalog terms" %} +NVIDIA limits Developer Program endpoints to prototyping, research, development, and testing. Serving production end users may require NVIDIA AI Enterprise licensing. Your prompts and model outputs are sent to NVIDIA under your NVIDIA agreement. Review the [API Catalog quickstart](https://docs.api.nvidia.com/nim/docs/api-quickstart) and the [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). +{% /callout %} + ## How Bring Your Own Key works - When you use the **Kilo Gateway** provider, Kilo checks if there's a BYOK key for the selected model's provider. From 62d290f9b52db297cf5022b27601c81508ee66c2 Mon Sep 17 00:00:00 2001 From: Joshua Lambert <25085430+lambertjosh@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:58:08 -0400 Subject: [PATCH 07/71] Apply suggestion from @lambertjosh --- packages/kilo-docs/pages/getting-started/byok.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-docs/pages/getting-started/byok.md b/packages/kilo-docs/pages/getting-started/byok.md index bf3d3ff842b..43f00eac318 100644 --- a/packages/kilo-docs/pages/getting-started/byok.md +++ b/packages/kilo-docs/pages/getting-started/byok.md @@ -92,7 +92,7 @@ Your IAM user or role must have the following permissions: Create an API key in the [NVIDIA API Catalog](https://build.nvidia.com/settings/api-keys), then add it as the NVIDIA provider. -NVIDIA models use their own `nvidia-byok/` prefix, so select a model such as `nvidia-byok/nvidia/nemotron-3-super-120b-a12b` rather than a Kilo Gateway model with the same name. Only NVIDIA-hosted models that support tool calling are available. +Models that do not support tool calling are excluded, as they are unlikely to be provide useful results. {% callout type="warning" title="NVIDIA API Catalog terms" %} NVIDIA limits Developer Program endpoints to prototyping, research, development, and testing. Serving production end users may require NVIDIA AI Enterprise licensing. Your prompts and model outputs are sent to NVIDIA under your NVIDIA agreement. Review the [API Catalog quickstart](https://docs.api.nvidia.com/nim/docs/api-quickstart) and the [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). From 605dc483e84fbbc3ff5e213e057f3791a14e9299 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Mon, 27 Jul 2026 17:30:02 -0400 Subject: [PATCH 08/71] chore(sdk): minimize web search schema diff --- packages/sdk/openapi.json | 5637 +++++++++++++++++++++++++++---------- 1 file changed, 4111 insertions(+), 1526 deletions(-) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 1451c3a6b3f..7bd369f6bb0 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -4211,10 +4211,10 @@ ] } }, - "/experimental/project/{projectID}/copy": { + "/experimental/project/{projectID}/copy/generate-name": { "post": { "tags": ["projectCopy"], - "operationId": "experimental.projectCopy.create", + "operationId": "experimental.projectCopy.generateName", "parameters": [ { "name": "projectID", @@ -4224,6 +4224,14 @@ }, "required": true }, + { + "name": "directory", + "in": "query", + "schema": { + "type": "string" + }, + "required": false + }, { "name": "workspace", "in": "query", @@ -4235,56 +4243,45 @@ ], "responses": { "200": { - "description": "Project copy created", + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProjectCopyCopy" + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"], + "additionalProperties": false } } } }, "400": { - "description": "ProjectCopyError | InvalidRequestError", + "description": "Bad request", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectCopyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/BadRequestError" } } } } }, - "description": "Create a local physical copy of a project using the selected strategy.", - "summary": "Create project copy", + "description": "Generate a short name for a project copy from task context.", + "summary": "Generate project copy name", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "strategy": { - "type": "string", - "enum": ["git_worktree"] - }, - "directory": { - "type": "string" - }, - "name": { - "type": "string" - }, "context": { "type": "string" } }, - "required": ["strategy", "directory"], "additionalProperties": false } } @@ -4293,145 +4290,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.experimental.projectCopy.create({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["projectCopy"], - "operationId": "experimental.projectCopy.remove", - "parameters": [ - { - "name": "projectID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "204": { - "description": "Project copy removed" - }, - "400": { - "description": "ProjectCopyError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectCopyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Remove a local physical copy of a project using the selected strategy.", - "summary": "Remove project copy", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } - }, - "required": ["directory"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.experimental.projectCopy.remove({\n ...\n})" - } - ] - } - }, - "/experimental/project/{projectID}/copy/refresh": { - "post": { - "tags": ["projectCopy"], - "operationId": "experimental.projectCopy.refresh", - "parameters": [ - { - "name": "projectID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "204": { - "description": "Project copies refreshed" - }, - "400": { - "description": "ProjectCopyError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectCopyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Discover local project copies using one or all configured strategies.", - "summary": "Refresh project copies", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.experimental.projectCopy.refresh({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.experimental.projectCopy.generateName({\n ...\n})" } ] } @@ -14735,6 +14594,16 @@ } } } + }, + "500": { + "description": "CloudSessionImportError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CloudSessionImportError" + } + } + } } }, "description": "Download a cloud-synced session and write it to local storage with fresh IDs.", @@ -16890,6 +16759,10 @@ }, "diff": { "type": "string" + }, + "workspace": { + "type": "string", + "enum": ["restored", "snapshots-disabled", "unavailable"] } }, "required": ["messageID"], @@ -19898,7 +19771,7 @@ }, "/api/health": { "get": { - "tags": ["kilo experimental HttpApi"], + "tags": ["Kilo HttpApi"], "operationId": "v2.health.get", "parameters": [], "security": [], @@ -19942,8 +19815,8 @@ } } }, - "description": "Check whether the v2 API server is ready to accept requests.", - "summary": "Check v2 server health", + "description": "Check whether the API server is ready to accept requests.", + "summary": "Check server health", "x-codeSamples": [ { "lang": "js", @@ -19952,9 +19825,77 @@ ] } }, + "/api/location": { + "get": { + "tags": ["Kilo HttpApi"], + "operationId": "v2.location.get", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Location.Info", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationInfo" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Resolve the requested location or the server default location.", + "summary": "Get location", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.location.get({\n ...\n})" + } + ] + } + }, "/api/agent": { "get": { - "tags": ["kilo experimental HttpApi"], + "tags": ["Kilo HttpApi"], "operationId": "v2.agent.list", "parameters": [ { @@ -20023,8 +19964,8 @@ } } }, - "description": "Retrieve currently registered v2 agents.", - "summary": "List v2 agents", + "description": "Retrieve currently registered agents.", + "summary": "List agents", "x-codeSamples": [ { "lang": "js", @@ -20035,7 +19976,7 @@ }, "/api/session": { "get": { - "tags": ["v2"], + "tags": ["sessions"], "operationId": "v2.session.list", "parameters": [ { @@ -20109,11 +20050,11 @@ "security": [], "responses": { "200": { - "description": "V2SessionsResponse", + "description": "SessionsResponse", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/V2SessionsResponse" + "$ref": "#/components/schemas/SessionsResponse" } } } @@ -20150,18 +20091,192 @@ } }, "description": "Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.", - "summary": "List v2 sessions", + "summary": "List sessions", "x-codeSamples": [ { "lang": "js", "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.session.list({\n ...\n})" } ] + }, + "post": { + "tags": ["sessions"], + "operationId": "v2.session.create", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SessionV2Info" + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Create a session at the requested location.", + "summary": "Create session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^ses" + }, + "agent": { + "type": "string" + }, + "model": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": ["id", "providerID"], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/LocationRef" + } + }, + "additionalProperties": false + } + } + }, + "required": true + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.session.create({\n ...\n})" + } + ] + } + }, + "/api/session/{sessionID}": { + "get": { + "tags": ["sessions"], + "operationId": "v2.session.get", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^ses.*" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SessionV2Info" + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve a session by ID.", + "summary": "Get session", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.session.get({\n ...\n})" + } + ] } }, "/api/session/{sessionID}/prompt": { "post": { - "tags": ["v2"], + "tags": ["sessions"], "operationId": "v2.session.prompt", "parameters": [ { @@ -20218,7 +20333,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionNotFoundError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } @@ -20234,8 +20356,8 @@ } } }, - "description": "Durably admit one v2 session input and schedule agent-loop execution unless resume is false.", - "summary": "Send v2 message", + "description": "Durably admit one session input and schedule agent-loop execution unless resume is false.", + "summary": "Send message", "requestBody": { "content": { "application/json": { @@ -20274,7 +20396,7 @@ }, "/api/session/{sessionID}/compact": { "post": { - "tags": ["v2"], + "tags": ["sessions"], "operationId": "v2.session.compact", "parameters": [ { @@ -20317,7 +20439,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionNotFoundError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } @@ -20333,8 +20462,8 @@ } } }, - "description": "Compact a v2 session conversation.", - "summary": "Compact v2 session", + "description": "Compact a session conversation.", + "summary": "Compact session", "x-codeSamples": [ { "lang": "js", @@ -20345,7 +20474,7 @@ }, "/api/session/{sessionID}/wait": { "post": { - "tags": ["v2"], + "tags": ["sessions"], "operationId": "v2.session.wait", "parameters": [ { @@ -20388,7 +20517,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionNotFoundError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } @@ -20404,8 +20540,8 @@ } } }, - "description": "Wait for a v2 session agent loop to become idle.", - "summary": "Wait for v2 session", + "description": "Wait for a session agent loop to become idle.", + "summary": "Wait for session", "x-codeSamples": [ { "lang": "js", @@ -20416,7 +20552,7 @@ }, "/api/session/{sessionID}/context": { "get": { - "tags": ["v2"], + "tags": ["sessions"], "operationId": "v2.session.context", "parameters": [ { @@ -20476,7 +20612,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionNotFoundError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } @@ -20492,8 +20635,8 @@ } } }, - "description": "Retrieve the active context messages for a v2 session (all messages after the last compaction).", - "summary": "Get v2 session context", + "description": "Retrieve the active context messages for a session (all messages after the last compaction).", + "summary": "Get session context", "x-codeSamples": [ { "lang": "js", @@ -20504,7 +20647,7 @@ }, "/api/session/{sessionID}/message": { "get": { - "tags": ["v2 messages"], + "tags": ["messages"], "operationId": "v2.session.messages", "parameters": [ { @@ -20546,11 +20689,11 @@ "security": [], "responses": { "200": { - "description": "V2SessionMessagesResponse", + "description": "SessionMessagesResponse", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/V2SessionMessagesResponse" + "$ref": "#/components/schemas/SessionMessagesResponse" } } } @@ -20587,7 +20730,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionNotFoundError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } @@ -20603,8 +20753,8 @@ } } }, - "description": "Retrieve projected v2 messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.", - "summary": "Get v2 session messages", + "description": "Retrieve projected messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.", + "summary": "Get session messages", "x-codeSamples": [ { "lang": "js", @@ -20615,7 +20765,7 @@ }, "/api/model": { "get": { - "tags": ["v2 models"], + "tags": ["models"], "operationId": "v2.model.list", "parameters": [ { @@ -20694,8 +20844,8 @@ } } }, - "description": "Retrieve available v2 models ordered by release date.", - "summary": "List v2 models", + "description": "Retrieve available models ordered by release date.", + "summary": "List models", "x-codeSamples": [ { "lang": "js", @@ -20706,7 +20856,7 @@ }, "/api/provider": { "get": { - "tags": ["v2 providers"], + "tags": ["providers"], "operationId": "v2.provider.list", "parameters": [ { @@ -20785,8 +20935,8 @@ } } }, - "description": "Retrieve active v2 AI providers so clients can show provider availability and configuration.", - "summary": "List v2 providers", + "description": "Retrieve active AI providers so clients can show provider availability and configuration.", + "summary": "List providers", "x-codeSamples": [ { "lang": "js", @@ -20797,7 +20947,7 @@ }, "/api/provider/{providerID}": { "get": { - "tags": ["v2 providers"], + "tags": ["providers"], "operationId": "v2.provider.get", "parameters": [ { @@ -20891,8 +21041,8 @@ } } }, - "description": "Retrieve a single v2 AI provider so clients can inspect its availability and endpoint settings.", - "summary": "Get v2 provider", + "description": "Retrieve a single AI provider so clients can inspect its availability and endpoint settings.", + "summary": "Get provider", "x-codeSamples": [ { "lang": "js", @@ -20901,9 +21051,1011 @@ ] } }, + "/api/integration": { + "get": { + "tags": ["integrations"], + "operationId": "v2.integration.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/LocationInfo" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IntegrationInfo" + } + } + }, + "required": ["location", "data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve available integrations and their authentication methods.", + "summary": "List integrations", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.integration.list({\n ...\n})" + } + ] + } + }, + "/api/integration/{integrationID}": { + "get": { + "tags": ["integrations"], + "operationId": "v2.integration.get", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/LocationInfo" + }, + "data": { + "$ref": "#/components/schemas/IntegrationInfo" + } + }, + "required": ["location", "data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve one integration and its authentication methods.", + "summary": "Get integration", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.integration.get({\n ...\n})" + } + ] + } + }, + "/api/integration/{integrationID}/connect/key": { + "post": { + "tags": ["integrations"], + "operationId": "v2.integration.connect.key", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Run a key authentication method and store the resulting credential.", + "summary": "Connect with key", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": ["key"], + "additionalProperties": false + } + } + }, + "required": true + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.integration.connect.key({\n ...\n})" + } + ] + } + }, + "/api/integration/{integrationID}/connect/oauth": { + "post": { + "tags": ["integrations"], + "operationId": "v2.integration.connect.oauth", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/LocationInfo" + }, + "data": { + "$ref": "#/components/schemas/IntegrationAttempt" + } + }, + "required": ["location", "data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Start an OAuth attempt and return the authorization details.", + "summary": "Begin OAuth connection", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "methodID": { + "type": "string" + }, + "inputs": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "label": { + "type": "string" + } + }, + "required": ["methodID", "inputs"], + "additionalProperties": false + } + } + }, + "required": true + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.integration.connect.oauth({\n ...\n})" + } + ] + } + }, + "/api/integration/attempt/{attemptID}": { + "get": { + "tags": ["integrations"], + "operationId": "v2.integration.attempt.status", + "parameters": [ + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/LocationInfo" + }, + "data": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["pending"] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false + } + }, + "required": ["status", "time"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["complete"] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false + } + }, + "required": ["status", "time"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["failed"] + }, + "message": { + "type": "string" + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false + } + }, + "required": ["status", "message", "time"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["expired"] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false + } + }, + "required": ["status", "time"], + "additionalProperties": false + } + ] + } + }, + "required": ["location", "data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Poll the current status of an OAuth attempt.", + "summary": "Get OAuth attempt status", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.integration.attempt.status({\n ...\n})" + } + ] + }, + "delete": { + "tags": ["integrations"], + "operationId": "v2.integration.attempt.cancel", + "parameters": [ + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Cancel an OAuth attempt and release its resources.", + "summary": "Cancel OAuth connection", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.integration.attempt.cancel({\n ...\n})" + } + ] + } + }, + "/api/integration/attempt/{attemptID}/complete": { + "post": { + "tags": ["integrations"], + "operationId": "v2.integration.attempt.complete", + "parameters": [ + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Complete a code-based OAuth attempt and store the resulting credential.", + "summary": "Complete OAuth connection", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "required": true + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.integration.attempt.complete({\n ...\n})" + } + ] + } + }, + "/api/credential/{credentialID}": { + "patch": { + "tags": ["Kilo HttpApi"], + "operationId": "v2.credential.update", + "parameters": [ + { + "name": "credentialID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Update a stored credential label.", + "summary": "Update credential", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string" + } + }, + "required": ["label"], + "additionalProperties": false + } + } + }, + "required": true + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.credential.update({\n ...\n})" + } + ] + }, + "delete": { + "tags": ["Kilo HttpApi"], + "operationId": "v2.credential.remove", + "parameters": [ + { + "name": "credentialID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Remove a stored integration credential.", + "summary": "Remove credential", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.credential.remove({\n ...\n})" + } + ] + } + }, "/api/permission/request": { "get": { - "tags": ["v2 permissions"], + "tags": ["permissions"], "operationId": "v2.permission.request.list", "parameters": [ { @@ -20982,184 +22134,9 @@ ] } }, - "/api/session/{sessionID}/permission/request": { - "get": { - "tags": ["v2 session permissions"], - "operationId": "v2.session.permission.list", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses.*" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionV2Request" - } - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionNotFoundError" - } - } - } - } - }, - "description": "Retrieve pending permission requests owned by a session.", - "summary": "List session permission requests", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.session.permission.list({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/permission/request/{requestID}/reply": { - "post": { - "tags": ["v2 session permissions"], - "operationId": "v2.session.permission.reply", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses.*" - }, - "required": true - }, - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^per" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | PermissionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/PermissionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Respond to a pending permission request owned by a session.", - "summary": "Reply to pending permission request", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "reply": { - "$ref": "#/components/schemas/PermissionV2Reply" - }, - "message": { - "type": "string" - } - }, - "required": ["reply"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.session.permission.reply({\n ...\n})" - } - ] - } - }, "/api/permission/saved": { "get": { - "tags": ["v2 saved permissions"], + "tags": ["permissions"], "operationId": "v2.permission.saved.list", "parameters": [ { @@ -21226,7 +22203,7 @@ }, "/api/permission/saved/{id}": { "delete": { - "tags": ["v2 saved permissions"], + "tags": ["permissions"], "operationId": "v2.permission.saved.remove", "parameters": [ { @@ -21274,9 +22251,194 @@ ] } }, - "/api/fs/read": { + "/api/session/{sessionID}/permission": { "get": { - "tags": ["v2 filesystem"], + "tags": ["permissions"], + "operationId": "v2.session.permission.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^ses.*" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionV2Request" + } + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve pending permission requests owned by a session.", + "summary": "List session permission requests", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.session.permission.list({\n ...\n})" + } + ] + } + }, + "/api/session/{sessionID}/permission/{requestID}/reply": { + "post": { + "tags": ["permissions"], + "operationId": "v2.session.permission.reply", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^ses.*" + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^per" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | PermissionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Respond to a pending permission request owned by a session.", + "summary": "Reply to pending permission request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reply": { + "$ref": "#/components/schemas/PermissionV2Reply" + }, + "message": { + "type": "string" + } + }, + "required": ["reply"], + "additionalProperties": false + } + } + }, + "required": true + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.session.permission.reply({\n ...\n})" + } + ] + } + }, + "/api/fs/read/*": { + "get": { + "tags": ["filesystem"], "operationId": "v2.fs.read", "parameters": [ { @@ -21304,14 +22466,6 @@ "schema": { "type": "string" }, - "required": true - }, - { - "name": "reference", - "in": "query", - "schema": { - "type": "string" - }, "required": false } ], @@ -21320,26 +22474,10 @@ "200": { "description": "Success", "content": { - "application/json": { + "application/octet-stream": { "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "anyOf": [ - { - "$ref": "#/components/schemas/FileSystemTextContent" - }, - { - "$ref": "#/components/schemas/FileSystemBinaryContent" - } - ] - } - }, - "required": ["location", "data"], - "additionalProperties": false + "type": "string", + "format": "binary" } } } @@ -21365,7 +22503,7 @@ } } }, - "description": "Read one file relative to the requested location.", + "description": "Serve one file relative to the requested location.", "summary": "Read file", "x-codeSamples": [ { @@ -21377,7 +22515,7 @@ }, "/api/fs/list": { "get": { - "tags": ["v2 filesystem"], + "tags": ["filesystem"], "operationId": "v2.fs.list", "parameters": [ { @@ -21406,14 +22544,6 @@ "type": "string" }, "required": false - }, - { - "name": "reference", - "in": "query", - "schema": { - "type": "string" - }, - "required": false } ], "security": [], @@ -21472,9 +22602,115 @@ ] } }, + "/api/fs/find": { + "get": { + "tags": ["filesystem"], + "operationId": "v2.fs.find", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "required": false, + "style": "deepObject", + "explode": true + }, + { + "name": "query", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string", + "enum": ["file", "directory"] + }, + "required": false + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "string" + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/LocationInfo" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemEntry" + } + } + }, + "required": ["location", "data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Find recursively ranked filesystem entries relative to the requested location.", + "summary": "Find files", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.fs.find({\n ...\n})" + } + ] + } + }, "/api/command": { "get": { - "tags": ["v2 commands"], + "tags": ["commands"], "operationId": "v2.command.list", "parameters": [ { @@ -21543,8 +22779,8 @@ } } }, - "description": "Retrieve currently registered v2 commands.", - "summary": "List v2 commands", + "description": "Retrieve currently registered commands.", + "summary": "List commands", "x-codeSamples": [ { "lang": "js", @@ -21555,7 +22791,7 @@ }, "/api/skill": { "get": { - "tags": ["v2 skills"], + "tags": ["skills"], "operationId": "v2.skill.list", "parameters": [ { @@ -21624,8 +22860,8 @@ } } }, - "description": "Retrieve currently registered v2 skills.", - "summary": "List v2 skills", + "description": "Retrieve currently registered skills.", + "summary": "List skills", "x-codeSamples": [ { "lang": "js", @@ -21636,7 +22872,7 @@ }, "/api/event": { "get": { - "tags": ["v2 events"], + "tags": ["events"], "operationId": "v2.event.subscribe", "parameters": [ { @@ -21692,8 +22928,8 @@ } } }, - "description": "Subscribe to native EventV2 payloads for a location.", - "summary": "Subscribe to v2 events", + "description": "Subscribe to native event payloads for a location.", + "summary": "Subscribe to events", "x-codeSamples": [ { "lang": "js", @@ -21704,7 +22940,7 @@ }, "/api/question/request": { "get": { - "tags": ["v2 questions"], + "tags": ["session questions"], "operationId": "v2.question.request.list", "parameters": [ { @@ -21783,9 +23019,94 @@ ] } }, - "/api/session/{sessionID}/question/request/{requestID}/reply": { + "/api/session/{sessionID}/question": { + "get": { + "tags": ["session questions"], + "operationId": "v2.session.question.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^ses.*" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2Request" + } + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve pending question requests owned by a session.", + "summary": "List session question requests", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.session.question.list({\n ...\n})" + } + ] + } + }, + "/api/session/{sessionID}/question/{requestID}/reply": { "post": { - "tags": ["v2 session questions"], + "tags": ["session questions"], "operationId": "v2.session.question.reply", "parameters": [ { @@ -21838,11 +23159,14 @@ "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/QuestionNotFoundError" + }, { "$ref": "#/components/schemas/SessionNotFoundError" }, { - "$ref": "#/components/schemas/QuestionNotFoundError" + "$ref": "#/components/schemas/SessionNotFoundError" } ] } @@ -21870,9 +23194,9 @@ ] } }, - "/api/session/{sessionID}/question/request/{requestID}/reject": { + "/api/session/{sessionID}/question/{requestID}/reject": { "post": { - "tags": ["v2 session questions"], + "tags": ["session questions"], "operationId": "v2.session.question.reject", "parameters": [ { @@ -21925,11 +23249,14 @@ "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/QuestionNotFoundError" + }, { "$ref": "#/components/schemas/SessionNotFoundError" }, { - "$ref": "#/components/schemas/QuestionNotFoundError" + "$ref": "#/components/schemas/SessionNotFoundError" } ] } @@ -21947,6 +23274,322 @@ ] } }, + "/api/reference": { + "get": { + "tags": ["reference"], + "operationId": "v2.reference.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/LocationInfo" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReferenceInfo" + } + } + }, + "required": ["location", "data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List references available in the requested location.", + "summary": "List references", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.reference.list({\n ...\n})" + } + ] + } + }, + "/experimental/project/{projectID}/copy": { + "post": { + "tags": ["projectCopy"], + "operationId": "v2.projectCopy.create", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "responses": { + "200": { + "description": "ProjectCopy.Copy", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectCopyCopy" + } + } + } + }, + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "strategy": { + "type": "string" + }, + "directory": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["strategy", "directory"], + "additionalProperties": false + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.projectCopy.create({\n ...\n})" + } + ] + }, + "delete": { + "tags": ["projectCopy"], + "operationId": "v2.projectCopy.remove", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "force": { + "type": "boolean" + } + }, + "required": ["directory", "force"], + "additionalProperties": false + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.projectCopy.remove({\n ...\n})" + } + ] + } + }, + "/experimental/project/{projectID}/copy/refresh": { + "post": { + "tags": ["projectCopy"], + "operationId": "v2.projectCopy.refresh", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.projectCopy.refresh({\n ...\n})" + } + ] + } + }, "/pty/{ptyID}/connect": { "get": { "tags": ["pty"], @@ -22176,6 +23819,9 @@ { "$ref": "#/components/schemas/EventSessionNextPromptPromoted" }, + { + "$ref": "#/components/schemas/EventSessionNextInterruptRequested" + }, { "$ref": "#/components/schemas/EventSessionNextContextUpdated" }, @@ -22300,61 +23946,10 @@ "$ref": "#/components/schemas/EventPermissionReplied" }, { - "$ref": "#/components/schemas/EventTodoUpdated" + "$ref": "#/components/schemas/EventReferenceUpdated" }, { - "$ref": "#/components/schemas/EventSessionStatus" - }, - { - "$ref": "#/components/schemas/EventSessionIdle" - }, - { - "$ref": "#/components/schemas/EventSessionCompacted" - }, - { - "$ref": "#/components/schemas/EventCommandExecuted" - }, - { - "$ref": "#/components/schemas/EventProjectDirectoriesUpdated" - }, - { - "$ref": "#/components/schemas/EventProjectUpdated" - }, - { - "$ref": "#/components/schemas/EventLspUpdated" - }, - { - "$ref": "#/components/schemas/EventFileEdited" - }, - { - "$ref": "#/components/schemas/EventFileWatcherUpdated" - }, - { - "$ref": "#/components/schemas/EventVcsBranchUpdated" - }, - { - "$ref": "#/components/schemas/EventWorkspaceReady" - }, - { - "$ref": "#/components/schemas/EventWorkspaceFailed" - }, - { - "$ref": "#/components/schemas/EventWorkspaceStatus" - }, - { - "$ref": "#/components/schemas/EventWorktreeReady" - }, - { - "$ref": "#/components/schemas/EventWorktreeFailed" - }, - { - "$ref": "#/components/schemas/EventAccountAdded" - }, - { - "$ref": "#/components/schemas/EventAccountRemoved" - }, - { - "$ref": "#/components/schemas/EventAccountSwitched" + "$ref": "#/components/schemas/EventIntegrationUpdated" }, { "$ref": "#/components/schemas/EventPermissionV2Asked" @@ -22362,6 +23957,15 @@ { "$ref": "#/components/schemas/EventPermissionV2Replied" }, + { + "$ref": "#/components/schemas/EventProjectDirectoriesUpdated" + }, + { + "$ref": "#/components/schemas/EventFileEdited" + }, + { + "$ref": "#/components/schemas/EventFileWatcherUpdated" + }, { "$ref": "#/components/schemas/EventPtyCreated" }, @@ -22383,6 +23987,45 @@ { "$ref": "#/components/schemas/EventQuestionV2Rejected" }, + { + "$ref": "#/components/schemas/EventTodoUpdated" + }, + { + "$ref": "#/components/schemas/EventSessionStatus" + }, + { + "$ref": "#/components/schemas/EventSessionIdle" + }, + { + "$ref": "#/components/schemas/EventSessionCompacted" + }, + { + "$ref": "#/components/schemas/EventCommandExecuted" + }, + { + "$ref": "#/components/schemas/EventProjectUpdated" + }, + { + "$ref": "#/components/schemas/EventLspUpdated" + }, + { + "$ref": "#/components/schemas/EventVcsBranchUpdated" + }, + { + "$ref": "#/components/schemas/EventWorkspaceReady" + }, + { + "$ref": "#/components/schemas/EventWorkspaceFailed" + }, + { + "$ref": "#/components/schemas/EventWorkspaceStatus" + }, + { + "$ref": "#/components/schemas/EventWorktreeReady" + }, + { + "$ref": "#/components/schemas/EventWorktreeFailed" + }, { "$ref": "#/components/schemas/EventServerInstanceDisposed" } @@ -23353,6 +24996,10 @@ }, "diff": { "type": "string" + }, + "workspace": { + "type": "string", + "enum": ["restored", "snapshots-disabled", "unavailable"] } }, "required": ["messageID"], @@ -23641,6 +25288,27 @@ "required": ["name", "data"], "additionalProperties": false }, + "ContentFilterError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["ContentFilterError"] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"], + "additionalProperties": false + } + }, + "required": ["name", "data"], + "additionalProperties": false + }, "APIError": { "type": "object", "properties": { @@ -23734,6 +25402,9 @@ { "$ref": "#/components/schemas/ContextOverflowError" }, + { + "$ref": "#/components/schemas/ContentFilterError" + }, { "$ref": "#/components/schemas/APIError" } @@ -24350,6 +26021,17 @@ }, "snapshot": { "type": "string" + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "number", + "minimum": 0 + } + }, + "required": ["start"], + "additionalProperties": false } }, "required": ["id", "sessionID", "messageID", "type"], @@ -24393,6 +26075,47 @@ "required": ["providerID", "modelID"], "additionalProperties": false }, + "generationID": { + "type": "string" + }, + "vercelID": { + "type": "string" + }, + "metrics": { + "type": "object", + "properties": { + "prompt": { + "type": "number" + }, + "generation": { + "type": "number" + }, + "source": { + "type": "string", + "enum": ["provider", "computed"] + } + }, + "required": ["source"], + "additionalProperties": false + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "number", + "minimum": 0 + }, + "end": { + "type": "number", + "minimum": 0 + }, + "elapsed": { + "type": "number" + } + }, + "required": ["start", "end", "elapsed"], + "additionalProperties": false + }, "cost": { "type": "number" }, @@ -24665,12 +26388,6 @@ "items": { "$ref": "#/components/schemas/PromptAgentAttachment" } - }, - "references": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptReferenceAttachment" - } } }, "required": ["text"], @@ -24981,6 +26698,70 @@ "required": ["name", "data"], "additionalProperties": false }, + "Pty": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^pty" + }, + "title": { + "type": "string" + }, + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "cwd": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["running", "exited"] + }, + "pid": { + "type": "integer", + "minimum": 0 + }, + "sessionID": { + "anyOf": [ + { + "type": "string", + "pattern": "^ses" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "title", "command", "args", "cwd", "status", "pid"], + "additionalProperties": false + }, + "Todo": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "Brief description of the task" + }, + "status": { + "type": "string", + "description": "Current status of the task: pending, in_progress, completed, cancelled" + }, + "priority": { + "type": "string", + "description": "Priority level of the task: high, medium, low" + } + }, + "required": ["content", "status", "priority"], + "additionalProperties": false + }, "SessionStatus": { "anyOf": [ { @@ -25072,51 +26853,6 @@ } ] }, - "Pty": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty" - }, - "title": { - "type": "string" - }, - "command": { - "type": "string" - }, - "args": { - "type": "array", - "items": { - "type": "string" - } - }, - "cwd": { - "type": "string" - }, - "status": { - "type": "string", - "enum": ["running", "exited"] - }, - "pid": { - "type": "integer", - "minimum": 0 - }, - "sessionID": { - "anyOf": [ - { - "type": "string", - "pattern": "^ses" - }, - { - "type": "null" - } - ] - } - }, - "required": ["id", "title", "command", "args", "cwd", "status", "pid"], - "additionalProperties": false - }, "GlobalEvent": { "type": "object", "properties": { @@ -25269,6 +27005,9 @@ { "$ref": "#/components/schemas/EventSessionNextPromptPromoted" }, + { + "$ref": "#/components/schemas/EventSessionNextInterruptRequested" + }, { "$ref": "#/components/schemas/EventSessionNextContextUpdated" }, @@ -25393,61 +27132,10 @@ "$ref": "#/components/schemas/EventPermissionReplied" }, { - "$ref": "#/components/schemas/EventTodoUpdated" + "$ref": "#/components/schemas/EventReferenceUpdated" }, { - "$ref": "#/components/schemas/EventSessionStatus" - }, - { - "$ref": "#/components/schemas/EventSessionIdle" - }, - { - "$ref": "#/components/schemas/EventSessionCompacted" - }, - { - "$ref": "#/components/schemas/EventCommandExecuted" - }, - { - "$ref": "#/components/schemas/EventProjectDirectoriesUpdated" - }, - { - "$ref": "#/components/schemas/EventProjectUpdated" - }, - { - "$ref": "#/components/schemas/EventLspUpdated" - }, - { - "$ref": "#/components/schemas/EventFileEdited" - }, - { - "$ref": "#/components/schemas/EventFileWatcherUpdated" - }, - { - "$ref": "#/components/schemas/EventVcsBranchUpdated" - }, - { - "$ref": "#/components/schemas/EventWorkspaceReady" - }, - { - "$ref": "#/components/schemas/EventWorkspaceFailed" - }, - { - "$ref": "#/components/schemas/EventWorkspaceStatus" - }, - { - "$ref": "#/components/schemas/EventWorktreeReady" - }, - { - "$ref": "#/components/schemas/EventWorktreeFailed" - }, - { - "$ref": "#/components/schemas/EventAccountAdded" - }, - { - "$ref": "#/components/schemas/EventAccountRemoved" - }, - { - "$ref": "#/components/schemas/EventAccountSwitched" + "$ref": "#/components/schemas/EventIntegrationUpdated" }, { "$ref": "#/components/schemas/EventPermissionV2Asked" @@ -25455,6 +27143,15 @@ { "$ref": "#/components/schemas/EventPermissionV2Replied" }, + { + "$ref": "#/components/schemas/EventProjectDirectoriesUpdated" + }, + { + "$ref": "#/components/schemas/EventFileEdited" + }, + { + "$ref": "#/components/schemas/EventFileWatcherUpdated" + }, { "$ref": "#/components/schemas/EventPtyCreated" }, @@ -25476,6 +27173,45 @@ { "$ref": "#/components/schemas/EventQuestionV2Rejected" }, + { + "$ref": "#/components/schemas/EventTodoUpdated" + }, + { + "$ref": "#/components/schemas/EventSessionStatus" + }, + { + "$ref": "#/components/schemas/EventSessionIdle" + }, + { + "$ref": "#/components/schemas/EventSessionCompacted" + }, + { + "$ref": "#/components/schemas/EventCommandExecuted" + }, + { + "$ref": "#/components/schemas/EventProjectUpdated" + }, + { + "$ref": "#/components/schemas/EventLspUpdated" + }, + { + "$ref": "#/components/schemas/EventVcsBranchUpdated" + }, + { + "$ref": "#/components/schemas/EventWorkspaceReady" + }, + { + "$ref": "#/components/schemas/EventWorkspaceFailed" + }, + { + "$ref": "#/components/schemas/EventWorkspaceStatus" + }, + { + "$ref": "#/components/schemas/EventWorktreeReady" + }, + { + "$ref": "#/components/schemas/EventWorktreeFailed" + }, { "$ref": "#/components/schemas/EventServerInstanceDisposed" }, @@ -25575,9 +27311,6 @@ { "$ref": "#/components/schemas/SyncEventSessionNextCompactionStarted" }, - { - "$ref": "#/components/schemas/SyncEventSessionNextCompactionDelta" - }, { "$ref": "#/components/schemas/SyncEventSessionNextCompactionEnded" } @@ -25618,44 +27351,6 @@ "additionalProperties": false, "description": "Server configuration for the kilo serve command" }, - "ReferenceConfigEntry": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "object", - "properties": { - "repository": { - "type": "string", - "description": "Git repository URL, host/path reference, or GitHub owner/repo shorthand" - }, - "branch": { - "type": "string" - } - }, - "required": ["repository"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Absolute path, ~/ path, or workspace-relative path to a local reference directory" - } - }, - "required": ["path"], - "additionalProperties": false - } - ] - }, - "ReferenceConfig": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/ReferenceConfigEntry" - } - }, "IndexingConfig": { "type": "object", "properties": { @@ -25850,7 +27545,8 @@ "items": { "type": "string", "pattern": "^\\s*\\.?[A-Za-z0-9][A-Za-z0-9_+-]*\\s*$" - } + }, + "minItems": 1 } }, "additionalProperties": false @@ -26213,7 +27909,7 @@ "properties": { "field": { "type": "string", - "enum": ["reasoning_content", "reasoning_details"] + "enum": ["reasoning", "reasoning_content", "reasoning_details"] } }, "required": ["field"], @@ -26539,8 +28235,37 @@ }, "additionalProperties": false }, + "references": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/ConfigV2ReferenceGit" + }, + { + "$ref": "#/components/schemas/ConfigV2ReferenceLocal" + } + ] + } + }, "reference": { - "$ref": "#/components/schemas/ReferenceConfig" + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/ConfigV2ReferenceGit" + }, + { + "$ref": "#/components/schemas/ConfigV2ReferenceLocal" + } + ] + } }, "watcher": { "type": "object", @@ -26647,6 +28372,10 @@ "hide_prompt_training_models": { "type": "boolean" }, + "web_search": { + "type": "boolean", + "description": "Make web search available to models from all providers (default: false)" + }, "sandbox": { "type": "object", "properties": { @@ -26905,9 +28634,6 @@ "type": "boolean" } }, - "web_search": { - "type": "boolean" - }, "attachment": { "$ref": "#/components/schemas/AttachmentConfig" }, @@ -27164,7 +28890,7 @@ "properties": { "field": { "type": "string", - "enum": ["reasoning_content", "reasoning_details"] + "enum": ["reasoning", "reasoning_content", "reasoning_details"] } }, "required": ["field"], @@ -27809,6 +29535,10 @@ }, "diff": { "type": "string" + }, + "workspace": { + "type": "string", + "enum": ["restored", "snapshots-disabled", "unavailable"] } }, "required": ["messageID"], @@ -28494,27 +30224,6 @@ "required": ["_tag", "projectID", "message"], "additionalProperties": false }, - "ProjectCopyError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["ProjectCopyError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, "PtyNotFoundError": { "type": "object", "properties": { @@ -28977,6 +30686,10 @@ }, "diff": { "type": "string" + }, + "workspace": { + "type": "string", + "enum": ["restored", "snapshots-disabled", "unavailable"] } }, "required": ["messageID"], @@ -29146,6 +30859,10 @@ }, "diff": { "type": "string" + }, + "workspace": { + "type": "string", + "enum": ["restored", "snapshots-disabled", "unavailable"] } }, "required": ["messageID"], @@ -29174,25 +30891,6 @@ } } }, - "Todo": { - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "Brief description of the task" - }, - "status": { - "type": "string", - "description": "Current status of the task: pending, in_progress, completed, cancelled" - }, - "priority": { - "type": "string", - "description": "Priority level of the task: high, medium, low" - } - }, - "required": ["content", "status", "priority"], - "additionalProperties": false - }, "Session3": { "type": "object", "properties": { @@ -29353,6 +31051,10 @@ }, "diff": { "type": "string" + }, + "workspace": { + "type": "string", + "enum": ["restored", "snapshots-disabled", "unavailable"] } }, "required": ["messageID"], @@ -29522,6 +31224,10 @@ }, "diff": { "type": "string" + }, + "workspace": { + "type": "string", + "enum": ["restored", "snapshots-disabled", "unavailable"] } }, "required": ["messageID"], @@ -29691,6 +31397,10 @@ }, "diff": { "type": "string" + }, + "workspace": { + "type": "string", + "enum": ["restored", "snapshots-disabled", "unavailable"] } }, "required": ["messageID"], @@ -29860,6 +31570,10 @@ }, "diff": { "type": "string" + }, + "workspace": { + "type": "string", + "enum": ["restored", "snapshots-disabled", "unavailable"] } }, "required": ["messageID"], @@ -30029,6 +31743,10 @@ }, "diff": { "type": "string" + }, + "workspace": { + "type": "string", + "enum": ["restored", "snapshots-disabled", "unavailable"] } }, "required": ["messageID"], @@ -30360,6 +32078,10 @@ }, "diff": { "type": "string" + }, + "workspace": { + "type": "string", + "enum": ["restored", "snapshots-disabled", "unavailable"] } }, "required": ["messageID"], @@ -30529,6 +32251,10 @@ }, "diff": { "type": "string" + }, + "workspace": { + "type": "string", + "enum": ["restored", "snapshots-disabled", "unavailable"] } }, "required": ["messageID"], @@ -31425,6 +33151,16 @@ "required": ["_tag"], "additionalProperties": false }, + "CloudSessionImportError": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": ["error"], + "additionalProperties": false + }, "AgentRequirementResult": { "type": "object", "properties": { @@ -32361,7 +34097,7 @@ "required": ["_tag", "message"], "additionalProperties": false }, - "V2SessionsResponse": { + "SessionsResponse": { "type": "object", "properties": { "data": { @@ -32451,7 +34187,7 @@ "required": ["_tag", "message"], "additionalProperties": false }, - "V2SessionMessagesResponse": { + "SessionMessagesResponse": { "type": "object", "properties": { "data": { @@ -32493,6 +34229,30 @@ "required": ["_tag", "providerID", "message"], "additionalProperties": false }, + "ProjectCopyError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["ProjectCopyError"] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "forceRequired": { + "type": "boolean" + } + }, + "required": ["message"], + "additionalProperties": false + } + }, + "required": ["name", "data"], + "additionalProperties": false + }, "effect_HttpApiError_Forbidden": { "type": "object", "properties": { @@ -34539,6 +36299,182 @@ "body": { "type": "object" }, + "generation": { + "type": "object", + "properties": { + "maxTokens": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "temperature": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "topP": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "topK": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "frequencyPenalty": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "presencePenalty": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "seed": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "stop": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "options": { + "type": "object" + }, "variant": { "type": "string" } @@ -34562,6 +36498,182 @@ }, "body": { "type": "object" + }, + "generation": { + "type": "object", + "properties": { + "maxTokens": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "temperature": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "topP": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "topK": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "frequencyPenalty": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "presencePenalty": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "seed": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "stop": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "options": { + "type": "object" } }, "required": ["id", "headers", "body"], @@ -35092,41 +37204,6 @@ "required": ["name"], "additionalProperties": false }, - "PromptReferenceAttachment": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "kind": { - "type": "string", - "enum": ["local", "git", "invalid"] - }, - "uri": { - "type": "string" - }, - "repository": { - "type": "string" - }, - "branch": { - "type": "string" - }, - "target": { - "type": "string" - }, - "targetUri": { - "type": "string" - }, - "problem": { - "type": "string" - }, - "source": { - "$ref": "#/components/schemas/PromptSource" - } - }, - "required": ["name", "kind"], - "additionalProperties": false - }, "EventSessionNextPrompted": { "type": "object", "properties": { @@ -35243,6 +37320,34 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + "EventSessionNextInterruptRequested": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["session.next.interrupt.requested"] + }, + "properties": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "pattern": "^ses" + } + }, + "required": ["timestamp", "sessionID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "EventSessionNextContextUpdated": { "type": "object", "properties": { @@ -35984,51 +38089,8 @@ "type": "string", "enum": ["file"] }, - "source": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["data"] - }, - "data": { - "type": "string" - } - }, - "required": ["type", "data"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["url"] - }, - "url": { - "type": "string" - } - }, - "required": ["type", "url"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["file"] - }, - "uri": { - "type": "string" - } - }, - "required": ["type", "uri"], - "additionalProperties": false - } - ] + "uri": { + "type": "string" }, "mime": { "type": "string" @@ -36037,7 +38099,7 @@ "type": "string" } }, - "required": ["type", "source", "mime"], + "required": ["type", "uri", "mime"], "additionalProperties": false }, "EventSessionNextToolProgress": { @@ -36134,6 +38196,12 @@ ] } }, + "outputPaths": { + "type": "array", + "items": { + "type": "string" + } + }, "result": {}, "provider": { "type": "object", @@ -36335,11 +38403,15 @@ "type": "string", "pattern": "^ses" }, + "messageID": { + "type": "string", + "pattern": "^msg_" + }, "text": { "type": "string" } }, - "required": ["timestamp", "sessionID", "text"], + "required": ["timestamp", "sessionID", "messageID", "text"], "additionalProperties": false } }, @@ -36366,9 +38438,20 @@ "type": "string", "pattern": "^ses" }, + "messageID": { + "type": "string", + "pattern": "^msg_" + }, + "reason": { + "type": "string", + "enum": ["auto", "manual"] + }, "text": { "type": "string" }, + "recent": { + "type": "string" + }, "include": { "type": "string" } @@ -36644,6 +38727,9 @@ { "$ref": "#/components/schemas/ContextOverflowError" }, + { + "$ref": "#/components/schemas/ContentFilterError" + }, { "$ref": "#/components/schemas/APIError" }, @@ -36818,26 +38904,7 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, - "SessionTodoInfo": { - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "Brief description of the task" - }, - "status": { - "type": "string", - "description": "Current status of the task: pending, in_progress, completed, cancelled" - }, - "priority": { - "type": "string", - "description": "Priority level of the task: high, medium, low" - } - }, - "required": ["content", "status", "priority"], - "additionalProperties": false - }, - "EventTodoUpdated": { + "EventReferenceUpdated": { "type": "object", "properties": { "id": { @@ -36845,259 +38912,7 @@ }, "type": { "type": "string", - "enum": ["todo.updated"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "todos": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SessionTodoInfo" - } - } - }, - "required": ["sessionID", "todos"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionStatus": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["session.status"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "status": { - "$ref": "#/components/schemas/SessionStatus" - } - }, - "required": ["sessionID", "status"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionIdle": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["session.idle"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionCompacted": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["session.compacted"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventCommandExecuted": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["command.executed"] - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "arguments": { - "type": "string" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - } - }, - "required": ["name", "sessionID", "arguments", "messageID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventProjectDirectoriesUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["project.directories.updated"] - }, - "properties": { - "type": "object", - "properties": { - "projectID": { - "type": "string" - } - }, - "required": ["projectID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventProjectUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["project.updated"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "worktree": { - "type": "string" - }, - "vcs": { - "type": "string", - "enum": ["git"] - }, - "name": { - "type": "string" - }, - "icon": { - "type": "object", - "properties": { - "url": { - "type": "string" - }, - "override": { - "type": "string" - }, - "color": { - "type": "string" - } - }, - "additionalProperties": false - }, - "commands": { - "type": "object", - "properties": { - "start": { - "type": "string", - "description": "Startup script to run when creating a new workspace (worktree)" - } - }, - "additionalProperties": false - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "integer", - "minimum": 0 - }, - "updated": { - "type": "integer", - "minimum": 0 - }, - "initialized": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["created", "updated"], - "additionalProperties": false - }, - "sandboxes": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["id", "worktree", "time", "sandboxes"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventLspUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["lsp.updated"] + "enum": ["reference.updated"] }, "properties": { "type": "object", @@ -37107,7 +38922,7 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventFileEdited": { + "EventIntegrationUpdated": { "type": "object", "properties": { "id": { @@ -37115,347 +38930,11 @@ }, "type": { "type": "string", - "enum": ["file.edited"] + "enum": ["integration.updated"] }, "properties": { "type": "object", - "properties": { - "file": { - "type": "string" - } - }, - "required": ["file"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventFileWatcherUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["file.watcher.updated"] - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "event": { - "type": "string", - "enum": ["add", "change", "unlink"] - } - }, - "required": ["file", "event"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventVcsBranchUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["vcs.branch.updated"] - }, - "properties": { - "type": "object", - "properties": { - "branch": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventWorkspaceReady": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["workspace.ready"] - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": ["name"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventWorkspaceFailed": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["workspace.failed"] - }, - "properties": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventWorkspaceStatus": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["workspace.status"] - }, - "properties": { - "type": "object", - "properties": { - "workspaceID": { - "type": "string", - "pattern": "^wrk" - }, - "status": { - "type": "string", - "enum": ["connected", "connecting", "disconnected", "error"] - } - }, - "required": ["workspaceID", "status"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventWorktreeReady": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["worktree.ready"] - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "branch": { - "type": "string" - } - }, - "required": ["name"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventWorktreeFailed": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["worktree.failed"] - }, - "properties": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "AuthOAuthCredential": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["oauth"] - }, - "refresh": { - "type": "string" - }, - "access": { - "type": "string" - }, - "expires": { - "type": "integer", - "minimum": 0 - }, - "accountId": { - "type": "string" - } - }, - "required": ["type", "refresh", "access", "expires"], - "additionalProperties": false - }, - "AuthApiKeyCredential": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["api"] - }, - "key": { - "type": "string" - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "required": ["type", "key"], - "additionalProperties": false - }, - "AuthCredential": { - "anyOf": [ - { - "$ref": "#/components/schemas/AuthOAuthCredential" - }, - { - "$ref": "#/components/schemas/AuthApiKeyCredential" - } - ] - }, - "AuthInfo": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "serviceID": { - "type": "string" - }, - "description": { - "type": "string" - }, - "credential": { - "$ref": "#/components/schemas/AuthCredential" - } - }, - "required": ["id", "serviceID", "description", "credential"], - "additionalProperties": false - }, - "EventAccountAdded": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["account.added"] - }, - "properties": { - "type": "object", - "properties": { - "account": { - "$ref": "#/components/schemas/AuthInfo" - } - }, - "required": ["account"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventAccountRemoved": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["account.removed"] - }, - "properties": { - "type": "object", - "properties": { - "account": { - "$ref": "#/components/schemas/AuthInfo" - } - }, - "required": ["account"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventAccountSwitched": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["account.switched"] - }, - "properties": { - "type": "object", - "properties": { - "serviceID": { - "type": "string" - }, - "from": { - "type": "string" - }, - "to": { - "type": "string" - } - }, - "required": ["serviceID"], - "additionalProperties": false + "properties": {} } }, "required": ["id", "type", "properties"], @@ -37564,6 +39043,82 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + "EventProjectDirectoriesUpdated": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["project.directories.updated"] + }, + "properties": { + "type": "object", + "properties": { + "projectID": { + "type": "string" + } + }, + "required": ["projectID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventFileEdited": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["file.edited"] + }, + "properties": { + "type": "object", + "properties": { + "file": { + "type": "string" + } + }, + "required": ["file"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventFileWatcherUpdated": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["file.watcher.updated"] + }, + "properties": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "event": { + "type": "string", + "enum": ["add", "change", "unlink"] + } + }, + "required": ["file", "event"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "EventPtyCreated": { "type": "object", "properties": { @@ -37831,6 +39386,403 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + "EventTodoUpdated": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["todo.updated"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "todos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Todo" + } + } + }, + "required": ["sessionID", "todos"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventSessionStatus": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["session.status"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "status": { + "$ref": "#/components/schemas/SessionStatus" + } + }, + "required": ["sessionID", "status"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventSessionIdle": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["session.idle"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses" + } + }, + "required": ["sessionID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventSessionCompacted": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["session.compacted"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses" + } + }, + "required": ["sessionID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventCommandExecuted": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["command.executed"] + }, + "properties": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "arguments": { + "type": "string" + }, + "messageID": { + "type": "string", + "pattern": "^msg" + } + }, + "required": ["name", "sessionID", "arguments", "messageID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventProjectUpdated": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["project.updated"] + }, + "properties": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "worktree": { + "type": "string" + }, + "vcs": { + "type": "string", + "enum": ["git"] + }, + "name": { + "type": "string" + }, + "icon": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "override": { + "type": "string" + }, + "color": { + "type": "string" + } + }, + "additionalProperties": false + }, + "commands": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "Startup script to run when creating a new workspace (worktree)" + } + }, + "additionalProperties": false + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "integer", + "minimum": 0 + }, + "updated": { + "type": "integer", + "minimum": 0 + }, + "initialized": { + "type": "integer", + "minimum": 0 + } + }, + "required": ["created", "updated"], + "additionalProperties": false + }, + "sandboxes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["id", "worktree", "time", "sandboxes"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventLspUpdated": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["lsp.updated"] + }, + "properties": { + "type": "object", + "properties": {} + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventVcsBranchUpdated": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["vcs.branch.updated"] + }, + "properties": { + "type": "object", + "properties": { + "branch": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventWorkspaceReady": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["workspace.ready"] + }, + "properties": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventWorkspaceFailed": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["workspace.failed"] + }, + "properties": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventWorkspaceStatus": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["workspace.status"] + }, + "properties": { + "type": "object", + "properties": { + "workspaceID": { + "type": "string", + "pattern": "^wrk" + }, + "status": { + "type": "string", + "enum": ["connected", "connecting", "disconnected", "error"] + } + }, + "required": ["workspaceID", "status"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventWorktreeReady": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["worktree.ready"] + }, + "properties": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "branch": { + "type": "string" + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventWorktreeFailed": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["worktree.failed"] + }, + "properties": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "SyncEventSessionCreated": { "type": "object", "properties": { @@ -39589,6 +41541,12 @@ ] } }, + "outputPaths": { + "type": "array", + "items": { + "type": "string" + } + }, "result": {}, "provider": { "type": "object", @@ -39817,59 +41775,6 @@ "required": ["type", "id", "syncEvent"], "additionalProperties": false }, - "SyncEventSessionNextCompactionDelta": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.compaction.delta.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "text"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, "SyncEventSessionNextCompactionEnded": { "type": "object", "properties": { @@ -39908,9 +41813,20 @@ "type": "string", "pattern": "^ses" }, + "messageID": { + "type": "string", + "pattern": "^msg_" + }, + "reason": { + "type": "string", + "enum": ["auto", "manual"] + }, "text": { "type": "string" }, + "recent": { + "type": "string" + }, "include": { "type": "string" } @@ -39926,6 +41842,41 @@ "required": ["type", "id", "syncEvent"], "additionalProperties": false }, + "ConfigV2ReferenceGit": { + "type": "object", + "properties": { + "repository": { + "type": "string" + }, + "branch": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + } + }, + "required": ["repository"], + "additionalProperties": false + }, + "ConfigV2ReferenceLocal": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + } + }, + "required": ["path"], + "additionalProperties": false + }, "PolicyEffect": { "type": "string", "enum": ["allow", "deny"] @@ -39950,19 +41901,19 @@ "ProjectDirectories": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "strategy": { + "type": "string" + } + }, + "required": ["directory"], + "additionalProperties": false } }, - "ProjectCopyCopy": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } - }, - "required": ["directory"], - "additionalProperties": false - }, "LocationInfo": { "type": "object", "properties": { @@ -40327,12 +42278,6 @@ "$ref": "#/components/schemas/PromptAgentAttachment" } }, - "references": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptReferenceAttachment" - } - }, "type": { "type": "string", "enum": ["user"] @@ -40560,6 +42505,12 @@ ] } }, + "outputPaths": { + "type": "array", + "items": { + "type": "string" + } + }, "structured": { "type": "object" }, @@ -40806,7 +42757,7 @@ "summary": { "type": "string" }, - "include": { + "recent": { "type": "string" }, "id": { @@ -40827,7 +42778,7 @@ "additionalProperties": false } }, - "required": ["type", "reason", "summary", "id", "time"], + "required": ["type", "reason", "summary", "recent", "id", "time"], "additionalProperties": false }, "SessionMessage": { @@ -40892,13 +42843,13 @@ "properties": { "via": { "type": "string", - "enum": ["account"] + "enum": ["credential"] }, - "service": { + "credentialID": { "type": "string" } }, - "required": ["via", "service"], + "required": ["via", "credentialID"], "additionalProperties": false }, { @@ -40984,6 +42935,295 @@ "required": ["id", "name", "enabled", "env", "api", "request"], "additionalProperties": false }, + "IntegrationWhen": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "op": { + "type": "string", + "enum": ["eq", "neq"] + }, + "value": { + "type": "string" + } + }, + "required": ["key", "op", "value"], + "additionalProperties": false + }, + "IntegrationTextPrompt": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["text"] + }, + "key": { + "type": "string" + }, + "message": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "when": { + "$ref": "#/components/schemas/IntegrationWhen" + } + }, + "required": ["type", "key", "message"], + "additionalProperties": false + }, + "IntegrationSelectPrompt": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["select"] + }, + "key": { + "type": "string" + }, + "message": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + }, + "hint": { + "type": "string" + } + }, + "required": ["label", "value"], + "additionalProperties": false + } + }, + "when": { + "$ref": "#/components/schemas/IntegrationWhen" + } + }, + "required": ["type", "key", "message", "options"], + "additionalProperties": false + }, + "IntegrationOAuthMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["oauth"] + }, + "label": { + "type": "string" + }, + "prompts": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/IntegrationTextPrompt" + }, + { + "$ref": "#/components/schemas/IntegrationSelectPrompt" + } + ] + } + } + }, + "required": ["id", "type", "label"], + "additionalProperties": false + }, + "IntegrationKeyMethod": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["key"] + }, + "label": { + "type": "string" + } + }, + "required": ["type"], + "additionalProperties": false + }, + "IntegrationEnvMethod": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["env"] + }, + "names": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["type", "names"], + "additionalProperties": false + }, + "ConnectionCredentialInfo": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["credential"] + }, + "id": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": ["type", "id", "label"], + "additionalProperties": false + }, + "ConnectionEnvInfo": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["env"] + }, + "name": { + "type": "string" + } + }, + "required": ["type", "name"], + "additionalProperties": false + }, + "ConnectionInfo": { + "anyOf": [ + { + "$ref": "#/components/schemas/ConnectionCredentialInfo" + }, + { + "$ref": "#/components/schemas/ConnectionEnvInfo" + } + ] + }, + "IntegrationInfo": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "methods": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/IntegrationOAuthMethod" + }, + { + "$ref": "#/components/schemas/IntegrationKeyMethod" + }, + { + "$ref": "#/components/schemas/IntegrationEnvMethod" + } + ] + } + }, + "connections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConnectionInfo" + } + } + }, + "required": ["id", "name", "methods", "connections"], + "additionalProperties": false + }, + "IntegrationAttempt": { + "type": "object", + "properties": { + "attemptID": { + "type": "string" + }, + "url": { + "type": "string" + }, + "instructions": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": ["auto", "code"] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false + } + }, + "required": ["attemptID", "url", "instructions", "mode", "time"], + "additionalProperties": false + }, "PermissionV2Request": { "type": "object", "properties": { @@ -41039,53 +43279,12 @@ "required": ["id", "projectID", "action", "resource"], "additionalProperties": false }, - "FileSystemTextContent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["text"] - }, - "content": { - "type": "string" - }, - "mime": { - "type": "string" - } - }, - "required": ["type", "content", "mime"], - "additionalProperties": false - }, - "FileSystemBinaryContent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["binary"] - }, - "content": { - "type": "string" - }, - "encoding": { - "type": "string", - "enum": ["base64"] - }, - "mime": { - "type": "string" - } - }, - "required": ["type", "content", "encoding", "mime"], - "additionalProperties": false - }, "FileSystemEntry": { "type": "object", "properties": { "path": { "type": "string" }, - "uri": { - "type": "string" - }, "type": { "type": "string", "enum": ["file", "directory"] @@ -41094,7 +43293,7 @@ "type": "string" } }, - "required": ["path", "uri", "type", "mime"], + "required": ["path", "type", "mime"], "additionalProperties": false }, "CommandV2Info": { @@ -41196,6 +43395,88 @@ "required": ["answers"], "additionalProperties": false }, + "ReferenceLocalSource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["local"] + }, + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + } + }, + "required": ["type", "path"], + "additionalProperties": false + }, + "ReferenceGitSource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["git"] + }, + "repository": { + "type": "string" + }, + "branch": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + } + }, + "required": ["type", "repository"], + "additionalProperties": false + }, + "ReferenceInfo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + }, + "source": { + "anyOf": [ + { + "$ref": "#/components/schemas/ReferenceLocalSource" + }, + { + "$ref": "#/components/schemas/ReferenceGitSource" + } + ] + } + }, + "required": ["name", "path", "source"], + "additionalProperties": false + }, + "ProjectCopyCopy": { + "type": "object", + "properties": { + "directory": { + "type": "string" + } + }, + "required": ["directory"], + "additionalProperties": false + }, "EventMemoryStatus1": { "type": "object", "properties": { @@ -42189,6 +44470,154 @@ "body": { "type": "object" }, + "generation": { + "type": "object", + "properties": { + "maxTokens": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "temperature": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "topP": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "topK": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "frequencyPenalty": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "presencePenalty": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "seed": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "stop": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "options": { + "type": "object" + }, "variant": { "type": "string" } @@ -42212,6 +44641,154 @@ }, "body": { "type": "object" + }, + "generation": { + "type": "object", + "properties": { + "maxTokens": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "temperature": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "topP": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "topK": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "frequencyPenalty": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "presencePenalty": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "seed": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "stop": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "options": { + "type": "object" } }, "required": ["id", "headers", "body"], @@ -42431,7 +45008,7 @@ }, { "name": "projectCopy", - "description": "Project copy management routes." + "description": "Project copy naming routes." }, { "name": "pty", @@ -42542,64 +45119,72 @@ "description": "Kilo memory routes." }, { - "name": "kilo experimental HttpApi", + "name": "Kilo HttpApi", "description": "Experimental HttpApi surface for selected instance routes." }, { - "name": "kilo experimental HttpApi", + "name": "Kilo HttpApi", "description": "Experimental HttpApi surface for selected instance routes." }, { - "name": "v2", - "description": "Experimental v2 routes." + "name": "Kilo HttpApi", + "description": "Experimental HttpApi surface for selected instance routes." }, { - "name": "v2 messages", - "description": "Experimental v2 message routes." + "name": "sessions", + "description": "Experimental session routes." }, { - "name": "v2 models", - "description": "Experimental v2 model routes." + "name": "messages", + "description": "Experimental message routes." }, { - "name": "v2 providers", - "description": "Experimental v2 provider routes." + "name": "models", + "description": "Experimental model routes." }, { - "name": "v2 permissions", - "description": "Experimental v2 permission routes." + "name": "providers", + "description": "Experimental provider routes." }, { - "name": "v2 session permissions", - "description": "Experimental v2 session permission routes." + "name": "integrations", + "description": "Integration discovery and authentication routes." }, { - "name": "v2 saved permissions", - "description": "Experimental v2 saved permission routes." + "name": "Kilo HttpApi", + "description": "Experimental HttpApi surface for selected instance routes." }, { - "name": "v2 filesystem", - "description": "Experimental v2 location-scoped filesystem routes." + "name": "permissions", + "description": "Experimental permission routes." }, { - "name": "v2 commands", - "description": "Experimental v2 command routes." + "name": "filesystem", + "description": "Experimental location-scoped filesystem routes." }, { - "name": "v2 skills", - "description": "Experimental v2 skill routes." + "name": "commands", + "description": "Experimental command routes." }, { - "name": "v2 events", - "description": "Experimental v2 event stream route." + "name": "skills", + "description": "Experimental skill routes." }, { - "name": "v2 questions", - "description": "Experimental v2 question routes." + "name": "events", + "description": "Experimental event stream route." }, { - "name": "v2 session questions", - "description": "Experimental v2 session question routes." + "name": "session questions", + "description": "Experimental session question routes." + }, + { + "name": "reference", + "description": "Location-scoped project references." + }, + { + "name": "projectCopy", + "description": "Project copy management routes." }, { "name": "pty", From b71a9c0c234cb2fbb55712e4e995e261dde9fab5 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Mon, 27 Jul 2026 22:10:56 -0400 Subject: [PATCH 09/71] fix(vscode): include web search in config exports --- .../webview-ui/src/components/settings/settings-io.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/settings-io.ts b/packages/kilo-vscode/webview-ui/src/components/settings/settings-io.ts index d4b1949d2a9..be43b46284a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/settings-io.ts +++ b/packages/kilo-vscode/webview-ui/src/components/settings/settings-io.ts @@ -34,6 +34,7 @@ export const KNOWN_KEYS: ReadonlyArray = [ "compaction", "commit_message", "tools", + "web_search", "auto_collapse_reasoning", "terminal_command_display", "code_edit_display", From 2dba76605c6234f2e0f13bd1e475daed5b332a44 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Tue, 28 Jul 2026 06:52:48 -0400 Subject: [PATCH 10/71] docs: fix typo in NVIDIA BYOK note --- packages/kilo-docs/pages/getting-started/byok.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-docs/pages/getting-started/byok.md b/packages/kilo-docs/pages/getting-started/byok.md index 43f00eac318..55b27425448 100644 --- a/packages/kilo-docs/pages/getting-started/byok.md +++ b/packages/kilo-docs/pages/getting-started/byok.md @@ -92,7 +92,7 @@ Your IAM user or role must have the following permissions: Create an API key in the [NVIDIA API Catalog](https://build.nvidia.com/settings/api-keys), then add it as the NVIDIA provider. -Models that do not support tool calling are excluded, as they are unlikely to be provide useful results. +Models that do not support tool calling are excluded, as they are unlikely to provide useful results. {% callout type="warning" title="NVIDIA API Catalog terms" %} NVIDIA limits Developer Program endpoints to prototyping, research, development, and testing. Serving production end users may require NVIDIA AI Enterprise licensing. Your prompts and model outputs are sent to NVIDIA under your NVIDIA agreement. Review the [API Catalog quickstart](https://docs.api.nvidia.com/nim/docs/api-quickstart) and the [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). From b0a546049e7fcb212c8d1db344a48531ce65bed9 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 28 Jul 2026 13:42:15 +0200 Subject: [PATCH 11/71] refactor(cli): remove provably unused kilocode code Delete code with zero references anywhere in the monorepo (including tests, docs, scripts, and dynamic import or string-based usage): - background-process/windows-job.ts: entire Windows Job Object FFI module left unreferenced after the background process runner refactor - session/prompt.ts: createShellDecoders helper, CODE_SWITCH_TEXT alias, and the now-unused StringDecoder import - text-stream.ts: openUtf8 wrapper - cli/cmd/tui/component/prompt/vim.ts: enterInsert helper - config/config.ts: KILO_CONFIG_FILES, AGENT_PATTERNS, COMMAND_PATTERNS - remote-attachments.ts: TEXT_PLAIN constant - plan-followup.ts: PLAN_PREFIX constant - server/httpapi/groups/background-process.ts: SessionParams schema - server/httpapi/groups/session-import.ts: SessionImportPayloads map - cloud/contracts.ts: RepositoryInput type alias --- .../background-process/windows-job.ts | 88 ------------------- .../cli/cmd/tui/component/prompt/vim.ts | 5 -- .../opencode/src/kilocode/cloud/contracts.ts | 1 - .../opencode/src/kilocode/config/config.ts | 14 --- .../opencode/src/kilocode/plan-followup.ts | 1 - .../src/kilocode/remote-attachments.ts | 1 - .../httpapi/groups/background-process.ts | 1 - .../server/httpapi/groups/session-import.ts | 7 -- .../opencode/src/kilocode/session/prompt.ts | 26 ------ packages/opencode/src/kilocode/text-stream.ts | 5 -- 10 files changed, 149 deletions(-) delete mode 100644 packages/opencode/src/kilocode/background-process/windows-job.ts diff --git a/packages/opencode/src/kilocode/background-process/windows-job.ts b/packages/opencode/src/kilocode/background-process/windows-job.ts deleted file mode 100644 index 85d05b38e6f..00000000000 --- a/packages/opencode/src/kilocode/background-process/windows-job.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { dlopen, ptr } from "bun:ffi" - -export namespace WindowsJob { - const LIMITS = 9 - const MEMBERS = 3 - const KILL_ON_CLOSE = 0x00002000 - const PROCESS_TERMINATE = 0x0001 - const PROCESS_SET_QUOTA = 0x0100 - const MORE_DATA = 234 - - function kernel() { - return dlopen("kernel32.dll", { - CreateJobObjectW: { args: ["ptr", "ptr"], returns: "u64" }, - SetInformationJobObject: { args: ["u64", "u32", "ptr", "u32"], returns: "i32" }, - OpenProcess: { args: ["u32", "i32", "u32"], returns: "u64" }, - AssignProcessToJobObject: { args: ["u64", "u64"], returns: "i32" }, - QueryInformationJobObject: { args: ["u64", "u32", "ptr", "u32", "ptr"], returns: "i32" }, - TerminateJobObject: { args: ["u64", "u32"], returns: "i32" }, - CloseHandle: { args: ["u64"], returns: "i32" }, - GetLastError: { args: [], returns: "u32" }, - }) - } - - export function create() { - const lib = (() => { - try { - return kernel() - } catch { - return undefined - } - })() - if (!lib) return - const handle = lib.symbols.CreateJobObjectW(null, null) - if (handle === 0n) { - const code = lib.symbols.GetLastError() - lib.close() - throw new Error(`CreateJobObjectW failed with Windows error ${code}`) - } - const limits = new Uint8Array(144) - new DataView(limits.buffer).setUint32(16, KILL_ON_CLOSE, true) - if (lib.symbols.SetInformationJobObject(handle, LIMITS, ptr(limits), limits.byteLength) === 0) { - const code = lib.symbols.GetLastError() - lib.symbols.CloseHandle(handle) - lib.close() - throw new Error(`SetInformationJobObject failed with Windows error ${code}`) - } - let closed = false - return { - assign(pid: number) { - const proc = lib.symbols.OpenProcess(PROCESS_TERMINATE | PROCESS_SET_QUOTA, 0, pid) - if (proc === 0n) throw new Error(`OpenProcess failed with Windows error ${lib.symbols.GetLastError()}`) - const assigned = lib.symbols.AssignProcessToJobObject(handle, proc) - const code = assigned === 0 ? lib.symbols.GetLastError() : 0 - lib.symbols.CloseHandle(proc) - if (assigned === 0) throw new Error(`AssignProcessToJobObject failed with Windows error ${code}`) - }, - members() { - let size = 4 * 1024 - while (true) { - const info = new Uint8Array(size) - const ok = lib.symbols.QueryInformationJobObject(handle, MEMBERS, ptr(info), info.byteLength, null) - const code = ok === 0 ? lib.symbols.GetLastError() : 0 - const view = new DataView(info.buffer) - const assigned = view.getUint32(0, true) - const count = view.getUint32(4, true) - if (ok !== 0 && count === assigned) { - return Array.from({ length: count }, (_, index) => Number(view.getBigUint64(8 + index * 8, true))) - } - if (ok === 0 && code !== MORE_DATA) { - throw new Error(`QueryInformationJobObject failed with Windows error ${code}`) - } - size = Math.max(size * 2, 8 + assigned * 8) - } - }, - terminate() { - if (lib.symbols.TerminateJobObject(handle, 1) === 0) { - throw new Error(`TerminateJobObject failed with Windows error ${lib.symbols.GetLastError()}`) - } - }, - close() { - if (closed) return - closed = true - lib.symbols.CloseHandle(handle) - lib.close() - }, - } - } -} diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/component/prompt/vim.ts b/packages/opencode/src/kilocode/cli/cmd/tui/component/prompt/vim.ts index 2b91606c86c..020d0be3793 100644 --- a/packages/opencode/src/kilocode/cli/cmd/tui/component/prompt/vim.ts +++ b/packages/opencode/src/kilocode/cli/cmd/tui/component/prompt/vim.ts @@ -877,8 +877,3 @@ export function handleVisualKey(doc: VimDoc, state: VimState, input: VimKey): Vi state.countDigits = "" return { handled: true } } - -export function enterInsert(state: VimState) { - state.mode = "insert" - resetPending(state) -} diff --git a/packages/opencode/src/kilocode/cloud/contracts.ts b/packages/opencode/src/kilocode/cloud/contracts.ts index a4716e547eb..a78b846b8cb 100644 --- a/packages/opencode/src/kilocode/cloud/contracts.ts +++ b/packages/opencode/src/kilocode/cloud/contracts.ts @@ -163,7 +163,6 @@ export const GetMessageResultOutputSchema = z } }) -export type RepositoryInput = z.infer export type AgentStartRequest = z.infer export type AgentSendRequest = z.infer export type GetMessageResultInput = z.infer diff --git a/packages/opencode/src/kilocode/config/config.ts b/packages/opencode/src/kilocode/config/config.ts index a33445494b8..608386d4fad 100644 --- a/packages/opencode/src/kilocode/config/config.ts +++ b/packages/opencode/src/kilocode/config/config.ts @@ -37,26 +37,12 @@ export namespace KilocodeConfig { // ── Config file constants ──────────────────────────────────────────── - /** Kilo-specific config file names (highest-to-lowest precedence within kilo). */ - export const KILO_CONFIG_FILES = ["kilo.jsonc", "kilo.json"] as const - /** All config file names in precedence order (kilo + opencode). */ export const ALL_CONFIG_FILES = ["kilo.jsonc", "kilo.json", "opencode.jsonc", "opencode.json"] as const /** Config directory suffixes in update-target preference order. */ export const KILO_DIR_SUFFIXES = [".kilo", ".kilocode"] as const - /** Path patterns for resolving kilo agent names from file paths. */ - export const AGENT_PATTERNS = ["/.kilo/agent/", "/.kilo/agents/", "/.kilocode/agent/", "/.kilocode/agents/"] as const - - /** Path patterns for resolving kilo command names from file paths. */ - export const COMMAND_PATTERNS = [ - "/.kilo/command/", - "/.kilo/commands/", - "/.kilocode/command/", - "/.kilocode/commands/", - ] as const - /** * Choose the project config file that Config.update should patch. * diff --git a/packages/opencode/src/kilocode/plan-followup.ts b/packages/opencode/src/kilocode/plan-followup.ts index 01de704a546..a0c726f94ca 100644 --- a/packages/opencode/src/kilocode/plan-followup.ts +++ b/packages/opencode/src/kilocode/plan-followup.ts @@ -157,7 +157,6 @@ export async function generateHandover(input: { export namespace PlanFollowup { const log = Log.create({ service: "plan.followup" }) - export const PLAN_PREFIX = "Implement the following plan:" export const ANSWER_NEW_SESSION = "Start new session" export const ANSWER_CONTINUE = "Continue here" export const ANSWER_KEEP_REFINING = "Keep refining" diff --git a/packages/opencode/src/kilocode/remote-attachments.ts b/packages/opencode/src/kilocode/remote-attachments.ts index 9008065cc90..b329dbb47d2 100644 --- a/packages/opencode/src/kilocode/remote-attachments.ts +++ b/packages/opencode/src/kilocode/remote-attachments.ts @@ -66,7 +66,6 @@ export namespace RemoteAttachments { sql: "text/plain", } export const BINARY_MIME = "application/octet-stream" - export const TEXT_PLAIN = "text/plain" // Hard cap on attachment bytes (5 MB + 1 byte so the helper aborts // strictly when the body exceeds the agreed ceiling). export const MAX_BYTES = 5 * 1024 * 1024 + 1 diff --git a/packages/opencode/src/kilocode/server/httpapi/groups/background-process.ts b/packages/opencode/src/kilocode/server/httpapi/groups/background-process.ts index e5b0dbc2dc7..208cd1f0215 100644 --- a/packages/opencode/src/kilocode/server/httpapi/groups/background-process.ts +++ b/packages/opencode/src/kilocode/server/httpapi/groups/background-process.ts @@ -22,7 +22,6 @@ export const BackgroundProcessPaths = { } as const export const Params = Schema.Struct({ processID: BackgroundProcess.ID }) -export const SessionParams = Schema.Struct({ sessionID: SessionID }) export const BackgroundProcessApi = HttpApi.make("background-process") .add( diff --git a/packages/opencode/src/kilocode/server/httpapi/groups/session-import.ts b/packages/opencode/src/kilocode/server/httpapi/groups/session-import.ts index 596ca4f57d6..9aca9d4581f 100644 --- a/packages/opencode/src/kilocode/server/httpapi/groups/session-import.ts +++ b/packages/opencode/src/kilocode/server/httpapi/groups/session-import.ts @@ -219,13 +219,6 @@ export const SessionImportPaths = { part: `${root}/part`, } as const -export const SessionImportPayloads = { - Project: ProjectSchema, - Session: SessionSchema, - Message: MessageSchema, - Part: PartSchema, -} as const - export const SessionImportApi = HttpApi.make("session-import") .add( HttpApiGroup.make("session-import") diff --git a/packages/opencode/src/kilocode/session/prompt.ts b/packages/opencode/src/kilocode/session/prompt.ts index a1fa29b8064..ee05f453581 100644 --- a/packages/opencode/src/kilocode/session/prompt.ts +++ b/packages/opencode/src/kilocode/session/prompt.ts @@ -1,7 +1,6 @@ // kilocode_change - new file import path from "path" import fs from "fs/promises" -import { StringDecoder } from "string_decoder" import { Cause, Effect, Exit, Fiber, Scope } from "effect" import { SessionID, PartID } from "@/session/schema" import { MessageV2 } from "@/session/message-v2" @@ -377,25 +376,6 @@ export namespace KiloSessionPrompt { } } - /** - * Creates StringDecoder-based helpers for shell stdout/stderr that correctly - * handle multi-byte UTF-8 characters split across chunks. - */ - export function createShellDecoders() { - const stdout = new StringDecoder("utf8") - const stderr = new StringDecoder("utf8") - return { - /** Decode a chunk from the given stream. */ - write(stream: "stdout" | "stderr", chunk: Buffer) { - return stream === "stdout" ? stdout.write(chunk) : stderr.write(chunk) - }, - /** Flush any trailing buffered bytes from both decoders. */ - flush() { - return stdout.end() + stderr.end() - }, - } - } - /** * Ensures the plan file directory exists. Pre-checks with `Filesystem.isDir` * because `fs.mkdir(recursive: true)` still throws `EEXIST` on Windows @@ -455,12 +435,6 @@ export namespace KiloSessionPrompt { add(`\n${body}\n`) } - /** - * Returns the CODE_SWITCH prompt text (plan-to-code transition). - * Used when switching from plan agent to code agent. - */ - export const CODE_SWITCH_TEXT = CODE_SWITCH - /** * Determines the close reason for a session turn. * Checks for an explicit reason first (e.g. set on error during runLoop), diff --git a/packages/opencode/src/kilocode/text-stream.ts b/packages/opencode/src/kilocode/text-stream.ts index 82f2f6cd7c2..bfaabd164eb 100644 --- a/packages/opencode/src/kilocode/text-stream.ts +++ b/packages/opencode/src/kilocode/text-stream.ts @@ -62,11 +62,6 @@ export function abortable(stream: Readable, signal?: AbortSignal) { return signal ? addAbortSignal(signal, stream) : stream } -/** UTF-8 text stream backed by an already-open file. */ -export function openUtf8(open: () => Readable, signal?: AbortSignal): Readable { - return utf8(open, signal).stream -} - export function safeSlice(text: string, end: number) { const sliced = text.slice(0, end) const last = sliced.charCodeAt(sliced.length - 1) From c08f37e88b7d063429fba5b164e31c94d1935610 Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Tue, 28 Jul 2026 15:26:14 +0200 Subject: [PATCH 12/71] feat(cli): execute shell commands in skill files with batch approval --- packages/opencode/src/cli/cmd/run.ts | 6 + packages/opencode/src/effect/runtime-flags.ts | 1 + .../opencode/src/kilocode/permission/drain.ts | 2 + .../opencode/src/kilocode/skills/inject.ts | 78 +++++++ packages/opencode/src/permission/index.ts | 13 ++ packages/opencode/src/skill/index.ts | 3 + packages/opencode/src/tool/skill.ts | 21 +- .../kilocode/permission/skill-shell.test.ts | 127 +++++++++++ .../test/kilocode/skills/inject.test.ts | 197 ++++++++++++++++++ .../tui/src/routes/session/permission.tsx | 24 ++- 10 files changed, 466 insertions(+), 6 deletions(-) create mode 100644 packages/opencode/src/kilocode/skills/inject.ts create mode 100644 packages/opencode/test/kilocode/permission/skill-shell.test.ts create mode 100644 packages/opencode/test/kilocode/skills/inject.test.ts diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 56241ace1a1..d77916109ed 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -840,6 +840,12 @@ export const RunCommand = effectCmd({ if (event.type === "permission.asked") { const permission = event.properties + // kilocode_change start - skill shell batches need an interactive human decision; never headless auto-approve + if (permission.metadata?.["skillShell"] === true) { + await client.permission.reply({ requestID: permission.id, reply: "reject" }) + continue + } + // kilocode_change end // kilocode_change start - approve root and tracked Task child permissions in auto mode if (args.auto) { if (!KiloRunAuto.allowed(auto, permission.sessionID)) continue diff --git a/packages/opencode/src/effect/runtime-flags.ts b/packages/opencode/src/effect/runtime-flags.ts index 188dc4869a0..f1bf3edb26c 100644 --- a/packages/opencode/src/effect/runtime-flags.ts +++ b/packages/opencode/src/effect/runtime-flags.ts @@ -20,6 +20,7 @@ export class Service extends ConfigService.Service()("@opencode/Runtime disableChannelDb: bool("KILO_DISABLE_CHANNEL_DB"), // kilocode_change disableEmbeddedWebUi: bool("KILO_DISABLE_EMBEDDED_WEB_UI"), disableExternalSkills: bool("KILO_DISABLE_EXTERNAL_SKILLS"), + disableSkillShell: bool("KILO_DISABLE_SKILL_SHELL"), // kilocode_change - disable shell injection in skill bodies disableLspDownload: bool("KILO_DISABLE_LSP_DOWNLOAD"), skipMigrations: bool("KILO_SKIP_MIGRATIONS"), // kilocode_change disableClaudeCodePrompt: Config.all({ diff --git a/packages/opencode/src/kilocode/permission/drain.ts b/packages/opencode/src/kilocode/permission/drain.ts index 78f960c867e..499f4f83e1e 100644 --- a/packages/opencode/src/kilocode/permission/drain.ts +++ b/packages/opencode/src/kilocode/permission/drain.ts @@ -33,6 +33,8 @@ export function drainCovered( // Never auto-resolve config file edit permissions const skill = ConfigProtection.globalSkillPattern(entry.info) if (ConfigProtection.isRequest(entry.info) && !skill) continue + // Never auto-resolve a skill shell batch; it must get an explicit reply. + if (entry.info.metadata?.["skillShell"] === true) continue const actions = entry.info.patterns.map((pattern: string) => { const rule = skill ? Permission.evaluate(entry.info.permission, skill, approved) diff --git a/packages/opencode/src/kilocode/skills/inject.ts b/packages/opencode/src/kilocode/skills/inject.ts new file mode 100644 index 00000000000..ceee78c7427 --- /dev/null +++ b/packages/opencode/src/kilocode/skills/inject.ts @@ -0,0 +1,78 @@ +import { Effect } from "effect" +import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" +import type { ChildProcessSpawner as Spawner } from "effect/unstable/process/ChildProcessSpawner" +import { ConfigMarkdown } from "@/config/markdown" +import { CommandTimeout } from "@/kilocode/command-timeout" +import { Shell } from "@opencode-ai/core/shell" +import type * as Tool from "@/tool/tool" + +// Shell injection for skill bodies mirrors Claude's "dynamic context injection": +// a `!`cmd`` placeholder in SKILL.md is replaced by the command's stdout before +// the content reaches the model. Unlike the slash-command path, this runs for +// model-initiated skill loads, so it is gated on three independent controls: +// +// 1. Trust: only skills from trusted sources (global ~/.claude, ~/.agents, +// KILO_CONFIG_DIR, and builtins) may execute. Untrusted project/downloaded +// skills never spawn a process. +// 2. Kill-switch: `disabled` (KILO_DISABLE_SKILL_SHELL) turns injection off +// entirely, matching Claude's disableSkillShellExecution. +// 3. Batch approval: every command in the file is presented once, up front, in +// a single permission prompt (the `skillShell` metadata marker forces this +// prompt regardless of any allow/deny/auto-approve rule). Approve runs the +// whole batch; reject aborts the skill load with nothing run. +// +// Substitution runs exactly once. Command output is inlined as plain text and is +// never re-scanned, so a command cannot emit a `!`cmd`` placeholder that a later +// pass would execute (second-order injection). + +const DISABLED_NOTE = "[skill shell execution disabled by policy]" +const UNTRUSTED_NOTE = "[skill shell execution disabled for untrusted skill]" + +export namespace SkillInject { + export type Options = { + content: string + trusted: boolean + disabled: boolean + ctx: Tool.Context + spawner: Spawner["Service"] + } + + export const render = Effect.fn("SkillInject.render")(function* (opts: Options) { + const matches = ConfigMarkdown.shell(opts.content) + if (matches.length === 0) return opts.content + + // Defense-in-depth ordering: policy checks first, approval gate last. + if (opts.disabled) return replace(opts.content, () => DISABLED_NOTE) + if (!opts.trusted) return replace(opts.content, () => UNTRUSTED_NOTE) + + // Deduplicate identical commands so the batch lists and runs each once. + const commands = Array.from(new Set(matches.map(([, cmd]) => cmd))) + + // Single up-front approval for the whole batch. `skillShell` forces one + // prompt even when rules would allow or deny; a reject/deny propagates as a + // defect and aborts the skill load without running anything. + yield* opts.ctx.ask({ + permission: "bash", + patterns: commands, + always: [], + metadata: { skillShell: true }, + }) + + const shell = Shell.preferred() + const outputs = new Map() + for (const command of commands) { + outputs.set( + command, + yield* CommandTimeout.text(command, shell).pipe(Effect.provideService(ChildProcessSpawner, opts.spawner)), + ) + } + + return replace(opts.content, (command) => outputs.get(command) ?? "") + }) + + // Replace only the exact matches found in the ORIGINAL content. Never re-scan + // the result, so inlined output containing `!`cmd`` stays inert. + function replace(content: string, value: (command: string) => string) { + return content.replace(ConfigMarkdown.SHELL_REGEX, (_, command: string) => value(command)) + } +} diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index f15feff5632..d5217693d20 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -155,6 +155,7 @@ function subset(permission: string, ruleset: Ruleset) { function covered(entry: PendingEntry, approved: Ruleset, local: Ruleset) { if (ConfigProtection.isRequest(entry.info)) return false + if (entry.info.metadata?.["skillShell"] === true) return false // kilocode_change - skill batch needs an explicit reply return entry.info.patterns.every((pattern) => { if (veto(entry.info.permission, pattern, entry.hardRuleset)) return false return resolve(entry.info.permission, pattern, entry.ruleset, approved, local).action === "allow" @@ -221,7 +222,19 @@ export const layer = Layer.effect( : false // kilocode_change end + // kilocode_change start - skill shell injection always prompts once, overriding every allow/deny/auto-approve rule + const forceAsk = request.metadata?.["skillShell"] === true + // kilocode_change end for (const pattern of request.patterns) { + // kilocode_change start - force a prompt over soft allow/deny rules, but never over a hard (plan-mode) veto + if (forceAsk) { + if (veto(request.permission, pattern, hardRuleset)) { + return yield* new DeniedError({ ruleset: subset(request.permission, hardRuleset ?? []) }) + } + needsAsk = true + continue + } + // kilocode_change end const rule = resolve(request.permission, pattern, ruleset, approved, local) // kilocode_change — include session-scoped rules yield* Effect.logInfo("evaluated", { permission: request.permission, pattern, action: rule }) // kilocode_change start — saved/session approvals cannot override hard Ask/Plan denials diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index 2477093d9fb..1b5f4241cc2 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -35,6 +35,7 @@ export const Info = Schema.Struct({ description: Schema.optional(Schema.String), location: Schema.String, content: Schema.String, + trusted: Schema.optional(Schema.Boolean), // kilocode_change - gate skill shell injection to trusted sources }) export type Info = Schema.Schema.Type @@ -151,6 +152,7 @@ const add = Effect.fnUntraced(function* (state: State, match: Match, events: Eve description: md.data.description, location: match.path, // kilocode_change content: md.content, + trusted: match.trusted, // kilocode_change } }) @@ -295,6 +297,7 @@ const loadSkills = Effect.fnUntraced(function* ( description: skill.description, location: BUILTIN_LOCATION, content: skill.content, + trusted: true, // kilocode_change - builtin skills ship in the binary } } // kilocode_change end diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index 4b849895048..ad2049afc2a 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -5,6 +5,11 @@ import { Ripgrep } from "@opencode-ai/core/ripgrep" import { Skill } from "../skill" import * as Tool from "./tool" import DESCRIPTION from "./skill.txt" +// kilocode_change start - gate + run shell injection in skill bodies +import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" +import { RuntimeFlags } from "@/effect/runtime-flags" +import { SkillInject } from "@/kilocode/skills/inject" +// kilocode_change end export const Parameters = Schema.Struct({ name: Schema.String.annotate({ description: "The name of the skill from available_skills" }), @@ -15,6 +20,8 @@ export const SkillTool = Tool.define( Effect.gen(function* () { const skill = yield* Skill.Service const ripgrep = yield* Ripgrep.Service + const flags = yield* RuntimeFlags.Service // kilocode_change + const spawner = yield* ChildProcessSpawner // kilocode_change return { description: DESCRIPTION, @@ -32,6 +39,16 @@ export const SkillTool = Tool.define( metadata: {}, }) + // kilocode_change start - render `!`cmd`` shell injection, gated by trust + kill-switch + batch approval + const content = yield* SkillInject.render({ + content: info.content, + trusted: info.trusted === true, + disabled: flags.disableSkillShell, + ctx, + spawner, + }) + // kilocode_change end + // kilocode_change start - built-in skills have no filesystem directory if (info.location === Skill.BUILTIN_LOCATION) { return { @@ -40,7 +57,7 @@ export const SkillTool = Tool.define( ``, `# Skill: ${info.name}`, "", - info.content.trim(), + content.trim(), // kilocode_change "", ].join("\n"), metadata: { @@ -68,7 +85,7 @@ export const SkillTool = Tool.define( ``, `# Skill: ${info.name}`, "", - info.content.trim(), + content.trim(), // kilocode_change "", `Base directory for this skill: ${base}`, "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.", diff --git a/packages/opencode/test/kilocode/permission/skill-shell.test.ts b/packages/opencode/test/kilocode/permission/skill-shell.test.ts new file mode 100644 index 00000000000..d6b1fa79811 --- /dev/null +++ b/packages/opencode/test/kilocode/permission/skill-shell.test.ts @@ -0,0 +1,127 @@ +import { PermissionV1 } from "@opencode-ai/core/v1/permission" +import { expect } from "bun:test" +import { Cause, Effect, Exit, Fiber, Layer } from "effect" +import { EventV2Bridge } from "@/event-v2-bridge" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { Database } from "@opencode-ai/core/database/database" +import { Permission } from "@/permission" +import { InstanceBootstrap } from "@/project/bootstrap-service" +import { InstanceStore } from "@/project/instance-store" +import { testEffect } from "../../lib/effect" +import { SessionID } from "@/session/schema" +import { RuntimeFlags } from "@/effect/runtime-flags" +import { Config } from "@/config/config" + +// skillShell forces a single up-front prompt over soft allow/deny/auto-approve +// rules, but must never override a hard (plan-mode) veto, and must never be +// auto-resolved while pending. + +const events = EventV2Bridge.defaultLayer +const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void })) +const env = Layer.mergeAll( + Permission.layer.pipe(Layer.provide(Database.defaultLayer), Layer.provide(events)), + events, + CrossSpawnSpawner.defaultLayer, + InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap)), +).pipe(Layer.provide(RuntimeFlags.layer()), Layer.provide(Config.defaultLayer)) +const it = testEffect(Layer.mergeAll(env, RuntimeFlags.layer())) + +const ask = (input: Parameters[0]) => + Effect.gen(function* () { + return yield* (yield* Permission.Service).ask(input) + }) + +const list = () => + Effect.gen(function* () { + return yield* (yield* Permission.Service).list() + }) + +const rejectAll = () => + Effect.gen(function* () { + const permission = yield* Permission.Service + for (const req of yield* permission.list()) yield* permission.reply({ requestID: req.id, reply: "reject" }) + }) + +const waitForPending = (count: number) => + Effect.gen(function* () { + const permission = yield* Permission.Service + return yield* Effect.gen(function* () { + while (true) { + const pending = yield* permission.list() + if (pending.length === count) return pending + yield* Effect.sleep("10 millis") + } + }).pipe(Effect.timeoutOrElse({ duration: "1 second", orElse: () => Effect.fail(new Error("timed out")) })) + }) + +const fail = (self: Effect.Effect) => + Effect.gen(function* () { + const exit = yield* self.pipe(Effect.exit) + if (Exit.isFailure(exit)) return Cause.squash(exit.cause) + throw new Error("expected permission effect to fail") + }) + +it.instance( + "skillShell - forces a prompt even when a matching allow rule exists", + () => + Effect.gen(function* () { + const fiber = yield* ask({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["printf hi"], + metadata: { skillShell: true }, + always: [], + ruleset: [{ permission: "bash", pattern: "*", action: "allow" }], + }).pipe(Effect.forkScoped) + + expect(yield* waitForPending(1)).toHaveLength(1) + yield* rejectAll() + yield* Fiber.await(fiber) + }), + { git: true }, +) + +it.instance( + "skillShell - is denied by a hard-ruleset veto instead of prompting", + () => + Effect.gen(function* () { + const err = yield* fail( + ask({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["rm -rf /"], + metadata: { skillShell: true }, + always: [], + ruleset: [{ permission: "bash", pattern: "*", action: "allow" }], + hardRuleset: [{ permission: "bash", pattern: "*", action: "deny" }], + }), + ) + + expect(err).toBeInstanceOf(PermissionV1.DeniedError) + expect(yield* list()).toHaveLength(0) + }), + { git: true }, +) + +it.instance( + "skillShell - a pending batch is not auto-resolved by allowEverything", + () => + Effect.gen(function* () { + const fiber = yield* ask({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["printf hi"], + metadata: { skillShell: true }, + always: [], + ruleset: [], + }).pipe(Effect.forkScoped) + + expect(yield* waitForPending(1)).toHaveLength(1) + yield* (yield* Permission.Service).allowEverything({ enable: true }) + // still pending: YOLO cannot silently approve a skill batch + expect(yield* list()).toHaveLength(1) + yield* rejectAll() + yield* Fiber.await(fiber) + }), + { git: true }, +) diff --git a/packages/opencode/test/kilocode/skills/inject.test.ts b/packages/opencode/test/kilocode/skills/inject.test.ts new file mode 100644 index 00000000000..1058968d1e3 --- /dev/null +++ b/packages/opencode/test/kilocode/skills/inject.test.ts @@ -0,0 +1,197 @@ +import { PermissionV1 } from "@opencode-ai/core/v1/permission" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { Ripgrep } from "@opencode-ai/core/ripgrep" +import { Effect, Exit, Layer } from "effect" +import { afterEach, describe, expect } from "bun:test" +import fs from "fs" +import os from "os" +import path from "path" +import type { Tool } from "@/tool/tool" +import { SkillTool } from "@/tool/skill" +import { SkillInject } from "@/kilocode/skills/inject" +import { ToolRegistry } from "@/tool/registry" +import { disposeAllInstances, TestInstance } from "../../fixture/fixture" +import { SessionID, MessageID } from "@/session/schema" +import { testEffect } from "../../lib/effect" + +// Global (~/.claude) skills are trusted, but Global.Service snapshots the home +// path when its layer is built, so KILO_TEST_HOME must be set before the runtime +// layer below is constructed — not inside a test body. +const HOME = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "skill-inject-home-"))) +process.env.KILO_TEST_HOME = HOME + +const baseCtx: Omit = { + sessionID: SessionID.make("ses_test"), + messageID: MessageID.make("msg_test"), + callID: "", + agent: "build", + abort: AbortSignal.any([]), + messages: [], + metadata: () => Effect.void, +} + +afterEach(async () => { + await disposeAllInstances() +}) + +const it = testEffect( + Layer.mergeAll(ToolRegistry.defaultLayer, CrossSpawnSpawner.defaultLayer).pipe(Layer.provide(Ripgrep.defaultLayer)), +) + +// Shell injection spawns real processes; skip on windows CI like the sibling suite. +const unix = process.platform !== "win32" ? it.instance : it.instance.skip + +afterEach(async () => { + // reset discovered skills between tests by clearing the global skill dir + await fs.promises.rm(path.join(HOME, ".agents"), { recursive: true, force: true }) +}) + +// Global ~/.agents skills are trusted (and, unlike ~/.claude, not gated by the +// KILO_DISABLE_CLAUDE_CODE flag the test env sets); project .kilo skills are untrusted. +function writeGlobalSkill(name: string, body: string) { + return Effect.promise(() => + Bun.write( + path.join(HOME, ".agents", "skills", name, "SKILL.md"), + `---\nname: ${name}\ndescription: ${name} test skill.\n---\n\n${body}\n`, + ), + ) +} + +function writeProjectSkill(dir: string, name: string, body: string) { + return Effect.promise(() => + Bun.write( + path.join(dir, ".kilo", "skill", name, "SKILL.md"), + `---\nname: ${name}\ndescription: ${name} test skill.\n---\n\n${body}\n`, + ), + ) +} + +function loadSkill(name: string, ask: Tool.Context["ask"]) { + return Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const agent = { name: "build", mode: "primary" as const, permission: [], options: {} } + const tool = (yield* registry.tools({ + providerID: "opencode" as any, + modelID: "gpt-5" as any, + agent, + })).find((t) => t.id === SkillTool.id) + if (!tool) throw new Error("Skill tool not found") + return yield* tool.execute({ name }, { ...baseCtx, ask }) + }) +} + +describe("skill shell injection", () => { + unix("runs the batch after a single forced approval listing every command", () => + Effect.gen(function* () { + yield* writeGlobalSkill("trusted-shell", "A: !`printf one` B: !`printf two`") + + const requests: Array> = [] + const result = yield* loadSkill("trusted-shell", (req) => + Effect.sync(() => { + requests.push(req) + }), + ) + + expect(result.output).toContain("A: one B: two") + // one skill-load ask plus exactly one batch bash ask carrying all commands + const bash = requests.filter((r) => r.permission === "bash") + expect(bash.length).toBe(1) + expect(bash[0].metadata?.["skillShell"]).toBe(true) + // patterns carry the command list the prompt renders + expect(bash[0].patterns).toEqual(["printf one", "printf two"]) + }), + ) + + unix("aborts the entire skill load when the batch is rejected", () => + Effect.gen(function* () { + yield* writeGlobalSkill("denied-shell", "Secret: !`printf leaked`") + + const exit = yield* loadSkill("denied-shell", (req) => + // Reject the batch. Tools wrap ctx.ask with Effect.orDie, so this reaches + // the injector as a defect and must abort the whole skill load. + req.permission === "bash" + ? Effect.fail(new PermissionV1.RejectedError()).pipe(Effect.orDie) + : Effect.void, + ).pipe(Effect.exit) + + expect(Exit.isFailure(exit)).toBe(true) + }), + ) + + unix("does not run shell injection for untrusted project skills", () => + Effect.gen(function* () { + const dir = (yield* TestInstance).directory + yield* writeProjectSkill(dir, "untrusted-shell", "Value: !`printf shouldnotrun`") + + const requests: Array> = [] + const result = yield* loadSkill("untrusted-shell", (req) => + Effect.sync(() => { + requests.push(req) + }), + ) + + expect(result.output).toContain("[skill shell execution disabled for untrusted skill]") + expect(result.output).not.toContain("shouldnotrun") + // no bash permission ask because nothing was scanned or spawned + expect(requests.some((r) => r.permission === "bash")).toBe(false) + }), + ) + + unix("does not re-execute shell placeholders emitted by command output", () => + Effect.gen(function* () { + // The command emits a literal placeholder `!echo pwned` + // built from octal escapes so the SKILL.md itself contains no nested + // backticks. If render re-scanned command output, `echo pwned` would run. + yield* writeGlobalSkill("nested-shell", "Out: !`printf '!\\140echo pwned\\140'`") + + const result = yield* loadSkill("nested-shell", () => Effect.void) + + expect(result.output).toContain("!`echo pwned`") + // "pwned" must appear only inside the inert placeholder, never executed alone. + expect(result.output).not.toMatch(/Out:\s*pwned\s*$/m) + }), + ) +}) + +// The disabled (kill-switch) and untrusted branches must short-circuit before +// asking permission or spawning. Passing an ask/spawner that throws proves +// neither is reached. +describe("SkillInject.render gating", () => { + const boom = () => { + throw new Error("must not be reached") + } + const spawner = new Proxy({}, { get: boom }) as any + const ctx = { ...baseCtx, ask: () => Effect.sync(boom) } as Tool.Context + + const run = (opts: { trusted: boolean; disabled: boolean; content?: string }) => + Effect.runPromise( + SkillInject.render({ + content: opts.content ?? "Value: !`printf ran`", + trusted: opts.trusted, + disabled: opts.disabled, + ctx, + spawner, + }), + ) + + it.effect("kill-switch replaces every command without asking or running it", () => + Effect.gen(function* () { + const out = yield* Effect.promise(() => run({ trusted: true, disabled: true })) + expect(out).toBe("Value: [skill shell execution disabled by policy]") + }), + ) + + it.effect("untrusted skills never ask or run their commands", () => + Effect.gen(function* () { + const out = yield* Effect.promise(() => run({ trusted: false, disabled: false })) + expect(out).toBe("Value: [skill shell execution disabled for untrusted skill]") + }), + ) + + it.effect("content without placeholders is returned unchanged", () => + Effect.gen(function* () { + const out = yield* Effect.promise(() => run({ trusted: true, disabled: false, content: "no commands here" })) + expect(out).toBe("no commands here") + }), + ) +}) diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx index 6e712ccd34c..a6f21d0c2fc 100644 --- a/packages/tui/src/routes/session/permission.tsx +++ b/packages/tui/src/routes/session/permission.tsx @@ -291,6 +291,20 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory? } if (permission === "bash") { + // kilocode_change start - skill shell batches list every command + if (props.request.metadata?.["skillShell"] === true) { + const commands = (props.request.patterns ?? []).filter((p): p is string => typeof p === "string") + return { + icon: "#", + title: "Run these skill commands?", + body: ( + + {(cmd) => {"$ " + cmd}} + + ), + } + } + // kilocode_change end // kilocode_change start const meta = props.request.metadata ?? {} const desc = @@ -446,10 +460,12 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory? ) - // kilocode_change start - hide "Always allow" for protected Kilo configuration access - const options: Record = props.request.metadata?.[ConfigProtection.DISABLE_ALWAYS_KEY] - ? { once: "Allow once", reject: "Reject" } - : { once: "Allow once", always: "Allow always", reject: "Reject" } + // kilocode_change start - skill shell batches are never persisted: only Allow / Reject + const options: Record = props.request.metadata?.["skillShell"] + ? { once: "Allow", reject: "Reject" } + : props.request.metadata?.[ConfigProtection.DISABLE_ALWAYS_KEY] + ? { once: "Allow once", reject: "Reject" } + : { once: "Allow once", always: "Allow always", reject: "Reject" } // kilocode_change end const body = ( From 2d784a0e3162818afdb89b2c2352e45fdaaa8c6d Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Tue, 28 Jul 2026 15:27:30 +0200 Subject: [PATCH 13/71] chore(cli): add changeset for skill shell execution --- .changeset/skill-shell-execution.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/skill-shell-execution.md diff --git a/.changeset/skill-shell-execution.md b/.changeset/skill-shell-execution.md new file mode 100644 index 00000000000..bbe09cf70d7 --- /dev/null +++ b/.changeset/skill-shell-execution.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": minor +--- + +Support executing shell commands embedded in skill files. Commands written as `` !`command` `` in a SKILL.md run when the skill loads and their output is inlined into the skill, gated by a single up-front approval that lists every command. Only trusted skills can run commands, and `KILO_DISABLE_SKILL_SHELL` disables the behavior. From 7650d0fd090d5b8982c962e6483da3035b59488f Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 11:12:34 +0200 Subject: [PATCH 14/71] fix(cli): require human approval for skill shell command batches --- packages/core/src/v1/permission.ts | 2 + .../kilocode/backend/cli/KiloCliDataParser.kt | 1 + .../views/permission/PermissionView.kt | 2 +- .../kotlin/ai/kilocode/rpc/dto/ChatDto.kt | 2 + .../handlers/permission-handler.ts | 2 +- packages/opencode/src/acp/permission.ts | 5 +- packages/opencode/src/cli/cmd/run.ts | 3 +- packages/opencode/src/permission/index.ts | 7 +++ .../instance/httpapi/groups/permission.ts | 1 + .../instance/httpapi/handlers/permission.ts | 1 + .../kilocode/permission/skill-shell.test.ts | 50 +++++++++++++++++++ .../tui/src/routes/session/permission.tsx | 2 + 12 files changed, 73 insertions(+), 5 deletions(-) diff --git a/packages/core/src/v1/permission.ts b/packages/core/src/v1/permission.ts index b241ccd9077..8c410562e61 100644 --- a/packages/core/src/v1/permission.ts +++ b/packages/core/src/v1/permission.ts @@ -45,6 +45,8 @@ export type Reply = typeof Reply.Type export const ReplyBody = Schema.Struct({ reply: Reply, message: Schema.String.pipe(Schema.optional), + // kilocode_change - set by clients when a human answered the prompt; the server refuses machine approvals of skill-shell batches + interactive: Schema.Boolean.pipe(Schema.optional), }).annotate({ identifier: "PermissionReplyBody" }) export type ReplyBody = typeof ReplyBody.Type diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt index 1d4fa2ee4b7..d4e376bb81e 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt @@ -1579,6 +1579,7 @@ object KiloCliDataParser { sb.append("""{"reply":${escape(reply.reply)}""") val msg = reply.message if (msg != null) sb.append(""","message":${escape(msg)}""") + if (reply.interactive) sb.append(""","interactive":true""") sb.append("}") return sb.toString() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt index d5323dd58a3..0ae649ef54e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt @@ -356,7 +356,7 @@ class PermissionView( card.setActionEnabled(ID_RUN, false) card.setActionEnabled(ID_DENY, false) rules.setControlsEnabled(false) - reply(id, PermissionReplyDto(reply = "once"), rulePayload()) + reply(id, PermissionReplyDto(reply = "once", interactive = true), rulePayload()) } @RequiresEdt diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt index edbbed7b02b..f4e222bbb23 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt @@ -328,6 +328,8 @@ data class ToolRefDto( data class PermissionReplyDto( val reply: String, val message: String? = null, + // Set when a human answered the prompt; the CLI ignores machine approvals of skill-shell batches. + val interactive: Boolean = false, ) @Serializable diff --git a/packages/kilo-vscode/src/kilo-provider/handlers/permission-handler.ts b/packages/kilo-vscode/src/kilo-provider/handlers/permission-handler.ts index 2b6e3608cac..52008fdab43 100644 --- a/packages/kilo-vscode/src/kilo-provider/handlers/permission-handler.ts +++ b/packages/kilo-vscode/src/kilo-provider/handlers/permission-handler.ts @@ -106,7 +106,7 @@ export async function handlePermissionResponse( } const replyResult = await ctx.client.permission - .reply({ requestID: permissionId, reply: response, directory: dir }, { throwOnError: true }) + .reply({ requestID: permissionId, reply: response, directory: dir, interactive: true }, { throwOnError: true }) .then(() => "ok" as const) .catch((error: unknown) => { if (isNotFoundError(error)) return "stale" as const diff --git a/packages/opencode/src/acp/permission.ts b/packages/opencode/src/acp/permission.ts index ba07fdf4afc..3a57b6e5419 100644 --- a/packages/opencode/src/acp/permission.ts +++ b/packages/opencode/src/acp/permission.ts @@ -81,14 +81,15 @@ export class Handler { await this.writeProposedEdit(session.id, permission.metadata).catch(() => {}) } - await this.reply(permission.id, reply, session.cwd) + await this.reply(permission.id, reply, session.cwd, true) // kilocode_change - human selected via requestPermission } - private async reply(requestID: string, reply: Reply, directory: string) { + private async reply(requestID: string, reply: Reply, directory: string, interactive = false) { // kilocode_change - interactive param await this.input.sdk.permission.reply({ requestID, reply, directory, + interactive, // kilocode_change }) } diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index d77916109ed..854ed6d1d9b 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -840,7 +840,8 @@ export const RunCommand = effectCmd({ if (event.type === "permission.asked") { const permission = event.properties - // kilocode_change start - skill shell batches need an interactive human decision; never headless auto-approve + // kilocode_change start - skill shell batches need an interactive human decision. The server ignores + // non-interactive approvals, so headless runs must reject explicitly rather than leave them pending. if (permission.metadata?.["skillShell"] === true) { await client.permission.reply({ requestID: permission.id, reply: "reject" }) continue diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index d5217693d20..bed5d68defc 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -303,6 +303,13 @@ export const layer = Layer.effect( const existing = pending.get(input.requestID) if (!existing) return yield* new PermissionV1.NotFoundError({ requestID: input.requestID }) + // kilocode_change start - skill-shell batches must be answered by a human; ignore machine approvals + // (auto-approve/YOLO clients omit `interactive`) so the prompt stays pending for a real decision. + if (existing.info.metadata?.["skillShell"] === true && input.reply !== "reject" && input.interactive !== true) { + return + } + // kilocode_change end + pending.delete(input.requestID) yield* events.publish(Event.Replied, { sessionID: existing.info.sessionID, diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/permission.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/permission.ts index daaa43534fa..b9457ffc033 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/permission.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/permission.ts @@ -12,6 +12,7 @@ const root = "/permission" const ReplyPayload = Schema.Struct({ reply: PermissionV1.Reply, message: Schema.optional(Schema.String), + interactive: Schema.optional(Schema.Boolean), // kilocode_change - human-answered flag; gates skill-shell approvals }) // kilocode_change start diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts index ce9ee63f4b9..b317cd794cc 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts @@ -30,6 +30,7 @@ export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "permiss requestID: ctx.params.requestID, reply: ctx.payload.reply, message: ctx.payload.message, + interactive: ctx.payload.interactive, // kilocode_change }) .pipe( Effect.catchTag("Permission.NotFoundError", (error) => diff --git a/packages/opencode/test/kilocode/permission/skill-shell.test.ts b/packages/opencode/test/kilocode/permission/skill-shell.test.ts index d6b1fa79811..6815dd3c95e 100644 --- a/packages/opencode/test/kilocode/permission/skill-shell.test.ts +++ b/packages/opencode/test/kilocode/permission/skill-shell.test.ts @@ -42,6 +42,11 @@ const rejectAll = () => for (const req of yield* permission.list()) yield* permission.reply({ requestID: req.id, reply: "reject" }) }) +const reply = (input: Parameters[0]) => + Effect.gen(function* () { + return yield* (yield* Permission.Service).reply(input) + }) + const waitForPending = (count: number) => Effect.gen(function* () { const permission = yield* Permission.Service @@ -125,3 +130,48 @@ it.instance( }), { git: true }, ) + +it.instance( + "skillShell - a machine approval (no interactive flag) is ignored and stays pending", + () => + Effect.gen(function* () { + const fiber = yield* ask({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["printf hi"], + metadata: { skillShell: true }, + always: [], + ruleset: [], + }).pipe(Effect.forkScoped) + + const [pending] = yield* waitForPending(1) + // An auto-approver replies without `interactive`; the server must ignore it. + yield* reply({ requestID: pending.id, reply: "once" }) + expect(yield* list()).toHaveLength(1) + yield* rejectAll() + yield* Fiber.await(fiber) + }), + { git: true }, +) + +it.instance( + "skillShell - an interactive approval resolves the request", + () => + Effect.gen(function* () { + const fiber = yield* ask({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["printf hi"], + metadata: { skillShell: true }, + always: [], + ruleset: [], + }).pipe(Effect.forkScoped) + + const [pending] = yield* waitForPending(1) + yield* reply({ requestID: pending.id, reply: "once", interactive: true }) + // human approval clears the prompt and the ask succeeds + expect(yield* list()).toHaveLength(0) + yield* Fiber.await(fiber) + }), + { git: true }, +) diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx index a6f21d0c2fc..77524ba6abc 100644 --- a/packages/tui/src/routes/session/permission.tsx +++ b/packages/tui/src/routes/session/permission.tsx @@ -192,6 +192,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory? requestID: props.request.id, directory: props.directory, workspace: project.workspace.current(), + interactive: true, // kilocode_change - human answered this prompt }) }} /> @@ -499,6 +500,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory? requestID: props.request.id, directory: props.directory, workspace: project.workspace.current(), + interactive: true, // kilocode_change - human answered this prompt }) }} /> From 0c417acfaa0c6f6127af5475c0d4fa7ae8d4a984 Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 11:14:05 +0200 Subject: [PATCH 15/71] chore(sdk): regenerate for permission reply interactive flag --- packages/sdk/js/src/v2/gen/sdk.gen.ts | 2 + packages/sdk/js/src/v2/gen/types.gen.ts | 2 + packages/sdk/openapi.json | 296 ++++++++++++++---------- 3 files changed, 174 insertions(+), 126 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 81f6ac2e965..fcdffdc9550 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -3752,6 +3752,7 @@ export class Permission extends HeyApiClient { workspace?: string reply?: "once" | "always" | "reject" message?: string + interactive?: boolean }, options?: Options, ) { @@ -3765,6 +3766,7 @@ export class Permission extends HeyApiClient { { in: "query", key: "workspace" }, { in: "body", key: "reply" }, { in: "body", key: "message" }, + { in: "body", key: "interactive" }, ], }, ], diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 61953cc620f..aad0b6b5cbd 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -7717,6 +7717,7 @@ export type AppSkillsResponses = { description?: string location: string content: string + trusted?: boolean }> } @@ -8628,6 +8629,7 @@ export type PermissionReplyData = { body?: { reply: "once" | "always" | "reject" message?: string + interactive?: boolean } path: { requestID: string diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 53526b5f007..c7cd8731026 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -3137,6 +3137,9 @@ }, "content": { "type": "string" + }, + "trusted": { + "type": "boolean" } }, "required": ["name", "location", "content"], @@ -5312,6 +5315,9 @@ }, "message": { "type": "string" + }, + "interactive": { + "type": "boolean" } }, "required": ["reply"], @@ -24496,6 +24502,15 @@ { "$ref": "#/components/schemas/EventServerInstanceDisposed" }, + { + "$ref": "#/components/schemas/EventSessionTurnOpen" + }, + { + "$ref": "#/components/schemas/EventSessionTurnClose" + }, + { + "$ref": "#/components/schemas/EventSessionQueueChanged" + }, { "$ref": "#/components/schemas/EventSessionNetworkAsked" }, @@ -24523,12 +24538,6 @@ { "$ref": "#/components/schemas/EventInteractive_terminalDeleted" }, - { - "$ref": "#/components/schemas/EventSessionTurnOpen" - }, - { - "$ref": "#/components/schemas/EventSessionTurnClose" - }, { "$ref": "#/components/schemas/EventSandboxStatusChanged" }, @@ -24557,10 +24566,10 @@ "$ref": "#/components/schemas/EventKilocodeNotebookCancelled" }, { - "$ref": "#/components/schemas/EventLspClientDiagnostics" + "$ref": "#/components/schemas/EventKilo-sessionsRemote-status-changed" }, { - "$ref": "#/components/schemas/EventKilo-sessionsRemote-status-changed" + "$ref": "#/components/schemas/EventLspClientDiagnostics" }, { "$ref": "#/components/schemas/EventMemoryStatus1" @@ -24818,10 +24827,10 @@ "$ref": "#/components/schemas/EventProjectUpdated" }, { - "$ref": "#/components/schemas/EventLspUpdated" + "$ref": "#/components/schemas/EventVcsBranchUpdated" }, { - "$ref": "#/components/schemas/EventVcsBranchUpdated" + "$ref": "#/components/schemas/EventLspUpdated" }, { "$ref": "#/components/schemas/EventWorkspaceReady" @@ -26838,7 +26847,7 @@ "type": "object", "properties": { "start": { - "type": "number", + "type": "integer", "minimum": 0 } }, @@ -26914,11 +26923,11 @@ "type": "object", "properties": { "start": { - "type": "number", + "type": "integer", "minimum": 0 }, "end": { - "type": "number", + "type": "integer", "minimum": 0 }, "elapsed": { @@ -27686,6 +27695,15 @@ { "$ref": "#/components/schemas/EventServerInstanceDisposed" }, + { + "$ref": "#/components/schemas/EventSessionTurnOpen" + }, + { + "$ref": "#/components/schemas/EventSessionTurnClose" + }, + { + "$ref": "#/components/schemas/EventSessionQueueChanged" + }, { "$ref": "#/components/schemas/EventSessionNetworkAsked" }, @@ -27713,12 +27731,6 @@ { "$ref": "#/components/schemas/EventInteractive_terminalDeleted" }, - { - "$ref": "#/components/schemas/EventSessionTurnOpen" - }, - { - "$ref": "#/components/schemas/EventSessionTurnClose" - }, { "$ref": "#/components/schemas/EventSandboxStatusChanged" }, @@ -27747,10 +27759,10 @@ "$ref": "#/components/schemas/EventKilocodeNotebookCancelled" }, { - "$ref": "#/components/schemas/EventLspClientDiagnostics" + "$ref": "#/components/schemas/EventKilo-sessionsRemote-status-changed" }, { - "$ref": "#/components/schemas/EventKilo-sessionsRemote-status-changed" + "$ref": "#/components/schemas/EventLspClientDiagnostics" }, { "$ref": "#/components/schemas/EventMemoryStatus" @@ -28008,10 +28020,10 @@ "$ref": "#/components/schemas/EventProjectUpdated" }, { - "$ref": "#/components/schemas/EventLspUpdated" + "$ref": "#/components/schemas/EventVcsBranchUpdated" }, { - "$ref": "#/components/schemas/EventVcsBranchUpdated" + "$ref": "#/components/schemas/EventLspUpdated" }, { "$ref": "#/components/schemas/EventWorkspaceReady" @@ -35218,6 +35230,96 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + "EventSessionTurnOpen": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["session.turn.open"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses" + } + }, + "required": ["sessionID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventSessionTurnClose": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["session.turn.close"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "parentID": { + "type": "string", + "pattern": "^ses" + }, + "reason": { + "type": "string", + "enum": ["completed", "error", "interrupted"] + } + }, + "required": ["sessionID", "reason"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventSessionQueueChanged": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["session.queue.changed"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "queued": { + "type": "array", + "items": { + "type": "string", + "pattern": "^msg" + } + } + }, + "required": ["sessionID", "queued"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "EventSessionNetworkAsked": { "type": "object", "properties": { @@ -35470,64 +35572,6 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventSessionTurnOpen": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["session.turn.open"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionTurnClose": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["session.turn.close"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "parentID": { - "type": "string", - "pattern": "^ses" - }, - "reason": { - "type": "string", - "enum": ["completed", "error", "interrupted"] - } - }, - "required": ["sessionID", "reason"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, "EventSandboxStatusChanged": { "type": "object", "properties": { @@ -35837,33 +35881,6 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventLspClientDiagnostics": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["lsp.client.diagnostics"] - }, - "properties": { - "type": "object", - "properties": { - "serverID": { - "type": "string" - }, - "path": { - "type": "string" - } - }, - "required": ["serverID", "path"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, "EventKilo-sessionsRemote-status-changed": { "type": "object", "properties": { @@ -35891,6 +35908,33 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + "EventLspClientDiagnostics": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["lsp.client.diagnostics"] + }, + "properties": { + "type": "object", + "properties": { + "serverID": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": ["serverID", "path"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "EventMemoryStatus": { "type": "object", "properties": { @@ -39854,24 +39898,6 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventLspUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["lsp.updated"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, "EventVcsBranchUpdated": { "type": "object", "properties": { @@ -39895,6 +39921,24 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + "EventLspUpdated": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["lsp.updated"] + }, + "properties": { + "type": "object", + "properties": {} + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "EventWorkspaceReady": { "type": "object", "properties": { From e7a7478b38d81fb785713fa062fcfd601cb05f34 Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 11:24:19 +0200 Subject: [PATCH 16/71] fix(cli): keep deny rules terminal for skill shell command batches --- packages/opencode/src/permission/index.ts | 19 ++++++---------- .../kilocode/permission/skill-shell.test.ts | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index bed5d68defc..ff6ad14fb07 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -222,19 +222,8 @@ export const layer = Layer.effect( : false // kilocode_change end - // kilocode_change start - skill shell injection always prompts once, overriding every allow/deny/auto-approve rule - const forceAsk = request.metadata?.["skillShell"] === true - // kilocode_change end + const forceAsk = request.metadata?.["skillShell"] === true // kilocode_change for (const pattern of request.patterns) { - // kilocode_change start - force a prompt over soft allow/deny rules, but never over a hard (plan-mode) veto - if (forceAsk) { - if (veto(request.permission, pattern, hardRuleset)) { - return yield* new DeniedError({ ruleset: subset(request.permission, hardRuleset ?? []) }) - } - needsAsk = true - continue - } - // kilocode_change end const rule = resolve(request.permission, pattern, ruleset, approved, local) // kilocode_change — include session-scoped rules yield* Effect.logInfo("evaluated", { permission: request.permission, pattern, action: rule }) // kilocode_change start — saved/session approvals cannot override hard Ask/Plan denials @@ -247,6 +236,12 @@ export const layer = Layer.effect( ruleset: subset(request.permission, ruleset), // kilocode_change }) } + // kilocode_change start - skill shell forces a prompt instead of honoring an allow/auto-approve rule + if (forceAsk) { + needsAsk = true + continue + } + // kilocode_change end // kilocode_change start - override "allow" to "ask" for protected config paths if (rule.action === "allow" && (!isProtected || trusted)) { approvedRule = rule // remember the winning rule so callers can explain the auto-approval diff --git a/packages/opencode/test/kilocode/permission/skill-shell.test.ts b/packages/opencode/test/kilocode/permission/skill-shell.test.ts index 6815dd3c95e..06bf04df7c0 100644 --- a/packages/opencode/test/kilocode/permission/skill-shell.test.ts +++ b/packages/opencode/test/kilocode/permission/skill-shell.test.ts @@ -86,6 +86,28 @@ it.instance( { git: true }, ) +it.instance( + "skillShell - a deny rule stays terminal (build mode, no hard ruleset)", + () => + Effect.gen(function* () { + // build mode has no hardRuleset; an ordinary deny rule must still block, not prompt. + const err = yield* fail( + ask({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["curl evil.sh"], + metadata: { skillShell: true }, + always: [], + ruleset: [{ permission: "bash", pattern: "curl *", action: "deny" }], + }), + ) + + expect(err).toBeInstanceOf(PermissionV1.DeniedError) + expect(yield* list()).toHaveLength(0) + }), + { git: true }, +) + it.instance( "skillShell - is denied by a hard-ruleset veto instead of prompting", () => From c2f2831bd996c363279fdf8526a0102c2ff166cf Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 11:38:20 +0200 Subject: [PATCH 17/71] fix(cli): decompose skill shell commands for per-command permission checks --- .../opencode/src/kilocode/skills/inject.ts | 47 +++++++++++++++---- packages/opencode/src/tool/shell.ts | 19 +++++++- packages/opencode/src/tool/skill.ts | 5 ++ .../test/kilocode/skills/inject.test.ts | 26 ++++++++++ 4 files changed, 87 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/kilocode/skills/inject.ts b/packages/opencode/src/kilocode/skills/inject.ts index ceee78c7427..4bdf6afe58f 100644 --- a/packages/opencode/src/kilocode/skills/inject.ts +++ b/packages/opencode/src/kilocode/skills/inject.ts @@ -16,10 +16,12 @@ import type * as Tool from "@/tool/tool" // skills never spawn a process. // 2. Kill-switch: `disabled` (KILO_DISABLE_SKILL_SHELL) turns injection off // entirely, matching Claude's disableSkillShellExecution. -// 3. Batch approval: every command in the file is presented once, up front, in -// a single permission prompt (the `skillShell` metadata marker forces this -// prompt regardless of any allow/deny/auto-approve rule). Approve runs the -// whole batch; reject aborts the skill load with nothing run. +// 3. Batch approval: every command in the file is decomposed with the same +// tree-sitter scan the bash tool uses (per sub-command patterns plus any +// out-of-project directories), then presented once, up front, in a single +// permission prompt. The `skillShell` marker forces this prompt regardless +// of any allow/auto-approve rule; a deny rule or plan-mode veto on any +// sub-command still blocks. Approve runs the batch; reject aborts the load. // // Substitution runs exactly once. Command output is inlined as plain text and is // never re-scanned, so a command cannot emit a `!`cmd`` placeholder that a later @@ -29,12 +31,20 @@ const DISABLED_NOTE = "[skill shell execution disabled by policy]" const UNTRUSTED_NOTE = "[skill shell execution disabled for untrusted skill]" export namespace SkillInject { + export type Decompose = (input: { + command: string + cwd: string + shell: string + }) => Effect.Effect<{ patterns: string[]; dirs: string[] }> + export type Options = { content: string trusted: boolean disabled: boolean + cwd: string ctx: Tool.Context spawner: Spawner["Service"] + decompose: Decompose } export const render = Effect.fn("SkillInject.render")(function* (opts: Options) { @@ -45,20 +55,39 @@ export namespace SkillInject { if (opts.disabled) return replace(opts.content, () => DISABLED_NOTE) if (!opts.trusted) return replace(opts.content, () => UNTRUSTED_NOTE) + const shell = Shell.preferred() // Deduplicate identical commands so the batch lists and runs each once. const commands = Array.from(new Set(matches.map(([, cmd]) => cmd))) - // Single up-front approval for the whole batch. `skillShell` forces one - // prompt even when rules would allow or deny; a reject/deny propagates as a - // defect and aborts the skill load without running anything. + // Decompose each command into sub-command patterns + out-of-project dir globs + // via the shared bash scan, so plan-mode denies and external_directory checks + // apply per sub-command instead of matching the raw string as one glob. + const patterns = new Set() + const dirs = new Set() + for (const command of commands) { + const scan = yield* opts.decompose({ command, cwd: opts.cwd, shell }) + for (const pattern of scan.patterns) patterns.add(pattern) + for (const dir of scan.dirs) dirs.add(dir) + } + + // Single up-front approval. Out-of-project directories are asked first, then + // the decomposed sub-commands. `skillShell` forces the prompt over allow/YOLO + // rules; a deny/veto on any sub-command propagates as a defect and aborts. + if (dirs.size > 0) { + yield* opts.ctx.ask({ + permission: "external_directory", + patterns: Array.from(dirs), + always: [], + metadata: { skillShell: true }, + }) + } yield* opts.ctx.ask({ permission: "bash", - patterns: commands, + patterns: Array.from(patterns), always: [], metadata: { skillShell: true }, }) - const shell = Shell.preferred() const outputs = new Map() for (const command of commands) { outputs.set( diff --git a/packages/opencode/src/tool/shell.ts b/packages/opencode/src/tool/shell.ts index 27b73acae08..b011f4b3c41 100644 --- a/packages/opencode/src/tool/shell.ts +++ b/packages/opencode/src/tool/shell.ts @@ -433,7 +433,24 @@ export const ShellPermission = Effect.gen(function* () { ) }) - return { ask: check, resolve } + // kilocode_change start - expose the tree-sitter scan (sub-command patterns + external-dir globs) for skill-shell batching + const dirGlob = (dir: string) => + process.platform === "win32" ? FSUtil.normalizePathPattern(path.join(dir, "*")) : path.join(dir, "*") + const decompose = Effect.fn("ShellTool.decompose")(function* (input: { command: string; cwd: string; shell: string }) { + const instance = yield* InstanceState.context + const ps = Shell.ps(input.shell) + return yield* Effect.scoped( + Effect.gen(function* () { + const tree = yield* Effect.acquireRelease(parse(input.command, ps), (tree) => Effect.sync(() => tree.delete())) + const scan = yield* collect(tree.rootNode, input.cwd, ps, input.shell, instance) + if (!containsPath(input.cwd, instance)) scan.dirs.add(input.cwd) + return { patterns: Array.from(scan.patterns), dirs: Array.from(scan.dirs, dirGlob) } + }), + ) + }) + // kilocode_change end + + return { ask: check, resolve, decompose } // kilocode_change - decompose for skill-shell }) // kilocode_change end diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index ad2049afc2a..ef2b9b8f549 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -7,7 +7,9 @@ import * as Tool from "./tool" import DESCRIPTION from "./skill.txt" // kilocode_change start - gate + run shell injection in skill bodies import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" +import { InstanceState } from "@/effect/instance-state" import { RuntimeFlags } from "@/effect/runtime-flags" +import { ShellPermission } from "./shell" import { SkillInject } from "@/kilocode/skills/inject" // kilocode_change end @@ -22,6 +24,7 @@ export const SkillTool = Tool.define( const ripgrep = yield* Ripgrep.Service const flags = yield* RuntimeFlags.Service // kilocode_change const spawner = yield* ChildProcessSpawner // kilocode_change + const permission = yield* ShellPermission // kilocode_change - decompose skill commands like the bash tool return { description: DESCRIPTION, @@ -44,8 +47,10 @@ export const SkillTool = Tool.define( content: info.content, trusted: info.trusted === true, disabled: flags.disableSkillShell, + cwd: yield* InstanceState.directory, ctx, spawner, + decompose: permission.decompose, }) // kilocode_change end diff --git a/packages/opencode/test/kilocode/skills/inject.test.ts b/packages/opencode/test/kilocode/skills/inject.test.ts index 1058968d1e3..f334bfe5ef0 100644 --- a/packages/opencode/test/kilocode/skills/inject.test.ts +++ b/packages/opencode/test/kilocode/skills/inject.test.ts @@ -102,6 +102,29 @@ describe("skill shell injection", () => { }), ) + unix("decomposes a compound command into per-sub-command patterns", () => + Effect.gen(function* () { + // A single placeholder with a chained command must not be asked as one glob + // pattern (which would let e.g. `cat *` match the whole string). Each + // sub-command must appear separately so deny/veto rules apply per command. + yield* writeGlobalSkill("compound-shell", "Out: !`cat README.md; printf hi`") + + const requests: Array> = [] + yield* loadSkill("compound-shell", (req) => + Effect.sync(() => { + requests.push(req) + }), + ) + + const bash = requests.filter((r) => r.permission === "bash") + expect(bash.length).toBe(1) + // both sub-commands are present as distinct patterns, not the raw string + expect(bash[0].patterns).toContain("cat README.md") + expect(bash[0].patterns).toContain("printf hi") + expect(bash[0].patterns).not.toContain("cat README.md; printf hi") + }), + ) + unix("aborts the entire skill load when the batch is rejected", () => Effect.gen(function* () { yield* writeGlobalSkill("denied-shell", "Secret: !`printf leaked`") @@ -162,6 +185,7 @@ describe("SkillInject.render gating", () => { } const spawner = new Proxy({}, { get: boom }) as any const ctx = { ...baseCtx, ask: () => Effect.sync(boom) } as Tool.Context + const decompose = (() => Effect.sync(boom)) as unknown as SkillInject.Decompose const run = (opts: { trusted: boolean; disabled: boolean; content?: string }) => Effect.runPromise( @@ -169,8 +193,10 @@ describe("SkillInject.render gating", () => { content: opts.content ?? "Value: !`printf ran`", trusted: opts.trusted, disabled: opts.disabled, + cwd: "/tmp", ctx, spawner, + decompose, }), ) From c74d448ffdc0b7f957f16aa8c119e15b54953d32 Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 11:57:19 +0200 Subject: [PATCH 18/71] fix(cli): apply skill trust to slash-command shell execution --- packages/opencode/src/command/index.ts | 2 ++ packages/opencode/src/session/prompt.ts | 7 +++++- .../skill-command-autocomplete.test.ts | 24 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 3160b15f2d9..c4cffa639b8 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -33,6 +33,7 @@ export const Info = Schema.Struct({ agent: Schema.optional(Schema.String), model: Schema.optional(Schema.String), source: Schema.optional(Schema.Literals(["command", "mcp", "skill"])), + trusted: Schema.optional(Schema.Boolean), // kilocode_change - skill-sourced templates only run `!`cmd`` shell when trusted // Some command templates are lazy promises from MCP prompt resolution. template: Schema.Unknown, subtask: Schema.optional(Schema.Boolean), @@ -67,6 +68,7 @@ function fromSkill(item: Skill.Info): Info { name: item.name, description: item.description, source: "skill", + trusted: item.trusted === true, get template() { return item.content }, diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index f1520cc2e61..83e70f7546d 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -2033,7 +2033,12 @@ export const layer = Layer.effect( } const shellMatches = ConfigMarkdown.shell(template) - if (shellMatches.length > 0) { + // kilocode_change start - untrusted skill templates must not spawn shell; mirror the skill tool's trust gate + const untrustedSkill = cmd.source === "skill" && cmd.trusted !== true + if (shellMatches.length > 0 && untrustedSkill) { + template = template.replace(bashRegex, () => "[skill shell execution disabled for untrusted skill]") + } else if (shellMatches.length > 0) { + // kilocode_change end const cfg = yield* config.get() const sh = Shell.preferred(cfg.shell) // kilocode_change start diff --git a/packages/opencode/test/kilocode/skill-command-autocomplete.test.ts b/packages/opencode/test/kilocode/skill-command-autocomplete.test.ts index f7e81a786f4..50ea098b034 100644 --- a/packages/opencode/test/kilocode/skill-command-autocomplete.test.ts +++ b/packages/opencode/test/kilocode/skill-command-autocomplete.test.ts @@ -54,4 +54,28 @@ Skill content. }, ), ) + + // The slash-command path runs a template's `!`cmd`` shell without a permission + // prompt, so it must only do so for trusted skills. A project-local skill is + // untrusted, and Command.Info carries the flag the prompt executor gates on. + it.live("marks project skills untrusted so their slash-command shell is disabled", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + yield* Effect.promise(() => + Bun.write( + path.join(dir, ".kilo", "skill", "proj", "SKILL.md"), + `---\nname: proj\ndescription: proj.\n---\n\nRun: !\`printf hi\`\n`, + ), + ) + + const command = yield* Command.Service + const proj = yield* command.get("proj") + + expect(proj?.source).toBe("skill") + expect(proj?.trusted).toBe(false) + }), + { git: true }, + ), + ) }) From cd266e8e55aacf7e56f7d77cb59369b1c55e8632 Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 12:17:38 +0200 Subject: [PATCH 19/71] fix(cli): show skill shell commands in ACP permission prompts --- packages/opencode/src/acp/permission.ts | 18 +++++++-- packages/opencode/test/acp/permission.test.ts | 38 ++++++++++++++++++- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/acp/permission.ts b/packages/opencode/src/acp/permission.ts index 3a57b6e5419..8bf102f2175 100644 --- a/packages/opencode/src/acp/permission.ts +++ b/packages/opencode/src/acp/permission.ts @@ -16,6 +16,17 @@ const permissionOptions: PermissionOption[] = [ { optionId: "reject", kind: "reject_once", name: "Reject" }, ] +// kilocode_change start - skill shell batches list their commands and are never persisted, so no "Always allow" +const skillShellOptions: PermissionOption[] = [ + { optionId: "once", kind: "allow_once", name: "Allow" }, + { optionId: "reject", kind: "reject_once", name: "Reject" }, +] + +function isSkillShell(metadata: PermissionEvent["properties"]["metadata"]) { + return (metadata as { skillShell?: unknown })?.skillShell === true +} +// kilocode_change end + export class Handler { private readonly queues = new Map>() @@ -51,18 +62,19 @@ export class Handler { return } + const skillShell = isSkillShell(permission.metadata) // kilocode_change - skill batches list commands and never persist const result = await this.input.connection .requestPermission({ sessionId: permission.sessionID, toolCall: { toolCallId: permission.tool?.callID ?? permission.id, status: "pending", - title: permission.permission, - rawInput: permission.metadata, + title: skillShell ? "Run skill shell commands" : permission.permission, // kilocode_change + rawInput: skillShell ? { ...permission.metadata, commands: permission.patterns } : permission.metadata, // kilocode_change kind: toToolKind(permission.permission), locations: toLocations(permission.permission, permission.metadata), }, - options: permissionOptions, + options: skillShell ? skillShellOptions : permissionOptions, // kilocode_change }) .catch(async () => { await this.reply(permission.id, "reject", session.cwd) diff --git a/packages/opencode/test/acp/permission.test.ts b/packages/opencode/test/acp/permission.test.ts index 080c8eeceb0..76ce5584c28 100644 --- a/packages/opencode/test/acp/permission.test.ts +++ b/packages/opencode/test/acp/permission.test.ts @@ -162,7 +162,11 @@ describe("acp permissions", () => { { optionId: "reject", kind: "reject_once", name: "Reject" }, ], }) - expect(harness.replies).toEqual([{ requestID: "perm_1", reply: "once", directory: "/workspace" }]) + // kilocode_change start - human selections are marked interactive + expect(harness.replies).toEqual([ + { requestID: "perm_1", reply: "once", directory: "/workspace", interactive: true }, + ]) + // kilocode_change end }) it("forwards external_directory metadata and locations to requestPermission", async () => { @@ -201,6 +205,38 @@ describe("acp permissions", () => { }) }) + // kilocode_change start - skill shell batches surface their command list and cannot be persisted + it("forwards skill shell commands and omits the persist option", async () => { + const harness = createHarness() + await createSession(harness.session, "ses_a") + + harness.subscription.handle( + permissionAsked("ses_a", "perm_skill", { + permission: "bash", + metadata: { skillShell: true }, + tool: { messageID: "msg_1", callID: "call_1" }, + }), + ) + + await pollUntil(() => harness.replies.length === 1, "skill shell permission was never replied") + + expect(harness.requests[0]).toMatchObject({ + toolCall: { + title: "Run skill shell commands", + rawInput: { skillShell: true, commands: ["*"] }, + }, + // no allow_always: skill shell is never persisted + options: [ + { optionId: "once", kind: "allow_once", name: "Allow" }, + { optionId: "reject", kind: "reject_once", name: "Reject" }, + ], + }) + expect(harness.requests[0].options.some((o) => o.kind === "allow_always")).toBe(false) + // the human selection is marked interactive so the server accepts the approval + expect(harness.replies[0]).toMatchObject({ requestID: "perm_skill", reply: "once", interactive: true }) + }) + // kilocode_change end + it("rejects non-selected outcomes", async () => { const harness = createHarness(() => Promise.resolve({ outcome: { outcome: "cancelled" } })) await createSession(harness.session, "ses_a") From 3cef158ef058095d30f851c83747ff3c7ed4c201 Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 12:27:21 +0200 Subject: [PATCH 20/71] fix(cli): confine downloaded skills to the cache directory --- packages/opencode/src/skill/discovery.ts | 16 ++++++++++++- .../opencode/test/skill/discovery.test.ts | 23 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/skill/discovery.ts b/packages/opencode/src/skill/discovery.ts index 0495bc637dc..24e78c14f81 100644 --- a/packages/opencode/src/skill/discovery.ts +++ b/packages/opencode/src/skill/discovery.ts @@ -71,8 +71,22 @@ export const layer: Layer.Layer skill.files.includes("SKILL.md")) + // kilocode_change start - remote index.json controls skill.name/file, so a crafted `../` could escape the + // cache and plant a SKILL.md in a trusted dir (e.g. ~/.agents/skills). Drop any skill whose paths escape it. + const rooted = (target: string) => { + const rel = path.relative(cache, target) + return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel) + } + const safe: typeof list = [] + for (const skill of list) { + const root = path.join(cache, skill.name) + if (rooted(root) && skill.files.every((file) => rooted(path.join(root, file)))) safe.push(skill) + else yield* Effect.logWarning("skipping skill with unsafe path", { url: index, skill: skill.name }) + } + // kilocode_change end + const dirs = yield* Effect.forEach( - list, + safe, // kilocode_change - was `list`; drop skills whose paths escape the cache (skill) => Effect.gen(function* () { const root = path.join(cache, skill.name) diff --git a/packages/opencode/test/skill/discovery.test.ts b/packages/opencode/test/skill/discovery.test.ts index 5dc5d5195bb..ad1a53463f4 100644 --- a/packages/opencode/test/skill/discovery.test.ts +++ b/packages/opencode/test/skill/discovery.test.ts @@ -24,6 +24,15 @@ beforeAll(async () => { async fetch(req) { const url = new URL(req.url) + // kilocode_change start - serve a crafted index whose skill name escapes the cache via `../` + if (url.pathname === "/evil/index.json") { + return Response.json({ skills: [{ name: "../../../.agents/skills/evil", files: ["SKILL.md"] }] }) + } + if (url.pathname.endsWith("/.agents/skills/evil/SKILL.md")) { + return new Response("---\nname: evil\ndescription: evil.\n---\npwned") + } + // kilocode_change end + // route /.well-known/skills/* to the fixture directory if (url.pathname.startsWith("/.well-known/skills/")) { const filePath = url.pathname.replace("/.well-known/skills/", "") @@ -114,6 +123,20 @@ describe("Discovery.pull", () => { }), ) + // kilocode_change start - path-traversal in the remote index must not plant a trusted skill + it.live("rejects a skill name that escapes the cache directory", () => + Effect.gen(function* () { + const fsys = yield* FSUtil.Service + const discovery = yield* Discovery.Service + const dirs = yield* discovery.pull(`http://localhost:${server.port}/evil/`) + // the traversal skill is skipped, nothing is planted outside the cache + expect(dirs).toEqual([]) + const escaped = path.join(cacheDir, "../../../.agents/skills/evil/SKILL.md") + expect(yield* fsys.existsSafe(escaped)).toBe(false) + }), + ) + // kilocode_change end + it.live("caches downloaded files on second pull", () => Effect.gen(function* () { // clear dir and downloadCount From 2d8377894dae4382fe8b624dce2466b8c5d10c77 Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 12:36:27 +0200 Subject: [PATCH 21/71] fix(cli): bound skill shell execution (cwd, abort, timeout, output cap) --- .../opencode/src/kilocode/skills/inject.ts | 45 ++++++++++++++----- packages/opencode/src/tool/skill.ts | 3 -- .../test/kilocode/skills/inject.test.ts | 32 ++++++++++++- 3 files changed, 65 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/kilocode/skills/inject.ts b/packages/opencode/src/kilocode/skills/inject.ts index 4bdf6afe58f..4c5d8f98460 100644 --- a/packages/opencode/src/kilocode/skills/inject.ts +++ b/packages/opencode/src/kilocode/skills/inject.ts @@ -1,8 +1,6 @@ import { Effect } from "effect" -import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" -import type { ChildProcessSpawner as Spawner } from "effect/unstable/process/ChildProcessSpawner" import { ConfigMarkdown } from "@/config/markdown" -import { CommandTimeout } from "@/kilocode/command-timeout" +import { Process } from "@/util/process" import { Shell } from "@opencode-ai/core/shell" import type * as Tool from "@/tool/tool" @@ -30,6 +28,12 @@ import type * as Tool from "@/tool/tool" const DISABLED_NOTE = "[skill shell execution disabled by policy]" const UNTRUSTED_NOTE = "[skill shell execution disabled for untrusted skill]" +// Execution bounds: model-initiated commands must not hang the load, blow up +// context, or overrun the batch. +const TIMEOUT_MS = 2 * 60 * 1000 +const MAX_OUTPUT_BYTES = 32 * 1024 +const MAX_COMMANDS = 32 + export namespace SkillInject { export type Decompose = (input: { command: string @@ -43,7 +47,6 @@ export namespace SkillInject { disabled: boolean cwd: string ctx: Tool.Context - spawner: Spawner["Service"] decompose: Decompose } @@ -56,8 +59,9 @@ export namespace SkillInject { if (!opts.trusted) return replace(opts.content, () => UNTRUSTED_NOTE) const shell = Shell.preferred() - // Deduplicate identical commands so the batch lists and runs each once. - const commands = Array.from(new Set(matches.map(([, cmd]) => cmd))) + // Deduplicate identical commands, then cap the batch so a skill can't queue + // an unbounded number of processes. + const commands = Array.from(new Set(matches.map(([, cmd]) => cmd))).slice(0, MAX_COMMANDS) // Decompose each command into sub-command patterns + out-of-project dir globs // via the shared bash scan, so plan-mode denies and external_directory checks @@ -88,17 +92,38 @@ export namespace SkillInject { metadata: { skillShell: true }, }) + // Run each command in the instance directory, bounded by ctx.abort (ESC) and a + // timeout, with output truncated so it can't blow up or poison the prompt. const outputs = new Map() for (const command of commands) { - outputs.set( - command, - yield* CommandTimeout.text(command, shell).pipe(Effect.provideService(ChildProcessSpawner, opts.spawner)), - ) + outputs.set(command, yield* run(command, shell, opts.cwd, opts.ctx.abort)) } return replace(opts.content, (command) => outputs.get(command) ?? "") }) + const run = Effect.fn("SkillInject.run")(function* (command: string, shell: string, cwd: string, abort: AbortSignal) { + const result = yield* Effect.promise(async () => { + // A cleared timer bounds the run without leaking a pending 2-minute timeout + // per command; ESC (ctx.abort) still kills the child via the same signal. + const controller = new AbortController() + const signal = AbortSignal.any([abort, controller.signal]) + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS) + try { + return await Process.text([command], { shell, cwd, abort: signal, nothrow: true }).catch(() => undefined) + } finally { + clearTimeout(timer) + } + }) + if (!result) return abort.aborted ? "[skill shell command aborted]" : "[skill shell command timed out]" + return truncate(result.text) + }) + + function truncate(text: string) { + if (Buffer.byteLength(text) <= MAX_OUTPUT_BYTES) return text + return text.slice(0, MAX_OUTPUT_BYTES) + "\n[skill shell output truncated]" + } + // Replace only the exact matches found in the ORIGINAL content. Never re-scan // the result, so inlined output containing `!`cmd`` stays inert. function replace(content: string, value: (command: string) => string) { diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index ef2b9b8f549..06e1bf93b32 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -6,7 +6,6 @@ import { Skill } from "../skill" import * as Tool from "./tool" import DESCRIPTION from "./skill.txt" // kilocode_change start - gate + run shell injection in skill bodies -import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" import { InstanceState } from "@/effect/instance-state" import { RuntimeFlags } from "@/effect/runtime-flags" import { ShellPermission } from "./shell" @@ -23,7 +22,6 @@ export const SkillTool = Tool.define( const skill = yield* Skill.Service const ripgrep = yield* Ripgrep.Service const flags = yield* RuntimeFlags.Service // kilocode_change - const spawner = yield* ChildProcessSpawner // kilocode_change const permission = yield* ShellPermission // kilocode_change - decompose skill commands like the bash tool return { @@ -49,7 +47,6 @@ export const SkillTool = Tool.define( disabled: flags.disableSkillShell, cwd: yield* InstanceState.directory, ctx, - spawner, decompose: permission.decompose, }) // kilocode_change end diff --git a/packages/opencode/test/kilocode/skills/inject.test.ts b/packages/opencode/test/kilocode/skills/inject.test.ts index f334bfe5ef0..b0b9e00f7ff 100644 --- a/packages/opencode/test/kilocode/skills/inject.test.ts +++ b/packages/opencode/test/kilocode/skills/inject.test.ts @@ -102,6 +102,18 @@ describe("skill shell injection", () => { }), ) + unix("runs commands in the instance directory", () => + Effect.gen(function* () { + const dir = (yield* TestInstance).directory + yield* writeGlobalSkill("cwd-shell", "Here: !`pwd`") + + const result = yield* loadSkill("cwd-shell", () => Effect.void) + + // pwd resolves to the instance dir (realpath), not the server process cwd + expect(result.output).toContain(path.basename(dir)) + }), + ) + unix("decomposes a compound command into per-sub-command patterns", () => Effect.gen(function* () { // A single placeholder with a chained command must not be asked as one glob @@ -174,6 +186,24 @@ describe("skill shell injection", () => { expect(result.output).not.toMatch(/Out:\s*pwned\s*$/m) }), ) + + unix("truncates oversized command output before inlining", () => + Effect.gen(function* () { + const dir = (yield* TestInstance).directory + // render directly (bypassing the tool's own output truncation) to assert the injector caps output + const rendered = yield* SkillInject.render({ + content: "Out: !`yes x | head -c 65536`", + trusted: true, + disabled: false, + cwd: dir, + ctx: { ...baseCtx, ask: () => Effect.void } as Tool.Context, + decompose: ({ command }) => Effect.succeed({ patterns: [command], dirs: [] }), + }) + + expect(rendered).toContain("[skill shell output truncated]") + expect(rendered.length).toBeLessThan(40000) + }), + ) }) // The disabled (kill-switch) and untrusted branches must short-circuit before @@ -183,7 +213,6 @@ describe("SkillInject.render gating", () => { const boom = () => { throw new Error("must not be reached") } - const spawner = new Proxy({}, { get: boom }) as any const ctx = { ...baseCtx, ask: () => Effect.sync(boom) } as Tool.Context const decompose = (() => Effect.sync(boom)) as unknown as SkillInject.Decompose @@ -195,7 +224,6 @@ describe("SkillInject.render gating", () => { disabled: opts.disabled, cwd: "/tmp", ctx, - spawner, decompose, }), ) From 8f4725729e1be1cdb46a5ad2a7690d343e12bdf5 Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 12:53:01 +0200 Subject: [PATCH 22/71] fix(cli): harden skill shell permission prompt display --- packages/opencode/src/kilocode/skills/display.ts | 11 +++++++++++ packages/opencode/src/kilocode/skills/inject.ts | 5 +++-- packages/opencode/src/tool/skill.ts | 1 + .../opencode/test/kilocode/skills/display.test.ts | 15 +++++++++++++++ .../opencode/test/kilocode/skills/inject.test.ts | 2 ++ packages/tui/src/routes/session/permission.tsx | 9 ++++++--- 6 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 packages/opencode/src/kilocode/skills/display.ts create mode 100644 packages/opencode/test/kilocode/skills/display.test.ts diff --git a/packages/opencode/src/kilocode/skills/display.ts b/packages/opencode/src/kilocode/skills/display.ts new file mode 100644 index 00000000000..93e8ac44f27 --- /dev/null +++ b/packages/opencode/src/kilocode/skills/display.ts @@ -0,0 +1,11 @@ +// Render a skill command for a permission prompt as a single, tamper-evident +// line: escape control chars (CR/LF/ESC/etc.) so a command can't repaint the +// terminal to make the visible text differ from what will execute. +export function displayCommand(command: string) { + return command.replace(/[\u0000-\u001f\u007f-\u009f]/g, (ch) => { + if (ch === "\n") return "\\n" + if (ch === "\r") return "\\r" + if (ch === "\t") return "\\t" + return "\\x" + ch.charCodeAt(0).toString(16).padStart(2, "0") + }) +} diff --git a/packages/opencode/src/kilocode/skills/inject.ts b/packages/opencode/src/kilocode/skills/inject.ts index 4c5d8f98460..3c738ad3d63 100644 --- a/packages/opencode/src/kilocode/skills/inject.ts +++ b/packages/opencode/src/kilocode/skills/inject.ts @@ -46,6 +46,7 @@ export namespace SkillInject { trusted: boolean disabled: boolean cwd: string + skill: string ctx: Tool.Context decompose: Decompose } @@ -82,14 +83,14 @@ export namespace SkillInject { permission: "external_directory", patterns: Array.from(dirs), always: [], - metadata: { skillShell: true }, + metadata: { skillShell: true, skill: opts.skill }, }) } yield* opts.ctx.ask({ permission: "bash", patterns: Array.from(patterns), always: [], - metadata: { skillShell: true }, + metadata: { skillShell: true, skill: opts.skill }, }) // Run each command in the instance directory, bounded by ctx.abort (ESC) and a diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index 06e1bf93b32..758696b02a0 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -46,6 +46,7 @@ export const SkillTool = Tool.define( trusted: info.trusted === true, disabled: flags.disableSkillShell, cwd: yield* InstanceState.directory, + skill: info.name, ctx, decompose: permission.decompose, }) diff --git a/packages/opencode/test/kilocode/skills/display.test.ts b/packages/opencode/test/kilocode/skills/display.test.ts new file mode 100644 index 00000000000..b7780e5138c --- /dev/null +++ b/packages/opencode/test/kilocode/skills/display.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "bun:test" +import { displayCommand } from "@/kilocode/skills/display" + +describe("displayCommand", () => { + it("escapes control characters so a command cannot repaint the prompt", () => { + // CR/ESC would otherwise let the visible text differ from what executes + const out = displayCommand("echo ok\r\x1b[2Krm -rf /\nnext") + expect(out).toBe("echo ok\\r\\x1b[2Krm -rf /\\nnext") + expect(out).not.toMatch(/[\u0000-\u001f]/) + }) + + it("leaves ordinary commands unchanged", () => { + expect(displayCommand("git status --short")).toBe("git status --short") + }) +}) diff --git a/packages/opencode/test/kilocode/skills/inject.test.ts b/packages/opencode/test/kilocode/skills/inject.test.ts index b0b9e00f7ff..f0e4f7f82e5 100644 --- a/packages/opencode/test/kilocode/skills/inject.test.ts +++ b/packages/opencode/test/kilocode/skills/inject.test.ts @@ -196,6 +196,7 @@ describe("skill shell injection", () => { trusted: true, disabled: false, cwd: dir, + skill: "big-shell", ctx: { ...baseCtx, ask: () => Effect.void } as Tool.Context, decompose: ({ command }) => Effect.succeed({ patterns: [command], dirs: [] }), }) @@ -223,6 +224,7 @@ describe("SkillInject.render gating", () => { trusted: opts.trusted, disabled: opts.disabled, cwd: "/tmp", + skill: "test", ctx, decompose, }), diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx index 77524ba6abc..6b1b391fcad 100644 --- a/packages/tui/src/routes/session/permission.tsx +++ b/packages/tui/src/routes/session/permission.tsx @@ -19,6 +19,7 @@ import { ConfigProtection } from "@/kilocode/permission/config-paths" import { splitDiffHunks } from "@/kilocode/tui/diff" import { normalizeUrls } from "@/kilocode/util/url" import { MemoryPermissionRegistry } from "@/kilocode/cli/cmd/tui/routes/session/memory-permission" +import { displayCommand } from "@/kilocode/skills/display" // kilocode_change end import { KILO_BASE_MODE, useBindings, useCommandShortcut } from "../../keymap" import { usePathFormatter } from "../../context/path-format" @@ -292,15 +293,17 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory? } if (permission === "bash") { - // kilocode_change start - skill shell batches list every command + // kilocode_change start - skill shell batches list every command, control-char-escaped so the + // displayed command cannot repaint the line to differ from what executes if (props.request.metadata?.["skillShell"] === true) { const commands = (props.request.patterns ?? []).filter((p): p is string => typeof p === "string") + const skill = typeof props.request.metadata?.["skill"] === "string" ? props.request.metadata["skill"] : undefined return { icon: "#", - title: "Run these skill commands?", + title: skill ? `Run shell commands from skill "${skill}"?` : "Run these skill commands?", body: ( - {(cmd) => {"$ " + cmd}} + {(cmd) => {"$ " + displayCommand(cmd)}} ), } From 9275fa41932b792a83cf0239a9e401d044a992a3 Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 13:11:03 +0200 Subject: [PATCH 23/71] test(vscode): expect interactive flag in permission reply assertions --- .../tests/unit/permission-recovery.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/permission-recovery.test.ts b/packages/kilo-vscode/tests/unit/permission-recovery.test.ts index df7a44d25d6..406076ec606 100644 --- a/packages/kilo-vscode/tests/unit/permission-recovery.test.ts +++ b/packages/kilo-vscode/tests/unit/permission-recovery.test.ts @@ -141,7 +141,9 @@ describe("handlePermissionResponse", () => { await handlePermissionResponse(fake, "p1", "s1", "once", [], []) - expect(replies).toEqual([{ requestID: "p1", reply: "once", directory: "/workspace/.kilo/worktrees/feature" }]) + expect(replies).toEqual([ + { requestID: "p1", reply: "once", directory: "/workspace/.kilo/worktrees/feature", interactive: true }, + ]) }) it("saves selected rules and replies in the recorded SSE directory", async () => { @@ -158,7 +160,9 @@ describe("handlePermissionResponse", () => { deniedAlways: ["rm *"], }, ]) - expect(replies).toEqual([{ requestID: "p1", reply: "reject", directory: "/workspace/.kilo/worktrees/feature" }]) + expect(replies).toEqual([ + { requestID: "p1", reply: "reject", directory: "/workspace/.kilo/worktrees/feature", interactive: true }, + ]) }) it("treats an SDK-wrapped 404 while saving rules as stale", async () => { @@ -192,7 +196,9 @@ describe("handlePermissionResponse", () => { await handlePermissionResponse(fake, "p1", "s1", "once", [], []) - expect(replies).toEqual([{ requestID: "p1", reply: "once", directory: "/workspace/.kilo/worktrees/feature" }]) + expect(replies).toEqual([ + { requestID: "p1", reply: "once", directory: "/workspace/.kilo/worktrees/feature", interactive: true }, + ]) expect(permDirs.has("p1")).toBe(false) expect(messages).toEqual([{ type: "permissionError", permissionID: "p1", stale: true }]) }) From 3ab1122e0fbbff5c6821acb21df7d3d93f52955f Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 29 Jul 2026 14:01:00 +0200 Subject: [PATCH 24/71] feat(agent-manager): add diff scope selector and base branch picker --- .../agent-manager-diff-scope-selector.md | 5 + ...00000-agent-manager-diff-scope-selector.md | 339 ++++++++++++++++++ .../src/agent-manager/AgentManagerProvider.ts | 44 ++- .../src/agent-manager/diff-scope.ts | 57 +++ .../kilo-vscode/src/agent-manager/types.ts | 32 ++ .../agent-manager/worktree-diff-controller.ts | 193 +++++----- .../kilo-vscode/src/diff/sources/catalog.ts | 13 +- .../kilo-vscode/src/diff/sources/staged.ts | 38 +- .../kilo-vscode/src/diff/sources/unstaged.ts | 38 +- .../kilo-vscode/src/diff/sources/worktree.ts | 61 +++- packages/kilo-vscode/src/diff/types.ts | 21 ++ .../tests/unit/agent-manager-arch.test.ts | 4 +- .../kilo-vscode/tests/unit/diff-scope.test.ts | 52 +++ .../agent-manager/AgentManagerApp.tsx | 171 ++++----- .../webview-ui/agent-manager/DiffPanel.tsx | 11 +- .../agent-manager/agent-manager.css | 9 + .../webview-ui/agent-manager/diff-messages.ts | 74 ++++ .../agent-manager/diff-review-scope.ts | 120 +++++++ .../agent-manager/diff-scope-state.ts | 91 +++++ .../webview-ui/agent-manager/i18n/ar.ts | 1 + .../webview-ui/agent-manager/i18n/br.ts | 1 + .../webview-ui/agent-manager/i18n/bs.ts | 1 + .../webview-ui/agent-manager/i18n/da.ts | 1 + .../webview-ui/agent-manager/i18n/de.ts | 1 + .../webview-ui/agent-manager/i18n/en.ts | 1 + .../webview-ui/agent-manager/i18n/es.ts | 1 + .../webview-ui/agent-manager/i18n/fr.ts | 1 + .../webview-ui/agent-manager/i18n/it.ts | 1 + .../webview-ui/agent-manager/i18n/ja.ts | 1 + .../webview-ui/agent-manager/i18n/ko.ts | 1 + .../webview-ui/agent-manager/i18n/nl.ts | 1 + .../webview-ui/agent-manager/i18n/no.ts | 1 + .../webview-ui/agent-manager/i18n/pl.ts | 1 + .../webview-ui/agent-manager/i18n/ru.ts | 1 + .../webview-ui/agent-manager/i18n/th.ts | 1 + .../webview-ui/agent-manager/i18n/tr.ts | 1 + .../webview-ui/agent-manager/i18n/uk.ts | 1 + .../webview-ui/agent-manager/i18n/zh.ts | 1 + .../webview-ui/agent-manager/i18n/zht.ts | 1 + .../webview-ui/agent-manager/revert-file.ts | 17 +- .../diff-viewer/DiffScopeControls.tsx | 55 +++ .../diff-viewer/FullScreenDiffView.tsx | 5 +- .../src/types/messages/extension-messages.ts | 13 + .../src/types/messages/webview-messages.ts | 21 ++ 44 files changed, 1287 insertions(+), 217 deletions(-) create mode 100644 .changeset/agent-manager-diff-scope-selector.md create mode 100644 .kilo/plans/1784100000000-agent-manager-diff-scope-selector.md create mode 100644 packages/kilo-vscode/src/agent-manager/diff-scope.ts create mode 100644 packages/kilo-vscode/tests/unit/diff-scope.test.ts create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/diff-messages.ts create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/diff-review-scope.ts create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/diff-scope-state.ts create mode 100644 packages/kilo-vscode/webview-ui/diff-viewer/DiffScopeControls.tsx diff --git a/.changeset/agent-manager-diff-scope-selector.md b/.changeset/agent-manager-diff-scope-selector.md new file mode 100644 index 00000000000..341e5ee7684 --- /dev/null +++ b/.changeset/agent-manager-diff-scope-selector.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Add a scope selector and base branch picker to the Agent Manager diff review. The side panel and full-screen review now let you switch between Branch, Staged, Unstaged, and Session scopes for the selected worktree, and the Branch scope's base branch can be overridden from a picker next to it. Branch stays the default, so existing review behavior is unchanged. diff --git a/.kilo/plans/1784100000000-agent-manager-diff-scope-selector.md b/.kilo/plans/1784100000000-agent-manager-diff-scope-selector.md new file mode 100644 index 00000000000..f827a2c3417 --- /dev/null +++ b/.kilo/plans/1784100000000-agent-manager-diff-scope-selector.md @@ -0,0 +1,339 @@ +# Bring the Changes scope selector and base branch picker into Agent Manager + +## Goal + +The standalone **Changes** editor panel has a scope selector (`GIT: Branch / Staged / Unstaged`, +`SESSION: Session`) plus a base branch picker (`main → origin/main [Default]`). Agent Manager has +two diff surfaces (compact side panel and full-screen review tab) with **no scope selector and no +base picker**: it always shows one fixed scope against one fixed base. + +Make both Agent Manager surfaces scope-aware and base-aware, reusing the existing components and +extension-side sources rather than duplicating them. + +## What exists today + +### Standalone Changes panel + +| Concern | Where | +|---|---| +| Panel host, ephemeral base override | `src/diff/DiffViewerProvider.ts:26-33`, `:101-119`, `:170-182` | +| Source enumeration and construction | `src/diff/sources/catalog.ts:73-115` | +| Scope select | `webview-ui/diff-viewer/DiffPickerHeader.tsx:50-86` | +| Base picker | `webview-ui/diff-viewer/BaseBranchPicker.tsx:77-142` | +| Branch list + auto base + HEAD | `src/diff/sources/catalog.ts:117-138` | +| Renderer | `webview-ui/diff-viewer/FullScreenDiffView.tsx:95` | + +Sources: `worktree.ts` (Branch), `staged.ts`, `unstaged.ts`, `session.ts`, `turn.ts`. Polling, +dedupe, and lazy per-file detail live in `src/diff/SourceController.ts`. + +### Agent Manager + +| Concern | Where | +|---|---| +| Diff controller (wraps the same `SourceController`) | `src/agent-manager/worktree-diff-controller.ts:35-80` | +| Synthetic single source, hardcoded capabilities | `src/agent-manager/worktree-diff-controller.ts:228-242` | +| Fixed base = `origin/` | `src/agent-manager/worktree-diff-controller.ts:217`, `WorktreeStateManager.ts:57-64` | +| `local` pseudo-context uses auto base | `src/agent-manager/worktree-diff-controller.ts:220-222` | +| Side panel | `webview-ui/agent-manager/DiffPanel.tsx:474-524` | +| Full-screen review (shared component) | `webview-ui/agent-manager/AgentManagerApp.tsx:3105-3131` | +| Data store keyed by session id | `webview-ui/agent-manager/AgentManagerApp.tsx:1675-1699` | + +So both systems already share `SourceController`, `local-diff.ts`, `GitOps`, `FullScreenDiffView`, +`FileTree`, `diff-state.ts`, `diff-requests.ts`. The gap is only the *selection* layer. + +## The architectural mismatch to resolve first + +The two systems key diff sources along orthogonal axes: + +- Standalone: keyed by **scope** (`workspace`, `staged`, `unstaged`, `session:`) inside one + fixed directory (`getWorkspaceRoot()`). +- Agent Manager: keyed by **context** (`sessionId` or `local`), which resolves to a directory and + base, with one fixed scope. + +Integration therefore needs a composite key `(context, scope)`: + +``` +ctx = "local" | "" +scope = "branch" | "staged" | "unstaged" | "session" +id = `${ctx}#${scope}` +``` + +`ctx#branch` is the default and reproduces today's behavior exactly. + +### Hard prerequisite: sources must accept an explicit directory + +Three sources resolve the workspace root themselves and cannot currently point at a worktree: + +- `src/diff/sources/worktree.ts:46`, `:60` +- `src/diff/sources/staged.ts:47` +- `src/diff/sources/unstaged.ts:53` +- `src/diff/sources/catalog.ts:118` (`listWorkspaceBranches`) + +`session.ts` is already directory-parameterized (`catalog.ts:111`), so Session scope is nearly free. + +Each source also constructs its own `GitOps` + `OutputChannel` (`worktree.ts:35-37`, +`staged.ts:43-45`). That is acceptable in the standalone panel where sources swap only on scope +change, but Agent Manager swaps sources on **every session selection**. Inject a `log` function and +reuse Agent Manager's shared `GitOps` (`AgentManagerProvider.ts:140-180`) instead of constructing +per source. + +## Is Branch still the right default per worktree? + +Yes, keep `Branch` as the default for every worktree context. Reasons, strongest first: + +1. **It matches what you ship.** Branch is `merge-base(HEAD, origin/) → current working + tree`: committed work, staged, unstaged, and untracked. That is exactly the payload + `Apply to local` builds (`GitOps.buildWorktreePatch`, used at + `worktree-diff-controller.ts:115`) and what a PR from that branch would contain. Reviewing the + same set you apply or push is the whole point of the review tab. +2. **No silent behavior change.** It is what Agent Manager does today, and the sidebar `Nf +N -N` + badge (`GitStatsPoller.ts:191-224`) uses the same base. A different default would make the + badge and the review disagree on first open. +3. **Stable under base movement.** merge-base semantics mean commits landing on `origin/main` + after the worktree branched do not pollute the diff. +4. **Session scope can be legitimately empty.** It depends on snapshots being enabled and degrades + to a `snapshots-disabled` notice (`sources/session.ts:33-66`). A default that can be empty for + configuration reasons is a bad default. + +One correction to the framing: Branch compares against the base **ref** (`origin/main`), not +against the local main checkout's working tree. If your local `main` has unpushed commits or dirty +files, the worktree diff does not account for them. "What changes if I apply this to my current +checkout" is a different question, answered today only by the `Apply to local` conflict check. A +`Local workspace` scope could answer it directly, but it is a non-goal here (see below). + +## What each scope means in Agent Manager + +| Scope | Worktree context | Local context | Value | +|---|---|---|---| +| Branch | `merge-base(HEAD, origin/)` to working tree | `merge-base(HEAD, auto base)` to working tree | Default. The reviewable/shippable set. | +| Staged | index vs `HEAD` inside the worktree | same, workspace root | "What did I stage for the next commit." Read-only. | +| Unstaged | working tree vs index, plus untracked | same | "What is not committed yet." Read-only. | +| Session | snapshot diff for the selected session, `directory = worktree.path` | selected local session | Highest new value: separates *this agent session's* edits from manual edits and setup-script output. | + +Two honest caveats to design around: + +- On a fresh worktree where the agent never commits (the common case), `Branch` ≈ `Staged` + + `Unstaged`, so the selector adds little until a commit exists. It is still worth shipping because + `Session` is valuable immediately and because committing agents are increasingly common. +- A worktree can hold several sessions. Expose only the **currently selected** session's Session + scope; a per-session submenu is a follow-up, not v1. + +## UI design + +### Full-screen review tab + +Put the controls at the head of the existing left toolbar group +(`FullScreenDiffView.tsx:542-571`), before the unified/split radio. Do **not** add a second row: +the standalone panel's separate header row should collapse into this same slot so both hosts render +one identical toolbar. + +``` ++-----------------------------------------------------------------------------------------------+ +| [Branch v] feat/foo -> origin/main [Default] | (Unified|Split) 12 files +340 -88 | | +| ^ scope ^ base picker ^ existing stats | +| Expand all Send 3 to chat [x] | ++-----------------------------------------------------------------------------------------------+ +| tree | diff | +``` + +### Compact side panel + +`DiffPanel`'s header (`DiffPanel.tsx:476-524`) already competes for width with the radio group, +stats, and three icon buttons in a resizable inspector. Add a **second compact row** under the +existing header rather than cramming one row: + +``` ++------------------------------------------+ +| Changes (Unified|Split) 12f +340 -88 | [expand] [fullscreen] [x] +| [Branch v] -> origin/main | ++------------------------------------------+ +``` + +Below a width threshold, drop the `-> origin/main` hint and keep only `[Branch v]`; the full base +picker stays reachable in the full-screen tab. The side panel must at least *display* the active +scope even when narrow, because scope state is shared with the review tab (single +`SourceController`) and an unexplained staged-only file list is confusing. + +### Dropdown + +Reuse `DiffPickerHeader` unchanged: it already renders grouped options with per-option tooltips +from `diffViewer.source..tooltip` (`webview-ui/src/i18n/en.ts:1235-1248`). + +``` ++--------------------------+ +| GIT | +| Branch | tooltip: all changes vs base, incl. local commits +| Staged | +| Unstaged | +| SESSION | +| Session | ++--------------------------+ +``` + +Deliberately **no per-scope file counts** in the dropdown. Counts would require polling every scope +continuously (four git pipelines per tick per worktree), which is not worth it. The Branch row's +"vs origin/main" context is already carried by the adjacent base picker. + +### UX traps to close + +- **Apply to local always applies Branch scope.** It builds its patch from + `remoteRef(worktree)` regardless of what the review shows. When the active scope is not `Branch`, + either label the button "Apply branch changes" or disable it with a tooltip explaining that apply + is branch-scoped. Otherwise users will read "Apply" as "apply what I am looking at". +- **Revert must follow source capabilities.** `staged` and `unstaged` declare + `capabilities.revert: false` (`staged.ts:29`, `unstaged.ts:29`). Agent Manager currently + hardcodes `{ revert: true, comments: true }` (`worktree-diff-controller.ts:233`). Send the real + descriptor capabilities and pass them into the already-existing `canRevert` / `canComment` props + (`FullScreenDiffView.tsx:88-91`). +- **Scope switch should not flash stale files.** Key the webview diff store by the composite key so + switching back to a previously fetched scope is instant and never renders another scope's files. + +## Base branch picker: semantics and the consistency problem + +In the standalone panel the base override is ephemeral and its `Default` means +"auto-resolved tracking or repo default" (`shared/target.ts:4-27`). In Agent Manager, `Default` +should mean **the worktree's recorded parent** (`origin/`), which is a deliberate +recorded value, not a guess. + +The real issue: as soon as a base override exists, the review toolbar and the sidebar badge can +disagree, because `GitStatsPoller` and `Apply to local` both derive from `remoteRef(worktree)`. + +Two coherent options: + +**A. View-local ephemeral override (cheap).** Mirrors the standalone panel. Zero risk to stats, +apply, and PR flows, but the sidebar badge will visibly disagree with the review toolbar the moment +the user overrides. Needs the review header to always show `vs ` so the difference is legible. + +**B. Persisted worktree base (recommended).** Treat the picker as editing the worktree's base: +persist `parentBranch` / `remote` (or a new `diffBase`) in `agent-manager.json` +(`WorktreeStateManager.ts:16-45`), and let stats, review, and apply all read the same value. This +keeps a single base per worktree with no divergence, and it fixes a real existing gap: today a +worktree's base cannot be changed after creation, so branching off the wrong base is unrecoverable +without recreating the worktree. + +Recommendation: ship **A** in the same phase as the scope selector to keep the diff small, then +promote to **B** as its own change, because B needs stats invalidation, apply, and PR paths updated +together and deserves isolated review. If B is chosen up front, do not also keep A: two competing +bases is worse than either. + +## Reuse plan + +The point of this work is to add zero new diff rendering code. + +### Reuse as-is + +- `DiffPickerHeader.tsx` (scope select, grouping, tooltips) +- `BaseBranchPicker.tsx` and `webview-ui/src/components/shared/BranchSelect.tsx` +- `FullScreenDiffView.tsx`, `FileTree.tsx`, `VirtualDiffList.tsx`, `diff-state.ts`, + `diff-requests.ts`, `diff-open-policy.ts` +- `SourceController.ts`, `sources/session.ts`, `local-diff.ts`, `GitOps.ts` +- All `diffViewer.source.*` and `diffViewer.baseBranch.*` i18n keys, already translated + +### New shared pieces (small) + +1. `webview-ui/diff-viewer/DiffScopeControls.tsx` - composes `DiffPickerHeader` + + `BaseBranchPicker`, plus a `compact` flag for the side panel. Consumed by three hosts: + standalone `DiffViewerApp`, Agent Manager review tab, Agent Manager side panel. +2. A leading toolbar slot prop on both renderers: `FullScreenDiffView` (`lead?: JSXElement`, + rendered first inside `am-review-toolbar-left`) and `DiffPanel` (second header row). Once + `FullScreenDiffView` has the slot, move the standalone panel's separate header row into it so + both hosts share one layout. +3. `webview-ui/agent-manager/diff-scope-state.ts` - composite key helpers and per-context scope + signal. Keep this out of `AgentManagerApp.tsx` (already 3191 lines). + +### Extension side + +4. Generalize `PanelContext` so `workspaceRoot` is the resolved diff directory, and make + `DiffSourceCatalog` build sources for that directory. `SourceController.setContext` is already + called by Agent Manager (`worktree-diff-controller.ts:79`, `:188`), so the plumbing exists. +5. `WorktreeDiffController.source()` delegates to `DiffSourceCatalog` instead of returning its + bespoke descriptor. This deletes Agent Manager's synthetic source and gets staged, unstaged, and + session scopes for free. +6. `src/agent-manager/diff-scope.ts` (new, vscode-free): composite id parse/format, scope to source + id mapping, capability lookup. `worktree-diff-controller.ts` is already 332 lines and + `tests/unit/agent-manager-arch.test.ts` enforces `maxLines` caps that must not be raised. + +## Message and state changes + +Inbound (webview to extension), all additive and optional so existing callers keep working: + +| Message | Change | +|---|---| +| `agentManager.requestWorktreeDiff` | add `scope?` | +| `agentManager.startDiffWatch` | add `scope?` | +| `agentManager.requestWorktreeDiffFile` | add `scope?` | +| `agentManager.revertWorktreeFile` | add `scope?` | +| `agentManager.requestDiffBranches` | new, `{ sessionId }` | +| `agentManager.setDiffBaseBranch` | new, `{ sessionId, branch? }` where `undefined` clears | + +No separate "set scope" message: a scope switch is a re-activation via `startDiffWatch` / +`requestWorktreeDiff` with the new scope. + +Outbound: `worktreeDiff`, `worktreeDiffLoading`, `worktreeDiffFile`, `revertWorktreeFileResult` +gain `scope` and the source `capabilities`; new `agentManager.diffBranches` reuses the existing +`WorkspaceBranchesResult` shape (`sources/catalog.ts:22-33`). + +Webview state: `diffDatas` and `diffFileLoading` re-key from `sessionId` to `${ctx}#${scope}`; +`diffSessionKey()` (`AgentManagerApp.tsx:1694-1699`) appends the scope so accordion open state +resets per scope. + +Known breakage to fix during the refactor: `shouldStopForWorktree` passes +`this.controller.currentId` into `shouldStopDiffPolling`, which compares it against orphaned +session ids (`delete-worktree.ts:11-20`). Pass the parsed context id, not the composite id. + +Target resolution ordering: today `ensureTarget` resolves lazily inside `fetch()`. With the catalog +owning source construction, the directory and base must be resolved *before* +`controller.activate()`. `activate` is already awaited by `request` and `start`, so awaiting +`ready()` plus target resolution first is safe, but this is the main refactor risk in the change. + +## Phasing + +1. **Parameterize sources by directory.** Add explicit `dir` / injected `log` / shared `GitOps` to + `worktree.ts`, `staged.ts`, `unstaged.ts`, and `listWorkspaceBranches`. No user-visible change; + standalone panel keeps passing the workspace root. Verify the standalone panel is unaffected. +2. **Composite keying and catalog delegation.** `diff-scope.ts`, controller delegates to the + catalog, messages gain `scope` and `capabilities`, webview re-keys. Still one scope exposed, so + still no visible change. This is the risky phase and should land on its own. +3. **Scope selector UI.** `DiffScopeControls`, toolbar slots in both hosts, standalone header moved + into the shared slot, `canRevert` / `canComment` wired from capabilities, Apply-scope guard. +4. **Base picker in Agent Manager.** `requestDiffBranches` / `setDiffBaseBranch`, ephemeral + override (option A), `Default` labeled as the worktree's recorded parent. +5. **Optional follow-up.** Promote the override to a persisted worktree base (option B) shared with + stats, apply, and PR. + +## Verification + +- `packages/kilo-vscode/`: `bun run typecheck`, `bun run lint`, `bun run test:unit`, `bun run knip` + (new exports must be imported somewhere). +- New unit tests: scope to source id mapping, composite id parse/format, base override resolution + for a worktree directory, and `shouldStopDiffPolling` with composite ids. Existing coverage to + keep green: `tests/unit/local-diff.test.ts`, `tests/unit/agent-manager-arch.test.ts`. +- Visual regression stories for the toolbar in both hosts, per the `vscode-visual-regression` + skill. +- Manual (self-test instance): worktree with a commit plus dirty files, switch Branch / Staged / + Unstaged / Session, confirm counts differ correctly, revert hidden in read-only scopes, side + panel and review tab stay in sync, base override changes the file set, and switching worktrees + resets to Branch. +- Changeset required (user-facing feature). + +## Non-goals + +- A `Local workspace` scope comparing a worktree against the local checkout's working tree. Useful, + but it needs a tree-to-tree diff path that does not exist in `local-diff.ts` and it overlaps with + the `Apply to local` conflict check. +- Per-session Session scope submenu when a worktree holds multiple sessions. +- Turn scope inside Agent Manager. Per-turn changes already open the standalone panel from the + transcript (`VscodeSessionTurn.tsx:179-200`). +- Per-scope file counts in the dropdown. +- Merging the standalone Changes panel into Agent Manager. Phase 3 makes them converge visually; + actually collapsing the two hosts is a separate decision. + +## Open questions + +1. Option A or B for the base picker (view-local override vs persisted worktree base). B is the + better end state; A is the smaller step. +2. Should the scope persist per worktree in `agent-manager.json` alongside `reviewDiffStyle`, or + always reset to Branch on selection change? Resetting is more predictable; persisting is less + repetitive for someone who lives in Session scope. +3. Should the side panel's scope control be interactive or display-only, given how little width it + has? diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index bf72af74ab8..2556bd391db 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -4,10 +4,12 @@ import type { KiloClient, Session } from "@kilocode/sdk/v2/client" import type { KiloConnectionService } from "../services/cli-backend" import { getErrorMessage } from "../kilo-provider-utils" import { resolveLocalDiffTarget } from "../diff/shared/target" +import { DiffSourceCatalog } from "../diff/sources/catalog" import { getDiffMarkdownRender, setDiffMarkdownRender } from "../review-settings" import { isAbsolutePath } from "../path-utils" import { WorktreeManager, type CreateWorktreeResult } from "./WorktreeManager" import { remoteRef, WorktreeStateManager, type Worktree } from "./WorktreeStateManager" +import { composeDiffId, normalizeScope } from "./diff-scope" import { handleSection } from "./section-handler" import { normalizeBaseBranch } from "./base-branch" import { GitStatsPoller, type LocalStats, type WorktreePresenceResult, type WorktreeStats } from "./GitStatsPoller" @@ -71,6 +73,7 @@ export class AgentManagerProvider implements Disposable { private orchestration: AgentManagerOrchestrationBridge private gitOps: GitOps private diffs: WorktreeDiffController + private diffCatalog: DiffSourceCatalog private naming: BranchNamingController private staleWorktreeIds = new Set() private toolRequests = new Set() @@ -148,12 +151,13 @@ export class AgentManagerProvider implements Disposable { log: (msg) => this.log(msg), }) const local = createLocalDiff(this.gitOps, (...args) => this.log(...args)) + this.diffCatalog = new DiffSourceCatalog(this.connectionService) this.diffs = new WorktreeDiffController({ getState: () => this.getStateManager(), getRoot: () => this.getRoot(), getStateReady: () => this.stateReady, + catalog: this.diffCatalog, git: this.gitOps, - localDiff: local.summary, localDiffFile: local.file, post: (msg) => this.postToWebview(msg), log: (...args) => this.log(...args), @@ -679,11 +683,11 @@ export class AgentManagerProvider implements Disposable { private onDiffMessage(m: AgentManagerInMessage): Record | null | undefined { if (m.type === "agentManager.requestWorktreeDiff") { - void this.diffs.request(m.sessionId) + void this.diffs.request(composeDiffId(m.sessionId, normalizeScope(m.scope))) return null } if (m.type === "agentManager.requestWorktreeDiffFile") { - void this.diffs.requestFile(m.sessionId, m.file) + void this.diffs.requestFile(composeDiffId(m.sessionId, normalizeScope(m.scope)), m.file) return null } if (m.type === "agentManager.applyWorktreeDiff") { @@ -691,23 +695,52 @@ export class AgentManagerProvider implements Disposable { return null } if (m.type === "agentManager.revertWorktreeFile") { - void this.diffs.revert(m.sessionId, m.file) + void this.diffs.revert(composeDiffId(m.sessionId, normalizeScope(m.scope)), m.file) return null } if (m.type === "agentManager.startDiffWatch") { - this.diffs.start(m.sessionId) + this.diffs.start(composeDiffId(m.sessionId, normalizeScope(m.scope))) return null } if (m.type === "agentManager.stopDiffWatch") { this.diffs.stop() return null } + if (m.type === "agentManager.requestDiffBranches") { + void this.sendDiffBranches(m.sessionId, m.scope) + return null + } + if (m.type === "agentManager.setDiffBaseBranch") { + void this.diffs.setBase(composeDiffId(m.sessionId, normalizeScope(m.scope)), m.branch).then(() => { + void this.sendDiffBranches(m.sessionId, m.scope) + }) + return null + } if (m.type === "agentManager.openFile") { this.openWorktreeFile(m.sessionId, m.filePath, m.line, m.column) return null } } + private async sendDiffBranches(sessionId: string, scope?: string): Promise { + const id = composeDiffId(sessionId, normalizeScope(scope)) + const result = await this.diffs.branches(id).catch((err) => { + this.log("Failed to list diff branches:", err instanceof Error ? err.message : String(err)) + return undefined + }) + if (!result) return + this.postToWebview({ + type: "agentManager.diffBranches", + sessionId: id, + branches: result.branches, + defaultBranch: result.defaultBranch, + autoBase: result.autoBase, + currentBase: result.currentBase, + isAuto: result.isAuto, + currentBranch: result.currentBranch, + }) + } + private onBridgeMessage(m: AgentManagerInMessage): Record | null | undefined { if (m.type !== "openFile") return undefined @@ -1914,6 +1947,7 @@ export class AgentManagerProvider implements Disposable { this.orchestration.dispose() this.visiblePresence.clear() this.diffs.stop() + this.diffCatalog.dispose() this.naming.dispose() this.statsPoller.stop() this.gitOps.dispose() diff --git a/packages/kilo-vscode/src/agent-manager/diff-scope.ts b/packages/kilo-vscode/src/agent-manager/diff-scope.ts new file mode 100644 index 00000000000..21d2e007935 --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/diff-scope.ts @@ -0,0 +1,57 @@ +/** + * Composite diff-source keying for Agent Manager. + * + * Agent Manager keys diff sources by *context* (a session id, or the `local` + * workspace pseudo-context) while the standalone Changes viewer keys by + * *scope* (branch / staged / unstaged / session). To expose scopes in Agent + * Manager we compose the two into a single id the SourceController can build. + * + * ctx = "local" | "" + * scope = "branch" | "staged" | "unstaged" | "session" + * id = `${ctx}#${scope}` + * + * `ctx#branch` is the default and reproduces the pre-scope behavior exactly. + */ + +export type DiffScope = "branch" | "staged" | "unstaged" | "session" + +export const DEFAULT_DIFF_SCOPE: DiffScope = "branch" + +const SEP = "#" + +export function composeDiffId(ctx: string, scope: DiffScope): string { + return `${ctx}${SEP}${scope}` +} + +/** + * Split a composite id back into context and scope. Tolerates a bare context + * id (no separator) by assuming the default branch scope, which keeps the + * pre-scope messages working unchanged. + */ +export function parseDiffId(id: string): { ctx: string; scope: DiffScope } { + const idx = id.lastIndexOf(SEP) + if (idx === -1) return { ctx: id, scope: DEFAULT_DIFF_SCOPE } + const scope = id.slice(idx + SEP.length) + if (isDiffScope(scope)) return { ctx: id.slice(0, idx), scope } + return { ctx: id, scope: DEFAULT_DIFF_SCOPE } +} + +export function isDiffScope(value: string): value is DiffScope { + return value === "branch" || value === "staged" || value === "unstaged" || value === "session" +} + +export function normalizeScope(value: unknown): DiffScope { + return typeof value === "string" && isDiffScope(value) ? value : DEFAULT_DIFF_SCOPE +} + +/** + * Map a scope to the underlying standalone-viewer source id the catalog knows + * how to build. `branch` maps to the workspace source; `session` is handled + * separately because it needs the session id embedded in the source id. + */ +export function scopeToSourceId(scope: DiffScope, ctx: string): string { + if (scope === "staged") return "staged" + if (scope === "unstaged") return "unstaged" + if (scope === "session") return `session:${ctx}` + return "workspace" +} diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 8950c68d22f..ae72d2cd015 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -286,6 +286,18 @@ interface RevertWorktreeFileResultMessage { message: string } +/** Branch picker data for a context's diff directory. */ +interface DiffBranchesMessage { + type: "agentManager.diffBranches" + sessionId: string + branches: BranchListItem[] + defaultBranch: string + autoBase?: string + currentBase?: string + isAuto: boolean + currentBranch?: string +} + interface PRStatusOutMessage { type: "agentManager.prStatus" worktreeId: string @@ -324,6 +336,7 @@ export type AgentManagerOutMessage = | WorktreeDiffMessage | WorktreeDiffFileMessage | RevertWorktreeFileResultMessage + | DiffBranchesMessage | PRStatusOutMessage | ActionOutMessage | RunStatusMessage @@ -512,6 +525,7 @@ interface ImportFromPRIn { interface RequestWorktreeDiffIn { type: "agentManager.requestWorktreeDiff" sessionId: string + scope?: string } interface ApplyWorktreeDiffIn { @@ -524,11 +538,13 @@ interface RequestWorktreeDiffFileIn { type: "agentManager.requestWorktreeDiffFile" sessionId: string file: string + scope?: string } interface StartDiffWatchIn { type: "agentManager.startDiffWatch" sessionId: string + scope?: string } interface StopDiffWatchIn { @@ -539,6 +555,20 @@ interface RevertWorktreeFileIn { type: "agentManager.revertWorktreeFile" sessionId: string file: string + scope?: string +} + +interface RequestDiffBranchesIn { + type: "agentManager.requestDiffBranches" + sessionId: string + scope?: string +} + +interface SetDiffBaseBranchIn { + type: "agentManager.setDiffBaseBranch" + sessionId: string + scope?: string + branch?: string } interface RefreshPRIn { @@ -803,6 +833,8 @@ export type AgentManagerInMessage = | StartDiffWatchIn | StopDiffWatchIn | RevertWorktreeFileIn + | RequestDiffBranchesIn + | SetDiffBaseBranchIn | RefreshPRIn | OpenPRIn | OpenSessionsIn diff --git a/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts b/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts index 75d538cc35c..dff233ca5cc 100644 --- a/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts +++ b/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts @@ -1,12 +1,13 @@ import { SourceController } from "../diff/SourceController" import { resolveLocalDiffTarget } from "../diff/shared/target" import { WorktreeDiffReverter, type StatusResolver } from "../diff/shared/reverter" -import type { DiffFile } from "../diff/types" -import type { DiffSource, DiffSourceDescriptor, DiffSourceFetch } from "../diff/sources/types" +import type { DiffFile, PanelContext } from "../diff/types" +import type { DiffSource } from "../diff/sources/types" +import type { DiffSourceCatalog } from "../diff/sources/catalog" import type { ApplyConflict, GitOps } from "./GitOps" import { shouldStopDiffPolling } from "./delete-worktree" -import { Semaphore } from "./semaphore" import { remoteRef, type ManagedSession, type WorktreeStateManager } from "./WorktreeStateManager" +import { parseDiffId, scopeToSourceId } from "./diff-scope" import type { AgentManagerOutMessage, WorktreeDiffEntry } from "./types" const LOCAL_DIFF_ID = "local" as const @@ -19,14 +20,11 @@ export interface WorktreeDiffControllerContext { getState: () => WorktreeStateManager | undefined getRoot: () => string | undefined getStateReady: () => Promise | undefined - /** - * In-process diff paths deliberately bypass the SDK client to keep git spawns - * out of the Bun `kilo serve` process (see oven-sh/bun#18265). - */ + /** Builds the underlying per-scope diff sources (workspace/staged/unstaged/session). */ + catalog: DiffSourceCatalog + /** Shared git ops, injected into sources so they don't spawn their own channels. */ git: GitOps - /** In-process diff summary (replaces client.worktree.diffSummary). */ - localDiff: (dir: string, base: string) => Promise - /** In-process single-file diff (replaces client.worktree.diffFile). */ + /** In-process single-file diff (replaces client.worktree.diffFile). Used by revert. */ localDiffFile: (dir: string, base: string, file: string) => Promise post: (msg: AgentManagerOutMessage) => void log: (...args: unknown[]) => void @@ -34,13 +32,14 @@ export interface WorktreeDiffControllerContext { export class WorktreeDiffController { private readonly controller: SourceController - private readonly details = new Semaphore(3) private target: Target | undefined private applying: string | undefined + /** Ephemeral per-context base override, keyed by context id. */ + private baseOverrides = new Map() constructor(private readonly ctx: WorktreeDiffControllerContext) { this.controller = new SourceController( - (id) => this.source(id), + (id, ctx) => this.source(id, ctx), () => [], (msg) => this.ctx.post(msg as AgentManagerOutMessage), { @@ -80,7 +79,11 @@ export class WorktreeDiffController { } public shouldStopForWorktree(path: string, sessions: ManagedSession[]): boolean { - return shouldStopDiffPolling(path, sessions, this.target, this.controller.currentId) + // Pass the parsed context id, not the composite id, so the orphaned-session + // check matches real session ids. + const current = this.controller.currentId + const ctxId = current ? parseDiffId(current).ctx : undefined + return shouldStopDiffPolling(path, sessions, this.target, ctxId) } public async apply(worktreeId: string, value?: unknown): Promise { @@ -144,38 +147,38 @@ export class WorktreeDiffController { } } - public async revert(sessionId: string, file: string): Promise { + public async revert(id: string, file: string): Promise { if (!file) return - if (this.controller.currentId !== sessionId) { - const result = await this.revertFile(sessionId, file) - this.postRevertResult(sessionId, file, result) + if (this.controller.currentId !== id) { + const result = await this.revertFile(id, file) + this.postRevertResult(id, file, result) return } await this.controller.revertFile(file) } - public async request(sessionId: string): Promise { - if (this.controller.currentId !== sessionId) { - await this.activate(sessionId, false, true) + public async request(id: string): Promise { + if (this.controller.currentId !== id) { + await this.activate(id, false, true) return } this.target = undefined await this.controller.refresh() } - public async requestFile(sessionId: string, file: string): Promise { + public async requestFile(id: string, file: string): Promise { if (!file) return - if (this.controller.currentId !== sessionId) { - this.ctx.post({ type: "agentManager.worktreeDiffFile", sessionId, file, diff: null }) + if (this.controller.currentId !== id) { + this.ctx.post({ type: "agentManager.worktreeDiffFile", sessionId: id, file, diff: null }) return } await this.controller.requestFile(file) } - public start(sessionId: string): void { - if (this.controller.isPolling && this.controller.currentId === sessionId) return - this.ctx.log(`Starting diff polling for session ${sessionId}`) - void this.activate(sessionId, true, true) + public start(id: string): void { + if (this.controller.isPolling && this.controller.currentId === id) return + this.ctx.log(`Starting diff polling for ${id}`) + void this.activate(id, true, true) } public stop(): void { @@ -183,92 +186,113 @@ export class WorktreeDiffController { this.target = undefined } - private async activate(sessionId: string, poll: boolean, fetch: boolean): Promise { + /** + * Set or clear an ephemeral base override for a context (worktree or local), + * then re-activate the current source so it refetches against the new base. + * Passing undefined clears the override and falls back to the recorded parent. + */ + public async setBase(id: string, branch: string | undefined): Promise { + const { ctx } = parseDiffId(id) + if (branch) this.baseOverrides.set(ctx, branch) + else this.baseOverrides.delete(ctx) this.target = undefined - this.controller.setContext({ workspaceRoot: this.ctx.getRoot() }) - await this.controller.activate(sessionId, { poll, fetch }) + await this.controller.reactivate() } - private async resolve(sessionId: string): Promise<{ directory: string; baseBranch: string } | undefined> { - if (sessionId === LOCAL_DIFF_ID) return await this.resolveLocal() + /** Branch picker data for a context's directory, using any active override. */ + public async branches(id: string) { + await this.ready("stateReady rejected, continuing diff branches resolve:") + const { ctx } = parseDiffId(id) + const target = await this.resolve(ctx) + if (!target) return undefined + return await this.ctx.catalog.listWorkspaceBranches(this.baseOverrides.get(ctx), target.directory) + } + + private async activate(id: string, poll: boolean, fetch: boolean): Promise { + this.target = undefined + await this.ready("stateReady rejected, continuing diff activate:") + const { ctx } = parseDiffId(id) + const resolved = await this.resolve(ctx) + this.target = resolved ? { sessionId: id, ...resolved } : undefined + this.controller.setContext({ + workspaceRoot: this.ctx.getRoot(), + dir: resolved?.directory, + // The resolved base already bakes in any ephemeral override (see + // resolve()), so pass it as the explicit base and leave + // baseBranchOverride unset to avoid double resolution. + baseBranch: resolved?.baseBranch, + // Agent Manager always knows its intended directory (LOCAL resolves to + // the root). Never fall back to the workspace root for an unresolvable + // worktree context — return an empty diff instead. + strictDir: true, + git: this.ctx.git, + log: (...args) => this.ctx.log(...args), + }) + await this.controller.activate(id, { poll, fetch }) + } + + private async resolve(ctxId: string): Promise<{ directory: string; baseBranch: string } | undefined> { + if (ctxId === LOCAL_DIFF_ID) return await this.resolveLocal() const state = this.ctx.getState() if (!state) { - this.ctx.log(`resolveDiffTarget: no state manager for session ${sessionId}`) + this.ctx.log(`resolveDiffTarget: no state manager for context ${ctxId}`) return undefined } - const session = state.getSession(sessionId) + const session = state.getSession(ctxId) if (!session) { this.ctx.log( - `resolveDiffTarget: session ${sessionId} not found in state (${state.getSessions().length} total sessions)`, + `resolveDiffTarget: session ${ctxId} not found in state (${state.getSessions().length} total sessions)`, ) return undefined } if (!session.worktreeId) { - this.ctx.log(`resolveDiffTarget: session ${sessionId} has no worktreeId (local session)`) + this.ctx.log(`resolveDiffTarget: session ${ctxId} has no worktreeId (local session)`) return undefined } const worktree = state.getWorktree(session.worktreeId) if (!worktree) { - this.ctx.log(`resolveDiffTarget: worktree ${session.worktreeId} not found for session ${sessionId}`) + this.ctx.log(`resolveDiffTarget: worktree ${session.worktreeId} not found for session ${ctxId}`) return undefined } - return { directory: worktree.path, baseBranch: remoteRef(worktree) } + const base = this.baseOverrides.get(ctxId) ?? remoteRef(worktree) + return { directory: worktree.path, baseBranch: base } } private async resolveLocal(): Promise<{ directory: string; baseBranch: string } | undefined> { - return await resolveLocalDiffTarget(this.ctx.git, (...args) => this.ctx.log(...args), this.ctx.getRoot()) + const root = this.ctx.getRoot() + if (!root) return undefined + const override = this.baseOverrides.get(LOCAL_DIFF_ID) + if (override) { + return { directory: root, baseBranch: override } + } + return await resolveLocalDiffTarget(this.ctx.git, (...args) => this.ctx.log(...args), root) } private async ready(msg: string): Promise { await this.ctx.getStateReady()?.catch((err) => this.ctx.log(msg, err)) } - private source(sessionId: string): DiffSource { - const descriptor: DiffSourceDescriptor = { - id: sessionId, - type: "workspace", - group: "Git", - capabilities: { revert: true, comments: true }, - } - + /** + * Build the active source for a composite id by delegating to the catalog. + * The composite id (ctx#scope) is preserved as the descriptor id so the + * webview keys diff data by context+scope. Context resolution (dir/base) + * already happened in activate() and is carried by the PanelContext. + */ + private source(id: string, panelCtx: PanelContext): DiffSource { + const { ctx, scope } = parseDiffId(id) + const built = this.ctx.catalog.build(scopeToSourceId(scope, ctx), panelCtx) return { - descriptor, - fetch: () => this.fetch(sessionId), - fetchFile: (file) => this.fetchFile(sessionId, file), - revert: (file) => this.revertFile(sessionId, file), + ...built, + descriptor: { ...built.descriptor, id }, } } - private async fetch(sessionId: string): Promise { - await this.ready("stateReady rejected, continuing diff resolve:") - const target = await this.ensureTarget(sessionId) - if (!target) return { diffs: [], stopPolling: true } - - const files = await this.ctx.localDiff(target.directory, target.baseBranch) - this.ctx.log(`Worktree diff returned ${files.length} file(s) for session ${sessionId}`) - return { diffs: files as AgentManagerDiffFile[] } - } - - private async fetchFile(sessionId: string, file: string): Promise { - await this.ready("stateReady rejected, continuing diff detail resolve:") - return this.details.run(async () => { - const target = await this.ensureTarget(sessionId) - if (!target) return null - - try { - return (await this.ctx.localDiffFile(target.directory, target.baseBranch, file)) as AgentManagerDiffFile | null - } catch (error) { - this.ctx.log("Failed to fetch worktree diff file:", error) - return null - } - }) - } - - private async revertFile(sessionId: string, file: string): Promise<{ ok: boolean; message: string }> { + private async revertFile(id: string, file: string): Promise<{ ok: boolean; message: string }> { await this.ready("stateReady rejected, continuing revert resolve:") - const target = await this.resolveTarget(sessionId) + const { ctx } = parseDiffId(id) + const target = await this.resolve(ctx) if (!target) return { ok: false, message: "Could not resolve diff target" } try { @@ -285,19 +309,6 @@ export class WorktreeDiffController { } } - private async ensureTarget(sessionId: string): Promise { - if (this.controller.currentId !== sessionId) return undefined - if (this.target?.sessionId === sessionId) return this.target - return await this.resolveTarget(sessionId) - } - - private async resolveTarget(sessionId: string): Promise { - const target = await this.resolve(sessionId) - if (!target) return undefined - this.target = { sessionId, ...target } - return this.target - } - private postRevertResult(sessionId: string, file: string, result: { ok: boolean; message: string }): void { this.ctx.post({ type: "agentManager.revertWorktreeFileResult", diff --git a/packages/kilo-vscode/src/diff/sources/catalog.ts b/packages/kilo-vscode/src/diff/sources/catalog.ts index fa36aaecf2d..608d4580afc 100644 --- a/packages/kilo-vscode/src/diff/sources/catalog.ts +++ b/packages/kilo-vscode/src/diff/sources/catalog.ts @@ -90,12 +90,13 @@ export class DiffSourceCatalog implements vscode.Disposable { } build(id: string, ctx: PanelContext): DiffSource { + const opts = { dir: () => ctx.dir, strictDir: ctx.strictDir, git: ctx.git, log: ctx.log } if (id === WORKSPACE_SOURCE_ID) { - return createWorktreeDiffSource({ baseBranchOverride: ctx.baseBranchOverride }) + return createWorktreeDiffSource({ ...opts, baseBranchOverride: ctx.baseBranchOverride, baseBranch: ctx.baseBranch }) } - if (id === STAGED_SOURCE_ID) return createStagedDiffSource() - if (id === UNSTAGED_SOURCE_ID) return createUnstagedDiffSource() + if (id === STAGED_SOURCE_ID) return createStagedDiffSource(opts) + if (id === UNSTAGED_SOURCE_ID) return createUnstagedDiffSource(opts) if (id.startsWith(TURN_PREFIX)) { const [sessionId, messageId] = id.slice(TURN_PREFIX.length).split(":") @@ -108,14 +109,14 @@ export class DiffSourceCatalog implements vscode.Disposable { if (id.startsWith(SESSION_PREFIX)) { const sessionId = id.slice(SESSION_PREFIX.length) if (!sessionId) throw new Error(`DiffSourceCatalog.build: empty session id in "${id}"`) - return createSessionDiffSource(sessionId, this.sessionFetch, ctx.workspaceRoot, this.checkSnapshotsEnabled) + return createSessionDiffSource(sessionId, this.sessionFetch, ctx.dir ?? ctx.workspaceRoot, this.checkSnapshotsEnabled) } throw new Error(`DiffSourceCatalog.build: unknown source id "${id}"`) } - async listWorkspaceBranches(override: string | undefined): Promise { - const root = getWorkspaceRoot() + async listWorkspaceBranches(override: string | undefined, dir?: string): Promise { + const root = dir ?? getWorkspaceRoot() if (!root) return undefined const git = this.ensureBranchGit() diff --git a/packages/kilo-vscode/src/diff/sources/staged.ts b/packages/kilo-vscode/src/diff/sources/staged.ts index d21a508902b..8cd250743cb 100644 --- a/packages/kilo-vscode/src/diff/sources/staged.ts +++ b/packages/kilo-vscode/src/diff/sources/staged.ts @@ -34,17 +34,39 @@ function stamp(entry: FileEntry, before: string, after: string): FileEntry { return { ...entry, stamp: `${entry.status}:${before}:${after}` } } +export interface StagedDiffSourceOptions { + /** + * Resolve the directory to diff. Defaults to the VS Code workspace root. + * Agent Manager passes a worktree path so the source diffs inside the + * worktree rather than the main checkout. + */ + dir?: () => string | undefined + /** + * When true, a `dir` that resolves to undefined yields an empty diff rather + * than falling back to the workspace root. + */ + strictDir?: boolean + /** Shared GitOps / log so sources don't each spawn their own channel. */ + git?: GitOps + log?: (...args: unknown[]) => void +} + /** * Diff between the git index and HEAD — what `git diff --cached` would show. * Polls on the standard interval; revert isn't supported (use `git reset` from * a real git client). Read-only view. */ -export function createStagedDiffSource(): DiffSource { - const output = vscode.window.createOutputChannel("Kilo Diff: Staged") - const log = (...args: unknown[]) => appendOutput(output, "StagedDiffSource", ...args) - const git = new GitOps({ log }) +export function createStagedDiffSource(opts: StagedDiffSourceOptions = {}): DiffSource { + const output = opts.git ? undefined : vscode.window.createOutputChannel("Kilo Diff: Staged") + const log = opts.log ?? ((...args: unknown[]) => appendOutput(output!, "StagedDiffSource", ...args)) + const git = opts.git ?? new GitOps({ log }) - const root = (): string | undefined => getWorkspaceRoot() + const root = (): string | undefined => { + const dir = opts.dir?.() + if (dir) return dir + if (opts.strictDir) return undefined + return getWorkspaceRoot() + } const listEntries = async (dir: string): Promise => { const [nameStatus, numstat, raw] = await Promise.all([ @@ -150,8 +172,10 @@ export function createStagedDiffSource(): DiffSource { }, dispose(): void { - git.dispose() - output.dispose() + // Only dispose resources we own (created here). Injected git/log are + // owned by the caller. + if (!opts.git) git.dispose() + output?.dispose() }, } } diff --git a/packages/kilo-vscode/src/diff/sources/unstaged.ts b/packages/kilo-vscode/src/diff/sources/unstaged.ts index b4d0c996161..5fcb03793d2 100644 --- a/packages/kilo-vscode/src/diff/sources/unstaged.ts +++ b/packages/kilo-vscode/src/diff/sources/unstaged.ts @@ -40,17 +40,39 @@ function stamp(entry: FileEntry, before: string, after: string): FileEntry { return { ...entry, stamp: `${entry.status}:${before}:${after}` } } +export interface UnstagedDiffSourceOptions { + /** + * Resolve the directory to diff. Defaults to the VS Code workspace root. + * Agent Manager passes a worktree path so the source diffs inside the + * worktree rather than the main checkout. + */ + dir?: () => string | undefined + /** + * When true, a `dir` that resolves to undefined yields an empty diff rather + * than falling back to the workspace root. + */ + strictDir?: boolean + /** Shared GitOps / log so sources don't each spawn their own channel. */ + git?: GitOps + log?: (...args: unknown[]) => void +} + /** * Diff between the working tree and the index — what `git diff` shows for * tracked files, plus untracked files (treated as fully-added). Read-only; * polls on the standard interval. */ -export function createUnstagedDiffSource(): DiffSource { - const output = vscode.window.createOutputChannel("Kilo Diff: Unstaged") - const log = (...args: unknown[]) => appendOutput(output, "UnstagedDiffSource", ...args) - const git = new GitOps({ log }) +export function createUnstagedDiffSource(opts: UnstagedDiffSourceOptions = {}): DiffSource { + const output = opts.git ? undefined : vscode.window.createOutputChannel("Kilo Diff: Unstaged") + const log = opts.log ?? ((...args: unknown[]) => appendOutput(output!, "UnstagedDiffSource", ...args)) + const git = opts.git ?? new GitOps({ log }) - const root = (): string | undefined => getWorkspaceRoot() + const root = (): string | undefined => { + const dir = opts.dir?.() + if (dir) return dir + if (opts.strictDir) return undefined + return getWorkspaceRoot() + } const listTracked = async (dir: string): Promise => { const [nameStatus, numstat, raw] = await Promise.all([ @@ -192,8 +214,10 @@ export function createUnstagedDiffSource(): DiffSource { }, dispose(): void { - git.dispose() - output.dispose() + // Only dispose resources we own (created here). Injected git/log are + // owned by the caller. + if (!opts.git) git.dispose() + output?.dispose() }, } } diff --git a/packages/kilo-vscode/src/diff/sources/worktree.ts b/packages/kilo-vscode/src/diff/sources/worktree.ts index 1f3d8ee25a4..89e750a1c87 100644 --- a/packages/kilo-vscode/src/diff/sources/worktree.ts +++ b/packages/kilo-vscode/src/diff/sources/worktree.ts @@ -23,6 +23,28 @@ export interface WorktreeDiffSourceOptions { * the current branch — only the comparison target changes. Reset on dispose. */ baseBranchOverride?: string + /** + * Resolve the directory to diff. Defaults to the VS Code workspace root. + * Agent Manager passes a worktree path so the source diffs inside the + * worktree rather than the main checkout. + */ + dir?: () => string | undefined + /** + * When true, a `dir` that resolves to undefined yields an empty diff rather + * than falling back to the workspace root. Prevents an unresolvable + * worktree context from silently diffing the main checkout. + */ + strictDir?: boolean + /** + * Explicit base branch to diff against. When set, the source skips + * auto-resolution (tracking → default) and diffs against this ref directly. + * Agent Manager passes the worktree's recorded parent so a worktree always + * compares against its own base even when the workspace default differs. + */ + baseBranch?: string + /** Shared GitOps / log so sources don't each spawn their own channel. */ + git?: GitOps + log?: (...args: unknown[]) => void } /** @@ -32,9 +54,16 @@ export interface WorktreeDiffSourceOptions { * extension host — no `kilo serve` round-trip. */ export function createWorktreeDiffSource(opts: WorktreeDiffSourceOptions = {}): DiffSource { - const output = vscode.window.createOutputChannel("Kilo Diff: Workspace") - const log = (...args: unknown[]) => appendOutput(output, "WorktreeDiffSource", ...args) - const git = new GitOps({ log }) + const output = opts.git ? undefined : vscode.window.createOutputChannel("Kilo Diff: Workspace") + const log = opts.log ?? ((...args: unknown[]) => appendOutput(output!, "WorktreeDiffSource", ...args)) + const git = opts.git ?? new GitOps({ log }) + + const root = (): string | undefined => { + const dir = opts.dir?.() + if (dir) return dir + if (opts.strictDir) return undefined + return getWorkspaceRoot() + } // Cached between fetches so repeated polling doesn't re-resolve the base // branch every tick. Reset only on dispose (when the source is swapped out). @@ -42,22 +71,32 @@ export function createWorktreeDiffSource(opts: WorktreeDiffSourceOptions = {}): const resolveTarget = async (): Promise => { if (target) return target + if (opts.baseBranch) { + const dir = root() + if (!dir) { + log("Local diff: no directory (explicit base mode)") + return + } + target = { directory: dir, baseBranch: opts.baseBranch } + log(`Local diff: using explicit base=${opts.baseBranch} dir=${dir}`) + return target + } if (opts.baseBranchOverride) { - const root = getWorkspaceRoot() - if (!root) { + const dir = root() + if (!dir) { log("Local diff: no workspace root (override mode)") return } - const resolved = await resolveOverrideRef(git, root, opts.baseBranchOverride, log) + const resolved = await resolveOverrideRef(git, dir, opts.baseBranchOverride, log) if (!resolved) { log(`Local diff: override base="${opts.baseBranchOverride}" could not be resolved, falling back to auto`) } else { - target = { directory: root, baseBranch: resolved } + target = { directory: dir, baseBranch: resolved } log(`Local diff: using override base=${resolved}`) return target } } - target = await resolveLocalDiffTarget(git, log, getWorkspaceRoot()) + target = await resolveLocalDiffTarget(git, log, root()) return target } @@ -109,8 +148,10 @@ export function createWorktreeDiffSource(opts: WorktreeDiffSourceOptions = {}): }, dispose(): void { - git.dispose() - output.dispose() + // Only dispose resources we own (created here). Injected git/log are + // owned by the caller. + if (!opts.git) git.dispose() + output?.dispose() target = undefined }, } diff --git a/packages/kilo-vscode/src/diff/types.ts b/packages/kilo-vscode/src/diff/types.ts index 913d4560a31..3ff72904e13 100644 --- a/packages/kilo-vscode/src/diff/types.ts +++ b/packages/kilo-vscode/src/diff/types.ts @@ -10,6 +10,27 @@ export interface PanelContext { hidePicker?: boolean /** User-picked base branch for the workspace source. Undefined = auto. */ baseBranchOverride?: string + /** + * Explicit directory to diff inside, overriding the workspace root lookup. + * Agent Manager passes a worktree path so its sources operate in the + * worktree rather than the main checkout. + */ + dir?: string + /** + * When true, a source whose `dir` resolves to undefined returns an empty + * diff instead of falling back to the workspace root. Agent Manager sets + * this so an unresolvable worktree context never silently diffs the main + * checkout. + */ + strictDir?: boolean + /** + * Explicit base ref for the workspace source, skipping auto-resolution. + * Agent Manager passes the worktree's recorded parent ref. + */ + baseBranch?: string + /** Shared GitOps / log injected by Agent Manager to avoid per-source channels. */ + git?: import("../agent-manager/GitOps").GitOps + log?: (...args: unknown[]) => void } export type DiffImageError = "too-large" | "unreadable" 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 804471f9bb0..10b88670865 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -570,7 +570,9 @@ describe("Agent Manager Provider — onMessage routing", () => { expect(text).toContain("class WorktreeDiffController") expect(text).toContain("buildWorktreePatch") expect(text).toContain("revertFile") - expect(text).toContain("diffSummary") + // Summary/detail diff data comes from the shared DiffSourceCatalog sources + // (workspace/staged/unstaged/session), not a bespoke in-controller pipeline. + expect(text).toContain("catalog.build") expect(text).toContain("shouldStopDiffPolling") expect(providerText).toContain("this.diffs") }) diff --git a/packages/kilo-vscode/tests/unit/diff-scope.test.ts b/packages/kilo-vscode/tests/unit/diff-scope.test.ts new file mode 100644 index 00000000000..7714ae4fa24 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/diff-scope.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from "bun:test" +import { + composeDiffId, + parseDiffId, + isDiffScope, + normalizeScope, + scopeToSourceId, + DEFAULT_DIFF_SCOPE, +} from "../../src/agent-manager/diff-scope" + +describe("diff-scope composite ids", () => { + it("round-trips context and scope", () => { + expect(parseDiffId(composeDiffId("local", "branch"))).toEqual({ ctx: "local", scope: "branch" }) + expect(parseDiffId(composeDiffId("ses_abc", "staged"))).toEqual({ ctx: "ses_abc", scope: "staged" }) + expect(parseDiffId(composeDiffId("ses_abc", "unstaged"))).toEqual({ ctx: "ses_abc", scope: "unstaged" }) + expect(parseDiffId(composeDiffId("ses_abc", "session"))).toEqual({ ctx: "ses_abc", scope: "session" }) + }) + + it("parses session ids containing no separator as default branch scope", () => { + expect(parseDiffId("ses_abc")).toEqual({ ctx: "ses_abc", scope: DEFAULT_DIFF_SCOPE }) + }) + + it("treats an unknown trailing segment as part of the context, not a scope", () => { + // A session id that happens to contain '#' but not a valid scope keeps the + // full id as context and falls back to branch. + expect(parseDiffId("ses_a#bogus")).toEqual({ ctx: "ses_a#bogus", scope: DEFAULT_DIFF_SCOPE }) + }) + + it("isDiffScope guards the closed enum", () => { + expect(isDiffScope("branch")).toBe(true) + expect(isDiffScope("staged")).toBe(true) + expect(isDiffScope("unstaged")).toBe(true) + expect(isDiffScope("session")).toBe(true) + expect(isDiffScope("turn")).toBe(false) + expect(isDiffScope("")).toBe(false) + }) + + it("normalizeScope falls back to branch for unknown input", () => { + expect(normalizeScope("staged")).toBe("staged") + expect(normalizeScope("nope")).toBe("branch") + expect(normalizeScope(undefined)).toBe("branch") + expect(normalizeScope(42)).toBe("branch") + }) + + it("maps scopes to catalog source ids", () => { + expect(scopeToSourceId("branch", "ses_abc")).toBe("workspace") + expect(scopeToSourceId("staged", "ses_abc")).toBe("staged") + expect(scopeToSourceId("unstaged", "ses_abc")).toBe("unstaged") + expect(scopeToSourceId("session", "ses_abc")).toBe("session:ses_abc") + expect(scopeToSourceId("branch", "local")).toBe("workspace") + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index ce6f795941b..ac86e26d1a1 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -20,9 +20,6 @@ import type { AgentManagerMultiVersionProgressMessage, AgentManagerSendInitialMessage, AgentManagerBranchesMessage, - AgentManagerWorktreeDiffMessage, - AgentManagerWorktreeDiffFileMessage, - AgentManagerWorktreeDiffLoadingMessage, AgentManagerApplyWorktreeDiffResultMessage, AgentManagerApplyWorktreeDiffStatus, AgentManagerApplyWorktreeDiffConflict, @@ -156,7 +153,10 @@ import { } from "./section-helpers" import { sectionAwareDetector } from "./section-dnd" import { ConstrainDragXAxis } from "./constrain-drag-x" -import { mergeWorktreeDiffs } from "../diff-viewer/diff-state" +import { DiffScopeControls } from "../diff-viewer/DiffScopeControls" +import { scopeCapabilities } from "./diff-scope-state" +import { createDiffReviewScope } from "./diff-review-scope" +import { handleDiffMessage } from "./diff-messages" import { initialMessage, seedInitialVariant } from "./initial-message" import { createMarkdownRender } from "./review-preferences" import { createSidebarCollapse } from "./sidebar-collapse" @@ -1491,40 +1491,13 @@ const AgentManagerContent: Component = () => { } } - if (msg.type === "agentManager.worktreeDiff") { - const ev = msg as AgentManagerWorktreeDiffMessage - let staleFiles: Set | undefined - setDiffDatas((prev) => { - const existing = prev[ev.sessionId] - const merged = existing - ? mergeWorktreeDiffs(existing, ev.diffs) - : { diffs: ev.diffs, stale: new Set() } - staleFiles = merged.stale - const next = merged.diffs - if (existing && existing.length === next.length && existing.every((old, i) => old === next[i])) return prev - return { ...prev, [ev.sessionId]: next } - }) - if (staleFiles) refreshStaleDiffs(ev.sessionId, staleFiles) - } - - if (msg.type === "agentManager.worktreeDiffFile") { - const ev = msg as AgentManagerWorktreeDiffFileMessage - if (ev.diff) { - setDiffDatas((prev) => { - const existing = prev[ev.sessionId] ?? [] - const next = existing.map((item) => (item.file === ev.diff!.file ? ev.diff! : item)) - return { ...prev, [ev.sessionId]: next } - }) - setDiffFilePending(ev.sessionId, ev.diff.file, false) - return - } - setDiffFilePending(ev.sessionId, ev.file, false) - } - - if (msg.type === "agentManager.worktreeDiffLoading") { - const ev = msg as AgentManagerWorktreeDiffLoadingMessage - setDiffLoading(ev.loading) - } + handleDiffMessage(msg, { + setDiffDatas, + setDiffFilePending, + setDiffLoading, + refreshStaleDiffs, + review, + }) if (msg.type === "agentManager.applyWorktreeDiffResult") { const ev = msg as AgentManagerApplyWorktreeDiffResultMessage @@ -1617,15 +1590,47 @@ const AgentManagerContent: Component = () => { const currentDiffSessionId = createMemo(selectedDiffSessionId) - // Start/stop diff watch when panel opens/closes, review tab opens, or session changes + // Diff scope + base branch state, shared by the side panel and review tab. + const review = createDiffReviewScope({ + ctx: currentDiffSessionId, + panelOpen: diffOpen, + reviewActive, + local: LOCAL, + vscode, + }) + // The composite id (ctx#scope) the extension keys diff data by. + const diffScopeId = review.id + + // Shared scope + base-picker controls for the side panel and review tab. + const diffScopeControls = (compact: boolean) => ( + + ) + + // Start/stop diff watch when panel opens/closes, review tab opens, scope + // changes, or session changes. createEffect(() => { const panel = diffOpen() - const review = reviewActive() + const active = reviewActive() + const scope = review.scope() - if (panel || review) { + if (panel || active) { const id = currentDiffSessionId() if (id) { - vscode.postMessage({ type: "agentManager.startDiffWatch", sessionId: id }) + vscode.postMessage({ type: "agentManager.startDiffWatch", sessionId: id, scope }) return } vscode.postMessage({ type: "agentManager.stopDiffWatch" }) @@ -1670,33 +1675,17 @@ const AgentManagerContent: Component = () => { tabFocus.restore() } - // Data for the review tab: use local diff data for local context, - // current session for selected worktree context, or first available in that worktree. + // Data for the review tab / side panel: keyed by the composite diff id + // (ctx#scope) the extension pushes, so each scope keeps its own file set and + // switching back to a fetched scope is instant. const reviewDiffs = createMemo(() => { const data = diffDatas() - const sel = selection() - const id = session.currentSessionID() - if (sel === LOCAL) return data[LOCAL] ?? [] - if (id && data[id]) { - const current = managedSessions().find((s) => s.id === id) - if (sel && current?.worktreeId === sel) return data[id]! - } - if (!sel) return [] - const ids = managedSessions() - .filter((s) => s.worktreeId === sel) - .map((s) => s.id) - for (const sid of ids) { - if (data[sid]) return data[sid]! - } - return [] + const key = diffScopeId() + if (!key) return [] + return data[key] ?? [] }) - const diffSessionKey = createMemo(() => { - const sel = selection() - if (sel === LOCAL) return `local:${LOCAL}` - if (sel === null) return `session:${session.currentSessionID() ?? ""}` - return `worktree:${sel}` - }) + const diffSessionKey = createMemo(() => diffScopeId() ?? "") const setSharedDiffStyle = (style: "unified" | "split") => { if (reviewDiffStyle() === style) return @@ -1731,29 +1720,39 @@ const AgentManagerContent: Component = () => { } const requestDiffFile = (file: string) => { - const sessionId = currentDiffSessionId() - if (!sessionId) return - if (diffFileLoading()[sessionId]?.[file]) return - setDiffFilePending(sessionId, file, true) - vscode.postMessage({ type: "agentManager.requestWorktreeDiffFile", sessionId, file }) + const id = diffScopeId() + if (!id) return + if (diffFileLoading()[id]?.[file]) return + setDiffFilePending(id, file, true) + vscode.postMessage({ + type: "agentManager.requestWorktreeDiffFile", + sessionId: currentDiffSessionId()!, + file, + scope: review.scope(), + }) } - const refreshStaleDiffs = (sessionId: string, files: Set) => { - const loading = diffFileLoading()[sessionId] ?? {} + const refreshStaleDiffs = (id: string, files: Set) => { + const loading = diffFileLoading()[id] ?? {} for (const file of files) { if (loading[file]) continue - setDiffFilePending(sessionId, file, true) - vscode.postMessage({ type: "agentManager.requestWorktreeDiffFile", sessionId, file }) + setDiffFilePending(id, file, true) + vscode.postMessage({ + type: "agentManager.requestWorktreeDiffFile", + sessionId: currentDiffSessionId()!, + file, + scope: review.scope(), + }) } } const diffFileLoadingForCurrent = createMemo(() => { - const sessionId = currentDiffSessionId() - if (!sessionId) return new Set() - return new Set(Object.keys(diffFileLoading()[sessionId] ?? {})) + const id = diffScopeId() + if (!id) return new Set() + return new Set(Object.keys(diffFileLoading()[id] ?? {})) }) - const revertCtl = createRevertFile(currentDiffSessionId, vscode, showToast, t) + const revertCtl = createRevertFile(diffScopeId, currentDiffSessionId, () => review.scope(), vscode, showToast, t) const handleConfigureSetupScript = () => { vscode.postMessage({ type: "agentManager.configureSetupScript" }) @@ -2755,12 +2754,19 @@ const AgentManagerContent: Component = () => { {t("agentManager.open.button")} - +
+ +
{props.lead}
+
@@ -636,7 +643,7 @@ export const DiffPanel: Component = (props) => { /> - + > + setDiffFilePending: (id: string, file: string, value: boolean) => void + setDiffLoading: Setter + refreshStaleDiffs: (id: string, files: Set) => void + review: DiffReviewScope +} + +/** Handle one inbound message; returns true when it was a diff message. */ +export function handleDiffMessage(msg: { type: string }, h: DiffMessageHandlers): boolean { + if (msg.type === "agentManager.worktreeDiff") { + const ev = msg as AgentManagerWorktreeDiffMessage + let staleFiles: Set | undefined + h.setDiffDatas((prev) => { + const existing = prev[ev.sessionId] + const merged = existing + ? mergeWorktreeDiffs(existing, ev.diffs) + : { diffs: ev.diffs, stale: new Set() } + staleFiles = merged.stale + const next = merged.diffs + if (existing && existing.length === next.length && existing.every((old, i) => old === next[i])) return prev + return { ...prev, [ev.sessionId]: next } + }) + if (staleFiles) h.refreshStaleDiffs(ev.sessionId, staleFiles) + return true + } + + if (msg.type === "agentManager.worktreeDiffFile") { + const ev = msg as AgentManagerWorktreeDiffFileMessage + if (ev.diff) { + h.setDiffDatas((prev) => { + const existing = prev[ev.sessionId] ?? [] + const next = existing.map((item) => (item.file === ev.diff!.file ? ev.diff! : item)) + return { ...prev, [ev.sessionId]: next } + }) + h.setDiffFilePending(ev.sessionId, ev.diff.file, false) + return true + } + h.setDiffFilePending(ev.sessionId, ev.file, false) + return true + } + + if (msg.type === "agentManager.worktreeDiffLoading") { + h.setDiffLoading((msg as AgentManagerWorktreeDiffLoadingMessage).loading) + return true + } + + if (msg.type === "agentManager.diffBranches") { + h.review.onBranches(msg as AgentManagerDiffBranchesMessage) + return true + } + + return false +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/diff-review-scope.ts b/packages/kilo-vscode/webview-ui/agent-manager/diff-review-scope.ts new file mode 100644 index 00000000000..249641837e5 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/diff-review-scope.ts @@ -0,0 +1,120 @@ +/** + * Diff scope + base branch state for the Agent Manager review surfaces. + * + * Owns the per-context scope selection, the branch picker data for the active + * context, and the message senders that drive both. Extracted from + * AgentManagerApp to keep that file under its line cap; both the side panel + * and the full-screen review tab consume the single instance returned here. + */ + +import { createEffect, createMemo, createSignal, type Accessor } from "solid-js" +import type { BranchInfo } from "../src/types/messages" +import { createDiffScope, isDiffScope, scopeDescriptors, type DiffScope } from "./diff-scope-state" + +interface VsCode { + postMessage(msg: unknown): void +} + +export interface DiffReviewScopeOptions { + /** Current diff context (worktree session id or the LOCAL pseudo-id). */ + ctx: Accessor + /** Whether the diff side panel is open. */ + panelOpen: Accessor + /** Whether the full-screen review tab is active. */ + reviewActive: Accessor + /** The id that marks the local pseudo-context (omits the Session scope). */ + local: string + vscode: VsCode +} + +export function createDiffReviewScope(opts: DiffReviewScopeOptions) { + const scope = createDiffScope(opts.ctx) + // The composite id (ctx#scope) the extension keys diff data by. + const id = createMemo(() => scope.id()) + + // Branch picker state for the active context (Branch scope only). + const [branches, setBranches] = createSignal([]) + const [loading, setLoading] = createSignal(false) + const [defaultBranch, setDefaultBranch] = createSignal("") + const [autoBase, setAutoBase] = createSignal(undefined) + const [currentBase, setCurrentBase] = createSignal(undefined) + const [isAuto, setIsAuto] = createSignal(true) + const [currentBranch, setCurrentBranch] = createSignal(undefined) + + // Scope descriptors for the current context. The `local` pseudo-context and + // contexts without a real session omit the Session scope. + const descriptors = createMemo(() => { + const ctx = opts.ctx() + if (!ctx) return [] + return scopeDescriptors(ctx, ctx !== opts.local) + }) + + const isBranch = () => scope.scope() === "branch" + + const select = (next: string) => { + const ctx = opts.ctx() + if (!ctx) return + const value = next.slice(ctx.length + 1) + scope.setScope(isDiffScope(value) ? value : "branch") + } + + const selectBase = (branch: string | undefined) => { + const ctx = opts.ctx() + if (!ctx) return + // Optimistic update; the extension echoes authoritative state back. + setCurrentBase(branch ?? autoBase()) + setIsAuto(branch === undefined) + opts.vscode.postMessage({ type: "agentManager.setDiffBaseBranch", sessionId: ctx, scope: scope.scope(), branch }) + } + + // Fetch branch picker data whenever the Branch scope becomes active for the + // current context. The extension owns override state, so ask each time. + createEffect(() => { + if (scope.scope() !== "branch") return + const ctx = opts.ctx() + if (!ctx) return + if (!opts.panelOpen() && !opts.reviewActive()) return + setLoading(true) + opts.vscode.postMessage({ type: "agentManager.requestDiffBranches", sessionId: ctx, scope: scope.scope() }) + }) + + /** Handle the extension's diffBranches push, ignoring stale contexts. */ + const onBranches = (ev: { + sessionId: string + branches: BranchInfo[] + defaultBranch: string + autoBase?: string + currentBase?: string + isAuto: boolean + currentBranch?: string + }) => { + if (ev.sessionId === id()) { + setBranches(ev.branches) + setDefaultBranch(ev.defaultBranch) + setAutoBase(ev.autoBase) + setCurrentBase(ev.currentBase) + setIsAuto(ev.isAuto) + setCurrentBranch(ev.currentBranch) + } + setLoading(false) + } + + return { + scope: scope.scope, + id, + descriptors, + isBranch, + select, + selectBase, + onBranches, + branches, + loading, + defaultBranch, + autoBase, + currentBase, + isAuto, + currentBranch, + } +} + +export type DiffReviewScope = ReturnType diff --git a/packages/kilo-vscode/webview-ui/agent-manager/diff-scope-state.ts b/packages/kilo-vscode/webview-ui/agent-manager/diff-scope-state.ts new file mode 100644 index 00000000000..a61e6c3a357 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/diff-scope-state.ts @@ -0,0 +1,91 @@ +/** + * Webview-side diff scope state for Agent Manager. + * + * Mirrors the extension's composite diff id (`ctx#scope`, see + * `src/agent-manager/diff-scope.ts`) and builds the fixed scope descriptor + * list shown in the scope selector. Agent Manager always offers the same four + * scopes per context, so the descriptors are computed client-side rather than + * pushed from the extension. + */ + +import { createMemo, createSignal, type Accessor } from "solid-js" +import type { DiffSourceDescriptor } from "../../src/diff/sources/types" + +export type DiffScope = "branch" | "staged" | "unstaged" | "session" + +export const DEFAULT_DIFF_SCOPE: DiffScope = "branch" + +const SEP = "#" + +export function composeDiffId(ctx: string, scope: DiffScope): string { + return `${ctx}${SEP}${scope}` +} + +export function isDiffScope(value: string): value is DiffScope { + return value === "branch" || value === "staged" || value === "unstaged" || value === "session" +} + +/** + * The fixed scope descriptors for a context. `workspace` maps to the Branch + * scope to reuse the existing i18n keys (`diffViewer.source.workspace.*`). + * Session scope is only meaningful for a real session context, so it is + * omitted for the `local` pseudo-context and for contexts without a session. + */ +export function scopeDescriptors(ctx: string, hasSession: boolean): DiffSourceDescriptor[] { + const out: DiffSourceDescriptor[] = [ + { id: composeDiffId(ctx, "branch"), type: "workspace", group: "Git", capabilities: { revert: true, comments: true } }, + { id: composeDiffId(ctx, "staged"), type: "staged", group: "Git", capabilities: { revert: false, comments: true } }, + { + id: composeDiffId(ctx, "unstaged"), + type: "unstaged", + group: "Git", + capabilities: { revert: false, comments: true }, + }, + ] + if (hasSession) { + out.push({ + id: composeDiffId(ctx, "session"), + type: "session", + group: "Session", + capabilities: { revert: false, comments: true }, + }) + } + return out +} + +/** + * Whether the Branch scope supports revert. Staged/unstaged/session are + * read-only; only the Branch scope can revert files back to the merge base. + */ +export function scopeCapabilities(scope: DiffScope): { revert: boolean; comments: boolean } { + return { revert: scope === "branch", comments: true } +} + +/** + * Per-context scope selection. Keeps the last-picked scope per context id so + * switching between worktrees restores each worktree's scope, while a brand + * new context defaults to Branch. + */ +export function createDiffScope(currentCtx: Accessor) { + const [scopes, setScopes] = createSignal>({}) + + const scope = createMemo((): DiffScope => { + const ctx = currentCtx() + if (!ctx) return DEFAULT_DIFF_SCOPE + return scopes()[ctx] ?? DEFAULT_DIFF_SCOPE + }) + + const id = createMemo(() => { + const ctx = currentCtx() + if (!ctx) return undefined + return composeDiffId(ctx, scope()) + }) + + const setScope = (next: DiffScope) => { + const ctx = currentCtx() + if (!ctx) return + setScopes((prev) => ({ ...prev, [ctx]: next })) + } + + return { scope, id, setScope } +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts index 79d4b74c51e..37b03ad9147 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -135,6 +135,7 @@ export const dict = { "agentManager.diff.revertFile": "استعادة الملف", "agentManager.diff.revertSuccess": "تم استعادة الملف", "agentManager.diff.revertError": "فشل الاستعادة", + "agentManager.diff.applyBranchOnly": "لا يعمل تطبيق التغييرات إلا على فرق الفرع الكامل. انتقل إلى نطاق Branch لتطبيقها.", "agentManager.open.button": "فتح", "agentManager.open.tooltip": "فتح Worktree هذا في VS Code", "agentManager.apply.globalButton": "تطبيق", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts index 67cdb9bcf34..91769ed735f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -138,6 +138,7 @@ export const dict = { "agentManager.diff.revertFile": "Reverter arquivo", "agentManager.diff.revertSuccess": "Arquivo revertido", "agentManager.diff.revertError": "Falha ao reverter", + "agentManager.diff.applyBranchOnly": "Aplicar funciona apenas no diff completo da branch. Mude para o escopo Branch para aplicar.", "agentManager.open.button": "Abrir", "agentManager.open.tooltip": "Abrir este Worktree no VS Code", "agentManager.apply.globalButton": "Aplicar", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts index f6fddfd6127..0c2f1456b73 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -138,6 +138,7 @@ export const dict = { "agentManager.diff.revertFile": "Vrati datoteku", "agentManager.diff.revertSuccess": "Datoteka vraćena", "agentManager.diff.revertError": "Vraćanje neuspješno", + "agentManager.diff.applyBranchOnly": "Primijeni radi samo s kompletnim diffom grane. Prebacite se na opseg Branch da biste primijenili.", "agentManager.open.button": "Otvori", "agentManager.open.tooltip": "Otvori ovaj worktree u VS Code-u", "agentManager.apply.globalButton": "Primijeni", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts index beb827b1c9d..e1a466c894c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -139,6 +139,7 @@ export const dict = { "agentManager.diff.revertFile": "Gendan fil", "agentManager.diff.revertSuccess": "Fil gendannet", "agentManager.diff.revertError": "Gendannelse fejlede", + "agentManager.diff.applyBranchOnly": "Anvend virker kun på hele Branch-diffen. Skift til Branch-området for at anvende.", "agentManager.open.button": "Åbn", "agentManager.open.tooltip": "Åbn dette Worktree i VS Code", "agentManager.apply.globalButton": "Anvend", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts index b9691ecbafd..2de026594cf 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -139,6 +139,7 @@ export const dict = { "agentManager.diff.revertFile": "Datei zurücksetzen", "agentManager.diff.revertSuccess": "Datei zurückgesetzt", "agentManager.diff.revertError": "Zurücksetzen fehlgeschlagen", + "agentManager.diff.applyBranchOnly": "Anwenden funktioniert nur für den vollständigen Branch-Diff. Wechsle zum Bereich Branch, um anzuwenden.", "agentManager.open.button": "Öffnen", "agentManager.open.tooltip": "Dieses Worktree in VS Code öffnen", "agentManager.apply.globalButton": "Anwenden", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts index 236f18423a4..16e45d06dad 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -142,6 +142,7 @@ export const dict = { "agentManager.diff.revertFile": "Revert file", "agentManager.diff.revertSuccess": "File reverted", "agentManager.diff.revertError": "Revert failed", + "agentManager.diff.applyBranchOnly": "Apply works on the full branch diff. Switch to the Branch scope to apply.", "agentManager.open.button": "Open", "agentManager.open.tooltip": "Open this worktree in VS Code", "agentManager.apply.globalButton": "Apply", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts index a75de2ce0c2..7f55de6a91c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -138,6 +138,7 @@ export const dict = { "agentManager.diff.revertFile": "Revertir archivo", "agentManager.diff.revertSuccess": "Archivo revertido", "agentManager.diff.revertError": "Error al revertir", + "agentManager.diff.applyBranchOnly": "Aplicar solo funciona con el diff completo de la rama. Cambia al ámbito Branch para aplicar.", "agentManager.open.button": "Abrir", "agentManager.open.tooltip": "Abrir este Worktree en VS Code", "agentManager.apply.globalButton": "Aplicar", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts index d8579c9cfd0..a279ac1f2e8 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -138,6 +138,7 @@ export const dict = { "agentManager.diff.revertFile": "Rétablir le fichier", "agentManager.diff.revertSuccess": "Fichier rétabli", "agentManager.diff.revertError": "Échec du rétablissement", + "agentManager.diff.applyBranchOnly": "Appliquer ne fonctionne que sur le diff complet de la branche. Passez à la portée Branch pour appliquer.", "agentManager.open.button": "Ouvrir", "agentManager.open.tooltip": "Ouvrir ce worktree dans VS Code", "agentManager.apply.globalButton": "Appliquer", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts index 11928074249..5a85e0df9ba 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts @@ -144,6 +144,7 @@ export const dict = { "agentManager.diff.revertFile": "Ripristina file", "agentManager.diff.revertSuccess": "File ripristinato", "agentManager.diff.revertError": "Ripristino non riuscito", + "agentManager.diff.applyBranchOnly": "Applica funziona solo sul diff completo del branch. Passa all'ambito Branch per applicare.", "agentManager.open.button": "Apri", "agentManager.open.tooltip": "Apri questo worktree in VS Code", "agentManager.apply.globalButton": "Applica", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts index d855af8e15d..0b32167f653 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -137,6 +137,7 @@ export const dict = { "agentManager.diff.revertFile": "ファイルを元に戻す", "agentManager.diff.revertSuccess": "ファイルを元に戻しました", "agentManager.diff.revertError": "元に戻せませんでした", + "agentManager.diff.applyBranchOnly": "適用はブランチ全体の差分に対してのみ利用できます。適用するにはスコープを Branch に切り替えてください。", "agentManager.open.button": "開く", "agentManager.open.tooltip": "このWorktreeをVS Codeで開く", "agentManager.apply.globalButton": "適用", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts index ae87aaf9933..43437bc683a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -136,6 +136,7 @@ export const dict = { "agentManager.diff.revertFile": "파일 되돌리기", "agentManager.diff.revertSuccess": "파일이 되돌려졌습니다", "agentManager.diff.revertError": "되돌리기 실패", + "agentManager.diff.applyBranchOnly": "적용은 전체 브랜치 diff에서만 작동합니다. 적용하려면 범위를 Branch로 전환하세요.", "agentManager.open.button": "열기", "agentManager.open.tooltip": "이 Worktree를 VS Code에서 열기", "agentManager.apply.globalButton": "적용", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts index 1965204a471..c3b40ceb784 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts @@ -143,6 +143,7 @@ export const dict = { "agentManager.diff.revertFile": "Bestand terugzetten", "agentManager.diff.revertSuccess": "Bestand teruggezet", "agentManager.diff.revertError": "Terugzetten mislukt", + "agentManager.diff.applyBranchOnly": "Toepassen werkt alleen op de volledige branch-diff. Schakel naar het bereik Branch om toe te passen.", "agentManager.open.button": "Openen", "agentManager.open.tooltip": "Open deze worktree in VS Code", "agentManager.apply.globalButton": "Toepassen", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts index 44efed03a51..37ac94071e8 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -137,6 +137,7 @@ export const dict = { "agentManager.diff.revertFile": "Tilbakestill fil", "agentManager.diff.revertSuccess": "Fil tilbakestilt", "agentManager.diff.revertError": "Tilbakestilling feilet", + "agentManager.diff.applyBranchOnly": "Bruk fungerer kun på hele Branch-diffen. Bytt til Branch-omfanget for å bruke.", "agentManager.open.button": "Åpne", "agentManager.open.tooltip": "Åpne dette Worktree-et i VS Code", "agentManager.apply.globalButton": "Bruk", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts index 137bf34132f..7b34943e968 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -137,6 +137,7 @@ export const dict = { "agentManager.diff.revertFile": "Cofnij plik", "agentManager.diff.revertSuccess": "Plik cofnięty", "agentManager.diff.revertError": "Cofanie nie powiodło się", + "agentManager.diff.applyBranchOnly": "Funkcja Zastosuj działa tylko z pełnym diffem brancha. Przełącz się na zakres Branch, aby zastosować.", "agentManager.open.button": "Otwórz", "agentManager.open.tooltip": "Otwórz ten Worktree w VS Code", "agentManager.apply.globalButton": "Zastosuj", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts index c0df2ab3c94..0fd387a78cf 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -138,6 +138,7 @@ export const dict = { "agentManager.diff.revertFile": "Откатить файл", "agentManager.diff.revertSuccess": "Файл откатан", "agentManager.diff.revertError": "Ошибка отката", + "agentManager.diff.applyBranchOnly": "Применение работает только с полным diff ветки. Чтобы применить изменения, переключитесь на область Branch.", "agentManager.open.button": "Открыть", "agentManager.open.tooltip": "Открыть этот Worktree в VS Code", "agentManager.apply.globalButton": "Применить", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts index 012b3776ba3..4608684420b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -133,6 +133,7 @@ export const dict = { "agentManager.diff.revertFile": "ย้อนกลับไฟล์", "agentManager.diff.revertSuccess": "ย้อนกลับไฟล์แล้ว", "agentManager.diff.revertError": "ย้อนกลับล้มเหลว", + "agentManager.diff.applyBranchOnly": "นำไปใช้ได้เฉพาะกับ diff ของ Branch ทั้งหมดเท่านั้น สลับไปที่ขอบเขต Branch เพื่อใช้งาน", "agentManager.open.button": "เปิด", "agentManager.open.tooltip": "เปิด Worktree นี้ใน VS Code", "agentManager.apply.globalButton": "นำไปใช้", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts index e18f88694b3..10d9ddff95c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts @@ -144,6 +144,7 @@ export const dict = { "agentManager.diff.revertFile": "Dosyayı geri al", "agentManager.diff.revertSuccess": "Dosya geri alındı", "agentManager.diff.revertError": "Geri alma başarısız", + "agentManager.diff.applyBranchOnly": "Uygula yalnızca tam Branch diff'inde çalışır. Uygulamak için Branch kapsamına geçin.", "agentManager.open.button": "Aç", "agentManager.open.tooltip": "Bu worktree'yi VS Code'da aç", "agentManager.apply.globalButton": "Uygula", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts index 1c1c1cbf22e..ed70a05f515 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts @@ -145,6 +145,7 @@ export const dict = { "agentManager.diff.revertFile": "Скасувати зміни файлу", "agentManager.diff.revertSuccess": "Файл відновлено", "agentManager.diff.revertError": "Не вдалося відновити", + "agentManager.diff.applyBranchOnly": "Застосування працює лише з повним diff гілки. Щоб застосувати зміни, перемкніться на область Branch.", "agentManager.open.button": "Відкрити", "agentManager.open.tooltip": "Відкрити це робоче дерево у VS Code", "agentManager.apply.globalButton": "Застосувати", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts index 2e0c7f5568c..acf24797b89 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -132,6 +132,7 @@ export const dict = { "agentManager.diff.revertFile": "还原文件", "agentManager.diff.revertSuccess": "文件已还原", "agentManager.diff.revertError": "还原失败", + "agentManager.diff.applyBranchOnly": "应用仅适用于完整的分支差异。请切换到 Branch 范围后再应用。", "agentManager.open.button": "打开", "agentManager.open.tooltip": "在 VS Code 中打开此 Worktree", "agentManager.apply.globalButton": "应用", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts index e4403c5aaff..7eaa9240c97 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -132,6 +132,7 @@ export const dict = { "agentManager.diff.revertFile": "還原檔案", "agentManager.diff.revertSuccess": "檔案已還原", "agentManager.diff.revertError": "還原失敗", + "agentManager.diff.applyBranchOnly": "套用僅適用於完整的分支差異。請切換至 Branch 範圍後再套用。", "agentManager.open.button": "開啟", "agentManager.open.tooltip": "在 VS Code 中開啟此 Worktree", "agentManager.apply.globalButton": "套用", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/revert-file.ts b/packages/kilo-vscode/webview-ui/agent-manager/revert-file.ts index 199a9b96f2d..03a3c96e44d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/revert-file.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/revert-file.ts @@ -12,7 +12,9 @@ interface Toast { } export function createRevertFile( + diffScopeId: Accessor, currentDiffSessionId: Accessor, + scope: Accessor, vscode: VsCode, showToast: (t: Toast) => void, t: (key: string) => string, @@ -20,20 +22,21 @@ export function createRevertFile( const [files, setFiles] = createSignal>>({}) const reverting = createMemo(() => { - const sessionId = currentDiffSessionId() - if (!sessionId) return new Set() - return files()[sessionId] ?? new Set() + const id = diffScopeId() + if (!id) return new Set() + return files()[id] ?? new Set() }) function revert(file: string) { + const id = diffScopeId() const sessionId = currentDiffSessionId() - if (!sessionId) return + if (!id || !sessionId) return setFiles((prev) => { - const set = new Set(prev[sessionId] ?? []) + const set = new Set(prev[id] ?? []) set.add(file) - return { ...prev, [sessionId]: set } + return { ...prev, [id]: set } }) - vscode.postMessage({ type: "agentManager.revertWorktreeFile", sessionId, file }) + vscode.postMessage({ type: "agentManager.revertWorktreeFile", sessionId, file, scope: scope() }) } function onResult(ev: AgentManagerRevertWorktreeFileResultMessage) { diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/DiffScopeControls.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/DiffScopeControls.tsx new file mode 100644 index 00000000000..ba9cc3dad8d --- /dev/null +++ b/packages/kilo-vscode/webview-ui/diff-viewer/DiffScopeControls.tsx @@ -0,0 +1,55 @@ +import { Show, type Component } from "solid-js" +import type { DiffSourceDescriptor } from "../../src/diff/sources/types" +import type { BranchInfo } from "../src/types/messages" +import { DiffPickerHeader } from "./DiffPickerHeader" +import { BaseBranchPicker } from "./BaseBranchPicker" + +interface DiffScopeControlsProps { + descriptors: DiffSourceDescriptor[] + currentId: string | undefined + onSelectScope: (id: string) => void + /** Show the base branch picker (only when the Branch scope is active). */ + showBase: boolean + branches: BranchInfo[] + branchesLoading: boolean + defaultBranch: string + autoBase: string | undefined + currentBase: string | undefined + isAuto: boolean + currentBranch: string | undefined + onSelectBase: (branch: string | undefined) => void + /** + * Compact mode for the narrow Agent Manager side panel: hides the + * `current → base` prefix so only the picker trigger remains. + */ + compact?: boolean +} + +/** + * Composes the scope selector and base branch picker into one control row. + * Shared by the standalone Changes header and the two Agent Manager diff + * surfaces so all three render the identical controls. + */ +export const DiffScopeControls: Component = (props) => { + return ( + + + + } + /> + ) +} diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx index 4ff64605f6e..2d63a6ebd1c 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx @@ -1,4 +1,4 @@ -import { type Component, createSignal, createMemo, createEffect, on, onCleanup, Show } from "solid-js" +import { type Component, createSignal, createMemo, createEffect, on, onCleanup, Show, type JSXElement } from "solid-js" import type { VirtualizerHandle } from "virtua/solid" // Styles are imported by the component so every consumer (sidebar diff viewer, // agent manager, storybook) picks them up automatically. Keep these imports here — @@ -89,6 +89,8 @@ interface FullScreenDiffViewProps { canRevert?: boolean /** Defaults to true. Disables comment creation and "Send all" when false. */ canComment?: boolean + /** Optional leading content rendered first in the toolbar's left group. */ + lead?: JSXElement onClose: () => void } @@ -541,6 +543,7 @@ export const FullScreenDiffView: Component = (props) => {/* Toolbar */}
+ {props.lead} Date: Wed, 29 Jul 2026 14:46:44 +0200 Subject: [PATCH 25/71] fix(cli): display verbatim skill commands in the permission prompt --- packages/opencode/src/acp/permission.ts | 2 +- packages/opencode/src/kilocode/skills/inject.ts | 14 +++++++++----- packages/opencode/test/acp/permission.test.ts | 5 +++-- .../opencode/test/kilocode/skills/inject.test.ts | 7 +++++-- packages/tui/src/routes/session/permission.tsx | 8 +++++--- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/opencode/src/acp/permission.ts b/packages/opencode/src/acp/permission.ts index 8bf102f2175..68555d7bff7 100644 --- a/packages/opencode/src/acp/permission.ts +++ b/packages/opencode/src/acp/permission.ts @@ -70,7 +70,7 @@ export class Handler { toolCallId: permission.tool?.callID ?? permission.id, status: "pending", title: skillShell ? "Run skill shell commands" : permission.permission, // kilocode_change - rawInput: skillShell ? { ...permission.metadata, commands: permission.patterns } : permission.metadata, // kilocode_change + rawInput: permission.metadata, // kilocode_change - metadata.commands carries the verbatim command list kind: toToolKind(permission.permission), locations: toLocations(permission.permission, permission.metadata), }, diff --git a/packages/opencode/src/kilocode/skills/inject.ts b/packages/opencode/src/kilocode/skills/inject.ts index 3c738ad3d63..d577ade7e21 100644 --- a/packages/opencode/src/kilocode/skills/inject.ts +++ b/packages/opencode/src/kilocode/skills/inject.ts @@ -75,22 +75,26 @@ export namespace SkillInject { for (const dir of scan.dirs) dirs.add(dir) } - // Single up-front approval. Out-of-project directories are asked first, then - // the decomposed sub-commands. `skillShell` forces the prompt over allow/YOLO - // rules; a deny/veto on any sub-command propagates as a defect and aborts. + // Single up-front approval. `patterns` are the decomposed sub-commands used for + // rule matching; `metadata.commands` is the verbatim per-placeholder list the + // prompt displays, so what is shown is exactly what runs (decomposition drops + // cd/set-location segments and splits pipelines, which must not hide from the + // user). `skillShell` forces the prompt over allow/YOLO rules; a deny/veto on + // any sub-command propagates as a defect and aborts. + const metadata = { skillShell: true, skill: opts.skill, commands } if (dirs.size > 0) { yield* opts.ctx.ask({ permission: "external_directory", patterns: Array.from(dirs), always: [], - metadata: { skillShell: true, skill: opts.skill }, + metadata, }) } yield* opts.ctx.ask({ permission: "bash", patterns: Array.from(patterns), always: [], - metadata: { skillShell: true, skill: opts.skill }, + metadata, }) // Run each command in the instance directory, bounded by ctx.abort (ESC) and a diff --git a/packages/opencode/test/acp/permission.test.ts b/packages/opencode/test/acp/permission.test.ts index 76ce5584c28..f789086d374 100644 --- a/packages/opencode/test/acp/permission.test.ts +++ b/packages/opencode/test/acp/permission.test.ts @@ -213,7 +213,8 @@ describe("acp permissions", () => { harness.subscription.handle( permissionAsked("ses_a", "perm_skill", { permission: "bash", - metadata: { skillShell: true }, + // metadata.commands carries the verbatim command list the injector sends + metadata: { skillShell: true, commands: ["git status", "printf hi"] }, tool: { messageID: "msg_1", callID: "call_1" }, }), ) @@ -223,7 +224,7 @@ describe("acp permissions", () => { expect(harness.requests[0]).toMatchObject({ toolCall: { title: "Run skill shell commands", - rawInput: { skillShell: true, commands: ["*"] }, + rawInput: { skillShell: true, commands: ["git status", "printf hi"] }, }, // no allow_always: skill shell is never persisted options: [ diff --git a/packages/opencode/test/kilocode/skills/inject.test.ts b/packages/opencode/test/kilocode/skills/inject.test.ts index f0e4f7f82e5..50e6c616412 100644 --- a/packages/opencode/test/kilocode/skills/inject.test.ts +++ b/packages/opencode/test/kilocode/skills/inject.test.ts @@ -97,8 +97,9 @@ describe("skill shell injection", () => { const bash = requests.filter((r) => r.permission === "bash") expect(bash.length).toBe(1) expect(bash[0].metadata?.["skillShell"]).toBe(true) - // patterns carry the command list the prompt renders + // patterns drive rule matching; metadata.commands is the verbatim list the prompt renders expect(bash[0].patterns).toEqual(["printf one", "printf two"]) + expect(bash[0].metadata?.["commands"]).toEqual(["printf one", "printf two"]) }), ) @@ -130,10 +131,12 @@ describe("skill shell injection", () => { const bash = requests.filter((r) => r.permission === "bash") expect(bash.length).toBe(1) - // both sub-commands are present as distinct patterns, not the raw string + // patterns are decomposed per sub-command so deny/veto rules apply to each expect(bash[0].patterns).toContain("cat README.md") expect(bash[0].patterns).toContain("printf hi") expect(bash[0].patterns).not.toContain("cat README.md; printf hi") + // but the prompt displays the verbatim placeholder, so nothing is hidden from the user + expect(bash[0].metadata?.["commands"]).toEqual(["cat README.md; printf hi"]) }), ) diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx index 6b1b391fcad..819ff4b71ba 100644 --- a/packages/tui/src/routes/session/permission.tsx +++ b/packages/tui/src/routes/session/permission.tsx @@ -293,10 +293,12 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory? } if (permission === "bash") { - // kilocode_change start - skill shell batches list every command, control-char-escaped so the - // displayed command cannot repaint the line to differ from what executes + // kilocode_change start - skill shell batches display the verbatim commands that will execute (from + // metadata.commands, never the decomposed patterns, which drop cd segments and split pipelines), + // control-char-escaped so the displayed command cannot repaint the line to differ from what runs if (props.request.metadata?.["skillShell"] === true) { - const commands = (props.request.patterns ?? []).filter((p): p is string => typeof p === "string") + const verbatim = props.request.metadata?.["commands"] + const commands = (Array.isArray(verbatim) ? verbatim : []).filter((p): p is string => typeof p === "string") const skill = typeof props.request.metadata?.["skill"] === "string" ? props.request.metadata["skill"] : undefined return { icon: "#", From 3655be492219e10420b6328681537ae80e68200d Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 14:58:27 +0200 Subject: [PATCH 26/71] fix(cli): authorize verbatim skill commands to block cd-chained escapes --- .../opencode/src/kilocode/skills/inject.ts | 7 +++++- .../kilocode/permission/skill-shell.test.ts | 25 +++++++++++++++++++ .../test/kilocode/skills/inject.test.ts | 14 +++++------ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/kilocode/skills/inject.ts b/packages/opencode/src/kilocode/skills/inject.ts index d577ade7e21..05beb703e69 100644 --- a/packages/opencode/src/kilocode/skills/inject.ts +++ b/packages/opencode/src/kilocode/skills/inject.ts @@ -66,10 +66,15 @@ export namespace SkillInject { // Decompose each command into sub-command patterns + out-of-project dir globs // via the shared bash scan, so plan-mode denies and external_directory checks - // apply per sub-command instead of matching the raw string as one glob. + // apply per sub-command instead of matching the raw string as one glob. Also + // authorize the verbatim command: decomposition drops cd/set-location segments + // and strips chaining metacharacters, so a payload like `cd $HOME; cat secret` + // would otherwise slip past the metachar deny rules (`*;*`, `*|*`, `*\n*`) and + // hide the escape. Keeping the raw string as a pattern makes those rules fire. const patterns = new Set() const dirs = new Set() for (const command of commands) { + patterns.add(command) const scan = yield* opts.decompose({ command, cwd: opts.cwd, shell }) for (const pattern of scan.patterns) patterns.add(pattern) for (const dir of scan.dirs) dirs.add(dir) diff --git a/packages/opencode/test/kilocode/permission/skill-shell.test.ts b/packages/opencode/test/kilocode/permission/skill-shell.test.ts index 06bf04df7c0..9f10a53349a 100644 --- a/packages/opencode/test/kilocode/permission/skill-shell.test.ts +++ b/packages/opencode/test/kilocode/permission/skill-shell.test.ts @@ -108,6 +108,31 @@ it.instance( { git: true }, ) +it.instance( + "skillShell - a cd-chained escape is vetoed via the verbatim command pattern", + () => + Effect.gen(function* () { + // The injector asks with the decomposed sub-command (`cat .ssh/id_rsa`, which + // readOnlyBash would allow) AND the verbatim command. In plan mode the metachar + // hard-veto (`*\n*` deny) must fire on the verbatim string, blocking the escape. + const err = yield* fail( + ask({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ['cd "$HOME"\ncat .ssh/id_rsa', "cat .ssh/id_rsa"], + metadata: { skillShell: true }, + always: [], + ruleset: [{ permission: "bash", pattern: "cat *", action: "allow" }], + hardRuleset: [{ permission: "bash", pattern: "*\n*", action: "deny" }], + }), + ) + + expect(err).toBeInstanceOf(PermissionV1.DeniedError) + expect(yield* list()).toHaveLength(0) + }), + { git: true }, +) + it.instance( "skillShell - is denied by a hard-ruleset veto instead of prompting", () => diff --git a/packages/opencode/test/kilocode/skills/inject.test.ts b/packages/opencode/test/kilocode/skills/inject.test.ts index 50e6c616412..d96a970f671 100644 --- a/packages/opencode/test/kilocode/skills/inject.test.ts +++ b/packages/opencode/test/kilocode/skills/inject.test.ts @@ -115,11 +115,12 @@ describe("skill shell injection", () => { }), ) - unix("decomposes a compound command into per-sub-command patterns", () => + unix("authorizes both the decomposed sub-commands and the verbatim command", () => Effect.gen(function* () { - // A single placeholder with a chained command must not be asked as one glob - // pattern (which would let e.g. `cat *` match the whole string). Each - // sub-command must appear separately so deny/veto rules apply per command. + // A chained placeholder is asked with per-sub-command patterns (so deny/veto + // rules apply to each) AND the verbatim string (so the metachar deny rules + // `*;*`/`*|*`/`*\n*` fire and cd/set-location escapes can't hide). The prompt + // still displays the verbatim placeholder. yield* writeGlobalSkill("compound-shell", "Out: !`cat README.md; printf hi`") const requests: Array> = [] @@ -131,11 +132,10 @@ describe("skill shell injection", () => { const bash = requests.filter((r) => r.permission === "bash") expect(bash.length).toBe(1) - // patterns are decomposed per sub-command so deny/veto rules apply to each expect(bash[0].patterns).toContain("cat README.md") expect(bash[0].patterns).toContain("printf hi") - expect(bash[0].patterns).not.toContain("cat README.md; printf hi") - // but the prompt displays the verbatim placeholder, so nothing is hidden from the user + // the raw chained string is authorized too, so metachar deny rules can match it + expect(bash[0].patterns).toContain("cat README.md; printf hi") expect(bash[0].metadata?.["commands"]).toEqual(["cat README.md; printf hi"]) }), ) From 6497a8dcb524f5481ecea1a1e62a97fec7b49d56 Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 15:07:13 +0200 Subject: [PATCH 27/71] fix(cli): fail closed when a skill batch has no authorizable commands --- .../opencode/src/kilocode/skills/inject.ts | 6 ++++++ .../test/kilocode/skills/inject.test.ts | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/packages/opencode/src/kilocode/skills/inject.ts b/packages/opencode/src/kilocode/skills/inject.ts index 05beb703e69..902647cac3a 100644 --- a/packages/opencode/src/kilocode/skills/inject.ts +++ b/packages/opencode/src/kilocode/skills/inject.ts @@ -80,6 +80,12 @@ export namespace SkillInject { for (const dir of scan.dirs) dirs.add(dir) } + // Fail closed: an empty pattern set would make the bash ask below auto-approve + // (Permission.ask iterates patterns, so forceAsk/veto never run for an empty + // list). Each command contributes its verbatim string above, so this is + // unreachable — but abort rather than risk a silent, unprompted execution. + if (patterns.size === 0) return yield* Effect.die(new Error("skill shell produced no authorizable commands")) + // Single up-front approval. `patterns` are the decomposed sub-commands used for // rule matching; `metadata.commands` is the verbatim per-placeholder list the // prompt displays, so what is shown is exactly what runs (decomposition drops diff --git a/packages/opencode/test/kilocode/skills/inject.test.ts b/packages/opencode/test/kilocode/skills/inject.test.ts index d96a970f671..0b213c68bf2 100644 --- a/packages/opencode/test/kilocode/skills/inject.test.ts +++ b/packages/opencode/test/kilocode/skills/inject.test.ts @@ -140,6 +140,27 @@ describe("skill shell injection", () => { }), ) + unix("still prompts for a cd-only command (no empty-pattern auto-approve)", () => + Effect.gen(function* () { + // `cd` decomposes to no sub-command patterns; without the verbatim command the + // bash ask would carry an empty pattern list and Permission.ask would silently + // auto-approve (forceAsk never runs on an empty list). The raw command keeps + // the prompt firing. + yield* writeGlobalSkill("cd-only", "Out: !`cd sub`") + + const requests: Array> = [] + yield* loadSkill("cd-only", (req) => + Effect.sync(() => { + requests.push(req) + }), + ) + + const bash = requests.filter((r) => r.permission === "bash") + expect(bash.length).toBe(1) + expect(bash[0].patterns).toContain("cd sub") + }), + ) + unix("aborts the entire skill load when the batch is rejected", () => Effect.gen(function* () { yield* writeGlobalSkill("denied-shell", "Secret: !`printf leaked`") From 6b1b017e5a6965fa68296f78911c4c63b6dada4b Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 15:21:30 +0200 Subject: [PATCH 28/71] fix(cli): resolve a parseable shell for skill command injection --- packages/opencode/src/kilocode/skills/inject.ts | 7 +++++-- packages/opencode/src/tool/skill.ts | 5 +++++ packages/opencode/test/kilocode/skills/inject.test.ts | 3 +++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/kilocode/skills/inject.ts b/packages/opencode/src/kilocode/skills/inject.ts index 902647cac3a..b13f5442dba 100644 --- a/packages/opencode/src/kilocode/skills/inject.ts +++ b/packages/opencode/src/kilocode/skills/inject.ts @@ -1,7 +1,6 @@ import { Effect } from "effect" import { ConfigMarkdown } from "@/config/markdown" import { Process } from "@/util/process" -import { Shell } from "@opencode-ai/core/shell" import type * as Tool from "@/tool/tool" // Shell injection for skill bodies mirrors Claude's "dynamic context injection": @@ -47,6 +46,7 @@ export namespace SkillInject { disabled: boolean cwd: string skill: string + shell: string ctx: Tool.Context decompose: Decompose } @@ -59,7 +59,10 @@ export namespace SkillInject { if (opts.disabled) return replace(opts.content, () => DISABLED_NOTE) if (!opts.trusted) return replace(opts.content, () => UNTRUSTED_NOTE) - const shell = Shell.preferred() + // `shell` is resolved by the caller via Shell.acceptable(cfg.shell), which + // rejects shells the tree-sitter bash scanner can't parse (fish/nu), keeping + // the parse used for the permission decision aligned with execution. + const shell = opts.shell // Deduplicate identical commands, then cap the batch so a skill can't queue // an unbounded number of processes. const commands = Array.from(new Set(matches.map(([, cmd]) => cmd))).slice(0, MAX_COMMANDS) diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index 758696b02a0..faa949c0c9e 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -6,6 +6,8 @@ import { Skill } from "../skill" import * as Tool from "./tool" import DESCRIPTION from "./skill.txt" // kilocode_change start - gate + run shell injection in skill bodies +import { Config } from "@/config/config" +import { Shell } from "@opencode-ai/core/shell" import { InstanceState } from "@/effect/instance-state" import { RuntimeFlags } from "@/effect/runtime-flags" import { ShellPermission } from "./shell" @@ -23,6 +25,7 @@ export const SkillTool = Tool.define( const ripgrep = yield* Ripgrep.Service const flags = yield* RuntimeFlags.Service // kilocode_change const permission = yield* ShellPermission // kilocode_change - decompose skill commands like the bash tool + const config = yield* Config.Service // kilocode_change - resolve a parseable shell for injection return { description: DESCRIPTION, @@ -41,12 +44,14 @@ export const SkillTool = Tool.define( }) // kilocode_change start - render `!`cmd`` shell injection, gated by trust + kill-switch + batch approval + const cfg = yield* config.get() const content = yield* SkillInject.render({ content: info.content, trusted: info.trusted === true, disabled: flags.disableSkillShell, cwd: yield* InstanceState.directory, skill: info.name, + shell: Shell.acceptable(cfg.shell), ctx, decompose: permission.decompose, }) diff --git a/packages/opencode/test/kilocode/skills/inject.test.ts b/packages/opencode/test/kilocode/skills/inject.test.ts index 0b213c68bf2..32f3811d5c5 100644 --- a/packages/opencode/test/kilocode/skills/inject.test.ts +++ b/packages/opencode/test/kilocode/skills/inject.test.ts @@ -1,6 +1,7 @@ import { PermissionV1 } from "@opencode-ai/core/v1/permission" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Ripgrep } from "@opencode-ai/core/ripgrep" +import { Shell } from "@opencode-ai/core/shell" import { Effect, Exit, Layer } from "effect" import { afterEach, describe, expect } from "bun:test" import fs from "fs" @@ -221,6 +222,7 @@ describe("skill shell injection", () => { disabled: false, cwd: dir, skill: "big-shell", + shell: Shell.acceptable(), ctx: { ...baseCtx, ask: () => Effect.void } as Tool.Context, decompose: ({ command }) => Effect.succeed({ patterns: [command], dirs: [] }), }) @@ -249,6 +251,7 @@ describe("SkillInject.render gating", () => { disabled: opts.disabled, cwd: "/tmp", skill: "test", + shell: Shell.acceptable(), ctx, decompose, }), From 572d8fdfd982e1e4bbe08c5dc6d8822ff29583f6 Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 15:48:22 +0200 Subject: [PATCH 29/71] fix(cli): let human surfaces approve skill shell batches --- .../session/controller/SessionController.kt | 39 +++++++++++++---- .../session/controller/PromptLifecycleTest.kt | 42 +++++++++++++++++++ .../src/cli/cmd/run/footer.permission.tsx | 5 ++- .../src/cli/cmd/run/permission.shared.ts | 11 +++-- .../src/kilo-sessions/remote-sender.ts | 3 ++ packages/opencode/src/permission/index.ts | 5 +++ .../test/cli/run/permission.shared.test.ts | 11 +++++ 7 files changed, 103 insertions(+), 13 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index 9b1e517ac6d..d032d281cc6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -722,7 +722,10 @@ class SessionController( LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission-auto rid=$id" } cs.launch { try { - if (!autoApprove) { + // Skill-shell batches must be answered by a human: the server refuses + // non-interactive approvals, so auto-approve must show the card (whose + // manual reply sets interactive=true) rather than send a machine reply. + if (!autoApprove || restore().meta.raw["skillShell"] == "true") { edt { if (disposed) return@edt model.setState(SessionState.AwaitingPermission(restore())) @@ -759,9 +762,16 @@ class SessionController( try { val permissions = sessions.pendingPermissions(directory).filter { it.sessionID in ids && it.id !in skip } val count = replyAll(permissions) - if (count == 0) return@launch + // Skill-shell requests are skipped by replyAll; surface one as a card so it + // isn't stranded (never machine-approved, never shown). + val card = skillShellCard(permissions)?.let { toPermission(it) } + if (count == 0 && card == null) return@launch runEdt { if (disposed) return@runEdt + if (card != null) { + updateModel { model.setState(SessionState.AwaitingPermission(card)) } + return@runEdt + } val current = model.state if (current is SessionState.AwaitingPermission && current.permission.sessionId in ids) { model.setState(SessionState.Busy(KiloBundle.message("session.status.considering"))) @@ -777,6 +787,8 @@ class SessionController( var count = 0 for (request in permissions) { if (!autoApprove) return count + // Skill-shell batches need a human; skip them here (callers surface the card). + if (request.metadata["skillShell"] == "true") continue sessions.replyPermission(request.id, directory, PermissionReplyDto("once")) capture("Permission Auto Approved", sessionProps(request.sessionID) + mapOf("tool" to request.permission, "source" to "drain")) count++ @@ -784,6 +796,11 @@ class SessionController( return count } + // A skill-shell request is never machine-approved (the server refuses non-interactive + // approvals); after draining, callers must surface one as a card so a human can answer. + private fun skillShellCard(permissions: List): PermissionRequestDto? = + permissions.lastOrNull { it.metadata["skillShell"] == "true" } + private fun updatePermission(id: String, state: PermissionRequestState, message: String? = null) { assertEdt() val current = model.state @@ -1156,11 +1173,15 @@ class SessionController( val permissions = sessions.pendingPermissions(directory).filter { it.sessionID == child } if (permissions.isEmpty()) return LOG.debug { "${ChatLogSummary.sid(sid ?: "pending")} kind=child-recovery child=$child permissions=${permissions.size}" } - if (autoApprove) { + // A skill-shell request must surface as a card even under auto-approve (replyAll + // skips it); prefer it over the last pending so a human can answer. + val show = if (autoApprove) { replyAll(permissions) - return + skillShellCard(permissions) ?: return + } else { + skillShellCard(permissions) ?: permissions.last() } - val last = toPermission(permissions.last()) + val last = toPermission(show) runEdt { if (disposed) return@runEdt if (child !in childIds) return@runEdt @@ -1201,9 +1222,12 @@ class SessionController( val permissions = sessions.pendingPermissions(directory).filter { it.sessionID == id } val questions = sessions.pendingQuestions(directory).filter { it.sessionID == id } val status = sessions.statuses.value[id] + // replyAll auto-approves the ordinary permissions and skips skill-shell ones. A + // skill-shell request must then fall through to a human card rather than go Busy. + val skillCard = skillShellCard(permissions) if (permissions.isNotEmpty() && autoApprove) { val count = replyAll(permissions) - if (count > 0) { + if (count > 0 && skillCard == null) { runEdt { if (disposed) return@runEdt if (sid != id) return@runEdt @@ -1226,7 +1250,8 @@ class SessionController( if (sid != id) return@runEdt updateModel { if (permissions.isNotEmpty()) { - model.setState(SessionState.AwaitingPermission(toPermission(permissions.last()))) + // Prefer a skill-shell request (needs a human) over the last pending. + model.setState(SessionState.AwaitingPermission(toPermission(skillCard ?: permissions.last()))) } else if (questions.isNotEmpty()) { model.setState(SessionState.AwaitingQuestion(toQuestion(questions.last()))) } else if (status != null) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt index 9d5e4225e15..768c9e36b39 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt @@ -254,6 +254,23 @@ class PromptLifecycleTest : SessionControllerTestBase() { ) } + fun `test auto approve does not machine-reply a skill shell batch`() { + val (m, _, _) = prompted() + + edt { m.setAutoApprove(true) } + // skill-shell batches must be answered by a human; auto-approve must show the card + // instead of sending a non-interactive reply the server would refuse. + emit( + ChatEventDto.PermissionAsked( + "ses_test", + permission("perm1").copy(metadata = mapOf("skillShell" to "true")), + ), + ) + + assertTrue(rpc.permissionReplies.isEmpty()) + assertTrue(m.model.state is SessionState.AwaitingPermission) + } + fun `test disabling auto approve before reply restores awaiting permission`() { val (m, _, _) = prompted() @@ -310,6 +327,31 @@ class PromptLifecycleTest : SessionControllerTestBase() { assertEquals("once", rpc.permissionReplies[0].third.reply) } + fun `test enabling auto approve surfaces a pending skill shell as a card`() { + val (m, _, _) = prompted() + rpc.pendingPermissionList.add(permission("perm_skill").copy(metadata = mapOf("skillShell" to "true"))) + + edt { m.setAutoApprove(true) } + flush() + + // skill-shell must not be machine-approved; it surfaces as a human card instead + assertTrue(rpc.permissionReplies.isEmpty()) + assertTrue(m.model.state is SessionState.AwaitingPermission) + } + + fun `test recovery surfaces a pending skill shell as a card under auto approve`() { + appRpc.state.value = ai.kilocode.rpc.dto.KiloAppStateDto(ai.kilocode.rpc.dto.KiloAppStatusDto.READY, config = ai.kilocode.rpc.dto.ConfigDto(model = "kilo/gpt-5")) + projectRpc.state.value = workspaceReady() + rpc.pendingPermissionList.add(permission("perm_skill").copy(metadata = mapOf("skillShell" to "true"))) + edt { KiloPluginSettings.setAutoApprove(true) } + + val m = controller("ses_test") + flush() + + assertTrue(rpc.permissionReplies.isEmpty()) + assertTrue(m.model.state is SessionState.AwaitingPermission) + } + fun `test auto approve drains pending permissions during recovery`() { appRpc.state.value = ai.kilocode.rpc.dto.KiloAppStateDto(ai.kilocode.rpc.dto.KiloAppStatusDto.READY, config = ai.kilocode.rpc.dto.ConfigDto(model = "kilo/gpt-5")) projectRpc.state.value = workspaceReady() diff --git a/packages/opencode/src/cli/cmd/run/footer.permission.tsx b/packages/opencode/src/cli/cmd/run/footer.permission.tsx index 0eb412a03bd..ab3ea3cc4f9 100644 --- a/packages/opencode/src/cli/cmd/run/footer.permission.tsx +++ b/packages/opencode/src/cli/cmd/run/footer.permission.tsx @@ -141,7 +141,8 @@ export function RunPermissionBody(props: { const info = createMemo(() => permissionInfo(props.request)) const ft = createMemo(() => toolFiletype(info().file)) const narrow = createMemo(() => footerWidthPolicy(dims().width).dialog.narrow) - const opts = createMemo(() => permissionOptions(state().stage)) + const skillShell = createMemo(() => props.request.metadata?.["skillShell"] === true) // kilocode_change + const opts = createMemo(() => permissionOptions(state().stage, skillShell())) // kilocode_change - skillShell-aware options const busy = createMemo(() => state().submitting) const title = createMemo(() => { if (state().stage === "always") { @@ -165,7 +166,7 @@ export function RunPermissionBody(props: { }) const shift = (dir: -1 | 1) => { - setState((prev) => permissionShift(prev, dir)) + setState((prev) => permissionShift(prev, dir, skillShell())) // kilocode_change - skillShell-aware options } const submit = async (next: PermissionReply) => { diff --git a/packages/opencode/src/cli/cmd/run/permission.shared.ts b/packages/opencode/src/cli/cmd/run/permission.shared.ts index 2db41936963..8b0f33a2413 100644 --- a/packages/opencode/src/cli/cmd/run/permission.shared.ts +++ b/packages/opencode/src/cli/cmd/run/permission.shared.ts @@ -77,9 +77,11 @@ export function createPermissionBodyState(requestID: string): PermissionBodyStat } } -export function permissionOptions(stage: PermissionStage): PermissionOption[] { +export function permissionOptions(stage: PermissionStage, skillShell?: boolean): PermissionOption[] { // kilocode_change - skillShell param if (stage === "permission") { - return ["once", "always", "reject"] + // kilocode_change start - skill-shell batches are never persisted, so no "Allow always" + return skillShell ? ["once", "reject"] : ["once", "always", "reject"] + // kilocode_change end } if (stage === "always") { @@ -146,12 +148,13 @@ export function permissionReply(requestID: string, reply: PermissionReply["reply return { requestID, reply, + interactive: true, // kilocode_change - footer replies are human-driven; the server refuses non-interactive skill-shell approvals ...(message && message.trim() ? { message: message.trim() } : {}), } } -export function permissionShift(state: PermissionBodyState, dir: -1 | 1): PermissionBodyState { - const list = permissionOptions(state.stage) +export function permissionShift(state: PermissionBodyState, dir: -1 | 1, skillShell?: boolean): PermissionBodyState { // kilocode_change - skillShell param + const list = permissionOptions(state.stage, skillShell) // kilocode_change - skillShell-aware options if (list.length === 0) { return state } diff --git a/packages/opencode/src/kilo-sessions/remote-sender.ts b/packages/opencode/src/kilo-sessions/remote-sender.ts index 8af252e6dd0..42a575cd804 100644 --- a/packages/opencode/src/kilo-sessions/remote-sender.ts +++ b/packages/opencode/src/kilo-sessions/remote-sender.ts @@ -38,6 +38,9 @@ const PermissionData = z.object({ requestID: z.string(), reply: z.enum(["once", "always", "reject"]), message: z.string().optional(), + // Set by a remote human client; threads through to permission.reply so the server + // accepts a human approval of a skill-shell batch (non-interactive ones are refused). + interactive: z.boolean().optional(), }) const SuggestionData = z.object({ diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index ff6ad14fb07..9ce38c952fc 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -300,7 +300,12 @@ export const layer = Layer.effect( // kilocode_change start - skill-shell batches must be answered by a human; ignore machine approvals // (auto-approve/YOLO clients omit `interactive`) so the prompt stays pending for a real decision. + // Log rather than fail silently: a genuine human client sets `interactive`, so a refused reply here + // means an auto-approver tried to answer — the request intentionally stays pending for a human. if (existing.info.metadata?.["skillShell"] === true && input.reply !== "reject" && input.interactive !== true) { + yield* Effect.logWarning("skill shell approval refused: requires an interactive human reply", { + id: input.requestID, + }) return } // kilocode_change end diff --git a/packages/opencode/test/cli/run/permission.shared.test.ts b/packages/opencode/test/cli/run/permission.shared.test.ts index aa843ccbbcc..75eb76f2ba2 100644 --- a/packages/opencode/test/cli/run/permission.shared.test.ts +++ b/packages/opencode/test/cli/run/permission.shared.test.ts @@ -6,6 +6,7 @@ import { permissionCancel, permissionEscape, permissionInfo, + permissionOptions, // kilocode_change permissionReject, permissionRun, } from "@/cli/cmd/run/permission.shared" @@ -29,6 +30,7 @@ describe("run permission shared", () => { expect(out.reply).toEqual({ requestID: "perm-1", reply: "once", + interactive: true, // kilocode_change }) }) @@ -41,6 +43,7 @@ describe("run permission shared", () => { expect(permissionRun(next.state, "perm-1", "confirm").reply).toEqual({ requestID: "perm-1", reply: "always", + interactive: true, // kilocode_change }) expect(permissionRun(next.state, "perm-1", "cancel").state).toMatchObject({ @@ -57,6 +60,7 @@ describe("run permission shared", () => { expect(out).toEqual({ requestID: "perm-1", reply: "reject", + interactive: true, // kilocode_change message: "use rg", }) @@ -130,6 +134,13 @@ describe("run permission shared", () => { }) }) + // kilocode_change start - skill-shell options + test("skill shell offers only Allow / Reject (never Allow always)", () => { + expect(permissionOptions("permission", true)).toEqual(["once", "reject"]) + expect(permissionOptions("permission")).toEqual(["once", "always", "reject"]) + }) + // kilocode_change end + test("formats always-allow copy for wildcard and explicit patterns", () => { expect(permissionAlwaysLines(req({ permission: "bash", always: ["*"] }))).toEqual([ "This will allow bash until Kilo is restarted.", From 70f6271a23ed9462979c735270cb68047be8ff1d Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 16:18:31 +0200 Subject: [PATCH 30/71] fix(cli): drop trust for skills symlinked into the project --- packages/opencode/src/kilocode/skill/trust.ts | 28 +++++++++++++++++ packages/opencode/src/skill/index.ts | 22 +++++++++----- .../test/kilocode/skills/inject.test.ts | 30 +++++++++++++++++++ 3 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 packages/opencode/src/kilocode/skill/trust.ts diff --git a/packages/opencode/src/kilocode/skill/trust.ts b/packages/opencode/src/kilocode/skill/trust.ts new file mode 100644 index 00000000000..fbafa6e6a99 --- /dev/null +++ b/packages/opencode/src/kilocode/skill/trust.ts @@ -0,0 +1,28 @@ +import { realpathSync } from "fs" +import path from "path" + +// A skill discovered under a trusted directory (~/.agents, ~/.claude, config dirs, +// KILO_CONFIG_DIR) mints trust: shell execution after one approval, and unconfined +// {env:}/{file:} substitution. Symlinks are followed during the scan, so a link from a +// trusted dir into the current project (a commonly suggested convenience) would otherwise +// grant project-controlled markdown that trust. Resolve the real path and drop trust when +// it lands inside the project, so project content is never trusted regardless of symlinks. +export function trustedInProject(match: string, projectRoot: string | undefined): boolean { + if (!projectRoot) return false + const real = (() => { + try { + return realpathSync.native(match) + } catch { + return match + } + })() + const root = (() => { + try { + return realpathSync.native(projectRoot) + } catch { + return projectRoot + } + })() + const rel = path.relative(root, real) + return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel)) +} diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index 1b5f4241cc2..b74dfffbd97 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -20,6 +20,7 @@ import { primaryPaths } from "../kilocode/primary-worktree" // kilocode_change import { Git } from "@/git" // kilocode_change import { isRecord } from "@/util/record" import { Flag } from "@opencode-ai/core/flag/flag" // kilocode_change +import { trustedInProject } from "../kilocode/skill/trust" // kilocode_change const CLAUDE_EXTERNAL_DIR = ".claude" const AGENTS_EXTERNAL_DIR = ".agents" @@ -160,7 +161,7 @@ const scan = Effect.fnUntraced(function* ( state: ScanState, root: string, pattern: string, - opts?: { dot?: boolean; scope?: string; trusted?: boolean; root?: string; sourceRoot?: string }, // kilocode_change + opts?: { dot?: boolean; scope?: string; trusted?: boolean; root?: string; sourceRoot?: string; projectRoot?: string }, // kilocode_change ) { const matches = yield* Effect.tryPromise({ try: () => @@ -182,12 +183,14 @@ const scan = Effect.fnUntraced(function* ( ) for (const match of matches) { - // kilocode_change start + // kilocode_change start - a trusted match whose realpath resolves inside the project (e.g. a + // symlink from ~/.agents/skills into the repo) must not mint trust for project-controlled content + const trusted = (opts?.trusted ?? false) && !trustedInProject(match, opts?.projectRoot) state.matches.set(match, { path: match, - trusted: opts?.trusted ?? false, - root: opts?.root, - sourceRoot: opts?.sourceRoot, + trusted, + root: trusted ? opts?.root : (opts?.root ?? opts?.projectRoot), + sourceRoot: trusted ? opts?.sourceRoot : (opts?.sourceRoot ?? opts?.projectRoot), }) // kilocode_change end state.dirs.add(path.dirname(match)) @@ -215,7 +218,7 @@ const discoverSkills = Effect.fnUntraced(function* ( for (const dir of externalDirs) { const root = path.join(global.home, dir) if (!(yield* fsys.isDir(root))) continue - yield* scan(state, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "global", trusted: true }) // kilocode_change + yield* scan(state, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "global", trusted: true, projectRoot }) // kilocode_change } // kilocode_change start @@ -252,6 +255,7 @@ const discoverSkills = Effect.fnUntraced(function* ( trusted, root: trusted ? undefined : projectRoot, sourceRoot: trusted ? undefined : sourceRoot, + projectRoot, }) // kilocode_change end } @@ -268,7 +272,11 @@ const discoverSkills = Effect.fnUntraced(function* ( // kilocode_change start - trust follows the config source that declared the path, never the selected path. const origin = cfg.skill_path_origins?.[item] const trusted = origin?.trusted === true && path.isAbsolute(expanded) - yield* scan(state, dir, SKILL_PATTERN, { trusted, root: trusted ? undefined : (origin?.root ?? projectRoot) }) + yield* scan(state, dir, SKILL_PATTERN, { + trusted, + root: trusted ? undefined : (origin?.root ?? projectRoot), + projectRoot, + }) // kilocode_change end } diff --git a/packages/opencode/test/kilocode/skills/inject.test.ts b/packages/opencode/test/kilocode/skills/inject.test.ts index 32f3811d5c5..309b84b4a61 100644 --- a/packages/opencode/test/kilocode/skills/inject.test.ts +++ b/packages/opencode/test/kilocode/skills/inject.test.ts @@ -197,6 +197,36 @@ describe("skill shell injection", () => { }), ) + unix("does not trust a global skill symlinked into the project", () => + Effect.gen(function* () { + const dir = (yield* TestInstance).directory + // A SKILL.md that lives in the project, symlinked into the trusted ~/.agents/skills dir + // (a suggested convenience), must not mint trust for project-controlled markdown. + const projectSkillDir = path.join(dir, "skills", "linked") + yield* Effect.promise(async () => { + await Bun.write( + path.join(projectSkillDir, "SKILL.md"), + "---\nname: linked\ndescription: linked test skill.\n---\n\nValue: !`printf shouldnotrun`\n", + ) + const linkDir = path.join(HOME, ".agents", "skills", "linked") + await fs.promises.mkdir(path.dirname(linkDir), { recursive: true }) + await fs.promises.symlink(projectSkillDir, linkDir, "dir") + }) + + const requests: Array> = [] + const result = yield* loadSkill("linked", (req) => + Effect.sync(() => { + requests.push(req) + }), + ) + + // realpath is inside the project → treated as untrusted, no execution, no bash ask + expect(result.output).toContain("[skill shell execution disabled for untrusted skill]") + expect(result.output).not.toContain("shouldnotrun") + expect(requests.some((r) => r.permission === "bash")).toBe(false) + }), + ) + unix("does not re-execute shell placeholders emitted by command output", () => Effect.gen(function* () { // The command emits a literal placeholder `!echo pwned` From da328dfd8e9221b432f67f0b54828279f6a0fd5b Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 16:26:45 +0200 Subject: [PATCH 31/71] fix(cli): validate and origin-pin remote skill downloads --- packages/opencode/src/skill/discovery.ts | 105 ++++++++++++------ .../opencode/test/skill/discovery.test.ts | 13 +++ 2 files changed, 87 insertions(+), 31 deletions(-) diff --git a/packages/opencode/src/skill/discovery.ts b/packages/opencode/src/skill/discovery.ts index 24e78c14f81..f4e0c50d226 100644 --- a/packages/opencode/src/skill/discovery.ts +++ b/packages/opencode/src/skill/discovery.ts @@ -1,3 +1,4 @@ +import { posix, win32 } from "node:path" // kilocode_change - pure segment/path validation helpers import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { httpClient, path } from "@opencode-ai/core/effect/layer-node-platform" import { NodePath } from "@effect/platform-node" @@ -10,6 +11,45 @@ import { Global } from "@opencode-ai/core/global" const skillConcurrency = 4 const fileConcurrency = 8 +// kilocode_change start - segment/relative-path validation mirrors core v2 SkillDiscovery so a remote +// index cannot smuggle traversal, absolute paths, URLs, or null bytes into a cache write target. +function isSafeSegment(value: string) { + return ( + value.length > 0 && value !== "." && value !== ".." && !value.includes("/") && !value.includes("\\") && !value.includes("\0") + ) +} + +function isSafeRelativePath(value: string) { + const segments = value.split("/") + return ( + value.length > 0 && + !value.includes("\\") && + !value.includes("\0") && + !value.includes("?") && + !value.includes("#") && + !URL.canParse(value) && + !posix.isAbsolute(value) && + !win32.isAbsolute(value) && + segments.every((segment) => { + try { + const decoded = decodeURIComponent(segment) + return ( + decoded.length > 0 && + decoded !== "." && + decoded !== ".." && + !decoded.includes("/") && + !decoded.includes("\\") && + !decoded.includes("\0") + ) + } catch { + return false + } + }) + ) +} + +// kilocode_change end + class IndexSkill extends Schema.Class("IndexSkill")({ name: Schema.String, files: Schema.Array(Schema.String), @@ -47,8 +87,8 @@ export const layer: Layer.Layer !skill.files.includes("SKILL.md")) - yield* Effect.forEach( - missing, - (skill) => Effect.logWarning("skill entry missing SKILL.md", { url: index, skill: skill.name }), - { discard: true }, - ) - const list = data.skills.filter((skill) => skill.files.includes("SKILL.md")) - - // kilocode_change start - remote index.json controls skill.name/file, so a crafted `../` could escape the - // cache and plant a SKILL.md in a trusted dir (e.g. ~/.agents/skills). Drop any skill whose paths escape it. - const rooted = (target: string) => { - const rel = path.relative(cache, target) + // kilocode_change start - the remote index controls skill.name and file, so validate every segment, + // pin file downloads to the index origin, and confine writes to the cache (mirrors core v2 SkillDiscovery) + const contained = (parent: string, child: string) => { + const rel = path.relative(parent, child) return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel) } - const safe: typeof list = [] - for (const skill of list) { + const plan = (skill: IndexSkill) => { + if (!skill.files.includes("SKILL.md")) return "skill entry missing SKILL.md" + if (!isSafeSegment(skill.name)) return "skipping skill with unsafe name" const root = path.join(cache, skill.name) - if (rooted(root) && skill.files.every((file) => rooted(path.join(root, file)))) safe.push(skill) - else yield* Effect.logWarning("skipping skill with unsafe path", { url: index, skill: skill.name }) + if (!contained(cache, root)) return "skipping skill with unsafe name" + const skillUrl = new URL(`${encodeURIComponent(skill.name)}/`, source) + const files: { url: string; dest: string }[] = [] + for (const file of skill.files) { + if (!isSafeRelativePath(file)) return "skipping skill with unsafe file path" + const resource = URL.parse(file, skillUrl) ?? undefined + if (!resource || resource.origin !== source.origin) return "skipping skill with cross-origin file" + const dest = path.join(root, file) + if (!contained(root, dest)) return "skipping skill with unsafe file path" + files.push({ url: resource.href, dest }) + } + return { root, files } + } + + const planned: { root: string; files: { url: string; dest: string }[] }[] = [] + for (const skill of data.skills) { + const result = plan(skill) + if (typeof result === "string") yield* Effect.logWarning(result, { url: index, skill: skill.name }) + else planned.push(result) } // kilocode_change end const dirs = yield* Effect.forEach( - safe, // kilocode_change - was `list`; drop skills whose paths escape the cache + planned, // kilocode_change - validated, origin-pinned, cache-confined download plans (skill) => Effect.gen(function* () { - const root = path.join(cache, skill.name) - - yield* Effect.forEach( - skill.files, - (file) => download(new URL(file, `${host}/${skill.name}/`).href, path.join(root, file)), - { - concurrency: fileConcurrency, - }, - ) - - const md = path.join(root, "SKILL.md") - return (yield* fs.exists(md).pipe(Effect.orDie)) ? root : null + yield* Effect.forEach(skill.files, (file) => download(file.url, file.dest), { + concurrency: fileConcurrency, + }) + const md = path.join(skill.root, "SKILL.md") + return (yield* fs.exists(md).pipe(Effect.orDie)) ? skill.root : null }), { concurrency: skillConcurrency }, ) diff --git a/packages/opencode/test/skill/discovery.test.ts b/packages/opencode/test/skill/discovery.test.ts index ad1a53463f4..f454a6b3a7d 100644 --- a/packages/opencode/test/skill/discovery.test.ts +++ b/packages/opencode/test/skill/discovery.test.ts @@ -31,6 +31,10 @@ beforeAll(async () => { if (url.pathname.endsWith("/.agents/skills/evil/SKILL.md")) { return new Response("---\nname: evil\ndescription: evil.\n---\npwned") } + // A file entry pointing at another origin (exfil/arbitrary-host download) must be rejected. + if (url.pathname === "/cross-origin/index.json") { + return Response.json({ skills: [{ name: "x", files: ["SKILL.md", "https://evil.example/payload"] }] }) + } // kilocode_change end // route /.well-known/skills/* to the fixture directory @@ -135,6 +139,15 @@ describe("Discovery.pull", () => { expect(yield* fsys.existsSafe(escaped)).toBe(false) }), ) + + it.live("rejects a skill file that points at another origin", () => + Effect.gen(function* () { + const discovery = yield* Discovery.Service + // a file entry resolving to a different host must be dropped (no download, skill skipped) + const dirs = yield* discovery.pull(`http://localhost:${server.port}/cross-origin/`) + expect(dirs).toEqual([]) + }), + ) // kilocode_change end it.live("caches downloaded files on second pull", () => From 8205f353f372a0397e5027fcc98a9ce0eff805ee Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 16:44:37 +0200 Subject: [PATCH 32/71] fix(cli): tighten skill shell execution and prompt-display bounds --- .../opencode/src/kilocode/skills/display.ts | 14 ++- .../opencode/src/kilocode/skills/inject.ts | 103 +++++++++++++----- .../test/kilocode/skills/display.test.ts | 7 ++ .../test/kilocode/skills/inject.test.ts | 45 ++++++++ 4 files changed, 135 insertions(+), 34 deletions(-) diff --git a/packages/opencode/src/kilocode/skills/display.ts b/packages/opencode/src/kilocode/skills/display.ts index 93e8ac44f27..199315ff409 100644 --- a/packages/opencode/src/kilocode/skills/display.ts +++ b/packages/opencode/src/kilocode/skills/display.ts @@ -1,11 +1,15 @@ -// Render a skill command for a permission prompt as a single, tamper-evident -// line: escape control chars (CR/LF/ESC/etc.) so a command can't repaint the -// terminal to make the visible text differ from what will execute. +// Render a skill command for a permission prompt as a single, tamper-evident line. +// Escape control chars (CR/LF/ESC/etc.) so a command can't repaint the terminal, and +// bidi/format controls (U+202A-202E, U+2066-2069, U+200E/F, U+2028/9) so a Trojan-Source +// style reorder can't make the visible text differ from what will execute. +const CONTROL = /[\u0000-\u001f\u007f-\u009f\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g + export function displayCommand(command: string) { - return command.replace(/[\u0000-\u001f\u007f-\u009f]/g, (ch) => { + return command.replace(CONTROL, (ch) => { if (ch === "\n") return "\\n" if (ch === "\r") return "\\r" if (ch === "\t") return "\\t" - return "\\x" + ch.charCodeAt(0).toString(16).padStart(2, "0") + const code = ch.charCodeAt(0) + return code <= 0xff ? "\\x" + code.toString(16).padStart(2, "0") : "\\u" + code.toString(16).padStart(4, "0") }) } diff --git a/packages/opencode/src/kilocode/skills/inject.ts b/packages/opencode/src/kilocode/skills/inject.ts index b13f5442dba..b3f77d1532a 100644 --- a/packages/opencode/src/kilocode/skills/inject.ts +++ b/packages/opencode/src/kilocode/skills/inject.ts @@ -29,9 +29,11 @@ const UNTRUSTED_NOTE = "[skill shell execution disabled for untrusted skill]" // Execution bounds: model-initiated commands must not hang the load, blow up // context, or overrun the batch. -const TIMEOUT_MS = 2 * 60 * 1000 +const TIMEOUT_MS = 2 * 60 * 1000 // per-command +const BUDGET_MS = 5 * 60 * 1000 // aggregate across the batch const MAX_OUTPUT_BYTES = 32 * 1024 const MAX_COMMANDS = 32 +const LIMIT_NOTE = "[skill shell command limit reached]" export namespace SkillInject { export type Decompose = (input: { @@ -52,12 +54,16 @@ export namespace SkillInject { } export const render = Effect.fn("SkillInject.render")(function* (opts: Options) { - const matches = ConfigMarkdown.shell(opts.content) - if (matches.length === 0) return opts.content + // Placeholders inside fenced code blocks are documentation examples, not live commands. + const fenced = fences(opts.content) + const live = ConfigMarkdown.shell(opts.content).filter((m) => !fenced(m.index)) + if (live.length === 0) return opts.content - // Defense-in-depth ordering: policy checks first, approval gate last. - if (opts.disabled) return replace(opts.content, () => DISABLED_NOTE) - if (!opts.trusted) return replace(opts.content, () => UNTRUSTED_NOTE) + // Defense-in-depth ordering: policy checks first, approval gate last. `replace` only + // rewrites live (unfenced) placeholders; fenced ones stay as literal text. + const replace = (value: (command: string) => string) => rewrite(opts.content, fenced, value) + if (opts.disabled) return replace(() => DISABLED_NOTE) + if (!opts.trusted) return replace(() => UNTRUSTED_NOTE) // `shell` is resolved by the caller via Shell.acceptable(cfg.shell), which // rejects shells the tree-sitter bash scanner can't parse (fish/nu), keeping @@ -65,7 +71,7 @@ export namespace SkillInject { const shell = opts.shell // Deduplicate identical commands, then cap the batch so a skill can't queue // an unbounded number of processes. - const commands = Array.from(new Set(matches.map(([, cmd]) => cmd))).slice(0, MAX_COMMANDS) + const commands = Array.from(new Set(live.map(([, cmd]) => cmd))).slice(0, MAX_COMMANDS) // Decompose each command into sub-command patterns + out-of-project dir globs // via the shared bash scan, so plan-mode denies and external_directory checks @@ -111,41 +117,80 @@ export namespace SkillInject { metadata, }) - // Run each command in the instance directory, bounded by ctx.abort (ESC) and a - // timeout, with output truncated so it can't blow up or poison the prompt. + // Run each command in the instance directory, bounded per-command by ctx.abort (ESC) + // and a timeout, and across the batch by an aggregate wall-clock budget, with output + // truncated so it can't blow up or poison the prompt. const outputs = new Map() + const deadline = Date.now() + BUDGET_MS for (const command of commands) { + if (Date.now() >= deadline) { + outputs.set(command, "[skill shell batch time budget exceeded]") + continue + } outputs.set(command, yield* run(command, shell, opts.cwd, opts.ctx.abort)) } - return replace(opts.content, (command) => outputs.get(command) ?? "") + // A placeholder that was capped out of `commands` isn't in `outputs`; mark it rather + // than silently inlining an empty string. + return replace((command) => outputs.get(command) ?? LIMIT_NOTE) }) const run = Effect.fn("SkillInject.run")(function* (command: string, shell: string, cwd: string, abort: AbortSignal) { - const result = yield* Effect.promise(async () => { - // A cleared timer bounds the run without leaking a pending 2-minute timeout - // per command; ESC (ctx.abort) still kills the child via the same signal. - const controller = new AbortController() - const signal = AbortSignal.any([abort, controller.signal]) - const timer = setTimeout(() => controller.abort(), TIMEOUT_MS) - try { - return await Process.text([command], { shell, cwd, abort: signal, nothrow: true }).catch(() => undefined) - } finally { - clearTimeout(timer) - } - }) - if (!result) return abort.aborted ? "[skill shell command aborted]" : "[skill shell command timed out]" + const timeout = new AbortController() + // A cleared timer bounds the run without leaking a pending 2-minute timeout per command; + // ESC (ctx.abort) still kills the child via the same combined signal. + const signal = AbortSignal.any([abort, timeout.signal]) + const timer = setTimeout(() => timeout.abort(), TIMEOUT_MS) + const result = yield* Effect.promise(() => + Process.text([command], { shell, cwd, abort: signal, nothrow: true }).catch(() => undefined), + ).pipe(Effect.ensuring(Effect.sync(() => clearTimeout(timer)))) + + // With nothrow the promise resolves even when the child was killed, inlining partial + // stdout; detect the kill via the signals so an aborted/timed-out command is marked. + if (abort.aborted) return "[skill shell command aborted]" + if (timeout.signal.aborted) return "[skill shell command timed out]" + if (!result) return "[skill shell command failed]" + // A failing command with empty stdout would inline ""; surface a marker with any stderr. + if (result.code !== 0 && result.text.length === 0) { + const err = result.stderr.toString().trim() + return err ? "[skill shell command failed]\n" + truncate(err) : "[skill shell command failed]" + } return truncate(result.text) }) + // Byte-accurate truncation: slice on a Buffer so a multibyte tail can't exceed the cap. function truncate(text: string) { - if (Buffer.byteLength(text) <= MAX_OUTPUT_BYTES) return text - return text.slice(0, MAX_OUTPUT_BYTES) + "\n[skill shell output truncated]" + const buf = Buffer.from(text) + if (buf.byteLength <= MAX_OUTPUT_BYTES) return text + return buf.toString("utf8", 0, MAX_OUTPUT_BYTES) + "\n[skill shell output truncated]" } - // Replace only the exact matches found in the ORIGINAL content. Never re-scan - // the result, so inlined output containing `!`cmd`` stays inert. - function replace(content: string, value: (command: string) => string) { - return content.replace(ConfigMarkdown.SHELL_REGEX, (_, command: string) => value(command)) + // Rewrite only live (unfenced) placeholders in the ORIGINAL content, substituting once and + // never re-scanning the result, so inlined output containing `!`cmd`` stays inert and a + // fenced documentation example is left as literal text. + function rewrite(content: string, fenced: (index: number) => boolean, value: (command: string) => string) { + return content.replace(ConfigMarkdown.SHELL_REGEX, (match, command: string, index: number) => + fenced(index) ? match : value(command), + ) + } + + // Return a predicate that reports whether a character offset falls inside a fenced code + // block (``` or ~~~), so placeholders in documentation examples are treated as inert. + function fences(content: string): (index: number) => boolean { + const ranges: Array<[number, number]> = [] + const fence = /^[ \t]*(`{3,}|~{3,})[^\n]*$/gm + let open: { start: number; marker: string } | undefined + for (const m of content.matchAll(fence)) { + const marker = m[1] + // CommonMark: a closing fence uses the same char and is at least as long as the opener, + // so an inner shorter/different fence stays content. Keep the real opener length. + if (!open) open = { start: m.index, marker } + else if (marker[0] === open.marker[0] && marker.length >= open.marker.length) { + ranges.push([open.start, m.index + m[0].length]) + open = undefined + } + } + if (open) ranges.push([open.start, content.length]) // unterminated fence runs to EOF + return (index: number) => ranges.some(([s, e]) => index >= s && index < e) } } diff --git a/packages/opencode/test/kilocode/skills/display.test.ts b/packages/opencode/test/kilocode/skills/display.test.ts index b7780e5138c..3cdcb49afcf 100644 --- a/packages/opencode/test/kilocode/skills/display.test.ts +++ b/packages/opencode/test/kilocode/skills/display.test.ts @@ -9,6 +9,13 @@ describe("displayCommand", () => { expect(out).not.toMatch(/[\u0000-\u001f]/) }) + it("escapes bidi/format controls so Trojan-Source reordering can't hide intent", () => { + // RLO (U+202E) + PDI (U+2069) would visually reorder the command in the prompt + const out = displayCommand("echo \u202esafe\u2069 rm -rf /") + expect(out).toBe("echo \\u202esafe\\u2069 rm -rf /") + expect(out).not.toMatch(/[\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/) + }) + it("leaves ordinary commands unchanged", () => { expect(displayCommand("git status --short")).toBe("git status --short") }) diff --git a/packages/opencode/test/kilocode/skills/inject.test.ts b/packages/opencode/test/kilocode/skills/inject.test.ts index 309b84b4a61..16846952941 100644 --- a/packages/opencode/test/kilocode/skills/inject.test.ts +++ b/packages/opencode/test/kilocode/skills/inject.test.ts @@ -227,6 +227,51 @@ describe("skill shell injection", () => { }), ) + unix("does not execute placeholders inside fenced code blocks", () => + Effect.gen(function* () { + // The fenced placeholder is a documentation example and must stay literal; only the + // live one runs. + yield* writeGlobalSkill("fenced-shell", "Live: !`printf LIVE`\n\n```\nExample: !`printf FENCED`\n```\n") + + const requests: Array> = [] + const result = yield* loadSkill("fenced-shell", (req) => + Effect.sync(() => { + requests.push(req) + }), + ) + + expect(result.output).toContain("Live: LIVE") + // the fenced example is left verbatim, not executed + expect(result.output).toContain("Example: !`printf FENCED`") + expect(result.output).not.toContain("Example: FENCED") + // only the live command is authorized + const bash = requests.filter((r) => r.permission === "bash") + expect(bash[0]?.patterns).toEqual(["printf LIVE"]) + }), + ) + + unix("treats a placeholder inside a nested (```` wrapping ```) fence as inert", () => + Effect.gen(function* () { + // The common "wrap a ``` example in a ```` fence" pattern must not execute the inner + // example; a shorter inner fence does not close the longer outer one. + const body = "Live: !`printf LIVE`\n\n````md\n```bash\nExample: !`printf FENCED`\n```\n````\n" + yield* writeGlobalSkill("nested-fence", body) + + const requests: Array> = [] + const result = yield* loadSkill("nested-fence", (req) => + Effect.sync(() => { + requests.push(req) + }), + ) + + expect(result.output).toContain("Live: LIVE") + expect(result.output).toContain("!`printf FENCED`") + expect(result.output).not.toContain("Example: FENCED") + const bash = requests.filter((r) => r.permission === "bash") + expect(bash[0]?.patterns).toEqual(["printf LIVE"]) + }), + ) + unix("does not re-execute shell placeholders emitted by command output", () => Effect.gen(function* () { // The command emits a literal placeholder `!echo pwned` From 6e46b809bf7fe356a8eb399bfba439850686ea47 Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 16:53:31 +0200 Subject: [PATCH 33/71] fix(cli): honor the kill-switch for skill slash-command shell --- .changeset/skill-shell-execution.md | 2 +- packages/opencode/src/kilocode/skills/display.ts | 5 +++++ packages/opencode/src/kilocode/skills/inject.ts | 12 +++++++----- packages/opencode/src/session/prompt.ts | 12 ++++++++---- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/.changeset/skill-shell-execution.md b/.changeset/skill-shell-execution.md index bbe09cf70d7..8689613673f 100644 --- a/.changeset/skill-shell-execution.md +++ b/.changeset/skill-shell-execution.md @@ -2,4 +2,4 @@ "@kilocode/cli": minor --- -Support executing shell commands embedded in skill files. Commands written as `` !`command` `` in a SKILL.md run when the skill loads and their output is inlined into the skill, gated by a single up-front approval that lists every command. Only trusted skills can run commands, and `KILO_DISABLE_SKILL_SHELL` disables the behavior. +Support executing shell commands embedded in skill files. Commands written as `` !`command` `` in a SKILL.md run and their output is inlined into the skill. Only trusted skills can run commands and `KILO_DISABLE_SKILL_SHELL` disables the behavior; when the model loads a skill, the commands are shown in a single up-front approval before running. diff --git a/packages/opencode/src/kilocode/skills/display.ts b/packages/opencode/src/kilocode/skills/display.ts index 199315ff409..d5182e73d0d 100644 --- a/packages/opencode/src/kilocode/skills/display.ts +++ b/packages/opencode/src/kilocode/skills/display.ts @@ -1,3 +1,8 @@ +// Markers inlined in place of a `!`cmd`` placeholder when it is not executed. Shared by the +// skill tool (inject.ts) and the slash-command path (session/prompt.ts) so both render identically. +export const SKILL_SHELL_DISABLED = "[skill shell execution disabled by policy]" +export const SKILL_SHELL_UNTRUSTED = "[skill shell execution disabled for untrusted skill]" + // Render a skill command for a permission prompt as a single, tamper-evident line. // Escape control chars (CR/LF/ESC/etc.) so a command can't repaint the terminal, and // bidi/format controls (U+202A-202E, U+2066-2069, U+200E/F, U+2028/9) so a Trojan-Source diff --git a/packages/opencode/src/kilocode/skills/inject.ts b/packages/opencode/src/kilocode/skills/inject.ts index b3f77d1532a..07f45863cfc 100644 --- a/packages/opencode/src/kilocode/skills/inject.ts +++ b/packages/opencode/src/kilocode/skills/inject.ts @@ -1,6 +1,7 @@ import { Effect } from "effect" import { ConfigMarkdown } from "@/config/markdown" import { Process } from "@/util/process" +import { SKILL_SHELL_DISABLED, SKILL_SHELL_UNTRUSTED } from "@/kilocode/skills/display" import type * as Tool from "@/tool/tool" // Shell injection for skill bodies mirrors Claude's "dynamic context injection": @@ -20,13 +21,14 @@ import type * as Tool from "@/tool/tool" // of any allow/auto-approve rule; a deny rule or plan-mode veto on any // sub-command still blocks. Approve runs the batch; reject aborts the load. // +// Trust and the kill-switch also gate the slash-command path (`/skill`, session/prompt.ts), +// which is user-initiated. Batch approval (control 3) is specific to this model-initiated +// tool path — the slash-command path is not prompted because the user invoked it directly. +// // Substitution runs exactly once. Command output is inlined as plain text and is // never re-scanned, so a command cannot emit a `!`cmd`` placeholder that a later // pass would execute (second-order injection). -const DISABLED_NOTE = "[skill shell execution disabled by policy]" -const UNTRUSTED_NOTE = "[skill shell execution disabled for untrusted skill]" - // Execution bounds: model-initiated commands must not hang the load, blow up // context, or overrun the batch. const TIMEOUT_MS = 2 * 60 * 1000 // per-command @@ -62,8 +64,8 @@ export namespace SkillInject { // Defense-in-depth ordering: policy checks first, approval gate last. `replace` only // rewrites live (unfenced) placeholders; fenced ones stay as literal text. const replace = (value: (command: string) => string) => rewrite(opts.content, fenced, value) - if (opts.disabled) return replace(() => DISABLED_NOTE) - if (!opts.trusted) return replace(() => UNTRUSTED_NOTE) + if (opts.disabled) return replace(() => SKILL_SHELL_DISABLED) + if (!opts.trusted) return replace(() => SKILL_SHELL_UNTRUSTED) // `shell` is resolved by the caller via Shell.acceptable(cfg.shell), which // rejects shells the tree-sitter bash scanner can't parse (fish/nu), keeping diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 5a04b9042ea..ad3377f7dc9 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -4,6 +4,7 @@ import path from "path" import { SessionV1 } from "@opencode-ai/core/v1/session" import os from "os" import { KiloSessionPrompt } from "@/kilocode/session/prompt" // kilocode_change +import { SKILL_SHELL_DISABLED, SKILL_SHELL_UNTRUSTED } from "@/kilocode/skills/display" // kilocode_change import { KiloSessionMessageOrder } from "@/kilocode/session/message-order" // kilocode_change import { KiloSessionPromptQueue } from "@/kilocode/session/prompt-queue" // kilocode_change import { KiloSession } from "@/kilocode/session" // kilocode_change @@ -2035,10 +2036,13 @@ export const layer = Layer.effect( } const shellMatches = ConfigMarkdown.shell(template) - // kilocode_change start - untrusted skill templates must not spawn shell; mirror the skill tool's trust gate - const untrustedSkill = cmd.source === "skill" && cmd.trusted !== true - if (shellMatches.length > 0 && untrustedSkill) { - template = template.replace(bashRegex, () => "[skill shell execution disabled for untrusted skill]") + // kilocode_change start - skill templates run !`cmd`` only when trusted and the kill-switch is off, + // mirroring the skill tool's gate (the slash-command path is user-initiated, so it is not prompted). + const skillTemplate = cmd.source === "skill" + const skillShellBlocked = skillTemplate && (cmd.trusted !== true || flags.disableSkillShell) + if (shellMatches.length > 0 && skillShellBlocked) { + const note = cmd.trusted !== true ? SKILL_SHELL_UNTRUSTED : SKILL_SHELL_DISABLED + template = template.replace(bashRegex, () => note) } else if (shellMatches.length > 0) { // kilocode_change end const cfg = yield* config.get() From 2da0517e3dbdb2d346a69eb66f14aea8574202ac Mon Sep 17 00:00:00 2001 From: Bruno Agatao Date: Wed, 29 Jul 2026 17:09:28 +0200 Subject: [PATCH 34/71] chore(cli): annotate skill download changes with kilocode_change markers --- packages/opencode/src/skill/discovery.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/skill/discovery.ts b/packages/opencode/src/skill/discovery.ts index f4e0c50d226..f48b1b4eb2e 100644 --- a/packages/opencode/src/skill/discovery.ts +++ b/packages/opencode/src/skill/discovery.ts @@ -87,8 +87,10 @@ export const layer: Layer.Layer Effect.gen(function* () { yield* Effect.forEach(skill.files, (file) => download(file.url, file.dest), { @@ -147,6 +150,7 @@ export const layer: Layer.Layer dir !== null) }) From eeb8c40af8faf262100b1f1b7a982e2983eaa8f9 Mon Sep 17 00:00:00 2001 From: emilieschario <14057155+emilieschario@users.noreply.github.com> Date: Wed, 29 Jul 2026 16:00:02 +0000 Subject: [PATCH 35/71] docs(kilo-docs): add CLI usage section for Cloud Agents Add documentation on how to start tasks from the CLI using the `kilo cloud` command and clarify how skills are handled in Cloud Agent sessions. Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../pages/code-with-ai/platforms/cloud-agent.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md b/packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md index 5a9fe22b843..294b8bdd02b 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md @@ -36,6 +36,16 @@ Before using Cloud Agents: Your work is always pushed to GitHub, ensuring nothing is lost. +## Starting Tasks from the CLI + +Use the `kilo cloud` command to run Cloud Agent tasks without opening the browser: + +```bash +kilo cloud start --prompt "Fix the flaky login test" --repo Kilo-Org/kilocode +``` + +`kilo cloud` can start tasks, send follow-up prompts, and check task status and results. Repository, branch, model, mode, and organization are inferred from your local checkout and CLI defaults unless you pass the matching flags. Add `--stream` to `kilo cloud start` to print task events as JSONL until the task completes. See the [CLI reference](/docs/code-with-ai/platforms/cli-reference#kilo-cloud) for all commands and options. + ## How Cloud Agents Work - Each user receives an **isolated Linux container** with common dev tools preinstalled (Node.js, git, gh CLI, glab CLI, etc.). @@ -98,6 +108,8 @@ You can customize each Cloud Agent session by also defining env vars and startup Cloud Agents support project-level [skills](/docs/code-with-ai/platforms/cli#skills) stored in your repository. When your repo is cloned, any skills in `.kilocode/skills/` are automatically available. +- Skills (skill folders uploaded as `.zip` archives, with up to 40 companion files per skill) + {% callout type="note" %} Global skills (`~/.kilocode/skills/`) are not available in Cloud Agents since there is no persistent user home directory. {% /callout %} From 0912f94cb164cd117185e8fdbbceda9915b1365c Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 16:31:59 +0000 Subject: [PATCH 36/71] docs(kilo-docs): rewrite skill archive bullet as prose --- packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md b/packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md index 294b8bdd02b..40c573ce582 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md @@ -108,7 +108,7 @@ You can customize each Cloud Agent session by also defining env vars and startup Cloud Agents support project-level [skills](/docs/code-with-ai/platforms/cli#skills) stored in your repository. When your repo is cloned, any skills in `.kilocode/skills/` are automatically available. -- Skills (skill folders uploaded as `.zip` archives, with up to 40 companion files per skill) +You can also upload skills as `.zip` archives through the Cloud UI. Each skill archive can include up to 40 companion files. {% callout type="note" %} Global skills (`~/.kilocode/skills/`) are not available in Cloud Agents since there is no persistent user home directory. From cf9c9dc34979ba5c780aece9e0c6d562478638ff Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 16:32:12 +0000 Subject: [PATCH 37/71] docs(kilo-docs): fold orphaned skills bullet into paragraph --- .../kilo-docs/pages/code-with-ai/platforms/cloud-agent.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md b/packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md index 40c573ce582..bc217800637 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md @@ -106,9 +106,7 @@ You can customize each Cloud Agent session by also defining env vars and startup ## Skills -Cloud Agents support project-level [skills](/docs/code-with-ai/platforms/cli#skills) stored in your repository. When your repo is cloned, any skills in `.kilocode/skills/` are automatically available. - -You can also upload skills as `.zip` archives through the Cloud UI. Each skill archive can include up to 40 companion files. +Cloud Agents support project-level [skills](/docs/code-with-ai/platforms/cli#skills) stored in your repository. When your repo is cloned, any skills in `.kilocode/skills/` are automatically available. Skill folders are uploaded as `.zip` archives, with up to 40 companion files per skill. {% callout type="note" %} Global skills (`~/.kilocode/skills/`) are not available in Cloud Agents since there is no persistent user home directory. From 3a2309cff0987bf10066ea056346f2b94b8192dc Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Wed, 29 Jul 2026 21:57:51 -0400 Subject: [PATCH 38/71] fix(vscode): clarify web search config scope --- .../src/routes/config/ToolsRoute.tsx | 2 +- .../src/components/settings/BrowserTab.tsx | 24 +++++++++++++++---- .../kilo-vscode/webview-ui/src/i18n/ar.ts | 5 ++-- .../kilo-vscode/webview-ui/src/i18n/br.ts | 6 ++--- .../kilo-vscode/webview-ui/src/i18n/bs.ts | 6 ++--- .../kilo-vscode/webview-ui/src/i18n/da.ts | 6 ++--- .../kilo-vscode/webview-ui/src/i18n/de.ts | 6 ++--- .../kilo-vscode/webview-ui/src/i18n/en.ts | 6 ++--- .../kilo-vscode/webview-ui/src/i18n/es.ts | 6 ++--- .../kilo-vscode/webview-ui/src/i18n/fa.ts | 7 +++++- .../kilo-vscode/webview-ui/src/i18n/fr.ts | 5 ++-- .../kilo-vscode/webview-ui/src/i18n/it.ts | 6 ++--- .../kilo-vscode/webview-ui/src/i18n/ja.ts | 6 ++--- .../kilo-vscode/webview-ui/src/i18n/ko.ts | 6 ++--- .../kilo-vscode/webview-ui/src/i18n/nl.ts | 6 ++--- .../kilo-vscode/webview-ui/src/i18n/no.ts | 6 ++--- .../kilo-vscode/webview-ui/src/i18n/pl.ts | 6 ++--- .../kilo-vscode/webview-ui/src/i18n/ru.ts | 6 ++--- .../kilo-vscode/webview-ui/src/i18n/th.ts | 6 ++--- .../kilo-vscode/webview-ui/src/i18n/tr.ts | 5 ++-- .../kilo-vscode/webview-ui/src/i18n/uk.ts | 6 ++--- .../kilo-vscode/webview-ui/src/i18n/zh.ts | 4 ++-- .../kilo-vscode/webview-ui/src/i18n/zht.ts | 4 ++-- packages/sdk/openapi.json | 7 +++--- 24 files changed, 69 insertions(+), 84 deletions(-) diff --git a/packages/kilo-console/src/routes/config/ToolsRoute.tsx b/packages/kilo-console/src/routes/config/ToolsRoute.tsx index 9dcb20b6e05..1dd1001e66c 100644 --- a/packages/kilo-console/src/routes/config/ToolsRoute.tsx +++ b/packages/kilo-console/src/routes/config/ToolsRoute.tsx @@ -83,7 +83,7 @@ export function ToolsRoute() { > Enable for all providers - Search requests connect directly to Exa or Parallel. + Search requests use Exa or Parallel. {(reason) => {reason()}} diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx index fabc7858b8a..eee016f3f51 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx @@ -1,4 +1,4 @@ -import { Component, createSignal, onCleanup, onMount } from "solid-js" +import { Component, Show, createSignal, onCleanup, onMount } from "solid-js" import { Switch } from "@kilocode/kilo-ui/switch" import { Card } from "@kilocode/kilo-ui/card" import { useVSCode } from "../../context/vscode" @@ -26,7 +26,7 @@ const Header: Component<{ title: string }> = (props) => ( const BrowserTab: Component = () => { const { postMessage, onMessage } = useVSCode() const { t } = useLanguage() - const { config, updateConfig } = useConfig() + const { globalConfig, projectConfig, updateGlobalConfig } = useConfig() const [settings, setSettings] = createSignal({ enabled: false, @@ -52,9 +52,11 @@ const BrowserTab: Component = () => { } const updateWebsearch = (checked: boolean) => { - updateConfig({ web_search: checked }) + updateGlobalConfig({ web_search: checked }) } + const overridden = () => projectConfig().web_search !== undefined + return (
{/* Info text */} @@ -84,12 +86,24 @@ const BrowserTab: Component = () => { t("settings.config.scope.global")} + last={!overridden()} > - + {t("settings.webTools.webSearch.title")} + + t("settings.config.scope.local")} + last + > + + {`${t("settings.webTools.webSearch.title")} (${t("settings.config.scope.local")})`} + + +
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 3ac74a5dc53..3f5e3e6ce6c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -650,12 +650,11 @@ export const dict = { "settings.agentBehaviour.title": "سلوك الوكيل", "settings.autoApprove.title": "الموافقة التلقائية", "settings.webTools.title": "أدوات الويب", - "settings.webTools.description": "اضبط البحث على الويب وأتمتة المتصفح. تتصل طلبات البحث مباشرةً بـ Exa أو Parallel.", + "settings.webTools.description": "اضبط البحث على الويب وأتمتة المتصفح.", "settings.webTools.webSearch.enable": "تمكين لجميع المزوّدين", "settings.webTools.browserAutomation": "أتمتة المتصفح", "settings.webTools.webSearch.title": "البحث على الويب", - "settings.webTools.webSearch.description": - "اجعل البحث على الويب متاحًا لنماذج جميع المزوّدين. تتصل عمليات البحث مباشرةً بـ Exa أو Parallel.", + "settings.webTools.webSearch.description": "اجعل البحث على الويب متاحًا لنماذج جميع المزوّدين.", "settings.checkpoints.title": "نقاط التحقق", "settings.display.title": "العرض", "settings.autocomplete.title": "الإكمال التلقائي", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 31a10d9a3de..494dabab5c1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -666,13 +666,11 @@ export const dict = { "settings.agentBehaviour.title": "Comportamento do Agente", "settings.autoApprove.title": "Aprovação Automática", "settings.webTools.title": "Ferramentas da Web", - "settings.webTools.description": - "Configure a pesquisa na web e a automação do navegador. As solicitações de pesquisa se conectam diretamente ao Exa ou Parallel.", + "settings.webTools.description": "Configure a pesquisa na web e a automação do navegador.", "settings.webTools.webSearch.enable": "Ativar para todos os provedores", "settings.webTools.browserAutomation": "Automação do navegador", "settings.webTools.webSearch.title": "Pesquisa na Web", - "settings.webTools.webSearch.description": - "Disponibilize a pesquisa na web para modelos de todos os provedores. As pesquisas se conectam diretamente ao Exa ou Parallel.", + "settings.webTools.webSearch.description": "Disponibilize a pesquisa na web para modelos de todos os provedores.", "settings.checkpoints.title": "Pontos de Verificação", "settings.display.title": "Exibição", "settings.autocomplete.title": "Autocompletar", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 98bdd64db15..59af81ddce3 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -706,13 +706,11 @@ export const dict = { "settings.agentBehaviour.title": "Ponašanje agenta", "settings.autoApprove.title": "Automatsko odobravanje", "settings.webTools.title": "Web alati", - "settings.webTools.description": - "Konfigurišite web pretragu i automatizaciju preglednika. Zahtjevi za pretragu povezuju se direktno s Exa ili Parallel.", + "settings.webTools.description": "Konfigurišite web pretragu i automatizaciju preglednika.", "settings.webTools.webSearch.enable": "Omogući za sve pružaoce", "settings.webTools.browserAutomation": "Automatizacija preglednika", "settings.webTools.webSearch.title": "Web pretraga", - "settings.webTools.webSearch.description": - "Omogućite web pretragu modelima svih pružalaca. Pretrage se povezuju direktno s Exa ili Parallel.", + "settings.webTools.webSearch.description": "Omogućite web pretragu modelima svih pružalaca.", "settings.checkpoints.title": "Kontrolne tačke", "settings.display.title": "Prikaz", "settings.autocomplete.title": "Automatsko dovršavanje", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index cf802a19026..c89da39cc80 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -704,13 +704,11 @@ export const dict = { "settings.agentBehaviour.title": "Agentadfærd", "settings.autoApprove.title": "Automatisk godkendelse", "settings.webTools.title": "Webværktøjer", - "settings.webTools.description": - "Konfigurer websøgning og browserautomatisering. Søgeanmodninger sendes direkte til Exa eller Parallel.", + "settings.webTools.description": "Konfigurer websøgning og browserautomatisering.", "settings.webTools.webSearch.enable": "Aktivér for alle udbydere", "settings.webTools.browserAutomation": "Browserautomatisering", "settings.webTools.webSearch.title": "Websøgning", - "settings.webTools.webSearch.description": - "Gør websøgning tilgængelig for modeller fra alle udbydere. Søgninger sendes direkte til Exa eller Parallel.", + "settings.webTools.webSearch.description": "Gør websøgning tilgængelig for modeller fra alle udbydere.", "settings.checkpoints.title": "Kontrolpunkter", "settings.display.title": "Visning", "settings.autocomplete.title": "Autofuldførelse", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 118f5bf2863..755de779b5c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -717,13 +717,11 @@ export const dict = { "settings.agentBehaviour.title": "Agentenverhalten", "settings.autoApprove.title": "Automatisch genehmigen", "settings.webTools.title": "Web-Tools", - "settings.webTools.description": - "Konfigurieren Sie Websuche und Browserautomatisierung. Suchanfragen werden direkt an Exa oder Parallel gesendet.", + "settings.webTools.description": "Konfigurieren Sie Websuche und Browserautomatisierung.", "settings.webTools.webSearch.enable": "Für alle Anbieter aktivieren", "settings.webTools.browserAutomation": "Browserautomatisierung", "settings.webTools.webSearch.title": "Websuche", - "settings.webTools.webSearch.description": - "Machen Sie die Websuche für Modelle aller Anbieter verfügbar. Suchanfragen werden direkt an Exa oder Parallel gesendet.", + "settings.webTools.webSearch.description": "Machen Sie die Websuche für Modelle aller Anbieter verfügbar.", "settings.checkpoints.title": "Prüfpunkte", "settings.display.title": "Anzeige", "settings.autocomplete.title": "Autovervollständigung", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 96ba2325692..3f8a1b01869 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -623,13 +623,11 @@ export const dict = { "settings.agentBehaviour.title": "Agent Behaviour", "settings.autoApprove.title": "Auto-Approve", "settings.webTools.title": "Web Tools", - "settings.webTools.description": - "Configure web search and browser automation. Search requests connect directly to Exa or Parallel.", + "settings.webTools.description": "Configure web search and browser automation.", "settings.webTools.webSearch.enable": "Enable for All Providers", "settings.webTools.browserAutomation": "Browser Automation", "settings.webTools.webSearch.title": "Web Search", - "settings.webTools.webSearch.description": - "Make web search available to models from all providers. Searches connect directly to Exa or Parallel.", + "settings.webTools.webSearch.description": "Make web search available to models from all providers.", "settings.checkpoints.title": "Checkpoints", "settings.display.title": "Display", "settings.autocomplete.title": "Autocomplete", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 90edf60e2a7..815e038ced7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -711,13 +711,11 @@ export const dict = { "settings.agentBehaviour.title": "Comportamiento del agente", "settings.autoApprove.title": "Aprobación automática", "settings.webTools.title": "Herramientas web", - "settings.webTools.description": - "Configura la búsqueda web y la automatización del navegador. Las solicitudes de búsqueda se conectan directamente a Exa o Parallel.", + "settings.webTools.description": "Configura la búsqueda web y la automatización del navegador.", "settings.webTools.webSearch.enable": "Habilitar para todos los proveedores", "settings.webTools.browserAutomation": "Automatización del navegador", "settings.webTools.webSearch.title": "Búsqueda web", - "settings.webTools.webSearch.description": - "Permite que los modelos de todos los proveedores usen la búsqueda web. Las búsquedas se conectan directamente a Exa o Parallel.", + "settings.webTools.webSearch.description": "Permite que los modelos de todos los proveedores usen la búsqueda web.", "settings.checkpoints.title": "Puntos de control", "settings.display.title": "Pantalla", "settings.autocomplete.title": "Autocompletado", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fa.ts b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts index 02a2bf9ab5a..47e6bc815cf 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts @@ -624,7 +624,12 @@ export const dict = { "settings.agentBehaviour.title": "رفتار عامل", "settings.autoApprove.title": "تأیید خودکار", - "settings.browser.title": "مرورگر", + "settings.webTools.title": "ابزارهای وب", + "settings.webTools.description": "جستجوی وب و اتوماسیون مرورگر را پیکربندی کنید.", + "settings.webTools.webSearch.enable": "فعال‌سازی برای همه ارائه‌دهندگان", + "settings.webTools.browserAutomation": "اتوماسیون مرورگر", + "settings.webTools.webSearch.title": "جستجوی وب", + "settings.webTools.webSearch.description": "جستجوی وب را برای مدل‌های همه ارائه‌دهندگان در دسترس قرار دهید.", "settings.checkpoints.title": "نقاط بازیابی", "settings.display.title": "نمایش", "settings.autocomplete.title": "تکمیل خودکار", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 69a56922138..956fa4ba09c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -717,13 +717,12 @@ export const dict = { "settings.agentBehaviour.title": "Comportement de l'agent", "settings.autoApprove.title": "Approbation automatique", "settings.webTools.title": "Outils web", - "settings.webTools.description": - "Configurez la recherche web et l’automatisation du navigateur. Les requêtes de recherche se connectent directement à Exa ou Parallel.", + "settings.webTools.description": "Configurez la recherche web et l’automatisation du navigateur.", "settings.webTools.webSearch.enable": "Activer pour tous les fournisseurs", "settings.webTools.browserAutomation": "Automatisation du navigateur", "settings.webTools.webSearch.title": "Recherche web", "settings.webTools.webSearch.description": - "Rendez la recherche web disponible pour les modèles de tous les fournisseurs. Les recherches se connectent directement à Exa ou Parallel.", + "Rendez la recherche web disponible pour les modèles de tous les fournisseurs.", "settings.checkpoints.title": "Points de contrôle", "settings.display.title": "Affichage", "settings.autocomplete.title": "Autocomplétion", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 2720738fbdc..e299fc0a057 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -530,13 +530,11 @@ export const dict = { "settings.agentBehaviour.title": "Comportamento agente", "settings.autoApprove.title": "Approvazione automatica", "settings.webTools.title": "Strumenti web", - "settings.webTools.description": - "Configura la ricerca web e l'automazione del browser. Le richieste di ricerca si connettono direttamente a Exa o Parallel.", + "settings.webTools.description": "Configura la ricerca web e l'automazione del browser.", "settings.webTools.webSearch.enable": "Abilita per tutti i provider", "settings.webTools.browserAutomation": "Automazione del browser", "settings.webTools.webSearch.title": "Ricerca web", - "settings.webTools.webSearch.description": - "Rendi disponibile la ricerca web ai modelli di tutti i provider. Le ricerche si connettono direttamente a Exa o Parallel.", + "settings.webTools.webSearch.description": "Rendi disponibile la ricerca web ai modelli di tutti i provider.", "settings.checkpoints.title": "Checkpoint", "settings.display.title": "Visualizzazione", "settings.autocomplete.title": "Autocompletamento", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index a911d6477cd..668e6fe8ce8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -698,13 +698,11 @@ export const dict = { "settings.agentBehaviour.title": "エージェントの動作", "settings.autoApprove.title": "自動承認", "settings.webTools.title": "ウェブツール", - "settings.webTools.description": - "ウェブ検索とブラウザ自動化を設定します。検索リクエストは Exa または Parallel に直接接続されます。", + "settings.webTools.description": "ウェブ検索とブラウザ自動化を設定します。", "settings.webTools.webSearch.enable": "すべてのプロバイダーで有効化", "settings.webTools.browserAutomation": "ブラウザ自動化", "settings.webTools.webSearch.title": "ウェブ検索", - "settings.webTools.webSearch.description": - "すべてのプロバイダーのモデルでウェブ検索を利用できるようにします。検索は Exa または Parallel に直接接続されます。", + "settings.webTools.webSearch.description": "すべてのプロバイダーのモデルでウェブ検索を利用できるようにします。", "settings.checkpoints.title": "チェックポイント", "settings.display.title": "表示", "settings.autocomplete.title": "オートコンプリート", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index a19ee0ddd07..2aad8394d5c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -658,13 +658,11 @@ export const dict = { "settings.agentBehaviour.title": "에이전트 동작", "settings.autoApprove.title": "자동 승인", "settings.webTools.title": "웹 도구", - "settings.webTools.description": - "웹 검색 및 브라우저 자동화를 구성합니다. 검색 요청은 Exa 또는 Parallel에 직접 연결됩니다.", + "settings.webTools.description": "웹 검색 및 브라우저 자동화를 구성합니다.", "settings.webTools.webSearch.enable": "모든 제공업체에 사용", "settings.webTools.browserAutomation": "브라우저 자동화", "settings.webTools.webSearch.title": "웹 검색", - "settings.webTools.webSearch.description": - "모든 제공업체의 모델에서 웹 검색을 사용할 수 있도록 합니다. 검색은 Exa 또는 Parallel에 직접 연결됩니다.", + "settings.webTools.webSearch.description": "모든 제공업체의 모델에서 웹 검색을 사용할 수 있도록 합니다.", "settings.checkpoints.title": "체크포인트", "settings.display.title": "디스플레이", "settings.autocomplete.title": "자동 완성", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 73af15b3172..962fb6c92ed 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -659,13 +659,11 @@ export const dict = { "settings.agentBehaviour.title": "Agent Gedrag", "settings.autoApprove.title": "Automatisch Goedkeuren", "settings.webTools.title": "Webtools", - "settings.webTools.description": - "Configureer zoeken op internet en browserautomatisering. Zoekopdrachten maken rechtstreeks verbinding met Exa of Parallel.", + "settings.webTools.description": "Configureer zoeken op internet en browserautomatisering.", "settings.webTools.webSearch.enable": "Inschakelen voor alle providers", "settings.webTools.browserAutomation": "Browserautomatisering", "settings.webTools.webSearch.title": "Zoeken op internet", - "settings.webTools.webSearch.description": - "Maak zoeken op internet beschikbaar voor modellen van alle providers. Zoekopdrachten maken rechtstreeks verbinding met Exa of Parallel.", + "settings.webTools.webSearch.description": "Maak zoeken op internet beschikbaar voor modellen van alle providers.", "settings.checkpoints.title": "Controlepunten", "settings.display.title": "Weergave", "settings.autocomplete.title": "Automatisch Aanvullen", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index f1e24a26560..e72c07b75cf 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -666,13 +666,11 @@ export const dict = { "settings.agentBehaviour.title": "Agentoppførsel", "settings.autoApprove.title": "Automatisk godkjenning", "settings.webTools.title": "Nettverktøy", - "settings.webTools.description": - "Konfigurer nettsøk og nettleserautomatisering. Søk sendes direkte til Exa eller Parallel.", + "settings.webTools.description": "Konfigurer nettsøk og nettleserautomatisering.", "settings.webTools.webSearch.enable": "Aktiver for alle leverandører", "settings.webTools.browserAutomation": "Nettleserautomatisering", "settings.webTools.webSearch.title": "Nettsøk", - "settings.webTools.webSearch.description": - "Gjør nettsøk tilgjengelig for modeller fra alle leverandører. Søk sendes direkte til Exa eller Parallel.", + "settings.webTools.webSearch.description": "Gjør nettsøk tilgjengelig for modeller fra alle leverandører.", "settings.checkpoints.title": "Kontrollpunkter", "settings.display.title": "Visning", "settings.autocomplete.title": "Autofullfør", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 32c2384b34e..435a35e6a9a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -662,13 +662,11 @@ export const dict = { "settings.agentBehaviour.title": "Zachowanie agenta", "settings.autoApprove.title": "Automatyczne zatwierdzanie", "settings.webTools.title": "Narzędzia internetowe", - "settings.webTools.description": - "Skonfiguruj wyszukiwanie w sieci i automatyzację przeglądarki. Żądania wyszukiwania łączą się bezpośrednio z Exa lub Parallel.", + "settings.webTools.description": "Skonfiguruj wyszukiwanie w sieci i automatyzację przeglądarki.", "settings.webTools.webSearch.enable": "Włącz dla wszystkich dostawców", "settings.webTools.browserAutomation": "Automatyzacja przeglądarki", "settings.webTools.webSearch.title": "Wyszukiwanie w sieci", - "settings.webTools.webSearch.description": - "Udostępnij wyszukiwanie w sieci modelom wszystkich dostawców. Wyszukiwania łączą się bezpośrednio z Exa lub Parallel.", + "settings.webTools.webSearch.description": "Udostępnij wyszukiwanie w sieci modelom wszystkich dostawców.", "settings.checkpoints.title": "Punkty kontrolne", "settings.display.title": "Wyświetlanie", "settings.autocomplete.title": "Autouzupełnianie", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 737da75a121..824ada6f698 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -703,13 +703,11 @@ export const dict = { "settings.agentBehaviour.title": "Поведение агента", "settings.autoApprove.title": "Автоодобрение", "settings.webTools.title": "Веб-инструменты", - "settings.webTools.description": - "Настройте веб-поиск и автоматизацию браузера. Поисковые запросы отправляются напрямую в Exa или Parallel.", + "settings.webTools.description": "Настройте веб-поиск и автоматизацию браузера.", "settings.webTools.webSearch.enable": "Включить для всех провайдеров", "settings.webTools.browserAutomation": "Автоматизация браузера", "settings.webTools.webSearch.title": "Веб-поиск", - "settings.webTools.webSearch.description": - "Сделайте веб-поиск доступным для моделей всех провайдеров. Поисковые запросы отправляются напрямую в Exa или Parallel.", + "settings.webTools.webSearch.description": "Сделайте веб-поиск доступным для моделей всех провайдеров.", "settings.checkpoints.title": "Контрольные точки", "settings.display.title": "Отображение", "settings.autocomplete.title": "Автодополнение", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 18677db990e..d0f653af10b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -696,13 +696,11 @@ export const dict = { "settings.agentBehaviour.title": "พฤติกรรมของเอเจนต์", "settings.autoApprove.title": "อนุมัติอัตโนมัติ", "settings.webTools.title": "เครื่องมือเว็บ", - "settings.webTools.description": - "กำหนดค่าการค้นหาเว็บและระบบอัตโนมัติของเบราว์เซอร์ คำขอค้นหาจะเชื่อมต่อโดยตรงกับ Exa หรือ Parallel", + "settings.webTools.description": "กำหนดค่าการค้นหาเว็บและระบบอัตโนมัติของเบราว์เซอร์", "settings.webTools.webSearch.enable": "เปิดใช้สำหรับผู้ให้บริการทั้งหมด", "settings.webTools.browserAutomation": "ระบบอัตโนมัติของเบราว์เซอร์", "settings.webTools.webSearch.title": "ค้นหาเว็บ", - "settings.webTools.webSearch.description": - "ทำให้โมเดลจากผู้ให้บริการทั้งหมดใช้การค้นหาเว็บได้ การค้นหาจะเชื่อมต่อโดยตรงกับ Exa หรือ Parallel", + "settings.webTools.webSearch.description": "ทำให้โมเดลจากผู้ให้บริการทั้งหมดใช้การค้นหาเว็บได้", "settings.checkpoints.title": "จุดตรวจสอบ", "settings.display.title": "การแสดงผล", "settings.autocomplete.title": "เติมข้อความอัตโนมัติ", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 1b2a6a3c70f..6f6824da423 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -654,13 +654,12 @@ export const dict = { "settings.agentBehaviour.title": "Ajan Davranışı", "settings.autoApprove.title": "Otomatik Onay", "settings.webTools.title": "Web Araçları", - "settings.webTools.description": - "Web aramasını ve tarayıcı otomasyonunu yapılandırın. Arama istekleri doğrudan Exa veya Parallel'e bağlanır.", + "settings.webTools.description": "Web aramasını ve tarayıcı otomasyonunu yapılandırın.", "settings.webTools.webSearch.enable": "Tüm Sağlayıcılar İçin Etkinleştir", "settings.webTools.browserAutomation": "Tarayıcı Otomasyonu", "settings.webTools.webSearch.title": "Web Araması", "settings.webTools.webSearch.description": - "Web aramasını tüm sağlayıcıların modelleri için kullanılabilir hale getirin. Aramalar doğrudan Exa veya Parallel'e bağlanır.", + "Web aramasını tüm sağlayıcıların modelleri için kullanılabilir hale getirin.", "settings.checkpoints.title": "Kontrol Noktaları", "settings.display.title": "Görünüm", "settings.autocomplete.title": "Otomatik Tamamlama", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index a9bca7db9e1..f984d8f7a91 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -656,13 +656,11 @@ export const dict = { "settings.agentBehaviour.title": "Поведінка агента", "settings.autoApprove.title": "Автоматичне схвалення", "settings.webTools.title": "Вебінструменти", - "settings.webTools.description": - "Налаштуйте вебпошук і автоматизацію браузера. Пошукові запити надсилаються безпосередньо до Exa або Parallel.", + "settings.webTools.description": "Налаштуйте вебпошук і автоматизацію браузера.", "settings.webTools.webSearch.enable": "Увімкнути для всіх постачальників", "settings.webTools.browserAutomation": "Автоматизація браузера", "settings.webTools.webSearch.title": "Вебпошук", - "settings.webTools.webSearch.description": - "Зробіть вебпошук доступним для моделей усіх постачальників. Пошукові запити надсилаються безпосередньо до Exa або Parallel.", + "settings.webTools.webSearch.description": "Зробіть вебпошук доступним для моделей усіх постачальників.", "settings.checkpoints.title": "Контрольні точки", "settings.display.title": "Відображення", "settings.autocomplete.title": "Автодоповнення", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 35ca21a4fd1..9d14ba7edad 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -680,11 +680,11 @@ export const dict = { "settings.agentBehaviour.title": "智能体行为", "settings.autoApprove.title": "自动审批", "settings.webTools.title": "网络工具", - "settings.webTools.description": "配置网页搜索和浏览器自动化。搜索请求会直接连接到 Exa 或 Parallel。", + "settings.webTools.description": "配置网页搜索和浏览器自动化。", "settings.webTools.webSearch.enable": "为所有提供商启用", "settings.webTools.browserAutomation": "浏览器自动化", "settings.webTools.webSearch.title": "网页搜索", - "settings.webTools.webSearch.description": "让所有提供商的模型都可使用网页搜索。搜索会直接连接到 Exa 或 Parallel。", + "settings.webTools.webSearch.description": "让所有提供商的模型都可使用网页搜索。", "settings.checkpoints.title": "检查点", "settings.display.title": "显示", "settings.autocomplete.title": "自动补全", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 57b018982b7..afc051308aa 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -640,11 +640,11 @@ export const dict = { "settings.agentBehaviour.title": "Agent 行為", "settings.autoApprove.title": "自動核准", "settings.webTools.title": "網路工具", - "settings.webTools.description": "設定網頁搜尋和瀏覽器自動化。搜尋請求會直接連線至 Exa 或 Parallel。", + "settings.webTools.description": "設定網頁搜尋和瀏覽器自動化。", "settings.webTools.webSearch.enable": "為所有供應商啟用", "settings.webTools.browserAutomation": "瀏覽器自動化", "settings.webTools.webSearch.title": "網頁搜尋", - "settings.webTools.webSearch.description": "讓所有供應商的模型都可使用網頁搜尋。搜尋會直接連線至 Exa 或 Parallel。", + "settings.webTools.webSearch.description": "讓所有供應商的模型都可使用網頁搜尋。", "settings.checkpoints.title": "檢查點", "settings.display.title": "顯示", "settings.autocomplete.title": "自動完成", diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 3036985783e..d201367cb86 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -29194,10 +29194,6 @@ "hide_prompt_training_models": { "type": "boolean" }, - "web_search": { - "type": "boolean", - "description": "Make web search available to models from all providers (default: false)" - }, "sandbox": { "type": "object", "properties": { @@ -29456,6 +29452,9 @@ "type": "boolean" } }, + "web_search": { + "type": "boolean" + }, "attachment": { "$ref": "#/components/schemas/AttachmentConfig" }, From ddd3557bf00fba99cbed389cbeffafa567a45c34 Mon Sep 17 00:00:00 2001 From: Kelly Sun Date: Wed, 29 Jul 2026 21:58:41 -0400 Subject: [PATCH 39/71] docs: address review - use built-in Mixlayer provider + /connect Per review feedback: Mixlayer is a built-in provider, so document the Connect-provider flow instead of manual custom-provider setup, and add the CLI /connect command as a method. --- .../kilo-docs/pages/ai-providers/mixlayer.md | 50 ++++++------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/packages/kilo-docs/pages/ai-providers/mixlayer.md b/packages/kilo-docs/pages/ai-providers/mixlayer.md index ac0221315a6..6d261849ba2 100644 --- a/packages/kilo-docs/pages/ai-providers/mixlayer.md +++ b/packages/kilo-docs/pages/ai-providers/mixlayer.md @@ -17,62 +17,44 @@ Mixlayer is an inference platform for open models such as GLM and Qwen, with a s ## Configuration in Kilo Code -Mixlayer's API is OpenAI-compatible, with the base URL `https://models.mixlayer.ai/v1`. Configure it through Kilo Code's **OpenAI Compatible** provider. +Mixlayer is available as a **built-in provider** in Kilo Code, so you can connect it directly — no custom provider setup needed. {% tabs %} {% tab label="VSCode" %} 1. Open **Settings** (gear icon) and go to the **Providers** tab. -2. Scroll to the bottom and click **Custom provider**. -3. Fill in the dialog: - - **Provider ID** — `mixlayer` - - **Display name** — `Mixlayer` - - **Provider API** — **OpenAI Compatible** - - **Base URL** — `https://models.mixlayer.ai/v1` - - **API key** — your Mixlayer API key -4. Kilo Code auto-fetches the available models from Mixlayer's `/v1/models` endpoint, so you can pick a model directly from the list. Click **Submit** to save. +2. Click **Connect provider**, search for **Mixlayer**, and select it. +3. Enter your Mixlayer API key. +4. Pick a model — Kilo Code fetches the available models automatically. {% /tab %} {% tab label="CLI" %} -Set the API key as an environment variable and define an OpenAI-compatible provider in your `kilo.json` config file (`~/.config/kilo/kilo.json` or `./kilo.json`): +**Method 1 — `/connect` (recommended)** -**Environment variable:** +Run `kilo`, then use the `/connect` command, select **Mixlayer**, and paste your API key when prompted: + +```bash +kilo +# then, inside Kilo, run: +/connect +``` + +**Method 2 — config file** + +Set your API key and add Mixlayer in your `kilo.json` config file (`~/.config/kilo/kilo.json` or `./kilo.json`): ```bash export MIXLAYER_API_KEY="your-api-key" ``` -**Config file:** - ```jsonc { "provider": { "mixlayer": { - "npm": "@ai-sdk/openai-compatible", "env": ["MIXLAYER_API_KEY"], - "options": { - "baseURL": "https://models.mixlayer.ai/v1", - }, - "models": { - "z-ai/glm-5.2": { - "name": "GLM-5.2", - "limit": { "context": 262144, "output": 262144 }, - }, - "qwen/qwen3.5-397b-a17b": { - "name": "Qwen3.5 397B A17B", - "limit": { "context": 131072, "output": 131072 }, - }, - }, }, }, -} -``` - -Then set your default model using the `provider-id/model-id` format: - -```jsonc -{ "model": "mixlayer/z-ai/glm-5.2", } ``` From 21263db2cdc04e0bc8126987676e2be7abeba9cf Mon Sep 17 00:00:00 2001 From: Kelly Sun Date: Wed, 29 Jul 2026 22:27:03 -0400 Subject: [PATCH 40/71] docs: fix intro to reference built-in provider (not OpenAI Compatible) --- packages/kilo-docs/pages/ai-providers/mixlayer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-docs/pages/ai-providers/mixlayer.md b/packages/kilo-docs/pages/ai-providers/mixlayer.md index 6d261849ba2..1ae3e8e74e7 100644 --- a/packages/kilo-docs/pages/ai-providers/mixlayer.md +++ b/packages/kilo-docs/pages/ai-providers/mixlayer.md @@ -5,7 +5,7 @@ description: "Run open models like GLM and Qwen on Mixlayer's OpenAI-compatible # Using Mixlayer With Kilo Code -Mixlayer is an inference platform for open models such as GLM and Qwen, with a serving stack built from scratch by core contributors to Candle. It exposes an OpenAI-compatible API, so you can use it in Kilo Code through the **OpenAI Compatible** provider. +Mixlayer is an inference platform for open models such as GLM and Qwen, with a serving stack built from scratch by core contributors to Candle. It exposes an OpenAI-compatible API and is available as a built-in provider in Kilo Code. **Website:** [https://mixlayer.com/](https://mixlayer.com/) From 2d2b73d74d0d0af7c076665ea898f406f27942e4 Mon Sep 17 00:00:00 2001 From: Kelly Sun Date: Wed, 29 Jul 2026 22:31:02 -0400 Subject: [PATCH 41/71] docs: remove stale limit.output reference in tips --- packages/kilo-docs/pages/ai-providers/mixlayer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-docs/pages/ai-providers/mixlayer.md b/packages/kilo-docs/pages/ai-providers/mixlayer.md index 1ae3e8e74e7..4f34abfaa1d 100644 --- a/packages/kilo-docs/pages/ai-providers/mixlayer.md +++ b/packages/kilo-docs/pages/ai-providers/mixlayer.md @@ -76,4 +76,4 @@ Tool calling and reasoning are supported across the model line. See the [Mixlaye - **Model list:** Kilo Code auto-detects available models from Mixlayer's `/v1/models` endpoint, so the picker stays current with your account. - **Pricing:** See the [Mixlayer console](https://console.mixlayer.com/) for current per-model pricing. -- **Reasoning:** Qwen models support a thinking mode; reasoning tokens count against the output budget, so allow enough `limit.output` when reasoning is enabled. +- **Reasoning:** Qwen models support a thinking mode; reasoning tokens count against the output budget, so give responses enough room when reasoning is enabled. From 49223b3adecb891b185d7a21bac8d4993d3bee65 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 30 Jul 2026 10:16:51 +0200 Subject: [PATCH 42/71] feat(agent-manager): run project scripts in the embedded side terminal --- .changeset/calm-run-terminals.md | 5 + .kilo/plans/agent-manager-script-terminals.md | 575 ++++++++++++++++++ packages/core/src/kilocode/pty/termination.ts | 164 +++++ packages/core/src/pty.ts | 59 +- .../test/kilocode/pty-termination.test.ts | 104 ++++ packages/core/test/pty/pty-session.test.ts | 61 ++ .../kilo-docs/pages/automate/agent-manager.md | 2 +- .../src/agent-manager/AgentManagerProvider.ts | 48 +- .../agent-manager/ScriptTerminalManager.ts | 390 ++++++++++++ .../__tests__/AgentManagerProvider.spec.ts | 6 + .../kilo-vscode/src/agent-manager/host.ts | 3 + .../src/agent-manager/run/controller.ts | 14 +- .../src/agent-manager/run/manager.ts | 56 +- .../kilo-vscode/src/agent-manager/run/task.ts | 73 --- .../src/agent-manager/script-terminal-url.ts | 16 + .../kilo-vscode/src/agent-manager/types.ts | 7 + .../src/agent-manager/vscode-host.ts | 4 + .../tests/unit/agent-manager-arch.test.ts | 27 +- .../agent-manager-terminal-chrome.test.ts | 22 + .../unit/agent-manager-terminal-state.test.ts | 94 ++- .../tests/unit/run-script-manager.test.ts | 4 +- .../unit/script-terminal-manager.test.ts | 338 ++++++++++ .../agent-manager/AgentManagerApp.tsx | 6 + .../agent-manager/agent-manager.css | 13 + .../terminal/SideTerminalPanel.tsx | 1 + .../terminal/SortableTerminalTab.tsx | 22 +- .../agent-manager/terminal/chrome.ts | 23 + .../agent-manager/terminal/render.tsx | 1 + .../agent-manager/terminal/state.ts | 143 ++++- .../src/types/messages/extension-messages.ts | 19 + packages/server/src/handlers/pty.ts | 1 + 31 files changed, 2171 insertions(+), 130 deletions(-) create mode 100644 .changeset/calm-run-terminals.md create mode 100644 .kilo/plans/agent-manager-script-terminals.md create mode 100644 packages/core/src/kilocode/pty/termination.ts create mode 100644 packages/core/test/kilocode/pty-termination.test.ts create mode 100644 packages/kilo-vscode/src/agent-manager/ScriptTerminalManager.ts delete mode 100644 packages/kilo-vscode/src/agent-manager/run/task.ts create mode 100644 packages/kilo-vscode/src/agent-manager/script-terminal-url.ts create mode 100644 packages/kilo-vscode/tests/unit/agent-manager-terminal-chrome.test.ts create mode 100644 packages/kilo-vscode/tests/unit/script-terminal-manager.test.ts create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/terminal/chrome.ts diff --git a/.changeset/calm-run-terminals.md b/.changeset/calm-run-terminals.md new file mode 100644 index 00000000000..e1f1df7b1ec --- /dev/null +++ b/.changeset/calm-run-terminals.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Run Agent Manager project scripts in a named side terminal without opening the bottom VS Code terminal panel. diff --git a/.kilo/plans/agent-manager-script-terminals.md b/.kilo/plans/agent-manager-script-terminals.md new file mode 100644 index 00000000000..98567058a00 --- /dev/null +++ b/.kilo/plans/agent-manager-script-terminals.md @@ -0,0 +1,575 @@ +# Agent Manager Script Terminals + +Status: implementation plan only + +Baseline researched: `main` at `a0364858a6e1b69a2e2dc5434a82d5cefbe79ea7` (`v7.4.17`) + +Related issues: + +- [#12595](https://github.com/Kilo-Org/kilocode/issues/12595), per-worktree session terminals in the embedded side panel +- [#11083](https://github.com/Kilo-Org/kilocode/issues/11083), setup blocks session start and disrupts the terminal layout +- [#7526](https://github.com/Kilo-Org/kilocode/issues/7526), original Agent Manager Run script feature +- [#12597](https://github.com/Kilo-Org/kilocode/issues/12597), multiple side terminals, already implemented +- [#12649](https://github.com/Kilo-Org/kilocode/issues/12649), setup-script migration split from this implementation + +## Goal + +Setup scripts and Run scripts must execute in first-class terminal tabs inside the Agent Manager right-side terminal panel. Neither action should reveal or require the bottom VS Code terminal panel. + +The result must: + +- run the existing platform-specific setup and Run script files on Windows, Linux, and macOS; +- preserve the current working directory, environment, exit status, timeout, and one-Run-per-context behavior; +- show live output, accept input for interactive scripts, and retain bounded scrollback after exit; +- stop the correct process and its descendants; +- survive Agent Manager webview reloads and context switching while the extension/backend remain alive; +- reject unsafe script paths and respect VS Code Workspace Trust; +- never construct a shell command string in the webview or inject a command through terminal input. + +## Issue Scope Correction + +Issue #12595 currently describes routing a plain session/worktree shell through the existing `terminalButtonDestination` preference. Implementing that issue literally does not migrate either script system: + +- setup scripts still use `vscode.tasks.executeTask()` through `task-runner.ts`; +- Run scripts still use `vscode.tasks.executeTask()` through `run/task.ts`; +- both task definitions use `TaskRevealKind.Always`, which opens the bottom terminal panel. + +Before implementation, revise #12595 or replace its acceptance criteria with this plan. The plain session-terminal routing can remain a small related change, but it is not sufficient for the stated product goal. + +This plan addresses only the terminal/output part of #11083. Setup remains awaited before the first worktree session starts in the initial implementation. Making setup asynchronous is a separate lifecycle change and should not be combined with the terminal migration. + +## Product Decisions + +### Script output always belongs to Agent Manager + +Setup and Run are Agent Manager operations, so they always use named side-panel terminals: + +- `Setup` for worktree setup +- `Run` for the selected Local or worktree context + +They do not follow `kilo-code.new.agentManager.terminalButtonDestination`. That setting continues to control only where an ordinary user-requested interactive shell opens. This keeps the meaning of the setting narrow and avoids adding another preference. + +### No automatic VS Code terminal fallback + +If script PTY creation fails, show an Agent Manager error and do not execute the script. Automatically falling back after an uncertain PTY failure could execute a setup or Run script twice. + +An explicit manual action may open an ordinary VS Code terminal, but it must not silently rerun the script. + +### Preserve current lifecycle semantics + +- Setup stays best-effort: nonzero exit is visible as a failure, but session creation continues as it does today. +- Setup keeps its five-minute timeout, but the timeout must now terminate the process tree instead of only rejecting the wait. +- Run remains a toggle: invoking Run while active requests Stop rather than starting a second process. +- One Run process exists per Local/worktree context. +- Run status remains in memory and continues to drive the existing Run/Stop button and worktree status badge. + +## Current Architecture + +### Setup + +`SetupScriptRunner` is already platform-neutral and receives an injected `RunTask` callback. It resolves: + +| Platform | Script | Executable and arguments | +|---|---|---| +| Linux/macOS | `.kilo/setup-script`, then `.sh` | `sh ` | +| Windows | `.ps1`, then `.cmd`, then `.bat` | `powershell.exe ... -File ` or `cmd.exe /d /s /c ` | + +The VS Code-specific adapter is `packages/kilo-vscode/src/agent-manager/task-runner.ts`. + +### Run + +`RunController` and `RunScriptManager` already separate discovery, lifecycle, and UI status from the execution adapter. `RunController` passes an explicit executable, argument array, cwd, environment, and completion callback to `startVscodeRunTask()`. + +The VS Code-specific adapter is `packages/kilo-vscode/src/agent-manager/run/task.ts`. + +### Embedded terminals + +Agent Manager already has: + +- xterm.js terminals in the right-side inspector; +- multiple side terminals per context; +- direct PTY WebSocket streaming; +- per-context tab selection and ordering; +- persistent mounting while switching terminal tabs, Agent Manager contexts, Diff, and PR views. + +The extension-side path is currently: + +```text +webview terminal.create + -> TerminalRouter + -> TerminalManager + -> legacy client.pty.create() + -> kilo serve PTY + -> WebSocket + -> xterm.js +``` + +It currently creates only default interactive shells. It does not expose command, args, or env. + +### Existing backend capability + +The canonical PTY service already accepts: + +```ts +{ + command?: string + args?: string[] + cwd?: string + title?: string + env?: Record +} +``` + +It also retains bounded output, publishes `pty.exited` with an exit code, and keeps exited PTY metadata until removal. The canonical SDK surface is `client.v2.pty` under `/api/pty`. + +## Proposed Architecture + +### One execution backend + +Use the existing `kilo serve` PTY service as the only new script execution backend. Do not add another `child_process`, `node-pty`, VS Code pseudoterminal, or webview process runner. + +The new flow is: + +```text +SetupScriptRunner or RunController + -> PTY-backed execution adapter + -> ScriptTerminalManager + -> client.v2.pty.create(command, args, cwd, env) + -> canonical PTY WebSocket + -> existing Agent Manager side terminal/xterm.js +``` + +The executable and argument array are resolved by trusted extension code using the existing platform-specific builders. The webview receives only terminal display and attachment metadata. + +### Separate system terminals from user terminals + +Add a provider-owned `ScriptTerminalManager` next to the current `TerminalManager`. + +Do not send setup or Run processes through the existing webview-created `terminal.created` flow. That flow requires a pending webview `createId` and correctly closes unsolicited terminals. Setup can begin before the webview requests a terminal, so it needs a separate synchronization path. + +Suggested record: + +```ts +type ScriptTerminalKind = "setup" | "run" +type ScriptTerminalState = "starting" | "running" | "stopping" | "exited" | "failed" + +interface ScriptTerminalRecord { + terminalId: string + ptyID: string + worktreeId: string | null + kind: ScriptTerminalKind + title: string + cwd: string + wsUrl: string + state: ScriptTerminalState + exitCode?: number + startedAt: number + endedAt?: number +} +``` + +Registry rules: + +- one active `Run` record per context; +- one `Setup` record per worktree setup attempt; +- a new Run replaces the previous exited Run record after removing its retained PTY; +- user-created `Terminal N` tabs remain independently managed by `TerminalRouter`; +- script records survive `TerminalRouter.dispose()` and webview reloads; +- extension/backend shutdown removes all remaining script PTYs. + +### Completion and race handling + +Subscribe to global `pty.exited` events through `KiloConnectionService` and map backend PTY IDs to script records. + +Account for a fast process that exits before the extension registers the returned PTY: + +1. Create the PTY. +2. Store the record and backend ID. +3. Immediately call canonical `pty.get()`. +4. If it is already exited, finish from the returned status and exit code. +5. Otherwise rely on `pty.exited`. + +On connection restoration, reconcile every running script record with `pty.get()`. A missing PTY is an execution failure, not a successful exit. + +### Webview synchronization + +Add an extension-to-webview script-terminal snapshot/update protocol, for example: + +```ts +type: "agentManager.scriptTerminals" +terminals: ScriptTerminalView[] +``` + +Send a full snapshot: + +- when a script terminal is created; +- when status or exit code changes; +- after `agentManager.requestState`; +- after the webview reloads or reattaches. + +The webview terminal state adds script terminals without requiring a pending `createId`. A script terminal is still rendered by the existing `TerminalTab` and side-terminal layer. + +Do not send executable paths, args, arbitrary env, or script contents to the webview. + +## Required PTY Hardening + +These are prerequisites for claiming behavior comparable to VS Code Tasks. + +### Do not mutate explicit command arguments + +`packages/core/src/pty.ts` currently appends `-l` whenever the executable looks like a login shell. With `command: "sh"` and `args: [scriptPath]`, that produces `sh scriptPath -l`, making `-l` a script argument. + +Only add login-shell arguments when the caller did not provide an explicit command. Explicit `command` plus `args` must reach the process unchanged. + +### Terminate the process tree + +PTY removal currently calls only the PTY process's `kill()`. That does not prove that child and grandchild processes are terminated. + +Reuse or generalize `packages/core/src/shell.ts` `killTree()` semantics: + +- Windows: `taskkill /pid /f /t`, hidden window; +- Linux/macOS: signal the process group, then escalate from `SIGTERM` to `SIGKILL`; +- retain the direct PTY kill as a fallback. + +Run Stop, worktree deletion, setup timeout, tab close, extension shutdown, and backend shutdown must all use the same tree-termination path. + +### Replay exited output + +Canonical PTYs retain up to 2 MiB of output and exited metadata, but `Pty.attach()` currently rejects exited sessions. A quick setup can finish before the webview attaches, and a webview reload can occur after a Run exits. + +Allow a read-only attachment to an exited retained PTY: + +1. replay the requested bounded buffer; +2. send cursor/status metadata; +3. close normally with the exit code available through the state protocol; +4. reject writes after exit. + +Keep legacy `/pty` behavior unchanged. Script terminals use canonical `/api/pty`. + +### Keep shared-file changes isolated + +The PTY hardening touches shared upstream-owned code. Keep the changes minimal, use `kilocode_change` annotations where required, and run the opencode annotation and Promise-facade guards. + +## Security Model + +### Workspace Trust + +Before setup or Run script execution, require `vscode.workspace.isTrusted`. If the workspace is restricted, show the standard trust-management action and do not create a PTY. + +VS Code blocks terminals and Tasks in Restricted Mode. Moving execution behind `kilo serve` must not bypass that boundary. + +### Script path validation + +Run scripts already require a regular file and reject symlinks that resolve outside the root `.kilo` directory. Extract and reuse this validation for setup scripts. + +Both script systems must: + +- accept only the existing fixed platform-specific filenames; +- require a regular file; +- reject directories, devices, and other special files; +- reject a symlink whose real target escapes the root `.kilo` directory; +- use an absolute script path and validated absolute cwd. + +### Command construction + +- Resolve executable and args in extension code. +- Pass args as an array to `pty.create()`. +- Never concatenate a POSIX/PowerShell command string. +- Keep the existing `cmd.exe` path quoting helper and add paths-with-spaces and quotes tests. +- Never send a script command by `sendText`, xterm paste, or WebSocket input. + +### Environment + +Use one environment builder for setup and Run: + +- Linux/macOS: cached login-shell environment, preserving user PATH tools such as Homebrew, nvm, pyenv, and Cargo; +- Windows: extension-host process environment; +- overlay `WORKTREE_PATH` and `REPO_PATH`; +- retain PTY-enforced stripping of `KILO_SERVER_PASSWORD` and `KILO_SERVER_USERNAME`; +- do not serialize the environment into webview messages or persisted Agent Manager state. + +This intentionally improves setup parity. Setup currently lacks the login-shell environment used by Run. + +### Failure behavior + +- A PTY create error does not trigger a second execution path. +- A missing exit code is not treated as success. +- A server disconnect marks the script indeterminate until canonical status reconciliation completes. +- If process-tree termination cannot be confirmed, keep the UI in an error/stopping state and log the failure. + +## UX + +### Run + +When the user clicks Run or presses `Cmd/Ctrl+E`: + +1. Keep current script discovery/configuration behavior. +2. Open the Agent Manager terminal inspector. +3. Create or replace the semantic `Run` side tab for the current context. +4. Activate and focus it so interactive scripts can accept input. +5. Keep the existing worktree card and toolbar status synchronized. + +While running: + +- the tab shows a spinner; +- Run changes to Stop as today; +- pressing Run/Stop requests process-tree termination; +- closing the running Run tab means Stop and close; +- hiding the inspector does not stop the process. + +After exit: + +- exit `0` shows success; +- nonzero exit shows failure and the exit code; +- output remains available until the tab is closed or a new Run replaces it. + +### Setup + +When setup begins: + +1. Add the new worktree context to the UI as today. +2. Open its `Setup` side tab and show live output. +3. Keep the existing setup/session sequencing unchanged. +4. Continue session creation after success or failure, preserving current best-effort behavior. + +While setup runs: + +- the tab shows a spinner; +- the terminal can accept input if the script prompts; +- the tab cannot be destroyed accidentally; users may hide the inspector; +- the five-minute timeout stops the process tree and marks the tab failed. + +After setup exits, the tab becomes closable and retains its output. + +### Rendering before a session exists + +Setup begins before the first session exists in a new worktree. Update Agent Manager's empty-context logic so a context with a side script terminal renders the detail/inspector host even without a session tab. + +Do not place the current blocking setup overlay above the terminal. Keep progress visible in the sidebar/worktree state and in the Setup tab itself. + +### Titles and ordering + +- Keep `Setup` and `Run` as semantic labels; do not replace them with OSC shell titles. +- User-created terminals continue to use OSC title updates. +- System terminals participate in the existing side tab strip and can retain stable positions. +- The `+` button always creates a user terminal and never another Run or Setup process. + +### Accessibility + +- Announce starting, running, stopped, succeeded, and failed state changes. +- Include the exit code in accessible status text. +- Preserve existing keyboard tab navigation and terminal focus restoration. +- A hidden terminal inspector remains inert and `aria-hidden` while the PTY continues running. + +## Cross-Platform Contract + +Use the extension host's platform. In WSL, Remote SSH, and Dev Containers this means the remote Linux environment and POSIX script names, not native Windows script names. + +| Environment | Script execution | Important checks | +|---|---|---| +| macOS | `sh