From 48c6d85c3cc5a57a729f14068e2346b17ed62088 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sun, 7 Jun 2026 18:41:27 +0900 Subject: feat(chat): live turn metrics — telemetry reducer + rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consume wire/transport-contract 0.3.0 (step-complete event + timing fields on usage/tool-result/done). Pure core/telemetry module: foldMetricEvent (reducer) + derived selectors (stepTps, turnTps, etc). TelemetryState is pure data, no active-turn tracking — consumers pass turnId to selectors. ChatStore wires foldMetricEvent into handleDelta and exposes telemetry + currentTurnId. ChatView shows step-metrics footer (time/TPS/tokens) on assistant text bubbles and durationMs badge on tool cards. New TurnSummary component renders turn-level stats (wall-clock, tokens, steps, TPS) in a DaisyUI stats block. Extended live-probe to verify telemetry events against bin/up (pending backend restart). 336 tests, typecheck 0, biome clean, build ok. --- src/app/App.svelte | 12 +- src/core/chunks/reducer.ts | 4 + src/core/telemetry/index.ts | 14 ++ src/core/telemetry/reducer.test.ts | 252 ++++++++++++++++++++++++++++++++ src/core/telemetry/reducer.ts | 122 ++++++++++++++++ src/core/telemetry/selectors.ts | 95 ++++++++++++ src/core/telemetry/types.ts | 35 +++++ src/core/wire/conformance.test.ts | 14 +- src/core/wire/conformance.ts | 2 + src/features/chat/index.ts | 2 + src/features/chat/store.svelte.ts | 12 ++ src/features/chat/store.test.ts | 46 ++++++ src/features/chat/ui.test.ts | 150 +++++++++++++++++-- src/features/chat/ui/ChatView.svelte | 93 ++++++++---- src/features/chat/ui/TurnSummary.svelte | 75 ++++++++++ 15 files changed, 887 insertions(+), 41 deletions(-) create mode 100644 src/core/telemetry/index.ts create mode 100644 src/core/telemetry/reducer.test.ts create mode 100644 src/core/telemetry/reducer.ts create mode 100644 src/core/telemetry/selectors.ts create mode 100644 src/core/telemetry/types.ts create mode 100644 src/features/chat/ui/TurnSummary.svelte (limited to 'src') diff --git a/src/app/App.svelte b/src/app/App.svelte index 61b4cb9..e1d59f9 100644 --- a/src/app/App.svelte +++ b/src/app/App.svelte @@ -1,6 +1,6 @@ -{#snippet chunkRow(rendered: RenderedChunk)} +{#snippet chunkRow(rendered: RenderedChunk, sIdx: number)} {#if rendered.role === "user"} -
{#if rendered.chunk.type === "text"} @@ -38,9 +52,6 @@
{:else if rendered.chunk.type === "thinking"} -
@@ -58,14 +69,18 @@
{:else if rendered.chunk.type === "tool-call" || rendered.chunk.type === "tool-result"} - + {@const step = currentTurnId ? stepMetrics(telemetry, currentTurnId, sIdx) : undefined} + {@const toolDur = step?.toolDurationMs}
{#if rendered.chunk.type === "tool-call"}
- {rendered.chunk.toolName} +
+ {rendered.chunk.toolName} + {#if toolDur !== undefined && toolDur > 0} + {formatMs(toolDur)} + {/if} +
{JSON.stringify(rendered.chunk.input, null, 2)}
{:else} @@ -73,19 +88,43 @@ class="w-fit max-w-full rounded-box bg-base-200 p-3 text-sm" class:text-error={rendered.chunk.isError} > - {rendered.chunk.toolName} +
+ {rendered.chunk.toolName} + {#if toolDur !== undefined && toolDur > 0} + {formatMs(toolDur)} + {/if} +
{rendered.chunk.content}
{/if}
{:else} - + {@const step = currentTurnId ? stepMetrics(telemetry, currentTurnId, sIdx) : undefined} + {@const tps = step ? stepTps(step) : undefined}
{#if rendered.chunk.type === "text"} -

{rendered.chunk.text}

+
    +
  • +

    {rendered.chunk.text}

    +
  • + {#if step && (step.genTotalMs !== undefined || tps !== undefined || step.usage?.outputTokens !== undefined)} +
  • + {#if step.genTotalMs !== undefined} + {formatMs(step.genTotalMs)} + {/if} + · + {#if tps !== undefined} + {Math.round(tps)} t/s + {/if} + · + {#if step.usage?.outputTokens !== undefined} + {step.usage.outputTokens} tok + {/if} +
  • + {/if} +
{:else if rendered.chunk.type === "error"}