"use client"; import { useTranslation } from "react-i18next"; import { formatTimeDistance, useDateLocale } from "@/lib/i18n/date-locale"; import { IconArchive, IconArrowRight, IconLoader2 } from "@tabler/icons-react"; import { CommandEmpty, CommandGroup, CommandItem, CommandShortcut } from "@kandev/ui/command"; import { Badge } from "@kandev/ui/badge"; import type { CommandPanelMode, CommandItem as CommandItemType } from "@/lib/commands/types"; import { getCommandSearchTerms, scoreCommandSearch, sortCommandsForSearch, } from "@/lib/commands/search"; import { formatShortcut } from "@/lib/keyboard/utils"; import type { Task } from "@/lib/types/http"; import type { FileSearchResult } from "@/lib/types/backend"; import { FileIcon } from "@/components/ui/file-icon"; import { useRepoDisplayName } from "@/hooks/domains/session/use-repo-display-name"; import { groupByRepositoryName, isSingleRepoGroup } from "@/lib/group-by-repo"; import { getTaskStateIconLabelKey, TaskStateIcon } from "@/components/task/task-state-icon"; import { resolveTaskResultActivity, type CommandPanelLiveTask, } from "@/lib/commands/task-result-activity"; export const MODE_COMMANDS: CommandPanelMode = "commands"; export const MODE_SEARCH_TASKS: CommandPanelMode = "search-tasks"; export const MODE_SEARCH_FILES: CommandPanelMode = "search-files"; export const MODE_SEARCH_CONTENT: CommandPanelMode = "search-content"; 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; } export function getTaskResultValue(task: Task) { return `__task:${task.id} ${task.title}`; } export function getFileResultValue(filePath: string) { return `__file:${filePath}`; } export type StepMap = Map; function CommandItemRow({ cmd, onSelect, }: { cmd: CommandItemType; onSelect: (cmd: CommandItemType) => void; }) { return ( onSelect(cmd)} > {cmd.icon && ( {cmd.icon} )} {cmd.label} {cmd.context && ( {cmd.context} )} {cmd.shortcut && {formatShortcut(cmd.shortcut)}} {cmd.enterMode && ( )} ); } type TaskResultItemProps = { task: Task; stepMap: StepMap; repoMap: Map; liveTasksById: Map; lastStepIdByWorkflowId: ReadonlyMap; onSelect: (task: Task) => void; }; function TaskResultItem({ task, stepMap, repoMap, liveTasksById, lastStepIdByWorkflowId, onSelect, }: TaskResultItemProps) { const { t } = useTranslation(); const locale = useDateLocale(); const isArchived = task.archived_at != null; const archivedLabel = t("tasks:archived"); const activity = resolveTaskResultActivity( task, liveTasksById.get(task.id), lastStepIdByWorkflowId, ); const step = stepMap.get(activity.workflowStepId); 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(formatTimeDistance(task.updated_at, locale)); } return ( onSelect(task)} forceMount >
{isArchived ? ( ) : ( )} {/* The title identifies the row, so it takes the free space and the non-shrinking badge and metadata cannot squeeze it away. Metadata is secondary and steps aside entirely on a phone. */} {task.title} {isArchived ? ( {archivedLabel} ) : ( step && ( {step.name} ) )} {details.length > 0 && ( {details.join(" · ")} )}
); } type TaskResultGroupProps = { tasks: Task[]; search: string; stepMap: StepMap; repoMap: Map; liveTasksById: Map; lastStepIdByWorkflowId: ReadonlyMap; onSelect: (task: Task) => void; testId?: string; }; function TaskResultGroup({ tasks, search, stepMap, repoMap, liveTasksById, lastStepIdByWorkflowId, onSelect, testId, }: TaskResultGroupProps) { const { t } = useTranslation(); if (tasks.length === 0) return null; return ( {tasks.map((task) => ( ))} ); } function TaskResultsLoader() { const { t } = useTranslation(); return (
); } type CommandsListContentProps = { commands: CommandItemType[]; grouped: [string, CommandItemType[]][]; search: string; onSelect: (cmd: CommandItemType) => void; taskResults: Task[]; isSearching: boolean; stepMap: StepMap; repoMap: Map; liveTasksById: Map; lastStepIdByWorkflowId: ReadonlyMap; onTaskSelect: (task: Task) => void; }; function SearchCommandGroups({ commands, search, onSelect, }: Pick) { const { t } = useTranslation(); const matches = sortCommandsForSearch(commands, search).filter( (cmd) => scoreCommandSearch(cmd.id, search, getCommandSearchTerms(cmd)) > 0, ); const regularMatches = matches.filter((cmd) => !cmd.searchOnly); const searchOnlyGroups = new Map(); for (const cmd of matches) { if (!cmd.searchOnly) continue; const groupItems = searchOnlyGroups.get(cmd.group) ?? []; groupItems.push(cmd); searchOnlyGroups.set(cmd.group, groupItems); } return ( <> {regularMatches.length > 0 && ( {regularMatches.map((cmd) => ( ))} )} {[...searchOnlyGroups].map(([group, items]) => ( {items.map((cmd) => ( ))} ))} ); } function IdleCommandGroups({ grouped, onSelect, }: Pick) { return ( <> {grouped.map(([group, items]) => { const visibleItems = items.filter((cmd) => !cmd.searchOnly); if (visibleItems.length === 0) return null; return ( {visibleItems.map((cmd) => ( ))} ); })} ); } /** * The commands scope mixes commands with task rows. Idle, the Active Tasks * group leads: reopening the palette is usually a jump back to work, and there * is no query to rank commands against. Once the user types, the ordering * flips — a typed query is a command query first, so matching commands lead and * the fuzzy task matches trail as a preview. The Tasks scope owns the full * task search. */ export function CommandsListContent({ commands, grouped, search, onSelect, taskResults, isSearching, stepMap, repoMap, liveTasksById, lastStepIdByWorkflowId, onTaskSelect, }: CommandsListContentProps) { const { t } = useTranslation(); const hasQuery = Boolean(search.trim()); const hasInlineResults = taskResults.length > 0 || isSearching; const taskRows = ( <> {isSearching && taskResults.length === 0 && } ); const commandRows = hasQuery ? ( ) : ( ); return ( <> {!hasInlineResults && {t("common:noCommandsFound")}} {hasQuery ? commandRows : taskRows} {hasQuery ? taskRows : commandRows} ); } type TaskSearchContentProps = { tasks: Task[]; isSearching: boolean; search: string; stepMap: StepMap; repoMap: Map; liveTasksById: Map; lastStepIdByWorkflowId: ReadonlyMap; onSelect: (task: Task) => void; }; export function TaskSearchContent({ tasks, isSearching, search, stepMap, repoMap, liveTasksById, lastStepIdByWorkflowId, onSelect, }: TaskSearchContentProps) { const { t } = useTranslation(); if (isSearching && tasks.length === 0) { return (
); } if (tasks.length === 0) return {t("common:noTasksFound")}; return ( ); } type FileSearchContentProps = { files: FileSearchResult[]; isSearching: boolean; search: string; sessionId: string | null; onSelect: (path: string) => void; }; export function FileSearchContent({ files, isSearching, search, sessionId, onSelect, }: FileSearchContentProps) { const { t } = useTranslation(); const getRepoDisplayName = useRepoDisplayName(sessionId); if (isSearching && files.length === 0) { return (
); } if (search.trim() && files.length === 0) return {t("common:noFilesFound")}; if (!search.trim()) return {t("common:typeToSearchFiles")}; const groups = groupByRepositoryName(files, (file) => file.repository_name); const singleRepo = isSingleRepoGroup(groups); return groups.map((group) => ( {group.items.map((file) => { const repositoryPrefix = file.repository_name ? `${file.repository_name}/` : ""; const displayPath = file.path.startsWith(repositoryPrefix) ? file.path.slice(repositoryPrefix.length) : file.path; const fileName = getFileName(displayPath); const lastSlash = displayPath.lastIndexOf("/"); const dir = lastSlash > 0 ? displayPath.slice(0, lastSlash) : ""; return ( onSelect(file.path)} forceMount > {fileName} {dir && {dir}} ); })} )); }