Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -113,52 +111,117 @@ export function AgentGroup({
{hasItems && (
<Expandable expanded={expanded}>
<ExpandableContent>
<div className='flex flex-col gap-1.5 pt-0.5'>
{items.map((item, idx) => {
if (item.type === 'tool') {
return (
<ToolCallItem
key={item.data.id}
toolName={item.data.toolName}
displayTitle={item.data.displayTitle}
status={item.data.status}
streamingArgs={item.data.streamingArgs}
/>
)
}
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 (
<div key={`thinking-${idx}`} className='pl-6'>
<ThinkingBlock
content={item.content}
isActive={
isStreaming && idx === items.length - 1 && item.endedAt === undefined
}
isStreaming={isStreaming}
startedAt={item.startedAt}
endedAt={item.endedAt}
<BoundedViewport isStreaming={isStreaming}>
<div className='flex flex-col gap-1.5 py-0.5'>
{items.map((item, idx) => {
if (item.type === 'tool') {
return (
<ToolCallItem
key={item.data.id}
toolName={item.data.toolName}
displayTitle={item.data.displayTitle}
status={item.data.status}
streamingArgs={item.data.streamingArgs}
/>
</div>
)
}
return (
<span
key={`text-${idx}`}
className='pl-6 font-base text-[13px] text-[var(--text-secondary)] leading-[18px] opacity-60'
>
{item.content.trim()}
</span>
)
}
return (
<span
key={`text-${idx}`}
className='pl-6 font-base text-[var(--text-secondary)] text-small'
>
{item.content.trim()}
</span>
)
})}
</div>
})}
</div>
</BoundedViewport>
</ExpandableContent>
</Expandable>
)}
</div>
)
}

interface BoundedViewportProps {
children: React.ReactNode
isStreaming: boolean
}

const BOTTOM_STICK_THRESHOLD_PX = 8

function BoundedViewport({ children, isStreaming }: BoundedViewportProps) {
const ref = useRef<HTMLDivElement>(null)
const rafRef = useRef<number | null>(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
}
Comment thread
TheodoreSpeaks marked this conversation as resolved.
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 (
<div className='relative'>
<div ref={ref} className={cn('max-h-[110px] overflow-y-auto pr-2', hasOverflow && 'py-1')}>
{children}
</div>
{hasOverflow && (
<>
<div className='pointer-events-none absolute top-0 right-2 left-0 h-3 bg-gradient-to-b from-[var(--bg)] to-transparent' />
<div className='pointer-events-none absolute right-2 bottom-0 left-0 h-3 bg-gradient-to-t from-[var(--bg)] to-transparent' />
</>
)}
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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) {
Expand Down
7 changes: 6 additions & 1 deletion apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading