diff --git a/src/ui/observability/format.test.ts b/src/ui/observability/format.test.ts
new file mode 100644
index 00000000..a361e756
--- /dev/null
+++ b/src/ui/observability/format.test.ts
@@ -0,0 +1,55 @@
+import { describe, expect, test } from "bun:test";
+
+import {
+ formatReportedCost,
+ formatStarted,
+ formatStepCount,
+} from "./format.js";
+
+describe("formatReportedCost", () => {
+ test("is a dash when no span reports a cost", () => {
+ expect(formatReportedCost([{}, { usage: {} }])).toBe("—");
+ });
+
+ test("shows zero when a span reports zero", () => {
+ expect(formatReportedCost([{ usage: { costUsd: 0 } }])).toBe("$0");
+ });
+
+ test("sums the costs that were reported", () => {
+ expect(
+ formatReportedCost([
+ { usage: { costUsd: 1.5 } },
+ { usage: {} },
+ { usage: { costUsd: 2.25 } },
+ ]),
+ ).toBe("$3.75");
+ });
+});
+
+describe("formatStepCount", () => {
+ test("uses the run total when the page is shorter", () => {
+ expect(formatStepCount({ stepCount: 12, spans: [{}, {}] })).toBe("12");
+ });
+
+ test("counts the loaded spans when no total was passed", () => {
+ expect(formatStepCount({ spans: [{}, {}, {}] })).toBe("3");
+ });
+});
+
+describe("formatStarted", () => {
+ test("returns an unparseable value unchanged", () => {
+ expect(formatStarted("not-a-time")).toBe("not-a-time");
+ });
+
+ test("formats a timestamp as a local date and time", () => {
+ const shown = formatStarted("2026-10-24T15:04:00.000Z");
+ expect(shown.includes("T")).toBe(false);
+ expect(shown.includes("2026-10-24")).toBe(false);
+ expect(/\bat\b/i.test(shown)).toBe(false);
+ expect(shown.includes(" · ")).toBe(true);
+ });
+
+ test("leaves a clock time unchanged", () => {
+ expect(formatStarted("09:14:02")).toBe("09:14:02");
+ });
+});
diff --git a/src/ui/observability/format.ts b/src/ui/observability/format.ts
index 62d6a29f..5692622c 100644
--- a/src/ui/observability/format.ts
+++ b/src/ui/observability/format.ts
@@ -32,6 +32,44 @@ export function formatCost(usd: number): string {
return `$${usd.toFixed(2)}`;
}
+export function formatReportedCost(
+ spans: readonly { usage?: { costUsd?: number } }[],
+): string {
+ let total = 0;
+ let known = false;
+ for (const span of spans) {
+ const cost = span.usage?.costUsd;
+ if (cost === undefined) continue;
+ known = true;
+ total += cost;
+ }
+ if (!known) return "—";
+ return formatCost(total);
+}
+
+export function formatStepCount(trace: {
+ stepCount?: number;
+ spans: readonly unknown[];
+}): string {
+ if (trace.stepCount !== undefined) return String(trace.stepCount);
+ return String(trace.spans.length);
+}
+
+export function formatStarted(value: string): string {
+ const parsed = Date.parse(value);
+ if (Number.isNaN(parsed)) return value;
+ const date = new Date(parsed);
+ const day = new Intl.DateTimeFormat(undefined, {
+ month: "short",
+ day: "numeric",
+ }).format(date);
+ const time = new Intl.DateTimeFormat(undefined, {
+ hour: "numeric",
+ minute: "2-digit",
+ }).format(date);
+ return `${day} · ${time}`;
+}
+
export function spanTokens(span: {
usage?: { inputTokens?: number; outputTokens?: number };
}): number {
diff --git a/src/ui/observability/model.ts b/src/ui/observability/model.ts
index ff7202aa..00e76114 100644
--- a/src/ui/observability/model.ts
+++ b/src/ui/observability/model.ts
@@ -230,6 +230,8 @@ export type Trace = {
readonly startedAt: string;
readonly durationMs: number;
readonly spans: readonly Span[];
+ /** Steps in the whole run. Omit to count `spans`. */
+ readonly stepCount?: number;
readonly placement?: Placement;
readonly principal?: PrincipalRef;
readonly tenant?: TenantRef;
diff --git a/src/ui/observability/trace-header.tsx b/src/ui/observability/trace-header.tsx
index f448d0a9..3276cffb 100644
--- a/src/ui/observability/trace-header.tsx
+++ b/src/ui/observability/trace-header.tsx
@@ -1,7 +1,9 @@
import { Badge } from "../badge.js";
import {
- formatCost,
formatDuration,
+ formatReportedCost,
+ formatStarted,
+ formatStepCount,
formatTokens,
spanTokens,
} from "./format.js";
@@ -26,10 +28,6 @@ export const STATUS_TONE = {
export function TraceHeader({ trace }: { readonly trace: Trace }) {
const tokens = trace.spans.reduce((sum, span) => sum + spanTokens(span), 0);
- const cost = trace.spans.reduce(
- (sum, span) => sum + (span.usage?.costUsd ?? 0),
- 0,
- );
return (
- {trace.actor} · {trace.startedAt}
+ {trace.actor} · {formatStarted(trace.startedAt)}