Problem
Once you have run more than a handful of coding-agent sessions there is no way to find one. The project rail can only show what fits in a sidebar — it caps discovered sessions per project — so conversations that exist on disk are simply unreachable. On this machine that is 73 project directories and several thousand Claude transcripts.
Two questions go unanswered: "which conversation was the one about X?" and, more painfully, "where is that thing I pasted?"
Proposal
A popup (Cmd+Shift+K, plus a magnifier in the rail header) over every known session — live tabs, stored session handles, and transcripts discovered on disk — deduped by (agent, session_id). Enter resumes: if that session's pane is still open in the right directory it activates that tab, otherwise it opens a new one with the resume command prefilled but not executed.
Results are staged, and the staging is the design:
- Names match synchronously against data already in memory and render on the same frame as the keystroke.
- Content matches inside transcript text and appends below, under its own header, with a one-line snippet and a progress readout. Name results never reorder.
Content search reads transcripts off the main thread into an in-memory digest keyed on (path, len, mtime) — the len component makes growth append-only, so a transcript that grew re-reads only [old_len, EOF). Matching is literal (memchr::memmem), never fuzzy, so an identifier like POA-2236 stays one token. What counts as content is decided structurally rather than by regex: a "type":"user" envelope whose blocks carry a tool_use_id is a tool result, not something the user typed — tool output and pasted file bodies are excluded, and the footer says so rather than letting a half-answer look complete.
Three constraints worth recording for anyone implementing this
- The mixer must stay synchronous. Registering an
AsyncDataSource withholds all results — including the instant name matches — for up to INITIAL_RESULTS_TIMEOUT (500 ms) while the palette still shows the previous query's rows, and that is also why Enter is a no-op while loading (search_bar.rs). Feeding a sync source from a model that owns its own spawn avoids both.
- Section order is a tier, and the direction is counterintuitive: the mixer sorts ascending on
(priority_tier, score, source_order) and the search bar then reverses for top-down, so a higher tier renders higher. This inverts silently with no compile error.
SelectionUpdate::Top does not skip static separators. A section header at index 0 gets selected when the popup opens, and the user's first Enter does nothing at all.
Implementation (fork, reusing the existing command-palette view rather than a new modal): https://github.com/samithaj/warp/tree/samithaj/session-search — built on #14720.
Scope note
Content search covers Claude Code only, because Codex keeps no per-cwd transcript store — its sessions are date-keyed rollouts. Codex sessions still appear in name search via recorded handles.
Problem
Once you have run more than a handful of coding-agent sessions there is no way to find one. The project rail can only show what fits in a sidebar — it caps discovered sessions per project — so conversations that exist on disk are simply unreachable. On this machine that is 73 project directories and several thousand Claude transcripts.
Two questions go unanswered: "which conversation was the one about X?" and, more painfully, "where is that thing I pasted?"
Proposal
A popup (
Cmd+Shift+K, plus a magnifier in the rail header) over every known session — live tabs, stored session handles, and transcripts discovered on disk — deduped by(agent, session_id). Enter resumes: if that session's pane is still open in the right directory it activates that tab, otherwise it opens a new one with the resume command prefilled but not executed.Results are staged, and the staging is the design:
Content search reads transcripts off the main thread into an in-memory digest keyed on
(path, len, mtime)— thelencomponent makes growth append-only, so a transcript that grew re-reads only[old_len, EOF). Matching is literal (memchr::memmem), never fuzzy, so an identifier likePOA-2236stays one token. What counts as content is decided structurally rather than by regex: a"type":"user"envelope whose blocks carry atool_use_idis a tool result, not something the user typed — tool output and pasted file bodies are excluded, and the footer says so rather than letting a half-answer look complete.Three constraints worth recording for anyone implementing this
AsyncDataSourcewithholds all results — including the instant name matches — for up toINITIAL_RESULTS_TIMEOUT(500 ms) while the palette still shows the previous query's rows, and that is also why Enter is a no-op while loading (search_bar.rs). Feeding a sync source from a model that owns its ownspawnavoids both.(priority_tier, score, source_order)and the search bar then reverses for top-down, so a higher tier renders higher. This inverts silently with no compile error.SelectionUpdate::Topdoes not skip static separators. A section header at index 0 gets selected when the popup opens, and the user's first Enter does nothing at all.Implementation (fork, reusing the existing command-palette view rather than a new modal): https://github.com/samithaj/warp/tree/samithaj/session-search — built on #14720.
Scope note
Content search covers Claude Code only, because Codex keeps no per-cwd transcript store — its sessions are date-keyed rollouts. Codex sessions still appear in name search via recorded handles.