import type { Issue, IssuePriority, IssueStatus, RoutineRunSummary, RoutineTrigger, } from "@paperclipai/shared"; import { ISSUE_PRIORITIES, ISSUE_STATUSES } from "@paperclipai/shared"; import { CalendarClock, Clock3, Play, Repeat, UserRound } from "lucide-react"; import { IssueRow } from "@/components/IssueRow"; import { MarkdownBody } from "@/components/MarkdownBody"; import { StatusBadge } from "@/components/StatusBadge"; import { Button } from "@/components/ui/button"; import { createIssueDetailLocationState } from "@/lib/issueDetailBreadcrumb"; import { Link } from "@/lib/router"; import { routineDetailHref, } from "./RoutineContextualSidebar"; import { useRoutineDetail } from "./routine-sections/context"; export type RoutineScheduleSummary = { label: string; detail: string; nextRunAt: Date | null; }; export function formatRoutineTimestamp(value: Date | string) { return new Date(value).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short", }); } export function summarizeRoutineSchedule(triggers: RoutineTrigger[]): RoutineScheduleSummary { const schedules = triggers.filter((trigger) => trigger.kind === "schedule" && trigger.enabled); const nextRunAt = schedules .map((trigger) => trigger.nextRunAt ? new Date(trigger.nextRunAt) : null) .filter((value): value is Date => value !== null && Number.isFinite(value.getTime())) .sort((left, right) => left.getTime() - right.getTime())[0] ?? null; if (schedules.length === 0) { const webhooks = triggers.filter((trigger) => trigger.kind === "webhook" && trigger.enabled); if (webhooks.length > 0) { return { label: `${webhooks.length} active webhook${webhooks.length === 1 ? "" : "s"}`, detail: "Runs on incoming requests", nextRunAt: null, }; } return { label: "No active schedule", detail: "Manual runs only", nextRunAt: null }; } const first = schedules[0]!; return { label: schedules.length === 1 ? "1 active schedule" : `${schedules.length} active schedules`, detail: first.cronExpression ? `${first.cronExpression}${first.timezone ? ` ยท ${first.timezone}` : ""}` : first.label ?? "Scheduled trigger", nextRunAt, }; } function normalizeIssueStatus(value: string): IssueStatus { return ISSUE_STATUSES.includes(value as IssueStatus) ? value as IssueStatus : "todo"; } function normalizeIssuePriority(value: string): IssuePriority { return ISSUE_PRIORITIES.includes(value as IssuePriority) ? value as IssuePriority : "medium"; } /** Display-only adapter for the canonical task row; run summaries intentionally carry compact task data. */ export function routineRunIssue( summary: NonNullable, run: RoutineRunSummary, companyId: string, projectId: string | null, ): Issue { return { ...summary, companyId, projectId, projectWorkspaceId: null, goalId: null, parentId: null, description: null, status: normalizeIssueStatus(summary.status), workMode: "standard", priority: normalizeIssuePriority(summary.priority), reviewPolicy: null, assigneeAgentId: null, assigneeUserId: null, checkoutRunId: null, executionRunId: null, executionAgentNameKey: null, executionLockedAt: null, createdByAgentId: null, createdByUserId: null, responsibleUserId: null, issueNumber: null, originKind: "routine_execution", originId: run.routineId, originRunId: run.id, requestDepth: 0, billingCode: null, assigneeAdapterOverrides: null, executionWorkspaceId: null, executionWorkspacePreference: null, executionWorkspaceSettings: null, startedAt: null, completedAt: null, cancelledAt: null, hiddenAt: null, createdAt: run.triggeredAt, }; } function OverviewFact({ icon: Icon, label, value, detail, }: { icon: typeof Clock3; label: string; value: React.ReactNode; detail?: React.ReactNode; }) { return (
{value}
{detail ?
{detail}
: null}
); } export function RoutineOverview() { const { routine, routineRuns, currentAssignee, hasLiveRun } = useRoutineDetail(); const schedule = summarizeRoutineSchedule(routine.triggers); const hasWebhook = routine.triggers.some((trigger) => trigger.kind === "webhook" && trigger.enabled); const sortedRuns = [...(routineRuns ?? [])].sort( (left, right) => new Date(right.triggeredAt).getTime() - new Date(left.triggeredAt).getTime(), ); const lastRun = sortedRuns[0] ?? null; const recentRuns = sortedRuns.slice(0, 5); const detailOrigin = createIssueDetailLocationState( routine.title, routineDetailHref(routine.id), "issues", ); const automationState = routine.status === "archived" ? "archived" : !routine.assigneeAgentId ? "draft" : routine.status; return (
} detail={hasLiveRun ? "A run is active now" : "No active run"} /> {schedule.detail}} /> : "No runs yet"} detail={lastRun ? formatRoutineTimestamp(lastRun.triggeredAt) : "Run manually or wait for a trigger"} />

Default agent

{currentAssignee ? (

Description

{routine.description?.trim() ? ( {routine.description} ) : (

No description yet.

)}

Recent runs

{recentRuns.length === 0 ? (

No runs yet. Run the routine now or wait for its schedule.

) : (
{recentRuns.map((run) => run.linkedIssue ? ( {formatRoutineTimestamp(run.triggeredAt)} )} /> ) : (
{run.trigger?.label ?? "Routine run"} {formatRoutineTimestamp(run.triggeredAt)}
))}
)}
); }