mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-19 10:02:16 +00:00
fix(tui): pass configured voice shortcut through TextInput layer
Thread the live parsed voiceRecordKey into TextInput so configured voice.record_key chords bubble to useInputHandlers instead of being consumed as editor input. This removes the last hardcoded Ctrl+B pass-through in the composer path while preserving existing global control chord behavior.
This commit is contained in:
parent
bae5a1bbe0
commit
ad579015b2
5 changed files with 75 additions and 16 deletions
43
ui-tui/src/__tests__/textInputPassThrough.test.ts
Normal file
43
ui-tui/src/__tests__/textInputPassThrough.test.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { shouldPassThroughToGlobalHandler } from '../components/textInput.js'
|
||||
import { DEFAULT_VOICE_RECORD_KEY, parseVoiceRecordKey } from '../lib/platform.js'
|
||||
|
||||
const key = (overrides: Record<string, unknown> = {}) =>
|
||||
({ ctrl: false, meta: false, ...overrides }) as any
|
||||
|
||||
describe('shouldPassThroughToGlobalHandler', () => {
|
||||
it('passes through the configured voice shortcut while composer is focused', () => {
|
||||
expect(
|
||||
shouldPassThroughToGlobalHandler('o', key({ ctrl: true }), parseVoiceRecordKey('ctrl+o'))
|
||||
).toBe(true)
|
||||
expect(
|
||||
shouldPassThroughToGlobalHandler('r', key({ meta: true }), parseVoiceRecordKey('alt+r'))
|
||||
).toBe(true)
|
||||
expect(
|
||||
shouldPassThroughToGlobalHandler(' ', key({ ctrl: true }), parseVoiceRecordKey('ctrl+space'))
|
||||
).toBe(true)
|
||||
expect(
|
||||
shouldPassThroughToGlobalHandler('', key({ ctrl: true, return: true }), parseVoiceRecordKey('ctrl+enter'))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the legacy default pass-through when no custom key is provided', () => {
|
||||
expect(shouldPassThroughToGlobalHandler('b', key({ ctrl: true }), DEFAULT_VOICE_RECORD_KEY)).toBe(true)
|
||||
expect(shouldPassThroughToGlobalHandler('b', key({ ctrl: true }))).toBe(true)
|
||||
})
|
||||
|
||||
it('does not swallow ordinary typing keys', () => {
|
||||
expect(shouldPassThroughToGlobalHandler('h', key(), parseVoiceRecordKey('ctrl+o'))).toBe(false)
|
||||
expect(shouldPassThroughToGlobalHandler('o', key(), parseVoiceRecordKey('ctrl+o'))).toBe(false)
|
||||
})
|
||||
|
||||
it('always passes through non-voice global control keys', () => {
|
||||
expect(shouldPassThroughToGlobalHandler('c', key({ ctrl: true }))).toBe(true)
|
||||
expect(shouldPassThroughToGlobalHandler('x', key({ ctrl: true }))).toBe(true)
|
||||
expect(shouldPassThroughToGlobalHandler('', key({ escape: true }))).toBe(true)
|
||||
expect(shouldPassThroughToGlobalHandler('', key({ tab: true }))).toBe(true)
|
||||
expect(shouldPassThroughToGlobalHandler('', key({ pageUp: true }))).toBe(true)
|
||||
expect(shouldPassThroughToGlobalHandler('', key({ pageDown: true }))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -321,6 +321,7 @@ export interface AppLayoutComposerProps {
|
|||
queuedDisplay: string[]
|
||||
submit: (value: string) => void
|
||||
updateInput: StateSetter<string>
|
||||
voiceRecordKey: ParsedVoiceRecordKey
|
||||
}
|
||||
|
||||
export interface AppLayoutProgressProps {
|
||||
|
|
|
|||
|
|
@ -784,9 +784,10 @@ export function useMainApp(gw: GatewayClient) {
|
|||
queueEditIdx: composerState.queueEditIdx,
|
||||
queuedDisplay: composerState.queuedDisplay,
|
||||
submit,
|
||||
updateInput: composerActions.setInput
|
||||
updateInput: composerActions.setInput,
|
||||
voiceRecordKey
|
||||
}),
|
||||
[cols, composerActions, composerState, empty, pagerPageSize, submit]
|
||||
[cols, composerActions, composerState, empty, pagerPageSize, submit, voiceRecordKey]
|
||||
)
|
||||
|
||||
// Pass current progress through unfrozen — streaming update throttling
|
||||
|
|
|
|||
|
|
@ -288,6 +288,7 @@ const ComposerPane = memo(function ComposerPane({
|
|||
onSubmit={composer.submit}
|
||||
placeholder={composer.empty ? PLACEHOLDER : ui.busy ? 'Ctrl+C to interrupt…' : ''}
|
||||
value={composer.input}
|
||||
voiceRecordKey={composer.voiceRecordKey}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,14 @@ import { type MutableRefObject, useEffect, useMemo, useRef, useState } from 'rea
|
|||
import { setInputSelection } from '../app/inputSelectionStore.js'
|
||||
import { readClipboardText, writeClipboardText } from '../lib/clipboard.js'
|
||||
import { cursorLayout, offsetFromPosition } from '../lib/inputMetrics.js'
|
||||
import { isActionMod, isMac, isMacActionFallback } from '../lib/platform.js'
|
||||
import {
|
||||
DEFAULT_VOICE_RECORD_KEY,
|
||||
isActionMod,
|
||||
isMac,
|
||||
isMacActionFallback,
|
||||
isVoiceToggleKey,
|
||||
type ParsedVoiceRecordKey
|
||||
} from '../lib/platform.js'
|
||||
|
||||
type InkExt = typeof Ink & {
|
||||
stringWidth: (s: string) => number
|
||||
|
|
@ -239,6 +246,7 @@ export function TextInput({
|
|||
onSubmit,
|
||||
mask,
|
||||
mouseApiRef,
|
||||
voiceRecordKey = DEFAULT_VOICE_RECORD_KEY,
|
||||
placeholder = '',
|
||||
focus = true
|
||||
}: TextInputProps) {
|
||||
|
|
@ -744,19 +752,9 @@ export function TextInput({
|
|||
return
|
||||
}
|
||||
|
||||
// Ctrl chords claimed by useInputHandlers — pass through instead of
|
||||
// letting them fall into readline-style nav or a literal char insert.
|
||||
// Ctrl+B = voice toggle, Ctrl+X = delete queued message while editing.
|
||||
if (
|
||||
(k.ctrl && inp === 'c') ||
|
||||
(k.ctrl && inp === 'b') ||
|
||||
(k.ctrl && inp === 'x') ||
|
||||
k.tab ||
|
||||
(k.shift && k.tab) ||
|
||||
k.pageUp ||
|
||||
k.pageDown ||
|
||||
k.escape
|
||||
) {
|
||||
// Chords claimed by useInputHandlers — pass through instead of letting
|
||||
// them fall into text-editing behavior here.
|
||||
if (shouldPassThroughToGlobalHandler(inp, k, voiceRecordKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1041,8 +1039,23 @@ interface TextInputProps {
|
|||
onSubmit?: (v: string) => void
|
||||
placeholder?: string
|
||||
value: string
|
||||
voiceRecordKey?: ParsedVoiceRecordKey
|
||||
}
|
||||
|
||||
export const shouldPassThroughToGlobalHandler = (
|
||||
input: string,
|
||||
key: Key,
|
||||
voiceRecordKey: ParsedVoiceRecordKey = DEFAULT_VOICE_RECORD_KEY
|
||||
): boolean =>
|
||||
(key.ctrl && input === 'c') ||
|
||||
(key.ctrl && input === 'x') ||
|
||||
key.tab ||
|
||||
(key.shift && key.tab) ||
|
||||
key.pageUp ||
|
||||
key.pageDown ||
|
||||
key.escape ||
|
||||
isVoiceToggleKey(key, input, voiceRecordKey)
|
||||
|
||||
export interface TextInputMouseApi {
|
||||
dragAt: (row: number, col: number) => void
|
||||
end: () => void
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue