Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/zcode-tui/src/context-status-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
RuntimeProjectionSnapshot
} from "./runtime-projection.ts";
import type { ContextCacheTrend, ContextCacheTurn } from "./context-cache.ts";
import type { SessionMetrics } from "./session-status.ts";
import { contextUsedPercent, type SessionMetrics } from "./session-status.ts";
import type { ZCodeTheme } from "./theme.ts";

const contextLabels: Record<RuntimeContextBreakdownItem["source"], string> = {
Expand Down Expand Up @@ -156,7 +156,10 @@ export class ContextDetailView implements Component {
const columns = totalChars > 0 ? Math.max(1, Math.round(item.chars / totalChars * barWidth)) : 0;
return contextStyle(item.source, this.theme)("█".repeat(columns));
}).join("");
const usedPercent = Math.max(0, Math.round(this.usage.used / this.usage.size * 100));
const usedPercent = contextUsedPercent({
contextUsed: this.usage.used,
contextWindow: this.usage.size
}) ?? 0;
const remaining = Math.max(0, this.usage.size - this.usage.used);
const usageStyle = usedPercent >= 90 ? this.theme.error : usedPercent >= 70 ? this.theme.warning : (text: string) => text;
const lines = [
Expand Down Expand Up @@ -306,7 +309,7 @@ export class StatusDetailView implements Component {
? [projection.lastError.code, projection.lastError.message].filter(Boolean).join(" · ")
: undefined],
["Turns", String(metrics.turnCount ?? projection?.turnCount ?? 0)],
["Tokens", metrics.totalTokens !== undefined ? formatTokens(metrics.totalTokens) : undefined],
["Session tokens", metrics.totalTokens !== undefined ? formatTokens(metrics.totalTokens) : undefined],
["Requests", metrics.modelRequestCount !== undefined
? `${metrics.modelRequestCount}${metrics.modelErrorCount ? ` · ${metrics.modelErrorCount} errors` : ""}`
: undefined],
Expand Down
27 changes: 18 additions & 9 deletions packages/zcode-tui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ import {
import {
contextRemainingPercent,
mergeMetrics,
mergeProjectionMetrics,
projectionMetrics,
sessionIdFromUsage,
usageMetrics,
Expand Down Expand Up @@ -1447,7 +1448,11 @@ class ZCodeTui {
}
if (Array.isArray(result.modelOptions)) this.modelOptions = [...result.modelOptions];
if (Array.isArray(result.effortOptions)) this.effortOptions = [...result.effortOptions];
this.sessionMetrics = mergeMetrics(this.sessionMetrics, projectionMetrics(result.projection));
this.sessionMetrics = mergeProjectionMetrics(
this.sessionMetrics,
projectionMetrics(result.projection),
Boolean(this.options.readSessionUsage)
);
if (Array.isArray(result.todos)) this.todos = normalizeTodos(result.todos);
if (Array.isArray(result.todoGroups)) this.todoGroups = normalizeTodoGroups(result);
this.applyRuntimeProjection(normalizeRuntimeProjection(result));
Expand Down Expand Up @@ -3950,8 +3955,8 @@ class ZCodeTui {
if (this.sessionMetrics.totalTokens !== undefined) {
const tokens = formatTokens(this.sessionMetrics.totalTokens);
fields.push({
text: this.theme.muted(`${tokens} tokens`),
compactText: this.theme.muted(`${tokens} tok`),
text: this.theme.muted(`session ${tokens} tokens`),
compactText: this.theme.muted(`session ${tokens}`),
priority: 20
});
}
Expand Down Expand Up @@ -4064,12 +4069,16 @@ class ZCodeTui {
this.runtimeProjection = projection;
this.reconcileTurnTiming(projection);
if (projection.sessionId) this.sessionId = projection.sessionId;
this.sessionMetrics = mergeMetrics(this.sessionMetrics, {
contextUsed: projection.contextUsage?.used,
contextWindow: projection.contextUsage?.size,
totalTokens: projection.totalTokenCount,
turnCount: projection.turnCount
});
this.sessionMetrics = mergeProjectionMetrics(
this.sessionMetrics,
{
contextUsed: projection.contextUsage?.used,
contextWindow: projection.contextUsage?.size,
totalTokens: projection.totalTokenCount,
turnCount: projection.turnCount
},
Boolean(this.options.readSessionUsage)
);
this.updateRuntimeActivity(false);
}

Expand Down
25 changes: 22 additions & 3 deletions packages/zcode-tui/src/session-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,29 @@ export function sessionIdFromUsage(value: unknown): string | undefined {
}

export function mergeMetrics(current: SessionMetrics, update: SessionMetrics | undefined): SessionMetrics {
return update ? { ...current, ...update } : current;
if (!update) return current;
const defined = Object.fromEntries(
Object.entries(update).filter((entry): entry is [string, number] => entry[1] !== undefined)
) as SessionMetrics;
return { ...current, ...defined };
}

export function contextRemainingPercent(metrics: SessionMetrics): number | undefined {
export function mergeProjectionMetrics(
current: SessionMetrics,
update: SessionMetrics | undefined,
hasAuthoritativeSessionUsage: boolean
): SessionMetrics {
if (!update || !hasAuthoritativeSessionUsage) return mergeMetrics(current, update);
const { totalTokens: _projectionTotal, ...contextMetrics } = update;
return mergeMetrics(current, contextMetrics);
}

export function contextUsedPercent(metrics: SessionMetrics): number | undefined {
if (metrics.contextUsed === undefined || !metrics.contextWindow) return undefined;
return Math.max(0, Math.min(100, Math.round((1 - metrics.contextUsed / metrics.contextWindow) * 100)));
return Math.max(0, Math.min(100, Math.round(metrics.contextUsed / metrics.contextWindow * 100)));
}

export function contextRemainingPercent(metrics: SessionMetrics): number | undefined {
const used = contextUsedPercent(metrics);
return used === undefined ? undefined : 100 - used;
}
26 changes: 26 additions & 0 deletions test/session-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { describe, expect, test } from "bun:test";

import {
contextRemainingPercent,
contextUsedPercent,
mergeMetrics,
mergeProjectionMetrics,
projectionMetrics,
sessionIdFromUsage,
usageMetrics
Expand Down Expand Up @@ -41,6 +43,30 @@ describe("TUI session status", () => {
expect(contextRemainingPercent({ contextUsed: 200, contextWindow: 100 })).toBe(0);
});

test("keeps session-store totals authoritative over runtime projections", () => {
const usage = { totalTokens: 18_500, inputTokens: 14_000 };
const projection = { totalTokens: 417_202, contextUsed: 32_000, contextWindow: 128_000 };

expect(mergeProjectionMetrics(usage, projection, true)).toEqual({
totalTokens: 18_500,
inputTokens: 14_000,
contextUsed: 32_000,
contextWindow: 128_000
});
expect(mergeProjectionMetrics({}, projection, false).totalTokens).toBe(417_202);
});

test("does not erase metrics with absent fields and keeps used plus remaining at 100%", () => {
expect(mergeMetrics(
{ totalTokens: 18_500, contextUsed: 32_000 },
{ contextWindow: 128_000, totalTokens: undefined }
)).toEqual({ totalTokens: 18_500, contextUsed: 32_000, contextWindow: 128_000 });

const metrics = { contextUsed: 1, contextWindow: 8 };
expect(contextUsedPercent(metrics)).toBe(13);
expect(contextRemainingPercent(metrics)).toBe(87);
});

test("extracts the official session identifier from usage snapshots", () => {
expect(sessionIdFromUsage({ sessionId: "sess_protocol" })).toBe("sess_protocol");
expect(sessionIdFromUsage({ sessionID: "sess_store" })).toBe("sess_store");
Expand Down