diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index b19e1240364..a9708660cff 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -68,7 +68,10 @@ import { ComposerPrimaryActions } from "./ComposerPrimaryActions"; import { ComposerPendingApprovalPanel } from "./ComposerPendingApprovalPanel"; import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel"; import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner"; -import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight"; +import { + resolveComposerMenuActiveItemId, + resolveComposerMenuNudgedItemId, +} from "./composerMenuHighlight"; import { searchSlashCommandItems } from "./composerSlashCommandSearch"; import { getComposerProviderState, @@ -1134,35 +1137,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) composerTerminalContextsRef.current = composerTerminalContexts; }, [composerTerminalContexts, composerTerminalContextsRef]); - // ------------------------------------------------------------------ - // Composer menu highlight sync - // ------------------------------------------------------------------ - useEffect(() => { - if (!composerMenuOpen) { - setComposerHighlightedItemId(null); - setComposerHighlightedSearchKey(null); - return; - } - const nextActiveItemId = resolveComposerMenuActiveItemId({ - items: composerMenuItems, - highlightedItemId: composerHighlightedItemId, - currentSearchKey: composerMenuSearchKey, - highlightedSearchKey: composerHighlightedSearchKey, - }); - setComposerHighlightedItemId((existing) => - existing === nextActiveItemId ? existing : nextActiveItemId, - ); - setComposerHighlightedSearchKey((existing) => - existing === composerMenuSearchKey ? existing : composerMenuSearchKey, - ); - }, [ - composerHighlightedItemId, - composerHighlightedSearchKey, - composerMenuItems, - composerMenuOpen, - composerMenuSearchKey, - ]); - const lastSyncedPendingInputRef = useRef<{ requestId: string | null; questionId: string | null; @@ -1576,18 +1550,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const nudgeComposerMenuHighlight = useCallback( (key: "ArrowDown" | "ArrowUp") => { if (composerMenuItems.length === 0) return; - const highlightedIndex = composerMenuItems.findIndex( - (item) => item.id === composerHighlightedItemId, + setComposerHighlightedItemId( + resolveComposerMenuNudgedItemId({ + items: composerMenuItems, + activeItemId: activeComposerMenuItem?.id ?? null, + direction: key === "ArrowDown" ? "next" : "previous", + }), ); - const normalizedIndex = - highlightedIndex >= 0 ? highlightedIndex : key === "ArrowDown" ? -1 : 0; - const offset = key === "ArrowDown" ? 1 : -1; - const nextIndex = - (normalizedIndex + offset + composerMenuItems.length) % composerMenuItems.length; - const nextItem = composerMenuItems[nextIndex]; - setComposerHighlightedItemId(nextItem?.id ?? null); + setComposerHighlightedSearchKey(composerMenuSearchKey); }, - [composerHighlightedItemId, composerMenuItems], + [activeComposerMenuItem?.id, composerMenuItems, composerMenuSearchKey], ); const blurMobileComposerAfterSend = useCallback(() => { diff --git a/apps/web/src/components/chat/composerMenuHighlight.test.ts b/apps/web/src/components/chat/composerMenuHighlight.test.ts index 08c0f2f24d3..68c9cf24a46 100644 --- a/apps/web/src/components/chat/composerMenuHighlight.test.ts +++ b/apps/web/src/components/chat/composerMenuHighlight.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from "vitest"; -import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight"; +import { + resolveComposerMenuActiveItemId, + resolveComposerMenuNudgedItemId, +} from "./composerMenuHighlight"; describe("resolveComposerMenuActiveItemId", () => { const items = [{ id: "top" }, { id: "second" }, { id: "third" }] as const; @@ -49,3 +52,61 @@ describe("resolveComposerMenuActiveItemId", () => { ).toBe("top"); }); }); + +describe("resolveComposerMenuNudgedItemId", () => { + const items = [{ id: "top" }, { id: "second" }, { id: "third" }] as const; + + it("moves from the active item", () => { + expect( + resolveComposerMenuNudgedItemId({ + items, + activeItemId: "second", + direction: "next", + }), + ).toBe("third"); + + expect( + resolveComposerMenuNudgedItemId({ + items, + activeItemId: "second", + direction: "previous", + }), + ).toBe("top"); + }); + + it("wraps around at either edge", () => { + expect( + resolveComposerMenuNudgedItemId({ + items, + activeItemId: "third", + direction: "next", + }), + ).toBe("top"); + + expect( + resolveComposerMenuNudgedItemId({ + items, + activeItemId: "top", + direction: "previous", + }), + ).toBe("third"); + }); + + it("starts from the first visible item when active state is stale", () => { + expect( + resolveComposerMenuNudgedItemId({ + items, + activeItemId: "missing", + direction: "next", + }), + ).toBe("top"); + + expect( + resolveComposerMenuNudgedItemId({ + items, + activeItemId: "missing", + direction: "previous", + }), + ).toBe("third"); + }); +}); diff --git a/apps/web/src/components/chat/composerMenuHighlight.ts b/apps/web/src/components/chat/composerMenuHighlight.ts index 3cc3d4324fc..9cfbf0496d3 100644 --- a/apps/web/src/components/chat/composerMenuHighlight.ts +++ b/apps/web/src/components/chat/composerMenuHighlight.ts @@ -18,3 +18,20 @@ export function resolveComposerMenuActiveItemId(input: { return input.items[0]?.id ?? null; } + +export function resolveComposerMenuNudgedItemId(input: { + items: ReadonlyArray<{ id: string }>; + activeItemId: string | null; + direction: "next" | "previous"; +}): string | null { + if (input.items.length === 0) { + return null; + } + + const activeIndex = input.items.findIndex((item) => item.id === input.activeItemId); + const normalizedIndex = activeIndex >= 0 ? activeIndex : input.direction === "next" ? -1 : 0; + const offset = input.direction === "next" ? 1 : -1; + const nextIndex = (normalizedIndex + offset + input.items.length) % input.items.length; + + return input.items[nextIndex]?.id ?? null; +} diff --git a/docs/react-scan-recordings/composer-menu-highlight-after.webm b/docs/react-scan-recordings/composer-menu-highlight-after.webm new file mode 100644 index 00000000000..c4d844be37f Binary files /dev/null and b/docs/react-scan-recordings/composer-menu-highlight-after.webm differ diff --git a/docs/react-scan-recordings/composer-menu-highlight-before.webm b/docs/react-scan-recordings/composer-menu-highlight-before.webm new file mode 100644 index 00000000000..53a333b7b77 Binary files /dev/null and b/docs/react-scan-recordings/composer-menu-highlight-before.webm differ