fix(terminal): skip already-focused refocus — stop Ink ?1004 focus-in frame stacking#342
Conversation
… frame stacking The Pear terminal pane stacked Claude Code's Ink TUI output (tool cards, banners, status line rendered 2-3x, accumulating with focus/tab interactions). Root cause: the renderer's focus helpers run a container.focus() -> textarea.focus() dance on every pointerdown / keydown / paste. xterm already focuses its textarea synchronously on mousedown, so the dance blurs the still-focused textarea (the container is tabIndex=0, so container.focus() pulls focus off it) and immediately refocuses it. Under DECSET ?1004 — which Claude Code's Ink UI enables — that pointless blur+focus pair emits "\x1b[O\x1b[I" to the PTY on every interaction; the TUI re-commits a frame per report, so duplicates stack in scrollback. The broker snapshot stays correct, so terminal-reconciler.ts (which repairs the viewport only) cannot undo the scrollback pile-up — the spurious reports must not be emitted. Fix: focusTerminalTextarea() skips the focus dance entirely when the xterm textarea is already the active element. Genuine transitions (first mount, input arriving while focus is elsewhere) still focus exactly once. The reconciler backstop is left fully intact. Reproduction (new, faithful — the existing fidelity/corruption suites all pass and do NOT reproduce this): tests/playwright/terminal-focus-stacking models a ?1004 TUI via a new ipc-mock setFocusRedraw knob that re-commits a frame on every focus report the renderer emits, then clicks an already-focused terminal repeatedly and asserts the frame stack does not grow. RED (before fix / on revert): 6 clicks stacked the frame to 16 (~2 per click) GREEN (after fix): the count stays flat across clicks Verified: new test RED->GREEN, revert-RED confirmed (settled 4 -> 16), `npm run test:fidelity` 8/8 green, `npm run typecheck:web` green, use-terminal/runtime-registry dom tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds a ChangesTerminal Focus-Stacking Regression Fix
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed due to a network error. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Findings Addressed Comments
Verification
GitHub API reports PR #342 mergeable with |
The vector
The Pear terminal pane stacked Claude Code's Ink TUI output — tool cards, the "currently unavailable" banner, input echoes, the bottom status line all rendered 2–3×, accumulating with focus/tab interactions.
Root cause: the renderer's focus helpers run a
container.focus()→textarea.focus()dance on everypointerdown/keydown/paste. xterm already focuses its textarea synchronously on mousedown, so re-running the dance blurs the still-focused textarea (the container istabIndex=0, socontainer.focus()pulls focus off it) and immediately refocuses it. Under DECSET ?1004 — which Claude Code's Ink UI enables — that pointless blur+focus pair emits\x1b[O\x1b[Ito the PTY on every interaction. The TUI reads it as "the user looked away / back" and re-commits a frame, so duplicate frames pile up in scrollback and grow with each click.Why a creation-vector fix (and the reconciler is untouched)
The broker snapshot —
terminal-reconciler.ts's ground truth — stays correct, and the reconciler only ever repairs the viewport, never scrollback (its repair payload is self-framing and explicitly leaves scrollback alone). So the stacked frames that scroll up are unreachable by the backstop; the only durable fix is to stop emitting the spurious focus reports at the source. The reconciler and all its gates are left fully intact.The fix
focusTerminalTextarea()skips the focus dance entirely when the xterm textarea is already the active element. Genuine focus transitions (first mount; input arriving while focus is elsewhere) still focus exactly once — the textarea isn't the active element in those cases. Applied to the mount-focus helper and the pointerdown/keydown/paste handlers.Reproduce-before-fix (no vacuous change)
The existing
fidelity-no-duplicationandrendering-corruptionsuites all pass and do not reproduce this — none model a ?1004 focus-in→redraw TUI. New spectests/playwright/terminal-focus-stacking.spec.tsdoes:ipc-mocksetFocusRedrawknob models a ?1004 TUI: every focus report the renderer emits (\x1b[I/\x1b[O) re-commits a frame to the PTY stream (the stacking behavior).?1004, lays a baseline frame, then clicks an already-focused terminal repeatedly and asserts the frame stack does not grow.RED → GREEN evidence
settled 4 → 16(~2 frames per click, unbounded growth).activeElementstays on the xterm textarea throughout.if (already focused) returnguard reproduces the failure (settled at 4, now 16).Verification
npm run test:fidelity→ 8/8 green (new spec + all existing fidelity/corruption tests, incl. "window focus events during stream" and "runtime remount mid-stream").npm run typecheck:web→ green.use-terminal.dom.test.ts(incl. "focuses xterm exactly once on first mount") andterminal-runtime-registry.dom.test.ts→ green.terminal-reconciler.tsnot modified.🤖 Generated with Claude Code