fix(chat): render subagent content as collapsible thinking blocks#3602
fix(chat): render subagent content as collapsible thinking blocks#3602waleedlatif1 wants to merge 5 commits intostagingfrom
Conversation
PR SummaryMedium Risk Overview Streaming/persistence now track subagent text separately and record subagent durations. Written by Cursor Bugbot for commit 1805b36. Configure here. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx
Show resolved
Hide resolved
|
Fixed the cursor bot's finding — when the |
|
@greptile |
|
@cursor review |
Greptile SummaryThis PR fixes SSE content routing so subagent text and reasoning tokens are correctly directed to Key changes:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant BE as Backend SSE
participant UC as useChat (processSSEStream)
participant MC as MessageContent (parseBlocks)
participant AG as AgentGroup
participant STB as SubagentThinkingBlock
BE->>UC: subagent_start { subagent: "agent-A" }
UC->>UC: activeSubagent = "agent-A"<br/>subagentStartTime = Date.now()<br/>blocks.push({ type: "subagent", content: "agent-A" })
UC->>MC: flush() → re-render
BE->>UC: content / reasoning chunk
UC->>UC: last block is subagent_text?<br/>→ append chunk<br/>else push new subagent_text block
UC->>MC: flush() → re-render
MC->>AG: segment { duration: undefined, isStreaming: true }
AG->>STB: isStreaming={true}, content, duration={undefined}
STB-->>STB: auto-expand, shimmer "Thinking" label
BE->>UC: subagent_end
UC->>UC: elapsed = Date.now() - subagentStartTime<br/>blocks[j].duration = elapsed (backward scan)<br/>activeSubagent = undefined
UC->>MC: flush() → re-render
MC->>AG: segment { duration: 2000, isStreaming: true }
AG->>STB: isStreaming={false}, duration=2000
STB-->>STB: collapse, label → "Thought for 2s"
BE->>UC: stream end (done event)
UC->>MC: isStreaming = false
MC->>AG: isStreaming={false}
AG->>STB: isStreaming={false} (no change)
|
...kspaceId]/home/components/message-content/components/agent-group/subagent-thinking-block.tsx
Outdated
Show resolved
Hide resolved
...kspaceId]/home/components/message-content/components/agent-group/subagent-thinking-block.tsx
Show resolved
Hide resolved
…ef to main-only branch
|
@cursor review |
|
@greptile |
...rkspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx
Show resolved
Hide resolved
...kspaceId]/home/components/message-content/components/agent-group/subagent-thinking-block.tsx
Show resolved
Hide resolved
|
@cursor review |
|
@greptile |
…ion on mid-stream stop
| let lastTextIdx = -1 | ||
| for (let i = items.length - 1; i >= 0; i--) { | ||
| if (items[i].type === 'text') { | ||
| lastTextIdx = i | ||
| break | ||
| } | ||
| } |
There was a problem hiding this comment.
lastTextIdx scan should be memoized
The backward linear scan runs on every render, which happens on every SSE flush during streaming. For an AgentGroup with many items this is fine today (small N), but the scan is executed inside the component body without any memoization guard. Since items is a prop that changes reference on every parent re-render, wrapping this in useMemo makes the intent explicit and avoids the scan on renders where items hasn't changed.
| let lastTextIdx = -1 | |
| for (let i = items.length - 1; i >= 0; i--) { | |
| if (items[i].type === 'text') { | |
| lastTextIdx = i | |
| break | |
| } | |
| } | |
| const lastTextIdx = useMemo(() => { | |
| for (let i = items.length - 1; i >= 0; i--) { | |
| if (items[i].type === 'text') return i | |
| } | |
| return -1 | |
| }, [items]) |
You'll also need to add useMemo to the import at line 3:
import { useEffect, useMemo, useRef, useState } from 'react'
| type='button' | ||
| onClick={toggle} | ||
| disabled={!hasContent && !isStreaming} | ||
| className='group inline-flex items-center gap-1 text-left text-[13px] text-[var(--text-secondary)] transition-colors hover:text-[var(--text-primary)]' |
There was a problem hiding this comment.
Button clickable but visually inert when streaming with empty content
disabled={!hasContent && !isStreaming} leaves the button enabled whenever isStreaming is true, even before any content has arrived. At that point there is no ChevronDown affordance and no visible content to expand — yet a click calls toggle(), sets expanded=true, and if the user clicks again it sets userCollapsedRef.current = true, permanently suppressing the auto-expand that fires when the first chunk arrives.
A user who clicks the "Thinking" label impatiently (before content) will then see the block never auto-expand, since userCollapsedRef has been set:
| type='button' | |
| onClick={toggle} | |
| disabled={!hasContent && !isStreaming} | |
| className='group inline-flex items-center gap-1 text-left text-[13px] text-[var(--text-secondary)] transition-colors hover:text-[var(--text-primary)]' | |
| disabled={!hasContent} |
This disables the button until content exists, regardless of streaming state. The auto-expand effect already handles opening the block as soon as hasContent becomes true during streaming, so there's no need for early interaction.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| blocks[j].duration = elapsed | ||
| break | ||
| } | ||
| } |
There was a problem hiding this comment.
Replayed SSE events produce near-zero subagent durations
Low Severity
During SSE stream reconnection, all cached batch events are enqueued as a single chunk and processed synchronously. subagentStartTime is set to Date.now() when subagent_start is processed, and subagent_end computes Date.now() - subagentStartTime moments later, yielding ~0ms. The 1s floor causes every replayed subagent to display "Thought for 1s" regardless of actual duration.


Summary
subagent_textblocks instead of leaking into main chat textSubagentThinkingBlockcomponent with streaming text carousel, shimmer animation, and collapsible "Thought for Xs" labeldurationandisStreamingthroughAgentGroupandMessageContentreasoningSSE events correctly (only render inside subagents, drop otherwise)Type of Change
Testing
Tested manually
Checklist