feat: fuzzy search for the model picker (WebUI + TUI + CLI)#36928
Merged
teknium1 merged 3 commits intoJun 1, 2026
Conversation
Adds fuzzy subsequence matching with quality ranking to the model pickers, replacing the WebUI's exact-substring filter and giving the TUI a search where it previously had none. - New fuzzy scorer (ui-tui/src/lib/fuzzy.ts + an identical copy at web/src/lib/fuzzy.ts, since the two are separate TS packages with no shared module). Matches a query as an ordered subsequence (so `g4o` matches `gpt-4o`), scores by quality (exact > prefix > word-boundary > contiguous > scattered) and returns matched character positions for highlighting. Multi-token AND semantics (`clad snnt` -> claude-sonnet). 15 vitest tests cover the algorithm. - WebUI ModelPickerDialog: ranked fuzzy filter on providers + models; matched characters in model rows are highlighted via <mark>. - TUI modelPicker: type-to-filter on the provider and model stages with live ranking. Backspace edits the filter, Ctrl+U clears it, Esc clears a non-empty filter before navigating back. Persist-global / disconnect shortcuts moved from g/d to Ctrl+G / Ctrl+D so letters feed the filter. Closes NousResearch#30849
Pure, refactor-independent helpers for type-to-filter search in the curses single-/radio-select menus: subsequence matching, filtered-index mapping, cursor reconciliation, scroll clamping, and an active-search key handler, plus unit tests. Salvaged from NousResearch#22758 (the curses event loop was since refactored into a shared driver on main, so the integration is rebuilt in a follow-up commit; these pure helpers and their tests carry over unchanged).
Wires the salvaged search helpers into the shared curses menu driver and turns on type-to-filter for the CLI model pickers (the 100+ model lists that previously required scrolling). - Search lives in the shared `_run_curses_menu` driver behind a `searchable` flag + `search_labels`, so both `curses_radiolist` and `curses_single_select` get it without per-menu duplication. `/` opens the filter, BACKSPACE edits, Ctrl+U clears, ESC clears the filter then cancels. Returned values are always original item indices. - `_filter_indices` RANKS matches (best-first) via a Python port of the TS scorer in ui-tui/src/lib/fuzzy.ts and web/src/lib/fuzzy.ts. The port is byte-identical in score: same per-char bonuses, prefix (+8) and exact (+20) bonuses, camelCase/word-boundary detection (matching on the lowercased target, boundary on the original case), and the -len*0.01 length tiebreak — so the CLI, TUI, and WebUI rank results identically. A cross-language parity test pins the exact scores. - `_prompt_model_selection` (the canonical picker across the model flows) and the custom-provider model list pass `searchable=True`. - Split `_decode_menu_key` out of `read_menu_key` so the search loop can peek the raw key (catch `/`) before nav decoding. - ESC during active search now clears the query (restores the full list) so a no-match filter can't strand the user; printable-key capture is restricted to ASCII to avoid Latin-1 mojibake. - Update two setup-menu tests whose mock signatures predate the new `searchable` kwarg; add ranked-scorer + parity + state-machine tests.
d9f9879 to
4215ad9
Compare
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds fuzzy search to the model picker across all three surfaces from #30849 — the WebUI dashboard, the TUI, and the CLI curses picker — with consistent ranking everywhere.
Before this: the WebUI used exact-substring matching (
g4owouldn't matchgpt-4o); the TUI had no search (arrow keys only); the CLI's curses model picker required scrolling 100+ models with no filter.Surfaces
Shared scorer (same algorithm in 3 languages/places, kept in sync):
ui-tui/src/lib/fuzzy.ts+ a logically identical copy atweb/src/lib/fuzzy.ts(separate TS packages, no shared module; the TUI copy carries the vitest suite sincewebhas no test runner)hermes_cli/curses_ui.py(_fuzzy_score/_filter_indices)All three rank identically: ordered subsequence match (
g4o→gpt-4o), quality scoring (exact > prefix > word-boundary > contiguous > scattered), multi-token AND (clad snnt→claude-sonnet-4), shorter-id tiebreak.WebUI (
ModelPickerDialog.tsx): ranked fuzzy filter on providers + models; matched characters highlighted via<mark>.TUI (
modelPicker.tsx): type-to-filter on provider + model stages with live ranking; Backspace/Ctrl+U edit, Esc clears-then-back;g/dshortcuts moved to Ctrl+G/Ctrl+D so letters feed the filter.CLI (
curses_ui.py):/opens a type-to-filter prompt in the curses pickers; BACKSPACE edits, Ctrl+U clears, ESC stops search then cancels. Wired into the shared_run_curses_menudriver behind asearchableflag, so bothcurses_radiolistandcurses_single_selectget it without per-menu duplication. Enabled on_prompt_model_selection(the canonical picker across the model flows) and the custom-provider model list. Returned values are always original item indices.Salvage / credit
The CLI curses-search helpers (subsequence matcher, filtered-index mapping, cursor reconciliation, scroll clamping, active-search key handler) and their unit tests are salvaged from #22758 by @counterposition. That PR predates a refactor on main that extracted the curses event loop into a shared
_run_curses_menudriver, so a clean cherry-pick of its inline-loop edits wasn't possible — the pure helpers + tests carry over unchanged (committed under the contributor's authorship), and the driver integration is rebuilt on the current surface in a follow-up commit. We also upgraded the contributor's plain subsequence filter to the ranked scorer so the CLI matches the TUI/WebUI ordering.#22758 also touched the TUI (
modelPicker,sessionPicker,skillsHub,agentsOverlay) with a subsequence-only, no-ranking, no-highlight approach; this PR's TUI half supersedes that. #23623 (Telegram model-picker search) is a separate surface, untouched.Test plan
npm run build✓, eslint clean ✓, 15 vitest tests pass (src/lib/fuzzy.test.ts)tsc -b✓,vite build✓, eslint clean ✓ruff checkclean ✓; 14 new pytest tests (test_curses_ui_search.py= contributor's;test_curses_ui_fuzzy_rank.py= ranked scorer); full curses/menu/setup suite green (46 passed); updated two setup-menu tests whose mock signatures predated the newsearchablekwargg4o→gpt-4o,son4→claude-sonnet-4,clad snnt→claude-sonnet-4,4o→gpt-4o,xyz→no matchCloses #30849
Infographic