Slash commands (#5718)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Douwe Osinga
2025-11-20 21:14:15 +01:00
committed by GitHub
parent 6e511d1e8a
commit bdf68bb4f5
28 changed files with 1675 additions and 1048 deletions
+18 -15
View File
@@ -18,7 +18,7 @@ import { useModelAndProvider } from './ModelAndProviderContext';
import { useWhisper } from '../hooks/useWhisper';
import { WaveformVisualizer } from './WaveformVisualizer';
import { toastError } from '../toasts';
import MentionPopover, { FileItemWithMatch } from './MentionPopover';
import MentionPopover, { DisplayItemWithMatch } from './MentionPopover';
import { useDictationSettings } from '../hooks/useDictationSettings';
import { COST_TRACKING_ENABLED, VOICE_DICTATION_ELEVENLABS_ENABLED } from '../updates';
import { CostTracker } from './bottom_menu/CostTracker';
@@ -215,15 +215,17 @@ export default function ChatInput({
query: string;
mentionStart: number;
selectedIndex: number;
isSlashCommand: boolean;
}>({
isOpen: false,
position: { x: 0, y: 0 },
query: '',
mentionStart: -1,
selectedIndex: 0,
isSlashCommand: false,
});
const mentionPopoverRef = useRef<{
getDisplayFiles: () => FileItemWithMatch[];
getDisplayFiles: () => DisplayItemWithMatch[];
selectFile: (index: number) => void;
}>(null);
@@ -563,13 +565,17 @@ export default function ChatInput({
setDisplayValue(val);
updateValue(val);
setHasUserTyped(true);
checkForMention(val, cursorPosition, evt.target);
checkForMentionOrSlash(val, cursorPosition, evt.target);
};
const checkForMention = (text: string, cursorPosition: number, textArea: HTMLTextAreaElement) => {
// Find the last @ before the cursor
const checkForMentionOrSlash = (
text: string,
cursorPosition: number,
textArea: HTMLTextAreaElement
) => {
const isSlashCommand = text.startsWith('/');
const beforeCursor = text.slice(0, cursorPosition);
const lastAtIndex = beforeCursor.lastIndexOf('@');
const lastAtIndex = isSlashCommand ? 0 : beforeCursor.lastIndexOf('@');
if (lastAtIndex === -1) {
// No @ found, close mention popover
@@ -597,6 +603,7 @@ export default function ChatInput({
query: afterAt,
mentionStart: lastAtIndex,
selectedIndex: 0, // Reset selection when query changes
isSlashCommand,
// filteredFiles will be populated by the MentionPopover component
}));
};
@@ -1024,13 +1031,13 @@ export default function ChatInput({
}
};
const handleMentionFileSelect = (filePath: string) => {
const handleMentionItemSelect = (itemText: string) => {
// Replace the @ mention with the file path
const beforeMention = displayValue.slice(0, mentionPopover.mentionStart);
const afterMention = displayValue.slice(
mentionPopover.mentionStart + 1 + mentionPopover.query.length
);
const newValue = `${beforeMention}${filePath}${afterMention}`;
const newValue = `${beforeMention}${itemText}${afterMention}`;
setDisplayValue(newValue);
setValue(newValue);
@@ -1040,7 +1047,7 @@ export default function ChatInput({
// Set cursor position after the inserted file path
setTimeout(() => {
if (textAreaRef.current) {
const newCursorPosition = beforeMention.length + filePath.length;
const newCursorPosition = beforeMention.length + itemText.length;
textAreaRef.current.setSelectionRange(newCursorPosition, newCursorPosition);
}
}, 0);
@@ -1453,7 +1460,6 @@ export default function ChatInput({
{/* Directory path */}
<DirSwitcher className="mr-0" />
<div className="w-px h-4 bg-border-default mx-2" />
<Tooltip>
<TooltipTrigger asChild>
<Button
@@ -1468,9 +1474,7 @@ export default function ChatInput({
</TooltipTrigger>
<TooltipContent>Attach file or directory</TooltipContent>
</Tooltip>
<div className="w-px h-4 bg-border-default mx-2" />
{/* Model selector, mode selector, alerts, summarize button */}
<div className="flex flex-row items-center">
{/* Cost Tracker */}
@@ -1538,7 +1542,6 @@ export default function ChatInput({
</Tooltip>
)}
</div>
{sessionId && diagnosticsOpen && (
<DiagnosticsModal
isOpen={diagnosticsOpen}
@@ -1546,12 +1549,12 @@ export default function ChatInput({
sessionId={sessionId}
/>
)}
<MentionPopover
ref={mentionPopoverRef}
isOpen={mentionPopover.isOpen}
isSlashCommand={mentionPopover.isSlashCommand}
onClose={() => setMentionPopover((prev) => ({ ...prev, isOpen: false }))}
onSelect={handleMentionFileSelect}
onSelect={handleMentionItemSelect}
position={mentionPopover.position}
query={mentionPopover.query}
selectedIndex={mentionPopover.selectedIndex}