diff options
| author | Adam Malczewski <[email protected]> | 2026-06-22 11:47:34 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-22 11:47:34 +0900 |
| commit | 5d47c500b2a313c9f09a6d8564007a0426de5f22 (patch) | |
| tree | 095f6db688c9940bc9cc2248f21b923b0b7215a5 | |
| parent | 82802a14bc5921c6b62756c3a1a8953c087b5b0d (diff) | |
| download | dispatch-web-5d47c500b2a313c9f09a6d8564007a0426de5f22.tar.gz dispatch-web-5d47c500b2a313c9f09a6d8564007a0426de5f22.zip | |
feat(metrics): show turn number in metrics bubble (turn N · ...)
The turn number comes from the entry's position in the metrics array
(1-based), which is correct regardless of trimming since stepId matching
aligns segments to the right entry. Now displays 'turn 3 · 12k tok' instead
of just 'turn · 12k tok'.
| -rw-r--r-- | src/core/metrics/format.ts | 3 | ||||
| -rw-r--r-- | src/core/metrics/place.ts | 1 | ||||
| -rw-r--r-- | src/core/metrics/types.ts | 3 | ||||
| -rw-r--r-- | src/features/chat/ui.test.ts | 8 | ||||
| -rw-r--r-- | src/features/chat/ui/ChatView.svelte | 4 |
5 files changed, 12 insertions, 7 deletions
diff --git a/src/core/metrics/format.ts b/src/core/metrics/format.ts index 4d69f25..534277c 100644 --- a/src/core/metrics/format.ts +++ b/src/core/metrics/format.ts @@ -155,7 +155,7 @@ export function viewExpectedCache(current: Usage, prev: Usage | null): CacheRate } /** Build a formatted view of a turn's aggregate metrics. */ -export function viewTurnMetrics(turn: TurnMetrics): TurnMetricsView { +export function viewTurnMetrics(turn: TurnMetrics, turnNumber?: number): TurnMetricsView { const total = totalTokens(turn.usage); let totalGenMs: number | undefined; for (const step of turn.steps) { @@ -166,6 +166,7 @@ export function viewTurnMetrics(turn: TurnMetrics): TurnMetricsView { } const tps = computeTps(turn.usage.outputTokens, totalGenMs); return { + label: turnNumber !== undefined ? `turn ${turnNumber}` : "turn", tokensLabel: `${formatTokens(total)} tok`, breakdown: formatBreakdown(turn.usage), tps: formatTps(tps), diff --git a/src/core/metrics/place.ts b/src/core/metrics/place.ts index cb16b30..1009b15 100644 --- a/src/core/metrics/place.ts +++ b/src/core/metrics/place.ts @@ -232,6 +232,7 @@ export function interleaveTurnMetrics( rows.push({ kind: "turn-metrics", turn: entry.total, + turnNumber: entryIdx + 1, cumulativeUsage: cumulativeByEntry[entryIdx] ?? entry.total.usage, prevTurnUsage: prevUsageByEntry[entryIdx] ?? null, }); diff --git a/src/core/metrics/types.ts b/src/core/metrics/types.ts index c22fd9f..5b96e0f 100644 --- a/src/core/metrics/types.ts +++ b/src/core/metrics/types.ts @@ -56,6 +56,8 @@ export type MetricsRow = | { readonly kind: "turn-metrics"; readonly turn: TurnMetrics; + /** 1-based turn number (the entry's position in the metrics array + 1). */ + readonly turnNumber: number; /** Cumulative usage across all finalized turns up to and including this one. */ readonly cumulativeUsage: Usage; /** @@ -87,6 +89,7 @@ export interface StepMetricsView { /** Formatted per-turn view for display. */ export interface TurnMetricsView { + readonly label: string; readonly tokensLabel: string; readonly breakdown: string; readonly tps: string | null; diff --git a/src/features/chat/ui.test.ts b/src/features/chat/ui.test.ts index 94db0ae..2891e5b 100644 --- a/src/features/chat/ui.test.ts +++ b/src/features/chat/ui.test.ts @@ -374,7 +374,7 @@ describe("ChatView", () => { expect(screen.getByText("Hello!")).toBeInTheDocument(); expect(screen.getByText(/step 1/)).toBeInTheDocument(); expect(screen.getAllByText(/150 tok/)).toHaveLength(2); - expect(screen.getByText(/turn · 150 tok \(100 in \/ 50 out\)/)).toBeInTheDocument(); + expect(screen.getByText(/turn 1 · 150 tok \(100 in \/ 50 out\)/)).toBeInTheDocument(); expect(screen.getByText(/1\.2s/)).toBeInTheDocument(); }); @@ -478,11 +478,11 @@ describe("ChatView", () => { // Both step-metrics and turn-metrics render expect(screen.getByText(/step 1/)).toBeInTheDocument(); - expect(screen.getByText(/turn · 100 tok/)).toBeInTheDocument(); + expect(screen.getByText(/turn 1 · 100 tok/)).toBeInTheDocument(); // They are in separate elements (different rows) const stepEl = screen.getByText(/step 1 · 100 tok/).closest("div"); - const turnEl = screen.getByText(/turn · 100 tok/).closest("div"); + const turnEl = screen.getByText(/turn 1 · 100 tok/).closest("div"); expect(stepEl).not.toBe(turnEl); }); @@ -556,7 +556,7 @@ describe("ChatView", () => { expect(screen.getByText(/step 1/)).toBeInTheDocument(); expect(screen.getAllByText(/15 tok/)).toHaveLength(2); // Turn metrics rendered - expect(screen.getByText(/turn · 15 tok \(10 in \/ 5 out\)/)).toBeInTheDocument(); + expect(screen.getByText(/turn 1 · 15 tok \(10 in \/ 5 out\)/)).toBeInTheDocument(); // No "null" or "undefined" in the DOM expect(screen.queryByText("null")).toBeNull(); expect(screen.queryByText("undefined")).toBeNull(); diff --git a/src/features/chat/ui/ChatView.svelte b/src/features/chat/ui/ChatView.svelte index d1d7709..2a17ac7 100644 --- a/src/features/chat/ui/ChatView.svelte +++ b/src/features/chat/ui/ChatView.svelte @@ -185,7 +185,7 @@ </div> </div> {:else if row.kind === "turn-metrics"} - {@const turnView = viewTurnMetrics(row.turn)} + {@const turnView = viewTurnMetrics(row.turn, row.turnNumber)} {@const lastCache = viewCacheRate(row.turn.usage)} {@const chatCache = viewCacheRate(row.cumulativeUsage)} {@const retention = viewExpectedCache(row.turn.usage, row.prevTurnUsage)} @@ -193,7 +193,7 @@ <div class="chat-bubble w-full max-w-5xl bg-transparent p-0"> <div class="flex flex-col gap-1 text-xs"> <div class="opacity-70"> - turn · {turnView.tokensLabel} ({turnView.breakdown}) + {turnView.label} · {turnView.tokensLabel} ({turnView.breakdown}) {#if turnView.tps} · {turnView.tps}{/if} {#if turnView.duration} · {turnView.duration}{/if} </div> |
