Skip to content

feat(ui): Mail-style session rows, search tokens, and filter-only search - #27

Open
hsusul wants to merge 6 commits into
mainfrom
feat/ui-macos-list-search
Open

hsusul wants to merge 6 commits into
mainfrom
feat/ui-macos-list-search

Conversation

@hsusul

@hsusul hsusul commented Oct 27, 2026

Copy link
Copy Markdown
Owner

Stacked on #26. Review the commits after 15d87f5 (15d87f5..ac52b37, two commits).

Why

PR 2 of the macOS redesign (docs/design/lore-design-spec.md v2, local). PR #26 laid down the tokens, primitives and the Mail window shell, but the session list and search still used the pre-redesign rows: a colored stripe and link-blue title for selection, a rule under every row, one row per search hit with field names ("in tool call") and raw JSON, and a filter line rendered as tinted text. This PR makes the list and search behave and look like Mail's message list and search field (spec 8, 9.2, 10, 11, 18.5 to 18.7).

What changed

Session rows (spec 9.2, 18.5)

  • Two-line 52px rows: title in --t-headline with a trailing Mail date ("2:00 PM", "Yesterday", the weekday, then "10/19/26"), and one line-2 run: "Claude Code, 6 messages, Partially parsed".
  • Inset hairlines between rows, hidden above and below the selected row. No hover fill, stripe, dot, or ring.
  • Selection is the open session. It is --selected with white text while the list has focus in the key window (tracked through list focus and blur plus window focus and blur), and --selected-unfocused with primary-color secondary text otherwise.
  • Arrow keys and j/k open sessions in place, as in Mail (spec 11). Enter moves focus into the reader, including when the session is already open.
  • "Load older sessions" selects the first newly loaded row and keeps focus on the list. The virtual list keeps fixed row heights.

Search field (spec 8)

  • A finished key:value filter (followed by a space) becomes a token inside the capsule: key: in mono --token-key, the value in sans. Backspace at the start of the text selects the last token, and a second Backspace deletes it, as in Mail. A Clear Search button empties the field.
  • A bare key: opens a suggestion popover (Radix Popover) with values: agents from listDetectedAgents, repositories by name from listRepositories, and has:patch / has:error. Arrow keys, Enter or Tab pick a value. Esc or a click elsewhere closes the popover and keeps the text. The last row opens the filter reference.
  • The filter reference opens from the magnifier, ? in an empty field, or ⌘/, never on plain focus. It now lists agent:, repo: (by name), worktree:, path:, has:patch, has:error first, then the other filters, then Sort By (Best Match, Newest, Oldest). ⌥⌘F and / focus the field.

Filter-only search

  • The guard stays removed, as it was in feat(ui): adopt macOS system tokens, shadcn primitives, and a Mail-style window shell #26. A query that is only filters, such as agent:codex, runs searchPage and lists the matching sessions in the order the backend returns them (newest first). A bare agent: with no value does not search yet, so the list stays put while the suggestion popover is open.
  • src/mock/ does not parse filters (it treats agent:codex as two words). The new behavior test, "lists the matching sessions for agent:codex alone", therefore drives a mocked ipc module, as the existing App tests do.

Search results (spec 9.2, 18.6, 18.7)

  • One fixed 68px row per session, grouped by session_id: title and date, the best snippet with --mark, then "N more matches" when the loaded pages are complete, or "More matches in this session" while more pages remain. No field names. Hits on raw tool-call JSON never become rows.
  • Filter-only results are two-line rows with the agent name and no message count (18.7).
  • A Mail-style scope bar sits under the toolbar while searching: "Search: All Sessions | ". A folder shows as a disabled option, because a folder cannot narrow a search yet. A sort menu sits at its trailing end.
  • The toolbar reads Searching “All Sessions”. Its subtitle reads "N sessions" only when the count is exact, and "More than N sessions" otherwise. Sort is also in the scope menu while searching.

States (spec 10)

  • No Results: "No Results", the query echoed (“lookbehind” with agent:codex), plus "Search All Sessions" when the search is scoped and "Clear Filters" when filters are present.
  • Loading shows nothing for 1 second, then a centered 15px spinner.

Theme

  • New tokens --row-h 52px, --hit-row-h 68px and --token-h 20px. Tailwind gains the aria-checked and data-selected variants.

Scope

In: list rows and selection, ListBox keyboard and focus, search field tokens and suggestions, the filter reference contents, result grouping, the scope bar, the toolbar search title and count, and the no-results and loading states.

Out: the reader, its header, actions and keyboard hint (PR 3), and the palette and settings (PR 4).

No changes to src/ipc.ts, src/main.tsx, src/mock/, crates/, or src-tauri/.

Tradeoffs

The reviewer accepted all eight deviations from the spec and the prototype:

  1. Rows do not show "N secrets redacted". SessionSummary has no secret count, and sessionSecretCount per row would mean one IPC call per row.
  2. The suggestion popover offers no folder values, because the grammar has no folder: filter. It offers no worktree: values, because the catalog carries only worktree counts.
  3. Sort is an icon menu at the end of the scope bar. A text pop-up there clipped the repository name at the 320px list width. Sort is also in the scope menu and in the filter reference.
  4. Arrow keys open sessions (spec 11), so "Load older sessions" opens the first new row it selects.
  5. On a focused (blue) selected row, search hits are underlined instead of yellow, because the translucent yellow turns olive over blue.
  6. A Clear Search button (from the prototype) is the mouse path for removing tokens.
  7. Filter-only rows show the agent name without a message count (18.7). An untitled session uses its snippet as its title.
  8. When a session's best hit is its title, the snippet comes from its next-best hit, so line 2 does not repeat line 1. The agent label "Claude" became "Claude Code" app-wide, which also changes the reader and transcript labels.

Blast Radius

UI only, in the list pane, the search field, and the toolbar title while searching. Behavior changes:

  • Arrow keys open sessions instead of moving a separate cursor.
  • Finished filters move to the front of the query as tokens. Filter order does not matter to the backend.
  • The sort pop-up moved from the results header to the scope bar menu.
  • The "Results" header and the per-row field names are gone.

Nothing new is persisted.

Verification

  • npm run lint: eslint, no errors.
  • npm run typecheck: tsc --noEmit, no errors.
  • npm test: 12 files, 123 tests passed (feat(ui): adopt macOS system tokens, shadcn primitives, and a Mail-style window shell #26 had 114). New or replaced tests cover:
    • filter-only agent:codex alone
    • token rendering and two-step Backspace
    • the suggestion popover keyboard flow, plus Esc and click-away
    • grouped hits with exact and inexact counts, and the content_json guard
    • focused and unfocused selection, including window blur
    • Load older keeping focus on the first new row
    • Enter on an already open session
    • focus after closing the filter reference with a click on a row
    • No Results with Search All Sessions and Clear Filters
    • the reference's repo: and worktree: entries and its sort options
  • npm run build: passed. CSS 39.92 kB (8.53 kB gzip), JS 346.15 kB (111.17 kB gzip).
  • Reviewer gate: approved after one must-fix (closing the filter reference by clicking a row pulled focus back to the search field) and three small fixes (stale load-more focus request, content_json guard, the list's top gap and keyboard scrolling). All are fixed in the second commit, with a regression test for the must-fix.
  • Screenshots in real WebKit (docs/design/tools/wkshot.swift) at 1280x800 and 900x700, light and dark. States: list, search with hits, filter-only results, no results, the suggestion popover, and, for head only, unfocused selection. They are gitignored, so they exist only locally:
    • /Volumes/990 Pro/Development/Lore/.claude/worktrees/agent-a465e531a504ecfbd/docs/design/shots/pr2/main/
    • /Volumes/990 Pro/Development/Lore/.claude/worktrees/agent-a465e531a504ecfbd/docs/design/shots/pr2/pr1/ (the feat(ui): adopt macOS system tokens, shadcn primitives, and a Mail-style window shell #26 baseline)
    • /Volumes/990 Pro/Development/Lore/.claude/worktrees/agent-a465e531a504ecfbd/docs/design/shots/pr2/head/
    • /Volumes/990 Pro/Development/Lore/.claude/worktrees/agent-a465e531a504ecfbd/docs/design/shots/pr2-review/ (the reviewer's shots)
  • The mock ignores filters, so the filter-only shots wrap the mock's invoke in the screenshot script only. Nothing under src/ changed for this.
  • Not run: the Tauri app and the Rust checks. No file under src-tauri/ or crates/ changed.

Add src/tokens.css with every spec v2 Section 4 to 6 token for light,
dark (system and data-theme), and Increase Contrast, with the WebKit
system color keywords declared after the measured fallbacks. Tailwind v3
runs with presets: [] and a theme built only from those tokens, so stock
classes such as bg-blue-500, p-7, and rounded-xl emit no CSS.

Drop IBM Plex for the system sans and mono stacks, and remap the existing
component styles onto the tokens. Vendor dialog, dropdown-menu,
context-menu, select, tooltip, command, tabs, popover, toggle, and
scroll-area as React 18 forwardRef wrappers over Radix and cmdk, restyled
to the spec, each with a behavior test.

Guard tests fail on a hex color, px literal, or arbitrary Tailwind value
in a component file, on any token pair under WCAG AA, and on stock
Tailwind classes producing CSS.
Replace the top bar, scope chip, and bottom status bar with the spec v2
window (Sections 7, 8, 9.1, 9.9):

- A source-list sidebar: All Sessions, Repositories, Folders, and
  Agents, with counts, Mail selection colors for focused and unfocused,
  arrow-key selection, collapsible sections, a folder context menu, and
  New Folder. Agent rows run the filter-only agent: search. Hidden below
  1000px; toggled with Control-Command-S or the toolbar button.
- A 52px toolbar split at the column edges: the scope title and session
  count over the list (a scope menu when the sidebar is hidden), an
  empty slot for reader actions, the palette button, Rescan, and the
  260px capsule search field. Empty areas are Tauri drag regions.
- Rescan shows a spinner, then a progress ring, a failure popover with
  Try Again, and the last scan time stored as ui.lastScanAt. Command-R
  rescans.
- The filter reference opens from the magnifier, ?, or Command-/,
  never on focus. Filter-only queries now search.
- Notices move from the status bar to a line under the toolbar.
With presets: [] a class outside the tokens compiles to nothing, so a
typo or a stock value fails silently. The new test compiles every
utility-looking class token in component strings and lists the ones
that emit no CSS. It would have caught left-1.5 in the menu indicator
before inset had a spacing scale.
The running Rescan button is aria-disabled, which grayed its spinner to
the disabled label color (about 1.6:1 on the toolbar). The spinner now
keeps the secondary label color. The scan warning moves before Rescan
so the controls after it never shift when it appears (spec 17.23).

Add the spec's --red-text and --orange-icon tokens for destructive
labels and the toolbar warning icon, use them for the existing red text,
and check both in the contrast test.
Session rows follow spec 9.2: 52px two-line rows with a Mail date and one
"Claude Code, 6 messages" run, inset hairlines hidden around the selection,
and a filled selection that is blue only while the list has focus in the key
window. Arrow keys open sessions in place and Enter moves into the reader;
"Load older sessions" selects the first new row and keeps focus on the list.

The search field turns finished filters into tokens (mono key, sans value),
deletes the last token with two Backspaces, offers values after a bare key
(agents, repositories, has:), and keeps the filter reference off plain focus.
The reference now lists repo: and worktree: and the sort options.

Search results show one 68px row per session with its best snippet and
"N more matches", or "More matches in this session" while pages remain.
Filter-only queries list sessions as two-line rows. A scope bar sits under
the toolbar while searching, the toolbar reads "Searching" with an exact or
"More than" count, and No Results offers Search All Sessions and Clear
Filters. Loading shows nothing for a second, then a spinner.
Closing the filter reference by clicking elsewhere no longer pulls focus
back to the search field, so a clicked row keeps the blue selection and
arrow keys. A pending "select the first new row" request is dropped as
soon as a load finishes, so a later scroll load cannot move focus. Raw
tool-call JSON hits never become rows. The list's top gap moves outside
the scroller so keyboard scrolling never clips the last row.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant