protocol, daemon, client, docs: step the scrollback viewport on the daemon #184

Merged
natsukium merged 1 commit from client/viewport-step into main 2026-10-19 08:53:32 +09:00
Owner

Closes #164.

Problem

Every relative scroll (line, half page, wheel, Home) was an absolute Viewport offset computed from the shadow, which only moves when the daemon's ViewportState lands. A step sent before that reply named a stale offset: End then LineUp could replace the queued End in its coalescing slot (the view never reached live) or undo it, and two quick LineUps landed one row up. The same staleness let typing right after a LineUp skip the snap to live, and passed a bare End to the program after an unacknowledged LineUp.

Change

  • Wire: new ordered arm Input::ViewportStep { rows: sint32 } (minor 2, unreleased, so no bump). The daemon rebases the subscriber's viewport and clamps viewport + rows, as it already resolves JumpPrompt. A minor-1 daemon is never sent it: the window falls back to today's absolute path.
  • Outgoing queue: ViewportStep is ordered and seals the Viewport slot; Key, KeyBytes and Paste now seal it too, so a request queued after a keystroke cannot take the snap-to-live slot ahead of it (that hole also existed on main).
  • Client: steps go out as ViewportStep; End goes through snap_to_live. A per-connection ViewportIntent (does the latest request target live?) gates snap_to_live and the bare-End passthrough; it can only be stale toward "browsing", costing one redundant Viewport(0). Steps are skipped on the alternate screen, where the daemon pins the view live. Search-hit navigation always sends its anchored request instead of skipping when the shadow already shows the hit.
  • felis-json v1: ViewportStep is outside the format (JsonError::Outside), like the fence pair, since a new variant inside v1 is a format break.
  • Docs: ipc.md (arm, queue seals, felis-json, ledger row 2), scrollback.md (why steps are daemon-side; the echoed request sequence from the issue's note is the rejected alternative), control-surfaces.md, CHANGELOG.

Review

Design: three pi/sol rounds (search navigation against a stale shadow, felis-json v1, keystroke seals), then PASS. Implementation: sol PASS; its non-blocking notes were the ViewportState reply wording (fixed) and that the client behavior is tested through the client-core helpers (step_request, ViewportIntent) and the queue rather than through App, which has no test constructor.

Verification

just check green on the rebased branch (an earlier run hit the known undiagnosed flake serve::session_task::tests::a_mirrors_report_is_stored_but_the_owner_still_answers, unrelated). Not driven in a real window.

Closes #164. ## Problem Every relative scroll (line, half page, wheel, Home) was an absolute `Viewport` offset computed from the shadow, which only moves when the daemon's `ViewportState` lands. A step sent before that reply named a stale offset: End then LineUp could replace the queued End in its coalescing slot (the view never reached live) or undo it, and two quick LineUps landed one row up. The same staleness let typing right after a LineUp skip the snap to live, and passed a bare End to the program after an unacknowledged LineUp. ## Change - **Wire**: new ordered arm `Input::ViewportStep { rows: sint32 }` (minor 2, unreleased, so no bump). The daemon rebases the subscriber's viewport and clamps `viewport + rows`, as it already resolves `JumpPrompt`. A minor-1 daemon is never sent it: the window falls back to today's absolute path. - **Outgoing queue**: `ViewportStep` is ordered and seals the `Viewport` slot; `Key`, `KeyBytes` and `Paste` now seal it too, so a request queued after a keystroke cannot take the snap-to-live slot ahead of it (that hole also existed on main). - **Client**: steps go out as `ViewportStep`; End goes through `snap_to_live`. A per-connection `ViewportIntent` (does the latest request target live?) gates `snap_to_live` and the bare-End passthrough; it can only be stale toward "browsing", costing one redundant `Viewport(0)`. Steps are skipped on the alternate screen, where the daemon pins the view live. Search-hit navigation always sends its anchored request instead of skipping when the shadow already shows the hit. - **felis-json v1**: `ViewportStep` is outside the format (`JsonError::Outside`), like the fence pair, since a new variant inside v1 is a format break. - **Docs**: ipc.md (arm, queue seals, felis-json, ledger row 2), scrollback.md (why steps are daemon-side; the echoed request sequence from the issue's note is the rejected alternative), control-surfaces.md, CHANGELOG. ## Review Design: three pi/sol rounds (search navigation against a stale shadow, felis-json v1, keystroke seals), then PASS. Implementation: sol PASS; its non-blocking notes were the `ViewportState` reply wording (fixed) and that the client behavior is tested through the client-core helpers (`step_request`, `ViewportIntent`) and the queue rather than through `App`, which has no test constructor. ## Verification `just check` green on the rebased branch (an earlier run hit the known undiagnosed flake `serve::session_task::tests::a_mirrors_report_is_stored_but_the_owner_still_answers`, unrelated). Not driven in a real window.
protocol, daemon, client, docs: step the scrollback viewport on the daemon
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 9s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 12s
windows / windows docs-only no-op (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Successful in 48s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m12s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 50s
pr / nix flake check (pull_request) Successful in 37s
pr / wire schema is compatible with the base (pull_request) Successful in 2s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 27s
windows / cargo clippy (Windows cross) (pull_request) Successful in 14s
windows / cargo nextest (Windows) (pull_request) Successful in 4m15s
windows / frontend smoke (Windows) (pull_request) Successful in 35s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m36s
48ce21b625
Every relative scroll was an absolute offset computed from the shadow,
which moves only when the daemon's ViewportState lands. A step sent
before that reply named a stale offset: End then LineUp could replace
the queued End in its coalescing slot or undo it, and two quick
LineUps landed one row up.

The window now sends Input::ViewportStep, an ordered arm the daemon
applies to the view it holds, as it already resolves a prompt jump;
an echoed request sequence would need a reply to every request and
still leave a step after a jump with nothing to count from.
Keystrokes seal the viewport slot so a later request cannot take the
snap-to-live queued ahead of them, and the window tracks whether its
latest request targets live so typing right after a step still snaps.

Closes #164

Assisted-by: Claude Code
natsukium deleted branch client/viewport-step 2026-10-19 08:53:32 +09:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
felis-terminal/felis!184
No description provided.