feat(web): live header statistics while a task runs (#75)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei ZhengandClaude Fable 5 authored and GitHub committed 2026-07-27 22:44:02 +08:00
1 parent a955046b73
commit 83e7b85eca
4 files changed
+235 -26

No files matched your search

+45
View File
@@ -42,6 +42,32 @@ export interface TokenBucketCounts {
output: number;
}
/** Three pricing buckets in USD per million tokens (mirrors the server's ModelPricingDto shape). */
export interface BucketPricing {
cacheRead: number;
cacheWrite: number;
output: number;
}
/**
* Converts a three-bucket Token count to USD at per-million-token pricing; null when no pricing
* is configured (uncosted — callers omit the figure rather than fabricating a $0). Shared by the
* per-reply stats row's turn cost and the header's live session cost, so the two conversions can
* never drift apart.
*/
export function bucketCostUsd(
buckets: TokenBucketCounts,
pricing: BucketPricing | null | undefined,
): number | null {
if (!pricing) return null;
return (
(buckets.cacheRead * pricing.cacheRead +
buckets.cacheWrite * pricing.cacheWrite +
buckets.output * pricing.output) /
1e6
);
}
/** Stats tracker: Session-level cumulative counts that persist across Tasks + this-Task delta counts. */
export interface TaskStatsTracker {
/** Current context occupancy = total from the most recent main-session request (not updated by compaction requests). */
@@ -287,6 +313,25 @@ export function endTask(t: TaskStatsTracker, elapsedMs: number): TaskStats | nul
return stats;
}
/**
* Session elapsed time for live display (the header chip): the settled cross-Task cumulative
* plus the currently running Task's wall clock so far. While no Task is open this is exactly
* `sessionElapsedMs`, so the idle rendering equals the settled value; the running addition
* counts from the model's task-start local clock (`taskStartLocalMs`), clamped so a clock
* anomaly never makes the sum go backwards. At task end {@link endTask} folds the Task's
* elapsed into `sessionElapsedMs` in the same model update that flips `taskOpen` off, so the
* live addition never double-counts across the boundary.
*/
export function liveSessionElapsedMs(
t: TaskStatsTracker,
taskOpen: boolean,
taskStartLocalMs: number | null,
nowMs: number,
): number {
const running = taskOpen && taskStartLocalMs != null ? Math.max(0, nowMs - taskStartLocalMs) : 0;
return t.sessionElapsedMs + running;
}
/** Localized labels for {@link formatTaskStats} (supplied by the view layer's active dictionary). */
export interface TaskStatsLabels {
/** Row prefix, e.g. "Stats" / "统计信息". */