Skip to content

Command search: setting to disable fuzzy matching, falling back to bash/zsh substring search (APP-5818) - #15818

Open
warp-agent-staging[bot] wants to merge 4 commits into
masterfrom
factory/command-search-disable-fuzzy-match
Open

Command search: setting to disable fuzzy matching, falling back to bash/zsh substring search (APP-5818)#15818
warp-agent-staging[bot] wants to merge 4 commits into
masterfrom
factory/command-search-disable-fuzzy-match

Conversation

@warp-agent-staging

@warp-agent-staging warp-agent-staging Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Description

Command Search history results are fuzzy on every existing path: with HistorySearchRankingV2 on it's Skim subsequence matching plus whitespace AND-tokenization and the recency/session/exit priors; with the flag off it's still Skim subsequence matching against the whole query. Neither is what a shell does. bash (reverse-search-history) and zsh (history-incremental-search-backward) are literal substring containment, walked backwards from the most recent command, no ranking.

This adds a "Fuzzy match command history in Command Search" toggle under Settings > Features > Terminal Input. When disabled, command history results fall back to that literal, no-ranking behavior; every other Command Search source (workflows, saved prompts, AI prompt history, env-var collections) keeps its own fuzzy matcher, unaffected by this setting.

Scope: history only, not the whole panel

The default Ctrl+R binding (workspace:show_command_search) opens Command Search unfiltered, so in the default flow every other source sits in the same panel next to history. This PR intentionally leaves those sources untouched rather than suppressing them or extending literal matching to them — confirmed with the requester as the desired scope for now, with two consequences worth stating plainly rather than leaving for a reviewer to discover:

  • The panel can look "half bash-like, half fuzzy." A broad, short query can return a wall of literal-substring history results sitting next to fuzzy-ranked workflows and prompts scored on a completely different basis.
  • A merely-containing history match can crowd out a stronger match from another source, because disabled-mode history's score is flat by design (see below) — match quality isn't a lever available to it, unlike every other source.

Suppressing non-history sources when the setting is disabled — closer to what a real shell's Ctrl+R actually shows — was raised and rejected for this iteration; noting it here as the documented alternative if this needs revisiting.

Matching semantics

Disabled mode is literal substring containment, ASCII-case-insensitive, no subsequence matching, no whitespace tokenization (cd hi orm no longer matches cd ~/projects/history_orm), no priors, ordered most-recent-first — matching bash/zsh's own behavior except for case-sensitivity (see below).

Cross-source scoring

History's score is compared against every other Command Search source inside the mixer (test_history_score_stays_comparable_to_other_sources_raw_skim_scale pins this for the enabled paths), so a mode that orders purely by recency can't just emit recency as the score without wrecking that comparability. Every literal-substring match instead gets the same score — literal_match_score(), derived from the query's own raw Skim self-match score (the theoretical ceiling for a query of that length) — so ties break via the mixer's stable sort, which preserves History::commands_shared()'s already-chronological candidate order. That's the same mechanism the existing blank-query zero-state bypass already relies on and tests; this just reuses it, gated on "is a literal substring match" instead of "query is blank."

Using the full self-match value outright turned out to be wrong, caught before review: for query "test", the self-match ceiling is 91 (raw Skim). A Workflow match on its highest-weighted field (content, weighted 0.5 in FuzzyMatchWorkflowResult::score) tops out at 91 * 0.5 = 45.5 for the exact same query, matched exactly. Scoring every literal history match at the full 91 meant a history entry that merely contained "test" somewhere unrelated would tie the best possible match any workflow could ever produce for that query — a 2x, quality-blind advantage baked into the scoring, not something requested. The fix scales the shared score by LITERAL_MATCH_SCORE_FRACTION = 0.5, the same weight FuzzyMatchWorkflowResult already gives its own top field, landing disabled-mode history in the same band a workflow's best match occupies rather than double it. disabled_fuzzy_matching_does_not_unconditionally_outrank_a_strong_competitor in searcher_tests.rs pins this: an exact-match AI prompt-history competitor now legitimately outranks a merely-containing history entry, which it could not before the fix.

Deliberate case-insensitivity deviation

bash and zsh are case-sensitive by default, but Warp's search is case-insensitive throughout (readline itself ships search-ignore-case), so the disabled mode stays case-insensitive rather than matching bash/zsh exactly.

Command Palette and context flag

Per AGENTS.md, added the matching Command Palette enable/disable entry ("fuzzy matching of command history in command search") and its context flag (COMMAND_SEARCH_FUZZY_MATCHING_FLAG), wired into the same keymap-context builder as the sibling InputSettings toggles.

Default is fuzzy enabled, so there's no behavior change for existing users.

Linked Issue

Linear: APP-5818

Testing

  • rank_tests.rs: literal substring matching is case-insensitive, doesn't match a fuzzy subsequence, doesn't tokenize a multi-word query, highlight indices span exactly the matched substring, blank query matches everything with no highlight, and literal_match_score is identical across matches for the same query (half the query's self-match score) and scales with query length.

  • searcher_tests.rs (7 new tests): disabled mode doesn't tokenize a multi-word query; short-circuits both the V2 and legacy ranking paths regardless of HistorySearchRankingV2; orders results most-recent-first despite an unfavorable raw-Skim tiebreak (an exact match inserted before a merely-containing one); a literal match stays comparable to a weak fuzzy competitor from another source; a literal match does not unconditionally outrank a strong (exact-match) competitor from another source, proving the scoring fix; and the blank-query zero state still works when fuzzy matching is disabled.

  • cargo test -p warp --lib search::command_search and settings_view (all passing), ./script/format, cargo clippy -p warp --all-targets --tests -- -D warnings, the workspace-wide cargo clippy --workspace --exclude warp_completer --all-targets --tests -- -D warnings, and cargo clippy -p warp_completer --all-targets --tests -- -D warnings (all clean).

  • Visual verification is outstanding. An earlier computer-use pass captured screenshots against a commit that predates a33dafada (the copy scoping fix below), so those captures showed stale wording and have been removed rather than left next to code they contradict. A recapture attempt against the current tip (039ad6338) confirmed the build was the correct internal dev.warp.WarpLocal (Channel::Local) binary, with warp-channel-config correctly installed and WARP_API_KEY present and non-empty (no "provided but IGNORED" warning) — but the app nonetheless never advanced past the "Welcome to Warp" onboarding screen to an authenticated session. The blocker is credential acceptance in the cloud verification environment, not the build, the channel, or anything in this change. The rendered toggle copy, tokenized/literal matching behavior, and Command Palette entry have not been visually re-confirmed since the copy fix landed; that verification is still needed before merge.

  • I have manually tested my changes locally with ./script/run

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

Adds a Terminal Input toggle that falls back to a literal, case-insensitive
substring search (bash/zsh's reverse-history-search semantics) instead of
Skim fuzzy matching, independent of HistorySearchRankingV2.
@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

This PR was generated with Warp.

Comment @warp-factory on this PR to send it follow-up work.

View run View conversation View on Slack

Verified each fix by deliberately reintroducing the regression it names
(V2 path ignoring the setting, then legacy path ignoring it) and confirming
only the matching test goes red, before reverting.
@warp-agent-staging
warp-agent-staging Bot marked this pull request as ready for review September 4, 2026 19:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant