"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 (
);
}