feat(usage-row): use database icon for cache, add ttft + throughput display

This commit is contained in:
luk
2026-07-02 00:25:54 +01:00
parent 308eabc6b3
commit 62efd9ded6
4 changed files with 37 additions and 6 deletions
@@ -81,6 +81,8 @@ export class ChatTranscriptBuilder {
#readArgs = new Map<string, Record<string, unknown>>();
#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<AgentMessage, { role: "toolResult" }>): void {
@@ -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));
@@ -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;
@@ -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 =