From 6efc7eda57c31f1925bffc3d6acc4419e9a6b15b Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Wed, 3 Jun 2026 19:29:42 -0500 Subject: [PATCH] refactor(hermes-ink): delete now-dead SGR mouse fragment recovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the tokenizer reassembling split CSI sequences across a flush (prior commit), no SGR mouse fragment can reach a text token anymore — terminals write a mouse report as one atomic sequence, and any read/flush split now re-joins in the tokenizer buffer instead of leaking. That makes the whole downstream recovery layer dead code: - SGR_MOUSE_FRAGMENT_RE, MOUSE_BURST_NOISE_RE, MOUSE_BURST_RESIDUE_RE - parseTextWithSgrMouseFragments / parseSgrMouseFragment / normalizeSgrMouseFragment - the whole-text mouse-burst noise fast path in parseMultipleKeypresses Remove all of it (~185 lines) and the tests that only exercised it. The narrow legacy X10 wheel-tail resynth stays (distinct mechanism, kept with its own test). This retires the #17701 → #18113 → #26781 → #28463 → #35512 regex hardening chain in favor of the one correct parser fix. --- .../hermes-ink/src/ink/parse-keypress.test.ts | 79 ++-------- .../hermes-ink/src/ink/parse-keypress.ts | 146 ++---------------- 2 files changed, 20 insertions(+), 205 deletions(-) diff --git a/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts b/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts index e2ea933697f..c84982d681a 100644 --- a/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts +++ b/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts @@ -97,76 +97,6 @@ describe('mouse wheel modifier decoding', () => { }) }) -describe('fragmented SGR mouse recovery', () => { - it('re-synthesizes bracket-only SGR mouse tails as mouse events', () => { - const [[mouse]] = parseMultipleKeypresses(INITIAL_STATE, '[<35;159;11M') - - expect(mouse).toMatchObject({ kind: 'mouse', button: 35, col: 159, row: 11, action: 'press' }) - }) - - it('re-synthesizes angle-only SGR mouse tails as mouse events', () => { - const [[mouse]] = parseMultipleKeypresses(INITIAL_STATE, '<35;159;11M') - - expect(mouse).toMatchObject({ kind: 'mouse', button: 35, col: 159, row: 11, action: 'press' }) - }) - - it('re-synthesizes degraded SGR mouse bursts without leaking prompt text', () => { - const [events] = parseMultipleKeypresses(INITIAL_STATE, '5;142;11M<35;159;11M35;124;26M35;119;26Mtyped') - - expect(events.slice(0, 4)).toEqual([ - expect.objectContaining({ kind: 'mouse', button: 5, col: 142, row: 11 }), - expect.objectContaining({ kind: 'mouse', button: 35, col: 159, row: 11 }), - expect.objectContaining({ kind: 'mouse', button: 35, col: 124, row: 26 }), - expect.objectContaining({ kind: 'mouse', button: 35, col: 119, row: 26 }) - ]) - expect(events[4]).toMatchObject({ kind: 'key', sequence: 'typed' }) - }) - - it('keeps isolated semicolon text that only resembles a prefixless mouse report', () => { - const [[key]] = parseMultipleKeypresses(INITIAL_STATE, 'see 1;2;3M for details') - - expect(key).toMatchObject({ kind: 'key', sequence: 'see 1;2;3M for details' }) - }) - - it('does not match prefixless fragments inside longer digit runs', () => { - const [[key]] = parseMultipleKeypresses(INITIAL_STATE, '1234;56;78M9;10;11M') - - expect(key).toMatchObject({ kind: 'key', sequence: '1234;56;78M9;10;11M' }) - }) - - it('swallows a fully degraded mouse-burst noise blob without leaking prompt text', () => { - // Captured from Windows Terminal during a heavy tool-call render: the event - // loop blocked past App's 50ms flush timer, so a long burst of SGR mouse - // reports (mode 1003 any-motion) arrived as text with prefixes AND - // too degraded for SGR_MOUSE_FRAGMENT_RE (1- and 2-param remnants, a - // stray focus-in `[I`), so without the whole-text noise fast path the entire - // blob types into the composer and locks the user out. - const blob = - 'M6M35;220;56M6M35;218;56M169;48M;157;47M;44M20;43M79;40M78;40M0M7M35;49;41M48;41M;47;40M9;15;32M[I;31M5;211;26M35;211;25M7M;220;1MM0M09;25M24M23M3;22MM18M99;26M32MM38M63;44M47MM1;51M M4M54M' - - const [events] = parseMultipleKeypresses(INITIAL_STATE, blob) - - expect(events).toEqual([]) - }) - - it('keeps plain prose that only contains scattered M and m letters', () => { - const [[key]] = parseMultipleKeypresses(INITIAL_STATE, 'Mmm MMM mmm yummy') - - expect(key).toMatchObject({ kind: 'key', sequence: 'Mmm MMM mmm yummy' }) - }) - - it('swallows noise wholesale even when it contains intact recoverable fragments', () => { - // A noise blob can carry a few intact ` { it('reassembles a report split by a mid-sequence watchdog flush into one mouse event', () => { // chunk 1: heavy render stalls the loop, only the prefix is read @@ -191,4 +121,13 @@ describe('flush-boundary SGR mouse reassembly', () => { expect(keys).toEqual([]) expect(state.incomplete).toBe('') }) + + it('re-synthesizes an orphaned X10 wheel tail (legacy mouse) into a scroll key', () => { + // X10 wheel-up = ESC[M + (0x40+32) + col + row. If the ESC was flushed as a + // lone Escape and the `[M…` payload arrives as text, resynthesize it. + const tail = '[M' + String.fromCharCode(0x60) + '!!' + const [[key]] = parseMultipleKeypresses(INITIAL_STATE, tail) + + expect(key).toMatchObject({ name: 'wheelup' }) + }) }) diff --git a/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts b/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts index 8f7cceb1b33..966e32bac74 100644 --- a/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts +++ b/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts @@ -63,35 +63,6 @@ const XTVERSION_RE = /^\x1bP>\|(.*?)(?:\x07|\x1b\\)$/s // Button 32=left-drag (0x20 | motion-bit). Plain 0/1/2 = left/mid/right click. // eslint-disable-next-line no-control-regex const SGR_MOUSE_RE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/ -const SGR_MOUSE_FRAGMENT_RE = /(? match[0].startsWith('[<') || match[0].startsWith('<')) - const isFragmentBurst = run.length > 1 - - if (!hasExplicitMousePrefix && !isFragmentBurst) { - continue - } - - if (first.index! > cursor) { - const gap = text.slice(cursor, first.index!) - // Skip pure mouse-leak residue between recovered fragments; only emit - // real text gaps as keypresses. - if (!MOUSE_BURST_RESIDUE_RE.test(gap)) { - parsed.push(parseKeypress(gap)) - } - } - - for (const match of run) { - parsed.push(parseSgrMouseFragment(match[0])) - } - - cursor = runEnd - consumedAny = true - } - - if (!consumedAny) { - return null - } - - if (cursor < text.length) { - const tail = text.slice(cursor) - // Swallow a pure mouse-leak residue tail (the head fragments recovered, but - // the burst trailed off into chewed-up shards). Emit only real trailing text. - if (!MOUSE_BURST_RESIDUE_RE.test(tail)) { - parsed.push(parseKeypress(tail)) - } - } - - return parsed -} - function parseKeypress(s: string = ''): ParsedKey { let parts