import { memo, useMemo } from "react"; import { Link } from "@/lib/router"; import { useQueries, useQuery } from "@tanstack/react-query"; import type { Issue, IssueRecoveryAction } 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 { deriveActiveRecoveryDisplayState, RECOVERY_CHIP_DEFAULT_TONE, } from "../lib/recovery-display"; import { ExternalLink } from "lucide-react"; import { Identity } from "./Identity"; import { RunChatSurface } from "./RunChatSurface"; import { useLiveRunTranscripts } from "./transcript/useLiveRunTranscripts"; function RunCardRecoveryChip({ action }: { action: IssueRecoveryAction }) { const state = deriveActiveRecoveryDisplayState(action); if (!state) return null; const tone = RECOVERY_CHIP_DEFAULT_TONE[state]; const Icon = tone.icon; return ( {tone.label} ); } 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 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: 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}` : ""} {issue?.activeRecoveryAction ? ( ) : null} )} ); });
{emptyMessage}