fix(mobile): CJK input loss — IME state machine, focus routing, and Android InputConnection recovery#143
Merged
Merged
Conversation
…ndroid InputConnection recovery Three independent root causes of intermittent Chinese character loss (English was unaffected because it bypasses the composition path): 1. input-cjk.js state machine: stuck _composing when compositionend never fires (WeChat/Sogou IMEs) silently swallowed all input; the deferred compositionend flush could reset the textarea mid-next-composition (cancels the live IME composition on iOS); the 100ms keydown-echo window discarded ANY input regardless of content. 2. Focus stealing: session-select / SSE-reconnect paths call terminal.focus() (15+ call sites), landing focus on xterm's hidden textarea; with the CJK onData gate active, everything typed there was swallowed. Fix: focus router in initTerminal routes ALL terminal.focus() calls to the CJK field while it is visible, plus a self-healing onData gate that reclaims focus when it swallows input. 3. Android InputConnection wedge (9-key IMEs + Chromium): the keyboard composes in its own UI but delivers zero DOM events. Fix: skip redundant textarea value/selection writes (they race IME session setup), and re-tapping the focused empty field forces a blur→focus cycle that restarts the input session. Diagnostics: input-cjk.js now traces every IME event/flush decision into the crash-diag breadcrumbs; /api/crash-diag stores beacons per page-load id (iOS PWA reloads no longer wipe the trail, concurrent clients no longer clobber each other) and flushes on visibilitychange. Tests: test/input-cjk.test.ts (vm-sandbox, 9 cases incl. regression guards for all three root causes).
…id-only retap recovery (PR Ark0N#143) - BLOCKER (privacy): the CJK diagnostic trace logged typed CONTENT — _esc(e.key) per keystroke, up to 24 chars of textarea value on focus/blur/compstart/ compend/input, and the flushed text — mirrored into _crashDiag, which persists to localStorage and beacons to POST /api/crash-diag. Traces are now content-free: key CLASS via _kdesc (any single code point → 'printable', named keys pass through), value lengths + phantom presence via _vdesc (len=N[+ph]), and 'flush send len=N'. _esc removed. - MAJOR: the onData self-heal refocused the CJK field whenever gated data arrived with focus elsewhere — but onData also fires for xterm's SELF-GENERATED query replies (DA/DSR/CPR/OSC during Ink redraws), so it stole focus from rename/search/settings inputs while output streamed. Now requires document.activeElement === this.terminal.textarea (genuine typed input) and bails when shouldSuppressTerminalQueryResponse(data) matches. - MAJOR: the pointerdown blur→setTimeout(focus,0) wedged-IME recovery ran on ALL platforms; on iOS tapping the focused empty field is normal and the async refocus is outside the user-gesture stack. The listener is now only registered when /Android/i.test(navigator.userAgent). - tests: trace-privacy test (no typed character or textarea value ever appears in the trace; lengths/key classes still recorded), iOS harness asserts the pointerdown recovery never cycles, self-heal source guard asserts both new conditions; vm harness gained a ua option (navigator injected, Android UA default so the existing wedged-IME test still exercises the recovery). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner
|
Merged — thank you @TeigenZhang! The IME state machine + Android InputConnection recovery is careful, well-documented work — CJK input on mobile is dramatically better for it. Review appended one commit: diagnostic traces are now content-free (the crash-diag pipeline persists locally and server-side, so raw keystrokes/textarea values couldn't ride along), the onData self-heal only refocuses on genuine typed input (xterm's self-generated DA/DSR query replies were stealing focus from other inputs), and the tap-to-unwedge recovery is gated to Android where the wedge exists. Your continued mobile/CJK work is hugely appreciated! 🙏 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes Chinese/Japanese/Korean (CJK) input silently disappearing on mobile — characters composed in the IME never reach the PTY, or land in the wrong place after switching apps. Three distinct root causes, one per layer:
compositionstart/compositionupdate/compositionendwere not tracked as a proper state machine, so a composition that was interrupted (app switch, focus steal) leftcjkActivestuck or cleared at the wrong time, dropping the committed text.terminal.focus(), which stole focus back to xterm's hidden textarea while the CJK field was visible. The IME then composed into a black hole (xterm'sonDatais gated bycjkActive). Now everyterminal.focus()routes through one chokepoint that keeps the CJK field focused when it's visible.InputConnectionmid-composition; the textarea silently stopped emitting input events. Detect the dead connection and re-establish it.Changes
input-cjk.js— composition state machine + Android InputConnection recoveryterminal-ui.js— focus router chokepoint so restore paths don't steal focus from the CJK fieldapp.js/server.ts— crash-diag tracing hooks used to pin down the Android casetest/input-cjk.test.ts— 9 new cases (241 lines) covering the state machine and focus routingTest plan
npm test -- test/input-cjk.test.ts— 9/9 passnpm run check:frontend-syntax