@@ -107,6 +107,16 @@ export class JsonEventEmitter {
107107 private previousContent = new Map < number , string > ( )
108108 // Track previous tool-use content for structured (non-append-only) delta computation.
109109 private previousToolUseContent = new Map < number , string > ( )
110+ // Track the currently active execute_command tool_use id for command_output correlation.
111+ private activeCommandToolUseId : number | undefined
112+ // Track command output snapshots by command tool-use id for delta computation.
113+ private previousCommandOutputByToolUseId = new Map < number , string > ( )
114+ // Track command ids whose output is being streamed from commandExecutionStatus updates.
115+ private statusDrivenCommandOutputIds = new Set < number > ( )
116+ // Track command ids that already emitted a terminal command_output done event.
117+ private completedCommandOutputIds = new Set < number > ( )
118+ // Suppress the next say:command_output completion message after status-driven streaming.
119+ private suppressNextCommandOutputSay = false
110120 // Track the completion result content
111121 private completionResultContent : string | undefined
112122 // Track the latest assistant text as a fallback for result.content.
@@ -288,6 +298,90 @@ export class JsonEventEmitter {
288298 return this . mode === "stream-json" && content === null
289299 }
290300
301+ private computeCommandOutputDelta ( commandId : number , fullOutput : string | undefined ) : string | null {
302+ const normalized = fullOutput ?? ""
303+ const previous = this . previousCommandOutputByToolUseId . get ( commandId ) || ""
304+
305+ if ( normalized === previous ) {
306+ return null
307+ }
308+
309+ this . previousCommandOutputByToolUseId . set ( commandId , normalized )
310+ return normalized . startsWith ( previous ) ? normalized . slice ( previous . length ) : normalized
311+ }
312+
313+ private emitCommandOutputEvent ( commandId : number , fullOutput : string | undefined , isDone : boolean ) : void {
314+ if ( this . mode === "stream-json" ) {
315+ const outputDelta = this . computeCommandOutputDelta ( commandId , fullOutput )
316+ const event : JsonEvent = {
317+ type : "tool_result" ,
318+ id : commandId ,
319+ subtype : "command" ,
320+ tool_result : { name : "execute_command" } ,
321+ }
322+
323+ if ( outputDelta !== null && outputDelta . length > 0 ) {
324+ event . tool_result = { name : "execute_command" , output : outputDelta }
325+ }
326+
327+ if ( isDone ) {
328+ event . done = true
329+ this . previousCommandOutputByToolUseId . delete ( commandId )
330+ this . statusDrivenCommandOutputIds . delete ( commandId )
331+ this . completedCommandOutputIds . add ( commandId )
332+ if ( this . activeCommandToolUseId === commandId ) {
333+ this . activeCommandToolUseId = undefined
334+ }
335+ }
336+
337+ // Suppress empty partial updates that carry no delta.
338+ if ( ! isDone && outputDelta === null ) {
339+ return
340+ }
341+
342+ this . emitEvent ( event )
343+ return
344+ }
345+
346+ this . emitEvent ( {
347+ type : "tool_result" ,
348+ id : commandId ,
349+ subtype : "command" ,
350+ tool_result : { name : "execute_command" , output : fullOutput } ,
351+ ...( isDone ? { done : true } : { } ) ,
352+ } )
353+
354+ if ( isDone ) {
355+ this . previousCommandOutputByToolUseId . delete ( commandId )
356+ this . statusDrivenCommandOutputIds . delete ( commandId )
357+ this . completedCommandOutputIds . add ( commandId )
358+ if ( this . activeCommandToolUseId === commandId ) {
359+ this . activeCommandToolUseId = undefined
360+ }
361+ }
362+ }
363+
364+ public emitCommandOutputChunk ( outputSnapshot : string ) : void {
365+ const commandId = this . activeCommandToolUseId
366+ if ( commandId === undefined ) {
367+ return
368+ }
369+
370+ this . statusDrivenCommandOutputIds . add ( commandId )
371+ this . emitCommandOutputEvent ( commandId , outputSnapshot , false )
372+ }
373+
374+ public emitCommandOutputDone ( ) : void {
375+ const commandId = this . activeCommandToolUseId
376+ if ( commandId === undefined ) {
377+ return
378+ }
379+
380+ this . statusDrivenCommandOutputIds . add ( commandId )
381+ this . suppressNextCommandOutputSay = true
382+ this . emitCommandOutputEvent ( commandId , undefined , true )
383+ }
384+
291385 /**
292386 * Get content to send for a message (delta for streaming, full for json mode).
293387 */
@@ -392,10 +486,7 @@ export class JsonEventEmitter {
392486 break
393487
394488 case "command_output" :
395- this . emitEvent ( {
396- type : "tool_result" ,
397- tool_result : { name : "execute_command" , output : msg . text } ,
398- } )
489+ this . handleCommandOutputMessage ( msg , isDone )
399490 break
400491
401492 case "user_feedback" :
@@ -517,6 +608,10 @@ export class JsonEventEmitter {
517608 const toolInfo = parseToolInfo ( msg . text )
518609
519610 if ( subtype === "command" ) {
611+ this . activeCommandToolUseId = msg . ts
612+ this . completedCommandOutputIds . delete ( msg . ts )
613+ this . suppressNextCommandOutputSay = false
614+
520615 if ( isStreamingPartial ) {
521616 const commandDelta = this . computeStructuredDelta ( msg . ts , msg . text )
522617 if ( commandDelta === null ) {
@@ -595,6 +690,21 @@ export class JsonEventEmitter {
595690 } )
596691 }
597692
693+ private handleCommandOutputMessage ( msg : ClineMessage , isDone : boolean ) : void {
694+ if ( this . suppressNextCommandOutputSay ) {
695+ if ( isDone ) {
696+ this . suppressNextCommandOutputSay = false
697+ }
698+ return
699+ }
700+
701+ const commandId = this . activeCommandToolUseId ?? msg . ts
702+ if ( this . statusDrivenCommandOutputIds . has ( commandId ) || this . completedCommandOutputIds . has ( commandId ) ) {
703+ return
704+ }
705+ this . emitCommandOutputEvent ( commandId , msg . text , isDone )
706+ }
707+
598708 /**
599709 * Handle task completion and emit result event.
600710 */
@@ -711,6 +821,11 @@ export class JsonEventEmitter {
711821 this . seenMessageIds . clear ( )
712822 this . previousContent . clear ( )
713823 this . previousToolUseContent . clear ( )
824+ this . activeCommandToolUseId = undefined
825+ this . previousCommandOutputByToolUseId . clear ( )
826+ this . statusDrivenCommandOutputIds . clear ( )
827+ this . completedCommandOutputIds . clear ( )
828+ this . suppressNextCommandOutputSay = false
714829 this . completionResultContent = undefined
715830 this . lastAssistantText = undefined
716831 this . expectPromptEchoAsUser = true
0 commit comments