feat(vscode): add thinking effort toggle to prompt input

Adds a variant cycle button to the VS Code extension's prompt input
that lets users toggle the thinking effort level (e.g. high/max) for
models that support reasoning, matching the TUI app behaviour.

- Extend ProviderModel types to expose variants and capabilities.reasoning
- Thread variant through the full send-message pipeline (webview → extension → CLI)
- Persist variant selections via VS Code globalState
- Render a thinking toggle button next to the model selector (only
  visible when the selected model supports reasoning variants)

Closes #172
This commit is contained in:
Igor Šćekić
2026-02-20 17:42:27 +01:00
parent b6739440af
commit 1416ebc174
6 changed files with 123 additions and 1 deletions
+20
View File
@@ -285,6 +285,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
message.providerID,
message.modelID,
message.agent,
message.variant,
files,
)
break
@@ -439,6 +440,23 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "telemetry":
TelemetryProxy.capture(message.event, message.properties)
break
case "persistVariant": {
const stored =
this.extensionContext?.globalState.get<Record<string, string | undefined>>("variantSelections") ?? {}
if (message.value === undefined) {
delete stored[message.key]
} else {
stored[message.key] = message.value
}
await this.extensionContext?.globalState.update("variantSelections", stored)
break
}
case "requestVariants": {
const variants =
this.extensionContext?.globalState.get<Record<string, string | undefined>>("variantSelections") ?? {}
this.postMessage({ type: "variantsLoaded", variants })
break
}
}
})
}
@@ -1002,6 +1020,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
providerID?: string,
modelID?: string,
agent?: string,
variant?: string,
files?: Array<{ mime: string; url: string }>,
): Promise<void> {
if (!this.httpClient) {
@@ -1057,6 +1076,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
providerID,
modelID,
agent,
variant,
})
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to send message:", error)
@@ -214,7 +214,7 @@ export class HttpClient {
sessionId: string,
parts: Array<{ type: "text"; text: string } | { type: "file"; mime: string; url: string }>,
directory: string,
options?: { providerID?: string; modelID?: string; agent?: string },
options?: { providerID?: string; modelID?: string; agent?: string; variant?: string },
): Promise<void> {
const body: Record<string, unknown> = { parts }
if (options?.providerID && options?.modelID) {
@@ -224,6 +224,9 @@ export class HttpClient {
if (options?.agent) {
body.agent = options.agent
}
if (options?.variant) {
body.variant = options.variant
}
await this.request<void>("POST", `/session/${sessionId}/message`, body, { directory, allowEmpty: true })
}
@@ -139,6 +139,8 @@ export interface ProviderModel {
latest?: boolean
// Actual shape returned by the server (Provider.Model)
limit?: { context: number; input?: number; output: number }
variants?: Record<string, Record<string, unknown>>
capabilities?: { reasoning: boolean }
}
// Provider definition
@@ -314,6 +314,18 @@ export const PromptInput: Component = () => {
<div class="prompt-input-hint-selectors">
<ModeSwitcher />
<ModelSelector />
<Show when={session.variantList().length > 0}>
<Tooltip value={language.t("command.model.variant.cycle")} placement="top">
<Button
variant="ghost"
size="small"
onClick={() => session.cycleVariant()}
aria-label={language.t("command.model.variant.cycle")}
>
{session.currentVariant() ?? language.t("common.default")}
</Button>
</Tooltip>
</Show>
</div>
<div class="prompt-input-hint-actions">
<Show
@@ -48,6 +48,7 @@ interface SessionStore {
todos: Record<string, TodoItem[]> // sessionID -> todos
modelSelections: Record<string, ModelSelection> // sessionID -> model
agentSelections: Record<string, string> // sessionID -> agent name
variantSelections: Record<string, string | undefined> // "providerID/modelID" -> variant name
}
interface SessionContextValue {
@@ -105,6 +106,11 @@ interface SessionContextValue {
setSessionModel: (sessionID: string, providerID: string, modelID: string) => void
setSessionAgent: (sessionID: string, name: string) => void
// Thinking variant for the selected model
variantList: () => string[]
currentVariant: () => string | undefined
cycleVariant: () => void
// Actions
sendMessage: (text: string, providerID?: string, modelID?: string, files?: FileAttachment[]) => void
abort: () => void
@@ -177,6 +183,7 @@ export const SessionProvider: ParentComponent = (props) => {
todos: {},
modelSelections: {},
agentSelections: {},
variantSelections: {},
})
// Keep pending selection in sync with provider default until the user
@@ -268,6 +275,56 @@ export const SessionProvider: ParentComponent = (props) => {
clearInterval(agentRetryTimer)
})
// Variant (thinking effort) selection — keyed by "providerID/modelID"
const variantKey = (sel: ModelSelection) => `${sel.providerID}/${sel.modelID}`
const variantList = () => {
const sel = selected()
if (!sel) return []
const model = provider.findModel(sel)
if (!model?.variants) return []
return Object.keys(model.variants)
}
const currentVariant = () => {
const sel = selected()
if (!sel) return undefined
const key = variantKey(sel)
const stored = store.variantSelections[key]
const list = variantList()
if (stored && list.includes(stored)) return stored
return undefined
}
const cycleVariant = () => {
const sel = selected()
if (!sel) return
const list = variantList()
if (list.length === 0) return
const key = variantKey(sel)
const current = store.variantSelections[key]
const next = (() => {
if (!current || !list.includes(current)) return list[0]
const idx = list.indexOf(current)
if (idx === list.length - 1) return undefined
return list[idx + 1]
})()
setStore("variantSelections", key, next)
vscode.postMessage({ type: "persistVariant", key, value: next })
}
// Load persisted variants from extension globalState
const unsubVariants = vscode.onMessage((message: ExtensionMessage) => {
if (message.type !== "variantsLoaded") return
for (const [k, v] of Object.entries(message.variants)) {
setStore("variantSelections", k, v)
}
})
vscode.postMessage({ type: "requestVariants" })
onCleanup(unsubVariants)
// Handle messages from extension
onMount(() => {
const unsubscribe = vscode.onMessage((message: ExtensionMessage) => {
@@ -631,6 +688,7 @@ export const SessionProvider: ParentComponent = (props) => {
providerID,
modelID,
agent,
variant: currentVariant(),
files,
})
}
@@ -866,6 +924,9 @@ export const SessionProvider: ParentComponent = (props) => {
},
allMessages,
allParts,
variantList,
currentVariant,
cycleVariant,
sendMessage,
abort,
compact,
@@ -215,6 +215,8 @@ export interface ProviderModel {
latest?: boolean
// Actual shape returned by the server (Provider.Model)
limit?: { context: number; input?: number; output: number }
variants?: Record<string, Record<string, unknown>>
capabilities?: { reasoning: boolean }
}
export interface Provider {
@@ -607,6 +609,12 @@ export interface AgentManagerMultiVersionProgressMessage {
groupId?: string
}
// Stored variant selections loaded from extension globalState (extension → webview)
export interface VariantsLoadedMessage {
type: "variantsLoaded"
variants: Record<string, string | undefined>
}
// Request webview to send initial prompt to a newly created session (extension → webview)
export interface AgentManagerSendInitialMessage {
type: "agentManager.sendInitialMessage"
@@ -663,6 +671,7 @@ export type ExtensionMessage =
| AgentManagerSendInitialMessage
| SetChatBoxMessage
| TriggerTaskMessage
| VariantsLoadedMessage
// ============================================
// Messages FROM webview TO extension
@@ -680,6 +689,7 @@ export interface SendMessageRequest {
providerID?: string
modelID?: string
agent?: string
variant?: string
files?: FileAttachment[]
}
@@ -947,6 +957,18 @@ export interface SetSessionsCollapsedRequest {
collapsed: boolean
}
// Variant persistence (webview → extension)
export interface PersistVariantRequest {
type: "persistVariant"
key: string
value: string | undefined
}
// Request stored variants from extension (webview → extension)
export interface RequestVariantsMessage {
type: "requestVariants"
}
export type WebviewMessage =
| SendMessageRequest
| AbortRequest
@@ -999,6 +1021,8 @@ export type WebviewMessage =
| CreateMultiVersionRequest
| SetTabOrderRequest
| SetSessionsCollapsedRequest
| PersistVariantRequest
| RequestVariantsMessage
// ============================================
// VS Code API type