From ebfb5a598e047bb0fc02fa2f6cbe1d3132f9e9c0 Mon Sep 17 00:00:00 2001 From: Alvin Ward Date: Mon, 22 Dec 2025 15:05:04 +0200 Subject: [PATCH 1/6] feat: add model selection below prompt for z.ai --- .../__tests__/getModelsByProvider.spec.ts | 121 ++++++++++++------ .../kilocode/hooks/useProviderModels.ts | 32 +++++ 2 files changed, 117 insertions(+), 36 deletions(-) diff --git a/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts b/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts index 9d9c2dd7c50..d1fe30c8723 100644 --- a/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts +++ b/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts @@ -1,51 +1,58 @@ -import { ModelInfo, ProviderName, providerNames } from "@roo-code/types" +import { + ModelInfo, + ProviderName, + providerNames, + internationalZAiModels, + internationalZAiDefaultModelId, + mainlandZAiModels, + mainlandZAiDefaultModelId, +} from "@roo-code/types" import { RouterModels } from "@roo/api" -import { getModelsByProvider } from "../useProviderModels" +import { getModelsByProvider, getOptionsForProvider } from "../useProviderModels" describe("getModelsByProvider", () => { - it("returns models for all providers", () => { - const testModel: ModelInfo = { - maxTokens: 4096, - contextWindow: 8192, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.1, - outputPrice: 0.2, - description: "Test model", - } + const testModel: ModelInfo = { + maxTokens: 4096, + contextWindow: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.1, + outputPrice: 0.2, + description: "Test model", + } - const routerModels: RouterModels = { - openrouter: { "test-model": testModel }, - requesty: { "test-model": testModel }, - glama: { "test-model": testModel }, - unbound: { "test-model": testModel }, - litellm: { "test-model": testModel }, - kilocode: { "test-model": testModel }, - "nano-gpt": { "test-model": testModel }, //kilocode_change - ollama: { "test-model": testModel }, - lmstudio: { "test-model": testModel }, - "io-intelligence": { "test-model": testModel }, - deepinfra: { "test-model": testModel }, - "vercel-ai-gateway": { "test-model": testModel }, - huggingface: { "test-model": testModel }, - gemini: { "test-model": testModel }, - ovhcloud: { "test-model": testModel }, - chutes: { "test-model": testModel }, - "sap-ai-core": { "test-model": testModel }, // kilocode_change - synthetic: { "test-model": testModel }, // kilocode_change - inception: { "test-model": testModel }, - roo: { "test-model": testModel }, - } + const routerModels: RouterModels = { + openrouter: { "test-model": testModel }, + requesty: { "test-model": testModel }, + glama: { "test-model": testModel }, + unbound: { "test-model": testModel }, + litellm: { "test-model": testModel }, + kilocode: { "test-model": testModel }, + "nano-gpt": { "test-model": testModel }, + ollama: { "test-model": testModel }, + lmstudio: { "test-model": testModel }, + "io-intelligence": { "test-model": testModel }, + deepinfra: { "test-model": testModel }, + "vercel-ai-gateway": { "test-model": testModel }, + huggingface: { "test-model": testModel }, + gemini: { "test-model": testModel }, + ovhcloud: { "test-model": testModel }, + chutes: { "test-model": testModel }, + "sap-ai-core": { "test-model": testModel }, + synthetic: { "test-model": testModel }, + inception: { "test-model": testModel }, + roo: { "test-model": testModel }, + } + it("returns models for all providers", () => { const exceptions = [ "fake-ai", // don't know what this is "huggingface", // don't know what this is "human-relay", // no models - "nano-gpt", // dynamic provider - models fetched from API //kilocode_change + "nano-gpt", // dynamic provider - models fetched from API "openai", // not implemented "roo", // don't care "virtual-quota-fallback", // no models - "zai", // has weird mainland/international distiction "vercel-ai-gateway", // different structure ] @@ -58,6 +65,7 @@ describe("getModelsByProvider", () => { provider, routerModels, kilocodeDefaultModel: "test-default-model", + options: {}, }), ] satisfies [ProviderName, ReturnType], ) @@ -66,4 +74,45 @@ describe("getModelsByProvider", () => { expect(providersWithoutModels).toStrictEqual([]) }) + + it("returns international Z.AI models when isChina is false", () => { + const result = getModelsByProvider({ + provider: "zai", + routerModels, + kilocodeDefaultModel: "test-default-model", + options: { isChina: false }, + }) + + expect(result.models).toEqual(internationalZAiModels) + expect(result.defaultModel).toEqual(internationalZAiDefaultModelId) + }) + + it("returns mainland Z.AI models when isChina is true", () => { + const result = getModelsByProvider({ + provider: "zai", + routerModels, + kilocodeDefaultModel: "test-default-model", + options: { isChina: true }, + }) + + expect(result.models).toEqual(mainlandZAiModels) + expect(result.defaultModel).toEqual(mainlandZAiDefaultModelId) + }) +}) + +describe("getOptionsForProvider", () => { + it("returns default options for non-zai providers", () => { + const result = getOptionsForProvider("openrouter") + expect(result).toEqual({}) + }) + + it("returns isChina: false for zai provider with default apiConfiguration", () => { + const result = getOptionsForProvider("zai", { zaiApiLine: "international_coding" }) + expect(result).toEqual({ isChina: false }) + }) + + it("returns isChina: true for zai provider with china_coding apiConfiguration", () => { + const result = getOptionsForProvider("zai", { zaiApiLine: "china_coding" }) + expect(result).toEqual({ isChina: true }) + }) }) diff --git a/webview-ui/src/components/kilocode/hooks/useProviderModels.ts b/webview-ui/src/components/kilocode/hooks/useProviderModels.ts index ebe9e9bcbcf..9cf7a166bcd 100644 --- a/webview-ui/src/components/kilocode/hooks/useProviderModels.ts +++ b/webview-ui/src/components/kilocode/hooks/useProviderModels.ts @@ -54,6 +54,10 @@ import { inceptionDefaultModelId, minimaxModels, minimaxDefaultModelId, + internationalZAiModels, + internationalZAiDefaultModelId, + mainlandZAiModels, + mainlandZAiDefaultModelId, } from "@roo-code/types" import type { ModelRecord, RouterModels } from "@roo/api" import { useRouterModels } from "../../ui/hooks/useRouterModels" @@ -68,10 +72,12 @@ export const getModelsByProvider = ({ provider, routerModels, kilocodeDefaultModel, + options = { isChina: false }, }: { provider: ProviderName routerModels: RouterModels kilocodeDefaultModel: string + options: { isChina?: boolean } }): { models: ModelRecord; defaultModel: string } => { switch (provider) { case "openrouter": { @@ -310,6 +316,19 @@ export const getModelsByProvider = ({ defaultModel: basetenDefaultModelId, } } + case "zai": { + if (options.isChina) { + return { + models: mainlandZAiModels, + defaultModel: mainlandZAiDefaultModelId, + } + } else { + return { + models: internationalZAiModels, + defaultModel: internationalZAiDefaultModelId, + } + } + } default: return { models: {}, @@ -318,6 +337,16 @@ export const getModelsByProvider = ({ } } +export const getOptionsForProvider = (provider: ProviderName, apiConfiguration?: ProviderSettings) => { + switch (provider) { + case "zai": + // Determine which Z.AI model set to use based on the API line configuration + return { isChina: apiConfiguration?.zaiApiLine === "china_coding" } + default: + return {} + } +} + export const useProviderModels = (apiConfiguration?: ProviderSettings) => { const provider = apiConfiguration?.apiProvider || "anthropic" @@ -337,12 +366,15 @@ export const useProviderModels = (apiConfiguration?: ProviderSettings) => { syntheticApiKey: apiConfiguration?.syntheticApiKey, // kilocode_change }) + const options = getOptionsForProvider(provider, apiConfiguration) + const { models, defaultModel } = apiConfiguration && typeof routerModels.data !== "undefined" ? getModelsByProvider({ provider, routerModels: routerModels.data, kilocodeDefaultModel, + options, }) : FALLBACK_MODELS From 9b0ff1151f02b8ea22b440797779ae95b62d4cc3 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Mon, 22 Dec 2025 16:33:43 +0100 Subject: [PATCH 2/6] Show inference provider when using Vercel AI Gateway --- .../providers/kilocode/vercel-ai-gateway.ts | 19 +++++++++++++++++++ src/api/providers/openrouter.ts | 6 +++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/api/providers/kilocode/vercel-ai-gateway.ts diff --git a/src/api/providers/kilocode/vercel-ai-gateway.ts b/src/api/providers/kilocode/vercel-ai-gateway.ts new file mode 100644 index 00000000000..137c296803b --- /dev/null +++ b/src/api/providers/kilocode/vercel-ai-gateway.ts @@ -0,0 +1,19 @@ +import * as z from "zod/v4" + +export const VercelAiGatewayChunkSchema = z.object({ + choices: z + .array( + z.object({ + delta: z.object({ + provider_metadata: z.object({ + gateway: z.object({ + routing: z.object({ + resolvedProvider: z.string(), + }), + }), + }), + }), + }), + ) + .min(1), +}) diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index eb97a823479..31a70bab8b8 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -49,6 +49,7 @@ import { isAnyRecognizedKiloCodeError } from "../../shared/kilocode/errorUtils" import type { ApiHandlerCreateMessageMetadata, SingleCompletionHandler } from "../index" import { handleOpenAIError } from "./utils/openai-error-handler" import { generateImageWithProvider, ImageGenerationResult } from "./utils/image-generation" +import { VercelAiGatewayChunkSchema } from "./kilocode/vercel-ai-gateway" // Add custom interface for OpenRouter params. type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & { @@ -341,7 +342,6 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH let lastUsage: CompletionUsage | undefined = undefined let inferenceProvider: string | undefined // kilocode_change - const toolCallAccumulator = new Map() // Accumulator for reasoning_details: accumulate text by type-index key const reasoningDetailsAccumulator = new Map< string, @@ -367,6 +367,10 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH if ("provider" in chunk && typeof chunk.provider === "string") { inferenceProvider = chunk.provider } + const vercelChunk = VercelAiGatewayChunkSchema.safeParse(chunk) + if (vercelChunk.success) { + inferenceProvider = vercelChunk.data.choices[0].delta.provider_metadata.gateway.routing.resolvedProvider + } // kilocode_change end verifyFinishReason(chunk.choices[0]) // kilocode_change From 690964040770cd21248e1bea964c995d8620d8e8 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 22 Dec 2025 18:51:17 +0100 Subject: [PATCH 3/6] feat: Add Agent Manager terminal switching (#4615) * feat: Add Agent Manager terminal switching * Delete .husky/_/post-checkout * Delete .husky/_/post-commit * Delete .husky/_/post-merge * Delete .husky/_/pre-push * Fix intendation comments * fix: add missing vscode terminal mocks to IPC tests * fix: add missing terminal mocks to CLI spawning test --- .changeset/agent-terminal-switch.md | 5 + .../agent-manager/AgentManagerProvider.ts | 39 +- .../kilocode/agent-manager/AgentRegistry.ts | 34 +- .../kilocode/agent-manager/CliOutputParser.ts | 79 +++- .../agent-manager/CliProcessHandler.ts | 97 ++++- .../agent-manager/SessionTerminalManager.ts | 175 +++++++++ .../AgentManagerProvider.ipc.spec.ts | 5 +- .../__tests__/AgentManagerProvider.spec.ts | 40 +- .../__tests__/AgentRegistry.spec.ts | 7 +- .../__tests__/CliOutputParser.spec.ts | 42 ++- .../__tests__/CliProcessHandler.spec.ts | 96 ++++- .../__tests__/SessionTerminalManager.spec.ts | 344 ++++++++++++++++++ .../__tests__/parallelModeParser.spec.ts | 9 + .../agent-manager/parallelModeParser.ts | 7 + .../src/i18n/locales/ar/agentManager.json | 3 +- .../src/i18n/locales/ca/agentManager.json | 3 +- .../src/i18n/locales/cs/agentManager.json | 3 +- .../src/i18n/locales/de/agentManager.json | 3 +- .../src/i18n/locales/en/agentManager.json | 3 +- .../src/i18n/locales/es/agentManager.json | 3 +- .../src/i18n/locales/fr/agentManager.json | 3 +- .../src/i18n/locales/hi/agentManager.json | 3 +- .../src/i18n/locales/id/agentManager.json | 3 +- .../src/i18n/locales/it/agentManager.json | 3 +- .../src/i18n/locales/ja/agentManager.json | 3 +- .../src/i18n/locales/ko/agentManager.json | 3 +- .../src/i18n/locales/nl/agentManager.json | 3 +- .../src/i18n/locales/pl/agentManager.json | 3 +- .../src/i18n/locales/pt-BR/agentManager.json | 3 +- .../src/i18n/locales/ru/agentManager.json | 3 +- .../src/i18n/locales/th/agentManager.json | 3 +- .../src/i18n/locales/tr/agentManager.json | 3 +- .../src/i18n/locales/uk/agentManager.json | 3 +- .../src/i18n/locales/vi/agentManager.json | 3 +- .../src/i18n/locales/zh-CN/agentManager.json | 3 +- .../src/i18n/locales/zh-TW/agentManager.json | 3 +- .../components/SessionDetail.tsx | 30 +- .../__tests__/SessionDetail.spec.tsx | 30 ++ 38 files changed, 1039 insertions(+), 66 deletions(-) create mode 100644 .changeset/agent-terminal-switch.md create mode 100644 src/core/kilocode/agent-manager/SessionTerminalManager.ts create mode 100644 src/core/kilocode/agent-manager/__tests__/SessionTerminalManager.spec.ts diff --git a/.changeset/agent-terminal-switch.md b/.changeset/agent-terminal-switch.md new file mode 100644 index 00000000000..ffb37fa1541 --- /dev/null +++ b/.changeset/agent-terminal-switch.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Add Agent Manager terminal switching so existing session terminals are revealed when changing sessions. diff --git a/src/core/kilocode/agent-manager/AgentManagerProvider.ts b/src/core/kilocode/agent-manager/AgentManagerProvider.ts index 08fb5cde03f..5a2d4ee9c38 100644 --- a/src/core/kilocode/agent-manager/AgentManagerProvider.ts +++ b/src/core/kilocode/agent-manager/AgentManagerProvider.ts @@ -5,6 +5,7 @@ import { t } from "i18next" import { AgentRegistry } from "./AgentRegistry" import { renameMapKey } from "./mapUtils" import { + buildParallelModeWorktreePath, parseParallelModeBranch, parseParallelModeWorktreePath, isParallelModeCompletionMessage, @@ -38,6 +39,7 @@ import type { ClineProvider } from "../../webview/ClineProvider" import { extractSessionConfigs, MAX_VERSION_COUNT } from "./multiVersionUtils" import { SessionManager } from "../../../shared/kilocode/cli-sessions/core/SessionManager" import { WorkspaceGitService } from "./WorkspaceGitService" +import { SessionTerminalManager } from "./SessionTerminalManager" /** * AgentManagerProvider @@ -54,6 +56,7 @@ export class AgentManagerProvider implements vscode.Disposable { private remoteSessionService: RemoteSessionService private processHandler: CliProcessHandler private eventProcessor: KilocodeEventProcessor + private terminalManager: SessionTerminalManager private sessionMessages: Map = new Map() // Track first api_req_started per session to filter user-input echoes private firstApiReqStarted: Map = new Map() @@ -72,6 +75,7 @@ export class AgentManagerProvider implements vscode.Disposable { ) { this.registry = new AgentRegistry() this.remoteSessionService = new RemoteSessionService({ outputChannel }) + this.terminalManager = new SessionTerminalManager(this.registry, this.outputChannel) // Initialize currentGitUrl from workspace void this.initializeCurrentGitUrl() @@ -288,6 +292,9 @@ export class AgentManagerProvider implements vscode.Disposable { case "agentManager.refreshSessionMessages": void this.refreshSessionMessages(message.sessionId as string) break + case "agentManager.showTerminal": + this.terminalManager.showTerminal(message.sessionId as string) + break case "agentManager.sessionShare": SessionManager.init() ?.shareSession(message.sessionId as string) @@ -669,17 +676,42 @@ export class AgentManagerProvider implements vscode.Disposable { } /** - * Handle welcome event from CLI - extracts worktree branch for parallel mode sessions + * Handle welcome event from CLI - extracts worktree branch and path for parallel mode sessions */ private handleWelcomeEvent(sessionId: string, event: WelcomeStreamEvent): void { + let updated = false + const session = this.registry.getSession(sessionId) + const existingWorktreePath = session?.parallelMode?.worktreePath + if (event.worktreeBranch) { this.outputChannel.appendLine( `[AgentManager] Session ${sessionId} worktree branch: ${event.worktreeBranch}`, ) if (this.registry.updateParallelModeInfo(sessionId, { branch: event.worktreeBranch })) { - this.postStateToWebview() + updated = true } } + + if (event.worktreePath) { + this.outputChannel.appendLine(`[AgentManager] Session ${sessionId} worktree path: ${event.worktreePath}`) + if (this.registry.updateParallelModeInfo(sessionId, { worktreePath: event.worktreePath })) { + updated = true + } + } + + if (!event.worktreePath && event.worktreeBranch && !existingWorktreePath) { + const derivedWorktreePath = buildParallelModeWorktreePath(event.worktreeBranch) + this.outputChannel.appendLine( + `[AgentManager] Session ${sessionId} derived worktree path: ${derivedWorktreePath}`, + ) + if (this.registry.updateParallelModeInfo(sessionId, { worktreePath: derivedWorktreePath })) { + updated = true + } + } + + if (updated) { + this.postStateToWebview() + } } private handleKilocodeEvent(sessionId: string, event: KilocodeStreamEvent): void { @@ -699,6 +731,8 @@ export class AgentManagerProvider implements vscode.Disposable { if (!sessionId) return + this.terminalManager.showExistingTerminal(sessionId) + // Check if we have cached messages to send immediately const cachedMessages = this.sessionMessages.get(sessionId) if (cachedMessages) { @@ -1165,6 +1199,7 @@ export class AgentManagerProvider implements vscode.Disposable { public dispose(): void { this.processHandler.dispose() + this.terminalManager.dispose() this.sessionMessages.clear() this.firstApiReqStarted.clear() diff --git a/src/core/kilocode/agent-manager/AgentRegistry.ts b/src/core/kilocode/agent-manager/AgentRegistry.ts index 83568a947b9..b8c450834c6 100644 --- a/src/core/kilocode/agent-manager/AgentRegistry.ts +++ b/src/core/kilocode/agent-manager/AgentRegistry.ts @@ -152,13 +152,16 @@ export class AgentRegistry { info: Partial>, ): AgentSession | undefined { const session = this.sessions.get(id) - if (!session || !session.parallelMode?.enabled) { + if (!session) { return undefined } + const currentParallelMode = session.parallelMode ?? { enabled: true } + session.parallelMode = { - ...session.parallelMode, + ...currentParallelMode, ...info, + enabled: true, } return session @@ -214,19 +217,26 @@ export class AgentRegistry { * Used when upgrading a provisional session to a real session ID. */ public renameSession(oldId: string, newId: string): boolean { - const session = this.sessions.get(oldId) - if (!session) { - return false + if (oldId === newId) { + return this.sessions.has(oldId) } - // Update the session's internal ID - session.sessionId = newId - - // Move in the map - this.sessions.delete(oldId) - this.sessions.set(newId, session) + const oldSession = this.sessions.get(oldId) + if (!oldSession) { + return false + } - // Update selectedId if it was pointing to the old ID + const targetSession = this.sessions.get(newId) + if (targetSession) { + // Prefer keeping the target session object (e.g. resuming an existing session), + // but merge in any provisional logs so early streaming isn't lost. + targetSession.logs = [...oldSession.logs, ...targetSession.logs] + this.sessions.delete(oldId) + } else { + this.sessions.delete(oldId) + oldSession.sessionId = newId + this.sessions.set(newId, oldSession) + } if (this._selectedId === oldId) { this._selectedId = newId } diff --git a/src/core/kilocode/agent-manager/CliOutputParser.ts b/src/core/kilocode/agent-manager/CliOutputParser.ts index 425c5b4cdaf..d5ea2b1b186 100644 --- a/src/core/kilocode/agent-manager/CliOutputParser.ts +++ b/src/core/kilocode/agent-manager/CliOutputParser.ts @@ -81,6 +81,7 @@ export interface SessionTitleGeneratedStreamEvent { export interface WelcomeStreamEvent { streamEventType: "welcome" worktreeBranch?: string + worktreePath?: string timestamp: number /** Configuration error instructions from CLI (indicates misconfigured CLI) */ instructions?: string[] @@ -153,8 +154,20 @@ export function parseCliChunk(chunk: string, buffer: string = ""): ParseResult { continue } - // Not JSON - strip VT characters before treating as plain text output event + // Fallback: handle concatenated JSON objects on a single line const cleanLine = stripVTControlCharacters(trimmedLine) + const extracted = extractJsonObjects(cleanLine) + if (extracted.length > 0) { + for (const obj of extracted) { + const extractedEvent = toStreamEvent(obj) + if (extractedEvent !== null) { + events.push(extractedEvent) + } + } + continue + } + + // Not JSON - treat as plain text output event if (cleanLine) { events.push(createOutputEvent(cleanLine)) } @@ -202,6 +215,13 @@ export class CliOutputParser { // Not JSON - strip VT characters before treating as plain text const cleanLine = stripVTControlCharacters(trimmedBuffer) + const extracted = extractJsonObjects(cleanLine) + if (extracted.length > 0) { + const events = extracted + .map((obj) => toStreamEvent(obj)) + .filter((extractedEvent): extractedEvent is StreamEvent => extractedEvent !== null) + return { events, remainingBuffer: "" } + } if (cleanLine) { return { events: [createOutputEvent(cleanLine)], remainingBuffer: "" } } @@ -243,14 +263,16 @@ function toStreamEvent(parsed: Record): StreamEvent | null { } } - // Detect welcome event from CLI (format: { type: "welcome", metadata: { welcomeOptions: { worktreeBranch: "...", instructions: [...] } }, ... }) + // Detect welcome event from CLI (format: { type: "welcome", metadata: { welcomeOptions: { worktreeBranch: "...", workspace: "...", instructions: [...] } }, ... }) if (parsed.type === "welcome") { const metadata = parsed.metadata as Record | undefined const welcomeOptions = metadata?.welcomeOptions as Record | undefined + const worktreePath = (welcomeOptions?.workspace || welcomeOptions?.worktreePath) as string | undefined const instructions = welcomeOptions?.instructions as string[] | undefined return { streamEventType: "welcome", worktreeBranch: welcomeOptions?.worktreeBranch as string | undefined, + worktreePath, timestamp: (parsed.timestamp as number) || Date.now(), instructions: Array.isArray(instructions) && instructions.length > 0 ? instructions : undefined, } @@ -271,6 +293,59 @@ function toStreamEvent(parsed: Record): StreamEvent | null { } } +function extractJsonObjects(line: string): Record[] { + const objects: Record[] = [] + let depth = 0 + let startIndex = -1 + let inString = false + let isEscaped = false + + for (let i = 0; i < line.length; i++) { + const ch = line[i] + + if (isEscaped) { + isEscaped = false + continue + } + + if (inString) { + if (ch === "\\") { + isEscaped = true + } else if (ch === '"') { + inString = false + } + continue + } + + if (ch === '"') { + inString = true + continue + } + + if (ch === "{") { + if (depth === 0) { + startIndex = i + } + depth += 1 + continue + } + + if (ch === "}") { + depth -= 1 + if (depth === 0 && startIndex !== -1) { + const candidate = line.slice(startIndex, i + 1) + const parsed = tryParseJson(candidate) + if (parsed) { + objects.push(parsed) + } + startIndex = -1 + } + } + } + + return objects +} + function createOutputEvent(content: string): OutputStreamEvent { return { streamEventType: "output", diff --git a/src/core/kilocode/agent-manager/CliProcessHandler.ts b/src/core/kilocode/agent-manager/CliProcessHandler.ts index 3259fd45a55..3d5cc979a47 100644 --- a/src/core/kilocode/agent-manager/CliProcessHandler.ts +++ b/src/core/kilocode/agent-manager/CliProcessHandler.ts @@ -10,6 +10,7 @@ import { } from "./CliOutputParser" import { AgentRegistry } from "./AgentRegistry" import { buildCliArgs } from "./CliArgsBuilder" +import { buildParallelModeWorktreePath } from "./parallelModeParser" import type { ClineMessage, ProviderSettings } from "@roo-code/types" import { extractApiReqFailedMessage, extractPayloadMessage } from "./askErrorParser" import { buildProviderEnvOverrides } from "./providerEnvMapper" @@ -40,6 +41,8 @@ interface PendingProcessInfo { desiredSessionId?: string desiredLabel?: string worktreeBranch?: string // Captured from welcome event before session_created + worktreePath?: string // Captured from welcome event before session_created + provisionalSessionId?: string // Used to show streaming content before session_created sawApiReqStarted?: boolean // Track if api_req_started arrived before session_created gitUrl?: string stderrBuffer: string[] // Capture stderr for error detection @@ -47,7 +50,6 @@ interface PendingProcessInfo { 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) } @@ -70,9 +72,9 @@ export interface CliProcessHandlerCallbacks { ) => void onChatMessages: (sessionId: string, messages: ClineMessage[]) => void onSessionCreated: (sawApiReqStarted: boolean) => void + onSessionRenamed?: (oldId: string, newId: string) => void onPaymentRequiredPrompt?: (payload: KilocodePayload) => void onSessionCompleted?: (sessionId: string, exitCode: number | null) => void // Called when process exits successfully - onSessionRenamed?: (oldId: string, newId: string) => void // Called when provisional session is upgraded to real session ID } export class CliProcessHandler { @@ -400,10 +402,34 @@ export class CliProcessHandler { // Capture worktree branch and configuration errors from welcome event (arrives before session_created) if (event.streamEventType === "welcome") { const welcomeEvent = event as WelcomeStreamEvent + const derivedWorktreePath = + !welcomeEvent.worktreePath && welcomeEvent.worktreeBranch + ? buildParallelModeWorktreePath(welcomeEvent.worktreeBranch) + : undefined + const resolvedWorktreePath = welcomeEvent.worktreePath ?? derivedWorktreePath + if (welcomeEvent.worktreeBranch) { this.pendingProcess.worktreeBranch = welcomeEvent.worktreeBranch this.debugLog(`Captured worktree branch from welcome: ${welcomeEvent.worktreeBranch}`) } + if (resolvedWorktreePath) { + this.pendingProcess.worktreePath = resolvedWorktreePath + const logMessage = derivedWorktreePath + ? `Derived worktree path from branch: ${resolvedWorktreePath}` + : `Captured worktree path from welcome: ${resolvedWorktreePath}` + this.callbacks.onLog(logMessage) + this.debugLog(logMessage) + } + if ( + this.pendingProcess.parallelMode && + this.pendingProcess.provisionalSessionId && + (welcomeEvent.worktreeBranch || resolvedWorktreePath) + ) { + this.registry.updateParallelModeInfo(this.pendingProcess.provisionalSessionId, { + branch: welcomeEvent.worktreeBranch, + worktreePath: resolvedWorktreePath, + }) + } const configError = this.extractConfigErrorFromWelcome(welcomeEvent) if (configError) { this.pendingProcess.configurationError = configError @@ -411,7 +437,7 @@ export class CliProcessHandler { } return } - // Handle kilocode events during pending state + if (event.streamEventType === "kilocode") { const payload = (event as KilocodeStreamEvent).payload @@ -425,28 +451,28 @@ export class CliProcessHandler { return } - // Track api_req_started for KilocodeEventProcessor + // Track api_req_started that arrives before session_created + // This is needed so KilocodeEventProcessor knows the user echo has already happened if (payload?.say === "api_req_started") { this.pendingProcess.sawApiReqStarted = true this.debugLog(`Captured api_req_started before session_created`) } - // Create provisional session on first content event (text, api_req_started, etc.) - // This ensures we don't lose the user's prompt echo or other early events + // Create provisional session on first content event (prompt echo, api_req_started, streaming text, etc.) if (!this.pendingProcess.provisionalSessionId) { this.createProvisionalSession(proc) } // Forward the event to the provisional session - if (this.pendingProcess?.provisionalSessionId) { + if (this.pendingProcess.provisionalSessionId) { onCliEvent(this.pendingProcess.provisionalSessionId, event) this.callbacks.onStateChanged() } return } - // If we have a provisional session, forward non-kilocode events to it - if (this.pendingProcess?.provisionalSessionId) { + // If we have a provisional session, forward non-kilocode events to it as well + if (this.pendingProcess.provisionalSessionId) { onCliEvent(this.pendingProcess.provisionalSessionId, event) this.callbacks.onStateChanged() return @@ -476,7 +502,8 @@ export class CliProcessHandler { const provisionalId = `provisional-${Date.now()}` this.pendingProcess.provisionalSessionId = provisionalId - const { prompt, startTime, parallelMode, desiredLabel, gitUrl, parser } = this.pendingProcess + const { prompt, startTime, parallelMode, desiredLabel, gitUrl, parser, worktreeBranch, worktreePath } = + this.pendingProcess this.registry.createSession(provisionalId, prompt, startTime, { parallelMode, @@ -484,6 +511,13 @@ export class CliProcessHandler { gitUrl, }) + if (parallelMode && (worktreeBranch || worktreePath)) { + this.registry.updateParallelModeInfo(provisionalId, { + branch: worktreeBranch, + worktreePath, + }) + } + this.activeSessions.set(provisionalId, { process: proc, parser }) if (proc.pid) { @@ -503,6 +537,7 @@ export class CliProcessHandler { provisionalSessionId: string, realSessionId: string, worktreeBranch: string | undefined, + worktreePath: string | undefined, parallelMode: boolean | undefined, ): void { this.debugLog(`Upgrading provisional session ${provisionalSessionId} -> ${realSessionId}`) @@ -517,8 +552,11 @@ export class CliProcessHandler { this.callbacks.onSessionRenamed?.(provisionalSessionId, realSessionId) - if (worktreeBranch && parallelMode) { - this.registry.updateParallelModeInfo(realSessionId, { branch: worktreeBranch }) + if (parallelMode && (worktreeBranch || worktreePath)) { + this.registry.updateParallelModeInfo(realSessionId, { + branch: worktreeBranch, + worktreePath, + }) } this.pendingProcess = null @@ -577,6 +615,7 @@ export class CliProcessHandler { parser, parallelMode, worktreeBranch, + worktreePath, desiredSessionId, desiredLabel, sawApiReqStarted, @@ -587,13 +626,24 @@ export class CliProcessHandler { // Use desired sessionId when provided (resuming) to keep UI continuity const sessionId = desiredSessionId ?? event.sessionId - // Handle provisional session upgrade if one exists - if (provisionalSessionId) { - this.upgradeProvisionalSession(provisionalSessionId, sessionId, worktreeBranch, parallelMode) + // If we created a provisional session, upgrade it instead of creating a second session entry. + // In some edge cases, fall back to the active session mapped to this process. + const provisionalIdFromProcess = this.findSessionIdForProcess(proc) + const effectiveProvisionalSessionId = + provisionalSessionId ?? + (provisionalIdFromProcess?.startsWith("provisional-") ? provisionalIdFromProcess : undefined) + + if (effectiveProvisionalSessionId && effectiveProvisionalSessionId !== sessionId) { + this.upgradeProvisionalSession( + effectiveProvisionalSessionId, + sessionId, + worktreeBranch, + worktreePath, + parallelMode, + ) return } - // Normal path: no provisional session, create the session now const existing = this.registry.getSession(sessionId) let session: ReturnType @@ -622,6 +672,19 @@ export class CliProcessHandler { this.debugLog(`Applied worktree branch: ${worktreeBranch}`) } + const resolvedWorktreePath = + worktreePath || (parallelMode && worktreeBranch ? buildParallelModeWorktreePath(worktreeBranch) : undefined) + + if (resolvedWorktreePath && parallelMode) { + this.registry.updateParallelModeInfo(session.sessionId, { worktreePath: resolvedWorktreePath }) + const logMessage = + worktreePath && worktreePath === resolvedWorktreePath + ? `Applied worktree path: ${resolvedWorktreePath}` + : `Applied derived worktree path: ${resolvedWorktreePath}` + this.callbacks.onLog(logMessage) + this.debugLog(logMessage) + } + // Clear pending session state this.registry.clearPendingSession() this.pendingProcess = null @@ -766,7 +829,7 @@ export class CliProcessHandler { this.callbacks.onPendingSessionChanged(null) this.pendingProcess = null - // Capture spawn error telemetry with context for debugging + // Capture spawn error telemetry with context for debugging. const { platform, shell } = getPlatformDiagnostics() const cliPathExtension = cliPath ? path.extname(cliPath).slice(1).toLowerCase() || undefined : undefined captureAgentManagerLoginIssue({ diff --git a/src/core/kilocode/agent-manager/SessionTerminalManager.ts b/src/core/kilocode/agent-manager/SessionTerminalManager.ts new file mode 100644 index 00000000000..3206b9f64ec --- /dev/null +++ b/src/core/kilocode/agent-manager/SessionTerminalManager.ts @@ -0,0 +1,175 @@ +import * as vscode from "vscode" +import type { AgentRegistry } from "./AgentRegistry" +import { buildParallelModeWorktreePath } from "./parallelModeParser" + +/** + * Manages VS Code terminals for agent sessions. + * Each session can have an associated terminal that opens in the session's worktree directory, + * or the main workspace folder for non-worktree sessions. + */ +export class SessionTerminalManager { + private sessionTerminals = new Map() + private disposables: vscode.Disposable[] = [] + + constructor( + private registry: AgentRegistry, + private outputChannel: vscode.OutputChannel, + ) { + // Clean up map when terminals are closed by user + this.disposables.push( + vscode.window.onDidCloseTerminal((terminal) => { + for (const [sessionId, entry] of this.sessionTerminals) { + if (entry.terminal === terminal) { + this.sessionTerminals.delete(sessionId) + this.outputChannel.appendLine( + `[AgentManager] Removed terminal mapping for session ${sessionId} (terminal closed)`, + ) + break + } + } + }), + ) + } + + /** + * Get the current workspace folder path. + */ + private getWorkspacePath(): string | undefined { + return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath + } + + /** + * Show (or create) a terminal for the given session. + * For worktree sessions, the terminal's cwd is set to the worktree path. + * For local sessions, the terminal's cwd is set to the main workspace folder. + */ + showTerminal(sessionId: string): void { + const session = this.registry.getSession(sessionId) + if (!session) { + this.outputChannel.appendLine(`[AgentManager] showTerminal: session not found (${sessionId})`) + vscode.window.showWarningMessage("Session not found") + return + } + + const isParallelSession = session.parallelMode?.enabled ?? false + const branchName = session.parallelMode?.branch + let worktreePath = session.parallelMode?.worktreePath + if (isParallelSession && !worktreePath && branchName) { + worktreePath = buildParallelModeWorktreePath(branchName) + this.registry.updateParallelModeInfo(sessionId, { worktreePath }) + this.outputChannel.appendLine( + `[AgentManager] showTerminal: derived worktree path from branch ${branchName}: ${worktreePath}`, + ) + } + const workspacePath = this.getWorkspacePath() + this.outputChannel.appendLine( + `[AgentManager] showTerminal: session=${sessionId} parallel=${isParallelSession} worktreePath=${worktreePath ?? "undefined"} workspacePath=${workspacePath ?? "undefined"}`, + ) + + if (isParallelSession && !worktreePath) { + this.outputChannel.appendLine( + `[AgentManager] showTerminal: worktree path missing for parallel session ${sessionId}`, + ) + vscode.window.showWarningMessage("Worktree path not available yet") + return + } + + const terminalCwd = worktreePath || workspacePath + + if (!terminalCwd) { + this.outputChannel.appendLine(`[AgentManager] showTerminal: no cwd resolved for session ${sessionId}`) + vscode.window.showWarningMessage("No workspace folder open") + return + } + + let entry = this.sessionTerminals.get(sessionId) + + // Check if terminal still exists (user might have closed it) + if (entry && entry.terminal.exitStatus !== undefined) { + this.sessionTerminals.delete(sessionId) + entry = undefined + this.outputChannel.appendLine( + `[AgentManager] showTerminal: previous terminal exited for session ${sessionId}`, + ) + } + + if (entry && entry.cwd !== terminalCwd) { + entry.terminal.dispose() + this.sessionTerminals.delete(sessionId) + entry = undefined + this.outputChannel.appendLine( + `[AgentManager] showTerminal: terminal cwd changed for session ${sessionId}, recreating (${terminalCwd})`, + ) + } + + if (!entry) { + // For worktree sessions, use branch name; for local sessions, use session label + const terminalName = worktreePath + ? `Agent: ${session.parallelMode?.branch || session.label}` + : `Agent: ${session.label} (local)` + const terminal = vscode.window.createTerminal({ + cwd: terminalCwd, + name: terminalName, + iconPath: new vscode.ThemeIcon("terminal"), + }) + entry = { terminal, cwd: terminalCwd } + this.sessionTerminals.set(sessionId, entry) + this.outputChannel.appendLine( + `[AgentManager] showTerminal: created terminal for session ${sessionId} (cwd=${terminalCwd})`, + ) + } + + entry.terminal.show() + } + + /** + * Close the terminal associated with a session. + */ + closeTerminal(sessionId: string): void { + const entry = this.sessionTerminals.get(sessionId) + if (entry) { + entry.terminal.dispose() + this.sessionTerminals.delete(sessionId) + this.outputChannel.appendLine(`[AgentManager] closeTerminal: disposed terminal for ${sessionId}`) + } + } + + /** + * Show the terminal for a session only if it already exists. + */ + showExistingTerminal(sessionId: string): boolean { + const entry = this.sessionTerminals.get(sessionId) + if (!entry) { + return false + } + + if (entry.terminal.exitStatus !== undefined) { + this.sessionTerminals.delete(sessionId) + this.outputChannel.appendLine( + `[AgentManager] showExistingTerminal: terminal exited for session ${sessionId}, clearing mapping`, + ) + return false + } + + entry.terminal.show() + this.outputChannel.appendLine(`[AgentManager] showExistingTerminal: revealed terminal for session ${sessionId}`) + return true + } + + /** + * Check if a session has an active terminal. + */ + hasTerminal(sessionId: string): boolean { + const entry = this.sessionTerminals.get(sessionId) + return entry !== undefined && entry.terminal.exitStatus === undefined + } + + /** + * Dispose of all resources. + */ + dispose(): void { + this.disposables.forEach((d) => d.dispose()) + // Optionally close all agent terminals on extension deactivate + // For now, we leave them open so users can continue using them + } +} diff --git a/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.ipc.spec.ts b/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.ipc.spec.ts index 63f71273770..cbedc512037 100644 --- a/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.ipc.spec.ts +++ b/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.ipc.spec.ts @@ -9,6 +9,8 @@ vi.mock("vscode", () => { const window = { showErrorMessage: vi.fn(), showWarningMessage: vi.fn(), + onDidCloseTerminal: vi.fn(() => ({ dispose: vi.fn() })), + createTerminal: vi.fn(() => ({ show: vi.fn(), dispose: vi.fn() })), } const Uri = { joinPath: vi.fn(), @@ -22,7 +24,8 @@ vi.mock("vscode", () => { Production: 2, Test: 3, } - return { window, Uri, workspace, ExtensionMode } + const ThemeIcon = vi.fn() + return { window, Uri, workspace, ExtensionMode, ThemeIcon } }) describe("AgentManagerProvider IPC paths", () => { diff --git a/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts b/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts index c98de1e47ae..acd03f0e240 100644 --- a/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts +++ b/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts @@ -33,7 +33,13 @@ describe("AgentManagerProvider CLI spawning", () => { let provider: InstanceType const mockContext = { extensionUri: {}, extensionPath: "", extensionMode: 1 /* Development */ } as any const mockOutputChannel = { appendLine: vi.fn() } as any - let mockWindow: { showErrorMessage: Mock; showWarningMessage: Mock; ViewColumn: { One: number } } + let mockWindow: { + showErrorMessage: Mock + showWarningMessage: Mock + ViewColumn: { One: number } + onDidCloseTerminal: Mock + createTerminal: Mock + } beforeEach(async () => { vi.resetModules() @@ -47,6 +53,8 @@ describe("AgentManagerProvider CLI spawning", () => { showErrorMessage: vi.fn().mockResolvedValue(undefined), showWarningMessage: vi.fn().mockResolvedValue(undefined), ViewColumn: { One: 1 }, + onDidCloseTerminal: vi.fn().mockReturnValue({ dispose: vi.fn() }), + createTerminal: vi.fn().mockReturnValue({ show: vi.fn(), sendText: vi.fn(), dispose: vi.fn() }), } vi.doMock("vscode", () => ({ @@ -131,11 +139,14 @@ describe("AgentManagerProvider CLI spawning", () => { showErrorMessage: vi.fn().mockResolvedValue(undefined), showWarningMessage: vi.fn().mockResolvedValue(undefined), ViewColumn: { One: 1 }, + onDidCloseTerminal: vi.fn().mockReturnValue({ dispose: vi.fn() }), + createTerminal: vi.fn().mockReturnValue({ show: vi.fn(), sendText: vi.fn(), dispose: vi.fn() }), }, env: { openExternal: vi.fn() }, Uri: { parse: vi.fn(), joinPath: vi.fn() }, ViewColumn: { One: 1 }, ExtensionMode: { Development: 1, Production: 2, Test: 3 }, + ThemeIcon: vi.fn(), })) vi.doMock("../../../../utils/fs", () => ({ @@ -235,6 +246,19 @@ describe("AgentManagerProvider CLI spawning", () => { expect(sessions[0].sessionId).toBe("cli-session-123") }) + it("shows existing terminal when selecting a session", () => { + const sessionId = "session-terminal" + const registry = (provider as any).registry + registry.createSession(sessionId, "prompt") + ;(provider as any).sessionMessages.set(sessionId, []) + + const showExistingTerminal = vi.spyOn((provider as any).terminalManager, "showExistingTerminal") + + ;(provider as any).selectSession(sessionId) + + expect(showExistingTerminal).toHaveBeenCalledWith(sessionId) + }) + it("adds metadata text for tool requests and skips non chat events", async () => { const registry = (provider as any).registry const sessionId = "test-session-meta" @@ -596,7 +620,12 @@ describe("AgentManagerProvider gitUrl filtering", () => { vi.resetModules() const mockWorkspaceFolder = { uri: { fsPath: "/tmp/workspace" } } - const mockWindow = { showErrorMessage: () => undefined, ViewColumn: { One: 1 } } + const mockWindow = { + showErrorMessage: () => undefined, + ViewColumn: { One: 1 }, + onDidCloseTerminal: vi.fn().mockReturnValue({ dispose: vi.fn() }), + createTerminal: vi.fn().mockReturnValue({ show: vi.fn(), sendText: vi.fn(), dispose: vi.fn() }), + } const mockProvider = { getState: vi.fn().mockResolvedValue({ apiConfiguration: { apiProvider: "kilocode" } }), } @@ -823,7 +852,12 @@ describe("AgentManagerProvider telemetry", () => { vi.clearAllMocks() const mockWorkspaceFolder = { uri: { fsPath: "/tmp/workspace" } } - const mockWindow = { showErrorMessage: () => undefined, ViewColumn: { One: 1 } } + const mockWindow = { + showErrorMessage: () => undefined, + ViewColumn: { One: 1 }, + onDidCloseTerminal: vi.fn().mockReturnValue({ dispose: vi.fn() }), + createTerminal: vi.fn().mockReturnValue({ show: vi.fn(), sendText: vi.fn(), dispose: vi.fn() }), + } const mockProvider = { getState: vi.fn().mockResolvedValue({ apiConfiguration: { apiProvider: "kilocode" } }), } diff --git a/src/core/kilocode/agent-manager/__tests__/AgentRegistry.spec.ts b/src/core/kilocode/agent-manager/__tests__/AgentRegistry.spec.ts index fb8e097ad1c..82089592f8c 100644 --- a/src/core/kilocode/agent-manager/__tests__/AgentRegistry.spec.ts +++ b/src/core/kilocode/agent-manager/__tests__/AgentRegistry.spec.ts @@ -289,10 +289,13 @@ describe("AgentRegistry", () => { expect(result).toBeUndefined() }) - it("returns undefined when updating session without parallelMode enabled", () => { + it("enables parallelMode when updating a session without parallelMode", () => { const session = registry.createSession("session-1", "no parallel mode") const result = registry.updateParallelModeInfo(session.sessionId, { branch: "test" }) - expect(result).toBeUndefined() + expect(result?.parallelMode).toEqual({ + enabled: true, + branch: "test", + }) }) it("preserves parallelMode info in getState", () => { diff --git a/src/core/kilocode/agent-manager/__tests__/CliOutputParser.spec.ts b/src/core/kilocode/agent-manager/__tests__/CliOutputParser.spec.ts index fcbd3cc41f9..a8afa53961c 100644 --- a/src/core/kilocode/agent-manager/__tests__/CliOutputParser.spec.ts +++ b/src/core/kilocode/agent-manager/__tests__/CliOutputParser.spec.ts @@ -104,14 +104,15 @@ describe("parseCliChunk", () => { expect(event.timestamp).toBeLessThanOrEqual(after) }) - it("should parse welcome event with worktree branch", () => { + it("should parse welcome event with worktree branch and path", () => { const result = parseCliChunk( - '{"type":"welcome","metadata":{"welcomeOptions":{"worktreeBranch":"feature/test-branch"}},"timestamp":1234567890}\n', + '{"type":"welcome","metadata":{"welcomeOptions":{"worktreeBranch":"feature/test-branch","workspace":"/tmp/worktree-path"}},"timestamp":1234567890}\n', ) expect(result.events).toHaveLength(1) expect(result.events[0]).toEqual({ streamEventType: "welcome", worktreeBranch: "feature/test-branch", + worktreePath: "/tmp/worktree-path", timestamp: 1234567890, instructions: undefined, }) @@ -123,6 +124,7 @@ describe("parseCliChunk", () => { expect(result.events[0]).toEqual({ streamEventType: "welcome", worktreeBranch: undefined, + worktreePath: undefined, timestamp: 1234567890, instructions: undefined, }) @@ -214,6 +216,23 @@ describe("parseCliChunk", () => { expect(result.events[0]).toMatchObject({ streamEventType: "kilocode", payload: { timestamp: 123 } }) }) + it("should parse concatenated JSON objects on a single line", () => { + const input = + '{"type":"welcome","metadata":{"welcomeOptions":{"worktreeBranch":"feature/test","workspace":"/tmp/worktree"}},"timestamp":1}' + + '{"event":"session_created","sessionId":"sess-1","timestamp":2}\n' + const result = parseCliChunk(input) + expect(result.events).toHaveLength(2) + expect(result.events[0]).toMatchObject({ + streamEventType: "welcome", + worktreeBranch: "feature/test", + worktreePath: "/tmp/worktree", + }) + expect(result.events[1]).toMatchObject({ + streamEventType: "session_created", + sessionId: "sess-1", + }) + }) + it("should collect non-JSON lines as output events with VT codes stripped", () => { const input = 'not json\n{"timestamp":123,"source":"cli","type":"info"}\nalso not json\n' const result = parseCliChunk(input) @@ -304,4 +323,23 @@ describe("CliOutputParser class", () => { expect(events[1]).toMatchObject({ streamEventType: "kilocode", payload: { content: "Hello World" } }) expect(events[2]).toMatchObject({ streamEventType: "status" }) }) + + it("should flush concatenated JSON objects", () => { + const parser = new CliOutputParser() + parser.parse( + '{"type":"welcome","metadata":{"welcomeOptions":{"worktreeBranch":"feature/test","workspace":"/tmp/worktree"}},"timestamp":1}' + + '{"event":"session_created","sessionId":"sess-1","timestamp":2}', + ) + const result = parser.flush() + expect(result.events).toHaveLength(2) + expect(result.events[0]).toMatchObject({ + streamEventType: "welcome", + worktreeBranch: "feature/test", + worktreePath: "/tmp/worktree", + }) + expect(result.events[1]).toMatchObject({ + streamEventType: "session_created", + sessionId: "sess-1", + }) + }) }) diff --git a/src/core/kilocode/agent-manager/__tests__/CliProcessHandler.spec.ts b/src/core/kilocode/agent-manager/__tests__/CliProcessHandler.spec.ts index 7f9dad552f4..ee078ee80d4 100644 --- a/src/core/kilocode/agent-manager/__tests__/CliProcessHandler.spec.ts +++ b/src/core/kilocode/agent-manager/__tests__/CliProcessHandler.spec.ts @@ -12,6 +12,11 @@ vi.mock("node:child_process", () => ({ spawn: vi.fn(), })) +vi.mock("../telemetry", () => ({ + captureAgentManagerLoginIssue: vi.fn(), + getPlatformDiagnostics: vi.fn(() => ({ platform: "darwin", shell: "bash" })), +})) + /** * Creates a mock ChildProcess with EventEmitter capabilities */ @@ -504,18 +509,19 @@ describe("CliProcessHandler", () => { expect(registry.getSessions()).toHaveLength(0) }) - it("captures worktree branch from welcome event and applies it on session creation", () => { + it("captures worktree branch and path from welcome event and applies them on session creation", () => { const onCliEvent = vi.fn() // Start in parallel mode handler.spawnProcess("/path/to/kilocode", "/workspace", "test prompt", { parallelMode: true }, onCliEvent) // First, emit welcome event with worktree branch (this arrives before session_created) const welcomeEvent = - '{"type":"welcome","metadata":{"welcomeOptions":{"worktreeBranch":"feature/test-branch"}}}\n' + '{"type":"welcome","metadata":{"welcomeOptions":{"worktreeBranch":"feature/test-branch","workspace":"/tmp/worktree-path"}}}\n' mockProcess.stdout.emit("data", Buffer.from(welcomeEvent)) // Verify branch was captured in pending process expect((handler as any).pendingProcess.worktreeBranch).toBe("feature/test-branch") + expect((handler as any).pendingProcess.worktreePath).toBe("/tmp/worktree-path") // Then emit session_created mockProcess.stdout.emit("data", Buffer.from('{"event":"session_created","sessionId":"session-1"}\n')) @@ -524,6 +530,24 @@ describe("CliProcessHandler", () => { const session = registry.getSession("session-1") expect(session?.parallelMode?.enabled).toBe(true) expect(session?.parallelMode?.branch).toBe("feature/test-branch") + expect(session?.parallelMode?.worktreePath).toBe("/tmp/worktree-path") + }) + + it("derives worktree path from branch when welcome event has no workspace", () => { + const onCliEvent = vi.fn() + const branch = "feature/test-branch" + handler.spawnProcess("/path/to/kilocode", "/workspace", "test prompt", { parallelMode: true }, onCliEvent) + + const welcomeEvent = `{"type":"welcome","metadata":{"welcomeOptions":{"worktreeBranch":"${branch}"}}}\n` + mockProcess.stdout.emit("data", Buffer.from(welcomeEvent)) + + const expectedPath = path.join(os.tmpdir(), `kilocode-worktree-${branch}`) + expect((handler as any).pendingProcess.worktreePath).toBe(expectedPath) + + mockProcess.stdout.emit("data", Buffer.from('{"event":"session_created","sessionId":"session-1"}\n')) + + const session = registry.getSession("session-1") + expect(session?.parallelMode?.worktreePath).toBe(expectedPath) }) it("does not apply worktree branch when not in parallel mode", () => { @@ -1167,6 +1191,30 @@ describe("CliProcessHandler", () => { }), ) }) + + it("emits spawn error telemetry when process fails during pending state", async () => { + const telemetry = await import("../telemetry") + + const onCliEvent = vi.fn() + handler.spawnProcess("/path/to/kilocode.cmd", "/workspace", "test prompt", undefined, onCliEvent) + + mockProcess.emit("error", new Error("ENOENT")) + + expect(telemetry.getPlatformDiagnostics).toHaveBeenCalled() + expect(telemetry.captureAgentManagerLoginIssue).toHaveBeenCalledWith( + expect.objectContaining({ + issueType: "cli_spawn_error", + platform: "darwin", + shell: "bash", + errorMessage: "ENOENT", + cliPath: "/path/to/kilocode.cmd", + cliPathExtension: "cmd", + }), + ) + expect(callbacks.onStartSessionFailed).toHaveBeenCalledWith( + expect.objectContaining({ type: "spawn_error", message: "ENOENT" }), + ) + }) }) describe("pending session timeout", () => { @@ -1464,6 +1512,50 @@ describe("CliProcessHandler", () => { expect(registry.getSession("real-session-123")).toBeDefined() }) + it("applies worktree info to provisional session from welcome event", () => { + const onCliEvent = vi.fn() + handler.spawnProcess("/path/to/kilocode", "/workspace", "test prompt", { parallelMode: true }, onCliEvent) + + const welcomeEvent = + '{"type":"welcome","metadata":{"welcomeOptions":{"worktreeBranch":"feature/test-branch","workspace":"/tmp/worktree-path"}}}\n' + mockProcess.stdout.emit("data", Buffer.from(welcomeEvent)) + + const kilocodeEvent = JSON.stringify({ + streamEventType: "kilocode", + payload: { type: "say", say: "user_feedback", content: "test prompt" }, + }) + mockProcess.stdout.emit("data", Buffer.from(kilocodeEvent + "\n")) + + const provisionalSession = registry.getSessions()[0] + expect(provisionalSession.sessionId).toMatch(/^provisional-/) + expect(provisionalSession.parallelMode?.branch).toBe("feature/test-branch") + expect(provisionalSession.parallelMode?.worktreePath).toBe("/tmp/worktree-path") + }) + + it("preserves worktree path when provisional session is upgraded", () => { + const onCliEvent = vi.fn() + handler.spawnProcess("/path/to/kilocode", "/workspace", "test prompt", { parallelMode: true }, onCliEvent) + + const welcomeEvent = + '{"type":"welcome","metadata":{"welcomeOptions":{"worktreeBranch":"feature/test-branch","workspace":"/tmp/worktree-path"}}}\n' + mockProcess.stdout.emit("data", Buffer.from(welcomeEvent)) + + const kilocodeEvent = JSON.stringify({ + streamEventType: "kilocode", + payload: { type: "say", say: "user_feedback", content: "test prompt" }, + }) + mockProcess.stdout.emit("data", Buffer.from(kilocodeEvent + "\n")) + + const provisionalId = registry.getSessions()[0].sessionId + expect(provisionalId).toMatch(/^provisional-/) + + mockProcess.stdout.emit("data", Buffer.from('{"event":"session_created","sessionId":"real-session-123"}\n')) + + const session = registry.getSession("real-session-123") + expect(session?.parallelMode?.branch).toBe("feature/test-branch") + expect(session?.parallelMode?.worktreePath).toBe("/tmp/worktree-path") + }) + it("calls onSessionRenamed callback when provisional session is upgraded", () => { const onCliEvent = vi.fn() handler.spawnProcess("/path/to/kilocode", "/workspace", "test prompt", undefined, onCliEvent) diff --git a/src/core/kilocode/agent-manager/__tests__/SessionTerminalManager.spec.ts b/src/core/kilocode/agent-manager/__tests__/SessionTerminalManager.spec.ts new file mode 100644 index 00000000000..5a80707cdce --- /dev/null +++ b/src/core/kilocode/agent-manager/__tests__/SessionTerminalManager.spec.ts @@ -0,0 +1,344 @@ +import { describe, it, expect, beforeEach, vi, type Mock } from "vitest" +import os from "node:os" +import path from "node:path" +import * as vscode from "vscode" +import { SessionTerminalManager } from "../SessionTerminalManager" +import { AgentRegistry } from "../AgentRegistry" + +const MOCK_WORKSPACE_PATH = "/tmp/workspace" + +// Mock vscode module +vi.mock("vscode", () => ({ + window: { + createTerminal: vi.fn(), + showWarningMessage: vi.fn(), + onDidCloseTerminal: vi.fn(() => ({ dispose: vi.fn() })), + }, + workspace: { + workspaceFolders: [{ uri: { fsPath: "/tmp/workspace" } }], + }, + ThemeIcon: vi.fn().mockImplementation((id: string) => ({ id })), +})) + +describe("SessionTerminalManager", () => { + let registry: AgentRegistry + let terminalManager: SessionTerminalManager + let mockTerminal: { + show: Mock + dispose: Mock + exitStatus: undefined | { code: number } + } + let onDidCloseTerminalCallback: ((terminal: unknown) => void) | undefined + let mockOutputChannel: { appendLine: Mock } + + beforeEach(() => { + vi.clearAllMocks() + + registry = new AgentRegistry() + + // Capture the onDidCloseTerminal callback + ;(vscode.window.onDidCloseTerminal as Mock).mockImplementation((callback) => { + onDidCloseTerminalCallback = callback + return { dispose: vi.fn() } + }) + + // Create mock terminal + mockTerminal = { + show: vi.fn(), + dispose: vi.fn(), + exitStatus: undefined, + } + ;(vscode.window.createTerminal as Mock).mockReturnValue(mockTerminal) + + mockOutputChannel = { appendLine: vi.fn() } + terminalManager = new SessionTerminalManager(registry, mockOutputChannel as unknown as vscode.OutputChannel) + }) + + describe("showTerminal", () => { + it("shows warning when session does not exist", () => { + terminalManager.showTerminal("non-existent") + + expect(vscode.window.showWarningMessage).toHaveBeenCalledWith("Session not found") + expect(vscode.window.createTerminal).not.toHaveBeenCalled() + }) + + it("creates terminal with workspace path for local session (no worktree)", () => { + // Create a session without parallelMode (local session) + registry.createSession("session-1", "test prompt") + + terminalManager.showTerminal("session-1") + + expect(vscode.window.createTerminal).toHaveBeenCalledWith({ + cwd: MOCK_WORKSPACE_PATH, + name: "Agent: test prompt (local)", + iconPath: expect.objectContaining({ id: "terminal" }), + }) + expect(mockTerminal.show).toHaveBeenCalled() + }) + + it("shows warning when parallelMode session does not have worktreePath", () => { + // Create a session with parallelMode but no worktreePath yet + registry.createSession("session-1", "test prompt", undefined, { parallelMode: true }) + + terminalManager.showTerminal("session-1") + + expect(vscode.window.showWarningMessage).toHaveBeenCalledWith("Worktree path not available yet") + expect(vscode.window.createTerminal).not.toHaveBeenCalled() + }) + + it("derives worktree path from branch when missing", () => { + const branch = "feature-branch" + registry.createSession("session-1", "test prompt", undefined, { parallelMode: true }) + registry.updateParallelModeInfo("session-1", { branch }) + + terminalManager.showTerminal("session-1") + + const expectedPath = path.join(os.tmpdir(), `kilocode-worktree-${branch}`) + expect(vscode.window.createTerminal).toHaveBeenCalledWith({ + cwd: expectedPath, + name: `Agent: ${branch}`, + iconPath: expect.objectContaining({ id: "terminal" }), + }) + expect(mockTerminal.show).toHaveBeenCalled() + }) + + it("creates terminal with worktree path when session has worktree path", () => { + // Create a session with parallelMode and worktreePath + registry.createSession("session-1", "test prompt", undefined, { parallelMode: true }) + registry.updateParallelModeInfo("session-1", { + worktreePath: "/tmp/worktree-path", + branch: "feature-branch", + }) + + terminalManager.showTerminal("session-1") + + expect(vscode.window.createTerminal).toHaveBeenCalledWith({ + cwd: "/tmp/worktree-path", + name: "Agent: feature-branch", + iconPath: expect.objectContaining({ id: "terminal" }), + }) + expect(mockTerminal.show).toHaveBeenCalled() + }) + + it("uses session label when branch name is not available for worktree session", () => { + registry.createSession("session-1", "test prompt", undefined, { parallelMode: true }) + registry.updateParallelModeInfo("session-1", { + worktreePath: "/tmp/worktree-path", + }) + + terminalManager.showTerminal("session-1") + + expect(vscode.window.createTerminal).toHaveBeenCalledWith({ + cwd: "/tmp/worktree-path", + name: "Agent: test prompt", + iconPath: expect.objectContaining({ id: "terminal" }), + }) + }) + + it("reuses existing terminal instead of creating new one", () => { + registry.createSession("session-1", "test prompt", undefined, { parallelMode: true }) + registry.updateParallelModeInfo("session-1", { + worktreePath: "/tmp/worktree-path", + branch: "feature-branch", + }) + + // First call creates terminal + terminalManager.showTerminal("session-1") + expect(vscode.window.createTerminal).toHaveBeenCalledTimes(1) + + // Second call reuses terminal + terminalManager.showTerminal("session-1") + expect(vscode.window.createTerminal).toHaveBeenCalledTimes(1) + expect(mockTerminal.show).toHaveBeenCalledTimes(2) + }) + + it("creates new terminal if previous one was closed", () => { + registry.createSession("session-1", "test prompt", undefined, { parallelMode: true }) + registry.updateParallelModeInfo("session-1", { + worktreePath: "/tmp/worktree-path", + branch: "feature-branch", + }) + + // First call creates terminal + terminalManager.showTerminal("session-1") + expect(vscode.window.createTerminal).toHaveBeenCalledTimes(1) + + // Simulate terminal being closed (exitStatus is set) + mockTerminal.exitStatus = { code: 0 } + + // Create a new mock terminal for the second call + const newMockTerminal = { + show: vi.fn(), + dispose: vi.fn(), + exitStatus: undefined, + } + ;(vscode.window.createTerminal as Mock).mockReturnValue(newMockTerminal) + + // Second call should create new terminal + terminalManager.showTerminal("session-1") + expect(vscode.window.createTerminal).toHaveBeenCalledTimes(2) + }) + }) + + describe("closeTerminal", () => { + it("disposes terminal when it exists", () => { + registry.createSession("session-1", "test prompt", undefined, { parallelMode: true }) + registry.updateParallelModeInfo("session-1", { + worktreePath: "/tmp/worktree-path", + }) + + terminalManager.showTerminal("session-1") + terminalManager.closeTerminal("session-1") + + expect(mockTerminal.dispose).toHaveBeenCalled() + }) + + it("does nothing when terminal does not exist", () => { + // Should not throw + terminalManager.closeTerminal("non-existent") + }) + + it("removes terminal from tracking after close", () => { + registry.createSession("session-1", "test prompt", undefined, { parallelMode: true }) + registry.updateParallelModeInfo("session-1", { + worktreePath: "/tmp/worktree-path", + }) + + terminalManager.showTerminal("session-1") + terminalManager.closeTerminal("session-1") + + expect(terminalManager.hasTerminal("session-1")).toBe(false) + }) + }) + + describe("hasTerminal", () => { + it("returns false when no terminal exists for session", () => { + expect(terminalManager.hasTerminal("session-1")).toBe(false) + }) + + it("returns true when terminal exists and is active", () => { + registry.createSession("session-1", "test prompt", undefined, { parallelMode: true }) + registry.updateParallelModeInfo("session-1", { + worktreePath: "/tmp/worktree-path", + }) + + terminalManager.showTerminal("session-1") + + expect(terminalManager.hasTerminal("session-1")).toBe(true) + }) + + it("returns false when terminal was closed", () => { + registry.createSession("session-1", "test prompt", undefined, { parallelMode: true }) + registry.updateParallelModeInfo("session-1", { + worktreePath: "/tmp/worktree-path", + }) + + terminalManager.showTerminal("session-1") + mockTerminal.exitStatus = { code: 0 } + + expect(terminalManager.hasTerminal("session-1")).toBe(false) + }) + }) + + describe("showExistingTerminal", () => { + it("returns false when terminal does not exist", () => { + expect(terminalManager.showExistingTerminal("session-1")).toBe(false) + expect(vscode.window.createTerminal).not.toHaveBeenCalled() + }) + + it("shows terminal when it exists and is active", () => { + registry.createSession("session-1", "test prompt", undefined, { parallelMode: true }) + registry.updateParallelModeInfo("session-1", { + worktreePath: "/tmp/worktree-path", + }) + + terminalManager.showTerminal("session-1") + const result = terminalManager.showExistingTerminal("session-1") + + expect(result).toBe(true) + expect(mockTerminal.show).toHaveBeenCalledTimes(2) + }) + + it("clears mapping when terminal has exited", () => { + registry.createSession("session-1", "test prompt", undefined, { parallelMode: true }) + registry.updateParallelModeInfo("session-1", { + worktreePath: "/tmp/worktree-path", + }) + + terminalManager.showTerminal("session-1") + mockTerminal.exitStatus = { code: 0 } + + const result = terminalManager.showExistingTerminal("session-1") + expect(result).toBe(false) + expect(terminalManager.hasTerminal("session-1")).toBe(false) + }) + }) + + describe("onDidCloseTerminal cleanup", () => { + it("removes terminal from tracking when user closes it", () => { + registry.createSession("session-1", "test prompt", undefined, { parallelMode: true }) + registry.updateParallelModeInfo("session-1", { + worktreePath: "/tmp/worktree-path", + }) + + terminalManager.showTerminal("session-1") + expect(terminalManager.hasTerminal("session-1")).toBe(true) + + // Simulate user closing the terminal + onDidCloseTerminalCallback?.(mockTerminal) + + // Terminal should be removed from tracking + // Note: hasTerminal checks exitStatus, but the map entry should be removed + // We need to verify by trying to show terminal again + ;(vscode.window.createTerminal as Mock).mockClear() + const newMockTerminal = { + show: vi.fn(), + dispose: vi.fn(), + exitStatus: undefined, + } + ;(vscode.window.createTerminal as Mock).mockReturnValue(newMockTerminal) + + terminalManager.showTerminal("session-1") + expect(vscode.window.createTerminal).toHaveBeenCalledTimes(1) + }) + + it("does not affect other sessions when one terminal is closed", () => { + // Create two sessions with terminals + registry.createSession("session-1", "test prompt 1", undefined, { parallelMode: true }) + registry.updateParallelModeInfo("session-1", { worktreePath: "/tmp/worktree-1" }) + + registry.createSession("session-2", "test prompt 2", undefined, { parallelMode: true }) + registry.updateParallelModeInfo("session-2", { worktreePath: "/tmp/worktree-2" }) + + const mockTerminal1 = { show: vi.fn(), dispose: vi.fn(), exitStatus: undefined } + const mockTerminal2 = { show: vi.fn(), dispose: vi.fn(), exitStatus: undefined } + + ;(vscode.window.createTerminal as Mock) + .mockReturnValueOnce(mockTerminal1) + .mockReturnValueOnce(mockTerminal2) + + terminalManager.showTerminal("session-1") + terminalManager.showTerminal("session-2") + + // Close only the first terminal + onDidCloseTerminalCallback?.(mockTerminal1) + + // Second terminal should still be tracked + expect(terminalManager.hasTerminal("session-2")).toBe(true) + }) + }) + + describe("dispose", () => { + it("disposes all event listeners", () => { + const disposeMock = vi.fn() + ;(vscode.window.onDidCloseTerminal as Mock).mockReturnValue({ dispose: disposeMock }) + + const manager = new SessionTerminalManager(registry, { + appendLine: vi.fn(), + } as unknown as vscode.OutputChannel) + manager.dispose() + + expect(disposeMock).toHaveBeenCalled() + }) + }) +}) diff --git a/src/core/kilocode/agent-manager/__tests__/parallelModeParser.spec.ts b/src/core/kilocode/agent-manager/__tests__/parallelModeParser.spec.ts index 6e81b5a3618..757891f3203 100644 --- a/src/core/kilocode/agent-manager/__tests__/parallelModeParser.spec.ts +++ b/src/core/kilocode/agent-manager/__tests__/parallelModeParser.spec.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from "vitest" import { + buildParallelModeWorktreePath, parseParallelModeBranch, parseParallelModeWorktreePath, isParallelModeCompletionMessage, @@ -112,4 +113,12 @@ describe("parallelModeParser", () => { expect(parseParallelModeCompletionBranch("Parallel mode complete!")).toBeUndefined() }) }) + + describe("buildParallelModeWorktreePath", () => { + it("uses the OS temp directory with the kilocode worktree prefix", () => { + const branch = "feature-123" + const result = buildParallelModeWorktreePath(branch) + expect(result).toContain(`kilocode-worktree-${branch}`) + }) + }) }) diff --git a/src/core/kilocode/agent-manager/parallelModeParser.ts b/src/core/kilocode/agent-manager/parallelModeParser.ts index 9ea4fa3ebdd..acc0d73247b 100644 --- a/src/core/kilocode/agent-manager/parallelModeParser.ts +++ b/src/core/kilocode/agent-manager/parallelModeParser.ts @@ -1,3 +1,6 @@ +import os from "node:os" +import path from "node:path" + export function parseParallelModeBranch(message: string): string | undefined { const match = message.match(/(?:Creating worktree with branch|Using existing branch):\s*(.+)/i) if (match) { @@ -12,6 +15,10 @@ export function parseParallelModeWorktreePath(message: string): string | undefin } return undefined } + +export function buildParallelModeWorktreePath(branch: string): string { + return path.join(os.tmpdir(), `kilocode-worktree-${branch}`) +} export function isParallelModeCompletionMessage(message: string): boolean { return message.includes("Parallel mode complete") } diff --git a/webview-ui/src/i18n/locales/ar/agentManager.json b/webview-ui/src/i18n/locales/ar/agentManager.json index eab35ad7f37..42061a4f113 100644 --- a/webview-ui/src/i18n/locales/ar/agentManager.json +++ b/webview-ui/src/i18n/locales/ar/agentManager.json @@ -58,7 +58,8 @@ "otherBranches": "فروع أخرى", "branchPickerTooltip": "تبديل الفرع الأساسي", "noBranches": "لم يتم العثور على فروع", - "noMatchingBranches": "لا توجد فروع تطابق بحثك" + "noMatchingBranches": "لا توجد فروع تطابق بحثك", + "openTerminal": "فتح الطرفية" }, "messages": { "waiting": "في انتظار رد الوكيل...", diff --git a/webview-ui/src/i18n/locales/ca/agentManager.json b/webview-ui/src/i18n/locales/ca/agentManager.json index face5c8f20f..72b852e6f7c 100644 --- a/webview-ui/src/i18n/locales/ca/agentManager.json +++ b/webview-ui/src/i18n/locales/ca/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "Altres branques", "branchPickerTooltip": "Canvia la branca base", "noBranches": "No s'han trobat branques", - "noMatchingBranches": "No hi ha branques que coincideixin amb la teva cerca" + "noMatchingBranches": "No hi ha branques que coincideixin amb la teva cerca", + "openTerminal": "Obrir terminal" }, "messages": { "waiting": "Esperant resposta de l'agent...", diff --git a/webview-ui/src/i18n/locales/cs/agentManager.json b/webview-ui/src/i18n/locales/cs/agentManager.json index b911aff48d7..c74102563db 100644 --- a/webview-ui/src/i18n/locales/cs/agentManager.json +++ b/webview-ui/src/i18n/locales/cs/agentManager.json @@ -55,7 +55,8 @@ "otherBranches": "Ostatní větve", "branchPickerTooltip": "Změnit základní větev", "noBranches": "Nebyly nalezeny žádné větve", - "noMatchingBranches": "Žádné větve neodpovídají tvému vyhledávání" + "noMatchingBranches": "Žádné větve neodpovídají tvému vyhledávání", + "openTerminal": "Otevřít terminál" }, "messages": { "waiting": "Čekání na odpověď agenta...", diff --git a/webview-ui/src/i18n/locales/de/agentManager.json b/webview-ui/src/i18n/locales/de/agentManager.json index 587db7365ed..fc0d6a750f6 100644 --- a/webview-ui/src/i18n/locales/de/agentManager.json +++ b/webview-ui/src/i18n/locales/de/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "Andere Branches", "branchPickerTooltip": "Basis-Branch wechseln", "noBranches": "Keine Branches gefunden", - "noMatchingBranches": "Keine Branches entsprechen deiner Suche" + "noMatchingBranches": "Keine Branches entsprechen deiner Suche", + "openTerminal": "Terminal öffnen" }, "messages": { "waiting": "Warte auf Antwort des Agenten...", diff --git a/webview-ui/src/i18n/locales/en/agentManager.json b/webview-ui/src/i18n/locales/en/agentManager.json index 065ead28d53..f1d452de587 100644 --- a/webview-ui/src/i18n/locales/en/agentManager.json +++ b/webview-ui/src/i18n/locales/en/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "Other branches", "branchPickerTooltip": "Switch base branch", "noBranches": "No branches found", - "noMatchingBranches": "No branches match your search" + "noMatchingBranches": "No branches match your search", + "openTerminal": "Open terminal" }, "messages": { "waiting": "Waiting for agent response...", diff --git a/webview-ui/src/i18n/locales/es/agentManager.json b/webview-ui/src/i18n/locales/es/agentManager.json index e701b535b7b..e839de0590a 100644 --- a/webview-ui/src/i18n/locales/es/agentManager.json +++ b/webview-ui/src/i18n/locales/es/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "Otras ramas", "branchPickerTooltip": "Cambiar rama base", "noBranches": "No se encontraron ramas", - "noMatchingBranches": "Ninguna rama coincide con tu búsqueda" + "noMatchingBranches": "Ninguna rama coincide con tu búsqueda", + "openTerminal": "Abrir terminal" }, "messages": { "waiting": "Esperando respuesta del agente...", diff --git a/webview-ui/src/i18n/locales/fr/agentManager.json b/webview-ui/src/i18n/locales/fr/agentManager.json index f537474f3ee..18306e6213d 100644 --- a/webview-ui/src/i18n/locales/fr/agentManager.json +++ b/webview-ui/src/i18n/locales/fr/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "Autres branches", "branchPickerTooltip": "Changer de branche de base", "noBranches": "Aucune branche trouvée", - "noMatchingBranches": "Aucune branche ne correspond à ta recherche" + "noMatchingBranches": "Aucune branche ne correspond à ta recherche", + "openTerminal": "Ouvrir le terminal" }, "messages": { "waiting": "En attente de la réponse de l'agent...", diff --git a/webview-ui/src/i18n/locales/hi/agentManager.json b/webview-ui/src/i18n/locales/hi/agentManager.json index 095fee0cb0b..25debc72c47 100644 --- a/webview-ui/src/i18n/locales/hi/agentManager.json +++ b/webview-ui/src/i18n/locales/hi/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "अन्य ब्रांच", "branchPickerTooltip": "बेस ब्रांच बदलें", "noBranches": "कोई ब्रांच नहीं मिली", - "noMatchingBranches": "आपकी खोज से मेल खाने वाली कोई ब्रांच नहीं" + "noMatchingBranches": "आपकी खोज से मेल खाने वाली कोई ब्रांच नहीं", + "openTerminal": "टर्मिनल खोलें" }, "messages": { "waiting": "एजेंट की प्रतिक्रिया की प्रतीक्षा में...", diff --git a/webview-ui/src/i18n/locales/id/agentManager.json b/webview-ui/src/i18n/locales/id/agentManager.json index cbc0182cea6..152f8b90718 100644 --- a/webview-ui/src/i18n/locales/id/agentManager.json +++ b/webview-ui/src/i18n/locales/id/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "Cabang lain", "branchPickerTooltip": "Ganti cabang dasar", "noBranches": "Tidak ada cabang ditemukan", - "noMatchingBranches": "Tidak ada cabang yang cocok dengan pencarian kamu" + "noMatchingBranches": "Tidak ada cabang yang cocok dengan pencarian kamu", + "openTerminal": "Buka terminal" }, "messages": { "waiting": "Menunggu respons agen...", diff --git a/webview-ui/src/i18n/locales/it/agentManager.json b/webview-ui/src/i18n/locales/it/agentManager.json index ecd8caebb64..4037b9b2019 100644 --- a/webview-ui/src/i18n/locales/it/agentManager.json +++ b/webview-ui/src/i18n/locales/it/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "Altri branch", "branchPickerTooltip": "Cambia branch di base", "noBranches": "Nessun branch trovato", - "noMatchingBranches": "Nessun branch corrisponde alla tua ricerca" + "noMatchingBranches": "Nessun branch corrisponde alla tua ricerca", + "openTerminal": "Apri terminale" }, "messages": { "waiting": "In attesa della risposta dell'agente...", diff --git a/webview-ui/src/i18n/locales/ja/agentManager.json b/webview-ui/src/i18n/locales/ja/agentManager.json index 5f84b0db397..fcec7994083 100644 --- a/webview-ui/src/i18n/locales/ja/agentManager.json +++ b/webview-ui/src/i18n/locales/ja/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "その他のブランチ", "branchPickerTooltip": "ベースブランチを切り替え", "noBranches": "ブランチが見つかりません", - "noMatchingBranches": "検索に一致するブランチがありません" + "noMatchingBranches": "検索に一致するブランチがありません", + "openTerminal": "ターミナルを開く" }, "messages": { "waiting": "エージェントの応答を待っています...", diff --git a/webview-ui/src/i18n/locales/ko/agentManager.json b/webview-ui/src/i18n/locales/ko/agentManager.json index 991d263d98b..440d5bf0cf1 100644 --- a/webview-ui/src/i18n/locales/ko/agentManager.json +++ b/webview-ui/src/i18n/locales/ko/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "다른 브랜치", "branchPickerTooltip": "기본 브랜치 변경", "noBranches": "브랜치를 찾을 수 없습니다", - "noMatchingBranches": "검색과 일치하는 브랜치가 없습니다" + "noMatchingBranches": "검색과 일치하는 브랜치가 없습니다", + "openTerminal": "터미널 열기" }, "messages": { "waiting": "에이전트 응답 대기 중...", diff --git a/webview-ui/src/i18n/locales/nl/agentManager.json b/webview-ui/src/i18n/locales/nl/agentManager.json index 4c921ba61ae..47fe692c0cd 100644 --- a/webview-ui/src/i18n/locales/nl/agentManager.json +++ b/webview-ui/src/i18n/locales/nl/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "Andere branches", "branchPickerTooltip": "Wijzig basisbranch", "noBranches": "Geen branches gevonden", - "noMatchingBranches": "Geen branches komen overeen met je zoekopdracht" + "noMatchingBranches": "Geen branches komen overeen met je zoekopdracht", + "openTerminal": "Terminal openen" }, "messages": { "waiting": "Wachten op antwoord van agent...", diff --git a/webview-ui/src/i18n/locales/pl/agentManager.json b/webview-ui/src/i18n/locales/pl/agentManager.json index b1ba2b20ca5..09d72fe03cf 100644 --- a/webview-ui/src/i18n/locales/pl/agentManager.json +++ b/webview-ui/src/i18n/locales/pl/agentManager.json @@ -56,7 +56,8 @@ "otherBranches": "Inne gałęzie", "branchPickerTooltip": "Zmień gałąź bazową", "noBranches": "Nie znaleziono gałęzi", - "noMatchingBranches": "Brak gałęzi pasujących do twojego wyszukiwania" + "noMatchingBranches": "Brak gałęzi pasujących do twojego wyszukiwania", + "openTerminal": "Otwórz terminal" }, "messages": { "waiting": "Oczekiwanie na odpowiedź agenta...", diff --git a/webview-ui/src/i18n/locales/pt-BR/agentManager.json b/webview-ui/src/i18n/locales/pt-BR/agentManager.json index 9de5f8de013..f4e1f415bbf 100644 --- a/webview-ui/src/i18n/locales/pt-BR/agentManager.json +++ b/webview-ui/src/i18n/locales/pt-BR/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "Outros branches", "branchPickerTooltip": "Mudar branch base", "noBranches": "Nenhum branch encontrado", - "noMatchingBranches": "Nenhum branch corresponde à sua pesquisa" + "noMatchingBranches": "Nenhum branch corresponde à sua pesquisa", + "openTerminal": "Abrir terminal" }, "messages": { "waiting": "Aguardando resposta do agente...", diff --git a/webview-ui/src/i18n/locales/ru/agentManager.json b/webview-ui/src/i18n/locales/ru/agentManager.json index c64429331fe..b25411abd30 100644 --- a/webview-ui/src/i18n/locales/ru/agentManager.json +++ b/webview-ui/src/i18n/locales/ru/agentManager.json @@ -56,7 +56,8 @@ "otherBranches": "Другие ветки", "branchPickerTooltip": "Сменить базовую ветку", "noBranches": "Ветки не найдены", - "noMatchingBranches": "Нет веток, соответствующих твоему поиску" + "noMatchingBranches": "Нет веток, соответствующих твоему поиску", + "openTerminal": "Открыть терминал" }, "messages": { "waiting": "Ожидание ответа агента...", diff --git a/webview-ui/src/i18n/locales/th/agentManager.json b/webview-ui/src/i18n/locales/th/agentManager.json index 6271a65dcba..1185e2ea78d 100644 --- a/webview-ui/src/i18n/locales/th/agentManager.json +++ b/webview-ui/src/i18n/locales/th/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "สาขาอื่นๆ", "branchPickerTooltip": "เปลี่ยนสาขาหลัก", "noBranches": "ไม่พบสาขา", - "noMatchingBranches": "ไม่มีสาขาที่ตรงกับการค้นหาของคุณ" + "noMatchingBranches": "ไม่มีสาขาที่ตรงกับการค้นหาของคุณ", + "openTerminal": "เปิดเทอร์มินัล" }, "messages": { "waiting": "รอการตอบกลับจากเอเจนต์...", diff --git a/webview-ui/src/i18n/locales/tr/agentManager.json b/webview-ui/src/i18n/locales/tr/agentManager.json index bc22929452b..c940da503f5 100644 --- a/webview-ui/src/i18n/locales/tr/agentManager.json +++ b/webview-ui/src/i18n/locales/tr/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "Diğer dallar", "branchPickerTooltip": "Temel dalı değiştir", "noBranches": "Dal bulunamadı", - "noMatchingBranches": "Aramanla eşleşen dal yok" + "noMatchingBranches": "Aramanla eşleşen dal yok", + "openTerminal": "Terminali aç" }, "messages": { "waiting": "Ajan yanıtı bekleniyor...", diff --git a/webview-ui/src/i18n/locales/uk/agentManager.json b/webview-ui/src/i18n/locales/uk/agentManager.json index 77086ef4af1..a137e9bd738 100644 --- a/webview-ui/src/i18n/locales/uk/agentManager.json +++ b/webview-ui/src/i18n/locales/uk/agentManager.json @@ -56,7 +56,8 @@ "otherBranches": "Інші гілки", "branchPickerTooltip": "Змінити базову гілку", "noBranches": "Гілки не знайдено", - "noMatchingBranches": "Немає гілок, що відповідають твоєму пошуку" + "noMatchingBranches": "Немає гілок, що відповідають твоєму пошуку", + "openTerminal": "Відкрити термінал" }, "messages": { "waiting": "Очікування відповіді агента...", diff --git a/webview-ui/src/i18n/locales/vi/agentManager.json b/webview-ui/src/i18n/locales/vi/agentManager.json index 2db1d275612..44b6629429f 100644 --- a/webview-ui/src/i18n/locales/vi/agentManager.json +++ b/webview-ui/src/i18n/locales/vi/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "Nhánh khác", "branchPickerTooltip": "Chuyển nhánh cơ sở", "noBranches": "Không tìm thấy nhánh nào", - "noMatchingBranches": "Không có nhánh nào khớp với tìm kiếm của bạn" + "noMatchingBranches": "Không có nhánh nào khớp với tìm kiếm của bạn", + "openTerminal": "Mở terminal" }, "messages": { "waiting": "Đang chờ phản hồi từ agent...", diff --git a/webview-ui/src/i18n/locales/zh-CN/agentManager.json b/webview-ui/src/i18n/locales/zh-CN/agentManager.json index 87b448565bf..29f826375d2 100644 --- a/webview-ui/src/i18n/locales/zh-CN/agentManager.json +++ b/webview-ui/src/i18n/locales/zh-CN/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "其他 Branch", "branchPickerTooltip": "切换基础 Branch", "noBranches": "未找到 Branch", - "noMatchingBranches": "没有匹配的 Branch" + "noMatchingBranches": "没有匹配的 Branch", + "openTerminal": "打开终端" }, "messages": { "waiting": "等待代理响应...", diff --git a/webview-ui/src/i18n/locales/zh-TW/agentManager.json b/webview-ui/src/i18n/locales/zh-TW/agentManager.json index 384c1354cb2..96d733564ec 100644 --- a/webview-ui/src/i18n/locales/zh-TW/agentManager.json +++ b/webview-ui/src/i18n/locales/zh-TW/agentManager.json @@ -54,7 +54,8 @@ "otherBranches": "其他 Branch", "branchPickerTooltip": "切換基礎 Branch", "noBranches": "找不到 Branch", - "noMatchingBranches": "沒有符合的 Branch" + "noMatchingBranches": "沒有符合的 Branch", + "openTerminal": "開啟終端機" }, "messages": { "waiting": "等待代理回應...", diff --git a/webview-ui/src/kilocode/agent-manager/components/SessionDetail.tsx b/webview-ui/src/kilocode/agent-manager/components/SessionDetail.tsx index a8fc91e3678..d632ca8a26e 100644 --- a/webview-ui/src/kilocode/agent-manager/components/SessionDetail.tsx +++ b/webview-ui/src/kilocode/agent-manager/components/SessionDetail.tsx @@ -18,7 +18,18 @@ import { ChatInput } from "./ChatInput" import { BranchPicker } from "./BranchPicker" import { vscode } from "../utils/vscode" import { formatRelativeTime, createRelativeTimeLabels } from "../utils/timeUtils" -import { Loader2, SendHorizontal, GitBranch, Folder, ChevronDown, AlertCircle, Zap, Layers, X } from "lucide-react" +import { + Loader2, + SendHorizontal, + GitBranch, + Folder, + ChevronDown, + AlertCircle, + Zap, + Layers, + X, + Terminal, +} from "lucide-react" import DynamicTextArea from "react-textarea-autosize" import { cn } from "../../../lib/utils" import { StandardTooltip } from "../../../components/ui" @@ -71,6 +82,7 @@ export function SessionDetail() { const showSpinner = sessionUiState?.showSpinner ?? false const isWorktree = selectedSession.parallelMode?.enabled const branchName = selectedSession.parallelMode?.branch + const isProvisionalSession = selectedSession.sessionId.startsWith("provisional-") // Determine if "Finish to Branch" button should be shown // Simplified logic: show when session is a worktree session and running @@ -114,6 +126,22 @@ export function SessionDetail() { )} +
+ {!isProvisionalSession && ( + + )} +
{selectedSession.status === "error" && selectedSession.error && ( diff --git a/webview-ui/src/kilocode/agent-manager/components/__tests__/SessionDetail.spec.tsx b/webview-ui/src/kilocode/agent-manager/components/__tests__/SessionDetail.spec.tsx index 3ab553714d0..f2a4702a502 100644 --- a/webview-ui/src/kilocode/agent-manager/components/__tests__/SessionDetail.spec.tsx +++ b/webview-ui/src/kilocode/agent-manager/components/__tests__/SessionDetail.spec.tsx @@ -323,4 +323,34 @@ describe("SessionDetail", () => { expect(screen.getByText("sessionDetail.runModeLocal")).toBeInTheDocument() }) }) + + describe("terminal button visibility", () => { + it("hides terminal button for provisional sessions", () => { + const session = createSession({ + sessionId: "provisional-123", + status: "running", + parallelMode: { enabled: true, branch: "feature/test" }, + }) + + renderWithStore(session, { + [session.sessionId]: createUiState({ isActive: true, showSpinner: false }), + }) + + expect(screen.queryByLabelText("sessionDetail.openTerminal")).not.toBeInTheDocument() + }) + + it("shows terminal button for non-provisional sessions", () => { + const session = createSession({ + sessionId: "real-session-123", + status: "running", + parallelMode: { enabled: true, branch: "feature/test" }, + }) + + renderWithStore(session, { + [session.sessionId]: createUiState({ isActive: true, showSpinner: false }), + }) + + expect(screen.getByLabelText("sessionDetail.openTerminal")).toBeInTheDocument() + }) + }) }) From 7fe600c5b48fdabd1ecb84a68792daf604451bca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:53:45 +0000 Subject: [PATCH 4/6] changeset version bump --- .changeset/agent-terminal-switch.md | 5 ---- .changeset/fix-macos-cli-spawn-path.md | 5 ---- .changeset/giant-buckets-clap.md | 5 ---- .changeset/hungry-lands-sin.md | 5 ---- .changeset/large-jars-train.md | 9 ------ .changeset/olive-carpets-swim.md | 5 ---- .changeset/petite-moose-wish.md | 5 ---- .changeset/salty-shirts-guess.md | 5 ---- .changeset/session-title-generated-event.md | 6 ---- .changeset/small-towns-march.md | 5 ---- .changeset/stale-towns-bathe.md | 5 ---- .changeset/vast-results-cheat.md | 5 ---- .changeset/yellow-plants-work.md | 5 ---- CHANGELOG.md | 32 +++++++++++++++++++++ cli/CHANGELOG.md | 10 +++++++ cli/package.dist.json | 2 +- cli/package.json | 2 +- src/package.json | 2 +- 18 files changed, 45 insertions(+), 73 deletions(-) delete mode 100644 .changeset/agent-terminal-switch.md delete mode 100644 .changeset/fix-macos-cli-spawn-path.md delete mode 100644 .changeset/giant-buckets-clap.md delete mode 100644 .changeset/hungry-lands-sin.md delete mode 100644 .changeset/large-jars-train.md delete mode 100644 .changeset/olive-carpets-swim.md delete mode 100644 .changeset/petite-moose-wish.md delete mode 100644 .changeset/salty-shirts-guess.md delete mode 100644 .changeset/session-title-generated-event.md delete mode 100644 .changeset/small-towns-march.md delete mode 100644 .changeset/stale-towns-bathe.md delete mode 100644 .changeset/vast-results-cheat.md delete mode 100644 .changeset/yellow-plants-work.md diff --git a/.changeset/agent-terminal-switch.md b/.changeset/agent-terminal-switch.md deleted file mode 100644 index ffb37fa1541..00000000000 --- a/.changeset/agent-terminal-switch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Add Agent Manager terminal switching so existing session terminals are revealed when changing sessions. diff --git a/.changeset/fix-macos-cli-spawn-path.md b/.changeset/fix-macos-cli-spawn-path.md deleted file mode 100644 index 54831b3af76..00000000000 --- a/.changeset/fix-macos-cli-spawn-path.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Fix Agent Manager failing to start on macOS when launched from Finder/Spotlight diff --git a/.changeset/giant-buckets-clap.md b/.changeset/giant-buckets-clap.md deleted file mode 100644 index 1080d014389..00000000000 --- a/.changeset/giant-buckets-clap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Introduces AI contribution tracking so users can better understand agentic coding impact diff --git a/.changeset/hungry-lands-sin.md b/.changeset/hungry-lands-sin.md deleted file mode 100644 index 65a68b98412..00000000000 --- a/.changeset/hungry-lands-sin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Reduce the incidence of read_file errors when using Claude models. diff --git a/.changeset/large-jars-train.md b/.changeset/large-jars-train.md deleted file mode 100644 index 0343c1a39cc..00000000000 --- a/.changeset/large-jars-train.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"kilo-code": patch ---- - -chore: update Gemini Cli models and metadata - -- Added gemini-3-flash-preview model configuration. -- Updated maxThinkingTokens for gemini-3-pro-preview to 32,768. -- Reordered model definitions to prioritize newer versions. diff --git a/.changeset/olive-carpets-swim.md b/.changeset/olive-carpets-swim.md deleted file mode 100644 index fd22c4a4e89..00000000000 --- a/.changeset/olive-carpets-swim.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Fix duplicate tool use in Anthropic diff --git a/.changeset/petite-moose-wish.md b/.changeset/petite-moose-wish.md deleted file mode 100644 index c1248aad842..00000000000 --- a/.changeset/petite-moose-wish.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"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 deleted file mode 100644 index ffd0def6b01..00000000000 --- a/.changeset/salty-shirts-guess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"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 deleted file mode 100644 index ae53250d344..00000000000 --- a/.changeset/session-title-generated-event.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"kilo-code": patch -"@kilocode/cli": patch ---- - -feat: add session_title_generated event emission to CLI diff --git a/.changeset/small-towns-march.md b/.changeset/small-towns-march.md deleted file mode 100644 index 8be9da81341..00000000000 --- a/.changeset/small-towns-march.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Add chat autocomplete telemetry diff --git a/.changeset/stale-towns-bathe.md b/.changeset/stale-towns-bathe.md deleted file mode 100644 index ac7605fa338..00000000000 --- a/.changeset/stale-towns-bathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Jetbrains - Autocomplete Telemetry diff --git a/.changeset/vast-results-cheat.md b/.changeset/vast-results-cheat.md deleted file mode 100644 index 8397044b814..00000000000 --- a/.changeset/vast-results-cheat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": minor ---- - -Add markdown theming support for Reasoning box content diff --git a/.changeset/yellow-plants-work.md b/.changeset/yellow-plants-work.md deleted file mode 100644 index 9bb67bcd17f..00000000000 --- a/.changeset/yellow-plants-work.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -fix(ollama): fix model not found error and context window display diff --git a/CHANGELOG.md b/CHANGELOG.md index 556ba2db9df..cdd2e93ce9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # kilo-code +## 4.140.1 + +### Patch Changes + +- [#4615](https://github.com/Kilo-Org/kilocode/pull/4615) [`6909640`](https://github.com/Kilo-Org/kilocode/commit/690964040770cd21248e1bea964c995d8620d8e8) Thanks [@marius-kilocode](https://github.com/marius-kilocode)! - Add Agent Manager terminal switching so existing session terminals are revealed when changing sessions. + +- [#4586](https://github.com/Kilo-Org/kilocode/pull/4586) [`a3988cd`](https://github.com/Kilo-Org/kilocode/commit/a3988cd201f21f7b7616d68cb2bb2c0387dd91c2) Thanks [@marius-kilocode](https://github.com/marius-kilocode)! - Fix Agent Manager failing to start on macOS when launched from Finder/Spotlight + +- [#4561](https://github.com/Kilo-Org/kilocode/pull/4561) [`3c18860`](https://github.com/Kilo-Org/kilocode/commit/3c188603cc4d8375be4abf6e1bb9217b64e9cd2b) Thanks [@jrf0110](https://github.com/jrf0110)! - Introduces AI contribution tracking so users can better understand agentic coding impact + +- [#4526](https://github.com/Kilo-Org/kilocode/pull/4526) [`10b4d6c`](https://github.com/Kilo-Org/kilocode/commit/10b4d6c02f5b310dd6e44204fa40675ca4d3d99b) Thanks [@chrarnoldus](https://github.com/chrarnoldus)! - Reduce the incidence of read_file errors when using Claude models. + +- [#4560](https://github.com/Kilo-Org/kilocode/pull/4560) [`5bdfe6b`](https://github.com/Kilo-Org/kilocode/commit/5bdfe6b9b68acf345e302791c15291c05a043204) Thanks [@crazyrabbit0](https://github.com/crazyrabbit0)! - chore: update Gemini Cli models and metadata + + - Added gemini-3-flash-preview model configuration. + - Updated maxThinkingTokens for gemini-3-pro-preview to 32,768. + - Reordered model definitions to prioritize newer versions. + +- [#4596](https://github.com/Kilo-Org/kilocode/pull/4596) [`1c33884`](https://github.com/Kilo-Org/kilocode/commit/1c3388442bd9a06dcb8aed29431c138726dbedc8) Thanks [@hank9999](https://github.com/hank9999)! - Fix duplicate tool use in Anthropic + +- [#4620](https://github.com/Kilo-Org/kilocode/pull/4620) [`ae6818b`](https://github.com/Kilo-Org/kilocode/commit/ae6818b5ea2d5504f9ee5eff9bdd963d9d82c51e) Thanks [@chrarnoldus](https://github.com/chrarnoldus)! - Fix duplictate tool call processing in Chutes, DeepInfra, LiteLLM and xAI providers. + +- [#4597](https://github.com/Kilo-Org/kilocode/pull/4597) [`e2bb5c1`](https://github.com/Kilo-Org/kilocode/commit/e2bb5c1891b6319954b46fcca3b35807fc1f8f90) Thanks [@marius-kilocode](https://github.com/marius-kilocode)! - 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`. + +- [#4590](https://github.com/Kilo-Org/kilocode/pull/4590) [`f2cc065`](https://github.com/Kilo-Org/kilocode/commit/f2cc0657870ae77a5720a872c9cd11b8315799b7) Thanks [@kiloconnect](https://github.com/apps/kiloconnect)! - feat: add session_title_generated event emission to CLI + +- [#4523](https://github.com/Kilo-Org/kilocode/pull/4523) [`e259b04`](https://github.com/Kilo-Org/kilocode/commit/e259b04037c71a9bdd9e53c174b70a975e772833) Thanks [@markijbema](https://github.com/markijbema)! - Add chat autocomplete telemetry + +- [#4582](https://github.com/Kilo-Org/kilocode/pull/4582) [`3de2547`](https://github.com/Kilo-Org/kilocode/commit/3de254757049d08d3c0c100768acc564d6de4888) Thanks [@catrielmuller](https://github.com/catrielmuller)! - Jetbrains - Autocomplete Telemetry + +- [#4488](https://github.com/Kilo-Org/kilocode/pull/4488) [`f7c3715`](https://github.com/Kilo-Org/kilocode/commit/f7c3715b4b7fea9fcd363d12bfb9467e9f169729) Thanks [@lifesized](https://github.com/lifesized)! - fix(ollama): fix model not found error and context window display + ## 4.140.0 ### Minor Changes diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 023d3228ab7..0e4d2262a39 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,5 +1,15 @@ # @kilocode/cli +## 0.18.0 + +### Minor Changes + +- [#4583](https://github.com/Kilo-Org/kilocode/pull/4583) [`845f8c1`](https://github.com/Kilo-Org/kilocode/commit/845f8c13b23496bf4aaf0792be9d52bf26645b64) Thanks [@kiloconnect](https://github.com/apps/kiloconnect)! - Add markdown theming support for Reasoning box content + +### Patch Changes + +- [#4590](https://github.com/Kilo-Org/kilocode/pull/4590) [`f2cc065`](https://github.com/Kilo-Org/kilocode/commit/f2cc0657870ae77a5720a872c9cd11b8315799b7) Thanks [@kiloconnect](https://github.com/apps/kiloconnect)! - feat: add session_title_generated event emission to CLI + ## 0.17.1 ### Patch Changes diff --git a/cli/package.dist.json b/cli/package.dist.json index 7a40a7aedfe..99518fbaaef 100644 --- a/cli/package.dist.json +++ b/cli/package.dist.json @@ -1,6 +1,6 @@ { "name": "@kilocode/cli", - "version": "0.17.1", + "version": "0.18.0", "description": "Terminal User Interface for Kilo Code", "type": "module", "main": "index.js", diff --git a/cli/package.json b/cli/package.json index 6495195c67a..236fe721a26 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/cli", - "version": "0.17.1", + "version": "0.18.0", "description": "Terminal User Interface for Kilo Code", "type": "module", "main": "dist/index.js", diff --git a/src/package.json b/src/package.json index 00c42d1dbfa..7e1bc3fd595 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "kilocode", - "version": "4.140.0", + "version": "4.140.1", "icon": "assets/icons/logo-outline-black.png", "galleryBanner": { "color": "#FFFFFF", From bb390bb537afb69e70f4060abfee727eda4c3cab Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Mon, 22 Dec 2025 20:04:39 +0100 Subject: [PATCH 5/6] More zod --- src/api/providers/kilocode/chunk-schema.ts | 29 +++++++++++++++++++ .../providers/kilocode/vercel-ai-gateway.ts | 19 ------------ src/api/providers/openrouter.ts | 14 ++++----- 3 files changed, 35 insertions(+), 27 deletions(-) create mode 100644 src/api/providers/kilocode/chunk-schema.ts delete mode 100644 src/api/providers/kilocode/vercel-ai-gateway.ts diff --git a/src/api/providers/kilocode/chunk-schema.ts b/src/api/providers/kilocode/chunk-schema.ts new file mode 100644 index 00000000000..63c6bb1f71b --- /dev/null +++ b/src/api/providers/kilocode/chunk-schema.ts @@ -0,0 +1,29 @@ +import * as z from "zod/v4" + +export const OpenRouterChunkSchema = z.object({ + provider: z.string().optional(), +}) + +export const VercelAiGatewayChunkSchema = z.object({ + choices: z.array( + z.object({ + delta: z.object({ + provider_metadata: z + .object({ + gateway: z + .object({ + routing: z + .object({ + resolvedProvider: z.string().optional(), + }) + .optional(), + }) + .optional(), + }) + .optional(), + }), + }), + ), +}) + +export const KiloCodeChunkSchema = OpenRouterChunkSchema.and(VercelAiGatewayChunkSchema) diff --git a/src/api/providers/kilocode/vercel-ai-gateway.ts b/src/api/providers/kilocode/vercel-ai-gateway.ts deleted file mode 100644 index 137c296803b..00000000000 --- a/src/api/providers/kilocode/vercel-ai-gateway.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as z from "zod/v4" - -export const VercelAiGatewayChunkSchema = z.object({ - choices: z - .array( - z.object({ - delta: z.object({ - provider_metadata: z.object({ - gateway: z.object({ - routing: z.object({ - resolvedProvider: z.string(), - }), - }), - }), - }), - }), - ) - .min(1), -}) diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 31a70bab8b8..97a7fef3ac4 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -49,7 +49,7 @@ import { isAnyRecognizedKiloCodeError } from "../../shared/kilocode/errorUtils" import type { ApiHandlerCreateMessageMetadata, SingleCompletionHandler } from "../index" import { handleOpenAIError } from "./utils/openai-error-handler" import { generateImageWithProvider, ImageGenerationResult } from "./utils/image-generation" -import { VercelAiGatewayChunkSchema } from "./kilocode/vercel-ai-gateway" +import { KiloCodeChunkSchema } from "./kilocode/chunk-schema" // Add custom interface for OpenRouter params. type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & { @@ -364,13 +364,11 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH } // kilocode_change start - if ("provider" in chunk && typeof chunk.provider === "string") { - inferenceProvider = chunk.provider - } - const vercelChunk = VercelAiGatewayChunkSchema.safeParse(chunk) - if (vercelChunk.success) { - inferenceProvider = vercelChunk.data.choices[0].delta.provider_metadata.gateway.routing.resolvedProvider - } + const kiloCodeChunk = KiloCodeChunkSchema.safeParse(chunk).data + inferenceProvider = + kiloCodeChunk?.choices?.[0]?.delta?.provider_metadata?.gateway?.routing?.resolvedProvider ?? + kiloCodeChunk?.provider ?? + inferenceProvider // kilocode_change end verifyFinishReason(chunk.choices[0]) // kilocode_change From 25de94b22fc103ebb9747433444f3fef9a7eeeb8 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Mon, 22 Dec 2025 20:08:43 +0100 Subject: [PATCH 6/6] Add model selection support for Z.ai changeset --- .changeset/tricky-glasses-yell.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tricky-glasses-yell.md diff --git a/.changeset/tricky-glasses-yell.md b/.changeset/tricky-glasses-yell.md new file mode 100644 index 00000000000..ea2077a388d --- /dev/null +++ b/.changeset/tricky-glasses-yell.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Added model selection support below prompt for Z.ai