mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 00:03:59 +00:00
fix(mobile): report composing state from accessory backspace
The accessory path edits the field itself and then mirrors it, but called applyLiveInputMirror with two arguments where the signature takes three. The local option type declared it 2-ary, so the type checker never saw the drop. An omitted composing flag is not "not composing": it selects the Android-only heuristic that holds the trailing non-ASCII run. A pinyin preedit is plain ASCII, so the heuristic reads it as committed text and sends it. Typing `ni hao`, tapping accessory Backspace, then picking a candidate put `ni ha` on the PTY before the commit, giving `ni ha你好`. Korean survived this by accident - the non-ASCII heuristic re-derives the correct hold for Hangul - which is why it went unnoticed. The held range is the fact the mirror needs, and it is already in scope. Refs #13345
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { computeTerminalLiveMirrorStep } from './terminal-live-preedit-mirror'
|
||||
import { getTerminalLiveAccessoryLocalEditText } from './terminal-live-text-commit'
|
||||
|
||||
/**
|
||||
* Accessory Backspace edits the field itself, so it must report whether a preedit
|
||||
* is still open. Omitting that report falls back to an Android-only code-point
|
||||
* heuristic that reads a pinyin preedit — plain ASCII — as committed text.
|
||||
*/
|
||||
describe('accessory backspace while composing', () => {
|
||||
const held = 'ni hao'
|
||||
|
||||
it('holds an ASCII preedit when composing is reported', () => {
|
||||
const edited = getTerminalLiveAccessoryLocalEditText({
|
||||
localEdit: 'backspace',
|
||||
fieldText: held
|
||||
})
|
||||
const step = computeTerminalLiveMirrorStep('', edited, {
|
||||
commitHeld: false,
|
||||
composing: true
|
||||
})
|
||||
expect(step.appendText).toBe('')
|
||||
expect(step.nextSentText).toBe('')
|
||||
expect(step.heldText).toBe('ni ha')
|
||||
})
|
||||
|
||||
it('leaks the same preedit to the PTY when the report is dropped', () => {
|
||||
const step = computeTerminalLiveMirrorStep('', 'ni ha', {
|
||||
commitHeld: false,
|
||||
composing: undefined
|
||||
})
|
||||
// Pins the failure the call site must avoid: this is what omitting the
|
||||
// third argument produced, and the candidate commit would append after it.
|
||||
expect(step.appendText).toBe('ni ha')
|
||||
})
|
||||
})
|
||||
@@ -190,7 +190,8 @@ describe('terminal live accessory input commit hook', () => {
|
||||
const result = await harness.commit({ bytes: '\x7f', localEdit: 'backspace' })
|
||||
|
||||
// Then
|
||||
expect(harness.applyLiveInputMirror).toHaveBeenCalledWith('terminal-a', '')
|
||||
// composing: a held syllable is still a preedit, so the mirror must not commit it.
|
||||
expect(harness.applyLiveInputMirror).toHaveBeenCalledWith('terminal-a', '', true)
|
||||
expect(result).toEqual({ kind: 'handled' })
|
||||
expect(harness.sent).toEqual([])
|
||||
})
|
||||
@@ -207,7 +208,8 @@ describe('terminal live accessory input commit hook', () => {
|
||||
const result = await harness.commit({ bytes: '\x7f', localEdit: 'backspace' })
|
||||
|
||||
// Then
|
||||
expect(harness.applyLiveInputMirror).toHaveBeenCalledWith('terminal-a', 'a')
|
||||
// not composing: nothing is held, so the shortened field is committed text and the diff DELs.
|
||||
expect(harness.applyLiveInputMirror).toHaveBeenCalledWith('terminal-a', 'a', false)
|
||||
expect(result).toEqual({ kind: 'handled' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function getTerminalLiveAccessoryInactiveInputCommitResult(
|
||||
|
||||
type TerminalLiveAccessoryInputCommitOptions = {
|
||||
readonly activeHandle: string | null
|
||||
readonly applyLiveInputMirror: (handle: string, fieldText: string) => void
|
||||
readonly applyLiveInputMirror: (handle: string, fieldText: string, composing?: boolean) => void
|
||||
readonly clearPendingLiveInputCommit: () => void
|
||||
readonly flushPendingLiveInputText: (expectedHandle: string | null) => Promise<boolean>
|
||||
readonly heldLiveInputTextRef: RefObject<string>
|
||||
@@ -81,7 +81,10 @@ export function useTerminalLiveAccessoryInputCommit({
|
||||
// field is edited here and the mirror diff syncs the PTY echo.
|
||||
setLiveInputCapture(editedText)
|
||||
liveInputRef.current?.setNativeProps({ text: editedText })
|
||||
applyLiveInputMirror(activeHandle, editedText)
|
||||
// Why pass it: omitted, the mirror falls back to an Android-only
|
||||
// code-point heuristic and commits a still-composing preedit — pinyin
|
||||
// preedit is plain ASCII, so only the held range knows it is unfinished.
|
||||
applyLiveInputMirror(activeHandle, editedText, heldText.length > 0)
|
||||
return { kind: 'handled' }
|
||||
}
|
||||
case 'commit-held-then-send':
|
||||
|
||||
Reference in New Issue
Block a user