From 7a461d82f9acc60ba056e134600ac6970e27191d Mon Sep 17 00:00:00 2001 From: Felipe Gobbi Date: Sun, 15 Feb 2026 17:56:46 -0300 Subject: [PATCH 1/3] feat: improve slash command search to match anywhere in name or description Replace prefix-only filtering (startsWith) with substring matching (includes) on both command name and description fields. Users can now find commands by typing any part of the name or description text (e.g., "synopsis" finds /history). Co-Authored-By: Claude Opus 4.6 --- src/renderer/App.tsx | 3 ++- src/renderer/components/InputArea.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index f1cce1441..9a1916ad9 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -9370,7 +9370,8 @@ You are taking over this conversation. Based on the context above, provide a bri // Check if command is only available in AI mode if ('aiOnly' in cmd && cmd.aiOnly && isTerminalMode) return false; // Check if command matches input - return cmd.command.toLowerCase().startsWith(inputValue.toLowerCase()); + const inputLower = inputValue.toLowerCase(); + return cmd.command.toLowerCase().includes(inputLower) || (cmd.description && cmd.description.toLowerCase().includes(inputLower)); }); if (e.key === 'ArrowDown') { diff --git a/src/renderer/components/InputArea.tsx b/src/renderer/components/InputArea.tsx index db6c09573..6b35fcf16 100644 --- a/src/renderer/components/InputArea.tsx +++ b/src/renderer/components/InputArea.tsx @@ -314,7 +314,7 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) { // Check if command is only available in AI mode if (cmd.aiOnly && isTerminalMode) return false; // Check if command matches input - return cmd.command.toLowerCase().startsWith(inputValueLower); + return cmd.command.toLowerCase().includes(inputValueLower) || (cmd.description && cmd.description.toLowerCase().includes(inputValueLower)); }); }, [slashCommands, isTerminalMode, inputValueLower]); From bd657bdfc1a21c12aa70fa7642f9cd0fad929e34 Mon Sep 17 00:00:00 2001 From: Felipe Gobbi Date: Sun, 15 Feb 2026 18:09:47 -0300 Subject: [PATCH 2/3] fix: strip leading slash before substring matching in command search The previous includes() change still compared "/prd" against "/GLOBAL:...", which never matched. Now both input and command name have the leading / stripped before comparison, so typing "prd" correctly finds "/GLOBAL:agents:prd-creator". Co-Authored-By: Claude Opus 4.6 --- src/renderer/App.tsx | 7 ++++--- src/renderer/components/InputArea.tsx | 8 +++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 9a1916ad9..292e6f22b 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -9369,9 +9369,10 @@ You are taking over this conversation. Based on the context above, provide a bri if ('terminalOnly' in cmd && cmd.terminalOnly && !isTerminalMode) return false; // Check if command is only available in AI mode if ('aiOnly' in cmd && cmd.aiOnly && isTerminalMode) return false; - // Check if command matches input - const inputLower = inputValue.toLowerCase(); - return cmd.command.toLowerCase().includes(inputLower) || (cmd.description && cmd.description.toLowerCase().includes(inputLower)); + // Check if command matches input (strip leading / from both for substring matching) + const searchTerm = inputValue.toLowerCase().replace(/^\//, ''); + const cmdName = cmd.command.toLowerCase().replace(/^\//, ''); + return cmdName.includes(searchTerm) || (cmd.description && cmd.description.toLowerCase().includes(searchTerm)); }); if (e.key === 'ArrowDown') { diff --git a/src/renderer/components/InputArea.tsx b/src/renderer/components/InputArea.tsx index 6b35fcf16..a567c6619 100644 --- a/src/renderer/components/InputArea.tsx +++ b/src/renderer/components/InputArea.tsx @@ -307,16 +307,18 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) { // PERF: Memoize both the lowercase conversion and filtered results to avoid // recalculating on every render - inputValue changes on every keystroke const inputValueLower = useMemo(() => inputValue.toLowerCase(), [inputValue]); + const searchTerm = useMemo(() => inputValueLower.replace(/^\//, ''), [inputValueLower]); const filteredSlashCommands = useMemo(() => { return slashCommands.filter((cmd) => { // Check if command is only available in terminal mode if (cmd.terminalOnly && !isTerminalMode) return false; // Check if command is only available in AI mode if (cmd.aiOnly && isTerminalMode) return false; - // Check if command matches input - return cmd.command.toLowerCase().includes(inputValueLower) || (cmd.description && cmd.description.toLowerCase().includes(inputValueLower)); + // Check if command matches input (strip leading / from both for substring matching) + const cmdName = cmd.command.toLowerCase().replace(/^\//, ''); + return cmdName.includes(searchTerm) || (cmd.description && cmd.description.toLowerCase().includes(searchTerm)); }); - }, [slashCommands, isTerminalMode, inputValueLower]); + }, [slashCommands, isTerminalMode, searchTerm]); // Ensure selectedSlashCommandIndex is valid for the filtered list const safeSelectedIndex = Math.min( From a466b53f971fb3bb9aef5ccc6976bb18c4205bde Mon Sep 17 00:00:00 2001 From: Felipe Gobbi Date: Sun, 15 Feb 2026 18:15:36 -0300 Subject: [PATCH 3/3] feat: add ranked scoring to slash command search results Results are now sorted by match quality: - Score 3: command name starts with search term (prefix match) - Score 2: command name contains search term (substring match) - Score 1: description contains search term This prevents low-relevance description matches from burying exact name matches. Co-Authored-By: Claude Opus 4.6 --- src/renderer/App.tsx | 21 ++++++++++++--------- src/renderer/components/InputArea.tsx | 19 +++++++++++-------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 292e6f22b..e389c9254 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -9364,16 +9364,19 @@ You are taking over this conversation. Based on the context above, provide a bri // Handle slash command autocomplete if (slashCommandOpen) { const isTerminalMode = activeSession?.inputMode === 'terminal'; - const filteredCommands = allSlashCommands.filter((cmd) => { - // Check if command is only available in terminal mode - if ('terminalOnly' in cmd && cmd.terminalOnly && !isTerminalMode) return false; - // Check if command is only available in AI mode - if ('aiOnly' in cmd && cmd.aiOnly && isTerminalMode) return false; - // Check if command matches input (strip leading / from both for substring matching) - const searchTerm = inputValue.toLowerCase().replace(/^\//, ''); + const searchTerm = inputValue.toLowerCase().replace(/^\//, ''); + const scored: { cmd: typeof allSlashCommands[number]; score: number }[] = []; + for (const cmd of allSlashCommands) { + if ('terminalOnly' in cmd && cmd.terminalOnly && !isTerminalMode) continue; + if ('aiOnly' in cmd && cmd.aiOnly && isTerminalMode) continue; + if (!searchTerm) { scored.push({ cmd, score: 0 }); continue; } const cmdName = cmd.command.toLowerCase().replace(/^\//, ''); - return cmdName.includes(searchTerm) || (cmd.description && cmd.description.toLowerCase().includes(searchTerm)); - }); + if (cmdName.startsWith(searchTerm)) { scored.push({ cmd, score: 3 }); continue; } + if (cmdName.includes(searchTerm)) { scored.push({ cmd, score: 2 }); continue; } + if (cmd.description && cmd.description.toLowerCase().includes(searchTerm)) { scored.push({ cmd, score: 1 }); continue; } + } + scored.sort((a, b) => b.score - a.score); + const filteredCommands = scored.map((s) => s.cmd); if (e.key === 'ArrowDown') { e.preventDefault(); diff --git a/src/renderer/components/InputArea.tsx b/src/renderer/components/InputArea.tsx index a567c6619..b4e9073b3 100644 --- a/src/renderer/components/InputArea.tsx +++ b/src/renderer/components/InputArea.tsx @@ -309,15 +309,18 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) { const inputValueLower = useMemo(() => inputValue.toLowerCase(), [inputValue]); const searchTerm = useMemo(() => inputValueLower.replace(/^\//, ''), [inputValueLower]); const filteredSlashCommands = useMemo(() => { - return slashCommands.filter((cmd) => { - // Check if command is only available in terminal mode - if (cmd.terminalOnly && !isTerminalMode) return false; - // Check if command is only available in AI mode - if (cmd.aiOnly && isTerminalMode) return false; - // Check if command matches input (strip leading / from both for substring matching) + const scored: { cmd: SlashCommand; score: number }[] = []; + for (const cmd of slashCommands) { + if (cmd.terminalOnly && !isTerminalMode) continue; + if (cmd.aiOnly && isTerminalMode) continue; + if (!searchTerm) { scored.push({ cmd, score: 0 }); continue; } const cmdName = cmd.command.toLowerCase().replace(/^\//, ''); - return cmdName.includes(searchTerm) || (cmd.description && cmd.description.toLowerCase().includes(searchTerm)); - }); + if (cmdName.startsWith(searchTerm)) { scored.push({ cmd, score: 3 }); continue; } + if (cmdName.includes(searchTerm)) { scored.push({ cmd, score: 2 }); continue; } + if (cmd.description && cmd.description.toLowerCase().includes(searchTerm)) { scored.push({ cmd, score: 1 }); continue; } + } + scored.sort((a, b) => b.score - a.score); + return scored.map((s) => s.cmd); }, [slashCommands, isTerminalMode, searchTerm]); // Ensure selectedSlashCommandIndex is valid for the filtered list