diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index cf04d28e1200b95759070779b769a33c8e5e1648..7be96df9c52a8bc2d0da5541f9baa60ad367e6ea 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -96,7 +96,7 @@ import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "./ui/menu" import { CursorIcon, Icon } from "./Icons"; import { cn, isMacPlatform, isWindowsPlatform } from "~/lib/utils"; import { Badge } from "./ui/badge"; -import { Command, CommandItem, CommandList } from "./ui/command"; +import { Command, CommandInput, CommandItem, CommandList } from "./ui/command"; function formatMessageMeta(createdAt: string, duration: string | null): string { if (!duration) return formatTimestamp(createdAt); @@ -220,27 +220,13 @@ const VscodeEntryIcon = memo(function VscodeEntryIcon(props: { const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: { item: ComposerCommandItem; - index: number; - isHighlighted: boolean; - keyboardHighlight: boolean; resolvedTheme: "light" | "dark"; - onHighlight: (index: number) => void; onSelect: (item: ComposerCommandItem) => void; }) { return ( { - props.onHighlight(props.index); - }} + className="cursor-pointer gap-2" onMouseDown={(event) => { event.preventDefault(); }} @@ -271,31 +257,30 @@ const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: { const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { items: ComposerCommandItem[]; - selectedIndex: number; - keyboardHighlight: boolean; resolvedTheme: "light" | "dark"; isLoading: boolean; triggerKind: ComposerTriggerKind | null; - onHighlight: (index: number) => void; + onHighlightedItemChange: (itemId: string | null) => void; onSelect: (item: ComposerCommandItem) => void; - menuRef: RefObject; + commandInputRef: RefObject; }) { return ( - -
+ { + props.onHighlightedItemChange(typeof highlightedValue === "string" ? highlightedValue : null); + }} + > +
+
+ +
- {props.items.map((item, index) => ( + {props.items.map((item) => ( ))} @@ -337,14 +322,12 @@ export default function ChatView() { const [nowTick, setNowTick] = useState(() => Date.now()); const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); const [composerCursor, setComposerCursor] = useState(0); - const [composerMenuIndex, setComposerMenuIndex] = useState(0); - const [composerKeyboardHighlight, setComposerKeyboardHighlight] = useState(false); + const [composerHighlightedItemId, setComposerHighlightedItemId] = useState(null); const messagesScrollRef = useRef(null); const messagesEndRef = useRef(null); const shouldAutoScrollRef = useRef(true); const textareaRef = useRef(null); - const composerMenuRef = useRef(null); - const previousComposerMenuIndexRef = useRef(0); + const composerCommandInputRef = useRef(null); const composerImagesRef = useRef([]); const dragDepthRef = useRef(0); const terminalOpenByThreadRef = useRef>({}); @@ -519,9 +502,13 @@ export default function ChatView() { })); }, [composerTrigger, workspaceEntries]); const composerMenuOpen = Boolean(composerTrigger); - const activeComposerMenuItem = - composerMenuItems[Math.min(composerMenuIndex, Math.max(0, composerMenuItems.length - 1))] ?? - null; + const activeComposerMenuItem = useMemo( + () => + composerMenuItems.find((item) => item.id === composerHighlightedItemId) ?? + composerMenuItems[0] ?? + null, + [composerHighlightedItemId, composerMenuItems], + ); const keybindings = keybindingsQuery.data ?? EMPTY_KEYBINDINGS; // Default true while loading to avoid toolbar flicker. const isGitRepo = branchesQuery.data?.isRepo ?? true; @@ -688,36 +675,14 @@ export default function ChatView() { }, [activeThread?.id]); useEffect(() => { - setComposerMenuIndex(0); - }, [composerTrigger?.kind, composerTrigger?.query, composerMenuItems.length]); - - useEffect(() => { - if (!composerMenuOpen) return; - setComposerKeyboardHighlight(false); - setComposerMenuIndex(0); - }, [composerMenuOpen]); - - useEffect(() => { - if (!composerMenuOpen || composerMenuItems.length === 0) { + if (!composerMenuOpen) { + setComposerHighlightedItemId(null); return; } - const clampedIndex = Math.min(composerMenuIndex, composerMenuItems.length - 1); - const previousIndex = previousComposerMenuIndexRef.current; - const direction = clampedIndex - previousIndex; - previousComposerMenuIndexRef.current = clampedIndex; - - const aheadIndex = - direction > 0 - ? Math.min(clampedIndex + 1, composerMenuItems.length - 1) - : direction < 0 - ? Math.max(clampedIndex - 1, 0) - : clampedIndex; - - const node = composerMenuRef.current?.querySelector( - `[data-composer-menu-index="${aheadIndex}"]`, + setComposerHighlightedItemId((existing) => + existing && composerMenuItems.some((item) => item.id === existing) ? existing : null, ); - node?.scrollIntoView({ block: "nearest" }); - }, [composerMenuIndex, composerMenuItems.length, composerMenuOpen]); + }, [composerMenuItems, composerMenuOpen]); useEffect(() => { if (!activeThread?.id || activeThread.terminalOpen) return; @@ -739,7 +704,7 @@ export default function ChatView() { return []; }); setComposerCursor(0); - setComposerMenuIndex(0); + setComposerHighlightedItemId(null); dragDepthRef.current = 0; setIsDragOverComposer(false); setExpandedImage(null); @@ -1128,7 +1093,7 @@ export default function ChatView() { setPrompt(""); setComposerImages([]); setComposerCursor(0); - setComposerMenuIndex(0); + setComposerHighlightedItemId(null); const sessionInfo = await ensureSession(sessionCwd); if (!sessionInfo) return; @@ -1271,9 +1236,15 @@ export default function ChatView() { }, [applyPromptReplacement, composerTrigger, onModelSelect], ); - const onHighlightComposerMenuItem = useCallback((index: number) => { - setComposerKeyboardHighlight(false); - setComposerMenuIndex(index); + const onComposerMenuItemHighlighted = useCallback((itemId: string | null) => { + setComposerHighlightedItemId(itemId); + }, []); + const nudgeComposerMenuHighlight = useCallback((key: "ArrowDown" | "ArrowUp") => { + const commandInput = composerCommandInputRef.current; + if (!commandInput) return; + commandInput.dispatchEvent( + new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }), + ); }, []); const isComposerMenuLoading = composerTriggerKind === "path" && @@ -1291,14 +1262,12 @@ export default function ChatView() { if (composerMenuOpen && composerMenuItems.length > 0) { if (e.key === "ArrowDown") { e.preventDefault(); - setComposerKeyboardHighlight(true); - setComposerMenuIndex((existing) => Math.min(existing + 1, composerMenuItems.length - 1)); + nudgeComposerMenuHighlight("ArrowDown"); return; } if (e.key === "ArrowUp") { e.preventDefault(); - setComposerKeyboardHighlight(true); - setComposerMenuIndex((existing) => Math.max(existing - 1, 0)); + nudgeComposerMenuHighlight("ArrowUp"); return; } if (e.key === "Tab" || e.key === "Enter") { @@ -1411,14 +1380,12 @@ export default function ChatView() {
)}