mirror of
https://github.com/simstudioai/sim.git
synced 2026-03-15 03:00:33 -04:00
subagent thinking text
This commit is contained in:
@@ -7,10 +7,14 @@ import type { ToolCallData } from '../../../../types'
|
||||
import { getAgentIcon } from '../../utils'
|
||||
import { ToolCallItem } from './tool-call-item'
|
||||
|
||||
export type AgentGroupItem =
|
||||
| { type: 'text'; content: string }
|
||||
| { type: 'tool'; data: ToolCallData }
|
||||
|
||||
interface AgentGroupProps {
|
||||
agentName: string
|
||||
agentLabel: string
|
||||
tools: ToolCallData[]
|
||||
items: AgentGroupItem[]
|
||||
autoCollapse?: boolean
|
||||
}
|
||||
|
||||
@@ -19,14 +23,20 @@ const FADE_MS = 300
|
||||
export function AgentGroup({
|
||||
agentName,
|
||||
agentLabel,
|
||||
tools,
|
||||
items,
|
||||
autoCollapse = false,
|
||||
}: AgentGroupProps) {
|
||||
const AgentIcon = getAgentIcon(agentName)
|
||||
const hasTools = tools.length > 0
|
||||
const hasItems = items.length > 0
|
||||
const toolItems = items.filter(
|
||||
(item): item is Extract<AgentGroupItem, { type: 'tool' }> => item.type === 'tool'
|
||||
)
|
||||
const allDone =
|
||||
hasTools &&
|
||||
tools.every((t) => t.status === 'success' || t.status === 'error' || t.status === 'cancelled')
|
||||
toolItems.length > 0 &&
|
||||
toolItems.every(
|
||||
(t) =>
|
||||
t.data.status === 'success' || t.data.status === 'error' || t.data.status === 'cancelled'
|
||||
)
|
||||
|
||||
const [expanded, setExpanded] = useState(!allDone)
|
||||
const [mounted, setMounted] = useState(!allDone)
|
||||
@@ -49,39 +59,55 @@ export function AgentGroup({
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={hasTools ? () => setExpanded((prev) => !prev) : undefined}
|
||||
className={cn('flex items-center gap-[8px]', hasTools && 'cursor-pointer')}
|
||||
>
|
||||
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>
|
||||
<AgentIcon className='h-[16px] w-[16px] text-[var(--text-icon)]' />
|
||||
</div>
|
||||
<span className='font-base text-[14px] text-[var(--text-body)]'>{agentLabel}</span>
|
||||
{hasTools && (
|
||||
{hasItems ? (
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setExpanded((prev) => !prev)}
|
||||
className='flex cursor-pointer items-center gap-[8px]'
|
||||
>
|
||||
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>
|
||||
<AgentIcon className='h-[16px] w-[16px] text-[var(--text-icon)]' />
|
||||
</div>
|
||||
<span className='font-base text-[14px] text-[var(--text-body)]'>{agentLabel}</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-[7px] w-[9px] text-[var(--text-icon)] transition-transform duration-150',
|
||||
!expanded && '-rotate-90'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
{hasTools && mounted && (
|
||||
</button>
|
||||
) : (
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>
|
||||
<AgentIcon className='h-[16px] w-[16px] text-[var(--text-icon)]' />
|
||||
</div>
|
||||
<span className='font-base text-[14px] text-[var(--text-body)]'>{agentLabel}</span>
|
||||
</div>
|
||||
)}
|
||||
{hasItems && mounted && (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-1.5 transition-opacity duration-300 ease-out',
|
||||
'flex flex-col gap-3 transition-opacity duration-300 ease-out',
|
||||
expanded ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
>
|
||||
{tools.map((tool) => (
|
||||
<ToolCallItem
|
||||
key={tool.id}
|
||||
toolName={tool.toolName}
|
||||
displayTitle={tool.displayTitle}
|
||||
status={tool.status}
|
||||
/>
|
||||
))}
|
||||
{items.map((item, idx) =>
|
||||
item.type === 'tool' ? (
|
||||
<ToolCallItem
|
||||
key={item.data.id}
|
||||
toolName={item.data.toolName}
|
||||
displayTitle={item.data.displayTitle}
|
||||
status={item.data.status}
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
key={`text-${idx}`}
|
||||
className='whitespace-pre-wrap pl-[24px] font-base text-[13px] text-[var(--text-secondary)]'
|
||||
>
|
||||
{item.content.trim()}
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export type { AgentGroupItem } from './agent-group'
|
||||
export { AgentGroup } from './agent-group'
|
||||
export { CircleStop } from './tool-call-item'
|
||||
|
||||
@@ -53,16 +53,16 @@ export function ToolCallItem({ toolName, displayTitle, status }: ToolCallItemPro
|
||||
<div className='flex items-center gap-[8px] pl-[24px]'>
|
||||
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>
|
||||
{status === 'executing' ? (
|
||||
<Loader className='h-[16px] w-[16px] text-[var(--text-icon)]' animate />
|
||||
<Loader className='h-[15px] w-[15px] text-[var(--text-tertiary)]' animate />
|
||||
) : status === 'cancelled' ? (
|
||||
<CircleStop className='h-[16px] w-[16px] text-[var(--text-icon)]' />
|
||||
<CircleStop className='h-[15px] w-[15px] text-[var(--text-tertiary)]' />
|
||||
) : Icon ? (
|
||||
<Icon className='h-[16px] w-[16px] text-[var(--text-icon)]' />
|
||||
<Icon className='h-[15px] w-[15px] text-[var(--text-tertiary)]' />
|
||||
) : (
|
||||
<CircleCheck className='h-[16px] w-[16px] text-[var(--text-icon)]' />
|
||||
<CircleCheck className='h-[15px] w-[15px] text-[var(--text-tertiary)]' />
|
||||
)}
|
||||
</div>
|
||||
<span className='font-base text-[14px] text-[var(--text-body)]'>{displayTitle}</span>
|
||||
<span className='font-base text-[13px] text-[var(--text-secondary)]'>{displayTitle}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export type { AgentGroupItem } from './agent-group'
|
||||
export { AgentGroup, CircleStop } from './agent-group'
|
||||
export { ChatContent } from './chat-content'
|
||||
export { Options } from './options'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import type { ContentBlock, OptionItem, SubagentName, ToolCallData } from '../../types'
|
||||
import { SUBAGENT_LABELS } from '../../types'
|
||||
import type { AgentGroupItem } from './components'
|
||||
import { AgentGroup, ChatContent, CircleStop, Options } from './components'
|
||||
|
||||
interface TextSegment {
|
||||
@@ -14,7 +15,7 @@ interface AgentGroupSegment {
|
||||
id: string
|
||||
agentName: string
|
||||
agentLabel: string
|
||||
tools: ToolCallData[]
|
||||
items: AgentGroupItem[]
|
||||
}
|
||||
|
||||
interface OptionsSegment {
|
||||
@@ -64,7 +65,18 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] {
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
const block = blocks[i]
|
||||
|
||||
if (block.type === 'text' || block.type === 'subagent_text') {
|
||||
if (block.type === 'subagent_text') {
|
||||
if (!block.content || !group) continue
|
||||
const lastItem = group.items[group.items.length - 1]
|
||||
if (lastItem?.type === 'text') {
|
||||
lastItem.content += block.content
|
||||
} else {
|
||||
group.items.push({ type: 'text', content: block.content })
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (block.type === 'text') {
|
||||
if (!block.content?.trim()) continue
|
||||
if (group) {
|
||||
segments.push(group)
|
||||
@@ -92,7 +104,7 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] {
|
||||
id: `agent-${key}-${i}`,
|
||||
agentName: key,
|
||||
agentLabel: resolveAgentLabel(key),
|
||||
tools: [],
|
||||
items: [],
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -113,7 +125,7 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] {
|
||||
id: `agent-${tc.name}-${i}`,
|
||||
agentName: tc.name,
|
||||
agentLabel: resolveAgentLabel(tc.name),
|
||||
tools: [],
|
||||
items: [],
|
||||
}
|
||||
}
|
||||
continue
|
||||
@@ -122,7 +134,7 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] {
|
||||
const tool = toToolData(tc)
|
||||
|
||||
if (tc.calledBy && group && group.agentName === tc.calledBy) {
|
||||
group.tools.push(tool)
|
||||
group.items.push({ type: 'tool', data: tool })
|
||||
} else if (tc.calledBy) {
|
||||
if (group) {
|
||||
segments.push(group)
|
||||
@@ -133,11 +145,11 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] {
|
||||
id: `agent-${tc.calledBy}-${i}`,
|
||||
agentName: tc.calledBy,
|
||||
agentLabel: resolveAgentLabel(tc.calledBy),
|
||||
tools: [tool],
|
||||
items: [{ type: 'tool', data: tool }],
|
||||
}
|
||||
} else {
|
||||
if (group && group.agentName === 'mothership') {
|
||||
group.tools.push(tool)
|
||||
group.items.push({ type: 'tool', data: tool })
|
||||
} else {
|
||||
if (group) {
|
||||
segments.push(group)
|
||||
@@ -148,7 +160,7 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] {
|
||||
id: `agent-mothership-${i}`,
|
||||
agentName: 'mothership',
|
||||
agentLabel: 'Mothership',
|
||||
tools: [tool],
|
||||
items: [{ type: 'tool', data: tool }],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,10 +228,15 @@ export function MessageContent({
|
||||
/>
|
||||
)
|
||||
case 'agent_group': {
|
||||
const toolItems = segment.items.filter((item) => item.type === 'tool')
|
||||
const allToolsDone =
|
||||
segment.tools.length > 0 &&
|
||||
segment.tools.every(
|
||||
(t) => t.status === 'success' || t.status === 'error' || t.status === 'cancelled'
|
||||
toolItems.length === 0 ||
|
||||
toolItems.every(
|
||||
(t) =>
|
||||
t.type === 'tool' &&
|
||||
(t.data.status === 'success' ||
|
||||
t.data.status === 'error' ||
|
||||
t.data.status === 'cancelled')
|
||||
)
|
||||
const hasFollowingText = segments.slice(i + 1).some((s) => s.type === 'text')
|
||||
return (
|
||||
@@ -227,7 +244,7 @@ export function MessageContent({
|
||||
<AgentGroup
|
||||
agentName={segment.agentName}
|
||||
agentLabel={segment.agentLabel}
|
||||
tools={segment.tools}
|
||||
items={segment.items}
|
||||
autoCollapse={allToolsDone && hasFollowingText}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
Bug,
|
||||
Calendar,
|
||||
ClipboardList,
|
||||
Connections,
|
||||
Database,
|
||||
File,
|
||||
FolderCode,
|
||||
Hammer,
|
||||
Integration,
|
||||
Layout,
|
||||
Library,
|
||||
Pencil,
|
||||
PlayOutline,
|
||||
@@ -42,7 +42,7 @@ const TOOL_ICONS: Record<MothershipToolName | SubagentName | 'mothership', IconC
|
||||
superagent: Blimp,
|
||||
user_table: TableIcon,
|
||||
workspace_file: File,
|
||||
create_workflow: Connections,
|
||||
create_workflow: Layout,
|
||||
edit_workflow: Pencil,
|
||||
build: Hammer,
|
||||
run: PlayOutline,
|
||||
@@ -58,6 +58,7 @@ const TOOL_ICONS: Record<MothershipToolName | SubagentName | 'mothership', IconC
|
||||
plan: ClipboardList,
|
||||
debug: Bug,
|
||||
edit: Pencil,
|
||||
fast_edit: Pencil,
|
||||
}
|
||||
|
||||
export function getAgentIcon(name: string): IconComponent {
|
||||
|
||||
@@ -383,6 +383,14 @@ export function useChat(
|
||||
return b
|
||||
}
|
||||
|
||||
const ensureSubagentTextBlock = (): ContentBlock => {
|
||||
const last = blocks[blocks.length - 1]
|
||||
if (last?.type === 'subagent_text') return last
|
||||
const b: ContentBlock = { type: 'subagent_text', content: '' }
|
||||
blocks.push(b)
|
||||
return b
|
||||
}
|
||||
|
||||
const flush = () => {
|
||||
streamingBlocksRef.current = [...blocks]
|
||||
setMessages((prev) =>
|
||||
@@ -450,10 +458,9 @@ export function useChat(
|
||||
lastContentSource !== contentSource &&
|
||||
runningText.length > 0 &&
|
||||
!runningText.endsWith('\n')
|
||||
const tb = ensureTextBlock()
|
||||
const normalizedChunk = needsBoundaryNewline ? `\n${chunk}` : chunk
|
||||
tb.content = (tb.content ?? '') + normalizedChunk
|
||||
runningText += normalizedChunk
|
||||
const tb = activeSubagent ? ensureSubagentTextBlock() : ensureTextBlock()
|
||||
tb.content = (tb.content ?? '') + chunk
|
||||
runningText += needsBoundaryNewline ? `\n${chunk}` : chunk
|
||||
lastContentSource = contentSource
|
||||
streamingContentRef.current = runningText
|
||||
flush()
|
||||
|
||||
@@ -72,6 +72,7 @@ export type MothershipToolName =
|
||||
| 'plan'
|
||||
| 'debug'
|
||||
| 'edit'
|
||||
| 'fast_edit'
|
||||
|
||||
/**
|
||||
* Subagent identifiers dispatched via `subagent_start` SSE events.
|
||||
@@ -93,6 +94,7 @@ export type SubagentName =
|
||||
| 'plan'
|
||||
| 'debug'
|
||||
| 'edit'
|
||||
| 'fast_edit'
|
||||
|
||||
export type ToolPhase =
|
||||
| 'workspace'
|
||||
@@ -179,6 +181,7 @@ export const SUBAGENT_LABELS: Record<SubagentName, string> = {
|
||||
plan: 'Plan agent',
|
||||
debug: 'Debug agent',
|
||||
edit: 'Edit agent',
|
||||
fast_edit: 'Edit agent',
|
||||
} as const
|
||||
|
||||
export interface ToolUIMetadata {
|
||||
@@ -223,6 +226,7 @@ export const TOOL_UI_METADATA: Partial<Record<MothershipToolName, ToolUIMetadata
|
||||
plan: { title: 'Planning', phaseLabel: 'Plan', phase: 'subagent' },
|
||||
debug: { title: 'Debugging', phaseLabel: 'Debug', phase: 'subagent' },
|
||||
edit: { title: 'Editing workflow', phaseLabel: 'Edit', phase: 'subagent' },
|
||||
fast_edit: { title: 'Editing workflow', phaseLabel: 'Edit', phase: 'subagent' },
|
||||
}
|
||||
|
||||
export interface SSEPayloadUI {
|
||||
|
||||
Reference in New Issue
Block a user