Selectors (model, settings, file/tree, slash, OAuth) re-filter the same
stable candidate list on every keystroke. fuzzyRank was re-normalizing the
query once per candidate (~400x/keystroke) and rebuilding each candidate's
search index from scratch on every keystroke.
- prepareQuery/fuzzyMatchCore: normalize + split the query a single time in
fuzzyRank instead of per candidate. Pure win on warm and cold paths.
- indexCache: memoize the per-text SearchIndex across keystrokes, with
conservative admission -- texts >4 KiB bypass the Map entirely (no memory
bloat from pasted prompts/transcripts searched via the message selector),
and admission stops at the cap instead of evicting (a stream of unique
one-off texts can't churn the map).
Incremental typing over a 400-item list drops ~58% (6.7ms -> 2.8ms for an
8-keystroke session); first-keystroke (cold) latency is unchanged (~0.5ms),
as the query-prep savings offset the cache overhead. Match ranking is
byte-identical, enforced by a golden-checksum guard in the new benchmark and
contract tests.
Adds packages/tui/bench/fuzzy.bench.ts (deterministic corpus, cold-start
session measurement, ranking-drift guard) and fuzzy contract tests for cache
transparency and the empty-normalized-query callback behavior.