Skip to content

fix(webview): anchor thinking timer to message timestamp to survive Virtuoso remounts#881

Open
umi008 wants to merge 4 commits into
Zoo-Code-Org:mainfrom
umi008:fix/656-reasoning-timer-reset
Open

fix(webview): anchor thinking timer to message timestamp to survive Virtuoso remounts#881
umi008 wants to merge 4 commits into
Zoo-Code-Org:mainfrom
umi008:fix/656-reasoning-timer-reset

Conversation

@umi008

@umi008 umi008 commented Jul 11, 2026

Copy link
Copy Markdown

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 ReasoningBlock used Date.now() as the timer anchor at component mount time — but inside a Virtuoso virtual list, expand/collapse can trigger DOM recycling that unmounts and remounts the component. On remount, startTimeRef gets a fresh Date.now(), and since isStreaming is already false (the message finished streaming before the user interacted), the timer effect never runs — leaving elapsed at 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 (ts prop, already passed from ChatRow.tsx but previously discarded in the destructuring) 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 0.

Test Procedure

  1. Start a chat with any provider that supports thinking/reasoning (Claude with extended thinking, DeepSeek, etc.)
  2. Wait for the thinking section to appear with a timer (e.g., "30s")
  3. Expand the thinking section, then collapse it
  4. Verify the timer still shows the original duration — not 0s or hidden
  5. Repeat with different locales (en, zh_CN) to confirm the fix is locale-independent

Pre-Submission Checklist

  • Issue Linked: Closes [BUG] Timer of Thinking will reset to 0 after expand and collapse it #656
  • Scope: One focused fix — anchors timer to stable message data
  • Self-Review: Props interface already had ts; component just wasn't using it — this is a pure hook into existing data
  • Testing: TypeScript compilation verified; tested the remount scenario with the init function fallback
  • Documentation Impact: No documentation updates required
  • Contribution Guidelines: Read and agree

Screenshots / Videos

N/A — state management fix, no visual change beyond the timer not resetting.

Documentation Updates

  • No documentation updates are required.

Additional Notes

Edge case: for messages scrolled away for minutes and then scrolled back, Date.now() - ts will 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

  • Bug Fixes
    • Improved elapsed-time display for streaming and non-streaming reasoning by basing the timer on the message’s creation time rather than when the UI mounts.
    • Elapsed-time now starts at 0 while streaming, remains consistent if the view is reopened, and stops updating when streaming ends.

…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
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

ReasoningBlock now anchors elapsed reasoning time to the message creation timestamp and adds tests for streaming, remounting, state transitions, collapsing, and zero-duration display.

Changes

Reasoning timer persistence

Layer / File(s) Summary
Timestamp-based timer initialization
webview-ui/src/components/chat/ReasoningBlock.tsx
Adds the ts prop and initializes elapsed time from Date.now() - ts while streaming; non-streaming instances start at zero.
Timer behavior validation
webview-ui/src/components/chat/__tests__/ReasoningBlock.spec.tsx
Tests elapsed-time calculation, remount persistence, streaming transitions, collapse behavior, and hiding the zero-duration label.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: taltas, navedmerchant, hannesrudolph

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: anchoring the thinking timer to the message timestamp to survive remounts.
Description check ✅ Passed The description includes the required issue link, change summary, test procedure, checklist, screenshots note, and documentation notes.
Linked Issues check ✅ Passed The change fixes #656 by anchoring the timer to ts so it no longer resets to 0 after expand/collapse remounts.
Out of Scope Changes check ✅ Passed The PR stays focused on the timer fix and its tests, with no unrelated code changes apparent.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
webview-ui/src/components/chat/ReasoningBlock.tsx (1)

17-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add Vitest coverage for the ReasoningBlock remount timer path.
Add a local test under webview-ui/src/components/chat/__tests__ that unmounts and remounts the component with the same ts and 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

📥 Commits

Reviewing files that changed from the base of the PR and between 116b70e and b84aa59.

📒 Files selected for processing (1)
  • webview-ui/src/components/chat/ReasoningBlock.tsx

Comment thread webview-ui/src/components/chat/ReasoningBlock.tsx
@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 11, 2026
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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
webview-ui/src/components/chat/__tests__/ReasoningBlock.spec.tsx (1)

132-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reset mockExtensionState in beforeEach instead 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 to beforeEach guarantees 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

📥 Commits

Reviewing files that changed from the base of the PR and between b84aa59 and 7898c54.

📒 Files selected for processing (1)
  • webview-ui/src/components/chat/__tests__/ReasoningBlock.spec.tsx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Timer of Thinking will reset to 0 after expand and collapse it

1 participant