diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index 787ed94659..887c520e25 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -273,9 +273,15 @@ export class AIChatManager { currentReply = $state('') currentReasoning = $state('') currentReasoningActive = $state(false) - // Live output-token count for the in-flight assistant message; drives the typing - // indicator's progress readout. Only populated by providers that stream usage. + // Live output-token count for the current turn; drives the typing indicator's + // progress readout. Only populated by providers that stream usage. currentOutputTokens = $state(0) + // A turn can span several API round-trips (one per tool-use step). Each round-trip's + // streamed usage.output_tokens is a running total scoped to that one message, so it + // restarts from near-zero every step. To keep the readout monotonic across the whole + // turn, fold each finished step's total into a base and add the in-flight step on top. + #outputTokensBase = 0 + #outputTokensStep = 0 displayMessages = $state([]) messages = $state([]) /** Provider-reported context size of the last committed turn (prompt + @@ -1641,6 +1647,8 @@ export class AIChatManager { this.currentReasoning = '' this.currentReasoningActive = false this.currentOutputTokens = 0 + this.#outputTokensBase = 0 + this.#outputTokensStep = 0 // Compaction trigger. Without a known context window there is no limit // to enforce, so compaction stays off rather than guessing one. @@ -1696,7 +1704,14 @@ export class AIChatManager { onNewToken: (token) => (this.currentReply += token), onReasoningDelta: (token) => (this.currentReasoning += token), onReasoningStart: () => (this.currentReasoningActive = true), - onOutputTokens: (n) => (this.currentOutputTokens = n), + onOutputTokens: (n) => { + // A drop means a new step's message started; bank the prior step's total. + if (n < this.#outputTokensStep) { + this.#outputTokensBase += this.#outputTokensStep + } + this.#outputTokensStep = n + this.currentOutputTokens = this.#outputTokensBase + n + }, onMessageEnd: () => { // Keep the streamed text for the abort/error paths. Non-empty only: // parsers flush (and reset) when a tool call starts after text, and