From 03239c0710be8742db237b53adc21c2ddc6479bb Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 14 May 2026 14:50:42 +0200 Subject: [PATCH] refactor(vscode): simplify speech input wiring --- packages/kilo-gateway/src/server/routes.ts | 23 ++++++------ packages/kilo-vscode/package.json | 1 - .../unit/speech-to-text-models-sync.test.ts | 19 ++++++++++ .../agent-manager/NewWorktreeDialog.tsx | 11 ++---- .../src/components/chat/PromptInput.tsx | 11 ++---- .../components/settings/ExperimentalTab.tsx | 13 +++---- .../components/speech-to-text/availability.ts | 23 ++++++++++++ .../webview-ui/src/context/config.tsx | 35 ++++++++++--------- 8 files changed, 80 insertions(+), 56 deletions(-) create mode 100644 packages/kilo-vscode/tests/unit/speech-to-text-models-sync.test.ts create mode 100644 packages/kilo-vscode/webview-ui/src/components/speech-to-text/availability.ts diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index d3060273693..f2cd17498fa 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -339,19 +339,16 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { }), ), async (c: any) => { - const auth = await Auth.get("kilo") + const proxy = await getProxyAuth() - if (!auth) { + if (!proxy.auth) { return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) } - const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined - if (!token) { + if (!proxy.token) { return c.json({ error: "No valid token found" }, 401) } - const organizationId = auth.type === "oauth" ? auth.accountId : undefined - const { prefix, suffix, model, maxTokens, temperature } = c.req.valid("json") const fimModel = model ?? "mistralai/codestral-2501" const fimMaxTokens = maxTokens ?? 256 @@ -362,8 +359,8 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { const headers = { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - ...buildKiloHeaders(undefined, { kilocodeOrganizationId: organizationId }), + Authorization: `Bearer ${proxy.token}`, + ...buildKiloHeaders(undefined, { kilocodeOrganizationId: proxy.organizationId }), [HEADER_FEATURE]: "autocomplete", } @@ -436,16 +433,16 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { }), ), async (c: any) => { - const auth = await getProxyAuth() - if (!auth.auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) + const proxy = await getProxyAuth() + if (!proxy.auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) - if (!auth.token) return c.json({ error: "No valid token found" }, 401) + if (!proxy.token) return c.json({ error: "No valid token found" }, 401) const body = c.req.valid("json") const headers = { "Content-Type": "application/json", - Authorization: `Bearer ${auth.token}`, - ...buildKiloHeaders(undefined, { kilocodeOrganizationId: auth.organizationId }), + Authorization: `Bearer ${proxy.token}`, + ...buildKiloHeaders(undefined, { kilocodeOrganizationId: proxy.organizationId }), [HEADER_FEATURE]: "vscode-extension", } diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index a760548755b..d073c0dde9d 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -783,7 +783,6 @@ }, "kilo-code.new.speechToText.model": { "type": "string", - "default": "openai/gpt-4o-mini-transcribe", "description": "Model to use for experimental speech-to-text voice input. Requires Kilo Gateway." }, "kilo-code.new.claudeCodeCompat": { diff --git a/packages/kilo-vscode/tests/unit/speech-to-text-models-sync.test.ts b/packages/kilo-vscode/tests/unit/speech-to-text-models-sync.test.ts new file mode 100644 index 00000000000..68081947ea1 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/speech-to-text-models-sync.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "bun:test" +import { readFileSync } from "node:fs" +import { join } from "node:path" +import { DEFAULT_SPEECH_TO_TEXT_MODEL, SPEECH_TO_TEXT_MODELS } from "../../src/speech-to-text/models" + +describe("speech-to-text model settings", () => { + const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf8")) + const prop = pkg.contributes.configuration.properties["kilo-code.new.speechToText.model"] + + it("keeps the default model in code instead of duplicating it in package.json", () => { + expect(prop.default).toBeUndefined() + expect(DEFAULT_SPEECH_TO_TEXT_MODEL.id).toBe(SPEECH_TO_TEXT_MODELS[0]?.id) + }) + + it("keeps the selectable model list out of package.json", () => { + expect(prop.enum).toBeUndefined() + expect(prop.enumDescriptions).toBeUndefined() + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index 46a94362862..aae4496eef7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -19,6 +19,7 @@ import { useConfig } from "../src/context/config" import { ModelSelectorBase } from "../src/components/shared/ModelSelector" import { ModeSwitcherBase } from "../src/components/shared/ModeSwitcher" import { SpeechToTextButton } from "../src/components/speech-to-text/SpeechToTextButton" +import { canUseSpeechToText, selectedSpeechToTextModel } from "../src/components/speech-to-text/availability" import { ThinkingSelectorBase } from "../src/components/shared/ThinkingSelector" import { MultiModelSelector, @@ -33,8 +34,6 @@ import { useSpeechToText } from "../src/components/speech-to-text/useSpeechToTex import { convertToMentionPath } from "../src/utils/path-mentions" import { insertSpacedText } from "../src/components/chat/prompt-input-utils" import { BranchSelect, BranchSelectPopover } from "../src/components/shared/BranchSelect" -import { KILO_PROVIDER_ID } from "../../src/shared/provider-model" -import { getSpeechToTextModel } from "../../src/speech-to-text/models" type VersionCount = 1 | 2 | 3 | 4 const VERSION_OPTIONS: VersionCount[] = [1, 2, 3, 4] @@ -99,12 +98,8 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran const [highlightedIndex, setHighlightedIndex] = createSignal(0) const [variant, setVariant] = createSignal(session.currentVariant()) const speech = useSpeechToText(vscode, server, { t }) - const canUseSpeech = () => - settings()["speechToText.enabled"] === true && - provider.connected().includes(KILO_PROVIDER_ID) && - !config().disabled_providers?.includes(KILO_PROVIDER_ID) && - !!server.profileData() - const speechModel = () => getSpeechToTextModel(String(settings()["speechToText.model"] ?? "")).id + const canUseSpeech = () => canUseSpeechToText(settings(), config(), provider.connected(), server.profileData()) + const speechModel = () => selectedSpeechToTextModel(settings()) // Variant list for the currently selected model const variants = createMemo(() => { 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 6ea3705c7ab..27d984c56a0 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -23,6 +23,7 @@ import { useProvider } from "../../context/provider" import { ModelSelector } from "../shared/ModelSelector" import { ModeSwitcher } from "../shared/ModeSwitcher" import { SpeechToTextButton } from "../speech-to-text/SpeechToTextButton" +import { canUseSpeechToText, selectedSpeechToTextModel } from "../speech-to-text/availability" import { ThinkingSelector } from "../shared/ThinkingSelector" import { useFileMention } from "../../hooks/useFileMention" import { useTerminalContext } from "../../hooks/useTerminalContext" @@ -40,8 +41,6 @@ import { fileName, dirName, buildHighlightSegments, atEnd, insertSpacedText, isP import type { ReviewComment, TextPart } from "../../types/messages" import { formatReviewCommentsMarkdown } from "../../utils/review-comment-markdown" import { pendingDraftKey, scopeDraftKey, sessionDraftKey } from "../../utils/prompt-drafts" -import { KILO_PROVIDER_ID } from "../../../../src/shared/provider-model" -import { getSpeechToTextModel } from "../../../../src/speech-to-text/models" // Per-session input text storage (module-level so it survives remounts) const drafts = new Map() @@ -332,12 +331,8 @@ export const PromptInput: Component = (props) => { const isBusy = () => isPromptBusy(session.status(), !!props.suggesting?.(), !!props.questioning?.()) const isDisabled = () => !server.isConnected() - const canUseSpeech = () => - settings()["speechToText.enabled"] === true && - provider.connected().includes(KILO_PROVIDER_ID) && - !config().disabled_providers?.includes(KILO_PROVIDER_ID) && - !!server.profileData() - const speechModel = () => getSpeechToTextModel(String(settings()["speechToText.model"] ?? "")).id + const canUseSpeech = () => canUseSpeechToText(settings(), config(), provider.connected(), server.profileData()) + const speechModel = () => selectedSpeechToTextModel(settings()) const hasInput = () => text().trim().length > 0 || imageAttach.images().length > 0 || reviewComments().length > 0 const canSend = () => hasInput() && !isDisabled() && !speech.active() && !terminal.pending() && !git.pending() && !props.blocked?.() diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx index 3dc39827c99..fb3eef09818 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx @@ -10,8 +10,8 @@ import { useServer } from "../../context/server" import { useVSCode } from "../../context/vscode" import type { ExtensionMessage } from "../../types/messages" import SettingsRow from "./SettingsRow" -import { KILO_PROVIDER_ID } from "../../../../src/shared/provider-model" -import { DEFAULT_SPEECH_TO_TEXT_MODEL, getSpeechToTextModel } from "../../../../src/speech-to-text/models" +import { DEFAULT_SPEECH_TO_TEXT_MODEL } from "../../../../src/speech-to-text/models" +import { hasSpeechToTextAccess, selectedSpeechToTextModel } from "../speech-to-text/availability" import { SPEECH_TO_TEXT_MODEL_OPTIONS } from "../speech-to-text/model-selector" interface ShareOption { @@ -46,13 +46,8 @@ const ExperimentalTab: Component = () => { }) const experimental = createMemo(() => config().experimental ?? {}) - const kiloReady = createMemo( - () => - provider.connected().includes(KILO_PROVIDER_ID) && - !config().disabled_providers?.includes(KILO_PROVIDER_ID) && - !!server.profileData(), - ) - const speechModel = createMemo(() => getSpeechToTextModel(String(settings()["speechToText.model"] ?? "")).id) + const kiloReady = createMemo(() => hasSpeechToTextAccess(config(), provider.connected(), server.profileData())) + const speechModel = createMemo(() => selectedSpeechToTextModel(settings())) const updateExperimental = (key: string, value: unknown) => { updateConfig({ diff --git a/packages/kilo-vscode/webview-ui/src/components/speech-to-text/availability.ts b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/availability.ts new file mode 100644 index 00000000000..af0293e2cf6 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/availability.ts @@ -0,0 +1,23 @@ +import { KILO_PROVIDER_ID } from "../../../../src/shared/provider-model" +import { getSpeechToTextModel } from "../../../../src/speech-to-text/models" + +type Cfg = { + disabled_providers?: string[] +} + +export function hasSpeechToTextAccess(cfg: Cfg, providers: readonly string[], profile: unknown | null): boolean { + return providers.includes(KILO_PROVIDER_ID) && !cfg.disabled_providers?.includes(KILO_PROVIDER_ID) && !!profile +} + +export function canUseSpeechToText( + settings: Record, + cfg: Cfg, + providers: readonly string[], + profile: unknown | null, +): boolean { + return settings["speechToText.enabled"] === true && hasSpeechToTextAccess(cfg, providers, profile) +} + +export function selectedSpeechToTextModel(settings: Record): string { + return getSpeechToTextModel(String(settings["speechToText.model"] ?? "")).id +} diff --git a/packages/kilo-vscode/webview-ui/src/context/config.tsx b/packages/kilo-vscode/webview-ui/src/context/config.tsx index f7dabed840f..346f88b169f 100644 --- a/packages/kilo-vscode/webview-ui/src/context/config.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/config.tsx @@ -72,23 +72,19 @@ export const ConfigProvider: ParentComponent = (props) => { // a configLoaded message that arrives before the DOM mount. const unsubscribe = vscode.onMessage((message: ExtensionMessage) => { if (message.type === "autocompleteSettingsLoaded") { - const patch = { + mergeSettings({ "autocomplete.enableAutoTrigger": message.settings.enableAutoTrigger, "autocomplete.enableSmartInlineTaskKeybinding": message.settings.enableSmartInlineTaskKeybinding, "autocomplete.enableChatAutocomplete": message.settings.enableChatAutocomplete, "autocomplete.model": message.settings.model, - } - setSavedSettings((prev) => ({ ...prev, ...patch })) - setSettings((prev) => ({ ...prev, ...patch, ...settingsDraft() })) + }) return } if (message.type === "speechToTextSettingsLoaded") { - const patch = { + mergeSettings({ "speechToText.enabled": message.settings.enabled, "speechToText.model": message.settings.model, - } - setSavedSettings((prev) => ({ ...prev, ...patch })) - setSettings((prev) => ({ ...prev, ...patch, ...settingsDraft() })) + }) return } if (message.type === "configLoaded") { @@ -151,17 +147,24 @@ export const ConfigProvider: ParentComponent = (props) => { onCleanup(unsubscribe) + function mergeSettings(patch: Record) { + setSavedSettings((prev) => ({ ...prev, ...patch })) + setSettings((prev) => ({ ...prev, ...patch, ...settingsDraft() })) + } + + const requestInitialData = () => { + vscode.postMessage({ type: "requestConfig" }) + vscode.postMessage({ type: "requestAutocompleteSettings" }) + vscode.postMessage({ type: "requestSpeechToTextSettings" }) + } + // Request config immediately; if the extension's httpClient is not yet ready, // extensionDataReady will fire once initialization completes and we retry once. - vscode.postMessage({ type: "requestConfig" }) - vscode.postMessage({ type: "requestAutocompleteSettings" }) - vscode.postMessage({ type: "requestSpeechToTextSettings" }) + requestInitialData() const fallback = setTimeout(() => { if (loading()) { - vscode.postMessage({ type: "requestConfig" }) - vscode.postMessage({ type: "requestAutocompleteSettings" }) - vscode.postMessage({ type: "requestSpeechToTextSettings" }) + requestInitialData() } }, 3000) @@ -170,9 +173,7 @@ export const ConfigProvider: ParentComponent = (props) => { unsubReady() clearTimeout(fallback) if (loading()) { - vscode.postMessage({ type: "requestConfig" }) - vscode.postMessage({ type: "requestAutocompleteSettings" }) - vscode.postMessage({ type: "requestSpeechToTextSettings" }) + requestInitialData() } })