style(runs): tighten stages, graph toolbar, billing, and stage sidebar

run-stages:
- replace the full-width tinted System/Assistant cards with a subtler
  left-accent bar and header so dense streams read cleanly
- unify the Running/Timed out/exit/duration indicators under a shared
  StatusPill, and bump stdout/stderr labels from 10px to 11px
- raise the selected stage header from text-sm font-medium to
  text-base font-semibold
- command output preformatted text is now text-sm on mobile

run-overview and run-graph:
- extract the floating direction/fit/zoom controls into a single
  GraphToolbar capsule with internal dividers, shared between both
  graph views
- drop the translucent canvas in favor of solid bg-panel-alt
- wrap the "no workflow graph" message in a proper empty-state panel

run-billing:
- tfoot now uses bg-overlay so totals read heavier than the body
- table headers gain font-medium and a readable fg-3 instead of
  fg-muted
- "By model" heading is a real section heading, not an eyebrow
- add an empty state when a run has no billing yet

stage-sidebar:
- cancelled stages now use NoSymbolIcon so they don't look identical
  to failed stages at a glance

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-19 16:51:55 -04:00
parent 9b5b84da64
commit 174c62e2f7
No known key found for this signature in database
6 changed files with 208 additions and 175 deletions

View file

@ -0,0 +1,84 @@
import { ArrowDownIcon, ArrowRightIcon, MinusIcon, PlusIcon } from "@heroicons/react/20/solid";
type Direction = "LR" | "TB";
export const GRAPH_ZOOM_STEPS = [25, 50, 75, 100, 150, 200];
export const GRAPH_DEFAULT_ZOOM_INDEX = 2;
export function GraphToolbar({
direction,
setDirection,
fitToWindow,
zoomIndex,
setZoomIndex,
}: {
direction: Direction;
setDirection: (d: Direction) => void;
fitToWindow: () => void;
zoomIndex: number;
setZoomIndex: (updater: (i: number) => number) => void;
}) {
const group =
"flex items-center gap-0.5 px-0.5 [&:not(:first-child)]:border-l [&:not(:first-child)]:border-line-strong [&:not(:first-child)]:pl-1 [&:not(:first-child)]:ml-1";
const btn =
"flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-teal-500";
const btnActive = "bg-overlay-strong text-fg-3";
const btnDisabled =
"disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted";
return (
<div
role="toolbar"
aria-label="Graph controls"
className="absolute right-3 top-3 z-10 flex items-center rounded-md border border-line bg-panel p-0.5"
>
<div className={group}>
<button
type="button"
title="Left to right"
onClick={() => setDirection("LR")}
aria-pressed={direction === "LR"}
className={`${btn} ${direction === "LR" ? btnActive : ""}`}
>
<ArrowRightIcon className="size-3.5" aria-hidden="true" />
</button>
<button
type="button"
title="Top to bottom"
onClick={() => setDirection("TB")}
aria-pressed={direction === "TB"}
className={`${btn} ${direction === "TB" ? btnActive : ""}`}
>
<ArrowDownIcon className="size-3.5" aria-hidden="true" />
</button>
</div>
<div className={group}>
<button type="button" title="Fit to window" onClick={fitToWindow} className={btn}>
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" className="size-3.5" aria-hidden="true">
<rect x="1" y="1" width="12" height="12" rx="1.5" strokeWidth="1.5" strokeDasharray="3 2" />
</svg>
</button>
</div>
<div className={group}>
<button
type="button"
title="Zoom out"
onClick={() => setZoomIndex((i) => Math.max(0, i - 1))}
disabled={zoomIndex === 0}
className={`${btn} ${btnDisabled}`}
>
<MinusIcon className="size-4" aria-hidden="true" />
</button>
<button
type="button"
title="Zoom in"
onClick={() => setZoomIndex((i) => Math.min(GRAPH_ZOOM_STEPS.length - 1, i + 1))}
disabled={zoomIndex === GRAPH_ZOOM_STEPS.length - 1}
className={`${btn} ${btnDisabled}`}
>
<PlusIcon className="size-4" aria-hidden="true" />
</button>
</div>
</div>
);
}

View file

@ -1,6 +1,12 @@
import { useState, useEffect, useRef } from "react";
import { Link, useRevalidator } from "react-router";
import { CheckCircleIcon, ArrowPathIcon, PauseCircleIcon, XCircleIcon } from "@heroicons/react/24/solid";
import {
ArrowPathIcon,
CheckCircleIcon,
NoSymbolIcon,
PauseCircleIcon,
XCircleIcon,
} from "@heroicons/react/24/solid";
import { DocumentTextIcon, MapIcon } from "@heroicons/react/24/outline";
import { formatDurationSecs } from "../lib/format";
@ -19,7 +25,7 @@ export const statusConfig: Record<StageStatus, { icon: typeof CheckCircleIcon; c
running: { icon: ArrowPathIcon, color: "text-teal-500" },
pending: { icon: PauseCircleIcon, color: "text-fg-muted" },
failed: { icon: XCircleIcon, color: "text-coral" },
cancelled: { icon: XCircleIcon, color: "text-fg-muted" },
cancelled: { icon: NoSymbolIcon, color: "text-fg-muted" },
};
interface StageSidebarProps {

View file

@ -39,12 +39,21 @@ export async function loader({ request, params }: any) {
export default function RunBilling({ loaderData }: any) {
const { stages, totalRuntime, totalUsdMicros, totalInput, totalOutput, modelBreakdown } =
loaderData;
if (!stages.length) {
return (
<div className="rounded-md border border-line bg-panel-alt p-10 text-center">
<p className="text-sm/6 text-fg-3">No billing recorded for this run yet.</p>
</div>
);
}
return (
<div className="space-y-6">
<div className="overflow-hidden rounded-md border border-line">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-line bg-panel/60 text-left text-xs text-fg-muted">
<tr className="border-b border-line bg-panel/60 text-left text-xs font-medium text-fg-3">
<th className="px-4 py-2.5 font-medium">Stage</th>
<th className="px-4 py-2.5 font-medium">Model</th>
<th className="px-4 py-2.5 font-medium text-right">Tokens</th>
@ -69,7 +78,7 @@ export default function RunBilling({ loaderData }: any) {
))}
</tbody>
<tfoot>
<tr className="border-t border-line-strong bg-panel/40">
<tr className="border-t border-line-strong bg-overlay">
<td className="px-4 py-3 font-medium text-fg">Total</td>
<td />
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-fg">
@ -88,13 +97,11 @@ export default function RunBilling({ loaderData }: any) {
</div>
<div>
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-fg-muted">
By Model
</h3>
<h3 className="mb-3 text-sm font-semibold text-fg">By model</h3>
<div className="overflow-hidden rounded-md border border-line">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-line bg-panel/60 text-left text-xs text-fg-muted">
<tr className="border-b border-line bg-panel/60 text-left text-xs font-medium text-fg-3">
<th className="px-4 py-2.5 font-medium">Model</th>
<th className="px-4 py-2.5 font-medium text-right">Stages</th>
<th className="px-4 py-2.5 font-medium text-right">Tokens</th>
@ -119,7 +126,7 @@ export default function RunBilling({ loaderData }: any) {
))}
</tbody>
<tfoot>
<tr className="border-t border-line-strong bg-panel/40">
<tr className="border-t border-line-strong bg-overlay">
<td className="px-4 py-3 font-medium text-fg">Total</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-fg">
{stages.length}

View file

@ -1,6 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useParams } from "react-router";
import { ArrowDownIcon, ArrowRightIcon, MinusIcon, PlusIcon } from "@heroicons/react/20/solid";
import { useTheme } from "../lib/theme";
import { getGraphTheme } from "../lib/graph-theme";
import { apiFetch, apiJsonOrNull } from "../api";
@ -8,6 +7,11 @@ import { isVisibleStage } from "../data/runs";
import { formatDurationSecs } from "../lib/format";
import { StageSidebar } from "../components/stage-sidebar";
import type { Stage } from "../components/stage-sidebar";
import {
GRAPH_DEFAULT_ZOOM_INDEX,
GRAPH_ZOOM_STEPS,
GraphToolbar,
} from "../components/graph-toolbar";
import type { PaginatedRunStageList } from "@qltysh/fabro-api-client";
export const handle = { wide: true };
@ -169,9 +173,6 @@ function annotateRunningNodes(svg: SVGSVGElement, gt: ReturnType<typeof getGraph
}
const ZOOM_STEPS = [25, 50, 75, 100, 150, 200];
const DEFAULT_ZOOM_INDEX = 2;
export default function RunGraph({ loaderData }: any) {
const { id } = useParams();
const { stages, graphSvg } = loaderData;
@ -179,11 +180,11 @@ export default function RunGraph({ loaderData }: any) {
const innerRef = useRef<HTMLDivElement>(null);
const svgRef = useRef<SVGSVGElement | null>(null);
const [error, setError] = useState<string | null>(null);
const [zoomIndex, setZoomIndex] = useState(DEFAULT_ZOOM_INDEX);
const [zoomIndex, setZoomIndex] = useState(GRAPH_DEFAULT_ZOOM_INDEX);
const [direction, setDirection] = useState<Direction>("LR");
const [pan, setPan] = useState({ x: 0, y: 0 });
const dragState = useRef<{ startX: number; startY: number; startPanX: number; startPanY: number } | null>(null);
const zoom = ZOOM_STEPS[zoomIndex];
const zoom = GRAPH_ZOOM_STEPS[zoomIndex];
const { theme } = useTheme();
const graphTheme = getGraphTheme(theme);
@ -264,8 +265,8 @@ export default function RunGraph({ loaderData }: any) {
const fitPct = Math.min(containerW / svgW, containerH / svgH) * 100;
let best = 0;
for (let i = ZOOM_STEPS.length - 1; i >= 0; i--) {
if (ZOOM_STEPS[i] <= fitPct) { best = i; break; }
for (let i = GRAPH_ZOOM_STEPS.length - 1; i >= 0; i--) {
if (GRAPH_ZOOM_STEPS[i] <= fitPct) { best = i; break; }
}
setZoomIndex(best);
setPan({ x: 0, y: 0 });
@ -280,61 +281,14 @@ export default function RunGraph({ loaderData }: any) {
<StageSidebar stages={stages} runId={id!} activeLink="graph" />
<div className="min-w-0 flex-1">
<div className="graph-svg relative rounded-md border border-line bg-panel-alt/40">
<div className="absolute right-3 top-3 z-10 flex items-center gap-2">
<div className="flex items-center gap-0.5 rounded-md border border-line bg-panel/90 p-0.5">
<button
type="button"
title="Left to right"
onClick={() => setDirection("LR")}
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "LR" ? "bg-overlay-strong text-fg-3" : "text-fg-muted hover:bg-overlay hover:text-fg-3"}`}
>
<ArrowRightIcon className="size-3.5" />
</button>
<button
type="button"
title="Top to bottom"
onClick={() => setDirection("TB")}
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "TB" ? "bg-overlay-strong text-fg-3" : "text-fg-muted hover:bg-overlay hover:text-fg-3"}`}
>
<ArrowDownIcon className="size-3.5" />
</button>
</div>
<div className="flex items-center rounded-md border border-line bg-panel/90 p-0.5">
<button
type="button"
title="Fit to window"
onClick={fitToWindow}
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3"
>
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" className="size-3.5" aria-hidden="true">
<rect x="1" y="1" width="12" height="12" rx="1.5" strokeWidth="1.5" strokeDasharray="3 2" />
</svg>
</button>
</div>
<div className="flex items-center gap-0.5 rounded-md border border-line bg-panel/90 p-0.5">
<button
type="button"
title="Zoom out"
onClick={() => setZoomIndex((i) => Math.max(0, i - 1))}
disabled={zoomIndex === 0}
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted"
>
<MinusIcon className="size-4" />
</button>
<button
type="button"
title="Zoom in"
onClick={() => setZoomIndex((i) => Math.min(ZOOM_STEPS.length - 1, i + 1))}
disabled={zoomIndex === ZOOM_STEPS.length - 1}
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted"
>
<PlusIcon className="size-4" />
</button>
</div>
</div>
<div className="graph-svg relative rounded-md border border-line bg-panel-alt">
<GraphToolbar
direction={direction}
setDirection={setDirection}
fitToWindow={fitToWindow}
zoomIndex={zoomIndex}
setZoomIndex={setZoomIndex}
/>
<div
ref={containerRef}

View file

@ -1,6 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router";
import { ArrowDownIcon, ArrowRightIcon, MinusIcon, PlusIcon } from "@heroicons/react/20/solid";
import { apiFetch, apiJsonOrNull } from "../api";
import { isVisibleStage } from "../data/runs";
import { formatDurationSecs } from "../lib/format";
@ -8,6 +7,11 @@ import { useTheme } from "../lib/theme";
import { getGraphTheme } from "../lib/graph-theme";
import { StageSidebar } from "../components/stage-sidebar";
import type { Stage } from "../components/stage-sidebar";
import {
GRAPH_DEFAULT_ZOOM_INDEX,
GRAPH_ZOOM_STEPS,
GraphToolbar,
} from "../components/graph-toolbar";
import type { PaginatedRunStageList } from "@qltysh/fabro-api-client";
export const handle = { wide: true };
@ -33,9 +37,6 @@ export async function loader({ request, params }: any) {
return { stages, graphSvg, runStatus };
}
const ZOOM_STEPS = [25, 50, 75, 100, 150, 200];
const DEFAULT_ZOOM_INDEX = 2;
type Direction = "LR" | "TB";
export default function RunOverview({ loaderData }: any) {
@ -46,11 +47,11 @@ export default function RunOverview({ loaderData }: any) {
const svgRef = useRef<SVGSVGElement | null>(null);
const navigate = useNavigate();
const { theme } = useTheme();
const [zoomIndex, setZoomIndex] = useState(DEFAULT_ZOOM_INDEX);
const [zoomIndex, setZoomIndex] = useState(GRAPH_DEFAULT_ZOOM_INDEX);
const [direction, setDirection] = useState<Direction>("LR");
const [pan, setPan] = useState({ x: 0, y: 0 });
const dragState = useRef<{ startX: number; startY: number; startPanX: number; startPanY: number } | null>(null);
const zoom = ZOOM_STEPS[zoomIndex];
const zoom = GRAPH_ZOOM_STEPS[zoomIndex];
// Render SVG with stage annotations
useEffect(() => {
@ -190,8 +191,8 @@ export default function RunOverview({ loaderData }: any) {
const fitPct = Math.min(containerW / svgW, containerH / svgH) * 100;
let best = 0;
for (let i = ZOOM_STEPS.length - 1; i >= 0; i--) {
if (ZOOM_STEPS[i] <= fitPct) { best = i; break; }
for (let i = GRAPH_ZOOM_STEPS.length - 1; i >= 0; i--) {
if (GRAPH_ZOOM_STEPS[i] <= fitPct) { best = i; break; }
}
setZoomIndex(best);
setPan({ x: 0, y: 0 });
@ -203,61 +204,14 @@ export default function RunOverview({ loaderData }: any) {
<div className="min-w-0 flex-1">
{graphSvg ? (
<div className="graph-svg relative rounded-md border border-line bg-panel-alt/40">
<div className="absolute right-3 top-3 z-10 flex items-center gap-2">
<div className="flex items-center gap-0.5 rounded-md border border-line bg-panel/90 p-0.5">
<button
type="button"
title="Left to right"
onClick={() => setDirection("LR")}
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "LR" ? "bg-overlay-strong text-fg-3" : "text-fg-muted hover:bg-overlay hover:text-fg-3"}`}
>
<ArrowRightIcon className="size-3.5" />
</button>
<button
type="button"
title="Top to bottom"
onClick={() => setDirection("TB")}
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "TB" ? "bg-overlay-strong text-fg-3" : "text-fg-muted hover:bg-overlay hover:text-fg-3"}`}
>
<ArrowDownIcon className="size-3.5" />
</button>
</div>
<div className="flex items-center rounded-md border border-line bg-panel/90 p-0.5">
<button
type="button"
title="Fit to window"
onClick={fitToWindow}
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3"
>
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" className="size-3.5" aria-hidden="true">
<rect x="1" y="1" width="12" height="12" rx="1.5" strokeWidth="1.5" strokeDasharray="3 2" />
</svg>
</button>
</div>
<div className="flex items-center gap-0.5 rounded-md border border-line bg-panel/90 p-0.5">
<button
type="button"
title="Zoom out"
onClick={() => setZoomIndex((i) => Math.max(0, i - 1))}
disabled={zoomIndex === 0}
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted"
>
<MinusIcon className="size-4" />
</button>
<button
type="button"
title="Zoom in"
onClick={() => setZoomIndex((i) => Math.min(ZOOM_STEPS.length - 1, i + 1))}
disabled={zoomIndex === ZOOM_STEPS.length - 1}
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted"
>
<PlusIcon className="size-4" />
</button>
</div>
</div>
<div className="graph-svg relative rounded-md border border-line bg-panel-alt">
<GraphToolbar
direction={direction}
setDirection={setDirection}
fitToWindow={fitToWindow}
zoomIndex={zoomIndex}
setZoomIndex={setZoomIndex}
/>
<div
ref={containerRef}
@ -276,7 +230,9 @@ export default function RunOverview({ loaderData }: any) {
</div>
</div>
) : (
<p className="text-sm text-fg-muted">No workflow graph available.</p>
<div className="rounded-md border border-line bg-panel-alt p-10 text-center">
<p className="text-sm/6 text-fg-3">No workflow graph available.</p>
</div>
)}
</div>
</div>

View file

@ -176,28 +176,52 @@ function Markdown({ content }: { content: string }) {
function SystemBlock({ content }: { content: string }) {
return (
<div className="rounded-md border border-amber/10 bg-amber/5 overflow-hidden">
<div className="flex items-center gap-2 px-3 py-2">
<section className="border-l-2 border-amber/50 pl-4">
<header className="mb-1.5 flex items-center gap-2">
<CommandLineIcon className="size-4 shrink-0 text-amber" />
<span className="text-xs font-medium text-fg-3">System Prompt</span>
</div>
<div className="border-t border-line px-3 py-2.5">
<Markdown content={content} />
</div>
</div>
<span className="text-xs font-medium text-fg-3">System prompt</span>
</header>
<Markdown content={content} />
</section>
);
}
function AssistantBlock({ content }: { content: string }) {
return (
<div className="rounded-md border border-teal-500/10 bg-teal-500/5 overflow-hidden">
<div className="flex items-center gap-2 px-3 py-2">
<section className="border-l-2 border-teal-500/50 pl-4">
<header className="mb-1.5 flex items-center gap-2">
<ChatBubbleLeftIcon className="size-4 shrink-0 text-teal-500" />
<span className="text-xs font-medium text-fg-3">Assistant</span>
</div>
<div className="border-t border-line px-3 py-2.5">
<Markdown content={content} />
</div>
</header>
<Markdown content={content} />
</section>
);
}
function StatusPill({
tone,
children,
}: {
tone: "running" | "failed" | "success" | "neutral";
children: React.ReactNode;
}) {
const toneClass = {
running: "bg-teal-500/15 text-teal-500",
failed: "bg-coral/15 text-coral",
success: "bg-mint/15 text-mint",
neutral: "bg-overlay text-fg-3",
}[tone];
return (
<span className={`rounded px-1.5 py-0.5 text-[11px] font-medium ${toneClass}`}>
{children}
</span>
);
}
function StreamLabel({ label }: { label: string }) {
return (
<div className="mb-1 font-mono text-[11px] uppercase tracking-wider text-fg-muted">
{label}
</div>
);
}
@ -215,46 +239,48 @@ function CommandBlock({ turn }: { turn: Extract<TurnType, { kind: "command" }> }
<span className="text-xs font-medium text-fg-3">
{turn.language === "python" ? "Python" : "Shell"}
</span>
{turn.running && (
<span className="ml-auto text-[11px] font-medium text-teal-500 animate-pulse">Running...</span>
)}
{!turn.running && turn.timedOut && (
<span className="ml-auto rounded bg-coral/15 px-1.5 py-0.5 text-[11px] font-medium text-coral">Timed out</span>
)}
{!turn.running && !turn.timedOut && (
<div className="ml-auto flex items-center gap-2">
<span className={`rounded px-1.5 py-0.5 text-[11px] font-medium ${failed ? "bg-coral/15 text-coral" : "bg-mint/15 text-mint"}`}>
exit {turn.exitCode ?? "?"}
</span>
{turn.durationMs != null && (
<span className="text-[11px] tabular-nums text-fg-muted">
{turn.durationMs < 1000 ? `${turn.durationMs}ms` : `${(turn.durationMs / 1000).toFixed(1)}s`}
</span>
)}
</div>
)}
<div className="ml-auto flex items-center gap-2">
{turn.running ? (
<StatusPill tone="running">Running</StatusPill>
) : turn.timedOut ? (
<StatusPill tone="failed">Timed out</StatusPill>
) : (
<>
<StatusPill tone={failed ? "failed" : "success"}>
exit {turn.exitCode ?? "?"}
</StatusPill>
{turn.durationMs != null && (
<StatusPill tone="neutral">
{turn.durationMs < 1000
? `${turn.durationMs}ms`
: `${(turn.durationMs / 1000).toFixed(1)}s`}
</StatusPill>
)}
</>
)}
</div>
</div>
{/* Script */}
{turn.script && (
<div className="border-t border-line px-3 py-2.5">
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-fg-3">{turn.script}</pre>
<pre className="whitespace-pre-wrap font-mono text-sm leading-relaxed text-fg-3 sm:text-xs">{turn.script}</pre>
</div>
)}
{/* stdout */}
{turn.stdout && (
<div className="border-t border-line px-3 py-2.5">
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-fg-muted">stdout</div>
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-fg-3">{turn.stdout}</pre>
<StreamLabel label="stdout" />
<pre className="whitespace-pre-wrap font-mono text-sm leading-relaxed text-fg-3 sm:text-xs">{turn.stdout}</pre>
</div>
)}
{/* stderr */}
{turn.stderr && (
<div className="border-t border-line px-3 py-2.5">
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-fg-muted">stderr</div>
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-coral">{turn.stderr}</pre>
<StreamLabel label="stderr" />
<pre className="whitespace-pre-wrap font-mono text-sm leading-relaxed text-coral sm:text-xs">{turn.stderr}</pre>
</div>
)}
</div>
@ -303,8 +329,8 @@ export default function RunStages({ loaderData }: any) {
<div className="min-w-0 flex-1 space-y-3">
<div className="flex items-center gap-2">
<SelectedIcon className={`size-5 ${selectedConfig.color} ${isRunning ? "animate-spin" : ""}`} />
<h3 className="text-sm font-medium text-fg">{selectedStage.name}</h3>
<span className="font-mono text-xs text-fg-muted">{headerDuration}</span>
<h3 className="text-base font-semibold text-fg">{selectedStage.name}</h3>
<span className="font-mono text-xs tabular-nums text-fg-muted">{headerDuration}</span>
</div>
{turns.map((turn: TurnType, i: number) => {