Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
26acf83
feat(T-1): define preview values — advances AC-1, AC-2, AC-3, AC-6, A…
smarzban Aug 2, 2026
f14aa12
feat(T-2): move active interaction state — advances AC-2, AC-3, AC-26…
smarzban Aug 2, 2026
b69e6ea
feat(T-3): apply preview documents — advances AC-1, AC-8, AC-10, AC-3…
smarzban Aug 2, 2026
0b9525c
feat(T-4): add split-preview layout policy — advances AC-14, AC-16, A…
smarzban Aug 2, 2026
8a66fc5
feat(T-5): render pinned and active preview projections — advances AC…
smarzban Aug 2, 2026
0a4a071
feat(T-6): add pinned snapshot lifecycle — advances AC-1, AC-2, AC-3,…
smarzban Aug 2, 2026
84edb73
feat(T-13): wire pin intent and re-root state — advances AC-4, AC-5, …
smarzban Aug 3, 2026
ae307e6
feat(T-7): route pinned focus and actions — advances AC-15, AC-16, AC…
smarzban Aug 3, 2026
8592b85
feat(T-8): copy captured pinned paths — advances AC-28, AC-29
smarzban Aug 3, 2026
d16d2ad
feat(T-9): resize preview divider by keyboard — advances AC-20, AC-22…
smarzban Aug 3, 2026
b594846
feat(T-10): route pinned preview mouse input — advances AC-21, AC-22,…
smarzban Aug 3, 2026
18d2500
test(T-11): cover pinned-preview session safety — advances AC-13, AC-…
smarzban Aug 3, 2026
9e181f8
docs(T-12): document pinned preview — advances AC-40
smarzban Aug 3, 2026
5c1dea6
fix(test): synchronize pinned preview dismissal before quit
smarzban Aug 3, 2026
6822c30
test: make pinned-preview quit zoom-state independent
smarzban Aug 3, 2026
36a6d6b
fix: make pinned L inert
smarzban Aug 3, 2026
d8fc0d6
perf: avoid cloning pinned search lines
smarzban Aug 3, 2026
fed2aa1
fix: reset hidden pinned viewport
smarzban Aug 3, 2026
d4dc77d
fix: keep pinned L rejected by policy, keep the arm defensively inert
smarzban Aug 3, 2026
a0221ca
test: close pinned preview coverage gaps
smarzban Aug 3, 2026
2f9c229
test: make the no-render oracle race-free via a synchronous render seq
smarzban Aug 3, 2026
306ea42
fix: dismiss any visible committed search before close
smarzban Aug 3, 2026
b9bf5f4
perf: borrow the pinned document on scroll and viewport paths
smarzban Aug 3, 2026
d7214d7
perf: drop the dead ViewState compatibility aliases
smarzban Aug 3, 2026
3cc422e
perf: share preview content behind an Arc
smarzban Aug 3, 2026
64628ee
docs+test: document pinned L, pin the divider's proportional mapping
smarzban Aug 3, 2026
0e96c9a
Merge origin/main into feat/pinned-preview-build
smarzban Aug 4, 2026
332da40
feat(pinned-preview): pin on the right, and pinning never takes a pan…
smarzban Aug 9, 2026
f2fdaed
feat(pinned-preview): name the pin's worktree only when it differs
smarzban Aug 9, 2026
738fd84
fix: recompute the active search on a reflow that lands with a pinned…
smarzban Aug 12, 2026
c4eba2c
chore: ignore local .empanel review records
smarzban Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
# Review-gate run logs / dismissal records: local audit trail, not part of the plugin
.review-gate/

# pi-empanel run records: local review artifacts, not part of the plugin
.empanel/

# Private, per-working-copy agent overlay: never committed (see AGENTS.md routing guideline)
AGENTS.local.md

Expand Down
7 changes: 5 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ is unit-testable with stubs.
| `worktree` | Enumerate the repo's git worktrees (`git worktree list --porcelain`) and overlay herdr's agent-active workspace + per-row agent status, feeding the switch-worktree picker. |
| `tree` | The rooted, `.gitignore`-aware file tree: filters (gitignored, changed-only, hidden/dotfiles), cursor, expansion, status markers, and the `]` / `[` changed-file jump. Optionally folds a chain of single-child directories into one row (`compact_dirs`). A folded row has to look inside a **collapsed** directory, which the tree never opens otherwise, so foldability is answered by a two-entry probe rather than a listing and the answer is memoized — re-probed wherever the controller re-reads git. Listings stay uncached, so a compacted frame reads exactly the directories an uncompacted one does. |
| `view_policy` | A pure decision: which view mode a file gets (changed → diff, markdown → rendered, else → syntax content) and the cycle order. |
| `preview` | Shared active/pinned preview values: an immutable applied document with captured origin identity, plus separate mutable interaction state (viewport, scroll, search, and paging) for each displayed preview. |
| `preview_layout` | Pure responsive geometry for the tree, active preview, and pinned preview. It keeps the preview divider distinct from the tree divider, applies the 20–80% pinned share from the right edge, and signals when a pin misses its 40-column floor so the no-pin tree/active geometry remains intact. |
| `focus_policy` | Pure focus cycle and action-target policy. With a visible pin it routes `Tab` through tree → active → pinned, skips an undrawn pin, gives pinned in-file navigation its own target, and rejects live-selection actions from the frozen reference. |
| `render` | Produce the content-pane text: classify the file, delegate styling to an external CLI, and **neutralize escape sequences** before display. |
| `presenter` | Draw the two-column (or zoomed / narrow) layout with ratatui, including persistent annotation markers and background-only styling; source-line backgrounds are applied beneath active line-select, ambient-selection, and search overlays, with a bounded one-cell cue for blank annotated lines. Scroll the tree/content and report viewport + pane geometry back for hit-testing. |
| `presenter` | Draw the two-column (or zoomed / narrow) layout with ratatui, including persistent annotation markers and background-only styling; source-line backgrounds are applied beneath active line-select, ambient-selection, and search overlays, with a bounded one-cell cue for blank annotated lines. Draw active and pinned preview projections through the same rendering path, surface the persistent hidden-pin widen notice on active, and report their independent viewport, scrollbar, and divider geometry back for hit-testing. |
| `picker` | The modal worktree-switcher overlay state (rows, cursor, horizontal scroll) drawn over the layout; captures its own nav / confirm / cancel keys while open. |
| `proc` | Shared subprocess reaping: one `wait_bounded` (child wait + poll + timeout-kill) used by both the content renderer and the update check, so the timeout-kill semantics are defined once. |
| `finder` | The modal go-to-file finder overlay state (query, ranked matches, cursor, scroll) drawn over the layout; captures its own keys while open and navigates the tree selection on confirm. |
Expand All @@ -48,7 +51,7 @@ is unit-testable with stubs.
| `help` | Help overlay state: the embedded changelog source and About text, plus the section and vertical scroll position for the `?` overlay; also formats the display-only **Settings** section (`settings_text`) showing the config's effective values and load outcome, and the display-only **Keybindings** section (`keybindings_text`) listing every action's effective key(s) and description, marking customized bindings and surfacing any ignored `[keys]` entries (view-only, never an editor for the config file). Pure; no I/O: the changelog is compiled in at build time. |
| `input` | The keybinding registry (the single source of truth for each global action's intent name, default key(s), and description), the key-spec parser (a bindable-key whitelist, no `Ctrl`/`Alt`), the bindings resolver (layers a user's `[keys]` config over the registry into the effective key → intent map, precedence `config > default`, with an `Esc`-always-closes floor), and the pure key → intent dispatcher that decodes crossterm events against those effective bindings. |
| `intent` | The closed set of user intents (one exhaustive enum). |
| `controller` | Orchestrate intents → state changes; hold ephemeral session state, including the root-bound `AnnotationStore`; dispatch renders to the worker; map mouse events against fed-back geometry; and rebuild root-bound services on a worktree switch. Its owned annotation projection root-joins file targets, follows the applied `content_path` rather than the live cursor, and exposes merged line ranges only when the applied render carries a source map. Feature submodules are `mod`, `mouse`, `help`, `finder`, `picker`, `infile`, `lineselect`, `annotation`, and `git_apply`. One `Modal` enum type-enforces exclusive input ownership. Only a successful root change clears annotations; failure and same-root paths preserve them. Quitting or switching worktree with a non-empty store raises the `Modal::DiscardConfirm` layer (for a quit, outside search/unzoom) rather than silently discarding it; its `y` proceeds only on a successful clipboard write, and a switch re-validates its held target before committing. |
| `controller` | Orchestrate intents → state changes; hold ephemeral session state, including the root-bound `AnnotationStore` and one frozen pinned preview; dispatch renders to the worker; map mouse events against fed-back geometry; and rebuild root-bound services on a worktree switch. A pin retains its captured origin and independent interaction state across re-root; it is removed only by unpin/replacement or session end. Its owned annotation projection root-joins file targets, follows the applied `content_path` rather than the live cursor, and exposes merged line ranges only when the applied render carries a source map. Feature submodules are `mod`, `mouse`, `help`, `finder`, `picker`, `infile`, `lineselect`, `annotation`, `pinned`, and `git_apply`. One `Modal` enum type-enforces exclusive input ownership. Only a successful root change clears annotations; failure and same-root paths preserve them. Quitting or switching worktree with a non-empty store raises the `Modal::DiscardConfirm` layer (for a quit, outside search/unzoom) rather than silently discarding it; its `y` proceeds only on a successful clipboard write, and a switch re-validates its held target before committing. |
| `app` | The event loop (`run()`): assemble the live components, then `draw → poll input → route to the controller (or the active modal) → drain finished renders`, until the user closes the viewer. |
| `update` | Fixed official HTTPS sources, bounded display-only remote notices through a fail-silent Official Repository Gateway; a 15-second refresh uses optional system `curl` documents (1 MiB each), then atomically publishes a complete, safe-to-delete advisory cache. `update_check` is the sole config setting for both notice types, with `HERDR_FILE_VIEWER_NO_UPDATE_CHECK` as an environment fallback; `404` withdraws a spotlight. |
| `config` | Load & resolve the read-only TOML config: path resolution (`$HERDR_PLUGIN_CONFIG_DIR`, else XDG fallback), defensive parse (malformed input degrades to defaults, never panics), and precedence (config > env > default) → the `EffectiveSettings` consumed at startup by `editor`, `render`, `opener`, and `update`; it also parses the `[keys]` remapping table into `KeySpec` (string-or-array) entries the `input` bindings resolver layers over the registry. Never writes the file. |
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ All notable changes to this project are documented here. The format is based on

## [Unreleased]

### Added
- Pinned preview: `p` freezes one settled file preview beside the active file for session-only comparison. Active stays beside the tree and pin sits on the right; when space is short the active/tree layout remains intact, the held pin says how to widen it, and `Tab` visits only visible regions. Its title carries the captured branch, naming the worktree only when the pin came from a different one, finder confirmation takes focus to the chosen active file, and branch-change replacement names both branches. The captured origin survives a worktree switch; each preview keeps independent scroll/search state, and `{` / `}` or the mouse resize their divider. → [usage](docs/usage.md#pinned-previews) · [keys](docs/keys.md)

### Fixed
- Agent skill: the launch instructions no longer tell agents to pass `--cwd`. herdr resolves the manifest's relative pane command against it, so the launch failed with `plugin_pane_open_failed` — or worse, inside a built plugin checkout, silently ran that checkout's binary. The skill and the `docs/usage.md` snippet now explain that the viewed root follows the *focused herdr pane's* directory, so an agent's own `cd` does not move it. Thanks @AntonyKor (#139) → [agent skill](skills/herdr-file-viewer/SKILL.md) · [usage](docs/usage.md#teach-your-agent)

Expand Down
15 changes: 14 additions & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,20 @@ Canonical vocabulary for this repo. Glossary only: no implementation detail, no
- **tree**: the left column: a recursive, expandable directory tree of the current
root, decorated with git status markers.
- **content pane**: the right column: shows the selected file as rendered markdown,
a diff, or syntax-highlighted content, depending on the active view mode.
a diff, or syntax-highlighted content, depending on the active view mode. Becomes the
**active preview** when a **pinned snapshot** is present.
- **active preview**: the content region that follows the current **tree** selection. When a
**pinned snapshot** exists and width permits, it shares the content area with that snapshot.
- **pinned snapshot**: one session-only, frozen copy of a file's displayed preview, including its
**pinned origin identity**. It does not follow later file changes or refreshes, survives a
**worktree switch**, and remains until replaced, explicitly unpinned, or the viewer exits.
- **pinned origin identity**: the exact `(root/worktree, branch state, root-relative path)` tuple
captured with a **pinned snapshot**; branch state is either the captured branch name or detached.
- **split-preview layout**: the comparison state that presents the **tree**, one **pinned snapshot**,
and the **active preview** as adjacent columns when width permits; the two previews scroll
independently.
- **preview divider**: the adjustable boundary between the **pinned snapshot** and **active preview**
in the **split-preview layout**; distinct from the tree/content divider.
- **view mode**: which rendering the content pane is showing (rendered markdown /
diff / content). Auto-selected per file, cyclable by the user.
- **diff baseline**: what a diff (and the meaning of "changed") is compared against:
Expand Down
4 changes: 2 additions & 2 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,10 @@
# Keybindings tab (e.g. refresh, nav_up, switch_worktree). A value is a KEY SPEC:
# a single string, or an array of strings. An entry REPLACES that action's
# default key(s), so list every key you want it to answer to. The full list of
# intent names (all 39 actions) is in docs/configuration.md (Keybindings) and the
# intent names (all 44 actions) is in docs/configuration.md (Keybindings) and the
# `?` overlay's Keybindings tab.
#
# Bindable keys: any single printable or shifted character (`g`, `<`, `?`, and
# Bindable keys: any single printable or shifted character (`g`, `<`, `{`, `?`, and
# capitals such as `W` are each their own key), plus the named keys Tab, Enter,
# Esc, the four arrows, Home, End, PageUp, PageDown, Space, Backspace, Delete,
# Insert, and F1 through F12 (named keys are case-insensitive). There are NO
Expand Down
11 changes: 10 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,11 @@ customized).
| | `toggle_focus` | `Tab` | Move focus between the tree and content columns |
| | `shrink_tree` | `<` | Narrow the tree column |
| | `grow_tree` | `>` | Widen the tree column |
| | `shrink_preview` | `{` | Give the pinned preview less horizontal space |
| | `grow_preview` | `}` | Give the pinned preview more horizontal space |
| | `toggle_wrap` | `w` | Force content-line wrapping on or off |
| | `toggle_zoom` | `z` | Hide the tree so content fills the frame, or restore the split |
| | `pin_preview` | `p` | Pin or unpin the settled preview as a reference |
| | `tree_scroll_left` | `H` | Scroll the tree pane left |
| | `tree_scroll_right` | `L` | Scroll the tree pane right |
| **Git & filters** | `toggle_ignore` | `i` | Reveal or hide gitignored files |
Expand Down Expand Up @@ -222,7 +225,7 @@ and annotation-overview `j`/`k`/arrows, `Enter`/`e`, `d`, uppercase `D`, `y`, `E
the finder and `:` / `/` prompts. Remapping a global action never changes these local modal keys.

**Bindable keys** are the modifier-free surface the viewer already uses: any printable or shifted
character (`g`, `<`, `?`, and capitals such as `A`, `D`, and `W` are each their own key), plus the named keys
character (`g`, `<`, `{`, `?`, and capitals such as `A`, `D`, and `W` are each their own key), plus the named keys
`Tab`, `Enter`, `Esc`, the four arrows, `Home`, `End`, `PageUp`, `PageDown`, `Space`, `Backspace`,
`Delete`, `Insert`, and `F1` through `F12` (named keys are matched case-insensitively). There are
**no `Ctrl` / `Alt` chords**: a chord never fires a viewer action, so terminal combinations like
Expand All @@ -243,6 +246,12 @@ rebound away, so you can never strand yourself (you may still move the `q` Close
action). Only the global keys are remappable; keys handled inside a modal (including line-select
and the annotation editor/overview) keep their fixed keys.

Shifted characters are distinct bindings, so `p`, `{`, and `}` are valid values for `pin_preview`,
`shrink_preview`, and `grow_preview` respectively; the latter two resize the pinned preview's share
at the preview divider. On Windows, AltGr typing continues to reach whichever character binding you
configure (including a shifted character), rather than being treated as a `Ctrl`/`Alt` shortcut; see
the [AltGr note](keys.md#keys) for the platform-specific input details.

See your bindings in effect any time in the `?` help overlay's **Keybindings** section. It groups
the actions into sections and shows, for each, its config-var name (the `[keys]` id you type to
remap it), its effective key(s), and its description, marking the ones you have customized. The full
Expand Down
Loading
Loading