mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-29 18:52:45 +08:00
feat(vscode): bind speech-to-text to Cmd/Ctrl+K with hold-to-send (#12798)
* feat(vscode): bind speech-to-text to Cmd/Ctrl+K with hold-to-send * fix(vscode): complete speech shortcut localization and cleanup * fix(vscode): preserve speech shortcut focus and tooltip state
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
@@ -380,6 +381,12 @@ export const NewWorktreeDialog: Component<{
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -429,6 +436,19 @@ export const NewWorktreeDialog: Component<{
|
||||
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 = () => {
|
||||
@@ -606,6 +626,7 @@ export const NewWorktreeDialog: Component<{
|
||||
adjustHeight()
|
||||
}}
|
||||
onKeyDown={onKey}
|
||||
onKeyUp={speechUp}
|
||||
onPaste={(e) => imageAttach.handlePaste(e)}
|
||||
rows={3}
|
||||
dir="auto"
|
||||
|
||||
@@ -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<Set<string>>
|
||||
}
|
||||
|
||||
type Event = Pick<KeyboardEvent, "key" | "altKey" | "ctrlKey" | "metaKey" | "shiftKey" | "repeat" | "timeStamp">
|
||||
|
||||
type Node = {
|
||||
host: HTMLDivElement
|
||||
dispose: VoidFunction
|
||||
setTextarea: (textarea: HTMLTextAreaElement) => void
|
||||
down: (event: Event, submit: () => void) => boolean
|
||||
up: (event: Pick<KeyboardEvent, "key" | "timeStamp">) => 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<KeyboardEvent, "key" | "timeStamp">) => {
|
||||
const key = reviewAnnotationSpeechKey(meta)
|
||||
if (!key) return false
|
||||
return nodes.get(key)?.up(event) ?? false
|
||||
},
|
||||
render,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -1028,6 +1029,32 @@ export const PromptInput: Component<PromptInputProps> = (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()
|
||||
@@ -1406,8 +1433,14 @@ export const PromptInput: Component<PromptInputProps> = (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={() => {
|
||||
|
||||
+51
-49
@@ -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> = (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 = () => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={click}
|
||||
disabled={locked()}
|
||||
aria-label={label()}
|
||||
aria-disabled={locked()}
|
||||
aria-busy={busy()}
|
||||
aria-pressed={props.speech.state() === "recording"}
|
||||
aria-keyshortcuts={speechShortcutValue()}
|
||||
class={`prompt-speech-button prompt-speech-button--${props.speech.state()}`}
|
||||
>
|
||||
{busy() ? (
|
||||
<Spinner style={{ width: "16px", height: "16px" }} />
|
||||
) : (
|
||||
<svg class="prompt-speech-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M8 10.5C9.38 10.5 10.5 9.38 10.5 8V4C10.5 2.62 9.38 1.5 8 1.5C6.62 1.5 5.5 2.62 5.5 4V8C5.5 9.38 6.62 10.5 8 10.5Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.2"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 7.5C3.5 10 5.5 12 8 12C10.5 12 12.5 10 12.5 7.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path d="M8 12V14.5M5.5 14.5H10.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
|
||||
</svg>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
|
||||
return (
|
||||
<Tooltip value={label()} placement="top" openDelay={0}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={click}
|
||||
disabled={locked()}
|
||||
aria-label={label()}
|
||||
aria-disabled={locked()}
|
||||
aria-busy={busy()}
|
||||
aria-pressed={props.speech.state() === "recording"}
|
||||
class={`prompt-speech-button prompt-speech-button--${props.speech.state()}`}
|
||||
>
|
||||
{busy() ? (
|
||||
<Spinner style={{ width: "16px", height: "16px" }} />
|
||||
) : (
|
||||
<svg class="prompt-speech-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M8 10.5C9.38 10.5 10.5 9.38 10.5 8V4C10.5 2.62 9.38 1.5 8 1.5C6.62 1.5 5.5 2.62 5.5 4V8C5.5 9.38 6.62 10.5 8 10.5Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.2"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 7.5C3.5 10 5.5 12 8 12C10.5 12 12.5 10 12.5 7.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path d="M8 12V14.5M5.5 14.5H10.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
|
||||
</svg>
|
||||
)}
|
||||
</Button>
|
||||
<Tooltip
|
||||
value={
|
||||
<Show when={props.speech.state() === "idle"} fallback={<span>{label()}</span>}>
|
||||
<div data-slot="tooltip-keybind">
|
||||
<span>{title()}</span>
|
||||
<span data-slot="tooltip-keybind-key">{speechShortcutLabel()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
}
|
||||
placement="top"
|
||||
openDelay={0}
|
||||
>
|
||||
{button()}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { SpeechState, SpeechToText } from "./useSpeechToText"
|
||||
|
||||
type Key = Pick<KeyboardEvent, "key" | "altKey" | "ctrlKey" | "metaKey" | "shiftKey">
|
||||
type Press = Key & Pick<KeyboardEvent, "repeat" | "timeStamp">
|
||||
|
||||
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<KeyboardEvent, "key" | "timeStamp">): 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)
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
+2
@@ -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": "جاري تحويل الصوت إلى نص... انقر للإلغاء.",
|
||||
|
||||
+2
@@ -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.",
|
||||
|
||||
+2
@@ -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.",
|
||||
|
||||
+2
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
+2
@@ -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.",
|
||||
|
||||
+2
@@ -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": "در حال رونویسی... برای لغو کلیک کنید.",
|
||||
|
||||
+2
@@ -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.",
|
||||
|
||||
+2
@@ -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.",
|
||||
|
||||
+2
@@ -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": "文字起こし中... クリックしてキャンセル。",
|
||||
|
||||
+2
@@ -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": "변환 중... 취소하려면 클릭하세요.",
|
||||
|
||||
+2
@@ -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.",
|
||||
|
||||
+2
@@ -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.",
|
||||
|
||||
+2
@@ -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ć.",
|
||||
|
||||
+2
@@ -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": "Распознавание... Нажмите для отмены.",
|
||||
|
||||
+2
@@ -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": "กำลังถอดเสียง... คลิกเพื่อยกเลิก",
|
||||
|
||||
+2
@@ -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.",
|
||||
|
||||
+2
@@ -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": "Транскрибування... Натисніть, щоб скасувати.",
|
||||
|
||||
+1
@@ -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": "正在转录... 点击取消。",
|
||||
|
||||
+1
@@ -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": "正在轉錄... 點擊取消。",
|
||||
|
||||
Reference in New Issue
Block a user