diff --git a/.changeset/quiet-speech-shortcut.md b/.changeset/quiet-speech-shortcut.md new file mode 100644 index 00000000000..aa18416e3b6 --- /dev/null +++ b/.changeset/quiet-speech-shortcut.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Bind voice input to Cmd/Ctrl+K in Kilo prompt and review comment fields, with hold-to-talk and release-to-send support. diff --git a/packages/kilo-docs/pages/code-with-ai/features/speech-to-text.md b/packages/kilo-docs/pages/code-with-ai/features/speech-to-text.md index f95f55bfadb..44a1e624bc5 100644 --- a/packages/kilo-docs/pages/code-with-ai/features/speech-to-text.md +++ b/packages/kilo-docs/pages/code-with-ai/features/speech-to-text.md @@ -54,6 +54,8 @@ When you are signed in to the enabled Kilo provider, a microphone button appears 3. Click again to stop recording 4. Your speech is transcribed into text +You can also use **Cmd/Ctrl+K** while a Kilo prompt or review comment field is focused. Tap it to start or stop recording, or hold it while speaking and release to transcribe and submit the focused field. Press it during transcription to cancel. + The feature includes real-time audio level visualization and voice activity detection to automatically detect when you're speaking. --- diff --git a/packages/kilo-vscode/tests/unit/use-speech-to-text.test.ts b/packages/kilo-vscode/tests/unit/use-speech-to-text.test.ts index 3b406e9cdd9..d02b3cb9f6a 100644 --- a/packages/kilo-vscode/tests/unit/use-speech-to-text.test.ts +++ b/packages/kilo-vscode/tests/unit/use-speech-to-text.test.ts @@ -1,6 +1,14 @@ import { describe, expect, it, mock } from "bun:test" import { createRoot } from "solid-js" import type { ExtensionMessage, WebviewMessage } from "../../webview-ui/src/types/messages" +import { + createSpeechShortcut, + isSpeechShortcut, + SPEECH_HOLD_MS, + speechShortcutLabel, + speechShortcutValue, + toggleSpeech, +} from "../../webview-ui/src/components/speech-to-text/shortcut" type Toast = { actions?: Array<{ onClick: string | (() => void) }> @@ -171,3 +179,151 @@ describe("useSpeechToText", () => { ctx.dispose() }) }) + +describe("speech shortcut", () => { + const key = (timeStamp: number, repeat = false) => ({ + key: "k", + metaKey: true, + ctrlKey: false, + altKey: false, + shiftKey: false, + repeat, + timeStamp, + }) + + it("accepts only the platform modifier with K", () => { + expect(isSpeechShortcut({ key: "k", metaKey: true, ctrlKey: false, altKey: false, shiftKey: false }, true)).toBe( + true, + ) + expect(isSpeechShortcut({ key: "k", metaKey: false, ctrlKey: true, altKey: false, shiftKey: false }, true)).toBe( + false, + ) + expect(isSpeechShortcut({ key: "k", metaKey: false, ctrlKey: true, altKey: false, shiftKey: false }, false)).toBe( + true, + ) + expect(isSpeechShortcut({ key: "k", metaKey: true, ctrlKey: false, altKey: false, shiftKey: false }, false)).toBe( + false, + ) + expect(isSpeechShortcut({ key: "k", metaKey: true, ctrlKey: false, altKey: true, shiftKey: false }, true)).toBe( + false, + ) + expect(isSpeechShortcut({ key: "k", metaKey: true, ctrlKey: false, altKey: false, shiftKey: true }, true)).toBe( + false, + ) + }) + + it("exposes platform-specific labels for the focused input", () => { + expect(speechShortcutLabel(true)).toBe("⌘K") + expect(speechShortcutValue(true)).toBe("Meta+K") + expect(speechShortcutLabel(false)).toBe("Ctrl+K") + expect(speechShortcutValue(false)).toBe("Control+K") + }) + + it("does not handle a shortcut when speech is unavailable", () => { + const ctx = setup() + let started = 0 + expect(toggleSpeech(ctx.speech, true, () => started++)).toBe(false) + expect(started).toBe(0) + ctx.dispose() + }) + + it("ignores key repeat and keeps a quick press recording", () => { + const ctx = setup() + const shortcut = createSpeechShortcut({ + speech: ctx.speech, + disabled: () => false, + start: () => ctx.speech.start({ model: "scribe", insert: () => {} }), + finish: () => ctx.speech.stop(), + mac: true, + }) + + expect(shortcut.down(key(0))).toBe(true) + expect(shortcut.down(key(50, true))).toBe(true) + expect(shortcut.down(key(100, true))).toBe(true) + expect(shortcut.up(key(SPEECH_HOLD_MS - 1))).toBe(true) + expect(ctx.sent).toHaveLength(1) + expect(ctx.speech.state()).toBe("starting") + + const start = ctx.sent[0] + if (start?.type !== "speechToTextStart") throw new Error("speech start message missing") + ctx.fire({ type: "speechToTextStarted", requestId: start.requestId }) + expect(ctx.speech.state()).toBe("recording") + ctx.dispose() + }) + + it("stops recording on a second quick press", () => { + const ctx = setup() + const shortcut = createSpeechShortcut({ + speech: ctx.speech, + disabled: () => false, + start: () => ctx.speech.start({ model: "scribe", insert: () => {} }), + finish: () => ctx.speech.stop(), + mac: true, + }) + + shortcut.down(key(0)) + shortcut.up(key(100)) + const start = ctx.sent[0] + if (start?.type !== "speechToTextStart") throw new Error("speech start message missing") + ctx.fire({ type: "speechToTextStarted", requestId: start.requestId }) + + shortcut.down(key(200)) + shortcut.down(key(250, true)) + shortcut.up(key(300)) + expect(ctx.speech.state()).toBe("transcribing") + expect(ctx.sent[1]).toEqual({ type: "speechToTextStop", requestId: start.requestId }) + ctx.dispose() + }) + + it("queues transcription and submit when a held press is released during startup", () => { + const ctx = setup() + let submitted = 0 + const shortcut = createSpeechShortcut({ + speech: ctx.speech, + disabled: () => false, + start: () => ctx.speech.start({ model: "scribe", insert: () => {} }), + finish: (submit) => ctx.speech.stop(submit ? { done: () => submitted++ } : undefined), + mac: true, + }) + + shortcut.down(key(0)) + shortcut.up(key(SPEECH_HOLD_MS)) + expect(ctx.speech.state()).toBe("starting") + expect(ctx.sent).toHaveLength(1) + + const start = ctx.sent[0] + if (start?.type !== "speechToTextStart") throw new Error("speech start message missing") + ctx.fire({ type: "speechToTextStarted", requestId: start.requestId }) + expect(ctx.speech.state()).toBe("transcribing") + expect(ctx.sent[1]).toEqual({ type: "speechToTextStop", requestId: start.requestId }) + + ctx.fire({ type: "speechToTextResult", requestId: start.requestId, text: "Held prompt" }) + expect(submitted).toBe(1) + ctx.dispose() + }) + + it("submits when macOS suppresses K key-up and only reports Command release", () => { + const ctx = setup() + let submitted = 0 + const shortcut = createSpeechShortcut({ + speech: ctx.speech, + disabled: () => false, + start: () => ctx.speech.start({ model: "scribe", insert: () => {} }), + finish: (submit) => ctx.speech.stop(submit ? { done: () => submitted++ } : undefined), + mac: true, + }) + + shortcut.down(key(0)) + const start = ctx.sent[0] + if (start?.type !== "speechToTextStart") throw new Error("speech start message missing") + ctx.fire({ type: "speechToTextStarted", requestId: start.requestId }) + + expect(shortcut.up({ key: "Meta", timeStamp: SPEECH_HOLD_MS })).toBe(true) + expect(ctx.speech.state()).toBe("transcribing") + expect(ctx.sent[1]).toEqual({ type: "speechToTextStop", requestId: start.requestId }) + + ctx.fire({ type: "speechToTextResult", requestId: start.requestId, text: "Held prompt" }) + expect(submitted).toBe(1) + ctx.dispose() + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index a91193c725a..72cf2fa870f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -39,6 +39,7 @@ import { import { useLanguage } from "../src/context/language" import { useImageAttachments, type ImageAttachment } from "../src/hooks/useImageAttachments" import { useSpeechToText } from "../src/components/speech-to-text/useSpeechToText" +import { createSpeechShortcut } from "../src/components/speech-to-text/shortcut" import { convertToMentionPath } from "../src/utils/path-mentions" import { insertSpacedText } from "../src/components/chat/prompt-input-utils" import { WandSparkles } from "@kilocode/kilo-ui/lucide" @@ -345,6 +346,12 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran } const onKey = (e: KeyboardEvent) => { + if (shortcut.down(e)) { + e.preventDefault() + e.stopPropagation() + return + } + // Shift+Tab cycles reasoning effort variants (setting: chat.shiftTabCyclesVariant). // When disabled or no variants exist, fall through to default focus navigation. if (e.key === "Tab" && e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) { @@ -394,6 +401,19 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran speech.start({ model: speechModel(), insert: insertSpeechText }) } + const shortcut = createSpeechShortcut({ + speech, + disabled: () => !canUseSpeech() || starting(), + start: startSpeech, + finish: (submit) => speech.stop(submit ? { done: handleSubmit } : undefined), + }) + const speechUp = (e: KeyboardEvent) => { + if (!shortcut.up(e)) return + e.preventDefault() + e.stopPropagation() + } + onCleanup(shortcut.reset) + const canEnhance = () => !starting() && !enhancing() && !speech.active() && server.isConnected() const handleEnhance = () => { @@ -571,6 +591,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran adjustHeight() }} onKeyDown={onKey} + onKeyUp={speechUp} onPaste={(e) => imageAttach.handlePaste(e)} rows={3} dir="auto" diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/review-annotation-speech.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/review-annotation-speech.tsx index 5ae72b627ed..1e94d176ba0 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/review-annotation-speech.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/review-annotation-speech.tsx @@ -3,6 +3,7 @@ import { render as renderSolid } from "solid-js/web" import { SpeechToTextButton } from "../src/components/speech-to-text/SpeechToTextButton" import { insertSpacedText } from "../src/components/chat/prompt-input-utils" import type { SpeechState, SpeechToText } from "../src/components/speech-to-text/useSpeechToText" +import { createSpeechShortcut } from "../src/components/speech-to-text/shortcut" import { reviewAnnotationSpeechKey, type AnnotationMeta } from "./review-annotations" type Props = { @@ -13,10 +14,14 @@ type Props = { keys: Accessor> } +type Event = Pick + type Node = { host: HTMLDivElement dispose: VoidFunction setTextarea: (textarea: HTMLTextAreaElement) => void + down: (event: Event, submit: () => void) => boolean + up: (event: Pick) => boolean } function insertReviewSpeechText(textarea: HTMLTextAreaElement, value: string): void { @@ -55,9 +60,9 @@ export function createReviewAnnotationSpeechRenderer(props: Props) { error: () => (mine() ? props.speech.error() : undefined), active: () => mine() && props.speech.active(), start: (opts) => start(opts.model), - stop: () => { + stop: (opts) => { if (!mine()) return - props.speech.stop() + props.speech.stop(opts) }, cancel: () => { if (!mine()) return @@ -71,6 +76,13 @@ export function createReviewAnnotationSpeechRenderer(props: Props) { }, } const blocked = () => props.speech.active() && !mine() + let submit = () => {} + const shortcut = createSpeechShortcut({ + speech, + disabled: () => !props.enabled() || blocked(), + start: () => start(props.model()), + finish: (send) => speech.stop(send ? { done: submit } : undefined), + }) const dispose = createRoot((root) => { const view = renderSolid( @@ -92,10 +104,18 @@ export function createReviewAnnotationSpeechRenderer(props: Props) { const node = { host, - dispose, + dispose: () => { + shortcut.reset() + dispose() + }, setTextarea: (next: HTMLTextAreaElement) => { field = next }, + down: (event: Event, next: () => void) => { + submit = next + return shortcut.down(event) + }, + up: shortcut.up, } nodes.set(key, node) return node @@ -134,6 +154,16 @@ export function createReviewAnnotationSpeechRenderer(props: Props) { return { active: props.speech.active, + down: (meta: AnnotationMeta, event: Event, submit: () => void) => { + const key = reviewAnnotationSpeechKey(meta) + if (!key) return false + return nodes.get(key)?.down(event, submit) ?? false + }, + up: (meta: AnnotationMeta, event: Pick) => { + const key = reviewAnnotationSpeechKey(meta) + if (!key) return false + return nodes.get(key)?.up(event) ?? false + }, render, } } diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts b/packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts index 806005f3485..17a9cf5f587 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts +++ b/packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts @@ -86,6 +86,8 @@ interface AnnotationHandlers { speech?: { active: () => boolean render: (meta: AnnotationMeta, textarea: HTMLTextAreaElement) => HTMLElement | undefined + down: (meta: AnnotationMeta, event: KeyboardEvent, submit: () => void) => boolean + up: (meta: AnnotationMeta, event: KeyboardEvent) => boolean } } @@ -309,6 +311,11 @@ export function buildReviewAnnotation( }) textarea.addEventListener("keydown", (event) => { + if (handlers.speech?.down(meta, event, send)) { + event.preventDefault() + event.stopPropagation() + return + } if (event.key === "Escape") { event.preventDefault() handlers.cancelDraft() @@ -319,6 +326,11 @@ export function buildReviewAnnotation( submit() } }) + textarea.addEventListener("keyup", (event) => { + if (!handlers.speech?.up(meta, event)) return + event.preventDefault() + event.stopPropagation() + }) textarea.addEventListener("input", update) return wrapper @@ -363,15 +375,24 @@ export function buildReviewAnnotation( handlers.setEditing(null) }) - saveButton.addEventListener("click", (event) => { - event.stopPropagation() + const save = () => { if (handlers.speech?.active()) return const text = textarea.value.trim() if (!text) return handlers.updateComment(comment.id, text) + } + + saveButton.addEventListener("click", (event) => { + event.stopPropagation() + save() }) textarea.addEventListener("keydown", (event) => { + if (handlers.speech?.down(meta, event, save)) { + event.preventDefault() + event.stopPropagation() + return + } if (event.key === "Escape") { event.preventDefault() handlers.setEditing(null) @@ -379,12 +400,14 @@ export function buildReviewAnnotation( } if (event.key === "Enter" && !event.shiftKey) { event.preventDefault() - if (handlers.speech?.active()) return - const text = textarea.value.trim() - if (!text) return - handlers.updateComment(comment.id, text) + save() } }) + textarea.addEventListener("keyup", (event) => { + if (!handlers.speech?.up(meta, event)) return + event.preventDefault() + event.stopPropagation() + }) return wrapper } 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 d63b0d6a219..18a4fe4b096 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -34,6 +34,7 @@ import { hasGitChangesMention } from "../../hooks/git-changes-context-utils" import { useSlashCommand } from "../../hooks/useSlashCommand" import { useGhostText } from "../../hooks/useGhostText" import { useSpeechToText } from "../speech-to-text/useSpeechToText" +import { createSpeechShortcut } from "../speech-to-text/shortcut" import { useImageAttachments, type ImageAttachment } from "../../hooks/useImageAttachments" import { convertToMentionPath } from "../../utils/path-mentions" import { SessionMentionPicker } from "./SessionMentionPicker" @@ -1024,6 +1025,32 @@ export const PromptInput: Component = (props) => { }) } + const shortcut = createSpeechShortcut({ + speech, + disabled: () => !canUseSpeech() || isDisabled(), + start: startSpeech, + finish: (submit) => { + if (submit) { + transcribeAndSend() + return + } + speech.stop() + }, + }) + const speechDown = (e: KeyboardEvent): boolean => { + if (!shortcut.down(e)) return false + e.preventDefault() + e.stopPropagation() + return true + } + const speechUp = (e: KeyboardEvent): boolean => { + if (!shortcut.up(e)) return false + e.preventDefault() + e.stopPropagation() + return true + } + onCleanup(shortcut.reset) + const handleSendClick = () => { if (speech.state() !== "recording" || !canSend()) { void handleSend() @@ -1402,8 +1429,14 @@ export const PromptInput: Component = (props) => { placeholder={placeholder()} value={text()} onInput={handleInput} - onKeyDown={handleKeyDown} - onKeyUp={syncGhost} + onKeyDown={(e) => { + if (speechDown(e)) return + handleKeyDown(e) + }} + onKeyUp={(e) => { + if (speechUp(e)) return + syncGhost() + }} onPaste={handlePaste} onClick={syncGhost} onFocus={syncGhost} diff --git a/packages/kilo-vscode/webview-ui/src/components/speech-to-text/SpeechToTextButton.tsx b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/SpeechToTextButton.tsx index c2e6f0fa738..86e63cd4c61 100644 --- a/packages/kilo-vscode/webview-ui/src/components/speech-to-text/SpeechToTextButton.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/SpeechToTextButton.tsx @@ -1,8 +1,9 @@ import { Button } from "@kilocode/kilo-ui/button" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { Spinner } from "@kilocode/kilo-ui/spinner" -import { onCleanup, type Component } from "solid-js" +import { onCleanup, Show, type Component } from "solid-js" import type { SpeechToText } from "./useSpeechToText" +import { speechShortcutLabel, speechShortcutValue, toggleSpeech } from "./shortcut" type Props = { speech: SpeechToText @@ -22,61 +23,62 @@ export const SpeechToTextButton: Component = (props) => { if (props.speech.state() === "error") return props.speech.error() || props.label("speechToText.tooltip.error") return props.label("speechToText.tooltip.start") } + const title = () => `${label()} ${props.label("speechToText.tooltip.shortcut")}` - const click = () => { - if (props.speech.state() === "starting") return - if (props.speech.state() === "recording") { - props.speech.stop() - return - } - if (props.speech.state() === "transcribing") { - props.speech.cancel() - return - } - if (props.speech.state() === "error") { - props.speech.clear() - return - } - if (unavailable()) return - props.start() - } + const click = () => toggleSpeech(props.speech, unavailable(), props.start) onCleanup(() => { if (props.speech.active()) props.speech.cancel() }) + const button = () => ( + + ) + return ( - - + {label()}}> +
+ {title()} + {speechShortcutLabel()} +
+ + } + placement="top" + openDelay={0} + > + {button()}
) } diff --git a/packages/kilo-vscode/webview-ui/src/components/speech-to-text/shortcut.ts b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/shortcut.ts new file mode 100644 index 00000000000..a5287a9f542 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/shortcut.ts @@ -0,0 +1,120 @@ +import type { SpeechState, SpeechToText } from "./useSpeechToText" + +type Key = Pick +type Press = Key & Pick + +export const SPEECH_HOLD_MS = 400 + +export function isSpeechShortcut(event: Key, mac = isMac()): boolean { + const mod = mac ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey + return event.key.toLowerCase() === "k" && mod && !event.altKey && !event.shiftKey +} + +export function speechShortcutLabel(mac = isMac()): string { + return mac ? "⌘K" : "Ctrl+K" +} + +export function speechShortcutValue(mac = isMac()): string { + return mac ? "Meta+K" : "Control+K" +} + +export function toggleSpeech(speech: SpeechToText, disabled: boolean, start: () => void): boolean { + if (speech.state() === "starting") return true + if (speech.state() === "recording") { + speech.stop() + return true + } + if (speech.state() === "transcribing") { + speech.cancel() + return true + } + if (speech.state() === "error") { + speech.clear() + return true + } + if (disabled) return false + start() + return true +} + +type ShortcutOptions = { + speech: SpeechToText + disabled: () => boolean + start: () => void + finish: (submit: boolean) => void + mac?: boolean +} + +export function createSpeechShortcut(opts: ShortcutOptions) { + let press: { state: SpeechState; time: number; mac: boolean } | undefined + + const release = (event: KeyboardEvent) => { + if (!up(event)) return + event.preventDefault() + event.stopPropagation() + } + + const disarm = () => { + if (typeof window === "undefined") return + window.removeEventListener("keyup", release, true) + window.removeEventListener("blur", loseFocus) + document.removeEventListener("visibilitychange", hide, true) + } + + const loseFocus = () => { + if (!press) return + press = undefined + disarm() + opts.finish(false) + } + + const hide = () => { + if (document.visibilityState === "hidden") loseFocus() + } + + const down = (event: Press): boolean => { + const mac = opts.mac ?? isMac() + if (!isSpeechShortcut(event, mac)) return false + if (event.repeat) return !!press + press = undefined + + const state = opts.speech.state() + if (state === "idle" && opts.disabled()) return false + press = { state, time: event.timeStamp, mac } + if (typeof window !== "undefined") { + window.addEventListener("keyup", release, true) + window.addEventListener("blur", loseFocus) + document.addEventListener("visibilitychange", hide, true) + } + + if (state === "idle") opts.start() + if (state === "transcribing" || state === "error") toggleSpeech(opts.speech, false, opts.start) + return true + } + + const up = (event: Pick): boolean => { + if (!press) return false + const key = event.key.toLowerCase() + const ended = key === "k" || (press.mac ? key === "meta" : key === "control") + if (!ended) return false + const current = press + press = undefined + disarm() + + if (current.state === "transcribing" || current.state === "error") return true + const submit = event.timeStamp - current.time >= SPEECH_HOLD_MS + if (current.state !== "idle" || submit) opts.finish(submit) + return true + } + + const reset = () => { + press = undefined + disarm() + } + + return { down, up, reset } +} + +function isMac(): boolean { + return typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) +} diff --git a/packages/kilo-vscode/webview-ui/src/components/speech-to-text/useSpeechToText.ts b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/useSpeechToText.ts index db5b518bc9e..44446b61cd0 100644 --- a/packages/kilo-vscode/webview-ui/src/components/speech-to-text/useSpeechToText.ts +++ b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/useSpeechToText.ts @@ -51,6 +51,7 @@ export function useSpeechToText(vscode: VSCode, server: Server, lang: Lang): Spe let insert: InsertTranscript | undefined let done: (() => void) | undefined let ready: (() => boolean) | undefined + let pending = false const unsub = vscode.onMessage((msg) => { if (!isSpeechMessage(msg)) return @@ -59,6 +60,7 @@ export function useSpeechToText(vscode: VSCode, server: Server, lang: Lang): Spe if (msg.type === "speechToTextStarted") { if (state() !== "starting") return setState("recording") + if (pending) transcribe() return } @@ -114,9 +116,18 @@ export function useSpeechToText(vscode: VSCode, server: Server, lang: Lang): Spe } function stop(opts?: StopOptions) { - if (state() !== "recording") return + if (state() !== "starting" && state() !== "recording") return done = opts?.done ready = opts?.ready + if (state() === "starting") { + pending = true + return + } + transcribe() + } + + function transcribe() { + pending = false setState("transcribing") vscode.postMessage({ type: "speechToTextStop", requestId: request }) } @@ -160,6 +171,7 @@ export function useSpeechToText(vscode: VSCode, server: Server, lang: Lang): Spe insert = undefined done = undefined ready = undefined + pending = false } return { state, error, active, start, stop, cancel, clear } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 834ad410a61..4aca678d7b7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -222,6 +222,8 @@ export const dict = { "انقر لتقييد الكتابة في نظام الملفات. يظل الوصول إلى الشبكة مسموحًا وفق إعدادات sandbox.", "speechToText.tooltip.start": "بدء الإدخال الصوتي باستخدام Kilo Gateway", + "speechToText.tooltip.shortcut": + "انقر أو اضغط على Cmd/Ctrl+K لبدء التسجيل أو إيقافه؛ اضغط باستمرار أثناء التحدث ثم اتركه لتحويل الكلام إلى نص وإرساله.", "speechToText.tooltip.starting": "جارٍ تشغيل الميكروفون... يُرجى الانتظار قبل التحدث.", "speechToText.tooltip.stop": "إيقاف التقاط الصوت", "speechToText.tooltip.transcribing": "جاري تحويل الصوت إلى نص... انقر للإلغاء.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index d83bb2026eb..46e7fd6c135 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -232,6 +232,8 @@ export const dict = { "Clique para restringir as operações de escrita no sistema de arquivos. O acesso à rede continua permitido pelas configurações do sandbox.", "speechToText.tooltip.start": "Iniciar entrada de voz com o Kilo Gateway", + "speechToText.tooltip.shortcut": + "Toque ou pressione Cmd/Ctrl+K para iniciar ou parar a gravação; mantenha o botão pressionado enquanto fala e solte-o para transcrever e enviar.", "speechToText.tooltip.starting": "Iniciando o microfone... Aguarde antes de falar.", "speechToText.tooltip.stop": "Parar captura", "speechToText.tooltip.transcribing": "Transcrevendo... Clique para cancelar.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 8700a0e49ab..520a51a0678 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -230,6 +230,8 @@ export const dict = { "Kliknite da ograničite pisanje u datotečni sistem. Pristup mreži ostaje dozvoljen prema vašim sandbox postavkama.", "speechToText.tooltip.start": "Započni glasovni unos sa Kilo Gateway", + "speechToText.tooltip.shortcut": + "Dodirnite dugme ili pritisnite Cmd/Ctrl+K da pokrenete ili zaustavite snimanje; držite dugme pritisnutim dok govorite, a zatim ga otpustite da biste pretvorili govor u tekst i poslali ga.", "speechToText.tooltip.starting": "Pokretanje mikrofona... Sačekajte prije nego što progovorite.", "speechToText.tooltip.stop": "Zaustavi hvatanje zvuka", "speechToText.tooltip.transcribing": "Prepisivanje... Kliknite da otkažete.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 8487f439354..d21ccf759eb 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -229,6 +229,8 @@ export const dict = { "Klik for at begrænse skriveadgang til filsystemet. Netværksadgang er fortsat tilladt ifølge dine sandboxindstillinger.", "speechToText.tooltip.start": "Start stemmeinput med Kilo Gateway", + "speechToText.tooltip.shortcut": + "Tryk på knappen eller brug Cmd/Ctrl+K til at starte eller stoppe optagelsen; hold knappen nede, mens du taler, og slip den for at transskribere og sende.", "speechToText.tooltip.starting": "Starter mikrofonen... Vent med at tale.", "speechToText.tooltip.stop": "Stop lydoptagelse", "speechToText.tooltip.transcribing": "Transskriberer... Klik for at annullere.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 4f9cb7e5e09..a0ea7610299 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -238,6 +238,8 @@ export const dict = { "Klicken, um Schreibvorgänge im Dateisystem einzuschränken. Der Netzwerkzugriff bleibt gemäß deinen Sandbox-Einstellungen erlaubt.", "speechToText.tooltip.start": "Spracheingabe mit Kilo Gateway starten", + "speechToText.tooltip.shortcut": + "Tippe oder drücke Cmd/Ctrl+K, um die Aufnahme zu starten oder zu stoppen; halte beim Sprechen gedrückt und lasse los, um zu transkribieren und abzusenden.", "speechToText.tooltip.starting": "Mikrofon wird gestartet... Bitte noch nicht sprechen.", "speechToText.tooltip.stop": "Audioerfassung beenden", "speechToText.tooltip.transcribing": "Transkribieren... Zum Abbrechen klicken.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 991100aab25..6e38f5ff211 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -227,6 +227,8 @@ export const dict = { "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.", "speechToText.tooltip.start": "Start voice input with Kilo Gateway", + "speechToText.tooltip.shortcut": + "Tap to start or stop recording. Hold while speaking, then release to transcribe and submit.", "speechToText.tooltip.starting": "Starting microphone... Wait to speak.", "speechToText.tooltip.stop": "Recording. Click to stop.", "speechToText.tooltip.transcribing": "Transcribing... Click to cancel.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index a3965777905..1caf01a9c0e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -233,6 +233,8 @@ export const dict = { "Haz clic para restringir las escrituras en el sistema de archivos. El acceso a la red seguirá permitido según la configuración de tu sandbox.", "speechToText.tooltip.start": "Iniciar entrada de voz con Kilo Gateway", + "speechToText.tooltip.shortcut": + "Toca o pulsa Cmd/Ctrl+K para iniciar o detener la grabación; mantén pulsado mientras hablas y suéltalo para transcribir y enviar.", "speechToText.tooltip.starting": "Iniciando el micrófono... Espera antes de hablar.", "speechToText.tooltip.stop": "Detener captura", "speechToText.tooltip.transcribing": "Transcribiendo... Haz clic para cancelar.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fa.ts b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts index 50d37c91314..928069dad2e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts @@ -227,6 +227,8 @@ export const dict = { "prompt.action.enhanceDescription": "دکمه «بهبود پرامپت» با ارائه زمینه بیشتر، توضیح یا بازنویسی، به بهتر کردن پرامپت شما کمک می‌کند. یک پرامپت تایپ کنید و دوباره روی دکمه کلیک کنید تا نحوه عملکرد آن را ببینید.", "speechToText.tooltip.start": "شروع ورودی صوتی با Kilo Gateway", + "speechToText.tooltip.shortcut": + "برای شروع یا توقف ضبط، روی دکمه ضربه بزنید یا Cmd/Ctrl+K را فشار دهید؛ هنگام صحبت دکمه را نگه دارید و سپس رها کنید تا گفتار به متن تبدیل و ارسال شود.", "speechToText.tooltip.starting": "در حال راه‌اندازی میکروفون... منتظر بمانید.", "speechToText.tooltip.stop": "در حال ضبط. برای توقف کلیک کنید.", "speechToText.tooltip.transcribing": "در حال رونویسی... برای لغو کلیک کنید.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index b64c2b10bd3..90d6a0d93c4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -232,6 +232,8 @@ export const dict = { "Cliquez pour restreindre les écritures dans le système de fichiers. L'accès au réseau reste autorisé par vos paramètres de sandbox.", "speechToText.tooltip.start": "Démarrer la saisie vocale avec Kilo Gateway", + "speechToText.tooltip.shortcut": + "Touchez ou appuyez sur Cmd/Ctrl+K pour démarrer ou arrêter l’enregistrement ; maintenez la touche pendant que vous parlez, puis relâchez-la pour transcrire et envoyer.", "speechToText.tooltip.starting": "Démarrage du microphone... Attendez avant de parler.", "speechToText.tooltip.stop": "Arrêter la capture audio", "speechToText.tooltip.transcribing": "Transcription en cours... Cliquez pour annuler.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 782b07d504d..5ff35d8cb14 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -1256,6 +1256,8 @@ export const dict = { // Speech to Text tooltips and errors "speechToText.tooltip.start": "Avvia input vocale con Kilo Gateway", + "speechToText.tooltip.shortcut": + "Tocca o premi Cmd/Ctrl+K per avviare o interrompere la registrazione; tieni premuto mentre parli e rilascia per trascrivere e inviare.", "speechToText.tooltip.starting": "Avvio del microfono... Attendi prima di parlare.", "speechToText.tooltip.stop": "Interrompi acquisizione", "speechToText.tooltip.transcribing": "Trascrizione... Fai clic per annullare.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 4f3be454a2c..66fe638da35 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -229,6 +229,8 @@ export const dict = { "クリックすると、ファイルシステムへの書き込みを制限します。サンドボックス設定により、ネットワークアクセスは引き続き許可されます。", "speechToText.tooltip.start": "Kilo Gatewayで音声入力を開始", + "speechToText.tooltip.shortcut": + "タップまたは Cmd/Ctrl+K を押して録音を開始/停止し、話している間は押し続け、離すと文字起こしして送信します。", "speechToText.tooltip.starting": "マイクを起動中... まだ話さないでください。", "speechToText.tooltip.stop": "音声キャプチャを停止", "speechToText.tooltip.transcribing": "文字起こし中... クリックしてキャンセル。", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 3930418698e..282acffa52a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -230,6 +230,8 @@ export const dict = { "클릭하면 파일 시스템 쓰기를 제한합니다. 샌드박스 설정에 따라 네트워크 액세스는 계속 허용됩니다.", "speechToText.tooltip.start": "Kilo Gateway로 음성 입력 시작", + "speechToText.tooltip.shortcut": + "탭하거나 Cmd/Ctrl+K를 눌러 녹음을 시작하거나 중지하고, 말하는 동안에는 누르고 있다가 놓으면 음성을 텍스트로 변환해 제출합니다.", "speechToText.tooltip.starting": "마이크를 시작하는 중... 잠시 후 말씀해 주세요.", "speechToText.tooltip.stop": "음성 캡처 중지", "speechToText.tooltip.transcribing": "변환 중... 취소하려면 클릭하세요.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 2109b5e9e53..4ed4b284f23 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -233,6 +233,8 @@ export const dict = { "Klik om schrijfbewerkingen in het bestandssysteem te beperken. Netwerktoegang blijft toegestaan volgens je sandboxinstellingen.", "speechToText.tooltip.start": "Spraakinvoer starten met Kilo Gateway", + "speechToText.tooltip.shortcut": + "Tik of druk op Cmd/Ctrl+K om de opname te starten of te stoppen; houd de knop ingedrukt terwijl je spreekt en laat deze los om te transcriberen en te verzenden.", "speechToText.tooltip.starting": "Microfoon wordt gestart... Wacht nog even met spreken.", "speechToText.tooltip.stop": "Audio vastleggen stoppen", "speechToText.tooltip.transcribing": "Transcriberen... Klik om te annuleren.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index d81042e1da0..580f7ff506d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -234,6 +234,8 @@ export const dict = { "Klikk for å begrense skrivetilgang til filsystemet. Nettverkstilgang er fortsatt tillatt av sandbox-innstillingene dine.", "speechToText.tooltip.start": "Start taleinndata med Kilo Gateway", + "speechToText.tooltip.shortcut": + "Trykk på knappen eller bruk Cmd/Ctrl+K for å starte eller stoppe opptaket; hold knappen inne mens du snakker, og slipp den for å transkribere og sende.", "speechToText.tooltip.starting": "Starter mikrofonen... Vent med å snakke.", "speechToText.tooltip.stop": "Stopp lydfangst", "speechToText.tooltip.transcribing": "Transkriberer... Klikk for å avbryte.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 0ca6c1289d6..1a2c0e2641d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -230,6 +230,8 @@ export const dict = { "Kliknij, aby ograniczyć zapisy w systemie plików. Ustawienia sandboxa nadal zezwalają na dostęp do sieci.", "speechToText.tooltip.start": "Rozpocznij wprowadzanie głosowe z Kilo Gateway", + "speechToText.tooltip.shortcut": + "Stuknij lub naciśnij Cmd/Ctrl+K, aby rozpocząć albo zatrzymać nagrywanie; przytrzymaj podczas mówienia, a następnie zwolnij, aby dokonać transkrypcji i wysłać.", "speechToText.tooltip.starting": "Uruchamianie mikrofonu... Poczekaj, zanim zaczniesz mówić.", "speechToText.tooltip.stop": "Zatrzymaj przechwytywanie dźwięku", "speechToText.tooltip.transcribing": "Transkrybowanie... Kliknij, aby anulować.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 6a9ed3d81f3..d0651d0e625 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -227,6 +227,8 @@ export const dict = { "Нажмите, чтобы ограничить запись в файловую систему. Доступ к сети останется разрешённым согласно настройкам песочницы.", "speechToText.tooltip.start": "Начать голосовой ввод с Kilo Gateway", + "speechToText.tooltip.shortcut": + "Коснитесь или нажмите Cmd/Ctrl+K, чтобы начать или остановить запись; удерживайте кнопку во время речи, затем отпустите её, чтобы транскрибировать и отправить.", "speechToText.tooltip.starting": "Запуск микрофона... Пока не говорите.", "speechToText.tooltip.stop": "Остановить захват звука", "speechToText.tooltip.transcribing": "Распознавание... Нажмите для отмены.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 7816dff799c..344acc5c41f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -227,6 +227,8 @@ export const dict = { "คลิกเพื่อจำกัดการเขียนในระบบไฟล์ การตั้งค่า sandbox ของคุณยังคงอนุญาตให้เข้าถึงเครือข่าย", "speechToText.tooltip.start": "เริ่มการป้อนข้อมูลด้วยเสียงด้วย Kilo Gateway", + "speechToText.tooltip.shortcut": + "แตะหรือกด Cmd/Ctrl+K เพื่อเริ่มหรือหยุดบันทึก จากนั้นกดค้างไว้ขณะพูด แล้วปล่อยเพื่อถอดเสียงและส่ง", "speechToText.tooltip.starting": "กำลังเริ่มไมโครโฟน... โปรดรอก่อนพูด", "speechToText.tooltip.stop": "หยุดจับเสียง", "speechToText.tooltip.transcribing": "กำลังถอดเสียง... คลิกเพื่อยกเลิก", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 033e5e3f10c..c4cdb4d2226 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -228,6 +228,8 @@ export const dict = { "Dosya sistemi yazma işlemlerini kısıtlamak için tıklayın. Sandbox ayarlarınız ağ erişimine izin vermeye devam ediyor.", "speechToText.tooltip.start": "Kilo Gateway ile sesli girişi başlatın", + "speechToText.tooltip.shortcut": + "Kaydı başlatmak veya durdurmak için dokunun ya da Cmd/Ctrl+K tuşlarına basın; konuşurken basılı tutun, ardından metne dönüştürüp göndermek için bırakın.", "speechToText.tooltip.starting": "Mikrofon başlatılıyor... Henüz konuşmayın.", "speechToText.tooltip.stop": "Ses yakalamayı durdur", "speechToText.tooltip.transcribing": "Metne dönüştürülüyor... İptal etmek için tıklayın.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index bc1cd3146bd..6f82c041064 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -229,6 +229,8 @@ export const dict = { "Натисніть, щоб обмежити запис у файлову систему. Доступ до мережі залишиться дозволеним відповідно до налаштувань пісочниці.", "speechToText.tooltip.start": "Почати голосове введення з Kilo Gateway", + "speechToText.tooltip.shortcut": + "Торкніться кнопки або натисніть Cmd/Ctrl+K, щоб почати чи зупинити запис; утримуйте кнопку під час мовлення, а потім відпустіть її, щоб транскрибувати й надіслати.", "speechToText.tooltip.starting": "Запуск мікрофона... Поки що не говоріть.", "speechToText.tooltip.stop": "Зупинити захоплення звуку", "speechToText.tooltip.transcribing": "Транскрибування... Натисніть, щоб скасувати.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 3a4ec2413b6..e98113e8d55 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -221,6 +221,7 @@ export const dict = { "点击以限制文件系统写入。根据你的沙盒设置,网络访问仍然允许。", "speechToText.tooltip.start": "使用 Kilo Gateway 开始语音输入", + "speechToText.tooltip.shortcut": "点击或按下 Cmd/Ctrl+K 开始或停止录音;说话时按住,松开后即可转录并提交。", "speechToText.tooltip.starting": "正在启动麦克风... 请稍后再说。", "speechToText.tooltip.stop": "停止捕获音频", "speechToText.tooltip.transcribing": "正在转录... 点击取消。", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index bbed2c93fc0..9795bca81a6 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -219,6 +219,7 @@ export const dict = { "prompt.action.sandbox.description.disabledNetworkAllowed": "點擊以限制檔案系統寫入。沙盒設定仍允許網路存取。", "speechToText.tooltip.start": "使用 Kilo Gateway 開始語音輸入", + "speechToText.tooltip.shortcut": "點擊或按下 Cmd/Ctrl+K 開始或停止錄音;說話時按住,放開後即可轉錄並提交。", "speechToText.tooltip.starting": "正在啟動麥克風... 請稍後再說。", "speechToText.tooltip.stop": "停止擷取音訊", "speechToText.tooltip.transcribing": "正在轉錄... 點擊取消。",