import { useQuery } from "@tanstack/react-query" import { Link } from "@tanstack/react-router" import { ChevronDown, ChevronRight, Download, RotateCcw } from "lucide-react" import { useState } from "react" import type { ArtifactRow, RunNodeRow } from "@/client" import { ValuePreview } from "@/components/Flow/ValuePreview" import { Button } from "@/components/ui/button" import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import useCustomToast from "@/hooks/useCustomToast" import { ago, cn, dur, si } from "@/lib/utils" import { MetricPicker, RunMetricChart, useMetricNames } from "./MetricChart" import { OpenInDashboard } from "./OpenInDashboard" import { CARD, curveLost, downloadArtifact, FLOW_GONE, isLive, paramText, runQueryOptions, shortCommit, shortId, tookDefault, useCancelRun, useFlowInputs, useRetryRun, } from "./queries" import { NodeStatusBadge, RunStatusBadge, statusReason } from "./RunStatus" const LABEL = "text-muted-foreground text-xs" export function RunDetail({ id }: { id: string }) { const { data: run, isPending } = useQuery(runQueryOptions(id)) const cancel = useCancelRun() const retry = useRetryRun() const names = useMetricNames(id) const { declared: current, gone } = useFlowInputs(run?.flow) const [metric, setMetric] = useState("") if (isPending || !run) return const shown = metric && names.includes(metric) ? metric : (names[0] ?? "") const nodes = run.nodes ?? [] const artifacts = run.artifacts ?? [] const reason = statusReason(run) // A restored node's curve comes from the run it was restored from, so it // goes when that run is deleted. Said once, where it applies. const lost = nodes.filter((node) => node.curve_lost) // What the flow declared when this run was submitted. A run from before // that was kept reads against the flow's current declarations. const declared = run.declared ? new Map(Object.entries(run.declared)) : current // Declared order first, so the panel reads the way the flow does; anything // the run carries that the flow no longer declares still shows, since it is // what the run was actually given. const fed = [ ...[...declared.keys()].map((name) => ({ name, value: name in run.params ? run.params[name] : declared.get(name), fromFlow: tookDefault(run, name), })), ...Object.keys(run.params) .filter((name) => !declared.has(name)) .map((name) => ({ name, value: run.params[name], fromFlow: tookDefault(run, name), })), ] return (
{run.flow}

{shortId(run.id)}

{run.group_id && ( in a sweep )} {run.draft && ( ran the working copy )} {gone && ( flow deleted )}
{isLive(run.status) ? ( ) : ( )}
{reason &&

{reason}

} {gone &&

{FLOW_GONE}

}
{ago(String(run.created_at ?? ""))} {run.duration_ms ? dur(run.duration_ms) : "—"} {run.actor || "—"} {run.cause} {run.seed ?? "—"} {/* The user's own repository for a code-declared flow; the store's commit is a generated shim and says less. */} {shortCommit(run.origin_commit || run.commit || "") || "—"} {run.labels.join(", ") || "—"} {shortId(run.params_digest || "")}
{/* What the run actually ran with, which is not the same list as what it was passed: a flow's inputs *are* its parameters, and one left alone took the flow's own value. Both are drawn, and which is which is marked — reading a number off a chart is worth nothing if the other half of the setting is invisible. */}

Inputs

{fed.length === 0 ? (

{gone && !run.declared ? "The run carried no parameters of its own, and its flow is gone, so what it took from the flow cannot be read back." : "This flow declares no inputs, so there was nothing to choose."}

) : (
{fed.map(({ name, value, fromFlow }) => ( ))}
)}
{names.length > 0 && (

Metrics

)} {lost.length > 0 && (

{curveLost(lost)}

)} {Object.keys(run.result ?? {}).length > 0 && (

Result

{Object.entries(run.result ?? {}).map(([key, value]) => ( ))}
)} {artifacts.length > 0 && }
) } /** * One named value: read directly when it is a scalar, unfolded when it is not. * * The same disclosure a node's ports use on the canvas — a run's report is a * record like any other, and serialising it onto one truncated line answers * nothing. */ export function Entry({ name, value, note, }: { name: string value: unknown /** Where the value came from, when it was not this run. */ note?: string }) { const structured = value !== null && typeof value === "object" return (
{name} {note && ( {note} )}
{structured ? ( ) : ( paramText(value) )}
) } function Fact({ label, children, }: { label: string children: React.ReactNode }) { return (
{label} {children}
) } /** What each node did, with its logs and its traceback behind a disclosure. */ function NodesTable({ nodes }: { nodes: RunNodeRow[] }) { const [open, setOpen] = useState(null) if (nodes.length === 0) return null return (
Node Status Took Worker Attempt {nodes.map((node) => { const detail = node.error || node.logs const isOpen = open === node.node return [ {detail && ( )} {node.node} {node.duration_ms ? dur(node.duration_ms) : "—"} {node.worker || "—"} {node.attempt} , isOpen && detail ? ( {node.error && (
                        {node.error}
                      
)} {node.logs && (
                        {node.logs}
                      
)}
) : null, ] })}
) } function Artifacts({ rows }: { rows: ArtifactRow[] }) { const { showErrorToast } = useCustomToast() return (
Artifact From Type Size {rows.map((row) => ( {row.name} {row.node} {row.media_type || "—"} {si(row.size)}B ))}
) }