History

Isolate command palette renders

Cursor Agent committed 5 months ago

Commit fdf22101 parent d1e85c41 file · +90 −37Browse files

Unified · Split

apps/web/src/components/CommandPalette.tsx+90−37
@@ -121,6 +121,8 @@ import type { ChatComposerHandle } from "./chat/ChatComposer"; · 120 unmodified lines
121 121
122const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = [];122const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = [];
123const BROWSE_STALE_TIME_MS = 30_000;123const BROWSE_STALE_TIME_MS = 30_000;
124type CommandPaletteOpenIntent = ReturnType<typeof useCommandPaletteStore.getState>["openIntent"];
125type BrowseEntry = FilesystemBrowseResult["entries"][number];
124 126
125function getLocalFileManagerName(platform: string): string {127function getLocalFileManagerName(platform: string): string {
126 if (isMacPlatform(platform)) {128 if (isMacPlatform(platform)) {
@@ -327,11 +329,31 @@ function errorMessage(error: unknown): string { · 200 unmodified lines
327}329}
328 330
329export function CommandPalette({ children }: { children: ReactNode }) {331export function CommandPalette({ children }: { children: ReactNode }) {
332 const composerHandleRef = useRef<ChatComposerHandle | null>(null);
333
334 return (
335 <ComposerHandleContext value={composerHandleRef}>
336 {children}
337 <CommandPaletteController />
338 </ComposerHandleContext>
339 );
340}
341
342function CommandPaletteController() {
330 const open = useCommandPaletteStore((store) => store.open);343 const open = useCommandPaletteStore((store) => store.open);
331 const setOpen = useCommandPaletteStore((store) => store.setOpen);344 const setOpen = useCommandPaletteStore((store) => store.setOpen);
345 useCommandPaletteShortcut();
346
347 return (
348 <CommandDialog open={open} onOpenChange={setOpen}>
349 <CommandPaletteDialog />
350 </CommandDialog>
351 );
352}
353
354function useCommandPaletteShortcut() {
332 const toggleOpen = useCommandPaletteStore((store) => store.toggleOpen);355 const toggleOpen = useCommandPaletteStore((store) => store.toggleOpen);
333 const keybindings = useServerKeybindings();356 const keybindings = useServerKeybindings();
334 const composerHandleRef = useRef<ChatComposerHandle | null>(null);
335 const routeTarget = useParams({357 const routeTarget = useParams({
336 strict: false,358 strict: false,
337 select: (params) => resolveThreadRouteTarget(params),359 select: (params) => resolveThreadRouteTarget(params),
@@ -362,32 +384,82 @@ export function CommandPalette({ children }: { children: ReactNode }) { · 24 unmodified lines
362 window.addEventListener("keydown", onKeyDown);384 window.addEventListener("keydown", onKeyDown);
363 return () => window.removeEventListener("keydown", onKeyDown);385 return () => window.removeEventListener("keydown", onKeyDown);
364 }, [keybindings, terminalOpen, toggleOpen]);386 }, [keybindings, terminalOpen, toggleOpen]);
365
366 return (
367 <ComposerHandleContext value={composerHandleRef}>
368 <CommandDialog open={open} onOpenChange={setOpen}>
369 {children}
370 <CommandPaletteDialog />
371 </CommandDialog>
372 </ComposerHandleContext>
373 );
374}387}
375 388
376function CommandPaletteDialog() {389function CommandPaletteDialog() {
377 const open = useCommandPaletteStore((store) => store.open);390 const open = useCommandPaletteStore((store) => store.open);
378 const setOpen = useCommandPaletteStore((store) => store.setOpen);391 const setOpen = useCommandPaletteStore((store) => store.setOpen);
392 useCloseCommandPaletteOnUnmount(setOpen);
393
394 if (!open) {
395 return null;
396 }
397
398 return <OpenCommandPaletteDialog />;
399}
379 400
401function useCloseCommandPaletteOnUnmount(setOpen: (open: boolean) => void) {
380 useEffect(() => {402 useEffect(() => {
381 return () => {403 return () => {
382 setOpen(false);404 setOpen(false);
383 };405 };
384 }, [setOpen]);406 }, [setOpen]);
407}
385 408
386 if (!open) {409function usePrefetchBrowsePaths({
387 return null;410 exactBrowseEntry,
388 }411 filteredBrowseEntryCount,
412 highlightedBrowseEntry,
413 isBrowsing,
414 prefetchBrowsePath,
415 query,
416}: {
417 readonly exactBrowseEntry: BrowseEntry | null;
418 readonly filteredBrowseEntryCount: number;
419 readonly highlightedBrowseEntry: BrowseEntry | null;
420 readonly isBrowsing: boolean;
421 readonly prefetchBrowsePath: (partialPath: string) => void;
422 readonly query: string;
423}) {
424 // Prefetch the parent and the most likely next child so browse navigation
425 // stays warm without scanning every child directory in large trees.
426 useEffect(() => {
427 if (!isBrowsing || filteredBrowseEntryCount === 0) return;
389 428
390 return <OpenCommandPaletteDialog />;429 if (canNavigateUp(query)) {
430 prefetchBrowsePath(getBrowseParentPath(query)!);
431 }
432
433 const nextChild = highlightedBrowseEntry ?? exactBrowseEntry;
434 if (nextChild) {
435 prefetchBrowsePath(appendBrowsePathSegment(query, nextChild.name));
436 }
437 }, [
438 exactBrowseEntry,
439 filteredBrowseEntryCount,
440 highlightedBrowseEntry,
441 isBrowsing,
442 prefetchBrowsePath,
443 query,
444 ]);
445}
446
447function useOpenAddProjectIntent({
448 clearOpenIntent,
449 openAddProjectFlow,
450 openIntent,
451}: {
452 readonly clearOpenIntent: () => void;
453 readonly openAddProjectFlow: () => void;
454 readonly openIntent: CommandPaletteOpenIntent;
455}) {
456 useLayoutEffect(() => {
457 if (openIntent?.kind !== "add-project") {
458 return;
459 }
460 clearOpenIntent();
461 openAddProjectFlow();
462 }, [clearOpenIntent, openAddProjectFlow, openIntent]);
391}463}
392 464
393function OpenCommandPaletteDialog() {465function OpenCommandPaletteDialog() {
@@ -587,27 +659,14 @@ function OpenCommandPaletteDialog() { · 193 unmodified lines
587 [browseEnvironmentId, currentProjectCwdForBrowse, fetchBrowseResult, queryClient],659 [browseEnvironmentId, currentProjectCwdForBrowse, fetchBrowseResult, queryClient],
588 );660 );
589 661
590 // Prefetch the parent and the most likely next child so browse navigation662 usePrefetchBrowsePaths({
591 // stays warm without scanning every child directory in large trees.
592 useEffect(() => {
593 if (!isBrowsing || filteredBrowseEntries.length === 0) return;
594
595 if (canNavigateUp(query)) {
596 prefetchBrowsePath(getBrowseParentPath(query)!);
597 }
598
599 const nextChild = highlightedBrowseEntry ?? exactBrowseEntry;
600 if (nextChild) {
601 prefetchBrowsePath(appendBrowsePathSegment(query, nextChild.name));
602 }
603 }, [
604 exactBrowseEntry,663 exactBrowseEntry,
605 filteredBrowseEntries.length,664 filteredBrowseEntryCount: filteredBrowseEntries.length,
606 highlightedBrowseEntry,665 highlightedBrowseEntry,
607 isBrowsing,666 isBrowsing,
608 prefetchBrowsePath,667 prefetchBrowsePath,
609 query,668 query,
610 ]);669 });
611 670
612 const openProjectFromSearch = useMemo(671 const openProjectFromSearch = useMemo(
613 () => async (project: (typeof projects)[number]) => {672 () => async (project: (typeof projects)[number]) => {
@@ -971,13 +1030,7 @@ function OpenCommandPaletteDialog() { · 357 unmodified lines
971 startAddProjectSourceSelection,1030 startAddProjectSourceSelection,
972 ]);1031 ]);
973 1032
974 useLayoutEffect(() => {1033 useOpenAddProjectIntent({ clearOpenIntent, openAddProjectFlow, openIntent });
975 if (openIntent?.kind !== "add-project") {
976 return;
977 }
978 clearOpenIntent();
979 openAddProjectFlow();
980 }, [clearOpenIntent, openAddProjectFlow, openIntent]);
981 1034
982 const actionItems: Array<CommandPaletteActionItem | CommandPaletteSubmenuItem> = [];1035 const actionItems: Array<CommandPaletteActionItem | CommandPaletteSubmenuItem> = [];
983 1036