From 3ef5c482919c43682a1dda037bb8296a5fc8bb21 Mon Sep 17 00:00:00 2001 From: Alvin Ward Date: Wed, 17 Dec 2025 16:17:48 +0200 Subject: [PATCH 01/10] feat: add GLM-4.6V model support for z.ai provider --- packages/types/src/providers/zai.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/types/src/providers/zai.ts b/packages/types/src/providers/zai.ts index e21fcc698bf..d597e7b1abf 100644 --- a/packages/types/src/providers/zai.ts +++ b/packages/types/src/providers/zai.ts @@ -100,6 +100,32 @@ export const internationalZAiModels = { description: "GLM-4.6 is Zhipu's newest model with an extended context window of up to 200k tokens, providing enhanced capabilities for processing longer documents and conversations.", }, + "glm-4.6v": { + maxTokens: 98_304, + contextWindow: 131_072, + supportsImages: true, + supportsPromptCache: true, + supportsNativeTools: true, + inputPrice: 0.3, + outputPrice: 0.9, + cacheWritesPrice: 0, + cacheReadsPrice: 0.05, + description: + "GLM-4.6V is Z.AI's multimodal visual reasoning model (image/video/text/file input), optimized for GUI tasks, grounding, document/video understanding, native function calling capabilities.", + }, + "glm-4.6v-flash": { + maxTokens: 98_304, + contextWindow: 131_072, + supportsImages: true, + supportsPromptCache: true, + supportsNativeTools: true, + inputPrice: 0, + outputPrice: 0, + cacheWritesPrice: 0, + cacheReadsPrice: 0, + description: + "GLM-4.6V-Flash is a free, high-speed multimodal model with visual reasoning capabilities, excellent for reasoning, coding, and agentic tasks.", + }, "glm-4-32b-0414-128k": { maxTokens: 98_304, contextWindow: 131_072, From 5c502a41541f4abb7df8a90238266f55405a0075 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Thu, 4 Dec 2025 01:00:20 -0500 Subject: [PATCH 02/10] Change default read permissions to not include full drive --- cli/src/config/__tests__/auto-approval.test.ts | 2 +- cli/src/config/defaults.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/config/__tests__/auto-approval.test.ts b/cli/src/config/__tests__/auto-approval.test.ts index 2171193f9e5..bb9cd4e2d9d 100644 --- a/cli/src/config/__tests__/auto-approval.test.ts +++ b/cli/src/config/__tests__/auto-approval.test.ts @@ -9,7 +9,7 @@ describe("Auto Approval Configuration", () => { enabled: true, read: { enabled: true, - outside: true, + outside: false, }, write: { enabled: true, diff --git a/cli/src/config/defaults.ts b/cli/src/config/defaults.ts index 44fb9d2a28d..82cac00f1e8 100644 --- a/cli/src/config/defaults.ts +++ b/cli/src/config/defaults.ts @@ -8,7 +8,7 @@ export const DEFAULT_AUTO_APPROVAL: AutoApprovalConfig = { enabled: true, read: { enabled: true, - outside: true, + outside: false, }, write: { enabled: true, From 6078a9ce77512faaebcda54ea9d2e909cf6b340c Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Thu, 4 Dec 2025 12:17:26 -0500 Subject: [PATCH 03/10] Add changeset for changing default read permissions --- .changeset/every-knives-dig.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/every-knives-dig.md diff --git a/.changeset/every-knives-dig.md b/.changeset/every-knives-dig.md new file mode 100644 index 00000000000..35e4af62974 --- /dev/null +++ b/.changeset/every-knives-dig.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": minor +--- + +Default read permissions now require approval for read operations outside the workspace From 4f13de78691522dac94056b49ab35b861709fb72 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Thu, 4 Dec 2025 12:38:49 -0500 Subject: [PATCH 04/10] Restructure the auto-approval CLI documentation for clarity --- apps/kilocode-docs/docs/cli.md | 112 ++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 50 deletions(-) diff --git a/apps/kilocode-docs/docs/cli.md b/apps/kilocode-docs/docs/cli.md index ebfee0b0fb9..cb6f17bcd7c 100644 --- a/apps/kilocode-docs/docs/cli.md +++ b/apps/kilocode-docs/docs/cli.md @@ -218,57 +218,11 @@ kilocode --parallel --auto "improve xyz" kilocode --parallel --auto "improve abc" ``` -## Autonomous mode (Non-Interactive) - -Autonomous mode allows Kilo Code to run in automated environments like CI/CD pipelines without requiring user interaction. - -```bash -# Run in autonomous mode with a prompt -kilocode --auto "Implement feature X" - -# Run in autonomous mode with piped input -echo "Fix the bug in app.ts" | kilocode --auto - -# Run in autonomous mode with timeout (in seconds) -kilocode --auto "Run tests" --timeout 300 - -# Run in autonomous mode with JSON output for structured parsing -kilocode --auto --json "Implement feature X" -``` - -### Autonomous Mode Behavior - -When running in Autonomous mode (`--auto` flag): - -1. **No User Interaction**: All approval requests are handled automatically based on configuration -2. **Auto-Approval/Rejection**: Operations are approved or rejected based on your auto-approval settings -3. **Follow-up Questions**: Automatically responded with a message instructing the AI to make autonomous decisions -4. **Automatic Exit**: The CLI exits automatically when the task completes or times out - -### JSON Output Mode +## Auto-approval settings -Use the `--json` flag with `--auto` to get structured JSON output instead of the default terminal UI. This is useful for programmatic integration and parsing of Kilo Code responses. +Auto-approval allows the Kilo Code CLI to perform operations without first requiring user confirmation. These settings can either be built up over time in interactive mode, or by editing your config file using `kilocode config` or editing the file directly at `~/.kilocode/config.json`. -```bash -# Standard autonomous mode with terminal UI -kilocode --auto "Fix the bug" - -# Autonomous mode with JSON output -kilocode --auto --json "Fix the bug" - -# With piped input -echo "Implement feature X" | kilocode --auto --json -``` - -**Requirements:** - -- The `--json` flag requires `--auto` mode to be enabled -- Output is sent to stdout as structured JSON for easy parsing -- Ideal for CI/CD pipelines and automated workflows - -### Auto-Approval Configuration - -Autonomous mode respects your auto-approval configuration. Edit your config file with `kilocode config` to customize: +### Default auto-approval settings ```json { @@ -276,7 +230,7 @@ Autonomous mode respects your auto-approval configuration. Edit your config file "enabled": true, "read": { "enabled": true, - "outside": true + "outside": false }, "write": { "enabled": true, @@ -359,6 +313,12 @@ The `execute.allowed` and `execute.denied` lists support hierarchical pattern ma } ``` +## Interactive Mode + +Interactive mode is the default mode when running Kilo Code without the `--auto` flag, designed to work interactively with a user through the console. + +In interactive mode Kilo Code will request approval for operations which have not been auto-approved, allowing the user to review and approve operations before they are executed, and optionally add them to the auto-approval list. + ### Interactive Command Approval When running in interactive mode, command approval requests now show hierarchical options: @@ -380,6 +340,58 @@ Selecting an "Always run" option will: This allows you to progressively build your auto-approval rules without manually editing the config file. +## Autonomous mode (Non-Interactive) + +Autonomous mode allows Kilo Code to run in automated environments like CI/CD pipelines without requiring user interaction. + +```bash +# Run in autonomous mode with a prompt +kilocode --auto "Implement feature X" + +# Run in autonomous mode with piped input +echo "Fix the bug in app.ts" | kilocode --auto + +# Run in autonomous mode with timeout (in seconds) +kilocode --auto "Run tests" --timeout 300 + +# Run in autonomous mode with JSON output for structured parsing +kilocode --auto --json "Implement feature X" +``` + +### Autonomous Mode Behavior + +When running in Autonomous mode (`--auto` flag): + +1. **No User Interaction**: All approval requests are handled automatically based on configuration +2. **Auto-Approval/Rejection**: Operations are approved or rejected based on your auto-approval settings +3. **Follow-up Questions**: Automatically responded with a message instructing the AI to make autonomous decisions +4. **Automatic Exit**: The CLI exits automatically when the task completes or times out + +### JSON Output Mode + +Use the `--json` flag with `--auto` to get structured JSON output instead of the default terminal UI. This is useful for programmatic integration and parsing of Kilo Code responses. + +```bash +# Standard autonomous mode with terminal UI +kilocode --auto "Fix the bug" + +# Autonomous mode with JSON output +kilocode --auto --json "Fix the bug" + +# With piped input +echo "Implement feature X" | kilocode --auto --json +``` + +**Requirements:** + +- The `--json` flag requires `--auto` mode to be enabled +- Output is sent to stdout as structured JSON for easy parsing +- Ideal for CI/CD pipelines and automated workflows + +### Auto-Approval in Autonomous Mode + +Autonomous mode respects your [auto-approval configuration](#auto-approval-settings). Operations which are not auto-approved will not be allowed. + ### Autonomous Mode Follow-up Questions In Autonomous mode, when the AI asks a follow-up question, it receives this response: From ace16c2a093f8b89709d0ab947d8f0f879dcfd5a Mon Sep 17 00:00:00 2001 From: Joshua Lambert <25085430+lambertjosh@users.noreply.github.com> Date: Thu, 4 Dec 2025 14:48:29 -0500 Subject: [PATCH 05/10] Changeset to patch Co-authored-by: Remon Oldenbeuving --- .changeset/every-knives-dig.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/every-knives-dig.md b/.changeset/every-knives-dig.md index 35e4af62974..6f718835233 100644 --- a/.changeset/every-knives-dig.md +++ b/.changeset/every-knives-dig.md @@ -1,5 +1,5 @@ --- -"@kilocode/cli": minor +"@kilocode/cli": patch --- Default read permissions now require approval for read operations outside the workspace From d27a57a358e72f4e2a909385cbf2dd631467f19b Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Wed, 17 Dec 2025 14:45:01 -0500 Subject: [PATCH 06/10] fix(cli): pass auto-approval settings from CLI config to extension The CLI was not blocking for approval on outside-workspace reads because: 1. Extension defaults alwaysAllowReadOnlyOutsideWorkspace to true when not set 2. CLI's mapConfigToExtensionState() was not mapping auto-approval settings 3. CLI has its own auto-approval config structure but never sent values to extension Changes: - cli/src/config/mapper.ts: Map all auto-approval settings from CLI config to extension state - cli/src/host/ExtensionHost.ts: Send auto-approval settings via updateSettings message - cli/src/config/__tests__/mapper.test.ts: Add 19 tests for auto-approval mapping --- cli/src/config/__tests__/mapper.test.ts | 306 ++++++++++++++++++++++++ cli/src/config/mapper.ts | 33 +++ cli/src/host/ExtensionHost.ts | 72 ++++++ 3 files changed, 411 insertions(+) create mode 100644 cli/src/config/__tests__/mapper.test.ts diff --git a/cli/src/config/__tests__/mapper.test.ts b/cli/src/config/__tests__/mapper.test.ts new file mode 100644 index 00000000000..b48a54b6376 --- /dev/null +++ b/cli/src/config/__tests__/mapper.test.ts @@ -0,0 +1,306 @@ +import { describe, it, expect } from "vitest" +import { mapConfigToExtensionState } from "../mapper.js" +import type { CLIConfig } from "../types.js" + +describe("mapConfigToExtensionState", () => { + const baseConfig: CLIConfig = { + version: "1.0.0", + mode: "code", + telemetry: true, + provider: "test-provider", + providers: [ + { + id: "test-provider", + provider: "anthropic", + apiKey: "test-key", + apiModelId: "claude-3-5-sonnet-20241022", + }, + ], + } + + describe("auto-approval settings mapping", () => { + it("should set all auto-approval settings to false when autoApproval.enabled is false", () => { + const config: CLIConfig = { + ...baseConfig, + autoApproval: { + enabled: false, + read: { enabled: true, outside: true }, + write: { enabled: true, outside: true, protected: true }, + }, + } + + const state = mapConfigToExtensionState(config) + + expect(state.autoApprovalEnabled).toBe(false) + expect(state.alwaysAllowReadOnly).toBe(false) + expect(state.alwaysAllowReadOnlyOutsideWorkspace).toBe(false) + expect(state.alwaysAllowWrite).toBe(false) + expect(state.alwaysAllowWriteOutsideWorkspace).toBe(false) + expect(state.alwaysAllowWriteProtected).toBe(false) + }) + + it("should set read settings correctly when autoApproval is enabled", () => { + const config: CLIConfig = { + ...baseConfig, + autoApproval: { + enabled: true, + read: { enabled: true, outside: false }, + }, + } + + const state = mapConfigToExtensionState(config) + + expect(state.autoApprovalEnabled).toBe(true) + expect(state.alwaysAllowReadOnly).toBe(true) + expect(state.alwaysAllowReadOnlyOutsideWorkspace).toBe(false) + }) + + it("should set alwaysAllowReadOnlyOutsideWorkspace to true only when all conditions are met", () => { + const config: CLIConfig = { + ...baseConfig, + autoApproval: { + enabled: true, + read: { enabled: true, outside: true }, + }, + } + + const state = mapConfigToExtensionState(config) + + expect(state.alwaysAllowReadOnlyOutsideWorkspace).toBe(true) + }) + + it("should set alwaysAllowReadOnlyOutsideWorkspace to false when read.enabled is false", () => { + const config: CLIConfig = { + ...baseConfig, + autoApproval: { + enabled: true, + read: { enabled: false, outside: true }, + }, + } + + const state = mapConfigToExtensionState(config) + + expect(state.alwaysAllowReadOnly).toBe(false) + expect(state.alwaysAllowReadOnlyOutsideWorkspace).toBe(false) + }) + + it("should set write settings correctly", () => { + const config: CLIConfig = { + ...baseConfig, + autoApproval: { + enabled: true, + write: { enabled: true, outside: true, protected: false }, + }, + } + + const state = mapConfigToExtensionState(config) + + expect(state.alwaysAllowWrite).toBe(true) + expect(state.alwaysAllowWriteOutsideWorkspace).toBe(true) + expect(state.alwaysAllowWriteProtected).toBe(false) + }) + + it("should set browser settings correctly", () => { + const config: CLIConfig = { + ...baseConfig, + autoApproval: { + enabled: true, + browser: { enabled: true }, + }, + } + + const state = mapConfigToExtensionState(config) + + expect(state.alwaysAllowBrowser).toBe(true) + }) + + it("should set execute settings correctly", () => { + const config: CLIConfig = { + ...baseConfig, + autoApproval: { + enabled: true, + execute: { + enabled: true, + allowed: ["npm", "git"], + denied: ["rm -rf"], + }, + }, + } + + const state = mapConfigToExtensionState(config) + + expect(state.alwaysAllowExecute).toBe(true) + expect(state.allowedCommands).toEqual(["npm", "git"]) + expect(state.deniedCommands).toEqual(["rm -rf"]) + }) + + it("should set MCP settings correctly", () => { + const config: CLIConfig = { + ...baseConfig, + autoApproval: { + enabled: true, + mcp: { enabled: true }, + }, + } + + const state = mapConfigToExtensionState(config) + + expect(state.alwaysAllowMcp).toBe(true) + }) + + it("should set mode switch settings correctly", () => { + const config: CLIConfig = { + ...baseConfig, + autoApproval: { + enabled: true, + mode: { enabled: true }, + }, + } + + const state = mapConfigToExtensionState(config) + + expect(state.alwaysAllowModeSwitch).toBe(true) + }) + + it("should set subtasks settings correctly", () => { + const config: CLIConfig = { + ...baseConfig, + autoApproval: { + enabled: true, + subtasks: { enabled: true }, + }, + } + + const state = mapConfigToExtensionState(config) + + expect(state.alwaysAllowSubtasks).toBe(true) + }) + + it("should set retry settings correctly", () => { + const config: CLIConfig = { + ...baseConfig, + autoApproval: { + enabled: true, + retry: { enabled: true, delay: 15 }, + }, + } + + const state = mapConfigToExtensionState(config) + + expect(state.alwaysApproveResubmit).toBe(true) + expect(state.requestDelaySeconds).toBe(15) + }) + + it("should set question settings correctly", () => { + const config: CLIConfig = { + ...baseConfig, + autoApproval: { + enabled: true, + question: { enabled: true, timeout: 30 }, + }, + } + + const state = mapConfigToExtensionState(config) + + expect(state.alwaysAllowFollowupQuestions).toBe(true) + expect(state.followupAutoApproveTimeoutMs).toBe(30000) // 30 seconds in ms + }) + + it("should set todo settings correctly", () => { + const config: CLIConfig = { + ...baseConfig, + autoApproval: { + enabled: true, + todo: { enabled: true }, + }, + } + + const state = mapConfigToExtensionState(config) + + expect(state.alwaysAllowUpdateTodoList).toBe(true) + }) + + it("should use default values when autoApproval is not provided", () => { + const config: CLIConfig = { + ...baseConfig, + } + + const state = mapConfigToExtensionState(config) + + expect(state.autoApprovalEnabled).toBe(false) + expect(state.alwaysAllowReadOnly).toBe(false) + expect(state.alwaysAllowReadOnlyOutsideWorkspace).toBe(false) + expect(state.alwaysAllowWrite).toBe(false) + expect(state.alwaysAllowWriteOutsideWorkspace).toBe(false) + expect(state.alwaysAllowWriteProtected).toBe(false) + expect(state.alwaysAllowBrowser).toBe(false) + expect(state.alwaysAllowMcp).toBe(false) + expect(state.alwaysAllowModeSwitch).toBe(false) + expect(state.alwaysAllowSubtasks).toBe(false) + expect(state.alwaysAllowExecute).toBe(false) + expect(state.allowedCommands).toEqual([]) + expect(state.deniedCommands).toEqual([]) + expect(state.alwaysAllowFollowupQuestions).toBe(false) + expect(state.alwaysAllowUpdateTodoList).toBe(false) + }) + }) + + describe("provider mapping", () => { + it("should map provider configuration correctly", () => { + const state = mapConfigToExtensionState(baseConfig) + + expect(state.apiConfiguration).toBeDefined() + expect(state.apiConfiguration?.apiProvider).toBe("anthropic") + expect(state.currentApiConfigName).toBe("test-provider") + }) + + it("should create listApiConfigMeta from providers", () => { + const state = mapConfigToExtensionState(baseConfig) + + expect(state.listApiConfigMeta).toHaveLength(1) + expect(state.listApiConfigMeta?.[0]).toEqual({ + id: "test-provider", + name: "test-provider", + apiProvider: "anthropic", + modelId: "claude-3-5-sonnet-20241022", + }) + }) + }) + + describe("telemetry mapping", () => { + it("should map telemetry enabled correctly", () => { + const config: CLIConfig = { + ...baseConfig, + telemetry: true, + } + + const state = mapConfigToExtensionState(config) + + expect(state.telemetrySetting).toBe("enabled") + }) + + it("should map telemetry disabled correctly", () => { + const config: CLIConfig = { + ...baseConfig, + telemetry: false, + } + + const state = mapConfigToExtensionState(config) + + expect(state.telemetrySetting).toBe("disabled") + }) + }) + + describe("mode mapping", () => { + it("should map mode correctly", () => { + const config: CLIConfig = { + ...baseConfig, + mode: "architect", + } + + const state = mapConfigToExtensionState(config) + + expect(state.mode).toBe("architect") + }) + }) +}) diff --git a/cli/src/config/mapper.ts b/cli/src/config/mapper.ts index 4dfc89b8c03..788d5032a4d 100644 --- a/cli/src/config/mapper.ts +++ b/cli/src/config/mapper.ts @@ -29,6 +29,12 @@ export function mapConfigToExtensionState( modelId: getModelIdForProvider(p), })) + // Map auto-approval settings from CLI config to extension state + // These settings control whether the extension auto-approves operations + // or asks the CLI for approval (which then prompts the user) + const autoApproval = config.autoApproval + const autoApprovalEnabled = autoApproval?.enabled ?? false + return { ...currentState, apiConfiguration, @@ -36,6 +42,33 @@ export function mapConfigToExtensionState( listApiConfigMeta, telemetrySetting: config.telemetry ? "enabled" : "disabled", mode: config.mode, + // Auto-approval settings - these control whether the extension auto-approves + // or defers to the CLI's approval flow + autoApprovalEnabled, + alwaysAllowReadOnly: autoApprovalEnabled && (autoApproval?.read?.enabled ?? false), + alwaysAllowReadOnlyOutsideWorkspace: + autoApprovalEnabled && (autoApproval?.read?.enabled ?? false) && (autoApproval?.read?.outside ?? false), + alwaysAllowWrite: autoApprovalEnabled && (autoApproval?.write?.enabled ?? false), + alwaysAllowWriteOutsideWorkspace: + autoApprovalEnabled && + (autoApproval?.write?.enabled ?? false) && + (autoApproval?.write?.outside ?? false), + alwaysAllowWriteProtected: + autoApprovalEnabled && + (autoApproval?.write?.enabled ?? false) && + (autoApproval?.write?.protected ?? false), + alwaysAllowBrowser: autoApprovalEnabled && (autoApproval?.browser?.enabled ?? false), + alwaysApproveResubmit: autoApprovalEnabled && (autoApproval?.retry?.enabled ?? false), + requestDelaySeconds: autoApproval?.retry?.delay ?? 10, + alwaysAllowMcp: autoApprovalEnabled && (autoApproval?.mcp?.enabled ?? false), + alwaysAllowModeSwitch: autoApprovalEnabled && (autoApproval?.mode?.enabled ?? false), + alwaysAllowSubtasks: autoApprovalEnabled && (autoApproval?.subtasks?.enabled ?? false), + alwaysAllowExecute: autoApprovalEnabled && (autoApproval?.execute?.enabled ?? false), + allowedCommands: autoApproval?.execute?.allowed ?? [], + deniedCommands: autoApproval?.execute?.denied ?? [], + alwaysAllowFollowupQuestions: autoApprovalEnabled && (autoApproval?.question?.enabled ?? false), + followupAutoApproveTimeoutMs: (autoApproval?.question?.timeout ?? 60) * 1000, + alwaysAllowUpdateTodoList: autoApprovalEnabled && (autoApproval?.todo?.enabled ?? false), } } catch (error) { logs.error("Failed to map config to extension state", "ConfigMapper", { error }) diff --git a/cli/src/host/ExtensionHost.ts b/cli/src/host/ExtensionHost.ts index 85c08c67c88..81ad448d97a 100644 --- a/cli/src/host/ExtensionHost.ts +++ b/cli/src/host/ExtensionHost.ts @@ -965,6 +965,78 @@ export class ExtensionHost extends EventEmitter { updatedSettings: { experiments }, }) } + + // Sync auto-approval settings to the extension + // These settings control whether the extension auto-approves operations + // or defers to the CLI's approval flow (which prompts the user) + const autoApprovalSettings: Record = {} + + // Only include settings that are explicitly set in configState + if (configState.autoApprovalEnabled !== undefined) { + autoApprovalSettings.autoApprovalEnabled = configState.autoApprovalEnabled + } + if (configState.alwaysAllowReadOnly !== undefined) { + autoApprovalSettings.alwaysAllowReadOnly = configState.alwaysAllowReadOnly + } + if (configState.alwaysAllowReadOnlyOutsideWorkspace !== undefined) { + autoApprovalSettings.alwaysAllowReadOnlyOutsideWorkspace = configState.alwaysAllowReadOnlyOutsideWorkspace + } + if (configState.alwaysAllowWrite !== undefined) { + autoApprovalSettings.alwaysAllowWrite = configState.alwaysAllowWrite + } + if (configState.alwaysAllowWriteOutsideWorkspace !== undefined) { + autoApprovalSettings.alwaysAllowWriteOutsideWorkspace = configState.alwaysAllowWriteOutsideWorkspace + } + if (configState.alwaysAllowWriteProtected !== undefined) { + autoApprovalSettings.alwaysAllowWriteProtected = configState.alwaysAllowWriteProtected + } + if (configState.alwaysAllowBrowser !== undefined) { + autoApprovalSettings.alwaysAllowBrowser = configState.alwaysAllowBrowser + } + if (configState.alwaysApproveResubmit !== undefined) { + autoApprovalSettings.alwaysApproveResubmit = configState.alwaysApproveResubmit + } + if (configState.requestDelaySeconds !== undefined) { + autoApprovalSettings.requestDelaySeconds = configState.requestDelaySeconds + } + if (configState.alwaysAllowMcp !== undefined) { + autoApprovalSettings.alwaysAllowMcp = configState.alwaysAllowMcp + } + if (configState.alwaysAllowModeSwitch !== undefined) { + autoApprovalSettings.alwaysAllowModeSwitch = configState.alwaysAllowModeSwitch + } + if (configState.alwaysAllowSubtasks !== undefined) { + autoApprovalSettings.alwaysAllowSubtasks = configState.alwaysAllowSubtasks + } + if (configState.alwaysAllowExecute !== undefined) { + autoApprovalSettings.alwaysAllowExecute = configState.alwaysAllowExecute + } + if (configState.allowedCommands !== undefined) { + autoApprovalSettings.allowedCommands = configState.allowedCommands + } + if (configState.deniedCommands !== undefined) { + autoApprovalSettings.deniedCommands = configState.deniedCommands + } + if (configState.alwaysAllowFollowupQuestions !== undefined) { + autoApprovalSettings.alwaysAllowFollowupQuestions = configState.alwaysAllowFollowupQuestions + } + if (configState.followupAutoApproveTimeoutMs !== undefined) { + autoApprovalSettings.followupAutoApproveTimeoutMs = configState.followupAutoApproveTimeoutMs + } + if (configState.alwaysAllowUpdateTodoList !== undefined) { + autoApprovalSettings.alwaysAllowUpdateTodoList = configState.alwaysAllowUpdateTodoList + } + + // Send auto-approval settings if any are present + if (Object.keys(autoApprovalSettings).length > 0) { + await this.sendWebviewMessage({ + type: "updateSettings", + updatedSettings: autoApprovalSettings, + }) + logs.debug("Auto-approval settings synchronized to extension", "ExtensionHost", { + settings: Object.keys(autoApprovalSettings), + }) + } } /** From 389a7eb322c7da6db739c779b7300072472483f3 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 18 Dec 2025 10:38:39 +0100 Subject: [PATCH 07/10] Add PostHog telemetry for Agent Manager (#4548) * Add PostHog telemetry for Agent Manage * Delete .husky/_/pre-push * Delete .husky/_/post-merge * Delete .husky/_/post-commit * Delete .husky/_/post-checkout * Fix provider tests --- packages/types/src/telemetry.ts | 2 + .../agent-manager/AgentManagerProvider.ts | 36 +++ .../agent-manager/CliProcessHandler.ts | 8 + .../__tests__/AgentManagerProvider.spec.ts | 1 + .../agent-manager/__tests__/telemetry.test.ts | 266 ++++++++++++++++++ src/core/kilocode/agent-manager/telemetry.ts | 39 ++- 6 files changed, 347 insertions(+), 5 deletions(-) create mode 100644 src/core/kilocode/agent-manager/__tests__/telemetry.test.ts diff --git a/packages/types/src/telemetry.ts b/packages/types/src/telemetry.ts index 8d684c0fc63..f8419ceadd1 100644 --- a/packages/types/src/telemetry.ts +++ b/packages/types/src/telemetry.ts @@ -56,6 +56,7 @@ export enum TelemetryEventName { AGENT_MANAGER_SESSION_COMPLETED = "Agent Manager Session Completed", AGENT_MANAGER_SESSION_STOPPED = "Agent Manager Session Stopped", AGENT_MANAGER_SESSION_ERROR = "Agent Manager Session Error", + AGENT_MANAGER_LOGIN_ISSUE = "Agent Manager Login Issue", // kilocode_change end TASK_CREATED = "Task Created", @@ -235,6 +236,7 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [ TelemetryEventName.AGENT_MANAGER_SESSION_COMPLETED, // kilocode_change TelemetryEventName.AGENT_MANAGER_SESSION_STOPPED, // kilocode_change TelemetryEventName.AGENT_MANAGER_SESSION_ERROR, // kilocode_change + TelemetryEventName.AGENT_MANAGER_LOGIN_ISSUE, // kilocode_change // kilocode_change end TelemetryEventName.TASK_CREATED, diff --git a/src/core/kilocode/agent-manager/AgentManagerProvider.ts b/src/core/kilocode/agent-manager/AgentManagerProvider.ts index 9d8016abfe2..5c5a26bf9c5 100644 --- a/src/core/kilocode/agent-manager/AgentManagerProvider.ts +++ b/src/core/kilocode/agent-manager/AgentManagerProvider.ts @@ -30,6 +30,8 @@ import { captureAgentManagerSessionCompleted, captureAgentManagerSessionStopped, captureAgentManagerSessionError, + captureAgentManagerLoginIssue, + getPlatformDiagnostics, } from "./telemetry" import type { ClineProvider } from "../../webview/ClineProvider" import { extractSessionConfigs, MAX_VERSION_COUNT } from "./multiVersionUtils" @@ -1156,6 +1158,10 @@ export class AgentManagerProvider implements vscode.Disposable { private showPaymentRequiredPrompt(payload?: KilocodePayload | { text?: string; content?: string }): void { const { title, message, buyCreditsUrl, rawText } = this.parsePaymentRequiredPayload(payload) + captureAgentManagerLoginIssue({ + issueType: "payment_required", + }) + const actionLabel = buyCreditsUrl ? "Open billing" : undefined const actions = actionLabel ? [actionLabel] : [] @@ -1169,6 +1175,14 @@ export class AgentManagerProvider implements vscode.Disposable { } private showCliNotFoundError(): void { + const hasNpm = canInstallCli((msg) => this.outputChannel.appendLine(`[AgentManager] ${msg}`)) + const { platform, shell } = getPlatformDiagnostics() + captureAgentManagerLoginIssue({ + issueType: "cli_not_found", + hasNpm, + platform, + shell, + }) this.showCliError({ type: "spawn_error", message: "CLI not found" }) } @@ -1234,6 +1248,11 @@ export class AgentManagerProvider implements vscode.Disposable { } private handleStartSessionApiFailure(error: { message?: string; authError?: boolean }): void { + captureAgentManagerLoginIssue({ + issueType: error.authError ? "auth_error" : "api_error", + httpStatusCode: error.authError ? 401 : undefined, + }) + const message = error.authError === true ? this.buildAuthReminderMessage(error.message || t("kilocode:agentManager.errors.sessionFailed")) @@ -1381,6 +1400,23 @@ export class AgentManagerProvider implements vscode.Disposable { private showCliError(error?: { type: "cli_outdated" | "spawn_error" | "unknown"; message: string }): void { const hasNpm = canInstallCli((msg) => this.outputChannel.appendLine(`[AgentManager] ${msg}`)) + const { platform, shell } = getPlatformDiagnostics() + if (error?.type === "cli_outdated") { + captureAgentManagerLoginIssue({ + issueType: "cli_outdated", + hasNpm, + platform, + shell, + }) + } else if (error?.type === "spawn_error" && error.message !== "CLI not found") { + captureAgentManagerLoginIssue({ + issueType: "cli_spawn_error", + hasNpm, + platform, + shell, + }) + } + switch (error?.type) { case "cli_outdated": if (hasNpm) { diff --git a/src/core/kilocode/agent-manager/CliProcessHandler.ts b/src/core/kilocode/agent-manager/CliProcessHandler.ts index 8ba02ef4778..b39089a172e 100644 --- a/src/core/kilocode/agent-manager/CliProcessHandler.ts +++ b/src/core/kilocode/agent-manager/CliProcessHandler.ts @@ -12,6 +12,7 @@ import { buildCliArgs } from "./CliArgsBuilder" import type { ClineMessage, ProviderSettings } from "@roo-code/types" import { extractApiReqFailedMessage, extractPayloadMessage } from "./askErrorParser" import { buildProviderEnvOverrides } from "./providerEnvMapper" +import { captureAgentManagerLoginIssue, getPlatformDiagnostics } from "./telemetry" /** * Timeout for pending sessions (ms) - if session_created event doesn't arrive within this time, @@ -406,6 +407,13 @@ export class CliProcessHandler { this.registry.clearPendingSession() this.pendingProcess = null + const { platform, shell } = getPlatformDiagnostics() + captureAgentManagerLoginIssue({ + issueType: "session_timeout", + platform, + shell, + }) + this.callbacks.onPendingSessionChanged(null) this.callbacks.onStartSessionFailed({ type: "unknown", diff --git a/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts b/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts index 12e3ada673f..7f7883ec21d 100644 --- a/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts +++ b/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts @@ -11,6 +11,7 @@ vi.mock("../telemetry", () => ({ captureAgentManagerSessionCompleted: vi.fn(), captureAgentManagerSessionStopped: vi.fn(), captureAgentManagerSessionError: vi.fn(), + captureAgentManagerLoginIssue: vi.fn(), })) let AgentManagerProvider: typeof import("../AgentManagerProvider").AgentManagerProvider diff --git a/src/core/kilocode/agent-manager/__tests__/telemetry.test.ts b/src/core/kilocode/agent-manager/__tests__/telemetry.test.ts new file mode 100644 index 00000000000..136621a5147 --- /dev/null +++ b/src/core/kilocode/agent-manager/__tests__/telemetry.test.ts @@ -0,0 +1,266 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { TelemetryEventName } from "@roo-code/types" +import { + captureAgentManagerOpened, + captureAgentManagerSessionStarted, + captureAgentManagerSessionCompleted, + captureAgentManagerSessionStopped, + captureAgentManagerSessionError, + captureAgentManagerLoginIssue, + getPlatformDiagnostics, + type AgentManagerLoginIssueProperties, +} from "../telemetry" + +// Mock the TelemetryService +vi.mock("@roo-code/telemetry", () => ({ + TelemetryService: { + hasInstance: vi.fn(), + instance: { + captureEvent: vi.fn(), + }, + }, +})) + +import { TelemetryService } from "@roo-code/telemetry" + +describe("Agent Manager Telemetry", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + vi.resetAllMocks() + }) + + describe("captureAgentManagerOpened", () => { + it("does not capture when TelemetryService has no instance", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(false) + + captureAgentManagerOpened() + + expect(TelemetryService.instance.captureEvent).not.toHaveBeenCalled() + }) + + it("captures event when TelemetryService has instance", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + + captureAgentManagerOpened() + + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith(TelemetryEventName.AGENT_MANAGER_OPENED) + }) + }) + + describe("captureAgentManagerSessionStarted", () => { + it("captures event with sessionId and useWorktree", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + + captureAgentManagerSessionStarted("session-123", true) + + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith( + TelemetryEventName.AGENT_MANAGER_SESSION_STARTED, + { sessionId: "session-123", useWorktree: true }, + ) + }) + }) + + describe("captureAgentManagerSessionCompleted", () => { + it("captures event with sessionId and useWorktree", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + + captureAgentManagerSessionCompleted("session-456", false) + + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith( + TelemetryEventName.AGENT_MANAGER_SESSION_COMPLETED, + { sessionId: "session-456", useWorktree: false }, + ) + }) + }) + + describe("captureAgentManagerSessionStopped", () => { + it("captures event with sessionId and useWorktree", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + + captureAgentManagerSessionStopped("session-789", true) + + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith( + TelemetryEventName.AGENT_MANAGER_SESSION_STOPPED, + { sessionId: "session-789", useWorktree: true }, + ) + }) + }) + + describe("captureAgentManagerSessionError", () => { + it("captures event with sessionId, useWorktree, and error", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + + captureAgentManagerSessionError("session-error", false, "Something went wrong") + + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith( + TelemetryEventName.AGENT_MANAGER_SESSION_ERROR, + { sessionId: "session-error", useWorktree: false, error: "Something went wrong" }, + ) + }) + + it("captures event without error message", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + + captureAgentManagerSessionError("session-error-2", true) + + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith( + TelemetryEventName.AGENT_MANAGER_SESSION_ERROR, + { sessionId: "session-error-2", useWorktree: true, error: undefined }, + ) + }) + }) + + describe("captureAgentManagerLoginIssue", () => { + it("does not capture when TelemetryService has no instance", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(false) + + captureAgentManagerLoginIssue({ issueType: "cli_not_found" }) + + expect(TelemetryService.instance.captureEvent).not.toHaveBeenCalled() + }) + + it("captures cli_not_found issue with hasNpm", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + + const props: AgentManagerLoginIssueProperties = { + issueType: "cli_not_found", + hasNpm: true, + } + + captureAgentManagerLoginIssue(props) + + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith( + TelemetryEventName.AGENT_MANAGER_LOGIN_ISSUE, + props, + ) + }) + + it("captures cli_outdated issue with hasNpm false", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + + const props: AgentManagerLoginIssueProperties = { + issueType: "cli_outdated", + hasNpm: false, + } + + captureAgentManagerLoginIssue(props) + + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith( + TelemetryEventName.AGENT_MANAGER_LOGIN_ISSUE, + props, + ) + }) + + it("captures auth_error issue with HTTP status code", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + + const props: AgentManagerLoginIssueProperties = { + issueType: "auth_error", + httpStatusCode: 401, + } + + captureAgentManagerLoginIssue(props) + + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith( + TelemetryEventName.AGENT_MANAGER_LOGIN_ISSUE, + props, + ) + }) + + it("captures payment_required issue", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + + const props: AgentManagerLoginIssueProperties = { + issueType: "payment_required", + } + + captureAgentManagerLoginIssue(props) + + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith( + TelemetryEventName.AGENT_MANAGER_LOGIN_ISSUE, + props, + ) + }) + + it("captures session_timeout issue", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + + const props: AgentManagerLoginIssueProperties = { + issueType: "session_timeout", + } + + captureAgentManagerLoginIssue(props) + + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith( + TelemetryEventName.AGENT_MANAGER_LOGIN_ISSUE, + props, + ) + }) + + it("captures api_error issue", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + + const props: AgentManagerLoginIssueProperties = { + issueType: "api_error", + } + + captureAgentManagerLoginIssue(props) + + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith( + TelemetryEventName.AGENT_MANAGER_LOGIN_ISSUE, + props, + ) + }) + + it("captures cli_spawn_error issue with hasNpm", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + + const props: AgentManagerLoginIssueProperties = { + issueType: "cli_spawn_error", + hasNpm: true, + } + + captureAgentManagerLoginIssue(props) + + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith( + TelemetryEventName.AGENT_MANAGER_LOGIN_ISSUE, + props, + ) + }) + + it("captures cli_not_found with platform and shell diagnostics", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + + const props: AgentManagerLoginIssueProperties = { + issueType: "cli_not_found", + hasNpm: true, + platform: "darwin", + shell: "zsh", + } + + captureAgentManagerLoginIssue(props) + + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith( + TelemetryEventName.AGENT_MANAGER_LOGIN_ISSUE, + props, + ) + }) + }) + + describe("getPlatformDiagnostics", () => { + it("returns platform and shell info", () => { + const diagnostics = getPlatformDiagnostics() + + expect(diagnostics.platform).toMatch(/^(darwin|win32|linux|other)$/) + // Shell may be undefined on Windows + if (diagnostics.shell) { + expect(typeof diagnostics.shell).toBe("string") + // Should be just the shell name, not a path + expect(diagnostics.shell).not.toContain("/") + } + }) + }) +}) diff --git a/src/core/kilocode/agent-manager/telemetry.ts b/src/core/kilocode/agent-manager/telemetry.ts index 6b78e993507..3d53774ab1a 100644 --- a/src/core/kilocode/agent-manager/telemetry.ts +++ b/src/core/kilocode/agent-manager/telemetry.ts @@ -1,11 +1,35 @@ +import * as path from "node:path" import { TelemetryService } from "@roo-code/telemetry" import { TelemetryEventName } from "@roo-code/types" -/** - * Agent Manager telemetry helpers. - * These functions encapsulate the TelemetryService.hasInstance() check - * and keep telemetry logic co-located with the agent-manager feature. - */ +export function getPlatformDiagnostics(): { platform: "darwin" | "win32" | "linux" | "other"; shell?: string } { + const platform = + process.platform === "darwin" || process.platform === "win32" || process.platform === "linux" + ? process.platform + : "other" + + const shellPath = process.env.SHELL + const shell = shellPath ? path.basename(shellPath) : undefined + + return { platform, shell } +} + +export type AgentManagerLoginIssueType = + | "cli_not_found" + | "cli_outdated" + | "cli_spawn_error" + | "auth_error" + | "payment_required" + | "api_error" + | "session_timeout" + +export interface AgentManagerLoginIssueProperties { + issueType: AgentManagerLoginIssueType + hasNpm?: boolean + httpStatusCode?: number + platform?: "darwin" | "win32" | "linux" | "other" + shell?: string +} export function captureAgentManagerOpened(): void { if (!TelemetryService.hasInstance()) return @@ -38,3 +62,8 @@ export function captureAgentManagerSessionError(sessionId: string, useWorktree: error, }) } + +export function captureAgentManagerLoginIssue(properties: AgentManagerLoginIssueProperties): void { + if (!TelemetryService.hasInstance()) return + TelemetryService.instance.captureEvent(TelemetryEventName.AGENT_MANAGER_LOGIN_ISSUE, properties) +} From 782347e9ed6cbaf42c88285cb8576801cd178d96 Mon Sep 17 00:00:00 2001 From: Kevin van Dijk Date: Thu, 18 Dec 2025 15:05:09 +0100 Subject: [PATCH 08/10] Add markers and changeset --- .changeset/all-things-cough.md | 5 +++++ packages/types/src/providers/zai.ts | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 .changeset/all-things-cough.md diff --git a/.changeset/all-things-cough.md b/.changeset/all-things-cough.md new file mode 100644 index 00000000000..269427d0ab5 --- /dev/null +++ b/.changeset/all-things-cough.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Add GLM-4.6V model support for z.ai provider diff --git a/packages/types/src/providers/zai.ts b/packages/types/src/providers/zai.ts index d597e7b1abf..9720d663390 100644 --- a/packages/types/src/providers/zai.ts +++ b/packages/types/src/providers/zai.ts @@ -100,6 +100,7 @@ export const internationalZAiModels = { description: "GLM-4.6 is Zhipu's newest model with an extended context window of up to 200k tokens, providing enhanced capabilities for processing longer documents and conversations.", }, + // kilocode_change start "glm-4.6v": { maxTokens: 98_304, contextWindow: 131_072, @@ -126,6 +127,7 @@ export const internationalZAiModels = { description: "GLM-4.6V-Flash is a free, high-speed multimodal model with visual reasoning capabilities, excellent for reasoning, coding, and agentic tasks.", }, + // kilocode_change end "glm-4-32b-0414-128k": { maxTokens: 98_304, contextWindow: 131_072, From 0c9fea4adc8f9dd2b66f858a6a21af2889eba0c2 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 18 Dec 2025 15:35:45 +0100 Subject: [PATCH 09/10] Fix agent manager cli spawning on Windows (#4549) * Add failing windows tests * Make tests fail * Make tests green --- .../agent-manager/CliProcessHandler.ts | 4 +- .../__tests__/CliProcessHandler.spec.ts | 110 ++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/core/kilocode/agent-manager/CliProcessHandler.ts b/src/core/kilocode/agent-manager/CliProcessHandler.ts index b39089a172e..d30feadb7d7 100644 --- a/src/core/kilocode/agent-manager/CliProcessHandler.ts +++ b/src/core/kilocode/agent-manager/CliProcessHandler.ts @@ -159,11 +159,13 @@ export class CliProcessHandler { const env = this.buildEnvWithApiConfiguration(options?.apiConfiguration) // Spawn CLI process + // On Windows, .cmd files are batch scripts that require shell execution + const needsShell = process.platform === "win32" && cliPath.toLowerCase().endsWith(".cmd") const proc = spawn(cliPath, cliArgs, { cwd: workspace, stdio: ["pipe", "pipe", "pipe"], env, - shell: false, + shell: needsShell, }) if (proc.pid) { diff --git a/src/core/kilocode/agent-manager/__tests__/CliProcessHandler.spec.ts b/src/core/kilocode/agent-manager/__tests__/CliProcessHandler.spec.ts index 626e5ffbcb5..204c1e3ed82 100644 --- a/src/core/kilocode/agent-manager/__tests__/CliProcessHandler.spec.ts +++ b/src/core/kilocode/agent-manager/__tests__/CliProcessHandler.spec.ts @@ -316,6 +316,116 @@ describe("CliProcessHandler", () => { if (previousProviderType === undefined) delete process.env.KILO_PROVIDER_TYPE else process.env.KILO_PROVIDER_TYPE = previousProviderType }) + + describe("Windows .cmd file handling", () => { + it("uses shell: true for .cmd files on Windows", () => { + const originalPlatform = process.platform + Object.defineProperty(process, "platform", { value: "win32", configurable: true }) + + try { + const onCliEvent = vi.fn() + handler.spawnProcess( + "C:\\Users\\test\\.kilocode\\cli\\pkg\\node_modules\\.bin\\kilocode.cmd", + "/workspace", + "test prompt", + undefined, + onCliEvent, + ) + + expect(spawnMock).toHaveBeenCalledWith( + "C:\\Users\\test\\.kilocode\\cli\\pkg\\node_modules\\.bin\\kilocode.cmd", + expect.any(Array), + expect.objectContaining({ shell: true }), + ) + } finally { + Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true }) + } + }) + + it("uses shell: true for .CMD files (case insensitive) on Windows", () => { + const originalPlatform = process.platform + Object.defineProperty(process, "platform", { value: "win32", configurable: true }) + + try { + const onCliEvent = vi.fn() + handler.spawnProcess( + "C:\\Users\\test\\kilocode.CMD", + "/workspace", + "test prompt", + undefined, + onCliEvent, + ) + + expect(spawnMock).toHaveBeenCalledWith( + "C:\\Users\\test\\kilocode.CMD", + expect.any(Array), + expect.objectContaining({ shell: true }), + ) + } finally { + Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true }) + } + }) + + it("uses shell: false for non-.cmd executables on Windows", () => { + const originalPlatform = process.platform + Object.defineProperty(process, "platform", { value: "win32", configurable: true }) + + try { + const onCliEvent = vi.fn() + handler.spawnProcess( + "C:\\Users\\test\\kilocode.exe", + "/workspace", + "test prompt", + undefined, + onCliEvent, + ) + + expect(spawnMock).toHaveBeenCalledWith( + "C:\\Users\\test\\kilocode.exe", + expect.any(Array), + expect.objectContaining({ shell: false }), + ) + } finally { + Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true }) + } + }) + + it("uses shell: false on macOS regardless of extension", () => { + const originalPlatform = process.platform + Object.defineProperty(process, "platform", { value: "darwin", configurable: true }) + + try { + const onCliEvent = vi.fn() + handler.spawnProcess("/usr/local/bin/kilocode", "/workspace", "test prompt", undefined, onCliEvent) + + expect(spawnMock).toHaveBeenCalledWith( + "/usr/local/bin/kilocode", + expect.any(Array), + expect.objectContaining({ shell: false }), + ) + } finally { + Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true }) + } + }) + + it("uses shell: false on Linux regardless of extension", () => { + const originalPlatform = process.platform + Object.defineProperty(process, "platform", { value: "linux", configurable: true }) + + try { + const onCliEvent = vi.fn() + handler.spawnProcess("/usr/bin/kilocode", "/workspace", "test prompt", undefined, onCliEvent) + + expect(spawnMock).toHaveBeenCalledWith( + "/usr/bin/kilocode", + expect.any(Array), + expect.objectContaining({ shell: false }), + ) + } finally { + Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true }) + } + }) + }) }) describe("session_created event handling", () => { From ded955536ec56845b4e6ef3fa96bc9a6dbc3ad5d Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 18 Dec 2025 15:35:55 +0100 Subject: [PATCH 10/10] Fix local installation (#4556) --- .../agent-manager/AgentManagerProvider.ts | 34 +++++----- .../agent-manager/CliProcessHandler.ts | 4 ++ .../__tests__/AgentManagerProvider.spec.ts | 64 +++++++++++++++++++ 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/src/core/kilocode/agent-manager/AgentManagerProvider.ts b/src/core/kilocode/agent-manager/AgentManagerProvider.ts index 5c5a26bf9c5..fd130245965 100644 --- a/src/core/kilocode/agent-manager/AgentManagerProvider.ts +++ b/src/core/kilocode/agent-manager/AgentManagerProvider.ts @@ -1337,34 +1337,38 @@ export class AgentManagerProvider implements vscode.Disposable { // Determine the shell config file based on the shell let configFile = "~/.bashrc" let pathCommand = `grep -qxF '${exportLine}' ${configFile} || echo '${exportLine}' >> ${configFile}` - let sourceCommand = `source ${configFile}` if (shellName === "zsh") { configFile = "~/.zshrc" pathCommand = `grep -qxF '${exportLine}' ${configFile} || echo '${exportLine}' >> ${configFile}` - sourceCommand = `source ${configFile}` } else if (shellName === "fish") { // Fish uses a different syntax for PATH configFile = "~/.config/fish/config.fish" const fishPathLine = `fish_add_path ${binDir}` pathCommand = `grep -qxF '${fishPathLine}' ${configFile} || echo '${fishPathLine}' >> ${configFile}` - sourceCommand = `source ${configFile}` } - const commands = [ - "clear", + // Note: We don't source the config file here because: + // 1. It can cause infinite loops if the config triggers terminal re-execution + // 2. The user will get the PATH update on their next terminal session + // 3. We provide the full path as an alternative for immediate use + // + // We avoid using 'clear' as it can cause issues with some shells. + // All commands are sent in a single sendText() call to ensure proper sequencing. + const fullCommand = [ getLocalCliInstallCommand(), - 'echo ""', - 'echo "✓ CLI installed locally"', - 'echo ""', pathCommand, - sourceCommand, - `echo "Added ${binDir} to PATH and reloaded config"`, - 'echo ""', - "echo \"Next step: Run 'kilocode auth' to authenticate\"", - "echo \"Alternatively, run '~/.kilocode/cli/pkg/node_modules/.bin/kilocode auth' to authenticate if not in PATH\"", - ] - terminal.sendText(commands.join(" ; ")) + `echo ""`, + `echo "✓ CLI installed locally"`, + `echo "Added ${binDir} to PATH in ${configFile}"`, + `echo ""`, + `echo "Next step: Run 'kilocode auth' to authenticate"`, + `echo "Or use the full path: ${binDir}/kilocode auth"`, + `echo ""`, + `echo "Note: Open a new terminal for PATH changes to take effect"`, + ].join(" && ") + + terminal.sendText(fullCommand) } } diff --git a/src/core/kilocode/agent-manager/CliProcessHandler.ts b/src/core/kilocode/agent-manager/CliProcessHandler.ts index d30feadb7d7..12d6145b23d 100644 --- a/src/core/kilocode/agent-manager/CliProcessHandler.ts +++ b/src/core/kilocode/agent-manager/CliProcessHandler.ts @@ -158,6 +158,10 @@ export class CliProcessHandler { const env = this.buildEnvWithApiConfiguration(options?.apiConfiguration) + // On Windows, .cmd files need to be executed through cmd.exe (shell: true) + // Without this, spawn() fails silently because .cmd files are batch scripts + const needsShell = process.platform === "win32" && cliPath.toLowerCase().endsWith(".cmd") + // Spawn CLI process // On Windows, .cmd files are batch scripts that require shell execution const needsShell = process.platform === "win32" && cliPath.toLowerCase().endsWith(".cmd") diff --git a/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts b/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts index 7f7883ec21d..2ae8d7c7cb7 100644 --- a/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts +++ b/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts @@ -89,6 +89,70 @@ describe("AgentManagerProvider CLI spawning", () => { expect(options?.shell).not.toBe(true) }) + it("spawns with shell: true on Windows when CLI path ends with .cmd", async () => { + // Reset modules to set up Windows-specific mock + vi.resetModules() + + const mockWorkspaceFolder = { uri: { fsPath: "/tmp/workspace" } } + const mockProvider = { + getState: vi.fn().mockResolvedValue({ apiConfiguration: { apiProvider: "kilocode" } }), + } + + vi.doMock("vscode", () => ({ + workspace: { workspaceFolders: [mockWorkspaceFolder] }, + window: { showErrorMessage: vi.fn(), showWarningMessage: vi.fn(), ViewColumn: { One: 1 } }, + env: { openExternal: vi.fn() }, + Uri: { parse: vi.fn(), joinPath: vi.fn() }, + ViewColumn: { One: 1 }, + ExtensionMode: { Development: 1, Production: 2, Test: 3 }, + })) + + vi.doMock("../../../../utils/fs", () => ({ + fileExistsAtPath: vi.fn().mockResolvedValue(false), + })) + + vi.doMock("../../../../services/code-index/managed/git-utils", () => ({ + getRemoteUrl: vi.fn().mockResolvedValue(undefined), + })) + + class TestProc extends EventEmitter { + stdout = new EventEmitter() + stderr = new EventEmitter() + kill = vi.fn() + pid = 1234 + } + + const spawnMock = vi.fn(() => new TestProc()) + // Return a .cmd path to simulate Windows local CLI installation + const execSyncMock = vi.fn(() => "C:\\Users\\test\\.kilocode\\cli\\pkg\\node_modules\\.bin\\kilocode.cmd") + + vi.doMock("node:child_process", () => ({ + spawn: spawnMock, + execSync: execSyncMock, + })) + + // Mock process.platform to be win32 + const originalPlatform = process.platform + Object.defineProperty(process, "platform", { value: "win32", writable: true }) + + try { + const module = await import("../AgentManagerProvider") + const windowsProvider = new module.AgentManagerProvider(mockContext, mockOutputChannel, mockProvider as any) + + await (windowsProvider as any).startAgentSession("test windows cmd") + + expect(spawnMock).toHaveBeenCalledTimes(1) + const [cmd, , options] = spawnMock.mock.calls[0] as unknown as [string, string[], Record] + expect(cmd.toLowerCase()).toContain(".cmd") + expect(options?.shell).toBe(true) + + windowsProvider.dispose() + } finally { + // Restore original platform + Object.defineProperty(process, "platform", { value: originalPlatform, writable: true }) + } + }) + it("creates pending session and waits for session_created event", async () => { await (provider as any).startAgentSession("test pending")