@@ -433,95 +433,89 @@ function ChatContentInner({
433433 ( ) => parseSpecialTags ( streamedContent , isRevealing ) ,
434434 [ streamedContent , isRevealing ]
435435 )
436- const hasSpecialContent = parsed . hasPendingTag || parsed . segments . some ( ( s ) => s . type !== 'text' )
437-
438- if ( hasSpecialContent ) {
439- type BlockSegment = Exclude <
440- ContentSegment ,
441- { type : 'text' } | { type : 'thinking' } | { type : 'workspace_resource' }
442- >
443- type RenderGroup =
444- | { kind : 'inline' ; markdown : string }
445- | { kind : 'block' ; segment : BlockSegment ; index : number }
446-
447- const groups : RenderGroup [ ] = [ ]
448- let pendingMarkdown = ''
449-
450- const flushMarkdown = ( ) => {
451- if ( pendingMarkdown . trim ( ) ) {
452- groups . push ( { kind : 'inline' , markdown : pendingMarkdown } )
453- }
454- pendingMarkdown = ''
455- }
456436
457- for ( let i = 0 ; i < parsed . segments . length ; i ++ ) {
458- const s = parsed . segments [ i ]
459- const nextSegment = parsed . segments [ i + 1 ]
460- if ( s . type === 'workspace_resource' ) {
461- // Files are addressed by their encoded VFS path (copied verbatim from the tag);
462- // workflows/tables/KBs by id. The angle-bracket link destination keeps the path
463- // intact through markdown parsing (tolerates parens) without re-encoding it.
464- const ref = s . data . type === 'file' ? ( s . data . path ?? s . data . id ?? '' ) : ( s . data . id ?? '' )
465- const label = s . data . title || ref
466- pendingMarkdown = appendInlineReferenceMarkdown (
467- pendingMarkdown ,
468- `[${ label } ](<#wsres-${ s . data . type } -${ ref } >)` ,
469- nextSegment
470- )
471- } else if ( s . type === 'text' || s . type === 'thinking' ) {
472- pendingMarkdown += s . content
473- } else {
474- flushMarkdown ( )
475- groups . push ( { kind : 'block' , segment : s , index : i } )
476- }
437+ type BlockSegment = Exclude <
438+ ContentSegment ,
439+ { type : 'text' } | { type : 'thinking' } | { type : 'workspace_resource' }
440+ >
441+ type RenderGroup =
442+ | { kind : 'inline' ; markdown : string }
443+ | { kind : 'block' ; segment : BlockSegment ; index : number }
444+
445+ const groups : RenderGroup [ ] = [ ]
446+ let pendingMarkdown = ''
447+
448+ const flushMarkdown = ( ) => {
449+ if ( pendingMarkdown . trim ( ) ) {
450+ groups . push ( { kind : 'inline' , markdown : pendingMarkdown } )
477451 }
478- flushMarkdown ( )
452+ pendingMarkdown = ''
453+ }
479454
480- return (
481- < div className = 'space-y-3' >
482- { groups . map ( ( group , i ) => {
483- if ( group . kind === 'inline' ) {
484- return (
485- < div
486- key = { `inline-${ i } ` }
487- className = { cn ( PROSE_CLASSES , '[&>:first-child]:mt-0 [&>:last-child]:mb-0' ) }
488- >
489- < Streamdown
490- key = { streamingTree ? 'stream' : 'settled' }
491- mode = { parserTree ? undefined : 'static' }
492- animated = { fadeActive ? STREAM_ANIMATION : false }
493- isAnimating = { streamingTree }
494- components = { MARKDOWN_COMPONENTS }
495- >
496- { group . markdown }
497- </ Streamdown >
498- </ div >
499- )
500- }
501- return (
502- < SpecialTags
503- key = { `special-${ group . index } ` }
504- segment = { group . segment }
505- onOptionSelect = { onOptionSelect }
506- />
507- )
508- } ) }
509- { parsed . hasPendingTag && isRevealing && < PendingTagIndicator /> }
510- </ div >
511- )
455+ for ( let i = 0 ; i < parsed . segments . length ; i ++ ) {
456+ const s = parsed . segments [ i ]
457+ const nextSegment = parsed . segments [ i + 1 ]
458+ if ( s . type === 'workspace_resource' ) {
459+ // Files are addressed by their encoded VFS path (copied verbatim from the tag);
460+ // workflows/tables/KBs by id. The angle-bracket link destination keeps the path
461+ // intact through markdown parsing (tolerates parens) without re-encoding it.
462+ const ref = s . data . type === 'file' ? ( s . data . path ?? s . data . id ?? '' ) : ( s . data . id ?? '' )
463+ const label = s . data . title || ref
464+ pendingMarkdown = appendInlineReferenceMarkdown (
465+ pendingMarkdown ,
466+ `[${ label } ](<#wsres-${ s . data . type } -${ ref } >)` ,
467+ nextSegment
468+ )
469+ } else if ( s . type === 'text' || s . type === 'thinking' ) {
470+ pendingMarkdown += s . content
471+ } else {
472+ flushMarkdown ( )
473+ groups . push ( { kind : 'block' , segment : s , index : i } )
474+ }
512475 }
476+ flushMarkdown ( )
513477
478+ /**
479+ * Plain text and special-tag content share ONE render structure. A message
480+ * with no special tags is simply a single inline group — it must NOT get a
481+ * dedicated JSX branch, because most replies gain a trailing `<options>` tag
482+ * (suggested follow-ups) at the very end, and switching branches at that
483+ * moment re-parents the Streamdown to a different tree position. React then
484+ * remounts it with a fresh animate plugin and the ENTIRE message re-fades
485+ * from transparent — the "flash at the conclusion". With the unified
486+ * structure the leading text group keeps its position (`inline-0`) and only
487+ * the new special block mounts.
488+ */
514489 return (
515- < div className = { cn ( PROSE_CLASSES , '[&>:first-child]:mt-0 [&>:last-child]:mb-0' ) } >
516- < Streamdown
517- key = { streamingTree ? 'stream' : 'settled' }
518- mode = { parserTree ? undefined : 'static' }
519- animated = { fadeActive ? STREAM_ANIMATION : false }
520- isAnimating = { streamingTree }
521- components = { MARKDOWN_COMPONENTS }
522- >
523- { streamedContent }
524- </ Streamdown >
490+ < div className = 'space-y-3' >
491+ { groups . map ( ( group , i ) => {
492+ if ( group . kind === 'inline' ) {
493+ return (
494+ < div
495+ key = { `inline-${ i } ` }
496+ className = { cn ( PROSE_CLASSES , '[&>:first-child]:mt-0 [&>:last-child]:mb-0' ) }
497+ >
498+ < Streamdown
499+ key = { streamingTree ? 'stream' : 'settled' }
500+ mode = { parserTree ? undefined : 'static' }
501+ animated = { fadeActive ? STREAM_ANIMATION : false }
502+ isAnimating = { streamingTree }
503+ components = { MARKDOWN_COMPONENTS }
504+ >
505+ { group . markdown }
506+ </ Streamdown >
507+ </ div >
508+ )
509+ }
510+ return (
511+ < SpecialTags
512+ key = { `special-${ group . index } ` }
513+ segment = { group . segment }
514+ onOptionSelect = { onOptionSelect }
515+ />
516+ )
517+ } ) }
518+ { parsed . hasPendingTag && isRevealing && < PendingTagIndicator /> }
525519 </ div >
526520 )
527521}
0 commit comments