diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx index 12128e905f..b7a073dab5 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx @@ -1,16 +1,14 @@ 'use client' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useLayoutEffect, useRef, useState } from 'react' import { ChevronDown, Expandable, ExpandableContent, PillsRing } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import type { ToolCallData } from '../../../../types' import { getAgentIcon } from '../../utils' -import { ThinkingBlock } from '../thinking-block' import { ToolCallItem } from './tool-call-item' export type AgentGroupItem = | { type: 'text'; content: string } - | { type: 'thinking'; content: string; startedAt?: number; endedAt?: number } | { type: 'tool'; data: ToolCallData } interface AgentGroupProps { @@ -113,52 +111,117 @@ export function AgentGroup({ {hasItems && ( -
- {items.map((item, idx) => { - if (item.type === 'tool') { - return ( - - ) - } - if (item.type === 'thinking') { - const elapsedMs = - item.startedAt !== undefined && item.endedAt !== undefined - ? item.endedAt - item.startedAt - : undefined - if (elapsedMs !== undefined && elapsedMs <= 3000) return null - return ( -
- +
+ {items.map((item, idx) => { + if (item.type === 'tool') { + return ( + -
+ ) + } + return ( + + {item.content.trim()} + ) - } - return ( - - {item.content.trim()} - - ) - })} -
+ })} +
+
)} ) } + +interface BoundedViewportProps { + children: React.ReactNode + isStreaming: boolean +} + +const BOTTOM_STICK_THRESHOLD_PX = 8 + +function BoundedViewport({ children, isStreaming }: BoundedViewportProps) { + const ref = useRef(null) + const rafRef = useRef(null) + const stickToBottomRef = useRef(true) + const [hasOverflow, setHasOverflow] = useState(false) + + useEffect(() => { + const el = ref.current + if (!el) return + // Any upward user input detaches auto-stick. A subsequent scroll-to-bottom + // (wheel back down or dragging scrollbar) re-attaches it. + const handleWheel = (e: WheelEvent) => { + if (e.deltaY < 0) stickToBottomRef.current = false + } + const handleScroll = () => { + const distance = el.scrollHeight - el.scrollTop - el.clientHeight + if (distance < BOTTOM_STICK_THRESHOLD_PX) stickToBottomRef.current = true + } + el.addEventListener('wheel', handleWheel, { passive: true }) + el.addEventListener('scroll', handleScroll, { passive: true }) + return () => { + el.removeEventListener('wheel', handleWheel) + el.removeEventListener('scroll', handleScroll) + } + }, []) + + useLayoutEffect(() => { + const el = ref.current + if (el) { + const next = el.scrollHeight > el.clientHeight + setHasOverflow((prev) => (prev === next ? prev : next)) + } + if (rafRef.current !== null) { + window.cancelAnimationFrame(rafRef.current) + rafRef.current = null + } + if (!isStreaming) return + const tick = () => { + const node = ref.current + if (!node || !stickToBottomRef.current) { + rafRef.current = null + return + } + const target = node.scrollHeight - node.clientHeight + const gap = target - node.scrollTop + if (gap < 1) { + rafRef.current = null + return + } + node.scrollTop = node.scrollTop + Math.max(1, gap * 0.18) + rafRef.current = window.requestAnimationFrame(tick) + } + rafRef.current = window.requestAnimationFrame(tick) + return () => { + if (rafRef.current !== null) { + window.cancelAnimationFrame(rafRef.current) + rafRef.current = null + } + } + }) + + return ( +
+
+ {children} +
+ {hasOverflow && ( + <> +
+
+ + )} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index 3223a9a54c..5b8ec63a46 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -164,7 +164,7 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { for (let i = 0; i < blocks.length; i++) { const block = blocks[i] - if (block.type === 'subagent_text') { + if (block.type === 'subagent_text' || block.type === 'subagent_thinking') { if (!block.content || !group) continue group.isDelegating = false const lastItem = group.items[group.items.length - 1] @@ -176,24 +176,6 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { continue } - if (block.type === 'subagent_thinking') { - if (!block.content || !group) continue - group.isDelegating = false - const lastItem = group.items[group.items.length - 1] - if (lastItem?.type === 'thinking' && lastItem.endedAt === undefined) { - lastItem.content += block.content - if (block.endedAt !== undefined) lastItem.endedAt = block.endedAt - } else { - group.items.push({ - type: 'thinking', - content: block.content, - startedAt: block.timestamp, - endedAt: block.endedAt, - }) - } - continue - } - if (block.type === 'thinking') { if (!block.content?.trim()) continue if (group) { diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index d77a363277..fc93752468 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -3001,7 +3001,12 @@ export function useChat( ...timing, } } - return { type: block.type, content: block.content, ...timing } + return { + type: block.type, + content: block.content, + ...(block.subagent ? { lane: 'subagent' } : {}), + ...timing, + } }) if (storedBlocks.length > 0) {