1- import React , { useEffect , useRef , useState } from "react"
1+ import React , { memo , useEffect , useRef , useState } from "react"
22import { useTranslation } from "react-i18next"
33
44import MarkdownBlock from "../common/MarkdownBlock"
@@ -12,45 +12,68 @@ interface ReasoningBlockProps {
1212 metadata ?: any
1313}
1414
15+ interface ElapsedTimeProps {
16+ isActive : boolean
17+ startTime : number
18+ }
19+
1520/**
16- * Render reasoning with a heading and a simple timer.
17- * - Heading uses i18n key chat:reasoning.thinking
18- * - Timer runs while reasoning is active (no persistence)
21+ * Memoized timer component that only re-renders itself
22+ * This prevents the entire ReasoningBlock from re-rendering every second
1923 */
20- export const ReasoningBlock = ( { content , isStreaming , isLast } : ReasoningBlockProps ) => {
24+ const ElapsedTime = memo ( ( { isActive , startTime } : ElapsedTimeProps ) => {
2125 const { t } = useTranslation ( )
22-
23- const startTimeRef = useRef < number > ( Date . now ( ) )
2426 const [ elapsed , setElapsed ] = useState < number > ( 0 )
2527
26- // Simple timer that runs while streaming
2728 useEffect ( ( ) => {
28- if ( isLast && isStreaming ) {
29- const tick = ( ) => setElapsed ( Date . now ( ) - startTimeRef . current )
30- tick ( )
29+ if ( isActive ) {
30+ const tick = ( ) => setElapsed ( Date . now ( ) - startTime )
31+ tick ( ) // Initial tick
3132 const id = setInterval ( tick , 1000 )
3233 return ( ) => clearInterval ( id )
34+ } else {
35+ setElapsed ( 0 )
3336 }
34- } , [ isLast , isStreaming ] )
37+ } , [ isActive , startTime ] )
38+
39+ if ( elapsed === 0 ) return null
3540
3641 const seconds = Math . floor ( elapsed / 1000 )
3742 const secondsLabel = t ( "chat:reasoning.seconds" , { count : seconds } )
3843
44+ return (
45+ < span className = "text-vscode-foreground tabular-nums flex items-center gap-1" >
46+ < Clock className = "w-4" />
47+ { secondsLabel }
48+ </ span >
49+ )
50+ } )
51+
52+ ElapsedTime . displayName = "ElapsedTime"
53+
54+ /**
55+ * Render reasoning with a heading and a simple timer.
56+ * - Heading uses i18n key chat:reasoning.thinking
57+ * - Timer runs while reasoning is active (no persistence)
58+ * - Timer is isolated in a memoized component to prevent full re-renders
59+ */
60+ export const ReasoningBlock = ( { content, isStreaming, isLast } : ReasoningBlockProps ) => {
61+ const { t } = useTranslation ( )
62+ const startTimeRef = useRef < number > ( Date . now ( ) )
63+
64+ // Only render markdown when there's actual content
65+ const hasContent = ( content ?. trim ( ) ?. length ?? 0 ) > 0
66+
3967 return (
4068 < div className = "py-1" >
4169 < div className = "flex items-center justify-between mb-2.5 pr-2" >
4270 < div className = "flex items-center gap-2" >
4371 < Lightbulb className = "w-4" />
4472 < span className = "font-bold text-vscode-foreground" > { t ( "chat:reasoning.thinking" ) } </ span >
4573 </ div >
46- { elapsed > 0 && (
47- < span className = "text-vscode-foreground tabular-nums flex items-center gap-1" >
48- < Clock className = "w-4" />
49- { secondsLabel }
50- </ span >
51- ) }
74+ < ElapsedTime isActive = { isLast && isStreaming } startTime = { startTimeRef . current } />
5275 </ div >
53- { ( content ?. trim ( ) ?. length ?? 0 ) > 0 && (
76+ { hasContent && (
5477 < div className = "px-3 italic text-vscode-descriptionForeground" >
5578 < MarkdownBlock markdown = { content } />
5679 </ div >
0 commit comments