feat: enable fuzzy matching for slash commands and session filter#467
feat: enable fuzzy matching for slash commands and session filter#467felipeggv wants to merge 7 commits intoRunMaestro:mainfrom
Conversation
The agent filter was using exact substring matching (.includes()), making it impossible to find agents with non-contiguous character queries. Now uses the existing fuzzyMatch() utility for consistent behavior with File Search Modal and Tab Switcher. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Slash command search was using startsWith() prefix matching, requiring exact contiguous character input. Now uses fuzzyMatchWithScore() with 3-tier ranking (prefix > fuzzy name > fuzzy description) in InputArea and fuzzyMatch() in useInputKeyDown for consistent keyboard navigation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update useInputKeyDown test to expect /clear fuzzy match for '/r' input since fuzzy matching now matches 'r' anywhere in command names. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review infoConfiguration used: Organization UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughIntroduce a fuzzy, scored matching helper for slash commands and apply fuzzy matching to session searches. Replace local prefix-only slash filtering with Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/renderer/components/SessionList.tsx (1)
1903-1940:⚠️ Potential issue | 🟡 MinorInconsistent filtering logic in group expansion effect.
This
useEffectstill uses.toLowerCase().includes()for matching (lines 1909-1911), while the mainsessionCategoriesmemo usesfuzzyMatch. This inconsistency means:
- Groups may not expand for sessions that match via fuzzy logic but not substring.
- User experience differs between what's filtered vs. what groups auto-expand.
Consider updating this effect to use
fuzzyMatchfor consistency.🔧 Proposed fix
useEffect(() => { if (sessionFilter) { // Find groups that contain matching sessions (search session name AND AI tab names) const groupsWithMatches = new Set<string>(); - const query = sessionFilter.toLowerCase(); + const query = sessionFilter; const matchingSessions = sessions.filter((s) => { - if (s.name.toLowerCase().includes(query)) return true; - if (s.aiTabs?.some((tab) => tab.name?.toLowerCase().includes(query))) return true; + if (fuzzyMatch(s.name, query)) return true; + if (s.aiTabs?.some((tab) => tab.name && fuzzyMatch(tab.name, query))) return true; return false; });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/SessionList.tsx` around lines 1903 - 1940, The effect that computes matchingSessions (inside the useEffect using sessionFilter) uses substring .toLowerCase().includes() which is inconsistent with the sessionCategories memo that uses fuzzyMatch; replace the substring checks with the same fuzzyMatch utility (call fuzzyMatch(sessionFilter, s.name) and fuzzyMatch(sessionFilter, tab.name) for aiTabs) so group expansion logic matches the main filter, and ensure matchingSessions and hasMatchingBookmarks use those fuzzyMatch checks; also update the useEffect dependency array to include sessionFilterOpen, sessions, and fuzzyMatch (or its module) so the effect re-runs when those change.
🧹 Nitpick comments (1)
src/renderer/hooks/input/useInputKeyDown.ts (1)
209-219: Filtering logic duplicated betweenuseInputKeyDownandInputArea.This file uses a simple boolean filter (prefix OR fuzzy name OR fuzzy description), while
InputArea.tsxuses a scoring-based approach with tiered ranking (prefix > fuzzy name > fuzzy description). The two implementations may produce different filtered results or ordering for the same input.Consider extracting a shared utility function to ensure consistent slash command filtering behavior across both locations.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/hooks/input/useInputKeyDown.ts` around lines 209 - 219, Duplicate filtering logic in useInputKeyDown (filteredCommands/searchTerm) and InputArea.tsx causes inconsistent results; extract a shared utility (e.g., computeSlashCommandScore or filterAndScoreSlashCommands) that accepts allSlashCommands, inputValue, isTerminalMode and returns a scored & sorted list using the same tiered ranking: exact/prefix match highest, fuzzy name next, fuzzy description last, while honoring terminalOnly and aiOnly flags; replace the inline logic in useInputKeyDown (and update InputArea to use the new utility) so both locations call the same function (reusing fuzzyMatch) and consume the sorted results.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@src/renderer/components/SessionList.tsx`:
- Around line 1903-1940: The effect that computes matchingSessions (inside the
useEffect using sessionFilter) uses substring .toLowerCase().includes() which is
inconsistent with the sessionCategories memo that uses fuzzyMatch; replace the
substring checks with the same fuzzyMatch utility (call
fuzzyMatch(sessionFilter, s.name) and fuzzyMatch(sessionFilter, tab.name) for
aiTabs) so group expansion logic matches the main filter, and ensure
matchingSessions and hasMatchingBookmarks use those fuzzyMatch checks; also
update the useEffect dependency array to include sessionFilterOpen, sessions,
and fuzzyMatch (or its module) so the effect re-runs when those change.
---
Nitpick comments:
In `@src/renderer/hooks/input/useInputKeyDown.ts`:
- Around line 209-219: Duplicate filtering logic in useInputKeyDown
(filteredCommands/searchTerm) and InputArea.tsx causes inconsistent results;
extract a shared utility (e.g., computeSlashCommandScore or
filterAndScoreSlashCommands) that accepts allSlashCommands, inputValue,
isTerminalMode and returns a scored & sorted list using the same tiered ranking:
exact/prefix match highest, fuzzy name next, fuzzy description last, while
honoring terminalOnly and aiOnly flags; replace the inline logic in
useInputKeyDown (and update InputArea to use the new utility) so both locations
call the same function (reusing fuzzyMatch) and consume the sorted results.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/__tests__/renderer/hooks/useInputKeyDown.test.tssrc/renderer/components/InputArea.tsxsrc/renderer/components/SessionList.tsxsrc/renderer/hooks/input/useInputKeyDown.ts
Greptile SummaryThis PR successfully replaces exact prefix matching with fuzzy matching for both slash command autocomplete and session filtering, improving the search experience by matching characters that appear anywhere in the command or session name (e.g., Key improvements:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant InputArea
participant useInputKeyDown
participant filterAndSortSlashCommands
participant fuzzyMatchWithScore
User->>InputArea: Types /r in input
InputArea->>InputArea: Extract searchTerm = 'r'
InputArea->>filterAndSortSlashCommands: filter(slashCommands, 'r', isTerminalMode)
filterAndSortSlashCommands->>fuzzyMatchWithScore: Check /clear command
fuzzyMatchWithScore-->>filterAndSortSlashCommands: matches=true, score=~107
filterAndSortSlashCommands->>fuzzyMatchWithScore: Check /reset command
fuzzyMatchWithScore-->>filterAndSortSlashCommands: matches=true, score=~107
filterAndSortSlashCommands-->>InputArea: Sorted array [/clear, /reset, ...]
InputArea->>InputArea: Render dropdown with sorted commands
User->>useInputKeyDown: Presses ArrowDown key
useInputKeyDown->>useInputKeyDown: Extract searchTerm = 'r'
useInputKeyDown->>filterAndSortSlashCommands: filter(allSlashCommands, 'r', isTerminalMode)
filterAndSortSlashCommands-->>useInputKeyDown: Same sorted array [/clear, /reset, ...]
useInputKeyDown->>useInputKeyDown: Increment selectedIndex
User->>useInputKeyDown: Presses Enter
useInputKeyDown->>useInputKeyDown: Get command at selectedIndex
useInputKeyDown->>InputArea: setInputValue(filteredCommands[selectedIndex])
Note over InputArea,useInputKeyDown: Both use same filterAndSortSlashCommands()<br/>ensuring consistent ordering between<br/>dropdown display and keyboard selection
Last reviewed commit: 013d812 |
| const filteredCommands = allSlashCommands.filter((cmd) => { | ||
| if ('terminalOnly' in cmd && cmd.terminalOnly && !isTerminalMode) return false; | ||
| if ('aiOnly' in cmd && cmd.aiOnly && isTerminalMode) return false; | ||
| return cmd.command.toLowerCase().startsWith(inputValue.toLowerCase()); | ||
| if (!searchTerm) return true; | ||
| const cmdName = cmd.command.toLowerCase().replace(/^\//, ''); | ||
| return ( | ||
| cmdName.startsWith(searchTerm) || | ||
| fuzzyMatch(cmdName, searchTerm) || | ||
| (cmd.description ? fuzzyMatch(cmd.description, searchTerm) : false) | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Ordering mismatch causes wrong command to be selected on Enter/Tab
filteredCommands here is built with Array.filter(), which preserves the original insertion order of allSlashCommands. However, filteredSlashCommands in InputArea.tsx sorts results by fuzzy-match score (prefix match scores 300, name fuzzy match ~100+score, description match is lowest priority). Both arrays are indexed by the same selectedSlashCommandIndex from InputContext.
This means what the user sees highlighted in the dropdown (the sorted order from InputArea.tsx) will often be a different item than what filteredCommands[selectedSlashCommandIndex] resolves to in this hook. For example, with commands registered as ['/beta', '/alpha'] and a query of 'a':
- Dropdown renders:
[/alpha (score 300), /beta (score ~207)]—/alphais visually first filteredCommandshere:[/beta, /alpha]— original order; pressing Enter at index 0 selects/beta
The fix is to either pass the already-sorted filteredSlashCommands array into this hook (removing the duplicated filtering logic), or replicate the same sort here. The cleanest approach is to stop duplicating the filter and instead consume the array that InputArea.tsx has already computed.
…rd handler Extract shared filterAndSortSlashCommands() utility to ensure both InputArea (visual dropdown) and useInputKeyDown (Enter/Tab handler) produce identically ordered command lists. Previously, InputArea sorted by relevance score while useInputKeyDown kept original array order, causing the wrong command to be inserted when pressing Enter/Tab. Also fixes SessionList group auto-expansion to use fuzzyMatch() consistently instead of substring .includes(), matching the main sessionCategories filter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/renderer/utils/search.ts`:
- Around line 154-158: Normalize the search term before doing the prefix check
so it matches the already-lowercased cmdName: compute a lowercase variant (e.g.
const normalizedSearchTerm = searchTerm.toLowerCase()) just before the prefix
match and use normalizedSearchTerm in the startsWith call instead of searchTerm;
leave fuzzyMatchWithScore as-is since it’s case-insensitive.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/renderer/components/InputArea.tsxsrc/renderer/components/SessionList.tsxsrc/renderer/hooks/input/useInputKeyDown.tssrc/renderer/utils/search.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/renderer/hooks/input/useInputKeyDown.ts
- src/renderer/components/InputArea.tsx
Prefix match compared lowercased cmdName against potentially mixed-case searchTerm, making it case-sensitive while fuzzyMatchWithScore was case-insensitive. Now lowercases searchTerm once at function entry. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@coderabbitai review All issues from previous reviews have been addressed:
|
|
✅ Actions performedReview triggered.
|
- Clamp selectedSlashCommandIndex in useInputKeyDown before accessing filteredCommands, preventing silent no-op when index exceeds list length after the user continues typing - Align SessionList group-expansion useEffect with sessionCategories memo: add worktree children branch/name matching and parentSessionId filtering, fix missing sessions and worktreeChildrenByParentId dependencies - Add 11 tests for filterAndSortSlashCommands covering mode filtering, prefix vs fuzzy ranking, case insensitivity, description matching, empty results, and generic type preservation - Remove duplicate SlashCommand interface from InputArea.tsx, import SlashCommandEntry from search.ts instead Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/renderer/hooks/input/useInputKeyDown.ts (1)
216-222:⚠️ Potential issue | 🟡 MinorArrow navigation can still produce an invalid index on empty results.
On Line 218, when
filteredCommands.lengthis0, the upper bound becomes-1, so ArrowDown can setselectedSlashCommandIndexto-1.Suggested fix
if (e.key === 'ArrowDown') { e.preventDefault(); - setSelectedSlashCommandIndex((prev) => Math.min(prev + 1, filteredCommands.length - 1)); + const maxIndex = Math.max(0, filteredCommands.length - 1); + setSelectedSlashCommandIndex((prev) => Math.min(Math.max(0, prev + 1), maxIndex)); } else if (e.key === 'ArrowUp') { e.preventDefault(); - setSelectedSlashCommandIndex((prev) => Math.max(prev - 1, 0)); + const maxIndex = Math.max(0, filteredCommands.length - 1); + setSelectedSlashCommandIndex((prev) => Math.min(Math.max(0, prev - 1), maxIndex)); } else if (e.key === 'Tab' || e.key === 'Enter') {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/hooks/input/useInputKeyDown.ts` around lines 216 - 222, In useInputKeyDown, the ArrowUp/ArrowDown handling can set selectedSlashCommandIndex to -1 when filteredCommands.length === 0; before calling setSelectedSlashCommandIndex (in the ArrowDown and ArrowUp branches) add a guard that returns early or computes the new index using a clamp that treats an empty filteredCommands array as having no valid indices (i.e., do nothing if filteredCommands.length === 0), ensuring setSelectedSlashCommandIndex is never called with a negative upper bound; reference setSelectedSlashCommandIndex, filteredCommands, and selectedSlashCommandIndex in your fix.
🧹 Nitpick comments (1)
src/renderer/components/SessionList.tsx (1)
1908-1928: Extract the session-match predicate to one shared helper.The same fuzzy-match decision tree is duplicated in both
sessionCategoriesand the group-expansion effect. Centralizing it will prevent future drift between visible results and expansion behavior.Also applies to: 1756-1784
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/SessionList.tsx` around lines 1908 - 1928, Extract the duplicated fuzzy-match logic into a single helper (e.g., sessionMatchesFilter) that accepts a session object, the sessionFilter string, and access to worktreeChildrenByParentId; move the decision tree currently in the useEffect (lines shown) and the identical logic used in sessionCategories into that helper, then replace both places to call sessionMatchesFilter(session, sessionFilter, worktreeChildrenByParentId) so expansion and visible-results use the exact same predicate and keep types/signatures consistent with existing Session and Worktree types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@src/renderer/hooks/input/useInputKeyDown.ts`:
- Around line 216-222: In useInputKeyDown, the ArrowUp/ArrowDown handling can
set selectedSlashCommandIndex to -1 when filteredCommands.length === 0; before
calling setSelectedSlashCommandIndex (in the ArrowDown and ArrowUp branches) add
a guard that returns early or computes the new index using a clamp that treats
an empty filteredCommands array as having no valid indices (i.e., do nothing if
filteredCommands.length === 0), ensuring setSelectedSlashCommandIndex is never
called with a negative upper bound; reference setSelectedSlashCommandIndex,
filteredCommands, and selectedSlashCommandIndex in your fix.
---
Nitpick comments:
In `@src/renderer/components/SessionList.tsx`:
- Around line 1908-1928: Extract the duplicated fuzzy-match logic into a single
helper (e.g., sessionMatchesFilter) that accepts a session object, the
sessionFilter string, and access to worktreeChildrenByParentId; move the
decision tree currently in the useEffect (lines shown) and the identical logic
used in sessionCategories into that helper, then replace both places to call
sessionMatchesFilter(session, sessionFilter, worktreeChildrenByParentId) so
expansion and visible-results use the exact same predicate and keep
types/signatures consistent with existing Session and Worktree types.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/__tests__/renderer/utils/search.test.tssrc/renderer/components/InputArea.tsxsrc/renderer/components/SessionList.tsxsrc/renderer/hooks/input/useInputKeyDown.tssrc/renderer/utils/search.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/renderer/components/InputArea.tsx
When filteredCommands.length === 0, ArrowDown/ArrowUp/Enter/Tab handlers would compute invalid indices (e.g. Math.min(prev+1, -1)). Added early return guard that only allows Escape when no commands match. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
All review comments have been addressed and verified:
Lint checks pass: |
|
All CodeRabbit and Greptile review comments have been addressed: case-insensitive prefix matching, arrow nav guard on empty results, index ordering mismatch resolved. Requesting re-review. @coderabbitai review @greptile review |
|
✅ Actions performedReview triggered.
|
Summary
Details
Previously, typing
/rwould only match commands starting with "r". Now it matches any command containing "r" anywhere in the name (e.g.,/clearmatches/rbecause "clear" contains "r"). Same improvement applied to the session filter in the Left Bar sidebar.Test plan
/in input, verify fuzzy autocomplete suggestions🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests