From 597e9c54b76e7c6cd1947299c668c78d24019cab Mon Sep 17 00:00:00 2001 From: Vadim Trunov Date: Sun, 22 Feb 2026 14:25:02 +0100 Subject: [PATCH] fix: slash commands with arguments bypass command execution (#392) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: intercept slash commands in handleSubmit to pass arguments correctly When a user types a slash command with arguments (e.g. `/feature implement dark mode`) and presses Enter, the command was not being intercepted as a slash command. Instead, the raw text was sent as a regular message to the Claude API, which responded with "Unknown skill: feature". Root cause: the command autocomplete menu (useSlashCommands) detects commands via the regex `/(^|\s)\/(\S*)$/` which only matches when the cursor is right after the command name with no spaces. As soon as the user types a space to add arguments, the pattern stops matching, the menu closes, and pressing Enter falls through to handleSubmit which sends the text as a plain message — completely bypassing command execution. This fix adds a slash command interceptor at the top of handleSubmit: - Checks if the trimmed input starts with `/` - Extracts the command name (text before the first space) - Looks up the command in the loaded slashCommands list - If found, delegates to executeCommand() which properly extracts arguments via regex and sends them to the backend /api/commands/execute endpoint - The backend then replaces $ARGUMENTS, $1, $2 etc. in the command template Changes: - Added `slashCommands` to the destructured return of useSlashCommands hook - Added slash command interception logic in handleSubmit before message dispatch - Added `executeCommand` and `slashCommands` to handleSubmit dependency array Co-Authored-By: Claude Opus 4.6 * fix: address review — pass rawInput param and cleanup UI state - Pass trimmedInput to executeCommand to avoid stale closure reads - Add UI cleanup (images, command menu, textarea) before early return - Update executeCommand signature to accept optional rawInput parameter Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .../chat/hooks/useChatComposerState.ts | 30 +++++++++++++++++-- src/components/chat/hooks/useSlashCommands.ts | 2 +- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/components/chat/hooks/useChatComposerState.ts b/src/components/chat/hooks/useChatComposerState.ts index ae37bce..ca430a1 100644 --- a/src/components/chat/hooks/useChatComposerState.ts +++ b/src/components/chat/hooks/useChatComposerState.ts @@ -271,13 +271,14 @@ export function useChatComposerState({ }, [setChatMessages]); const executeCommand = useCallback( - async (command: SlashCommand) => { + async (command: SlashCommand, rawInput?: string) => { if (!command || !selectedProject) { return; } try { - const commandMatch = input.match(new RegExp(`${escapeRegExp(command.name)}\\s*(.*)`)); + const effectiveInput = rawInput ?? input; + const commandMatch = effectiveInput.match(new RegExp(`${escapeRegExp(command.name)}\\s*(.*)`)); const args = commandMatch && commandMatch[1] ? commandMatch[1].trim().split(/\s+/) : []; @@ -351,6 +352,7 @@ export function useChatComposerState({ ); const { + slashCommands, slashCommandsCount, filteredCommands, frequentCommands, @@ -473,6 +475,28 @@ export function useChatComposerState({ return; } + // Intercept slash commands: if input starts with /commandName, execute as command with args + const trimmedInput = currentInput.trim(); + if (trimmedInput.startsWith('/')) { + const firstSpace = trimmedInput.indexOf(' '); + const commandName = firstSpace > 0 ? trimmedInput.slice(0, firstSpace) : trimmedInput; + const matchedCommand = slashCommands.find((cmd: SlashCommand) => cmd.name === commandName); + if (matchedCommand) { + executeCommand(matchedCommand, trimmedInput); + setInput(''); + inputValueRef.current = ''; + setAttachedImages([]); + setUploadingImages(new Map()); + setImageErrors(new Map()); + resetCommandMenuState(); + setIsTextareaExpanded(false); + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + } + return; + } + } + let messageContent = currentInput; const selectedThinkingMode = thinkingModes.find((mode: { id: string; prefix?: string }) => mode.id === thinkingMode); if (selectedThinkingMode && selectedThinkingMode.prefix) { @@ -639,6 +663,7 @@ export function useChatComposerState({ codexModel, currentSessionId, cursorModel, + executeCommand, isLoading, onSessionActive, pendingViewSessionRef, @@ -654,6 +679,7 @@ export function useChatComposerState({ setClaudeStatus, setIsLoading, setIsUserScrolledUp, + slashCommands, thinkingMode, ], ); diff --git a/src/components/chat/hooks/useSlashCommands.ts b/src/components/chat/hooks/useSlashCommands.ts index f14fc43..067cd24 100644 --- a/src/components/chat/hooks/useSlashCommands.ts +++ b/src/components/chat/hooks/useSlashCommands.ts @@ -22,7 +22,7 @@ interface UseSlashCommandsOptions { input: string; setInput: Dispatch>; textareaRef: RefObject; - onExecuteCommand: (command: SlashCommand) => void | Promise; + onExecuteCommand: (command: SlashCommand, rawInput?: string) => void | Promise; } const getCommandHistoryKey = (projectName: string) => `command_history_${projectName}`;