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/.changeset/every-knives-dig.md b/.changeset/every-knives-dig.md new file mode 100644 index 00000000000..6f718835233 --- /dev/null +++ b/.changeset/every-knives-dig.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Default read permissions now require approval for read operations outside the workspace 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: 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/__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/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, 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), + }) + } } /** diff --git a/packages/types/src/providers/zai.ts b/packages/types/src/providers/zai.ts index e21fcc698bf..9720d663390 100644 --- a/packages/types/src/providers/zai.ts +++ b/packages/types/src/providers/zai.ts @@ -100,6 +100,34 @@ 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, + 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.", + }, + // kilocode_change end "glm-4-32b-0414-128k": { maxTokens: 98_304, contextWindow: 131_072, 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..fd130245965 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")) @@ -1318,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) } } @@ -1381,6 +1404,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..12d6145b23d 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, @@ -157,12 +158,18 @@ 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") const proc = spawn(cliPath, cliArgs, { cwd: workspace, stdio: ["pipe", "pipe", "pipe"], env, - shell: false, + shell: needsShell, }) if (proc.pid) { @@ -406,6 +413,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..2ae8d7c7cb7 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 @@ -88,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") 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", () => { 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) +}