fix(mobile): pace repeated live-input edits

This commit is contained in:
Brennan Benson
2026-08-27 15:38:00 -07:00
parent 3a6b31efa4
commit 48af953871
5 changed files with 34 additions and 9 deletions
@@ -70,7 +70,7 @@ function createAccessoryInputCommitHarness({
}
}
const applyLiveInputMirror = vi.fn(
(_handle: string, _fieldText: string, _composing?: boolean) => {}
async (_handle: string, _fieldText: string, _composing?: boolean) => true
)
const clearPendingLiveInputCommit = vi.fn(() => {})
const flushPendingLiveInputText = vi.fn(async (_expectedHandle: string | null) => flushResult)
@@ -21,7 +21,11 @@ export async function getTerminalLiveAccessoryInactiveInputCommitResult(
type TerminalLiveAccessoryInputCommitOptions = {
readonly activeHandle: string | null
readonly applyLiveInputMirror: (handle: string, fieldText: string, composing?: boolean) => void
readonly applyLiveInputMirror: (
handle: string,
fieldText: string,
composing?: boolean
) => Promise<boolean>
readonly clearPendingLiveInputCommit: () => void
readonly flushPendingLiveInputText: (expectedHandle: string | null) => Promise<boolean>
readonly heldLiveInputTextRef: RefObject<string>
@@ -84,8 +88,12 @@ export function useTerminalLiveAccessoryInputCommit({
setLiveInputCapture(editedText)
liveInputRef.current?.setNativeProps({ text: editedText })
// Preserve undefined so Android's heuristic hold still settles on its timer.
applyLiveInputMirror(activeHandle, editedText, liveInputComposingRef.current)
return { kind: 'handled' }
const sent = await applyLiveInputMirror(
activeHandle,
editedText,
liveInputComposingRef.current
)
return sent ? { kind: 'handled' } : { kind: 'suppress-raw' }
}
case 'commit-held-then-send': {
const sent = await sendTerminalLiveControlAfterPendingFlush(
@@ -190,6 +190,20 @@ describe('terminal live input commit hook', () => {
await vi.waitFor(() => expect(sent).toEqual(['한']))
})
it('Given a failed mirrored Backspace When accessory input commits Then reports failure', async () => {
const { handlers, sent } = createTerminalLiveInputCommitHarness({ sendResult: false })
changeLiveInput(handlers, 'a', false)
await vi.waitFor(() => expect(sent).toEqual(['a']))
const result = await handlers.handleLiveInputAccessoryBytes({
bytes: '\x7f',
localEdit: 'backspace'
})
expect(sent).toEqual(['a', '\x7f'])
expect(result).toEqual({ kind: 'suppress-raw' })
})
it('Given a held syllable When the settle timer elapses Then commits it to the terminal', async () => {
// Given
vi.useFakeTimers()
@@ -133,7 +133,7 @@ export function useTerminalLiveInputCommit<TTabType extends string>({
// that differs from the native field text, so the controlled capture must
// echo the field verbatim; only the PTY mirror sees normalized text.
setLiveInputCapture(nativeEvent.text)
applyLiveInputMirror(
void applyLiveInputMirror(
activeHandle,
normalizeTerminalTextInput(nativeEvent.text),
nativeEvent.isComposing
@@ -30,7 +30,11 @@ type RunTerminalLiveMirrorStep = (
) => Promise<boolean>
type TerminalLivePendingInputFlush = {
readonly applyLiveInputMirror: (handle: string, fieldText: string, composing?: boolean) => void
readonly applyLiveInputMirror: (
handle: string,
fieldText: string,
composing?: boolean
) => Promise<boolean>
readonly clearPendingLiveInputCommit: () => void
readonly flushPendingLiveInputText: (expectedHandle: string | null) => Promise<boolean>
readonly heldLiveInputTextRef: RefObject<string>
@@ -153,9 +157,8 @@ export function useTerminalLivePendingInputFlush<TTabType extends string>({
}, [runMirrorStep])
const applyLiveInputMirror = useCallback(
(handle: string, fieldText: string, composing?: boolean): void => {
void runMirrorStep(handle, fieldText, false, composing)
},
(handle: string, fieldText: string, composing?: boolean): Promise<boolean> =>
runMirrorStep(handle, fieldText, false, composing),
[runMirrorStep]
)