feat(cli): display real-time token consumption during streaming (#2742)#3329
feat(cli): display real-time token consumption during streaming (#2742)#3329qqqys wants to merge 7 commits intoQwenLM:mainfrom
Conversation
…LM#2742) Show ↓/↑ token count in the spinner during model execution: - ↓ when receiving content, ↑ when waiting for API response - Accumulates across the whole turn (tool calls don't reset) - Includes agent/subagent token consumption - Uses useAnimationFrame hook (50ms polling) to avoid flickering Co-authored-by: Qwen-Coder <[email protected]>
📋 Review SummaryThis PR implements real-time token consumption display in the loading indicator, showing 🔍 General Feedback
🎯 Specific Feedback🟢 Medium
🔵 Low
✅ Highlights
|
- Replace unsafe type assertion with proper type guard in Composer - Fix license header in useAnimationFrame.ts to match project standard - Clarify tokenCount is replaced (not accumulated) per USAGE_METADATA event - Use multi-line JSDoc format for isReceivingContent prop - Improve re-sync comment in useAnimationFrame hook - Revert unrelated streamingState dep change in AppContainer Co-authored-by: Qwen-Coder <[email protected]>
tanzhenxin
left a comment
There was a problem hiding this comment.
Review
Real-time token display in the spinner is a good feature, and the approach is sound (chars/4 estimation, 50ms ref polling to avoid re-renders). The ↑/↓ phase arrows are a nice touch. Two bugs to fix.
Issues
-
Subagent token aggregation mixes incompatible units. The main stream estimates output-only tokens (chars/4), but subagent
tokenCountcomes fromtotalTokenCountwhich is input + output. These get summed together, so a subagent with large context dominates the display. Suggestion: use output-only token counts for agents (candidatesTokenCount), or display agent tokens separately. -
Subagent multi-round tokens overwritten instead of accumulated.
USAGE_METADATAis emitted per subagent round, but the code overwritestokenCountinstead of accumulating. Multi-round subagents under-report. Suggestion:display.tokenCount = (display.tokenCount ?? 0) + event.usage.totalTokenCount.
Verdict
REQUEST_CHANGES — The token aggregation bugs need to be fixed.
Subagent token display had two bugs: - Used totalTokenCount (input+output) instead of candidatesTokenCount (output-only), causing mixed units when aggregated with main stream - Overwrote tokenCount per round instead of accumulating, so multi-round subagents only showed the last round's count Co-Authored-By: Qwen-Coder <[email protected]>
|
Both issues fixed in d393f23:
|
tanzhenxin
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround — both High-severity issues are correctly fixed end-to-end: the unit-mismatch via candidatesTokenCount in agent.ts:497, and multi-round accumulation via the per-invocation accumulatedOutputTokens closure. The commit message on d393f23 is clear about the rationale.
A couple of things I'd still tidy up before merging:
-
The jsdoc on
AgentResultDisplay.tokenCountinpackages/core/src/tools/tools.ts:502still says "(input + output)" — that's now stale since the value is output-only. Worth fixing in this PR since the commit that landed the accurate semantics is the one that stranded the doc. -
useAnimationFramecan briefly render the previous turn's count at the start of a new turn. The ref is reset to0inuseGeminiStream.ts:1367when a non-ToolResultsubmit begins, but the hook'suseStatestill holds the previous value until the 50ms tick fires. Simplest fix is to readwatchRef.currentsynchronously on render, or key/reset the hook on new top-level turns so the first render starts at0.
One design thought for later (not a blocker): the spinner now always shows chars/4 during streaming, whereas the old code used server-reported candidatesTokenCount from sessionStats.metrics. chars/4 is the right call during streaming since usage metadata isn't available yet, but once the first response's usage lands it'd be strictly better to swap to the real count. Happy to track this as a follow-up.
Verdict: comment — fine to merge once (1) and (2) are in, (3) can ship separately.
Interpolate displayed token count toward the real value (3/frame for small gaps, ~20% for medium, 50 for large) so chunked arrivals like tool-call args no longer cause visible jumps. Also accumulate tool call args JSON length into the streaming estimate, matching Claude Code's input_json_delta handling. Co-Authored-By: Qwen-Coder <[email protected]>
The 50ms useAnimationFrame poll lived in Composer, causing its entire subtree (InputPrompt, Footer, KeyboardShortcuts) to reconcile 20×/sec during streaming. Combined with the spinner and streamed text deltas, ink redrew enough lines to produce visible terminal flicker. Move the animation hook into LoadingIndicator so only that component re-renders per frame, and slow polling to 100ms to match the spinner cadence. Co-Authored-By: Qwen-Coder <[email protected]>
1. AgentResultDisplay.tokenCount jsdoc said "(input + output)" but the value has been output-only since d393f23 — update the comment so it matches the implementation. 2. useAnimationFrame held the previous turn's count in state until the next interval tick, briefly flashing stale numbers when a new turn reset the ref to 0. Snap displayRef down synchronously on render and return Math.min(displayValue, ref.current) so the reset is reflected immediately; the interval tick still catches state up afterward. Co-Authored-By: Qwen-Coder <[email protected]>
|
Thanks for the careful review! Both blockers addressed in e8eaa7c: (1) Stale jsdoc on (2) Previous turn's count flashing on new turn (
This way new turns start at 0 immediately, with no dependency on tick cadence (which became 100ms in e0147e3 to scope token-animation re-renders to (3) Swap to server-reported |
…ealtime-token-display
TLDR
Display real-time token consumption in the spinner/loading indicator during model execution. Shows
↓ N tokenswhen receiving output and↑ N tokenswhen waiting for API response, with agent/subagent tokens included in the total. Token count accumulates across the whole turn and resets on new user queries.Screenshots / Video Demo
Dive Deeper
Architecture
The implementation follows claude-code's approach with adaptations for Qwen Code's Ink-based UI:
Data flow:
Key design decisions:
Ref-based character counting —
streamingResponseLengthRefaccumulates output characters inhandleContentEventwithout triggering React re-renders. Tokens are estimated aschars / 4.useAnimationFramehook — Polls the ref at 50ms intervals but only triggerssetStatewhen the value actually changes. This avoids both the flickering from per-delta state updates and the waste of unconditional 50ms re-renders.Turn-level accumulation — The character counter resets only on new user queries (
submitType !== ToolResult), not on tool-result continuations. This matches claude-code's behavior where token count only increases within a turn.Phase detection —
isReceivingContentis set tofalsewhen enteringsubmitQuery(requesting) andtrueon the first content event (responding). This drives the↑/↓arrow direction.Agent token aggregation — The agent tool now forwards
USAGE_METADATAevents toAgentResultDisplay.tokenCount. Composer aggregates these frompendingGeminiHistoryItemsand adds them to the streaming estimate.Files changed
useAnimationFrame.tsuseGeminiStream.tsstreamingResponseLengthRef(char counter) andisReceivingContent(phase flag)UIStateContext.tsxUIStateinterfaceAppContainer.tsxComposer.tsxuseAnimationFrame, agent token aggregationLoadingIndicator.tsxisReceivingContentprop for dynamic↑/↓arrowtools.tstokenCounttoAgentResultDisplayagent.tsUSAGE_METADATAevents to displayReviewer Test Plan
Basic streaming — Send a simple prompt and verify
↓ N tokensappears in the spinner, increasing as output streams.Tool calls — Send a prompt that triggers tool use (e.g. "read file X"). Verify:
↑while waiting for API after tool result↓when model resumes outputNew turn reset — After a response completes, send a new prompt. Verify token count resets to 0 (no stale flash from previous turn).
Agent/subagent — Launch a task that uses the Agent tool. Verify the main spinner includes the subagent's token consumption.
Narrow terminal — Resize terminal to < 80 columns. Verify tokens are hidden gracefully.
Cancel — Press Esc during streaming. Verify no errors or stale display.
Testing Matrix
Linked issues / bugs
Closes #2742