Skip to content

Commit e4a1616

Browse files
Merge pull request #133 from corbitsdev/cl-11450-make-the-trace-header-describe-the-whole-run
fix: describe the whole run in the trace header
2 parents b404ee3 + ff6d038 commit e4a1616

5 files changed

Lines changed: 115 additions & 8 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, expect, test } from "bun:test";
2+
3+
import {
4+
formatReportedCost,
5+
formatStarted,
6+
formatStepCount,
7+
} from "./format.js";
8+
9+
describe("formatReportedCost", () => {
10+
test("is a dash when no span reports a cost", () => {
11+
expect(formatReportedCost([{}, { usage: {} }])).toBe("—");
12+
});
13+
14+
test("shows zero when a span reports zero", () => {
15+
expect(formatReportedCost([{ usage: { costUsd: 0 } }])).toBe("$0");
16+
});
17+
18+
test("sums the costs that were reported", () => {
19+
expect(
20+
formatReportedCost([
21+
{ usage: { costUsd: 1.5 } },
22+
{ usage: {} },
23+
{ usage: { costUsd: 2.25 } },
24+
]),
25+
).toBe("$3.75");
26+
});
27+
});
28+
29+
describe("formatStepCount", () => {
30+
test("uses the run total when the page is shorter", () => {
31+
expect(formatStepCount({ stepCount: 12, spans: [{}, {}] })).toBe("12");
32+
});
33+
34+
test("counts the loaded spans when no total was passed", () => {
35+
expect(formatStepCount({ spans: [{}, {}, {}] })).toBe("3");
36+
});
37+
});
38+
39+
describe("formatStarted", () => {
40+
test("returns an unparseable value unchanged", () => {
41+
expect(formatStarted("not-a-time")).toBe("not-a-time");
42+
});
43+
44+
test("formats a timestamp as a local date and time", () => {
45+
const shown = formatStarted("2026-10-24T15:04:00.000Z");
46+
expect(shown.includes("T")).toBe(false);
47+
expect(shown.includes("2026-10-24")).toBe(false);
48+
expect(/\bat\b/i.test(shown)).toBe(false);
49+
expect(shown.includes(" · ")).toBe(true);
50+
});
51+
52+
test("leaves a clock time unchanged", () => {
53+
expect(formatStarted("09:14:02")).toBe("09:14:02");
54+
});
55+
});

‎src/ui/observability/format.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,44 @@ export function formatCost(usd: number): string {
3232
return `$${usd.toFixed(2)}`;
3333
}
3434

35+
export function formatReportedCost(
36+
spans: readonly { usage?: { costUsd?: number } }[],
37+
): string {
38+
let total = 0;
39+
let known = false;
40+
for (const span of spans) {
41+
const cost = span.usage?.costUsd;
42+
if (cost === undefined) continue;
43+
known = true;
44+
total += cost;
45+
}
46+
if (!known) return "—";
47+
return formatCost(total);
48+
}
49+
50+
export function formatStepCount(trace: {
51+
stepCount?: number;
52+
spans: readonly unknown[];
53+
}): string {
54+
if (trace.stepCount !== undefined) return String(trace.stepCount);
55+
return String(trace.spans.length);
56+
}
57+
58+
export function formatStarted(value: string): string {
59+
const parsed = Date.parse(value);
60+
if (Number.isNaN(parsed)) return value;
61+
const date = new Date(parsed);
62+
const day = new Intl.DateTimeFormat(undefined, {
63+
month: "short",
64+
day: "numeric",
65+
}).format(date);
66+
const time = new Intl.DateTimeFormat(undefined, {
67+
hour: "numeric",
68+
minute: "2-digit",
69+
}).format(date);
70+
return `${day} · ${time}`;
71+
}
72+
3573
export function spanTokens(span: {
3674
usage?: { inputTokens?: number; outputTokens?: number };
3775
}): number {

‎src/ui/observability/model.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ export type Trace = {
230230
readonly startedAt: string;
231231
readonly durationMs: number;
232232
readonly spans: readonly Span[];
233+
/** Steps in the whole run. Omit to count `spans`. */
234+
readonly stepCount?: number;
233235
readonly placement?: Placement;
234236
readonly principal?: PrincipalRef;
235237
readonly tenant?: TenantRef;

‎src/ui/observability/trace-header.tsx‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Badge } from "../badge.js";
22
import {
3-
formatCost,
43
formatDuration,
4+
formatReportedCost,
5+
formatStarted,
6+
formatStepCount,
57
formatTokens,
68
spanTokens,
79
} from "./format.js";
@@ -26,10 +28,6 @@ export const STATUS_TONE = {
2628

2729
export function TraceHeader({ trace }: { readonly trace: Trace }) {
2830
const tokens = trace.spans.reduce((sum, span) => sum + spanTokens(span), 0);
29-
const cost = trace.spans.reduce(
30-
(sum, span) => sum + (span.usage?.costUsd ?? 0),
31-
0,
32-
);
3331
return (
3432
<header className="flex flex-col gap-3 border-b border-border px-4 py-3">
3533
<div className="flex flex-wrap items-end justify-between gap-4">
@@ -46,14 +44,14 @@ export function TraceHeader({ trace }: { readonly trace: Trace }) {
4644
</Badge>
4745
</div>
4846
<p className="font-mono text-xs text-muted-foreground">
49-
{trace.actor} · {trace.startedAt}
47+
{trace.actor} · {formatStarted(trace.startedAt)}
5048
</p>
5149
</div>
5250
<dl className="flex gap-6 text-right">
5351
<Metric label="Duration" value={formatDuration(trace.durationMs)} />
5452
<Metric label="Tokens" value={formatTokens(tokens)} />
55-
<Metric label="Cost" value={formatCost(cost)} />
56-
<Metric label="Steps" value={String(trace.spans.length)} />
53+
<Metric label="Cost" value={formatReportedCost(trace.spans)} />
54+
<Metric label="Steps" value={formatStepCount(trace)} />
5755
</dl>
5856
</div>
5957
</header>

‎stories/observability/views/run.stories.tsx‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ export const NameOnly = () => (
3434
);
3535
NameOnly.storyName = "Name only";
3636

37+
export const ParseableStart = () => {
38+
const run = {
39+
...review,
40+
startedAt: "2026-10-24T15:04:00.000Z",
41+
spans: [span("turn", review), span("sub", review)],
42+
};
43+
return (
44+
<div className="border border-border">
45+
<TraceHeader trace={run} />
46+
</div>
47+
);
48+
};
49+
ParseableStart.storyName = "Parseable start";
50+
3751
export const WithSteps = () => (
3852
<div className="h-[640px] border border-border">
3953
<Run trace={review}>

0 commit comments

Comments
 (0)