"use client"; import type { Dispatch, SetStateAction } from "react"; import { formatDistanceToNow } from "date-fns"; import { IconArchive, IconArrowRight, IconHammer, IconLoader2 } from "@tabler/icons-react"; import { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandShortcut, } from "@kandev/ui/command"; import { Kbd, KbdGroup } from "@kandev/ui/kbd"; import { Badge } from "@kandev/ui/badge"; import type { CommandPanelMode, CommandItem as CommandItemType } from "@/lib/commands/types"; import { formatShortcut } from "@/lib/keyboard/utils"; import { getShortcut } from "@/lib/keyboard/shortcut-overrides"; import { useAppStore } from "@/components/state-provider"; import type { Task } from "@/lib/types/http"; import { FileIcon } from "@/components/ui/file-icon"; const ARCHIVED_STATES = new Set(["COMPLETED", "CANCELLED", "FAILED"]); export const MODE_COMMANDS: CommandPanelMode = "commands"; export const MODE_SEARCH_FILES: CommandPanelMode = "search-files"; const STEP_COLOR_MAP: Record = { "bg-slate-500": "#64748b", "bg-red-500": "#ef4444", "bg-orange-500": "#f97316", "bg-yellow-500": "#eab308", "bg-green-500": "#22c55e", "bg-cyan-500": "#06b6d4", "bg-blue-500": "#3b82f6", "bg-indigo-500": "#6366f1", "bg-purple-500": "#a855f7", }; function getFileName(filePath: string) { return filePath.split("/").pop() ?? filePath; } function getCommandValue(cmd: CommandItemType) { return cmd.id + " " + cmd.label + " " + (cmd.keywords?.join(" ") ?? ""); } export function getTaskResultValue(task: Task) { return `__task:${task.id} ${task.title}`; } export function getFileResultValue(filePath: string) { return `__file:${filePath}`; } function CommandItemRow({ cmd, onSelect, }: { cmd: CommandItemType; onSelect: (cmd: CommandItemType) => void; }) { return ( onSelect(cmd)}> {cmd.icon && {cmd.icon}} {cmd.label} {cmd.shortcut && {formatShortcut(cmd.shortcut)}} {cmd.enterMode && ( )} ); } type TaskResultItemProps = { task: Task; stepMap: Map; repoMap: Map; onSelect: (task: Task) => void; }; function TaskResultItem({ task, stepMap, repoMap, onSelect }: TaskResultItemProps) { const isArchived = ARCHIVED_STATES.has(task.state); const step = stepMap.get(task.workflow_step_id); const stepHex = step ? STEP_COLOR_MAP[step.color] : undefined; const rawPath = task.primary_working_directory ?? (task.repositories?.[0] ? repoMap.get(task.repositories[0].repository_id) : undefined); const workDir = rawPath ? getFileName(rawPath) : undefined; const details: string[] = []; if (workDir) details.push(workDir); if (task.primary_agent_name) details.push(task.primary_agent_name); if (task.updated_at) { details.push(formatDistanceToNow(new Date(task.updated_at), { addSuffix: true })); } return ( onSelect(task)} className={isArchived ? "opacity-60" : ""} forceMount >
{isArchived ? ( ) : ( )} {task.title} {step && ( {step.name} )} {details.length > 0 && ( {details.join(" · ")} )}
); } type CommandsListContentProps = { commands: CommandItemType[]; grouped: [string, CommandItemType[]][]; search: string; onSelect: (cmd: CommandItemType) => void; taskResults: Task[]; isSearching: boolean; stepMap: Map; repoMap: Map; onTaskSelect: (task: Task) => void; }; function CommandsListContent({ commands, grouped, search, onSelect, taskResults, isSearching, stepMap, repoMap, onTaskSelect, }: CommandsListContentProps) { const hasInlineResults = taskResults.length > 0 || isSearching; return ( <> {!hasInlineResults && !isSearching && No commands found.} {isSearching && taskResults.length === 0 && (
)} {taskResults.length > 0 && ( {taskResults.map((task) => ( ))} )} {search.trim() ? ( {commands.map((cmd) => ( ))} ) : ( grouped.map(([group, items]) => ( {items.map((cmd) => ( ))} )) )} ); } type FileSearchContentProps = { files: string[]; isSearching: boolean; search: string; onSelect: (path: string) => void; }; function FileSearchContent({ files, isSearching, search, onSelect }: FileSearchContentProps) { if (isSearching && files.length === 0) { return (
); } if (search.trim() && files.length === 0) return No files found.; if (!search.trim()) return Type to search files...; return ( {files.map((filePath) => { const fileName = getFileName(filePath); const lastSlash = filePath.lastIndexOf("/"); const dir = lastSlash > 0 ? filePath.slice(0, lastSlash) : ""; return ( onSelect(filePath)} forceMount > {fileName} {dir && {dir}} ); })} ); } function getInputPlaceholder(mode: CommandPanelMode, inputCommand: CommandItemType | null) { if (mode === "input") return inputCommand?.inputPlaceholder ?? "Enter value..."; if (mode === "search-tasks") return "Search for tasks..."; if (mode === MODE_SEARCH_FILES) return "Search for files..."; return "Type a command..."; } function getEnterLabel(mode: CommandPanelMode) { if (mode === "input") return "Confirm"; if (mode === "search-tasks" || mode === MODE_SEARCH_FILES) return "Open"; return "Select"; } function getModeLabel(mode: CommandPanelMode, inputCommand: CommandItemType | null) { if (mode === "input") return inputCommand?.label; if (mode === "search-tasks") return "Tasks"; if (mode === MODE_SEARCH_FILES) return "Files"; return null; } function CommandPanelFooter({ mode }: { mode: CommandPanelMode }) { const keyboardShortcuts = useAppStore((s) => s.userSettings.keyboardShortcuts); return (
{mode === MODE_COMMANDS && ( <> ↑ ↓ Navigate {formatShortcut(getShortcut("FILE_SEARCH", keyboardShortcuts))} File Search )} ↵ {getEnterLabel(mode)} {mode !== MODE_COMMANDS && ( ⌫ Back )} esc Close
); } export type CommandPanelViewProps = { open: boolean; setOpen: (open: boolean) => void; mode: CommandPanelMode; inputCommand: CommandItemType | null; selectedValue: string; setSelectedValue: Dispatch>; search: string; setSearch: (value: string) => void; handleKeyDown: (e: React.KeyboardEvent) => void; goBack: () => void; fileResults: string[]; isSearchingFiles: boolean; handleFileSelect: (filePath: string) => void; commands: CommandItemType[]; grouped: Array<[string, CommandItemType[]]>; handleSelect: (cmd: CommandItemType) => void; isSearching: boolean; taskResults: Task[]; stepMap: Map; repoMap: Map; handleTaskSelect: (task: Task) => void; }; export function CommandPanelView({ open, setOpen, mode, inputCommand, selectedValue, setSelectedValue, search, setSearch, handleKeyDown, goBack, fileResults, isSearchingFiles, handleFileSelect, commands, grouped, handleSelect, isSearching, taskResults, stepMap, repoMap, handleTaskSelect, }: CommandPanelViewProps) { const modeLabel = getModeLabel(mode, inputCommand); return (
{mode !== MODE_COMMANDS && ( )}
{mode === MODE_COMMANDS && ( )} {mode === MODE_SEARCH_FILES && ( )} {mode === "input" && (!search.trim() ? ( {inputCommand?.inputPlaceholder ?? "Enter a value..."} ) : ( Press Enter to confirm ))}
); }