Repository navigation
Full-screen agents like Grok Build come back painted after a relaunch, instead of a blank pane until the window is resized - #171
Open
michael-dg wants to merge 1 commit into
Conversation
…, instead of a blank pane until the window is resized When an Attach replays a ring that wrapped, the DAEMON owes the app a repaint and jiggles its PTY (rows-1, then back). An app that reads its size a moment after SIGWINCH (Grok Build, yazi) never saw a change, so relaunching nebula left its pane blank but for the tail of an escape sequence the replay had cut in half (`?2026l`), until a real resize. - The TUI attached before its first frame, at the fallback grid (80×24), and sent the drawn pane's size an instant later: the PTY bounced from its size to 80×24 and back, every session on every relaunch. The main loop now holds an attach asked for before the pane has a size (`PaneState::size_before_attach`) and sends it right after the next frame, sized to the pane that frame drew, or to the fallback grid when it drew none, so nothing waits longer than a frame. Views built without a terminal (every unit test) attach at once, as before. - The jiggle's two resizes went out back to back. `PtySession::resize_forcing_repaint` (formerly `resize_with_jiggle`) decides when the attach lands: a new size is a plain resize, the same size spawns a jiggle that holds the shrunken size for `JIGGLE_HOLD` (100 ms), in a task of its own so the client's requests never wait. The jiggle reads the session's size at both ends, so a real resize during the hold wins. An exact replay still resizes once. Tests: a_jiggle_lets_the_child_see_the_shrunken_size (fails with no hold), a_resize_during_the_jiggle_is_kept (fails if the jiggle restores the size it started from), an_attach_before_the_pane_has_a_size_waits_for_the_frame, a_held_attach_goes_out_after_a_frame_with_no_pane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Full-screen apps that read their size a moment after SIGWINCH came back as a blank pane after a relaunch of nebula, until the window was resized — Grok Build is the agent where it showed. They now come back painted.
Contents: 🐛 Symptom · 🔍 Cause and fix · 📸 Before / After · 🔁 Flow ·⚠️ Risk · 🔧 Technical overview · 📝 Notes
🐛 Symptom
Quit the TUI and relaunch it while a Grok Build session has been running for a while: its pane comes back blank but for its logo, with the tail of a cut escape sequence at the top left (
?2026l,d=i,i=1,q=2). It stays blank until the window is really resized. Claude Code sessions do not show it.🔍 Cause and fix
When an Attach replays a ring that has wrapped, the top of the app's screen is lost, so the DAEMON jiggles the PTY (one row less, then back) to make the app repaint. An app that reads its size a moment after SIGWINCH never saw a change, for two reasons:
An exact replay (a ring that never wrapped) still resizes once, as before. No protocol change.
📸 Before / After
An isolated nebula running Grok Build through
NEBULA_AGENT_CMD(hence the card readingclaude), its ring shrunk to 6 KiB so it wraps in seconds, the TUI killed and relaunched, captured 10 s later.mainat a00b760🔁 Flow
Verdict: 🟢 Low risk — no new surface, no protocol change; a relaunch sizes each PTY once instead of bouncing it, and only a wrapped-ring attach pays a 100 ms jiggle, off the request loop.
ClientRequest, no exec, no file; the held attach is local TUI state and never waits longer than a framepending_attachdebounce inPaneStateand fires from the draw step that runssync_pty_sizeRollback:
git revert <merge>; nothing persisted.🔧 Technical overview
TUI.
event_loop/selection.rs—send_attachholds the attach whilePaneState::size_before_attachis set and the pane has no size;send_held_attachsends it after the frame, throughattach_sized(the former body).event_loop/loop_run.rssets the flag (unit tests never draw, so they keep attaching at once) and callssend_held_attachaftersync_pty_size.DAEMON.
pty/mod.rs—resize_with_jigglebecomesresize_forcing_repaint: a new size is a plain resize, the same size spawnsjiggle.attach.rs— a one-line change to call it.Not Grok-specific. Bytes each app writes in the 1.5 s after a jiggle, 100×30 PTY:
Not done. A server-side screen model (tmux's way) would need no repaint from the app at all, but it is an architecture change.
Tests.
a_jiggle_lets_the_child_see_the_shrunken_size(fails with no hold) anda_resize_during_the_jiggle_is_kept(fails if the jiggle restores the size it started from) inpty/mod.rs;an_attach_before_the_pane_has_a_size_waits_for_the_frameanda_held_attach_goes_out_after_a_frame_with_no_paneinevent_loop/tests.Gate.
cargo fmt --check,cargo clippy --workspace --all-targets --locked -- -D warnings,cargo test --workspace: 1,812 passed on Linux.issues::tests::the_edit_sends_the_title_on_argv_and_the_body_down_stdinis flaky onmainwith or without this change.📝 Notes
mainat a00b760; Add orchestrator view and privileged commands #157's orchestrator attaches throughsend_attachtoo, the one place anAttachis built.nebula killneeded;nebula reloadmoves a running DAEMON onto it.🤖 Generated with Claude Code