refactor(vscode): simplify speech input wiring

This commit is contained in:
marius-kilocode
2026-05-14 14:50:42 +02:00
parent 3885ff8179
commit 03239c0710
8 changed files with 80 additions and 56 deletions
+10 -13
View File
@@ -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",
}
-1
View File
@@ -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": {
@@ -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()
})
})
@@ -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<string | undefined>(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(() => {
@@ -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<string, string>()
@@ -332,12 +331,8 @@ export const PromptInput: Component<PromptInputProps> = (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?.()
@@ -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({
@@ -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<string, unknown>,
cfg: Cfg,
providers: readonly string[],
profile: unknown | null,
): boolean {
return settings["speechToText.enabled"] === true && hasSpeechToTextAccess(cfg, providers, profile)
}
export function selectedSpeechToTextModel(settings: Record<string, unknown>): string {
return getSpeechToTextModel(String(settings["speechToText.model"] ?? "")).id
}
@@ -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<string, unknown>) {
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()
}
})