1- import React , { useEffect , useRef , useState } from "react"
1+ import React , { memo , useState , useEffect } from "react"
22import { useTranslation } from "react-i18next"
33
44import MarkdownBlock from "../common/MarkdownBlock"
55import { Lightbulb } from "lucide-react"
6+ import { ElapsedTime } from "./ElapsedTime"
67
78interface ReasoningBlockProps {
89 content : string
910 ts : number
1011 isStreaming : boolean
1112 isLast : boolean
13+ isExpanded ?: boolean
1214 metadata ?: any
1315}
1416
1517/**
1618 * Render reasoning with a heading and a simple timer.
1719 * - Heading uses i18n key chat:reasoning.thinking
18- * - Timer runs while reasoning is active (no persistence)
20+ * - Timer is isolated in ElapsedTime component to prevent parent re-renders
21+ * - Content is debounced during streaming to reduce re-render frequency
1922 */
20- export const ReasoningBlock = ( { content, isStreaming, isLast } : ReasoningBlockProps ) => {
23+ export const ReasoningBlock = memo ( ( { content, isStreaming, isLast, isExpanded = false } : ReasoningBlockProps ) => {
2124 const { t } = useTranslation ( )
2225
23- const startTimeRef = useRef < number > ( Date . now ( ) )
24- const [ elapsed , setElapsed ] = useState < number > ( 0 )
26+ // Debounce content updates during streaming
27+ const [ debouncedContent , setDebouncedContent ] = useState ( content )
2528
26- // Simple timer that runs while streaming
2729 useEffect ( ( ) => {
28- if ( isLast && isStreaming ) {
29- const tick = ( ) => setElapsed ( Date . now ( ) - startTimeRef . current )
30- tick ( )
31- const id = setInterval ( tick , 1000 )
32- return ( ) => clearInterval ( id )
30+ if ( isStreaming ) {
31+ // Debounce content updates to ~10 updates per second max
32+ const timer = setTimeout ( ( ) => {
33+ setDebouncedContent ( content )
34+ } , 100 )
35+ return ( ) => clearTimeout ( timer )
36+ } else {
37+ // Immediately update when streaming ends
38+ setDebouncedContent ( content )
3339 }
34- } , [ isLast , isStreaming ] )
40+ } , [ content , isStreaming ] )
3541
36- const seconds = Math . floor ( elapsed / 1000 )
37- const secondsLabel = t ( "chat:reasoning.seconds" , { count : seconds } )
42+ // Only render markdown if expanded and content exists
43+ const shouldRenderMarkdown = isExpanded && ( debouncedContent ?. trim ( ) ?. length ?? 0 ) > 0
3844
3945 return (
4046 < div >
@@ -43,17 +49,15 @@ export const ReasoningBlock = ({ content, isStreaming, isLast }: ReasoningBlockP
4349 < Lightbulb className = "w-4" />
4450 < span className = "font-bold text-vscode-foreground" > { t ( "chat:reasoning.thinking" ) } </ span >
4551 </ div >
46- { elapsed > 0 && (
47- < span className = "text-sm text-vscode-descriptionForeground tabular-nums flex items-center gap-1" >
48- { secondsLabel }
49- </ span >
50- ) }
52+ < ElapsedTime isStreaming = { isStreaming } isLast = { isLast } />
5153 </ div >
52- { ( content ?. trim ( ) ?. length ?? 0 ) > 0 && (
54+ { shouldRenderMarkdown && (
5355 < div className = "border-l border-vscode-descriptionForeground/20 ml-2 pl-4 pb-1 text-vscode-descriptionForeground" >
54- < MarkdownBlock markdown = { content } />
56+ < MarkdownBlock markdown = { debouncedContent } />
5557 </ div >
5658 ) }
5759 </ div >
5860 )
59- }
61+ } )
62+
63+ ReasoningBlock . displayName = "ReasoningBlock"
0 commit comments