Skip to content

feat: add pinned preview - #137

Merged
smarzban merged 31 commits into
mainfrom
feat/pinned-preview-build
Aug 12, 2026
Merged

feat: add pinned preview#137
smarzban merged 31 commits into
mainfrom
feat/pinned-preview-build

Conversation

@smarzban

@smarzban smarzban commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Summary

Adds one session-only pinned reference beside the tree-driven active preview. A pin freezes the
currently displayed file view and its viewport/search state, while the active side continues to
follow tree selection. The feature supports independent focus, scrolling, search, captured-path
copy, responsive three-region/single-region layouts, keyboard and mouse divider resizing, and
worktree-safe pin identity.

Acceptance criteria

  • AC-1–AC-13: snapshot fidelity, lifecycle, immutable origin identity, and session-only lifetime.
  • AC-14–AC-25: responsive split layout, focus cycling, and bounded keyboard/mouse resizing.
  • AC-26–AC-29: independent interaction plus captured relative/absolute path copy.
  • AC-31–AC-32: unavailable pinned actions are effect-free and explained.
  • AC-33–AC-35: default/custom pin binding and effective-binding help.
  • AC-36–AC-39: no-pin compatibility plus end-to-end file and Git read-only safety.
  • AC-40: tracked usage, key, configuration, architecture, and changelog documentation.

AC-30 was withdrawn before build: displayed-text selection/copy from pinned focus is deliberately
out of scope; captured-path copy remains covered by AC-28 and AC-29.

Coverage

Task Criteria advanced
T-1 AC-1, AC-2, AC-3, AC-6, AC-7, AC-9
T-2 AC-2, AC-3, AC-26, AC-27, AC-36
T-3 AC-1, AC-8, AC-10, AC-36, AC-37
T-4 AC-14, AC-16, AC-18, AC-19, AC-22, AC-25, AC-36
T-5 AC-12, AC-14, AC-18, AC-19, AC-25, AC-36
T-6 AC-1 through AC-10, AC-19, AC-23
T-13 AC-4, AC-5, AC-6, AC-7, AC-11, AC-24, AC-33, AC-34, AC-35
T-7 AC-15, AC-16, AC-17, AC-26, AC-27, AC-31, AC-32, AC-36
T-8 AC-28, AC-29
T-9 AC-20, AC-22, AC-23, AC-36
T-10 AC-21, AC-22, AC-26
T-11 AC-13, AC-36, AC-38, AC-39
T-12 AC-40

Verification

Fresh ship-boundary verification at 9e181f8 passed:

  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test (including the pinned-preview E2E)
  • cargo build --release
  • cargo audit (the repository's already-allowed RUSTSEC-2026-0190 warning for anyhow 1.0.102)

Agent SDLC corroboration passed: sdlc-check 0.18.0: all checks passed — 0 findings, 0 notes.

Criterion Type Proof
AC-1 test-backed preview::tests::preview_document_equality_covers_every_captured_value
AC-2 test-backed preview::tests::cloned_preview_interaction_state_is_equal_then_mutates_independently
AC-3 test-backed preview::tests::cloned_preview_interaction_state_is_equal_then_mutates_independently
AC-4 test-backed pinned_preview::rejected_pin_attempts_leave_no_file_and_directory_targets_explained
AC-5 test-backed pinned_preview::rejected_pin_attempts_leave_no_file_and_directory_targets_explained
AC-6 test-backed preview::tests::the_same_relative_path_in_two_roots_has_a_different_preview_identity
AC-7 test-backed pinned_preview::pin_lifecycle_clones_the_settled_preview_and_toggles_the_same_identity
AC-8 test-backed unpinned_navigation_projects_the_existing_active_interaction
AC-9 test-backed preview::tests::preview_document_equality_covers_every_captured_value
AC-10 test-backed a_width_reflow_preserves_scroll_and_recomputes_a_committed_search
AC-11 test-backed reroot::pin_and_ratio_survive_re_root
AC-12 test-backed presenter_pinned::pinned_origin_identity_is_visible_and_neutralized
AC-13 test-backed e2e_pinned_preview::pinned_preview_journey_leaves_files_and_git_unchanged
AC-14 test-backed preview_layout::tests::pin_split_floor_cannot_be_bypassed_by_ratio
AC-15 test-backed pinned_preview::pin_focus_cycles_and_removal_returns_focus_to_active
AC-16 test-backed preview_layout::tests::pin_present_tiny_bodies_suppress_structural_regions
AC-17 test-backed pinned_preview::pin_focus_cycles_and_removal_returns_focus_to_active
AC-18 test-backed presenter_pinned::wide_three_region
AC-19 test-backed preview_layout::tests::equal_preview_split_rounds_by_at_most_one_column
AC-20 test-backed pinned_preview::preview_ratio_resizes_and_clamps
AC-21 test-backed pinned_preview::mouse_routes_pinned_scroll_and_preview_divider_drag_without_touching_active
AC-22 test-backed pinned_preview::preview_ratio_resizes_and_clamps
AC-23 test-backed pinned_preview::preview_ratio_resizes_and_clamps
AC-24 test-backed reroot::pin_and_ratio_survive_re_root
AC-25 test-backed preview_layout::tests::preview_ratio_is_clamped_and_preserved_across_widths
AC-26 test-backed pinned_preview::pinned_scroll_and_search_are_independent
AC-27 test-backed pinned_preview::pinned_scroll_and_search_are_independent
AC-28 test-backed pinned_preview::pinned_path_copy_uses_captured_origin
AC-29 test-backed pinned_preview::pinned_path_copy_uses_captured_origin
AC-31 test-backed pinned_preview::pinned_unavailable_actions_are_consumed_with_a_notice
AC-32 test-backed pinned_preview::pinned_unavailable_actions_are_consumed_with_a_notice
AC-33 test-backed input::tests::pin_and_preview_resize_default_and_custom_bindings_decode
AC-34 test-backed input::tests::pin_and_preview_resize_default_and_custom_bindings_decode
AC-35 test-backed help::tests::keybindings_text_reports_the_effective_pin_preview_binding
AC-36 test-backed unpinned_navigation_projects_the_existing_active_interaction
AC-37 test-backed no_pin_navigation_does_not_add_content_provider_work
AC-38 test-backed e2e_pinned_preview::pinned_preview_journey_leaves_files_and_git_unchanged
AC-39 test-backed e2e_pinned_preview::pinned_preview_journey_leaves_files_and_git_unchanged
AC-40 test-backed docs_consistency::pinned_preview_documentation_covers_ac40

Provenance

The run entered at the brief stage from the owner prompt (ingested 2026-07-30); acceptance criteria,
design, stack, and plan were produced in this run. The only mid-build amendment added the necessary
internal src/controller/pinned.rs accessors to T-7; it changed no scope, criterion, component, or
task ordering. No links are untraced.

The full local spec and task ledger live under gitignored docs/specs/; this PR body publishes the
acceptance contract, coverage, and proof map for review.

smarzban added 26 commits August 2, 2026 18:29
… AC-4, AC-5, AC-6, AC-7, AC-8, AC-9, AC-10, AC-19, AC-23
…AC-6, AC-7, AC-11, AC-24, AC-33, AC-34, AC-35
Keep the finder because the narrow PTY can hide an active tree selection, while its unique UNPIN match preserves the active-preview assertion. Remove its state-dependent z toggle and the help-overlay barrier, then dismiss the committed search and send two q keys in one write so either finder zoom polarity exits cleanly.
The panic fix was right, removing TreeScrollRight from unavailable_from_pinned was
not: the plan (T-7) requires rejecting the pinned L arm with the standard
pinned-action notice, and AC-32 requires an unavailable action to produce a
visible notice. Restore the policy entry and its AC-31/AC-32 assertion, and keep
Effects::noop() in the match arm purely as a defensive net so a future policy
change cannot turn a keypress into a process abort.
The coverage pass replaced a 250ms wall-clock poll with an immediate provider-count
read, which is deterministic but weaker: dispatch_render runs the provider on a
spawned thread, so an immediate count read can pass before a wrongly dispatched
render increments it. Expose Controller::render_seq (bumped synchronously inside
dispatch_render, before the worker spawns) and assert on that, with the provider
count as corroboration. Verified by injecting a dispatch_render into the pin path:
the oracle fails, as it must.
From pinned focus, q/Esc quit past the active preview's still-highlighted
search (and vice versa): the close layer only consulted the focused
preview. Dismiss the focused preview's search first, then the other
preview's while its pane is on screen; a hidden pane's stale search no
longer costs a keypress that visibly changes nothing.
scroll_pinned_h and scroll_pinned_to_line cloned every content line per
event to dodge a two-field borrow; borrow through the snapshot instead
(the pattern the pinned search already uses). set_pinned_viewport gains
the same unchanged-size early return as its active twin, so idle draws
stop re-walking the frozen document.
The Presenter draws only the active/pinned projections; the thirteen
alias fields were read by no src code yet cost a second full deep clone
of the rendered content on every frame, pin or no pin. Tests migrate to
the projection fields (and to the Controller accessors where they were
probing controller state, not the draw model). Every pre-existing
snapshot is byte-identical.
PreviewDocument and PreviewProjection now hold Arc<Text>, so building
the per-frame active and pinned projections is a pointer bump instead of
a deep copy of every styled line, and a wrap/view presentation change
re-shares the lines instead of re-copying them. The one remaining
draw-side copy (Paragraph needs an owned Text) is documented in place.
docs/usage.md listed 11 unavailable-from-pinned keys while the policy
rejects 12 — add L. The new divider test asserts interior columns map
proportionally (including one step inside each clamp bound), so a
snap-to-extremes or wrong-origin mapping fails instead of hiding behind
the 20/80 endpoints.
smarzban added a commit that referenced this pull request Aug 4, 2026
…e tells

The last outstanding case from the review. Three tests proved "no render was
dispatched" by sleeping 50ms and hoping a wrong render would have landed by then;
two more slept 50ms to give a superseded render a chance to arrive.

- Adds Controller::render_seq (a read-only accessor; both dispatch_render and
  dispatch_reflow bump latest_seq synchronously BEFORE spawning the worker), and
  the three absence tests now assert an unchanged seq. That is a real proof, not
  a race: verified by dropping the `width_changed` gate so a height-only change
  reflows — the test fails.
- The two superseded-render tests wait on an in-flight counter reaching zero
  instead of a fixed sleep, so they wait for the worker actually finishing rather
  than guessing how long that takes.

HONEST LIMIT, recorded in the code and in AGENTS.md rather than glossed: making
that wait deterministic does not make the detection reliable. Renders run a
thread each and the polling loop drains earlier results while waiting for the
newest, so perturbing poll's `seq == latest_seq` guard does NOT fail those two
tests. They remain end-to-end sanity checks; the guard's real proof would be a
unit test feeding poll a stale-seq result directly. Better to write the limit
down than to leave a future reader trusting them past it.

No 50ms blind sleeps remain in the file.

Note for PR #137: it adds an identical render_seq on its branch, so it will need
its copy dropped when it rebases onto main.
Brings v1.15.0, the agent-skill --cwd fix (#141) and the determinism work (#142)
onto the branch. Four resolutions worth naming:

- src/controller/mod.rs: both sides had added `render_seq` at different offsets,
  so git merged BOTH copies silently rather than conflicting. Dropped this
  branch's earlier copy and kept main's, whose doc describes the real single
  ordered worker (this branch's said "before it spawns the worker", which was
  wrong — there is one long-lived worker, not a thread per render).
- CHANGELOG.md: the merge landed the pinned-preview entry inside the RELEASED
  1.15.0 section, which would have claimed an unshipped feature was in that
  release. The repo's own pinned_preview_docs_cover_the_frozen_reference_contract
  guard caught it; moved to Unreleased. The advisory-notices entry stays deleted
  per the release decision.
- AGENTS.md: took main's wording in both hunks — it replaced the two facts this
  branch had merely bumped (Intent::ALL's count, the ratatui patch version) with
  an instruction to read them from the source.
- tests/docs_consistency.rs: kept both sides' additions (this branch's
  ARCHITECTURE const and main's --cwd drift guard).

Green after merge: cargo test, fmt, clippy.
…e away

Applies the 2026-08-09 spec amendment (AC-12, AC-14 to AC-18, AC-22 amended;
AC-41, AC-42 added).

- Order is now tree | active | pinned, so the preview that tracks the cursor
  sits beside the tree and the frozen snapshot is parked at the edge. The
  session ratio still names the pinned share, now measured from the right.
- Pinning never reflows or collapses. Below the 40-column preview floor the
  geometry is identical to the same session with no pin: the pin is simply not
  drawn, and a persistent "Pinned: <path> - widen to view" notice sits on the
  active preview. The held pin survives at full fidelity and reappears when
  there is room. Previously `p` at any ordinary width left only the tree.
- Focus cycles tree, active, pinned, and skips an undrawn pin; resizing is
  inert while the pin is hidden.
- The pinned title shows the captured branch and root only. The path led the
  title and clipped the origin away on a narrow pane, defeating AC-12's whole
  purpose; it stays available through the widen notice and `y`/`Y`.
- A finder confirm from pinned focus moves focus to the active preview, where
  the file opened. Changed-file jumps still keep pinned focus per AC-31.
- Replacing a pin because the branch changed now names both branch states
  instead of silently turning `p` from unpin into replace.
Amends AC-12 again. `Pinned: [main]` for a pin from the worktree you are
viewing, `Pinned: [main @ other-checkout]` when it came from another one.
Repeating the current worktree's own name distinguished nothing and cost the
width that clipped the origin away in the first place.

The comparison is over whole root paths, decided in the controller where both
are known, never over basenames: two worktrees of one repo routinely share a
basename, and comparing those would report a foreign pin as local, which is
exactly the confusion AC-12 exists to prevent.

Intended consequence, recorded in the spec: a worktree switch leaves the frozen
snapshot untouched (AC-11) but does change whether its title names a worktree,
because what it has to distinguish itself from has changed. tests/reroot.rs
pins that behaviour.
… search prompt open

poll() routed an open Search prompt to refresh_search unconditionally, but
refresh_search dispatches by prompt_target: a prompt opened on the pinned preview
recomputed only the frozen pinned matches and skipped the is_reflow branch, leaving
the active preview's committed matches computed against pre-reflow rendered lines
(wrong highlights, n/N jumping to stale rows). The active side now takes the
promptless rule whenever the prompt belongs to the pinned side.
pi-empanel writes run artifacts here. They are machine-local and should
not ship with the plugin.
@smarzban
smarzban merged commit 8a3efa1 into main Aug 12, 2026
7 checks passed
@smarzban
smarzban deleted the feat/pinned-preview-build branch August 12, 2026 21:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant