Skip to content

fix: shadow xterm v6.0.0 DECRQM handler to stop terminal freeze on new sessions - #159

Merged
aterrylu merged 1 commit into
mainfrom
terry/fix-xterm-decrqm-crash
Apr 21, 2026
Merged

fix: shadow xterm v6.0.0 DECRQM handler to stop terminal freeze on new sessions#159
aterrylu merged 1 commit into
mainfrom
terry/fix-xterm-decrqm-crash

Conversation

@aterrylu

Copy link
Copy Markdown
Owner

Problem

Existing (resumed) agent sessions work fine, but brand-new sessions are stuck on the WARNING: Loading development channels page — keystrokes do nothing, Enter doesn't dismiss the warning. Reverting source code didn't help because the bug is in node_modules, not source.

Root cause

@xterm/xterm v6.0.0's built-in requestMode CSI $p handler throws ReferenceError: t is not defined after Vite minification. Claude Code's Ink UI emits CSI ? 2026 $ p (DEC Request Mode, mode 2026 — synchronized-output probe) during startup. The throw happens asynchronously inside xterm's WriteBuffer._innerWrite loop, which means:

  1. It aborts the parser mid-chunk.
  2. The catch in _innerWrite's scheduled setTimeout is never entered — the throw escapes to the event loop.
  3. _innerWrite is only re-scheduled when the WriteBuffer is empty, but it's now stuck with unprocessed chunks, so no further writes ever render.

The browser terminal freezes on whatever was drawn right before the crash. For new sessions that's the channels warning page; resumed sessions survived because their Ink probe had already been processed in an earlier mount.

sequenceDiagram
  participant PTY as Server PTY
  participant WS as WebSocket
  participant WB as xterm WriteBuffer
  participant P as xterm Parser
  Note over PTY: Claude Code Ink UI starts
  PTY->>WS: …<br>CSI ? 2026 $ p<br>welcome screen bytes…
  WS->>WB: write(data)
  WB->>P: _innerWrite → parse chunk
  P-->>P: dispatch CSI $p → requestMode()
  Note over P: ReferenceError: t is not defined
  Note over WB: _innerWrite throws, buffer never drains
  Note over WS: Subsequent writes queued but never rendered
  Note over P: Terminal frozen on the warning page
Loading

Why only new sessions: existing sessions had already written their Ink probes before the browser tab was last opened, so their terminals were already past the crash point.

Fix

Register no-op CSI $ p and CSI ? $ p handlers in createXtermBackend() that return true, short-circuiting xterm's built-in buggy handler before it runs. Also added a defense-in-depth try/catch around terminal.write() — it won't catch xterm's async setTimeout-scheduled throws, but guards against any future synchronous parser exceptions.

Test plan

Added xterm-backend.test.ts with three regression tests verifying:

  • No DECRPM response emitted for CSI ? 2026 $ p (our shadow returns true)
  • No DECRPM response emitted for CSI 4 $ p
  • Subsequent writes continue processing after a DECRQM sequence

Tests fail if the shadow handlers are removed — xterm's built-in handler would either emit a response via onData (unminified) or throw (minified).

  • bun vitest run src/terminal/xterm-backend.test.ts — 3 passed
  • bunx tsc --noEmit clean
  • Full dashboard test suite: 165 passed, 2 pre-existing store.test.ts failures (not related to this PR)
  • Verified in browser via Playwright: created a new agent, terminal rendered the full Claude Code welcome screen, typed "hi" at the prompt, zero console errors
  • Existing sessions unaffected (verified by switching between panes)

Risks

  • Low. The only behavior change is that autonomOS terminals no longer respond to DECRQM probes. No app I'm aware of depends on that response; Ink treats the absence of a response as "mode not supported" and falls back to unsynchronized output, which is the xterm.js default behavior anyway.
  • If xterm.js fixes the bug in a future release, our shadow handlers become redundant but still harmless — they just preempt the built-in handler before it runs.

Alternatives considered

  1. Downgrade to xterm.js v5.x — stable and well-tested, but would require reverting any v6 API usage introduced in feat: swappable terminal renderer — xterm.js + ghostty-web #127.
  2. Upgrade to 6.1.0-beta.197 — unknown whether the bug is fixed; betas aren't recommended for prod.
  3. Disable Vite minification for xterm — would need build.rollupOptions.output.manualChunks + per-chunk terser opts; larger blast radius.
  4. Try/catch around terminal.write() — doesn't work because xterm's parser runs async via setTimeout; uncaught throws escape our try/catch.

The shadow handler is the narrowest, most targeted fix.

🤖 Generated with Claude Code

…w sessions

xterm.js v6.0.0's built-in `requestMode` CSI `$p` handler throws
`ReferenceError: t is not defined` after Vite minification. Claude Code's
Ink UI emits `CSI ? 2026 $ p` (synchronized-output probe) during startup,
which crashes xterm's parser mid-write. The crash leaves unprocessed chunks
in the WriteBuffer and no `_innerWrite` is rescheduled, so the browser
terminal freezes on whatever was drawn right before the crash — for new
sessions, that's the `WARNING: Loading development channels` page.

Resumed sessions survived because their Ink probe had already been written
and rendered in an earlier browser session before the faulty sequence
arrived; freshly-spawned sessions hit the probe every time you mount them,
which is why "existing sessions work, new sessions are stuck."

Fix: register no-op CSI handlers for `$ p` and `? $ p` in createXtermBackend
so they short-circuit before xterm's built-in buggy handler runs. Added a
defense-in-depth try/catch around terminal.write() — it can't catch the
async parser throws xterm uses via setTimeout, but protects against any
future sync parser exceptions.

Regression tests verify:
- No DECRPM response is emitted for `CSI ? 2026 $ p` (our shadow returns true)
- No DECRPM response is emitted for `CSI 4 $ p`
- Subsequent writes continue processing after a DECRQM sequence

Tests fail if the shadow handlers are removed (xterm's built-in handler
would emit a response via onData, or throw in a minified build).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid fix — the DECRQM shadow handlers are the right approach for the xterm.js v6.0.0 minification bug, and the regression tests provide genuine coverage. The defensive try/catch around terminal.write() is a reasonable belt-and-suspenders addition even though it cant catch async xterm throws. Approved.

@aterrylu
aterrylu merged commit 9cd57af into main Apr 21, 2026
1 check passed
@aterrylu
aterrylu deleted the terry/fix-xterm-decrqm-crash branch April 21, 2026 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants