Add live pane scrollback UI and continuity hardening#12
Open
broady wants to merge 52 commits into
Open
Conversation
Routes live scrollback view state into ratatui pane chrome so mouse-wheel nmux-owned scrollback views display the existing scroll badge. This completes the live mouse-wheel behavior where tracked panes receive wheel mouse input and untracked panes scroll nmux's cached scrollback view.
Preserved scrollback updates can replace the visible surface style table while keeping old scrollback rows. Merge and remap style tables in that path so later scrollback fetches do not reject valid history runs, and paint the scrollback badge on the right pane border where it is visible during nmux-owned scrolling.
Older daemon state can contain scrollback rows whose preserved run style ids no longer exist in the chunk style table. Normalize those historical runs to the default style during scrollback chunk decode so the client can still render and dump the session while keeping live surface validation strict.
Adds an explicit --bug-report-dir/NMUX_BUG_REPORT_DIR path that writes process errors and raw failing server frames only when the user opts in. The captured PaneSurfaceSnapshot report exposed stale style ids in live snapshots, so snapshot decode now falls back to default style for unknown style ids while preserving strict cached-state validation.
Changes wheel scrolling from a viewport-sized tail jump to a bottom-anchored view that moves by a small row step. Replaces the right-border SCROLL label with a scrollbar track, thumb, and clickable arrow cells so scrollback mode has visible position and controls.
Live RTT pong frames share the same transport as scrollback replies, so they can arrive while the redraw client is waiting for a scrollback chunk. Queue interleaved pongs as pending live frames instead of treating them as fatal unexpected envelope bodies.
The first mouse-wheel scroll up previously did nothing when the available history was shorter than the pane viewport, because the client required a scrollback start greater than one. Treat any nonempty history as scrollable so a first wheel notch can show short scrollback instead of appearing dead.
Adds the scrollback redesign as the next feature because the current start-line/chunk-swap approach still feels janky under mouse wheel use. The next implementation should make scrolling a per-client viewport over daemon-owned terminal history instead of a shared session mutation.
Replaces start-line chunk swapping with a per-client offset-from-bottom viewport for live scrollback. Wheel and scrollbar controls now move a local view by small row steps, fetch only the visible history slice, compose that history with the cached live screen, and keep app mouse tracking routed through to the pane.
Composed live scrollback views now carry a structured rendered surface summary instead of falling back to plain text. This keeps history row styles and live-screen row styles visible while remapping live style ids into the merged style table.
Some terminal engines expose tail scrollback that already includes the visible live screen. Normalize the first tail probe by detecting overlap with the cached live surface so composed scrollback views do not append the newest rows twice.
Removes the live-row splice and overlap heuristic from live scrollback rendering. Scroll mode now fetches one contiguous styled range from the daemon transcript and renders it directly, which avoids omitted rows and color mismatches caused by combining daemon scrollback with cached live rows.
Clamps scroll offsets to the actual scrollable range so short transcripts no longer jump into partial top-aligned views, fixes scrollbar math to use the daemon transcript count consistently, and removes the scroll title badge so entering scroll mode changes less chrome around the pane.
Entering scrollback from live mode now moves one row before subsequent wheel events use the normal row step. This reduces the perceived skip when the first physical wheel gesture transitions from live rendering into the daemon transcript viewport.
Adds a 1000-line, 24-row scrollback regression that sends the first wheel-up calculation through the live viewport math and verifies the displayed top line is 1000 - 24 - 3. Also keeps structured default terminal colors unset in the TUI renderer so scrollback does not force palette text to white.
Uses the terminal color state carried by structured surface and scrollback summaries when rendering style-id defaults in the TUI. This avoids hard-coded or host-terminal defaults changing prompt colors when live output is viewed through scrollback.
Carries the existing libghostty style table into main-screen updates so cached scrollback rows keep valid style IDs when later scrollback chunks are fetched. Also renders the scroll thumb as a right-edge half block so the scrollbar stays visually one cell wide on the pane border.
Forces the live frontend to request SGR mouse reports from the outer terminal even when the pane app requested legacy X10/Button/Any tracking, then lets the daemon encode back to the app-owned mouse format. This prevents legacy mouse reports from being split into an Escape UI key and restores wheel scrolling inside fullscreen TUIs such as btop. Also re-extracts libghostty rows for OSC palette color changes so palette-indexed style tables stay current after the full validation gate.
Routes wheel events to pane applications only while the target pane is on the alternate surface. This keeps btop-style fullscreen scrolling working while restoring nmux-owned scrollback for the main shell surface, even if mouse tracking remains enabled or stale after a TUI exits.
When a pane already had the seeded startup scrollback, libghostty updates preserved that short cache instead of extracting backend history once normal shell output exceeded the viewport. Wheel scrollback then looked empty in regular shell sessions even though alternate-screen app wheel forwarding worked. Extract backend history when the cache is too short and route wheel events over pane chrome to the same pane scrollback path.
Always render a one-cell scrollbar rail for panes so scroll affordance is visible before entering scrollback. Add track hit targets that map left-press and button-motion to a client-local scrollback position, and request host button-motion for regular prompt panes so dragging works without handing events to fullscreen apps.
Adds continuity invariants for libghostty main-screen transcript extraction across large output, small chunks, resize, palette changes, alternate-screen transitions, daemon scrollback fetches, and replayed trace events. Replaces the optimistic scrollback cache shortcut with a proven-overlap fast path so cached rows are reused only when the current backend surface overlaps the cached transcript contiguously. Adds debug diagnostics for scrollback fetch version, total, surface rows, and returned range.
Captures raw pane output only when the bug-report directory is explicitly configured, so missing-scrollback reports can be replayed through the terminal engine. The replay test asserts the captured chunks rebuild a contiguous libghostty transcript and that the visible surface remains the transcript tail.
Large PTY reads could advance libghostty's extractable history window before nmux had preserved the earlier rows, leaving only the tail of long listings in pane scrollback. Chunk libghostty writes and splice each proven viewport overlap into nmux's cached transcript so deep output remains contiguous.
The redraw speculative echo path wrote predicted input into whatever SGR state the outer terminal already had active, so typed text could inherit stale prompt, file, or status colors. Reset styles before and after the underlined prediction in both redraw paths so local echo appears as default terminal input.
The speculative echo renderer now resets SGR state around predicted input so local echo cannot inherit stale terminal colors. Update the unit and live CLI expectations that exercise the broader speculative echo path to assert the reset-bracketed sequence.
Live-bottom pane chrome was falling back to the visible viewport height when no local scrollback view was active, so proportional scrollbar math rendered a full-height thumb. Keep the latest daemon-reported scrollback total from fetched chunks and use it for live pane chrome so the thumb stays proportional and bottom anchored.
Live redraw clients need scrollbar metadata even when they skip an initial scrollback fetch and before the user scrolls. Add scrollback version and total-line metadata to surface snapshots and patches, then feed that into live pane chrome so repeated output resizes the thumb immediately. ADR 0049 records the protocol decision.
Cached libghostty scrollback merges now prefer the longest overlap closest to the cached tail. This keeps repeated empty prompt rows from being collapsed when later output, such as an ls burst, arrives after reconnect or live scroll hydration.
Replaces the cached text-overlap scrollback merge with a libghostty tracked-row boundary so capped backend history can be stitched deterministically. This preserves repeated blank rows and deep output without guessing based on row text, and clears the tracked boundary before resize because libghostty tracked refs are not safe across that path.
Treat setup-time framed I/O WouldBlock and timeout errors as stale default daemon signals. This lets bare nmux replace an unresponsive shared daemon and retry instead of surfacing Resource temporarily unavailable as a process error.
Adds an opt-in interrupt report for clients running with --bug-report-dir. SIGINT now writes a small signal-interrupt report before exiting, and raw Ctrl-C input also records the same report shape so interactive hangs can leave inspection data even without a crash.
Unbounded live daemons now wait on listener/client/pty file descriptor readiness instead of waking every 20ms while idle. Bounded and cycle-driven live loops keep the timer so test and finite-client cycle progress is unchanged.
When live output arrives while a pane is scrolled, keep the active scrollback view's total history count in sync and preserve its historical anchor by increasing the bottom offset by newly added rows. This prevents later wheel math from using stale totals and losing freshly produced output.
Replaces the live scroll path's client-side surface/history stitching with daemon-owned viewport intent and viewport snapshot/patch frames. This follows Ghostty's timeline-plus-viewport model closely enough that active, pinned, top, and delta views are all clamped and rendered by the daemon. ADR 0050 records the protocol v2 break and why compatibility with the old split model is intentionally not preserved.
Switches remaining scrollback and surface test paths onto pane viewport intent/snapshot frames and removes the old FlatBuffers bodies from the protocol schema. Also hardens frame reads against partial nonblocking stalls and preserves requested viewport kind so pinned scrollback responses are not mistaken for active updates.
Removes the remaining attach-time known surface compatibility field so protocol v2 negotiates cached pane state through known viewports only. Updates client state plumbing and current protocol docs to match the viewport-only architecture.
Post-input live output was waiting on the long coalescing quiet window even after the surface had already changed, which made simple commands feel like 100-250ms RTT stalls. Use the normal short background quiet window for changed output while keeping the longer coalesce deadline only for waiting on delayed first output, and cover the regression with a notify-fd latency test.
Bare interactive nmux intentionally leaves the shared live-forever daemon running after client detach, which made the latest lag report look like an unkillable process while also producing only a thin signal report. Record live socket, session, pane, surface, scrollback, RTT, FPS, and render/decode stats when raw live stdin sees Ctrl-C so future lag dumps contain enough state to diagnose the active client.
Avoid rebuilding the full libghostty scrollback transcript once live output grows beyond the small-history threshold. This keeps the visible surface current while preserving the existing scrollback snapshot, matching the 60fps policy of dropping history before dropping client frames. Adds a latency-bench case selector so the repeated-output redraw path can be profiled directly.
Keeps daemon scrollback contiguous by treating deep main-screen history as a backing transcript updated with range appends, while visible frames still come from libghostty without extracting full history. Also moves manual bug-report dumps to SIGUSR1 so Ctrl-C remains forwarded terminal input.
Held Enter could flood the live client as a large raw stdin burst or many structured Enter frames before the client drained surface updates, creating backpressure that looked like the TUI exited. Keep Enter raw unless a menu is active and read stdin bytes in small chunks so input bursts interleave with frame reads.
Carries structured row runs beside the daemon-owned scrollback transcript so chunk rebasing no longer strips SGR color from range updates. Also treats a leading CRLF after a prompt as movement to the next command row instead of replacing the prompt with an empty row.
Prevents live redraw scrollback from rendering an unrelated active-surface update as the requested historical viewport. Also fixes live attach status when a client cache version is ahead of the daemon so the client refreshes from a snapshot instead of exiting on a false Current status.
Typing while viewing pinned scrollback now returns the pane to the cached live surface before forwarding keyboard or paste bytes. This prevents held Enter from driving output under a stale viewport and makes the TUI stay in its live alternate-screen view during the stress case.
The live TUI now owns raw mode on the attached terminal more directly and periodically forces a full redraw repair while running. This keeps echoed or line-discipline scroll damage from leaving the alternate-screen UI blank during held Enter after scrolling.
Prioritizes local TTY input and bounds daemon stream draining so held keys cannot starve detach/control handling. Falls back from libghostty-vt resize when the cursor would be outside the target viewport, trading terminal-engine fidelity for keeping the daemon alive during aggressive scroll/input stress.
Spawns the shared default live daemon directly in a new session instead of through a nohup shell job. This keeps the daemon out of the client terminal process group so launch wrappers cannot leave nmux stopped by terminal job-control signals.
Propagates --bug-report-dir into the persistent default daemon and installs the SIGUSR1 report hook for the daemon builtin path. This lets a leftover daemon write a diagnostic report instead of ignoring or dying on SIGUSR1 when the foreground client is already gone.
Keeps partial structured terminal input, including SGR mouse reports, across stdin byte reads instead of forwarding incomplete escape suffixes to the pane. This prevents scroll and mouse bursts from leaking raw control fragments into the shell prompt when a report crosses a read chunk boundary.
After forwarding stdin bytes, prefer daemon surface frames during the post-input grace window so repeated Enter does not starve redraw updates. Treat stale or non-adjacent live surface patches as dropped frames at the client cache boundary instead of exiting the TUI on a patch base mismatch.
Held Enter could trigger repeated full-screen repair clears and leave the live TUI painting stale intermediate surface frames. Remove the input-driven clear path, defer interactive stdin surface paints until the stream catches up, and keep speculative echo in the pane surface cache so split/scrollback redraws see it.
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
--bug-report-dirso missing-scrollback reports can be replayed as regression fixtures.Protocol
Verification
nix develop . -c cargo fmt --checknix develop . -c cargo test -p nmux-cli bug_report --features libghostty-vtnix develop . -c cargo test -p nmux-cli tests::usage_mentions_live_interactive_flags --bin nmuxnix develop . -c cargo test -p nmux-core --features libghostty-vtnix develop . -c just checknix develop . -c just local-smoke