Search
The suv search command opens an interactive search TUI that lets you find, filter, and reuse commands from your recorded shell history. It supports literal-word matching with a forgiving fallback that ignores separators, a view of every run or of identical commands grouped, a Smart rank that learns which commands you run often, which usually fail and what you usually run next, a scrollable detail pane, and a rich set of keyboard shortcuts.
Basic Usage
Launch the interactive search TUI:
suv search Start typing to filter your history. Use the arrow keys to navigate, and press Enter (labelled Use) to put the selected command on your prompt for review when you opened search with Ctrl+R, or to print it when you ran suv search directly. Enter never runs anything.
How Matching Works
Each word you type must appear as a literal substring in the searched field (in any order) — so results always contain what you typed. Matching is case-insensitive. If nothing matches as typed, a terms query is tried once more with separators ignored, so hiveagent finds hive-agent (see below). Case is folded as described in Upper and lower case. Results are ranked by match quality: the query as a prefix first, then as a contiguous phrase, then your words in order, then in any order. Recall and search show your own (human-typed) commands by default; press Ctrl+A to include AI-agent commands. See How results are ranked for the full ordering.
Since 0.4.0, database-level substring queries on the command field have an FTS5 trigram index, and since 0.5.1 every terms and literal query actually uses it: before, SQLite never handed those queries to the index, so each one scanned the whole history. On the 100,000-entry benchmark corpus (Apple M4 Max, BENCHMARKS.md) most judged queries now return in well under a millisecond instead of 10–40 ms, with the same results; fuzzy is about twice as fast, and prefix mode fell from 15 ms to 70 µs. Since 0.4.2, text you type or edit inside the open TUI takes the same path: every matching mode narrows candidates in the database, over your whole eligible history, so how old a match is never decides whether it is found. The query passed with -q/--query (which the Ctrl+R widget uses for any text already on your prompt) now goes through that same pipeline instead of a separate one.
Matching Modes and Scopes
Matching decides which commands are eligible, scope decides where to look, and ranking decides only the order. They are three separate controls, and changing one never changes what another does.
Matching — --match, or Ctrl+X in the search UI: terms (the default — every whitespace-separated word must appear as a substring, in any order), literal (the whole query exactly as typed, spaces and punctuation included), prefix (the command starts with the query), and fuzzy (the query's letters appear in order, gaps allowed, so gco finds git checkout). Words are ANDed, never ORed; there is no quoting syntax and punctuation is never stripped, so git-push is one word — with one exception, the fallback below.
When nothing matches: separators ignored
Since 0.5.1, when a terms query matches nothing in the current scope and filters, it is asked again with separators ignored — whitespace, -, _, ., / and : are dropped from both your words and the commands. hiveagent then finds hive-agent, hive_agent, hive.agent, hive/agent and hive:agent, and hive-agent finds hiveagent. Every word must still appear.
- The results title says
ignoring punctuation (-_./:), the status row saysMatch terms (loose), and the matched text, separators inside it included, is underlined as usual. - It never mixes with exact matches: if anything matches as typed, only that is shown, so the count and every page come from one rule.
literal,prefixandfuzzyhave no fallback. The MCPsearch_commandstool falls back the same way for its whole query and says so in its response.
Upper and lower case
Every mode ignores case. Since 0.5.1, terms (for a word with a non-ASCII letter in it) and fuzzy fold Unicode case rather than only lowercasing: ÉCHO finds écho, σ finds ΟΔΟΣ, and straße finds STRASSE. A word typed in plain ASCII is looked up in the search index, which folds ASCII case only, so STRASSE does not find straße; literal and prefix are answered by SQLite's LIKE, which folds ASCII only too. There are no language-specific rules: Turkish İ folds to i plus a combining dot, and dotless ı matches only itself.
Scope — --scope, Ctrl+P to cycle, Ctrl+R to reset: all (default), directory (commands run in exactly the current directory, not its subdirectories — --here is the same thing), workspace (anywhere under the nearest enclosing Git repository; a linked worktree is its own workspace and a nested repository wins over its parent), and session (the current shell session). A scope that cannot apply here — no repository, no session — says so on stderr and falls back explicitly; it never silently widens.
suv search --compact draws the same UI inline under your prompt instead of taking over the screen, so the output you were reading stays visible. All three have config keys — search.match_mode, search.scope and search.compact — that set the default for every recall, and all default to the behaviour of earlier releases. Since 0.5.0 you can set them on the Search tab of suv settings (Default Match Mode, Starting Scope, Compact Recall) as well as in config.toml.
Reading the Results
Why a row matched
The characters your query matched are underlined and emphasised in every row, over the usual syntax colours: every occurrence of each word in terms mode, the whole query in literal, the start in prefix, and the characters the subsequence rule walked in fuzzy. Highlighting applies when you search command text, the default field.
Executions and Commands
Search shows the same results in two views, and Ctrl+U switches between them:
- Commands (where recall opens) — identical commands share one row, with a Last used age and a Runs count (for example
26×) of how many runs matched, so a command you ran fifty times does not push every other candidate down the list. A command is grouped only with exactly the same text: whitespace, flags and case are never normalised. - Executions — every recorded run, with its time, directory and outcome. On a wide terminal the columns are Time, Command, Path and Status, plus Ran by when agent commands are shown; each run's session and duration are in the detail pane.
The results title says which view you are in, counts in its units, and names the key that switches: Executions 1-50 of 558 · ^U group, or Commands 1-50 of 139 · ^U every run. The command you had selected stays selected across the switch, on whichever page it lands, and going back to every run selects its most recent run. If that command was deleted meanwhile, a message in the footer says so instead of putting another command under the cursor.
Since 0.5.0 recall opens in Commands unless your config.toml says otherwise: set show_unique_by_default = false in its [search] section, or turn off Start in Commands View in suv settings, to open in Executions. A value already in your config — including one suv settings wrote when you saved it — is kept. suv search -u opens in Commands either way.
Long commands and hidden whitespace
A row too long for the list ends in …; the selected row wraps so every character of it is on screen. Because commands are grouped only by exact text, whitespace you could not otherwise see is drawn: · for a leading or trailing space (a long run shows its first markers and how many more, such as +4), ⇥ for each tab, ↵ for each line break, and · for any other non-space whitespace. Runs of spaces inside a command are kept. The markers change only what is drawn, not what is stored, what Enter puts on your prompt, or how commands are grouped.
Inspect the exact text
When a command holds anything that cannot be read off its text — a tab, a line break, an edge space, or a character that draws as nothing, such as a zero-width space, a bidi override or a byte-order mark — the detail pane adds a Raw line that spells it out with escapes (for example "printf·'a\t\tb'" or \u{200b}), with every space drawn as · so none can hide at a line break.
Press Ctrl+V to open the same raw view for the highlighted command in a scrollable overlay, however long it is: every space drawn as ·, every hidden character escaped, a line per line break, and its length in characters and bytes. Scroll with the arrow keys or PageUp / PageDown; Enter or Esc closes it without accepting the command.
Typing never waits for a search
Since 0.5.0 each query runs on a background thread with its own read-only connection. A keystroke that changes the query interrupts the search still running, and results that arrive for an older query are discarded. While a slow search runs, the results title shows searching…, and an empty list says Searching… rather than explaining an earlier query's empty result. Enter, navigation, paging, and mode or scope changes first wait for the results of what is typed now, so the command you accept always matches the query on screen. Esc leaves at once.
Status row and footer
The row between the search box and the results shows the current Scope, Match mode, whether Agents are shown, and the Rank (Smart or Recent), followed by a badge for each active filter. An active filter always stays visible: on a narrow terminal the Rank segment gives way first. The search box holds only what you typed; whether results are grouped is shown in the results title.
The filter panel (Ctrl+F) refuses what it cannot read. Since 0.5.1, pressing Enter with a date it cannot read (last tuesday), a tag that does not exist, or an exit code that is not a whole number applies nothing: the filters already applied stay, the panel stays open on that field, and a red line names the accepted date formats, the existing tags, or the rule. Editing the field clears the message; a blank field still clears its filter. The --after and --before flags likewise stop with could not read --after "last tuesday" as a date and exit 1 instead of showing everything.
The footer keeps to the core keys at any width — use (Enter), navigate, filter (Ctrl+F), detail (Tab), mode (Ctrl+X) and scope (Ctrl+P), plus quit and help. Press ? to list every shortcut; all of them work whether or not the footer shows them.
Command-Line Flags
You can pre-filter results before the TUI opens by passing flags:
| Flag | Description | Example |
|---|---|---|
-q / --query | Pre-fill the search query | suv search -q "docker build" |
-u / --unique | Start in the Commands view even when your config opens Executions | suv search -u |
--after | Show commands after a date or relative time | suv search --after "3 days ago" |
--before | Show commands before a date or relative time | suv search --before "2024-01-01" |
--tag | Filter by session tag | suv search --tag deploy |
--exit-code | Filter by exit code (0 = success, 1+ = failure) | suv search --exit-code 1 |
--executor | Filter by executor (e.g., claude-code, cursor, user) | suv search --executor claude-code |
--here | Only show commands run in the current directory | suv search --here |
--field | Search one field: command, cwd, session, or executor | suv search --field cwd |
--failed | Show only commands that exited non-zero | suv search --failed |
--include-agents | Include AI-agent / CI / script commands (hidden by default) | suv search --include-agents |
Keyboard Shortcuts
These shortcuts are available inside the search TUI:
| Key | Action |
|---|---|
| Type any text | Substring search / filter results |
| Up / Down | Navigate through results |
| Tab | Toggle detail preview pane |
| Shift+Tab | Move focus into the detail pane to scroll it, when its contents don't fit (see Detail Preview Pane); Esc, Shift+Tab or any other key returns to the list |
| Enter | Use: put the highlighted command on your prompt (from Ctrl+R) or print it (from suv search). It never runs the command. |
| Esc | Exit search without selecting, at once, even while a search is running. Ctrl+C does the same in one press |
| Ctrl+X | Cycle the matching mode (terms, literal, prefix, fuzzy) |
| Ctrl+P / Ctrl+R | Cycle the scope / reset it to all history |
| Ctrl+S | Toggle Smart rank (directory, frecency, outcome and sequence boosts for typed queries; on by default) and Recent rank |
| Ctrl+L | Toggle directory filter (show only current directory) |
| Ctrl+E | Toggle failed-only filter (commands that exited non-zero) |
| Ctrl+A | Toggle showing AI-agent / CI / script commands (hidden by default) |
| Ctrl+U | Switch between Executions (every run) and Commands (identical commands grouped); the selected command stays selected |
| Ctrl+V | Inspect the highlighted command's exact text in a scrollable raw view (see Inspect the exact text) |
| Ctrl+F | Open filter panel (date, exit code, executor, tag) |
| Ctrl+O | Toggle bookmarks-only results |
| PageUp / PageDown | Move the selection 10 rows up / down |
| Home / End | First / last result on the current page |
| Ctrl+B | Toggle bookmark on the highlighted command |
| Ctrl+N | Add or edit a note on the highlighted command |
| Ctrl+T | Tag the current session |
| Ctrl+K | Open the timeline of the session the highlighted command ran in, the same one suv sessions shows (since 0.5.3). Esc comes back to recall where you left it, and Ctrl+C closes both. Compact recall has too few rows for a timeline and says so instead. |
| Ctrl+Y | Copy the highlighted command to clipboard |
| Ctrl+D | Delete the highlighted entry |
| Ctrl+G | Go to a specific page number |
| Left / Right | Previous / next page of results |
| F1 / ? | Show help overlay with all shortcuts (the footer shows only the core keys) |
Smart Mode
Smart mode is on by default (context_boost = true under [search]). Press Ctrl+S to toggle it; the status row shows the current ranking in its Rank segment (Smart or Recent), and the selected command stays selected. For a terms or fuzzy query, Smart rank multiplies each match's score, within its match tier, by four boosts:
- This directory — entries whose recorded directory is exactly your current working directory (
cwd_boost_percent). Commands run in subdirectories or parent directories are not boosted, and the directory column is highlighted for rows recorded in the current directory. - Frecency (since 0.5.1) — how often and how recently you ran that exact command. Each run counts 16 in its first hour, 8 that day, 4 for three days, 1 for a week, 0.5 for a month and 0.25 after that; the boost is the fourth root of one plus the sum, so twenty runs a day or two ago (×3.0) beat one run five minutes ago (×2.0) without burying it. It is the same definition the MCP
suggest_nexttool ranks by. - Outcome (since 0.5.1) — a command that usually fails, more than half of at least three runs with a known exit status, is scaled down, to a quarter when it has never succeeded. One failure among many successes changes nothing, and a run stopped with Ctrl+C (exit 130) is not a failure.
- Sequence (since 0.5.1) — what you usually run next after the command you just ran in this shell. Recall looks at the newest 500 runs of that command in your history and, for each, at the next command recorded in the same session within 10 minutes; a command seen there at least twice is boosted by up to ×3, in proportion to how often it came next. So after
git add -A,git commitcomes before thegit statusyou run far more often. It needs the shell hook's session ($SUVADU_SESSION_ID). Turn off this boost alone with Sequence Boost insuv settingsor:[search] sequence_boost = false
Frecency, outcomes and sequences count every run of that exact text in your history, not only the matching ones; agent runs count only while agent commands are shown. No boost lifts a command over a closer textual match, and matching, counts and paging are the same in both ranks. Recent rank leaves the four boosts out and orders exactly as earlier releases did. With an empty query, and in literal and prefix modes, results are listed newest first in both ranks. To see only commands from the current directory instead of boosting them, use the directory filter (Ctrl+L or --here).
The statistics are read on the first keystroke of a recall session and reused after that. Measured on an Apple M4 Max with a 100,000-run history (BENCHMARKS.md), that first keystroke costs 11 ms more for a one-letter query in the Commands view; later keystrokes cost what they did before.
How results are ranked
When you type a query in the TUI, each candidate entry is scored and sorted by these keys, in order:
- Literal-token requirement. In
termsmode, entries where any typed word is missing as a literal substring are dropped, even if they would match as a fuzzy subsequence (for example,gcodoes not matchgit checkout; usefuzzyfor that). The separator fallback applies the same rule with separators removed. - Match tier. Query as a prefix of the field, then the query as a contiguous substring, then all words in query order, then all words in any order. A better tier always ranks first, regardless of boosts.
- Boosted matcher score. Within a tier, entries are ordered by a fuzzy-matcher score (from the
nucleomatcher) adjusted by:- Long-command penalty — fields longer than
length_thresholdcharacters (default 80) have their score scaled by √(threshold ÷ length). - Human boost — entries from a human terminal or with an unknown executor have their score raised by
human_boost_percent(default 33%). - Directory boost — in Smart mode only, entries from exactly the current directory have their score raised by
cwd_boost_percent(default 50%). - Frecency, outcome and sequence — in Smart mode only, the multipliers described in Smart Mode.
- Long-command penalty — fields longer than
- Interactive tiebreak. On an equal score, commands from a terminal, an IDE terminal, or an unknown executor rank above agent, bot, CI, and script commands.
Remaining ties go to the newest run (Executions) or the most-run command (Commands). The three scoring values can be changed in Settings. This ranking applies only to queries typed in the TUI; see the known limitation above for which entries are considered.
Detail Preview Pane
Press Tab to toggle the detail preview pane. When visible, it shows full metadata for the highlighted command, leading with where it ran and how it ended, so those are what you see when the pane has only a few rows below the results:
- Full command text
- Raw — only when the command holds whitespace or characters that cannot be read off its text; see Inspect the exact text
- Path, Exit and Time — the working directory, exit code, and date and time of the run. In the Commands view these are the latest matching run's, labelled Last path, Last exit and Last run, followed by Runs, the number of runs that matched
- Duration
- Session ID and tag
- Branch — since 0.5.1, the git branch the command started on (or the short hash of a detached HEAD), when it was recorded inside a repository; see Git branch capture
- Executor (user, claude-code, cursor, etc.)
- Bookmark and note status
- Output — since 0.5.1, for a failed command whose output was kept, the end of what it printed. Only commands run by an agent integration or
suv wrapwith Keep Failure Output on have it; commands typed in your shell never do. See Failure output - Last time this failed, you fixed it with … — since 0.5.1, for a failed command, what fixed it before, most often first:
Last time this failed, you fixed it with: npm install, then reran it (3×). It is worked out from your history when the command is selected, never stored; see below
This is useful when multiple similar commands appear in results and you need to identify the exact one you want.
When the pane's contents don't fit, press Shift+Tab to move focus into it, then scroll with Up/Down, PageUp/PageDown and Home/End (or j/k/g/G in vim Normal mode). A scrollbar sits outside the pane's border and the title shows the rows in view, such as 12–32/67. Esc, Shift+Tab or any other key returns to the list.
How "you fixed it with" is worked out
Nothing new is recorded for it. When you select a failed command, Suvadu takes the newest 50 failures of exactly that command (any exit status other than 0 and Ctrl+C's 130) and, after each one, looks at the next 20 commands of the same session, started within 15 minutes, in the same directory, skipping other failures:
- a successful command similar to the failed one is its fix — the same program with the same first non-flag argument (
cargo build→cargo build --release), or a small edit of the line (gti status→git status); - if that is the failed command again, unchanged, then a setup step that passed before it — the same program (
npm installbeforenpm test) or a package install — is the fix; with no setup step it passed when you reran it unchanged; - looking around in between (
cd,ls,cat,git status/diff/log/show, an editor) is skipped, and any other successful command ends the search without a fix.
Fixes are counted across those failures and the most common is shown. It is an inference from what ran next, never an observed cause. Each lookup took 0.6–0.7 ms on a 100,000-command history on an Apple M4 Max (BENCHMARKS.md), once per selected failed command. The MCP what_failed and learn_from_failures tools add the same answer as a fixed before (inferred) line.
Vim Bindings
Enable vim-style modal navigation with vim_mode = true in your config (suv settings → Search tab), or in config.toml:
[search]
vim_mode = true When enabled, the search TUI starts in Insert mode (typing searches as usual). Press Esc to switch to Normal mode for navigation:
| Key | Normal Mode |
|---|---|
| j / k | Navigate down / up |
| Ctrl+D / Ctrl+U | Half-page scroll down / up |
| g / G | Jump to first / last entry |
| h / l | Previous / next page |
| / or i | Switch to Insert mode (search) |
| Enter | Use: put the command on your prompt (never runs it) |
| Tab | Toggle detail pane (with the pane focused by Shift+Tab, j/k/g/G scroll it) |
| q or Esc | Quit |
The current mode is shown in the footer as NORMAL or INSERT. All Ctrl+key shortcuts (filters, bookmarks, notes, etc.) work in both modes.
^U only outside Normal mode. All other Ctrl shortcuts work in both modes.
Examples
Find failed commands from the last 3 days
suv search --exit-code 1 --after "3 days ago" Opens the search TUI pre-filtered to show only commands that exited with code 1, from the last 3 days.
See what an AI agent ran in the current directory
suv search --executor claude-code --here Filters to commands executed by Claude Code in your current working directory. Useful for reviewing what an AI agent did in a project.
Search with a pre-filled query
suv search -q "git rebase" Opens the TUI with "git rebase" already typed into the search box. The first list comes from a database query for that phrase across all recorded history, newest first.
Start with identical commands grouped, whatever the config says
suv search -u Opens in the Commands view — each distinct command text is one row, with when it last ran and how many runs matched — even if show_unique_by_default = false makes Executions your default. Press Ctrl+U to see every run instead.
Filter by session tag
suv search --tag deploy Shows only commands from sessions tagged "deploy". Tags can be applied with suv tag associate <name> or Ctrl+T in the search TUI.