feat(coding-agent): merge consecutive cache warming notices into one line

Back-to-back cache warms update a single 'Cache warmed: $<total>' line with the cumulative cost instead of adding one line per warm. Each warm is still persisted as its own usage entry.
This commit is contained in:
Vegard Stikbakke 2026-11-04 16:08:25 +01:00
parent 90e2040468
commit a013448fb5
4 changed files with 22 additions and 8 deletions

View file

@ -16,6 +16,7 @@
- The default model for the `anthropic` provider is now Claude Opus 5.5 (`claude-opus-5-5`) instead of Claude Opus 4.8
- Default models moved to newer models at the same or a lower price: `amazon-bedrock` Claude Opus 5.5, `azure` GPT-6.1 Sol, `google` and `google-vertex` Gemini 4 Argon, `vercel-ai-gateway` GLM 5.3, `baseten` GLM 5.3, `minimax` and `minimax-cn` MiniMax-M3, `qwen-token-plan` and `qwen-token-plan-cn` Qwen3.8 Max, `opencode-go` Kimi K3.1 Pro, and Kimi K2.7 Code instead of Kimi K2.6 for `moonshotai`, `moonshotai-cn`, `huggingface`, `opencode`, `openrouter`, `cloudflare-workers-ai` and `cloudflare-ai-gateway`
- Consecutive cache warms now show as one `Cache warmed: $<total>` line with the cumulative cost instead of one line per warm
### Fixed

View file

@ -447,9 +447,9 @@ export function formatCacheWarmingStatus(status: CacheWarmingStatus, now = Date.
return `${formatCacheWarmingDecisionTime(status.nextWarmAt, now)} (${details})`;
}
/** One-line transcript text for persisted cache-warming usage. */
export function formatCacheWarmingUsage(entry: UsageEntry): string {
const note = entry.note ? ` (${entry.note})` : "";
const cost = entry.usage.cost.total.toFixed(6).replace(/(\.\d{3}\d*?)0+$/, "$1");
return `Cache warmed${note}: $${cost}`;
/** One-line transcript text for the total cost of one or more consecutive cache warms. */
export function formatCacheWarmingUsage(totalCost: number, note?: string): string {
const noteText = note ? ` (${note})` : "";
const cost = totalCost.toFixed(6).replace(/(\.\d{3}\d*?)0+$/, "$1");
return `Cache warmed${noteText}: $${cost}`;
}

View file

@ -504,6 +504,8 @@ export class InteractiveMode {
private changelogMarkdown: string | undefined = undefined;
private extensionChangelogUpdates: ExtensionChangelogUpdate[] = [];
private startupNoticesShown = false;
/** Last cache-warming notice, extended in place while it is the last chat line. */
private cacheWarmingNotice: { component: ThemedText; total: { cost: number }; note: string | undefined } | undefined;
private anthropicSubscriptionWarningShown = false;
// Status line tracking (for mutating immediately-sequential status updates)
@ -4239,9 +4241,18 @@ export class InteractiveMode {
private addCacheWarmingUsage(entry: UsageEntry): void {
if (!this.settingsManager.getShowCacheMissNotices()) return;
// Back-to-back warms collapse into one line with the cumulative cost.
const previous = this.cacheWarmingNotice;
if (previous && this.chatContainer.children.at(-1) === previous.component && previous.note === entry.note) {
previous.total.cost += entry.usage.cost.total;
previous.component.invalidate();
return;
}
const total = { cost: entry.usage.cost.total };
const component = new ThemedText(() => theme.fg("dim", formatCacheWarmingUsage(total.cost, entry.note)), 1, 0);
this.cacheWarmingNotice = { component, total, note: entry.note };
this.chatContainer.addChild(new Spacer(1));
const usage = formatCacheWarmingUsage(entry);
this.chatContainer.addChild(new ThemedText(() => theme.fg("dim", usage), 1, 0));
this.chatContainer.addChild(component);
}
/**

View file

@ -338,7 +338,9 @@ describe("cache warming", () => {
usage,
"extension override",
);
expect(formatCacheWarmingUsage(entry)).toBe("Cache warmed (extension override): $0.029497");
expect(formatCacheWarmingUsage(entry.usage.cost.total, entry.note)).toBe(
"Cache warmed (extension override): $0.029497",
);
});
});