import { AgentIdentity } from "@/components/AgentIdentity"; import { memo, useMemo } from "react"; import { Link } from "@/lib/router"; import { useQueries, 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 { Clock3 } from "lucide-react"; import { StatusGlyph } from "./StatusGlyph"; import { RunChatSurface } from "./RunChatSurface"; import { useLiveRunTranscripts } from "./transcript/useLiveRunTranscripts"; import { usePublishSharedQueryData, useSharedPollingQuery } from "../hooks/useSharedPolling"; 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[] = []; const EMPTY_RUNS: LiveRunForIssue[] = []; const runStatusLabels: Record = { running: "Running", queued: "Queued", succeeded: "Succeeded", failed: "Failed", timed_out: "Timed out", cancelled: "Cancelled", interrupted: "Interrupted", }; interface ActiveAgentsPanelProps { companyId: string; title?: string; minRunCount?: number; fetchLimit?: number; cardLimit?: number; gridClassName?: string; cardClassName?: string; emptyMessage?: string; queryScope?: string; showMoreLink?: boolean; showTranscripts?: boolean; dedupeLinkedTasks?: 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, showTranscripts = false, dedupeLinkedTasks = false, }: ActiveAgentsPanelProps) { const effectiveFetchLimit = fetchLimit; const liveRunsQueryKey = [...queryKeys.liveRuns(companyId), queryScope, { minRunCount, fetchLimit: effectiveFetchLimit, dedupeLinkedTasks }] as const; const sharedLiveRuns = useSharedPollingQuery({ companyId, resourceKey: `live-runs:${queryScope}:${minRunCount}:${effectiveFetchLimit ?? "default"}:${dedupeLinkedTasks}`, queryKey: liveRunsQueryKey, enabled: !!companyId, leaderOnly: true, }); const { data: liveRuns, dataUpdatedAt: liveRunsUpdatedAt } = useQuery({ queryKey: liveRunsQueryKey, queryFn: () => heartbeatsApi.liveRunsForCompany(companyId, { minCount: minRunCount, limit: effectiveFetchLimit, distinctTasks: dedupeLinkedTasks, }), enabled: sharedLiveRuns.enabled, }); usePublishSharedQueryData(sharedLiveRuns, liveRuns, liveRunsUpdatedAt); const runs = liveRuns ?? []; const cardRuns = useMemo(() => { if (!dedupeLinkedTasks) return runs; // The endpoint orders active runs first, then recent completed runs. Keep // the first run for each task so an active attempt wins over its history. const seenIssueIds = new Set(); return runs.filter((run) => { if (!run.issueId) return true; if (seenIssueIds.has(run.issueId)) return false; seenIssueIds.add(run.issueId); return true; }); }, [dedupeLinkedTasks, runs]); const visibleRuns = useMemo(() => cardRuns.slice(0, cardLimit), [cardLimit, cardRuns]); const hiddenRunCount = Math.max(0, cardRuns.length - visibleRuns.length); const visibleIssueIds = useMemo( () => [...new Set(visibleRuns.map((run) => run.issueId).filter((issueId): issueId is string => Boolean(issueId)))], [visibleRuns], ); const issueQueries = useQueries({ queries: visibleIssueIds.map((issueId) => ({ queryKey: queryKeys.issues.detail(issueId), queryFn: () => issuesApi.get(issueId), staleTime: 30_000, retry: false, })), }); const issueById = useMemo(() => { const map = new Map(); for (const query of issueQueries) { const issue = query.data; if (issue) map.set(issue.id, issue); } return map; }, [issueQueries]); const { transcriptByRun, hasOutputForRun } = useLiveRunTranscripts({ runs: showTranscripts ? visibleRuns : EMPTY_RUNS, companyId, maxChunksPerRun: DASHBOARD_MAX_CHUNKS_PER_RUN, logPollIntervalMs: DASHBOARD_LOG_POLL_INTERVAL_MS, logReadLimitBytes: DASHBOARD_LOG_READ_LIMIT_BYTES, enabled: showTranscripts, }); return (

{title}

{runs.length === 0 ? (

{emptyMessage}

) : (
{visibleRuns.map((run) => ( visibleIssueIds[index] === run.issueId && query.isError)} className={cardClassName} /> ))}
)} {showMoreLink && runs.length > 0 && (
{hiddenRunCount > 0 ? `${hiddenRunCount} more active/recent run${hiddenRunCount === 1 ? "" : "s"}` : "View all runs"}
)}
); } export const AgentRunCard = memo(function AgentRunCard({ companyId, run, issue, transcript = EMPTY_TRANSCRIPT, hasOutput = false, showTranscript = false, issueLoadFailed = false, className, }: { companyId: string; run: LiveRunForIssue; issue?: Pick; transcript?: TranscriptEntry[]; hasOutput?: boolean; showTranscript?: boolean; issueLoadFailed?: boolean; className?: string; }) { const statusLabel = runStatusLabels[run.status] ?? run.status.replace(/[_-]/g, " "); const runUrl = `/agents/${run.agentId}/runs/${run.id}`; const timestamp = run.finishedAt ? `Finished ${relativeTime(run.finishedAt)}` : run.startedAt ? `Started ${relativeTime(run.startedAt)}` : `Queued ${relativeTime(run.createdAt)}`; const taskStatus = issue?.status === "in_review" && issue.externalConversationState === "waiting" ? "idle" : issue?.status ?? "backlog"; const taskTitle = issue?.title ?? (issueLoadFailed ? "Task unavailable" : "Loading task…"); return (
{run.issueId ? ( {taskTitle} {issue?.identifier ?? run.issueId.slice(0, 8)} ) : ( {run.invocationSource === "timer" ? "Scheduled heartbeat" : "No linked task"} )}
{showTranscript && (
)}
); });