1125 lines · 36.6 KB · TypeScript
RawDownload
"use client"; import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime";import {  DEFAULT_MODEL_BY_PROVIDER,  type EnvironmentId,  type FilesystemBrowseResult,  type ProjectId,} from "@t3tools/contracts";import { useQuery, useQueryClient } from "@tanstack/react-query";import { useNavigate, useParams } from "@tanstack/react-router";import {  ArrowDownIcon,  ArrowLeftIcon,  ArrowUpIcon,  CornerLeftUpIcon,  FolderIcon,  FolderPlusIcon,  MessageSquareIcon,  SettingsIcon,  SquarePenIcon,} from "lucide-react";import {  useCallback,  useDeferredValue,  useEffect,  useMemo,  useRef,  useState,  type KeyboardEvent,  type ReactNode,} from "react";import { useShallow } from "zustand/react/shallow";import { useCommandPaletteStore } from "../commandPaletteStore";import { readEnvironmentApi } from "../environmentApi";import { readPrimaryEnvironmentDescriptor, usePrimaryEnvironmentId } from "../environments/primary";import {  useSavedEnvironmentRegistryStore,  useSavedEnvironmentRuntimeStore,} from "../environments/runtime";import { useHandleNewThread } from "../hooks/useHandleNewThread";import { useSettings } from "../hooks/useSettings";import { readLocalApi } from "../localApi";import {  startNewThreadInProjectFromContext,  startNewThreadFromContext,} from "../lib/chatThreadActions";import {  appendBrowsePathSegment,  canNavigateUp,  ensureBrowseDirectoryPath,  findProjectByPath,  getBrowseDirectoryPath,  getBrowseLeafPathSegment,  getBrowseParentPath,  hasTrailingPathSeparator,  inferProjectTitleFromPath,  isExplicitRelativeProjectPath,  isFilesystemBrowseQuery,  isUnsupportedWindowsProjectPath,  resolveProjectPathForDispatch,} from "../lib/projectPaths";import { isTerminalFocused } from "../lib/terminalFocus";import { getLatestThreadForProject } from "../lib/threadSort";import { cn, isMacPlatform, isWindowsPlatform, newCommandId, newProjectId } from "../lib/utils";import {  selectProjectsAcrossEnvironments,  selectSidebarThreadsAcrossEnvironments,  useStore,} from "../store";import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";import { buildThreadRouteParams, resolveThreadRouteTarget } from "../threadRoutes";import {  ADDON_ICON_CLASS,  buildBrowseGroups,  buildProjectActionItems,  buildRootGroups,  buildThreadActionItems,  type CommandPaletteActionItem,  type CommandPaletteSubmenuItem,  type CommandPaletteView,  filterBrowseEntries,  filterCommandPaletteGroups,  getCommandPaletteInputPlaceholder,  getCommandPaletteMode,  ITEM_ICON_CLASS,  RECENT_THREAD_LIMIT,} from "./CommandPalette.logic";import { resolveEnvironmentOptionLabel } from "./BranchToolbar.logic";import { CommandPaletteResults } from "./CommandPaletteResults";import { ProjectFavicon } from "./ProjectFavicon";import { ThreadRowLeadingStatus, ThreadRowTrailingStatus } from "./ThreadStatusIndicators";import { useServerKeybindings } from "../rpc/serverState";import { resolveShortcutCommand } from "../keybindings";import {  Command,  CommandDialog,  CommandDialogPopup,  CommandFooter,  CommandInput,  CommandPanel,} from "./ui/command";import { Button } from "./ui/button";import { Kbd, KbdGroup } from "./ui/kbd";import { stackedThreadToast, toastManager } from "./ui/toast";import { ComposerHandleContext, useComposerHandleContext } from "../composerHandleContext";import type { ChatComposerHandle } from "./chat/ChatComposer"; const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = [];const BROWSE_STALE_TIME_MS = 30_000; function getLocalFileManagerName(platform: string): string {  if (isMacPlatform(platform)) {    return "Finder";  }  if (isWindowsPlatform(platform)) {    return "Explorer";  }  return "Files";} function getEnvironmentBrowsePlatform(os: string | null | undefined): string {  if (os === "windows") {    return "Win32";  }  if (os === "darwin") {    return "MacIntel";  }  if (os === "linux") {    return "Linux";  }  return typeof navigator === "undefined" ? "" : navigator.platform;} interface AddProjectEnvironmentOption {  readonly environmentId: EnvironmentId;  readonly label: string;  readonly isPrimary: boolean;} export function CommandPalette({ children }: { children: ReactNode }) {  const open = useCommandPaletteStore((store) => store.open);  const setOpen = useCommandPaletteStore((store) => store.setOpen);  const toggleOpen = useCommandPaletteStore((store) => store.toggleOpen);  const keybindings = useServerKeybindings();  const composerHandleRef = useRef<ChatComposerHandle | null>(null);  const routeTarget = useParams({    strict: false,    select: (params) => resolveThreadRouteTarget(params),  });  const routeThreadRef = routeTarget?.kind === "server" ? routeTarget.threadRef : null;  const terminalOpen = useTerminalStateStore((state) =>    routeThreadRef      ? selectThreadTerminalState(state.terminalStateByThreadKey, routeThreadRef).terminalOpen      : 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]);   return (    <ComposerHandleContext.Provider value={composerHandleRef}>      <CommandDialog open={open} onOpenChange={setOpen}>        {children}        <CommandPaletteDialog />      </CommandDialog>    </ComposerHandleContext.Provider>  );} function CommandPaletteDialog() {  const open = useCommandPaletteStore((store) => store.open);  const setOpen = useCommandPaletteStore((store) => store.setOpen);   useEffect(() => {    return () => {      setOpen(false);    };  }, [setOpen]);   if (!open) {    return null;  }   return <OpenCommandPaletteDialog />;} function OpenCommandPaletteDialog() {  const navigate = useNavigate();  const setOpen = useCommandPaletteStore((store) => store.setOpen);  const openIntent = useCommandPaletteStore((store) => store.openIntent);  const clearOpenIntent = useCommandPaletteStore((store) => store.clearOpenIntent);  const composerHandleRef = useComposerHandleContext();  const [query, setQuery] = useState("");  const deferredQuery = useDeferredValue(query);  const isActionsOnly = deferredQuery.startsWith(">");  const queryClient = useQueryClient();  const [highlightedItemValue, setHighlightedItemValue] = useState<string | null>(null);  const settings = useSettings();  const { activeDraftThread, activeThread, defaultProjectRef, handleNewThread } =    useHandleNewThread();  const projects = useStore(useShallow(selectProjectsAcrossEnvironments));  const threads = useStore(useShallow(selectSidebarThreadsAcrossEnvironments));  const keybindings = useServerKeybindings();  const [viewStack, setViewStack] = useState<CommandPaletteView[]>([]);  const currentView = viewStack.at(-1) ?? null;  const [browseGeneration, setBrowseGeneration] = useState(0);  const [addProjectEnvironmentId, setAddProjectEnvironmentId] = useState<EnvironmentId | null>(    null,  );  const [isPickingProjectFolder, setIsPickingProjectFolder] = useState(false);  const primaryEnvironmentId = usePrimaryEnvironmentId();  const primaryEnvironmentLabel = readPrimaryEnvironmentDescriptor()?.label ?? null;  const savedEnvironmentRegistry = useSavedEnvironmentRegistryStore((state) => state.byId);  const savedEnvironmentRuntimeById = useSavedEnvironmentRuntimeStore((state) => state.byId);   const addProjectEnvironmentOptions = useMemo(() => {    const options: AddProjectEnvironmentOption[] = [];    const seenEnvironmentIds = new Set<EnvironmentId>();     if (primaryEnvironmentId) {      seenEnvironmentIds.add(primaryEnvironmentId);      options.push({        environmentId: primaryEnvironmentId,        label: resolveEnvironmentOptionLabel({          isPrimary: true,          environmentId: primaryEnvironmentId,          runtimeLabel: primaryEnvironmentLabel,        }),        isPrimary: true,      });    }     for (const record of Object.values(savedEnvironmentRegistry)) {      if (seenEnvironmentIds.has(record.environmentId)) {        continue;      }       const runtimeState = savedEnvironmentRuntimeById[record.environmentId];      options.push({        environmentId: record.environmentId,        label: resolveEnvironmentOptionLabel({          isPrimary: false,          environmentId: record.environmentId,          runtimeLabel: runtimeState?.descriptor?.label ?? null,          savedLabel: record.label,        }),        isPrimary: false,      });    }     options.sort((left, right) => {      if (left.isPrimary !== right.isPrimary) {        return left.isPrimary ? -1 : 1;      }      return left.label.localeCompare(right.label);    });     return options;  }, [    primaryEnvironmentId,    primaryEnvironmentLabel,    savedEnvironmentRegistry,    savedEnvironmentRuntimeById,  ]);  const defaultAddProjectEnvironmentId = addProjectEnvironmentOptions[0]?.environmentId ?? null;  const browseEnvironmentId = addProjectEnvironmentId ?? defaultAddProjectEnvironmentId;  const browseEnvironmentPlatform = useMemo(() => {    const os =      browseEnvironmentId && primaryEnvironmentId && browseEnvironmentId === primaryEnvironmentId        ? (readPrimaryEnvironmentDescriptor()?.platform.os ?? null)        : browseEnvironmentId          ? (savedEnvironmentRuntimeById[browseEnvironmentId]?.descriptor?.platform.os ??            savedEnvironmentRuntimeById[browseEnvironmentId]?.serverConfig?.environment.platform              .os ??            null)          : null;    return getEnvironmentBrowsePlatform(os);  }, [browseEnvironmentId, primaryEnvironmentId, savedEnvironmentRuntimeById]);  const isBrowsing = isFilesystemBrowseQuery(query, browseEnvironmentPlatform);  const paletteMode = getCommandPaletteMode({ currentView, isBrowsing });  const getAddProjectInitialQueryForEnvironment = useCallback(    (environmentId: EnvironmentId | null): string => {      const environmentSettings =        environmentId && primaryEnvironmentId && environmentId === primaryEnvironmentId          ? settings          : environmentId            ? savedEnvironmentRuntimeById[environmentId]?.serverConfig?.settings            : null;      const baseDirectory = environmentSettings?.addProjectBaseDirectory?.trim() ?? "";      if (baseDirectory.length === 0) {        return "~/";      }      return ensureBrowseDirectoryPath(baseDirectory);    },    [primaryEnvironmentId, savedEnvironmentRuntimeById, settings],  );   const projectCwdById = useMemo(    () => new Map<ProjectId, string>(projects.map((project) => [project.id, project.cwd])),    [projects],  );  const projectTitleById = useMemo(    () => new Map<ProjectId, string>(projects.map((project) => [project.id, project.name])),    [projects],  );   const activeThreadId = activeThread?.id;  const currentProjectEnvironmentId =    activeThread?.environmentId ?? activeDraftThread?.environmentId ?? null;  const currentProjectId = activeThread?.projectId ?? activeDraftThread?.projectId ?? null;  const currentProjectCwd = currentProjectId    ? (projectCwdById.get(currentProjectId) ?? null)    : null;  const currentProjectCwdForBrowse =    browseEnvironmentId && currentProjectEnvironmentId === browseEnvironmentId      ? currentProjectCwd      : null;  const relativePathNeedsActiveProject =    isExplicitRelativeProjectPath(query.trim()) && currentProjectCwdForBrowse === null;  const browseDirectoryPath = isBrowsing ? getBrowseDirectoryPath(query) : "";  const browseFilterQuery =    isBrowsing && !hasTrailingPathSeparator(query) ? getBrowseLeafPathSegment(query) : "";   const fetchBrowseResult = useCallback(    async (partialPath: string): Promise<FilesystemBrowseResult | null> => {      if (!browseEnvironmentId) return null;      const api = readEnvironmentApi(browseEnvironmentId);      if (!api) return null;      return api.filesystem.browse({        partialPath,        ...(currentProjectCwdForBrowse ? { cwd: currentProjectCwdForBrowse } : {}),      });    },    [browseEnvironmentId, currentProjectCwdForBrowse],  );   const { data: browseResult, isPending: isBrowsePending } = useQuery({    queryKey: [      "filesystemBrowse",      browseEnvironmentId,      browseDirectoryPath,      currentProjectCwdForBrowse,    ],    queryFn: () => fetchBrowseResult(browseDirectoryPath),    staleTime: BROWSE_STALE_TIME_MS,    enabled:      isBrowsing &&      browseDirectoryPath.length > 0 &&      browseEnvironmentId !== null &&      !relativePathNeedsActiveProject,  });  const browseEntries = browseResult?.entries ?? EMPTY_BROWSE_ENTRIES;  const {    filteredEntries: filteredBrowseEntries,    highlightedEntry: highlightedBrowseEntry,    exactEntry: exactBrowseEntry,  } = useMemo(    () => filterBrowseEntries({ browseEntries, browseFilterQuery, highlightedItemValue }),    [browseEntries, browseFilterQuery, highlightedItemValue],  );   const prefetchBrowsePath = useCallback(    (partialPath: string) => {      void queryClient.prefetchQuery({        queryKey: [          "filesystemBrowse",          browseEnvironmentId,          partialPath,          currentProjectCwdForBrowse,        ],        queryFn: () => fetchBrowseResult(partialPath),        staleTime: BROWSE_STALE_TIME_MS,      });    },    [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));    }  }, [    exactBrowseEntry,    filteredBrowseEntries.length,    highlightedBrowseEntry,    isBrowsing,    prefetchBrowsePath,    query,  ]);   const openProjectFromSearch = useMemo(    () => async (project: (typeof projects)[number]) => {      const latestThread = getLatestThreadForProject(        threads.filter((thread) => thread.environmentId === project.environmentId),        project.id,        settings.sidebarThreadSortOrder,      );      if (latestThread) {        await navigate({          to: "/$environmentId/$threadId",          params: buildThreadRouteParams(            scopeThreadRef(latestThread.environmentId, latestThread.id),          ),        });        return;      }       await handleNewThread(scopeProjectRef(project.environmentId, project.id), {        envMode: settings.defaultThreadEnvMode,      });    },    [      handleNewThread,      navigate,      settings.defaultThreadEnvMode,      settings.sidebarThreadSortOrder,      threads,    ],  );   const projectSearchItems = useMemo(    () =>      buildProjectActionItems({        projects,        valuePrefix: "project",        icon: (project) => (          <ProjectFavicon            environmentId={project.environmentId}            cwd={project.cwd}            className={ITEM_ICON_CLASS}          />        ),        runProject: openProjectFromSearch,      }),    [openProjectFromSearch, projects],  );   const projectThreadItems = useMemo(    () =>      buildProjectActionItems({        projects,        valuePrefix: "new-thread-in",        icon: (project) => (          <ProjectFavicon            environmentId={project.environmentId}            cwd={project.cwd}            className={ITEM_ICON_CLASS}          />        ),        runProject: async (project) => {          await startNewThreadInProjectFromContext(            {              activeDraftThread,              activeThread,              defaultProjectRef,              defaultThreadEnvMode: settings.defaultThreadEnvMode,              handleNewThread,            },            scopeProjectRef(project.environmentId, project.id),          );        },      }),    [      activeDraftThread,      activeThread,      defaultProjectRef,      handleNewThread,      projects,      settings.defaultThreadEnvMode,    ],  );   const allThreadItems = useMemo(

Lines 1–500 of 1125.