diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 7862191d40..292bd32b78 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -121,6 +121,8 @@ import type { ChatComposerHandle } from "./chat/ChatComposer"; const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = []; const BROWSE_STALE_TIME_MS = 30_000; +type CommandPaletteOpenIntent = ReturnType["openIntent"]; +type BrowseEntry = FilesystemBrowseResult["entries"][number]; function getLocalFileManagerName(platform: string): string { if (isMacPlatform(platform)) { @@ -327,11 +329,31 @@ function errorMessage(error: unknown): string { } export function CommandPalette({ children }: { children: ReactNode }) { + const composerHandleRef = useRef(null); + + return ( + + {children} + + + ); +} + +function CommandPaletteController() { const open = useCommandPaletteStore((store) => store.open); const setOpen = useCommandPaletteStore((store) => store.setOpen); + useCommandPaletteShortcut(); + + return ( + + + + ); +} + +function useCommandPaletteShortcut() { const toggleOpen = useCommandPaletteStore((store) => store.toggleOpen); const keybindings = useServerKeybindings(); - const composerHandleRef = useRef(null); const routeTarget = useParams({ strict: false, select: (params) => resolveThreadRouteTarget(params), @@ -362,32 +384,82 @@ export function CommandPalette({ children }: { children: ReactNode }) { window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [keybindings, terminalOpen, toggleOpen]); - - return ( - - - {children} - - - - ); } function CommandPaletteDialog() { const open = useCommandPaletteStore((store) => store.open); const setOpen = useCommandPaletteStore((store) => store.setOpen); + useCloseCommandPaletteOnUnmount(setOpen); + + if (!open) { + return null; + } + + return ; +} +function useCloseCommandPaletteOnUnmount(setOpen: (open: boolean) => void) { useEffect(() => { return () => { setOpen(false); }; }, [setOpen]); +} - if (!open) { - return null; - } +function usePrefetchBrowsePaths({ + exactBrowseEntry, + filteredBrowseEntryCount, + highlightedBrowseEntry, + isBrowsing, + prefetchBrowsePath, + query, +}: { + readonly exactBrowseEntry: BrowseEntry | null; + readonly filteredBrowseEntryCount: number; + readonly highlightedBrowseEntry: BrowseEntry | null; + readonly isBrowsing: boolean; + readonly prefetchBrowsePath: (partialPath: string) => void; + readonly query: string; +}) { + // Prefetch the parent and the most likely next child so browse navigation + // stays warm without scanning every child directory in large trees. + useEffect(() => { + if (!isBrowsing || filteredBrowseEntryCount === 0) return; - return ; + if (canNavigateUp(query)) { + prefetchBrowsePath(getBrowseParentPath(query)!); + } + + const nextChild = highlightedBrowseEntry ?? exactBrowseEntry; + if (nextChild) { + prefetchBrowsePath(appendBrowsePathSegment(query, nextChild.name)); + } + }, [ + exactBrowseEntry, + filteredBrowseEntryCount, + highlightedBrowseEntry, + isBrowsing, + prefetchBrowsePath, + query, + ]); +} + +function useOpenAddProjectIntent({ + clearOpenIntent, + openAddProjectFlow, + openIntent, +}: { + readonly clearOpenIntent: () => void; + readonly openAddProjectFlow: () => void; + readonly openIntent: CommandPaletteOpenIntent; +}) { + useLayoutEffect(() => { + if (openIntent?.kind !== "add-project") { + return; + } + clearOpenIntent(); + openAddProjectFlow(); + }, [clearOpenIntent, openAddProjectFlow, openIntent]); } function OpenCommandPaletteDialog() { @@ -587,27 +659,14 @@ function OpenCommandPaletteDialog() { [browseEnvironmentId, currentProjectCwdForBrowse, fetchBrowseResult, queryClient], ); - // Prefetch the parent and the most likely next child so browse navigation - // stays warm without scanning every child directory in large trees. - useEffect(() => { - if (!isBrowsing || filteredBrowseEntries.length === 0) return; - - if (canNavigateUp(query)) { - prefetchBrowsePath(getBrowseParentPath(query)!); - } - - const nextChild = highlightedBrowseEntry ?? exactBrowseEntry; - if (nextChild) { - prefetchBrowsePath(appendBrowsePathSegment(query, nextChild.name)); - } - }, [ + usePrefetchBrowsePaths({ exactBrowseEntry, - filteredBrowseEntries.length, + filteredBrowseEntryCount: filteredBrowseEntries.length, highlightedBrowseEntry, isBrowsing, prefetchBrowsePath, query, - ]); + }); const openProjectFromSearch = useMemo( () => async (project: (typeof projects)[number]) => { @@ -971,13 +1030,7 @@ function OpenCommandPaletteDialog() { startAddProjectSourceSelection, ]); - useLayoutEffect(() => { - if (openIntent?.kind !== "add-project") { - return; - } - clearOpenIntent(); - openAddProjectFlow(); - }, [clearOpenIntent, openAddProjectFlow, openIntent]); + useOpenAddProjectIntent({ clearOpenIntent, openAddProjectFlow, openIntent }); const actionItems: Array = []; diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index e8c5bbe0b1..2d3407f0c9 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -175,7 +175,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"; @@ -2619,20 +2618,22 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( }, [updateSettings], ); + const setCommandPaletteOpen = useCommandPaletteStore((store) => store.setOpen); + const handleCommandPaletteClick = useCallback(() => { + setCommandPaletteOpen(true); + }, [setCommandPaletteOpen]); return ( - - } + Search @@ -2641,7 +2642,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( {commandPaletteShortcutLabel} ) : null} - + diff --git a/react-scan-recordings/after-command-palette.webm b/react-scan-recordings/after-command-palette.webm new file mode 100644 index 0000000000..86ee1cc2c6 Binary files /dev/null and b/react-scan-recordings/after-command-palette.webm differ diff --git a/react-scan-recordings/after-frame.png b/react-scan-recordings/after-frame.png new file mode 100644 index 0000000000..768d44644d Binary files /dev/null and b/react-scan-recordings/after-frame.png differ diff --git a/react-scan-recordings/before-command-palette.webm b/react-scan-recordings/before-command-palette.webm new file mode 100644 index 0000000000..a9bb92be55 Binary files /dev/null and b/react-scan-recordings/before-command-palette.webm differ diff --git a/react-scan-recordings/before-frame.png b/react-scan-recordings/before-frame.png new file mode 100644 index 0000000000..c4b9cee0c5 Binary files /dev/null and b/react-scan-recordings/before-frame.png differ