Merge pull request #4185 from DeprecatedLuke/feat/make-token-info-nice
feat/make token info nicer
This commit is contained in:
@@ -91,6 +91,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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user