@@ -44,6 +44,56 @@ interface StoppedSegment {
4444
4545type MessageSegment = TextSegment | AgentGroupSegment | OptionsSegment | StoppedSegment
4646
47+ function getAgentGroupActivityKey ( items : AgentGroupItem [ ] ) : string {
48+ return items
49+ . map ( ( item ) => {
50+ if ( item . type === 'text' || item . type === 'thinking' ) {
51+ return `${ item . type } :${ item . content . length } `
52+ }
53+ if ( item . type === 'tool' ) {
54+ return [
55+ 'tool' ,
56+ item . data . id ,
57+ item . data . status ,
58+ item . data . displayTitle ,
59+ item . data . streamingArgs ?. length ?? 0 ,
60+ ] . join ( ':' )
61+ }
62+ return [
63+ 'agent' ,
64+ item . group . id ,
65+ item . group . isDelegating ? 1 : 0 ,
66+ item . group . isOpen ? 1 : 0 ,
67+ getAgentGroupActivityKey ( item . group . items ) ,
68+ ] . join ( ':' )
69+ } )
70+ . join ( '|' )
71+ }
72+
73+ /**
74+ * Compact identity for what the transcript is visibly rendering. Main-lane
75+ * reasoning and other suppressed blocks intentionally do not affect it, while
76+ * activity in every nested/parallel lane does.
77+ */
78+ function getVisibleStreamActivityKey ( segments : MessageSegment [ ] ) : string {
79+ return segments
80+ . map ( ( segment ) => {
81+ if ( segment . type === 'text' ) return `text:${ segment . id } :${ segment . content . length } `
82+ if ( segment . type === 'options' ) {
83+ return `options:${ segment . items . map ( ( item ) => `${ item . id } :${ item . label . length } ` ) . join ( ',' ) } `
84+ }
85+ if ( segment . type === 'stopped' ) return 'stopped'
86+ return [
87+ 'agent' ,
88+ segment . id ,
89+ segment . isDelegating ? 1 : 0 ,
90+ segment . isOpen ? 1 : 0 ,
91+ getAgentGroupActivityKey ( segment . items ) ,
92+ ] . join ( ':' )
93+ } )
94+ . join ( '||' )
95+ }
96+
4797const SUBAGENT_KEYS = new Set ( Object . keys ( SUBAGENT_LABELS ) )
4898
4999/**
@@ -799,7 +849,15 @@ function MessageContentInner({
799849 } , [ ] )
800850 const [ isStreamIdle , setIsStreamIdle ] = useState ( false )
801851
802- // Every rendered stream snapshot restarts the quiet-period clock. A layout
852+ const segments : MessageSegment [ ] =
853+ parsed . length > 0
854+ ? parsed
855+ : fallbackContent ?. trim ( )
856+ ? [ { type : 'text' as const , id : 'text-fallback' , content : fallbackContent } ]
857+ : [ ]
858+ const visibleStreamActivityKey = getVisibleStreamActivityKey ( segments )
859+
860+ // Every visible stream update restarts the quiet-period clock. A layout
803861 // effect clears an already-visible indicator before paint, so a chunk from
804862 // any parallel lane hides the one turn-level loader without a stale flash.
805863 useLayoutEffect ( ( ) => {
@@ -811,14 +869,7 @@ function MessageContentInner({
811869 setIsStreamIdle ( false )
812870 const timeout = setTimeout ( ( ) => setIsStreamIdle ( true ) , STREAM_IDLE_DELAY_MS )
813871 return ( ) => clearTimeout ( timeout )
814- } , [ blocks , fallbackContent , isStreaming ] )
815-
816- const segments : MessageSegment [ ] =
817- parsed . length > 0
818- ? parsed
819- : fallbackContent ?. trim ( )
820- ? [ { type : 'text' as const , id : 'text-fallback' , content : fallbackContent } ]
821- : [ ]
872+ } , [ visibleStreamActivityKey , isStreaming ] )
822873
823874 const lastSegment = segments [ segments . length - 1 ]
824875 const hasTrailingTextSegment = lastSegment ?. type === 'text'
0 commit comments