Repository navigation
cmux-tui + app: a hidden terminal's output reaches the app once a second, at once when shown (SWITCH FRAMES SF-5 C) - #32446
Merged
Conversation
…d at once when shown (SWITCH FRAMES SF-5 C, expected red) Busy gate on a 2x display (2026-11-02): with 8 warm hidden terminals printing 20 lines/s the Mac app woke about 115 times a second, because each hidden terminal's attach stream sent every output frame at once. This adds the raw command set-attach-visibility (attach-visibility-v1; spec, inventory, SDK schema, generated bindings): it records the viewer's visibility on its snapshot request gate and pokes its worker, but the worker does not use it yet, so the test's batching assert fails (21 writes in 2 s to a hidden viewer must give at most 3 output events).
…ned second, at once when shown (SWITCH FRAMES SF-5 C) While a viewer is hidden (set-attach-visibility), its worker keeps output in one batch and sends it as one output event on the next whole wall-clock second (every hidden viewer on the same second, as Chrome aligns a hidden page's timers), at once past 256 KiB (a hidden yes stays bounded), before any snapshot, READY or other frame (stream order), before the stream ends, and at once when the viewer is shown again (the command pokes its wait). Offsets stay contiguous; a digest never names a position past bytes still in the batch. A hidden viewer with no output waits with no deadline.
…howing ends it (SWITCH FRAMES SF-5 C, expected red) TerminalAttachment.sendHidden now plans its commands in hiddenSteps (sizing unchanged) and can send set-attach-visibility, but the plan never asks for it yet, so the test fails on its asserts.
…ut; showing it ends the batching first (SWITCH FRAMES SF-5 C) With attach-visibility-v1, TerminalAttachment.sendHidden also sends set-attach-visibility: after the view stops counting toward the grid when it hides, and before it counts again when it shows, so the waiting output goes out before the view's first shown frame. The attach machine already sends hide on a re-attach while hidden, so a new link is batched too. DaemonCapability.attachVisibility (optional; export updated).
…ted by api-compat-check --write, step cc256ad6)
…op's if (review of SF-5 C)
…hidden-attach-batch # Conflicts: # Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Connection/DaemonEndpoint.swift # cmux-tui/spec/commands.md
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
lawrencecchen
temporarily deployed
to
ci-routing-agent
November 2, 2026 16:17 — with
GitHub Actions
Inactive
Contributor
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Shipped in:
|
lawrencecchen
temporarily deployed
to
ci-routing-agent
November 2, 2026 16:21 — with
GitHub Actions
Inactive
… shown, then the newest (review of #32446, expected red)
lawrencecchen
had a problem deploying
to
ci-routing-agent
November 2, 2026 16:26 — with
GitHub Actions
Error
…shown, then the newest; the idle digest follows every batch flush (review of #32446) The attach's notification thread shares the viewer's request gate: while the viewer is hidden it keeps only the newest scroll-changed and sends it when the viewer is shown again (set_visible wakes the thread's mailbox); notifications are never held. The app applies no scroll-changed from an attach, so these were pure wakeups. The digest deadline is set whenever output joins a hidden batch, so a flush on the byte cap or before another frame still gets its idle digest.
lawrencecchen
temporarily deployed
to
ci-routing-agent
November 2, 2026 16:29 — with
GitHub Actions
Inactive
lawrencecchen
temporarily deployed
to
ci-routing-agent
November 2, 2026 16:40 — with
GitHub Actions
Inactive
…in; a VisibilityWaker type alias (review of #32446)
…hidden-attach-batch
lawrencecchen
temporarily deployed
to
ci-routing-agent
November 2, 2026 16:49 — with
GitHub Actions
Inactive
lawrencecchen
temporarily deployed
to
ci-routing-agent
November 2, 2026 16:57 — with
GitHub Actions
Inactive
lawrencecchen
temporarily deployed
to
ci-routing-agent
November 2, 2026 16:58 — with
GitHub Actions
Inactive
Contributor
Author
|
Review (subagent, correctness first): SHIP at 6bd5627 An independent review agent read the full diff (read only; it built and ran nothing), in three rounds.
|
lawrencecchen
added a commit
that referenced
this pull request
Nov 2, 2026
…e until the terminal drew its waiting output SWITCH FRAMES SF-5, pre-show (lead decision on #32446). With attach-visibility-v1 a hidden terminal's output waits in the daemon (once a second); shown again, it came one round trip after the show, so a switch showed the older screen first and the newest one a frame or more later. - Daemon (attach-visibility-shown-v1): a snapshot viewer shown again gets `shown` once its stream holds every byte printed before the show. - App: the reveal, on the switch's own turn, already sends visibility(true) before the view shows. The session now asks its IO whether the daemon held output (before the IO hears the show) and waits for `shown`; the waiting output parses on the output lane (off main), and the session waits until a frame drawn after that parse presents (as TerminalFrameHold). - The pane (tab switch) and the window (workspace switch) keep what they showed until then through PanePaintHold, as for a page's first paint: the first shown frame is the newest screen; a slow parse keeps the last frame, never shows the older screen. Bound: 500 ms, as every hold. - TerminalAttachment's pure hidden-step plan moves to its own file (file cap).
This branch was previously deployed
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.
Summary
SWITCH FRAMES SF-5 (part C). After #32384 and #32380, with 8 warm hidden terminals each printing 20 lines/s the app's main thread was quiet (wakes near baseline), but the whole app still woke about 115 times a second (24 busy hidden: 114/s; 0 busy: 4/s; cmux24, 2x display). Cause: each warm hidden terminal has its own attach connection, and the daemon's snapshot attach worker sent every output frame at once. Each frame woke the app's reader thread, its event consumer, its output lane and Ghostty's renderer thread.
Fix, as Chrome throttles a background page's timers (aligned to 1 s), never by delaying anything the person sees:
set-attach-visibility {surface, visible}(capabilityattach-visibility-v1). While a snapshot viewer is hidden, its attach worker keeps the output in one batch and sends it as oneoutputevent on the next whole wall-clock second. Every hidden viewer of the daemon uses the same second, so the app's wakeups coalesce.yesstays bounded), before any snapshot, READY or other frame (stream order), at the end of the stream, and when the viewer is shown again (the command pokes the worker's wait).commands.md), inventory, SDK schema, generated bindings and the api-compat surface are updated.scroll-changed(the newest replaces the older) and sends the newest when the viewer is shown. The app applies noscroll-changedfrom an attach, so these were pure wakeups, once per daemon frame of a printing terminal. Notifications on the attach are never held. The idle digest is re-armed after every batch flush.TerminalAttachment.sendHidden(conceal/reveal of a tab, and a re-attach while hidden) also sendsset-attach-visibilitywhen the daemon has the capability: after the view stops counting toward the grid when it hides, and before it counts again when it shows.DaemonCapability.attachVisibility(optional); the export is updated.What still arrives while hidden (lead's constraint 1)
Showing a hidden terminal (lead's constraint 3)
The app sends
visible: truebefore it counts toward the grid again, and the daemon sends the waiting output at once (the test: within 250 ms of the show, in practice one local round trip). Until that output is parsed, the pane shows the state as of the last batch, at most 1 s old. Because a batch is oneoutputevent, it parses in one step and never draws half. Limit: the waiting bytes are still in the daemon at the show, so they are not parsed in the same main-actor turn as the show.Testing
a_hidden_snapshot_viewer_gets_no_scroll_changed_until_shown(review): red at 428a6bb (step 2b751734): "4 scroll-changed events reached a hidden viewer".server::terminal_snapshot::socket_tests::a_hidden_snapshot_viewer_gets_output_once_a_second_and_at_once_when_shown(Linux, cargo nextest):TerminalAttachHiddenBatchTests(the order of sizing and visibility commands on hide and show, with and without the capability): red 5/5 at d484d49 (step 588f0c77, 3 asserts), green 5/5 at 6f71e0f (step cda860c4). Compiles of both passed.check-spec-inventory.pyandcheck-sdk-schema.pypass;bindings/codegen/generate.py --checkpasses; the api-compat surface is the oneapi-compat-check --writeproduced (step cc256ad6).scroll-changedevents the review found.Observed / Not run
--full), a switch video on this head.Changelog
Proof
No capture: no visible change.