Back to Fresh

Global / Universal Search — UX Design

docs/internal/global-search-ux.md

0.3.948.8 KB
Original Source

Global / Universal Search — UX Design

Status: Design Document (UX exploration — wireframes + scope taxonomy) Date: May 2026 Branch: claude/live-grep-global-search-ux-EFOGo Driving idea: Grow today's Live Grep (project-wide text search) into a single "one-stop" universal search that can also look inside terminals (including closed ones), diagnostics, git history, other worktrees, and all Orchestrator sessions — with an explicit, visible scope picker so the user controls where the search runs.


1. Goal

Live Grep today searches project files only. The ask is to turn it into a universal search surface where the user can opt scopes in and out from checkboxes in the overlay's toolbar, so it becomes the place you reach for whenever you're trying to find anything — not just text in tracked files.

Two concrete behavioural changes are wanted up front:

  1. Stop searching ignored files by default. Today this is perceived as noisy; ignored/hidden files should be an explicit, off-by-default opt-in.
  2. Include open terminals by default. Terminal scrollback is often where the thing you're hunting for actually is (a stack trace, a printed path, a command you ran).

Everything else (closed-terminal history, diagnostics, git history, other worktrees, all sessions, …) is opt-in.


2. How Live Grep works today (grounding)

Captured live from this repo (Alt+/ on crates/fresh-editor/src/app/workspace.rs, query terminal):

┌────────────────────────────────────────────────────────────────────┬───────────────────────────────┐
│Live grep: terminal                                          1 / 1000 │▾ 29 │   else                  │
│Provider: git-grep · 1000+ matches · Alt+P switch grep provider · Alt+M save … │ 30 │   VERSION=$(grep …    │  ← PREVIEW
│──────────────────────────────────────────────────────────────────────────────│ 31 │   fi                  │     PANE
│  .github/workflows/release-npm.yml:54  "description": "A modern termi… │ 32 │   echo "version=…    │
│  CHANGELOG.md:9                        New built-in **environment-man… │ 33 │   echo "Publishing…  │
│  CHANGELOG.md:43                       * **Terminals**: line-number g… │ 34 │                       │
│  CHANGELOG.md:45                       * **Closed terminals no longer… │ 35 │   - name: Setup Node… │
│  … (results list, grouped/streamed)                                    │ …                       │
└────────────────────────────────────────────────────────────────────┴───────────────────────────────┘
 Found 1000 matches                                                          [status bar]

Anatomy of the floating overlay:

  • Row 1 — input: Live grep: <query> with a right-aligned selected / total counter.
  • Row 2 — toolbar/title: Provider: <name> · [1000+ matches] · <hint> · <hint>. This is the only "chrome" row today and it is purely informational — there are no controls in it.
  • Separator, then a two-pane body: results list (left, file:line content) and a preview pane (right, the file around the match).
  • Status bar (host, bottom): Found N matches.

Implementation facts that shape the design:

  • It's a plugin. crates/fresh-editor/plugins/live_grep.ts drives a Finder overlay (plugins/lib/finder.ts) and owns a provider registry: git-grep (default inside a repo) → rgagackgrep, selected by priority + an isAvailable() probe. Each provider shells out and normalises to { file, line, column, content }. Alt+P cycles providers.
  • Results are homogeneous today — every row is a file:line:col location, so "open" just means editor.openFile(file, line, col).
  • A "Return to Work" resume cache lives in core (services/live_grep_state.rs): last query, selected index, and a display cache of matches so Alt+R re-opens the overlay without re-running the search.
  • Checkbox/toggle widgets already exist. plugins/lib/widgets.ts exports toggle(checked, label) rendered as [v] label / [ ] label, plus row/col/button/spacer/flexSpacer. search_replace.ts already ships an options row built from these ([v] All files [ ] Case [ ] Regex [ ] Word), with Alt+C/R/W mnemonics and Tab focus. The new toolbar should reuse this exact vocabulary for consistency (NN/g #4).

2.1 Terminal scrollback is already under the data dir

A central worry in the brief was "we may need to move scrollback buffers under the data dir." Good news: they already are.

  • DirectoryContext::terminals_dir()data_dir/terminals ($XDG_DATA_HOME/fresh/terminals/ on Linux), and terminal_dir_for(working_dir) namespaces by an encoded working-dir path (config_io.rs). Backing files are not written into the project tree.
  • Each terminal streams scrollback incrementally to its backing file (services/terminal/term.rs::flush_new_scrollback).

So the storage location is fine. The blocker for searching closed terminals is the retention policy: on explicit close, the backing file is deleted

// crates/fresh-editor/src/app/buffer_close.rs (close path)
let _ = self.authority.filesystem.remove_file(path);   // backing file

Required change for closed-terminal search: stop deleting on close; instead retain the backing file and garbage-collect by age / count / total size. A small index (terminal id → cwd, shell, title, closed-at, byte size, path) makes retained terminals discoverable and GC-able. This is the only storage change needed; see §8.

2.2 Where the "searches ignored files" feeling comes from

The default provider (git-grep) and rg both respect .gitignore. The rg built-in additionally hard-excludes .git, node_modules, target, *.lock. The annoyance is real in two cases: (a) when no VCS provider is available and a raw grep -rn . runs, and (b) for users who want ignored content excluded but have no visible control to confirm it. Either way the fix is the same: make "ignored & hidden files" a visible, off-by-default toggle rather than a provider-dependent accident (NN/g #6 recognition, #1 visibility).


3. What might a user want to search in?

The core design question. Below is the full candidate taxonomy, grouped, with a proposed default. The grouping matters because it drives the wireframes — 12 flat checkboxes is a usability failure (NN/g #8 minimalist design), but 4 groups of 2–3 is scannable.

GroupScopeDefaultNotes / how
FilesProject files (tracked / not ignored)ONtoday's behaviour
Ignored & hidden files (.gitignore, dotfiles)OFFflips rg/grep flags; fixes §2.2
Open buffers incl. unsaved editsONon-disk grep misses unsaved changes; search live buffer text
TerminalsOpen terminals (live scrollback)ONsearch current backing files
Closed terminal history (retained)OFFneeds §8 retention; can be large/old
Code intelligenceLSP diagnostics (all buffers / workspace)OFFmatch on message text; jump to range
Workspace symbolsOFFworkspace/symbol; different match semantics (names not lines)
History / VCSGit history (commit messages + diffs)OFFgit log -G<re> / git grep <rev>; jump to commit/line
Multi-rootAll git worktreesOFFiterate worktree roots
All Orchestrator sessionsOFFeach session ≈ one worktree (see orchestrator-sessions-design.md)

Stretch / later candidates (enumerated so we don't forget them, but not proposed for v1 — adding them now would violate minimalist design):

  • File names / paths (find a file, not its contents) — overlaps Quick Open.
  • Bookmarks (app/bookmarks.rs) and TODO/FIXME markers.
  • Recent files / file history, command history, clipboard/kill-ring.
  • Settings keys, help/docs, command palette commands.
  • Quickfix snapshots already exported from prior searches.
  • Plugin / LSP logs under the state dir.

Design stance: ship the 10 grouped scopes above; expose the long tail later behind the same "more scopes" affordance rather than the toolbar. A "one-stop search" is judged by trustworthy defaults + easy opt-in, not by how many checkboxes are visible at once.

3.1 Defaults on open (the "same as today, mostly" rule)

Opening universal search (the rebound Alt+/) starts with:

[v] Project files   [ ] Ignored/hidden   [v] Open buffers   [v] Open terminals
[ ] Closed terminals  [ ] Diagnostics  [ ] Symbols  [ ] Git history  [ ] Worktrees  [ ] Sessions

i.e. today's project search, minus ignored files, plus open terminals + unsaved buffers. Toggles are sticky within a session and persisted to workspace state, so a user who turns on "Git history" keeps it until they turn it off. A Default scope reset (and named presets, §6/Alt-D) covers "put it back."


4. NN/g heuristics driving the design

HeuristicApplication
#1 Visibility of system statusPer-scope match counts in the toolbar (Files 320 · Terminals 14 · Git 5); spinner per source while streaming; clear zero-result state.
#2 Match real worldPlain labels ("Open terminals", "Git history"), not flags (--no-ignore).
#3 User control & freedomOne keystroke per scope; Esc closes; resume (Alt+R) unchanged.
#4 Consistency & standardsReuse toggle() widget + Alt+letter pattern from search_replace.ts.
#5 Error preventionIgnored/hidden off by default; expensive scopes (closed terminals, all sessions) off by default and visibly labelled.
#6 Recognition over recallScopes are visible checkboxes, not memorised CLI flags.
#7 Flexibility & efficiencyMnemonics for experts; presets ("Code", "Everything", "Terminals") for one-shot scoping.
#8 Aesthetic & minimalistGrouping + progressive disclosure — primary toggles inline, long tail behind "More ▾".
#9 Recover from errorsIf a source fails (no git, provider missing) show a per-source inline note, don't fail the whole search.
#10 Help & docsHint row shows the mnemonics; each scope has a tooltip/description line.

Central tension: discoverability of many scopes vs a crowded toolbar in a narrow terminal. The captured overlay's toolbar row already nearly overflows with just provider + 2 hints. The alternatives below differ mainly in how they resolve this tension.


5. Wireframe alternatives

All wireframes keep the existing two-pane body (results + preview) and status bar; they differ in the toolbar / scope-control region.

Alternative A — Flat inline checkbox toolbar (wrap to 2 rows)

The literal reading of the brief: more checkboxes in the top toolbar row.

┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Search: terminal│                                                            128 matches · 1 / 128   │
��� [v] Project  [ ] Ignored  [v] Buffers  [v] Terminals  [ ] Closed term  [ ] Diagnostics  [ ] Git ▸  │
│ Provider: git-grep · Alt+P provider · Alt+M save · Tab to focus scopes                              │
│────────────────────────────────────────────────────────────────────────────────────────────────── │
│  src/app/workspace.rs:12   all terminal backing files contain complete state …                     │
│  term/manager.rs:465       "Terminal backing file write error: {}"                                  │
│  ⟫ TERMINAL  (closed) build-2  npm ERR! code ELIFECYCLE  …                          [source: term]  │
│  …                                                                                                  │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘
  • Pros: dead simple, exactly matches search_replace.ts, zero new widgets, fully keyboard-navigable today.
  • Cons: 10 scopes do not fit on one 80-col row; wrapping to 2–3 rows eats vertical space that the results list needs; the overflow for the tail is awkward; no per-source counts without more width.
  • Verdict: fine for ≤4 scopes, breaks down at 10. Good fallback for narrow terminals (see Alt-B's responsive collapse).

Progressive disclosure. The 4 default-on-ish scopes stay inline; everything else lives behind a Scopes ▾ button that opens a checklist popover. The button shows a summary count of active scopes.

┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Search: terminal│                                                            128 matches · 1 / 128   │
│ [v] Files  [ ] Ignored  [v] Buffers  [v] Terminals   ▏  Scopes (4) ▾   ▏  Provider: git-grep        │
│────────────────────────────────────────────────────────────────────────────────────────────────── │
│  Files 96 · Buffers 18 · Terminals 14                                       Alt+P provider · Alt+M  │
│────────────────────────────────────────────────────────────────────────────────────────────────── │
│  src/app/workspace.rs:12   all terminal backing files contain complete state …                     │
│  … results …                                                                                        │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

   pressing  Scopes ▾  (or Tab → Space) drops a popover:

   ┌─ Search in ───────────────────────────┐
   │ FILES                                  │
   │   [v] Project files          Alt+1     │
   │   [ ] Ignored & hidden       Alt+2     │
   │   [v] Open buffers (unsaved) Alt+3     │
   │ TERMINALS                              │
   │   [v] Open terminals         Alt+4     │
   │   [ ] Closed terminal history Alt+5    │
   │ CODE INTEL                             │
   │   [ ] Diagnostics            Alt+6     │
   │   [ ] Workspace symbols      Alt+7     │
   │ HISTORY & SCOPE                        │
   │   [ ] Git history            Alt+8     │
   │   [ ] All worktrees          Alt+9     │
   │   [ ] All sessions           Alt+0     │
   │ ─────────────────────────────────────  │
   │  Presets: Code · Everything · Terminals │
   │  [Reset to defaults]                    │
   └─────────────────────────────────────────┘
  • Pros: toolbar stays uncluttered (NN/g #8); the popover groups scopes so they're scannable (NN/g #6); per-source counts fit on the secondary row (NN/g #1); scales to the long-tail scopes without redesign; the inline 4 are the high-frequency ones so most sessions never open the popover.
  • Cons: the rarer scopes are one interaction away (acceptable — they're rare); one new popover widget (though it's just a col() of toggle()s in an existing overlay).
  • Verdict: best balance of discoverability and restraint. Recommended, ideally combined with Alt-D presets inside the popover. Narrow terminals collapse the inline 4 into the popover too (everything behind Scopes ▾), which is the responsive answer to Alt-A's wrapping problem.

Alternative C — Left "Sources" rail (three-pane)

A persistent vertical list of sources on the far left, each a checkbox with its own live count; results in the middle; preview on the right.

┌────────────────┬───────────────────────────────────────────────┬───────────────────────────────┐
│ SEARCH IN      │ Search: terminal                  1 / 128      │  PREVIEW                       │
│ [v] Files   96 │───────────────────────────────────────────────│  12 │ all terminal backing …   │
│ [ ] Ignored  – │  src/app/workspace.rs:12  all terminal back…   │  13 │ before serializing …     │
│ [v] Buffers 18 │  term/manager.rs:465      "Terminal backing…   │  …                             │
│ [v] Terminal14 │  ⟫ build-2 (closed)  npm ERR! ELIFECYCLE …     │                                │
│ [ ] Closed   – │  diag  workspace.rs:120  unused import `Foo`   │                                │
│ [ ] Diagnos  – │  git  a1b2c3d  "fix terminal backing file…"    │                                │
│ [ ] Symbols  – │  …                                             │                                │
│ [ ] Git      – │                                                │                                │
│ [ ] Worktree – │                                                │                                │
│ [ ] Sessions – │                                                │                                │
│ ────────────── │                                                │                                │
│ Presets ▸      │                                                │                                │
└────────────────┴───────────────────────────────────────────────┴───────────────────────────────┘
  • Pros: every scope + its live count visible at once (strong NN/g #1); toggling a source instantly re-filters; mirrors VS Code's search-sources mental model; great on wide screens.
  • Cons: three panes is tight under ~120 cols — preview gets squeezed; a big departure from today's two-pane overlay (more implementation + a new layout); the rail is always-on chrome even for users who never change scopes.
  • Verdict: most powerful for "command-center" search, best for wide monitors. Hold as a power-user / future layout, possibly a toggle (Alt+B shows/hides the rail). Not the default because of the narrow-terminal squeeze.

Alternative D — Scope presets (segmented control)

Lead with named presets instead of individual checkboxes; expose the raw toggles only via "Customize."

┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Search: terminal│                                                            128 matches · 1 / 128   │
│ Scope:  ( Code )  Everything   Terminals   This session    ·   Customize ▾                          │
│────────────────────────────────────────────────────────────────────────────────────────────────── │
│  … results, grouped by source …                                                                     │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

  Code         = Project files + Open buffers
  Everything   = + Ignored/hidden + Terminals (open+closed) + Diagnostics + Git history
  Terminals    = Open + Closed terminal history only
  This session = Files + Buffers + Terminals for the active worktree/session only
  • Pros: fastest for the common intents (NN/g #7); tiny footprint; "Everything" literally delivers the "one-stop" promise in one click; great recognition.
  • Cons: presets hide which scopes are active unless we annotate; users still need the raw toggles for odd combinations (hence "Customize"); preset names need care to be self-explanatory.
  • Verdict: excellent complement, weak as the sole mechanism. Fold the presets into Alt-B's popover (and optionally surface them as a one-line segmented control) — presets for speed, checkboxes for precision.

6. Recommendation

Adopt Alternative B (inline primary toggles + grouped Scopes ▾ popover) with Alternative D presets embedded in that popover, and keep Alternative C's sources-rail as an opt-in power layout behind a toggle for wide terminals.

Rationale: B keeps the everyday overlay calm and fast (most searches use the four default scopes and never open the popover), satisfies discoverability through a grouped checklist rather than a wall of checkboxes, and scales to the long-tail scopes without another redesign. D makes the "one-stop everything" intent a single choice. C remains the answer for users who want a persistent command center on a big screen.

Responsive behaviour: under a width threshold, collapse the inline toggles into Scopes ▾ so a narrow terminal never wraps the toolbar (this is the principled fix for Alt-A's overflow).


7. Cross-source result model

Universal search breaks the "every row is file:line" assumption. A unified result needs:

  • a source tag (small glyph or short label, themed) so a row's origin is obvious at a glance — e.g. file, buf, term, diag, sym, git;
  • grouping by source in the list (collapsible headers with counts), so a burst of terminal hits doesn't bury file hits;
  • a type-appropriate activate action:
    • file / buffer / ignored → open at file:line:col (today's path);
    • open terminal → focus that terminal + scroll to the line in scrollback;
    • closed terminal → open its retained backing file as a read-only buffer at the line (we already restore terminals this way — workspace.rs:: load_terminal_backing_file_as_buffer);
    • diagnostic → open the buffer at the diagnostic range;
    • symbol → go-to-definition location;
    • git history → open the commit/diff (reuse git_log plugin) at the hit.

The plugin's provider registry generalises cleanly: today providers return GrepMatch[]; universal search makes each scope a streaming source that yields tagged results into one merged, capped, debounced list. Per-source caps keep one chatty source (e.g. a 200 MB terminal log) from starving the rest, and per-source failures degrade gracefully (NN/g #9) — a missing git binary shows "Git history: git not found", it doesn't abort the file search.


8. Storage / retention change (closed terminals)

The only backend change the UX depends on. Scrollback already lives under data_dir/terminals/<encoded-cwd>/; the work is retention + discovery:

  1. Don't delete on close. In buffer_close.rs, stop remove_file-ing the terminal backing file when "search closed terminals" is a feature; move that file into a retained area (or just leave it and mark it closed).
  2. Index retained terminals. A manifest (JSON under data_dir/terminals/) mapping terminal_id → { cwd, shell, title, closed_at, bytes, path } so the closed-terminals scope can enumerate, label ("build-2 (closed 2h ago)"), GC, and open them.
  3. Garbage collect. Bound by count / age / total bytes (config). Without a cap, retained scrollback grows unbounded — note term.rs already keeps a "generous scrollback" per live terminal, so retained logs can be large.
  4. Scope by cwd by default. Because files are namespaced per working dir, "closed terminals" naturally means this project's closed terminals; "all sessions / all worktrees" widens to other encoded-cwd dirs.

Privacy note worth flagging to the user: retaining closed-terminal scrollback means commands and their output (possibly secrets) persist on disk after close. Retention should be opt-in / configurable with a clear setting and a way to purge (NN/g #5, #3).


9. Keyboard & interaction model

  • Rebind Alt+/ to open universal search (the overlay below); keep Alt+R resume and Alt+P provider-cycle behaviour.
  • Tab moves focus into the toolbar; Space toggles the focused scope; Alt+1…0 toggle scopes directly (mnemonic map shown in the popover).
  • Scopes ▾ opens/closes the popover (e.g. Alt+S).
  • Preset keys optional (e.g. number row when popover focused) — or click.
  • Toggling any scope re-runs the (debounced) search and updates per-source counts; flipping a scope off is instant client-side hide when results are cached, full re-run otherwise.
  • Sticky scopes persisted to workspace state; Reset to defaults restores §3.1.

10. Phasing

  1. P0 — Scope toolbar + the two behavioural fixes. Inline toggles (Alt-B primary row) for Files / Ignored / Buffers / Open terminals. Default ignored off, terminals on. No backend retention yet (open terminals only). Delivers the headline asks immediately.
  2. P1 — Scopes ▾ popover + grouping + per-source counts + tagged results. Adds Diagnostics, Symbols, Git history as streaming sources.
  3. P2 — Closed-terminal retention (§8) and the Closed-terminals scope.
  4. P3 — Multi-root: All worktrees, All Orchestrator sessions.
  5. P4 — Presets (Alt-D) and the opt-in sources rail (Alt-C) for wide terminals; long-tail scopes (names, bookmarks, history, clipboard…).

10b. Implementation status (live)

Landed in live_grep.ts + keymap + i18n (no core Rust changes):

  • Scope model & fan-out. search() runs every enabled scope and merges tagged GrepMatches into one capped list; non-file rows carry a source badge (ign / buf / term / diag).
  • Toolbar checkboxes rendered via setPromptTitle ([v] Files [ ] Ignored [v] Buffers [v] Terminals [ ] Diagnostics · Provider: …).
  • Toggles need no new core Action. In prompt context the host resolves any Alt+<char> against the keymap and dispatches unknown action names as plugin actions, so Alt+L/H/U/T/Dlive_grep_toggle_* handlers. This is the reusable mechanism for future scope toggles.
  • Scopes implemented: Files (provider), Ignored (rg --no-ignore --hidden / git-grep --untracked --no-exclude-standard), Buffers (modified open buffers via getBufferText), Terminals (grep the <data_dir>/terminals/*.txt backing files, ANSI-stripped), Diagnostics (getAllDiagnostics). Defaults: Files/Buffers/Terminals on, Ignored/ Diagnostics off.

Known limitations / follow-ups:

  • Terminals scope spans all projects Resolved. The host now exposes getTerminalDir() (→ <data_dir>/terminals/<encoded-cwd>/ via a new PluginServiceBridge::terminal_dir(working_dir) that delegates to DirectoryContext::terminal_dir_for). The scope greps only the current project/worktree's subdir. Verified: a planted other-project log is not matched.
  • Terminal hits open the backing file at the line, not the live terminal. Fine for "find it"; focusing the live terminal is a refinement.
  • Toggles only work in the live overlay, not the Resume (cached) overlay.

Closed-terminal retention — LANDED

Backing files are named by terminal id: <data_dir>/terminals/<encoded-cwd>/ fresh-terminal-<id>.txt. Terminal ids restart per session, so simply not deleting the file on close would let a new terminal with the same id clobber a retained log from a prior session.

Implemented in buffer_close.rs: on terminal close the rendered backing file is renamed to fresh-terminal-<id>-closed-<epoch_ms>.txt (instead of deleted), which is collision-free against future same-id terminals. The raw .log is still deleted. A count-bounded GC (gc_retained_terminal_backings, currently MAX_RETAINED = 200 per dir) prunes the oldest retained files, ordering by the epoch embedded in the filename so it needs no filesystem metadata. The Terminals scope already globs *.txt, so retained logs are searchable with no plugin change — verified end-to-end: open terminal → produce scrollback → close → Universal Search finds the hit in the -closed- file.

Retention is currently unconditional (the chosen "on by default" stance). Follow-ups: a config toggle to disable, a "purge terminal history" command, age/size-based GC limits, and the per-cwd listTerminalLogs() host API (§8) so the scope can scope to the current project and show friendly titles instead of raw paths.

11. Open questions

  • Result ordering across sources — interleave by relevance, or always group by source? (Proposed: group, with a "flatten" option.)
  • Ignored vs hidden — one toggle or two? (Proposed: one "Ignored & hidden"; split later if users ask.)
  • Buffers as a scope vs always-on overlay — should unsaved-buffer hits always shadow on-disk hits regardless of the Files toggle? (Proposed: Buffers is its own scope, default on.)
  • Closed-terminal retention default — off (privacy) vs on (utility)? Proposed off, with an obvious enable + purge.
  • Presets — ship fixed presets, or user-definable saved scopes from the start?

12. Toolbar layout redesign — full-width header band

Follow-up after the scope toolbar shipped: the checkboxes don't fit. This section examines the current rendering, states the goals, assesses the widget engine, and proposes layout alternatives with wireframes.

12.1 The problem (observed live)

The Live Grep overlay (render_overlay_prompt in app/render.rs) is an 80%×80% centred frame. Today it lays the input + toolbar inside the left results_area and gives the preview the entire right half for the full height:

overlay (≥120 cols → split):
┌──────────────────────────────────────────────┬─────────────────────────────┐
│ Search: terminal                     1 / 1000 │ preview pane                │
│ [v] Files  [ ] Ignored  [v] Buffers  [v] T…   │  (full height, right half)  │   ← toolbar CUT at the
│ ──────────────────────────────────────────────│                             │     column divider
│ results list (left half only)                  │                             │
└──────────────────────────────────────────────┴─────────────────────────────┘

overlay (<120 cols → no preview): toolbar still overflows the single column:
│ [v] Files  [ ] Ignored  [v] Buffers  [v] Terminals  [ ] Diagnostics · Provider: git-grep · 100│  ← truncated

Two width sinks compound: (a) the preview steals the right half, so the toolbar only gets ~½ the frame; (b) even at full frame width the toolbar string (5 toggles + Provider: + match count + 2 key hints) is ~130 cols and overflows. Result: the last toggles and the hints are clipped.

12.2 Goals (from the request)

  1. Toolbar spans the full pane width. Move input + toolbar into a full-width header band at the top; put results | preview side-by-side below it (not beside the toolbar).
  2. Tab cycles focus: input field → each checkbox in turn (then results). Space/Enter toggles the focused checkbox.
  3. Overflow wraps: when the toggles don't fit on one line they flow onto additional lines, growing the header band's height (and shrinking the body area by the same amount).

12.3 Can we use the widget Row/Col system? — Yes

crates/fresh-editor/src/widgets/render.rs::render_spec(spec, prev, prev_focus_key, panel_width) already renders Row/Col/Toggle/Button/ TextInput/Spacer(flex) and returns:

  • entries — the rendered OverlayRows (drop straight into the band),
  • tabbable + focus_key + focus_cursorTab focus is built in,
  • hits — mouse hit-testing (click a checkbox to toggle), and
  • instance_states — persisted per-widget state across re-renders.

This is the same engine the floating_widget_panel and the search/replace options row use ([v] All files [ ] Case [ ] Regex [ ] Word). So the toolbar can be a real row(toggle(...), toggle(...), …) spec rather than a hand-built styled-text string — gaining focus, theming, and click for free.

Two gaps to close:

  • Row is single-line (no wrap field). For goal #3 we either (a) chunk the toggles into multiple row()s inside a col() in the plugin, using the known frame width (search/replace already reads panel.viewportWidth to do width math), or (b) add a wrap: bool to the Row widget and let the engine reflow. (a) is no core change; (b) is the cleaner long-term primitive and also helps other plugins.
  • The overlay input is drawn by hand today, not as a TextInput widget, and Tab isn't routed to the widget focus cursor. The header band needs to own a combined focus ring: input ↔ toggles.

12.4 Layout alternatives

Alternative A — Full-width header band (RECOMMENDED)

Header (input + wrapped toggles + meta) spans the full inner width; the results/preview split lives below it.

┌─ Universal Search ──────────────────────────────────────────────────────────┐
│ Search: terminal                                                    1 / 1000  │  ← input row (full width)
│ [v] Files ⌥L   [ ] Ignored ⌥H   [v] Buffers ⌥U   [v] Terminals ⌥T   [ ] Diag ⌥D│  ← toggles, each with its
│ git-grep ⌥P · 1000+ matches                                                    │     own accelerator inline
├───────────────────────────────────────────┬──────────────────────────────────┤
│ .github/workflows/release.yml:320  desc…   │  29 │   else                     │
│ CHANGELOG.md:43  **Terminals**: line-num…  │  30 │   VERSION=$(grep …          │
│ [term] …closed-….txt:2  ZZTERMTOKEN…        │  31 │   fi                       │
│ …                                           │  …                               │
├───────────────────────────────────────────────────────────────────────────--─┤
│ Tab move · Space toggle · Enter open · Esc close                              │  ← footer: generic hints only
└───────────────────────────────────────────────────────────────────────────────┘

The ⌥L/⌥H/… accelerators and ⌥P render in the keybinding-hint theme colour (ui.help_key_fg, the same key the current toolbar already uses for its hint segments), so they read as "press this to reach this control" rather than as part of the label.

Narrow terminal (toggles wrap; band grows, body shrinks):

┌─ Universal Search ───────────────────────────────────┐
│ Search: terminal                           1 / 1000   │
│ [v] Files ⌥L   [ ] Ignored ⌥H   [v] Buffers ⌥U        │  ← wrapped to
│ [v] Terminals ⌥T   [ ] Diagnostics ⌥D                 │     two lines
│ git-grep ⌥P · 1000+ matches                           │
├──────────────────────────────┬────────────────────────┤
│ results                      │ preview                │
└──────────────────────────────┴────────────────────────┘
  • Pros: directly satisfies all three goals; toggles get full width so all five fit on one line at ≥~90 cols; preview keeps its side-by-side value; minimal disruption to the Finder's streaming/preview machinery (only the band geometry + a widget render are new).
  • Cons: header eats 3 rows (input + toggles + meta); on a short terminal that's noticeable. Mitigate by folding the meta row into the bottom footer (setPromptFooter) so the band is input + toggles only.
  • Verdict: recommended. It's the literal request and the smallest change that uses the widget engine.

Alternative A′ — A, but generic hints in the footer (leanest header)

Same as A, except the footer carries only the generic, always-available hints (Tab/Shift+Tab move, Space toggle, Enter open, Esc close). The header keeps the input + toggles, and each toggle still carries its own accelerator inline (⌥L/⌥H/…) — see §12.5 for why per-control hints don't belong in the footer.

┌─ Universal Search ──────────────────────────────────────────────────────────┐
│ Search: terminal                                                    1 / 1000  │
│ [v] Files ⌥L   [ ] Ignored ⌥H   [v] Buffers ⌥U   [v] Terminals ⌥T   [ ] Diag ⌥D│
├───────────────────────────────────────────┬──────────────────────────────────┤
│ results                                     │ preview                          │
├─────────────────────────────────────────────────────────────────────────────-┤
│ Tab move · Space toggle · Enter open · Esc close              git-grep · 1000+ │  ← footer: generic hints
└───────────────────────────────────────────────────────────────────────────────┘
  • Pros: header is just 2 rows; generic hints live where users already look for them (footer); setPromptFooter already exists.
  • Cons: provider/count slightly less glanceable than in a dedicated row.
  • Verdict: best if vertical space is precious; pairs well with A.

Alternative B — Left scope rail (full-width input on top)

Input spans the top; a narrow checkbox rail sits left of the results, preview on the right.

┌─ Universal Search ──────────────────────────────────────────────────────────┐
│ Search: terminal                                                    1 / 1000  │
├──────────────────┬───────────────────────────────┬───────────────────────────┤
│ SEARCH IN        │ results                       │ preview                   │
│ [v] Files     96 │  release.yml:320  desc…       │  29 │ else                │
│ [ ] Ignored    – │  CHANGELOG.md:43  **Term…     │  30 │ VERSION=$(grep …    │
│ [v] Buffers   18 │  [term] …closed.txt:2  ZZ…    │  31 │ fi                  │
│ [v] Terminals 14 │  …                            │  …                          │
│ [ ] Diagnos    – │                               │                           │
└──────────────────┴───────────────────────────────┴───────────────────────────┘
  • Pros: never overflows (vertical list); shows per-scope counts; Tab walks the rail naturally; great on wide screens.
  • Cons: three columns is tight under ~120 cols (preview squeezed); always-on rail chrome even when the user never changes scopes; larger departure from today's two-pane overlay.
  • Verdict: strong "command-center" option (matches §5 Alt-C), but heavier than A and weak on narrow terminals. Hold as an optional wide-screen mode.

Alternative C — Single adaptive header line, wrap on demand

Toggles share the input line when there's room; spill below only when needed.

wide:   │ Search: terminal          [v]Files [ ]Ign [v]Buf [v]Term [ ]Diag  1/1000 │
narrow: │ Search: terminal                                              1 / 1000   │
        │ [v]Files [ ]Ign [v]Buf [v]Term [ ]Diag                                  │
  • Pros: most compact (1 row when wide); no wasted vertical space.
  • Cons: cramped — input and toggles fight for one line; abbreviated labels hurt recognition (NN/g #6); wrap math is fiddlier.
  • Verdict: clever but the weakest for clarity. Not recommended as the default; could be a "compact" density option later.

Two kinds of hints, two homes:

  • Generic, always-available actions (Tab/Shift+Tab to move focus, Space to toggle the focused control, Enter to open, Esc to close) → the footer. They aren't tied to one widget, so a single shared line is the natural place and keeps the controls uncluttered.
  • A specific control's accelerator (the Alt+key that jumps straight to that control) → rendered at the control itself, in the keybinding-hint colour. Alt+L means "Files", so it belongs next to the Files toggle, not buried in a footer list the user has to scan. This is recognition-over-recall (NN/g #6): the affordance sits where the action happens. The provider accelerator (⌥P) likewise sits on the provider label, since that's the "control" it cycles.

Concretely each toggle renders [v] <label> <accel> where <accel> (e.g. ⌥L) uses the ui.help_key_fg theme key — the same hint colour the toolbar already uses — so it's visually distinct from the white label. Action-only hints with no on-screen control (e.g. "save matches", Alt+M) have nowhere to attach, so those stay in the footer.

Accelerators are width-cheap (~3 cols each) but can be dropped first when wrapping is tight on a very narrow terminal — the label and checkbox are the essential part; the Alt+key is progressive enhancement.

12.5b Tab-focus model (applies to A/A′/B)

Single focus ring owned by the header band, driven by render_spec's tabbable/focus_key:

[ input ] → [v]Files ⌥L → [ ]Ignored ⌥H → [v]Buffers ⌥U → [v]Terminals ⌥T → [ ]Diag ⌥D → (results) ↺
  • Tab / Shift+Tab advance / retreat the focus cursor.
  • When a toggle is focused, Space (and Enter) flips it and re-runs the search; when the input is focused, typing edits the query as today.
  • The per-control Alt+L/H/U/T/D shortcuts keep working regardless of focus (direct jumps/toggles), so power users skip the ring entirely.
  • Mouse: render_spec emits hits, so clicking a checkbox toggles it.

12.5c Mouse-modal overlay (LANDED)

The floating overlay is mouse-modal: while it's open, only its own targets act on a click (result list, scrollbar, and — once wired — the toolbar controls); every other mouse event is swallowed so it never reaches the buffer behind the overlay and moves its cursor. Implemented in mouse_input.rs for left-click, double-click, triple-click, right-click, Ctrl+right-click, and drag (only the overlay's own scrollbar drag is honoured). Covered by test_live_grep_overlay_is_mouse_modal. This was a prerequisite for the full-width header band, which sits directly over the buffer — without it, a click on the toolbar row landed in the buffer.

12.6 Overflow / wrap (goal #3) — two ways

  1. Plugin-side chunking (no core change). The plugin learns the frame width (expose it like the search/replace panel's viewportWidth, or a getOverlayWidth()), greedily packs toggles per line, and emits col(row(t,t,t), row(t,t)). The band height = number of rows.
  2. Row { wrap: true } (core primitive). Teach the widget engine to reflow a Row's children onto new lines when panel_width is exceeded. More reusable (every plugin benefits) and keeps the spec declarative. Preferred if we touch the engine anyway.

Either way, render_overlay_prompt must compute the band height from the rendered toolbar (not assume 1 line) and subtract it from the body before splitting results | preview.

12.7 Recommendation & sketch

Ship Alternative A (header band) + A′ (hints in footer), render the toolbar via render_spec, and do overflow with Row { wrap } so the engine owns reflow. Keep Alternative B's scope-rail as a future wide-screen "sources" mode.

Implementation sketch:

  • Core (render.rs::render_overlay_prompt): build the band as col(input_row?, wrapping_toggle_row); render with render_spec at panel_width = inner.width; measure its row count band_h; lay out header = inner[0..band_h] full width, then split inner[band_h..] into results | preview. Route Tab/Space/click to the band's focus cursor; move the input into the ring (either as a TextInput widget or a hand-focus case that yields to the widget ring on Tab).
  • Plugin (live_grep.ts): replace the setPromptTitle(styledText) toolbar with a setPromptToolbar(spec)-style API that hands core the row(toggle…) spec. Each toggle carries its accelerator — either via a new Toggle { accel: Option<String> } field rendered in ui.help_key_fg, or, with zero engine change, by emitting row(toggle(checked,label), raw([{text:" ⌥L", style:{fg:"ui.help_key_fg"}}])) per control. Move only the generic hints (Tab/Space/Enter/Esc) to setPromptFooter; keep per-control accelerators inline.
  • Widget engine: add Row { wrap: bool } (+ height-aware layout); optionally Toggle { accel } so the accelerator is a first-class, themeable segment rather than a hand-built raw tail.

This is a core-rendering change (new setPromptToolbar plumbing + band geometry + focus routing + Row wrap), so it's a larger slice than the plugin-only work so far — flagged for sign-off before implementing.