feat: add persistent pull request sort and filter menus

This commit is contained in:
Valdemar Lorenzen
2026-10-21 13:30:38 +02:00
parent 37b47ebc8a
commit fbcb519e13
22 changed files with 1557 additions and 181 deletions
+14
View File
@@ -109,6 +109,20 @@ Indicators compare the current editor text (including unsaved edits) against Git
`config.pull_request_review_requested_symbol` and `config.pull_request_approved_symbol` control the markers before PR numbers in the pull-request picker. Defaults are `"🙏"` for an outstanding personal review request and `"👍"` for your active approval, using the API-authenticated account. Requests take precedence over approvals when both apply. Each option accepts a string without control characters; multi-character and wide Unicode symbols are supported. Set either to `""` to hide that state, or both to `""` to remove the marker gutter. PR numbers and metadata stay aligned by terminal-cell width, including rows without a marker. Non-strings and strings containing tabs, newlines, or other control characters reject the configuration.
`config.pull_request_sort` and `config.pull_request_filters` optionally override saved pull-request picker settings, independently. `nil` or an omitted option uses saved preferences, then built-in defaults. A configured section replaces that entire saved section; omitted sort criteria use built-in defaults, not previously saved rules.
```ruby
config.pull_request_sort = {
updated_at: { precedence: 1, direction: :desc },
created_at: { direction: :asc }
}
config.pull_request_filters = [:requested, :approved]
```
Sort fields are `requested`, `updated_at`, `approved`, and `created_at`, in that default precedence order. Default direction is `desc`; timestamp criteria support `asc` for oldest first. `precedence` is `nil` (unnumbered default position) or a unique integer in `1..4`. Filters are any subset of `requested` (Requested from me), `approved` (Approved by me without an outstanding personal request), and `other` (Other / unavailable). `[]` excludes all requests; `{}` resets sorting to built-in defaults. Unknown fields/options, duplicate ranks/filters, and invalid types or directions reject configuration.
Enter Apply in the picker saves global preferences to `$XDG_STATE_HOME/vamp/pull_requests.json`, or `~/.local/state/vamp/pull_requests.json` if XDG state is unset or relative. This also works with `--no-config`. Configuration remains a startup override, not a lock: UI edits work and persist, but explicitly configured sections win again next startup. Save failures report session-only changes. Corrupt, incompatible, or non-regular storage is reported and preserved rather than reset or overwritten.
`config.resolve_history_layout` controls the read-only conflict inputs opened by `:resolve.history`: `"columns"` (default) places left/base/right side by side; `"base_below"` places left/right above a full-width base. Both layouts share aligned rows and synchronized vertical scrolling. Tab/Shift+Tab follows layout order (left/base/right or left/right/base); directional pane actions follow the displayed geometry. Narrow terminals show the focused source only; the base-below layout does the same when there is insufficient height. This setting does not change the editable Resolution view or ordinary diffs. Invalid values reject the configuration.
```ruby
+1 -1
View File
@@ -88,7 +88,7 @@ Clipboard checks use isolated providers rather than reading or overwriting the d
Diff checks cover staged category/filter navigation, stale request cancellation, row alignment, read-only buffer isolation, and rendered panes. Repository integration fixtures exercise Git and Git-backed jj, advertised but unfetched remote branches/bookmarks/tags, imported historical commit/change IDs, conflicts, unusual paths, binary/mode/link/submodule changes, and unchanged active metadata. Remote fixtures are local bare copies; they never push. Git fixture identities and commit dates are fixed.
Pull-request checks cover personal review filters and priority sorting alongside fuzzy queries, category changes, streamed metadata updates, identity-preserving selection, and immediate opening. Live PTY smoke at 120×30 and 44×16 cycles both controls, combines filters with search, restores defaults, and exits cleanly. A disposable curl fixture supplies all forge responses, and the editor and descendants run with networking denied.
Pull-request checks cover ranked sort menus, rank collisions/default slots, timestamp directions, independent personal review flags, checkbox unions, Apply/Cancel, category/query composition, streamed updates, and selection identity. Storage and session checks cover restart persistence, config section overrides, corrupt/incompatible-file preservation, and failed writes with isolated paths. Live PTY smoke at 100×24 and 44×16 exercises checkbox Apply/Cancel, restart restoration, creation-rank movement and ascending direction, and a config filter override. All forge responses come from a disposable curl fixture; the editor and descendants run with networking denied.
Diff-stat checks cover unopened frozen files, additions/deletions/renames, binary/mode changes, CRLF/EOF, refresh, oversized-file laziness, filtering, live edits/undo, and inactive tabs. Live PTY smoke opens a two-file Git Working comparison, observes selected-file `+2 -1` alongside whole-diff `+2 -2` before opening the second file, edits and undoes text, switches to an ordinary tab, and exits with source bytes and HEAD unchanged.
+5 -1
View File
@@ -412,7 +412,11 @@ Immediately before `#number`, **🙏 marks an outstanding personal review reques
A later request for changes or dismissal removes 👍; comment-only and pending reviews do not replace an existing approval. The mark is not the PR's overall approval decision or a guarantee that you reviewed its current head; the forge controls approval resets after new commits. Customize either symbol with [`config.pull_request_review_requested_symbol` and `config.pull_request_approved_symbol`](configuration.md); an empty string hides that symbol. Multi-cell symbols remain aligned, and decorations are not part of searchable PR labels.
**Ctrl+f** cycles the review filter through **All → Requested → Approved**. **Ctrl+s** cycles sorting through **Newest → Requested first → Approved first**. Both use the personal review states above, even when their symbols are hidden; a re-review request belongs to Requested, not Approved. Filters combine with the selected category and fuzzy query. Review priority sorts ahead of fuzzy relevance; within each group, fuzzy ranking is unchanged, or newest creation time first when the query is empty (higher PR number breaks date ties). Sorting and filtering preserve the selected PR when it remains visible, including as pages arrive or metadata changes. Category counts still report all loaded requests, and partial-search notices still apply. These controls only use already-loaded metadata and make no additional API requests.
**Ctrl+s** opens sorting settings in the floating picker. The default comparison order is **Requested from me → Updated at → Approved by me → Created at**, with true statuses and newest timestamps first. Up/Down or the configured selection bindings navigate. Press **1–4** to assign the highlighted criterion an absolute precedence; it moves to that position and stays highlighted. Occupied explicit ranks shift forward (wrapping to a free slot); unnumbered criteria fill the remaining slots in their default relative order. Only explicitly assigned ranks display numbers. **Backspace** clears an explicit rank; **Left/Right** toggles oldest/newest first for timestamp criteria. Comparisons are lexicographic, with higher PR number as the final tie-breaker. Fuzzy text still determines which requests match; these settings determine their order.
**Ctrl+f** opens review filter checkboxes. **Space** toggles the highlighted bucket: **Requested from me**, **Approved by me**, or **Other / unavailable**. Checked buckets combine with OR, then intersect with the category and text query. All checked includes every review state; none checked produces no matches. These are personal states, not the PR's team approval threshold: teammates' outstanding requests do not cancel your approval. A new request for your own review takes precedence over your earlier approval. Unavailable metadata remains Other, not a claim that the PR has never been reviewed.
**Enter** applies either settings view; **Escape** discards its draft and returns to the PR list. Applied settings preserve the selected request where possible and carry across tabs and sessions. Category counts remain loaded-category totals, and partial-search notices still apply. Settings use already-loaded metadata without additional API requests. Explicit [configuration overrides](configuration.md#preferences) win again at startup; the settings view indicates when this applies.
Review-state reads reuse the same API credentials as listing. GitHub matches the current list's [requested reviewers](https://docs.github.com/en/rest/pulls/review-requests#list-requested-reviewers-for-a-pull-request) and filters published [reviews by author and decision state](https://docs.github.com/en/graphql/reference/pulls#pullrequest). GitLab filters approvals by the authenticated approver and reads individual reviewer [interaction states](https://docs.gitlab.com/api/graphql/reference/) in separate bounded, paginated batches; unsupported or denied reviewer-state fields do not discard known approvals or size metadata. Gitea reuses [paginated review reads per PR](https://docs.gitea.com/api/operations/repo-list-pull-reviews/) for both states, so its pages can take longer to load.