Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/salty-shirts-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Fix Agent Manager not showing error when CLI is misconfigured. When the CLI exits with a configuration error (e.g., missing kilocodeToken), the extension now detects this and shows an error popup with options to run `kilocode auth` or `kilocode config`.
45 changes: 43 additions & 2 deletions src/core/kilocode/agent-manager/AgentManagerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1244,7 +1244,24 @@ export class AgentManagerProvider implements vscode.Disposable {
return
}
terminal.show()
terminal.sendText("kilocode auth")
// Use resolved CLI path if available, fallback to "kilocode" for PATH lookup
void this.sessionLauncher.getPrewarmedCliPath().then((cliPath) => {
const cmd = cliPath ? `"${cliPath}" auth` : "kilocode auth"
terminal.sendText(cmd)
})
}

private runConfigureInTerminal(): void {
const terminal = this.createCliTerminal("Kilocode CLI Config")
if (!terminal) {
return
}
terminal.show()
// Use resolved CLI path if available, fallback to "kilocode" for PATH lookup
void this.sessionLauncher.getPrewarmedCliPath().then((cliPath) => {
const cmd = cliPath ? `"${cliPath}" config` : "kilocode config"
terminal.sendText(cmd)
})
}

private showCliAuthReminder(message?: string): void {
Expand Down Expand Up @@ -1418,7 +1435,10 @@ export class AgentManagerProvider implements vscode.Disposable {
terminal.sendText(commands.join(" && "))
}

private showCliError(error?: { type: "cli_outdated" | "spawn_error" | "unknown"; message: string }): void {
private showCliError(error?: {
type: "cli_outdated" | "spawn_error" | "unknown" | "cli_configuration_error"
message: string
}): void {
const hasNpm = canInstallCli((msg) => this.outputChannel.appendLine(`[AgentManager] ${msg}`))

const { platform, shell } = getPlatformDiagnostics()
Expand All @@ -1436,6 +1456,12 @@ export class AgentManagerProvider implements vscode.Disposable {
platform,
shell,
})
} else if (error?.type === "cli_configuration_error") {
captureAgentManagerLoginIssue({
issueType: "cli_configuration_error",
platform,
shell,
})
}

switch (error?.type) {
Expand Down Expand Up @@ -1508,6 +1534,21 @@ export class AgentManagerProvider implements vscode.Disposable {
}
break
}
case "cli_configuration_error": {
// CLI is installed but misconfigured (e.g., missing kilocodeToken)
// Offer to configure via terminal
const configureLabel = t("kilocode:agentManager.actions.configureCli")
const authLabel = t("kilocode:agentManager.actions.loginCli")
const errorMessage = t("kilocode:agentManager.errors.cliMisconfigured")
void vscode.window.showErrorMessage(errorMessage, authLabel, configureLabel).then((selection) => {
if (selection === authLabel) {
this.runAuthInTerminal()
} else if (selection === configureLabel) {
this.runConfigureInTerminal()
}
})
break
}
default: {
const errorMessage = error?.message
? t("kilocode:agentManager.errors.sessionFailedWithMessage", { message: error.message })
Expand Down
6 changes: 5 additions & 1 deletion src/core/kilocode/agent-manager/CliOutputParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ export interface WelcomeStreamEvent {
streamEventType: "welcome"
worktreeBranch?: string
timestamp: number
/** Configuration error instructions from CLI (indicates misconfigured CLI) */
instructions?: string[]
}

export type StreamEvent =
Expand Down Expand Up @@ -223,14 +225,16 @@ function toStreamEvent(parsed: Record<string, unknown>): StreamEvent | null {
}
}

// Detect welcome event from CLI (format: { type: "welcome", metadata: { welcomeOptions: { worktreeBranch: "..." } }, ... })
// Detect welcome event from CLI (format: { type: "welcome", metadata: { welcomeOptions: { worktreeBranch: "...", instructions: [...] } }, ... })
if (parsed.type === "welcome") {
const metadata = parsed.metadata as Record<string, unknown> | undefined
const welcomeOptions = metadata?.welcomeOptions as Record<string, unknown> | undefined
const instructions = welcomeOptions?.instructions as string[] | undefined
return {
streamEventType: "welcome",
worktreeBranch: welcomeOptions?.worktreeBranch as string | undefined,
timestamp: (parsed.timestamp as number) || Date.now(),
instructions: Array.isArray(instructions) && instructions.length > 0 ? instructions : undefined,
}
}

Expand Down
124 changes: 122 additions & 2 deletions src/core/kilocode/agent-manager/CliProcessHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ import { captureAgentManagerLoginIssue, getPlatformDiagnostics } from "./telemet
*/
const PENDING_SESSION_TIMEOUT_MS = 30_000

/**
* Maximum size for stdout buffer (bytes) - prevents memory issues when buffering output
* before session_created. We only need enough to detect configuration errors.
*/
const MAX_STDOUT_BUFFER_SIZE = 64 * 1024

/**
* Tracks a pending session while waiting for CLI's session_created event.
* Note: This is only used for NEW sessions. Resume sessions go directly to activeSessions.
Expand All @@ -37,10 +43,12 @@ interface PendingProcessInfo {
sawApiReqStarted?: boolean // Track if api_req_started arrived before session_created
gitUrl?: string
stderrBuffer: string[] // Capture stderr for error detection
stdoutBuffer: string[] // Capture raw stdout for configuration error detection when JSON is truncated
timeoutId?: NodeJS.Timeout // Timer for auto-failing stuck pending sessions
hadShellPath?: boolean // Track if shell PATH was used (for telemetry)
cliPath?: string // CLI path for error telemetry
provisionalSessionId?: string // Temporary session ID created when api_req_started arrives (before session_created)
configurationError?: string // Captured from welcome event instructions (indicates misconfigured CLI)
}

interface ActiveProcessInfo {
Expand All @@ -56,7 +64,7 @@ export interface CliProcessHandlerCallbacks {
onPendingSessionChanged: (pendingSession: { prompt: string; label: string; startTime: number } | null) => void
onStartSessionFailed: (
error?:
| { type: "cli_outdated" | "spawn_error" | "unknown"; message: string }
| { type: "cli_outdated" | "spawn_error" | "unknown" | "cli_configuration_error"; message: string }
| { type: "api_req_failed"; message: string; payload?: KilocodePayload; authError?: boolean }
| { type: "payment_required"; message: string; payload?: KilocodePayload },
) => void
Expand All @@ -81,6 +89,14 @@ export class CliProcessHandler {
this.callbacks.onDebugLog?.(message)
}

/** Extract configuration error from welcome event if present */
private extractConfigErrorFromWelcome(welcomeEvent: WelcomeStreamEvent): string | undefined {
if (welcomeEvent.instructions && welcomeEvent.instructions.length > 0) {
return welcomeEvent.instructions.join("\n")
}
return undefined
}

/** Clear the pending session timeout if it exists */
private clearPendingTimeout(): void {
if (this.pendingProcess?.timeoutId) {
Expand Down Expand Up @@ -223,6 +239,7 @@ export class CliProcessHandler {
desiredLabel: options?.label,
gitUrl: options?.gitUrl,
stderrBuffer: [],
stdoutBuffer: [],
timeoutId: setTimeout(() => this.handlePendingTimeout(), PENDING_SESSION_TIMEOUT_MS),
hadShellPath: !!options?.shellPath, // Track for telemetry
cliPath,
Expand All @@ -234,6 +251,13 @@ export class CliProcessHandler {
const chunkStr = chunk.toString()
this.debugLog(`stdout chunk (${chunkStr.length} bytes): ${chunkStr.slice(0, 200)}`)

// Capture raw stdout for configuration error detection (in case JSON is truncated)
// Cap buffer size to prevent memory issues
if (this.pendingProcess && this.pendingProcess.process === proc) {
this.pendingProcess.stdoutBuffer.push(chunkStr)
this.capStdoutBuffer()
}

const { events } = parser.parse(chunkStr)

for (const event of events) {
Expand Down Expand Up @@ -373,13 +397,18 @@ export class CliProcessHandler {

// If this is the pending process, handle specially
if (this.pendingProcess && this.pendingProcess.process === proc) {
// Capture worktree branch from welcome event (arrives before session_created)
// Capture worktree branch and configuration errors from welcome event (arrives before session_created)
if (event.streamEventType === "welcome") {
const welcomeEvent = event as WelcomeStreamEvent
if (welcomeEvent.worktreeBranch) {
this.pendingProcess.worktreeBranch = welcomeEvent.worktreeBranch
this.debugLog(`Captured worktree branch from welcome: ${welcomeEvent.worktreeBranch}`)
}
const configError = this.extractConfigErrorFromWelcome(welcomeEvent)
if (configError) {
this.pendingProcess.configurationError = configError
this.debugLog(`Captured CLI configuration error: ${configError}`)
}
return
}
// Handle kilocode events during pending state
Expand Down Expand Up @@ -621,15 +650,61 @@ export class CliProcessHandler {
): void {
if (this.pendingProcess && this.pendingProcess.process === proc) {
this.clearPendingTimeout()

// Flush any buffered parser output before checking for errors
// This is important because the welcome event JSON might be split across chunks
const { events } = this.pendingProcess.parser.flush()
for (const event of events) {
// Process welcome events to capture configuration errors
if (event.streamEventType === "welcome") {
const configError = this.extractConfigErrorFromWelcome(event as WelcomeStreamEvent)
if (configError) {
this.pendingProcess.configurationError = configError
this.debugLog(`Captured CLI configuration error from flush: ${configError}`)
}
}
}

// Extract configuration error to local variable before clearing pendingProcess
let configurationError = this.pendingProcess.configurationError

// Fallback: Check raw stdout for configuration error patterns if JSON parsing didn't capture it
// This handles cases where the CLI sends truncated JSON before exiting
if (!configurationError) {
const rawStdout = this.pendingProcess.stdoutBuffer.join("")
configurationError = this.detectConfigurationErrorFromRawOutput(rawStdout)
if (configurationError) {
this.debugLog(`Captured CLI configuration error from raw output: ${configurationError}`)
}
}

const stderrOutput = this.pendingProcess.stderrBuffer.join("\n")
this.registry.clearPendingSession()
this.callbacks.onPendingSessionChanged(null)
this.pendingProcess = null

// Check for CLI configuration error (e.g., missing kilocodeToken)
// CLI may exit with code 0 when showing configuration error instructions
if (configurationError) {
this.callbacks.onStartSessionFailed({
type: "cli_configuration_error",
message: configurationError,
})
this.callbacks.onStateChanged()
return
}

if (code !== 0) {
// Detect CLI version/compatibility issues from stderr
const errorInfo = this.detectCliError(stderrOutput, code)
this.callbacks.onStartSessionFailed(errorInfo)
} else {
// Generic fallback: CLI exited with code 0 before session_created
// This ensures the user never gets "nothing happened"
this.callbacks.onStartSessionFailed({
type: "unknown",
message: stderrOutput || "CLI exited before creating a session",
})
}
this.callbacks.onStateChanged()
return
Expand Down Expand Up @@ -814,4 +889,49 @@ export class CliProcessHandler {
message: stderrOutput || "Unknown error",
}
}

/**
* Cap the stdout buffer size to prevent memory issues.
* Keeps the most recent data up to MAX_STDOUT_BUFFER_SIZE.
*/
private capStdoutBuffer(): void {
if (!this.pendingProcess) {
return
}

const buffer = this.pendingProcess.stdoutBuffer
const totalSize = buffer.reduce((sum, chunk) => sum + chunk.length, 0)

if (totalSize > MAX_STDOUT_BUFFER_SIZE) {
// Join, trim from the start, and replace buffer with single trimmed string
const joined = buffer.join("")
const trimmed = joined.slice(joined.length - MAX_STDOUT_BUFFER_SIZE)
this.pendingProcess.stdoutBuffer = [trimmed]
}
}

/**
* Detect configuration errors from raw stdout output.
* This is a fallback for when the CLI sends truncated JSON that can't be parsed.
* Looks for patterns like "Configuration Error" or "instructions" containing error text.
*/
private detectConfigurationErrorFromRawOutput(rawOutput: string): string | undefined {
// Look for "Configuration Error" pattern in the raw output
// The CLI outputs this when config.json is incomplete or invalid
if (rawOutput.includes('"instructions":') && rawOutput.includes("Configuration Error")) {
// Return a generic configuration error message since we can't parse the full details
return "CLI configuration is incomplete or invalid. Please run 'kilocode config' or 'kilocode auth' to configure."
}

// Also check for common configuration error indicators
if (
rawOutput.includes("kilocodeToken is required") ||
rawOutput.includes("config.json is incomplete") ||
rawOutput.includes("apiKey is required")
) {
return "CLI configuration is incomplete or invalid. Please run 'kilocode config' or 'kilocode auth' to configure."
}

return undefined
}
}
8 changes: 8 additions & 0 deletions src/core/kilocode/agent-manager/CliSessionLauncher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ export class CliSessionLauncher {
return this.gitUrlPromise ?? undefined
}

/**
* Get the pre-warmed CLI path, or null if not available.
* This is useful for terminal commands that need the resolved CLI path.
*/
public async getPrewarmedCliPath(): Promise<string | null> {
return this.cliPathPromise ?? null
}

/**
* Spawn a CLI process with all the standard setup.
* Handles CLI path lookup, git URL resolution, API config, and event callback wiring.
Expand Down
28 changes: 28 additions & 0 deletions src/core/kilocode/agent-manager/__tests__/CliOutputParser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ describe("parseCliChunk", () => {
streamEventType: "welcome",
worktreeBranch: "feature/test-branch",
timestamp: 1234567890,
instructions: undefined,
})
})

Expand All @@ -92,6 +93,33 @@ describe("parseCliChunk", () => {
streamEventType: "welcome",
worktreeBranch: undefined,
timestamp: 1234567890,
instructions: undefined,
})
})

it("should parse welcome event with configuration error instructions", () => {
const result = parseCliChunk(
'{"type":"welcome","metadata":{"welcomeOptions":{"instructions":["Configuration Error: config.json is incomplete","kilocodeToken is required"]}},"timestamp":1234567890}\n',
)
expect(result.events).toHaveLength(1)
expect(result.events[0]).toEqual({
streamEventType: "welcome",
worktreeBranch: undefined,
timestamp: 1234567890,
instructions: ["Configuration Error: config.json is incomplete", "kilocodeToken is required"],
})
})

it("should not include instructions when array is empty", () => {
const result = parseCliChunk(
'{"type":"welcome","metadata":{"welcomeOptions":{"instructions":[]}},"timestamp":1234567890}\n',
)
expect(result.events).toHaveLength(1)
expect(result.events[0]).toEqual({
streamEventType: "welcome",
worktreeBranch: undefined,
timestamp: 1234567890,
instructions: undefined,
})
})

Expand Down
Loading
Loading