"use client"; import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime"; import type { ProjectId } from "@t3tools/contracts"; import { useNavigate, useParams } from "@tanstack/react-router"; import { ArrowDownIcon, ArrowLeftIcon, ArrowUpIcon, MessageSquareIcon, SettingsIcon, SquarePenIcon, } from "lucide-react"; import { useDeferredValue, useEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode, } from "react"; import { useShallow } from "zustand/react/shallow"; import { useCommandPaletteStore } from "../commandPaletteStore"; import { useHandleNewThread } from "../hooks/useHandleNewThread"; import { useSettings } from "../hooks/useSettings"; import { startNewThreadInProjectFromContext, startNewThreadFromContext, } from "../lib/chatThreadActions"; import { isTerminalFocused } from "../lib/terminalFocus"; import { getLatestThreadForProject } from "../lib/threadSort"; import { cn } from "../lib/utils"; import { selectProjectsAcrossEnvironments, selectSidebarThreadsAcrossEnvironments, useStore, } from "../store"; import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; import { buildThreadRouteParams, resolveThreadRouteTarget } from "../threadRoutes"; import { ADDON_ICON_CLASS, buildProjectActionItems, buildRootGroups, buildThreadActionItems, type CommandPaletteActionItem, type CommandPaletteSubmenuItem, type CommandPaletteView, filterCommandPaletteGroups, getCommandPaletteInputPlaceholder, getCommandPaletteMode, ITEM_ICON_CLASS, RECENT_THREAD_LIMIT, } from "./CommandPalette.logic"; import { CommandPaletteResults } from "./CommandPaletteResults"; import { ProjectFavicon } from "./ProjectFavicon"; import { useServerKeybindings } from "../rpc/serverState"; import { resolveShortcutCommand } from "../keybindings"; import { Command, CommandDialog, CommandDialogPopup, CommandFooter, CommandInput, CommandPanel, } from "./ui/command"; import { Kbd, KbdGroup } from "./ui/kbd"; import { toastManager } from "./ui/toast"; import { ComposerHandleContext, useComposerHandleContext } from "../composerHandleContext"; import type { ChatComposerHandle } from "./chat/ChatComposer"; 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(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 ( {children} ); } function CommandPaletteDialog() { const open = useCommandPaletteStore((store) => store.open); const setOpen = useCommandPaletteStore((store) => store.setOpen); useEffect(() => { return () => { setOpen(false); }; }, [setOpen]); if (!open) { return null; } return ; } function OpenCommandPaletteDialog() { const navigate = useNavigate(); const setOpen = useCommandPaletteStore((store) => store.setOpen); const composerHandleRef = useComposerHandleContext(); const [query, setQuery] = useState(""); const deferredQuery = useDeferredValue(query); const isActionsOnly = deferredQuery.startsWith(">"); 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([]); const currentView = viewStack.at(-1) ?? null; const paletteMode = getCommandPaletteMode({ currentView }); const projectTitleById = useMemo( () => new Map(projects.map((project) => [project.id, project.name])), [projects], ); const activeThreadId = activeThread?.id; const currentProjectId = activeThread?.projectId ?? activeDraftThread?.projectId ?? null; 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) => ( ), runProject: openProjectFromSearch, }), [openProjectFromSearch, projects], ); const projectThreadItems = useMemo( () => buildProjectActionItems({ projects, valuePrefix: "new-thread-in", icon: (project) => ( ), 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( () => buildThreadActionItems({ threads, ...(activeThreadId ? { activeThreadId } : {}), projectTitleById, sortOrder: settings.sidebarThreadSortOrder, icon: , runThread: async (thread) => { await navigate({ to: "/$environmentId/$threadId", params: buildThreadRouteParams(scopeThreadRef(thread.environmentId, thread.id)), }); }, }), [activeThreadId, navigate, projectTitleById, settings.sidebarThreadSortOrder, threads], ); const recentThreadItems = allThreadItems.slice(0, RECENT_THREAD_LIMIT); function pushView(item: CommandPaletteSubmenuItem): void { setViewStack((previousViews) => [ ...previousViews, { addonIcon: item.addonIcon, groups: item.groups, ...(item.initialQuery ? { initialQuery: item.initialQuery } : {}), }, ]); setQuery(item.initialQuery ?? ""); } function popView(): void { setViewStack((previousViews) => previousViews.slice(0, -1)); setQuery(""); } function handleQueryChange(nextQuery: string): void { setQuery(nextQuery); if (nextQuery === "" && currentView?.initialQuery) { popView(); } } const actionItems: Array = []; if (projects.length > 0) { const activeProjectTitle = 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} ), icon: , shortcutCommand: "chat.new", run: async () => { await startNewThreadFromContext({ activeDraftThread, activeThread, defaultProjectRef, defaultThreadEnvMode: settings.defaultThreadEnvMode, 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: "projects", label: "Projects", items: projectThreadItems }], }); } actionItems.push({ kind: "action", value: "action:settings", searchTerms: ["settings", "preferences", "configuration", "keybindings"], title: "Open settings", icon: , run: async () => { await navigate({ to: "/settings" }); }, }); const rootGroups = buildRootGroups({ actionItems, recentThreadItems }); const activeGroups = currentView ? currentView.groups : rootGroups; const displayedGroups = filterCommandPaletteGroups({ activeGroups, query: deferredQuery, isInSubmenu: currentView !== null, projectSearchItems: projectSearchItems, threadSearchItems: allThreadItems, }); const inputPlaceholder = getCommandPaletteInputPlaceholder(paletteMode); const isSubmenu = paletteMode === "submenu"; function handleKeyDown(event: KeyboardEvent): void { if (event.key === "Backspace" && query === "" && isSubmenu) { event.preventDefault(); popView(); } } function executeItem(item: CommandPaletteActionItem | CommandPaletteSubmenuItem): void { if (item.kind === "submenu") { pushView(item); return; } if (!item.keepOpen) { setOpen(false); } void item.run().catch((error: unknown) => { toastManager.add({ type: "error", title: "Unable to run command", description: error instanceof Error ? error.message : "An unexpected error occurred.", }); }); } return ( { composerHandleRef?.current?.focusAtEnd(); return false; }} > ), } : {})} onKeyDown={handleKeyDown} />
Navigate Enter Select {isSubmenu ? ( Backspace Back ) : null} Esc Close
); }