mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 07:29:42 +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:
+21
-4
@@ -834,7 +834,13 @@ export class WebServer extends EventEmitter {
|
||||
// Keep the body as a RAW STRING and parse it inside the handler — a global
|
||||
// text/plain -> JSON parser would let a cross-site "simple request" (no CORS
|
||||
// preflight) submit JSON to any route. See security review C2.
|
||||
let _crashBreadcrumbs = '';
|
||||
// Keyed by the client's per-page-load id so (a) a page reload (fresh id)
|
||||
// ARCHIVES the previous page's breadcrumbs instead of overwriting them —
|
||||
// iOS PWA reloads used to wipe the trace of the very bug being chased —
|
||||
// and (b) concurrent clients (desktop + phone) don't clobber each other.
|
||||
// Same id replaces in place (each beacon carries the full ring buffer).
|
||||
const MAX_CRASH_PAGES = 10;
|
||||
const _crashPages = new Map<string, { at: number; data: string }>();
|
||||
this.app.addContentTypeParser('text/plain;charset=UTF-8', { parseAs: 'string' }, (_req, body, done) => {
|
||||
done(null, body);
|
||||
});
|
||||
@@ -844,17 +850,28 @@ export class WebServer extends EventEmitter {
|
||||
this.app.post('/api/crash-diag', (req, reply) => {
|
||||
const raw = typeof req.body === 'string' ? req.body : '';
|
||||
let data = raw;
|
||||
let pageId = 'legacy';
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as { data?: unknown };
|
||||
const parsed = JSON.parse(raw) as { data?: unknown; id?: unknown };
|
||||
if (parsed && typeof parsed.data === 'string') data = parsed.data;
|
||||
if (parsed && typeof parsed.id === 'string' && parsed.id) pageId = parsed.id.slice(0, 64);
|
||||
} catch {
|
||||
/* not JSON — treat the raw beacon text as the breadcrumbs */
|
||||
}
|
||||
_crashBreadcrumbs = String(data || '');
|
||||
_crashPages.delete(pageId); // re-insert = move to MRU end
|
||||
_crashPages.set(pageId, { at: Date.now(), data: String(data || '') });
|
||||
if (_crashPages.size > MAX_CRASH_PAGES) {
|
||||
const oldest = _crashPages.keys().next().value;
|
||||
if (oldest !== undefined) _crashPages.delete(oldest);
|
||||
}
|
||||
reply.code(204).send();
|
||||
});
|
||||
this.app.get('/api/crash-diag', (_req, reply) => {
|
||||
reply.code(200).send({ breadcrumbs: _crashBreadcrumbs, timestamp: Date.now() });
|
||||
const pages = [..._crashPages.entries()].map(([id, p]) => ({ id, at: p.at, data: p.data }));
|
||||
const breadcrumbs = pages
|
||||
.map((p) => `═══ page ${p.id} (last beacon ${new Date(p.at).toISOString()}) ═══\n${p.data}`)
|
||||
.join('\n\n');
|
||||
reply.code(200).send({ breadcrumbs, pages, timestamp: Date.now() });
|
||||
});
|
||||
|
||||
// Register all route modules
|
||||
|
||||
Reference in New Issue
Block a user