@@ -190,13 +190,50 @@ class NestedAgentSessionComponent extends Container {
190190 private cachedLines ?: string [ ] ;
191191 private cachedShowImages ?: boolean ;
192192
193+ /** Tracks whether a render microtask is already queued for this batch. */
194+ private renderScheduled = false ;
195+ /** Monotonic token: incrementing invalidates stale queued microtask callbacks. */
196+ private renderScheduleToken = 0 ;
197+
193198 private clearRenderCache ( ) : void {
194199 this . cachedWidth = undefined ;
195200 this . cachedExpanded = undefined ;
196201 this . cachedLines = undefined ;
197202 this . cachedShowImages = undefined ;
198203 }
199204
205+ /** Reset batching guard + invalidate any queued microtask via token bump. */
206+ private resetRenderBatching ( ) : void {
207+ this . renderScheduled = false ;
208+ this . renderScheduleToken ++ ;
209+ }
210+
211+ /**
212+ * Queue one microtask per event batch. The token guard prevents stale
213+ * callbacks from running after dispose or error resets the batching state.
214+ */
215+ private scheduleRender ( ) : void {
216+ if ( this . renderScheduled ) {
217+ return ;
218+ }
219+ this . renderScheduled = true ;
220+ const token = ++ this . renderScheduleToken ;
221+ queueMicrotask ( ( ) => {
222+ if ( ! this . renderScheduled || this . renderScheduleToken !== token ) {
223+ return ;
224+ }
225+ this . renderScheduled = false ;
226+ if ( this . isStaleSession ( ) ) {
227+ // Drop optimistic same-turn event mutations when ownership changed
228+ // before the parent had a chance to render them.
229+ this . rebuildFromSession ( ) ;
230+ this . clearRenderCache ( ) ;
231+ return ;
232+ }
233+ this . requestRender ( ) ;
234+ } ) ;
235+ }
236+
200237 setRequestRender ( requestRender : ( ) => void ) : void {
201238 this . requestRender = requestRender ;
202239 }
@@ -247,6 +284,7 @@ class NestedAgentSessionComponent extends Container {
247284 this . ownedToolCallId = toolCallId ;
248285 this . state = state ;
249286 this . attachedChildSessionEpoch = state . childSessionEpoch ;
287+ this . resetRenderBatching ( ) ;
250288 this . liveOutcome = this . details ?. outcome ?? "running" ;
251289 this . toolNames . clear ( ) ;
252290 this . toolComponentFailures . clear ( ) ;
@@ -315,6 +353,7 @@ class NestedAgentSessionComponent extends Container {
315353 const session = this . session ;
316354 const ownedToolCallId = this . ownedToolCallId ;
317355 const liveChildSessions = this . state ?. liveChildSessions ;
356+ this . resetRenderBatching ( ) ;
318357 this . clearRenderCache ( ) ;
319358 this . details = undefined ;
320359 this . nestTheme = undefined ;
@@ -730,8 +769,13 @@ class NestedAgentSessionComponent extends Container {
730769 case "tool_execution_end" : this . handleToolExecutionEnd ( event ) ; break ;
731770 }
732771 this . clearRenderCache ( ) ;
733- this . requestRender ( ) ;
772+ // Coalesce child bursts within the current turn. The first event queues
773+ // one microtask-backed parent invalidate; later same-turn events just
774+ // mutate state, so the next render observes the latest snapshot.
775+ this . scheduleRender ( ) ;
734776 } catch ( error ) {
777+ this . clearRenderCache ( ) ;
778+ this . resetRenderBatching ( ) ;
735779 // Prevent a single bad event from killing the subscription.
736780 // The TUI degrades gracefully — stale content until next successful event.
737781 console . warn ( "[spawn] Event handler error:" , event . type , this . ownedToolCallId , error ) ;
0 commit comments