1- import React , { useEffect , useRef , useState } from "react"
1+ import React , { useEffect , useRef , useState , useMemo , memo } from "react"
22import { useTranslation } from "react-i18next"
3+ import debounce from "debounce"
34
45import MarkdownBlock from "../common/MarkdownBlock"
56import { Clock , Lightbulb } from "lucide-react"
@@ -12,47 +13,99 @@ interface ReasoningBlockProps {
1213 metadata ?: any
1314}
1415
16+ interface ElapsedTimeProps {
17+ isActive : boolean
18+ startTime : number
19+ }
20+
1521/**
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)
22+ * Memoized timer component that only re-renders itself
23+ * This prevents the entire ReasoningBlock from re-rendering every second
1924 */
20- export const ReasoningBlock = ( { content , isStreaming , isLast } : ReasoningBlockProps ) => {
25+ const ElapsedTime = memo ( ( { isActive , startTime } : ElapsedTimeProps ) => {
2126 const { t } = useTranslation ( )
22-
23- const startTimeRef = useRef < number > ( Date . now ( ) )
2427 const [ elapsed , setElapsed ] = useState < number > ( 0 )
2528
26- // Simple timer that runs while streaming
2729 useEffect ( ( ) => {
28- if ( isLast && isStreaming ) {
29- const tick = ( ) => setElapsed ( Date . now ( ) - startTimeRef . current )
30- tick ( )
30+ if ( isActive ) {
31+ const tick = ( ) => setElapsed ( Date . now ( ) - startTime )
32+ tick ( ) // Initial tick
3133 const id = setInterval ( tick , 1000 )
3234 return ( ) => clearInterval ( id )
35+ } else {
36+ setElapsed ( 0 )
3337 }
34- } , [ isLast , isStreaming ] )
38+ } , [ isActive , startTime ] )
39+
40+ if ( elapsed === 0 ) return null
3541
3642 const seconds = Math . floor ( elapsed / 1000 )
3743 const secondsLabel = t ( "chat:reasoning.seconds" , { count : seconds } )
3844
45+ return (
46+ < span className = "text-vscode-foreground tabular-nums flex items-center gap-1" >
47+ < Clock className = "w-4" />
48+ { secondsLabel }
49+ </ span >
50+ )
51+ } )
52+
53+ ElapsedTime . displayName = "ElapsedTime"
54+
55+ /**
56+ * Render reasoning with a heading and a simple timer.
57+ * - Heading uses i18n key chat:reasoning.thinking
58+ * - Timer runs while reasoning is active (no persistence)
59+ * - Timer is isolated in a memoized component to prevent full re-renders
60+ * - Content updates are debounced to prevent excessive re-renders during streaming
61+ */
62+ export const ReasoningBlock = ( { content, isStreaming, isLast } : ReasoningBlockProps ) => {
63+ const { t } = useTranslation ( )
64+ const startTimeRef = useRef < number > ( Date . now ( ) )
65+ const [ debouncedContent , setDebouncedContent ] = useState < string > ( content || "" )
66+
67+ // Create a debounced function to update content
68+ // This limits content updates to a maximum of ~10 per second (100ms debounce)
69+ const updateDebouncedContent = useMemo (
70+ ( ) =>
71+ debounce ( ( newContent : string ) => {
72+ setDebouncedContent ( newContent )
73+ } , 100 ) ,
74+ [ ] ,
75+ )
76+
77+ // Update debounced content when content changes
78+ useEffect ( ( ) => {
79+ if ( isStreaming ) {
80+ // During streaming, use debounced updates
81+ updateDebouncedContent ( content || "" )
82+ } else {
83+ // When not streaming, update immediately for final content
84+ setDebouncedContent ( content || "" )
85+ // Cancel any pending debounced updates
86+ updateDebouncedContent . clear ( )
87+ }
88+ } , [ content , isStreaming , updateDebouncedContent ] )
89+
90+ // Cleanup debounce on unmount
91+ useEffect ( ( ) => {
92+ return ( ) => {
93+ updateDebouncedContent . clear ( )
94+ }
95+ } , [ updateDebouncedContent ] )
96+
3997 return (
4098 < div className = "py-1" >
4199 < div className = "flex items-center justify-between mb-2.5 pr-2" >
42100 < div className = "flex items-center gap-2" >
43101 < Lightbulb className = "w-4" />
44102 < span className = "font-bold text-vscode-foreground" > { t ( "chat:reasoning.thinking" ) } </ span >
45103 </ 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- ) }
104+ < ElapsedTime isActive = { isLast && isStreaming } startTime = { startTimeRef . current } />
52105 </ div >
53- { ( content ?. trim ( ) ?. length ?? 0 ) > 0 && (
106+ { ( debouncedContent ?. trim ( ) ?. length ?? 0 ) > 0 && (
54107 < div className = "px-3 italic text-vscode-descriptionForeground" >
55- < MarkdownBlock markdown = { content } />
108+ < MarkdownBlock markdown = { debouncedContent } />
56109 </ div >
57110 ) }
58111 </ div >
0 commit comments