Merge branch 'main' into fix/plan_exit_regression

This commit is contained in:
Marian Alexandru Alecu
2026-03-06 15:15:30 +02:00
committed by GitHub
31 changed files with 263 additions and 88 deletions
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:107e9937d6640579e6b9823e609858be9060e7a6d2855bea1564b1f8585e3c60
size 3880
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5fc5cb33f9536414b23c63e0bb19259862f6a227603602802e8a41c15e898a47
size 4110
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5debe43731dd71da23ad462caad112b5af6e588d1b2ebf5b151ed66d87e5d477
size 29253
oid sha256:ca60d19590a31f4faa5c79c163b0ea24bae70599d0068c17fd475195b602bf4e
size 32506
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9b782a2ce193e54bd4fdd2ebc1490ac9674325a806ccabd56a7d3ff20c87e423
size 39285
oid sha256:ca2d904ad6ea886fc32714fafcae38748a14411e1ee6f436a6bfaf91d0088c59
size 42199
@@ -575,6 +575,20 @@ export const PromptInput: Component = () => {
<ModeSwitcher />
<ModelSelector />
<ThinkingSelector />
<Show when={session.hasModelOverride()}>
<Tooltip value={language.t("prompt.action.resetModel")} placement="top">
<Button
variant="ghost"
size="small"
onClick={() => session.clearModelOverride()}
aria-label={language.t("prompt.action.resetModel")}
>
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
<path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z" />
</svg>
</Button>
</Tooltip>
</Show>
</div>
<div class="prompt-input-hint-actions">
<Tooltip value={language.t("prompt.action.enhance")} placement="top">
@@ -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<ProviderOption[]>(() =>
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 (
<div>
{/* Model selection */}
@@ -99,6 +113,27 @@ const ProvidersTab: Component = () => {
</SettingsRow>
</Card>
{/* Model per Mode */}
<h4 style={{ "margin-top": "24px", "margin-bottom": "8px" }}>{language.t("settings.providers.modeModels")}</h4>
<Card>
<For each={allAgents()}>
{(agent, index) => (
<SettingsRow
title={agent.name.charAt(0).toUpperCase() + agent.name.slice(1)}
last={index() === allAgents().length - 1}
>
<ModelSelectorBase
value={parseModelConfig(config().agent?.[agent.name]?.model ?? undefined)}
onSelect={handleModeModelSelect(agent.name)}
placement="bottom-start"
allowClear
clearLabel={language.t("settings.providers.notSet")}
/>
</SettingsRow>
)}
</For>
</Card>
{/* Disabled providers */}
<h4 style={{ "margin-top": "16px", "margin-bottom": "8px" }}>{language.t("settings.providers.disabled")}</h4>
<Card>
@@ -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,
) => (
<div
@@ -9,11 +9,12 @@ const SettingsRow: Component<{ title: string; description: string; last?: boolea
"margin-bottom": props.last ? "0" : "8px",
"padding-bottom": props.last ? "0" : "8px",
"border-bottom": props.last ? "none" : "1px solid var(--border-weak-base)",
...(props.description == null ? { "align-items": "center" } : {}),
}}
>
<div data-slot="settings-row-label">
<div data-slot="settings-row-label-title">{props.title}</div>
<div data-slot="settings-row-label-subtitle">{props.description}</div>
<div data-slot="settings-row-label-title" style={props.description == null ? { "margin-bottom": "0" } : {}}>{props.title}</div>
{props.description != null && <div data-slot="settings-row-label-subtitle">{props.description}</div>}
</div>
<div data-slot="settings-row-input">{props.children}</div>
</div>
@@ -14,6 +14,37 @@ interface ConfigContextValue {
updateConfig: (partial: Partial<Config>) => void
}
function isRecord(value: unknown): value is Record<string, unknown> {
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>): Config {
const result: Record<string, unknown> = { ...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<Config>)
} 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<string, unknown> = {}
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<ConfigContextValue>()
export const ConfigProvider: ParentComponent = (props) => {
@@ -59,8 +90,8 @@ export const ConfigProvider: ParentComponent = (props) => {
onCleanup(() => clearInterval(retryTimer))
function updateConfig(partial: Partial<Config>) {
// 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 })
}
@@ -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<string, Message[]> // sessionID -> messages
parts: Record<string, Part[]> // messageID -> parts
todos: Record<string, TodoItem[]> // sessionID -> todos
modelSelections: Record<string, ModelSelection> // sessionID -> model
modelSelections: Record<string, ModelSelection> // agentName -> model (global, extension-lifetime)
agentSelections: Record<string, string> // sessionID -> agent name
variantSelections: Record<string, string> // "providerID/modelID" -> variant name
}
@@ -96,9 +97,11 @@ interface SessionContextValue {
questions: Accessor<QuestionRequest[]>
questionErrors: Accessor<Set<string>>
// Model selection (per-session)
// Model selection (global, extension-lifetime)
selected: Accessor<ModelSelection | null>
selectModel: (providerID: string, modelID: string) => void
hasModelOverride: Accessor<boolean>
clearModelOverride: () => void
// Cost and context usage for the current session
totalCost: Accessor<number>
@@ -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<Set<string>>(new Set())
// Pending model selection for before a session exists
const [pendingModelSelection, setPendingModelSelection] = createSignal<ModelSelection | null>(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<Record<string, boolean>>({})
// Agents (modes) loaded from the CLI backend
const [agents, setAgents] = createSignal<AgentInfo[]>([])
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<string | null>(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<ModelSelection | null>(() => {
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<ModelSelection | null>(() => {
const agentName = selectedAgentName()
return getModeModel(agentName) ?? provider.defaultSelection()
})
/** True when the active model differs from what the config dictates. */
const hasModelOverride = createMemo<boolean>(() => {
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<string>())
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)
@@ -230,6 +230,7 @@ export const dict = {
"prompt.action.send": "إرسال",
"prompt.action.stop": "توقف",
"prompt.action.enhance": "تحسين النص",
"prompt.action.resetModel": "إعادة تعيين النموذج إلى الافتراضي",
"prompt.action.enhanceDescription":
"زر «حسّن الموجه» يطوّر موجهك بإضافة سياق أو توضيح أو إعادة صياغة. جرّب اكتب موجه هنا ثم اضغط الزر مرة ثانية وشوف النتيجة.",
@@ -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.",
@@ -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.",
@@ -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.",
@@ -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.",
@@ -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)",
@@ -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.",
@@ -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.",
@@ -230,6 +230,7 @@ export const dict = {
"prompt.action.send": "送信",
"prompt.action.stop": "停止",
"prompt.action.enhance": "プロンプトを改善",
"prompt.action.resetModel": "モデルをデフォルトにリセット",
"prompt.action.enhanceDescription":
"「プロンプトを強化」ボタンは、追加コンテキスト、説明、または言い換えを提供することで、リクエストを改善します。ここにリクエストを入力し、ボタンを再度クリックして動作を確認してください。",
@@ -234,6 +234,7 @@ export const dict = {
"prompt.action.send": "전송",
"prompt.action.stop": "중지",
"prompt.action.enhance": "프롬프트 개선",
"prompt.action.resetModel": "모델을 기본값으로 재설정",
"prompt.action.enhanceDescription":
"'프롬프트 향상' 버튼은 추가 컨텍스트, 명확화 또는 재구성을 제공하여 요청을 개선합니다. 여기에 요청을 입력한 다음 버튼을 다시 클릭하여 작동 방식을 확인해보세요.",
@@ -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.",
@@ -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.",
@@ -231,6 +231,7 @@ export const dict = {
"prompt.action.send": "Отправить",
"prompt.action.stop": "Остановить",
"prompt.action.enhance": "Улучшить промпт",
"prompt.action.resetModel": "Сбросить модель на значение по умолчанию",
"prompt.action.enhanceDescription":
"Кнопка 'Улучшить запрос' помогает сделать ваш запрос лучше, предоставляя дополнительный контекст, уточнения или переформулировку. Попробуйте ввести запрос и снова нажать кнопку, чтобы увидеть, как это работает.",
@@ -231,6 +231,7 @@ export const dict = {
"prompt.action.send": "ส่ง",
"prompt.action.stop": "หยุด",
"prompt.action.enhance": "ปรับปรุงพรอมต์",
"prompt.action.resetModel": "รีเซ็ตโมเดลเป็นค่าเริ่มต้น",
"prompt.action.enhanceDescription":
"ปุ่ม 'ปรับปรุงพรอมต์' ช่วยปรับปรุงพรอมต์ของคุณโดยให้บริบทเพิ่มเติม ชี้แจง หรือเขียนใหม่ ลองพิมพ์พรอมต์ที่นี่และคลิกปุ่มอีกครั้งเพื่อดูว่ามันทำงานอย่างไร",
@@ -231,6 +231,7 @@ export const dict = {
"prompt.action.send": "发送",
"prompt.action.stop": "停止",
"prompt.action.enhance": "优化提示词",
"prompt.action.resetModel": "重置模型为默认值",
"prompt.action.enhanceDescription":
"'增强提示'按钮通过提供额外上下文、澄清或重新表述来帮助改进您的请求。尝试在此处输入请求,然后再次点击按钮查看其工作原理。",
@@ -231,6 +231,7 @@ export const dict = {
"prompt.action.send": "傳送",
"prompt.action.stop": "停止",
"prompt.action.enhance": "改善提示詞",
"prompt.action.resetModel": "重置模型為預設值",
"prompt.action.enhanceDescription":
"「強化提示詞」按鈕可透過提供額外內容、說明或改寫來協助改善提示詞。試著在這裡輸入提示詞,再點選一次按鈕以了解其運作方式。",
@@ -136,6 +136,8 @@ export function mockSessionValue(overrides?: {
questionErrors: () => new Set<string>(),
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 }],
@@ -66,6 +66,8 @@ const WithSessions: ParentComponent<{ sessions?: typeof mockSessions }> = (props
questionErrors: () => new Set<string>(),
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 }],
@@ -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 = {
</PromptProviders>
),
}
// ---------------------------------------------------------------------------
// Stories — model override active (reset button visible)
// ---------------------------------------------------------------------------
export const WithModelOverride420: Story = {
name: "With model override — 420px",
render: () => (
<PromptProviders modelOverride>
<PromptInput />
</PromptProviders>
),
}
export const WithModelOverride200: Story = {
name: "With model override — 200px",
render: () => (
<PromptProviders modelOverride>
<PromptInput />
</PromptProviders>
),
}
@@ -257,7 +257,7 @@ export type PermissionLevel = "allow" | "ask" | "deny"
export type PermissionConfig = Partial<Record<string, PermissionLevel>>
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<string, AgentConfig>
provider?: Record<string, ProviderConfig>
+25 -6
View File
@@ -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<string, unknown>)) // 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<string, unknown>): Record<string, unknown> {
const result: Record<string, unknown> = {}
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<string, unknown>) as Info // kilocode_change - strip null delete sentinels
await Filesystem.writeJson(filepath, merged)
return merged
}
+5 -3
View File
@@ -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.
*/