56b2753b64
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.