Skip to content

Commit 2cbecb5

Browse files
improvement(mothership): stable thinking indicator and jump-free streaming scroll (#5828)
* improvement(mothership): stable thinking indicator and jump-free streaming scroll * fix(mothership): suppress shimmer over executing tool rows and seed chase interrupt baseline * fix(mothership): keep shimmer mounted through the slot collapse so it animates out * improvement(mothership): tighten transcript bottom padding, timed slot-exit latch, cleanup pass * fix(mothership): bridge hidden special-tag streaming with the shimmer * fix(mothership): hold sizer floor through reveal and reset chase deadline on park * improvement(mothership): swap actions into the thinking slot at settle, quicken the chase
1 parent 1b48eeb commit 2cbecb5

7 files changed

Lines changed: 333 additions & 237 deletions

File tree

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { extractTextContent } from '@/lib/core/utils/react-node-text'
1717
import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon'
1818
import {
1919
type ContentSegment,
20-
PendingTagIndicator,
2120
parseSpecialTags,
2221
SpecialTags,
2322
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
@@ -396,8 +395,13 @@ interface ChatContentProps {
396395
onQuestionDismiss?: () => void
397396
onWorkspaceResourceSelect?: (resource: MothershipResource) => void
398397
onRevealStateChange?: (isRevealing: boolean) => void
399-
/** Reports whether this segment is actively painting text or its own pending-tag indicator. */
398+
/** Reports whether this segment is actively painting text. */
400399
onStreamActivityChange?: (active: boolean) => void
400+
/**
401+
* Reports whether a special tag is mid-stream — bytes arriving but rendering
402+
* nothing (tags are suppressed until complete). A wait from the user's POV.
403+
*/
404+
onPendingTagChange?: (pending: boolean) => void
401405
}
402406

403407
function ChatContentInner({
@@ -409,6 +413,7 @@ function ChatContentInner({
409413
onWorkspaceResourceSelect,
410414
onRevealStateChange,
411415
onStreamActivityChange,
416+
onPendingTagChange,
412417
}: ChatContentProps) {
413418
const onWorkspaceResourceSelectRef = useRef(onWorkspaceResourceSelect)
414419
onWorkspaceResourceSelectRef.current = onWorkspaceResourceSelect
@@ -530,12 +535,17 @@ function ChatContentInner({
530535
() => parseSpecialTags(streamedContent, isRevealing),
531536
[streamedContent, isRevealing]
532537
)
533-
const hasPendingIndicator = parsed.hasPendingTag && isRevealing
534538

535539
useEffect(() => {
536-
onStreamActivityChange?.(hasRevealBacklog || hasPendingIndicator)
540+
onStreamActivityChange?.(hasRevealBacklog)
537541
return () => onStreamActivityChange?.(false)
538-
}, [hasPendingIndicator, hasRevealBacklog, onStreamActivityChange])
542+
}, [hasRevealBacklog, onStreamActivityChange])
543+
544+
const hasPendingTag = parsed.hasPendingTag && isRevealing
545+
useEffect(() => {
546+
onPendingTagChange?.(hasPendingTag)
547+
return () => onPendingTagChange?.(false)
548+
}, [hasPendingTag, onPendingTagChange])
539549

540550
type BlockSegment = Exclude<
541551
ContentSegment,
@@ -624,7 +634,6 @@ function ChatContentInner({
624634
/>
625635
)
626636
})}
627-
{hasPendingIndicator && <PendingTagIndicator />}
628637
</div>
629638
)
630639
}

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -559,13 +559,18 @@ export function SpecialTags({
559559
}
560560
}
561561

562+
interface PendingTagIndicatorProps {
563+
/** Activity phrase next to the loader; crossfades on change. */
564+
label: string
565+
}
566+
562567
/**
563-
* Renders a "Thinking" shimmer while a special tag is still streaming in.
568+
* Renders the turn-level activity shimmer.
564569
*/
565-
export function PendingTagIndicator() {
570+
export function PendingTagIndicator({ label }: PendingTagIndicatorProps) {
566571
return (
567572
<div className='animate-stream-fade-in py-2'>
568-
<ThinkingLoader size={20} startVariant='corners' label='Thinking…' labelRatio={0.7} />
573+
<ThinkingLoader size={20} startVariant='corners' label={label} labelRatio={0.7} />
569574
</div>
570575
)
571576
}

apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.test.ts

Lines changed: 25 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { modelToContentBlocks } from '@/app/workspace/[workspaceId]/home/hooks/s
1414
import type { ContentBlock } from '../../types'
1515
import {
1616
assistantMessageHasVisibleExecutingTool,
17+
deriveThinkingLabel,
1718
parseBlocks,
18-
shouldShowTrailingThinking,
1919
shouldSmoothTextSegment,
2020
} from './message-content'
2121

@@ -628,62 +628,6 @@ describe('narration text seams', () => {
628628
})
629629
})
630630

631-
describe('shouldShowTrailingThinking', () => {
632-
it('shows one turn-level indicator while an open subagent waits between completed steps', () => {
633-
expect(
634-
shouldShowTrailingThinking({
635-
isStreaming: true,
636-
isStreamIdle: true,
637-
isRenderingStream: false,
638-
hasExecutingTool: false,
639-
lastSegmentType: 'agent_group',
640-
})
641-
).toBe(true)
642-
})
643-
644-
it('stays hidden while a chunk is rendering or before the stream becomes idle', () => {
645-
expect(
646-
shouldShowTrailingThinking({
647-
isStreaming: true,
648-
isStreamIdle: true,
649-
isRenderingStream: true,
650-
hasExecutingTool: false,
651-
lastSegmentType: 'text',
652-
})
653-
).toBe(false)
654-
expect(
655-
shouldShowTrailingThinking({
656-
isStreaming: true,
657-
isStreamIdle: false,
658-
isRenderingStream: false,
659-
hasExecutingTool: false,
660-
lastSegmentType: 'agent_group',
661-
})
662-
).toBe(false)
663-
})
664-
665-
it('does not duplicate an executing tool row or survive a stopped turn', () => {
666-
expect(
667-
shouldShowTrailingThinking({
668-
isStreaming: true,
669-
isStreamIdle: true,
670-
isRenderingStream: false,
671-
hasExecutingTool: true,
672-
lastSegmentType: 'agent_group',
673-
})
674-
).toBe(false)
675-
expect(
676-
shouldShowTrailingThinking({
677-
isStreaming: true,
678-
isStreamIdle: true,
679-
isRenderingStream: false,
680-
hasExecutingTool: false,
681-
lastSegmentType: 'stopped',
682-
})
683-
).toBe(false)
684-
})
685-
})
686-
687631
describe('parseBlocks legacy — thinking between top-level tools', () => {
688632
it('keeps consecutive mothership tools in one group across intervening thinking', () => {
689633
const blocks: ContentBlock[] = [
@@ -793,3 +737,27 @@ describe('assistantMessageHasVisibleExecutingTool', () => {
793737
expect(assistantMessageHasVisibleExecutingTool(blocks)).toBe(false)
794738
})
795739
})
740+
741+
describe('deriveThinkingLabel', () => {
742+
it('maps the most recent block to an activity phrase', () => {
743+
expect(deriveThinkingLabel([])).toBe('Thinking…')
744+
expect(deriveThinkingLabel([{ type: 'thinking', content: 'hm', timestamp: 1 }])).toBe(
745+
'Thinking…'
746+
)
747+
// A stall after streamed text is the agent deciding what's next, not generating.
748+
expect(deriveThinkingLabel([mainText('hi')])).toBe('Thinking…')
749+
expect(deriveThinkingLabel([{ type: 'subagent_text', content: 'x', timestamp: 1 }])).toBe(
750+
'Thinking…'
751+
)
752+
expect(deriveThinkingLabel([{ type: 'subagent_end', spanId: 'S1', timestamp: 1 }])).toBe(
753+
'Returning…'
754+
)
755+
})
756+
757+
it('shows Dispatching for the dispatch call, then yields to the opened lane', () => {
758+
expect(deriveThinkingLabel([mainToolCall('t1', 'workflow')])).toBe('Dispatching…')
759+
expect(deriveThinkingLabel([mainToolCall('t1', 'workspace_file')])).toBe('Dispatching…')
760+
expect(deriveThinkingLabel([mainToolCall('t1', 'grep')])).toBe('Thinking…')
761+
expect(deriveThinkingLabel([subagentStart('workflow', 'S1', 'main')])).toBeNull()
762+
})
763+
})

0 commit comments

Comments
 (0)