"use client"; import { useEffect, type Dispatch, type SetStateAction } from "react"; import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next"; import { Command, CommandEmpty, CommandInput, CommandList } from "@kandev/ui/command"; import { CommandPanelDialog } from "./command-panel-dialog"; import { Kbd, KbdGroup } from "@kandev/ui/kbd"; import type { CommandPanelMode, CommandItem as CommandItemType } from "@/lib/commands/types"; import type { Task } from "@/lib/types/http"; import type { CommandPanelLiveTask } from "@/lib/commands/task-result-activity"; import type { FileSearchResult } from "@/lib/types/backend"; import { WorkspaceContentSearch } from "@/components/workspace-content-search"; import { CommandPanelScopeSwitcher, getAdjacentCommandPanelScope, isCommandPanelScopeMode, type CommandPanelScopeMode, } from "@/components/command-panel-scope-switcher"; import type { WorkspaceContentSearchError } from "@/hooks/domains/session/use-workspace-content-search"; import type { WorkspaceContentSearchResult } from "@/lib/types/backend"; import { CommandPanelConfirmation, dismissCommandConfirmation, getCommandConfirmationState, } from "@/components/command-panel-confirmation"; import { CommandsListContent, FileSearchContent, MODE_COMMANDS, MODE_SEARCH_CONTENT, MODE_SEARCH_FILES, MODE_SEARCH_TASKS, TaskSearchContent, type StepMap, } from "@/components/command-panel-results"; export { MODE_COMMANDS, MODE_SEARCH_CONTENT, MODE_SEARCH_FILES, MODE_SEARCH_TASKS, getFileResultValue, getTaskResultValue, } from "@/components/command-panel-results"; // `mode` stays an untranslated discriminant — it is compared with `===` // throughout. Only the labels it selects are copy. function getInputPlaceholder( t: TFunction, mode: CommandPanelMode, inputCommand: CommandItemType | null, ) { if (mode === "input") return inputCommand?.inputPlaceholder ?? t("common:enterValue"); if (mode === MODE_SEARCH_TASKS) return t("common:searchForTasks"); if (mode === MODE_SEARCH_FILES) return t("common:searchForFiles"); if (mode === MODE_SEARCH_CONTENT) return t("common:searchTaskContents"); return t("common:typeACommand"); } function getEnterLabel(t: TFunction, mode: CommandPanelMode) { if (mode === "input") return t("common:confirm"); if (mode === MODE_SEARCH_TASKS || mode === MODE_SEARCH_FILES || mode === MODE_SEARCH_CONTENT) { return t("common:open"); } return t("common:select"); } function getModeLabel(t: TFunction, mode: CommandPanelMode, inputCommand: CommandItemType | null) { if (mode === "input") return inputCommand?.label; if (mode === MODE_SEARCH_TASKS) return t("common:tasks"); if (mode === MODE_SEARCH_FILES) return t("common:files"); if (mode === MODE_SEARCH_CONTENT) return t("common:contents"); return null; } function CommandPanelFooter({ mode }: { mode: CommandPanelMode }) { const { t } = useTranslation(); const isScopeMode = isCommandPanelScopeMode(mode); return (
{isScopeMode && ( <> ↑ ↓ {t("common:navigate")} Tab {t("common:switchMode")} )} ↵ {getEnterLabel(t, mode)} {!isScopeMode && ( ⌫ {t("common:back")} )} {/* A key name, not copy — it labels the physical key. */} esc {t("common: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; onScopeChange: (mode: CommandPanelScopeMode) => void; goBack: () => void; fileResults: FileSearchResult[]; isSearchingFiles: boolean; handleFileSelect: (filePath: string) => void; contentResults: WorkspaceContentSearchResult[]; isSearchingContent: boolean; contentSearchError: WorkspaceContentSearchError | null; activeSessionId: string | null; workspaceSearchAvailable: boolean; handleContentSelect: (result: WorkspaceContentSearchResult) => void; commands: CommandItemType[]; grouped: Array<[string, CommandItemType[]]>; handleSelect: (cmd: CommandItemType) => void; isSearching: boolean; taskResults: Task[]; stepMap: StepMap; repoMap: Map; liveTasksById: Map; lastStepIdByWorkflowId: ReadonlyMap; handleTaskSelect: (task: Task) => void; }; function CommandPanelInputHeader({ mode, inputCommand, search, setSearch, handleKeyDown, onScopeChange, goBack, workspaceSearchAvailable, }: CommandPanelViewProps) { const { t } = useTranslation(); const isTopLevelMode = isCommandPanelScopeMode(mode); const modeLabel = getModeLabel(t, mode, inputCommand); const onInputKeyDown = (event: React.KeyboardEvent) => { if ( isTopLevelMode && event.key === "Tab" && !event.altKey && !event.ctrlKey && !event.metaKey ) { event.preventDefault(); onScopeChange(getAdjacentCommandPanelScope(mode, event.shiftKey, workspaceSearchAvailable)); return; } handleKeyDown(event); }; return ( // Four scope tabs do not fit beside the input on a phone, and the tablist // does not shrink, so the input would collapse to a few characters. The // min-width forces the tablist onto its own row instead of squeezing the // query out of view.
{!isTopLevelMode && ( )} {isTopLevelMode && ( )}
); } function CommandPanelResultList(props: CommandPanelViewProps) { const { t } = useTranslation(); const { mode, inputCommand, search, fileResults, isSearchingFiles, handleFileSelect, contentResults, isSearchingContent, contentSearchError, activeSessionId, handleContentSelect, commands, grouped, handleSelect, isSearching, taskResults, stepMap, repoMap, liveTasksById, lastStepIdByWorkflowId, handleTaskSelect, } = props; const { confirmationCommand, visibleCommands, visibleGroups } = getCommandConfirmationState( commands, grouped, ); return ( {confirmationCommand && } {mode === MODE_COMMANDS && ( )} {mode === MODE_SEARCH_TASKS && ( )} {mode === MODE_SEARCH_FILES && ( )} {mode === MODE_SEARCH_CONTENT && ( )} {mode === "input" && (!search.trim() ? ( {inputCommand?.inputPlaceholder ?? t("common:enterAValue")} ) : ( {t("common:pressEnterToConfirm")} ))} ); } export function CommandPanelView(props: CommandPanelViewProps) { const { open, setOpen, mode, selectedValue, setSelectedValue, workspaceSearchAvailable, onScopeChange, } = props; const workspaceModeUnavailable = !workspaceSearchAvailable && (mode === MODE_SEARCH_FILES || mode === MODE_SEARCH_CONTENT); const renderedProps = workspaceModeUnavailable ? { ...props, mode: MODE_COMMANDS } : props; useEffect(() => { if (workspaceModeUnavailable) onScopeChange("commands"); }, [onScopeChange, workspaceModeUnavailable]); const handleOpenChange = (nextOpen: boolean) => { if (!nextOpen) dismissCommandConfirmation(props.commands); setOpen(nextOpen); }; return ( ); }