From 308eabc6b3d516c8a66313f0a89b6438728870df Mon Sep 17 00:00:00 2001 From: luk <16418011+DeprecatedLuke@users.noreply.github.com> Date: Wed, 1 Jul 2026 21:08:28 +0100 Subject: [PATCH 1/2] feat(status-line): add throughput icon and simplify cache segments --- packages/coding-agent/CHANGELOG.md | 1 + .../src/modes/components/status-line/segments.ts | 6 +++--- packages/coding-agent/src/modes/theme/theme.ts | 8 +++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 73fc6ccd9..69a8d0d15 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -67,6 +67,7 @@ - Fixed `models.yml` remote compaction schema support for V2 streaming endpoint fields. ([#4146](https://github.com/can1357/oh-my-pi/issues/4146)) - Fixed the SSH tool to reject `cwd` values of `~` and `~/...` before sending guaranteed-bad quoted tilde paths to remote POSIX shells. ([#4002](https://github.com/can1357/oh-my-pi/issues/4002)) +- Status line token throughput segment now uses a dedicated tachometer icon (`icon.throughput`) instead of reusing the output arrow; cache read/write segments use a single database icon instead of stacking input/output arrows alongside it. ## [16.2.12] - 2026-07-01 diff --git a/packages/coding-agent/src/modes/components/status-line/segments.ts b/packages/coding-agent/src/modes/components/status-line/segments.ts index 3bf9e38c1..0b1487a7d 100644 --- a/packages/coding-agent/src/modes/components/status-line/segments.ts +++ b/packages/coding-agent/src/modes/components/status-line/segments.ts @@ -379,7 +379,7 @@ const tokenRateSegment: StatusLineSegment = { const { tokensPerSecond } = ctx.usageStats; if (!tokensPerSecond) return { content: "", visible: false }; - const content = withIcon(theme.icon.output, `${tokensPerSecond.toFixed(1)}/s`); + const content = withIcon(theme.icon.throughput, `${tokensPerSecond.toFixed(1)}/s`); return { content: theme.fg("statusLineOutput", content), visible: true }; }, }; @@ -498,7 +498,7 @@ const cacheReadSegment: StatusLineSegment = { const { cacheRead } = ctx.usageStats; if (!cacheRead) return { content: "", visible: false }; - const parts = [theme.icon.cache, theme.icon.output, formatNumber(cacheRead)].filter(Boolean); + const parts = [theme.icon.cache, formatNumber(cacheRead)].filter(Boolean); const content = parts.join(" "); return { content: theme.fg("statusLineSpend", content), visible: true }; }, @@ -510,7 +510,7 @@ const cacheWriteSegment: StatusLineSegment = { const { cacheWrite } = ctx.usageStats; if (!cacheWrite) return { content: "", visible: false }; - const parts = [theme.icon.cache, theme.icon.input, formatNumber(cacheWrite)].filter(Boolean); + const parts = [theme.icon.cache, formatNumber(cacheWrite)].filter(Boolean); const content = parts.join(" "); return { content: theme.fg("statusLineOutput", content), visible: true }; }, diff --git a/packages/coding-agent/src/modes/theme/theme.ts b/packages/coding-agent/src/modes/theme/theme.ts index 4947d4f94..d588e61f0 100644 --- a/packages/coding-agent/src/modes/theme/theme.ts +++ b/packages/coding-agent/src/modes/theme/theme.ts @@ -115,6 +115,7 @@ export type SymbolKey = | "icon.cacheMiss" | "icon.input" | "icon.output" + | "icon.throughput" | "icon.host" | "icon.session" | "icon.package" @@ -319,6 +320,7 @@ const UNICODE_SYMBOLS: SymbolMap = { "icon.cacheMiss": "โŠ˜", "icon.input": "โคต", "icon.output": "โคด", + "icon.throughput": "โšก", "icon.host": "๐Ÿ–ฅ", "icon.session": "๐Ÿ†”", "icon.package": "๐Ÿ“ฆ", @@ -597,6 +599,8 @@ const NERD_SYMBOLS: SymbolMap = { "icon.input": "\uf090", // pick: ๏‚‹ | alt: ๏ก โ†’ "icon.output": "\uf08b", + // pick: (nf-fa-tachometer) | alt: โšก โ†ฌ + "icon.throughput": "\uf0e4", // pick: ๏„‰ | alt: ๏„ˆ ๏„  "icon.host": "\uf109", // pick: ๏• | alt: ๏€— ๏‚  @@ -827,10 +831,11 @@ const ASCII_SYMBOLS: SymbolMap = { "icon.ghost": "@", "icon.agents": "AG", "icon.job": "bg", + "icon.output": "out:", + "icon.throughput": "tok/s:", "icon.cache": "cache", "icon.cacheMiss": "!", "icon.input": "in:", - "icon.output": "out:", "icon.host": "host", "icon.session": "id", "icon.package": "[P]", @@ -1820,6 +1825,7 @@ export class Theme { cacheMiss: this.#symbols["icon.cacheMiss"], input: this.#symbols["icon.input"], output: this.#symbols["icon.output"], + throughput: this.#symbols["icon.throughput"], host: this.#symbols["icon.host"], session: this.#symbols["icon.session"], package: this.#symbols["icon.package"], 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 2/2] 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 =