fix(launch): every visible terminal shows its last frame in a reopen's first frame; launch metric is the last pane

Merged
LA
4d ago
+546-45

Summary

After Cmd-Q (keep sessions) and reopen, the window's first frame showed only the first terminal with its last content; every other visible terminal pane was empty for 125-200 ms (REOPEN-F1, REOPEN VIDEO films on cmux24, 5 of 5 cycles; last content of every pane at 377-419 ms after main(), target 300 ms). The app's own mark terminal.launch_frame_shown is once-only, so it reported the first pane and hid this.

Cause. ContentPresentationScheduler makes at most one new surface (or page) per display frame, on purpose (surface creation compiles Metal pipelines on the main thread). A terminal's saved frame from the last quit (TerminalLaunchFrames, read and decoded off the main thread before NSApplication.run) was drawn only on that pane's surface, so each pane after the first stayed alpha-held (LaunchReveal) until its own surface came frames later.

Fix. A pane the scheduler keeps for a later frame shows what it has without a new surface (PresentablePane.showWhileWaiting): a visible terminal shows its saved frame as one layer in its pane (TerminalLaunchFrames.showAhead; no decode on the main thread, O(1) per pane). When its surface comes, showSelected hands the same frame to the session's frame hold in the same transaction (one frame on screen, never two or none). The one-new-surface-per-frame budget stays; nothing waits or is delayed.

Metric (rule: per visible terminal, the reopen number is the max). Each terminal of the launch's first window frame is recorded apart: debug.timings gets launch_terminals ({tab: {asked_ms, frame_shown_ms, missed}}) and launch_terminals_frame_shown_max_ms, and the new launch mark launch.visible_terminals_drawn is the time the LAST pane showed its frame (marked after first_window_frame_committed). terminal.launch_frame_shown stays as the first pane's, for older readers only. bench-startup.py, launch-connect-journey.py and scripts/journeys/ext_restore.py read the max now; the REOPEN VIDEO kit already reads these names.

App lines: the wiring adds 15 (PaneController hook, scheduler call, debug.timings rows, prefetch and close wiring; the logic is in CmuxNextTerminal), and this PR moves TerminalTargetGate (no behavior change) from CmuxNextApp to CmuxNextDaemon, which owns TerminalAttachment.Target (-56). Net app_lines falls; no exception.

Testing

Focused command (fleet): cmux-ci run --class light --script scripts/ci/package-test-lane.sh --arg=suite --arg=Packages/macOS/CmuxNext --arg=ReopenEveryTerminalFirstFrameTests (two PaneControllers come on screen in one pass with saved frames: both must show a frame in the same turn, debug.timings must report each and the max, and the waiting pane's surface must take its frame over).

Observed / Not run

  • Observed: the fleet red and green runs above; prepush gate (guardrails, arch ratchet: app_lines -41).
  • Two earlier heads failed on the fleet and are fixed: d6259739 did not compile (PaneController lacked import CmuxNextTerminal), 3e46c4d6's test read a main-actor constant from a Sendable closure.
  • Not run: a filmed reopen of this build (asked of the REOPEN VIDEO owner through STATUS; the film is the end-result proof); bench-startup.py light and STRESS before/after (perf-hot launch-first-frame); the Home page column, which is a page and still waits for its page (not in scope here).

Changelog

Fixed: After reopening cmux, every visible terminal shows its last content in the first frame, not only the focused one

Proof

No capture: the change is in the first ~200 ms after launch; the REOPEN VIDEO film of the fixed build is the proof (STATUS, REOPEN VIDEO owner).