mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 09:19:42 +02:00
fix(tiles): wire the Android soft-keyboard controller into every tile (#541 parity)
#541 fixed Android autocorrect duplicating the typed line in the primary pane: xterm's keyCode-229 textarea diff is append-only, so an autocorrect on space (delete a word, insert the corrected one) sent the whole line again. The fix, an edit-based diff that sends one DEL per deleted code point and then the inserted text, lives in terminal-keycode229-recovery.js together with #441's next-keydown drain (a character committed in the same task as Enter goes out ahead of the \r) and the original orphaned-insertText recovery. Only the primary pane created that controller, so a grid tile or the split's Pane B still ran xterm's stock behaviour. Both are gated on width alone (1180 CSS px), which a wide Android tablet clears. TerminalTile now creates its own controller in connect(), after the xterm opens and before the first await, handed this tile's textarea, this tile's CompositionHelper and _onTerminalData as the send path, so recovered bytes go to the tile's own session through the exactly-once queue. As in the primary pane, handleKeyEvent runs first in the custom key handler, above the keyCode-229 early return, and notifyCanonicalData sits in the onData lambda, gated on the same two CodemanTerminalInput predicates, never in _onTerminalData, which the recovered bytes also take. destroy() tears the controller down before disposing the xterm, which restores xterm's own diff and removes the capture listeners. No mode or device gate, matching the primary. The module itself is unchanged apart from its header; terminal-ui.js gains only a comment naming the twin. Tests: test/terminal-tile-input.test.ts now loads the real module into its vm harness (with window timers, without which create() would silently throw and every test would run against no controller) and drives a fake CompositionHelper carrying xterm's own append-only diff. It covers install and restore on the tile's own helper and textarea, autocorrect sent as an edit (with a control reproducing the device-log duplicate), the last character and an autocorrect each followed by Enter in one task, a self-rescued 229 key delivered once, the onData gate ignoring query replies and focus reports, two refused inserts after one keydown both recovered, robustness when the controller throws, per-tile controllers, and a source pin keeping the call above the early return. Removing the create, the handleKeyEvent call, the notify, its gate, or the destroy each turns at least one of them red, as does moving the notify into _onTerminalData. The browser suite gains a TerminalTile block in test/terminal-keycode229-recovery.browser.test.ts (real xterm, trusted execCommand input, chunks asserted to address the tile's session, with a destroyed-controller control). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -70,6 +70,12 @@ export class FakeTerminal {
|
||||
* puts it.
|
||||
*/
|
||||
static emulateScroll = false;
|
||||
/**
|
||||
* Opt-in, set by a test BEFORE the tile connects (and reset after): extra
|
||||
* fields merged into `_core`, e.g. xterm's `_compositionHelper` for the
|
||||
* keyCode-229 controller, which reads it once when the tile creates it.
|
||||
*/
|
||||
static coreFactory: ((term: FakeTerminal) => Record<string, unknown>) | null = null;
|
||||
options: Record<string, unknown>;
|
||||
cols = 80;
|
||||
rows = 24;
|
||||
@@ -88,7 +94,10 @@ export class FakeTerminal {
|
||||
querySelector: (sel: string) =>
|
||||
sel === '.xterm-screen' ? { getBoundingClientRect: () => ({ ...this.screenRect }) } : null,
|
||||
};
|
||||
_core = { _renderService: { dimensions: { css: { cell: { width: 8, height: 16 } } } } };
|
||||
_core: Record<string, unknown> = {
|
||||
_renderService: { dimensions: { css: { cell: { width: 8, height: 16 } } } },
|
||||
...(FakeTerminal.coreFactory?.(this) ?? {}),
|
||||
};
|
||||
constructor(options: Record<string, unknown>) {
|
||||
this.options = { ...options };
|
||||
FakeTerminal.last = this;
|
||||
@@ -109,12 +118,26 @@ export class FakeTerminal {
|
||||
}
|
||||
keyHandler: ((ev: Record<string, unknown>) => boolean) | null = null;
|
||||
focusListeners: Array<() => void> = [];
|
||||
/** Every other textarea listener, with the capture flag it was added with (the keyCode-229 controller's). */
|
||||
textareaListeners: Array<{ type: string; fn: (ev: Record<string, unknown>) => void; capture: unknown }> = [];
|
||||
textarea = {
|
||||
addEventListener: (type: string, fn: () => void) => {
|
||||
if (type === 'focus') this.focusListeners.push(fn);
|
||||
/** The helper textarea's text, which xterm's keyCode-229 diff (and the controller's) reads. */
|
||||
value: '',
|
||||
addEventListener: (type: string, fn: (ev?: Record<string, unknown>) => void, capture?: unknown) => {
|
||||
if (type === 'focus') this.focusListeners.push(fn as () => void);
|
||||
else this.textareaListeners.push({ type, fn, capture });
|
||||
},
|
||||
removeEventListener: (type: string, fn: () => void) => {
|
||||
removeEventListener: (type: string, fn: (ev?: Record<string, unknown>) => void, capture?: unknown) => {
|
||||
if (type === 'focus') this.focusListeners = this.focusListeners.filter((f) => f !== fn);
|
||||
else {
|
||||
this.textareaListeners = this.textareaListeners.filter(
|
||||
(l) => !(l.type === type && l.fn === fn && Boolean(l.capture) === Boolean(capture))
|
||||
);
|
||||
}
|
||||
},
|
||||
/** Delivers `ev` to the textarea's listeners of `type`, in registration order. */
|
||||
fire: (type: string, ev: Record<string, unknown> = {}) => {
|
||||
for (const l of this.textareaListeners.filter((x) => x.type === type)) l.fn({ type, ...ev });
|
||||
},
|
||||
};
|
||||
focusTextarea() {
|
||||
|
||||
Reference in New Issue
Block a user