authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-02 13:54:02-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-02 14:16:08-08:00
log5e78386cdc805ee64ccc745454ce476475597f6f
tree5430e3351dbcc8595b1127520f2392ca2a0953e9
parentc378afeff852c06d7a558c4842ae9033b3775627

Improve composer menu highlighting and suppress triggers near mentions

Adds visual active-item highlighting to the command menu, suppresses trigger detection when the cursor is adjacent to a mention pill, and resets transient UI state on thread change. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

2 files changed, 59 insertions(+), 7 deletions(-)

apps/web/src/components/ChatView.tsx+33-4
...@@ -361,12 +361,16 @@ const VscodeEntryIcon = memo(function VscodeEntryIcon(props: {...@@ -361,12 +361,16 @@ const VscodeEntryIcon = memo(function VscodeEntryIcon(props: {
361const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: {361const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: {
362 item: ComposerCommandItem;362 item: ComposerCommandItem;
363 resolvedTheme: "light" | "dark";363 resolvedTheme: "light" | "dark";
364 isActive: boolean;
364 onSelect: (item: ComposerCommandItem) => void;365 onSelect: (item: ComposerCommandItem) => void;
365}) {366}) {
366 return (367 return (
367 <CommandItem368 <CommandItem
368 value={props.item.id}369 value={props.item.id}
369 className="cursor-pointer select-none gap-2"370 className={cn(
371 "cursor-pointer select-none gap-2",
372 props.isActive && "bg-accent text-accent-foreground",
373 )}
370 onMouseDown={(event) => {374 onMouseDown={(event) => {
371 event.preventDefault();375 event.preventDefault();
372 }}376 }}
...@@ -400,6 +404,7 @@ const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {...@@ -400,6 +404,7 @@ const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {
400 resolvedTheme: "light" | "dark";404 resolvedTheme: "light" | "dark";
401 isLoading: boolean;405 isLoading: boolean;
402 triggerKind: ComposerTriggerKind | null;406 triggerKind: ComposerTriggerKind | null;
407 activeItemId: string | null;
403 onHighlightedItemChange: (itemId: string | null) => void;408 onHighlightedItemChange: (itemId: string | null) => void;
404 onSelect: (item: ComposerCommandItem) => void;409 onSelect: (item: ComposerCommandItem) => void;
405}) {410}) {
...@@ -419,6 +424,7 @@ const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {...@@ -419,6 +424,7 @@ const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {
419 key={item.id}424 key={item.id}
420 item={item}425 item={item}
421 resolvedTheme={props.resolvedTheme}426 resolvedTheme={props.resolvedTheme}
427 isActive={props.activeItemId === item.id}
422 onSelect={props.onSelect}428 onSelect={props.onSelect}
423 />429 />
424 ))}430 ))}
...@@ -1389,6 +1395,26 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1389,6 +1395,26 @@ export default function ChatView({ threadId }: ChatViewProps) {
1389 scheduleStickToBottom();1395 scheduleStickToBottom();
1390 }, [phase, scheduleStickToBottom, timelineEntries]);1396 }, [phase, scheduleStickToBottom, timelineEntries]);
13911397
1398 useEffect(() => {
1399 setExpandedWorkGroups({});
1400 }, [activeThread?.id]);
1401
1402 useEffect(() => {
1403 if (!composerMenuOpen) {
1404 setComposerHighlightedItemId(null);
1405 return;
1406 }
1407 setComposerHighlightedItemId((existing) =>
1408 existing && composerMenuItems.some((item) => item.id === existing)
1409 ? existing
1410 : (composerMenuItems[0]?.id ?? null),
1411 );
1412 }, [composerMenuItems, composerMenuOpen]);
1413
1414 useEffect(() => {
1415 setIsRevertingCheckpoint(false);
1416 }, [activeThread?.id]);
1417
1392 useEffect(() => {1418 useEffect(() => {
1393 if (!activeThread?.id || terminalState.terminalOpen) return;1419 if (!activeThread?.id || terminalState.terminalOpen) return;
1394 const frame = window.requestAnimationFrame(() => {1420 const frame = window.requestAnimationFrame(() => {
...@@ -2274,12 +2300,14 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -2274,12 +2300,14 @@ export default function ChatView({ threadId }: ChatViewProps) {
2274 workspaceEntriesQuery.isLoading ||2300 workspaceEntriesQuery.isLoading ||
2275 workspaceEntriesQuery.isFetching);2301 workspaceEntriesQuery.isFetching);
22762302
2277 const onPromptChange = useCallback((nextPrompt: string, nextCursor: number) => {2303 const onPromptChange = useCallback((nextPrompt: string, nextCursor: number, cursorAdjacentToMention: boolean) => {
2278 promptRef.current = nextPrompt;2304 promptRef.current = nextPrompt;
2279 setPrompt(nextPrompt);2305 setPrompt(nextPrompt);
2280 setComposerCursor(nextCursor);2306 setComposerCursor(nextCursor);
2281 setComposerTrigger(2307 setComposerTrigger(
2282 detectComposerTrigger(nextPrompt, expandCollapsedComposerCursor(nextPrompt, nextCursor)),2308 cursorAdjacentToMention
2309 ? null
2310 : detectComposerTrigger(nextPrompt, expandCollapsedComposerCursor(nextPrompt, nextCursor)),
2283 );2311 );
2284 }, [setPrompt]);2312 }, [setPrompt]);
22852313
...@@ -2301,8 +2329,8 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -2301,8 +2329,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
2301 const selectedItem = activeComposerMenuItemRef.current ?? currentItems[0];2329 const selectedItem = activeComposerMenuItemRef.current ?? currentItems[0];
2302 if (selectedItem) {2330 if (selectedItem) {
2303 onSelectComposerItem(selectedItem);2331 onSelectComposerItem(selectedItem);
2332 return true;
2304 }2333 }
2305 return true;
2306 }2334 }
2307 }2335 }
23082336
...@@ -2464,6 +2492,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -2464,6 +2492,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
2464 resolvedTheme={resolvedTheme}2492 resolvedTheme={resolvedTheme}
2465 isLoading={isComposerMenuLoading}2493 isLoading={isComposerMenuLoading}
2466 triggerKind={composerTriggerKind}2494 triggerKind={composerTriggerKind}
2495 activeItemId={activeComposerMenuItem?.id ?? null}
2467 onHighlightedItemChange={onComposerMenuItemHighlighted}2496 onHighlightedItemChange={onComposerMenuItemHighlighted}
2468 onSelect={onSelectComposerItem}2497 onSelect={onSelectComposerItem}
2469 />2498 />
apps/web/src/components/ComposerPromptEditor.tsx+26-3
...@@ -395,7 +395,7 @@ interface ComposerPromptEditorProps {...@@ -395,7 +395,7 @@ interface ComposerPromptEditorProps {
395 disabled: boolean;395 disabled: boolean;
396 placeholder: string;396 placeholder: string;
397 className?: string;397 className?: string;
398 onChange: (nextValue: string, nextCursor: number) => void;398 onChange: (nextValue: string, nextCursor: number, cursorAdjacentToMention: boolean) => void;
399 onCommandKeyDown?: (399 onCommandKeyDown?: (
400 key: "ArrowDown" | "ArrowUp" | "Enter" | "Tab",400 key: "ArrowDown" | "ArrowUp" | "Enter" | "Tab",
401 event: KeyboardEvent,401 event: KeyboardEvent,
...@@ -540,6 +540,29 @@ function ComposerMentionSelectionNormalizePlugin() {...@@ -540,6 +540,29 @@ function ComposerMentionSelectionNormalizePlugin() {
540 return null;540 return null;
541}541}
542542
543function $isCursorAdjacentToMention(): boolean {
544 const selection = $getSelection();
545 if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false;
546 const anchorNode = selection.anchor.getNode();
547
548 if (anchorNode instanceof ComposerMentionNode) return true;
549
550 if ($isElementNode(anchorNode)) {
551 const childIndex = selection.anchor.offset - 1;
552 if (childIndex >= 0) {
553 const child = anchorNode.getChildAtIndex(childIndex);
554 if (child instanceof ComposerMentionNode) return true;
555 }
556 }
557
558 if ($isTextNode(anchorNode) && selection.anchor.offset === 0) {
559 const prev = anchorNode.getPreviousSibling();
560 if (prev instanceof ComposerMentionNode) return true;
561 }
562
563 return false;
564}
565
543function ComposerMentionBackspacePlugin() {566function ComposerMentionBackspacePlugin() {
544 const [editor] = useLexicalComposerContext();567 const [editor] = useLexicalComposerContext();
545568
...@@ -662,7 +685,7 @@ function ComposerPromptEditorInner({...@@ -662,7 +685,7 @@ function ComposerPromptEditorInner({
662 value: snapshotRef.current.value,685 value: snapshotRef.current.value,
663 cursor: boundedCursor,686 cursor: boundedCursor,
664 };687 };
665 onChangeRef.current(snapshotRef.current.value, boundedCursor);688 onChangeRef.current(snapshotRef.current.value, boundedCursor, false);
666 },689 },
667 [editor],690 [editor],
668 );691 );
...@@ -715,7 +738,7 @@ function ComposerPromptEditorInner({...@@ -715,7 +738,7 @@ function ComposerPromptEditorInner({
715 value: nextValue,738 value: nextValue,
716 cursor: nextCursor,739 cursor: nextCursor,
717 };740 };
718 onChangeRef.current(nextValue, nextCursor);741 onChangeRef.current(nextValue, nextCursor, $isCursorAdjacentToMention());
719 });742 });
720 }, []);743 }, []);
721744