Sessions lens — native popover navigation

A quick session switcher in the embedded (macOS) build. It replaces the session dropdown in the transcript's sticky header — not the left sidebar, which was never the right fit for switching and is removed. The full-fat view, with sentiment maps and user journeys, stays one click away as Sessions index. Real pixels below: the shipped .badge atom, the macOS AppKit type ladder from tokens-desktop.css, and ExportPopoverRow's exact geometry (308pt wide, 6/12 row padding, 10pt gap, 6pt corner radius).

Artefact — what ships
Commentary — why, and what's still open
Palette
Appearance
Study size
Stacked badges
Active row

Multi-participant row shape — the two candidates

A · single-line, compressed badge — not chosen
Fishkeeping
20 Sessions · 7h 39m
report
B · stacked speaker rows — chosen ✓
Fishkeeping
20 Sessions · 7h 39m
report

Rows are real Fishkeeping data — genuine names and durations, genuine multi-participant sessions (#6, #8, #10) where two or three people share one session and the second and third have no name, because the pipeline only named the first speaker in each. Codes stay globally sequential the way the pipeline assigns them, so a three-person session pushes every later code along — #11 is p16, not p11. Timestamps are substituted (Fishkeeping is a transcript-only import; see the note under the swatches). Twelve sessions is the default because that's the top of the common range — switch to 6 or 34 above to see the short case and the scrolling case.

Why a popover and not an NSMenu. The thing being replaced is already a dropdown, so this isn't a sidebar being demoted into a menu — it's a dropdown moving to the titlebar and gaining the density it always wanted. NSMenu can't do a two-line row with a muted subtitle, and the duration/date line is the whole reason this beats the old selector. The list is sized for what actually happens: 5–12 sessions very commonly, occasionally 30+. At 12 nothing scrolls; past that it does, and the “full fat” answer was always Sessions index rather than a longer menu.

Active row — four treatments, same three rows

Decided: the real AppKit source-list capsule — which decides the implementation, not just the styling. The capsule colour is internal to NSTableView, so having it means using the control: the popover stays a popover (SwiftUI .popover, same as the share menu) and its content becomes an NSTableView with style = .sourceList. The other three below are kept switchable as the record of what was weighed. Note the specimens are SwiftUI-style approximations — only the shipped control draws the true capsule.

The popover family has no precedent for a selected rowExportPopoverRow has hover only (Color.primary.opacity(0.06)), because actions have no “current one”. This adds a state the family doesn't have, so it has to borrow from somewhere.

Three different “selected” values are already in play

Flip the palette above and watch these three converge and diverge. Under Edo the web nav-selection and the Lozenge are byte-identical (#e6ddc9) — they were authored as one decision. Under Default they are not even the same hue: the web sidebar washes rows neutral grey #efefef while the Lozenge is pale blue #eef4fc. AppKit's real capsule is a third value under both, and unreadable under both.

So “match the project sidebar” cannot be satisfied literally on any palette. The question the side-by-side actually answers is which mismatch reads as deliberate — a popover that agrees with the web list it replaces, or one that agrees with the native list above it.

On the date: rows show duration · formatFinderDate(...) — exact parity with the Sessions grid's Start column. Sessions ingested from media carry a real clock time (st_birthtime, s01_ingest.py:23). Sessions imported from transcript text with a bare YYYY-MM-DD header do not, and render 11 Feb 2026, 00:00 — the last row of each popover is pinned to that state so the failure mode is visible rather than described.