import { memo, useMemo } from "react"; import { Link } from "@/lib/router"; import { useQuery } from "@tanstack/react-query"; import type { Issue } from "@paperclipai/shared"; import { heartbeatsApi, type LiveRunForIssue } from "../api/heartbeats"; import type { TranscriptEntry } from "../adapters"; import { issuesApi } from "../api/issues"; import { queryKeys } from "../lib/queryKeys"; import { cn, relativeTime } from "../lib/utils"; import { ExternalLink } from "lucide-react"; import { Identity } from "./Identity"; import { RunChatSurface } from "./RunChatSurface"; import { useLiveRunTranscripts } from "./transcript/useLiveRunTranscripts"; const MIN_DASHBOARD_RUNS = 4; const DASHBOARD_RUN_CARD_LIMIT = 4; const DASHBOARD_LOG_POLL_INTERVAL_MS = 15_000; const DASHBOARD_LOG_READ_LIMIT_BYTES = 64_000; const DASHBOARD_MAX_CHUNKS_PER_RUN = 40; const EMPTY_TRANSCRIPT: TranscriptEntry[] = []; function isRunActive(run: LiveRunForIssue): boolean { return run.status === "queued" || run.status === "running"; } interface ActiveAgentsPanelProps { companyId: string; title?: string; minRunCount?: number; fetchLimit?: number; cardLimit?: number; gridClassName?: string; cardClassName?: string; emptyMessage?: string; queryScope?: string; showMoreLink?: boolean; } export function ActiveAgentsPanel({ companyId, title = "Agents", minRunCount = MIN_DASHBOARD_RUNS, fetchLimit, cardLimit = DASHBOARD_RUN_CARD_LIMIT, gridClassName, cardClassName, emptyMessage = "No recent agent runs.", queryScope = "dashboard", showMoreLink = true, }: ActiveAgentsPanelProps) { const { data: liveRuns } = useQuery({ queryKey: [...queryKeys.liveRuns(companyId), queryScope, { minRunCount, fetchLimit }], queryFn: () => heartbeatsApi.liveRunsForCompany(companyId, { minCount: minRunCount, limit: fetchLimit }), }); const runs = liveRuns ?? []; const visibleRuns = useMemo(() => runs.slice(0, cardLimit), [cardLimit, runs]); const hiddenRunCount = Math.max(0, runs.length - visibleRuns.length); const { data: issues } = useQuery({ queryKey: [...queryKeys.issues.list(companyId), "with-routine-executions"], queryFn: () => issuesApi.list(companyId, { includeRoutineExecutions: true }), enabled: visibleRuns.length > 0, }); const issueById = useMemo(() => { const map = new Map(); for (const issue of issues ?? []) { map.set(issue.id, issue); } return map; }, [issues]); const { transcriptByRun, hasOutputForRun } = useLiveRunTranscripts({ runs: visibleRuns, companyId, maxChunksPerRun: DASHBOARD_MAX_CHUNKS_PER_RUN, logPollIntervalMs: DASHBOARD_LOG_POLL_INTERVAL_MS, logReadLimitBytes: DASHBOARD_LOG_READ_LIMIT_BYTES, enableRealtimeUpdates: false, }); return (

{title}

{runs.length === 0 ? (

{emptyMessage}

) : (
{visibleRuns.map((run) => ( ))}
)} {showMoreLink && hiddenRunCount > 0 && (
{hiddenRunCount} more active/recent run{hiddenRunCount === 1 ? "" : "s"}
)}
); } const AgentRunCard = memo(function AgentRunCard({ companyId, run, issue, transcript, hasOutput, isActive, className, }: { companyId: string; run: LiveRunForIssue; issue?: Issue; transcript: TranscriptEntry[]; hasOutput: boolean; isActive: boolean; className?: string; }) { return (
{isActive ? ( ) : ( )}
{isActive ? "Live now" : run.finishedAt ? `Finished ${relativeTime(run.finishedAt)}` : `Started ${relativeTime(run.createdAt)}`}
{run.issueId && (
{issue?.identifier ?? run.issueId.slice(0, 8)} {issue?.title ? ` - ${issue.title}` : ""}
)}
); });