Merge pull request #143 from TeigenZhang/pr/cjk-input-loss

fix(mobile): CJK input loss — IME state machine, focus routing, and Android InputConnection recovery
This commit is contained in:
Ark0N
2026-07-12 13:23:19 +02:00
committed by GitHub
5 changed files with 538 additions and 19 deletions
+46 -2
View File
@@ -316,6 +316,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 =
@@ -638,7 +657,28 @@ 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.
// Only GENUINE typed input qualifies: onData also fires for xterm's
// self-generated query replies (DA/DSR/CPR/OSC during Ink redraws),
// which arrive no matter what has focus — so require focus to be on
// xterm's own textarea and bail on query replies, or this would steal
// focus from the rename/search/settings inputs while output streams.
const cjkEl = document.getElementById('cjkInput');
if (
cjkEl?.classList.contains('cjk-input-visible') &&
document.activeElement === this.terminal.textarea &&
!window.CodemanTerminalInput?.shouldSuppressTerminalQueryResponse(data)
) {
_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
@@ -1633,7 +1673,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);
},