import { useMemo, useState } from "react"; import { Link } from "@/lib/router"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { heartbeatsApi, type LiveRunForIssue } from "../api/heartbeats"; import { queryKeys } from "../lib/queryKeys"; import { formatDateTime } from "../lib/utils"; import { ExternalLink, Square } from "lucide-react"; import { Identity } from "./Identity"; import { StatusBadge } from "./StatusBadge"; import { RunTranscriptView } from "./transcript/RunTranscriptView"; import { useLiveRunTranscripts } from "./transcript/useLiveRunTranscripts"; interface LiveRunWidgetProps { issueId: string; companyId?: string | null; } function toIsoString(value: string | Date | null | undefined): string | null { if (!value) return null; return typeof value === "string" ? value : value.toISOString(); } function isRunActive(status: string): boolean { return status === "queued" || status === "running"; } export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) { const queryClient = useQueryClient(); const [cancellingRunIds, setCancellingRunIds] = useState(new Set()); const { data: liveRuns } = useQuery({ queryKey: queryKeys.issues.liveRuns(issueId), queryFn: () => heartbeatsApi.liveRunsForIssue(issueId), enabled: !!issueId, refetchInterval: 3000, }); const { data: activeRun } = useQuery({ queryKey: queryKeys.issues.activeRun(issueId), queryFn: () => heartbeatsApi.activeRunForIssue(issueId), enabled: !!issueId, refetchInterval: 3000, }); const runs = useMemo(() => { const deduped = new Map(); for (const run of liveRuns ?? []) { deduped.set(run.id, run); } if (activeRun) { deduped.set(activeRun.id, { id: activeRun.id, status: activeRun.status, invocationSource: activeRun.invocationSource, triggerDetail: activeRun.triggerDetail, startedAt: toIsoString(activeRun.startedAt), finishedAt: toIsoString(activeRun.finishedAt), createdAt: toIsoString(activeRun.createdAt) ?? new Date().toISOString(), agentId: activeRun.agentId, agentName: activeRun.agentName, adapterType: activeRun.adapterType, issueId, }); } return [...deduped.values()].sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), ); }, [activeRun, issueId, liveRuns]); const { transcriptByRun, hasOutputForRun } = useLiveRunTranscripts({ runs, companyId }); const handleCancelRun = async (runId: string) => { setCancellingRunIds((prev) => new Set(prev).add(runId)); try { await heartbeatsApi.cancel(runId); queryClient.invalidateQueries({ queryKey: queryKeys.issues.liveRuns(issueId) }); queryClient.invalidateQueries({ queryKey: queryKeys.issues.activeRun(issueId) }); } finally { setCancellingRunIds((prev) => { const next = new Set(prev); next.delete(runId); return next; }); } }; if (runs.length === 0) return null; return (
Live Runs
Streamed with the same transcript UI used on the full run detail page.
{runs.map((run) => { const isActive = isRunActive(run.status); const transcript = transcriptByRun.get(run.id) ?? []; return (
{run.id.slice(0, 8)} {formatDateTime(run.startedAt ?? run.createdAt)}
{isActive && ( )} Open run
); })}
); }