feat(chat): fuzzy-search the "Run in" project picker#205
Conversation
Picking a project for a new thread meant scanning a long unfiltered list and clicking. The popover is now a cmdk Command with a search input: open it, type, Enter. Arrow keys move the highlight, the query resets on close, and "Open another project..." stays outside the list so it is still reachable when nothing matches. Ranking lives in the new src/lib/fuzzy.ts — a scored subsequence matcher where prefix beats substring beats scattered match, word-boundary and camelCase hits score like initials (hap -> hermes-agent-personal), and shorter candidates win ties. It replaces the two copy-pasted fuzzyMatch helpers in the GitHub issue/PR pickers. Matching name AND path at once was tried and dropped: a short query is a subsequence of nearly every long path, so the list never narrowed. A query containing "/" switches the haystack to paths instead, which is how two checkouts of the same repo get disambiguated.
Review — Opus 5 (high effort)The matcher is good and the reverted approach note is the right call. I hand-checked several of the score claims rather than trusting the tests:
I also checked that replacing the two copy-pasted And you got the focus right — dropping See my comment on #212, which keeps the handler and therefore ships a search box that does nothing until you click it. Three small things1. Empty-query empty state. 2. Home matches loosely. 3. const at = haystack.indexOf(needle[qi], cursor); // haystack = text.toLowerCase()
...
else if (isCamelHump(text, at)) score += 8; // text = originalDeliberate (you need the original case to see a hump) and safe for ASCII, but Note this PR collides head-on with #212, which adds a different search implementation to the same popover. Only one can land as-is. |
|
Review complete by GPT 5.6 Sol xhigh. I reviewed the fuzzy scorer/ranking, name-vs-path matching rule, cmdk focus and keyboard selection, empty state, query reset, and open-project escape hatch. The focused matcher and picker suites passed 51/51 tests. GPT 5.6 Sol xhigh found no errors in this PR as it stands against Coordination note: a pairwise merge-tree check shows textual conflicts with open PRs #211 and #212 in the same picker. Those changes need to be integrated deliberately so fuzzy search survives the scope-row simplification and Active/Settled sectioning. |
The location picker's new search field never received focus: the popover's onOpenAutoFocus handler focused the cmdk root div, and since neither shadcn's CommandInput nor cmdk's Command.Input sets autoFocus — and cmdk's root keydown only handles navigation keys — printable keystrokes went nowhere until the user clicked the box. The same handler/CommandInput pairing shipped in the branch picker, the launch model picker, and the multi-provider model picker. Fix it centrally: the open-autofocus handler (now focusCmdkOnOpen) aims at [cmdk-input] when the popover has one and falls back to [cmdk-root] otherwise, so input-less pickers keep their arrow-key navigation. Dropping the handler on input-bearing popovers would not have been enough: in the multi-provider picker the provider rail's buttons precede the input in the DOM, so Radix's first-tabbable default would focus the rail. The handler's docstring claimed the input's own autoFocus made root-focus safe — no such autoFocus exists; the docstring now describes the real contract. Regression tests type with NO prior click on the input, the interaction that masked the bug (userEvent.type clicks first, focusing the input as a side effect). Also stop matching project paths in the location search. Project paths share a long common prefix (/home/…/projects/), so any short query that grazed it subsequence-matched every row at an identical score and the filter read as dead — the same failure that got name-and-path matching reverted in the workspace switcher (#205). The item value keeps the path after a tab separator purely to stay unique across same-basename projects (cmdk keys selection state on value); a custom filter strips it before scoring. partitionProjectScopes now documents that any future parked lifecycle beyond settled must be folded into the Active/Settled partition, or all-parked projects would read as Active here.
Why
Starting a new agent chat meant scanning an unfiltered project list and clicking the right row. With a dozen-plus projects that is a mouse trip every time.
What
The "Run in" popover is now a cmdk
Commandwith aSearch projects…input:↑/↓→Enter, no mouse in the flowhome,~)Ranking lives in a new
src/lib/fuzzy.ts: a scored subsequence matcher where prefix beats substring beats scattered match, word-boundary and camelCase hits score like initials (hap→hermes-agent-personal,vas→vexis-agent-site), and shorter candidates win ties. It also replaces the two copy-pastedfuzzyMatchhelpers in the GitHub issue/PR pickers.One reverted approach, worth knowing
Matching each project against its name and its full path was the obvious first cut, and it doesn't narrow anything — a short query like
veis a subsequence of nearly every long path, so the whole list survived (caught in the browser, not in tests). Matching is name-only now; a query containing/switches the haystack to paths, which is how two checkouts of the same repo get disambiguated. Both behaviors are pinned by tests.Verification
npm run checkclean; 3082 tests pass (21 new for the matcher, 10 for the picker)ve→ narrowed tovexisalone →Enter→ scope switched and the checkout/branch controls appearednpm run verify's Rust half fails in this worktree for an unrelated reason (src-tauri/binaries/sidecar absent in a fresh worktree); the diff is frontend-only