Skip to content

perf(terminal): silence replies provoked by replay and coalesce keystrokes - #749

Merged
Juliusolsson05 merged 3 commits into
mainfrom
perf/terminal-replay-input-storm
Sep 3, 2026
Merged

Juliusolsson05 merged 3 commits into
mainfrom
perf/terminal-replay-input-storm

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 3, 2026 •

Copy link
Copy Markdown
Owner

Problem

Attaching an inline terminal writes up to 512 KiB of replay into xterm in one write. xterm answers every terminal query embedded in that stream — cursor-position reports (ESC[6n), device attributes, OSC colour queries — through term.onData, and the three xterm hosts forwarded each chunk to the provider with its own ipcRenderer.invoke. Today's performance journal: one attach at 20:18:57 → 21,795 session:input calls in 15 s and a 7,448 ms renderer long task; other attaches → 2,071 and 2,236 calls; a steady ~5 Hz trickle while any pane is attached. Main then wrote thousands of stale ESC[row;colR replies into the agent's stdin — answers to questions the provider asked minutes ago about a screen it has since repainted. Details in #745.

What changed

  • New src/renderer/src/workspace/tile-tree/terminalInputForwarder.ts: replay(term, chunks) writes the replay and holds a latch until xterm's write callback confirms each chunk is parsed — everything onData emits meanwhile is dropped; onData(data) outside a replay coalesces same-tick chunks into one sendInput (one microtask of latency).
  • AgentTerminalLeaf, TerminalLeaf and AgentInlineTerminal use it for both the buffer replay and the pre-attach backlog, and acknowledge user activity only for data that was actually forwarded (a replay reply is not interaction).

Keystrokes typed during the ~100 ms replay window are dropped with the replies — the pane has not painted yet, and a stale reply cannot be told from a real key (both are escape sequences).

Linked issues

Fixes #745. Refs #103, #390.

Verification

  • terminalInputForwarder.test.ts (unit): replies emitted while a replay is parsed are dropped and nothing is sent; live data after the replay is forwarded, coalesced per tick in order; an empty replay resolves without holding the latch; multi-chunk replay keeps order and releases only after the last callback. 4 tests passed.
  • --project renderer src/renderer/src/workspace/tile-tree/ — passing; npx tsc -b — clean.
  • After: session:input bursts on session:agent-pty-attach should disappear from the journal and the renderer long task with them.

Not in scope

Answering DSR/DA in the headless terminal so responses are consistent whether or not a pane is attached, and switching keystroke transport to a fire-and-forget send (both noted in #745).

Plan: docs/superpowers/plans/2026-09-03-terminal-replay-input-storm.md.

Known limitations

  • Replies to the TUI's LIVE queries are still forwarded one session:input per repaint burst (~5 Hz per attached pane) and still call acknowledgeSession, which clears the unread/ACTION badge without user interaction. Tracked in perf(terminal): live TUI query replies still cost one session:input each and clear the unread badge #751; the structural fix is answering DSR/DA in the headless terminal so the renderer never has to.
  • Keystrokes typed during the replay parse window are dropped with the replies (the pane has not painted yet). The window is one task hop plus the synchronous parse of up to 512 KiB, unmeasured; worth a journal number post-merge.
  • A TUI whose startup probes land in the pre-attach buffer (global terminal view mounts the pane at spawn) no longer gets them answered on replay and waits out its probe timeout — the same behaviour feed view and background sessions already had, now consistent.
  • Two consumers on one session (tile leaf plus the debug inline terminal) still answer each live query twice (pre-existing).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa

Juliusolsson05 and others added 3 commits September 3, 2026 13:40
…rokes

Attaching an inline terminal wrote up to 512 KiB of replay into xterm at
once, and xterm answered every terminal query embedded in it — cursor
position reports, device attributes, colour queries — through onData, each
forwarded to the provider as its own IPC invoke: one attach measured
21,795 session:input calls in 15 s with a 7.4 s renderer long task, and
main wrote thousands of stale ESC[row;colR replies into the agent's stdin.

A shared forwarder now holds a latch while xterm parses replayed content
and drops whatever it provokes, coalesces same-tick chunks into one IPC
call, and only acknowledges user activity for data that was forwarded.
All three xterm hosts use it.

Fixes #745
Refs #103, #390

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
…d prove the host path

Review of #749: a synchronous throw from term.write (xterm's pending-bytes
watermark) left replayDepth raised for chunks that would never call back,
which is a pane that drops every keystroke for the rest of its life. The
executor now releases the unqueued chunks' share and rejects. The host
test's xterm mock never invoked the write callback, so it modelled exactly
that stuck latch; it now calls back and asserts that a keystroke typed
after attach reaches sendInput. A unit test covers xterm's synchronous
callback fast path, and the inline debug terminal joins its backlog like
the leaves do.

Refs #745, #751

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Review applied:

  • Host-level guard: the leaf test's xterm mock now invokes the write callback, and a new case types a key after attach and asserts it reaches sendInput (and acknowledges) — the deaf-pane failure mode is now covered through the real wiring.
  • Synchronous callback path has its own forwarder test.
  • Throwing term.write releases the unqueued chunks' share of the latch and rejects instead of leaving the pane deaf.
  • Live-reply trickle + acknowledge clearing the ACTION badge split into perf(terminal): live TUI query replies still cost one session:input each and clear the unread badge #751 (this PR keeps Fixes #745 for the replay storm it measured); PR body gained a Known limitations section incl. the startup-probe consistency note; paste wording corrected; inline terminal joins its backlog like the leaves.

@Juliusolsson05
Juliusolsson05 merged commit b73893b into main Sep 3, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the perf/terminal-replay-input-storm branch September 3, 2026 21:01
Juliusolsson05 added a commit that referenced this pull request Sep 3, 2026
…tings instead of unmounting it

MainSurface rendered the focus-takeover surfaces instead of the
GlobalEditorShell subtree, so entering Reader Mode with terminal panes
disposed every xterm and detached every PTY, and leaving re-attached and
replayed up to 512 KiB into each brand-new terminal — a multi-second
freeze and lost scrollback per toggle. Global Editor fullscreen already
retains the workspace under display:none and tells the terminal ownership
boundary that mounted no longer means visible; RetainedWorkspaceSurface
applies the same pattern one level up for the surfaces that bypass the
shell, with a layout-neutral display:contents wrapper when visible.

TileLeaf folds the new hidden flag into its Tail-All mask so un-hiding is
a genuine false→true transition that re-pins, and the shell's fullscreen
Escape listener stands down while hidden so Escape in Reader Mode exits
Reader rather than also collapsing a fullscreen editor underneath.

Fixes #752
Refs #745, #749

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
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.

perf(terminal): attaching an agent terminal replays 512 KB into xterm and floods stdin with query replies

1 participant