Skip to content

Commit f86e18d

Browse files
Mingwwwwcursoragent
andcommitted
feat(ui): render CommandMenu in ComposerV2 and detect slash input
- Import and render CommandMenu component in ComposerV2 (was previously unused — all command menu props were prefixed with _ and ignored) - Implement slash detection in handleCommandInputChange: typing / at the start of input or after whitespace opens the command menu with fuzzy filtering - Auto-execute skills on menu selection: skills without argumentHint trigger immediate submit (equivalent to typing /skill + Enter), while commands needing args still insert into the input field - Pass inputValueRef and handleSubmitRef to useSlashCommands for direct submit triggering from the auto-execute path Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b2a5646 commit f86e18d

3 files changed

Lines changed: 66 additions & 12 deletions

File tree

ui/src/components/chat-v2/ComposerV2.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
import type { ChatRunMode, PendingPermissionRequest, PermissionMode } from '../chat/types/types';
2828
import PermissionRequestsBanner from '../chat/view/subcomponents/PermissionRequestsBanner';
2929
import ImageAttachment from '../chat/view/subcomponents/ImageAttachment';
30+
import CommandMenu from '../chat/view/subcomponents/CommandMenu';
3031
import { cn } from '../../lib/utils.js';
3132

3233
interface MentionableFile {
@@ -264,12 +265,12 @@ export default function ComposerV2({
264265
filteredFiles,
265266
selectedFileIndex,
266267
onSelectFile,
267-
filteredCommands: _filteredCommands,
268-
selectedCommandIndex: _selectedCommandIndex,
269-
onCommandSelect: _onCommandSelect,
270-
onCloseCommandMenu: _onCloseCommandMenu,
271-
isCommandMenuOpen: _isCommandMenuOpen,
272-
frequentCommands: _frequentCommands,
268+
filteredCommands,
269+
selectedCommandIndex,
270+
onCommandSelect,
271+
onCloseCommandMenu,
272+
isCommandMenuOpen,
273+
frequentCommands,
273274
onToggleCommandMenu: _onToggleCommandMenu,
274275
onInsertMention,
275276
getRootProps,
@@ -418,6 +419,21 @@ export default function ComposerV2({
418419
>
419420
<input {...getInputProps()} />
420421

422+
<CommandMenu
423+
commands={filteredCommands}
424+
selectedIndex={selectedCommandIndex}
425+
onSelect={onCommandSelect}
426+
onClose={onCloseCommandMenu}
427+
isOpen={isCommandMenuOpen}
428+
frequentCommands={frequentCommands}
429+
position={(() => {
430+
const ta = textareaRef?.current;
431+
if (!ta) return { top: 0, left: 0, bottom: 90 };
432+
const rect = ta.getBoundingClientRect();
433+
return { top: rect.top - 8, left: rect.left, bottom: window.innerHeight - rect.top + 8 };
434+
})()}
435+
/>
436+
421437
<div className="relative">
422438
<div
423439
ref={inputHighlightRef}

ui/src/components/chat/hooks/useChatComposerState.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,8 @@ export function useChatComposerState({
518518
setInput,
519519
textareaRef,
520520
onExecuteCommand: executeCommand,
521+
inputValueRef,
522+
handleSubmitRef,
521523
});
522524

523525
const {

ui/src/components/chat/hooks/useSlashCommands.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ interface UseSlashCommandsOptions {
2424
setInput: Dispatch<SetStateAction<string>>;
2525
textareaRef: RefObject<HTMLTextAreaElement>;
2626
onExecuteCommand: (command: SlashCommand, rawInput?: string) => void | Promise<void>;
27+
inputValueRef?: { current: string };
28+
handleSubmitRef?: { current: ((event: any) => Promise<void>) | null };
2729
}
2830

2931
const getCommandHistoryKey = (projectName: string) => `command_history_${projectName}`;
@@ -55,6 +57,8 @@ export function useSlashCommands({
5557
setInput,
5658
textareaRef,
5759
onExecuteCommand,
60+
inputValueRef: externalInputValueRef,
61+
handleSubmitRef: externalHandleSubmitRef,
5862
}: UseSlashCommandsOptions) {
5963
const [slashCommands, setSlashCommands] = useState<SlashCommand[]>([]);
6064
const [filteredCommands, setFilteredCommands] = useState<SlashCommand[]>([]);
@@ -240,11 +244,20 @@ export function useSlashCommands({
240244

241245
const autoExecuteCommand = useCallback(
242246
(command: SlashCommand) => {
247+
trackCommandUsage(command);
243248
resetCommandMenuState();
244-
setInput('');
245-
onExecuteCommand(command, command.name);
249+
const commandText = command.name;
250+
setInput(commandText);
251+
if (externalInputValueRef) {
252+
externalInputValueRef.current = commandText;
253+
}
254+
setTimeout(() => {
255+
if (externalHandleSubmitRef?.current) {
256+
externalHandleSubmitRef.current({ preventDefault: () => {} });
257+
}
258+
}, 0);
246259
},
247-
[resetCommandMenuState, setInput, onExecuteCommand],
260+
[trackCommandUsage, resetCommandMenuState, setInput, externalInputValueRef, externalHandleSubmitRef],
248261
);
249262

250263
// Insert the picked command name into the textarea and leave the caret right
@@ -334,10 +347,33 @@ export function useSlashCommands({
334347
}, [showCommandMenu, slashCommands, textareaRef]);
335348

336349
const handleCommandInputChange = useCallback(
337-
() => {
338-
resetCommandMenuState();
350+
(value?: string, cursorPos?: number) => {
351+
if (value === undefined || cursorPos === undefined) {
352+
resetCommandMenuState();
353+
return;
354+
}
355+
356+
const textBeforeCursor = value.slice(0, cursorPos);
357+
const slashMatch = textBeforeCursor.match(/(^|\s)(\/)(\S*)$/);
358+
359+
if (!slashMatch) {
360+
resetCommandMenuState();
361+
return;
362+
}
363+
364+
const slashIdx = textBeforeCursor.lastIndexOf('/');
365+
const query = slashMatch[3] || '';
366+
367+
setSlashPosition(slashIdx);
368+
setShowCommandMenu(true);
369+
setSelectedCommandIndex(query ? -1 : 0);
370+
371+
clearCommandQueryTimer();
372+
commandQueryTimerRef.current = window.setTimeout(() => {
373+
setCommandQuery(query);
374+
}, COMMAND_QUERY_DEBOUNCE_MS);
339375
},
340-
[resetCommandMenuState],
376+
[resetCommandMenuState, clearCommandQueryTimer],
341377
);
342378

343379
const handleCommandMenuKeyDown = useCallback(

0 commit comments

Comments
 (0)