docs/internal/global-search-ux.md
Status: Design Document (UX exploration — wireframes + scope taxonomy) Date: May 2026 Branch:
claude/live-grep-global-search-ux-EFOGoDriving 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.
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:
Everything else (closed-terminal history, diagnostics, git history, other worktrees, all sessions, …) is opt-in.
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:
Live grep: <query> with a right-aligned selected / total
counter.Provider: <name> · [1000+ matches] · <hint> · <hint>.
This is the only "chrome" row today and it is purely informational — there
are no controls in it.file:line content)
and a preview pane (right, the file around the match).Found N matches.Implementation facts that shape the design:
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) → rg → ag → ack → grep, selected by
priority + an isAvailable() probe. Each provider shells out and normalises
to { file, line, column, content }. Alt+P cycles providers.file:line:col location, so
"open" just means editor.openFile(file, line, col).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.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).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.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.
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).
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.
| Group | Scope | Default | Notes / how |
|---|---|---|---|
| Files | Project files (tracked / not ignored) | ON | today's behaviour |
Ignored & hidden files (.gitignore, dotfiles) | OFF | flips rg/grep flags; fixes §2.2 | |
| Open buffers incl. unsaved edits | ON | on-disk grep misses unsaved changes; search live buffer text | |
| Terminals | Open terminals (live scrollback) | ON | search current backing files |
| Closed terminal history (retained) | OFF | needs §8 retention; can be large/old | |
| Code intelligence | LSP diagnostics (all buffers / workspace) | OFF | match on message text; jump to range |
| Workspace symbols | OFF | workspace/symbol; different match semantics (names not lines) | |
| History / VCS | Git history (commit messages + diffs) | OFF | git log -G<re> / git grep <rev>; jump to commit/line |
| Multi-root | All git worktrees | OFF | iterate worktree roots |
| All Orchestrator sessions | OFF | each 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):
app/bookmarks.rs) and TODO/FIXME markers.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.
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."
| Heuristic | Application |
|---|---|
| #1 Visibility of system status | Per-scope match counts in the toolbar (Files 320 · Terminals 14 · Git 5); spinner per source while streaming; clear zero-result state. |
| #2 Match real world | Plain labels ("Open terminals", "Git history"), not flags (--no-ignore). |
| #3 User control & freedom | One keystroke per scope; Esc closes; resume (Alt+R) unchanged. |
| #4 Consistency & standards | Reuse toggle() widget + Alt+letter pattern from search_replace.ts. |
| #5 Error prevention | Ignored/hidden off by default; expensive scopes (closed terminals, all sessions) off by default and visibly labelled. |
| #6 Recognition over recall | Scopes are visible checkboxes, not memorised CLI flags. |
| #7 Flexibility & efficiency | Mnemonics for experts; presets ("Code", "Everything", "Terminals") for one-shot scoping. |
| #8 Aesthetic & minimalist | Grouping + progressive disclosure — primary toggles inline, long tail behind "More ▾". |
| #9 Recover from errors | If a source fails (no git, provider missing) show a per-source inline note, don't fail the whole search. |
| #10 Help & docs | Hint 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.
All wireframes keep the existing two-pane body (results + preview) and status bar; they differ in the toolbar / scope-control region.
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] │
│ … │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘
search_replace.ts, zero new widgets,
fully keyboard-navigable today.▸ overflow for the tail is
awkward; no per-source counts without more width.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] │
└─────────────────────────────────────────┘
col() of toggle()s in an
existing overlay).Scopes ▾),
which is the responsive answer to Alt-A's wrapping problem.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 ▸ │ │ │
└────────────────┴───────────────────────────────────────────────┴───────────────────────────────┘
Alt+B shows/hides
the rail). Not the default because of the narrow-terminal squeeze.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
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).
Universal search breaks the "every row is file:line" assumption. A unified
result needs:
file, buf, term, diag, sym, git;file:line:col (today's path);workspace.rs:: load_terminal_backing_file_as_buffer);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.
The only backend change the UX depends on. Scrollback already lives under
data_dir/terminals/<encoded-cwd>/; the work is retention + discovery:
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).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.term.rs already keeps a
"generous scrollback" per live terminal, so retained logs can be large.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).
Alt+/ to open universal search (the overlay below); keep
Alt+R resume and Alt+P provider-cycle behaviour.Alt+1…0 toggle scopes directly (mnemonic map shown in the popover).Scopes ▾ opens/closes the popover (e.g. Alt+S).Reset to defaults restores §3.1.Scopes ▾ popover + grouping + per-source counts + tagged results.
Adds Diagnostics, Symbols, Git history as streaming sources.Landed in live_grep.ts + keymap + i18n (no core Rust changes):
search() runs every enabled scope and
merges tagged GrepMatches into one capped list; non-file rows carry a
source badge (ign / buf / term / diag).setPromptTitle
([v] Files [ ] Ignored [v] Buffers [v] Terminals [ ] Diagnostics · Provider: …).Alt+<char> against the keymap and dispatches unknown action names
as plugin actions, so Alt+L/H/U/T/D → live_grep_toggle_* handlers.
This is the reusable mechanism for future scope toggles.--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:
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.Resume (cached) overlay.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.
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.
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.
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_cursor — Tab focus is built in,hits — mouse hit-testing (click a checkbox to toggle), andinstance_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.TextInput
widget, and Tab isn't routed to the widget focus cursor. The header band
needs to own a combined focus ring: input ↔ toggles.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 │
└──────────────────────────────┴────────────────────────┘
setPromptFooter) so the band is input + toggles only.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
└───────────────────────────────────────────────────────────────────────────────┘
setPromptFooter already exists.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 – │ │ │
└──────────────────┴───────────────────────────────┴───────────────────────────┘
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 │
Two kinds of hints, two homes:
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.
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.Space (and Enter) flips it and re-runs the
search; when the input is focused, typing edits the query as today.Alt+L/H/U/T/D shortcuts keep working regardless of focus
(direct jumps/toggles), so power users skip the ring entirely.render_spec emits hits, so clicking a checkbox toggles it.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.
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.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.
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:
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).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.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.