From 9a4fc0aa0b56e7bb083ecc615647ed8f6b2ef4c2 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/features/chat/store.svelte.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'src/features/chat/store.svelte.ts') diff --git a/src/features/chat/store.svelte.ts b/src/features/chat/store.svelte.ts index 1d8ab17..58c165f 100644 --- a/src/features/chat/store.svelte.ts +++ b/src/features/chat/store.svelte.ts @@ -13,6 +13,8 @@ import { selectChunks, selectMessages, } from "../../core/chunks"; +import type { TelemetryState } from "../../core/telemetry"; +import { foldMetricEvent, initialState as telemetryInitialState } from "../../core/telemetry"; import type { ConversationCache } from "../conversation-cache"; import type { ChatTransport, HistorySync } from "./ports"; @@ -30,6 +32,8 @@ export interface ChatStore { readonly pendingSync: boolean; readonly error: string | null; readonly model: string | undefined; + readonly telemetry: TelemetryState; + readonly currentTurnId: string | null; handleDelta(msg: ChatDeltaMessage | ChatErrorMessage): void; send(text: string): void; setModel(model: string): void; @@ -42,6 +46,7 @@ export function createChatStore(deps: ChatStoreDependencies): ChatStore { let _pendingSync = $state(false); let _error = $state(null); let _model = $state(deps.model); + let telemetry = $state(telemetryInitialState()); let disposed = false; async function syncTail(): Promise { @@ -76,6 +81,12 @@ export function createChatStore(deps: ChatStoreDependencies): ChatStore { get model(): string | undefined { return _model; }, + get telemetry(): TelemetryState { + return telemetry; + }, + get currentTurnId(): string | null { + return transcript.currentTurnId; + }, handleDelta(msg: ChatDeltaMessage | ChatErrorMessage): void { if (msg.type === "chat.error") { @@ -89,6 +100,7 @@ export function createChatStore(deps: ChatStoreDependencies): ChatStore { return; } transcript = foldEvent(transcript, msg.event); + telemetry = foldMetricEvent(telemetry, msg.event); if (transcript.sealedTurnId !== null) { void syncTail(); } -- cgit v1.2.3