From ea3cffb4aae718f933b038ea9612ce887196308e Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Thu, 5 Mar 2026 16:31:11 +0100 Subject: [PATCH 01/44] fix: Remove disposeAll() on auth --- packages/kilo-vscode/src/KiloProvider.ts | 14 ++++++++++++-- packages/opencode/src/kilocode/dispose.ts | 20 -------------------- packages/opencode/src/provider/auth.ts | 2 -- packages/opencode/src/server/server.ts | 2 -- 4 files changed, 12 insertions(+), 26 deletions(-) delete mode 100644 packages/opencode/src/kilocode/dispose.ts diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index f43f25129d7..50498d3c225 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -2117,14 +2117,24 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper }, this.cachedLegacyData?.settings, ) + + // Dispose all instances after migration + // Reloading the data will be handled once the server replies with a global.disposed event + await this.client.global.dispose().catch((e: unknown) => + console.warn("[Kilo New] KiloProvider: global.dispose() after migration failed:", e), + ) + // Only mark as completed if at least one item succeeded — if everything failed // the user can still re-run migration via Settings → About. const anySuccess = results.some((r) => r.status === "success") + + + if (anySuccess) { await MigrationService.setMigrationStatus(this.extensionContext, "completed") } - // Refresh providers so webview immediately sees the newly-migrated API keys - await this.fetchAndSendProviders() + + this.postMessage({ type: "legacyMigrationComplete", results }) } catch (error) { console.error("[Kilo New] KiloProvider: ❌ Migration failed", error) diff --git a/packages/opencode/src/kilocode/dispose.ts b/packages/opencode/src/kilocode/dispose.ts deleted file mode 100644 index 7f62dcbc494..00000000000 --- a/packages/opencode/src/kilocode/dispose.ts +++ /dev/null @@ -1,20 +0,0 @@ -// kilocode_change - new file -/** - * Debounced Instance.disposeAll() helper. - * - * Bulk operations like legacy migration call client.auth.set() for each provider - * in rapid succession. Calling Instance.disposeAll() immediately on every auth - * change causes repeated disposal/recreation cycles. This module coalesces all - * dispose requests within a 300ms window into a single disposal call. - */ -import { Instance } from "../project/instance" - -let timer: ReturnType | undefined - -export function scheduleDisposeAll() { - if (timer) clearTimeout(timer) - timer = setTimeout(() => { - timer = undefined - void Instance.disposeAll().catch(() => undefined) - }, 300) -} diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index 9cf665a801d..fffa9233ee4 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -125,7 +125,6 @@ export namespace ProviderAuth { // kilocode_change start - invalidate provider/model cache after auth change ModelCache.clear(input.providerID) - scheduleDisposeAll() // kilocode_change end return @@ -147,7 +146,6 @@ export namespace ProviderAuth { }) // kilocode_change start - invalidate provider/model cache after auth change ModelCache.clear(input.providerID) - scheduleDisposeAll() // kilocode_change end }, ) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 62671ba25cc..26809ef5261 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -171,7 +171,6 @@ export namespace Server { await Auth.set(providerID, info) // kilocode_change start - invalidate provider/model cache after auth change ModelCache.clear(providerID) - scheduleDisposeAll() // kilocode_change end return c.json(true) }, @@ -205,7 +204,6 @@ export namespace Server { await Auth.remove(providerID) // kilocode_change start - invalidate provider/model cache after auth removal ModelCache.clear(providerID) - scheduleDisposeAll() // kilocode_change end return c.json(true) }, From d4f7a6892507ba667fe453aff0ce721ed063c30d Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Thu, 5 Mar 2026 16:32:32 +0100 Subject: [PATCH 02/44] fix: Removed dependencies --- packages/opencode/src/provider/auth.ts | 1 - packages/opencode/src/server/server.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index fffa9233ee4..b8648d609ab 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -8,7 +8,6 @@ import { NamedError } from "@opencode-ai/util/error" import { Auth } from "@/auth" import { Telemetry } from "@kilocode/kilo-telemetry" // kilocode_change import { ModelCache } from "./model-cache" // kilocode_change -import { scheduleDisposeAll } from "../kilocode/dispose" // kilocode_change export namespace ProviderAuth { const state = Instance.state(async () => { diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 26809ef5261..8e7f4f38a79 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -19,7 +19,6 @@ import { Agent } from "../agent/agent" import { Skill } from "../skill/skill" import { Auth } from "../auth" import { ModelCache } from "../provider/model-cache" // kilocode_change -import { scheduleDisposeAll } from "../kilocode/dispose" // kilocode_change import { Flag } from "../flag/flag" import { Command } from "../command" import { Global } from "../global" From 927f75e54025b9d76a70bec5a3d0f02084929e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 5 Mar 2026 18:35:09 +0100 Subject: [PATCH 03/44] feat: add model per mode settings (#6211) Add a "Model per Mode" section in Provider settings where users can set a default model for each primary agent (code, plan, debug, orchestrator, ask). The per-mode model is used automatically when starting new sessions in that mode, with the precedence: session override > per-mode config > global default model. Also fixes the "unset" bug where clearing a model default didn't persist because undefined values were stripped during JSON serialization. Now uses null as a delete sentinel throughout the config pipeline. --- .../src/components/settings/ProvidersTab.tsx | 50 ++++++++++++++++++- .../webview-ui/src/context/config.tsx | 35 ++++++++++++- .../webview-ui/src/context/session.tsx | 40 +++++++++++++-- .../kilo-vscode/webview-ui/src/i18n/en.ts | 2 + .../webview-ui/src/types/messages.ts | 6 +-- packages/opencode/src/config/config.ts | 27 +++++++--- 6 files changed, 143 insertions(+), 17 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx index a953352c5d2..aec7acfe4e6 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx @@ -6,6 +6,7 @@ import { IconButton } from "@kilocode/kilo-ui/icon-button" import { useConfig } from "../../context/config" import { useProvider } from "../../context/provider" import { useLanguage } from "../../context/language" +import { useSession } from "../../context/session" import { ModelSelectorBase } from "../shared/ModelSelector" import type { ModelSelection } from "../../types/messages" import SettingsRow from "./SettingsRow" @@ -31,6 +32,7 @@ const ProvidersTab: Component = () => { const { config, updateConfig } = useConfig() const provider = useProvider() const language = useLanguage() + const session = useSession() const providerOptions = createMemo(() => Object.keys(provider.providers()) @@ -61,13 +63,27 @@ const ProvidersTab: Component = () => { function handleModelSelect(configKey: "model" | "small_model") { return (providerID: string, modelID: string) => { if (!providerID || !modelID) { - updateConfig({ [configKey]: undefined }) + updateConfig({ [configKey]: null }) } else { updateConfig({ [configKey]: `${providerID}/${modelID}` }) } } } + const primaryAgents = createMemo(() => + session.agents().filter((a) => a.mode === "primary"), + ) + + function handleModeModelSelect(agentName: string) { + return (providerID: string, modelID: string) => { + if (!providerID || !modelID) { + updateConfig({ agent: { [agentName]: { model: null } } }) + } else { + updateConfig({ agent: { [agentName]: { model: `${providerID}/${modelID}` } } }) + } + } + } + return (
{/* Model selection */} @@ -99,6 +115,38 @@ const ProvidersTab: Component = () => { + {/* Model per Mode */} +

{language.t("settings.providers.modeModels")}

+ +
0 ? "1px solid var(--border-weak-base)" : "none", + }} + > + {language.t("settings.providers.modeModels.description")} +
+ + {(agent, index) => ( + + + + )} + +
+ {/* Disabled providers */}

{language.t("settings.providers.disabled")}

diff --git a/packages/kilo-vscode/webview-ui/src/context/config.tsx b/packages/kilo-vscode/webview-ui/src/context/config.tsx index 9155da011ca..12ca11dc282 100644 --- a/packages/kilo-vscode/webview-ui/src/context/config.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/config.tsx @@ -14,6 +14,37 @@ interface ConfigContextValue { updateConfig: (partial: Partial) => void } +function isRecord(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value) +} + +/** Deep merge two objects, with source values overriding target values. */ +function deepMerge(target: Config, source: Partial): Config { + const result: Record = { ...target } + for (const [key, value] of Object.entries(source)) { + if (isRecord(value) && isRecord(result[key])) { + result[key] = deepMerge(result[key] as Config, value as Partial) + } else { + result[key] = value + } + } + return result as Config +} + +/** Recursively remove keys whose value is null (null = "deleted"). */ +function stripNulls(obj: Config): Config { + const result: Record = {} + for (const [key, value] of Object.entries(obj)) { + if (value === null || value === undefined) continue + if (isRecord(value)) { + result[key] = stripNulls(value as Config) + } else { + result[key] = value + } + } + return result as Config +} + const ConfigContext = createContext() export const ConfigProvider: ParentComponent = (props) => { @@ -59,8 +90,8 @@ export const ConfigProvider: ParentComponent = (props) => { onCleanup(() => clearInterval(retryTimer)) function updateConfig(partial: Partial) { - // Optimistically update local state - setConfig((prev) => ({ ...prev, ...partial })) + // Optimistically update local state with deep merge + null stripping + setConfig((prev) => stripNulls(deepMerge(prev, partial))) // Send to extension for persistence vscode.postMessage({ type: "updateConfig", config: partial }) } diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 27f9cc6646b..6749efd7385 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -20,6 +20,7 @@ import { createStore, produce } from "solid-js/store" import { useVSCode } from "./vscode" import { useServer } from "./server" import { useProvider } from "./provider" +import { useConfig } from "./config" import { useLanguage } from "./language" import { showToast } from "@kilocode/kilo-ui/toast" import type { @@ -144,6 +145,7 @@ export const SessionProvider: ParentComponent = (props) => { const vscode = useVSCode() const server = useServer() const provider = useProvider() + const { config } = useConfig() const language = useLanguage() // Current session ID @@ -202,7 +204,7 @@ export const SessionProvider: ParentComponent = (props) => { variantSelections: {}, }) - // Keep pending selection in sync with provider default until the user + // Keep pending selection in sync with provider/mode default until the user // explicitly changes it (or a session exists). createEffect(() => { const def = provider.defaultSelection() @@ -214,23 +216,45 @@ export const SessionProvider: ParentComponent = (props) => { return } - setPendingModelSelection(def) + // Per-mode config takes priority over global default + const pendingAgent = pendingAgentSelection() ?? defaultAgent() + const modeModel = getModeModel(pendingAgent) + setPendingModelSelection(modeModel ?? def) }) // If we have no pending yet, initialize it from provider default. createEffect(() => { if (!pendingModelSelection()) { - setPendingModelSelection(provider.defaultSelection()) + const pendingAgent = pendingAgentSelection() ?? defaultAgent() + const modeModel = getModeModel(pendingAgent) + setPendingModelSelection(modeModel ?? provider.defaultSelection()) } }) + /** Parse a "provider/model" config string into a ModelSelection (or null). */ + function getModeModel(agentName: string): ModelSelection | null { + const raw = config().agent?.[agentName]?.model + if (!raw) return null + const slash = raw.indexOf("/") + if (slash <= 0) return null + return { providerID: raw.slice(0, slash), modelID: raw.slice(slash + 1) } + } + // Per-session model selection + // Precedence: session override > per-mode config > global default > kilo/auto const selected = createMemo(() => { const sessionID = currentSessionID() if (sessionID) { - return store.modelSelections[sessionID] ?? provider.defaultSelection() + const sessionModel = store.modelSelections[sessionID] + if (sessionModel) return sessionModel + const agentName = store.agentSelections[sessionID] ?? defaultAgent() + return getModeModel(agentName) ?? provider.defaultSelection() } - return pendingModelSelection() + // Pre-session: check pending agent's per-mode default + const pending = pendingModelSelection() + if (pending) return pending + const pendingAgent = pendingAgentSelection() ?? defaultAgent() + return getModeModel(pendingAgent) ?? provider.defaultSelection() }) // Per-session agent selection @@ -788,6 +812,12 @@ export const SessionProvider: ParentComponent = (props) => { setStore("agentSelections", id, name) } else { setPendingAgentSelection(name) + // When switching mode pre-session, update pending model to per-mode default + // (unless user explicitly set a model for this session) + if (!pendingWasUserSet()) { + const modeModel = getModeModel(name) + setPendingModelSelection(modeModel ?? provider.defaultSelection()) + } } } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index cf8a9123e86..34d08218156 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -988,6 +988,8 @@ export const dict = { "settings.providers.defaultModel.description": "Primary model for conversations", "settings.providers.smallModel.title": "Small Model", "settings.providers.smallModel.description": "Lightweight model for title generation and other quick tasks", + "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels.description": "Override the default model for specific modes. If not set, the global default model is used.", "settings.providers.disabled": "Disabled Providers", "settings.providers.disabled.description": "Providers to hide from the provider list", "settings.providers.enabled": "Enabled Providers (Allowlist)", diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index 7f253ff1706..bbb92241dee 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -257,7 +257,7 @@ export type PermissionLevel = "allow" | "ask" | "deny" export type PermissionConfig = Partial> export interface AgentConfig { - model?: string + model?: string | null prompt?: string temperature?: number top_p?: number @@ -309,8 +309,8 @@ export interface ExperimentalConfig { export interface Config { permission?: PermissionConfig - model?: string - small_model?: string + model?: string | null + small_model?: string | null default_agent?: string agent?: Record provider?: Record diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index d73f9c00156..fe86ccfb8c2 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -774,7 +774,7 @@ export namespace Config { export const Agent = z .object({ - model: ModelId.optional(), + model: ModelId.nullable().optional(), variant: z .string() .optional() @@ -1129,8 +1129,8 @@ export namespace Config { .array(z.string()) .optional() .describe("When set, ONLY these providers will be enabled. All other providers will be ignored"), - model: ModelId.describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(), - small_model: ModelId.describe( + model: ModelId.nullable().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(), + small_model: ModelId.nullable().describe( "Small model to use for tasks like title generation in the format of provider/model", ).optional(), // kilocode_change start - renamed from "build" to "code" @@ -1407,7 +1407,7 @@ export namespace Config { export async function update(config: Info) { const filepath = path.join(Instance.directory, "config.json") const existing = await loadFile(filepath) - await Filesystem.writeJson(filepath, mergeDeep(existing, config)) + await Filesystem.writeJson(filepath, stripNulls(mergeDeep(existing, config) as Record)) await Instance.dispose() } @@ -1427,9 +1427,24 @@ export namespace Config { return !!value && typeof value === "object" && !Array.isArray(value) } + /** Recursively remove keys whose value is null (used after mergeDeep to honor delete sentinels). */ + function stripNulls(obj: Record): Record { + const result: Record = {} + for (const [key, value] of Object.entries(obj)) { + if (value === null) continue + if (isRecord(value)) { + result[key] = stripNulls(value) + } else { + result[key] = value + } + } + return result + } + function patchJsonc(input: string, patch: unknown, path: string[] = []): string { if (!isRecord(patch)) { - const edits = modify(input, path, patch, { + // null means "delete this key" — pass undefined to jsonc-parser's modify() + const edits = modify(input, path, patch === null ? undefined : patch, { formattingOptions: { insertSpaces: true, tabSize: 2, @@ -1488,7 +1503,7 @@ export namespace Config { const next = await (async () => { if (!filepath.endsWith(".jsonc")) { const existing = parseConfig(before, filepath) - const merged = mergeDeep(existing, config) + const merged = stripNulls(mergeDeep(existing, config) as Record) as Info await Filesystem.writeJson(filepath, merged) return merged } From b360640d87cd262df57b1e6ca5b80d36b29e0042 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Mar 2026 18:07:21 +0000 Subject: [PATCH 04/44] chore: update kilo-vscode visual regression baselines --- .../settings/providers-configure-chromium-linux.png | 4 ++-- .../settings/settings-panel-chromium-linux.png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/providers-configure-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/providers-configure-chromium-linux.png index b3dabe85f58..f1c71267506 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/providers-configure-chromium-linux.png +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/providers-configure-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5debe43731dd71da23ad462caad112b5af6e588d1b2ebf5b151ed66d87e5d477 -size 29253 +oid sha256:678e5f50021e13b0fc9d7af02ff26dd80b79f62bedb9417ade040cd49c3d2cca +size 38798 diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/settings-panel-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/settings-panel-chromium-linux.png index ff5fde6e1bc..9297d5171e6 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/settings-panel-chromium-linux.png +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/settings-panel-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b782a2ce193e54bd4fdd2ebc1490ac9674325a806ccabd56a7d3ff20c87e423 -size 39285 +oid sha256:d317f27c6e3cb810d90759953f1bacedd33fb4e0f09bf23b8845d293cb4c3220 +size 48191 From e5c3cc13e9bf70c92b32cb59f10ee4ed0d630eee Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Fri, 6 Mar 2026 09:13:43 +0100 Subject: [PATCH 05/44] refactor: extract ErrorDisplay component and shared unwrapError utility --- .../src/components/chat/ErrorDisplay.tsx | 26 ++++++++++++ .../src/components/chat/VscodeSessionTurn.tsx | 8 ++-- .../webview-ui/src/utils/errorUtils.ts | 41 +++++++++++++++++++ 3 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx create mode 100644 packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx new file mode 100644 index 00000000000..8824e4781a2 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx @@ -0,0 +1,26 @@ + +import { Component, createMemo, Show } from "solid-js" +import { Card } from "@kilocode/kilo-ui/card" +import type { AssistantMessage } from "@kilocode/sdk/v2" +import { unwrapError } from "../../utils/errorUtils" + +interface ErrorDisplayProps { + error: NonNullable +} + +export const ErrorDisplay: Component = (props) => { + + const errorText = createMemo(() => { + const msg = props.error.data?.message + if (typeof msg === "string") return unwrapError(msg) + if (msg === undefined || msg === null) return "" + return unwrapError(String(msg)) + }) + + + return ( + + {errorText()} + + ) +} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx index eacbef679f6..c67197087b4 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx @@ -28,6 +28,8 @@ import type { Part as SDKPart, FileDiff, } from "@kilocode/sdk/v2" +import { ErrorDisplay } from "./ErrorDisplay" + function getDirectory(path: string): string { const sep = path.includes("/") ? "/" : "\\" const idx = path.lastIndexOf(sep) @@ -326,11 +328,9 @@ export const VscodeSessionTurn: Component = (props) => {
- {/* Error card */} + {/* Error handling */} - - {errorText()} - + )} diff --git a/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts b/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts new file mode 100644 index 00000000000..3314a3d381b --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts @@ -0,0 +1,41 @@ +export function unwrapError(message: string): string { + const text = message.replace(/^Error:\s*/, "").trim() + const tryParse = (v: string) => { + try { + return JSON.parse(v) as unknown + } catch { + return undefined + } + } + const read = (v: string) => { + const first = tryParse(v) + if (typeof first !== "string") return first + return tryParse(first.trim()) + } + let json = read(text) + if (json === undefined) { + const start = text.indexOf("{") + const end = text.lastIndexOf("}") + if (start !== -1 && end > start) json = read(text.slice(start, end + 1)) + } + if (!json || typeof json !== "object" || Array.isArray(json)) return message + const rec = json as Record + const err = + rec.error && typeof rec.error === "object" && !Array.isArray(rec.error) + ? (rec.error as Record) + : undefined + if (err) { + const type = typeof err.type === "string" ? err.type : undefined + const msg = typeof err.message === "string" ? err.message : undefined + if (type && msg) return `${type}: ${msg}` + if (msg) return msg + if (type) return type + const code = typeof err.code === "string" ? err.code : undefined + if (code) return code + } + const msg = typeof rec.message === "string" ? rec.message : undefined + if (msg) return msg + const reason = typeof rec.error === "string" ? rec.error : undefined + if (reason) return reason + return message +} From acc0380818d3025e593a86634d424594c0610723 Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Fri, 6 Mar 2026 09:19:18 +0100 Subject: [PATCH 06/44] refactor: use shared unwrapError in VscodeSessionTurn --- .../src/components/chat/ErrorDisplay.tsx | 1 - .../src/components/chat/VscodeSessionTurn.tsx | 43 +------------------ 2 files changed, 1 insertion(+), 43 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx index 8824e4781a2..214ffc7fda0 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx @@ -17,7 +17,6 @@ export const ErrorDisplay: Component = (props) => { return unwrapError(String(msg)) }) - return ( {errorText()} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx index c67197087b4..a93d2a8ebe2 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx @@ -29,6 +29,7 @@ import type { FileDiff, } from "@kilocode/sdk/v2" import { ErrorDisplay } from "./ErrorDisplay" +import { unwrapError } from "../../utils/errorUtils" function getDirectory(path: string): string { const sep = path.includes("/") ? "/" : "\\" @@ -42,48 +43,6 @@ function getFilename(path: string): string { return idx === -1 ? path : path.slice(idx + 1) } -function unwrapError(message: string): string { - const text = message.replace(/^Error:\s*/, "").trim() - const tryParse = (v: string) => { - try { - return JSON.parse(v) as unknown - } catch { - return undefined - } - } - const read = (v: string) => { - const first = tryParse(v) - if (typeof first !== "string") return first - return tryParse(first.trim()) - } - let json = read(text) - if (json === undefined) { - const start = text.indexOf("{") - const end = text.lastIndexOf("}") - if (start !== -1 && end > start) json = read(text.slice(start, end + 1)) - } - if (!json || typeof json !== "object" || Array.isArray(json)) return message - const rec = json as Record - const err = - rec.error && typeof rec.error === "object" && !Array.isArray(rec.error) - ? (rec.error as Record) - : undefined - if (err) { - const type = typeof err.type === "string" ? err.type : undefined - const msg = typeof err.message === "string" ? err.message : undefined - if (type && msg) return `${type}: ${msg}` - if (msg) return msg - if (type) return type - const code = typeof err.code === "string" ? err.code : undefined - if (code) return code - } - const msg = typeof rec.message === "string" ? rec.message : undefined - if (msg) return msg - const reason = typeof rec.error === "string" ? rec.error : undefined - if (reason) return reason - return message -} - interface VscodeSessionTurnProps { sessionID: string messageID: string From 512642662f3ca2b5eb7559c76cbc4ae1240ff548 Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Fri, 6 Mar 2026 09:37:45 +0100 Subject: [PATCH 07/44] feat: add error parsing and classification helpers for Kilo API errors --- .../webview-ui/src/utils/errorUtils.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts b/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts index 3314a3d381b..183bd86b355 100644 --- a/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts +++ b/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts @@ -39,3 +39,57 @@ export function unwrapError(message: string): string { if (reason) return reason return message } + +export const errorCodes = { + PAID_MODEL_AUTH_REQUIRED: "PAID_MODEL_AUTH_REQUIRED", + PROMOTION_MODEL_LIMIT_REACHED: "PROMOTION_MODEL_LIMIT_REACHED", +} as const + +export interface ParsedError { + statusCode?: number + code?: string + message?: string +} + +export function parseAssistantError( + error: { name: string; data?: Record } | null | undefined, +): ParsedError | null { + if (!error) return null + if (error.name !== "APIError") return null + + const data = error.data + if (!data) return null + + const statusCode = typeof data.statusCode === "number" ? data.statusCode : undefined + const message = typeof data.message === "string" ? data.message : undefined + + let code: string | undefined + if (typeof data.responseBody === "string") { + try { + const body = JSON.parse(data.responseBody) as Record + const bodyError = body.error as Record | undefined + if (bodyError && typeof bodyError.code === "string") { + code = bodyError.code + } else if (typeof body.code === "string") { + code = body.code + } + } catch { + // responseBody is not valid JSON — ignore + } + } + + return { statusCode, code, message } +} + +export function isUnauthorizedPaidModelError(parsed: ParsedError | null): boolean { + if (!parsed) return false + return parsed.statusCode === 401 && parsed.code === errorCodes.PAID_MODEL_AUTH_REQUIRED +} + +export function isUnauthorizedPromotionLimitError(parsed: ParsedError | null): boolean { + if (!parsed) return false + return ( + (parsed.statusCode === 401 || parsed.statusCode === 429) && + parsed.code === errorCodes.PROMOTION_MODEL_LIMIT_REACHED + ) +} From b5c17df9d60988429e9ddc8c8e11fa22b521ccc8 Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Fri, 6 Mar 2026 09:39:32 +0100 Subject: [PATCH 08/44] feat: add specific error displays for paid model and promotion limit in ErrorDisplay --- .../src/components/chat/ErrorDisplay.tsx | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx index 214ffc7fda0..905e97fc721 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx @@ -1,14 +1,21 @@ - -import { Component, createMemo, Show } from "solid-js" +import { Component, createMemo, Switch, Match } from "solid-js" import { Card } from "@kilocode/kilo-ui/card" +import { Button } from "@kilocode/kilo-ui/button" import type { AssistantMessage } from "@kilocode/sdk/v2" -import { unwrapError } from "../../utils/errorUtils" +import { + unwrapError, + parseAssistantError, + isUnauthorizedPaidModelError, + isUnauthorizedPromotionLimitError, +} from "../../utils/errorUtils" interface ErrorDisplayProps { error: NonNullable + onLogin?: () => void } export const ErrorDisplay: Component = (props) => { + const parsed = createMemo(() => parseAssistantError(props.error)) const errorText = createMemo(() => { const msg = props.error.data?.message @@ -18,8 +25,25 @@ export const ErrorDisplay: Component = (props) => { }) return ( - - {errorText()} - + + {errorText()} + + }> + + +
Sign in required
+

You need to sign in to use this model.

+ +
+
+ + +
Sign up required
+

You need to sign up to keep using this model.

+ +
+
+ ) } From b3c404a274a75440969e70746fbd26b28a9fffc0 Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Fri, 6 Mar 2026 09:46:17 +0100 Subject: [PATCH 09/44] feat: wire up login callback from VscodeSessionTurn to ErrorDisplay --- .../src/components/chat/VscodeSessionTurn.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx index a93d2a8ebe2..f9fef46f616 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx @@ -16,7 +16,6 @@ import { Collapsible } from "@kilocode/kilo-ui/collapsible" import { Accordion } from "@kilocode/kilo-ui/accordion" import { DiffChanges } from "@kilocode/kilo-ui/diff-changes" import { Icon } from "@kilocode/kilo-ui/icon" -import { Card } from "@kilocode/kilo-ui/card" import { StickyAccordionHeader } from "@kilocode/kilo-ui/sticky-accordion-header" import { useData } from "@kilocode/kilo-ui/context/data" import { useDiffComponent } from "@kilocode/kilo-ui/context/diff" @@ -29,7 +28,7 @@ import type { FileDiff, } from "@kilocode/sdk/v2" import { ErrorDisplay } from "./ErrorDisplay" -import { unwrapError } from "../../utils/errorUtils" +import { useServer } from "../../context/server" function getDirectory(path: string): string { const sep = path.includes("/") ? "/" : "\\" @@ -53,6 +52,7 @@ export const VscodeSessionTurn: Component = (props) => { const data = useData() const i18n = useI18n() const diffComponent = useDiffComponent() + const server = useServer() const emptyMessages: SDKMessage[] = [] const emptyParts: SDKPart[] = [] @@ -100,13 +100,6 @@ export const VscodeSessionTurn: Component = (props) => { () => assistantMessages().find((m) => m.error && m.error.name !== "MessageAbortedError")?.error, ) - const errorText = createMemo(() => { - const msg = error()?.data?.message - if (typeof msg === "string") return unwrapError(msg) - if (msg === undefined || msg === null) return "" - return unwrapError(String(msg)) - }) - // Diffs from message summary const diffs = createMemo(() => { const rawDiffs = (message() as unknown as { summary?: { diffs?: unknown[] } } | undefined)?.summary?.diffs @@ -289,7 +282,7 @@ export const VscodeSessionTurn: Component = (props) => { {/* Error handling */} - + )} From 84c5df6ea3a608db22752b6200f0b0e0057a9cae Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Fri, 6 Mar 2026 10:17:20 +0100 Subject: [PATCH 10/44] style: use plain layout with legacy copy for auth error displays --- .../src/components/chat/ErrorDisplay.tsx | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx index 905e97fc721..308318e8c20 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx @@ -31,18 +31,28 @@ export const ErrorDisplay: Component = (props) => { }> - -
Sign in required
-

You need to sign in to use this model.

- -
+
+
+ ✨ + You need to sign in to use this model +
+

+ Sign in or create an account to access over 500 models, use credits at cost, or bring your own key. +

+ +
- -
Sign up required
-

You need to sign up to keep using this model.

- -
+
+
+ 🕙 + You need to sign up to keep going +
+

+ Sign up for free to continue and explore 500 other models. Takes 2 minutes, no credit card required. Or come back later. +

+ +
) From a3c2f362542b30f1021abc99039ad12cefab1718 Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Fri, 6 Mar 2026 10:56:13 +0100 Subject: [PATCH 11/44] style: add auth-prompt component styles using data-component/data-slot convention --- .../src/components/chat/ErrorDisplay.tsx | 20 +++++------ .../webview-ui/src/styles/chat.css | 34 +++++++++++++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx index 308318e8c20..1fa83fb06be 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx @@ -31,24 +31,24 @@ export const ErrorDisplay: Component = (props) => { }> -
-
- ✨ - You need to sign in to use this model +
+
+ ✨ + You need to sign in to use this model
-

+

Sign in or create an account to access over 500 models, use credits at cost, or bring your own key.

-
-
- 🕙 - You need to sign up to keep going +
+
+ 🕙 + You need to sign up to keep going
-

+

Sign up for free to continue and explore 500 other models. Takes 2 minutes, no credit card required. Or come back later.

diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat.css b/packages/kilo-vscode/webview-ui/src/styles/chat.css index 1edf8d0a9bf..bbeacc39a75 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat.css @@ -1284,6 +1284,40 @@ } } +/* ============================================ + Auth Prompt (sign in / sign up error displays) + ============================================ */ + +[data-component="auth-prompt"] { + display: flex; + flex-direction: column; + gap: 8px; +} + +[data-component="auth-prompt"] [data-slot="auth-prompt-header"] { + display: flex; + align-items: center; + gap: 8px; +} + +[data-component="auth-prompt"] [data-slot="auth-prompt-icon"] { + font-size: 16px; + line-height: 1; +} + +[data-component="auth-prompt"] [data-slot="auth-prompt-title"] { + font-weight: 600; + color: var(--text-strong, var(--vscode-foreground)); + font-size: 13px; +} + +[data-component="auth-prompt"] [data-slot="auth-prompt-description"] { + color: var(--text-weak, var(--vscode-descriptionForeground)); + font-size: 12px; + margin: 0; + line-height: 1.4; +} + /* ============================================ Enhance Prompt Spinner ============================================ */ From 5f75c2fbe2792d38c601fad0cf7696262b5125f3 Mon Sep 17 00:00:00 2001 From: Marius Date: Fri, 6 Mar 2026 11:03:21 +0100 Subject: [PATCH 12/44] docs: improve JSDoc comments in GitOps agent-manager module (#6684) * docs: improve JSDoc comments in GitOps agent-manager module * Update packages/kilo-vscode/src/agent-manager/GitOps.ts Co-authored-by: kilo-code-bot[bot] <240665456+kilo-code-bot[bot]@users.noreply.github.com> * Update packages/kilo-vscode/src/agent-manager/GitOps.ts Co-authored-by: kilo-code-bot[bot] <240665456+kilo-code-bot[bot]@users.noreply.github.com> --------- Co-authored-by: kilo-code-bot[bot] <240665456+kilo-code-bot[bot]@users.noreply.github.com> --- .../kilo-vscode/src/agent-manager/GitOps.ts | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/kilo-vscode/src/agent-manager/GitOps.ts b/packages/kilo-vscode/src/agent-manager/GitOps.ts index d94b1269b09..6fe353bd7b5 100644 --- a/packages/kilo-vscode/src/agent-manager/GitOps.ts +++ b/packages/kilo-vscode/src/agent-manager/GitOps.ts @@ -62,6 +62,7 @@ export class GitOps { return this.runGit(args, cwd) } + /** Return the name of the currently checked-out branch, or `"HEAD"` if detached. */ async currentBranch(cwd: string): Promise { return this.raw(["rev-parse", "--abbrev-ref", "HEAD"], cwd).catch(() => "") } @@ -87,6 +88,7 @@ export class GitOps { return "origin" } + /** Resolve the upstream tracking ref for `branch`, or `undefined` if none is set. Note: the `@{upstream}` check uses the current HEAD, not `branch`. */ async resolveTrackingBranch(cwd: string, branch: string): Promise { const upstream = await this.raw(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd).catch(() => "") if (upstream) return upstream @@ -139,6 +141,7 @@ export class GitOps { return job } + /** Return the set of worktree paths for the repo, excluding bare entries. */ async listWorktreePaths(cwd: string): Promise> { const raw = await this.raw(["worktree", "list", "--porcelain"], cwd) const paths = new Set() @@ -151,12 +154,13 @@ export class GitOps { /** * Compute working-tree stats (staged + unstaged + untracked) without requiring - * a remote or base branch — mirrors the superset approach of running - * `git diff --numstat` and `git ls-files --others`. + * a remote or base branch. Combines `git diff HEAD --numstat` for tracked + * changes with `git ls-files --others` for new files. + * + * Returns aggregate file count, additions, and deletions across the working tree. */ async workingTreeStats(cwd: string): Promise<{ files: number; additions: number; deletions: number }> { - // Staged + unstaged changes relative to HEAD (like superset's dual - // git diff --cached --numstat + git diff --numstat, combined). + // Single diff against HEAD captures both staged and unstaged changes. const [numstat, untracked] = await Promise.all([ this.raw(["diff", "HEAD", "--numstat"], cwd).catch(() => ""), this.raw(["ls-files", "--others", "--exclude-standard"], cwd).catch(() => ""), @@ -177,8 +181,8 @@ export class GitOps { ) : { files: 0, additions: 0, deletions: 0 } - // Count lines in untracked files as additions (like superset's - // applyUntrackedLineCount). Cap at 1MB to avoid reading huge binaries. + // Count lines in untracked files as additions. Cap at 1MB to avoid + // reading large binary files into memory. if (!untracked) return tracked const paths = untracked.split("\n").filter((line) => line.trim()) @@ -205,9 +209,9 @@ export class GitOps { } /** - * Count commits ahead and behind in a single `rev-list --left-right --count` - * call (like superset's approach). Falls back through upstream → remote/branch - * → remote/parentBranch → parentBranch. + * Count commits ahead and behind using `rev-list --left-right --count`. + * Tries the best available ref in order: upstream tracking branch → + * remote/branch → remote/parentBranch → local parentBranch. */ async aheadBehind(cwd: string, parentBranch: string): Promise<{ ahead: number; behind: number }> { const upstream = await this.raw(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd).catch(() => "") @@ -241,6 +245,10 @@ export class GitOps { return { ahead, behind } } + /** + * Build a binary-safe patch of all working-tree changes relative to the + * merge-base with `baseBranch`. Optionally scoped to `selectedFiles`. + */ async buildWorktreePatch(sourcePath: string, baseBranch: string, selectedFiles?: string[]): Promise { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), "kilo-apply-")) const index = nodePath.join(tmp, "index") From 732cc14012a4f4d7709827abec7383cb0e53782b Mon Sep 17 00:00:00 2001 From: Marius Date: Fri, 6 Mar 2026 11:03:41 +0100 Subject: [PATCH 13/44] fix(agent-manager): resolve macOS shell PATH for GUI-launched VS Code (#6683) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(agent-manager): resolve macOS shell PATH for GUI-launched VS Code * fix: address PR review — concurrent callers, fallback detection, multiline parsing - Use shared promise so concurrent ENOENT callers wait on one resolution instead of rejecting prematurely - Compare resolved PATH against original before marking fix as succeeded, so fallback-to-process.env doesn't permanently block retries - Parse env output with proper KEY= regex to handle multiline values (BASH_FUNC_*, certs) without corrupting other entries --- .../src/agent-manager/WorktreeManager.ts | 12 +- .../src/agent-manager/shell-env.ts | 173 ++++++++++++++++++ .../kilo-vscode/tests/unit/shell-env.test.ts | 76 ++++++++ 3 files changed, 253 insertions(+), 8 deletions(-) create mode 100644 packages/kilo-vscode/src/agent-manager/shell-env.ts create mode 100644 packages/kilo-vscode/tests/unit/shell-env.test.ts diff --git a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts index 59d50dff70f..79abe8620d1 100644 --- a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts +++ b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts @@ -8,10 +8,10 @@ import * as path from "path" import * as fs from "fs" -import * as cp from "child_process" import simpleGit, { type SimpleGit } from "simple-git" import { generateBranchName, sanitizeBranchName } from "./branch-name" import type { GitOps } from "./GitOps" +import { execWithShellEnv } from "./shell-env" import { parsePRUrl, localBranchName, @@ -561,13 +561,9 @@ export class WorktreeManager { } } - private exec(cmd: string, args: string[], timeout = 120000): Promise { - return new Promise((resolve, reject) => { - cp.execFile(cmd, args, { cwd: this.root, timeout, encoding: "utf-8" }, (error, stdout) => { - if (error) reject(error) - else resolve(stdout) - }) - }) + private async exec(cmd: string, args: string[], timeout = 120000): Promise { + const { stdout } = await execWithShellEnv(cmd, args, { cwd: this.root, timeout }) + return stdout } private async gitExec(args: string[]): Promise { diff --git a/packages/kilo-vscode/src/agent-manager/shell-env.ts b/packages/kilo-vscode/src/agent-manager/shell-env.ts new file mode 100644 index 00000000000..54e8f53b32d --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/shell-env.ts @@ -0,0 +1,173 @@ +/** + * macOS shell environment PATH resolution. + * + * When VS Code is launched from Finder, Spotlight, or the Dock, the extension + * host inherits a minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin) that excludes + * directories added by package managers (homebrew, nvm, pipx, etc.) and shell + * profiles (.zshrc, .bash_profile). + * + * This module lazily resolves the user's real PATH by spawning a login shell + * and caches the result. The fix is applied on first ENOENT and persisted to + * process.env.PATH so all subsequent child_process calls benefit. + */ + +import { type ExecFileOptionsWithStringEncoding, execFile } from "child_process" +import * as os from "os" +import { promisify } from "util" + +const run = promisify(execFile) + +// Environment variable keys match: letters, digits, underscores, starting with a non-digit. +const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*=/ + +let cached: Record | null = null +let cacheTime = 0 +let wasFallback = false +const TTL = 60_000 +const FALLBACK_TTL = 10_000 + +/** In-flight fix promise so concurrent ENOENT callers wait on the same resolution. */ +let fixing: Promise | null = null +let fixed = false + +/** + * Parse `env` output, handling multiline variable values correctly. + * + * A new entry starts when a line matches `KEY=value` (KEY is a valid + * environment variable name). Lines that don't match are continuations + * of the previous value. + */ +function parseEnvOutput(stdout: string): Record { + const env: Record = {} + let key: string | null = null + let value = "" + + for (const line of stdout.split("\n")) { + const match = ENV_KEY_RE.exec(line) + if (match) { + if (key) env[key] = value + const idx = match[0].length - 1 // position of '=' + key = line.substring(0, idx) + value = line.substring(idx + 1) + } else if (key) { + value += "\n" + line + } + } + if (key) env[key] = value + return env +} + +/** + * Spawn the user's login shell to capture environment variables (primarily PATH). + * Uses `-lc` (login + command) — avoids `-i` (interactive) to skip TTY prompts. + * Results are cached for 1 minute (10 seconds when the fallback was used). + */ +export async function getShellEnvironment(): Promise> { + const now = Date.now() + const ttl = wasFallback ? FALLBACK_TTL : TTL + if (cached && now - cacheTime < ttl) return { ...cached } + + const shell = process.env.SHELL || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash") + + try { + const { stdout } = await run(shell, ["-lc", "env"], { + timeout: 10_000, + env: { ...process.env, HOME: os.homedir() }, + }) + + const env = parseEnvOutput(stdout) + cached = env + cacheTime = now + wasFallback = false + return { ...env } + } catch (error) { + console.warn(`[shell-env] Failed to get shell environment: ${error}. Falling back to process.env`) + const env: Record = {} + for (const [key, value] of Object.entries(process.env)) { + if (typeof value === "string") env[key] = value + } + cached = env + cacheTime = now + wasFallback = true + return { ...env } + } +} + +/** + * Attempt to resolve the shell environment and patch process.env.PATH. + * Returns true if PATH was actually changed, false otherwise. + */ +async function resolvePath(): Promise { + const original = process.env.PATH + const env = await getShellEnvironment() + + if (env.PATH && env.PATH !== original) { + process.env.PATH = env.PATH + console.log("[shell-env] Patched process.env.PATH for GUI app") + return true + } + // Shell env was a fallback or PATH didn't change — resolution didn't help + return false +} + +/** + * Execute a command, retrying once with shell environment on ENOENT. + * + * On macOS GUI launches, binaries installed by homebrew / nvm / etc. are not + * on the inherited PATH. When the first exec fails with ENOENT (command not + * found), this function resolves the user's login shell environment, patches + * process.env.PATH permanently, and retries the command. + * + * Concurrent callers that hit ENOENT share a single resolution promise so + * none are rejected prematurely. + */ +export async function execWithShellEnv( + cmd: string, + args: string[], + options?: Omit, +): Promise<{ stdout: string; stderr: string }> { + try { + return await run(cmd, args, { ...options, encoding: "utf8" }) + } catch (error) { + if ( + process.platform !== "darwin" || + !(error instanceof Error) || + !("code" in error) || + (error as NodeJS.ErrnoException).code !== "ENOENT" + ) { + throw error + } + + // Already resolved and PATH was actually changed — no point retrying resolution. + // Just retry with the (already-patched) process.env. + if (fixed) { + return await run(cmd, args, { ...options, encoding: "utf8" }) + } + + // If another caller is already resolving, wait for it then retry. + if (fixing) { + await fixing + return await run(cmd, args, { ...options, encoding: "utf8" }) + } + + console.log(`[shell-env] "${cmd}" not found, resolving shell environment`) + + fixing = resolvePath() + try { + fixed = await fixing + } finally { + fixing = null + } + + return await run(cmd, args, { ...options, encoding: "utf8" }) + } +} + +/** Clear the cached environment (for tests). */ +export function clearShellEnvCache(): void { + cached = null + cacheTime = 0 + wasFallback = false + fixing = null + fixed = false +} diff --git a/packages/kilo-vscode/tests/unit/shell-env.test.ts b/packages/kilo-vscode/tests/unit/shell-env.test.ts new file mode 100644 index 00000000000..e3e2520866d --- /dev/null +++ b/packages/kilo-vscode/tests/unit/shell-env.test.ts @@ -0,0 +1,76 @@ +import { afterEach, describe, expect, it } from "bun:test" +import { getShellEnvironment, execWithShellEnv, clearShellEnvCache } from "../../src/agent-manager/shell-env" + +afterEach(() => { + clearShellEnvCache() +}) + +describe("getShellEnvironment", () => { + it("returns an object with PATH", async () => { + const env = await getShellEnvironment() + expect(env).toBeDefined() + expect(typeof env.PATH).toBe("string") + expect(env.PATH!.length).toBeGreaterThan(0) + }) + + it("returns HOME", async () => { + const env = await getShellEnvironment() + expect(typeof env.HOME).toBe("string") + }) + + it("caches results across calls", async () => { + const first = await getShellEnvironment() + const second = await getShellEnvironment() + expect(first.PATH).toBe(second.PATH) + }) + + it("returns a copy (mutations don't corrupt cache)", async () => { + const first = await getShellEnvironment() + first.PATH = "/mutated" + const second = await getShellEnvironment() + expect(second.PATH).not.toBe("/mutated") + }) + + it("handles multiline env values without corrupting PATH", async () => { + // PATH should never contain newlines — verify it parses correctly + // even if other env vars have multiline values (e.g. BASH_FUNC_*) + const env = await getShellEnvironment() + expect(env.PATH).toBeDefined() + expect(env.PATH).not.toContain("\n") + }) +}) + +describe("execWithShellEnv", () => { + it("executes a simple command", async () => { + const { stdout } = await execWithShellEnv("echo", ["hello"]) + expect(stdout.trim()).toBe("hello") + }) + + it("passes cwd option through", async () => { + const { stdout } = await execWithShellEnv("pwd", [], { cwd: "/tmp" }) + // /tmp may resolve to /private/tmp on macOS + expect(stdout.trim()).toMatch(/\/tmp$/) + }) + + it("throws on non-ENOENT errors", async () => { + await expect(execWithShellEnv("ls", ["--nonexistent-flag-that-fails"])).rejects.toThrow() + }) + + it("concurrent calls don't reject prematurely", async () => { + // Both calls should succeed — neither should throw due to a race + const [a, b] = await Promise.all([execWithShellEnv("echo", ["first"]), execWithShellEnv("echo", ["second"])]) + expect(a.stdout.trim()).toBe("first") + expect(b.stdout.trim()).toBe("second") + }) +}) + +describe("clearShellEnvCache", () => { + it("forces fresh resolution on next call", async () => { + const first = await getShellEnvironment() + clearShellEnvCache() + const second = await getShellEnvironment() + // Both should succeed and contain PATH + expect(first.PATH).toBeDefined() + expect(second.PATH).toBeDefined() + }) +}) From ea8da42a37576ebc9d0d8a5b0b50349ca918bfb9 Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Fri, 6 Mar 2026 11:08:18 +0100 Subject: [PATCH 14/44] feat: add i18n keys and wire up useLanguage for error displays --- .../src/components/chat/ErrorDisplay.tsx | 14 ++++++++------ packages/kilo-vscode/webview-ui/src/i18n/en.ts | 7 +++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx index 1fa83fb06be..0dff10f341e 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx @@ -2,6 +2,7 @@ import { Component, createMemo, Switch, Match } from "solid-js" import { Card } from "@kilocode/kilo-ui/card" import { Button } from "@kilocode/kilo-ui/button" import type { AssistantMessage } from "@kilocode/sdk/v2" +import { useLanguage } from "../../context/language" import { unwrapError, parseAssistantError, @@ -15,6 +16,7 @@ interface ErrorDisplayProps { } export const ErrorDisplay: Component = (props) => { + const { t } = useLanguage() const parsed = createMemo(() => parseAssistantError(props.error)) const errorText = createMemo(() => { @@ -34,24 +36,24 @@ export const ErrorDisplay: Component = (props) => {
✨ - You need to sign in to use this model + {t("error.paidModel.title")}

- Sign in or create an account to access over 500 models, use credits at cost, or bring your own key. + {t("error.paidModel.description")}

- +
🕙 - You need to sign up to keep going + {t("error.promotionLimit.title")}

- Sign up for free to continue and explore 500 other models. Takes 2 minutes, no credit card required. Or come back later. + {t("error.promotionLimit.description")}

- +
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index cf8a9123e86..615785ee812 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -409,6 +409,13 @@ export const dict = { "error.globalSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?", + "error.paidModel.title": "You need to sign in to use this model", + "error.paidModel.description": "Sign in or create an account to access over 500 models, use credits at cost, or bring your own key.", + "error.paidModel.action": "Sign In", + "error.promotionLimit.title": "You need to sign up to keep going", + "error.promotionLimit.description": "Sign up for free to continue and explore 500 other models. Takes 2 minutes, no credit card required. Or come back later.", + "error.promotionLimit.action": "Sign Up", + "error.chain.unknown": "Unknown error", "error.chain.causedBy": "Caused by:", "error.chain.apiError": "API error", From b5c1c4052142ba3b4e8afaa73f605a52ef585c39 Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Fri, 6 Mar 2026 11:11:03 +0100 Subject: [PATCH 15/44] chore: bun run format --- packages/kilo-vscode/src/KiloProvider.ts | 23 +++++++-------- .../src/components/chat/ErrorDisplay.tsx | 28 ++++++++++--------- .../components/shared/model-selector-utils.ts | 3 +- .../kilo-vscode/webview-ui/src/i18n/en.ts | 6 ++-- .../webview-ui/src/utils/errorUtils.ts | 3 +- 5 files changed, 31 insertions(+), 32 deletions(-) diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 2603f571656..f29c2298b4b 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -1613,9 +1613,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper console.log("[Kilo New] KiloProvider: 🔐 Login successful") - await this.client.global.dispose().catch((e: unknown) => - console.warn("[Kilo New] KiloProvider: global.dispose() after login failed:", e), - ) + await this.client.global + .dispose() + .catch((e: unknown) => console.warn("[Kilo New] KiloProvider: global.dispose() after login failed:", e)) // Step 4: Fetch profile and push to webview const { data: profileData } = await this.client.kilo.profile(undefined, { throwOnError: true }) @@ -1662,9 +1662,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper return } - await this.client.global.dispose().catch((e: unknown) => - console.warn("[Kilo New] KiloProvider: global.dispose() after org switch failed:", e), - ) + await this.client.global + .dispose() + .catch((e: unknown) => console.warn("[Kilo New] KiloProvider: global.dispose() after org switch failed:", e)) // Org switch succeeded — refresh profile and providers independently (best-effort) try { @@ -1719,10 +1719,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper data: null, }) - await this.client.global.dispose().catch((e: unknown) => - console.warn("[Kilo New] KiloProvider: global.dispose() after logout failed:", e), - ) - + await this.client.global + .dispose() + .catch((e: unknown) => console.warn("[Kilo New] KiloProvider: global.dispose() after logout failed:", e)) } catch (error) { console.error("[Kilo New] KiloProvider: ❌ Logout failed:", error) this.postMessage({ @@ -1730,8 +1729,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper message: getErrorMessage(error) || "Failed to logout", }) } - - } /** @@ -1858,7 +1855,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper // Refresh provider and agent lists when the server signals a state disposal if (event.type === "server.instance.disposed" || event.type === "global.disposed") { - void this.reloadAfterAuthChange(); + void this.reloadAfterAuthChange() return } diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx index 0dff10f341e..52362b0653b 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx @@ -27,21 +27,23 @@ export const ErrorDisplay: Component = (props) => { }) return ( - - {errorText()} - - }> + + {errorText()} + + } + >
✨ {t("error.paidModel.title")}
-

- {t("error.paidModel.description")} -

- +

{t("error.paidModel.description")}

+
@@ -50,10 +52,10 @@ export const ErrorDisplay: Component = (props) => { 🕙 {t("error.promotionLimit.title")}
-

- {t("error.promotionLimit.description")} -

- +

{t("error.promotionLimit.description")}

+
diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts b/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts index 5c559477c44..51007700e61 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts @@ -31,8 +31,7 @@ export function buildTriggerLabel( hasProviders: boolean, labels: { select: string; noProviders: string; notSet: string }, ): string { - if (resolvedName) - return providerID === KILO_GATEWAY_ID ? stripSubProviderPrefix(resolvedName) : resolvedName + if (resolvedName) return providerID === KILO_GATEWAY_ID ? stripSubProviderPrefix(resolvedName) : resolvedName if (raw?.providerID && raw?.modelID) { return raw.providerID === KILO_GATEWAY_ID ? raw.modelID : `${raw.providerID} / ${raw.modelID}` } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 615785ee812..417a1e9f067 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -410,10 +410,12 @@ export const dict = { "error.globalSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?", "error.paidModel.title": "You need to sign in to use this model", - "error.paidModel.description": "Sign in or create an account to access over 500 models, use credits at cost, or bring your own key.", + "error.paidModel.description": + "Sign in or create an account to access over 500 models, use credits at cost, or bring your own key.", "error.paidModel.action": "Sign In", "error.promotionLimit.title": "You need to sign up to keep going", - "error.promotionLimit.description": "Sign up for free to continue and explore 500 other models. Takes 2 minutes, no credit card required. Or come back later.", + "error.promotionLimit.description": + "Sign up for free to continue and explore 500 other models. Takes 2 minutes, no credit card required. Or come back later.", "error.promotionLimit.action": "Sign Up", "error.chain.unknown": "Unknown error", diff --git a/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts b/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts index 183bd86b355..2fde89059f9 100644 --- a/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts +++ b/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts @@ -89,7 +89,6 @@ export function isUnauthorizedPaidModelError(parsed: ParsedError | null): boolea export function isUnauthorizedPromotionLimitError(parsed: ParsedError | null): boolean { if (!parsed) return false return ( - (parsed.statusCode === 401 || parsed.statusCode === 429) && - parsed.code === errorCodes.PROMOTION_MODEL_LIMIT_REACHED + (parsed.statusCode === 401 || parsed.statusCode === 429) && parsed.code === errorCodes.PROMOTION_MODEL_LIMIT_REACHED ) } From cb53d541b618e8b0137d34f8606bb9202c1441a8 Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Fri, 6 Mar 2026 11:25:07 +0100 Subject: [PATCH 16/44] test: add unit tests for errorUtils helpers --- .../kilo-vscode/tests/unit/errorUtils.test.ts | 115 ++++++++++++++++++ .../webview-ui/src/utils/errorUtils.ts | 4 + 2 files changed, 119 insertions(+) create mode 100644 packages/kilo-vscode/tests/unit/errorUtils.test.ts diff --git a/packages/kilo-vscode/tests/unit/errorUtils.test.ts b/packages/kilo-vscode/tests/unit/errorUtils.test.ts new file mode 100644 index 00000000000..89c8f054f42 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/errorUtils.test.ts @@ -0,0 +1,115 @@ +import { describe, it, expect } from "bun:test" +import { + unwrapError, + parseAssistantError, + isUnauthorizedPaidModelError, + isUnauthorizedPromotionLimitError, +} from "../../webview-ui/src/utils/errorUtils" + +describe("unwrapError", () => { + it("returns plain string messages unchanged", () => { + expect(unwrapError("something went wrong")).toBe("something went wrong") + }) + + it("extracts message from JSON error object with error.message", () => { + const input = JSON.stringify({ error: { message: "rate limit exceeded" } }) + expect(unwrapError(input)).toBe("rate limit exceeded") + }) + + it("returns original message for malformed JSON", () => { + expect(unwrapError("{not valid json")).toBe("{not valid json") + }) + + it("strips leading 'Error: ' prefix before parsing", () => { + const json = JSON.stringify({ message: "connection refused" }) + expect(unwrapError(`Error: ${json}`)).toBe("connection refused") + }) +}) + +describe("parseAssistantError", () => { + it("returns null for null input", () => { + expect(parseAssistantError(null)).toBeNull() + }) + + it("returns null for undefined input", () => { + expect(parseAssistantError(undefined)).toBeNull() + }) + + it("returns null for non-APIError (e.g. MessageAbortedError)", () => { + expect(parseAssistantError({ name: "MessageAbortedError" })).toBeNull() + }) + + it("returns null when APIError has no data", () => { + expect(parseAssistantError({ name: "APIError" })).toBeNull() + }) + + it("extracts statusCode and message from APIError data", () => { + const result = parseAssistantError({ + name: "APIError", + data: { statusCode: 401, message: "Unauthorized" }, + }) + expect(result).toEqual({ statusCode: 401, code: undefined, message: "Unauthorized" }) + }) + + it("extracts code from responseBody JSON with error.code", () => { + const responseBody = JSON.stringify({ error: { code: "PAID_MODEL_AUTH_REQUIRED" } }) + const result = parseAssistantError({ + name: "APIError", + data: { statusCode: 401, message: "Unauthorized", responseBody }, + }) + expect(result).toEqual({ statusCode: 401, code: "PAID_MODEL_AUTH_REQUIRED", message: "Unauthorized" }) + }) + + it("extracts code from responseBody JSON with top-level code", () => { + const responseBody = JSON.stringify({ code: "PROMOTION_MODEL_LIMIT_REACHED" }) + const result = parseAssistantError({ + name: "APIError", + data: { statusCode: 429, message: "Too Many Requests", responseBody }, + }) + expect(result).toEqual({ statusCode: 429, code: "PROMOTION_MODEL_LIMIT_REACHED", message: "Too Many Requests" }) + }) + + it("handles invalid responseBody JSON gracefully", () => { + const result = parseAssistantError({ + name: "APIError", + data: { statusCode: 500, message: "Server Error", responseBody: "not json" }, + }) + expect(result).toEqual({ statusCode: 500, code: undefined, message: "Server Error" }) + }) + + it("handles missing responseBody", () => { + const result = parseAssistantError({ + name: "APIError", + data: { statusCode: 403, message: "Forbidden" }, + }) + expect(result).toEqual({ statusCode: 403, code: undefined, message: "Forbidden" }) + }) +}) + +describe("isUnauthorizedPaidModelError", () => { + it("returns true for 401 + PAID_MODEL_AUTH_REQUIRED", () => { + expect(isUnauthorizedPaidModelError({ statusCode: 401, code: "PAID_MODEL_AUTH_REQUIRED" })).toBe(true) + }) + + it("returns false for 401 + different code", () => { + expect(isUnauthorizedPaidModelError({ statusCode: 401, code: "SOMETHING_ELSE" })).toBe(false) + }) + + it("returns false for null input", () => { + expect(isUnauthorizedPaidModelError(null)).toBe(false) + }) +}) + +describe("isUnauthorizedPromotionLimitError", () => { + it("returns true for 401 + PROMOTION_MODEL_LIMIT_REACHED", () => { + expect(isUnauthorizedPromotionLimitError({ statusCode: 401, code: "PROMOTION_MODEL_LIMIT_REACHED" })).toBe(true) + }) + + it("returns true for 429 + PROMOTION_MODEL_LIMIT_REACHED", () => { + expect(isUnauthorizedPromotionLimitError({ statusCode: 429, code: "PROMOTION_MODEL_LIMIT_REACHED" })).toBe(true) + }) + + it("returns false for null input", () => { + expect(isUnauthorizedPromotionLimitError(null)).toBe(false) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts b/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts index 2fde89059f9..fd98d145fb6 100644 --- a/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts +++ b/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts @@ -86,6 +86,10 @@ export function isUnauthorizedPaidModelError(parsed: ParsedError | null): boolea return parsed.statusCode === 401 && parsed.code === errorCodes.PAID_MODEL_AUTH_REQUIRED } +/** + * Accepts both 401 (current backend) and 429 (future backend) to support + * the transition. Keep 401 until the backend is updated to return 429. + */ export function isUnauthorizedPromotionLimitError(parsed: ParsedError | null): boolean { if (!parsed) return false return ( From 9f47e782ef0a676665ddabad7ccc63c190987638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 6 Mar 2026 11:50:30 +0100 Subject: [PATCH 17/44] style: add more margin between subtitle and first item --- .../webview-ui/src/components/settings/ProvidersTab.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx index aec7acfe4e6..6326ebe6f36 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx @@ -116,14 +116,13 @@ const ProvidersTab: Component = () => { {/* Model per Mode */} -

{language.t("settings.providers.modeModels")}

+

{language.t("settings.providers.modeModels")}

0 ? "1px solid var(--border-weak-base)" : "none", + "margin-bottom": primaryAgents().length > 0 ? "12px" : "0", }} > {language.t("settings.providers.modeModels.description")} From 61726dd8de42e6192cbecb480905aa090f46b4cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 6 Mar 2026 11:50:38 +0100 Subject: [PATCH 18/44] chore: update gen types --- packages/sdk/js/src/v2/gen/types.gen.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 47b9b58eff4..5504e3b0459 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1083,7 +1083,7 @@ export type PermissionConfig = | PermissionActionConfig export type AgentConfig = { - model?: string + model?: string | null /** * Default model variant for this agent (applies only when using the agent's configured model). */ @@ -1126,6 +1126,8 @@ export type AgentConfig = { [key: string]: | unknown | string + | null + | string | number | { [key: string]: boolean @@ -1375,11 +1377,11 @@ export type Config = { /** * Model to use in the format of provider/model, eg anthropic/claude-2 */ - model?: string + model?: string | null /** * Small model to use for tasks like title generation in the format of provider/model */ - small_model?: string + small_model?: string | null /** * Default agent to use when none is specified. Must be a primary agent. Falls back to 'code' if not set or if the specified agent is invalid. */ From 4e7619c2c927959a9e77bd8f18c05ceffe597431 Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Fri, 6 Mar 2026 12:02:25 +0100 Subject: [PATCH 19/44] chore: Translations --- packages/kilo-vscode/webview-ui/src/i18n/ar.ts | 7 +++++++ packages/kilo-vscode/webview-ui/src/i18n/br.ts | 7 +++++++ packages/kilo-vscode/webview-ui/src/i18n/bs.ts | 7 +++++++ packages/kilo-vscode/webview-ui/src/i18n/da.ts | 7 +++++++ packages/kilo-vscode/webview-ui/src/i18n/de.ts | 7 +++++++ packages/kilo-vscode/webview-ui/src/i18n/es.ts | 7 +++++++ packages/kilo-vscode/webview-ui/src/i18n/fr.ts | 7 +++++++ packages/kilo-vscode/webview-ui/src/i18n/ja.ts | 7 +++++++ packages/kilo-vscode/webview-ui/src/i18n/ko.ts | 7 +++++++ packages/kilo-vscode/webview-ui/src/i18n/no.ts | 7 +++++++ packages/kilo-vscode/webview-ui/src/i18n/pl.ts | 7 +++++++ packages/kilo-vscode/webview-ui/src/i18n/ru.ts | 7 +++++++ packages/kilo-vscode/webview-ui/src/i18n/th.ts | 7 +++++++ packages/kilo-vscode/webview-ui/src/i18n/zh.ts | 7 +++++++ packages/kilo-vscode/webview-ui/src/i18n/zht.ts | 7 +++++++ 15 files changed, 105 insertions(+) diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 290837a1b1b..ef80d52e253 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -405,6 +405,13 @@ export const dict = { "error.globalSync.connectFailed": "تعذر الاتصال بالخادم. هل هناك خادم يعمل في `{{url}}`؟", + "error.paidModel.title": "يجب عليك تسجيل الدخول لاستخدام هذا النموذج", + "error.paidModel.description": "سجّل الدخول أو أنشئ حسابًا للوصول إلى أكثر من 500 نموذج، واستخدم الرصيد بسعر التكلفة، أو استخدم مفتاحك الخاص.", + "error.paidModel.action": "تسجيل الدخول", + "error.promotionLimit.title": "يجب عليك التسجيل للمتابعة", + "error.promotionLimit.description": "سجّل مجانًا للمتابعة واستكشاف أكثر من 500 نموذج. يستغرق دقيقتين، بدون بطاقة ائتمان. أو عُد لاحقًا.", + "error.promotionLimit.action": "التسجيل", + "error.chain.unknown": "خطأ غير معروف", "error.chain.causedBy": "بسبب:", "error.chain.apiError": "خطأ API", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index fadb33f8772..ec8051447c9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -405,6 +405,13 @@ export const dict = { "error.globalSync.connectFailed": "Não foi possível conectar ao servidor. Há um servidor executando em `{{url}}`?", + "error.paidModel.title": "Você precisa fazer login para usar este modelo", + "error.paidModel.description": "Faça login ou crie uma conta para acessar mais de 500 modelos, usar créditos a preço de custo ou trazer sua própria chave.", + "error.paidModel.action": "Fazer login", + "error.promotionLimit.title": "Você precisa se cadastrar para continuar", + "error.promotionLimit.description": "Cadastre-se gratuitamente para continuar e explorar mais de 500 modelos. Leva 2 minutos, sem cartão de crédito. Ou volte mais tarde.", + "error.promotionLimit.action": "Cadastrar-se", + "error.chain.unknown": "Erro desconhecido", "error.chain.causedBy": "Causado por:", "error.chain.apiError": "Erro de API", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 5bd3f7f64f9..d8f216856dc 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -410,6 +410,13 @@ export const dict = { "error.globalSync.connectFailed": "Nije moguće povezati se na server. Da li server radi na `{{url}}`?", + "error.paidModel.title": "Morate se prijaviti da biste koristili ovaj model", + "error.paidModel.description": "Prijavite se ili kreirajte račun za pristup preko 500 modela, koristite kredite po cijeni koštanja ili donesite vlastiti ključ.", + "error.paidModel.action": "Prijava", + "error.promotionLimit.title": "Morate se registrovati da biste nastavili", + "error.promotionLimit.description": "Registrujte se besplatno da nastavite i istražite preko 500 modela. Traje 2 minute, bez kreditne kartice. Ili se vratite kasnije.", + "error.promotionLimit.action": "Registracija", + "error.chain.unknown": "Nepoznata greška", "error.chain.causedBy": "Uzrok:", "error.chain.apiError": "API greška", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index e5109827edd..fb40b948367 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -407,6 +407,13 @@ export const dict = { "error.globalSync.connectFailed": "Kunne ikke forbinde til server. Kører der en server på `{{url}}`?", + "error.paidModel.title": "Du skal logge ind for at bruge denne model", + "error.paidModel.description": "Log ind eller opret en konto for at få adgang til over 500 modeller, brug kreditter til kostpris, eller medbring din egen nøgle.", + "error.paidModel.action": "Log ind", + "error.promotionLimit.title": "Du skal tilmelde dig for at fortsætte", + "error.promotionLimit.description": "Tilmeld dig gratis for at fortsætte og udforske over 500 modeller. Tager 2 minutter, intet kreditkort nødvendigt. Eller kom tilbage senere.", + "error.promotionLimit.action": "Tilmeld dig", + "error.chain.unknown": "Ukendt fejl", "error.chain.causedBy": "Forårsaget af:", "error.chain.apiError": "API-fejl", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index f8aeedc00e2..f5fdd001474 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -412,6 +412,13 @@ export const dict = { "error.globalSync.connectFailed": "Verbindung zum Server fehlgeschlagen. Läuft ein Server unter `{{url}}`?", + "error.paidModel.title": "Du musst dich anmelden, um dieses Modell zu nutzen", + "error.paidModel.description": "Melde dich an oder erstelle ein Konto, um auf über 500 Modelle zuzugreifen, Credits zum Selbstkostenpreis zu nutzen oder deinen eigenen Schlüssel mitzubringen.", + "error.paidModel.action": "Anmelden", + "error.promotionLimit.title": "Du musst dich registrieren, um weiterzumachen", + "error.promotionLimit.description": "Registriere dich kostenlos, um weiterzumachen und über 500 Modelle zu entdecken. Dauert 2 Minuten, keine Kreditkarte nötig. Oder komm später wieder.", + "error.promotionLimit.action": "Registrieren", + "error.chain.unknown": "Unbekannter Fehler", "error.chain.causedBy": "Verursacht durch:", "error.chain.apiError": "API-Fehler", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index e2fcae70ba1..2c0441906e3 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -408,6 +408,13 @@ export const dict = { "error.globalSync.connectFailed": "No se pudo conectar al servidor. ¿Hay un servidor ejecutándose en `{{url}}`?", + "error.paidModel.title": "Necesitas iniciar sesión para usar este modelo", + "error.paidModel.description": "Inicia sesión o crea una cuenta para acceder a más de 500 modelos, usar créditos a precio de coste o traer tu propia clave.", + "error.paidModel.action": "Iniciar sesión", + "error.promotionLimit.title": "Necesitas registrarte para continuar", + "error.promotionLimit.description": "Regístrate gratis para continuar y explorar más de 500 modelos. Solo 2 minutos, sin tarjeta de crédito. O vuelve más tarde.", + "error.promotionLimit.action": "Registrarse", + "error.chain.unknown": "Error desconocido", "error.chain.causedBy": "Causado por:", "error.chain.apiError": "Error de API", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index c8c0b376a4f..be1442efa54 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -412,6 +412,13 @@ export const dict = { "error.globalSync.connectFailed": "Impossible de se connecter au serveur. Y a-t-il un serveur en cours d'exécution à `{{url}}` ?", + "error.paidModel.title": "Vous devez vous connecter pour utiliser ce modèle", + "error.paidModel.description": "Connectez-vous ou créez un compte pour accéder à plus de 500 modèles, utiliser des crédits au prix coûtant ou apporter votre propre clé.", + "error.paidModel.action": "Se connecter", + "error.promotionLimit.title": "Vous devez vous inscrire pour continuer", + "error.promotionLimit.description": "Inscrivez-vous gratuitement pour continuer et explorer plus de 500 modèles. 2 minutes, sans carte bancaire. Ou revenez plus tard.", + "error.promotionLimit.action": "S'inscrire", + "error.chain.unknown": "Erreur inconnue", "error.chain.causedBy": "Causé par :", "error.chain.apiError": "Erreur API", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index d73838ef306..cffda38fc98 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -405,6 +405,13 @@ export const dict = { "error.globalSync.connectFailed": "サーバーに接続できませんでした。`{{url}}`でサーバーが実行されていますか?", + "error.paidModel.title": "このモデルを使用するにはサインインが必要です", + "error.paidModel.description": "サインインまたはアカウントを作成して、500以上のモデルにアクセスし、原価でクレジットを使用するか、独自のキーを持ち込みましょう。", + "error.paidModel.action": "サインイン", + "error.promotionLimit.title": "続けるにはサインアップが必要です", + "error.promotionLimit.description": "無料でサインアップして、500以上のモデルを探索しましょう。2分で完了、クレジットカード不要。または後でお戻りください。", + "error.promotionLimit.action": "サインアップ", + "error.chain.unknown": "不明なエラー", "error.chain.causedBy": "原因:", "error.chain.apiError": "APIエラー", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 689c864b39f..1b0ce4bbe72 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -409,6 +409,13 @@ export const dict = { "error.globalSync.connectFailed": "서버에 연결할 수 없습니다. `{{url}}`에서 서버가 실행 중인가요?", + "error.paidModel.title": "이 모델을 사용하려면 로그인이 필요합니다", + "error.paidModel.description": "로그인하거나 계정을 만들어 500개 이상의 모델에 접근하고, 원가로 크레딧을 사용하거나, 자체 키를 가져오세요.", + "error.paidModel.action": "로그인", + "error.promotionLimit.title": "계속하려면 가입이 필요합니다", + "error.promotionLimit.description": "무료로 가입하여 500개 이상의 모델을 탐색하세요. 2분이면 완료, 신용카드 불필요. 또는 나중에 다시 오세요.", + "error.promotionLimit.action": "가입하기", + "error.chain.unknown": "알 수 없는 오류", "error.chain.causedBy": "원인:", "error.chain.apiError": "API 오류", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 8badc1290d9..2cd6c75c195 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -409,6 +409,13 @@ export const dict = { "error.globalSync.connectFailed": "Kunne ikke koble til server. Kjører det en server på `{{url}}`?", + "error.paidModel.title": "Du må logge inn for å bruke denne modellen", + "error.paidModel.description": "Logg inn eller opprett en konto for å få tilgang til over 500 modeller, bruk kreditter til kostpris, eller ta med din egen nøkkel.", + "error.paidModel.action": "Logg inn", + "error.promotionLimit.title": "Du må registrere deg for å fortsette", + "error.promotionLimit.description": "Registrer deg gratis for å fortsette og utforske over 500 modeller. Tar 2 minutter, ingen kredittkort nødvendig. Eller kom tilbake senere.", + "error.promotionLimit.action": "Registrer deg", + "error.chain.unknown": "Ukjent feil", "error.chain.causedBy": "Forårsaket av:", "error.chain.apiError": "API-feil", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 7e159f26048..51926e0c92c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -407,6 +407,13 @@ export const dict = { "error.globalSync.connectFailed": "Nie można połączyć się z serwerem. Czy serwer działa pod adresem `{{url}}`?", + "error.paidModel.title": "Musisz się zalogować, aby użyć tego modelu", + "error.paidModel.description": "Zaloguj się lub utwórz konto, aby uzyskać dostęp do ponad 500 modeli, korzystać z kredytów po kosztach lub użyć własnego klucza.", + "error.paidModel.action": "Zaloguj się", + "error.promotionLimit.title": "Musisz się zarejestrować, aby kontynuować", + "error.promotionLimit.description": "Zarejestruj się za darmo, aby kontynuować i odkryć ponad 500 modeli. Zajmie to 2 minuty, bez karty kredytowej. Lub wróć później.", + "error.promotionLimit.action": "Zarejestruj się", + "error.chain.unknown": "Nieznany błąd", "error.chain.causedBy": "Spowodowany przez:", "error.chain.apiError": "Błąd API", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 99ffd1b6d2b..b4d7e0c91ff 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -408,6 +408,13 @@ export const dict = { "error.globalSync.connectFailed": "Не удалось подключиться к серверу. Запущен ли сервер по адресу `{{url}}`?", + "error.paidModel.title": "Для использования этой модели необходимо войти", + "error.paidModel.description": "Войдите или создайте аккаунт для доступа к более чем 500 моделям, используйте кредиты по себестоимости или подключите свой ключ.", + "error.paidModel.action": "Войти", + "error.promotionLimit.title": "Для продолжения необходимо зарегистрироваться", + "error.promotionLimit.description": "Зарегистрируйтесь бесплатно, чтобы продолжить и исследовать более 500 моделей. Займёт 2 минуты, кредитная карта не нужна. Или вернитесь позже.", + "error.promotionLimit.action": "Зарегистрироваться", + "error.chain.unknown": "Неизвестная ошибка", "error.chain.causedBy": "Причина:", "error.chain.apiError": "Ошибка API", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 0fc0bf2776e..471d39133ff 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -406,6 +406,13 @@ export const dict = { "error.globalSync.connectFailed": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ มีเซิร์ฟเวอร์ทำงานอยู่ที่ `{{url}}` หรือไม่?", + "error.paidModel.title": "คุณต้องเข้าสู่ระบบเพื่อใช้โมเดลนี้", + "error.paidModel.description": "เข้าสู่ระบบหรือสร้างบัญชีเพื่อเข้าถึงโมเดลกว่า 500 รายการ ใช้เครดิตในราคาทุน หรือใช้คีย์ของคุณเอง", + "error.paidModel.action": "เข้าสู่ระบบ", + "error.promotionLimit.title": "คุณต้องสมัครสมาชิกเพื่อดำเนินการต่อ", + "error.promotionLimit.description": "สมัครฟรีเพื่อดำเนินการต่อและสำรวจโมเดลกว่า 500 รายการ ใช้เวลา 2 นาที ไม่ต้องใช้บัตรเครดิต หรือกลับมาทีหลัง", + "error.promotionLimit.action": "สมัครสมาชิก", + "error.chain.unknown": "ข้อผิดพลาดที่ไม่รู้จัก", "error.chain.causedBy": "สาเหตุ:", "error.chain.apiError": "ข้อผิดพลาด API", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 57353016d3d..6cac7e33b0b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -404,6 +404,13 @@ export const dict = { "error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?", + "error.paidModel.title": "您需要登录才能使用此模型", + "error.paidModel.description": "登录或创建账户以访问500多个模型,按成本使用积分,或使用您自己的密钥。", + "error.paidModel.action": "登录", + "error.promotionLimit.title": "您需要注册才能继续", + "error.promotionLimit.description": "免费注册以继续探索500多个模型。只需2分钟,无需信用卡。或稍后再来。", + "error.promotionLimit.action": "注册", + "error.chain.unknown": "未知错误", "error.chain.causedBy": "原因:", "error.chain.apiError": "API 错误", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 521b923115a..1c135f028c9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -405,6 +405,13 @@ export const dict = { "error.globalSync.connectFailed": "無法連線到伺服器。是否有伺服器正在 `{{url}}` 執行?", + "error.paidModel.title": "您需要登入才能使用此模型", + "error.paidModel.description": "登入或建立帳戶以存取500多個模型,按成本使用點數,或使用您自己的金鑰。", + "error.paidModel.action": "登入", + "error.promotionLimit.title": "您需要註冊才能繼續", + "error.promotionLimit.description": "免費註冊以繼續探索500多個模型。只需2分鐘,無需信用卡。或稍後再來。", + "error.promotionLimit.action": "註冊", + "error.chain.unknown": "未知錯誤", "error.chain.causedBy": "原因:", "error.chain.apiError": "API 錯誤", From bc25237c967867303e60a266e7cf0ab52e6f13bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 6 Mar 2026 12:08:26 +0100 Subject: [PATCH 20/44] feat: show custom agents in Model per Mode and simplify row layout Include agents with mode "all" alongside "primary" in the mode model section. Remove per-mode descriptions for a cleaner two-column layout and vertically center rows without descriptions. --- .../src/components/settings/ProvidersTab.tsx | 12 +----------- .../src/components/settings/SettingsRow.tsx | 7 ++++--- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx index 6326ebe6f36..e9c1277fcf1 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx @@ -71,7 +71,7 @@ const ProvidersTab: Component = () => { } const primaryAgents = createMemo(() => - session.agents().filter((a) => a.mode === "primary"), + session.agents().filter((a) => a.mode === "primary" || a.mode === "all"), ) function handleModeModelSelect(agentName: string) { @@ -118,20 +118,10 @@ const ProvidersTab: Component = () => { {/* Model per Mode */}

{language.t("settings.providers.modeModels")}

-
0 ? "12px" : "0", - }} - > - {language.t("settings.providers.modeModels.description")} -
{(agent, index) => ( = ( +const SettingsRow: Component<{ title: string; description?: string; last?: boolean; children: JSX.Element }> = ( props, ) => (
-
{props.title}
-
{props.description}
+
{props.title}
+ {props.description != null &&
{props.description}
}
{props.children}
From 05e79a45b7a01a28feba750213550349e5338da6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Mar 2026 11:10:19 +0000 Subject: [PATCH 21/44] chore: update kilo-vscode visual regression baselines --- .../settings/providers-configure-chromium-linux.png | 4 ++-- .../settings/settings-panel-chromium-linux.png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/providers-configure-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/providers-configure-chromium-linux.png index f1c71267506..66924a2dcc2 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/providers-configure-chromium-linux.png +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/providers-configure-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:678e5f50021e13b0fc9d7af02ff26dd80b79f62bedb9417ade040cd49c3d2cca -size 38798 +oid sha256:ca60d19590a31f4faa5c79c163b0ea24bae70599d0068c17fd475195b602bf4e +size 32506 diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/settings-panel-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/settings-panel-chromium-linux.png index 9297d5171e6..d2d5413490f 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/settings-panel-chromium-linux.png +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/settings-panel-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d317f27c6e3cb810d90759953f1bacedd33fb4e0f09bf23b8845d293cb4c3220 -size 48191 +oid sha256:ca2d904ad6ea886fc32714fafcae38748a14411e1ee6f436a6bfaf91d0088c59 +size 42199 From 068a1bc3e890fcae297c2d07db8bed33e3507729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 6 Mar 2026 12:15:45 +0100 Subject: [PATCH 22/44] feat: show all agents in Model per Mode section Remove filtering so every agent appears in the Model per Mode settings, including subagents and hidden agents. --- .../webview-ui/src/components/settings/ProvidersTab.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx index e9c1277fcf1..ff257890085 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx @@ -70,9 +70,7 @@ const ProvidersTab: Component = () => { } } - const primaryAgents = createMemo(() => - session.agents().filter((a) => a.mode === "primary" || a.mode === "all"), - ) + const allAgents = createMemo(() => session.agents()) function handleModeModelSelect(agentName: string) { return (providerID: string, modelID: string) => { @@ -118,11 +116,11 @@ const ProvidersTab: Component = () => { {/* Model per Mode */}

{language.t("settings.providers.modeModels")}

- + {(agent, index) => ( Date: Fri, 6 Mar 2026 12:27:13 +0100 Subject: [PATCH 23/44] feat: add reset model button when session override is active Show a small x button next to the thinking effort selector when the active model differs from the config default for the current mode. Clicking it clears the session override and reverts to settings. --- .../src/components/chat/PromptInput.tsx | 14 ++++++++ .../webview-ui/src/context/session.tsx | 34 +++++++++++++++++++ .../kilo-vscode/webview-ui/src/i18n/ar.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/br.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/bs.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/da.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/de.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/en.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/es.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/fr.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ja.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ko.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/no.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/pl.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ru.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/th.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/zh.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/zht.ts | 1 + 18 files changed, 64 insertions(+) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index bd796ab8daf..a45bbb7be9a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -575,6 +575,20 @@ export const PromptInput: Component = () => { + + + + +
diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 6749efd7385..b0fc5f0f7f1 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -100,6 +100,8 @@ interface SessionContextValue { // Model selection (per-session) selected: Accessor selectModel: (providerID: string, modelID: string) => void + hasModelOverride: Accessor + clearModelOverride: () => void // Cost and context usage for the current session totalCost: Accessor @@ -277,6 +279,36 @@ export const SessionProvider: ParentComponent = (props) => { } } + /** The config/default model for the current mode (what settings says). */ + const configModel = createMemo(() => { + const agentName = selectedAgentName() + return getModeModel(agentName) ?? provider.defaultSelection() + }) + + /** True when the active model differs from what the config dictates. */ + const hasModelOverride = createMemo(() => { + const sel = selected() + const cfg = configModel() + if (!sel || !cfg) return false + return sel.providerID !== cfg.providerID || sel.modelID !== cfg.modelID + }) + + /** Clear the session-level model override, falling back to config default. */ + function clearModelOverride() { + const id = currentSessionID() + if (id) { + setStore( + "modelSelections", + produce((selections) => { + delete selections[id] + }), + ) + } else { + setPendingWasUserSet(false) + setPendingModelSelection(null) + } + } + // Handle agentsLoaded immediately (not in onMount) so we never miss // the initial push that arrives before the DOM mounts. This mirrors the // pattern used by ProviderProvider for providersLoaded. @@ -1112,6 +1144,8 @@ export const SessionProvider: ParentComponent = (props) => { questionErrors, selected, selectModel, + hasModelOverride, + clearModelOverride, totalCost, contextUsage, agents, diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 290837a1b1b..6b8949b187e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -230,6 +230,7 @@ export const dict = { "prompt.action.send": "إرسال", "prompt.action.stop": "توقف", "prompt.action.enhance": "تحسين النص", + "prompt.action.resetModel": "إعادة تعيين النموذج إلى الافتراضي", "prompt.action.enhanceDescription": "زر «حسّن الموجه» يطوّر موجهك بإضافة سياق أو توضيح أو إعادة صياغة. جرّب اكتب موجه هنا ثم اضغط الزر مرة ثانية وشوف النتيجة.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index fadb33f8772..29f2ae98904 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -231,6 +231,7 @@ export const dict = { "prompt.action.send": "Enviar", "prompt.action.stop": "Parar", "prompt.action.enhance": "Melhorar prompt", + "prompt.action.resetModel": "Redefinir modelo para o padrão", "prompt.action.enhanceDescription": "O botão 'Aprimorar prompt' ajuda a melhorar seu pedido fornecendo contexto adicional, esclarecimentos ou reformulações. Tente digitar um pedido aqui e clique no botão novamente para ver como funciona.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 5bd3f7f64f9..444a8bcfd45 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -232,6 +232,7 @@ export const dict = { "prompt.action.send": "Pošalji", "prompt.action.stop": "Zaustavi", "prompt.action.enhance": "Poboljšaj prompt", + "prompt.action.resetModel": "Resetuj model na zadani", "prompt.action.enhanceDescription": "Dugme 'Poboljšaj prompt' pomaže poboljšati vaš zahtjev pružajući dodatni kontekst, pojašnjenje ili preformulaciju. Pokušajte upisati zahtjev ovdje i ponovo kliknite na dugme da vidite kako funkcioniše.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index e5109827edd..ca10a659d05 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -231,6 +231,7 @@ export const dict = { "prompt.action.send": "Send", "prompt.action.stop": "Stop", "prompt.action.enhance": "Forbedr prompt", + "prompt.action.resetModel": "Nulstil model til standard", "prompt.action.enhanceDescription": "Knappen 'Forbedr prompt' hjælper med at forbedre din forespørgsel ved at give ekstra kontekst, præcisering eller omformulering. Prøv at skrive en forespørgsel her og klik på knappen igen for at se hvordan det virker.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index f8aeedc00e2..3070740c49f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -235,6 +235,7 @@ export const dict = { "prompt.action.send": "Senden", "prompt.action.stop": "Stopp", "prompt.action.enhance": "Prompt verbessern", + "prompt.action.resetModel": "Modell auf Standard zurücksetzen", "prompt.action.enhanceDescription": "Die Schaltfläche 'Prompt verbessern' hilft, deine Anfrage durch zusätzlichen Kontext, Klarstellungen oder Umformulierungen zu verbessern. Versuche, hier eine Anfrage einzugeben und klicke erneut auf die Schaltfläche, um zu sehen, wie es funktioniert.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 34d08218156..ed76a18b9de 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -232,6 +232,7 @@ export const dict = { "prompt.action.send": "Send", "prompt.action.stop": "Stop", "prompt.action.enhance": "Enhance prompt", + "prompt.action.resetModel": "Reset model to default", "prompt.action.enhanceDescription": "The 'Enhance Prompt' button helps improve your prompt by providing additional context, clarification, or rephrasing. Try typing a prompt in here and clicking the button again to see how it works.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index e2fcae70ba1..23ab7e92055 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -231,6 +231,7 @@ export const dict = { "prompt.action.send": "Enviar", "prompt.action.stop": "Detener", "prompt.action.enhance": "Mejorar prompt", + "prompt.action.resetModel": "Restablecer modelo al predeterminado", "prompt.action.enhanceDescription": "El botón 'Mejorar el mensaje' ayuda a mejorar tu petición proporcionando contexto adicional, aclaraciones o reformulaciones. Intenta escribir una petición aquí y haz clic en el botón nuevamente para ver cómo funciona.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index c8c0b376a4f..4ddea526d90 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -232,6 +232,7 @@ export const dict = { "prompt.action.send": "Envoyer", "prompt.action.stop": "Arrêter", "prompt.action.enhance": "Améliorer le prompt", + "prompt.action.resetModel": "Réinitialiser le modèle par défaut", "prompt.action.enhanceDescription": "Le bouton 'Améliorer la requête' aide à améliorer votre demande en fournissant un contexte supplémentaire, des clarifications ou des reformulations. Essayez de taper une demande ici et cliquez à nouveau sur le bouton pour voir comment cela fonctionne.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index d73838ef306..76985b9a283 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -230,6 +230,7 @@ export const dict = { "prompt.action.send": "送信", "prompt.action.stop": "停止", "prompt.action.enhance": "プロンプトを改善", + "prompt.action.resetModel": "モデルをデフォルトにリセット", "prompt.action.enhanceDescription": "「プロンプトを強化」ボタンは、追加コンテキスト、説明、または言い換えを提供することで、リクエストを改善します。ここにリクエストを入力し、ボタンを再度クリックして動作を確認してください。", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 689c864b39f..0c69aad0e19 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -234,6 +234,7 @@ export const dict = { "prompt.action.send": "전송", "prompt.action.stop": "중지", "prompt.action.enhance": "프롬프트 개선", + "prompt.action.resetModel": "모델을 기본값으로 재설정", "prompt.action.enhanceDescription": "'프롬프트 향상' 버튼은 추가 컨텍스트, 명확화 또는 재구성을 제공하여 요청을 개선합니다. 여기에 요청을 입력한 다음 버튼을 다시 클릭하여 작동 방식을 확인해보세요.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 8badc1290d9..924d9502929 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -234,6 +234,7 @@ export const dict = { "prompt.action.send": "Send", "prompt.action.stop": "Stopp", "prompt.action.enhance": "Forbedre prompt", + "prompt.action.resetModel": "Tilbakestill modell til standard", "prompt.action.enhanceDescription": "Knappen 'Forbedre prompt' hjelper med å forbedre forespørselen din ved å gi ekstra kontekst, avklaring eller omformulering. Prøv å skrive en forespørsel her og klikk på knappen igjen for å se hvordan det fungerer.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 7e159f26048..48c9ce7f823 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -231,6 +231,7 @@ export const dict = { "prompt.action.send": "Wyślij", "prompt.action.stop": "Zatrzymaj", "prompt.action.enhance": "Ulepsz prompt", + "prompt.action.resetModel": "Zresetuj model do domyślnego", "prompt.action.enhanceDescription": "Przycisk 'Ulepsz podpowiedź' pomaga ulepszyć Twoją prośbę, dostarczając dodatkowy kontekst, wyjaśnienia lub przeformułowania. Spróbuj wpisać prośbę tutaj i kliknij przycisk ponownie, aby zobaczyć, jak to działa.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 99ffd1b6d2b..8182ba60b15 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -231,6 +231,7 @@ export const dict = { "prompt.action.send": "Отправить", "prompt.action.stop": "Остановить", "prompt.action.enhance": "Улучшить промпт", + "prompt.action.resetModel": "Сбросить модель на значение по умолчанию", "prompt.action.enhanceDescription": "Кнопка 'Улучшить запрос' помогает сделать ваш запрос лучше, предоставляя дополнительный контекст, уточнения или переформулировку. Попробуйте ввести запрос и снова нажать кнопку, чтобы увидеть, как это работает.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 0fc0bf2776e..5f7a17ef4ff 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -231,6 +231,7 @@ export const dict = { "prompt.action.send": "ส่ง", "prompt.action.stop": "หยุด", "prompt.action.enhance": "ปรับปรุงพรอมต์", + "prompt.action.resetModel": "รีเซ็ตโมเดลเป็นค่าเริ่มต้น", "prompt.action.enhanceDescription": "ปุ่ม 'ปรับปรุงพรอมต์' ช่วยปรับปรุงพรอมต์ของคุณโดยให้บริบทเพิ่มเติม ชี้แจง หรือเขียนใหม่ ลองพิมพ์พรอมต์ที่นี่และคลิกปุ่มอีกครั้งเพื่อดูว่ามันทำงานอย่างไร", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 57353016d3d..6c8c4813a34 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -231,6 +231,7 @@ export const dict = { "prompt.action.send": "发送", "prompt.action.stop": "停止", "prompt.action.enhance": "优化提示词", + "prompt.action.resetModel": "重置模型为默认值", "prompt.action.enhanceDescription": "'增强提示'按钮通过提供额外上下文、澄清或重新表述来帮助改进您的请求。尝试在此处输入请求,然后再次点击按钮查看其工作原理。", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 521b923115a..8927c21274f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -231,6 +231,7 @@ export const dict = { "prompt.action.send": "傳送", "prompt.action.stop": "停止", "prompt.action.enhance": "改善提示詞", + "prompt.action.resetModel": "重置模型為預設值", "prompt.action.enhanceDescription": "「強化提示詞」按鈕可透過提供額外內容、說明或改寫來協助改善提示詞。試著在這裡輸入提示詞,再點選一次按鈕以了解其運作方式。", From 51e843d511d6d5c47ade65c1fe423e2911e462f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 6 Mar 2026 12:44:13 +0100 Subject: [PATCH 24/44] refactor: make session model override per-mode instead of global Change modelSelections store to use composite keys (sessionID/agentName) so each mode independently tracks its own model override. Switching between modes preserves each mode's override separately. --- .../webview-ui/src/context/session.tsx | 118 +++++++++++------- 1 file changed, 73 insertions(+), 45 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index b0fc5f0f7f1..53c8257a201 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -48,7 +48,7 @@ interface SessionStore { messages: Record // sessionID -> messages parts: Record // messageID -> parts todos: Record // sessionID -> todos - modelSelections: Record // sessionID -> model + modelSelections: Record // "sessionID/agentName" -> model agentSelections: Record // sessionID -> agent name variantSelections: Record // "providerID/modelID" -> variant name } @@ -181,9 +181,9 @@ export const SessionProvider: ParentComponent = (props) => { // Tracks question IDs that failed so the UI can reset sending state const [questionErrors, setQuestionErrors] = createSignal>(new Set()) - // Pending model selection for before a session exists - const [pendingModelSelection, setPendingModelSelection] = createSignal(null) - const [pendingWasUserSet, setPendingWasUserSet] = createSignal(false) + // Pending model selection for before a session exists (per-mode) + const [pendingModelSelections, setPendingModelSelections] = createSignal>({}) + const [pendingWasUserSet, setPendingWasUserSet] = createSignal>({}) // Agents (modes) loaded from the CLI backend const [agents, setAgents] = createSignal([]) @@ -206,6 +206,9 @@ export const SessionProvider: ParentComponent = (props) => { variantSelections: {}, }) + /** Composite key for per-session per-mode model selections. */ + const modelKey = (sessionID: string, agentName: string) => `${sessionID}/${agentName}` + // Keep pending selection in sync with provider/mode default until the user // explicitly changes it (or a session exists). createEffect(() => { @@ -214,22 +217,23 @@ export const SessionProvider: ParentComponent = (props) => { return } - if (pendingWasUserSet()) { + const pendingAgent = pendingAgentSelection() ?? defaultAgent() + if (pendingWasUserSet()[pendingAgent]) { return } // Per-mode config takes priority over global default - const pendingAgent = pendingAgentSelection() ?? defaultAgent() const modeModel = getModeModel(pendingAgent) - setPendingModelSelection(modeModel ?? def) + setPendingModelSelections((prev) => ({ ...prev, [pendingAgent]: (modeModel ?? def)! })) }) // If we have no pending yet, initialize it from provider default. createEffect(() => { - if (!pendingModelSelection()) { - const pendingAgent = pendingAgentSelection() ?? defaultAgent() + const pendingAgent = pendingAgentSelection() ?? defaultAgent() + if (!pendingModelSelections()[pendingAgent]) { const modeModel = getModeModel(pendingAgent) - setPendingModelSelection(modeModel ?? provider.defaultSelection()) + const sel = modeModel ?? provider.defaultSelection() + if (sel) setPendingModelSelections((prev) => ({ ...prev, [pendingAgent]: sel })) } }) @@ -242,20 +246,20 @@ export const SessionProvider: ParentComponent = (props) => { return { providerID: raw.slice(0, slash), modelID: raw.slice(slash + 1) } } - // Per-session model selection - // Precedence: session override > per-mode config > global default > kilo/auto + // Per-session per-mode model selection + // Precedence: session+mode override > per-mode config > global default > kilo/auto const selected = createMemo(() => { const sessionID = currentSessionID() if (sessionID) { - const sessionModel = store.modelSelections[sessionID] - if (sessionModel) return sessionModel const agentName = store.agentSelections[sessionID] ?? defaultAgent() + const sessionModel = store.modelSelections[modelKey(sessionID, agentName)] + if (sessionModel) return sessionModel return getModeModel(agentName) ?? provider.defaultSelection() } - // Pre-session: check pending agent's per-mode default - const pending = pendingModelSelection() - if (pending) return pending + // Pre-session: check pending agent's per-mode override const pendingAgent = pendingAgentSelection() ?? defaultAgent() + const pending = pendingModelSelections()[pendingAgent] + if (pending) return pending return getModeModel(pendingAgent) ?? provider.defaultSelection() }) @@ -271,11 +275,12 @@ export const SessionProvider: ParentComponent = (props) => { function selectModel(providerID: string, modelID: string) { const selection: ModelSelection = { providerID, modelID } const id = currentSessionID() + const agentName = selectedAgentName() if (id) { - setStore("modelSelections", id, selection) + setStore("modelSelections", modelKey(id, agentName), selection) } else { - setPendingWasUserSet(true) - setPendingModelSelection(selection) + setPendingWasUserSet((prev) => ({ ...prev, [agentName]: true })) + setPendingModelSelections((prev) => ({ ...prev, [agentName]: selection })) } } @@ -293,19 +298,29 @@ export const SessionProvider: ParentComponent = (props) => { return sel.providerID !== cfg.providerID || sel.modelID !== cfg.modelID }) - /** Clear the session-level model override, falling back to config default. */ + /** Clear the per-mode model override, falling back to config default. */ function clearModelOverride() { + const agentName = selectedAgentName() const id = currentSessionID() if (id) { + const key = modelKey(id, agentName) setStore( "modelSelections", produce((selections) => { - delete selections[id] + delete selections[key] }), ) } else { - setPendingWasUserSet(false) - setPendingModelSelection(null) + setPendingWasUserSet((prev) => { + const next = { ...prev } + delete next[agentName] + return next + }) + setPendingModelSelections((prev) => { + const next = { ...prev } + delete next[agentName] + return next + }) } } @@ -488,15 +503,18 @@ export const SessionProvider: ParentComponent = (props) => { setStore("messages", session.id, []) } - // If there's a pending model selection, assign it to this new session. + // Transfer pending per-mode model selections to the new session. // Guard against duplicate sessionCreated events (HTTP response + SSE) // which would overwrite the user's selection with the effect-restored default. - const pending = pendingModelSelection() - if (pending && !store.modelSelections[session.id]) { - setStore("modelSelections", session.id, pending) - setPendingModelSelection(null) - setPendingWasUserSet(false) + const pendingModels = pendingModelSelections() + for (const [agent, sel] of Object.entries(pendingModels)) { + const key = modelKey(session.id, agent) + if (!store.modelSelections[key]) { + setStore("modelSelections", key, sel) + } } + setPendingModelSelections({}) + setPendingWasUserSet({}) // Transfer pending agent selection to the new session const pendingAgent = pendingAgentSelection() @@ -713,7 +731,9 @@ export const SessionProvider: ParentComponent = (props) => { setStore( "modelSelections", produce((selections) => { - delete selections[sessionID] + for (const key of Object.keys(selections)) { + if (key.startsWith(`${sessionID}/`)) delete selections[key] + } }), ) setStore( @@ -780,9 +800,12 @@ export const SessionProvider: ParentComponent = (props) => { batch(() => { setStore("sessions", session.id, session) - const pending = pendingModelSelection() - if (pending && !store.modelSelections[session.id]) { - setStore("modelSelections", session.id, pending) + const pendingModels = pendingModelSelections() + for (const [agent, sel] of Object.entries(pendingModels)) { + const key = modelKey(session.id, agent) + if (!store.modelSelections[key]) { + setStore("modelSelections", key, sel) + } } const pendingAgent = pendingAgentSelection() if (pendingAgent && !store.agentSelections[session.id]) { @@ -844,11 +867,12 @@ export const SessionProvider: ParentComponent = (props) => { setStore("agentSelections", id, name) } else { setPendingAgentSelection(name) - // When switching mode pre-session, update pending model to per-mode default - // (unless user explicitly set a model for this session) - if (!pendingWasUserSet()) { + // When switching mode pre-session, initialize pending model for the new mode + // if the user hasn't explicitly set one for it + if (!pendingWasUserSet()[name] && !pendingModelSelections()[name]) { const modeModel = getModeModel(name) - setPendingModelSelection(modeModel ?? provider.defaultSelection()) + const sel = modeModel ?? provider.defaultSelection() + if (sel) setPendingModelSelections((prev) => ({ ...prev, [name]: sel })) } } } @@ -986,9 +1010,9 @@ export const SessionProvider: ParentComponent = (props) => { return } - // Reset pending selection to default for the new session - setPendingModelSelection(provider.defaultSelection()) - setPendingWasUserSet(false) + // Reset pending selections to defaults for the new session + setPendingModelSelections({}) + setPendingWasUserSet({}) setPendingAgentSelection(defaultAgent()) vscode.postMessage({ type: "createSession" }) } @@ -1000,8 +1024,8 @@ export const SessionProvider: ParentComponent = (props) => { setPermissions([]) setQuestions([]) setQuestionErrors(new Set()) - setPendingModelSelection(provider.defaultSelection()) - setPendingWasUserSet(false) + setPendingModelSelections({}) + setPendingWasUserSet({}) setPendingAgentSelection(defaultAgent()) vscode.postMessage({ type: "clearSession" }) } @@ -1152,9 +1176,13 @@ export const SessionProvider: ParentComponent = (props) => { selectedAgent: selectedAgentName, selectAgent, getSessionAgent: (sessionID: string) => store.agentSelections[sessionID] ?? defaultAgent(), - getSessionModel: (sessionID: string) => store.modelSelections[sessionID] ?? provider.defaultSelection(), + getSessionModel: (sessionID: string) => { + const agentName = store.agentSelections[sessionID] ?? defaultAgent() + return store.modelSelections[modelKey(sessionID, agentName)] ?? provider.defaultSelection() + }, setSessionModel: (sessionID: string, providerID: string, modelID: string) => { - setStore("modelSelections", sessionID, { providerID, modelID }) + const agentName = store.agentSelections[sessionID] ?? defaultAgent() + setStore("modelSelections", modelKey(sessionID, agentName), { providerID, modelID }) }, setSessionAgent: (sessionID: string, name: string) => { setStore("agentSelections", sessionID, name) From b54c0a55b9d7869155c0011b02bc79594d97d74a Mon Sep 17 00:00:00 2001 From: Marius Date: Fri, 6 Mar 2026 12:59:25 +0100 Subject: [PATCH 25/44] feat(agent-manager): WorktreeManager backend improvements (#6693) --- .../src/agent-manager/WorktreeManager.ts | 234 +++++++++++++++--- .../tests/unit/worktree-manager.test.ts | 151 ++++++++++- 2 files changed, 349 insertions(+), 36 deletions(-) diff --git a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts index 79abe8620d1..231dd842c5c 100644 --- a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts +++ b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts @@ -37,10 +37,23 @@ export interface WorktreeInfo { sessionId?: string } +export type StartPointSource = "remote" | "local-tracking" | "local-branch" | "fallback" + +export interface StartPointResult { + ref: string + branch: string + source: StartPointSource + warning?: string +} + +export type WorktreeProgressStep = "syncing" | "verifying" | "fetching" | "creating" + export interface CreateWorktreeResult { branch: string path: string parentBranch: string + startPointSource: StartPointSource + startPointWarning?: string } export interface ExternalWorktreeItem { @@ -97,6 +110,7 @@ export class WorktreeManager { existingBranch?: string baseBranch?: string branchName?: string + onProgress?: (step: WorktreeProgressStep, message: string, detail?: string) => void }): Promise { return this.withGitLock(() => this.createWorktreeImpl(params)) } @@ -106,6 +120,7 @@ export class WorktreeManager { existingBranch?: string baseBranch?: string branchName?: string + onProgress?: (step: WorktreeProgressStep, message: string, detail?: string) => void }): Promise { const repo = await this.git.checkIsRepo() if (!repo) @@ -113,20 +128,39 @@ export class WorktreeManager { "This folder is not a git repository. Initialize a repository or open a git project to use worktrees.", ) + // Git LFS Pre-flight Check + if (await this.repoUsesLfs()) { + if (!(await this.checkLfsAvailable())) { + throw new Error( + "This repository uses Git LFS, but git-lfs was not found. Please install Git LFS to use this repository.", + ) + } + } + await this.ensureDir() await this.ensureGitExclude() - const parent = params.baseBranch || (await this.currentBranch()) + // Resolve start point (parent branch) + let parent: string + let startPoint: StartPointResult | undefined - // Validate baseBranch exists if explicitly provided - if (params.baseBranch) { - const exists = await this.branchExists(params.baseBranch) - if (!exists) throw new Error(`Base branch "${params.baseBranch}" does not exist`) - // Check if the base branch is a remote-only branch and fetch it - const branches = await this.git.branch() - if (!branches.all.includes(params.baseBranch) && branches.all.includes(`remotes/origin/${params.baseBranch}`)) { - await this.git.fetch("origin", params.baseBranch) + if (params.existingBranch) { + // Existing branch provided directly + parent = params.existingBranch + startPoint = { + ref: params.existingBranch, + branch: params.existingBranch, + source: "local-branch", } + } else { + // Resolve best start point for new branch + const requestedBase = params.baseBranch || (await this.defaultBranch()) + params.onProgress?.("verifying", `Resolving start point: ${requestedBase}`) + + startPoint = await this.resolveStartPoint(requestedBase, params.onProgress, { + allowFallback: !params.baseBranch, // Only fallback if user didn't explicitly request a specific base + }) + parent = startPoint.branch } const sanitized = params.branchName ? sanitizeBranchName(params.branchName) : undefined @@ -145,12 +179,15 @@ export class WorktreeManager { await this.removeWorktreeImpl(worktreePath) } + params.onProgress?.("creating", `Creating worktree for ${branch}...`) + + // Dereference to commit SHA to prevent upstream tracking for new branches + const startRef = params.existingBranch ? undefined : `${startPoint.ref}^{commit}` + try { const args = params.existingBranch ? ["worktree", "add", worktreePath, branch] - : params.baseBranch - ? ["worktree", "add", "-b", branch, worktreePath, params.baseBranch] - : ["worktree", "add", "-b", branch, worktreePath] + : ["worktree", "add", "-b", branch, worktreePath, startRef!] await this.git.raw(args) } catch (error) { const msg = error instanceof Error ? error.message : String(error) @@ -167,14 +204,20 @@ export class WorktreeManager { branch = `${branch}-${Date.now()}` const retryDir = branch.replace(/\//g, "-") worktreePath = path.join(this.dir, retryDir) - const retryArgs = params.baseBranch - ? ["worktree", "add", "-b", branch, worktreePath, params.baseBranch] - : ["worktree", "add", "-b", branch, worktreePath] + const retryArgs = params.existingBranch + ? ["worktree", "add", worktreePath, branch] + : ["worktree", "add", "-b", branch, worktreePath, startRef!] await this.git.raw(retryArgs) } this.log(`Created worktree: ${worktreePath} (branch: ${branch}, base: ${parent})`) - return { branch, path: worktreePath, parentBranch: parent } + return { + branch, + path: worktreePath, + parentBranch: parent, + startPointSource: startPoint.source, + startPointWarning: startPoint.warning, + } } /** @@ -385,6 +428,125 @@ export class WorktreeManager { } } + async resolveStartPoint( + branch: string, + onProgress?: (step: WorktreeProgressStep, message: string, detail?: string) => void, + opts?: { allowFallback?: boolean }, + ): Promise { + const { allowFallback = true } = opts || {} + + // 1. Remote fetch + if (await this.hasOriginRemote()) { + onProgress?.("fetching", `Fetching origin/${branch}...`) + try { + await this.git.fetch("origin", branch) + if (await this.refExistsLocally(`origin/${branch}`)) { + return { + ref: `origin/${branch}`, + branch: branch, + source: "remote", + } + } + } catch (e) { + this.log(`Failed to fetch origin/${branch}: ${e}`) + } + } + + // 2. Stale local tracking ref (offline fallback) + if (await this.refExistsLocally(`origin/${branch}`)) { + return { + ref: `origin/${branch}`, + branch: branch, + source: "local-tracking", + warning: "Used stale remote tracking branch (fetch failed)", + } + } + + // 3. Local branch + if (await this.refExistsLocally(branch)) { + return { + ref: branch, + branch: branch, + source: "local-branch", + } + } + + // 4. Derived fallback + if (allowFallback) { + const fallbacks = await this.derivedFallbackBranches(branch) + for (const fallback of fallbacks) { + if (fallback === branch) continue // already tried + try { + const res = await this.resolveStartPoint(fallback, onProgress, { allowFallback: false }) + return { + ...res, + source: "fallback", + warning: `Branch "${branch}" not found, falling back to "${fallback}"`, + } + } catch { + // continue + } + } + } + + throw new Error(`Could not resolve start point for branch "${branch}"`) + } + + async hasOriginRemote(): Promise { + try { + const remotes = await this.git.getRemotes() + return remotes.some((r) => r.name === "origin") + } catch { + return false + } + } + + async refExistsLocally(ref: string): Promise { + try { + await this.git.raw(["rev-parse", "--verify", `${ref}^{commit}`]) + return true + } catch { + return false + } + } + + async derivedFallbackBranches(requested: string): Promise { + const defaults = [] + try { + defaults.push(await this.defaultBranch()) + } catch {} + return defaults + } + + async repoUsesLfs(): Promise { + // Check .git/lfs/ directory + const gitDir = await this.resolveGitDir() + if (fs.existsSync(path.join(gitDir, "lfs"))) return true + + // Check .gitattributes + try { + const attributes = await fs.promises.readFile(path.join(this.root, ".gitattributes"), "utf-8") + if (attributes.includes("filter=lfs")) return true + } catch {} + + // Check .git/info/attributes + try { + const infoAttributes = await fs.promises.readFile(path.join(gitDir, "info", "attributes"), "utf-8") + if (infoAttributes.includes("filter=lfs")) return true + } catch {} + + return false + } + + async checkLfsAvailable(): Promise { + try { + await execWithShellEnv("git", ["lfs", "version"], { cwd: this.root, timeout: 5000 }) + return true + } catch { + return false + } + } + async currentBranch(): Promise { if (this.ops) { const branch = await this.ops.currentBranch(this.root) @@ -404,28 +566,32 @@ export class WorktreeManager { } async defaultBranch(): Promise { - if (this.ops) { - const remote = await this.ops.resolveRemote(this.root) - const resolved = await this.ops.resolveDefaultBranch(this.root) - if (resolved) { - const prefix = `${remote}/` - return resolved.startsWith(prefix) ? resolved.slice(prefix.length) : resolved - } - } else { - try { - const head = await this.git.raw(["symbolic-ref", "refs/remotes/origin/HEAD"]) - const match = head.trim().match(/refs\/remotes\/origin\/(.+)$/) - if (match) return match[1] - } catch {} + // 1. Try symbolic-ref + try { + const head = await this.git.raw(["symbolic-ref", "refs/remotes/origin/HEAD"]) + const match = head.trim().match(/refs\/remotes\/origin\/(.+)$/) + if (match) return match[1] + } catch (e) { + this.log(`defaultBranch: symbolic-ref failed: ${e}`) } + // 2. Try current branch (if not detached) try { - const branches = await this.git.branch() - if (branches.all.includes("main")) return "main" - if (branches.all.includes("master")) return "master" - } catch {} + const current = await this.currentBranch() + if (current && current !== "HEAD") return current + } catch (e) { + this.log(`defaultBranch: currentBranch failed: ${e}`) + } - return "main" + // 3. Try first local branch + try { + const branches = await this.git.branchLocal() + if (branches.all.length > 0) return branches.all[0] + } catch (e) { + this.log(`defaultBranch: branchLocal failed: ${e}`) + } + + throw new Error("Could not determine default branch") } // --------------------------------------------------------------------------- diff --git a/packages/kilo-vscode/tests/unit/worktree-manager.test.ts b/packages/kilo-vscode/tests/unit/worktree-manager.test.ts index 6cb5dea05e9..af92a94adfe 100644 --- a/packages/kilo-vscode/tests/unit/worktree-manager.test.ts +++ b/packages/kilo-vscode/tests/unit/worktree-manager.test.ts @@ -281,7 +281,7 @@ describe("WorktreeManager.createWorktree", () => { expect(result.path).toContain(path.join(".kilocode", "worktrees")) }) - it("records parentBranch as current branch", async () => { + it("records parentBranch as default branch", async () => { const root = await createTempRepo() const git = simpleGit(root) const branch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim() @@ -465,7 +465,7 @@ describe("WorktreeManager.discoverWorktrees", () => { const discovered = await mgr.discoverWorktrees() expect(discovered.length).toBe(1) - expect(discovered[0].sessionId).toBeUndefined() + expect(discovered[0]?.sessionId).toBeUndefined() }) it("recovers parentBranch from persisted metadata", async () => { @@ -666,3 +666,150 @@ describe("WorktreeManager.checkedOutBranches", () => { expect(checked.has(wt.branch)).toBe(false) }) }) + +// --------------------------------------------------------------------------- +// WorktreeManager -- Start Point Resolution & Helpers +// --------------------------------------------------------------------------- + +describe("WorktreeManager helpers", () => { + it("hasOriginRemote returns false when no remote exists", async () => { + const root = await createTempRepo() + const mgr = createManager(root) + expect(await mgr.hasOriginRemote()).toBe(false) + }) + + it("hasOriginRemote returns true when origin exists", async () => { + const root = await createTempRepo() + const git = simpleGit(root) + await git.addRemote("origin", "https://example.com/repo.git") + const mgr = createManager(root) + expect(await mgr.hasOriginRemote()).toBe(true) + }) + + it("refExistsLocally verifies refs", async () => { + const root = await createTempRepo() + const git = simpleGit(root) + const mgr = createManager(root) + + const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim() + expect(await mgr.refExistsLocally(head)).toBe(true) + expect(await mgr.refExistsLocally("nonexistent")).toBe(false) + expect(await mgr.refExistsLocally("origin/HEAD")).toBe(false) + }) + + it("repoUsesLfs detects .gitattributes", async () => { + const root = await createTempRepo() + const mgr = createManager(root) + expect(await mgr.repoUsesLfs()).toBe(false) + + await fs.writeFile(path.join(root, ".gitattributes"), "*.png filter=lfs diff=lfs merge=lfs -text") + expect(await mgr.repoUsesLfs()).toBe(true) + }) + + it("repoUsesLfs detects .git/lfs directory", async () => { + const root = await createTempRepo() + const mgr = createManager(root) + expect(await mgr.repoUsesLfs()).toBe(false) + + await fs.mkdir(path.join(root, ".git", "lfs"), { recursive: true }) + expect(await mgr.repoUsesLfs()).toBe(true) + }) +}) + +describe("WorktreeManager.resolveStartPoint", () => { + it("falls back to local branch when no remote exists", async () => { + const root = await createTempRepo() + const git = simpleGit(root) + const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim() + const mgr = createManager(root) + + const res = await mgr.resolveStartPoint(head) + expect(res.source).toBe("local-branch") + expect(res.ref).toBe(head) + }) + + it("falls back to default branch when requested does not exist", async () => { + const root = await createTempRepo() + const git = simpleGit(root) + const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim() + const mgr = createManager(root) + + const res = await mgr.resolveStartPoint("nonexistent-feature") + expect(res.source).toBe("fallback") + expect(res.branch).toBe(head) // fallback to default (HEAD) + expect(res.warning).toContain("falling back to") + }) + + it("does not fallback when allowFallback is false", async () => { + const root = await createTempRepo() + const mgr = createManager(root) + + await expect(mgr.resolveStartPoint("nonexistent", undefined, { allowFallback: false })).rejects.toThrow( + "Could not resolve start point", + ) + }) +}) + +describe("WorktreeManager.createWorktree advanced", () => { + it("returns startPointSource in result", async () => { + const root = await createTempRepo() + const mgr = createManager(root) + const res = await mgr.createWorktree({ prompt: "source-test" }) + + expect(res.startPointSource).toBe("local-branch") // no remote in temp repo + }) + + it("does not set upstream tracking on new branch", async () => { + const root = await createTempRepo() + const mgr = createManager(root) + const res = await mgr.createWorktree({ prompt: "no-upstream" }) + + const git = simpleGit(res.path) + // Checking upstream should fail + let error + try { + await git.revparse(["--abbrev-ref", `${res.branch}@{upstream}`]) + } catch (e) { + error = e + } + expect(error).toBeDefined() + }) + + it("fires onProgress callbacks", async () => { + const root = await createTempRepo() + const mgr = createManager(root) + const steps: string[] = [] + + await mgr.createWorktree({ + prompt: "progress-test", + onProgress: (step) => steps.push(step), + }) + + expect(steps).toContain("verifying") + expect(steps).toContain("creating") + }) + + it("creates from an explicitly selected base branch", async () => { + const root = await createTempRepo() + const git = simpleGit(root) + const mgr = createManager(root) + + // Create a new branch 'develop' + await git.checkoutLocalBranch("develop") + await fs.writeFile(path.join(root, "dev.txt"), "dev") + await git.add(".") + await git.commit("dev commit") + + // Create worktree from 'develop' + const res = await mgr.createWorktree({ + prompt: "feature", + baseBranch: "develop", + }) + + expect(res.parentBranch).toBe("develop") + const wtGit = simpleGit(res.path) + const headParams = await wtGit.log(["-1"]) + const devParams = await git.log(["-1"]) + expect(headParams.latest?.hash).toBe(devParams.latest?.hash) + }) +}) From 8294af67d8ea33e799b6f80989858e28579272db Mon Sep 17 00:00:00 2001 From: Marius Date: Fri, 6 Mar 2026 12:59:52 +0100 Subject: [PATCH 26/44] fix(agent-manager): prevent compare model selector from hiding create worktree button (#6695) --- packages/kilo-vscode/src/KiloProvider.ts | 23 +- .../agent-manager/AgentManagerApp.tsx | 578 ++++++++++-------- .../agent-manager/agent-manager.css | 42 +- .../webview-ui/agent-manager/i18n/ar.ts | 1 + .../webview-ui/agent-manager/i18n/br.ts | 1 + .../webview-ui/agent-manager/i18n/bs.ts | 1 + .../webview-ui/agent-manager/i18n/da.ts | 1 + .../webview-ui/agent-manager/i18n/de.ts | 1 + .../webview-ui/agent-manager/i18n/en.ts | 1 + .../webview-ui/agent-manager/i18n/es.ts | 1 + .../webview-ui/agent-manager/i18n/fr.ts | 1 + .../webview-ui/agent-manager/i18n/ja.ts | 1 + .../webview-ui/agent-manager/i18n/ko.ts | 1 + .../webview-ui/agent-manager/i18n/no.ts | 1 + .../webview-ui/agent-manager/i18n/pl.ts | 1 + .../webview-ui/agent-manager/i18n/ru.ts | 1 + .../webview-ui/agent-manager/i18n/th.ts | 1 + .../webview-ui/agent-manager/i18n/zh.ts | 1 + .../webview-ui/agent-manager/i18n/zht.ts | 1 + .../components/shared/model-selector-utils.ts | 3 +- 20 files changed, 367 insertions(+), 295 deletions(-) diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 2603f571656..f29c2298b4b 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -1613,9 +1613,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper console.log("[Kilo New] KiloProvider: 🔐 Login successful") - await this.client.global.dispose().catch((e: unknown) => - console.warn("[Kilo New] KiloProvider: global.dispose() after login failed:", e), - ) + await this.client.global + .dispose() + .catch((e: unknown) => console.warn("[Kilo New] KiloProvider: global.dispose() after login failed:", e)) // Step 4: Fetch profile and push to webview const { data: profileData } = await this.client.kilo.profile(undefined, { throwOnError: true }) @@ -1662,9 +1662,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper return } - await this.client.global.dispose().catch((e: unknown) => - console.warn("[Kilo New] KiloProvider: global.dispose() after org switch failed:", e), - ) + await this.client.global + .dispose() + .catch((e: unknown) => console.warn("[Kilo New] KiloProvider: global.dispose() after org switch failed:", e)) // Org switch succeeded — refresh profile and providers independently (best-effort) try { @@ -1719,10 +1719,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper data: null, }) - await this.client.global.dispose().catch((e: unknown) => - console.warn("[Kilo New] KiloProvider: global.dispose() after logout failed:", e), - ) - + await this.client.global + .dispose() + .catch((e: unknown) => console.warn("[Kilo New] KiloProvider: global.dispose() after logout failed:", e)) } catch (error) { console.error("[Kilo New] KiloProvider: ❌ Logout failed:", error) this.postMessage({ @@ -1730,8 +1729,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper message: getErrorMessage(error) || "Failed to logout", }) } - - } /** @@ -1858,7 +1855,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper // Refresh provider and agent lists when the server signals a state disposal if (event.type === "server.instance.disposed" || event.type === "global.disposed") { - void this.reloadAfterAuthChange(); + void this.reloadAfterAuthChange() return } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 53299f60f86..b59b5674137 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -2679,6 +2679,7 @@ const NewWorktreeDialog: Component<{ onClose: () => void }> = (props) => { const [branchName, setBranchName] = createSignal("") const [baseBranch, setBaseBranch] = createSignal(null) const [baseBranchOpen, setBaseBranchOpen] = createSignal(false) + const [compareOpen, setCompareOpen] = createSignal(false) const [highlightedIndex, setHighlightedIndex] = createSignal(0) const imageAttach = useImageAttachments() @@ -2826,296 +2827,331 @@ const NewWorktreeDialog: Component<{ onClose: () => void }> = (props) => { {/* New tab */}
- setName(e.currentTarget.value)} - /> - {/* Prompt input — reuses the sidebar chat-input base classes for consistent styling */} -
- 0}> -
- - {(img) => ( -
- {img.filename} - -
- )} -
-
-
-
-
-