feat(sessions): pin browse modes — recency order, ungroup, pinned-only
Summary
Three complaints about the 📌 Pinned zone from a live review (2026-08-20), answered from the same header line — vertical space is the scarce resource in a menu-bar popup, so this adds no new chrome.
| Complaint | Before | Now |
|---|---|---|
| "the zone isn't ordered by latest activity" | pinnedAt ASC — oldest pin first | lastTimestamp DESC (recency), like every other list |
| "sometimes I want everything in one time-ordered list, without a pinned block on top" | impossible: collapsing removed pins from the zone and the timeline at once, so they became invisible | collapsing ungroups: pins fall back to their chronological slot with the ★ |
| "sometimes I only want to browse — or search — the pinned list" | impossible | new only chip on the right of the header |
▾ 📌 Pinned (7) [only] ← zone at top (default)
▸ 📌 Pinned (7) [only] ← ungrouped: pins sit in time order, marked ★
📌 Pinned (7) [ONLY] ← list AND search scoped to pins
Both toggles persist in localStorage (codev-pinned-collapsed, codev-pinned-only).
Why ordering changed back
PR #136 deliberately chose pinnedAt ASC so a new pin appended at the zone bottom instead of reshuffling rows under the cursor. That reasoning was about the moment you pin; it cost the ordering used everywhere else, and buried the session touched five minutes ago beneath months-old pins. The hover-suppression added in the same PR (suppressHoverSelection()) already absorbs the layout movement it was avoiding.
Correctness details that are easy to miss
- A pinned session is never folded away as a "minor session". While pins lived in the zone this could not happen; once ungrouped, a pinned session with ≤2 messages and no title would have been eaten by the junk fold. Pinning is an explicit "keep this", so the predicate now skips pins outright.
- Pins outside the loaded ~100 stay visible in every mode. They exist only in the by-id resolved set, so ungrouping would have made them vanish from the zone and the timeline — the exact bug this PR set out to remove, in a rarer form. They are appended to the timeline instead (the loaded list is the top-N by recency, so anything outside it is older than every row above).
- Pinned-only can never become a trap. The header stays rendered on an empty result set (and during a search), the empty state names the way out, and removing the last pin clears the stored preference — keyed off a real non-empty → empty transition, because at mount the marks are empty until the IPC load lands and a plain check would wipe a legitimately stored preference.
Implementation
src/session-list-view.ts — new pure module (no fs, no electron, no React). ~130 lines of "which rows appear, in which group, in which order" moved verbatim out of the 2000-line component, which had made the only available check "run the app". That is precisely where the bugs have been: PR #136 needed five rounds of live testing, all of them list/index interactions, and this PR now multiplies three independent browse states together.
10 new tests (63 total) cover the matrix — grouped / ungrouped / pinned-only / pinned-only + search — plus the two invariants above.
Mutation-verified rather than assumed. Reverting each guard in turn turns the suite red, and restoring it turns it green:
| Reverted guard | Result |
|---|---|
| pin guard in the minor predicate | 1 failed / 62 passed |
| out-of-window pin append | 2 failed / 61 passed |
recency ordering (back to pinnedAt) | 1 failed / 62 passed |
| pinned-only scope filter | 1 failed / 62 passed |
| (restored) | 63 passed |
Also fixes a type annotation the extraction surfaced: sessions carry epoch-ms numbers, but formatRelativeTime declared string. It only survived because every caller went through an any row. No behavior change — new Date() takes both.
npx tsc --noEmit clean.
Docs
docs/session-finding-plan.md §4.4 had drifted from what shipped — it still described dual placement ("the section is a shortcut, not a move") and pinnedAt desc ordering, both reversed during PR #136. Rewritten to the shipped design with the rejected alternatives kept as the record, and §8's two settled questions closed.
§4.5 / §4.6 / §6 now carry the measurements from the same review, so the next two PRs don't re-derive them:
- Titles: median 44 chars, 64% exceed the UI's 35-char hard slice, and 48 of 125 titles (38%) share their first 35 chars with another title — the largest group is 8 sessions all rendering as
fred-ff nextjs backend and mcp arch. The title column, the primary identification signal in this app, is ambiguous for 38% of sessions before search is involved at all. - PR references: of 2,506 (session, PR-number) pairs mentioned in prompts, only 19.4% carry both
#Nand the URL — 80.6% are reachable by one query form only. - Frecency: 3 of the frecency top-10 fall outside the recency top-20, and 3 of 7 real pins land in the frecency top-10 while three others rank #26/#63/#72 — so a "Frequent" list is complementary to pins, not a replacement, and its marginal value is modest.
How to test (yarn make)
- Order — the zone should now lead with the pin you touched most recently, not the one you pinned first.
- Ungroup — click
▾ 📌 Pinned (N): the zone disappears and every pinned session reappears in its normal chronological position with a gold ★. Nothing should be missing; count the rows. - Only — click
only: the list is just your pins. Type in the search box — results stay scoped to pins. Clickonlyagain to leave. - Only + no match — with
onlyon, search for gibberish: the header must stay visible so you can click your way out. - Junk pin — pin a ≤2-message untitled session, then ungroup: it must stay in the list, not fall into the minor fold.
- Old pin — pin something well down the list (or found via deep search), then ungroup: it should appear once, at the bottom.
⌘D/⇧⌘Dshould behave exactly as before in all three modes.
Out of scope (next PRs, per the same review)
- Row readability: middle-ellipsis titles, hover-for-full-title, match-aware highlight windows (§4.5).
- PR-reference canonicalization
#123⇄ URL, and theB4search filters it unlocks (§4.6). - The "Frequent" frecency scope (§6,
D4).
Note
Draft PR #119 (feat/worktree-launch, 2026-04-27) also touches src/switcher-ui.tsx; its only overlap here is the formatRelativeTime line. That branch is four months behind main and will need a rebase regardless.
🤖 On behalf of @grimmerk — generated with Claude Code
Summary by cubic
Adds three browse modes for pinned sessions and makes the marks store authoritative to prevent accidental pin loss. Old: the pinned zone sorted by pin time and collapsing hid pins; unreadable stores were treated as empty and could be overwritten. New: the zone sorts by last activity, collapsing ungroups pins into the timeline, an “only” chip scopes list and search, and unknown reads are neither broadcast nor written over.
- Pinned sessions never fold into “minor”; pins outside the loaded window show exactly once when ungrouped; the header count reflects the scoped total while “only” is on.
- List building is a pure, tested module (
src/session-list-view.ts);mergeSessionsByIdwidens search candidates to include by-id pins; search re-applies the live query when deep results or enrichment arrive;formatRelativeTimeaccepts numbers. - UI: the header is always rendered; “only” scopes both list and search; toggles persist via effects; selection/order remain stable when toggling.
Marks store (authoritative-or-unknown)
- Reads return
{marks, known}; ENOENT is authoritative; any lossy read (malformed, future-version, or normalization would change bytes) isknown:false. mutateMarksFile/mutateSessionMarksrefuse to write whenknown:false; the watcher drops unknown-read broadcasts; IPCgetSessionMarksincludesknown; the renderer ignores unknown initial reads and only clears pinned‑only after an authoritative read.- All four main-side mutations now go through
mutateMarksFileto avoid overwriting a store we could not read. Tests cover authority, write refusal, watcher behavior, and list invariants.
Summary by CodeRabbit
-
New Features
- Added a dedicated pinned-session area with recency sorting.
- Added controls to group pinned sessions or display them chronologically.
- Added a persistent “Pinned only” filter for browsing and search.
- Improved search matching for older pinned sessions by title, branch, and pull request.
- Kept unresolved and out-of-window pinned sessions visible.
- Updated session counts to reflect active filters.
- Preserved active sessions and excluded pinned sessions from minor-session folding.
-
Bug Fixes
- Prevented session marks from being overwritten when stored data cannot be read.
-
Documentation
- Updated guidance for pinned-session browsing, search, grouping, ordering, and filtering.