mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 16:02:55 +00:00
Fix iOS native keyboard dictation in mobile terminal inputs
iOS terminates an active keyboard-dictation (and IME) session whenever JS writes text into the focused field that differs from the native text. Both terminal inputs echoed normalized text back into their controlled value, so smart-dash rewrites and dictation hypothesis revisions triggered native setTextAndSelection writes that killed dictation. Store the raw field text in the controlled value and apply dash normalization only on the PTY mirror / send path, which already normalizes. Refs stablyai/orca#7925. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -5061,9 +5061,10 @@ export default function SessionScreen() {
|
||||
}
|
||||
style={styles.textInput}
|
||||
value={input}
|
||||
onChangeText={(text) =>
|
||||
setInput((previousText) => normalizeTerminalTextInput(text, previousText))
|
||||
}
|
||||
// Why: iOS kills an active dictation/IME session when JS
|
||||
// writes a value that differs from the native field text;
|
||||
// store the raw field text and normalize at send time.
|
||||
onChangeText={setInput}
|
||||
placeholder="Type a command…"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
autoCapitalize="none"
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const sessionRouteSource = readFileSync(
|
||||
new URL('../../app/h/[hostId]/session/[worktreeId].tsx', import.meta.url),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
// Why: iOS terminates an active keyboard-dictation (and IME) session whenever
|
||||
// JS writes a value into the focused field that differs from the native text
|
||||
// (RN applies it via setTextAndSelection / _setAttributedString). Terminal
|
||||
// inputs therefore must echo the raw field text in their controlled value and
|
||||
// apply dash normalization only on the send/mirror path. See stablyai/orca#7925.
|
||||
describe('terminal iOS dictation write-back', () => {
|
||||
it('does not write normalized text back into the buffered command input value', () => {
|
||||
expect(sessionRouteSource).toContain('onChangeText={setInput}')
|
||||
expect(sessionRouteSource).not.toContain(
|
||||
'setInput((previousText) => normalizeTerminalTextInput'
|
||||
)
|
||||
})
|
||||
|
||||
it('still normalizes the buffered command text at send time', () => {
|
||||
expect(sessionRouteSource).toContain('normalizeTerminalTextInput(input)')
|
||||
})
|
||||
})
|
||||
@@ -202,6 +202,32 @@ describe('terminal live input commit hook', () => {
|
||||
await vi.waitFor(() => expect(sent).toEqual(['a', 'b']))
|
||||
})
|
||||
|
||||
it('Given iOS smart-dash text When the change arrives Then the capture echoes the raw field text and the PTY gets normalized bytes', async () => {
|
||||
// Given
|
||||
const { captures, handlers, sent } = createTerminalLiveInputCommitHarness()
|
||||
|
||||
// When: iOS smart punctuation rewrote "--" into an en dash inside the field
|
||||
handlers.handleLiveInputChange('a–')
|
||||
|
||||
// Then: writing "a--" back into the controlled value would kill an active
|
||||
// iOS dictation/IME session, so the capture must keep what iOS produced
|
||||
expect(captures).toEqual(['a–'])
|
||||
await vi.waitFor(() => expect(sent).toEqual(['a--']))
|
||||
})
|
||||
|
||||
it('Given dictation-style hypothesis revisions When changes arrive Then the field is never rewritten and the PTY converges', async () => {
|
||||
// Given
|
||||
const { captures, handlers, sent } = createTerminalLiveInputCommitHarness()
|
||||
|
||||
// When: iOS dictation replaces its hypothesis as recognition refines
|
||||
handlers.handleLiveInputChange('high')
|
||||
handlers.handleLiveInputChange('hi there')
|
||||
|
||||
// Then: captures only echo the field; the mirror repairs the PTY with DELs
|
||||
expect(captures).toEqual(['high', 'hi there'])
|
||||
await vi.waitFor(() => expect(sent).toEqual(['high', '\x7f\x7f there']))
|
||||
})
|
||||
|
||||
it('Given a trailing space after Hangul When the change arrives Then the space commits the held syllable', async () => {
|
||||
// Given
|
||||
const { handlers, sent } = createTerminalLiveInputCommitHarness()
|
||||
|
||||
@@ -109,9 +109,11 @@ export function useTerminalLiveInputCommit<TTabType extends string>({
|
||||
clearPendingLiveInputCommit()
|
||||
return
|
||||
}
|
||||
const normalizedText = normalizeTerminalTextInput(text)
|
||||
setLiveInputCapture(normalizedText)
|
||||
applyLiveInputMirror(activeHandle, normalizedText)
|
||||
// Why: iOS kills an active dictation/IME session when JS writes a value
|
||||
// that differs from the native field text, so the controlled capture must
|
||||
// echo the field verbatim; only the PTY mirror sees normalized text.
|
||||
setLiveInputCapture(text)
|
||||
applyLiveInputMirror(activeHandle, normalizeTerminalTextInput(text))
|
||||
},
|
||||
[
|
||||
activeHandle,
|
||||
|
||||
Reference in New Issue
Block a user