diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/prompt-input/with-model-override-200-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/prompt-input/with-model-override-200-chromium-linux.png new file mode 100644 index 00000000000..e84e4e7cf68 --- /dev/null +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/prompt-input/with-model-override-200-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:107e9937d6640579e6b9823e609858be9060e7a6d2855bea1564b1f8585e3c60 +size 3880 diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/prompt-input/with-model-override-420-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/prompt-input/with-model-override-420-chromium-linux.png new file mode 100644 index 00000000000..580f5fdf016 --- /dev/null +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/prompt-input/with-model-override-420-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5fc5cb33f9536414b23c63e0bb19259862f6a227603602802e8a41c15e898a47 +size 4110 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..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:5debe43731dd71da23ad462caad112b5af6e588d1b2ebf5b151ed66d87e5d477 -size 29253 +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 ff5fde6e1bc..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:9b782a2ce193e54bd4fdd2ebc1490ac9674325a806ccabd56a7d3ff20c87e423 -size 39285 +oid sha256:ca2d904ad6ea886fc32714fafcae38748a14411e1ee6f436a6bfaf91d0088c59 +size 42199 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/components/settings/ProvidersTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx index a953352c5d2..ff257890085 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,25 @@ 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 allAgents = createMemo(() => session.agents()) + + 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 +113,27 @@ const ProvidersTab: Component = () => { + {/* Model per Mode */} +

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

+ + + {(agent, index) => ( + + + + )} + + + {/* Disabled providers */}

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

diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/SettingsRow.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/SettingsRow.tsx index f487a2d191b..300c2c8957b 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/SettingsRow.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/SettingsRow.tsx @@ -1,6 +1,6 @@ import { Component, JSX } from "solid-js" -const SettingsRow: Component<{ title: string; description: string; last?: boolean; children: JSX.Element }> = ( +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}
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..4447ecf78cb 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -1,7 +1,7 @@ /** * Session context * Manages session state, messages, and handles SSE events from the extension. - * Also owns per-session model selection (provider context is catalog-only). + * Also owns global (extension-lifetime) model selection (provider context is catalog-only). */ import { @@ -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 { @@ -47,7 +48,7 @@ interface SessionStore { messages: Record // sessionID -> messages parts: Record // messageID -> parts todos: Record // sessionID -> todos - modelSelections: Record // sessionID -> model + modelSelections: Record // agentName -> model (global, extension-lifetime) agentSelections: Record // sessionID -> agent name variantSelections: Record // "providerID/modelID" -> variant name } @@ -96,9 +97,11 @@ interface SessionContextValue { questions: Accessor questionErrors: Accessor> - // Model selection (per-session) + // Model selection (global, extension-lifetime) selected: Accessor selectModel: (providerID: string, modelID: string) => void + hasModelOverride: Accessor + clearModelOverride: () => void // Cost and context usage for the current session totalCost: Accessor @@ -144,6 +147,7 @@ export const SessionProvider: ParentComponent = (props) => { const vscode = useVSCode() const server = useServer() const provider = useProvider() + const { config } = useConfig() const language = useLanguage() // Current session ID @@ -177,15 +181,15 @@ 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) + // Tracks whether the user has explicitly set a model override per agent (to + // prevent the default-sync effect from overwriting it). + const [userSetAgents, setUserSetAgents] = createSignal>({}) // Agents (modes) loaded from the CLI backend const [agents, setAgents] = createSignal([]) const [defaultAgent, setDefaultAgent] = createSignal("code") - // Pending agent selection for before a session exists (mirrors pendingModelSelection) + // Pending agent selection for before a session exists const [pendingAgentSelection, setPendingAgentSelection] = createSignal(null) // Cloud session preview state @@ -202,35 +206,35 @@ export const SessionProvider: ParentComponent = (props) => { variantSelections: {}, }) - // Keep pending selection in sync with provider default until the user - // explicitly changes it (or a session exists). + // Keep model selection in sync with provider/mode default until the user + // explicitly overrides it. createEffect(() => { const def = provider.defaultSelection() - if (currentSessionID()) { - return - } + const agentName = selectedAgentName() + if (userSetAgents()[agentName]) return - if (pendingWasUserSet()) { - return - } - - setPendingModelSelection(def) + // Per-mode config takes priority over global default + const modeModel = getModeModel(agentName) + const sel = modeModel ?? def + if (sel) setStore("modelSelections", agentName, sel) }) - // If we have no pending yet, initialize it from provider default. - createEffect(() => { - if (!pendingModelSelection()) { - setPendingModelSelection(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 + // Global model selection per agent/mode + // Precedence: user override > per-mode config > global default > kilo/auto const selected = createMemo(() => { - const sessionID = currentSessionID() - if (sessionID) { - return store.modelSelections[sessionID] ?? provider.defaultSelection() - } - return pendingModelSelection() + const agentName = selectedAgentName() + const override = store.modelSelections[agentName] + if (override) return override + return getModeModel(agentName) ?? provider.defaultSelection() }) // Per-session agent selection @@ -243,14 +247,39 @@ export const SessionProvider: ParentComponent = (props) => { }) function selectModel(providerID: string, modelID: string) { - const selection: ModelSelection = { providerID, modelID } - const id = currentSessionID() - if (id) { - setStore("modelSelections", id, selection) - } else { - setPendingWasUserSet(true) - setPendingModelSelection(selection) - } + const agentName = selectedAgentName() + setUserSetAgents((prev) => ({ ...prev, [agentName]: true })) + setStore("modelSelections", agentName, { providerID, modelID }) + } + + /** 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 per-mode model override, falling back to config default. */ + function clearModelOverride() { + const agentName = selectedAgentName() + setUserSetAgents((prev) => { + const next = { ...prev } + delete next[agentName] + return next + }) + setStore( + "modelSelections", + produce((selections) => { + delete selections[agentName] + }), + ) } // Handle agentsLoaded immediately (not in onMount) so we never miss @@ -432,16 +461,6 @@ export const SessionProvider: ParentComponent = (props) => { setStore("messages", session.id, []) } - // If there's a pending model selection, assign it to this 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) - } - // Transfer pending agent selection to the new session const pendingAgent = pendingAgentSelection() if (pendingAgent && !store.agentSelections[session.id]) { @@ -654,12 +673,6 @@ export const SessionProvider: ParentComponent = (props) => { delete todos[sessionID] }), ) - setStore( - "modelSelections", - produce((selections) => { - delete selections[sessionID] - }), - ) setStore( "agentSelections", produce((selections) => { @@ -724,10 +737,6 @@ 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 pendingAgent = pendingAgentSelection() if (pendingAgent && !store.agentSelections[session.id]) { setStore("agentSelections", session.id, pendingAgent) @@ -788,6 +797,13 @@ export const SessionProvider: ParentComponent = (props) => { setStore("agentSelections", id, name) } else { setPendingAgentSelection(name) + // When switching mode, initialize model for the new mode if the user + // hasn't explicitly set one for it + if (!userSetAgents()[name] && !store.modelSelections[name]) { + const modeModel = getModeModel(name) + const sel = modeModel ?? provider.defaultSelection() + if (sel) setStore("modelSelections", name, sel) + } } } @@ -924,9 +940,7 @@ export const SessionProvider: ParentComponent = (props) => { return } - // Reset pending selection to default for the new session - setPendingModelSelection(provider.defaultSelection()) - setPendingWasUserSet(false) + // Reset agent selection to default for the new session (model overrides persist) setPendingAgentSelection(defaultAgent()) vscode.postMessage({ type: "createSession" }) } @@ -938,8 +952,6 @@ export const SessionProvider: ParentComponent = (props) => { setPermissions([]) setQuestions([]) setQuestionErrors(new Set()) - setPendingModelSelection(provider.defaultSelection()) - setPendingWasUserSet(false) setPendingAgentSelection(defaultAgent()) vscode.postMessage({ type: "clearSession" }) } @@ -1082,15 +1094,22 @@ export const SessionProvider: ParentComponent = (props) => { questionErrors, selected, selectModel, + hasModelOverride, + clearModelOverride, totalCost, contextUsage, agents, selectedAgent: selectedAgentName, selectAgent, getSessionAgent: (sessionID: string) => store.agentSelections[sessionID] ?? defaultAgent(), - getSessionModel: (sessionID: string) => store.modelSelections[sessionID] ?? provider.defaultSelection(), - setSessionModel: (sessionID: string, providerID: string, modelID: string) => { - setStore("modelSelections", sessionID, { providerID, modelID }) + getSessionModel: (sessionID: string) => { + const agentName = store.agentSelections[sessionID] ?? defaultAgent() + return store.modelSelections[agentName] ?? provider.defaultSelection() + }, + setSessionModel: (_sessionID: string, providerID: string, modelID: string) => { + const agentName = selectedAgentName() + setUserSetAgents((prev) => ({ ...prev, [agentName]: true })) + setStore("modelSelections", agentName, { providerID, modelID }) }, setSessionAgent: (sessionID: string, name: string) => { setStore("agentSelections", sessionID, name) 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 cf8a9123e86..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.", @@ -988,6 +989,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/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": "「強化提示詞」按鈕可透過提供額外內容、說明或改寫來協助改善提示詞。試著在這裡輸入提示詞,再點選一次按鈕以了解其運作方式。", diff --git a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx index cd19c6f4ce4..7bfdeae4a65 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx @@ -136,6 +136,8 @@ export function mockSessionValue(overrides?: { questionErrors: () => new Set(), selected: () => ({ providerID: "kilo", modelID: "anthropic/claude-sonnet-4-6" }), selectModel: noop, + hasModelOverride: () => false, + clearModelOverride: noop, totalCost: () => 0, contextUsage: () => undefined, agents: () => [{ name: "code", description: "Code mode", mode: "primary" as const }], diff --git a/packages/kilo-vscode/webview-ui/src/stories/history.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/history.stories.tsx index 0b968e5f91a..e07a1e6ddc8 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/history.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/history.stories.tsx @@ -66,6 +66,8 @@ const WithSessions: ParentComponent<{ sessions?: typeof mockSessions }> = (props questionErrors: () => new Set(), selected: () => ({ providerID: "kilo", modelID: "anthropic/claude-sonnet-4-6" }), selectModel: noop, + hasModelOverride: () => false, + clearModelOverride: noop, totalCost: () => 0, contextUsage: () => undefined, agents: () => [{ name: "code", description: "Code mode", mode: "primary" as const }], diff --git a/packages/kilo-vscode/webview-ui/src/stories/prompt-input.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/prompt-input.stories.tsx index 40f76ce3ded..764f1718c02 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/prompt-input.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/prompt-input.stories.tsx @@ -23,7 +23,9 @@ const agents = [ { name: "architect", description: "Plan and design before implementation", mode: "primary" as const }, ] -const PromptProviders: ParentComponent<{ variants?: boolean }> = (props) => { +const noop = () => {} + +const PromptProviders: ParentComponent<{ variants?: boolean; modelOverride?: boolean }> = (props) => { const base = mockSessionValue({ status: "idle" }) const session = { ...base, @@ -31,6 +33,8 @@ const PromptProviders: ParentComponent<{ variants?: boolean }> = (props) => { selectedAgent: () => "code", variantList: () => (props.variants ? ["low", "medium", "high"] : []), currentVariant: () => (props.variants ? ("medium" as string | undefined) : undefined), + hasModelOverride: () => props.modelOverride ?? false, + clearModelOverride: noop, } return ( @@ -97,3 +101,25 @@ export const WithThinking200: Story = { ), } + +// --------------------------------------------------------------------------- +// Stories — model override active (reset button visible) +// --------------------------------------------------------------------------- + +export const WithModelOverride420: Story = { + name: "With model override — 420px", + render: () => ( + + + + ), +} + +export const WithModelOverride200: Story = { + name: "With model override — 200px", + render: () => ( + + + + ), +} diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index 259d3d1f369..4ac67521177 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..460ecf259d1 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(), // kilocode_change - nullable for delete sentinel variant: z .string() .optional() @@ -1129,10 +1129,12 @@ 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( + // kilocode_change start - nullable for delete sentinel + 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 end // kilocode_change start - renamed from "build" to "code" default_agent: z .string() @@ -1407,7 +1409,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)) // kilocode_change - strip null delete sentinels await Instance.dispose() } @@ -1427,9 +1429,26 @@ export namespace Config { return !!value && typeof value === "object" && !Array.isArray(value) } + // kilocode_change start - strip null delete sentinels after merge + /** 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 + } + // kilocode_change end + function patchJsonc(input: string, patch: unknown, path: string[] = []): string { if (!isRecord(patch)) { - const edits = modify(input, path, patch, { + // kilocode_change - 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 +1507,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 // kilocode_change - strip null delete sentinels await Filesystem.writeJson(filepath, merged) return merged } 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. */