You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Reviving #190, which you listed second in #173. Rebased onto current master (fa18eee).
The bug
batchTerminalWrite() splits a large payload into 32 KB frames, but after writing the
first frame it only re-armed the drain loop from the incoming output path. When a burst
is the last thing a terminal emits — a build finishing, a long git log, a Codex
response landing in one go — nothing further arrives to wake the loop, so the remaining
chunks sit in pendingWrites indefinitely.
The visible symptom is a terminal that stops mid-output and only completes when you type
something or unrelated output happens to arrive.
Repro
Open a session, run a command whose final output exceeds the 32 KB first-frame budget
(e.g. git log -p | head -c 100000), and don't touch the terminal. The tail never renders.
The fix
The drain loop re-schedules itself while work remains, instead of depending on an external
wake. No change to the frame budgets or the anti-flicker behaviour they exist for — only
to what re-arms the loop.
Test
test/terminal-flush-budget.test.ts — "drains a large final batch without waiting for
unrelated terminal output". It queues 96 KB, then drains only the scheduled callbacks,
with no further input.
Verified against currentmaster by checking the test file out onto unmodified master in a scratch worktree:
master writes the first 32 KB frame and strands the other 64 KB. The other six tests in
the file pass there, so the harness itself is sound on master.
With the change: 7 passed, pendingWrites empty and writeFrameScheduled false.
Full suite on top of current master: 4075 passed, 0 failed. tsc --noEmit clean.
The terminal-anti-flicker.md rewrite is in this PR, as you asked — the doc described
the old client-side parser and would have been wrong on its own.
On the terminal-ui.js churn since July 29: I read the commits rather than assuming.
Thirteen touch the file since then, and none touch the write/flush path this PR changes —
I checked each for edits to batchTerminalWrite / pendingWrites / writeFrameScheduled
/ _safeYield and all came back clean. The work (#205 scrollback paging, wheel capture,
touch forwarding, glide/easing) sits in the scroll-routing paths, exactly as you predicted.
The rebase was mechanical: one commit, cherry-picked onto fa18eee with no conflicts.
Notes
No new setting; always on, since the stranded-tail state is never desirable.
Branched from current master, single commit, merges cleanly.
Reviewed against the terminal render pipeline and the anti-flicker invariants.
Root cause confirmed. The old yield callback cleared writeFrameScheduled only after flushPendingWrites() returned, so when a large batch requeued its remainder, the reschedule attempt inside the flush saw the flag still set and skipped. The tail then sat in pendingWrites until unrelated output arrived, which is exactly the reported symptom: truncated final responses and shell commands that look idle until you poke them. Clearing the flag before the flush is the right fix, and unifying the three duplicated scheduling sites into _scheduleTerminalWriteFlush() removes the chance of the same mistake recurring in one of them.
What I verified:
No runaway scheduling: the early return on pendingWrites.length === 0 plus the budget cap in flushPendingWrites() means the chain strictly drains and terminates.
The new test is a real regression test: on the old code the second and third 32KB frames never get scheduled, so it fails there and passes here (96KB codex batch draining as three 32KB writes with the queue empty and the flag reset at the end).
Server-side idle detection is unaffected: this is purely the browser render path; output-silence and token-stability detection read PTY output on the server.
The doc rewrite in terminal-anti-flicker.md now matches what the code actually does (xterm.js 6 native DEC 2026 handling, mode-aware 32/64KB budgets, functions living in terminal-ui.js), including removing the stale 50ms discard trade-off section.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reviving #190, which you listed second in #173. Rebased onto current
master(fa18eee).The bug
batchTerminalWrite()splits a large payload into 32 KB frames, but after writing thefirst frame it only re-armed the drain loop from the incoming output path. When a burst
is the last thing a terminal emits — a build finishing, a long
git log, a Codexresponse landing in one go — nothing further arrives to wake the loop, so the remaining
chunks sit in
pendingWritesindefinitely.The visible symptom is a terminal that stops mid-output and only completes when you type
something or unrelated output happens to arrive.
Repro
Open a session, run a command whose final output exceeds the 32 KB first-frame budget
(e.g.
git log -p | head -c 100000), and don't touch the terminal. The tail never renders.The fix
The drain loop re-schedules itself while work remains, instead of depending on an external
wake. No change to the frame budgets or the anti-flicker behaviour they exist for — only
to what re-arms the loop.
Test
test/terminal-flush-budget.test.ts— "drains a large final batch without waiting forunrelated terminal output". It queues 96 KB, then drains only the scheduled callbacks,
with no further input.
Verified against current
masterby checking the test file out onto unmodifiedmasterin a scratch worktree:master writes the first 32 KB frame and strands the other 64 KB. The other six tests in
the file pass there, so the harness itself is sound on master.
With the change: 7 passed,
pendingWritesempty andwriteFrameScheduledfalse.Full suite on top of current master: 4075 passed, 0 failed.
tsc --noEmitclean.On your two notes from #173
The
terminal-anti-flicker.mdrewrite is in this PR, as you asked — the doc describedthe old client-side parser and would have been wrong on its own.
On the
terminal-ui.jschurn since July 29: I read the commits rather than assuming.Thirteen touch the file since then, and none touch the write/flush path this PR changes —
I checked each for edits to
batchTerminalWrite/pendingWrites/writeFrameScheduled/
_safeYieldand all came back clean. The work (#205 scrollback paging, wheel capture,touch forwarding, glide/easing) sits in the scroll-routing paths, exactly as you predicted.
The rebase was mechanical: one commit, cherry-picked onto
fa18eeewith no conflicts.Notes
master, single commit, merges cleanly.🤖 Generated with Claude Code