Skip to content

feat: Add fuzzy search for slash commands#678

Closed
scriptease wants to merge 2 commits intoRunMaestro:mainfrom
scriptease:main
Closed

feat: Add fuzzy search for slash commands#678
scriptease wants to merge 2 commits intoRunMaestro:mainfrom
scriptease:main

Conversation

@scriptease
Copy link
Copy Markdown

@scriptease scriptease commented Mar 28, 2026

Fuzzy slash command matching in action

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

  • Fuzzy matching for slash commands: Type abbreviations like /splan to match /speckit.plan, /ohelp to match /openspec.help, etc. Uses the same fuzzyMatchWithScore engine that powers @ mention file completion.
  • Match highlighting: Matched characters render in bold with non-matched characters slightly dimmed (80% opacity), giving clear visual feedback on what's matching.
  • Sorted by relevance: Results are ranked by match quality — exact prefixes first, then boundary-anchored matches, then general subsequences.
  • Added . as a word boundary character in fuzzyMatchWithScore, so dot-separated command names (e.g., speckit.plan) get proper boundary bonuses.
  • Added fuzzyMatchWithIndices utility to search.ts for highlight rendering.
  • Applied consistently across desktop (InputArea.tsx, useInputKeyDown.ts) and mobile (SlashCommandAutocomplete.tsx).

Test plan

  • Tests added to search.test.ts covering fuzzy matching for slash commands and fuzzyMatchWithIndices
  • Type /splan → should show /speckit.plan with s, p, l, a, n bolded
  • Type /hist → should show /history as top match (prefix still works)
  • Type / → should show all commands (no filter)
  • Arrow keys and Tab/Enter selection still work correctly
  • Verify on mobile web interface

Mobile version

Fuzzy slash command matching in action mobile

Summary by CodeRabbit

  • New Features

    • Slash command autocomplete now uses fuzzy matching, highlights matched characters, and ranks results by relevance.
    • Tab/Enter selection is more robust to list changes, preventing out-of-bounds selection.
  • Improvements

    • Boundary-aware matching with optional dot support improves cross-boundary and abbreviation matches.
    • Empty input (or just "/") now surfaces available commands while still respecting mode restrictions.
  • Tests

    • Added tests covering match indices, empty/non-match cases, boundary behavior, scoring, and ranking.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 28, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Fuzzy subsequence matching and index-returning matching were added; fuzzyMatchWithScore accepts optional extraBoundaryChars (e.g., .) for boundary-aware scoring. Slash-command autocomplete (desktop and mobile) and input handling now use fuzzy matching, score-based sorting, and matched-character highlighting. Tests cover indices and boundary behavior.

Changes

Cohort / File(s) Summary
Search utility
src/renderer/utils/search.ts
Added fuzzyMatchWithIndices(text, query, extraBoundaryChars?) returning matched character indices; extended fuzzyMatchWithScore(text, query, extraBoundaryChars?) to accept extra boundary chars and apply boundary-aware scoring and selection.
Tests
src/__tests__/renderer/utils/search.test.ts
Added tests for fuzzyMatchWithIndices (empty/too-long guards, prefix matches, dot-boundary behavior, greedy fallback) and additional fuzzyMatchWithScore assertions for boundary scoring and ranking.
Desktop input & hook
src/renderer/components/InputArea.tsx, src/renderer/hooks/input/useInputKeyDown.ts
Replaced prefix-only slash-command filtering with fuzzy matching (strip leading /, lowercase) using fuzzyMatchWithScore; clamp selection index; sort candidates by score; render highlighted matched characters via fuzzyMatchWithIndices (with offset for leading /).
Mobile autocomplete
src/web/mobile/SlashCommandAutocomplete.tsx
Switched to fuzzy-match filtering and score-based sorting when input starts with /; memoized filtered/sorted list; highlight matched characters using fuzzyMatchWithIndices with / offset; preserve original rendering when no query.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through letters, dot and dash,
Found hidden hops in each small cache.
Scores lined up and indices bright,
I nudged the menu into light.
Autocomplete — a moonlit bite.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: adding fuzzy search functionality to slash commands, which is the core focus across test additions and updates to InputArea, useInputKeyDown, and SlashCommandAutocomplete components.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@scriptease scriptease changed the title Add fuzzy search for slash commands feat: Add fuzzy search for slash commands Mar 28, 2026
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 28, 2026

Greptile Summary

This PR adds fuzzy matching to slash command autocomplete across desktop (InputArea.tsx, useInputKeyDown.ts) and mobile (SlashCommandAutocomplete.tsx), reusing the existing fuzzyMatchWithScore engine (extended with . as a word-boundary character) and adding a new fuzzyMatchWithIndices utility for highlight rendering. The implementation is clean and well-tested, with two minor issues worth addressing:

  • Redundant scoring in sort comparators (InputArea.tsx, useInputKeyDown.ts, SlashCommandAutocomplete.tsx): Each sort comparison calls fuzzyMatchWithScore twice (once per operand), and the same item's score gets recomputed across many comparisons. Scores should be pre-computed before sorting.
  • Sort/filter guard mismatch on mobile (SlashCommandAutocomplete.tsx): The filter returns all commands unconditionally when inputValue doesn't start with '/', but the sort only skips ordering when !query. If the autocomplete is opened via button while the input contains non-/ text, all commands will be fuzzy-sorted by that text, producing an unexpected ordering.

Confidence Score: 5/5

Safe 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

Filename Overview
src/renderer/utils/search.ts Adds dot (.) as a word-boundary character in fuzzyMatchWithScore and introduces fuzzyMatchWithIndices — a greedy left-to-right subsequence matcher that returns matched character positions. Both algorithms are consistent (same greedy strategy), so highlight positions always correspond to what the scoring function matched.
src/renderer/components/InputArea.tsx Replaces startsWith filtering with fuzzyMatchWithScore and adds character-level highlight rendering via fuzzyMatchWithIndices. The sort comparator calls fuzzyMatchWithScore twice per comparison — a redundant computation that should be pre-computed. Logic is otherwise correct.
src/renderer/hooks/input/useInputKeyDown.ts Updates keyboard-navigation command filtering from startsWith to fuzzy matching, consistent with InputArea.tsx. Same double-computation pattern in the sort comparator as the other files.
src/web/mobile/SlashCommandAutocomplete.tsx Adds fuzzy filtering and highlight rendering for mobile; wraps in useMemo (improvement). Has a logic inconsistency where the sort applies fuzzy ordering even when the filter shows all commands unconditionally (e.g. when inputValue doesn't start with /), and shares the double-computation issue in the sort comparator.
src/tests/renderer/utils/search.test.ts Adds clear, well-structured tests for fuzzyMatchWithIndices and dot-boundary slash-command fuzzy matching scenarios. Coverage is good.

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]
Loading

Reviews (1): Last reviewed commit: "feat: Add fuzzy search for slash command..." | Re-trigger Greptile

Comment on lines +330 to +333
.sort((a, b) => {
if (!query) return 0;
return (
fuzzyMatchWithScore(b.command.slice(1), query).score -
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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:

Suggested change
.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);

Comment on lines +103 to +109
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 -
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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:

Suggested change
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;
});

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 06856a8 and 07017ab.

📒 Files selected for processing (5)
  • src/__tests__/renderer/utils/search.test.ts
  • src/renderer/components/InputArea.tsx
  • src/renderer/hooks/input/useInputKeyDown.ts
  • src/renderer/utils/search.ts
  • src/web/mobile/SlashCommandAutocomplete.tsx

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in src/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 shared rankSlashCommands(...) 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

📥 Commits

Reviewing files that changed from the base of the PR and between 185a453 and 194c403.

📒 Files selected for processing (5)
  • src/__tests__/renderer/utils/search.test.ts
  • src/renderer/components/InputArea.tsx
  • src/renderer/hooks/input/useInputKeyDown.ts
  • src/renderer/utils/search.ts
  • src/web/mobile/SlashCommandAutocomplete.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/tests/renderer/utils/search.test.ts

@scriptease
Copy link
Copy Markdown
Author

Not sure how to address this:
Run npx prettier --check . Checking formatting... [warn] docs/releases.md [warn] Code style issues found in the above file. Run Prettier with --write to fix. Error: Process completed with exit code 1.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/renderer/hooks/input/useInputKeyDown.ts (1)

210-222: Minor inefficiency: fuzzyMatchWithScore called 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

📥 Commits

Reviewing files that changed from the base of the PR and between 194c403 and c2009bb.

📒 Files selected for processing (5)
  • src/__tests__/renderer/utils/search.test.ts
  • src/renderer/components/InputArea.tsx
  • src/renderer/hooks/input/useInputKeyDown.ts
  • src/renderer/utils/search.ts
  • src/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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
src/web/mobile/SlashCommandAutocomplete.tsx (1)

118-123: Slight behavior difference from desktop: resetting vs clamping.

This useEffect resets selectedIndex to 0 when it exceeds the filtered list length. The desktop implementation (in useInputKeyDown.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 double fuzzyMatchWithScore calls.

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.score with 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 compute filteredCommands identically. 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

📥 Commits

Reviewing files that changed from the base of the PR and between c2009bb and 40e65c2.

📒 Files selected for processing (5)
  • src/__tests__/renderer/utils/search.test.ts
  • src/renderer/components/InputArea.tsx
  • src/renderer/hooks/input/useInputKeyDown.ts
  • src/renderer/utils/search.ts
  • src/web/mobile/SlashCommandAutocomplete.tsx

@scriptease scriptease force-pushed the main branch 2 times, most recently from 9806758 to cc4932b Compare March 29, 2026 08:25
  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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants