import type { ProjectScript, ResolvedKeybindingsConfig, T3ProjectFileScript, } from "@t3tools/contracts"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { ChevronDownIcon, DownloadIcon, PlusIcon, SettingsIcon, WrenchIcon } from "lucide-react"; import React, { useCallback, useMemo, useState } from "react"; import { commandForProjectScript, primaryProjectScript } from "~/projectScripts"; import { shortcutLabelForCommand } from "~/keybindings"; import { EMPTY_PROJECT_SCRIPT_INPUT, editorRequestForScript, ProjectScriptEditorDialog, ScriptIcon, type NewProjectScriptInput, type ProjectScriptActionResult, type ProjectScriptEditorRequest, } from "./projectScriptEditor"; import { Button } from "./ui/button"; import { Group, GroupSeparator } from "./ui/group"; import { Menu, MenuGroup, MenuGroupLabel, MenuItem, MenuPopup, MenuSeparator, MenuShortcut, MenuTrigger, } from "./ui/menu"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { cn } from "~/lib/utils"; import { THREAD_DETAILS_PANEL_CHEVRON_CLASS, THREAD_DETAILS_PANEL_ICON_CLASS, THREAD_DETAILS_PANEL_ROW_POPUP_CLASS, THREAD_DETAILS_PANEL_ROW_CLASS, THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS, THREAD_DETAILS_PANEL_SPLIT_PRIMARY_CLASS, THREAD_DETAILS_PANEL_SPLIT_SECONDARY_CLASS, THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS, } from "./chat/threadDetailsPanelStyles"; export type { NewProjectScriptInput, ProjectScriptActionResult }; const NO_FILE_SCRIPTS: ReadonlyArray = []; interface ProjectScriptsControlProps { displayMode?: "toolbar" | "panel"; scripts: ReadonlyArray; /** Scripts declared in the project's checked-in t3.json, offered for import. */ fileScripts?: ReadonlyArray; keybindings: ResolvedKeybindingsConfig; preferredScriptId?: string | null; onRunScript: (script: ProjectScript) => void; onAddScript: (input: NewProjectScriptInput) => Promise; onUpdateScript: ( scriptId: string, input: NewProjectScriptInput, ) => Promise; onDeleteScript: (scriptId: string) => Promise; } export default function ProjectScriptsControl({ displayMode = "toolbar", scripts, fileScripts = NO_FILE_SCRIPTS, keybindings, preferredScriptId = null, onRunScript, onAddScript, onUpdateScript, onDeleteScript, }: ProjectScriptsControlProps) { const isPanel = displayMode === "panel"; const ActionGroup = isPanel ? "div" : Group; const panelAnchorRef = React.useRef(null); const addScriptFormId = React.useId(); const [editingScriptId, setEditingScriptId] = useState(null); const [actionsMenuOpen, setActionsMenuOpen] = useState({ scripts: false, imports: false, }); const [editorRequest, setEditorRequest] = useState(null); const primaryScript = useMemo(() => { if (preferredScriptId) { const preferred = scripts.find((script) => script.id === preferredScriptId); if (preferred) return preferred; } return primaryProjectScript(scripts); }, [preferredScriptId, scripts]); const importableScripts = useMemo( () => fileScripts.filter( (fileScript) => !scripts.some( (script) => script.command === fileScript.command || script.name.toLowerCase() === fileScript.name.toLowerCase(), ), ), [fileScripts, scripts], ); const dropdownItemClassName = "data-highlighted:bg-transparent data-highlighted:text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground data-highlighted:hover:bg-accent data-highlighted:hover:text-accent-foreground data-highlighted:focus-visible:bg-accent data-highlighted:focus-visible:text-accent-foreground"; const openAddDialog = () => { setEditorRequest({ scriptId: null, initial: EMPTY_PROJECT_SCRIPT_INPUT }); }; const openEditDialog = (script: ProjectScript) => { setActionsMenuOpen({ scripts: false, imports: false }); setEditorRequest(editorRequestForScript(script, keybindings)); }; const submitScript = useCallback( (scriptId: string | null, input: NewProjectScriptInput) => scriptId === null ? onAddScript(input) : onUpdateScript(scriptId, input), [onAddScript, onUpdateScript], ); const importFileScript = async (fileScript: T3ProjectFileScript) => { const payload: NewProjectScriptInput = { name: fileScript.name, command: fileScript.command, icon: fileScript.icon ?? "play", runOnWorktreeCreate: fileScript.runOnWorktreeCreate ?? false, keybinding: null, previewUrl: fileScript.previewUrl ?? null, autoOpenPreview: fileScript.previewUrl ? (fileScript.autoOpenPreview ?? false) : false, }; const result = await onAddScript(payload); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { // Surface the failure through the regular add dialog, prefilled so the // user can adjust and retry. const error = squashAtomCommandFailure(result); setEditorRequest({ scriptId: null, initial: payload, error: error instanceof Error ? error.message : "Failed to import action.", }); } }; const importMenuItems = importableScripts.length > 0 && ( <> {primaryScript && } From t3.json {importableScripts.map((fileScript) => ( void importFileScript(fileScript)} > {fileScript.name} ))} ); return ( <> {primaryScript ? ( onRunScript(primaryScript)} /> } > {primaryScript.name} Run {primaryScript.name} {isPanel ? ( ) : importableScripts.length > 0 ? ( isPanel ? (
) : ( setActionsMenuOpen({ scripts: false, imports: open })} > } > Actions {importMenuItems} Add action ) ) : ( } > {isPanel ? "Add project script" : "Add action"} {isPanel ? "Add project script" : "Add action"} )} void onDeleteScript(scriptId)} onClose={() => setEditorRequest(null)} /> ); }