The split control at the end of the transcript's sticky row stays the web's. In the Mac app only, its caret pops a real AppKit menu under the control, with the same choices: the chain/timeline pair, then the timeline detail, each detail with its lanes as a subtitle. Browser and HTML export keep today's web menu.
Same state in all four: Session timeline on, Standard detail. Left column is the shipped web menu. Right column is the NSMenu the caret would pop in the Mac app, right edge to the control's right edge (part 2 compares the two alignments).
The split control is pinned to the right end of the content column. The hatched area is outside the window.
A · left edge under the control's left edge is how a Mac pull-down opens (the Gruber pass recommended it, medium confidence): the menu belongs to the whole control. Here that runs the menu past the window's edge, which AppKit allows; it only keeps a menu on the screen, not inside its window. With the tag sidebar open, it would also cover the sidebar.
B · right edge to the control's right edge is what the web menu does today, and stays inside the window. AppKit will not do it by itself: native computes x = anchor.maxX − menu.size.width before popping, which is one line.
Decided: B (owner, 18 Oct 2026). The control is a trailing-edge control, and B keeps the menu over the content it changes.
<select>, so a web control opening a native menu is how a WKWebView normally behaves. The menu gets the material, type-select, VoiceOver menu semantics, dismissal when the app deactivates, and it can extend outside the web view.Menu with primaryAction in the toolbar): it moves the control away from the slot it changes, adds a per-route toolbar item (the toolbar's overflow rules are documented as fragile), and the menu bar already has View ▸ Show/Hide Session Timeline and View ▸ Timeline Detail ▸.| Piece | Status | Where |
|---|---|---|
| Actions: toggle, set lanes (2/3/5) | shipped | menuAction("toggleSessionTimeline"), ("setSessionTimelineLanes", {lanes}), handled by applyTimelineMenuAction in TranscriptTimelineStore.ts |
| State mirror | shipped | timeline-state → BridgeHandler.timelineOn / timelineLanes / timelineAvailable |
| Web rect → web-view coordinates | shipped | PersonPickerPresenter.viewRect(for:in:) (safe-area inset + zoom; the trap it fixes is in desktop/CLAUDE.md) |
| NSMenu popped under an anchor, target/action closures | shipped pattern | SearchFieldViews.swift ~300–318 |
Bridge message timeline-menu {anchor} | new | web → native, posted by the caret when isEmbedded(), instead of opening the list |
| Menu builder | new, small | a pure function from (on, lanes, i18n) to items, so it gets a unit test (the house rule: a decision lives in a helper, not a view) |
| Closed echo | new | native → web when the menu closes: aria-expanded back to false and focus back on the caret. popUp is synchronous, so this is one call after it returns |
Pick mapping: User journey or Session timeline sends the toggle only when it differs from the current state; a detail sends set-lanes, which also turns the timeline on (the store already does that). Pass positioning: nil: passing the ticked item makes it a pop-up, which centres that item over the control instead of opening below it.
Commands menu item shows a subtitle needs a build to see; if it doesn't, the hints live only here, which is a fair asymmetry.| Where | Text (en) | Key | Status |
|---|---|---|---|
| Radio 1 | User journey | common.sessions.colJourney | shipped |
| Radio 2 | Session timeline | common.sessions.tapestry.label | shipped |
| Section header | Timeline detail | common.sessions.tapestry.detail | shipped (the caret's own name) |
| Details | Compact · Standard · Full | common.sessions.tapestry.compact / standard / full | shipped |
| Subtitles | Sections · Moderator · Participant / + Sentiment / + Themes | composed from quotes.sections, sessions.speakerPlaceholder.*, sessions.colSentiment, quotes.themes | shipped composition |
No new keys.
The NSMenu is drawn by hand from measured metrics (24 pt rows and a 22 pt check column, PickerMetrics, macOS 27). macOS 15 draws 22 pt rows and a smaller corner radius; the section header's exact type and indent are approximated. Check against a real NSMenu in Diagnostics ▸ Picker Lab before trusting a pixel here.