Skip to content

Commit 3961a9c

Browse files
fix(mothership): unify plain/special render branches to stop whole-message re-fade when options arrive
1 parent 61f6e6c commit 3961a9c

1 file changed

Lines changed: 77 additions & 83 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx

Lines changed: 77 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)