From 62efd9ded65f65af69cf7c96cd821e2f9dc09c44 Mon Sep 17 00:00:00 2001 From: luk <16418011+DeprecatedLuke@users.noreply.github.com> Date: Wed, 1 Jul 2026 21:19:46 +0100 Subject: [PATCH] feat(usage-row): use database icon for cache, add ttft + throughput display --- .../components/chat-transcript-builder.ts | 12 +++++++++++- .../src/modes/components/usage-row.ts | 18 ++++++++++++++++-- .../src/modes/controllers/event-controller.ts | 4 +++- .../coding-agent/src/modes/utils/ui-helpers.ts | 9 +++++++-- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/packages/coding-agent/src/modes/components/chat-transcript-builder.ts b/packages/coding-agent/src/modes/components/chat-transcript-builder.ts index c460ee463..98f92bfed 100644 --- a/packages/coding-agent/src/modes/components/chat-transcript-builder.ts +++ b/packages/coding-agent/src/modes/components/chat-transcript-builder.ts @@ -81,6 +81,8 @@ export class ChatTranscriptBuilder { #readArgs = new Map>(); #readGroup: ReadToolGroupComponent | null = null; #pendingUsage: Usage | undefined; + #pendingUsageDuration: number | undefined; + #pendingUsageTtft: number | undefined; #lastAssistantUsage: Usage | undefined; #waitingPoll: ToolExecutionComponent | null = null; #todoSnapshot: ToolExecutionComponent | null = null; @@ -127,6 +129,8 @@ export class ChatTranscriptBuilder { this.#readArgs.clear(); this.#readGroup = null; this.#pendingUsage = undefined; + this.#pendingUsageDuration = undefined; + this.#pendingUsageTtft = undefined; this.#lastAssistantUsage = undefined; this.#waitingPoll = null; this.#todoSnapshot = null; @@ -192,8 +196,12 @@ export class ChatTranscriptBuilder { if (!this.#pendingUsage) return; this.#readGroup?.seal(); this.#readGroup = null; - this.container.addChild(createUsageRowBlock(this.#pendingUsage)); + this.container.addChild( + createUsageRowBlock(this.#pendingUsage, this.#pendingUsageDuration, this.#pendingUsageTtft), + ); this.#pendingUsage = undefined; + this.#pendingUsageDuration = undefined; + this.#pendingUsageTtft = undefined; } #appendChatMessage(message: AgentMessage): void { @@ -343,6 +351,8 @@ export class ChatTranscriptBuilder { } this.#pendingUsage = settings.get("display.showTokenUsage") ? message.usage : undefined; + this.#pendingUsageDuration = message.duration; + this.#pendingUsageTtft = message.ttft; } #appendToolResult(message: Extract): void { diff --git a/packages/coding-agent/src/modes/components/usage-row.ts b/packages/coding-agent/src/modes/components/usage-row.ts index 65efa4f77..78b012623 100644 --- a/packages/coding-agent/src/modes/components/usage-row.ts +++ b/packages/coding-agent/src/modes/components/usage-row.ts @@ -3,13 +3,27 @@ import { Container, Spacer, Text } from "@oh-my-pi/pi-tui"; import { formatNumber } from "@oh-my-pi/pi-utils"; import { theme } from "../../modes/theme/theme"; -export function createUsageRowBlock(usage: Usage): Container { +/** Below this the rate is nonsense (cached/instant responses yield absurd tok/s). */ +const MIN_DURATION_MS = 100; + +export function createUsageRowBlock(usage: Usage, durationMs?: number, ttftMs?: number): Container { const totalInput = usage.input + usage.cacheWrite; const parts: string[] = []; parts.push(`${theme.icon.input} ${formatNumber(totalInput)}`); parts.push(`${theme.icon.output} ${formatNumber(usage.output)}`); if (usage.cacheRead > 0) { - parts.push(`cache: ${formatNumber(usage.cacheRead)}`); + parts.push(`${theme.icon.cache} ${formatNumber(usage.cacheRead)}`); + } + if (ttftMs && ttftMs > 0) { + parts.push(`${theme.icon.time} ${(ttftMs / 1000).toFixed(1)}s`); + } + if (durationMs && durationMs > MIN_DURATION_MS && usage.output > 0) { + // Throughput excludes TTFT — generation time is duration minus time-to-first-token. + const genMs = durationMs - (ttftMs ?? 0); + if (genMs > MIN_DURATION_MS) { + const tokPerSec = (usage.output / genMs) * 1000; + parts.push(`${theme.icon.throughput} ${tokPerSec.toFixed(1)}/s`); + } } const block = new Container(); block.addChild(new Spacer(1)); diff --git a/packages/coding-agent/src/modes/controllers/event-controller.ts b/packages/coding-agent/src/modes/controllers/event-controller.ts index f94cfefb9..4524d5d28 100644 --- a/packages/coding-agent/src/modes/controllers/event-controller.ts +++ b/packages/coding-agent/src/modes/controllers/event-controller.ts @@ -786,7 +786,9 @@ export class EventController { this.#lastAssistantComponent = this.ctx.streamingComponent; this.#lastAssistantComponent.markTranscriptBlockFinalized(); if (settings.get("display.showTokenUsage")) { - this.ctx.chatContainer.addChild(createUsageRowBlock(event.message.usage)); + this.ctx.chatContainer.addChild( + createUsageRowBlock(event.message.usage, event.message.duration, event.message.ttft), + ); } this.ctx.streamingComponent = undefined; this.ctx.streamingMessage = undefined; diff --git a/packages/coding-agent/src/modes/utils/ui-helpers.ts b/packages/coding-agent/src/modes/utils/ui-helpers.ts index e96e18de5..5b16b4909 100644 --- a/packages/coding-agent/src/modes/utils/ui-helpers.ts +++ b/packages/coding-agent/src/modes/utils/ui-helpers.ts @@ -297,12 +297,16 @@ export class UiHelpers { // read run so the row sits under it. Mirrors the live path, where the read // group is created during streaming and the row is appended below it. let pendingUsage: Usage | undefined; + let pendingUsageDuration: number | undefined; + let pendingUsageTtft: number | undefined; const flushPendingUsage = () => { if (!pendingUsage) return; readGroup?.seal(); readGroup = null; - this.ctx.chatContainer.addChild(createUsageRowBlock(pendingUsage)); + this.ctx.chatContainer.addChild(createUsageRowBlock(pendingUsage, pendingUsageDuration, pendingUsageTtft)); pendingUsage = undefined; + pendingUsageDuration = undefined; + pendingUsageTtft = undefined; }; // Rebuild-time mirror of the event controller's displaceable-poll // bookkeeping: a `job` poll that found every watched job still running is @@ -450,11 +454,12 @@ export class UiHelpers { false, content.id, ); - } else { this.ctx.pendingTools.set(content.id, component); } } pendingUsage = this.ctx.settings.get("display.showTokenUsage") ? message.usage : undefined; + pendingUsageDuration = message.duration; + pendingUsageTtft = message.ttft; } else if (message.role === "toolResult") { const pendingReadComponent = this.ctx.pendingTools.get(message.toolCallId); const isReadGroupResult =