mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
fix(mobile): CJK input loss — IME state machine, focus routing, and Android 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).
This commit is contained in:
@@ -305,6 +305,25 @@ Object.assign(CodemanApp.prototype, {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Focus router ──
|
||||
// While the CJK field is visible, EVERY terminal.focus() call must land on
|
||||
// the CJK field instead. Focusing xterm's hidden textarea in CJK mode sends
|
||||
// the IME's output into a black hole: the keyboard composes normally, but
|
||||
// onData is gated by cjkActive, so nothing reaches the field OR the PTY.
|
||||
// Session select / SSE-reconnect restore paths call terminal.focus() and
|
||||
// were silently stealing focus after every app switch on mobile (the
|
||||
// intermittent "Chinese input goes nowhere" bug). One chokepoint here
|
||||
// covers all ~15 call sites plus any future ones.
|
||||
const _xtermFocus = this.terminal.focus.bind(this.terminal);
|
||||
this.terminal.focus = () => {
|
||||
const cjkEl = document.getElementById('cjkInput');
|
||||
if (cjkEl?.classList.contains('cjk-input-visible')) {
|
||||
cjkEl.focus();
|
||||
} else {
|
||||
_xtermFocus();
|
||||
}
|
||||
};
|
||||
|
||||
// On mobile Safari, delay initial fit() to allow layout to settle
|
||||
// This prevents 0-column terminals caused by fit() running before container is sized
|
||||
const isMobileSafari =
|
||||
@@ -627,7 +646,19 @@ Object.assign(CodemanApp.prototype, {
|
||||
// is on, because cjkActive stays true the whole time the field is visible.
|
||||
const isMouseReport = /^\x1b\[<\d+;\d+;\d+[Mm]$/.test(data);
|
||||
// CJK input has focus — block xterm from sending keystrokes to PTY
|
||||
if (!isMouseReport && (window.cjkActive || document.activeElement?.id === 'cjkInput')) return;
|
||||
if (!isMouseReport && (window.cjkActive || document.activeElement?.id === 'cjkInput')) {
|
||||
// Self-heal: if the CJK field is visible but focus drifted to xterm's
|
||||
// hidden textarea (e.g. something called terminal.focus()), everything
|
||||
// typed lands HERE and is swallowed — keyboard shows the IME composing
|
||||
// while both the CJK field and the terminal stay empty. Route focus
|
||||
// back so the very next keystroke lands in the CJK field again.
|
||||
const cjkEl = document.getElementById('cjkInput');
|
||||
if (cjkEl?.classList.contains('cjk-input-visible') && document.activeElement !== cjkEl) {
|
||||
_crashDiag.log('CJK regain-focus (onData swallowed input)');
|
||||
cjkEl.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.activeSessionId) {
|
||||
// Filter terminal query replies generated by xterm.js itself.
|
||||
// Forwarding them through the WebSocket injects DA/DSR/CPR replies
|
||||
@@ -1622,7 +1653,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
// CJK textarea already provides visual feedback — bypass local echo
|
||||
// buffering so each composed word reaches the PTY immediately.
|
||||
_handleCjkInput(text) {
|
||||
if (!this.activeSessionId) return;
|
||||
if (!this.activeSessionId) {
|
||||
_crashDiag.log(`CJK send DROP no-session len=${text.length}`);
|
||||
return;
|
||||
}
|
||||
_crashDiag.log(`CJK send→${this.activeSessionId.slice(0, 8)} len=${text.length}`);
|
||||
this._sendInputAsync(this.activeSessionId, text);
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user