Part 1 proposes a search for each lens. Part 2 merges them into one field that finds people, quotes, transcript lines, tags, themes, signals and sessions. It searches the current project, or every project in the current folder, and lists the results in a single view.
What exists today. Search only works in the Quotes lens. The SPA filters in the browser: a case-insensitive substring match from 3 characters, with a 150 ms debounce (utils/filter.ts). It checks the quote text, speaker name, tag names and sentiment. Matches are highlighted with .search-mark. On the Mac the field is a native capsule (QuotesToolbarControls.swift) and ⌘F focuses it. Sessions, Codebook and Signals show a disabled SearchComingSoonButton, and the Project lens shows no search control at all.
No search runs on the server. No route takes a q parameter, and nothing uses FTS5 or embeddings. MCP search_quotes loads every quote and runs the same substring match in Python. Stale claim: design-multi-project.md §3b says FTS5 is "already available in the per-project schema". It is not.
The per-lens searches come in two different shapes. A filter narrows a set: rows that don't match disappear. A find moves a cursor through a document: nothing disappears, and you step from hit to hit. Four lenses are filters and the transcript is a find. That is why ⌘G was withdrawn from the Quotes lens (a filter has no cursor to step), and why it comes back in the transcript.
Quotes · filter · shipped, plus a count
“I got all the way to the end and then the delivery was £45, which is more than the shelf.”
“Click and collect or delivery? I don’t know which one is cheaper until I’ve picked.”
Keep it as it is, and add two things. (1) Put the match count inside the capsule and in the window subtitle (“23 of 214”). The vanilla report used to show a count, and the SPA dropped it. (2) Allow token prefixes in the same field, p3, #trust and is:starred, so the keyboard can reach the tag sidebar's job. Both Mail and Finder offer the same thing as a suggestion menu. Part 2 builds that menu and makes it the shared mechanism, so this lens needs no separate syntax.
Session transcript · find, not filter · the step recorded as next
Every hit is highlighted and one of them is the current hit (outlined). ⌘G and ⇧⌘G step forward and back, and the transcript scrolls to keep the current hit in view. This brings back the Find Next and Find Previous menu items that are commented out at MenuCommands.swift:751-791. It is also the condition the private notes set for restoring them.
When the playhead follows the current hit, a researcher can hear every mention of a word in about a minute. No other lens can do that. It is also the highest-value item on record (menu review, Finding 20). It needs no server work: the transcript is already loaded, so this is a DOM walk plus a cursor.
Sessions · filter rows + count hits in transcripts
| Session | Participant | Mentions |
|---|---|---|
| s2 | Tom | 7 |
| s1 | Amira | 4 |
| s5 | Jo | 2 |
| s3 | Priya | — |
This matches participant names, speaker codes, session ids, filenames and dates, and it also counts mentions in each transcript. Clicking a row opens that transcript with Find already running on the same word, so the list and the transcript chain together.
Rows with no match are dimmed rather than hidden, so the session numbering stays readable. The counts need either every transcript loaded in the client or the server endpoint from Part 3.
Codebook · filter tags by name + definition
This searches tag names first and then their definitions. A match inside a definition is shown as a short snippet, so you can see why the row matched. Groups with no matching tag disappear, because in this lens hiding is what a filter is for. The same field also filters the Library sheet (design-codebook-v2.md:254: the manager needs search more than the catalogue does).
Signals · filter cards, dim the heatmap
A card matches when its section or theme label matches, or when the quotes behind it contain the word. The card then says how many of its quotes do (“delivery in 5”). In the heatmap, cells that don't match are dimmed rather than removed, so the grid keeps its shape. Signals are computed when the page is requested, so this filters their labels and the quote ids behind them. Nothing new needs to be stored.
Project · no local search: the field is the global one
The dashboard has too little on it to filter. Its field is therefore the global search from Part 2 (placeholder “Search project”) rather than a disabled button. It is also where a researcher who doesn't yet know which lens holds the answer will start.
Summary of Part 1.
| Lens | Shape | Matches | Non-match | ⌘G | Needs server? |
|---|---|---|---|---|---|
| Quotes | filter | text, edits, speaker, tags, sentiment | hidden | — | no (shipped) |
| Transcript | find | segment text | kept | next / prev | no |
| Sessions | filter + counts | people, codes, ids, filenames, dates; transcript mentions | dimmed | — | for counts |
| Codebook | filter | tag and group names, definitions | hidden | — | no |
| Signals | filter | section and theme labels, quotes behind cards | dimmed (grid) | — | no |
| Project | global entry | everything (Part 2) | — | — | yes |
All six use one native capsule, extended beyond Quotes. The bridge message would change from setSearchQuery to setSearchQuery {lens, text}. The canSearch gate would open on every lens except Welcome, which stays dim by decision (Finding 19). Search is still not restored between app launches (LensMemory.swift:25).
The merge keeps the per-lens field and adds suggestions as you type, in the way Mail does. Pressing ↩ does what it does today and filters the lens you are in. Choosing a suggestion turns the field's contents into a token (a person, a tag, a theme) or opens that item. ⌘↩ opens every result, grouped by type, in a single results view.
after typing “deliv” in the Quotes lens
“The kitchen planner kept losing my measurements whenever I went back a step.”
“Click and collect or delivery? I don’t know which one is cheaper until I’ve picked.”
“I liked that it showed the stock at my store straight away.”
“I got all the way to the end and then the delivery was £45, which is more than the shelf.”
“The fitting kit was extra too. Every screen found another tenner.”
“I got all the way to the end and then the delivery was £45…”
“Click and collect or delivery? I don’t know which one is cheaper until I’ve picked.”
“I got all the way to the end and then the delivery was £45…”
“It fits, great — but will they deliver up four flights?”
“Collecting it back costs the same as the delivery did.”
This is the pattern Mail and Finder already use. The first row, highlighted, is always the free-text match for the lens you are in, so pressing ↩ keeps today's behaviour exactly. Below it, one suggestion per type (people, tags, sections, signals, transcripts), with a count on every row so the menu also answers “how much is there?”. The menu is capped at about 8 rows, and the last one opens all results.
Keys. This follows Mail. ⌘F searches what you are looking at: Find in a transcript, the filter in any other lens. ⌥⌘F searches the whole project, as Mail's Mailbox Search does. ⌥⌘F currently belongs to Focus Mode, which gives it back and needs a new shortcut; ⌃⌘F is the system's Enter Full Screen, so it is not available. / keeps working on the web.
Choosing a person or a tag turns it into a token, the blue pill that Mail uses. Tokens combine with AND: here, p2 and the tag hidden costs. The token for a tag does the same job as the tag sidebar, and the two stay in sync: the sidebar shows the tag ticked. Backspace removes the last token.
Open decision: should tokens stay applied when you switch lens? My recommendation is yes, within a session. The query describes what you are looking at, and the lens is only how you view it. So p2 in Quotes becomes p2's sessions in Sessions and p2's cells in Signals. The query is still not restored after a relaunch.
The results view is a pseudo-lens. It uses the same window and the same sidebar geometry as the other lenses, and the sidebar's rows become result types, the way Mail's sidebar becomes mailboxes. Each group shows its first few rows, and “Show all in Quotes →” carries the query into the real lens, where it becomes a filter. The scope bar is Finder's (“Search: This Mac | ‘Folder’”).
In folder scope, the rules from design-multi-project.md §3c apply to how results are shown. The project name is the first thing on every row, above the quote and in bold, not in fine print (Finding 5). People appear as speaker codes only, because a participant who consented to one study may not have consented to being linked across studies. Showing names would need an explicit opt-in with a warning. Projects on unmounted volumes are named, not silently skipped (§3b constraints).
Scope: you asked for all projects, but the design record says the folder is the boundary. design-multi-project.md §3b says: “Global — Not supported.” §3c Finding 1 says the folder is the confidentiality boundary between competing clients, “period”. The threat it names is a freelancer who shares their screen, types “onboarding”, and shows client A's participants to client B. The mockup therefore offers This project and this folder. Adding “All projects” later is one more item on the scope bar, and the provenance rules above already assume results come from more than one project. Your options:
Build order. Each step ships on its own.
SearchComingSoonButton. The bridge message gains a lens field.GET /api/projects/{id}/search?q=&kinds=&limit=, returning a count per type plus the top N hits of each. Back it with an FTS5 table per project covering quote text and edits, transcript segments, people, tag names and definitions, and heading edits. Build the table after stage 12 and update it on edit. Signals are computed at request time, so the endpoint runs load_signals and matches labels and quote ids. MCP search_quotes should call the same core, following the grounding rule of one shared core and never a parallel sibling.ServeFleet), so the natural fan-out is for each project's own sidecar to answer for its own database. That may also avoid the question of whether one sidecar can read another project's folder under the sandbox. Unverified: that last point needs measuring, not assuming.New pieces this adds (the no-bespoke rule means they are flagged, not just drawn): the suggestions popover, search tokens in the field, the results pseudo-lens and its scope bar, the provenance pill, and the transcript's current-hit outline. Everything else reuses shipped parts: .search-mark, .badge with group tints, the quote card, the sidebar row, and the native capsule.
Native note: the capsule is a SwiftUI TextField, not an NSSearchField. Tokens (NSSearchField's token support, .searchable(tokens:)) are the one feature that argues for revisiting the choice left open in §4.4 of design-desktop-nav-toolbar-rearrangement.md. The popover can be a SwiftUI popover either way.