fix(webview): anchor thinking timer to message timestamp to survive Virtuoso remounts#881
fix(webview): anchor thinking timer to message timestamp to survive Virtuoso remounts#881umi008 wants to merge 4 commits into
Conversation
…emounts The ReasoningBlock previously anchored its elapsed-time display to Date.now() at component mount time. When Virtuoso's virtual list recycles or remounts the component (e.g. during expand/collapse toggles or scroll), the timer reset to 0 because a fresh Date.now() was used and the effect guard (isLast && isStreaming) was false on remount. Now the component uses the message creation timestamp (ts, already passed as a prop but previously discarded) as the timer anchor. On remount after streaming has finished, elapsed is initialized from Date.now() - ts, giving a reasonable approximation of the thinking duration instead of showing 0s. This also explains the zh_CN-specific reproduction: CJK character widths in the header created slightly different DOM geometry, pushing the layout past Virtuoso's recycle threshold in certain viewports. Fixes Zoo-Code-Org#656
📝 WalkthroughWalkthrough
ChangesReasoning timer persistence
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
webview-ui/src/components/chat/ReasoningBlock.tsx (1)
17-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd Vitest coverage for the ReasoningBlock remount timer path.
Add a local test underwebview-ui/src/components/chat/__tests__that unmounts and remounts the component with the sametsand asserts the elapsed label still reflects the original message timestamp.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@webview-ui/src/components/chat/ReasoningBlock.tsx` around lines 17 - 29, Add a Vitest test under the chat component tests covering ReasoningBlock remount behavior: render it with a fixed ts, advance mocked time, unmount and remount using the same ts, then assert the elapsed label reflects time since the original timestamp rather than resetting on mount. Use the existing ReasoningBlock render props and translation/state mocks as needed.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@webview-ui/src/components/chat/ReasoningBlock.tsx`:
- Line 29: Initialize the elapsed state in ReasoningBlock directly from
Math.max(0, Date.now() - ts) for both streaming and non-streaming states,
removing the isStreaming conditional so remounts preserve the current elapsed
time and avoid the temporary hidden label.
---
Nitpick comments:
In `@webview-ui/src/components/chat/ReasoningBlock.tsx`:
- Around line 17-29: Add a Vitest test under the chat component tests covering
ReasoningBlock remount behavior: render it with a fixed ts, advance mocked time,
unmount and remount using the same ts, then assert the elapsed label reflects
time since the original timestamp rather than resetting on mount. Use the
existing ReasoningBlock render props and translation/state mocks as needed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 5d92d708-5c96-4b59-ac23-c94d8d20b1f5
📒 Files selected for processing (1)
webview-ui/src/components/chat/ReasoningBlock.tsx
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Covers the ts-anchored elapsed timer behavior including: - Non-streaming elapsed anchored to message timestamp - Streaming timer that ticks with real time - Remount survival (Virtuoso recycle) — elapsed anchored to ts, not mount - Timer stops when streaming ends - Collapse behavior - Zero-elapsed label hiding
There was a problem hiding this comment.
🧹 Nitpick comments (1)
webview-ui/src/components/chat/__tests__/ReasoningBlock.spec.tsx (1)
132-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReset
mockExtensionStateinbeforeEachinstead of manually at end of test.If the assertion on line 138 fails, the reset on line 141 never executes, and subsequent tests inherit
reasoningBlockCollapsed: true. Moving the reset tobeforeEachguarantees a clean slate per test regardless of pass/fail outcomes.♻️ Proposed fix
beforeEach(() => { vi.useFakeTimers() vi.setSystemTime(Date.now()) + mockExtensionState.reasoningBlockCollapsed = false })Then remove the manual reset:
// Reset for other tests - mockExtensionState.reasoningBlockCollapsed = false })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@webview-ui/src/components/chat/__tests__/ReasoningBlock.spec.tsx` around lines 132 - 142, Reset mockExtensionState.reasoningBlockCollapsed in the test suite’s beforeEach setup so every ReasoningBlock test starts with the default state, even when an assertion fails. Remove the manual reset from the “collapses when reasoningBlockCollapsed is true” test while preserving its test-specific assignment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@webview-ui/src/components/chat/__tests__/ReasoningBlock.spec.tsx`:
- Around line 132-142: Reset mockExtensionState.reasoningBlockCollapsed in the
test suite’s beforeEach setup so every ReasoningBlock test starts with the
default state, even when an assertion fails. Remove the manual reset from the
“collapses when reasoningBlockCollapsed is true” test while preserving its
test-specific assignment.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 37f2f27c-3b06-4d70-9db0-e3f701ab821e
📒 Files selected for processing (1)
webview-ui/src/components/chat/__tests__/ReasoningBlock.spec.tsx
Related GitHub Issue
Closes: #656
Description
The thinking/reasoning timer (e.g., "Thought for 30s") was resetting to 0 or disappearing when the user expanded and then collapsed the thinking section. This happened because
ReasoningBlockusedDate.now()as the timer anchor at component mount time — but inside aVirtuosovirtual list, expand/collapse can trigger DOM recycling that unmounts and remounts the component. On remount,startTimeRefgets a freshDate.now(), and sinceisStreamingis alreadyfalse(the message finished streaming before the user interacted), the timer effect never runs — leavingelapsedat 0.This was more visible with zh_CN locale because CJK character widths in the header text produced different DOM geometry, pushing the layout past Virtuoso recycle thresholds in viewports where Latin text would not trigger a remount.
Fix: The component now uses the message creation timestamp (
tsprop, already passed fromChatRow.tsxbut previously discarded in the destructuring) as the timer anchor. On remount after streaming has finished,elapsedis initialized fromDate.now() - ts, giving a reasonable approximation of the thinking duration instead of 0.Test Procedure
Pre-Submission Checklist
ts; component just wasn't using it — this is a pure hook into existing dataScreenshots / Videos
N/A — state management fix, no visual change beyond the timer not resetting.
Documentation Updates
Additional Notes
Edge case: for messages scrolled away for minutes and then scrolled back,
Date.now() - tswill show longer than the actual thinking time. This is cosmetic and far preferable to showing 0 or hiding the timer entirely — and only affects the unlikely case of a late remount after significant wall-clock time.Summary by CodeRabbit