diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index cea7e6bcb5..d80793aa91 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -1248,6 +1248,7 @@ export function MasterSearchHeader({ recentQueries={recentQueries} commandScopes={commandScopes} placement={commandSurfacePlacement} + requiresTypedQueryToOpen={usesPhoneFooterDock} dropdownOpen={commandDropdownOpen} onDropdownOpenChange={setCommandDropdownOpen} onQueryChange={onQueryChange} diff --git a/src/components/clinical-dashboard/universal-search-command-surface.tsx b/src/components/clinical-dashboard/universal-search-command-surface.tsx index e9b37518e8..6089a1b81d 100644 --- a/src/components/clinical-dashboard/universal-search-command-surface.tsx +++ b/src/components/clinical-dashboard/universal-search-command-surface.tsx @@ -1,7 +1,15 @@ "use client"; import { AlertTriangle, Clock, CornerDownLeft, Search, X } from "lucide-react"; -import { useEffect, useId, useMemo, useState, type KeyboardEvent as ReactKeyboardEvent, type ReactNode } from "react"; +import { + useEffect, + useId, + useMemo, + useRef, + useState, + type KeyboardEvent as ReactKeyboardEvent, + type ReactNode, +} from "react"; import { modeActionItemsFor, @@ -261,6 +269,7 @@ export function UniversalSearchCommandSurface({ onFocusSearchInput, onListboxIdReady, placement = "inline", + requiresTypedQueryToOpen = false, children, }: { modeId: AppModeId; @@ -279,14 +288,20 @@ export function UniversalSearchCommandSurface({ onFocusSearchInput?: () => void; onListboxIdReady?: (listboxId: string) => void; placement?: CommandSurfacePlacement; + requiresTypedQueryToOpen?: boolean; children: ReactNode; }) { const config = searchCommandSurfaceConfig(modeId); const listboxId = useId(); const [activeIndex, setActiveIndex] = useState(-1); const trimmedQuery = query.trim(); + const composerFocusedRef = useRef(false); const mode = appModeDefinition(modeId); + function canOpenDropdownNow() { + return !requiresTypedQueryToOpen || trimmedQuery.length > 0; + } + const showSafetyBanner = modeId === "differentials" && differentialRedFlagTerms.some((term) => trimmedQuery.toLowerCase().includes(term)); const showFormCodeHint = modeId === "forms" && isFormCodeQuery(trimmedQuery); @@ -514,12 +529,14 @@ export function UniversalSearchCommandSurface({ function handleComposerKeyDown(event: ReactKeyboardEvent) { if (event.key === "ArrowDown") { event.preventDefault(); + if (!canOpenDropdownNow()) return; onDropdownOpenChange(true); setActiveIndex((current) => (current + 1) % Math.max(flatItems.length, 1)); return; } if (event.key === "ArrowUp") { event.preventDefault(); + if (!canOpenDropdownNow()) return; onDropdownOpenChange(true); setActiveIndex((current) => (current <= 0 ? flatItems.length - 1 : current - 1)); return; @@ -551,6 +568,16 @@ export function UniversalSearchCommandSurface({ onListboxIdReady?.(listboxId); }, [listboxId, onListboxIdReady]); + useEffect(() => { + if (requiresTypedQueryToOpen && composerFocusedRef.current && trimmedQuery.length > 0) { + onDropdownOpenChange(true); + } + if (requiresTypedQueryToOpen && trimmedQuery.length === 0) { + onDropdownOpenChange(false); + setActiveIndex(-1); + } + }, [requiresTypedQueryToOpen, trimmedQuery, onDropdownOpenChange]); + useEffect(() => { function handleSlashFocus(event: KeyboardEvent) { if (event.key !== "/" || event.metaKey || event.ctrlKey || event.altKey) return; @@ -599,8 +626,14 @@ export function UniversalSearchCommandSurface({ handleComposerKeyDown(event as unknown as ReactKeyboardEvent); } }} - onFocusCapture={() => onDropdownOpenChange(true)} + onFocusCapture={() => { + composerFocusedRef.current = true; + if (canOpenDropdownNow()) { + onDropdownOpenChange(true); + } + }} onBlurCapture={(event) => { + composerFocusedRef.current = false; if (!event.currentTarget.contains(event.relatedTarget as Node | null)) { onDropdownOpenChange(false); setActiveIndex(-1); diff --git a/src/lib/rag.ts b/src/lib/rag.ts index e0afa8b456..29b7df1d9e 100644 --- a/src/lib/rag.ts +++ b/src/lib/rag.ts @@ -5940,7 +5940,7 @@ export async function searchChunksWithTelemetry(args: SearchChunksArgs) { owner_filter: ownerScopeForDocumentFilteredRetrieval( args.ownerId, documentFilter ? [documentFilter] : undefined, - documentFilter ? undefined : args.allowGlobalSearch, + args.allowGlobalSearch, ), }); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 66b6548dab..a5d3e942c3 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -97,9 +97,13 @@ async function commandSurfaceOpensAbovePill(page: Page) { { timeout: 10_000 }, ); await input.click(); + await input.fill(""); + await expect(page.getByRole("listbox")).toHaveCount(0); + await input.press("ArrowDown"); + await expect(page.getByRole("listbox")).toHaveCount(0); + + await input.fill("li"); await expect(async () => { - await input.press("ArrowDown"); - await expect(page.getByText("Examples", { exact: true }).first()).toBeVisible(); await expect(page.getByRole("listbox").first()).toBeVisible(); }).toPass({ timeout: 15_000 });