feat: Add fuzzy search for slash commands#678
feat: Add fuzzy search for slash commands#678scriptease wants to merge 2 commits intoRunMaestro:mainfrom
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughFuzzy subsequence matching and index-returning matching were added; Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant Input as Input Component
participant Hook as Autocomplete Hook
participant Search as Search Utils
participant UI as Rendered UI
User->>Input: Type text (e.g. "/splan")
Input->>Hook: send raw input
Hook->>Hook: normalize -> query (strip '/', lowercase)
Hook->>Search: fuzzyMatchWithScore(cmd.command.slice(1), query, ".")
Search-->>Hook: {matches, score}
Hook->>Hook: filter by matches, sort by score desc
Hook-->>Input: ranked commands
Input->>Search: fuzzyMatchWithIndices(cmd.command.slice(1), query, ".")
Search-->>Input: matched indices (offset for '/')
Input->>UI: render commands with highlighted indices
UI-->>User: display ranked, highlighted suggestions
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds fuzzy matching to slash command autocomplete across desktop (
Confidence Score: 5/5Safe to merge — both findings are minor P2 style/logic issues that don't affect the primary user path. All remaining findings are P2. The double-scoring in sort is a performance micro-issue with no user-visible impact at typical slash-command counts. The sort/filter guard mismatch on mobile is an edge case (requires the autocomplete to be open with non-'/' text in the input) and produces odd ordering rather than broken behavior. Core fuzzy matching logic, highlight rendering, and keyboard navigation are all correct and well-tested. src/web/mobile/SlashCommandAutocomplete.tsx (sort/filter guard mismatch); the double-scoring pattern also appears in src/renderer/components/InputArea.tsx and src/renderer/hooks/input/useInputKeyDown.ts Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User types '/' + query] --> B[Strip leading '/' → query]
B --> C{query empty?}
C -- Yes --> D[Show all commands, original order]
C -- No --> E[fuzzyMatchWithScore per command]
E --> F{matches?}
F -- No --> G[Exclude command]
F -- Yes --> H[Retain command with score]
H --> I[Sort descending by score]
I --> J[Render filtered & sorted list]
J --> K{query empty for render?}
K -- Yes --> L[Render plain text]
K -- No --> M[fuzzyMatchWithIndices → matched positions]
M --> N[Wrap matched chars in bold span\nDim unmatched chars to 80% opacity]
Reviews (1): Last reviewed commit: "feat: Add fuzzy search for slash command..." | Re-trigger Greptile |
| .sort((a, b) => { | ||
| if (!query) return 0; | ||
| return ( | ||
| fuzzyMatchWithScore(b.command.slice(1), query).score - |
There was a problem hiding this comment.
fuzzyMatchWithScore called twice per comparison in sort
The .sort() comparator calls fuzzyMatchWithScore on both a and b for every comparison. Since sort runs in O(n log n), and fuzzyMatchWithScore is called twice per comparison, each command's score is re-computed many times. The same pattern appears in src/renderer/hooks/input/useInputKeyDown.ts (line 217–220) and src/web/mobile/SlashCommandAutocomplete.tsx (line 106–109).
Pre-computing the scores once before sorting is cleaner and avoids the redundancy:
| .sort((a, b) => { | |
| if (!query) return 0; | |
| return ( | |
| fuzzyMatchWithScore(b.command.slice(1), query).score - | |
| .sort((a, b) => { | |
| if (!query) return 0; | |
| const scoreA = fuzzyMatchWithScore(a.command.slice(1), query).score; | |
| const scoreB = fuzzyMatchWithScore(b.command.slice(1), query).score; | |
| return scoreB - scoreA; | |
| }); |
For a more thorough fix, score all filtered results up-front (score once per item instead of once per comparison):
const scored = slashCommands
.filter(...)
.map(cmd => ({ cmd, score: query ? fuzzyMatchWithScore(cmd.command.slice(1), query).score : 0 }))
.sort((a, b) => b.score - a.score)
.map(({ cmd }) => cmd);| if (!inputValue || !inputValue.startsWith('/') || !query) return true; | ||
| return fuzzyMatchWithScore(cmd.command.slice(1), query).matches; | ||
| }) | ||
| .sort((a, b) => { | ||
| if (!query) return 0; | ||
| return ( | ||
| fuzzyMatchWithScore(b.command.slice(1), query).score - |
There was a problem hiding this comment.
Sort applies fuzzy ordering when filter passes all commands unconditionally
When !inputValue || !inputValue.startsWith('/') is true, the filter short-circuits and returns true for every command — showing all commands regardless of query. However, the .sort() only skips ordering when !query. If inputValue has text that doesn't start with '/' (e.g. the autocomplete was opened via button while the field has existing text), query will be non-empty and all commands will be fuzzy-sorted by that incidental text — producing a confusing ordering that the user did not request.
The fix is to align the sort guard with the same condition the filter uses:
| if (!inputValue || !inputValue.startsWith('/') || !query) return true; | |
| return fuzzyMatchWithScore(cmd.command.slice(1), query).matches; | |
| }) | |
| .sort((a, b) => { | |
| if (!query) return 0; | |
| return ( | |
| fuzzyMatchWithScore(b.command.slice(1), query).score - | |
| .sort((a, b) => { | |
| if (!query || !inputValue || !inputValue.startsWith('/')) return 0; | |
| return fuzzyMatchWithScore(b.command.slice(1), query).score - fuzzyMatchWithScore(a.command.slice(1), query).score; | |
| }); |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/__tests__/renderer/utils/search.test.ts (1)
509-513: Assert the full dot-boundary index vector.This only checks the length and
indices[0], so the current[0, 1, 9, 10, 11]output would still pass. Once the matcher is fixed, assert the exact array here so the highlight regression stays covered.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/renderer/utils/search.test.ts` around lines 509 - 513, Update the test for fuzzyMatchWithIndices in the "returns correct indices for fuzzy match across dot boundary" case to assert the exact index vector instead of only length and the first element; specifically replace the loose checks with a single equality assertion that indices equals [0,8,9,10,11] (matching 's' at 0 and 'plan' at 8–11), ensuring the test fails if the matcher returns the older [0,1,9,10,11] result.
🤖 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/hooks/input/useInputKeyDown.ts`:
- Around line 209-220: The Enter/Tab handler is using a stale
selectedSlashCommandIndex against a newly filtered list; before accepting the
selection clamp selectedSlashCommandIndex into the valid range of
filteredCommands (e.g., Math.max(0, Math.min(selectedSlashCommandIndex,
filteredCommands.length - 1))) and use that clampedIndex to pick the command.
Update the logic in useInputKeyDown (the Enter/Tab branch that reads
selectedSlashCommandIndex and uses filteredCommands) so it recalculates/clamps
the index immediately after building filteredCommands and before performing the
accept action.
In `@src/renderer/utils/search.ts`:
- Around line 127-143: The current fuzzyMatchWithIndices returns the first
subsequence found; change it to prefer the ranked/boundary-aligned match by
doing a reverse-greedy match: in fuzzyMatchWithIndices, iterate lowerText from
the end and match lowerQuery from its last char backwards, collecting indices,
then reverse the collected indices before returning; this finds the
latest/boundary-aligned subsequence (e.g., picks the `.plan` p) and still
returns [] if the full query cannot be matched.
- Around line 77-84: The change added '.' as a word-boundary in the shared
fuzzyMatchWithScore heuristic, which unintentionally boosts scores in
FileSearchModal, TabSwitcherModal, useAtMentionCompletion, and SendToAgentModal;
revert that global change by making the dot boundary opt-in: add a parameter to
fuzzyMatchWithScore (e.g., allowDotBoundary or boundaryChars override)
defaulting to the previous behavior (no dot boundary), update only the
slash-command caller(s) to pass true (or include '.' in the boundaryChars) so
dot-scoring applies only for slash commands, and leave all other callers
unchanged.
---
Nitpick comments:
In `@src/__tests__/renderer/utils/search.test.ts`:
- Around line 509-513: Update the test for fuzzyMatchWithIndices in the "returns
correct indices for fuzzy match across dot boundary" case to assert the exact
index vector instead of only length and the first element; specifically replace
the loose checks with a single equality assertion that indices equals
[0,8,9,10,11] (matching 's' at 0 and 'plan' at 8–11), ensuring the test fails if
the matcher returns the older [0,1,9,10,11] result.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 5a67c039-d8b0-44c3-857c-20961ba1f9e5
📒 Files selected for processing (5)
src/__tests__/renderer/utils/search.test.tssrc/renderer/components/InputArea.tsxsrc/renderer/hooks/input/useInputKeyDown.tssrc/renderer/utils/search.tssrc/web/mobile/SlashCommandAutocomplete.tsx
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/renderer/components/InputArea.tsx (1)
321-335: Extract slash-command ranking into one shared helper.This exact filter/score/sort pipeline now exists here, in
src/renderer/hooks/input/useInputKeyDown.ts, and insrc/web/mobile/SlashCommandAutocomplete.tsx. Since desktop keyboard accept recomputes its own list, any future tweak in one copy can desync the visible order from what Enter/Tab inserts. A small sharedrankSlashCommands(...)helper would keep render and selection behavior locked together.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/InputArea.tsx` around lines 321 - 335, Extract the filter/score/sort pipeline into a shared helper (e.g., export function rankSlashCommands(slashCommands, inputValueLower, isTerminalMode)) that: strips leading "/", builds query, filters out cmd.terminalOnly when !isTerminalMode and cmd.aiOnly when isTerminalMode, computes score using fuzzyMatchWithScore(cmd.command.slice(1), query, '.') (score 0 when no query), sorts descending by score, and returns the ordered array of cmd objects; replace the inline useMemo logic that builds filteredSlashCommands with a call to rankSlashCommands(slashCommands, inputValueLower, isTerminalMode), and update the analogous usages in src/renderer/hooks/input/useInputKeyDown.ts and src/web/mobile/SlashCommandAutocomplete.tsx to call the same helper so rendering and selection use identical ranking logic.
🤖 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 145-174: The current two-pass in fuzzyMatchWithIndices always
picks the first boundaryMatch even if that choice prevents matching the rest of
lowerQuery; change the selection logic so a boundary position is only chosen if
the remaining query chars can be subsequence-matched after that boundary.
Concretely, when you find boundaryMatch inside the for-loop, run a quick
lookahead subsequence check (scan lowerText from boundaryMatch+1 for
lowerQuery[qi+1..]) and only accept boundaryMatch if that check succeeds;
otherwise fall back to firstMatch (or continue searching for the next boundary
and repeat the lookahead). Keep using the same variables (lowerQuery, lowerText,
isBoundary, qi, ti, firstMatch, boundaryMatch, indices) and update chosen/ti
accordingly so the algorithm remains suffix-safe.
---
Nitpick comments:
In `@src/renderer/components/InputArea.tsx`:
- Around line 321-335: Extract the filter/score/sort pipeline into a shared
helper (e.g., export function rankSlashCommands(slashCommands, inputValueLower,
isTerminalMode)) that: strips leading "/", builds query, filters out
cmd.terminalOnly when !isTerminalMode and cmd.aiOnly when isTerminalMode,
computes score using fuzzyMatchWithScore(cmd.command.slice(1), query, '.')
(score 0 when no query), sorts descending by score, and returns the ordered
array of cmd objects; replace the inline useMemo logic that builds
filteredSlashCommands with a call to rankSlashCommands(slashCommands,
inputValueLower, isTerminalMode), and update the analogous usages in
src/renderer/hooks/input/useInputKeyDown.ts and
src/web/mobile/SlashCommandAutocomplete.tsx to call the same helper so rendering
and selection use identical ranking logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: cb7f3edf-57bf-4fed-9f0c-8c7807bb5e40
📒 Files selected for processing (5)
src/__tests__/renderer/utils/search.test.tssrc/renderer/components/InputArea.tsxsrc/renderer/hooks/input/useInputKeyDown.tssrc/renderer/utils/search.tssrc/web/mobile/SlashCommandAutocomplete.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/tests/renderer/utils/search.test.ts
|
Not sure how to address this: |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/renderer/hooks/input/useInputKeyDown.ts (1)
210-222: Minor inefficiency:fuzzyMatchWithScorecalled twice per matching command.Each command that passes the filter has its score computed once in
.filter()(line 215) and again in.map()(line 219). Consider computing once and filtering/sorting together:♻️ Suggested refactor to compute score once
- const filteredCommands = allSlashCommands - .filter((cmd) => { - if ('terminalOnly' in cmd && cmd.terminalOnly && !isTerminalMode) return false; - if ('aiOnly' in cmd && cmd.aiOnly && isTerminalMode) return false; - if (!query) return true; - return fuzzyMatchWithScore(cmd.command.slice(1), query, '.').matches; - }) - .map((cmd) => ({ - cmd, - score: query ? fuzzyMatchWithScore(cmd.command.slice(1), query, '.').score : 0, - })) - .sort((a, b) => b.score - a.score) - .map(({ cmd }) => cmd); + const filteredCommands = allSlashCommands + .filter((cmd) => { + if ('terminalOnly' in cmd && cmd.terminalOnly && !isTerminalMode) return false; + if ('aiOnly' in cmd && cmd.aiOnly && isTerminalMode) return false; + return true; + }) + .map((cmd) => { + const { matches, score } = query + ? fuzzyMatchWithScore(cmd.command.slice(1), query, '.') + : { matches: true, score: 0 }; + return { cmd, matches, score }; + }) + .filter(({ matches }) => matches) + .sort((a, b) => b.score - a.score) + .map(({ cmd }) => cmd);🤖 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 210 - 222, The current filteredCommands pipeline calls fuzzyMatchWithScore twice per command; refactor so each command's score is computed once by mapping allSlashCommands to {cmd, score} (compute score as query ? fuzzyMatchWithScore(cmd.command.slice(1), query, '.').score : 0) while respecting the terminalOnly/aiOnly checks using isTerminalMode, then filter out entries with score/mode exclusion, sort by score, and finally map back to cmd; update the variable filteredCommands to use this single-pass map→filter→sort→map flow referencing filteredCommands, allSlashCommands, fuzzyMatchWithScore, query, and isTerminalMode.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/renderer/hooks/input/useInputKeyDown.ts`:
- Around line 210-222: The current filteredCommands pipeline calls
fuzzyMatchWithScore twice per command; refactor so each command's score is
computed once by mapping allSlashCommands to {cmd, score} (compute score as
query ? fuzzyMatchWithScore(cmd.command.slice(1), query, '.').score : 0) while
respecting the terminalOnly/aiOnly checks using isTerminalMode, then filter out
entries with score/mode exclusion, sort by score, and finally map back to cmd;
update the variable filteredCommands to use this single-pass map→filter→sort→map
flow referencing filteredCommands, allSlashCommands, fuzzyMatchWithScore, query,
and isTerminalMode.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: cf0bfb7b-93ea-4df7-ac54-d61ba4fb1234
📒 Files selected for processing (5)
src/__tests__/renderer/utils/search.test.tssrc/renderer/components/InputArea.tsxsrc/renderer/hooks/input/useInputKeyDown.tssrc/renderer/utils/search.tssrc/web/mobile/SlashCommandAutocomplete.tsx
✅ Files skipped from review due to trivial changes (1)
- src/tests/renderer/utils/search.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- src/web/mobile/SlashCommandAutocomplete.tsx
- src/renderer/utils/search.ts
- src/renderer/components/InputArea.tsx
There was a problem hiding this comment.
🧹 Nitpick comments (4)
src/web/mobile/SlashCommandAutocomplete.tsx (1)
118-123: Slight behavior difference from desktop: resetting vs clamping.This
useEffectresetsselectedIndexto 0 when it exceeds the filtered list length. The desktop implementation (inuseInputKeyDown.ts) clamps to the last valid index instead. Both are valid UX choices, but the inconsistency could be confusing if users switch between platforms.Consider aligning the behavior if consistent cross-platform UX is desired.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/web/mobile/SlashCommandAutocomplete.tsx` around lines 118 - 123, The current useEffect in SlashCommandAutocomplete resets selectedIndex to 0 when it exceeds filteredCommands.length; change it to clamp to the last valid index instead to match desktop behavior in useInputKeyDown.ts — i.e., when filteredCommands.length > 0 and selectedIndex >= filteredCommands.length call onSelectedIndexChange?.(filteredCommands.length - 1) rather than 0 so selectedIndex is clamped to the last item; keep the same dependency list and optional chaining.src/renderer/components/InputArea.tsx (1)
321-336: Minor optimization: avoid doublefuzzyMatchWithScorecalls.Each command is scored twice: once in the filter (line 328) and once in the map (line 332). Consider combining these into a single pass:
♻️ Suggested optimization
const filteredSlashCommands = useMemo(() => { const query = inputValueLower.replace(/^\//, ''); return slashCommands - .filter((cmd) => { + .map((cmd) => { if (cmd.terminalOnly && !isTerminalMode) return false; if (cmd.aiOnly && isTerminalMode) return false; - if (!query) return true; - return fuzzyMatchWithScore(cmd.command.slice(1), query, '.').matches; + if (!query) return { cmd, score: 0 }; + const result = fuzzyMatchWithScore(cmd.command.slice(1), query, '.'); + return result.matches ? { cmd, score: result.score } : false; }) - .map((cmd) => ({ - cmd, - score: query ? fuzzyMatchWithScore(cmd.command.slice(1), query, '.').score : 0, - })) + .filter((item): item is { cmd: typeof slashCommands[0]; score: number } => item !== false) .sort((a, b) => b.score - a.score) .map(({ cmd }) => cmd); }, [slashCommands, isTerminalMode, inputValueLower]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/InputArea.tsx` around lines 321 - 336, The current filteredSlashCommands use fuzzyMatchWithScore twice for each cmd (once in filter and again in map); change to a single pass by mapping slashCommands to objects that compute fuzzyMatchWithScore once (store matches and score), then filter based on terminalOnly/aiOnly and the computed matches (or allow when query is empty), sort by stored score, and finally map to cmd; update references in the closure for filteredSlashCommands and keep dependencies [slashCommands, isTerminalMode, inputValueLower].src/__tests__/renderer/utils/search.test.ts (1)
555-567: Tests 555-559 and 561-567 have identical assertions.Both tests assert
withDot.score > withoutDot.scorewith identical setup. The test names suggest different intents ("gives dot boundary bonus only when opted in" vs "does not give dot boundary bonus without extraBoundaryChars") but they're testing the same thing.Consider removing the duplicate or clarifying the distinct behavior each should verify.
♻️ Suggested fix: remove duplicate test
it('gives dot boundary bonus only when opted in', () => { const withDot = fuzzyMatchWithScore('hello.world', 'w', '.'); const withoutDot = fuzzyMatchWithScore('hello.world', 'w'); expect(withDot.score).toBeGreaterThan(withoutDot.score); }); - - it('does not give dot boundary bonus without extraBoundaryChars', () => { - // Without opt-in, dot is not treated as a boundary - const withDot = fuzzyMatchWithScore('hello.world', 'w', '.'); - const withoutDot = fuzzyMatchWithScore('hello.world', 'w'); - // Opting in should give a higher score than not opting in - expect(withDot.score).toBeGreaterThan(withoutDot.score); - });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/renderer/utils/search.test.ts` around lines 555 - 567, Remove the duplicate test that repeats the same assertion: delete the test case named "does not give dot boundary bonus without extraBoundaryChars" (the block that calls fuzzyMatchWithScore('hello.world','w','.') and fuzzyMatchWithScore('hello.world','w') and asserts withDot.score > withoutDot.score) since the earlier test "gives dot boundary bonus only when opted in" already covers this behavior; keep the first test and ensure only one assertion about fuzzyMatchWithScore's dot-boundary opt-in remains.src/renderer/hooks/input/useInputKeyDown.ts (1)
209-222: Consider extracting shared filtering logic.The slash command filtering and scoring logic here duplicates what's in
InputArea.tsx(lines 321-336). Both computefilteredCommandsidentically. While this works correctly, consider extracting this into a shared utility or custom hook to avoid divergence if the fuzzy matching logic changes.🤖 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 - 222, The slash-command filtering and scoring logic in useInputKeyDown.ts (the filteredCommands computation using allSlashCommands and fuzzyMatchWithScore) is duplicated in InputArea.tsx; extract that shared logic into a single utility or hook (e.g., create useFilteredSlashCommands or a helper function filterAndScoreSlashCommands) and replace the inline code in both useInputKeyDown.ts and InputArea.tsx to call the new function; ensure the exported helper accepts parameters used above (allSlashCommands, inputValue, isTerminalMode, fuzzyMatchWithScore) and returns the same sorted array of commands so behavior is unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/__tests__/renderer/utils/search.test.ts`:
- Around line 555-567: Remove the duplicate test that repeats the same
assertion: delete the test case named "does not give dot boundary bonus without
extraBoundaryChars" (the block that calls
fuzzyMatchWithScore('hello.world','w','.') and
fuzzyMatchWithScore('hello.world','w') and asserts withDot.score >
withoutDot.score) since the earlier test "gives dot boundary bonus only when
opted in" already covers this behavior; keep the first test and ensure only one
assertion about fuzzyMatchWithScore's dot-boundary opt-in remains.
In `@src/renderer/components/InputArea.tsx`:
- Around line 321-336: The current filteredSlashCommands use fuzzyMatchWithScore
twice for each cmd (once in filter and again in map); change to a single pass by
mapping slashCommands to objects that compute fuzzyMatchWithScore once (store
matches and score), then filter based on terminalOnly/aiOnly and the computed
matches (or allow when query is empty), sort by stored score, and finally map to
cmd; update references in the closure for filteredSlashCommands and keep
dependencies [slashCommands, isTerminalMode, inputValueLower].
In `@src/renderer/hooks/input/useInputKeyDown.ts`:
- Around line 209-222: The slash-command filtering and scoring logic in
useInputKeyDown.ts (the filteredCommands computation using allSlashCommands and
fuzzyMatchWithScore) is duplicated in InputArea.tsx; extract that shared logic
into a single utility or hook (e.g., create useFilteredSlashCommands or a helper
function filterAndScoreSlashCommands) and replace the inline code in both
useInputKeyDown.ts and InputArea.tsx to call the new function; ensure the
exported helper accepts parameters used above (allSlashCommands, inputValue,
isTerminalMode, fuzzyMatchWithScore) and returns the same sorted array of
commands so behavior is unchanged.
In `@src/web/mobile/SlashCommandAutocomplete.tsx`:
- Around line 118-123: The current useEffect in SlashCommandAutocomplete resets
selectedIndex to 0 when it exceeds filteredCommands.length; change it to clamp
to the last valid index instead to match desktop behavior in useInputKeyDown.ts
— i.e., when filteredCommands.length > 0 and selectedIndex >=
filteredCommands.length call onSelectedIndexChange?.(filteredCommands.length -
1) rather than 0 so selectedIndex is clamped to the last item; keep the same
dependency list and optional chaining.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0aad44e1-da56-42da-ad17-fc4cbaff9e26
📒 Files selected for processing (5)
src/__tests__/renderer/utils/search.test.tssrc/renderer/components/InputArea.tsxsrc/renderer/hooks/input/useInputKeyDown.tssrc/renderer/utils/search.tssrc/web/mobile/SlashCommandAutocomplete.tsx
9806758 to
cc4932b
Compare
1. Pre-compute scores — All 3 files now .map() scores once, then .sort() on cached values instead of calling fuzzyMatchWithScore per comparison. 2. Sort guard aligned with filter — Mobile's sort now checks shouldFuzzyFilter (same condition as filter), preventing incidental sorting when autocomplete is opened via button. 3. Clamped index on Enter/Tab — useInputKeyDown.ts now clamps selectedSlashCommandIndex to the filtered list length before accepting, preventing stale-index no-ops. 4. . boundary is opt-in — Reverted . from the shared fuzzyMatchWithScore heuristic. Added extraBoundaryChars param — only slash command call sites pass '.'. 5. Boundary-aware highlight indices — fuzzyMatchWithIndices now prefers boundary-anchored positions (e.g., splan → speckit.plan highlights s at 0 and plan at 8–11, not the p at index 1). 6. Adjust fuzzyMatchWithIndices: Fall to greedy when boundary choice would prevent remaining match 7. Rebased to please linter of release.md outside of the commit
Hi, I enjoy your project!
I wanted to add fuzzy search to the slash command for skills after having to type the speckit[dot] prefix way too many times.
I reused the fuzzy search used by the @ command to filter files. I made a styling that in my opinion should fit all themes but I am happy to change it to whatever or even no highlighting at all like the @ command".
I haven't looked into the performance of the fuzzy scoring but the number of skills should be reasonable and it uses the same fuzzy search used by the @ command!
I hope you like it!
Florian
Summary
/splanto match/speckit.plan,/ohelpto match/openspec.help, etc. Uses the samefuzzyMatchWithScoreengine that powers@mention file completion..as a word boundary character infuzzyMatchWithScore, so dot-separated command names (e.g.,speckit.plan) get proper boundary bonuses.fuzzyMatchWithIndicesutility tosearch.tsfor highlight rendering.InputArea.tsx,useInputKeyDown.ts) and mobile (SlashCommandAutocomplete.tsx).Test plan
search.test.tscovering fuzzy matching for slash commands andfuzzyMatchWithIndices/splan→ should show/speckit.planwiths,p,l,a,nbolded/hist→ should show/historyas top match (prefix still works)/→ should show all commands (no filter)Mobile version
Summary by CodeRabbit
New Features
Improvements
Tests