Unified · Split
| @@ -121,6 +121,8 @@ import type { ChatComposerHandle } from "./chat/ChatComposer"; · 120 unmodified lines | ||
| 121 | 121 | |
| 122 | 122 | const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = []; |
| 123 | 123 | const BROWSE_STALE_TIME_MS = 30_000; |
| 124 | +type CommandPaletteOpenIntent = ReturnType<typeof useCommandPaletteStore.getState>["openIntent"]; | |
| 125 | +type BrowseEntry = FilesystemBrowseResult["entries"][number]; | |
| 124 | 126 | |
| 125 | 127 | function getLocalFileManagerName(platform: string): string { |
| 126 | 128 | if (isMacPlatform(platform)) { |
| @@ -327,11 +329,31 @@ function errorMessage(error: unknown): string { · 200 unmodified lines | ||
| 327 | 329 | } |
| 328 | 330 | |
| 329 | 331 | export 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 | + | |
| 342 | +function CommandPaletteController() { | |
| 330 | 343 | const open = useCommandPaletteStore((store) => store.open); |
| 331 | 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 | + | |
| 354 | +function useCommandPaletteShortcut() { | |
| 332 | 355 | const toggleOpen = useCommandPaletteStore((store) => store.toggleOpen); |
| 333 | 356 | const keybindings = useServerKeybindings(); |
| 334 | − const composerHandleRef = useRef<ChatComposerHandle | null>(null); | |
| 335 | 357 | const routeTarget = useParams({ |
| 336 | 358 | strict: false, |
| 337 | 359 | select: (params) => resolveThreadRouteTarget(params), |
| @@ -362,32 +384,82 @@ export function CommandPalette({ children }: { children: ReactNode }) { · 24 unmodified lines | ||
| 362 | 384 | window.addEventListener("keydown", onKeyDown); |
| 363 | 385 | return () => window.removeEventListener("keydown", onKeyDown); |
| 364 | 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 | |
| 376 | 389 | function CommandPaletteDialog() { |
| 377 | 390 | const open = useCommandPaletteStore((store) => store.open); |
| 378 | 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 | |
| 401 | +function useCloseCommandPaletteOnUnmount(setOpen: (open: boolean) => void) { | |
| 380 | 402 | useEffect(() => { |
| 381 | 403 | return () => { |
| 382 | 404 | setOpen(false); |
| 383 | 405 | }; |
| 384 | 406 | }, [setOpen]); |
| 407 | +} | |
| 385 | 408 | |
| 386 | − if (!open) { | |
| 387 | − return null; | |
| 388 | − } | |
| 409 | +function usePrefetchBrowsePaths({ | |
| 410 | + exactBrowseEntry, | |
| 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 | + | |
| 447 | +function 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 | |
| 393 | 465 | function OpenCommandPaletteDialog() { |
| @@ -587,27 +659,14 @@ function OpenCommandPaletteDialog() { · 193 unmodified lines | ||
| 587 | 659 | [browseEnvironmentId, currentProjectCwdForBrowse, fetchBrowseResult, queryClient], |
| 588 | 660 | ); |
| 589 | 661 | |
| 590 | − // Prefetch the parent and the most likely next child so browse navigation | |
| 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 | − }, [ | |
| 662 | + usePrefetchBrowsePaths({ | |
| 604 | 663 | exactBrowseEntry, |
| 605 | − filteredBrowseEntries.length, | |
| 664 | + filteredBrowseEntryCount: filteredBrowseEntries.length, | |
| 606 | 665 | highlightedBrowseEntry, |
| 607 | 666 | isBrowsing, |
| 608 | 667 | prefetchBrowsePath, |
| 609 | 668 | query, |
| 610 | − ]); | |
| 669 | + }); | |
| 611 | 670 | |
| 612 | 671 | const openProjectFromSearch = useMemo( |
| 613 | 672 | () => async (project: (typeof projects)[number]) => { |
| @@ -971,13 +1030,7 @@ function OpenCommandPaletteDialog() { · 357 unmodified lines | ||
| 971 | 1030 | startAddProjectSourceSelection, |
| 972 | 1031 | ]); |
| 973 | 1032 | |
| 974 | − useLayoutEffect(() => { | |
| 975 | − if (openIntent?.kind !== "add-project") { | |
| 976 | − return; | |
| 977 | − } | |
| 978 | − clearOpenIntent(); | |
| 979 | − openAddProjectFlow(); | |
| 980 | − }, [clearOpenIntent, openAddProjectFlow, openIntent]); | |
| 1033 | + useOpenAddProjectIntent({ clearOpenIntent, openAddProjectFlow, openIntent }); | |
| 981 | 1034 | |
| 982 | 1035 | const actionItems: Array<CommandPaletteActionItem | CommandPaletteSubmenuItem> = []; |
| 983 | 1036 | |