subagent thinking text

This commit is contained in:
Emir Karabeg
2026-03-13 20:04:35 -07:00
parent 9953fda800
commit 7cc013e523
8 changed files with 107 additions and 50 deletions

View File

@@ -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>

View File

@@ -1,2 +1,3 @@
export type { AgentGroupItem } from './agent-group'
export { AgentGroup } from './agent-group'
export { CircleStop } from './tool-call-item'

View File

@@ -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>
)
}

View File

@@ -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'

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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()

View File

@@ -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 {