diff --git a/ui-tui/src/__tests__/platform.test.ts b/ui-tui/src/__tests__/platform.test.ts index dbb6f0fe6..8995b9c6f 100644 --- a/ui-tui/src/__tests__/platform.test.ts +++ b/ui-tui/src/__tests__/platform.test.ts @@ -31,6 +31,36 @@ describe('platform action modifier', () => { }) }) +describe('isVoiceToggleKey', () => { + it('matches raw Ctrl+B on macOS (doc-default across platforms)', async () => { + const { isVoiceToggleKey } = await importPlatform('darwin') + + expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'b')).toBe(true) + expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'B')).toBe(true) + }) + + it('matches Cmd+B on macOS (preserve platform muscle memory)', async () => { + const { isVoiceToggleKey } = await importPlatform('darwin') + + expect(isVoiceToggleKey({ ctrl: false, meta: true, super: false }, 'b')).toBe(true) + expect(isVoiceToggleKey({ ctrl: false, meta: false, super: true }, 'b')).toBe(true) + }) + + it('matches Ctrl+B on non-macOS platforms', async () => { + const { isVoiceToggleKey } = await importPlatform('linux') + + expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'b')).toBe(true) + }) + + it('does not match unmodified b or other Ctrl combos', async () => { + const { isVoiceToggleKey } = await importPlatform('darwin') + + expect(isVoiceToggleKey({ ctrl: false, meta: false, super: false }, 'b')).toBe(false) + expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'a')).toBe(false) + expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'c')).toBe(false) + }) +}) + describe('isMacActionFallback', () => { it('routes raw Ctrl+K and Ctrl+W to readline kill-to-end / delete-word on macOS', async () => { const { isMacActionFallback } = await importPlatform('darwin') diff --git a/ui-tui/src/app/slash/commands/session.ts b/ui-tui/src/app/slash/commands/session.ts index 5f17667f0..90a1beb3f 100644 --- a/ui-tui/src/app/slash/commands/session.ts +++ b/ui-tui/src/app/slash/commands/session.ts @@ -192,7 +192,7 @@ export const sessionCommands: SlashCommand[] = [ ctx.gateway.rpc('voice.toggle', { action }).then( ctx.guarded(r => { ctx.voice.setVoiceEnabled(!!r.enabled) - ctx.transcript.sys(`voice: ${r.enabled ? 'on' : 'off'}`) + ctx.transcript.sys(`voice: ${r.enabled ? 'on — press Ctrl+B to record' : 'off'}`) }) ) } diff --git a/ui-tui/src/app/useInputHandlers.ts b/ui-tui/src/app/useInputHandlers.ts index 72cd5b9e5..cfc3eed7c 100644 --- a/ui-tui/src/app/useInputHandlers.ts +++ b/ui-tui/src/app/useInputHandlers.ts @@ -8,7 +8,7 @@ import type { SudoRespondResponse, VoiceRecordResponse } from '../gatewayTypes.js' -import { isAction, isMac } from '../lib/platform.js' +import { isAction, isMac, isVoiceToggleKey } from '../lib/platform.js' import { getInputSelection } from './inputSelectionStore.js' import type { InputHandlerContext, InputHandlerResult } from './interfaces.js' @@ -370,7 +370,7 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult { return actions.newSession() } - if (isAction(key, ch, 'b')) { + if (isVoiceToggleKey(key, ch)) { return voice.recording ? voiceStop() : voiceStart() } diff --git a/ui-tui/src/components/textInput.tsx b/ui-tui/src/components/textInput.tsx index e91143c00..394c3c67a 100644 --- a/ui-tui/src/components/textInput.tsx +++ b/ui-tui/src/components/textInput.tsx @@ -623,7 +623,19 @@ export function TextInput({ return } - if ((k.ctrl && inp === 'c') || k.tab || (k.shift && k.tab) || k.pageUp || k.pageDown || k.escape) { + // Ctrl+B is the documented voice-recording toggle (see platform.ts → + // isVoiceToggleKey). Pass it through so the app-level handler in + // useInputHandlers receives it instead of being swallowed here as + // either backward-word nav (line below) or a literal 'b' insertion. + if ( + (k.ctrl && inp === 'c') || + (k.ctrl && inp === 'b') || + k.tab || + (k.shift && k.tab) || + k.pageUp || + k.pageDown || + k.escape + ) { return } diff --git a/ui-tui/src/lib/platform.ts b/ui-tui/src/lib/platform.ts index ab694baaf..9e85da16f 100644 --- a/ui-tui/src/lib/platform.ts +++ b/ui-tui/src/lib/platform.ts @@ -33,3 +33,17 @@ export const isMacActionFallback = ( /** Match action-modifier + a single character (case-insensitive). */ export const isAction = (key: { ctrl: boolean; meta: boolean; super?: boolean }, ch: string, target: string): boolean => isActionMod(key) && ch.toLowerCase() === target + +/** + * Voice recording toggle key (Ctrl+B). + * + * Documented as "Ctrl+B" everywhere: tips.py, config.yaml's voice.record_key + * default, and the Python CLI prompt_toolkit handler. We accept raw Ctrl+B on + * every platform so the TUI matches those docs. On macOS we additionally + * accept Cmd+B (the platform action modifier) so existing macOS muscle memory + * keeps working. + */ +export const isVoiceToggleKey = ( + key: { ctrl: boolean; meta: boolean; super?: boolean }, + ch: string +): boolean => (key.ctrl || isActionMod(key)) && ch.toLowerCase() === 'b'