From b84aa591aaa064c278c62582f30d9ba928e71371 Mon Sep 17 00:00:00 2001 From: umi008 Date: Fri, 10 Jul 2026 20:23:54 -0600 Subject: [PATCH] fix(webview): anchor thinking timer to message timestamp to survive remounts 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 #656 --- webview-ui/src/components/chat/ReasoningBlock.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/webview-ui/src/components/chat/ReasoningBlock.tsx b/webview-ui/src/components/chat/ReasoningBlock.tsx index 11166f5ae1..271655475b 100644 --- a/webview-ui/src/components/chat/ReasoningBlock.tsx +++ b/webview-ui/src/components/chat/ReasoningBlock.tsx @@ -14,14 +14,19 @@ interface ReasoningBlockProps { metadata?: any } -export const ReasoningBlock = ({ content, isStreaming, isLast }: ReasoningBlockProps) => { +export const ReasoningBlock = ({ content, isStreaming, isLast, ts }: ReasoningBlockProps) => { const { t } = useTranslation() const { reasoningBlockCollapsed } = useExtensionState() const [isCollapsed, setIsCollapsed] = useState(reasoningBlockCollapsed) - const startTimeRef = useRef(Date.now()) - const [elapsed, setElapsed] = useState(0) + // Anchor the elapsed timer to the message creation timestamp (ts) + // rather than component mount time. When Virtuoso recycles or + // remounts this component (e.g. during expand/collapse in a + // virtualized list), the timer survives because ts is a stable + // prop from the message data rather than a fresh Date.now(). + const startTimeRef = useRef(ts) + const [elapsed, setElapsed] = useState(() => (isStreaming ? 0 : Math.max(0, Date.now() - ts))) const contentRef = useRef(null) useEffect(() => {