Unified · Split
| @@ -121,6 +121,8 @@ import type { ChatComposerHandle } from "./chat/ChatComposer"; · 120 unmodified lines | |||
| 121 | 121 | ||
| 122 | const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = []; | 122 | const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = []; |
| 123 | const BROWSE_STALE_TIME_MS = 30_000; | 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 | function getLocalFileManagerName(platform: string): string { | 127 | function 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 | ||
| 329 | export function CommandPalette({ children }: { children: ReactNode }) { | 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 | 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 | |||
| 354 | function 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 | ||
| 376 | function CommandPaletteDialog() { | 389 | function 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 | ||
| 401 | function 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) { | 409 | function 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 | |||
| 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 | function OpenCommandPaletteDialog() { | 465 | function 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 navigation | 662 | 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 | ||