import { threadPullRequestLinkMode } from "@t3tools/client-runtime/thread-pull-request-compatibility"; import { visibleThreadPullRequests } from "@t3tools/shared/threadPullRequests"; import { scopedThreadKey, scopeProjectRef, scopeThreadRef, } from "@t3tools/client-runtime/environment"; import { canCreateProjectInEnvironment, getCloneDestinationBrowsePath, getCloneDestinationPath, getCloneDirectoryName, getDefaultCloneUrl, getNewProjectPathPreview, getNewProjectPublishTarget, getNewProjectPublishTargets, getNewProjectRepository, normalizePastedCloneUrl, addProjectRemoteSourceLabel, addProjectRemoteSourcePathHint, addProjectRemoteSourceProvider, sortAddProjectProviderSources, type AddProjectRemoteSource, type AddProjectRemoteSourceReadiness, } from "@t3tools/client-runtime/operations/projects"; import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; import { connectionStatusText } from "@t3tools/client-runtime/connection"; import { resolveThreadReferenceCopyTarget } from "@t3tools/client-runtime/thread-reference"; import { canPreloadBrowsePath, createBrowseNavigationCoordinator, filterFilesystemBrowseEntries, getFilesystemBrowsePath, } from "@t3tools/client-runtime/state/filesystem"; import { isAtomCommandInterrupted, settlePromise, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { AuthOrchestrationOperateScope, AuthSourceControlWriteScope, AuthFilesystemReadScope, type DesktopWslState, type EnvironmentId, type FilesystemBrowseResult, SourceControlProviderKind, PRIMARY_LOCAL_ENVIRONMENT_ID, resolveEnvironmentMachineKind, } from "@t3tools/contracts"; import { useLocation, useNavigate } from "@tanstack/react-router"; import { ArrowLeftIcon, ChartNoAxesColumnIcon, CheckIcon, ChevronRightIcon, CornerLeftUpIcon, FileSearchIcon, FolderGit2Icon, FolderIcon, FolderPlusIcon, MessageSquareDashedIcon, LinkIcon, MessageSquareIcon, PaletteIcon, RotateCcwIcon, SettingsIcon, SquarePenIcon, TextSearchIcon, UserPlusIcon, } from "lucide-react"; import { openThreadFind } from "./chat/threadFindStore"; import { useDeferredValue, useEffect, useReducer, useRef, useState, type KeyboardEvent, type ReactNode, } from "react"; import type { LegendListRef } from "@legendapp/list/react"; import { useAtomValue } from "@effect/atom-react"; import { isDesktopLocalConnectionTarget } from "../connection/desktopLocal"; import { useDesktopLocalBootstraps } from "../connection/useDesktopLocalBootstraps"; import { useHandleNewThread } from "../hooks/useHandleNewThread"; import { useOpenPanelPullRequestUrl } from "../hooks/useOpenPanelPullRequestUrl"; import { writeTextToClipboard } from "../hooks/useCopyToClipboard"; import { composerPromptLink, copyPromptLink, promptLinkRepositoryOf, } from "./promptLinks/copyPromptLink"; import { useClientSettings } from "../hooks/useSettings"; import { useStableCallback } from "../hooks/useStableCallback"; import { readLocalApi } from "../localApi"; import { desktopLocalBackendId } from "../connection/desktopLocal"; import { filesystemEnvironment, useFilesystemReadAccess } from "../state/filesystem"; import { projectEnvironment } from "../state/projects"; import { useEnvironmentQuery } from "../state/query"; import { serverEnvironment } from "../state/server"; import { threadEnvironment } from "../state/threads"; import { readEnvironmentScope, useEnvironmentScope } from "~/state/session"; import { sourceControlEnvironment } from "../state/sourceControl"; import { useAtomCommand } from "../state/use-atom-command"; import { useOrchestrationCommand } from "../state/use-orchestration-command"; import { useAtomQueryRunner } from "../state/use-atom-query-runner"; import { useScratchProject } from "../hooks/useScratchProject"; import { useNewProject } from "../hooks/useNewProject"; import { isScratchProject } from "@t3tools/client-runtime/state/projects"; import { useEnvironmentProviders, useEnvironments, usePrimaryEnvironmentId, } from "../state/environments"; import { useProjects, useServerConfigs, useThreadShells, waitForProject } from "../state/entities"; import { resolveThreadActionProjectRef, startNewThreadFromContext } from "../lib/chatThreadActions"; import { appendBrowsePathSegment, ensureBrowseDirectoryPath, findProjectByPath, getBrowseDirectoryPath, hasTrailingPathSeparator, inferProjectTitleFromPath, isExplicitRelativeProjectPath, isUnsupportedWindowsProjectPath, resolveProjectPathForDispatch, } from "../lib/projectPaths"; import { dispatchCommandPalette, useCommandPaletteMount, useCommandPaletteStore, } from "../commandPaletteStore"; import { PULL_REQUESTS_PANEL_REF, useRightPanelStore } from "../rightPanelStore"; import { getLatestThreadForProject, sortThreads } from "../lib/threadSort"; import { cn, getLocalFileManagerName, isMacPlatform, isWindowsPlatform, newProjectId, } from "../lib/utils"; import { buildThreadRouteParams } from "../threadRoutes"; import { useAvailableSettingsSearchItems } from "./settings/useAvailableSettingsSearchItems"; import { applyWslEnvironmentConfiguration, parseWslUncPath, resolveProjectPickerTarget, resolveWslProjectSelection, } from "../wslPaths"; import { ADDON_ICON_CLASS, browseInputEndPaddingClass, buildBrowseGroups, buildRootGroups, buildLinkedThreadActionItems, buildCommandPaletteRows, enumerateCommandPaletteItems, findHighlightedCommandPaletteItem, type CommandPaletteActionItem, type CommandPaletteOpenIntent, type CommandPaletteProject, type CommandPaletteReturnContext, type CommandPaletteSubmenuItem, type CommandPaletteView, filterCommandPaletteGroups, buildNestedSearchItems, filterPinnedBrowseEntries, getCommandPaletteInputPlaceholder, getCommandPaletteMode, ITEM_ICON_CLASS, RECENT_THREAD_LIMIT, createCommandPaletteSearchState, type CommandPaletteGroup, resolveAddProjectEntry, type AddProjectCloneFlow, type AddProjectEnvironmentOption, buildPaletteRemoteSourceReadiness, errorMessage, getEnvironmentBrowsePlatform, remoteProjectInputPlaceholder, reduceCommandPaletteSearchState, type SearchOverlayMode, } from "./CommandPalette.logic"; import { resolveEnvironmentOptionLabel } from "./BranchToolbar.logic"; import { CommandPaletteContent } from "./CommandPaletteContent"; import { selectCommandPaletteClientSettings, useEnvironmentLookups, useProjectLookups, useSortedProjectGroups, } from "./CommandPaletteLookups"; import { buildNewThreadInProjectItems, buildProjectSearchItems, } from "./CommandPaletteProjectItems"; import { CommandPaletteSubmitButton, NewProjectPublishHostPicker, NewProjectSummary, RemoteRepositorySummary, remoteProjectSourceIcon, sourceControlHostIcon, } from "./CommandPaletteAddProject"; import { useCommandPaletteThreadItems } from "./CommandPaletteThreadItems"; import { useCommandPaletteThemeItems } from "./CommandPaletteThemeItems"; import { CommandPaletteVirtualizedResults, scrollCommandPaletteRowIntoView, } from "./CommandPaletteResults"; import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; import { Checkbox } from "./ui/checkbox"; import { ProjectFilePicker } from "./files/ProjectFilePicker"; import { openLinkPullRequestDialog } from "./pullRequest/LinkPullRequestDialog"; import { canShareWithGuestAtom } from "./chat/guestShareAvailability"; import { openGuestShareDialog } from "./chat/guestShareDialogState"; import { ProjectContentSearchDialog } from "./search/ProjectContentSearchDialog"; import { settingsForThisPlatform, SETTINGS_SECTION_LABELS } from "./settings/settingsSearch"; import { primaryServerKeybindingsAtom } from "../state/server"; import { resolveShortcutCommand, threadJumpIndexFromCommand } from "../keybindings"; import { CommandDialog, CommandDialogPopup, CommandFooterAction } from "./ui/command"; import { Button } from "./ui/button"; import { stackedThreadToast, toastManager } from "./ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { ComposerHandleContext, useComposerHandleContext } from "../composerHandleContext"; import type { ChatComposerHandle } from "./chat/ChatComposer"; import { pickableProjectGroups } from "../logicalProject"; import { buildSidebarProjectPickerEntries } from "../sidebarProjectGrouping"; import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; import { readPullRequestListPreferences } from "~/components/pullRequest/pullRequestListPreferences"; import { useThreadRouteTarget } from "../hooks/useThreadRouteTarget"; import { threadRecentUse } from "@t3tools/shared/paletteSearch"; const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = []; const ADD_PROJECT_ENTRY_GROUP = "add-project"; const NEW_THREAD_IN_GROUP = "projects"; /** The submenu an open intent starts in; its groups resolve while rendering. */ function openIntentView(intent: CommandPaletteOpenIntent | null): CommandPaletteView | null { switch (intent?.kind) { case "add-project": return { addonIcon: , groups: [{ value: ADD_PROJECT_ENTRY_GROUP, label: "", items: [] }], }; case "new-thread-in": return { addonIcon: , groups: [{ value: NEW_THREAD_IN_GROUP, label: "Projects", items: [] }], }; case "change-theme": return { addonIcon: , groups: [{ value: "themes", label: "Change theme", items: [] }], }; case "search": case undefined: return null; } } export function CommandPalette({ children }: { children: ReactNode }) { useCommandPaletteMount(); const state = useCommandPaletteStore(); const setOpen = (open: boolean) => dispatchCommandPalette({ _tag: "SetOpen", open }); const openChildSearch = (mode: "files" | "content", context: CommandPaletteReturnContext) => dispatchCommandPalette({ _tag: "OpenChildSearch", mode, context }); const composerHandleRef = useRef(null); return ( { if (!open && eventDetails.reason === "escape-key" && state.mode !== "command") { eventDetails.cancel(); dispatchCommandPalette({ _tag: "BackToCommand" }); return; } setOpen(open); }} >
{children}
); } function CommandPaletteDialog(props: { readonly mode: SearchOverlayMode; readonly session: number; readonly openIntent: CommandPaletteOpenIntent | null; readonly setOpen: (open: boolean) => void; readonly openChildSearch: ( mode: "files" | "content", context: CommandPaletteReturnContext, ) => void; readonly initialContext: CommandPaletteReturnContext | undefined; }) { const composerHandleRef = useComposerHandleContext(); return ( { const composer = composerHandleRef?.current; if (!composer) return true; composer.focusAtEnd(); return false; }} onBackdropPointerDown={() => { props.setOpen(false); }} > {props.mode === "files" ? ( ) : props.mode === "content" ? ( ) : ( )} ); } function OpenCommandPaletteDialog(props: { readonly openIntent: CommandPaletteOpenIntent | null; readonly setOpen: (open: boolean) => void; readonly openChildSearch: ( mode: "files" | "content", context: CommandPaletteReturnContext, ) => void; readonly initialContext: CommandPaletteReturnContext | undefined; }) { const navigate = useNavigate(); const pathname = useLocation({ select: (location) => location.pathname }); const { openChildSearch, setOpen } = props; // Keyed by palette session: the intent and restored search only seed this // mount, and closing must not change what the closing palette shows. const [initialSearch] = useState( () => props.initialContext ?? { query: props.openIntent?.kind === "search" ? props.openIntent.query : "", linkedThreadSearch: props.openIntent?.kind === "search" ? props.openIntent : null, }, ); const linkedThreadSearch = initialSearch.linkedThreadSearch; const [searchState, dispatchSearch] = useReducer(reduceCommandPaletteSearchState, null, () => { const view = props.initialContext ? null : openIntentView(props.openIntent); return view ? { query: "", views: [{ view, parentQuery: "" }] } : createCommandPaletteSearchState(initialSearch.query); }); const { query, views: viewStack } = searchState; const setQuery = (query: string) => dispatchSearch({ type: "query", query }); const deferredQuery = useDeferredValue(query); const isActionsOnly = deferredQuery.startsWith(">"); const [highlightedItemValue, setHighlightedItemValue] = useState(null); const resultListRef = useRef(null); // A pushed view drops the typed highlight until the user moves it again. const highlightClearedRef = useRef(viewStack.length > 0); function clearTypedHighlight(): void { highlightClearedRef.current = true; setHighlightedItemValue(null); } const clientSettings = useClientSettings(selectCommandPaletteClientSettings); const createProject = useAtomCommand(projectEnvironment.create, { reportFailure: false, }); const updateProject = useOrchestrationCommand(projectEnvironment.update, { label: "project unarchive", }); const { scratchEnvironmentId, scratchWorkspaceRootFor, startScratchThread } = useScratchProject(); const lookupRepository = useAtomQueryRunner(sourceControlEnvironment.repository, { reportFailure: false, }); const loadBrowsePath = useAtomQueryRunner(filesystemEnvironment.browse, { reportFailure: false, reportDefect: false, }); const cloneRepository = useAtomCommand(sourceControlEnvironment.cloneRepository, { reportFailure: false, }); const startProjectClone = useAtomCommand(sourceControlEnvironment.startProjectClone, { reportFailure: false, }); const stopThreadSession = useAtomCommand(threadEnvironment.stopSession, { reportFailure: false, }); const refreshProviders = useAtomCommand(serverEnvironment.refreshProviders, { reportFailure: false, }); const { environments } = useEnvironments(); const desktopLocalBootstraps = useDesktopLocalBootstraps(); const primaryEnvironmentId = usePrimaryEnvironmentId(); const availableSettingsSearchItems = useAvailableSettingsSearchItems(); const { activeDraftThread, activeThread, defaultProjectRef, handleNewThread } = useHandleNewThread(); const paletteRouteTarget = useThreadRouteTarget(); const projects = useProjects(); const referenceThreadRef = pathname === "/pull-requests" ? environments.some( (environment) => environment.serverConfig?.environment.capabilities.pullRequests === true, ) ? PULL_REQUESTS_PANEL_REF : null : activeThread ? scopeThreadRef(activeThread.environmentId, activeThread.id) : null; const openPanelPullRequestUrl = useOpenPanelPullRequestUrl(referenceThreadRef); const activeThreadServerConfig = useServerConfigs().get( activeThread?.environmentId ?? ("" as EnvironmentId), ); const canShareActiveThreadWithGuest = useAtomValue( canShareWithGuestAtom(activeThread?.environmentId ?? ("" as EnvironmentId)), ); const activeThreadReferenceCopyTarget = referenceThreadRef === null || (pathname === "/pull-requests" && !openPanelPullRequestUrl) ? null : resolveThreadReferenceCopyTarget({ threadId: referenceThreadRef.threadId, openPanelPullRequestUrl, pullRequests: activeThread?.pullRequests, linkedPullRequestUrl: activeThread?.linkedPullRequest?.url ?? activeThread?.branchPullRequest?.url ?? null, }); const copyActiveThreadReference = async () => { const target = activeThreadReferenceCopyTarget; if (target === null) return; try { const didCopy = await writeTextToClipboard(target.value, target.clipboardTarget); if (!didCopy) return; toastManager.add({ type: "success", title: target.successTitle, description: target.value, }); } catch (error) { console.error(error); toastManager.add( stackedThreadToast({ type: "error", title: target.failureTitle, description: error instanceof Error ? error.message : "An error occurred.", }), ); } }; const threads = useThreadShells(); const keybindings = useAtomValue(primaryServerKeybindingsAtom); const providersByEnvironment = useEnvironmentProviders(); const { changeThemeItem, changeAppearanceItem, themeEditorItem } = useCommandPaletteThemeItems(); const currentView = viewStack.at(-1)?.view ?? null; const threadSearchQuery = currentView === null && !isActionsOnly ? deferredQuery : ""; const [browseGeneration, setBrowseGeneration] = useState(0); const [browseNavigation] = useState(createBrowseNavigationCoordinator); const [addProjectEnvironmentId, setAddProjectEnvironmentId] = useState( null, ); const [isPickingProjectFolder, setIsPickingProjectFolder] = useState(false); const [addProjectCloneFlow, setAddProjectCloneFlow] = useState(null); const [newProjectFlow, setNewProjectFlow] = useState<{ readonly environmentId: EnvironmentId; readonly sourcesEnvironmentId: EnvironmentId | null; } | null>(null); const [newProjectPublishes, setNewProjectPublishes] = useState(false); const [newProjectPublishKind, setNewProjectPublishKind] = useState(null); const [isCreatingNewProject, setIsCreatingNewProject] = useState(false); // State lags a render behind, so a repeated Enter could start a second create. const newProjectSubmittingRef = useRef(false); const createNewProject = useNewProject(); const cloneLookupGeneration = useRef(0); const [isRemoteProjectLookingUp, setIsRemoteProjectLookingUp] = useState(false); const [isRemoteProjectCloning, setIsRemoteProjectCloning] = useState(false); const { environmentLabelById, projectEnvironmentLocationById } = useEnvironmentLookups(); const projectGroups = useSortedProjectGroups(environmentLabelById); const contextualProjectRef = resolveThreadActionProjectRef({ activeDraftThread, activeThread: activeThread ?? undefined, defaultProjectRef, handleNewThread, }); const projectPickerEntries = buildSidebarProjectPickerEntries({ groups: pickableProjectGroups(projectGroups), preferredProjectRef: contextualProjectRef, }); const pickerProjects = projectPickerEntries.map(({ group, targetProject }) => ({ ...targetProject, displayName: group.displayName, })); const projectGroupByTargetKey = new Map( projectPickerEntries.map(({ group, targetProject }) => [ `${targetProject.environmentId}:${targetProject.id}`, group, ]), ); const addProjectEnvironmentOptions = (() => { const options = environments .filter((environment) => canCreateProjectInEnvironment(environment.connection.phase)) .map((environment): AddProjectEnvironmentOption => { const isPrimary = environment.entry.target._tag === "PrimaryConnectionTarget"; return { environmentId: environment.environmentId, label: resolveEnvironmentOptionLabel({ isPrimary, environmentId: environment.environmentId, runtimeLabel: environment.label, }), isPrimary, machine: resolveEnvironmentMachineKind(environment.serverConfig), isConnected: canCreateProjectInEnvironment(environment.connection.phase), status: connectionStatusText(environment.connection), }; }); options.sort((left, right) => { if (left.isPrimary !== right.isPrimary) { return left.isPrimary ? -1 : 1; } return left.label.localeCompare(right.label); }); return options; })(); const defaultAddProjectEnvironmentId = addProjectEnvironmentOptions.find((option) => option.isConnected)?.environmentId ?? null; const wslAddProjectEnvironmentOption = addProjectEnvironmentOptions.find((option) => { if (!option.isConnected) { return false; } const environment = environments.find( (candidate) => candidate.environmentId === option.environmentId, ); return environment ? desktopLocalBackendId(environment.entry.target)?.startsWith("wsl:") === true : false; }) ?? null; const browseEnvironmentId = addProjectEnvironmentId ?? defaultAddProjectEnvironmentId; const canCreateProject = useEnvironmentScope(browseEnvironmentId, AuthOrchestrationOperateScope); const browseEnvironment = environments.find((environment) => environment.environmentId === browseEnvironmentId) ?? null; const browseEnvironmentIsDesktopLocal = browseEnvironment !== null && isDesktopLocalConnectionTarget(browseEnvironment.entry.target); // pickFolder routes desktop-local environments by pool id, not catalog environmentId. const browseDesktopInstanceId = (() => { if (!browseEnvironmentIsDesktopLocal || browseEnvironment === null) { return null; } const displayUrl = browseEnvironment.displayUrl; if (displayUrl === null) { return null; } return ( desktopLocalBootstraps.find((bootstrap) => bootstrap.httpBaseUrl === displayUrl)?.id ?? null ); })(); const sourceControlDiscovery = useEnvironmentQuery( browseEnvironmentId === null ? null : sourceControlEnvironment.discovery({ environmentId: browseEnvironmentId, input: {}, }), ); const browseEnvironmentPlatform = getEnvironmentBrowsePlatform( browseEnvironment?.serverConfig?.environment.platform.os, ); const isRemoteProjectCloneFlow = addProjectCloneFlow !== null; const isRemoteProjectRepositoryStep = addProjectCloneFlow?.step === "repository"; const pinnedCloneDirectoryName = addProjectCloneFlow?.step === "confirm" ? getCloneDirectoryName( addProjectCloneFlow.repository?.nameWithOwner ?? addProjectCloneFlow.remoteUrl, ) : ""; const browsePath = getFilesystemBrowsePath( query, browseEnvironmentPlatform, browseEnvironmentId !== null && !isRemoteProjectRepositoryStep && newProjectFlow === null, ); const isBrowsing = browsePath.isBrowsing; const browseDirectoryPath = browsePath.directoryPath; const paletteMode = getCommandPaletteMode({ currentView, isBrowsing }); const getAddProjectInitialQueryForEnvironment = (environmentId: EnvironmentId | null): string => { const environment = environments.find((candidate) => candidate.environmentId === environmentId); const environmentSettings = environment?.serverConfig?.settings ?? null; const baseDirectory = environmentSettings?.addProjectBaseDirectory?.trim() ?? ""; if (baseDirectory.length === 0) { return "~/"; } return ensureBrowseDirectoryPath(baseDirectory); }; const { projectCwdById, projectByKey, projectTitleById } = useProjectLookups(); const activeThreadKey = activeThread ? scopedThreadKey(scopeThreadRef(activeThread.environmentId, activeThread.id)) : null; const supportsThreadFind = environments.find((environment) => environment.environmentId === activeThread?.environmentId) ?.serverConfig?.threadFind === true; const currentProjectEnvironmentId = activeThread?.environmentId ?? activeDraftThread?.environmentId ?? null; const currentProjectId = activeThread?.projectId ?? activeDraftThread?.projectId ?? null; const scratchTargetEnvironmentId = scratchEnvironmentId( currentProjectEnvironmentId ?? primaryEnvironmentId, ); const currentProjectCwd = currentProjectId ? (projectCwdById.get(currentProjectId) ?? null) : null; const currentProjectCwdForBrowse = browseEnvironmentId && currentProjectEnvironmentId === browseEnvironmentId ? currentProjectCwd : null; const getBrowseCwdForEnvironment = (environmentId: EnvironmentId | null): string | null => environmentId && currentProjectEnvironmentId === environmentId ? currentProjectCwd : null; const relativePathNeedsActiveProject = isExplicitRelativeProjectPath(query.trim()) && currentProjectCwdForBrowse === null; const browseAccess = useFilesystemReadAccess(browseEnvironmentId); const hasBrowseTarget = isBrowsing && browsePath.directoryPath.length > 0 && browseEnvironmentId !== null && !relativePathNeedsActiveProject; const browseQuery = useEnvironmentQuery( browseAccess.canReadFiles && hasBrowseTarget ? filesystemEnvironment.browse({ environmentId: browseEnvironmentId, input: { partialPath: browsePath.directoryPath, ...(currentProjectCwdForBrowse ? { cwd: currentProjectCwdForBrowse } : {}), }, }) : null, ); const browseResult = browseQuery.data; const isBrowsePending = hasBrowseTarget && (browseAccess.isPending || browseQuery.isPending); const browseAccessError = hasBrowseTarget && !browseAccess.isPending && !browseAccess.canReadFiles ? (browseAccess.error ?? "This connection cannot browse host folders.") : null; const browseEntries = browseResult?.entries ?? EMPTY_BROWSE_ENTRIES; const { visibleEntries: visibleBrowseEntries, exactEntry: exactBrowseEntry } = pinnedCloneDirectoryName ? filterPinnedBrowseEntries({ browseEntries, filterQuery: browsePath.filterQuery, pinnedDirectoryName: pinnedCloneDirectoryName, caseSensitive: !isWindowsPlatform(browseEnvironmentPlatform), }) : filterFilesystemBrowseEntries(browseEntries, browsePath.filterQuery); const prefetchBrowsePath = async ( partialPath: string, environmentId: EnvironmentId | null = browseEnvironmentId, cwd: string | null = currentProjectCwdForBrowse, ): Promise => { if (!environmentId) { return; } const environment = environments.find((candidate) => candidate.environmentId === environmentId); if ( !readEnvironmentScope(environmentId, AuthFilesystemReadScope) || !canPreloadBrowsePath(environment?.connection.phase) ) { return; } await loadBrowsePath({ environmentId, input: { partialPath, ...(cwd ? { cwd } : {}), }, }); }; useEffect( () => () => { browseNavigation.invalidate(); // A replaced or closed session must not report its in-flight lookup. cloneLookupGeneration.current += 1; }, [browseNavigation], ); const openProjectFromSearch = async (project: (typeof projects)[number]) => { const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`); const groupedProjectKeys = group ? new Set( group.memberProjectRefs.map( (projectRef) => `${projectRef.environmentId}:${projectRef.projectId}`, ), ) : null; const latestThread = groupedProjectKeys ? (sortThreads( threads.filter( (thread) => thread.archivedAt === null && groupedProjectKeys.has(`${thread.environmentId}:${thread.projectId}`), ), clientSettings.sidebarThreadSortOrder, )[0] ?? null) : getLatestThreadForProject( threads.filter((thread) => thread.environmentId === project.environmentId), project.id, clientSettings.sidebarThreadSortOrder, ); if (latestThread) { await navigate({ to: "/$environmentId/$threadId", params: buildThreadRouteParams(scopeThreadRef(latestThread.environmentId, latestThread.id)), }); return; } await handleNewThread(scopeProjectRef(project.environmentId, project.id)); }; const projectSearchItems = buildProjectSearchItems({ projects: pickerProjects, projectGroupByTargetKey, locationByEnvironmentId: projectEnvironmentLocationById, runProject: openProjectFromSearch, }); const projectThreadItems = (() => { const isScratch = (project: CommandPaletteProject) => isScratchProject(project, scratchWorkspaceRootFor(project.environmentId)); const projectItems = enumerateCommandPaletteItems( buildNewThreadInProjectItems({ projects: pickerProjects.filter((project) => !isScratch(project)), projectGroupByTargetKey, locationByEnvironmentId: projectEnvironmentLocationById, runProject: async (project) => { const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`); const contextualRefBelongsToGroup = contextualProjectRef !== null && group?.memberProjectRefs.some( (projectRef) => projectRef.environmentId === contextualProjectRef.environmentId && projectRef.projectId === contextualProjectRef.projectId, ); await handleNewThread( contextualRefBelongsToGroup ? contextualProjectRef : scopeProjectRef(project.environmentId, project.id), ); }, }), ); if (scratchTargetEnvironmentId === null) return projectItems; const noProjectIndex = pickerProjects[0] !== undefined && isScratch(pickerProjects[0]) ? 0 : 1; return [ ...projectItems.slice(0, noProjectIndex), { kind: "action" as const, value: "new-thread-in:no-project", searchTerms: ["no project", "without project", "none"], title: "No project", icon: , shortcutCommand: "chat.newWithoutProject" as const, run: () => startScratchThread(scratchTargetEnvironmentId), }, ...projectItems.slice(noProjectIndex), ]; })(); const { threadItems: allThreadItems, threadMessageItems, now: searchNow, isSearchPending: isThreadSearchPending, } = useCommandPaletteThreadItems({ activeThread: activeThread ?? null, threadSearchQuery }); // Recent means used recently: written in or opened, whichever is later. The // open thread would always lead that list, so it is left out. const activeThreadValue = activeThread ? `thread:${activeThread.environmentId}:${activeThread.id}` : null; const recentThreadItems = allThreadItems .filter((item) => item.value !== activeThreadValue) .toSorted( (left, right) => (right.threadSearch ? threadRecentUse(right.threadSearch) : 0) - (left.threadSearch ? threadRecentUse(left.threadSearch) : 0), ) .slice(0, RECENT_THREAD_LIMIT); const pushPaletteView = useStableCallback((view: CommandPaletteView): void => { browseNavigation.invalidate(); dispatchSearch({ type: "push", view }); highlightClearedRef.current = true; setHighlightedItemValue(null); }); function popView(): void { browseNavigation.invalidate(); setAddProjectCloneFlow(null); setNewProjectFlow(null); if (viewStack.length <= 1) { setAddProjectEnvironmentId(null); } else if (newProjectFlow?.sourcesEnvironmentId) { setAddProjectEnvironmentId(newProjectFlow.sourcesEnvironmentId); } dispatchSearch({ type: "back" }); setHighlightedItemValue(null); } function handleQueryChange(nextQuery: string): void { browseNavigation.invalidate(); clearTypedHighlight(); setQuery(nextQuery); if (nextQuery === "" && currentView?.initialQuery) { popView(); } } const startAddProjectBrowse = async (environmentId: EnvironmentId): Promise => { const initialQuery = getAddProjectInitialQueryForEnvironment(environmentId); const initialBrowsePath = getBrowseDirectoryPath(initialQuery); const browseCwd = getBrowseCwdForEnvironment(environmentId); const view: CommandPaletteView = { addonIcon: , groups: [], initialQuery, }; await browseNavigation.run( () => initialBrowsePath.length > 0 ? prefetchBrowsePath(initialBrowsePath, environmentId, browseCwd) : Promise.resolve(), () => { setAddProjectEnvironmentId(environmentId); setAddProjectCloneFlow(null); pushPaletteView(view); }, ); }; const startAddProjectClone = ( environmentId: EnvironmentId, source: AddProjectRemoteSource, ): void => { setAddProjectEnvironmentId(environmentId); setAddProjectCloneFlow({ step: "repository", environmentId, source }); pushPaletteView({ addonIcon: remoteProjectSourceIcon(source, ADDON_ICON_CLASS), groups: [], initialQuery: "", }); }; const newProjectsRootFor = (environmentId: EnvironmentId | null): string | null => environments.find((environment) => environment.environmentId === environmentId)?.serverConfig ?.newProjectsRoot ?? null; const startNewProject = ( environmentId: EnvironmentId, sourcesEnvironmentId: EnvironmentId | null, ): void => { setAddProjectEnvironmentId(environmentId); setAddProjectCloneFlow(null); setNewProjectFlow({ environmentId, sourcesEnvironmentId }); setNewProjectPublishes(false); setNewProjectPublishKind(null); pushPaletteView({ addonIcon: , groups: [], }); }; const openSourceControlSettings = () => { setOpen(false); void navigate({ to: "/settings/source-control" }); }; const buildAddProjectSourceGroups = ( environmentId: EnvironmentId, readinessBySource: AddProjectRemoteSourceReadiness, ): CommandPaletteView["groups"] => { const sourceItems: Array = [ { kind: "action", value: `action:add-project:${environmentId}:local`, searchTerms: ["local", "folder", "directory", "browse"], title: "Local folder", description: "Browse a folder on disk", icon: , keepOpen: true, run: async () => { await startAddProjectBrowse(environmentId); }, }, ]; if (newProjectsRootFor(environmentId) !== null) { sourceItems.unshift({ kind: "action", value: `action:add-project:${environmentId}:new`, searchTerms: ["new project", "create", "empty", "repository", "git init"], title: "New project", description: "Start a new Git repository from a name", icon: , keepOpen: true, run: async () => { startNewProject(environmentId, environmentId); }, }); } const orderedSources: ReadonlyArray = [ "url", ...sortAddProjectProviderSources(readinessBySource), ]; for (const source of orderedSources) { const label = addProjectRemoteSourceLabel(source); const title = source === "url" ? "Git URL" : `${label} repository`; const description = source === "url" ? "Clone from a remote URL" : `Clone ${label} ${addProjectRemoteSourcePathHint(source)}`; const readiness = readinessBySource(source); const disabledHint = readiness.hint; const titleTrailingContent = readiness.ready ? undefined : ( { openSourceControlSettings(); }} > Setup Required } /> {disabledHint ?? "Open Settings -> Source control to configure this provider."} ); if (!readiness.ready) { sourceItems.push({ kind: "action", value: `action:add-project:${environmentId}:${source}:not-ready`, searchTerms: ["clone", "remote", "repository", "repo", "git", label, "setup required"], title, description, disabled: true, icon: remoteProjectSourceIcon(source, ITEM_ICON_CLASS), ...(titleTrailingContent ? { titleTrailingContent } : {}), run: async () => {}, }); continue; } sourceItems.push({ kind: "action", value: `action:add-project:${environmentId}:${source}`, searchTerms: ["clone", "remote", "repository", "repo", "git", label], title, description, icon: remoteProjectSourceIcon(source, ITEM_ICON_CLASS), ...(titleTrailingContent ? { titleTrailingContent } : {}), keepOpen: true, run: async () => { startAddProjectClone(environmentId, source); }, }); } return [{ value: `sources:${environmentId}`, label: "Sources", items: sourceItems }]; }; const startAddProjectSourceSelection = (environmentId: EnvironmentId): void => { const environment = environments.find((candidate) => candidate.environmentId === environmentId); if (!canCreateProjectInEnvironment(environment?.connection.phase)) { toastManager.add( stackedThreadToast({ type: "error", title: "Environment unavailable", description: `${environment?.label ?? "The selected environment"} is not connected.`, }), ); return; } setAddProjectEnvironmentId(environmentId); setAddProjectCloneFlow(null); pushPaletteView({ addonIcon: , groups: buildAddProjectSourceGroups( environmentId, buildPaletteRemoteSourceReadiness( browseEnvironmentId === environmentId ? sourceControlDiscovery.data : null, ), ), }); }; const buildEnvironmentItem = ( option: AddProjectEnvironmentOption, value: string, run: (environmentId: EnvironmentId) => void, ): CommandPaletteActionItem => ({ kind: "action", value, searchTerms: [option.label, option.environmentId, option.isPrimary ? "this device" : ""], title: option.label, description: option.isConnected ? option.isPrimary ? "This device" : option.environmentId : option.status, disabled: !option.isConnected, icon: , keepOpen: true, run: async () => { run(option.environmentId); }, }); const addProjectEnvironmentItems = addProjectEnvironmentOptions.map((option) => buildEnvironmentItem( option, `action:add-project:environment:${option.environmentId}`, startAddProjectSourceSelection, ), ); const newProjectEnvironmentOptions = addProjectEnvironmentOptions.filter( (option) => option.isConnected && newProjectsRootFor(option.environmentId) !== null, ); const addProjectEnvironmentGroups = [ { value: "environments", label: "Environments", items: addProjectEnvironmentItems, }, ]; const openConnectionSettings = () => { setOpen(false); void navigate({ to: "/settings/connections" }); }; const addProjectEntry = resolveAddProjectEntry( addProjectEnvironmentOptions, defaultAddProjectEnvironmentId, ); const openAddProjectFlow = () => { switch (addProjectEntry.kind) { case "connect-environment": openConnectionSettings(); return; case "choose-environment": pushPaletteView({ addonIcon: , groups: addProjectEnvironmentGroups, }); return; case "choose-source": startAddProjectSourceSelection(addProjectEntry.environmentId); return; } }; // The view an "Add project" intent opens to follows the environments as they // connect, so it resolves here instead of being fixed when the palette opened. const addProjectEntryGroups = (): ReadonlyArray => { switch (addProjectEntry.kind) { case "connect-environment": return [ { value: ADD_PROJECT_ENTRY_GROUP, label: "", items: [ { kind: "action", value: "action:add-project:connect-environment", searchTerms: ["connect", "environment", "connection", "settings"], title: "Connect an environment", description: "No connected environment can add a project", icon: , keepOpen: true, run: async () => { openConnectionSettings(); }, }, ], }, ]; case "choose-environment": return addProjectEnvironmentGroups; case "choose-source": return buildAddProjectSourceGroups( addProjectEntry.environmentId, buildPaletteRemoteSourceReadiness( browseEnvironmentId === addProjectEntry.environmentId ? sourceControlDiscovery.data : null, ), ); } }; const openNewProjectFlow = () => { const firstOption = newProjectEnvironmentOptions[0]; if (firstOption) startNewProject(firstOption.environmentId, null); }; const actionItems: Array = []; if (projects.length > 0) { const activeProjectTitle = projectPickerEntries.find((entry) => entry.isPreferred)?.group.displayName ?? (currentProjectId ? (projectTitleById.get(currentProjectId) ?? null) : null); if (activeProjectTitle) { actionItems.push({ kind: "action", value: "action:new-thread", searchTerms: ["new thread", "chat", "create", "draft"], title: ( <> New thread in {activeProjectTitle} ), searchTitle: `New thread in ${activeProjectTitle}`, icon: , shortcutCommand: "chat.newLocal", run: async () => { await startNewThreadFromContext({ activeDraftThread, activeThread: activeThread ?? undefined, defaultProjectRef, handleNewThread, }); }, }); } actionItems.push({ kind: "submenu", value: "action:new-thread-in", searchTerms: ["new thread", "project", "pick", "choose", "select"], title: "New thread in...", icon: , addonIcon: , groups: [{ value: NEW_THREAD_IN_GROUP, label: "Projects", items: projectThreadItems }], }); } if (scratchTargetEnvironmentId !== null) { actionItems.push({ kind: "action", value: "action:new-thread-without-project", searchTerms: ["new thread", "no project", "without project", "none", "chat"], title: "New thread without a project", icon: , shortcutCommand: "chat.newWithoutProject", run: () => startScratchThread(scratchTargetEnvironmentId), }); } if (activeThreadKey !== null && supportsThreadFind) { actionItems.push({ kind: "action", value: "find-current-thread", title: "Find in current thread", searchTerms: ["find", "search", "messages", "plans"], icon: , shortcutCommand: "chat.find", run: async () => { openThreadFind(activeThreadKey); }, }); } const promptLinkTarget = paletteRouteTarget ? paletteRouteTarget.kind === "server" ? paletteRouteTarget.threadRef : paletteRouteTarget.draftId : null; const promptLinkProjectRef = activeThread ?? activeDraftThread; const promptLinkInput = promptLinkTarget === null ? null : { target: promptLinkTarget, project: projects.find( (project) => project.environmentId === promptLinkProjectRef?.environmentId && project.id === promptLinkProjectRef.projectId, ), providers: (promptLinkProjectRef ? providersByEnvironment.get(promptLinkProjectRef.environmentId) : undefined) ?? [], threadModelSelection: activeThread?.modelSelection ?? null, }; const plainPromptLink = promptLinkInput ? composerPromptLink({ ...promptLinkInput, withContext: false }) : null; if (promptLinkInput !== null && plainPromptLink !== null) { actionItems.push({ kind: "action", value: "action:copy-prompt-link", searchTerms: ["copy", "share", "prompt", "link"], title: "Copy prompt link", icon: , run: () => copyPromptLink(plainPromptLink), }); if (promptLinkRepositoryOf(promptLinkInput.project) !== null) { const contextLink = composerPromptLink({ ...promptLinkInput, withContext: true }); if (contextLink !== null) { actionItems.push({ kind: "action", value: "action:copy-prompt-link-with-context", searchTerms: ["copy", "share", "prompt", "link", "repository", "model"], title: "Copy prompt link with repo and model", icon: , run: () => copyPromptLink(contextLink), }); } } } if (activeThreadReferenceCopyTarget !== null) { actionItems.push({ kind: "action", value: "action:copy-thread-reference", searchTerms: activeThreadReferenceCopyTarget.kind === "pull-request" ? ["copy", "pull request", "link", "reference"] : ["copy", "thread id", "reference"], title: activeThreadReferenceCopyTarget.kind === "pull-request" ? "Copy PR link" : "Copy thread ID", description: activeThreadReferenceCopyTarget.value, icon: , shortcutCommand: "thread.copyReference", run: copyActiveThreadReference, }); } if ( activeThread !== null && threadPullRequestLinkMode(activeThreadServerConfig?.environment.capabilities) !== "unsupported" ) { const threadRef = scopeThreadRef(activeThread.environmentId, activeThread.id); actionItems.push({ kind: "action", value: "action:link-pull-request", searchTerms: ["link", "pull request", "pr", "attach", "stack"], title: "Link pull request to thread", icon: , run: async () => { openLinkPullRequestDialog(threadRef); }, }); if (activeThreadServerConfig?.environment.capabilities.threadPullRequests === true) { actionItems.push({ kind: "action", value: "action:open-thread-pull-requests", searchTerms: ["pull requests", "linked", "stack", "prs"], title: "Show linked pull requests", disabled: visibleThreadPullRequests(activeThread.pullRequests).length === 0, icon: , run: async () => { useRightPanelStore.getState().open(threadRef, "pull-requests"); }, }); } } if (activeThread !== null && canShareActiveThreadWithGuest) { const threadRef = scopeThreadRef(activeThread.environmentId, activeThread.id); actionItems.push({ kind: "action", value: "action:share-thread-with-guest", searchTerms: ["share", "guest", "invite", "link", "teammate"], title: "Share with guest", icon: , run: async () => { openGuestShareDialog(threadRef); }, }); } if (activeThread !== null) { const thread = activeThread; actionItems.push({ kind: "action", value: "action:restart-agent-session", searchTerms: ["restart", "reset", "reload", "agent", "session", "skills", "plugins", "mcp"], title: "Restart agent session", icon: , run: async () => { const { environmentId } = thread; if (thread.runtime !== null) { const stopped = await stopThreadSession({ environmentId, input: { threadId: thread.id }, }); if (stopped._tag === "Failure") throw squashAtomCommandFailure(stopped); } toastManager.add({ type: "success", title: "Agent session will restart", description: "Your next message starts a fresh session.", }); const project = projectByKey.get(`${environmentId}:${thread.projectId}`); if (!project) return; const refreshed = await refreshProviders({ environmentId, input: { instanceId: thread.runtime?.providerInstanceId ?? thread.modelSelection.instanceId, cwd: thread.worktreePath ?? project.workspaceRoot, fresh: true, }, }); if (refreshed._tag === "Failure") throw squashAtomCommandFailure(refreshed); }, }); } actionItems.push({ kind: "action", value: "action:open-file-picker", searchTerms: ["go to file", "open file", "file picker", "find file", "quick open"], title: "Go to file", icon: , keepOpen: true, shortcutCommand: "filePicker.toggle", run: async () => { openChildSearch("files", { query, linkedThreadSearch }); }, }); actionItems.push({ kind: "action", value: "action:search-project-contents", searchTerms: ["search project", "find in files", "grep", "content search", "text search"], title: "Search project contents", icon: , keepOpen: true, shortcutCommand: "projectSearch.toggle", run: async () => { openChildSearch("content", { query, linkedThreadSearch }); }, }); if (newProjectEnvironmentOptions.length > 0) { actionItems.push({ kind: "action", value: "action:new-project", searchTerms: ["new project", "create project", "empty", "repository", "repo", "git init"], title: "New project", icon: , keepOpen: true, run: async () => { openNewProjectFlow(); }, }); } actionItems.push({ kind: "action", value: "action:add-project", searchTerms: [ "add project", "folder", "directory", "browse", "clone", "remote", "repository", "repo", "git", ...sourceControlClients.definitions.flatMap((definition) => definition.pickerLabel .toLowerCase() .split(/[^a-z0-9]+/u) .filter(Boolean), ), "url", "environment", ], title: "Add project", icon: , keepOpen: true, run: async () => { openAddProjectFlow(); }, }); if (wslAddProjectEnvironmentOption) { actionItems.push({ kind: "action", value: "action:add-project:wsl-folder", searchTerms: ["add project", "open", "wsl", "linux", "folder", "directory"], title: "Open WSL folder", description: wslAddProjectEnvironmentOption.label, icon: , keepOpen: true, run: async () => { await startAddProjectBrowse(wslAddProjectEnvironmentOption.environmentId); }, }); } actionItems.push(changeThemeItem); actionItems.push(changeAppearanceItem); actionItems.push(themeEditorItem); if ( environments.some( (environment) => environment.serverConfig?.environment.capabilities.pullRequests === true, ) ) { actionItems.push({ kind: "action", value: "action:pull-requests", searchTerms: [ ...new Set( sourceControlClients.definitions.flatMap((definition) => { const shortLabel = definition.changeRequest.shortLabel.toLowerCase(); return [ definition.changeRequest.plural, `${shortLabel}s`, shortLabel, ...definition.label.toLowerCase().split(/[^a-z0-9]+/u), ].filter(Boolean); }), ), "review", "merge", "branch", ], title: "Open pull requests", icon: , run: async () => { await navigate({ to: "/pull-requests", search: readPullRequestListPreferences() }); }, }); } actionItems.push({ kind: "action", value: "action:usage", searchTerms: ["usage", "use", "tokens", "cost", "spend", "limits", "stats", "analytics"], title: "Open usage", icon: , shortcutCommand: "usage.open", run: async () => { await navigate({ to: "/usage" }); }, }); actionItems.push({ kind: "action", value: "action:settings", searchTerms: ["settings", "preferences", "configuration", "keybindings"], title: "Open settings", icon: , run: async () => { await navigate({ to: "/settings" }); }, }); const contextualProjectGroup = (contextualProjectRef ? projectGroupByTargetKey.get( `${contextualProjectRef.environmentId}:${contextualProjectRef.projectId}`, ) : null) ?? projectGroups[0] ?? null; if (contextualProjectGroup) { actionItems.push({ kind: "action", value: "action:project-settings", searchTerms: [ "project", "settings", "name", "icon", "scripts", "model", "workspace", "grouping", "checkout", "remove", "t3.json", ], title: "Project settings", description: contextualProjectGroup.displayName, icon: , run: async () => { await navigate({ to: "/projects/$projectKey", params: { projectKey: contextualProjectGroup.projectKey }, }); }, }); } const rootGroups = buildRootGroups({ actionItems, recentThreadItems }); // Every setting goes to the palette's own matcher, so settings get the same // word starts and typo tolerance as threads. const settingsSearchItems: CommandPaletteActionItem[] = settingsForThisPlatform( availableSettingsSearchItems, ).map((item) => ({ kind: "action", value: `setting:${item.id}`, searchTerms: [item.title, SETTINGS_SECTION_LABELS[item.to], ...(item.searchTerms ?? [])], title: item.title, description: `Settings · ${SETTINGS_SECTION_LABELS[item.to]}`, ...(item.secondary ? { secondary: true } : {}), icon: , run: async () => { await navigate({ to: item.to, hash: item.targetId ?? item.id, replace: pathname === item.to, hashScrollIntoView: false, }); }, })); const sourceSelectionViewValue = addProjectEnvironmentId === null ? null : `sources:${addProjectEnvironmentId}`; const activeGroups = addProjectEnvironmentId !== null && currentView !== null && currentView.groups[0]?.value === sourceSelectionViewValue ? buildAddProjectSourceGroups( addProjectEnvironmentId, buildPaletteRemoteSourceReadiness(sourceControlDiscovery.data), ) : currentView?.groups[0]?.value === ADD_PROJECT_ENTRY_GROUP ? addProjectEntryGroups() : currentView?.groups[0]?.value === NEW_THREAD_IN_GROUP ? [{ value: NEW_THREAD_IN_GROUP, label: "Projects", items: projectThreadItems }] : currentView?.groups[0]?.value === "themes" ? changeThemeItem.groups : currentView?.groups[0]?.value === "appearance" ? changeAppearanceItem.groups : (currentView?.groups ?? rootGroups); const isLinkedThreadSearch = linkedThreadSearch?.linkedThreads !== undefined && deferredQuery === linkedThreadSearch.query; const filteredGroups = filterCommandPaletteGroups({ activeGroups, query: deferredQuery, isInSubmenu: currentView !== null, ...(deferredQuery.trim().length > 0 ? { allOptions: { rootGroups, nestedItems: buildNestedSearchItems(actionItems, deferredQuery), }, } : {}), projectSearchItems: projectSearchItems, settingsSearchItems, threadSearchItems: isLinkedThreadSearch && linkedThreadSearch?.linkedThreads ? buildLinkedThreadActionItems({ ...linkedThreadSearch.linkedThreads, query: linkedThreadSearch.query, icon: , runThread: async (thread) => { await navigate({ to: "/$environmentId/$threadId", params: buildThreadRouteParams(scopeThreadRef(thread.environmentId, thread.id)), }); }, }) : allThreadItems, threadMessageItems: isLinkedThreadSearch ? [] : threadMessageItems, now: searchNow, }); const handleAddProjectForEnvironment = async (input: { readonly environmentId: EnvironmentId; readonly rawCwd: string; readonly platform: string; readonly currentProjectCwd: string | null; }) => { const environment = environments.find( (candidate) => candidate.environmentId === input.environmentId, ); if (!canCreateProjectInEnvironment(environment?.connection.phase)) { toastManager.add( stackedThreadToast({ type: "error", title: "Environment unavailable", description: `${environment?.label ?? "The selected environment"} is not connected.`, }), ); return; } const rawCwd = input.rawCwd; if (isUnsupportedWindowsProjectPath(rawCwd.trim(), input.platform)) { toastManager.add( stackedThreadToast({ type: "error", title: "Failed to add project", description: "Windows-style paths are only supported on Windows.", }), ); return; } if (isExplicitRelativeProjectPath(rawCwd.trim()) && !input.currentProjectCwd) { toastManager.add( stackedThreadToast({ type: "error", title: "Failed to add project", description: "Relative paths require an active project.", }), ); return; } const cwd = resolveProjectPathForDispatch(rawCwd, input.currentProjectCwd); if (cwd.length === 0) return; const existing = findProjectByPath( projects.filter((project) => project.environmentId === input.environmentId), cwd, ); if (existing) { // Adding an archived project's folder again brings the project back. if (existing.archivedAt != null) { void updateProject({ environmentId: existing.environmentId, input: { projectId: existing.id, archived: false }, }); } const latestThread = getLatestThreadForProject( threads.filter((thread) => thread.environmentId === existing.environmentId), existing.id, clientSettings.sidebarThreadSortOrder, ); if (latestThread && latestThread.settledOverride !== "settled") { await navigate({ to: "/$environmentId/$threadId", params: buildThreadRouteParams( scopeThreadRef(latestThread.environmentId, latestThread.id), ), }); } else { const navigationResult = await settlePromise(() => handleNewThread(scopeProjectRef(existing.environmentId, existing.id)), ); if (navigationResult._tag === "Failure") { const error = squashAtomCommandFailure(navigationResult); toastManager.add( stackedThreadToast({ type: "error", title: "Failed to open project", description: error instanceof Error ? error.message : "An error occurred.", }), ); return; } } setOpen(false); return; } if (!readEnvironmentScope(input.environmentId, AuthOrchestrationOperateScope)) { toastManager.add( stackedThreadToast({ type: "error", title: "Cannot add project", description: "This connection cannot add projects.", }), ); return; } const projectId = newProjectId(); const createResult = await createProject({ environmentId: input.environmentId, input: { projectId, title: inferProjectTitleFromPath(cwd), workspaceRoot: cwd, createWorkspaceRootIfMissing: true, defaultModelSelection: null, }, }); if (createResult._tag === "Failure") { if (!isAtomCommandInterrupted(createResult)) { const error = squashAtomCommandFailure(createResult); toastManager.add( stackedThreadToast({ type: "error", title: "Failed to add project", description: error instanceof Error ? error.message : "An error occurred.", }), ); } return; } const navigationResult = await settlePromise(() => handleNewThread(scopeProjectRef(input.environmentId, projectId)), ); if (navigationResult._tag === "Failure") { const error = squashAtomCommandFailure(navigationResult); toastManager.add( stackedThreadToast({ type: "error", title: "Failed to add project", description: error instanceof Error ? error.message : "An error occurred.", }), ); return; } setOpen(false); }; const handleAddProject = async (rawCwd: string) => { if (!browseEnvironmentId) return; await handleAddProjectForEnvironment({ environmentId: browseEnvironmentId, rawCwd, platform: browseEnvironmentPlatform, currentProjectCwd: currentProjectCwdForBrowse, }); }; const newProjectPublishTargets = newProjectFlow === null ? [] : getNewProjectPublishTargets(sourceControlDiscovery.data ?? null); const newProjectPublishTarget = getNewProjectPublishTarget( newProjectPublishTargets, newProjectPublishKind, ); const newProjectName = query.trim(); const canSubmitNewProject = newProjectFlow !== null && newProjectName.length > 0 && !isCreatingNewProject && canCreateProjectInEnvironment(browseEnvironment?.connection.phase); async function submitNewProject(): Promise { if (newProjectFlow === null || !canSubmitNewProject || newProjectSubmittingRef.current) { return; } newProjectSubmittingRef.current = true; setIsCreatingNewProject(true); const created = await settlePromise(() => createNewProject({ environmentId: newProjectFlow.environmentId, name: newProjectName, publishTo: newProjectPublishes ? newProjectPublishTarget : null, }), ); newProjectSubmittingRef.current = false; setIsCreatingNewProject(false); if (created._tag === "Failure") throw squashAtomCommandFailure(created); if (created.value) setOpen(false); } function getDefaultCloneParentPath(environmentId: EnvironmentId): string { return getAddProjectInitialQueryForEnvironment(environmentId); } async function submitAddProjectCloneFlow(destinationPathInput?: string): Promise { if (!addProjectCloneFlow) { return; } if (!canCreateProjectInEnvironment(browseEnvironment?.connection.phase)) { toastManager.add( stackedThreadToast({ type: "error", title: "Environment unavailable", description: `${browseEnvironment?.label ?? "The selected environment"} is not connected.`, }), ); return; } if (addProjectCloneFlow.step === "repository") { const rawRepository = query.trim(); if (rawRepository.length === 0 || isRemoteProjectLookingUp) { return; } const provider = addProjectRemoteSourceProvider(addProjectCloneFlow.source); if (!provider) { const destinationPath = getCloneDestinationPath( getDefaultCloneParentPath(addProjectCloneFlow.environmentId), getCloneDirectoryName(rawRepository), ); setAddProjectCloneFlow({ step: "confirm", environmentId: addProjectCloneFlow.environmentId, source: addProjectCloneFlow.source, repositoryInput: rawRepository, repository: null, remoteUrl: normalizePastedCloneUrl(rawRepository), }); setHighlightedItemValue(null); setQuery(destinationPath); setBrowseGeneration((generation) => generation + 1); return; } setIsRemoteProjectLookingUp(true); const lookupGeneration = ++cloneLookupGeneration.current; const lookupResult = await lookupRepository({ environmentId: addProjectCloneFlow.environmentId, input: { provider, repository: rawRepository, }, }); if (lookupGeneration !== cloneLookupGeneration.current) return; setIsRemoteProjectLookingUp(false); if (lookupResult._tag === "Failure") { if (!isAtomCommandInterrupted(lookupResult)) { toastManager.add( stackedThreadToast({ type: "error", title: "Repository lookup failed", description: errorMessage(squashAtomCommandFailure(lookupResult)), }), ); } return; } const repository = lookupResult.value; const destinationPath = getCloneDestinationPath( getDefaultCloneParentPath(addProjectCloneFlow.environmentId), getCloneDirectoryName(repository.nameWithOwner), ); setAddProjectCloneFlow({ step: "confirm", environmentId: addProjectCloneFlow.environmentId, source: addProjectCloneFlow.source, repositoryInput: rawRepository, repository, remoteUrl: getDefaultCloneUrl(repository), }); setHighlightedItemValue(null); setQuery(destinationPath); setBrowseGeneration((generation) => generation + 1); return; } const rawDestination = (destinationPathInput ?? query).trim(); if ( !readEnvironmentScope(addProjectCloneFlow.environmentId, AuthSourceControlWriteScope) || !readEnvironmentScope(addProjectCloneFlow.environmentId, AuthOrchestrationOperateScope) ) { toastManager.add( stackedThreadToast({ type: "error", title: "Clone unavailable", description: "This connection needs permission to write source control and add projects.", }), ); return; } if (rawDestination.length === 0 || isRemoteProjectCloning) return; if (isUnsupportedWindowsProjectPath(rawDestination, browseEnvironmentPlatform)) { toastManager.add( stackedThreadToast({ type: "error", title: "Clone failed", description: "Windows-style paths are only supported on Windows.", }), ); return; } if (isExplicitRelativeProjectPath(rawDestination) && !currentProjectCwdForBrowse) { toastManager.add( stackedThreadToast({ type: "error", title: "Clone failed", description: "Relative paths require an active project.", }), ); return; } const destinationPath = resolveProjectPathForDispatch( rawDestination, currentProjectCwdForBrowse, ); if (destinationPath.length === 0) { return; } // Older servers lack clone tracking; wait for the blocking clone before adding the project. if (browseEnvironment?.serverConfig?.environment.capabilities.projectCloneTracking !== true) { setIsRemoteProjectCloning(true); const cloneResult = await cloneRepository({ environmentId: addProjectCloneFlow.environmentId, input: { remoteUrl: addProjectCloneFlow.remoteUrl, destinationPath, }, }); setIsRemoteProjectCloning(false); if (cloneResult._tag === "Failure") { if (!isAtomCommandInterrupted(cloneResult)) { toastManager.add( stackedThreadToast({ type: "error", title: "Clone failed", description: errorMessage(squashAtomCommandFailure(cloneResult)), }), ); } return; } await handleAddProject(cloneResult.value.cwd); return; } const projectId = newProjectId(); setIsRemoteProjectCloning(true); const startResult = await startProjectClone({ environmentId: addProjectCloneFlow.environmentId, input: { projectId, title: inferProjectTitleFromPath(destinationPath), // oxlint-disable-next-line react/purity -- runs in an event handler (oxc#27309) createdAt: new Date().toISOString(), remoteUrl: addProjectCloneFlow.remoteUrl, destinationPath, }, }); setIsRemoteProjectCloning(false); if (startResult._tag === "Failure") { if (!isAtomCommandInterrupted(startResult)) { toastManager.add( stackedThreadToast({ type: "error", title: "Clone failed", description: errorMessage(squashAtomCommandFailure(startResult)), }), ); } return; } setOpen(false); const projectRef = scopeProjectRef(addProjectCloneFlow.environmentId, projectId); await waitForProject(projectRef, 3_000).catch(() => null); const navigationResult = await settlePromise(() => handleNewThread(projectRef)); if (navigationResult._tag === "Failure") { const error = squashAtomCommandFailure(navigationResult); toastManager.add( stackedThreadToast({ type: "error", title: "Failed to open project", description: error instanceof Error ? error.message : "An error occurred.", }), ); } } const browseTo = async (name: string): Promise => { const nextQuery = pinnedCloneDirectoryName ? getCloneDestinationBrowsePath({ browseDirectoryPath: browsePath.directoryPath, selectedDirectoryName: name, cloneDirectoryName: pinnedCloneDirectoryName, caseSensitive: !isWindowsPlatform(browseEnvironmentPlatform), }) : appendBrowsePathSegment(query, name); await browseNavigation.run( () => prefetchBrowsePath(getBrowseDirectoryPath(nextQuery)), () => { setHighlightedItemValue(null); setQuery(nextQuery); setBrowseGeneration((generation) => generation + 1); }, ); }; const browseUp = async (): Promise => { const parentPath = browsePath.parentPath; if (parentPath === null) { return; } const nextQuery = getCloneDestinationPath(parentPath, pinnedCloneDirectoryName); await browseNavigation.run( () => prefetchBrowsePath(parentPath), () => { setHighlightedItemValue(null); setQuery(nextQuery); setBrowseGeneration((generation) => generation + 1); }, ); }; const resolvedAddProjectPath = hasTrailingPathSeparator(query) ? (browseResult?.parentPath ?? query.trim()) : (exactBrowseEntry?.fullPath ?? query.trim()); const canBrowseUp = !relativePathNeedsActiveProject && browsePath.canBrowseUp; const browseGroups = buildBrowseGroups({ browseEntries: visibleBrowseEntries, browseQuery: query, canBrowseUp, upIcon: , directoryIcon: , browseUp, browseTo, }); const cloneDestinationBrowseGroups = browseGroups.map((group) => group.value === "directories" ? { ...group, label: "Select where to clone" } : group, ); const newProjectsRoot = newProjectFlow ? newProjectsRootFor(newProjectFlow.environmentId) : null; const newProjectPathPreview = newProjectsRoot === null ? null : getNewProjectPathPreview(newProjectsRoot, newProjectName); const newProjectPublishToggleValue = "new-project:publish"; const showExistingProjectSources = (environmentId: EnvironmentId) => { const sourcesEnvironmentId = newProjectFlow?.sourcesEnvironmentId ?? null; if (sourcesEnvironmentId === environmentId) { popView(); return; } if (sourcesEnvironmentId !== null) { dispatchSearch({ type: "back" }); } popView(); startAddProjectSourceSelection(environmentId); }; const switchNewProjectEnvironment = (environmentId: EnvironmentId) => { setNewProjectFlow((flow) => (flow === null ? flow : { ...flow, environmentId })); setAddProjectEnvironmentId(environmentId); }; const selectedNewProjectEnvironment = newProjectFlow === null ? undefined : newProjectEnvironmentOptions.find( (option) => option.environmentId === newProjectFlow.environmentId, ); const showNewProjectMachines = newProjectFlow !== null && (newProjectEnvironmentOptions.length > 1 || (selectedNewProjectEnvironment === undefined && newProjectEnvironmentOptions.length > 0)); const newProjectEnvironmentLabel = showNewProjectMachines ? (selectedNewProjectEnvironment?.label ?? environmentLabelById.get(newProjectFlow.environmentId) ?? null) : null; const newProjectMachineGroup: CommandPaletteView["groups"][number] | null = !showNewProjectMachines || newProjectFlow === null ? null : { value: "new-project-machines", label: "Environments", items: newProjectEnvironmentOptions.map((option) => ({ ...buildEnvironmentItem( option, `new-project:environment:${option.environmentId}`, switchNewProjectEnvironment, ), ...(isCreatingNewProject ? { disabled: true } : {}), ...(option.environmentId === newProjectFlow.environmentId ? { titleTrailingContent: ( ), } : {}), })), }; const newProjectExistingGroup: CommandPaletteView["groups"][number] | null = newProjectFlow === null ? null : { value: "new-project-existing", label: "", items: [ { kind: "action", value: "new-project:existing", searchTerms: [], title: "Add existing project", description: "Open a folder or clone a repository", icon: , titleTrailingContent: ( ), keepOpen: true, run: async () => { showExistingProjectSources(newProjectFlow.environmentId); }, }, ], }; const newProjectOptionGroups: CommandPaletteView["groups"] = newProjectPublishTarget === null || newProjectPathPreview === null ? [] : [ { value: "new-project-options", label: "Options", items: [ { kind: "action", value: newProjectPublishToggleValue, searchTerms: [], title: newProjectPublishTargets.length > 1 ? "Create private repository" : `Create private repository on ${newProjectPublishTarget.definition.label}`, description: newProjectName.length > 0 ? getNewProjectRepository(newProjectPublishTarget, newProjectPathPreview) : (newProjectPublishTarget.owner ?? `Your ${newProjectPublishTarget.definition.label} account`), icon: sourceControlHostIcon(newProjectPublishTarget.definition, ITEM_ICON_CLASS), titleTrailingContent: ( {newProjectPublishTargets.length > 1 ? ( ) : null} ), ...(isCreatingNewProject ? { disabled: true } : {}), keepOpen: true, run: async () => { setNewProjectPublishes((publishes) => !publishes); }, }, ], }, ]; let displayedGroups: CommandPaletteView["groups"] = filteredGroups; if (newProjectFlow !== null) { displayedGroups = [ ...(newProjectMachineGroup ? [newProjectMachineGroup] : []), ...newProjectOptionGroups, ...(newProjectExistingGroup ? [newProjectExistingGroup] : []), ]; } else if (addProjectCloneFlow?.step === "repository") { displayedGroups = []; } else if (addProjectCloneFlow?.step === "confirm") { displayedGroups = relativePathNeedsActiveProject ? [] : cloneDestinationBrowseGroups; } else if (isBrowsing) { displayedGroups = relativePathNeedsActiveProject ? [] : browseGroups; } const resultRows = buildCommandPaletteRows(displayedGroups); const autoHighlightsFirstRow = !isBrowsing && !isRemoteProjectCloneFlow && newProjectFlow === null; const inputPlaceholder = newProjectFlow !== null ? "Project name" : (remoteProjectInputPlaceholder(addProjectCloneFlow) ?? getCommandPaletteInputPlaceholder(paletteMode)); const isSubmenu = paletteMode === "submenu" || paletteMode === "submenu-browse"; const hasHighlightedBrowseItem = highlightedItemValue?.startsWith("browse:") ?? false; const canWriteSourceControl = useEnvironmentScope( addProjectCloneFlow?.environmentId ?? null, AuthSourceControlWriteScope, ); const canCreateClonedProject = useEnvironmentScope( addProjectCloneFlow?.environmentId ?? null, AuthOrchestrationOperateScope, ); const canCloneProject = canWriteSourceControl && canCreateClonedProject; const isCloneDestinationStep = addProjectCloneFlow?.step === "confirm"; const canSubmitBrowsePath = isBrowsing && !relativePathNeedsActiveProject && canCreateProject && (!isCloneDestinationStep || canCloneProject) && canCreateProjectInEnvironment(browseEnvironment?.connection.phase); const willCreateProjectPath = canSubmitBrowsePath && !isBrowsePending && browseAccessError === null && query.trim().length > 0 && !hasHighlightedBrowseItem && (hasTrailingPathSeparator(query) ? !browseResult : exactBrowseEntry === null); const useMetaForMod = isMacPlatform(navigator.platform); const submitModifierLabel = useMetaForMod ? "\u2318" : "Ctrl"; const submitActionLabel = isCloneDestinationStep ? willCreateProjectPath ? "Create & Clone" : "Clone" : willCreateProjectPath ? "Create & Add" : "Add"; const addShortcutLabel = hasHighlightedBrowseItem ? `${submitModifierLabel} Enter` : "Enter"; const hasHighlightedNewProjectItem = newProjectFlow !== null && highlightedItemValue !== null; const createShortcutLabel = hasHighlightedNewProjectItem ? `${submitModifierLabel} Enter` : "Enter"; const remoteProjectButtonLabel = addProjectCloneFlow ? addProjectCloneFlow.source === "url" ? "Continue" : "Lookup" : null; const isRemoteProjectPending = isRemoteProjectLookingUp || isRemoteProjectCloning; const canSubmitRemoteProjectFlow = addProjectCloneFlow?.step === "repository" && query.trim().length > 0 && canCreateProjectInEnvironment(browseEnvironment?.connection.phase) && !isRemoteProjectPending; const fileManagerName = getLocalFileManagerName(navigator.platform); const canOpenProjectFromFileManager = isBrowsing && browseEnvironmentId !== null && (browseEnvironmentId === primaryEnvironmentId || (browseEnvironmentIsDesktopLocal && browseDesktopInstanceId !== null)) && typeof window !== "undefined" && window.desktopBridge !== undefined; const fileManagerInitialPath = (() => { if (!canOpenProjectFromFileManager) { return undefined; } const trimmedQuery = query.trim(); if (trimmedQuery.length === 0) { return undefined; } const initialPath = hasTrailingPathSeparator(query) ? (browseResult?.parentPath ?? trimmedQuery) : browseDirectoryPath || trimmedQuery; const resolvedPath = resolveProjectPathForDispatch(initialPath, currentProjectCwdForBrowse); return resolvedPath.length > 0 ? resolvedPath : undefined; })(); function isPrimaryModifierPressed(event: KeyboardEvent): boolean { return useMetaForMod ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey; } function handleKeyDown(event: KeyboardEvent): void { const command = resolveShortcutCommand(event, keybindings, { platform: navigator.platform, context: { modelPickerOpen: false, editableFocus: true }, }); // Global new-thread handlers ignore shortcuts while the palette is open. const isJump = threadJumpIndexFromCommand(command ?? "") !== null; const isNewThread = !event.nativeEvent.isComposing && event.keyCode !== 229 && (command === "chat.new" || command === "chat.newLocal" || command === "chat.newWithoutProject"); const matchingItem = isJump || isNewThread ? displayedGroups .flatMap((group) => group.items) .find((item) => item.shortcutCommand === command) : undefined; if (isJump || matchingItem) { (event as typeof event & { preventBaseUIHandler?: () => void }).preventBaseUIHandler?.(); event.preventDefault(); event.stopPropagation(); if (matchingItem) { executeItem(matchingItem); } return; } if (command === "thread.copyReference") { event.preventDefault(); event.stopPropagation(); if (activeThreadReferenceCopyTarget === null) return; setOpen(false); void copyActiveThreadReference(); return; } // ChatView ignores shortcuts while the palette is open, so intercept find here. if (command === "chat.find" && activeThreadKey !== null && !event.nativeEvent.isComposing) { event.preventDefault(); event.stopPropagation(); setOpen(false); if (supportsThreadFind) openThreadFind(activeThreadKey); else toastManager.add({ id: "thread-find-unavailable", title: "Thread search is unavailable on this server.", description: "Update the server to enable it.", }); return; } if (addProjectCloneFlow?.step === "repository" && event.key === "Enter") { event.preventDefault(); void submitAddProjectCloneFlow(); return; } if ( newProjectFlow !== null && event.key === "Enter" && (!hasHighlightedNewProjectItem || isPrimaryModifierPressed(event)) && // Enter that confirms an IME composition is part of typing the name. !event.nativeEvent.isComposing && event.keyCode !== 229 ) { // Base UI would otherwise also click the highlighted row. (event as typeof event & { preventBaseUIHandler?: () => void }).preventBaseUIHandler?.(); event.preventDefault(); void submitNewProject(); return; } const shouldSubmitBrowsePath = canSubmitBrowsePath && event.key === "Enter" && (!hasHighlightedBrowseItem || isPrimaryModifierPressed(event)); if (shouldSubmitBrowsePath) { event.preventDefault(); if (isCloneDestinationStep) { void submitAddProjectCloneFlow(resolvedAddProjectPath); } else { void handleAddProject(resolvedAddProjectPath); } return; } if (event.key === "Backspace" && query === "" && isSubmenu) { event.preventDefault(); popView(); return; } if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) return; const firstItemValue = autoHighlightsFirstRow ? resultRows.itemValues[0] : undefined; if ( event.key === "ArrowDown" && highlightClearedRef.current && firstItemValue && !event.nativeEvent.isComposing ) { (event as typeof event & { preventBaseUIHandler?: () => void }).preventBaseUIHandler?.(); event.preventDefault(); highlightClearedRef.current = false; setHighlightedItemValue(firstItemValue); scrollCommandPaletteRowIntoView( resultListRef.current, resultRows.rowIndexByItemIndex[0] ?? 0, ); return; } // Virtualization may unmount the highlighted row; handle Enter directly instead of Base UI's click. if (event.key === "Enter" && !event.nativeEvent.isComposing && event.keyCode !== 229) { const highlightedItem = findHighlightedCommandPaletteItem( displayedGroups, highlightedItemValue ?? firstItemValue ?? null, ); if (highlightedItem) { (event as typeof event & { preventBaseUIHandler?: () => void }).preventBaseUIHandler?.(); event.preventDefault(); event.stopPropagation(); executeItem(highlightedItem); } } } function executeItem(item: CommandPaletteActionItem | CommandPaletteSubmenuItem): void { if (item.disabled) { return; } if (item.kind === "submenu") { pushPaletteView(item); return; } if (!item.keepOpen) { setOpen(false); } void item.run().catch((error: unknown) => { toastManager.add( stackedThreadToast({ type: "error", title: "Unable to run command", description: error instanceof Error ? error.message : "An unexpected error occurred.", }), ); }); } const handleOpenProjectFromFileManager = async () => { if (!canOpenProjectFromFileManager || isPickingProjectFolder) { return; } const api = readLocalApi(); if (!api) { return; } setIsPickingProjectFolder(true); let pickedPath: string | null = null; let desktopWslState: DesktopWslState | null = null; try { desktopWslState = browseEnvironmentId === primaryEnvironmentId && browseEnvironmentPlatform === "Linux" ? ((await window.desktopBridge?.getWslState().catch(() => null)) ?? null) : null; // WSL-only primaries need their pool id from desktop settings; the native picker converts WSL UNC // paths. const pickerTargetEnvironmentId = resolveProjectPickerTarget({ browseEnvironmentId, primaryEnvironmentId, desktopInstanceId: browseDesktopInstanceId, wslConfiguration: desktopWslState, }); const pickerOptions = { ...(fileManagerInitialPath ? { initialPath: fileManagerInitialPath } : {}), ...(pickerTargetEnvironmentId ? { targetEnvironmentId: pickerTargetEnvironmentId } : {}), }; pickedPath = await api.dialogs.pickFolder( Object.keys(pickerOptions).length > 0 ? pickerOptions : undefined, ); } catch { setIsPickingProjectFolder(false); return; } setIsPickingProjectFolder(false); if (!pickedPath) { return; } if (parseWslUncPath(pickedPath)) { if (desktopWslState === null) { desktopWslState = (await window.desktopBridge?.getWslState().catch(() => null)) ?? null; } let primaryRunningDistro: string | null = null; try { primaryRunningDistro = window.desktopBridge ?.getLocalEnvironmentBootstraps() .find((bootstrap) => bootstrap.id === PRIMARY_LOCAL_ENVIRONMENT_ID)?.runningDistro ?? null; } catch {} const selection = resolveWslProjectSelection( pickedPath, applyWslEnvironmentConfiguration( environments.flatMap((environment) => { const backendId = desktopLocalBackendId(environment.entry.target); if (!backendId) { return []; } const bootstrap = desktopLocalBootstraps.find( (candidate) => candidate.httpBaseUrl === environment.displayUrl, ); const runningDistro = bootstrap?.runningDistro ?? null; return [{ environmentId: environment.environmentId, backendId, runningDistro }]; }), primaryEnvironmentId, desktopWslState ?? null, primaryRunningDistro, ), ); if (!selection) { toastManager.add( stackedThreadToast({ type: "error", title: "Could not add WSL project", description: "Start the matching WSL backend, then choose the folder again.", }), ); return; } await handleAddProjectForEnvironment({ environmentId: selection.environmentId, rawCwd: selection.linuxPath, platform: "Linux", currentProjectCwd: null, }); return; } await handleAddProject(pickedPath); }; const inputAccessory = newProjectFlow !== null ? ( { void submitNewProject(); }} /> ) : addProjectCloneFlow?.step === "repository" ? ( { void submitAddProjectCloneFlow(); }} /> ) : isBrowsing ? ( { if (relativePathNeedsActiveProject) { return; } if (isCloneDestinationStep) { void submitAddProjectCloneFlow(resolvedAddProjectPath); } else { void handleAddProject(resolvedAddProjectPath); } }} /> ) : null; const footerActionLabel = newProjectFlow !== null ? highlightedItemValue === null ? "Create" : highlightedItemValue === newProjectPublishToggleValue ? "Toggle" : "Select" : addProjectCloneFlow?.step === "repository" ? (remoteProjectButtonLabel ?? "Continue") : !canSubmitBrowsePath || hasHighlightedBrowseItem ? "Select" : undefined; const footerTrailing = canOpenProjectFromFileManager ? ( { void handleOpenProjectFromFileManager(); }} > {`Open in ${fileManagerName}`} ) : isThreadSearchPending && !isLinkedThreadSearch && !isBrowsing ? ( // In the footer, so the list keeps its height while message results may // still arrive below it. Searching messages… ) : null; return ( ), } : isBrowsing ? { startAddon: } : {}), onKeyDown: handleKeyDown, }} mode="none" items={resultRows.itemValues} virtualized onItemHighlighted={(value, eventDetails) => { if (eventDetails.reason === "none" && highlightClearedRef.current) return; highlightClearedRef.current = false; setHighlightedItemValue(typeof value === "string" ? value : null); const rowIndex = resultRows.rowIndexByItemIndex[eventDetails.index]; if (eventDetails.reason === "keyboard" && rowIndex !== undefined) { scrollCommandPaletteRowIntoView(resultListRef.current, rowIndex); } }} onValueChange={handleQueryChange} showBackHint={isSubmenu} value={query} > {newProjectPathPreview !== null ? ( ) : null} {addProjectCloneFlow?.step === "confirm" ? ( ) : null} {browseAccessError ? (
{browseAccessError}
) : isBrowsePending && browseResult === null ? (
Loading folders...
) : null}
); }