From 9a64fec875250bbcb1f231e4aac8da1a1a09fd95 Mon Sep 17 00:00:00 2001 From: flamboh Date: Sat, 3 Oct 2026 17:42:22 +0000 Subject: [PATCH 1/3] perf(web): DOM changes no longer restyle the whole page Two composer banner selectors used an unqualified `+:has([data-chat-composer-form])`. Chrome tracked that :has() on and , so any element inserted or removed anywhere restyled the entire document. The rest of each selector already requires the composer form, so `+*` matches the same elements. --- apps/web/src/components/chat/ComposerBanner.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/ComposerBanner.tsx b/apps/web/src/components/chat/ComposerBanner.tsx index 78753c36097b..cbcb58897157 100644 --- a/apps/web/src/components/chat/ComposerBanner.tsx +++ b/apps/web/src/components/chat/ComposerBanner.tsx @@ -111,7 +111,7 @@ function Attachment({ className, ...props }: ComponentProps<"div">) { "mx-auto -mb-[calc(1rem+1px)] w-[calc(100%-2*var(--chat-composer-drawer-inset))]", // Adjacent attachments share their outline, including notices outside the form. "[&+[data-slot=composer-banner-attachment]_[data-composer-banner-surface=attached]]:before:rounded-none [&+[data-slot=composer-banner-attachment]_[data-composer-banner-surface=attached]]:before:border-t-0", - "[&+:has([data-chat-composer-form])_[data-chat-composer-form]>[data-slot=composer-banner-attachment]:first-child_[data-composer-banner-surface=attached]]:before:rounded-none [&+:has([data-chat-composer-form])_[data-chat-composer-form]>[data-slot=composer-banner-attachment]:first-child_[data-composer-banner-surface=attached]]:before:border-t-0", + "[&+*_[data-chat-composer-form]>[data-slot=composer-banner-attachment]:first-child_[data-composer-banner-surface=attached]]:before:rounded-none [&+*_[data-chat-composer-form]>[data-slot=composer-banner-attachment]:first-child_[data-composer-banner-surface=attached]]:before:border-t-0", className, )} {...props} From c26d21d2f3a2476115bd414b17b4e52a0bef03e3 Mon Sep 17 00:00:00 2001 From: flamboh Date: Sat, 3 Oct 2026 08:41:51 +0000 Subject: [PATCH 2/3] perf(web): virtualize command palette results Broad thread searches mounted a heavy row for every match, so typing a common word into the command palette blocked the main thread for seconds on large histories. The palette now renders its flattened groups through LegendList with Base UI's virtualized mode, so only the visible rows mount. Results, ordering, headings and keyboard behaviour are unchanged: keyboard highlights scroll into view, Enter runs the tracked highlight even when its row is unmounted, and typing still hides the highlight until the first ArrowDown. useThreadSearch now returns the settled query so thread items only rebuild when results land, not on every keystroke. --- .../components/CommandPalette.logic.test.ts | 45 +++++++ .../src/components/CommandPalette.logic.ts | 59 ++++++++++ apps/web/src/components/CommandPalette.tsx | 67 +++++++++-- .../src/components/CommandPaletteResults.tsx | 110 ++++++++++++++++-- apps/web/src/components/ui/autocomplete.tsx | 28 ++++- apps/web/src/components/ui/command.tsx | 27 +++++ .../src/state/queries.threadSearch.test.tsx | 64 ++++++++++ apps/web/src/state/queries.ts | 3 + 8 files changed, 387 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/state/queries.threadSearch.test.tsx diff --git a/apps/web/src/components/CommandPalette.logic.test.ts b/apps/web/src/components/CommandPalette.logic.test.ts index 916b66a32b34..2bd7c118c69f 100644 --- a/apps/web/src/components/CommandPalette.logic.test.ts +++ b/apps/web/src/components/CommandPalette.logic.test.ts @@ -5,12 +5,14 @@ import { makeThreadFixture } from "../test-fixtures"; import { buildBrowseGroups, buildCommandPaletteProjectMetadata, + buildCommandPaletteRows, buildProjectActionItems, buildThreadActionItems, buildLinkedThreadActionItems, enumerateCommandPaletteItems, filterPinnedBrowseEntries, filterCommandPaletteGroups, + findHighlightedCommandPaletteItem, reduceCommandPaletteUiState, type CommandPaletteActionItem, type CommandPaletteGroup, @@ -849,3 +851,46 @@ describe("filterCommandPaletteGroups", () => { ]); }); }); + +describe("virtualized command palette rows", () => { + const action = (value: string, disabled = false): CommandPaletteActionItem => ({ + kind: "action", + value, + searchTerms: [], + title: value, + icon: null, + ...(disabled ? { disabled } : {}), + run: async () => {}, + }); + const groups: CommandPaletteGroup[] = [ + { value: "actions", label: "Actions", items: [action("new-thread"), action("offline", true)] }, + { value: "threads", label: "Threads", items: [action("thread-a"), action("thread-b")] }, + ]; + + it("keeps group order and headings while indexing only enabled items", () => { + const { rows, itemValues, rowIndexByItemIndex } = buildCommandPaletteRows(groups); + + expect(rows.map((row) => (row.kind === "label" ? `# ${row.label}` : row.key))).toEqual([ + "# Actions", + "actions:new-thread", + "actions:offline", + "# Threads", + "threads:thread-a", + "threads:thread-b", + ]); + expect(itemValues).toEqual(["new-thread", "thread-a", "thread-b"]); + expect(rowIndexByItemIndex).toEqual([1, 4, 5]); + expect(rows.flatMap((row) => (row.kind === "item" ? [row.itemIndex] : []))).toEqual([ + 0, + null, + 1, + 2, + ]); + }); + + it("resolves Enter to the highlighted item without needing its row mounted", () => { + expect(findHighlightedCommandPaletteItem(groups, "thread-b")?.value).toBe("thread-b"); + expect(findHighlightedCommandPaletteItem(groups, "offline")).toBeNull(); + expect(findHighlightedCommandPaletteItem(groups, null)).toBeNull(); + }); +}); diff --git a/apps/web/src/components/CommandPalette.logic.ts b/apps/web/src/components/CommandPalette.logic.ts index d031f36ed35c..49e963f68313 100644 --- a/apps/web/src/components/CommandPalette.logic.ts +++ b/apps/web/src/components/CommandPalette.logic.ts @@ -172,6 +172,65 @@ export interface CommandPaletteView { readonly initialQuery?: string; } +export type CommandPaletteRow = + | { + readonly kind: "label"; + readonly key: string; + readonly label: string; + readonly first: boolean; + } + | { + readonly kind: "item"; + readonly key: string; + readonly item: CommandPaletteActionItem | CommandPaletteSubmenuItem; + /** Position among enabled items, or null for disabled rows the keyboard skips. */ + readonly itemIndex: number | null; + }; + +/** + * Flattens groups into the rows a virtualized list renders. `itemValues` is the + * highlightable item order Base UI navigates; `rowIndexByItemIndex` maps a + * highlight back to its row for scrolling. + */ +export function buildCommandPaletteRows(groups: ReadonlyArray) { + const rows: CommandPaletteRow[] = []; + const itemValues: string[] = []; + const rowIndexByItemIndex: number[] = []; + for (const group of groups) { + if (group.label) { + rows.push({ + kind: "label", + key: `group:${group.value}`, + label: group.label, + first: rows.length === 0, + }); + } + for (const item of group.items) { + const itemIndex = item.disabled ? null : itemValues.length; + if (itemIndex !== null) { + itemValues.push(item.value); + rowIndexByItemIndex.push(rows.length); + } + rows.push({ kind: "item", key: `${group.value}:${item.value}`, item, itemIndex }); + } + } + return { rows, itemValues, rowIndexByItemIndex }; +} + +/** The enabled item Enter should run for a highlight, whether or not its row is mounted. */ +export function findHighlightedCommandPaletteItem( + groups: ReadonlyArray, + highlightedItemValue: string | null, +): CommandPaletteActionItem | CommandPaletteSubmenuItem | null { + if (highlightedItemValue === null) return null; + for (const group of groups) { + for (const item of group.items) { + if (item.value === highlightedItemValue && !item.disabled) return item; + } + } + return null; +} + export function enumerateCommandPaletteItems( items: ReadonlyArray, ): CommandPaletteActionItem[] { diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index aad0e71a06b0..ae777a064277 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -77,6 +77,7 @@ import { type KeyboardEvent, type ReactNode, } from "react"; +import type { LegendListRef } from "@legendapp/list/react"; import { useAtomValue } from "@effect/atom-react"; import { isDesktopLocalConnectionTarget } from "../connection/desktopLocal"; @@ -157,7 +158,9 @@ import { buildRootGroups, buildThreadActionItems, buildLinkedThreadActionItems, + buildCommandPaletteRows, enumerateCommandPaletteItems, + findHighlightedCommandPaletteItem, type CommandPaletteActionItem, type CommandPaletteOpenIntent, type CommandPaletteSubmenuItem, @@ -174,7 +177,10 @@ import { import { orderItemsByPreferredIds, sortLogicalProjectsForSidebar } from "./Sidebar.logic"; import { resolveEnvironmentOptionLabel } from "./BranchToolbar.logic"; import { CommandPaletteContent } from "./CommandPaletteContent"; -import { CommandPaletteResults } from "./CommandPaletteResults"; +import { + CommandPaletteVirtualizedResults, + scrollCommandPaletteRowIntoView, +} from "./CommandPaletteResults"; import { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitLabIcon, ForgejoIcon } from "./Icons"; import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; import { Checkbox } from "./ui/checkbox"; @@ -694,6 +700,14 @@ function OpenCommandPaletteDialog(props: { const deferredQuery = useDeferredValue(query); const isActionsOnly = deferredQuery.startsWith(">"); const [highlightedItemValue, setHighlightedItemValue] = useState(null); + const resultListRef = useRef(null); + // Typing clears the highlight. Base UI keeps its own on the first row, but the + // palette shows none until the user navigates, and the first ArrowDown lands on it. + const highlightClearedRef = useRef(false); + function clearTypedHighlight(): void { + highlightClearedRef.current = true; + setHighlightedItemValue(null); + } const clientSettings = useClientSettings(); const createProject = useAtomCommand(projectEnvironment.create, { reportFailure: false, @@ -1425,7 +1439,7 @@ function OpenCommandPaletteDialog(props: { ? { source: match.source, snippet: match.snippet, - query: threadSearchQuery, + query: threadSearch.query, } : undefined; }, @@ -1446,7 +1460,7 @@ function OpenCommandPaletteDialog(props: { projectTitleById, providerEntryByEnvironmentAndInstanceId, threadContentMatchByKey, - threadSearchQuery, + threadSearch.query, threads, ], ); @@ -1494,7 +1508,7 @@ function OpenCommandPaletteDialog(props: { function handleQueryChange(nextQuery: string): void { browseNavigation.invalidate(); - setHighlightedItemValue(null); + clearTypedHighlight(); setQuery(nextQuery); if (nextQuery === "" && currentView?.initialQuery) { popView(); @@ -2938,6 +2952,7 @@ function OpenCommandPaletteDialog(props: { } else if (isBrowsing) { displayedGroups = relativePathNeedsActiveProject ? [] : browseGroups; } + const resultRows = buildCommandPaletteRows(displayedGroups); const inputPlaceholder = newProjectFlow !== null @@ -3083,6 +3098,35 @@ function OpenCommandPaletteDialog(props: { if (event.key === "Backspace" && query === "" && isSubmenu) { event.preventDefault(); popView(); + return; + } + + const firstItemValue = resultRows.itemValues[0]; + if (event.key === "ArrowDown" && highlightClearedRef.current && firstItemValue) { + (event as typeof event & { preventBaseUIHandler?: () => void }).preventBaseUIHandler?.(); + event.preventDefault(); + highlightClearedRef.current = false; + setHighlightedItemValue(firstItemValue); + scrollCommandPaletteRowIntoView( + resultListRef.current, + resultRows.rowIndexByItemIndex[0] ?? 0, + ); + return; + } + + // Base UI clicks the highlighted row on Enter, which does nothing once the + // virtualized list has unmounted it, so run the tracked highlight directly. + if (event.key === "Enter" && !event.nativeEvent.isComposing && event.keyCode !== 229) { + const highlightedItem = findHighlightedCommandPaletteItem( + displayedGroups, + highlightedItemValue ?? firstItemValue ?? null, + ); + if (highlightedItem) { + (event as typeof event & { preventBaseUIHandler?: () => void }).preventBaseUIHandler?.(); + event.preventDefault(); + event.stopPropagation(); + executeItem(highlightedItem); + } } } @@ -3387,8 +3431,16 @@ function OpenCommandPaletteDialog(props: { onKeyDown: handleKeyDown, }} mode="none" - onItemHighlighted={(value) => { + items={resultRows.itemValues} + virtualized + onItemHighlighted={(value, eventDetails) => { + if (eventDetails.reason === "none" && highlightClearedRef.current) return; + highlightClearedRef.current = false; setHighlightedItemValue(typeof value === "string" ? value : null); + const rowIndex = resultRows.rowIndexByItemIndex[eventDetails.index]; + if (eventDetails.reason === "keyboard" && rowIndex !== undefined) { + scrollCommandPaletteRowIntoView(resultListRef.current, rowIndex); + } }} onValueChange={handleQueryChange} showBackHint={isSubmenu} @@ -3426,8 +3478,9 @@ function OpenCommandPaletteDialog(props: { ) : null} - void; } +function CommandPaletteEmptyState(props: { emptyStateMessage?: string; isActionsOnly: boolean }) { + return ( +
+ {props.emptyStateMessage ?? + (props.isActionsOnly + ? "No matching actions." + : "No matching commands, projects, or threads.")} +
+ ); +} + export function CommandPaletteResults(props: CommandPaletteResultsProps) { if (props.groups.length === 0) { - return ( -
- {props.emptyStateMessage ?? - (props.isActionsOnly - ? "No matching actions." - : "No matching commands, projects, or threads.")} -
- ); + return ; } return ( @@ -63,6 +74,87 @@ export function CommandPaletteResults(props: CommandPaletteResultsProps) { ); } +/** + * Scrolls a keyboard highlight into view the way the unvirtualized list did: + * nearest edge, clear of the scroll fade. Rows outside the rendered window + * fall back to the list's own scrolling. + */ +export function scrollCommandPaletteRowIntoView(list: LegendListRef | null, rowIndex: number) { + const element = list?.getState?.().elementAtIndex(rowIndex); + if (element instanceof HTMLElement) { + element.scrollIntoView({ block: "nearest" }); + return; + } + void list?.scrollIndexIntoView?.({ index: rowIndex, animated: false }); +} + +interface CommandPaletteVirtualizedResultsProps extends Omit { + rows: ReadonlyArray; + listRef: RefObject; +} + +/** + * Renders only the visible rows. The parent passes the flat item order to the + * Command root as `items` with `virtualized`, scrolls keyboard highlights into + * view through `listRef`, and runs Enter itself since the row may be unmounted. + */ +export function CommandPaletteVirtualizedResults(props: CommandPaletteVirtualizedResultsProps) { + const { listRef } = props; + const [scrollFade, setScrollFade] = useState({ top: false, bottom: false }); + const updateScrollFade = useCallback(() => { + const scrollElement = listRef.current?.getScrollableNode?.(); + if (!(scrollElement instanceof HTMLElement)) return; + const top = scrollElement.scrollTop > 1; + const bottom = + scrollElement.scrollHeight - scrollElement.clientHeight - scrollElement.scrollTop > 1; + setScrollFade((current) => + current.top === top && current.bottom === bottom ? current : { top, bottom }, + ); + }, [listRef]); + + if (props.rows.length === 0) { + return ; + } + + return ( + + + ref={listRef} + data={props.rows} + keyExtractor={(row) => row.key} + getItemType={(row) => row.kind} + extraData={props.highlightedItemValue} + renderItem={({ item: row }) => + row.kind === "label" ? ( +
+ {row.label} +
+ ) : row.itemIndex === null ? ( + + ) : ( + + ) + } + estimatedItemSize={40} + drawDistance={400} + onLayout={updateScrollFade} + onScroll={updateScrollFade} + contentContainerClassName="px-2" + className={cn( + "min-h-0 scroll-py-6 overflow-x-hidden overscroll-y-contain py-2", + getVirtualizedScrollFadeClassName(scrollFade), + )} + /> +
+ ); +} + function DisabledCommandPaletteResultRow(props: { item: CommandPaletteActionItem | CommandPaletteSubmenuItem; }) { @@ -96,6 +188,7 @@ function DisabledCommandPaletteResultRow(props: { } function CommandPaletteResultRow(props: { + index?: number; item: CommandPaletteActionItem | CommandPaletteSubmenuItem; isActive: boolean; keybindings: ResolvedKeybindingsConfig; @@ -107,6 +200,7 @@ function CommandPaletteResultRow(props: { return ( { diff --git a/apps/web/src/components/ui/autocomplete.tsx b/apps/web/src/components/ui/autocomplete.tsx index 8251d8c24b7c..548efd944897 100644 --- a/apps/web/src/components/ui/autocomplete.tsx +++ b/apps/web/src/components/ui/autocomplete.tsx @@ -146,11 +146,35 @@ function AutocompleteGroup({ className, ...props }: AutocompletePrimitive.Group. ); } +const GROUP_LABEL_CLASS_NAME = "px-2 py-1.5 font-medium text-muted-foreground text-xs"; + function AutocompleteGroupLabel({ className, ...props }: AutocompletePrimitive.GroupLabel.Props) { return ( + ); +} + +// A group label for virtualized lists, where rows cannot be wrapped in a group. +function AutocompleteListHeading({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AutocompleteListVirtualized({ className, ...props }: AutocompletePrimitive.List.Props) { + return ( + ); @@ -226,6 +250,8 @@ export { AutocompleteGroupLabel, AutocompleteEmpty, AutocompleteList, + AutocompleteListHeading, + AutocompleteListVirtualized, AutocompleteClear, AutocompleteCollection, }; diff --git a/apps/web/src/components/ui/command.tsx b/apps/web/src/components/ui/command.tsx index d3a83865151d..b3a473ba1d22 100644 --- a/apps/web/src/components/ui/command.tsx +++ b/apps/web/src/components/ui/command.tsx @@ -12,6 +12,8 @@ import { AutocompleteInput, AutocompleteItem, AutocompleteList, + AutocompleteListHeading, + AutocompleteListVirtualized, } from "~/components/ui/autocomplete"; import { DIALOG_BACKDROP_CLASS, DIALOG_POPUP_CLASS } from "~/components/ui/dialog-styles"; import { Button } from "~/components/ui/button"; @@ -123,6 +125,29 @@ function CommandList({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function CommandListHeading({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + function CommandPanel({ className, ...props }: React.ComponentProps<"div">) { return (
({ + matches: [{ threadId: "thread-1", source: "user", snippet: "a link" }], + isLoading: false, +})); +vi.mock("@effect/atom-react", async (importOriginal) => ({ + ...(await importOriginal()), + useAtomValue: (atom: { label?: ReadonlyArray }) => + String(atom.label?.[0]).includes("empty") ? { matches: [], isLoading: false } : searchResults, +})); + +const environmentIds = [EnvironmentId.make("local")]; +type ThreadSearch = ReturnType; + +function Probe(props: { query: string; onResult: (result: ThreadSearch) => void }) { + props.onResult(useThreadSearch(environmentIds, props.query)); + return null; +} + +describe("useThreadSearch", () => { + let renderer: ReactTestRenderer | null = null; + beforeEach(() => { + vi.useFakeTimers(); + vi.stubGlobal("window", globalThis); + }); + afterEach(() => { + act(() => renderer?.unmount()); + renderer = null; + vi.unstubAllGlobals(); + vi.useRealTimers(); + }); + + it("keeps the settled query and matches stable while typing, then reports the settled query", () => { + const results: ThreadSearch[] = []; + const render = (query: string) => { + const element = results.push(result)} />; + act(() => { + if (renderer) renderer.update(element); + else renderer = create(element); + }); + return results.at(-1)!; + }; + + render(""); + const typing = ["li", "lin", "link "].map(render); + expect(typing.map((result) => result.query)).toEqual(["", "", ""]); + expect(new Set(typing.map((result) => result.matches)).size).toBe(1); + expect(typing.every((result) => result.isPending)).toBe(true); + + act(() => { + vi.advanceTimersByTime(200); + }); + const settled = results.at(-1)!; + expect(settled.query).toBe("link"); + expect(settled.matches).toBe(searchResults.matches); + expect(settled.isPending).toBe(false); + }); +}); diff --git a/apps/web/src/state/queries.ts b/apps/web/src/state/queries.ts index 15211efab4e6..01bfa8864bb7 100644 --- a/apps/web/src/state/queries.ts +++ b/apps/web/src/state/queries.ts @@ -72,6 +72,8 @@ export function useThreadSearch( query: string, ): { readonly matches: ReadonlyArray; + /** The settled query `matches` came from; it only changes when results do. */ + readonly query: string; readonly isPending: boolean; } { const normalizedQuery = query.trim(); @@ -88,6 +90,7 @@ export function useThreadSearch( const isDebouncing = canSearch && normalizedQuery !== debouncedQuery; return { matches: isDebouncing ? EMPTY_THREAD_SEARCH_MATCHES : result.matches, + query: settledQuery ?? "", isPending: canSearch && (isDebouncing || result.isLoading), }; } From c795acbdd008ee3ca0d91ca20927d323dc2ba4b2 Mon Sep 17 00:00:00 2001 From: flamboh Date: Sat, 3 Oct 2026 18:56:47 +0000 Subject: [PATCH 3/3] fix(web): palette fallbacks follow Base UI for modifiers, browse mode and submenus Modified Enter/ArrowDown no longer run the tracked highlight (Base UI ignores them). Browse, clone and new-project modes don't auto-highlight, so ArrowDown and Enter there are left to Base UI. Entering a submenu hides the highlight until the first navigation, as typing does. --- apps/web/src/components/CommandPalette.tsx | 24 +++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index ae777a064277..f7b78b5ae676 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -701,8 +701,9 @@ function OpenCommandPaletteDialog(props: { const isActionsOnly = deferredQuery.startsWith(">"); const [highlightedItemValue, setHighlightedItemValue] = useState(null); const resultListRef = useRef(null); - // Typing clears the highlight. Base UI keeps its own on the first row, but the - // palette shows none until the user navigates, and the first ArrowDown lands on it. + // Typing or entering a submenu clears the highlight. Base UI keeps its own on the + // first row, but the palette shows none until the user navigates, and the first + // ArrowDown lands on it. const highlightClearedRef = useRef(false); function clearTypedHighlight(): void { highlightClearedRef.current = true; @@ -1477,6 +1478,7 @@ function OpenCommandPaletteDialog(props: { ...(view.initialQuery ? { initialQuery: view.initialQuery } : {}), }, ]); + highlightClearedRef.current = true; setHighlightedItemValue(null); setQuery(view.initialQuery ?? ""); }, @@ -2953,6 +2955,8 @@ function OpenCommandPaletteDialog(props: { displayedGroups = relativePathNeedsActiveProject ? [] : browseGroups; } const resultRows = buildCommandPaletteRows(displayedGroups); + const autoHighlightsFirstRow = + !isBrowsing && !isRemoteProjectCloneFlow && newProjectFlow === null; const inputPlaceholder = newProjectFlow !== null @@ -3101,8 +3105,16 @@ function OpenCommandPaletteDialog(props: { return; } - const firstItemValue = resultRows.itemValues[0]; - if (event.key === "ArrowDown" && highlightClearedRef.current && firstItemValue) { + // Base UI ignores navigation keys with modifiers, so these fallbacks do too. + if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) return; + // Base UI only keeps a hidden highlight on the first row when it auto-highlights. + const firstItemValue = autoHighlightsFirstRow ? resultRows.itemValues[0] : undefined; + if ( + event.key === "ArrowDown" && + highlightClearedRef.current && + firstItemValue && + !event.nativeEvent.isComposing + ) { (event as typeof event & { preventBaseUIHandler?: () => void }).preventBaseUIHandler?.(); event.preventDefault(); highlightClearedRef.current = false; @@ -3393,9 +3405,7 @@ function OpenCommandPaletteDialog(props: {