import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Link, useLocation, useNavigate, useParams } from "@/lib/router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Activity as ActivityIcon, ChevronDown, ChevronRight, Clock3, Copy, History as HistoryIcon, Play, Plus, Repeat, Save, SlidersHorizontal, } from "lucide-react"; import { ApiError } from "../api/client"; import { routinesApi, type RoutineTriggerResponse, type RotateRoutineTriggerResponse, type RestoreRoutineRevisionResponse } from "../api/routines"; import { TriggerListCard } from "../components/TriggerListCard"; import { TriggerDialog } from "../components/TriggerDialog"; import { ConfirmDialog } from "../components/ConfirmDialog"; import { RoutineHistoryTab, type RoutineHistoryDirtyFieldDescriptor, } from "../components/RoutineHistoryTab"; import { heartbeatsApi } from "../api/heartbeats"; import { LiveRunWidget } from "../components/LiveRunWidget"; import { agentsApi } from "../api/agents"; import { projectsApi } from "../api/projects"; import { accessApi } from "../api/access"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { usePanel } from "../context/PanelContext"; import { useToastActions } from "../context/ToastContext"; import { cn } from "../lib/utils"; import { queryKeys } from "../lib/queryKeys"; import { buildMarkdownMentionOptions } from "../lib/company-members"; import { timeAgo } from "../lib/timeAgo"; import { ToggleSwitch } from "@/components/ui/toggle-switch"; import { EmptyState } from "../components/EmptyState"; import { PageSkeleton } from "../components/PageSkeleton"; import { AgentIcon } from "../components/AgentIconPicker"; import { InlineEntitySelector, type InlineEntityOption } from "../components/InlineEntitySelector"; import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "../components/MarkdownEditor"; import { RoutineRunVariablesDialog, type RoutineRunDialogSubmitData, } from "../components/RoutineRunVariablesDialog"; import { RoutineVariablesEditor, RoutineVariablesHint } from "../components/RoutineVariablesEditor"; import { RunButton } from "../components/AgentActionButtons"; import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees"; import { getRecentProjectIds, trackRecentProject } from "../lib/recent-projects"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Badge } from "@/components/ui/badge"; import type { RoutineDetail as RoutineDetailType, RoutineTrigger, RoutineVariable } from "@paperclipai/shared"; const concurrencyPolicies = ["coalesce_if_active", "always_enqueue", "skip_if_active"]; const catchUpPolicies = ["skip_missed", "enqueue_missed_with_cap"]; const routineTabs = ["triggers", "runs", "activity", "history"] as const; const concurrencyPolicyDescriptions: Record = { coalesce_if_active: "Keep one follow-up run queued while an active run is still working.", always_enqueue: "Queue every trigger occurrence, even if several runs stack up.", skip_if_active: "Drop overlapping trigger occurrences while the routine is already active.", }; const catchUpPolicyDescriptions: Record = { skip_missed: "Ignore schedule windows that were missed while the routine or scheduler was paused.", enqueue_missed_with_cap: "Catch up missed schedule windows in capped batches after recovery.", }; type RoutineTab = (typeof routineTabs)[number]; type SecretMessage = { title: string; entries: Array<{ webhookUrl: string; webhookSecret: string; }>; }; function autoResizeTextarea(element: HTMLTextAreaElement | null) { if (!element) return; element.style.height = "auto"; element.style.height = `${element.scrollHeight}px`; } function isRoutineTab(value: string | null): value is RoutineTab { return value !== null && routineTabs.includes(value as RoutineTab); } function getRoutineTabFromSearch(search: string): RoutineTab { const tab = new URLSearchParams(search).get("tab"); return isRoutineTab(tab) ? tab : "triggers"; } function formatActivityDetailValue(value: unknown): string { if (value === null) return "null"; if (typeof value === "string") return value; if (typeof value === "number" || typeof value === "boolean") return String(value); if (Array.isArray(value)) return value.length === 0 ? "[]" : value.map((item) => formatActivityDetailValue(item)).join(", "); try { return JSON.stringify(value); } catch { return "[unserializable]"; } } function getLocalTimezone(): string { try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch { return "UTC"; } } function buildRoutineMutationPayload(input: { title: string; description: string; projectId: string; assigneeAgentId: string; priority: string; concurrencyPolicy: string; catchUpPolicy: string; variables: RoutineVariable[]; }) { return { ...input, description: input.description.trim() || null, projectId: input.projectId || null, assigneeAgentId: input.assigneeAgentId || null, }; } export function RoutineDetail() { const { routineId } = useParams<{ routineId: string }>(); const { selectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); const navigate = useNavigate(); const location = useLocation(); const { pushToast } = useToastActions(); const { openPanel, closePanel, panelVisible, setPanelVisible } = usePanel(); const hydratedRoutineIdRef = useRef(null); const titleInputRef = useRef(null); const descriptionEditorRef = useRef(null); const assigneeSelectorRef = useRef(null); const projectSelectorRef = useRef(null); const [secretMessage, setSecretMessage] = useState(null); const [advancedOpen, setAdvancedOpen] = useState(false); const [saveConflict, setSaveConflict] = useState(false); const [runVariablesOpen, setRunVariablesOpen] = useState(false); const [triggerDialogOpen, setTriggerDialogOpen] = useState(false); const [editingTrigger, setEditingTrigger] = useState(null); const [triggerPendingDelete, setTriggerPendingDelete] = useState(null); const [togglingTriggerId, setTogglingTriggerId] = useState(null); const [editDraft, setEditDraft] = useState<{ title: string; description: string; projectId: string; assigneeAgentId: string; priority: string; concurrencyPolicy: string; catchUpPolicy: string; variables: RoutineVariable[]; }>({ title: "", description: "", projectId: "", assigneeAgentId: "", priority: "medium", concurrencyPolicy: "coalesce_if_active", catchUpPolicy: "skip_missed", variables: [], }); const activeTab = useMemo(() => getRoutineTabFromSearch(location.search), [location.search]); const { data: routine, isLoading, error } = useQuery({ queryKey: queryKeys.routines.detail(routineId!), queryFn: () => routinesApi.get(routineId!), enabled: !!routineId, }); const activeIssueId = routine?.activeIssue?.id; const { data: liveRuns } = useQuery({ queryKey: queryKeys.issues.liveRuns(activeIssueId!), queryFn: () => heartbeatsApi.liveRunsForIssue(activeIssueId!), enabled: !!activeIssueId, refetchInterval: 3000, }); const hasLiveRun = (liveRuns ?? []).length > 0; const { data: routineRuns } = useQuery({ queryKey: queryKeys.routines.runs(routineId!), queryFn: () => routinesApi.listRuns(routineId!), enabled: !!routineId, refetchInterval: hasLiveRun ? 3000 : false, }); const relatedActivityIds = useMemo( () => ({ triggerIds: routine?.triggers.map((trigger) => trigger.id) ?? [], runIds: routineRuns?.map((run) => run.id) ?? [], }), [routine?.triggers, routineRuns], ); const { data: activity } = useQuery({ queryKey: [ ...queryKeys.routines.activity(selectedCompanyId!, routineId!), relatedActivityIds.triggerIds.join(","), relatedActivityIds.runIds.join(","), ], queryFn: () => routinesApi.activity(selectedCompanyId!, routineId!, relatedActivityIds), enabled: !!selectedCompanyId && !!routineId && !!routine, }); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), queryFn: () => agentsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: projects } = useQuery({ queryKey: queryKeys.projects.list(selectedCompanyId!), queryFn: () => projectsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: companyMembers } = useQuery({ queryKey: queryKeys.access.companyUserDirectory(selectedCompanyId!), queryFn: () => accessApi.listUserDirectory(selectedCompanyId!), enabled: !!selectedCompanyId, }); const routineDefaults = useMemo( () => routine ? { title: routine.title, description: routine.description ?? "", projectId: routine.projectId ?? "", assigneeAgentId: routine.assigneeAgentId ?? "", priority: routine.priority, concurrencyPolicy: routine.concurrencyPolicy, catchUpPolicy: routine.catchUpPolicy, variables: routine.variables, } : null, [routine], ); const dirtyFields = useMemo(() => { if (!routineDefaults) return []; const result: RoutineHistoryDirtyFieldDescriptor[] = []; if (editDraft.title !== routineDefaults.title) result.push({ key: "title", label: "the title" }); if (editDraft.description !== routineDefaults.description) { result.push({ key: "description", label: "the description" }); } if (editDraft.projectId !== routineDefaults.projectId) { result.push({ key: "projectId", label: "the project" }); } if (editDraft.assigneeAgentId !== routineDefaults.assigneeAgentId) { result.push({ key: "assigneeAgentId", label: "the default agent" }); } if (editDraft.priority !== routineDefaults.priority) { result.push({ key: "priority", label: "the priority" }); } if (editDraft.concurrencyPolicy !== routineDefaults.concurrencyPolicy) { result.push({ key: "concurrencyPolicy", label: "the concurrency policy" }); } if (editDraft.catchUpPolicy !== routineDefaults.catchUpPolicy) { result.push({ key: "catchUpPolicy", label: "the catch-up policy" }); } if (JSON.stringify(editDraft.variables) !== JSON.stringify(routineDefaults.variables)) { result.push({ key: "variables", label: "the variables" }); } return result; }, [editDraft, routineDefaults]); const isEditDirty = dirtyFields.length > 0; useEffect(() => { if (!routine) return; setBreadcrumbs([{ label: "Routines", href: "/routines" }, { label: routine.title }]); if (!routineDefaults) return; const changedRoutine = hydratedRoutineIdRef.current !== routine.id; if (changedRoutine || !isEditDirty) { setEditDraft(routineDefaults); hydratedRoutineIdRef.current = routine.id; } }, [routine, routineDefaults, isEditDirty, setBreadcrumbs]); useEffect(() => { autoResizeTextarea(titleInputRef.current); }, [editDraft.title, routine?.id]); const copySecretValue = async (label: string, value: string) => { try { await navigator.clipboard.writeText(value); pushToast({ title: `${label} copied`, tone: "success" }); } catch (error) { pushToast({ title: `Failed to copy ${label.toLowerCase()}`, body: error instanceof Error ? error.message : "Clipboard access was denied.", tone: "error", }); } }; const setActiveTab = useCallback((value: string) => { if (!routineId || !isRoutineTab(value)) return; const params = new URLSearchParams(location.search); if (value === "triggers") { params.delete("tab"); } else { params.set("tab", value); } const search = params.toString(); navigate( { pathname: location.pathname, search: search ? `?${search}` : "", }, { replace: true }, ); }, [location.pathname, location.search, navigate, routineId]); const saveRoutine = useMutation({ mutationFn: () => { const payload = buildRoutineMutationPayload(editDraft); const baseRevisionId = routine?.latestRevisionId ?? null; return routinesApi.update(routineId!, { ...payload, ...(baseRevisionId ? { baseRevisionId } : {}), }); }, onSuccess: async () => { setSaveConflict(false); await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }), queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) }), queryClient.invalidateQueries({ queryKey: queryKeys.routines.activity(selectedCompanyId!, routineId!) }), queryClient.invalidateQueries({ queryKey: queryKeys.routines.revisions(routineId!) }), ]); }, onError: (error) => { if (error instanceof ApiError && error.status === 409) { setSaveConflict(true); pushToast({ title: "Routine changed", body: "Someone else updated this routine. Reload to see the latest revision.", tone: "warn", }); return; } pushToast({ title: "Failed to save routine", body: error instanceof Error ? error.message : "Paperclip could not save the routine.", tone: "error", }); }, }); const saveRoutineRef = useRef(saveRoutine); useEffect(() => { saveRoutineRef.current = saveRoutine; }, [saveRoutine]); const runRoutine = useMutation({ mutationFn: (data?: RoutineRunDialogSubmitData) => routinesApi.run(routineId!, { ...(data?.variables && Object.keys(data.variables).length > 0 ? { variables: data.variables } : {}), ...(data?.assigneeAgentId !== undefined ? { assigneeAgentId: data.assigneeAgentId } : {}), ...(data?.projectId !== undefined ? { projectId: data.projectId } : {}), ...(data?.executionWorkspaceId !== undefined ? { executionWorkspaceId: data.executionWorkspaceId } : {}), ...(data?.executionWorkspacePreference !== undefined ? { executionWorkspacePreference: data.executionWorkspacePreference } : {}), ...(data?.executionWorkspaceSettings !== undefined ? { executionWorkspaceSettings: data.executionWorkspaceSettings } : {}), }), onSuccess: async () => { pushToast({ title: "Routine run started", tone: "success" }); setRunVariablesOpen(false); setActiveTab("runs"); await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }), queryClient.invalidateQueries({ queryKey: queryKeys.routines.runs(routineId!) }), queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) }), queryClient.invalidateQueries({ queryKey: queryKeys.routines.activity(selectedCompanyId!, routineId!) }), ]); }, onError: (error) => { pushToast({ title: "Routine run failed", body: error instanceof Error ? error.message : "Paperclip could not start the routine run.", tone: "error", }); }, }); const updateRoutineStatus = useMutation({ mutationFn: (status: string) => routinesApi.update(routineId!, { status }), onSuccess: async (_data, status) => { pushToast({ title: "Routine saved", body: status === "paused" ? "Automation paused." : "Automation enabled.", tone: "success", }); await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }), queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) }), ]); }, onError: (error) => { pushToast({ title: "Failed to update routine", body: error instanceof Error ? error.message : "Paperclip could not update the routine.", tone: "error", }); }, }); const createTrigger = useMutation({ mutationFn: async (body: Record): Promise => { // Auto-label when the caller didn't provide one (e.g. dialog left the // Label field blank). Keeps the existing "schedule-2"-style numbering // behaviour so existing routines keep unique-ish labels. const kind = String(body.kind ?? "schedule"); const trimmedLabel = typeof body.label === "string" ? body.label.trim() : ""; let finalLabel: string; if (trimmedLabel.length > 0 && trimmedLabel !== kind) { finalLabel = trimmedLabel; } else { const existingOfKind = (routine?.triggers ?? []).filter((t) => t.kind === kind).length; finalLabel = existingOfKind > 0 ? `${kind}-${existingOfKind + 1}` : kind; } return routinesApi.createTrigger(routineId!, { ...body, label: finalLabel }); }, onSuccess: async (result) => { setTriggerDialogOpen(false); if (result.secretMaterial) { setSecretMessage({ title: "Webhook trigger created", entries: [{ webhookUrl: result.secretMaterial.webhookUrl, webhookSecret: result.secretMaterial.webhookSecret, }], }); } else { pushToast({ title: "Trigger added", body: "The routine schedule was saved.", tone: "success", }); } await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }), queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) }), queryClient.invalidateQueries({ queryKey: queryKeys.routines.activity(selectedCompanyId!, routineId!) }), ]); }, onError: (error) => { pushToast({ title: "Failed to add trigger", body: error instanceof Error ? error.message : "Paperclip could not create the trigger.", tone: "error", }); }, }); const updateTrigger = useMutation({ mutationFn: ({ id, patch }: { id: string; patch: Record }) => routinesApi.updateTrigger(id, patch), onSuccess: async () => { pushToast({ title: "Trigger saved", tone: "success", }); setTriggerDialogOpen(false); setEditingTrigger(null); await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }), queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) }), queryClient.invalidateQueries({ queryKey: queryKeys.routines.activity(selectedCompanyId!, routineId!) }), ]); }, onError: (error) => { pushToast({ title: "Failed to update trigger", body: error instanceof Error ? error.message : "Paperclip could not update the trigger.", tone: "error", }); }, onSettled: () => { setTogglingTriggerId(null); }, }); const deleteTrigger = useMutation({ mutationFn: (id: string) => routinesApi.deleteTrigger(id), onSuccess: async () => { pushToast({ title: "Trigger deleted", tone: "success", }); setTriggerPendingDelete(null); await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }), queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) }), queryClient.invalidateQueries({ queryKey: queryKeys.routines.activity(selectedCompanyId!, routineId!) }), ]); }, onError: (error) => { pushToast({ title: "Failed to delete trigger", body: error instanceof Error ? error.message : "Paperclip could not delete the trigger.", tone: "error", }); }, }); const rotateTrigger = useMutation({ mutationFn: (id: string): Promise => routinesApi.rotateTriggerSecret(id), onSuccess: async (result) => { setSecretMessage({ title: "Webhook secret rotated", entries: [{ webhookUrl: result.secretMaterial.webhookUrl, webhookSecret: result.secretMaterial.webhookSecret, }], }); await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }), queryClient.invalidateQueries({ queryKey: queryKeys.routines.activity(selectedCompanyId!, routineId!) }), ]); }, onError: (error) => { pushToast({ title: "Failed to rotate webhook secret", body: error instanceof Error ? error.message : "Paperclip could not rotate the webhook secret.", tone: "error", }); }, }); const agentById = useMemo( () => new Map((agents ?? []).map((agent) => [agent.id, agent])), [agents], ); const projectById = useMemo( () => new Map((projects ?? []).map((project) => [project.id, project])), [projects], ); const recentAssigneeIds = useMemo(() => getRecentAssigneeIds(), [routine?.id]); const recentProjectIds = useMemo(() => getRecentProjectIds(), [routine?.id]); const assigneeOptions = useMemo( () => sortAgentsByRecency( (agents ?? []).filter((agent) => agent.status !== "terminated"), recentAssigneeIds, ).map((agent) => ({ id: agent.id, label: agent.name, searchText: `${agent.name} ${agent.role} ${agent.title ?? ""}`, })), [agents, recentAssigneeIds], ); const projectOptions = useMemo( () => (projects ?? []).map((project) => ({ id: project.id, label: project.name, searchText: project.description ?? "", })), [projects], ); const mentionOptions = useMemo(() => { return buildMarkdownMentionOptions({ agents, projects, members: companyMembers?.users, }); }, [agents, companyMembers?.users, projects]); const currentAssignee = editDraft.assigneeAgentId ? agentById.get(editDraft.assigneeAgentId) ?? null : null; const currentProject = editDraft.projectId ? projectById.get(editDraft.projectId) ?? null : null; const activityTabsPanel = useMemo(() => { if (!routine) return null; return ( Triggers Runs {hasLiveRun && } Activity History {routine.triggers.length === 0 ? (

No triggers yet

Triggers fire this routine on a schedule or via webhook.

) : (
{routine.triggers.map((trigger) => ( { setEditingTrigger(trigger); setTriggerDialogOpen(true); }} onDelete={() => setTriggerPendingDelete(trigger)} onToggleEnabled={(enabled) => { setTogglingTriggerId(trigger.id); updateTrigger.mutate({ id: trigger.id, patch: { enabled } }); }} onRotateSecret={ trigger.kind === "webhook" ? () => rotateTrigger.mutate(trigger.id) : undefined } togglePending={togglingTriggerId === trigger.id} /> ))}
)}
{hasLiveRun && activeIssueId && routine && ( )} {(routineRuns ?? []).length === 0 ? (

No runs yet.

) : (
{(routineRuns ?? []).map((run) => (
{run.source} {run.status.replaceAll("_", " ")}
{(run.trigger || run.linkedIssue) && (
{run.trigger && ( {run.trigger.label ?? run.trigger.kind} )} {run.linkedIssue && ( {run.linkedIssue.identifier ?? run.linkedIssue.id.slice(0, 8)} )}
)} {timeAgo(run.triggeredAt)}
))}
)}
{(activity ?? []).length === 0 ? (

No activity yet.

) : (
{(activity ?? []).map((event) => (
{event.action.replaceAll(".", " ")} {event.details && Object.keys(event.details).length > 0 && (
{Object.entries(event.details).slice(0, 3).map(([key, value], i) => ( {i > 0 && ·} {key.replaceAll("_", " ")}:{" "} {formatActivityDetailValue(value)} ))}
)} {timeAgo(event.createdAt)}
))}
)}
{ if (routineDefaults) setEditDraft(routineDefaults); }} onSaveEdits={() => { const currentSave = saveRoutineRef.current; if (!currentSave.isPending && editDraft.title.trim()) { currentSave.mutate(); } }} agents={agentById} projects={projectById} onRestoreSecretMaterials={(response: RestoreRoutineRevisionResponse) => { if (response.secretMaterials.length > 0) { setSecretMessage({ title: response.secretMaterials.length === 1 ? "Webhook trigger restored" : `${response.secretMaterials.length} webhook triggers restored`, entries: response.secretMaterials.map((recreated) => ({ webhookUrl: recreated.webhookUrl, webhookSecret: recreated.webhookSecret, })), }); } }} onRestored={(response: RestoreRoutineRevisionResponse) => { setSaveConflict(false); queryClient.setQueryData( queryKeys.routines.detail(routineId!), (prev) => prev ? { ...prev, ...response.routine, latestRevisionId: response.revision.id, latestRevisionNumber: response.revision.revisionNumber, } : prev, ); setEditDraft({ title: response.routine.title, description: response.routine.description ?? "", projectId: response.routine.projectId ?? "", assigneeAgentId: response.routine.assigneeAgentId ?? "", priority: response.routine.priority, concurrencyPolicy: response.routine.concurrencyPolicy, catchUpPolicy: response.routine.catchUpPolicy, variables: response.routine.variables, }); hydratedRoutineIdRef.current = response.routine.id; }} />
); }, [ activeIssueId, activeTab, activity, agentById, dirtyFields, editDraft.title, hasLiveRun, isEditDirty, projectById, queryClient, rotateTrigger.mutate, routine, routineDefaults, routineRuns, routineId, setActiveTab, togglingTriggerId, updateTrigger.mutate, ]); useEffect(() => { if (!activityTabsPanel) { closePanel(); return; } openPanel(activityTabsPanel); return () => closePanel(); }, [activityTabsPanel, closePanel, openPanel]); if (!selectedCompanyId) { return ; } if (isLoading) { return ; } if (error || !routine) { return (

{error instanceof Error ? error.message : "Routine not found"}

); } const automationEnabled = routine.status === "active"; const selectedProject = routine.projectId ? (projects?.find((project) => project.id === routine.projectId) ?? null) : null; const automationToggleDisabled = updateRoutineStatus.isPending || routine.status === "archived"; const automationLabel = routine.status === "archived" ? "Archived" : !routine.assigneeAgentId ? "Draft" : automationEnabled ? "Active" : "Paused"; const automationLabelClassName = routine.status === "archived" ? "text-muted-foreground" : automationEnabled ? "text-emerald-400" : "text-muted-foreground"; return (
{/* Header: editable title + actions */}