diff --git a/.changeset/fix-macos-cli-spawn-path.md b/.changeset/fix-macos-cli-spawn-path.md new file mode 100644 index 00000000000..54831b3af76 --- /dev/null +++ b/.changeset/fix-macos-cli-spawn-path.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix Agent Manager failing to start on macOS when launched from Finder/Spotlight diff --git a/.changeset/hungry-lands-sin.md b/.changeset/hungry-lands-sin.md new file mode 100644 index 00000000000..65a68b98412 --- /dev/null +++ b/.changeset/hungry-lands-sin.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Reduce the incidence of read_file errors when using Claude models. diff --git a/.changeset/petite-moose-wish.md b/.changeset/petite-moose-wish.md new file mode 100644 index 00000000000..c1248aad842 --- /dev/null +++ b/.changeset/petite-moose-wish.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix duplictate tool call processing in Chutes, DeepInfra, LiteLLM and xAI providers. diff --git a/.changeset/salty-shirts-guess.md b/.changeset/salty-shirts-guess.md new file mode 100644 index 00000000000..ffd0def6b01 --- /dev/null +++ b/.changeset/salty-shirts-guess.md @@ -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`. diff --git a/.changeset/session-title-generated-event.md b/.changeset/session-title-generated-event.md new file mode 100644 index 00000000000..ae53250d344 --- /dev/null +++ b/.changeset/session-title-generated-event.md @@ -0,0 +1,6 @@ +--- +"kilo-code": patch +"@kilocode/cli": patch +--- + +feat: add session_title_generated event emission to CLI diff --git a/.changeset/vast-results-cheat.md b/.changeset/vast-results-cheat.md new file mode 100644 index 00000000000..8397044b814 --- /dev/null +++ b/.changeset/vast-results-cheat.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": minor +--- + +Add markdown theming support for Reasoning box content diff --git a/cli/src/cli.ts b/cli/src/cli.ts index 1fcadd94d71..0e343b8f160 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -169,6 +169,11 @@ export class CLI { console.log(JSON.stringify(message)) } }, + onSessionTitleGenerated: (message) => { + if (this.options.json) { + console.log(JSON.stringify(message)) + } + }, platform: "cli", getOrganizationId: async () => { const state = this.service?.getState() diff --git a/cli/src/constants/themes/alpha.ts b/cli/src/constants/themes/alpha.ts index 4032d526c59..72190c5eef9 100644 --- a/cli/src/constants/themes/alpha.ts +++ b/cli/src/constants/themes/alpha.ts @@ -58,6 +58,17 @@ export const alphaTheme: Theme = { lineNumber: "cyan", }, + markdown: { + text: "white", + heading: "yellow", + strong: "white", + em: "white", + code: "green", + blockquote: "gray", + link: "cyan", + list: "white", + }, + ui: { border: { default: "gray", diff --git a/cli/src/constants/themes/ansi-light.ts b/cli/src/constants/themes/ansi-light.ts index c2a188f273e..6a0a34e4a65 100644 --- a/cli/src/constants/themes/ansi-light.ts +++ b/cli/src/constants/themes/ansi-light.ts @@ -54,6 +54,17 @@ export const ansiLightTheme: Theme = { lineNumber: "gray", }, + markdown: { + text: "#444", + heading: "blue", + strong: "#000", + em: "#444", + code: "green", + blockquote: "gray", + link: "blue", + list: "#444", + }, + ui: { border: { default: "#e1e4e8", diff --git a/cli/src/constants/themes/ansi.ts b/cli/src/constants/themes/ansi.ts index 429e1fa98ba..6c8118ac8af 100644 --- a/cli/src/constants/themes/ansi.ts +++ b/cli/src/constants/themes/ansi.ts @@ -54,6 +54,17 @@ export const ansiTheme: Theme = { lineNumber: "gray", }, + markdown: { + text: "white", + heading: "cyan", + strong: "white", + em: "white", + code: "green", + blockquote: "gray", + link: "cyan", + list: "white", + }, + ui: { border: { default: "gray", diff --git a/cli/src/constants/themes/atom-one-dark.ts b/cli/src/constants/themes/atom-one-dark.ts index 003df5b97c4..65cd677daa0 100644 --- a/cli/src/constants/themes/atom-one-dark.ts +++ b/cli/src/constants/themes/atom-one-dark.ts @@ -54,6 +54,17 @@ export const atomOneDarkTheme: Theme = { lineNumber: "#5c6370", }, + markdown: { + text: "#abb2bf", + heading: "#61aeee", + strong: "#ffffff", + em: "#abb2bf", + code: "#98c379", + blockquote: "#5c6370", + link: "#61aeee", + list: "#abb2bf", + }, + ui: { border: { default: "#5c6370", diff --git a/cli/src/constants/themes/ayu-dark.ts b/cli/src/constants/themes/ayu-dark.ts index 9308c964bfb..8bdecfb823b 100644 --- a/cli/src/constants/themes/ayu-dark.ts +++ b/cli/src/constants/themes/ayu-dark.ts @@ -54,6 +54,17 @@ export const ayuDarkTheme: Theme = { lineNumber: "#646A71", }, + markdown: { + text: "#aeaca6", + heading: "#FFB454", + strong: "#ffffff", + em: "#aeaca6", + code: "#AAD94C", + blockquote: "#646A71", + link: "#59C2FF", + list: "#aeaca6", + }, + ui: { border: { default: "#3D4149", diff --git a/cli/src/constants/themes/ayu-light.ts b/cli/src/constants/themes/ayu-light.ts index 21ccf80431f..bee5e6f0482 100644 --- a/cli/src/constants/themes/ayu-light.ts +++ b/cli/src/constants/themes/ayu-light.ts @@ -54,6 +54,17 @@ export const ayuLightTheme: Theme = { lineNumber: "#a6aaaf", }, + markdown: { + text: "#5c6166", + heading: "#55b4d4", + strong: "#000000", + em: "#5c6166", + code: "#86b300", + blockquote: "#a6aaaf", + link: "#55b4d4", + list: "#5c6166", + }, + ui: { border: { default: "#e1e4e8", diff --git a/cli/src/constants/themes/dark.ts b/cli/src/constants/themes/dark.ts index cc8df75d91a..2d6ecb91cab 100644 --- a/cli/src/constants/themes/dark.ts +++ b/cli/src/constants/themes/dark.ts @@ -54,6 +54,17 @@ export const darkTheme: Theme = { lineNumber: "#858585", }, + markdown: { + text: "#cccccc", + heading: "#faf74f", + strong: "#ffffff", + em: "#d4d4d4", + code: "#89d185", + blockquote: "#858585", + link: "#3794ff", + list: "#cccccc", + }, + ui: { border: { default: "#3c3c3c", diff --git a/cli/src/constants/themes/dracula.ts b/cli/src/constants/themes/dracula.ts index 2a72bdc161c..e4369db26d6 100644 --- a/cli/src/constants/themes/dracula.ts +++ b/cli/src/constants/themes/dracula.ts @@ -54,6 +54,17 @@ export const draculaTheme: Theme = { lineNumber: "#6272a4", }, + markdown: { + text: "#a3afb7", + heading: "#ff79c6", + strong: "#ffffff", + em: "#a3afb7", + code: "#50fa7b", + blockquote: "#6272a4", + link: "#8be9fd", + list: "#a3afb7", + }, + ui: { border: { default: "#6272a4", diff --git a/cli/src/constants/themes/github-dark.ts b/cli/src/constants/themes/github-dark.ts index fa3f99d576c..9d44f0571b0 100644 --- a/cli/src/constants/themes/github-dark.ts +++ b/cli/src/constants/themes/github-dark.ts @@ -54,6 +54,17 @@ export const githubDarkTheme: Theme = { lineNumber: "#8b949e", }, + markdown: { + text: "#c9d1d9", + heading: "#58a6ff", + strong: "#ffffff", + em: "#c9d1d9", + code: "#3fb950", + blockquote: "#8b949e", + link: "#58a6ff", + list: "#c9d1d9", + }, + ui: { border: { default: "#30363d", diff --git a/cli/src/constants/themes/github-light.ts b/cli/src/constants/themes/github-light.ts index 8f7dec10fe1..c5e23ed4588 100644 --- a/cli/src/constants/themes/github-light.ts +++ b/cli/src/constants/themes/github-light.ts @@ -54,6 +54,17 @@ export const githubLightTheme: Theme = { lineNumber: "#999", }, + markdown: { + text: "#24292e", + heading: "#0086b3", + strong: "#000000", + em: "#24292e", + code: "#008080", + blockquote: "#998", + link: "#0086b3", + list: "#24292e", + }, + ui: { border: { default: "#e1e4e8", diff --git a/cli/src/constants/themes/googlecode.ts b/cli/src/constants/themes/googlecode.ts index 8d694f5ba7d..c088d9e6685 100644 --- a/cli/src/constants/themes/googlecode.ts +++ b/cli/src/constants/themes/googlecode.ts @@ -54,6 +54,17 @@ export const googleCodeTheme: Theme = { lineNumber: "#5f6368", }, + markdown: { + text: "#444", + heading: "#066", + strong: "#000", + em: "#444", + code: "#080", + blockquote: "#5f6368", + link: "#066", + list: "#444", + }, + ui: { border: { default: "#e1e4e8", diff --git a/cli/src/constants/themes/light.ts b/cli/src/constants/themes/light.ts index 8630d3ad6e1..91ec4c6a2ad 100644 --- a/cli/src/constants/themes/light.ts +++ b/cli/src/constants/themes/light.ts @@ -54,6 +54,17 @@ export const lightTheme: Theme = { lineNumber: "#237893", }, + markdown: { + text: "#616161", + heading: "#000000", + strong: "#000000", + em: "#616161", + code: "#388a34", + blockquote: "#717171", + link: "#006ab1", + list: "#616161", + }, + ui: { border: { default: "#cecece", diff --git a/cli/src/constants/themes/shades-of-purple.ts b/cli/src/constants/themes/shades-of-purple.ts index a991136b705..1b1bdd24606 100644 --- a/cli/src/constants/themes/shades-of-purple.ts +++ b/cli/src/constants/themes/shades-of-purple.ts @@ -54,6 +54,17 @@ export const shadesOfPurpleTheme: Theme = { lineNumber: "#726c86", }, + markdown: { + text: "#e3dfff", + heading: "#4d21fc", + strong: "#ffffff", + em: "#e3dfff", + code: "#A5FF90", + blockquote: "#726c86", + link: "#a1feff", + list: "#e3dfff", + }, + ui: { border: { default: "#a599e9", diff --git a/cli/src/constants/themes/xcode.ts b/cli/src/constants/themes/xcode.ts index 8e4b11f3e72..ce3c8ab17a1 100644 --- a/cli/src/constants/themes/xcode.ts +++ b/cli/src/constants/themes/xcode.ts @@ -54,6 +54,17 @@ export const xcodeTheme: Theme = { lineNumber: "#c0c0c0", }, + markdown: { + text: "#444", + heading: "#0E0EFF", + strong: "#000", + em: "#444", + code: "#007400", + blockquote: "#c0c0c0", + link: "#0E0EFF", + list: "#444", + }, + ui: { border: { default: "#e1e4e8", diff --git a/cli/src/types/theme.ts b/cli/src/types/theme.ts index 12a74ecc5c2..6a40b847af0 100644 --- a/cli/src/types/theme.ts +++ b/cli/src/types/theme.ts @@ -69,6 +69,18 @@ export interface Theme { lineNumber: string } + /** Markdown rendering colors */ + markdown: { + text: string + heading: string + strong: string + em: string + code: string + blockquote: string + link: string + list: string + } + /** UI structure colors */ ui: { border: { diff --git a/cli/src/ui/components/MarkdownText.tsx b/cli/src/ui/components/MarkdownText.tsx index 40ec7fdf407..d7930f7c27f 100644 --- a/cli/src/ui/components/MarkdownText.tsx +++ b/cli/src/ui/components/MarkdownText.tsx @@ -2,9 +2,78 @@ import React, { useState, useEffect, useRef, useCallback } from "react" import { Text } from "ink" import { parse, setOptions } from "marked" import TerminalRenderer, { type TerminalRendererOptions } from "marked-terminal" +import chalk, { type ChalkInstance } from "chalk" +import type { Theme } from "../../types/theme.js" export type MarkdownTextProps = TerminalRendererOptions & { children: string + theme?: Theme +} + +/** + * Named chalk color methods that return ChalkInstance + */ +type ChalkColorMethod = + | "black" + | "red" + | "green" + | "yellow" + | "blue" + | "magenta" + | "cyan" + | "white" + | "gray" + | "grey" + | "blackBright" + | "redBright" + | "greenBright" + | "yellowBright" + | "blueBright" + | "magentaBright" + | "cyanBright" + | "whiteBright" + +/** + * Convert a color string (hex or named) to a chalk function + */ +const colorToChalk = (color: string): ChalkInstance => { + // If it starts with #, it's a hex color + if (color.startsWith("#")) { + return chalk.hex(color) + } + // Otherwise, it's a named color - use chalk's named color methods + // Check if it's a valid color method name + if (isChalkColorMethod(color)) { + return chalk[color] + } + return chalk.white +} + +/** + * Type guard to check if a string is a valid chalk color method + */ +const isChalkColorMethod = (color: string): color is ChalkColorMethod => { + const validColors: ChalkColorMethod[] = [ + "black", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white", + "gray", + "grey", + "blackBright", + "redBright", + "greenBright", + "yellowBright", + "blueBright", + "magentaBright", + "cyanBright", + "whiteBright", + ] + return validColors.includes(color as ChalkColorMethod) } /** @@ -108,7 +177,7 @@ const calculateAdaptiveSpeed = ( * @param options - Optional TerminalRenderer configuration * @returns Rendered markdown text with typewriter animation for streaming content */ -export const MarkdownText: React.FC = ({ children, ...options }) => { +export const MarkdownText: React.FC = ({ children, theme, ...options }) => { // State for displayed text (what user sees) const [displayedText, setDisplayedText] = useState("") @@ -238,9 +307,24 @@ export const MarkdownText: React.FC = ({ children, ...options } try { + // Merge theme colors with user options if theme is provided + const rendererOptions: TerminalRendererOptions = theme + ? { + ...options, + text: colorToChalk(theme.markdown.text), + heading: colorToChalk(theme.markdown.heading), + strong: colorToChalk(theme.markdown.strong), + em: colorToChalk(theme.markdown.em), + code: colorToChalk(theme.markdown.code), + blockquote: colorToChalk(theme.markdown.blockquote), + link: colorToChalk(theme.markdown.link), + list: colorToChalk(theme.markdown.list), + } + : options + // Configure marked to use the terminal renderer setOptions({ - renderer: new TerminalRenderer(options), + renderer: new TerminalRenderer(rendererOptions), }) // Parse markdown on the displayed text (efficient - only once per update) diff --git a/cli/src/ui/components/__tests__/MarkdownText.test.tsx b/cli/src/ui/components/__tests__/MarkdownText.test.tsx index d29ee7246fd..04962126756 100644 --- a/cli/src/ui/components/__tests__/MarkdownText.test.tsx +++ b/cli/src/ui/components/__tests__/MarkdownText.test.tsx @@ -288,4 +288,168 @@ const code = "example"; vi.useRealTimers() }) }) + + describe("Theme support", () => { + it("should render with hex color theme", () => { + const theme = { + id: "test", + name: "Test", + type: "dark" as const, + brand: { primary: "#00ff00", secondary: "#ff00ff" }, + semantic: { + success: "#00ff00", + error: "#ff0000", + warning: "#ffff00", + info: "#00ffff", + neutral: "#888888", + }, + interactive: { + prompt: "#ffffff", + selection: "#444444", + hover: "#555555", + disabled: "#333333", + focus: "#666666", + }, + messages: { user: "#00ff00", assistant: "#0000ff", system: "#888888", error: "#ff0000" }, + actions: { approve: "#00ff00", reject: "#ff0000", cancel: "#888888", pending: "#ffff00" }, + code: { + addition: "#00ff00", + deletion: "#ff0000", + modification: "#ffff00", + context: "#888888", + lineNumber: "#444444", + }, + markdown: { + text: "#ffffff", + heading: "#00ff00", + strong: "#ff0000", + em: "#ffff00", + code: "#00ffff", + blockquote: "#888888", + link: "#0000ff", + list: "#ff00ff", + }, + ui: { + border: { default: "#444444", active: "#00ff00", warning: "#ffff00", error: "#ff0000" }, + text: { primary: "#ffffff", secondary: "#cccccc", dimmed: "#888888", highlight: "#00ff00" }, + background: { default: "#000000", elevated: "#111111" }, + }, + status: { online: "#00ff00", offline: "#ff0000", busy: "#ffff00", idle: "#888888" }, + } + + const { lastFrame } = render(Hello World) + expect(lastFrame()).toContain("Hello World") + }) + + it("should render with named color theme", () => { + const theme = { + id: "test", + name: "Test", + type: "light" as const, + brand: { primary: "green", secondary: "magenta" }, + semantic: { success: "green", error: "red", warning: "yellow", info: "cyan", neutral: "gray" }, + interactive: { prompt: "white", selection: "gray", hover: "gray", disabled: "gray", focus: "gray" }, + messages: { user: "green", assistant: "blue", system: "gray", error: "red" }, + actions: { approve: "green", reject: "red", cancel: "gray", pending: "yellow" }, + code: { + addition: "green", + deletion: "red", + modification: "yellow", + context: "gray", + lineNumber: "gray", + }, + markdown: { + text: "white", + heading: "green", + strong: "red", + em: "yellow", + code: "cyan", + blockquote: "gray", + link: "blue", + list: "magenta", + }, + ui: { + border: { default: "gray", active: "green", warning: "yellow", error: "red" }, + text: { primary: "white", secondary: "gray", dimmed: "gray", highlight: "green" }, + background: { default: "black", elevated: "gray" }, + }, + status: { online: "green", offline: "red", busy: "yellow", idle: "gray" }, + } + + const { lastFrame } = render(Hello World) + expect(lastFrame()).toContain("Hello World") + }) + + it("should fall back to white for unknown color names", () => { + const theme = { + id: "test", + name: "Test", + type: "custom" as const, + brand: { primary: "invalidcolor", secondary: "invalidcolor" }, + semantic: { + success: "invalidcolor", + error: "invalidcolor", + warning: "invalidcolor", + info: "invalidcolor", + neutral: "invalidcolor", + }, + interactive: { + prompt: "invalidcolor", + selection: "invalidcolor", + hover: "invalidcolor", + disabled: "invalidcolor", + focus: "invalidcolor", + }, + messages: { + user: "invalidcolor", + assistant: "invalidcolor", + system: "invalidcolor", + error: "invalidcolor", + }, + actions: { + approve: "invalidcolor", + reject: "invalidcolor", + cancel: "invalidcolor", + pending: "invalidcolor", + }, + code: { + addition: "invalidcolor", + deletion: "invalidcolor", + modification: "invalidcolor", + context: "invalidcolor", + lineNumber: "invalidcolor", + }, + markdown: { + text: "invalidcolor", + heading: "invalidcolor", + strong: "invalidcolor", + em: "invalidcolor", + code: "invalidcolor", + blockquote: "invalidcolor", + link: "invalidcolor", + list: "invalidcolor", + }, + ui: { + border: { + default: "invalidcolor", + active: "invalidcolor", + warning: "invalidcolor", + error: "invalidcolor", + }, + text: { + primary: "invalidcolor", + secondary: "invalidcolor", + dimmed: "invalidcolor", + highlight: "invalidcolor", + }, + background: { default: "invalidcolor", elevated: "invalidcolor" }, + }, + status: { online: "invalidcolor", offline: "invalidcolor", busy: "invalidcolor", idle: "invalidcolor" }, + } + + // Should still render without throwing errors + const { lastFrame } = render(Hello World) + expect(lastFrame()).toContain("Hello World") + }) + }) }) diff --git a/cli/src/ui/messages/extension/say/SayReasoningMessage.tsx b/cli/src/ui/messages/extension/say/SayReasoningMessage.tsx index f16731a4654..bc0ddace923 100644 --- a/cli/src/ui/messages/extension/say/SayReasoningMessage.tsx +++ b/cli/src/ui/messages/extension/say/SayReasoningMessage.tsx @@ -25,7 +25,7 @@ export const SayReasoningMessage: React.FC = ({ message } {message.text && ( - {message.text} + {message.text} {message.partial && ( {" "} diff --git a/src/api/providers/base-openai-compatible-provider.ts b/src/api/providers/base-openai-compatible-provider.ts index 1efc03ec5fe..d31843f37b1 100644 --- a/src/api/providers/base-openai-compatible-provider.ts +++ b/src/api/providers/base-openai-compatible-provider.ts @@ -14,7 +14,6 @@ import { BaseProvider } from "./base-provider" import { verifyFinishReason } from "./kilocode/verifyFinishReason" import { handleOpenAIError } from "./utils/openai-error-handler" import { fetchWithTimeout } from "./kilocode/fetchWithTimeout" // kilocode_change -import { ToolCallAccumulator } from "./kilocode/nativeToolCallHelpers" // kilocode_change import { calculateApiCostOpenAI } from "../../shared/cost" import { getApiRequestTimeout } from "./utils/timeout-config" diff --git a/src/api/providers/chutes.ts b/src/api/providers/chutes.ts index e024efb9ada..78ac7e591f3 100644 --- a/src/api/providers/chutes.ts +++ b/src/api/providers/chutes.ts @@ -11,7 +11,6 @@ import { ApiStream } from "../transform/stream" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { RouterProvider } from "./router-provider" -import { addNativeToolCallsToParams, ToolCallAccumulator } from "./kilocode/nativeToolCallHelpers" export class ChutesHandler extends RouterProvider implements SingleCompletionHandler { constructor(options: ApiHandlerOptions) { @@ -57,8 +56,6 @@ export class ChutesHandler extends RouterProvider implements SingleCompletionHan params.temperature = this.options.modelTemperature ?? info.temperature } - addNativeToolCallsToParams(params, this.options, metadata) // kilocode_change - return params } @@ -69,8 +66,6 @@ export class ChutesHandler extends RouterProvider implements SingleCompletionHan ): ApiStream { const model = await this.fetchModel() - const toolCallAccumulator = new ToolCallAccumulator() // kilocode_change - if (model.id.includes("DeepSeek-R1")) { const stream = await this.client.chat.completions.create({ ...this.getCompletionParams(systemPrompt, messages, metadata), @@ -89,8 +84,6 @@ export class ChutesHandler extends RouterProvider implements SingleCompletionHan for await (const chunk of stream) { const delta = chunk.choices[0]?.delta - yield* toolCallAccumulator.processChunk(chunk) // kilocode_change - if (delta?.content) { for (const processedChunk of matcher.update(delta.content)) { yield processedChunk @@ -132,8 +125,6 @@ export class ChutesHandler extends RouterProvider implements SingleCompletionHan for await (const chunk of stream) { const delta = chunk.choices[0]?.delta - yield* toolCallAccumulator.processChunk(chunk) // kilocode_change - if (delta?.content) { yield { type: "text", text: delta.content } } diff --git a/src/api/providers/deepinfra.ts b/src/api/providers/deepinfra.ts index 9a7f9c2bec2..af8bae140f2 100644 --- a/src/api/providers/deepinfra.ts +++ b/src/api/providers/deepinfra.ts @@ -13,7 +13,6 @@ import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from ". import { RouterProvider } from "./router-provider" import { getModelParams } from "../transform/model-params" import { getModels } from "./fetchers/modelCache" -import { addNativeToolCallsToParams, ToolCallAccumulator } from "./kilocode/nativeToolCallHelpers" export class DeepInfraHandler extends RouterProvider implements SingleCompletionHandler { constructor(options: ApiHandlerOptions) { @@ -94,12 +93,9 @@ export class DeepInfraHandler extends RouterProvider implements SingleCompletion const { data: stream } = await this.client.chat.completions.create(requestOptions).withResponse() let lastUsage: OpenAI.CompletionUsage | undefined - const toolCallAccumulator = new ToolCallAccumulator() // kilocode_change for await (const chunk of stream) { const delta = chunk.choices[0]?.delta - yield* toolCallAccumulator.processChunk(chunk) // kilocode_change - if (delta?.content) { yield { type: "text", text: delta.content } } diff --git a/src/api/providers/kilocode/model-settings.ts b/src/api/providers/kilocode/model-settings.ts index 89554ca1227..efab4a0393d 100644 --- a/src/api/providers/kilocode/model-settings.ts +++ b/src/api/providers/kilocode/model-settings.ts @@ -1,9 +1,9 @@ -import { ModelInfo, toolNames } from "@roo-code/types" +import { ModelInfo } from "@roo-code/types" import { z } from "zod" export const ModelSettingsSchema = z.object({ - included_tools: z.array(z.enum(toolNames)).nullish(), - excluded_tools: z.array(z.enum(toolNames)).nullish(), + included_tools: z.array(z.string()).nullish(), + excluded_tools: z.array(z.string()).nullish(), }) export const VersionedModelSettingsSchema = z.record(z.string(), ModelSettingsSchema) diff --git a/src/api/providers/lite-llm.ts b/src/api/providers/lite-llm.ts index f1ba43340d2..9acbc35d074 100644 --- a/src/api/providers/lite-llm.ts +++ b/src/api/providers/lite-llm.ts @@ -12,7 +12,6 @@ import { convertToOpenAiMessages } from "../transform/openai-format" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { RouterProvider } from "./router-provider" -import { addNativeToolCallsToParams, ToolCallAccumulator } from "./kilocode/nativeToolCallHelpers" /** * LiteLLM provider handler @@ -153,13 +152,10 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa let lastUsage - const toolCallAccumulator = new ToolCallAccumulator() // kilocode_change for await (const chunk of completion) { const delta = chunk.choices[0]?.delta const usage = chunk.usage as LiteLLMUsage - yield* toolCallAccumulator.processChunk(chunk) // kilocode_change - if (delta?.content) { yield { type: "text", text: delta.content } } @@ -236,13 +232,12 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa requestOptions.temperature = this.options.modelTemperature ?? 0 } - // kilocode_change start + // GPT-5 models require max_completion_tokens instead of the deprecated max_tokens parameter if (isGPT5Model && info.maxTokens) { requestOptions.max_completion_tokens = info.maxTokens } else if (info.maxTokens) { requestOptions.max_tokens = info.maxTokens } - // kilocode_change end const response = await this.client.chat.completions.create(requestOptions) return response.choices[0]?.message.content || "" diff --git a/src/api/providers/xai.ts b/src/api/providers/xai.ts index 7177b53c172..8c4ee2d1f6e 100644 --- a/src/api/providers/xai.ts +++ b/src/api/providers/xai.ts @@ -15,7 +15,6 @@ import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { verifyFinishReason } from "./kilocode/verifyFinishReason" // kilocode_change import { handleOpenAIError } from "./utils/openai-error-handler" -import { addNativeToolCallsToParams, ToolCallAccumulator } from "./kilocode/nativeToolCallHelpers" const XAI_DEFAULT_TEMPERATURE = 0 @@ -84,14 +83,11 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler throw handleOpenAIError(error, this.providerName) } - const toolCallAccumulator = new ToolCallAccumulator() // kilocode_change for await (const chunk of stream) { verifyFinishReason(chunk.choices[0]) // kilocode_change const delta = chunk.choices[0]?.delta const finishReason = chunk.choices[0]?.finish_reason - yield* toolCallAccumulator.processChunk(chunk) // kilocode_change - if (delta?.content) { yield { type: "text", diff --git a/src/core/kilocode/agent-manager/AgentManagerProvider.ts b/src/core/kilocode/agent-manager/AgentManagerProvider.ts index 00ad935cd61..08fb5cde03f 100644 --- a/src/core/kilocode/agent-manager/AgentManagerProvider.ts +++ b/src/core/kilocode/agent-manager/AgentManagerProvider.ts @@ -10,20 +10,21 @@ import { isParallelModeCompletionMessage, parseParallelModeCompletionBranch, } from "./parallelModeParser" +import { findKilocodeCli, type CliDiscoveryResult } from "./CliPathResolver" import { canInstallCli, getCliInstallCommand, getLocalCliInstallCommand, getLocalCliBinDir } from "./CliInstaller" import { CliProcessHandler, type CliProcessHandlerCallbacks } from "./CliProcessHandler" import type { StreamEvent, KilocodeStreamEvent, KilocodePayload, WelcomeStreamEvent } from "./CliOutputParser" import { extractRawText, tryParsePayloadJson } from "./askErrorParser" import { RemoteSessionService } from "./RemoteSessionService" import { KilocodeEventProcessor } from "./KilocodeEventProcessor" -import { CliSessionLauncher } from "./CliSessionLauncher" import type { RemoteSession } from "./types" import { getUri } from "../../webview/getUri" import { getNonce } from "../../webview/getNonce" import { getViteDevServerConfig } from "../../webview/getViteDevServerConfig" import { getRemoteUrl } from "../../../services/code-index/managed/git-utils" import { normalizeGitUrl } from "./normalizeGitUrl" -import type { ClineMessage, ProviderSettings } from "@roo-code/types" +import type { ClineMessage } from "@roo-code/types" +import type { ProviderSettings } from "@roo-code/types" import { captureAgentManagerOpened, captureAgentManagerSessionStarted, @@ -53,7 +54,6 @@ export class AgentManagerProvider implements vscode.Disposable { private remoteSessionService: RemoteSessionService private processHandler: CliProcessHandler private eventProcessor: KilocodeEventProcessor - private sessionLauncher: CliSessionLauncher private sessionMessages: Map = new Map() // Track first api_req_started per session to filter user-input echoes private firstApiReqStarted: Map = new Map() @@ -73,12 +73,6 @@ export class AgentManagerProvider implements vscode.Disposable { this.registry = new AgentRegistry() this.remoteSessionService = new RemoteSessionService({ outputChannel }) - // Initialize session launcher with pre-warming - // Pre-warming starts slow lookups (CLI: 500-2000ms, git: 50-100ms) immediately - // so they complete before the user clicks "Start" to reduce time-to-first-token - this.sessionLauncher = new CliSessionLauncher(outputChannel, () => this.getApiConfigurationForCli()) - this.sessionLauncher.startPrewarm() - // Initialize currentGitUrl from workspace void this.initializeCurrentGitUrl() @@ -204,8 +198,6 @@ export class AgentManagerProvider implements vscode.Disposable { () => { this.panel = undefined this.stopAllAgents() - // Clear pre-warm state when panel closes - this.sessionLauncher.clearPrewarm() }, null, this.disposables, @@ -217,7 +209,7 @@ export class AgentManagerProvider implements vscode.Disposable { captureAgentManagerOpened() } - /** Rename session key in all session-keyed maps. */ + /** Rename session key in all session-keyed maps when provisional session is upgraded. */ private handleSessionRenamed(oldId: string, newId: string): void { this.outputChannel.appendLine(`[AgentManager] Renaming session: ${oldId} -> ${newId}`) @@ -444,10 +436,23 @@ export class AgentManagerProvider implements vscode.Disposable { return } + // Get workspace folder early to fetch git URL before spawning // Note: we intentionally allow starting parallel mode from within an existing git worktree. // Git worktrees share a common .git dir, so `git worktree add/remove` still works from a worktree root. const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath + // Get git URL for the workspace (used for filtering sessions) + let gitUrl: string | undefined + if (workspaceFolder) { + try { + gitUrl = normalizeGitUrl(await getRemoteUrl(workspaceFolder)) + } catch (error) { + this.outputChannel.appendLine( + `[AgentManager] Could not get git URL: ${error instanceof Error ? error.message : String(error)}`, + ) + } + } + const onSetupFailed = () => { if (!workspaceFolder) { void vscode.window.showErrorMessage("Please open a folder before starting an agent.") @@ -455,12 +460,12 @@ export class AgentManagerProvider implements vscode.Disposable { this.postMessage({ type: "agentManager.startSessionFailed" }) } - // Git URL lookup is now handled by spawnCliWithCommonSetup using pre-warmed promise await this.spawnCliWithCommonSetup( prompt, { parallelMode: options?.parallelMode, label: options?.labelOverride, + gitUrl, existingBranch: options?.existingBranch, }, onSetupFailed, @@ -474,7 +479,7 @@ export class AgentManagerProvider implements vscode.Disposable { /** * Common helper to spawn a CLI process with standard setup. - * Delegates to CliSessionLauncher for pre-warming and spawning. + * Handles CLI path lookup, workspace folder validation, API config, and event callback wiring. * @returns true if process was spawned, false if setup failed */ private async spawnCliWithCommonSetup( @@ -488,23 +493,47 @@ export class AgentManagerProvider implements vscode.Disposable { }, onSetupFailed?: () => void, ): Promise { - const result = await this.sessionLauncher.spawn( + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath + if (!workspaceFolder) { + this.outputChannel.appendLine("ERROR: No workspace folder open") + onSetupFailed?.() + return false + } + + const cliDiscovery = await findKilocodeCli((msg) => this.outputChannel.appendLine(`[AgentManager] ${msg}`)) + if (!cliDiscovery) { + this.outputChannel.appendLine("ERROR: kilocode CLI not found") + this.showCliNotFoundError() + onSetupFailed?.() + return false + } + + const processStartTime = Date.now() + let apiConfiguration: ProviderSettings | undefined + try { + apiConfiguration = await this.getApiConfigurationForCli() + } catch (error) { + this.outputChannel.appendLine( + `[AgentManager] Failed to read provider settings for CLI: ${ + error instanceof Error ? error.message : String(error) + }`, + ) + } + + this.processHandler.spawnProcess( + cliDiscovery.cliPath, + workspaceFolder, prompt, - options, - this.processHandler, + { ...options, apiConfiguration, shellPath: cliDiscovery.shellPath }, (sid, event) => { - if (result.processStartTime && !this.processStartTimes.has(sid)) { - this.processStartTimes.set(sid, result.processStartTime) + if (!this.processStartTimes.has(sid)) { + this.processStartTimes.set(sid, processStartTime) } this.handleCliEvent(sid, event) }, - () => { - this.showCliNotFoundError() - onSetupFailed?.() - }, ) - return result.success + return true } /** @@ -1138,7 +1167,7 @@ export class AgentManagerProvider implements vscode.Disposable { this.processHandler.dispose() this.sessionMessages.clear() this.firstApiReqStarted.clear() - this.sessionLauncher.clearPrewarm() + this.panel?.dispose() this.disposables.forEach((d) => d.dispose()) } @@ -1218,6 +1247,15 @@ export class AgentManagerProvider implements vscode.Disposable { terminal.sendText("kilocode auth") } + private runConfigureInTerminal(): void { + const terminal = this.createCliTerminal("Kilocode CLI Config") + if (!terminal) { + return + } + terminal.show() + terminal.sendText("kilocode config") + } + private showCliAuthReminder(message?: string): void { const authLabel = t("kilocode:agentManager.actions.loginCli") const combined = this.buildAuthReminderMessage(message) @@ -1389,7 +1427,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() @@ -1407,6 +1448,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) { @@ -1479,6 +1526,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 }) diff --git a/src/core/kilocode/agent-manager/CliOutputParser.ts b/src/core/kilocode/agent-manager/CliOutputParser.ts index 6f7ce5a0561..425c5b4cdaf 100644 --- a/src/core/kilocode/agent-manager/CliOutputParser.ts +++ b/src/core/kilocode/agent-manager/CliOutputParser.ts @@ -71,10 +71,19 @@ export interface SessionCreatedStreamEvent { timestamp: number } +export interface SessionTitleGeneratedStreamEvent { + streamEventType: "session_title_generated" + sessionId: string + title: string + timestamp: number +} + export interface WelcomeStreamEvent { streamEventType: "welcome" worktreeBranch?: string timestamp: number + /** Configuration error instructions from CLI (indicates misconfigured CLI) */ + instructions?: string[] } export type StreamEvent = @@ -85,6 +94,7 @@ export type StreamEvent = | CompleteStreamEvent | InterruptedStreamEvent | SessionCreatedStreamEvent + | SessionTitleGeneratedStreamEvent | WelcomeStreamEvent /** @@ -223,14 +233,26 @@ function toStreamEvent(parsed: Record): StreamEvent | null { } } - // Detect welcome event from CLI (format: { type: "welcome", metadata: { welcomeOptions: { worktreeBranch: "..." } }, ... }) + // Detect session_title_generated event from CLI (format: { event: "session_title_generated", sessionId: "...", title: "...", timestamp: ... }) + if (parsed.event === "session_title_generated" && typeof parsed.sessionId === "string" && typeof parsed.title === "string") { + return { + streamEventType: "session_title_generated", + sessionId: parsed.sessionId as string, + title: parsed.title as string, + timestamp: (parsed.timestamp as number) || Date.now(), + } + } + + // Detect welcome event from CLI (format: { type: "welcome", metadata: { welcomeOptions: { worktreeBranch: "...", instructions: [...] } }, ... }) if (parsed.type === "welcome") { const metadata = parsed.metadata as Record | undefined const welcomeOptions = metadata?.welcomeOptions as Record | 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, } } diff --git a/src/core/kilocode/agent-manager/CliPathResolver.ts b/src/core/kilocode/agent-manager/CliPathResolver.ts index eebcdf2a51b..79e553f8042 100644 --- a/src/core/kilocode/agent-manager/CliPathResolver.ts +++ b/src/core/kilocode/agent-manager/CliPathResolver.ts @@ -1,9 +1,21 @@ import * as path from "node:path" import * as fs from "node:fs" -import { execSync } from "node:child_process" +import { execSync, spawnSync } from "node:child_process" import { fileExistsAtPath } from "../../../utils/fs" import { getLocalCliPath } from "./CliInstaller" +/** + * Result of CLI discovery including optional shell environment. + * The shellPath is captured from login shell lookup on macOS/Linux to ensure + * the spawned CLI process has access to the same tools (like git) that were + * available when finding the CLI. + */ +export interface CliDiscoveryResult { + cliPath: string + /** PATH from login shell - use this when spawning CLI on macOS to ensure tools like git are available */ + shellPath?: string +} + /** * Case-insensitive lookup for environment variables. * Windows environment variables can have inconsistent casing (PATH, Path, path). @@ -111,8 +123,15 @@ export async function findExecutable( * - Direct PATH might find stale system-wide installations (e.g., old homebrew version) * - When we auto-update via `npm install -g`, it installs to the user's node (nvm etc.) * - So we need to find the CLI in the same location where updates go + * + * @returns CliDiscoveryResult with cliPath and optional shellPath for spawning */ -export async function findKilocodeCli(log?: (msg: string) => void): Promise { +export async function findKilocodeCli(log?: (msg: string) => void): Promise { + // Capture shell PATH early for use when spawning (macOS/Linux only) + // This ensures the CLI has access to the same tools (git, etc.) that were available + // when finding it, even when the editor is launched from Finder/Spotlight + const shellEnv = process.platform !== "win32" ? getLoginShellPath(log) : undefined + // 1) Explicit override from settings try { // Lazy import avoids hard dep when running in non-extension contexts @@ -122,7 +141,7 @@ export async function findKilocodeCli(log?: (msg: string) => void): Promise void): Promise void): Promise void): Promise void): Promise void): string | undefined { + if (process.platform === "win32") { + return undefined + } + + const userShell = process.env.SHELL || "/bin/bash" + const shellName = path.basename(userShell) + + // Use -i -l (interactive + login) to source both .zprofile/.bash_profile AND .zshrc/.bashrc + // stdio: ['ignore', 'pipe', 'pipe'] prevents stdin from blocking + const shellArgs = shellName === "tcsh" || shellName === "csh" ? ["-ic"] : ["-i", "-l", "-c"] + + // Use markers to reliably extract PATH even if shell prints banners/warnings + const startMarker = "__KILO_PATH_START__" + const endMarker = "__KILO_PATH_END__" + const command = `printf '${startMarker}%s${endMarker}\\n' "$PATH"` + + try { + const result = spawnSync(userShell, [...shellArgs, command], { + encoding: "utf-8", + timeout: 10000, + env: { ...process.env, HOME: process.env.HOME }, + stdio: ["ignore", "pipe", "pipe"], // stdin ignored, stdout/stderr captured + }) + + if (result.error) { + log?.(`Could not capture shell PATH: ${result.error}`) + return undefined + } + + const output = result.stdout ?? "" + + // Extract PATH from between markers + const startIdx = output.indexOf(startMarker) + const endIdx = output.indexOf(endMarker) + + if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) { + log?.(`Could not find PATH markers in shell output`) + return undefined + } + + const shellPath = output.slice(startIdx + startMarker.length, endIdx) + + if (shellPath && shellPath !== process.env.PATH) { + log?.(`Captured shell PATH (${shellPath.split(":").length} entries)`) + return shellPath + } + } catch (error) { + log?.(`Could not capture shell PATH: ${error}`) + } + + return undefined +} + /** * Try to find kilocode by running `which` in a login shell. * This sources the user's shell profile (~/.zshrc, ~/.bashrc, etc.) diff --git a/src/core/kilocode/agent-manager/CliProcessHandler.ts b/src/core/kilocode/agent-manager/CliProcessHandler.ts index dbf2afdc5a0..3259fd45a55 100644 --- a/src/core/kilocode/agent-manager/CliProcessHandler.ts +++ b/src/core/kilocode/agent-manager/CliProcessHandler.ts @@ -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. @@ -37,9 +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 { @@ -55,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 @@ -80,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) { @@ -87,9 +104,17 @@ export class CliProcessHandler { } } - private buildEnvWithApiConfiguration(apiConfiguration?: ProviderSettings): NodeJS.ProcessEnv { + private buildEnvWithApiConfiguration(apiConfiguration?: ProviderSettings, shellPath?: string): NodeJS.ProcessEnv { const baseEnv = { ...process.env } + // On macOS/Linux, use the shell PATH to ensure CLI can access tools like git + // This is critical when the editor is launched from Finder/Spotlight, as the + // extension host doesn't inherit the user's shell environment + if (shellPath) { + baseEnv.PATH = shellPath + this.debugLog(`Using shell PATH for CLI spawn`) + } + const overrides = buildProviderEnvOverrides( apiConfiguration, baseEnv, @@ -118,6 +143,8 @@ export class CliProcessHandler { gitUrl?: string apiConfiguration?: ProviderSettings existingBranch?: string + /** Shell PATH from login shell - ensures CLI can access tools like git on macOS */ + shellPath?: string } | undefined, onCliEvent: (sessionId: string, event: StreamEvent) => void, @@ -160,7 +187,7 @@ export class CliProcessHandler { this.debugLog(`Command: ${cliPath} ${cliArgs.join(" ")}`) this.debugLog(`Working dir: ${workspace}`) - const env = this.buildEnvWithApiConfiguration(options?.apiConfiguration) + const env = this.buildEnvWithApiConfiguration(options?.apiConfiguration, options?.shellPath) // On Windows, .cmd files need to be executed through cmd.exe (shell: true) // Without this, spawn() fails silently because .cmd files are batch scripts @@ -212,7 +239,9 @@ 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, } } @@ -222,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) { @@ -361,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 @@ -494,15 +535,23 @@ export class CliProcessHandler { ) const stderrOutput = this.pendingProcess.stderrBuffer.join("\n") + const hadShellPath = this.pendingProcess.hadShellPath this.pendingProcess.process.kill("SIGTERM") this.registry.clearPendingSession() this.pendingProcess = null const { platform, shell } = getPlatformDiagnostics() + + // Enhanced telemetry for session_timeout to help diagnose issues like #4579 captureAgentManagerLoginIssue({ issueType: "session_timeout", platform, shell, + hasStderr: !!stderrOutput, + // Truncate stderr to first 500 chars to avoid sending too much data + stderrPreview: stderrOutput ? stderrOutput.slice(0, 500) : undefined, + // Track if our shell PATH fix was applied (helps verify fix effectiveness) + hadShellPath, }) this.callbacks.onPendingSessionChanged(null) @@ -601,15 +650,57 @@ export class CliProcessHandler { ): void { if (this.pendingProcess && this.pendingProcess.process === proc) { this.clearPendingTimeout() + + // Start with any config error captured during streaming + let configurationError = this.pendingProcess.configurationError + + // Flush any buffered parser output - welcome event JSON might be split across chunks + const { events } = this.pendingProcess.parser.flush() + for (const event of events) { + if (event.streamEventType === "welcome" && !configurationError) { + configurationError = this.extractConfigErrorFromWelcome(event as WelcomeStreamEvent) + if (configurationError) { + this.debugLog(`Captured CLI configuration error from flush: ${configurationError}`) + } + } + } + + // Fallback: Check raw stdout for configuration error patterns if JSON parsing didn't capture it + 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 @@ -794,4 +885,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 + } } diff --git a/src/core/kilocode/agent-manager/CliSessionLauncher.ts b/src/core/kilocode/agent-manager/CliSessionLauncher.ts index 4fbfa3a1422..6ae8b60f220 100644 --- a/src/core/kilocode/agent-manager/CliSessionLauncher.ts +++ b/src/core/kilocode/agent-manager/CliSessionLauncher.ts @@ -1,5 +1,5 @@ import * as vscode from "vscode" -import { findKilocodeCli } from "./CliPathResolver" +import { findKilocodeCli, type CliDiscoveryResult } from "./CliPathResolver" import { CliProcessHandler } from "./CliProcessHandler" import type { StreamEvent } from "./CliOutputParser" import { getRemoteUrl } from "../../../services/code-index/managed/git-utils" @@ -44,7 +44,7 @@ export type GetApiConfigurationFn = () => Promise */ export class CliSessionLauncher { // Pre-warm promises for CLI path and git URL lookups - private cliPathPromise: Promise | null = null + private cliPathPromise: Promise | null = null private gitUrlPromise: Promise | null = null constructor( @@ -89,6 +89,15 @@ 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 { + const result = await this.cliPathPromise + return result?.cliPath ?? null + } + /** * Spawn a CLI process with all the standard setup. * Handles CLI path lookup, git URL resolution, API config, and event callback wiring. @@ -125,7 +134,7 @@ export class CliSessionLauncher { const gitUrlPromise = this.resolveGitUrlPromise(options.gitUrl, workspaceFolder) // Run CLI path lookup, git URL lookup, and API config fetch in parallel - const [cliPath, resolvedGitUrl, apiConfiguration] = await Promise.all([ + const [cliDiscovery, resolvedGitUrl, apiConfiguration] = await Promise.all([ cliPathPromise, gitUrlPromise, this.getApiConfiguration().catch((error) => { @@ -137,13 +146,13 @@ export class CliSessionLauncher { ]) this.log( - `Parallel lookups completed in ${Date.now() - spawnStart}ms (CLI: ${cliPath ? "found" : "not found"}, gitUrl: ${resolvedGitUrl ?? "none"})`, + `Parallel lookups completed in ${Date.now() - spawnStart}ms (CLI: ${cliDiscovery ? "found" : "not found"}, gitUrl: ${resolvedGitUrl ?? "none"})`, ) // Clear pre-warm promises after use (they're single-use per panel open) this.clearPrewarm() - if (!cliPath) { + if (!cliDiscovery) { this.log("ERROR: kilocode CLI not found") onSetupFailed?.() return { success: false } @@ -152,10 +161,10 @@ export class CliSessionLauncher { const processStartTime = Date.now() processHandler.spawnProcess( - cliPath, + cliDiscovery.cliPath, workspaceFolder, prompt, - { ...options, gitUrl: resolvedGitUrl, apiConfiguration }, + { ...options, gitUrl: resolvedGitUrl, apiConfiguration, shellPath: cliDiscovery.shellPath }, onCliEvent, ) diff --git a/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts b/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts index bae9b2dfcf1..c98de1e47ae 100644 --- a/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts +++ b/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts @@ -1,8 +1,10 @@ import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from "vitest" import { EventEmitter } from "node:events" +import * as path from "node:path" import * as telemetry from "../telemetry" -const MOCK_CLI_PATH = "/mock/path/to/kilocode" +const isWindows = process.platform === "win32" +const MOCK_CLI_PATH = isWindows ? "C:\\mock\\path\\to\\kilocode" : "/mock/path/to/kilocode" // Mock the local telemetry module vi.mock("../telemetry", () => ({ @@ -15,6 +17,16 @@ vi.mock("../telemetry", () => ({ captureAgentManagerLoginIssue: vi.fn(), })) +// Mock CliPathResolver to return CliDiscoveryResult object +// Note: vi.mock is hoisted, so we inline the platform check instead of using MOCK_CLI_PATH +vi.mock("../CliPathResolver", () => ({ + findKilocodeCli: vi.fn().mockResolvedValue({ + cliPath: process.platform === "win32" ? "C:\\mock\\path\\to\\kilocode" : "/mock/path/to/kilocode", + shellPath: undefined, + }), + findExecutable: vi.fn().mockResolvedValue(undefined), +})) + let AgentManagerProvider: typeof import("../AgentManagerProvider").AgentManagerProvider describe("AgentManagerProvider CLI spawning", () => { @@ -97,6 +109,103 @@ describe("AgentManagerProvider CLI spawning", () => { expect(options?.shell).not.toBe(true) }) + // Windows-specific test - runs only on Windows CI + // We don't simulate Windows on other platforms - let the actual Windows CI test it + const windowsOnlyTest = isWindows ? it : it.skip + + windowsOnlyTest("spawns with shell: true when CLI path ends with .cmd", async () => { + vi.resetModules() + + const testNpmDir = "C:\\npm" + const testWorkspace = "C:\\tmp\\workspace" + const cmdPath = path.join(testNpmDir, "kilocode") + ".CMD" + + const mockWorkspaceFolder = { uri: { fsPath: testWorkspace } } + const mockProvider = { + getState: vi.fn().mockResolvedValue({ apiConfiguration: { apiProvider: "kilocode" } }), + } + + vi.doMock("vscode", () => ({ + workspace: { workspaceFolders: [mockWorkspaceFolder] }, + window: { + showErrorMessage: vi.fn().mockResolvedValue(undefined), + showWarningMessage: vi.fn().mockResolvedValue(undefined), + 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), + })) + + // Mock CliPathResolver to return .cmd path for Windows test + vi.doMock("../CliPathResolver", () => ({ + findKilocodeCli: vi.fn().mockResolvedValue({ cliPath: cmdPath, shellPath: undefined }), + findExecutable: vi.fn().mockResolvedValue(cmdPath), + })) + + vi.doMock("node:fs", () => ({ + existsSync: vi.fn().mockReturnValue(false), + readdirSync: vi.fn().mockReturnValue([]), + promises: { + stat: vi.fn().mockImplementation((filePath: string) => { + if (filePath === cmdPath) { + return Promise.resolve({ isFile: () => true }) + } + return Promise.reject(Object.assign(new Error("ENOENT"), { code: "ENOENT" })) + }), + lstat: vi.fn().mockImplementation((filePath: string) => { + if (filePath === cmdPath) { + return Promise.resolve({ isFile: () => true, isSymbolicLink: () => false }) + } + return Promise.reject(Object.assign(new Error("ENOENT"), { code: "ENOENT" })) + }), + }, + })) + + class TestProc extends EventEmitter { + stdout = new EventEmitter() + stderr = new EventEmitter() + kill = vi.fn() + pid = 1234 + } + + const spawnMock = vi.fn(() => new TestProc()) + vi.doMock("node:child_process", () => ({ + spawn: spawnMock, + execSync: vi.fn().mockImplementation(() => { + throw new Error("not found") + }), + })) + + const originalPath = process.env.PATH + process.env.PATH = testNpmDir + + 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 { + process.env.PATH = originalPath + } + }) + it("creates pending session and waits for session_created event", async () => { await (provider as any).startAgentSession("test pending") @@ -560,23 +669,10 @@ describe("AgentManagerProvider gitUrl filtering", () => { }) it("handles git URL retrieval errors gracefully", async () => { - // Need to recreate provider with the mock rejection set up BEFORE construction - // because pre-warming happens in the constructor - provider.dispose() mockGetRemoteUrl.mockRejectedValue(new Error("No remote configured")) + const spawnProcessSpy = vi.spyOn((provider as any).processHandler, "spawnProcess") - const mockContext = { extensionUri: {}, extensionPath: "", extensionMode: 1 } as any - const mockOutputChannel = { appendLine: vi.fn() } as any - const mockClineProvider = { - getState: vi.fn().mockResolvedValue({ apiConfiguration: { apiProvider: "kilocode" } }), - } - - const module = await import("../AgentManagerProvider") - const newProvider = new module.AgentManagerProvider(mockContext, mockOutputChannel, mockClineProvider as any) - - const spawnProcessSpy = vi.spyOn((newProvider as any).processHandler, "spawnProcess") - - await (newProvider as any).startAgentSession("test prompt") + await (provider as any).startAgentSession("test prompt") // Should still spawn process without gitUrl expect(spawnProcessSpy).toHaveBeenCalledWith( @@ -586,8 +682,6 @@ describe("AgentManagerProvider gitUrl filtering", () => { expect.objectContaining({ gitUrl: undefined }), expect.any(Function), ) - - newProvider.dispose() }) it("stores gitUrl on created session", async () => { diff --git a/src/core/kilocode/agent-manager/__tests__/CliOutputParser.spec.ts b/src/core/kilocode/agent-manager/__tests__/CliOutputParser.spec.ts index c4143fb74be..fcbd3cc41f9 100644 --- a/src/core/kilocode/agent-manager/__tests__/CliOutputParser.spec.ts +++ b/src/core/kilocode/agent-manager/__tests__/CliOutputParser.spec.ts @@ -73,6 +73,37 @@ describe("parseCliChunk", () => { expect(event.timestamp).toBeLessThanOrEqual(after) }) + it("should parse session_title_generated event from CLI", () => { + const result = parseCliChunk( + '{"event":"session_title_generated","sessionId":"sess-abc-123","title":"My Session Title","timestamp":1234567890}\n', + ) + expect(result.events).toHaveLength(1) + expect(result.events[0]).toEqual({ + streamEventType: "session_title_generated", + sessionId: "sess-abc-123", + title: "My Session Title", + timestamp: 1234567890, + }) + }) + + it("should use current timestamp when session_title_generated has no timestamp", () => { + const before = Date.now() + const result = parseCliChunk( + '{"event":"session_title_generated","sessionId":"sess-xyz","title":"Test Title"}\n', + ) + const after = Date.now() + + expect(result.events).toHaveLength(1) + expect(result.events[0]).toMatchObject({ + streamEventType: "session_title_generated", + sessionId: "sess-xyz", + title: "Test Title", + }) + const event = result.events[0] as { timestamp: number } + expect(event.timestamp).toBeGreaterThanOrEqual(before) + expect(event.timestamp).toBeLessThanOrEqual(after) + }) + it("should parse welcome event with worktree branch", () => { const result = parseCliChunk( '{"type":"welcome","metadata":{"welcomeOptions":{"worktreeBranch":"feature/test-branch"}},"timestamp":1234567890}\n', @@ -82,6 +113,7 @@ describe("parseCliChunk", () => { streamEventType: "welcome", worktreeBranch: "feature/test-branch", timestamp: 1234567890, + instructions: undefined, }) }) @@ -92,6 +124,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, }) }) diff --git a/src/core/kilocode/agent-manager/__tests__/CliPathResolver.spec.ts b/src/core/kilocode/agent-manager/__tests__/CliPathResolver.spec.ts index 7d420fdf061..5e91ca7d7e6 100644 --- a/src/core/kilocode/agent-manager/__tests__/CliPathResolver.spec.ts +++ b/src/core/kilocode/agent-manager/__tests__/CliPathResolver.spec.ts @@ -10,9 +10,15 @@ describe("findKilocodeCli", () => { const loginShellTests = isWindows ? it.skip : it - loginShellTests("finds CLI via login shell and returns trimmed result", async () => { + loginShellTests("finds CLI via login shell and returns CliDiscoveryResult with cliPath", async () => { + // spawnSync is used for getLoginShellPath (with markers), execSync for findViaLoginShell + const testShellPath = "/custom/path:/usr/bin" + const spawnSyncMock = vi + .fn() + .mockReturnValue({ stdout: `__KILO_PATH_START__${testShellPath}__KILO_PATH_END__\n` }) const execSyncMock = vi.fn().mockReturnValue("/Users/test/.nvm/versions/node/v20/bin/kilocode\n") - vi.doMock("node:child_process", () => ({ execSync: execSyncMock })) + + vi.doMock("node:child_process", () => ({ execSync: execSyncMock, spawnSync: spawnSyncMock })) vi.doMock("../../../../utils/fs", () => ({ fileExistsAtPath: vi.fn().mockResolvedValue(false) })) vi.doMock("node:fs", () => ({ existsSync: vi.fn().mockReturnValue(false), @@ -22,14 +28,17 @@ describe("findKilocodeCli", () => { const { findKilocodeCli } = await import("../CliPathResolver") const result = await findKilocodeCli() - expect(result).toBe("/Users/test/.nvm/versions/node/v20/bin/kilocode") - expect(execSyncMock).toHaveBeenCalledWith( - expect.stringContaining("which kilocode"), - expect.objectContaining({ encoding: "utf-8" }), - ) + expect(result).not.toBeNull() + expect(result?.cliPath).toBe("/Users/test/.nvm/versions/node/v20/bin/kilocode") + // shellPath should be captured from login shell via spawnSync + expect(result?.shellPath).toBe(testShellPath) }) loginShellTests("falls back to findExecutable when login shell fails", async () => { + const testShellPath = "/custom/path:/usr/bin" + const spawnSyncMock = vi + .fn() + .mockReturnValue({ stdout: `__KILO_PATH_START__${testShellPath}__KILO_PATH_END__\n` }) const execSyncMock = vi.fn().mockImplementation(() => { throw new Error("login shell failed") }) @@ -39,7 +48,8 @@ describe("findKilocodeCli", () => { } return Promise.reject(new Error("ENOENT")) }) - vi.doMock("node:child_process", () => ({ execSync: execSyncMock })) + + vi.doMock("node:child_process", () => ({ execSync: execSyncMock, spawnSync: spawnSyncMock })) vi.doMock("../../../../utils/fs", () => ({ fileExistsAtPath: vi.fn().mockResolvedValue(false) })) vi.doMock("node:fs", () => ({ existsSync: vi.fn().mockReturnValue(false), @@ -49,17 +59,20 @@ describe("findKilocodeCli", () => { const { findKilocodeCli } = await import("../CliPathResolver") const result = await findKilocodeCli() - expect(result).toBe("/usr/local/bin/kilocode") + expect(result?.cliPath).toBe("/usr/local/bin/kilocode") }) it("falls back to npm paths when all PATH lookups fail", async () => { + const spawnSyncMock = vi.fn().mockImplementation(() => { + throw new Error("not found") + }) const execSyncMock = vi.fn().mockImplementation(() => { throw new Error("not found") }) const fileExistsMock = vi.fn().mockImplementation((path: string) => { return Promise.resolve(path.includes("kilocode")) }) - vi.doMock("node:child_process", () => ({ execSync: execSyncMock })) + vi.doMock("node:child_process", () => ({ execSync: execSyncMock, spawnSync: spawnSyncMock })) vi.doMock("../../../../utils/fs", () => ({ fileExistsAtPath: fileExistsMock })) vi.doMock("node:fs", () => ({ existsSync: vi.fn().mockReturnValue(false), @@ -70,6 +83,7 @@ describe("findKilocodeCli", () => { const result = await findKilocodeCli() expect(result).not.toBeNull() + expect(result?.cliPath).toBeDefined() expect(fileExistsMock).toHaveBeenCalled() }) @@ -78,6 +92,9 @@ describe("findKilocodeCli", () => { execSync: vi.fn().mockImplementation(() => { throw new Error("not found") }), + spawnSync: vi.fn().mockImplementation(() => { + throw new Error("not found") + }), })) vi.doMock("../../../../utils/fs", () => ({ fileExistsAtPath: vi.fn().mockResolvedValue(false) })) vi.doMock("node:fs", () => ({ @@ -98,6 +115,9 @@ describe("findKilocodeCli", () => { execSync: vi.fn().mockImplementation(() => { throw new Error("not found") }), + spawnSync: vi.fn().mockImplementation(() => { + throw new Error("not found") + }), })) vi.doMock("../../../../utils/fs", () => ({ fileExistsAtPath: vi.fn().mockResolvedValue(false) })) vi.doMock("node:fs", () => ({ @@ -265,4 +285,35 @@ describe("findExecutable", () => { expect(result).toBe("/usr/bin/kilocode") expect(statMock).not.toHaveBeenCalledWith(expect.stringContaining(".CMD")) }) + + // Login shell PATH capture test - skipped on Windows + const loginShellTests = isWindows ? it.skip : it + + loginShellTests("captures shell PATH for spawning CLI on macOS", async () => { + // spawnSync returns output with markers to handle shell startup noise + const testPath = "/opt/homebrew/bin:/usr/local/bin:/usr/bin" + const spawnSyncMock = vi + .fn() + .mockReturnValue({ stdout: `some banner\n__KILO_PATH_START__${testPath}__KILO_PATH_END__\n` }) + const execSyncMock = vi.fn().mockReturnValue("/opt/homebrew/bin/kilocode\n") + + vi.doMock("node:child_process", () => ({ execSync: execSyncMock, spawnSync: spawnSyncMock })) + vi.doMock("../../../../utils/fs", () => ({ fileExistsAtPath: vi.fn().mockResolvedValue(false) })) + + const { findKilocodeCli } = await import("../CliPathResolver") + const result = await findKilocodeCli() + + expect(result).not.toBeNull() + expect(result?.cliPath).toBe("/opt/homebrew/bin/kilocode") + expect(result?.shellPath).toBe(testPath) + + // Verify spawnSync was called with correct args for login shell + expect(spawnSyncMock).toHaveBeenCalledWith( + expect.any(String), + expect.arrayContaining(["-i", "-l", "-c"]), + expect.objectContaining({ + stdio: ["ignore", "pipe", "pipe"], + }), + ) + }) }) diff --git a/src/core/kilocode/agent-manager/__tests__/CliProcessHandler.spec.ts b/src/core/kilocode/agent-manager/__tests__/CliProcessHandler.spec.ts index 2e15fe30a47..7f9dad552f4 100644 --- a/src/core/kilocode/agent-manager/__tests__/CliProcessHandler.spec.ts +++ b/src/core/kilocode/agent-manager/__tests__/CliProcessHandler.spec.ts @@ -885,7 +885,7 @@ describe("CliProcessHandler", () => { expect(callbacks.onStartSessionFailed).toHaveBeenCalled() }) - it("handles pending process exit with success (no session created)", () => { + it("handles pending process exit with success (no session created) - shows generic error", () => { const onCliEvent = vi.fn() handler.spawnProcess("/path/to/kilocode", "/workspace", "test prompt", undefined, onCliEvent) @@ -893,7 +893,168 @@ describe("CliProcessHandler", () => { mockProcess.emit("exit", 0, null) expect(registry.pendingSession).toBeNull() - expect(callbacks.onStartSessionFailed).not.toHaveBeenCalled() + // Generic fallback ensures user never gets "nothing happened" + expect(callbacks.onStartSessionFailed).toHaveBeenCalledWith({ + type: "unknown", + message: "CLI exited before creating a session", + }) + }) + + it("handles CLI configuration error from welcome event instructions", () => { + const onCliEvent = vi.fn() + handler.spawnProcess("/path/to/kilocode", "/workspace", "test prompt", undefined, onCliEvent) + + // Emit welcome event with configuration error instructions (like misconfigured CLI) + const welcomeEvent = + '{"type":"welcome","metadata":{"welcomeOptions":{"instructions":["Configuration Error: config.json is incomplete","kilocodeToken is required"]}}}\n' + mockProcess.stdout.emit("data", Buffer.from(welcomeEvent)) + + // Exit with code 0 (CLI exits normally after showing configuration error) + mockProcess.emit("exit", 0, null) + + expect(registry.pendingSession).toBeNull() + expect(callbacks.onStartSessionFailed).toHaveBeenCalledWith({ + type: "cli_configuration_error", + message: "Configuration Error: config.json is incomplete\nkilocodeToken is required", + }) + }) + + it("reports generic error when no instructions present but exits before session created", () => { + const onCliEvent = vi.fn() + handler.spawnProcess("/path/to/kilocode", "/workspace", "test prompt", undefined, onCliEvent) + + // Emit normal welcome event without instructions + const welcomeEvent = '{"type":"welcome","metadata":{"welcomeOptions":{}}}\n' + mockProcess.stdout.emit("data", Buffer.from(welcomeEvent)) + + // Exit with code 0 + mockProcess.emit("exit", 0, null) + + expect(registry.pendingSession).toBeNull() + // Generic fallback - not a configuration error, but still an error + expect(callbacks.onStartSessionFailed).toHaveBeenCalledWith({ + type: "unknown", + message: "CLI exited before creating a session", + }) + }) + + it("handles CLI configuration error when welcome event JSON is split across chunks", () => { + const onCliEvent = vi.fn() + handler.spawnProcess("/path/to/kilocode", "/workspace", "test prompt", undefined, onCliEvent) + + // Emit welcome event split across multiple chunks (simulating real CLI behavior) + // First chunk contains partial JSON + const chunk1 = + '{"type":"welcome","metadata":{"welcomeOptions":{"instructions":["Configuration Error: config.json is incomplete",' + mockProcess.stdout.emit("data", Buffer.from(chunk1)) + + // Second chunk contains the rest of the JSON with newline + const chunk2 = '"kilocodeToken is required"]}}}\n' + mockProcess.stdout.emit("data", Buffer.from(chunk2)) + + // Exit with code 0 (CLI exits normally after showing configuration error) + mockProcess.emit("exit", 0, null) + + expect(registry.pendingSession).toBeNull() + expect(callbacks.onStartSessionFailed).toHaveBeenCalledWith({ + type: "cli_configuration_error", + message: "Configuration Error: config.json is incomplete\nkilocodeToken is required", + }) + }) + + it("handles CLI configuration error when welcome event is flushed on process exit", () => { + const onCliEvent = vi.fn() + handler.spawnProcess("/path/to/kilocode", "/workspace", "test prompt", undefined, onCliEvent) + + // Emit welcome event WITHOUT trailing newline (will be buffered) + const welcomeEvent = + '{"type":"welcome","metadata":{"welcomeOptions":{"instructions":["Configuration Error: missing token"]}}}' + mockProcess.stdout.emit("data", Buffer.from(welcomeEvent)) + + // Exit with code 0 - parser should flush and capture the configuration error + mockProcess.emit("exit", 0, null) + + expect(registry.pendingSession).toBeNull() + expect(callbacks.onStartSessionFailed).toHaveBeenCalledWith({ + type: "cli_configuration_error", + message: "Configuration Error: missing token", + }) + }) + + it("handles CLI configuration error from truncated JSON using raw output fallback", () => { + const onCliEvent = vi.fn() + handler.spawnProcess("/path/to/kilocode", "/workspace", "test prompt", undefined, onCliEvent) + + // Emit truncated JSON (simulating CLI exiting before sending complete JSON) + // This is the real-world scenario where the CLI sends partial output + const truncatedJson = '{"type":"welcome","metadata":{"welcomeOptions":{"instructions":["Configuration Error' + mockProcess.stdout.emit("data", Buffer.from(truncatedJson)) + + // Exit with code 0 - JSON can't be parsed, but raw output should be checked + mockProcess.emit("exit", 0, null) + + expect(registry.pendingSession).toBeNull() + expect(callbacks.onStartSessionFailed).toHaveBeenCalledWith({ + type: "cli_configuration_error", + message: + "CLI configuration is incomplete or invalid. Please run 'kilocode config' or 'kilocode auth' to configure.", + }) + }) + + it("handles CLI configuration error when raw output contains kilocodeToken error", () => { + const onCliEvent = vi.fn() + handler.spawnProcess("/path/to/kilocode", "/workspace", "test prompt", undefined, onCliEvent) + + // Emit output containing the specific error message + const output = "Some output... kilocodeToken is required and cannot be empty... more output" + mockProcess.stdout.emit("data", Buffer.from(output)) + + mockProcess.emit("exit", 0, null) + + expect(callbacks.onStartSessionFailed).toHaveBeenCalledWith({ + type: "cli_configuration_error", + message: + "CLI configuration is incomplete or invalid. Please run 'kilocode config' or 'kilocode auth' to configure.", + }) + }) + + it("caps stdout buffer to prevent memory issues with large output", () => { + const onCliEvent = vi.fn() + handler.spawnProcess("/path/to/kilocode", "/workspace", "test prompt", undefined, onCliEvent) + + // Send a lot of data to the stdout buffer (more than 64KB) + const largeChunk = "x".repeat(32 * 1024) // 32KB chunks + mockProcess.stdout.emit("data", Buffer.from(largeChunk)) + mockProcess.stdout.emit("data", Buffer.from(largeChunk)) + mockProcess.stdout.emit("data", Buffer.from(largeChunk)) // 96KB total + + // Access the private pendingProcess to check buffer size + const pendingProcess = (handler as any).pendingProcess + const bufferSize = pendingProcess.stdoutBuffer.reduce((sum: number, chunk: string) => sum + chunk.length, 0) + + // Buffer should be capped at 64KB + expect(bufferSize).toBeLessThanOrEqual(64 * 1024) + }) + + it("preserves most recent data when capping buffer", () => { + const onCliEvent = vi.fn() + handler.spawnProcess("/path/to/kilocode", "/workspace", "test prompt", undefined, onCliEvent) + + // Send chunks with identifiable content + const oldData = "OLD_DATA_".repeat(8 * 1024) // ~80KB of old data + const newData = "kilocodeToken is required" // This should be preserved + + mockProcess.stdout.emit("data", Buffer.from(oldData)) + mockProcess.stdout.emit("data", Buffer.from(newData)) + + // Exit with code 0 - should detect config error from preserved recent data + mockProcess.emit("exit", 0, null) + + expect(callbacks.onStartSessionFailed).toHaveBeenCalledWith({ + type: "cli_configuration_error", + message: + "CLI configuration is incomplete or invalid. Please run 'kilocode config' or 'kilocode auth' to configure.", + }) }) }) diff --git a/src/core/kilocode/agent-manager/__tests__/telemetry.test.ts b/src/core/kilocode/agent-manager/__tests__/telemetry.test.ts index 136621a5147..f1cc0244417 100644 --- a/src/core/kilocode/agent-manager/__tests__/telemetry.test.ts +++ b/src/core/kilocode/agent-manager/__tests__/telemetry.test.ts @@ -200,6 +200,26 @@ describe("Agent Manager Telemetry", () => { ) }) + it("captures session_timeout with enhanced diagnostic fields", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + + const props: AgentManagerLoginIssueProperties = { + issueType: "session_timeout", + platform: "darwin", + shell: "zsh", + hasStderr: true, + stderrPreview: "error: git command not found", + hadShellPath: false, + } + + captureAgentManagerLoginIssue(props) + + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith( + TelemetryEventName.AGENT_MANAGER_LOGIN_ISSUE, + props, + ) + }) + it("captures api_error issue", () => { vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) @@ -231,6 +251,23 @@ describe("Agent Manager Telemetry", () => { ) }) + it("captures cli_configuration_error issue with platform diagnostics", () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + + const props: AgentManagerLoginIssueProperties = { + issueType: "cli_configuration_error", + platform: "darwin", + shell: "fish", + } + + 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) diff --git a/src/core/kilocode/agent-manager/telemetry.ts b/src/core/kilocode/agent-manager/telemetry.ts index 322ce237cf7..6e7b2b91936 100644 --- a/src/core/kilocode/agent-manager/telemetry.ts +++ b/src/core/kilocode/agent-manager/telemetry.ts @@ -18,6 +18,7 @@ export type AgentManagerLoginIssueType = | "cli_not_found" | "cli_outdated" | "cli_spawn_error" + | "cli_configuration_error" | "auth_error" | "payment_required" | "api_error" @@ -29,6 +30,12 @@ export interface AgentManagerLoginIssueProperties { httpStatusCode?: number platform?: "darwin" | "win32" | "linux" | "other" shell?: string + /** For session_timeout: whether stderr had any output */ + hasStderr?: boolean + /** For session_timeout: first 500 chars of stderr (truncated for privacy) */ + stderrPreview?: string + /** For session_timeout: whether shell PATH was captured and used */ + hadShellPath?: boolean // Spawn error details for debugging Windows issues errorMessage?: string cliPath?: string diff --git a/src/core/prompts/tools/native-tools/read_file.ts b/src/core/prompts/tools/native-tools/read_file.ts index bf43f26c8af..6f8fa8ed396 100644 --- a/src/core/prompts/tools/native-tools/read_file.ts +++ b/src/core/prompts/tools/native-tools/read_file.ts @@ -19,6 +19,22 @@ export function createReadFileTool(partialReadsEnabled: boolean = true): OpenAI. " }] }. " + "The 'path' is required and relative to workspace. " + // kilocode_change start + const example = { + files: [ + { + path: "relative/path.ts", + line_ranges: partialReadsEnabled + ? [ + [1, 50], + [100, 150], + ] + : undefined, + }, + ], + } + // kilocode_change end + const optionalRangesDescription = partialReadsEnabled ? "The 'line_ranges' is optional for reading specific sections. Each range is a [start, end] tuple (1-based inclusive). " : "" @@ -81,6 +97,7 @@ export function createReadFileTool(partialReadsEnabled: boolean = true): OpenAI. }, }, required: ["files"], + examples: [example], // kilocode_change additionalProperties: false, }, }, diff --git a/src/core/tools/SearchAndReplaceTool.ts b/src/core/tools/SearchAndReplaceTool.ts index 1dee5ef0e38..675bea589ea 100644 --- a/src/core/tools/SearchAndReplaceTool.ts +++ b/src/core/tools/SearchAndReplaceTool.ts @@ -13,6 +13,7 @@ import { EXPERIMENT_IDS, experiments } from "../../shared/experiments" import { sanitizeUnifiedDiff, computeDiffStats } from "../diff/stats" import { BaseTool, ToolCallbacks } from "./BaseTool" import type { ToolUse } from "../../shared/tools" +import { normalizeLineEndings_kilocode } from "./kilocode/normalizeLineEndings" interface SearchReplaceOperation { search: string @@ -308,8 +309,4 @@ function escapeRegExp(input: string): string { return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") } -function normalizeLineEndings_kilocode(input: string, useCrLf: boolean): string { - return input.replaceAll(/\r?\n/g, useCrLf ? "\r\n" : "\n") -} - export const searchAndReplaceTool = new SearchAndReplaceTool() diff --git a/src/core/tools/SearchReplaceTool.ts b/src/core/tools/SearchReplaceTool.ts index b0150d04a17..4b889626a1b 100644 --- a/src/core/tools/SearchReplaceTool.ts +++ b/src/core/tools/SearchReplaceTool.ts @@ -13,6 +13,7 @@ import { EXPERIMENT_IDS, experiments } from "../../shared/experiments" import { sanitizeUnifiedDiff, computeDiffStats } from "../diff/stats" import { BaseTool, ToolCallbacks } from "./BaseTool" import type { ToolUse } from "../../shared/tools" +import { normalizeLineEndings_kilocode } from "./kilocode/normalizeLineEndings" interface SearchReplaceParams { file_path: string @@ -114,8 +115,11 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> { return } + const useCrLf_kilocode = fileContent.includes("\r\n") + const normalizedOldString_kilocode = normalizeLineEndings_kilocode(old_string, useCrLf_kilocode) + // Check for exact match (literal string, not regex) - const matchCount = fileContent.split(old_string).length - 1 + const matchCount = fileContent.split(normalizedOldString_kilocode).length - 1 if (matchCount === 0) { task.consecutiveMistakeCount++ @@ -142,7 +146,8 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> { } // Apply the single replacement - const newContent = fileContent.replace(old_string, new_string) + const normalizedNewString_kilocode = normalizeLineEndings_kilocode(new_string, useCrLf_kilocode) + const newContent = fileContent.replace(normalizedOldString_kilocode, normalizedNewString_kilocode) // Check if any changes were made if (newContent === fileContent) { diff --git a/src/core/tools/__tests__/searchReplaceTool.spec.ts b/src/core/tools/__tests__/searchReplaceTool.spec.ts index b9b1e453d4a..7ac6f95546d 100644 --- a/src/core/tools/__tests__/searchReplaceTool.spec.ts +++ b/src/core/tools/__tests__/searchReplaceTool.spec.ts @@ -379,4 +379,118 @@ describe("searchReplaceTool", () => { expect(mockCline.fileContextTracker.trackFileContext).toHaveBeenCalledWith(testFilePath, "roo_edited") }) }) + + // kilocode_change start + describe("line ending handling", () => { + it("handles files with Unix line endings (LF)", async () => { + const fileContent = "Line 1\nLine 2\nLine 3" + const oldString = "Line 2" + const newString = "Modified Line 2" + + await executeSearchReplaceTool( + { + old_string: oldString, + new_string: newString, + }, + { fileContent }, + ) + + expect(mockCline.consecutiveMistakeCount).toBe(0) + expect(mockAskApproval).toHaveBeenCalled() + expect(mockCline.recordToolUsage).toHaveBeenCalledWith("search_replace") + }) + + it("handles files with Windows line endings (CRLF)", async () => { + const fileContent = "Line 1\r\nLine 2\r\nLine 3" + const oldString = "Line 2" + const newString = "Modified Line 2" + + await executeSearchReplaceTool( + { + old_string: oldString, + new_string: newString, + }, + { fileContent }, + ) + + expect(mockCline.consecutiveMistakeCount).toBe(0) + expect(mockAskApproval).toHaveBeenCalled() + expect(mockCline.recordToolUsage).toHaveBeenCalledWith("search_replace") + }) + + it("normalizes search string with LF to match file with CRLF", async () => { + const fileContent = "Line 1\r\nLine 2\r\nLine 3" + const oldString = "Line 1\nLine 2" // LF in search string + const newString = "Modified Lines" + + await executeSearchReplaceTool( + { + old_string: oldString, + new_string: newString, + }, + { fileContent }, + ) + + expect(mockCline.consecutiveMistakeCount).toBe(0) + expect(mockAskApproval).toHaveBeenCalled() + expect(mockCline.recordToolUsage).toHaveBeenCalledWith("search_replace") + }) + + it("normalizes search string with CRLF to match file with LF", async () => { + const fileContent = "Line 1\nLine 2\nLine 3" + const oldString = "Line 1\r\nLine 2" // CRLF in search string + const newString = "Modified Lines" + + await executeSearchReplaceTool( + { + old_string: oldString, + new_string: newString, + }, + { fileContent }, + ) + + expect(mockCline.consecutiveMistakeCount).toBe(0) + expect(mockAskApproval).toHaveBeenCalled() + expect(mockCline.recordToolUsage).toHaveBeenCalledWith("search_replace") + }) + + it("preserves CRLF line endings in replacement text for CRLF files", async () => { + const fileContent = "Line 1\r\nLine 2\r\nLine 3" + const oldString = "Line 2" + const newString = "Modified\nLine 2" // LF in replacement + + await executeSearchReplaceTool( + { + old_string: oldString, + new_string: newString, + }, + { fileContent }, + ) + + // The tool should normalize the replacement to use CRLF + expect(mockCline.consecutiveMistakeCount).toBe(0) + expect(mockAskApproval).toHaveBeenCalled() + expect(mockCline.recordToolUsage).toHaveBeenCalledWith("search_replace") + }) + + it("preserves LF line endings in replacement text for LF files", async () => { + const fileContent = "Line 1\nLine 2\nLine 3" + const oldString = "Line 2" + const newString = "Modified\r\nLine 2" // CRLF in replacement + + await executeSearchReplaceTool( + { + old_string: oldString, + new_string: newString, + }, + { fileContent }, + ) + + // The tool should normalize the replacement to use LF + expect(mockCline.consecutiveMistakeCount).toBe(0) + expect(mockAskApproval).toHaveBeenCalled() + expect(mockCline.recordToolUsage).toHaveBeenCalledWith("search_replace") + }) + }) + // kilocode_change end }) diff --git a/src/core/tools/kilocode/normalizeLineEndings.ts b/src/core/tools/kilocode/normalizeLineEndings.ts new file mode 100644 index 00000000000..a6a6b8cb669 --- /dev/null +++ b/src/core/tools/kilocode/normalizeLineEndings.ts @@ -0,0 +1,10 @@ +/** + * Normalizes line endings in a string to match the specified style. + * + * @param input - The input string to normalize + * @param useCrLf - If true, converts to CRLF (\r\n); if false, converts to LF (\n) + * @returns The string with normalized line endings + */ +export function normalizeLineEndings_kilocode(input: string, useCrLf: boolean): string { + return input.replaceAll(/\r?\n/g, useCrLf ? "\r\n" : "\n") +} diff --git a/src/i18n/locales/ar/kilocode.json b/src/i18n/locales/ar/kilocode.json index ecac6826a26..a7c8d8963f8 100644 --- a/src/i18n/locales/ar/kilocode.json +++ b/src/i18n/locales/ar/kilocode.json @@ -181,6 +181,7 @@ "errors": { "cliOutdated": "إصدار Kilocode CLI لديك قديم ولا يدعم مدير الوكلاء. يرجى التحديث إلى أحدث إصدار.", "cliNotFound": "لم يتم العثور على Kilocode CLI. يرجى تثبيته لاستخدام مدير الوكلاء.", + "cliMisconfigured": "Kilocode CLI غير مُهيأ. قم بتشغيل 'kilocode auth' لتسجيل الدخول أو 'kilocode config' للإعداد.", "sessionFailed": "فشل بدء جلسة الوكيل", "sessionFailedWithMessage": "فشل بدء جلسة الوكيل: {{message}}" }, @@ -190,6 +191,7 @@ "getHelp": "الحصول على المساعدة", "runInTerminal": "تشغيل في الطرفية", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "تحديث عام", "updateLocal": "تحديث محلي", "installGlobal": "تثبيت عام", diff --git a/src/i18n/locales/ca/kilocode.json b/src/i18n/locales/ca/kilocode.json index e39fbb78013..39a494a6b92 100644 --- a/src/i18n/locales/ca/kilocode.json +++ b/src/i18n/locales/ca/kilocode.json @@ -177,6 +177,7 @@ "errors": { "cliOutdated": "La teva versió de Kilocode CLI està desactualitzada i no és compatible amb el Gestor d'Agents. Si us plau, actualitza a la versió més recent.", "cliNotFound": "No s'ha trobat Kilocode CLI. Si us plau, instal·la'l per utilitzar el Gestor d'Agents.", + "cliMisconfigured": "Kilocode CLI no està configurat. Executa 'kilocode auth' per iniciar sessió o 'kilocode config' per configurar.", "sessionFailed": "Error en iniciar la sessió de l'agent", "sessionFailedWithMessage": "Error en iniciar la sessió de l'agent: {{message}}" }, @@ -186,6 +187,7 @@ "getHelp": "Obtenir Ajuda", "runInTerminal": "Executar al Terminal", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "Actualitzar Global", "updateLocal": "Actualitzar Local", "installGlobal": "Instal·lar Global", diff --git a/src/i18n/locales/cs/kilocode.json b/src/i18n/locales/cs/kilocode.json index 5e553a236c2..5f2a020edc8 100644 --- a/src/i18n/locales/cs/kilocode.json +++ b/src/i18n/locales/cs/kilocode.json @@ -106,6 +106,7 @@ "errors": { "cliOutdated": "Tvoje verze Kilocode CLI je zastaralá a nepodporuje Agent Manager. Prosím aktualizuj na nejnovější verzi.", "cliNotFound": "Kilocode CLI nenalezeno. Prosím nainstaluj ho pro použití Agent Manageru.", + "cliMisconfigured": "Kilocode CLI není nakonfigurováno. Spusť 'kilocode auth' pro přihlášení nebo 'kilocode config' pro konfiguraci.", "sessionFailed": "Nepodařilo se spustit relaci agenta", "sessionFailedWithMessage": "Nepodařilo se spustit relaci agenta: {{message}}" }, @@ -115,6 +116,7 @@ "getHelp": "Získat Pomoc", "runInTerminal": "Spustit v Terminálu", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "Aktualizovat Globálně", "updateLocal": "Aktualizovat Lokálně", "installGlobal": "Instalovat Globálně", diff --git a/src/i18n/locales/de/kilocode.json b/src/i18n/locales/de/kilocode.json index e05f8989f13..6c42719db7d 100644 --- a/src/i18n/locales/de/kilocode.json +++ b/src/i18n/locales/de/kilocode.json @@ -177,6 +177,7 @@ "errors": { "cliOutdated": "Deine Kilocode CLI-Version ist veraltet und unterstützt den Agent Manager nicht. Bitte aktualisiere auf die neueste Version.", "cliNotFound": "Kilocode CLI nicht gefunden. Bitte installiere es, um den Agent Manager zu nutzen.", + "cliMisconfigured": "Kilocode CLI ist nicht konfiguriert. Bitte führe 'kilocode auth' aus, um dich anzumelden, oder 'kilocode config' zum Konfigurieren.", "sessionFailed": "Agent-Sitzung konnte nicht gestartet werden", "sessionFailedWithMessage": "Agent-Sitzung konnte nicht gestartet werden: {{message}}" }, @@ -186,6 +187,7 @@ "getHelp": "Hilfe erhalten", "runInTerminal": "Im Terminal ausführen", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "Global Aktualisieren", "updateLocal": "Lokal Aktualisieren", "installGlobal": "Global Installieren", diff --git a/src/i18n/locales/en/kilocode.json b/src/i18n/locales/en/kilocode.json index 145f27ecac9..3690c906c74 100644 --- a/src/i18n/locales/en/kilocode.json +++ b/src/i18n/locales/en/kilocode.json @@ -162,6 +162,7 @@ "errors": { "cliOutdated": "Your Kilocode CLI version is outdated and doesn't support the Agent Manager. Please update to the latest version.", "cliNotFound": "Kilocode CLI not found. Please install it to use the Agent Manager.", + "cliMisconfigured": "Kilocode CLI is not configured. Please run 'kilocode auth' to sign in or 'kilocode config' to configure.", "sessionFailed": "Failed to start agent session", "sessionFailedWithMessage": "Failed to start agent session: {{message}}" }, @@ -174,7 +175,8 @@ "updateLocal": "Update Local", "installGlobal": "Install Global", "installLocal": "Install Local", - "loginCli": "Run kilocode auth" + "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config" }, "terminal": { "installMessage": "Running npm install to install the Kilocode CLI. Once complete, you can close this terminal and try starting the Agent Manager again.", diff --git a/src/i18n/locales/es/kilocode.json b/src/i18n/locales/es/kilocode.json index 0b6fb487e21..257c2a1ea37 100644 --- a/src/i18n/locales/es/kilocode.json +++ b/src/i18n/locales/es/kilocode.json @@ -177,6 +177,7 @@ "errors": { "cliOutdated": "Tu versión de Kilocode CLI está desactualizada y no es compatible con el Gestor de Agentes. Por favor, actualiza a la última versión.", "cliNotFound": "No se encontró Kilocode CLI. Por favor, instálalo para usar el Gestor de Agentes.", + "cliMisconfigured": "Kilocode CLI no está configurado. Ejecuta 'kilocode auth' para iniciar sesión o 'kilocode config' para configurar.", "sessionFailed": "Error al iniciar la sesión del agente", "sessionFailedWithMessage": "Error al iniciar la sesión del agente: {{message}}" }, @@ -186,6 +187,7 @@ "getHelp": "Obtener Ayuda", "runInTerminal": "Ejecutar en Terminal", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "Actualizar Global", "updateLocal": "Actualizar Local", "installGlobal": "Instalar Global", diff --git a/src/i18n/locales/fr/kilocode.json b/src/i18n/locales/fr/kilocode.json index 257cca0dadf..c2e0c65f93a 100644 --- a/src/i18n/locales/fr/kilocode.json +++ b/src/i18n/locales/fr/kilocode.json @@ -177,6 +177,7 @@ "errors": { "cliOutdated": "Ta version de Kilocode CLI est obsolète et ne prend pas en charge le Gestionnaire d'Agents. Mets-la à jour vers la dernière version.", "cliNotFound": "Kilocode CLI introuvable. Installe-le pour utiliser le Gestionnaire d'Agents.", + "cliMisconfigured": "Kilocode CLI n'est pas configuré. Exécute 'kilocode auth' pour te connecter ou 'kilocode config' pour configurer.", "sessionFailed": "Échec du démarrage de la session de l'agent", "sessionFailedWithMessage": "Échec du démarrage de la session de l'agent : {{message}}" }, @@ -186,6 +187,7 @@ "getHelp": "Obtenir de l'Aide", "runInTerminal": "Exécuter dans le Terminal", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "Mettre à jour Global", "updateLocal": "Mettre à jour Local", "installGlobal": "Installer Global", diff --git a/src/i18n/locales/hi/kilocode.json b/src/i18n/locales/hi/kilocode.json index ebbaa35fe32..30b591b1d62 100644 --- a/src/i18n/locales/hi/kilocode.json +++ b/src/i18n/locales/hi/kilocode.json @@ -106,6 +106,7 @@ "errors": { "cliOutdated": "आपका Kilocode CLI संस्करण पुराना है और Agent Manager का समर्थन नहीं करता। कृपया नवीनतम संस्करण में अपडेट करें।", "cliNotFound": "Kilocode CLI नहीं मिला। Agent Manager का उपयोग करने के लिए कृपया इसे इंस्टॉल करें।", + "cliMisconfigured": "Kilocode CLI कॉन्फ़िगर नहीं है। साइन इन करने के लिए 'kilocode auth' चलाएं या कॉन्फ़िगर करने के लिए 'kilocode config' चलाएं।", "sessionFailed": "एजेंट सत्र प्रारंभ करने में विफल", "sessionFailedWithMessage": "एजेंट सत्र प्रारंभ करने में विफल: {{message}}" }, @@ -115,6 +116,7 @@ "getHelp": "सहायता प्राप्त करें", "runInTerminal": "टर्मिनल में चलाएं", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "ग्लोबल अपडेट करें", "updateLocal": "लोकल अपडेट करें", "installGlobal": "ग्लोबल इंस्टॉल करें", diff --git a/src/i18n/locales/id/kilocode.json b/src/i18n/locales/id/kilocode.json index 2b8bc0e5cf8..73ab98878e3 100644 --- a/src/i18n/locales/id/kilocode.json +++ b/src/i18n/locales/id/kilocode.json @@ -177,6 +177,7 @@ "errors": { "cliOutdated": "Versi Kilocode CLI kamu sudah usang dan tidak mendukung Agent Manager. Harap perbarui ke versi terbaru.", "cliNotFound": "Kilocode CLI tidak ditemukan. Harap instal untuk menggunakan Agent Manager.", + "cliMisconfigured": "Kilocode CLI belum dikonfigurasi. Jalankan 'kilocode auth' untuk masuk atau 'kilocode config' untuk mengonfigurasi.", "sessionFailed": "Gagal memulai sesi agen", "sessionFailedWithMessage": "Gagal memulai sesi agen: {{message}}" }, @@ -186,6 +187,7 @@ "getHelp": "Dapatkan Bantuan", "runInTerminal": "Jalankan di Terminal", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "Perbarui Global", "updateLocal": "Perbarui Lokal", "installGlobal": "Instal Global", diff --git a/src/i18n/locales/it/kilocode.json b/src/i18n/locales/it/kilocode.json index be26ab463bc..77c9a3ef38f 100644 --- a/src/i18n/locales/it/kilocode.json +++ b/src/i18n/locales/it/kilocode.json @@ -177,6 +177,7 @@ "errors": { "cliOutdated": "La tua versione di Kilocode CLI è obsoleta e non supporta l'Agent Manager. Aggiorna all'ultima versione.", "cliNotFound": "Kilocode CLI non trovato. Installalo per utilizzare l'Agent Manager.", + "cliMisconfigured": "Kilocode CLI non è configurato. Esegui 'kilocode auth' per accedere o 'kilocode config' per configurare.", "sessionFailed": "Impossibile avviare la sessione dell'agente", "sessionFailedWithMessage": "Impossibile avviare la sessione dell'agente: {{message}}" }, @@ -186,6 +187,7 @@ "getHelp": "Ottieni Aiuto", "runInTerminal": "Esegui nel Terminale", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "Aggiorna Globale", "updateLocal": "Aggiorna Locale", "installGlobal": "Installa Globale", diff --git a/src/i18n/locales/ja/kilocode.json b/src/i18n/locales/ja/kilocode.json index f4354ffb445..e2b577c3b30 100644 --- a/src/i18n/locales/ja/kilocode.json +++ b/src/i18n/locales/ja/kilocode.json @@ -177,6 +177,7 @@ "errors": { "cliOutdated": "Kilocode CLIのバージョンが古く、Agent Managerをサポートしていません。最新バージョンに更新してください。", "cliNotFound": "Kilocode CLIが見つかりません。Agent Managerを使用するにはインストールしてください。", + "cliMisconfigured": "Kilocode CLIが設定されていません。'kilocode auth'でサインインするか、'kilocode config'で設定してください。", "sessionFailed": "エージェントセッションの開始に失敗しました", "sessionFailedWithMessage": "エージェントセッションの開始に失敗しました: {{message}}" }, @@ -186,6 +187,7 @@ "getHelp": "ヘルプを表示", "runInTerminal": "ターミナルで実行", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "グローバル更新", "updateLocal": "ローカル更新", "installGlobal": "グローバルインストール", diff --git a/src/i18n/locales/ko/kilocode.json b/src/i18n/locales/ko/kilocode.json index 58c57dba12a..44f573edfc9 100644 --- a/src/i18n/locales/ko/kilocode.json +++ b/src/i18n/locales/ko/kilocode.json @@ -177,6 +177,7 @@ "errors": { "cliOutdated": "Kilocode CLI 버전이 오래되어 Agent Manager를 지원하지 않습니다. 최신 버전으로 업데이트하세요.", "cliNotFound": "Kilocode CLI를 찾을 수 없습니다. Agent Manager를 사용하려면 설치하세요.", + "cliMisconfigured": "Kilocode CLI가 구성되지 않았습니다. 'kilocode auth'를 실행하여 로그인하거나 'kilocode config'로 구성하세요.", "sessionFailed": "에이전트 세션 시작 실패", "sessionFailedWithMessage": "에이전트 세션 시작 실패: {{message}}" }, @@ -186,6 +187,7 @@ "getHelp": "도움말 보기", "runInTerminal": "터미널에서 실행", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "전역 업데이트", "updateLocal": "로컬 업데이트", "installGlobal": "전역 설치", diff --git a/src/i18n/locales/nl/kilocode.json b/src/i18n/locales/nl/kilocode.json index d71151b832b..48bbb632dda 100644 --- a/src/i18n/locales/nl/kilocode.json +++ b/src/i18n/locales/nl/kilocode.json @@ -177,6 +177,7 @@ "errors": { "cliOutdated": "Je Kilocode CLI-versie is verouderd en ondersteunt de Agent Manager niet. Update naar de nieuwste versie.", "cliNotFound": "Kilocode CLI niet gevonden. Installeer het om de Agent Manager te gebruiken.", + "cliMisconfigured": "Kilocode CLI is niet geconfigureerd. Voer 'kilocode auth' uit om in te loggen of 'kilocode config' om te configureren.", "sessionFailed": "Kon agentsessie niet starten", "sessionFailedWithMessage": "Kon agentsessie niet starten: {{message}}" }, @@ -186,6 +187,7 @@ "getHelp": "Hulp krijgen", "runInTerminal": "Uitvoeren in Terminal", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "Globaal Bijwerken", "updateLocal": "Lokaal Bijwerken", "installGlobal": "Globaal Installeren", diff --git a/src/i18n/locales/pl/kilocode.json b/src/i18n/locales/pl/kilocode.json index 1a1d275db3c..b78e618fcd1 100644 --- a/src/i18n/locales/pl/kilocode.json +++ b/src/i18n/locales/pl/kilocode.json @@ -106,6 +106,7 @@ "errors": { "cliOutdated": "Twoja wersja Kilocode CLI jest przestarzała i nie obsługuje Agent Managera. Zaktualizuj do najnowszej wersji.", "cliNotFound": "Nie znaleziono Kilocode CLI. Zainstaluj go, aby korzystać z Agent Managera.", + "cliMisconfigured": "Kilocode CLI nie jest skonfigurowany. Uruchom 'kilocode auth', aby się zalogować lub 'kilocode config', aby skonfigurować.", "sessionFailed": "Nie udało się uruchomić sesji agenta", "sessionFailedWithMessage": "Nie udało się uruchomić sesji agenta: {{message}}" }, @@ -115,6 +116,7 @@ "getHelp": "Uzyskaj Pomoc", "runInTerminal": "Uruchom w Terminalu", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "Aktualizuj Globalnie", "updateLocal": "Aktualizuj Lokalnie", "installGlobal": "Zainstaluj Globalnie", diff --git a/src/i18n/locales/pt-BR/kilocode.json b/src/i18n/locales/pt-BR/kilocode.json index ff05ff7216d..dc74ea9f46c 100644 --- a/src/i18n/locales/pt-BR/kilocode.json +++ b/src/i18n/locales/pt-BR/kilocode.json @@ -177,6 +177,7 @@ "errors": { "cliOutdated": "Sua versão do Kilocode CLI está desatualizada e não suporta o Gerenciador de Agentes. Atualize para a versão mais recente.", "cliNotFound": "Kilocode CLI não encontrado. Instale-o para usar o Gerenciador de Agentes.", + "cliMisconfigured": "Kilocode CLI não está configurado. Execute 'kilocode auth' para entrar ou 'kilocode config' para configurar.", "sessionFailed": "Falha ao iniciar sessão do agente", "sessionFailedWithMessage": "Falha ao iniciar sessão do agente: {{message}}" }, @@ -186,6 +187,7 @@ "getHelp": "Obter Ajuda", "runInTerminal": "Executar no Terminal", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "Atualizar Global", "updateLocal": "Atualizar Local", "installGlobal": "Instalar Global", diff --git a/src/i18n/locales/ru/kilocode.json b/src/i18n/locales/ru/kilocode.json index a0c3980479e..1a55f545b3d 100644 --- a/src/i18n/locales/ru/kilocode.json +++ b/src/i18n/locales/ru/kilocode.json @@ -177,6 +177,7 @@ "errors": { "cliOutdated": "Твоя версия Kilocode CLI устарела и не поддерживает Agent Manager. Обнови до последней версии.", "cliNotFound": "Kilocode CLI не найден. Установи его для использования Agent Manager.", + "cliMisconfigured": "Kilocode CLI не настроен. Выполни 'kilocode auth' для входа или 'kilocode config' для настройки.", "sessionFailed": "Не удалось запустить сессию агента", "sessionFailedWithMessage": "Не удалось запустить сессию агента: {{message}}" }, @@ -186,6 +187,7 @@ "getHelp": "Получить Помощь", "runInTerminal": "Запустить в Терминале", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "Обновить Глобально", "updateLocal": "Обновить Локально", "installGlobal": "Установить Глобально", diff --git a/src/i18n/locales/th/kilocode.json b/src/i18n/locales/th/kilocode.json index 6ca9a2360df..a88a83a2b6c 100644 --- a/src/i18n/locales/th/kilocode.json +++ b/src/i18n/locales/th/kilocode.json @@ -106,6 +106,7 @@ "errors": { "cliOutdated": "เวอร์ชัน Kilocode CLI ของคุณล้าสมัยและไม่รองรับ Agent Manager โปรดอัปเดตเป็นเวอร์ชันล่าสุด", "cliNotFound": "ไม่พบ Kilocode CLI โปรดติดตั้งเพื่อใช้งาน Agent Manager", + "cliMisconfigured": "Kilocode CLI ยังไม่ได้ตั้งค่า เรียกใช้ 'kilocode auth' เพื่อลงชื่อเข้าใช้ หรือ 'kilocode config' เพื่อตั้งค่า", "sessionFailed": "เริ่มเซสชันเอเจนต์ไม่สำเร็จ", "sessionFailedWithMessage": "เริ่มเซสชันเอเจนต์ไม่สำเร็จ: {{message}}" }, @@ -115,6 +116,7 @@ "getHelp": "รับความช่วยเหลือ", "runInTerminal": "เรียกใช้ในเทอร์มินัล", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "อัปเดตแบบ Global", "updateLocal": "อัปเดตแบบ Local", "installGlobal": "ติดตั้งแบบ Global", diff --git a/src/i18n/locales/tr/kilocode.json b/src/i18n/locales/tr/kilocode.json index 331cfefda5f..975a4f8ba35 100644 --- a/src/i18n/locales/tr/kilocode.json +++ b/src/i18n/locales/tr/kilocode.json @@ -177,6 +177,7 @@ "errors": { "cliOutdated": "Kilocode CLI sürümün eski ve Agent Manager'ı desteklemiyor. Lütfen en son sürüme güncelle.", "cliNotFound": "Kilocode CLI bulunamadı. Agent Manager'ı kullanmak için lütfen yükle.", + "cliMisconfigured": "Kilocode CLI yapılandırılmamış. Giriş yapmak için 'kilocode auth' veya yapılandırmak için 'kilocode config' çalıştır.", "sessionFailed": "Ajan oturumu başlatılamadı", "sessionFailedWithMessage": "Ajan oturumu başlatılamadı: {{message}}" }, @@ -186,6 +187,7 @@ "getHelp": "Yardım Al", "runInTerminal": "Terminalde Çalıştır", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "Global Güncelle", "updateLocal": "Lokal Güncelle", "installGlobal": "Global Yükle", diff --git a/src/i18n/locales/uk/kilocode.json b/src/i18n/locales/uk/kilocode.json index 1d95007f1af..5936eb5936c 100644 --- a/src/i18n/locales/uk/kilocode.json +++ b/src/i18n/locales/uk/kilocode.json @@ -177,6 +177,7 @@ "errors": { "cliOutdated": "Твоя версія Kilocode CLI застаріла і не підтримує Agent Manager. Будь ласка, оновись до останньої версії.", "cliNotFound": "Kilocode CLI не знайдено. Будь ласка, встанови його для використання Agent Manager.", + "cliMisconfigured": "Kilocode CLI не налаштовано. Виконай 'kilocode auth' для входу або 'kilocode config' для налаштування.", "sessionFailed": "Не вдалося запустити сесію агента", "sessionFailedWithMessage": "Не вдалося запустити сесію агента: {{message}}" }, @@ -186,6 +187,7 @@ "getHelp": "Отримати Допомогу", "runInTerminal": "Запустити в Терміналі", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "Оновити Глобально", "updateLocal": "Оновити Локально", "installGlobal": "Встановити Глобально", diff --git a/src/i18n/locales/vi/kilocode.json b/src/i18n/locales/vi/kilocode.json index 01e500f2d97..af1b03bbf1c 100644 --- a/src/i18n/locales/vi/kilocode.json +++ b/src/i18n/locales/vi/kilocode.json @@ -177,6 +177,7 @@ "errors": { "cliOutdated": "Phiên bản Kilocode CLI của bạn đã cũ và không hỗ trợ Agent Manager. Vui lòng cập nhật lên phiên bản mới nhất.", "cliNotFound": "Không tìm thấy Kilocode CLI. Vui lòng cài đặt để sử dụng Agent Manager.", + "cliMisconfigured": "Kilocode CLI chưa được cấu hình. Chạy 'kilocode auth' để đăng nhập hoặc 'kilocode config' để cấu hình.", "sessionFailed": "Không thể khởi động phiên agent", "sessionFailedWithMessage": "Không thể khởi động phiên agent: {{message}}" }, @@ -186,6 +187,7 @@ "getHelp": "Nhận Trợ Giúp", "runInTerminal": "Chạy trong Terminal", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "Cập Nhật Toàn Cục", "updateLocal": "Cập Nhật Cục Bộ", "installGlobal": "Cài Đặt Toàn Cục", diff --git a/src/i18n/locales/zh-CN/kilocode.json b/src/i18n/locales/zh-CN/kilocode.json index 3dd1916d945..cbcf492b587 100644 --- a/src/i18n/locales/zh-CN/kilocode.json +++ b/src/i18n/locales/zh-CN/kilocode.json @@ -106,6 +106,7 @@ "errors": { "cliOutdated": "你的 Kilocode CLI 版本已过时,不支持 Agent Manager。请更新到最新版本。", "cliNotFound": "未找到 Kilocode CLI。请安装以使用 Agent Manager。", + "cliMisconfigured": "Kilocode CLI 未配置。请运行 'kilocode auth' 登录或 'kilocode config' 进行配置。", "sessionFailed": "启动代理会话失败", "sessionFailedWithMessage": "启动代理会话失败: {{message}}" }, @@ -115,6 +116,7 @@ "getHelp": "获取帮助", "runInTerminal": "运行命令", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "全局更新", "updateLocal": "本地更新", "installGlobal": "全局安装", diff --git a/src/i18n/locales/zh-TW/kilocode.json b/src/i18n/locales/zh-TW/kilocode.json index a3bacd5afb6..4206ed6f7e8 100644 --- a/src/i18n/locales/zh-TW/kilocode.json +++ b/src/i18n/locales/zh-TW/kilocode.json @@ -177,6 +177,7 @@ "errors": { "cliOutdated": "你的 Kilocode CLI 版本已過時且不支援 Agent Manager。請更新至最新版本。", "cliNotFound": "找不到 Kilocode CLI。請安裝以使用 Agent Manager。", + "cliMisconfigured": "Kilocode CLI 未設定。請執行 'kilocode auth' 登入或 'kilocode config' 進行設定。", "sessionFailed": "啟動代理工作階段失敗", "sessionFailedWithMessage": "啟動代理工作階段失敗: {{message}}" }, @@ -186,6 +187,7 @@ "getHelp": "取得協助", "runInTerminal": "在終端機執行", "loginCli": "Run kilocode auth", + "configureCli": "Run kilocode config", "updateGlobal": "更新全域", "updateLocal": "更新本地", "installGlobal": "安裝全域", diff --git a/src/shared/kilocode/cli-sessions/core/SessionManager.ts b/src/shared/kilocode/cli-sessions/core/SessionManager.ts index 912beabe031..137779e3537 100644 --- a/src/shared/kilocode/cli-sessions/core/SessionManager.ts +++ b/src/shared/kilocode/cli-sessions/core/SessionManager.ts @@ -19,13 +19,18 @@ import { GitStateService } from "./GitStateService.js" import { SessionStateManager } from "./SessionStateManager.js" import { SyncQueue } from "./SyncQueue.js" import { TokenValidationService } from "./TokenValidationService.js" -import { SessionTitleService } from "./SessionTitleService.js" +import { SessionTitleService, type SessionTitleGeneratedMessage } from "./SessionTitleService.js" import { SessionLifecycleService } from "./SessionLifecycleService.js" -import { SessionSyncService, type SessionCreatedMessage, type SessionSyncedMessage } from "./SessionSyncService.js" +import { + SessionSyncService, + type SessionCreatedMessage, + type SessionSyncedMessage, +} from "./SessionSyncService.js" import { LOG_SOURCES } from "../config.js" // Re-export types for external consumers export type { SessionCreatedMessage, SessionSyncedMessage } from "./SessionSyncService.js" +export type { SessionTitleGeneratedMessage } from "./SessionTitleService.js" export type { ListSessionsInput, ListSessionsOutput, @@ -45,6 +50,7 @@ export interface SessionManagerDependencies extends TrpcClientDependencies { onSessionCreated: (message: SessionCreatedMessage) => void onSessionRestored: () => void onSessionSynced: (message: SessionSyncedMessage) => void + onSessionTitleGenerated: (message: SessionTitleGeneratedMessage) => void getOrganizationId: (taskId: string) => Promise getMode: (taskId: string) => Promise getModel: (taskId: string) => Promise @@ -126,6 +132,7 @@ export class SessionManager { stateManager: this.stateManager, extensionMessenger: dependencies.extensionMessenger, logger: this.logger, + onSessionTitleGenerated: dependencies.onSessionTitleGenerated, }) this.gitStateService = new GitStateService({ logger: this.logger, diff --git a/src/shared/kilocode/cli-sessions/core/SessionTitleService.ts b/src/shared/kilocode/cli-sessions/core/SessionTitleService.ts index b448da0506f..cb0b3b59350 100644 --- a/src/shared/kilocode/cli-sessions/core/SessionTitleService.ts +++ b/src/shared/kilocode/cli-sessions/core/SessionTitleService.ts @@ -5,6 +5,16 @@ import type { ILogger } from "../types/ILogger.js" import type { SessionClient } from "./SessionClient.js" import type { SessionStateManager } from "./SessionStateManager.js" +/** + * Message emitted when a session title has been generated and updated. + */ +export interface SessionTitleGeneratedMessage { + sessionId: string + title: string + timestamp: number + event: "session_title_generated" +} + /** * Dependencies required by SessionTitleService. */ @@ -13,6 +23,7 @@ export interface SessionTitleServiceDependencies { stateManager: SessionStateManager extensionMessenger: IExtensionMessenger logger: ILogger + onSessionTitleGenerated?: (message: SessionTitleGeneratedMessage) => void } /** @@ -35,6 +46,7 @@ export class SessionTitleService { private readonly stateManager: SessionStateManager private readonly extensionMessenger: IExtensionMessenger private readonly logger: ILogger + private readonly onSessionTitleGenerated: (message: SessionTitleGeneratedMessage) => void /** * Creates a new SessionTitleService instance. @@ -55,6 +67,7 @@ export class SessionTitleService { this.stateManager = dependencies.stateManager this.extensionMessenger = dependencies.extensionMessenger this.logger = dependencies.logger + this.onSessionTitleGenerated = dependencies.onSessionTitleGenerated ?? (() => {}) this.maxTitleLength = config.maxLength ?? DEFAULT_CONFIG.title.maxLength this.truncatedTitleLength = config.truncatedLength ?? DEFAULT_CONFIG.title.truncatedLength @@ -169,6 +182,14 @@ Summary:` sessionId, title: trimmedTitle, }) + + // Emit session_title_generated event + this.onSessionTitleGenerated({ + sessionId, + title: trimmedTitle, + timestamp: Date.now(), + event: "session_title_generated", + }) } /** diff --git a/src/shared/kilocode/cli-sessions/core/__tests__/SessionManager.spec.ts b/src/shared/kilocode/cli-sessions/core/__tests__/SessionManager.spec.ts index c6d4685edf0..68b9ba8d14a 100644 --- a/src/shared/kilocode/cli-sessions/core/__tests__/SessionManager.spec.ts +++ b/src/shared/kilocode/cli-sessions/core/__tests__/SessionManager.spec.ts @@ -43,6 +43,7 @@ describe("SessionManager", () => { let mockOnSessionCreated: any let mockOnSessionRestored: any let mockOnSessionSynced: any + let mockOnSessionTitleGenerated: any let mockGetOrganizationId: any let mockGetMode: any let mockGetModel: any @@ -127,6 +128,7 @@ describe("SessionManager", () => { mockOnSessionCreated = vi.fn() mockOnSessionRestored = vi.fn() mockOnSessionSynced = vi.fn() + mockOnSessionTitleGenerated = vi.fn() mockGetOrganizationId = vi.fn().mockResolvedValue("org-123") mockGetMode = vi.fn().mockResolvedValue("code") mockGetModel = vi.fn().mockResolvedValue("gpt-4") @@ -144,6 +146,7 @@ describe("SessionManager", () => { onSessionCreated: mockOnSessionCreated, onSessionRestored: mockOnSessionRestored, onSessionSynced: mockOnSessionSynced, + onSessionTitleGenerated: mockOnSessionTitleGenerated, getOrganizationId: mockGetOrganizationId, getMode: mockGetMode, getModel: mockGetModel, @@ -177,6 +180,7 @@ describe("SessionManager", () => { onSessionCreated: mockOnSessionCreated, onSessionRestored: mockOnSessionRestored, onSessionSynced: mockOnSessionSynced, + onSessionTitleGenerated: mockOnSessionTitleGenerated, getOrganizationId: mockGetOrganizationId, getMode: mockGetMode, getModel: mockGetModel, @@ -208,6 +212,7 @@ describe("SessionManager", () => { onSessionCreated: mockOnSessionCreated, onSessionRestored: mockOnSessionRestored, onSessionSynced: mockOnSessionSynced, + onSessionTitleGenerated: mockOnSessionTitleGenerated, getOrganizationId: mockGetOrganizationId, getMode: mockGetMode, getModel: mockGetModel, @@ -259,6 +264,7 @@ describe("SessionManager", () => { onSessionCreated: mockOnSessionCreated, onSessionRestored: mockOnSessionRestored, onSessionSynced: mockOnSessionSynced, + onSessionTitleGenerated: mockOnSessionTitleGenerated, getOrganizationId: mockGetOrganizationId, getMode: mockGetMode, getModel: mockGetModel, @@ -373,6 +379,7 @@ describe("SessionManager", () => { onSessionCreated: mockOnSessionCreated, onSessionRestored: mockOnSessionRestored, onSessionSynced: mockOnSessionSynced, + onSessionTitleGenerated: mockOnSessionTitleGenerated, getOrganizationId: mockGetOrganizationId, getMode: mockGetMode, getModel: mockGetModel, @@ -402,6 +409,7 @@ describe("SessionManager", () => { onSessionCreated: mockOnSessionCreated, onSessionRestored: mockOnSessionRestored, onSessionSynced: mockOnSessionSynced, + onSessionTitleGenerated: mockOnSessionTitleGenerated, getOrganizationId: mockGetOrganizationId, getMode: mockGetMode, getModel: mockGetModel, @@ -423,6 +431,7 @@ describe("SessionManager", () => { onSessionCreated: mockOnSessionCreated, onSessionRestored: mockOnSessionRestored, onSessionSynced: mockOnSessionSynced, + onSessionTitleGenerated: mockOnSessionTitleGenerated, getOrganizationId: mockGetOrganizationId, getMode: mockGetMode, getModel: mockGetModel, diff --git a/src/shared/kilocode/cli-sessions/core/__tests__/SessionTitleService.spec.ts b/src/shared/kilocode/cli-sessions/core/__tests__/SessionTitleService.spec.ts index 33bc725b7ac..07f6c136a2f 100644 --- a/src/shared/kilocode/cli-sessions/core/__tests__/SessionTitleService.spec.ts +++ b/src/shared/kilocode/cli-sessions/core/__tests__/SessionTitleService.spec.ts @@ -215,18 +215,38 @@ describe("SessionTitleService", () => { it("updates state manager with timestamp", async () => { await service.updateTitle("session-123", "Test title") - + expect(mockStateManager.updateTimestamp).toHaveBeenCalledWith("session-123", "2023-01-01T10:00:00Z") }) - + it("logs success message", async () => { await service.updateTitle("session-123", "Test title") - + expect(mockLogger.info).toHaveBeenCalledWith("Session title updated successfully", "SessionTitleService", { sessionId: "session-123", title: "Test title", }) }) + + it("emits session_title_generated event", async () => { + const onSessionTitleGenerated = vi.fn() + const serviceWithCallback = new SessionTitleService({ + sessionClient: mockSessionClient as any, + stateManager: mockStateManager as any, + extensionMessenger: mockExtensionMessenger as any, + logger: mockLogger as any, + onSessionTitleGenerated, + }) + + await serviceWithCallback.updateTitle("session-123", "Test title") + + expect(onSessionTitleGenerated).toHaveBeenCalledWith({ + sessionId: "session-123", + title: "Test title", + timestamp: expect.any(Number), + event: "session_title_generated", + }) + }) }) describe("generateAndUpdateTitle", () => { diff --git a/src/shared/kilocode/cli-sessions/extension/session-manager-utils.ts b/src/shared/kilocode/cli-sessions/extension/session-manager-utils.ts index 70967d09566..7486b8e2297 100644 --- a/src/shared/kilocode/cli-sessions/extension/session-manager-utils.ts +++ b/src/shared/kilocode/cli-sessions/extension/session-manager-utils.ts @@ -58,6 +58,9 @@ export function kilo_initializeSessionManager({ onSessionSynced: (message) => { log(`Session synced: ${message.sessionId}`) }, + onSessionTitleGenerated: (message) => { + log(`Session title generated: ${message.sessionId} - ${message.title}`) + }, platform: vscode.env.appName, getOrganizationId: async (taskId: string) => { const result = await (async () => {