import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Navigate, useNavigate, useParams, useSearchParams } from "@/lib/router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertCircle, History, Pencil, Repeat, Sparkles, X } from "lucide-react"; import { ApiError } from "../api/client"; import { routinesApi, type RoutineTriggerResponse, type RotateRoutineTriggerResponse, type RestoreRoutineRevisionResponse, } from "../api/routines"; import { secretsApi } from "../api/secrets"; import { type RoutineHistoryDirtyFieldDescriptor } from "../components/RoutineHistoryTab"; import { heartbeatsApi } from "../api/heartbeats"; 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 { useToastActions } from "../context/ToastContext"; import { useStreamlinedUiEnabled } from "../hooks/useStreamlinedUiEnabled"; import { queryKeys } from "../lib/queryKeys"; import { copyTextToClipboard } from "../lib/clipboard"; import { routineOverviewCopyText } from "../lib/routine-description-copy"; import { buildMarkdownMentionOptions } from "../lib/company-members"; import { ToggleSwitch } from "@/components/ui/toggle-switch"; import { EmptyState } from "../components/EmptyState"; import { PageSkeleton } from "../components/PageSkeleton"; import { type InlineEntityOption } from "../components/InlineEntitySelector"; import { type MarkdownEditorRef, type MentionOption } from "../components/MarkdownEditor"; import { RoutineRunVariablesDialog, type RoutineRunDialogSubmitData, } from "../components/RoutineRunVariablesDialog"; import { RunButton } from "../components/AgentActionButtons"; import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees"; import { getRecentProjectIds, trackRecentProject } from "../lib/recent-projects"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { RoutineOverview } from "../components/RoutineOverview"; import { RoutineSectionHeading } from "../components/RoutineSectionHeading"; import { RoutineSectionPicker, RoutineSubSidebar } from "../components/RoutineSubSidebar"; import { isRoutineDetailView, resolveRoutineDetailDestination, routineDetailHref, } from "../components/RoutineContextualSidebar"; import { RoutineSaveBar } from "../components/RoutineSaveBar"; import { EDITABLE_SECTIONS, SECTION_FIELD_KEYS, RoutineDetailContext, createDefaultNewTrigger, type RoutineDetailContextValue, type RoutineEditDraft, type RoutineSectionKey, type SecretMessage, } from "../components/routine-sections/context"; import { OverviewSection, TriggersSection, VariablesSection, SecretsSection, DeliverySection, } from "../components/routine-sections/editable-sections"; import { ActivitySection, HistorySection, RunsSection, } from "../components/routine-sections/operate-sections"; import type { RoutineDetail as RoutineDetailType, RoutineEnvConfig, RoutineVariable, } from "@paperclipai/shared"; export function buildRoutineProjectOptions( projects: ReadonlyArray<{ id: string; name: string; description?: string | null; archivedAt?: Date | string | null }>, ): InlineEntityOption[] { return projects .filter((project) => !project.archivedAt) .map((project) => ({ id: project.id, label: project.name, searchText: project.description ?? "", })); } const SECTION_TITLES: Record = { overview: "Overview", triggers: "Triggers", variables: "Variables", secrets: "Secrets", delivery: "Delivery", runs: "Runs", activity: "Activity", history: "Version history", }; function autoResizeTextarea(element: HTMLTextAreaElement | null) { if (!element) return; element.style.height = "auto"; element.style.height = `${element.scrollHeight}px`; } function getLocalTimezone(): string { try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch { return "UTC"; } } function buildRoutineMutationPayload(input: RoutineEditDraft) { return { ...input, description: input.description.trim() || null, projectId: input.projectId || null, assigneeAgentId: input.assigneeAgentId || null, env: input.env && Object.keys(input.env).length > 0 ? input.env : null, }; } export function RoutineDetail() { const { routineId, section: sectionParam } = useParams<{ routineId: string; section?: string }>(); const [searchParams] = useSearchParams(); const triggerSetup = sectionParam === "triggers" && searchParams.has("triggerSetup"); const { selectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); const navigate = useNavigate(); const { pushToast } = useToastActions(); const { enabled: streamlinedUiEnabled } = useStreamlinedUiEnabled(); 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 [saveConflict, setSaveConflict] = useState(false); const [runVariablesOpen, setRunVariablesOpen] = useState(false); const [overviewEditing, setOverviewEditing] = useState(false); const [newTrigger, setNewTrigger] = useState(createDefaultNewTrigger); const [editDraft, setEditDraft] = useState({ title: "", description: "", projectId: "", assigneeAgentId: "", priority: "medium", concurrencyPolicy: "coalesce_if_active", catchUpPolicy: "skip_missed", activityGatePolicy: "always", activityGateScope: "company", variables: [], env: null, }); const legacyOperateSection = !streamlinedUiEnabled && (sectionParam === "runs" || sectionParam === "activity") ? sectionParam : null; const section: RoutineSectionKey = legacyOperateSection ?? (isRoutineDetailView(sectionParam) ? sectionParam : "overview"); const navigateToSection = useCallback( (next: RoutineSectionKey, options?: { replace?: boolean }) => { if (!routineId) return; if (!streamlinedUiEnabled && (next === "runs" || next === "activity")) { navigate(`/routines/${routineId}/${next}`, { replace: options?.replace ?? true }); return; } navigate( resolveRoutineDetailDestination({ routineId, section: next }), { replace: options?.replace ?? true }, ); }, [navigate, routineId, streamlinedUiEnabled], ); 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, isLoading: activityLoading, error: activityError } = 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!, { includeArchived: true }), queryFn: () => projectsApi.list(selectedCompanyId!, { includeArchived: true }), enabled: !!selectedCompanyId, }); const { data: companyMembers } = useQuery({ queryKey: queryKeys.access.companyUserDirectory(selectedCompanyId!), queryFn: () => accessApi.listUserDirectory(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: availableSecrets = [] } = useQuery({ queryKey: selectedCompanyId ? queryKeys.secrets.list(selectedCompanyId) : ["secrets", "none"], queryFn: () => secretsApi.list(selectedCompanyId!), enabled: Boolean(selectedCompanyId), }); const createSecret = useMutation({ mutationFn: (input: { name: string; value: string }) => { if (!selectedCompanyId) throw new Error("Select an organization to create secrets"); return secretsApi.create(selectedCompanyId, input); }, onSuccess: () => { if (!selectedCompanyId) return; queryClient.invalidateQueries({ queryKey: queryKeys.secrets.list(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, activityGatePolicy: routine.activityGatePolicy, activityGateScope: routine.activityGateScope, variables: routine.variables, env: routine.env ?? null, } : 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 (editDraft.activityGatePolicy !== routineDefaults.activityGatePolicy) { result.push({ key: "activityGatePolicy", label: "the advanced run policy" }); } if (editDraft.activityGateScope !== routineDefaults.activityGateScope) { result.push({ key: "activityGateScope", label: "the activity gate scope" }); } if (JSON.stringify(editDraft.variables) !== JSON.stringify(routineDefaults.variables)) { result.push({ key: "variables", label: "the variables" }); } if (JSON.stringify(editDraft.env ?? null) !== JSON.stringify(routineDefaults.env ?? null)) { result.push({ key: "env", label: "the secrets" }); } return result; }, [editDraft, routineDefaults]); const isEditDirty = dirtyFields.length > 0; const sectionDirtyFields = useCallback( (target: RoutineSectionKey) => { const keys = SECTION_FIELD_KEYS[target]; if (!keys) return []; return dirtyFields.filter((field) => keys.includes(field.key)); }, [dirtyFields], ); const isSectionDirty = useCallback( (target: RoutineSectionKey) => sectionDirtyFields(target).length > 0, [sectionDirtyFields], ); const discardSection = useCallback( (target: RoutineSectionKey) => { if (!routineDefaults) return; const keys = SECTION_FIELD_KEYS[target]; if (!keys) return; setEditDraft((current) => { const next = { ...current } as Record; for (const key of keys) { next[key] = (routineDefaults as Record)[key]; } return next as RoutineEditDraft; }); }, [routineDefaults], ); useEffect(() => { if (!routine) return; if (!triggerSetup) 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, triggerSetup]); useEffect(() => { autoResizeTextarea(titleInputRef.current); }, [editDraft.title, routine?.id]); useEffect(() => { if (section !== "overview") setOverviewEditing(false); }, [routineId, section]); const copySecretValue = useCallback( async (label: string, value: string) => { try { await copyTextToClipboard(value); pushToast({ title: `${label} copied`, tone: "success" }); } catch (copyError) { pushToast({ title: `Failed to copy ${label.toLowerCase()}`, body: copyError instanceof Error ? copyError.message : "Clipboard access was denied.", tone: "error", }); } }, [pushToast], ); 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: (mutationError) => { if (mutationError instanceof ApiError && mutationError.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: mutationError instanceof Error ? mutationError.message : "Paperclip could not save the routine.", tone: "error", }); }, }); 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); navigateToSection("runs"); await Promise.all([ queryClient.invalidateQueries({ queryKey: [...queryKeys.issues.list(selectedCompanyId!), "routine", routineId!] }), 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: (runError) => { pushToast({ title: "Routine run failed", body: runError instanceof Error ? runError.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: (statusError) => { pushToast({ title: "Failed to update routine", body: statusError instanceof Error ? statusError.message : "Paperclip could not update the routine.", tone: "error", }); }, }); const createTrigger = useMutation({ mutationFn: async (): Promise => { const existingOfKind = (routine?.triggers ?? []).filter((t) => t.kind === newTrigger.kind).length; const autoLabel = existingOfKind > 0 ? `${newTrigger.kind}-${existingOfKind + 1}` : newTrigger.kind; return routinesApi.createTrigger(routineId!, { kind: newTrigger.kind, label: autoLabel, ...(newTrigger.kind === "schedule" ? { cronExpression: newTrigger.cronExpression.trim(), timezone: getLocalTimezone() } : {}), ...(newTrigger.kind === "webhook" ? { signingMode: newTrigger.signingMode, replayWindowSec: Number(newTrigger.replayWindowSec || "300") } : {}), }); }, onSuccess: async (result) => { 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: (triggerError) => { pushToast({ title: "Failed to add trigger", body: triggerError instanceof Error ? triggerError.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", body: "The routine cadence update 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: (triggerError) => { pushToast({ title: "Failed to update trigger", body: triggerError instanceof Error ? triggerError.message : "Paperclip could not update the trigger.", tone: "error", }); }, }); const deleteTrigger = useMutation({ mutationFn: (id: string) => routinesApi.deleteTrigger(id), onSuccess: async () => { pushToast({ title: "Trigger deleted", 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: (triggerError) => { pushToast({ title: "Failed to delete trigger", body: triggerError instanceof Error ? triggerError.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: (triggerError) => { pushToast({ title: "Failed to rotate webhook secret", body: triggerError instanceof Error ? triggerError.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( () => buildRoutineProjectOptions(projects ?? []), [projects], ); const mentionOptions = useMemo( () => buildMarkdownMentionOptions({ agents, projects: (projects ?? []).filter((project) => !project.archivedAt), members: companyMembers?.users, }), [agents, companyMembers?.users, projects], ); // Wrap track-recent side-effects so the section components stay declarative. const setEditDraftTracked: typeof setEditDraft = useCallback((updater) => { setEditDraft((current) => { const next = typeof updater === "function" ? (updater as (c: RoutineEditDraft) => RoutineEditDraft)(current) : updater; if (next.assigneeAgentId && next.assigneeAgentId !== current.assigneeAgentId) { trackRecentAssignee(next.assigneeAgentId); } if (next.projectId && next.projectId !== current.projectId) { trackRecentProject(next.projectId); } return next; }); }, []); const currentAssignee = editDraft.assigneeAgentId ? agentById.get(editDraft.assigneeAgentId) ?? null : null; const currentProject = editDraft.projectId ? projectById.get(editDraft.projectId) ?? null : null; const reloadLatest = useCallback(() => { setSaveConflict(false); if (routineDefaults) setEditDraft(routineDefaults); queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }); }, [queryClient, routineDefaults, routineId]); const onHistoryRestoreSecretMaterials = useCallback((response: RestoreRoutineRevisionResponse) => { if (response.secretMaterials.length > 0) { navigateToSection("triggers"); 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, })), }); } }, [navigateToSection]); const onHistoryRestored = useCallback( (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, activityGatePolicy: response.routine.activityGatePolicy, activityGateScope: response.routine.activityGateScope, variables: response.routine.variables as RoutineVariable[], env: (response.routine.env ?? null) as RoutineEnvConfig | null, }); hydratedRoutineIdRef.current = response.routine.id; }, [queryClient, routineId], ); if (!selectedCompanyId) { return ; } const legacyTab = new URLSearchParams(window.location.search).get("tab"); const validLegacySection = !streamlinedUiEnabled && (sectionParam === "runs" || sectionParam === "activity"); if (routineId && (legacyTab || !sectionParam || (!isRoutineDetailView(sectionParam) && !validLegacySection))) { return ( ); } if (isLoading) { return ; } if (error || !routine || !routineDefaults) { return ( ); } const automationEnabled = routine.status === "active"; 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"; const contextValue: RoutineDetailContextValue = { routine, routineId: routineId!, companyId: routine.companyId, editDraft, setEditDraft: setEditDraftTracked, routineDefaults, dirtyFields, isEditDirty, sectionDirtyFields, isSectionDirty, discardSection, saveRoutine, saveConflict, reloadLatest, automationEnabled, automationLabel, automationLabelClassName, automationToggleDisabled, onToggleAutomation: () => { if (!automationEnabled && !routine.assigneeAgentId) { pushToast({ title: "Default agent required", body: "Set a default agent before enabling routine automation.", tone: "warn", }); return; } updateRoutineStatus.mutate(automationEnabled ? "paused" : "active"); }, onOpenRunDialog: () => setRunVariablesOpen(true), runRoutinePending: runRoutine.isPending, newTrigger, setNewTrigger, createTrigger, updateTrigger, deleteTrigger, rotateTrigger, secretMessage, setSecretMessage, copySecretValue, availableSecrets, createSecret, agents: agents ?? [], projects: projects ?? [], agentById, projectById, assigneeOptions, projectOptions, recentAssigneeIds, recentProjectIds, mentionOptions, currentAssignee, currentProject, routineRuns, activity, hasLiveRun, activeIssueId, titleInputRef, descriptionEditorRef, assigneeSelectorRef, projectSelectorRef, onHistoryRestoreSecretMaterials, onHistoryRestored, navigateToSection, }; if (triggerSetup) return ; const isEditableSection = EDITABLE_SECTIONS.includes(section); return ( Skip to section {/* The global shell owns routine navigation. This surface keeps one content scroll owner and a compact action header. */}
{!streamlinedUiEnabled ? ( next === "runs" || next === "activity" ? `/routines/${routine.id}/${next}` : routineDetailHref(routine.id, next)} isSectionDirty={isSectionDirty} hasLiveRun={hasLiveRun} onNavigate={navigateToSection} /> ) : null}
{!streamlinedUiEnabled ? ( ) : null}
{section === "overview" && overviewEditing ? (