diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index ebfc260ca43..cfe4a8c7192 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -30,6 +30,7 @@ import { useCallback, useDeferredValue, useEffect, + useEffectEvent, useLayoutEffect, useMemo, useRef, @@ -327,11 +328,20 @@ function errorMessage(error: unknown): string { } export function CommandPalette({ children }: { children: ReactNode }) { - const open = useCommandPaletteStore((store) => store.open); - const setOpen = useCommandPaletteStore((store) => store.setOpen); + const composerHandleRef = useRef(null); + + return ( + + + {children} + + + ); +} + +function CommandPaletteKeyboardShortcutController() { const toggleOpen = useCommandPaletteStore((store) => store.toggleOpen); const keybindings = useServerKeybindings(); - const composerHandleRef = useRef(null); const routeTarget = useParams({ strict: false, select: (params) => resolveThreadRouteTarget(params), @@ -343,51 +353,89 @@ export function CommandPalette({ children }: { children: ReactNode }) { : false, ); - useEffect(() => { - const onKeyDown = (event: globalThis.KeyboardEvent) => { - if (event.defaultPrevented) return; - const command = resolveShortcutCommand(event, keybindings, { - context: { - terminalFocus: isTerminalFocused(), - terminalOpen, - }, - }); - if (command !== "commandPalette.toggle") { - return; - } - event.preventDefault(); - event.stopPropagation(); - toggleOpen(); - }; - window.addEventListener("keydown", onKeyDown); - return () => window.removeEventListener("keydown", onKeyDown); - }, [keybindings, terminalOpen, toggleOpen]); + useCommandPaletteKeyboardShortcut({ + keybindings, + terminalOpen, + toggleOpen, + }); - return ( - - - {children} - - - - ); + return null; } -function CommandPaletteDialog() { +function useCommandPaletteKeyboardShortcut({ + keybindings, + terminalOpen, + toggleOpen, +}: { + keybindings: ReturnType; + terminalOpen: boolean; + toggleOpen: () => void; +}) { + const onKeyDown = useEffectEvent((event: globalThis.KeyboardEvent) => { + if (event.defaultPrevented) return; + const command = resolveShortcutCommand(event, keybindings, { + context: { + terminalFocus: isTerminalFocused(), + terminalOpen, + }, + }); + if (command !== "commandPalette.toggle") { + return; + } + event.preventDefault(); + event.stopPropagation(); + toggleOpen(); + }); + + useEffect(() => { + const listener = (event: globalThis.KeyboardEvent) => onKeyDown(event); + window.addEventListener("keydown", listener); + return () => window.removeEventListener("keydown", listener); + }, [onKeyDown]); +} + +function CommandPaletteDialogRoot() { const open = useCommandPaletteStore((store) => store.open); const setOpen = useCommandPaletteStore((store) => store.setOpen); + useCloseCommandPaletteOnUnmount(setOpen); + + return ( + + {open ? : null} + + ); +} +function useCloseCommandPaletteOnUnmount(setOpen: (open: boolean) => void) { useEffect(() => { return () => { setOpen(false); }; }, [setOpen]); +} - if (!open) { - return null; - } +function usePrefetchBrowseParent({ + filteredBrowseEntriesLength, + isBrowsing, + prefetchBrowsePath, + query, +}: { + filteredBrowseEntriesLength: number; + isBrowsing: boolean; + prefetchBrowsePath: (partialPath: string) => void; + query: string; +}) { + // Prefetch only the parent (for back-navigation). Prefetching the + // highlighted child on every arrow-key press triggers a macOS TCC prompt + // whenever the highlighted entry is a permission-gated home dir (Music, + // Documents, Downloads, Desktop, etc.), so we wait for explicit navigation. + useEffect(() => { + if (!isBrowsing || filteredBrowseEntriesLength === 0) return; - return ; + if (canNavigateUp(query)) { + prefetchBrowsePath(getBrowseParentPath(query)!); + } + }, [filteredBrowseEntriesLength, isBrowsing, prefetchBrowsePath, query]); } function OpenCommandPaletteDialog() { @@ -583,17 +631,12 @@ function OpenCommandPaletteDialog() { [browseEnvironmentId, currentProjectCwdForBrowse, fetchBrowseResult, queryClient], ); - // Prefetch only the parent (for back-navigation). Prefetching the - // highlighted child on every arrow-key press triggers a macOS TCC prompt - // whenever the highlighted entry is a permission-gated home dir (Music, - // Documents, Downloads, Desktop, etc.), so we wait for explicit navigation. - useEffect(() => { - if (!isBrowsing || filteredBrowseEntries.length === 0) return; - - if (canNavigateUp(query)) { - prefetchBrowsePath(getBrowseParentPath(query)!); - } - }, [filteredBrowseEntries.length, isBrowsing, prefetchBrowsePath, query]); + usePrefetchBrowseParent({ + filteredBrowseEntriesLength: filteredBrowseEntries.length, + isBrowsing, + prefetchBrowsePath, + query, + }); const openProjectFromSearch = useMemo( () => async (project: (typeof projects)[number]) => { diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index dc5acaaadc7..cbe73dfd2a3 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -176,7 +176,6 @@ import { import { sortThreads } from "../lib/threadSort"; import { SidebarUpdatePill } from "./sidebar/SidebarUpdatePill"; import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; -import { CommandDialogTrigger } from "./ui/command"; import { readEnvironmentApi } from "../environmentApi"; import { useSettings, useUpdateSettings } from "~/hooks/useSettings"; import { useServerKeybindings } from "../rpc/serverState"; @@ -2551,6 +2550,7 @@ interface SidebarProjectsContentProps { projectGroupingMode: SidebarProjectGroupingMode; threadPreviewCount: SidebarThreadPreviewCount; updateSettings: ReturnType["updateSettings"]; + openCommandPalette: () => void; openAddProject: () => void; isManualProjectSorting: boolean; projectDnDSensors: ReturnType; @@ -2592,6 +2592,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( projectGroupingMode, threadPreviewCount, updateSettings, + openCommandPalette, openAddProject, isManualProjectSorting, projectDnDSensors, @@ -2649,14 +2650,12 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( - - } + Search @@ -2665,7 +2664,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( {commandPaletteShortcutLabel} ) : null} - + @@ -2834,7 +2833,11 @@ export default function Sidebar() { }); const routeThreadKey = routeThreadRef ? scopedThreadKey(routeThreadRef) : null; const keybindings = useServerKeybindings(); + const setCommandPaletteOpen = useCommandPaletteStore((store) => store.setOpen); const openAddProjectCommandPalette = useCommandPaletteStore((store) => store.openAddProject); + const openCommandPalette = useCallback(() => { + setCommandPaletteOpen(true); + }, [setCommandPaletteOpen]); const [expandedThreadListsByProject, setExpandedThreadListsByProject] = useState< ReadonlySet >(() => new Set()); @@ -3455,6 +3458,7 @@ export default function Sidebar() { projectGroupingMode={sidebarProjectGroupingMode} threadPreviewCount={sidebarThreadPreviewCount} updateSettings={updateSettings} + openCommandPalette={openCommandPalette} openAddProject={openAddProjectCommandPalette} isManualProjectSorting={isManualProjectSorting} projectDnDSensors={projectDnDSensors}