docs/superpowers/plans/2026-09-06-native-wheel-scrolling.md
Ref: Size: 16.6 KiB History
# Native parity — wheel scrolling Status: implemented and independently reviewed; CI and functional checks passed. Final NVIDIA timing check passed; earlier timing misses remain documented. User acceptance is pending. Authorized by the user's next-sprint continuation on 2026-09-06. Opening architecture cleanup and visible-text selection are implemented and validated; whole text-selection demo acceptance remains pending. Continue in the existing parity worktree on branch `gui-text-selection`. This is the first functional slice of git-collab issue `8b16e26b`. ## Sprint goal Scroll the pane under the pointer with the wheel: shell history at a prompt, arrow input in an alternate-screen application without mouse reporting, and terminal mouse reports when an application requests them. Match existing TUI behavior while keeping each pane independent. ## Order within the overall mouse feature After visible-text selection, this wheel slice is the next planned sprint item. Application click/drag forwarding and application clipboard writes follow it. That slice includes Shift+drag as the required native-selection override; see the [application mouse acceptance criteria](2026-09-06-native-text-selection.md#required-follow-up-application-mouse-and-clipboard-behavior). Wheel delivery alone does not complete the overall mouse feature. Preserve native selection and copy throughout these slices; GUI paste remains deferred. ## Observable acceptance - Three panes on two daemons, including off-origin and unfocused panes. Wheel input affects only the pane under the pointer and does not move keyboard focus. - At a shell prompt, each whole notch moves three history rows, with bounds at the oldest retained history and live output. Return to live output restores the cursor and ongoing updates. Typing returns the receiving pane to live view, matching the TUI. Fractional wheel events accumulate per pane without leaking into another pane; honor SDL's flipped direction flag. - An alternate-screen app without mouse reporting receives the same arrow-key behavior as the TUI, including application cursor-key mode. A mouse-reporting app receives wheel reports with correct pane-relative terminal coordinates and negotiated encoding. Verify the actual PTY bytes independently. - Picker/recovery modals and active divider resizing do not leak wheel input to underlying sessions. Headers, dividers and empty workspace do not scroll a neighbouring pane accidentally. - Delayed history replies cannot repaint live mode, a replacement attachment or a resized viewport. Reconnect, detach and End retain bounded cleanup. The authoritative live replica stays independent of the displayed history view. - Demonstrate real NVIDIA Wayland wheel input at 200% and across the retained scale transitions; verify other panes, focus, PTY sizes and shell identities. Visible-text selection and clipboard copy are delivered by the separate text-selection sprint; wheel work must preserve them. Clipboard paste, general app click/drag forwarding, ligatures, font fallback and terminal-wall retirement remain outside this wheel slice. Horizontal wheel behavior is outside this initial vertical scrolling slice. ## Opening architecture findings and ownership ### Reuse and ownership map | Behavior or rule | Existing implementation and callers | Intended owner | Reuse, refactor or add; what can be deleted | | --- | --- | --- | --- | | SDL coordinates and event injection | `frame.physicalPoint`, ordinary pointer events and `parseHook` | SDL adapter | Extend with wheel events; no separate test-only scrolling path | | Pane targeting, modal exclusion, selection cancellation | `interaction.Controller` pointer handlers and shared `client.selection.Drag` | Window-free controller | Reuse content hit testing and attachment checks; per-pane fractional remainder | | History positioning and row decoding | `Replica.scrollStart`, protocol scrollback request/chunk codecs; TUI and wasm consumers | Shared client pump | Reuse existing wire and grid rows; add bounded request/result ownership without a second replica | | Displayed view and selection coordinates | `runtime.Live.capture`, `SelectionVersion`, controller selection range | Pump/runtime snapshot boundary | Keep live replica independent; snapshot carries the displayed history origin and freshness | | Alternate-screen arrow encoding | `keymap.encode`, TUI `sendAltScroll` | Shared client input policy | Reuse key encoding; keep frozen TUI behavior unchanged | | Application wheel ownership and encoding | `TermModes.appMouse`, `mouse_modes`; TUI forwards existing terminal bytes | Pump and pure client encoder | Decide using admitted mode frames; SDL supplies semantic intent, so a terminal byte parser is unnecessary | | Real input and independent acceptance | `SelectionRig`, `LifecycleRig`, `wayland_pointer.Pointer` | Retained integration fixtures | Extend established plural-pane fixtures and virtual pointer axis input | The earlier font-settings cleanup already supplies the useful opening refactor; the current inspection found no additional cleanup needed before this feature. Application mouse encodings are checked against the [xterm mouse protocol](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking). Selection stays attached to its source text when the viewport moves, including scrolling offscreen and back or returning through live view. A held drag keeps its original text anchor and extends using the current pane coordinates. Live redraws preserve the selected range; geometry, screen/connection and history-watermark changes still invalidate it. Cached history keeps exact source freshness. These are part of wheel acceptance, not later polish. `frame.zig` owns SDL events and logical-to-framebuffer conversion through `physicalPoint`. `interaction.Controller` owns pane hit-testing and modal policy; keep these decisions window-free so native-core tests can cover them. `runtime.Live.capture` currently copies the pump's live grid only. The GUI runtime owns the chosen pane view and transfers independent snapshots to the painter under the existing pump lock. Keep observation hooks passive; assess extracting touched hooks without turning that into an unrelated refactor. `client.session_pump` currently has no scrollback mailbox request or chunk handling. The wire already has `encodeScrollbackReq`, `scrollback_chunk` and server extraction. `Replica.history_rows` and `scrollStart` supply history positioning; `wasm_core` and the TUI provide existing reply-decoding examples. Do not add a second wire protocol or let GUI code access the daemon engine. Review asynchronous request correlation and live/history snapshot ownership before implementing the smallest required shared-client extension. `ClientCore.terminal_modes`, `TermModes.appMouse`, alternate-screen and cursor-key flags supply app routing policy. SDL already supplies wheel deltas: the TUI's private `MouseFilter.wheelNotches` parses encoded terminal input and is not a ready-made SDL helper. Share semantic encoding where justified; keep the frozen terminal wall behavior unchanged. Assign Luna the bounded implementation and Terra adversarial review through the repository sprint skill. The sprint lead owns shared-client integration, real oracles, validation, recording, webpage, cleanup and commits. Coordinate any shared wire/lifecycle meaning change across clients. ## Validation and handoff Follow `docs/native-sprint-workflow.md`: opening and closing cleanup, focused client/core tests, real plural-pane wheel integration, full CI/native gates and isolated NVIDIA stress. Build both binaries in ReleaseSafe. Preserve earlier NVIDIA timing failures and the offscreen-growth limitation; neither is waived. Finish with a localhost review webpage containing achievements, an actual GUI video, controls, evidence and limits. Record user demo approval separately from checks. Publish only if explicitly requested; preserve unrelated services. ## Architecture review before implementation The user asked explicitly for duplication, hexagonal architecture opportunities and refactoring. Review decisions: - Give font settings one owner in `font_options.zig`; both CLI and config parsing call its point-size policy. Entry adapters retain their diagnostic mapping, and the font backend retains installed-face validation. Sharing a helper from `config.zig` alone would remove duplication but put domain policy in an input adapter. - Move font settings and config/theme source ownership to `native_core`, with public aliases in `native`. The renderer consumes theme values; window/font libraries are not needed to test config parsing, precedence and theme derivation. - Preserve the existing SDL → controller → runtime → session-pump boundary. Add semantic wheel intent at the transport mailbox and interpret it using the pump-owned terminal modes, after consuming available frames. Reading modes in the GUI and queueing pre-encoded input could race an incoming mode change. Verify the ordering with real mode frames and independently observed PTY bytes. - Keep live/history snapshots behind the pump/runtime boundary. Wheel work should narrow snapshot access rather than introduce new GUI reads of pump internals; request correlation and view ownership belong in its implementation contract. - Do not copy the TUI interaction core or mouse byte parser into the GUI. Reuse protocol/replica behavior and share only pure semantic encoding when an actual second caller needs it. The frozen wall keeps its behavior. - Extend the existing test input adapter with a real SDL wheel event. A separate test-only scroll path would leave actual event routing untested. Broader hook extraction is a later package unless this slice exposes a concrete need. Config and theme line readers look similar but intentionally differ: unknown config keys are fatal, unknown theme keys warn, missing config is allowed, and a missing selected theme is fatal. Their shared color/palette parsing is already centralized. A generic parser framework or filesystem interface is not justified by the current callers; retain the existing pure parse/merge functions and small file-loading adapters. ### Opening cleanup evidence `font_options.zig` now owns the point-size parser and validity rule. Config and CLI adapters preserve their existing errors; installed-face checks remain with the font backend. The native core owns font settings, config and theme, and the native rendering module retains public aliases. Removed the redundant empty family check after the text parser had already rejected empty values. Independent review found no behavior/API issue; it caught and removed a stale point-size comment left above palette parsing. The module split is checked by actual dependency probes: with `PKG_CONFIG_LIBDIR` set to an empty directory and `PKG_CONFIG_PATH` empty, `native-core-test` passed all 29 tests, while the full native build failed on missing SDL3 metadata as expected. This is a build boundary check, not merely a directory rename. Logs are in `dist/wheel-opening-cleanup/`. Native build, native unit/core tests and formatting passed. Full CI (check, terminal integration, agent, throughput) and final native integration passed with no gate changes. The first native integration launch overlapped CI's temporary source-ban probe and stopped before compiling; the retry started after the probe was removed. The original failed log is retained, and the workflow now calls out this build-serialization requirement. This refactor preserves appearance behavior and uses the approved appearance recordings as its visual baseline; no new visible feature or performance claim is made. The following functional wheel delivery supplies its own real-input recording and local review page. ## Wheel implementation and closing review `frame` translates SDL wheel coordinates and modifiers into controller input. The controller excludes non-content regions and modes, retains fractional notches by pane attachment, and passes semantic intent through `Runtime.wheel`. The pump drains available mode frames before interpreting queued wheel input. Existing keyboard/resize work before a deferred wheel can run between bounded receive batches; the wheel and following messages retain FIFO order. Shell scrolling uses the existing scrollback request and grid decoder. The pump owns one optional history grid and one outstanding request; its cancelled request remains a tombstone until the reply is drained. New transport attachment clears that tombstone, but same-wire resync retains it. A two-second timeout reconnects rather than risking correlation with a later reply. View origin and grid are captured under the same lock; all selection hit/paint coordinates use that origin. The live replica remains untouched by history decoding. The selected distance from live stays constant as output arrives. Output or mode changes invalidate copying and schedule a new history fetch. The previous view can remain painted while a reply is pending, but cannot be copied as if current. This handles ring pruning even when the wire's history-row count remains flat. Returning to live, resizing, reconnecting and replacing an attachment invalidate the old view. As before, the wire does not provide an atomic source-version precondition at the daemon. Pure wheel encoding lives in `keymap`, reusing its arrow encoder and Zig's UTF-8 encoder. SGR pixels, SGR cells, URXVT, UTF-8 and legacy X10 are supported with pane-relative coordinates. No runtime dependency, protocol message or terminal wall feature was added. Closing cleanup removed the hand-written UTF-8 helper and the old duplicated receive-loop body. Independent review checked ownership, request cancellation, pending input-buffer transfer, coordinate bounds and same-wire resync. The existing QUIC batching regression caught a receive-loop change that delayed mailbox work; the final implementation preserves its 64-frame batches and passes that regression. A continuously readable stream can delay a wheel waiting for its pending mode frames and the input ordered after that wheel. A continuously changing history can likewise postpone a safe selectable refresh. The next shared-client scheduling change owns investigating a finite protocol ordering boundary if this is encountered; dropping input or guessing a newer mode is not the fix. ### Delivery evidence Final CI, client/native tests, full native integration, real Wayland wheel and clipboard acceptance, and desktop/mobile local video playback/seek checks passed. The final isolated NVIDIA run passed with frame p99 18.745 ms and sampled input-to-painted upper bound 65.3 ms (5 ms polling). Earlier current/baseline runs missed the frame gate; retained evidence does not establish the cause. The continuous recording is 27 seconds with real Wayland pointer/wheel input, three panes on two daemons, a separate clipboard reader and real less. Local review page: http://127.0.0.1:18776/wheel-scrolling/ . The loopback server is recorded in `dist/wheel-scrolling/server.json`; stop it after acceptance. No remote publication is required or pending. Publishing is opt-in at the user's request. Sources, recording and all validation logs remain in the worktree. ### Hands-on feedback: keep the highlight while scrolling The user expects a completed selection to remain attached to its source text, including scrolling it offscreen and back. `Pump` now preserves selection freshness through local viewport requests, history replies and return-to-live. Input explicitly invalidates selection at the mailbox boundary; terminal output, modes, geometry and attachment changes retain their existing guards. Existing absolute selection rows and `Live.view_origin` already provide correct painting and hit testing, so no new production state or abstraction is needed. A held drag retains its original anchor; subsequent motion or release resolves its endpoint against the current viewport. The real Wayland regression covers both a release on the moved source row and extending across newly visible rows. The user requested hands-on review instead of a new demo recording for this fix. Client/native units, the full native integration suite and real NVIDIA Wayland wheel/copy regressions passed. The repository check gate passed; full CI stopped on an existing nested-agent exit timeout, which passed on targeted rerun without changes. Full-run CI success is therefore not claimed for this feedback commit. Evidence: `dist/wheel-scrolling/selection-preserve-*.log`; details in `RETRO.md`. Active-output follow-up: live selection now survives counter/redraw frames, following Ghostty; a cached-history origin remains strict even after returning live. See the text-selection plan's active-output entry for semantics and limits.