@@ -236,70 +236,64 @@ const ChatBubble = ({
236236 components = { {
237237 a : ( { href, children } ) => (
238238 < LinkButton href = { href } children = { children } />
239- ) ,
240- // Ensure code blocks are rendered with a plain background
241- pre : ( { node, ...props } ) => (
242- < pre className = "bg-transparent p-0" { ...props } />
243- ) ,
244- code : ( { node, ...props } ) => (
245- < code className = "text-white" { ...props } />
246239 )
247240 } }
248241 />
249242 )
250243 }
251244
252245 const contentParts = [ ]
253- // Regex to capture all custom tags: <think>, <tool_code>, and <tool_result>
254246 const regex =
255247 / ( < t h i n k > [ \s \S ] * ?< \/ t h i n k > | < t o o l _ c o d e n a m e = " [ ^ " ] + " > [ \s \S ] * ?< \/ t o o l _ c o d e > | < t o o l _ r e s u l t t o o l _ n a m e = " [ ^ " ] + " > [ \s \S ] * ?< \/ t o o l _ r e s u l t > ) / g
256248 let lastIndex = 0
257249
250+ // --- START OF THE ROBUST FIX ---
251+
252+ // Helper function to check for and filter out junk tokens
253+ const isJunk = ( text ) => {
254+ const trimmed = text . trim ( )
255+ if ( trimmed === "" ) return true // Ignore whitespace-only strings
256+
257+ // This regex identifies common junk patterns seen in logs.
258+ // It looks for fragments of closing tags or orphaned tag-like words.
259+ const junkRegex = / ^ ( < \/ ( \w + > ) ? | _ c o d e > | c o d e > | o d e > | > ) $ /
260+ return junkRegex . test ( trimmed )
261+ }
262+
258263 for ( const match of message . matchAll ( regex ) ) {
259- // Capture the text before the current tag
264+ // 1. Process the text * before* the current valid tag
260265 if ( match . index > lastIndex ) {
261266 const textContent = message . substring ( lastIndex , match . index )
262- const isPartialTag =
263- textContent . startsWith ( "<tool_" ) ||
264- textContent . startsWith ( "<think" )
265- if ( textContent . trim ( ) && ! isPartialTag ) {
266- contentParts . push ( {
267- type : "text" ,
268- content : textContent
269- } )
267+ // Only add the text if it's NOT junk
268+ if ( ! isJunk ( textContent ) ) {
269+ contentParts . push ( { type : "text" , content : textContent } )
270270 }
271271 }
272272
273- const tag = match [ 0 ] // The full tag match
273+ // 2. Process the valid, complete tag itself
274+ const tag = match [ 0 ]
274275 let subMatch
275276
276- // Check for <tool_code>
277277 if (
278278 ( subMatch = tag . match (
279279 / < t o o l _ c o d e n a m e = " ( [ ^ " ] + ) " > ( [ \s \S ] * ?) < \/ t o o l _ c o d e > /
280280 ) )
281281 ) {
282282 const toolName = subMatch [ 1 ]
283- const toolCode = subMatch [ 2 ] . trim ( )
284- if ( toolName && toolCode ) {
285- contentParts . push ( {
286- type : "tool_code" ,
287- name : toolName ,
288- code : toolCode
289- } )
290- }
291- }
292- // Check for <tool_result>
293- else if (
283+ // Handle empty tool_code blocks gracefully
284+ const toolCode = subMatch [ 2 ] ? subMatch [ 2 ] . trim ( ) : "{}"
285+ contentParts . push ( {
286+ type : "tool_code" ,
287+ name : toolName ,
288+ code : toolCode
289+ } )
290+ } else if (
294291 ( subMatch = tag . match (
295292 / < t o o l _ r e s u l t t o o l _ n a m e = " ( [ ^ " ] + ) " > ( [ \s \S ] * ?) < \/ t o o l _ r e s u l t > /
296293 ) )
297294 ) {
298- // We parse tool_result but do not add it to contentParts, effectively hiding it.
299- // The agent's summary should be in a subsequent "text" part.
300- }
301- // Check for <think>
302- if ( ( subMatch = tag . match ( / < t h i n k > ( [ \s \S ] * ?) < \/ t h i n k > / ) ) ) {
295+ // We correctly parse and then ignore/hide tool_result
296+ } else if ( ( subMatch = tag . match ( / < t h i n k > ( [ \s \S ] * ?) < \/ t h i n k > / ) ) ) {
303297 const thinkContent = subMatch [ 1 ] . trim ( )
304298 if ( thinkContent ) {
305299 contentParts . push ( {
@@ -309,23 +303,24 @@ const ChatBubble = ({
309303 }
310304 }
311305
306+ // 3. Update our position in the message string
312307 lastIndex = match . index + tag . length
313308 }
314309
315- // Capture any remaining text after the last tag
310+ // 4. Process any remaining text after the last valid tag
316311 if ( lastIndex < message . length ) {
317- const textContent = message . substring ( lastIndex )
318- const isPartialTag =
319- textContent . startsWith ( "<tool_" ) ||
320- textContent . startsWith ( "<think" )
321- if ( textContent . trim ( ) && ! isPartialTag ) {
322- contentParts . push ( {
323- type : "text" , // The final part of the message
324- content : textContent
325- } )
312+ const remainingText = message . substring ( lastIndex )
313+ // Also check the final part for junk or incomplete streaming tags
314+ const openBrackets = ( message . match ( / < / g) || [ ] ) . length
315+ const closeBrackets = ( message . match ( / > / g) || [ ] ) . length
316+
317+ if ( ! isJunk ( remainingText ) && openBrackets <= closeBrackets ) {
318+ contentParts . push ( { type : "text" , content : remainingText } )
326319 }
327320 }
321+ // --- END OF THE ROBUST FIX ---
328322
323+ // The rest of the rendering logic remains the same
329324 return contentParts . map ( ( part , index ) => {
330325 const partId = `${ part . type } _${ index } `
331326 if ( part . type === "think" && part . content ) {
@@ -368,14 +363,12 @@ const ChatBubble = ({
368363 />
369364 )
370365 }
371-
372366 if ( part . type === "text" && part . content . trim ( ) ) {
373367 return (
374368 < ReactMarkdown
375369 key = { partId }
376370 className = "prose prose-invert"
377371 remarkPlugins = { [ remarkGfm ] }
378- // The agent's summary and textual response
379372 children = { part . content }
380373 components = { {
381374 a : ( { href, children } ) => (
0 commit comments