Files
triggerdotdev--trigger.dev/apps/webapp/app/components/dashboard-agent/RunDiagnosisCard.tsx
T
2026-07-13 13:32:57 +08:00

224 lines
7.7 KiB
TypeScript

import { Link } from "@remix-run/react";
import type { DiagnosisBlock } from "@internal/dashboard-agent";
import { Badge } from "~/components/primitives/Badge";
import { toSafeUrl } from "~/components/runs/v3/agent/AgentMessageView";
import { useOptionalEnvironment } from "~/hooks/useEnvironment";
import { useOptionalOrganization } from "~/hooks/useOrganizations";
import { useOptionalProject } from "~/hooks/useProject";
import { cn } from "~/utils/cn";
import { v3RunPath } from "~/utils/pathBuilder";
// The "why did this run fail?" failure card — the first block in the dashboard
// agent's view catalog. Rendered from a `diagnosis` block the agent emits via
// the render_view tool (see internal-packages/dashboard-agent tool-schemas).
// Everything here is plain presentation of validated fields; no markup comes
// from the model, so there's nothing to sanitize beyond outbound URLs.
const CATEGORY_LABELS: Record<DiagnosisBlock["category"], string> = {
user_code_error: "Code error",
configuration: "Configuration",
dependency: "Dependency",
timeout: "Timeout",
out_of_memory: "Out of memory",
rate_limit: "Rate limit",
external_service: "External service",
infrastructure: "Infrastructure",
cancellation: "Cancelled",
unknown: "Unknown",
};
const CONFIDENCE_STYLES: Record<DiagnosisBlock["confidence"], string> = {
high: "border-emerald-500/40 text-emerald-400",
medium: "border-amber-500/40 text-amber-400",
low: "border-border-bright text-text-dimmed",
};
const EVIDENCE_LABELS: Record<DiagnosisBlock["evidence"][number]["type"], string> = {
error: "Error",
failed_span: "Failed span",
child_run: "Child run",
logs: "Logs",
deploy: "Deploy",
source: "Source",
historical_match: "History",
};
// Build a run-page path in the current org/project/env, or null when that route
// context is absent (e.g. the storybook page) so the card degrades to plain
// text rather than throwing.
function useRunPath(runId: string): string | null {
const organization = useOptionalOrganization();
const project = useOptionalProject();
const environment = useOptionalEnvironment();
if (!organization || !project || !environment) return null;
return v3RunPath(organization, project, environment, { friendlyId: runId });
}
// Internal link to a run page, built from the canonical path builder so it stays
// correct if the route shape changes. Falls back to plain text off-context.
function RunLink({ runId, className }: { runId: string; className?: string }) {
const to = useRunPath(runId);
if (!to) return <span className={cn("font-mono text-text-dimmed", className)}>{runId}</span>;
return (
<Link to={to} className={cn("text-indigo-400 underline hover:text-indigo-300", className)}>
{runId}
</Link>
);
}
// Render an evidence `reference`: a run id links to its run page, an https URL
// becomes an external link, everything else (error id, file:line, version) is
// shown as monospace text.
function EvidenceReference({ reference }: { reference: string }) {
if (/^run_[a-z0-9]+$/i.test(reference)) {
return <RunLink runId={reference} className="font-mono text-xs" />;
}
const safeUrl = toSafeUrl(reference);
if (safeUrl) {
return (
<a
href={safeUrl}
target="_blank"
rel="noopener noreferrer"
className="font-mono text-xs text-indigo-400 underline hover:text-indigo-300"
>
{reference}
</a>
);
}
return <span className="font-mono text-xs text-text-dimmed">{reference}</span>;
}
function DiagnosisActions({ actions }: { actions: NonNullable<DiagnosisBlock["actions"]> }) {
const buttonClass =
"inline-flex items-center rounded border border-border-bright bg-background-bright px-2.5 py-1 text-xs text-text-bright transition-colors hover:border-border-brightest hover:bg-background-hover";
return (
<div className="flex flex-wrap gap-2 pt-1">
{actions.map((action, i) => {
if (action.kind === "view_run" && /^run_[a-z0-9]+$/i.test(action.target)) {
return (
<RunActionButton
key={i}
runId={action.target}
label={action.label}
className={buttonClass}
/>
);
}
if (action.kind === "docs") {
const safeUrl = toSafeUrl(action.target);
if (!safeUrl) return null;
return (
<a
key={i}
href={safeUrl}
target="_blank"
rel="noopener noreferrer"
className={buttonClass}
>
{action.label}
</a>
);
}
return null;
})}
</div>
);
}
function RunActionButton({
runId,
label,
className,
}: {
runId: string;
label: string;
className: string;
}) {
const to = useRunPath(runId);
if (!to) return <span className={className}>{label}</span>;
return (
<Link to={to} className={className}>
{label}
</Link>
);
}
export function RunDiagnosisCard({ block }: { block: DiagnosisBlock }) {
const evidence = block.evidence ?? [];
const nextSteps = block.nextSteps ?? [];
const actions = block.actions ?? [];
return (
<div className="overflow-hidden rounded-lg border border-border-bright bg-background-dimmed">
<div className="flex flex-wrap items-center gap-2 border-b border-grid-bright bg-background-bright px-3 py-2">
<span className="text-xs font-medium text-text-dimmed">Run diagnosis</span>
<Badge variant="small" className="border-rose-500/40 text-rose-400">
{CATEGORY_LABELS[block.category] ?? block.category}
</Badge>
<Badge variant="small" className={cn("uppercase", CONFIDENCE_STYLES[block.confidence])}>
{block.confidence} confidence
</Badge>
{block.runId ? <RunLink runId={block.runId} className="ml-auto font-mono text-xs" /> : null}
</div>
<div className="space-y-3 px-3 py-3">
<p className="text-sm text-text-bright">{block.summary}</p>
<Section title="Likely cause">
<p className="text-sm text-text-dimmed">{block.likelyCause}</p>
</Section>
{evidence.length > 0 ? (
<Section title="Evidence">
<ul className="space-y-1.5">
{evidence.map((item, i) => (
<li key={i} className="text-xs text-text-dimmed">
<span className="mr-1.5 rounded-sm bg-background-raised px-1 py-0.5 text-[10px] uppercase tracking-wide text-text-dimmed">
{EVIDENCE_LABELS[item.type] ?? item.type}
</span>
<span className="text-text-bright">{item.detail}</span>
{item.reference ? (
<span className="ml-1.5">
<EvidenceReference reference={item.reference} />
</span>
) : null}
</li>
))}
</ul>
</Section>
) : null}
{block.impact ? (
<Section title="Impact">
<p className="text-sm text-text-dimmed">{block.impact}</p>
</Section>
) : null}
{nextSteps.length > 0 ? (
<Section title="Next steps">
<ol className="list-decimal space-y-1 pl-4">
{nextSteps.map((step, i) => (
<li key={i} className="text-sm text-text-dimmed">
{step}
</li>
))}
</ol>
</Section>
) : null}
{actions.length > 0 ? <DiagnosisActions actions={actions} /> : null}
</div>
</div>
);
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="space-y-1">
<h4 className="text-xs font-medium uppercase tracking-wide text-text-dimmed">{title}</h4>
{children}
</div>
);
}