@@ -45,6 +45,56 @@ const updateBlocksRecursively = (
4545 } )
4646}
4747
48+ // Helper function to process buffered text and filter out tool calls
49+ const processToolCallBuffer = (
50+ bufferState : { buffer : string ; insideToolCall : boolean } ,
51+ onTextOutput : ( text : string ) => void ,
52+ ) => {
53+ let processed = false
54+
55+ if (
56+ ! bufferState . insideToolCall &&
57+ bufferState . buffer . includes ( '<codebuff_tool_call>' )
58+ ) {
59+ const openTagIndex = bufferState . buffer . indexOf ( '<codebuff_tool_call>' )
60+ const text = bufferState . buffer . substring ( 0 , openTagIndex )
61+ if ( text ) {
62+ onTextOutput ( text )
63+ }
64+ bufferState . insideToolCall = true
65+ bufferState . buffer = bufferState . buffer . substring (
66+ openTagIndex + '<codebuff_tool_call>' . length ,
67+ )
68+ processed = true
69+ } else if (
70+ bufferState . insideToolCall &&
71+ bufferState . buffer . includes ( '</codebuff_tool_call>' )
72+ ) {
73+ const closeTagIndex = bufferState . buffer . indexOf ( '</codebuff_tool_call>' )
74+ bufferState . insideToolCall = false
75+ bufferState . buffer = bufferState . buffer . substring (
76+ closeTagIndex + '</codebuff_tool_call>' . length ,
77+ )
78+ processed = true
79+ } else if ( ! bufferState . insideToolCall && bufferState . buffer . length > 25 ) {
80+ // Output safe text, keeping last 25 chars in buffer (enough to buffer <codebuff_tool_call>)
81+ const safeToOutput = bufferState . buffer . substring (
82+ 0 ,
83+ bufferState . buffer . length - 25 ,
84+ )
85+ if ( safeToOutput ) {
86+ onTextOutput ( safeToOutput )
87+ }
88+ bufferState . buffer = bufferState . buffer . substring (
89+ bufferState . buffer . length - 25 ,
90+ )
91+ }
92+
93+ if ( processed ) {
94+ processToolCallBuffer ( bufferState , onTextOutput )
95+ }
96+ }
97+
4898interface UseSendMessageOptions {
4999 setMessages : React . Dispatch < React . SetStateAction < ChatMessage [ ] > >
50100 setFocusedAgentId : ( id : string | null ) => void
@@ -404,57 +454,8 @@ export const useSendMessage = ({
404454 signal : abortController . signal ,
405455
406456 handleStreamChunk : ( chunk : any ) => {
407- const keys = Object . keys ( chunk )
408- . filter ( ( k ) => ! isNaN ( Number ( k ) ) )
409- . sort ( ( a , b ) => Number ( a ) - Number ( b ) )
410- let text = keys . map ( ( k ) => chunk [ k ] ) . join ( '' )
411-
412- text = text . replace (
413- / < c o d e b u f f _ t o o l _ c a l l > [ \s \S ] * ?< \/ c o d e b u f f _ t o o l _ c a l l > / g,
414- '' ,
415- )
416-
417- if ( ! text ) return
418-
419- if ( ! hasReceivedContent ) {
420- hasReceivedContent = true
421- setIsWaitingForResponse ( false )
422- }
423-
424- logger . info ( 'setMessages: handleStreamChunk (main agent text)' , {
425- text,
426- } )
427- queueMessageUpdate ( ( prev ) =>
428- prev . map ( ( msg ) => {
429- if ( msg . id !== aiMessageId ) {
430- return msg
431- }
432-
433- const blocks : ContentBlock [ ] = msg . blocks ? [ ...msg . blocks ] : [ ]
434- const lastBlock = blocks [ blocks . length - 1 ]
435-
436- if ( lastBlock && lastBlock . type === 'text' ) {
437- const newContent = lastBlock . content + text
438- const updatedTextBlock : ContentBlock = {
439- type : 'text' ,
440- content : newContent ,
441- }
442- return {
443- ...msg ,
444- blocks : [ ...blocks . slice ( 0 , - 1 ) , updatedTextBlock ] ,
445- }
446- }
447-
448- const newTextBlock : ContentBlock = {
449- type : 'text' ,
450- content : text ,
451- }
452- return {
453- ...msg ,
454- blocks : [ ...blocks , newTextBlock ] ,
455- }
456- } ) ,
457- )
457+ // Streaming chunks are also sent via text events, so we ignore them here to avoid duplication
458+ // Text events have better handling for tool call filtering
458459 } ,
459460
460461 handleEvent : ( event : any ) => {
@@ -471,59 +472,9 @@ export const useSendMessage = ({
471472
472473 bufferState . buffer += chunk
473474
474- const processBuffer = ( ) => {
475- let processed = false
476- if (
477- ! bufferState . insideToolCall &&
478- bufferState . buffer . includes ( '<codebuff_tool_call>' )
479- ) {
480- const openTagIndex = bufferState . buffer . indexOf (
481- '<codebuff_tool_call>' ,
482- )
483- const text = bufferState . buffer . substring ( 0 , openTagIndex )
484- if ( text ) {
485- updateAgentContent ( agentId , { type : 'text' , content : text } )
486- }
487- bufferState . insideToolCall = true
488- bufferState . buffer = bufferState . buffer . substring (
489- openTagIndex + '<codebuff_tool_call>' . length ,
490- )
491- processed = true
492- } else if (
493- bufferState . insideToolCall &&
494- bufferState . buffer . includes ( '</codebuff_tool_call>' )
495- ) {
496- const closeTagIndex = bufferState . buffer . indexOf (
497- '</codebuff_tool_call>' ,
498- )
499- // Skip the tool call content - we'll handle it via tool_call event
500- bufferState . insideToolCall = false
501- bufferState . buffer = bufferState . buffer . substring (
502- closeTagIndex + '</codebuff_tool_call>' . length ,
503- )
504- processed = true
505- } else if (
506- ! bufferState . insideToolCall &&
507- bufferState . buffer . length > 50
508- ) {
509- const safeToOutput = bufferState . buffer . substring (
510- 0 ,
511- bufferState . buffer . length - 50 ,
512- )
513- updateAgentContent ( agentId , {
514- type : 'text' ,
515- content : safeToOutput ,
516- } )
517- bufferState . buffer = bufferState . buffer . substring (
518- bufferState . buffer . length - 50 ,
519- )
520- }
521-
522- if ( processed ) {
523- processBuffer ( )
524- }
525- }
526- processBuffer ( )
475+ processToolCallBuffer ( bufferState , ( text ) => {
476+ updateAgentContent ( agentId , { type : 'text' , content : text } )
477+ } )
527478 return
528479 }
529480
@@ -533,15 +484,13 @@ export const useSendMessage = ({
533484 '' ,
534485 )
535486
536- if ( text . includes ( '<codebuff_tool_call>' ) ) {
537- logger . warn ( 'Tool XML detected in text event post-filter' , {
538- agentId : event . agentId ?? 'root' ,
539- textPreview : text . slice ( 0 , 80 ) ,
540- } )
541- }
542-
543487 if ( ! text ) return
544488
489+ if ( ! hasReceivedContent ) {
490+ hasReceivedContent = true
491+ setIsWaitingForResponse ( false )
492+ }
493+
545494 if ( event . agentId ) {
546495 logger . info ( 'setMessages: text event with agentId' , {
547496 agentId : event . agentId ,
@@ -551,7 +500,6 @@ export const useSendMessage = ({
551500 type : 'text' ,
552501 content : text ,
553502 } )
554- return
555503 } else {
556504 logger . info ( 'setMessages: text event without agentId' , {
557505 textPreview : text . slice ( 0 , 100 ) ,
@@ -567,7 +515,14 @@ export const useSendMessage = ({
567515 : [ ]
568516 const lastBlock = blocks [ blocks . length - 1 ]
569517
518+ // Deduplicate: if the new text is already at the end of the last block, skip it
570519 if ( lastBlock && lastBlock . type === 'text' ) {
520+ if ( lastBlock . content . endsWith ( text ) ) {
521+ logger . info ( 'Skipping duplicate main agent text' , {
522+ textPreview : text . slice ( 0 , 100 ) ,
523+ } )
524+ return msg
525+ }
571526 const updatedTextBlock : ContentBlock = {
572527 type : 'text' ,
573528 content : lastBlock . content + text ,
@@ -588,8 +543,8 @@ export const useSendMessage = ({
588543 }
589544 } ) ,
590545 )
591- return
592546 }
547+ return
593548 }
594549
595550 if ( event . type === 'finish' && event . totalCost !== undefined ) {
0 commit comments