mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-07 06:52:28 +00:00
fix(web): align Agent tool dialog with TUI ctrl+o expand (#585)
This commit is contained in:
@@ -28,6 +28,7 @@ const STANDARD_TOOLS: Record<string, string> = {
|
||||
'TodoWrite': 'Update Tasks',
|
||||
'TodoRead': 'Read Tasks',
|
||||
'Task': 'Launch Agent',
|
||||
'Agent': 'Launch Agent',
|
||||
|
||||
// Team management
|
||||
'TeamCreate': 'Create Team',
|
||||
|
||||
@@ -101,6 +101,7 @@ function formatToolArgumentsDetailed(tool: string, args: any): string {
|
||||
return `Command: ${truncate(cmd, MAX_TOOL_ARGS_LENGTH)}`
|
||||
}
|
||||
|
||||
case 'Agent':
|
||||
case 'Task': {
|
||||
const desc = args.description || args.prompt || ''
|
||||
return `Task: ${truncate(desc, MAX_TOOL_ARGS_LENGTH)}`
|
||||
|
||||
@@ -324,6 +324,88 @@ describe('reduceTimeline', () => {
|
||||
expect(toolBlock.invokedAt).toBe(1_700_000_000_500)
|
||||
})
|
||||
|
||||
it('populates block.children for Agent tool (same as Task)', () => {
|
||||
// Agent tool_use message with a sidechain group
|
||||
const agentToolMsg: TracedMessage = {
|
||||
id: 'msg-agent',
|
||||
localId: null,
|
||||
createdAt: 1_700_000_000_000,
|
||||
role: 'agent',
|
||||
content: [{
|
||||
type: 'tool-call',
|
||||
id: 'tc-agent-1',
|
||||
name: 'Agent',
|
||||
input: { prompt: 'explore stuff', subagent_type: 'general-purpose' },
|
||||
description: null,
|
||||
uuid: 'u-agent',
|
||||
parentUUID: null
|
||||
}],
|
||||
isSidechain: false
|
||||
} as TracedMessage
|
||||
|
||||
// Sidechain child message that would be in the group for msg-agent
|
||||
const sidechainChild: TracedMessage = {
|
||||
id: 'sc-msg-1',
|
||||
localId: null,
|
||||
createdAt: 1_700_000_001_000,
|
||||
role: 'agent',
|
||||
content: [{
|
||||
type: 'tool-call',
|
||||
id: 'tc-glob-1',
|
||||
name: 'Glob',
|
||||
input: { pattern: '**/*.ts' },
|
||||
description: null,
|
||||
uuid: 'u-sc-1',
|
||||
parentUUID: null
|
||||
}],
|
||||
isSidechain: true,
|
||||
sidechainId: 'msg-agent'
|
||||
} as TracedMessage
|
||||
|
||||
// Build groups map the way the real pipeline does it
|
||||
const groups = new Map<string, TracedMessage[]>()
|
||||
groups.set('msg-agent', [sidechainChild])
|
||||
|
||||
const ctx = { ...makeContext(), groups }
|
||||
const { blocks } = reduceTimeline([agentToolMsg], ctx)
|
||||
|
||||
const agentBlock = blocks.find(b => b.kind === 'tool-call') as any
|
||||
expect(agentBlock).toBeDefined()
|
||||
// block.children must be populated for Agent (was broken before fix)
|
||||
expect(agentBlock.children.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('suppresses prompt-text duplicate for Agent tool (same as Task)', () => {
|
||||
// When an agent message contains an Agent tool_use, Claude often writes
|
||||
// the prompt as a text block before the tool_use. The reducer must skip
|
||||
// that duplicate text just like it does for Task.
|
||||
const prompt = 'explore the repository structure'
|
||||
const agentMsg: TracedMessage = {
|
||||
id: 'msg-agent-dup',
|
||||
localId: null,
|
||||
createdAt: 1_700_000_000_000,
|
||||
role: 'agent',
|
||||
content: [
|
||||
{ type: 'text', text: prompt, uuid: 'u-text', parentUUID: null },
|
||||
{
|
||||
type: 'tool-call',
|
||||
id: 'tc-agent-2',
|
||||
name: 'Agent',
|
||||
input: { prompt, subagent_type: 'Explore' },
|
||||
description: null,
|
||||
uuid: 'u-agent',
|
||||
parentUUID: null
|
||||
}
|
||||
],
|
||||
isSidechain: false
|
||||
} as TracedMessage
|
||||
|
||||
const { blocks } = reduceTimeline([agentMsg], makeContext())
|
||||
// text block with same content as Agent.input.prompt must be suppressed
|
||||
const textBlocks = blocks.filter(b => b.kind === 'agent-text')
|
||||
expect(textBlocks).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('keeps toolBlocksById reference identity when applying turn-duration to a tool-call', () => {
|
||||
const toolCallMsg: TracedMessage = {
|
||||
id: 'msg-tool',
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { TracedMessage } from '@/chat/tracer'
|
||||
import { createCliOutputBlock, isCliOutputText, mergeCliOutputBlocks } from '@/chat/reducerCliOutput'
|
||||
import { parseMessageAsEvent } from '@/chat/reducerEvents'
|
||||
import { ensureToolBlock, extractTitleFromChangeTitleInput, isChangeTitleToolName, type PermissionEntry } from '@/chat/reducerTools'
|
||||
import { isSubagentToolName } from '@/chat/subagentTool'
|
||||
|
||||
export function reduceTimeline(
|
||||
messages: TracedMessage[],
|
||||
@@ -124,11 +125,11 @@ export function reduceTimeline(
|
||||
}
|
||||
|
||||
if (msg.role === 'agent') {
|
||||
// When the message contains a Task tool_use, Claude often writes the
|
||||
// prompt as a text block before the tool_use block. We only want to
|
||||
// When the message contains a Task/Agent tool_use, Claude often writes
|
||||
// the prompt as a text block before the tool_use block. We only want to
|
||||
// suppress that exact prompt text — not every text block in the message.
|
||||
const taskToolCall = msg.content.find(
|
||||
(c) => c.type === 'tool-call' && c.name === 'Task'
|
||||
(c) => c.type === 'tool-call' && isSubagentToolName(c.name)
|
||||
)
|
||||
const taskPromptText: string | null = (() => {
|
||||
if (!taskToolCall || taskToolCall.type !== 'tool-call') return null
|
||||
@@ -259,7 +260,7 @@ export function reduceTimeline(
|
||||
block.tool.startedAt = msg.createdAt
|
||||
}
|
||||
|
||||
if (c.name === 'Task' && !context.consumedGroupIds.has(msg.id)) {
|
||||
if (isSubagentToolName(c.name) && !context.consumedGroupIds.has(msg.id)) {
|
||||
const sidechain = context.groups.get(msg.id) ?? null
|
||||
if (sidechain && sidechain.length > 0) {
|
||||
context.consumedGroupIds.add(msg.id)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Returns true when the tool name identifies a subagent invocation.
|
||||
*
|
||||
* The Claude Code SDK has used two names for the same concept:
|
||||
* - 'Task' — earlier SDK releases
|
||||
* - 'Agent' — later SDK releases (OPUS 4.7+ environment)
|
||||
*
|
||||
* Both share the same input shape: { prompt: string, subagent_type: string }.
|
||||
* The tracer, reducer, and UI surfaces must treat them identically.
|
||||
* Keeping both ensures sessions recorded under either name continue to work.
|
||||
*/
|
||||
export function isSubagentToolName(name: string): boolean {
|
||||
return name === 'Task' || name === 'Agent'
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Tests for traceMessages — verifies that both Task and Agent tool names
|
||||
* are indexed and matched when grouping sidechain messages.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { NormalizedMessage } from '@/chat/types'
|
||||
import { traceMessages } from '@/chat/tracer'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function makeAgentMsg(overrides: Partial<NormalizedMessage> & { id: string }): NormalizedMessage {
|
||||
const { id, ...rest } = overrides
|
||||
return {
|
||||
id,
|
||||
localId: null,
|
||||
createdAt: 1_700_000_000_000,
|
||||
role: 'agent',
|
||||
isSidechain: false,
|
||||
content: [],
|
||||
...rest,
|
||||
} as NormalizedMessage
|
||||
}
|
||||
|
||||
function makeToolCallMsg(
|
||||
id: string,
|
||||
toolName: 'Task' | 'Agent',
|
||||
prompt: string,
|
||||
): NormalizedMessage {
|
||||
return makeAgentMsg({
|
||||
id,
|
||||
content: [
|
||||
{
|
||||
type: 'tool-call',
|
||||
id: `tc-${id}`,
|
||||
name: toolName,
|
||||
input: { prompt, subagent_type: 'general-purpose' },
|
||||
description: null,
|
||||
uuid: `uuid-${id}`,
|
||||
parentUUID: null,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
function makeSidechainRootMsg(id: string, prompt: string): NormalizedMessage {
|
||||
return {
|
||||
id,
|
||||
localId: null,
|
||||
createdAt: 1_700_000_001_000,
|
||||
role: 'agent',
|
||||
isSidechain: true,
|
||||
content: [
|
||||
{
|
||||
type: 'sidechain',
|
||||
uuid: `uuid-sc-${id}`,
|
||||
prompt,
|
||||
},
|
||||
],
|
||||
} as NormalizedMessage
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task — existing behaviour preserved
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('traceMessages — Task tool name (preserved)', () => {
|
||||
it('matches sidechain root to a Task tool_use message', () => {
|
||||
const prompt = 'list .ts files'
|
||||
const taskMsg = makeToolCallMsg('msg-task', 'Task', prompt)
|
||||
const sidechainRoot = makeSidechainRootMsg('sc-root', prompt)
|
||||
|
||||
const result = traceMessages([taskMsg, sidechainRoot])
|
||||
const sc = result.find(m => m.id === 'sc-root')
|
||||
expect(sc).toBeDefined()
|
||||
expect(sc!.sidechainId).toBe('msg-task')
|
||||
})
|
||||
|
||||
it('does not assign sidechainId when prompt does not match', () => {
|
||||
const taskMsg = makeToolCallMsg('msg-task', 'Task', 'original prompt')
|
||||
const sidechainRoot = makeSidechainRootMsg('sc-root', 'different prompt')
|
||||
|
||||
const result = traceMessages([taskMsg, sidechainRoot])
|
||||
const sc = result.find(m => m.id === 'sc-root')
|
||||
expect(sc).toBeDefined()
|
||||
expect(sc!.sidechainId).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agent — new SDK tool name (regression fix)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('traceMessages — Agent tool name (regression fix)', () => {
|
||||
it('indexes Agent prompt and matches sidechain root to the Agent message', () => {
|
||||
const prompt = 'explore the repo structure'
|
||||
const agentMsg = makeToolCallMsg('msg-agent', 'Agent', prompt)
|
||||
const sidechainRoot = makeSidechainRootMsg('sc-root', prompt)
|
||||
|
||||
const result = traceMessages([agentMsg, sidechainRoot])
|
||||
const sc = result.find(m => m.id === 'sc-root')
|
||||
expect(sc).toBeDefined()
|
||||
// Before fix: sidechainId would be undefined because 'Agent' was not indexed
|
||||
expect(sc!.sidechainId).toBe('msg-agent')
|
||||
})
|
||||
|
||||
it('does not assign sidechainId when Agent prompt does not match', () => {
|
||||
const agentMsg = makeToolCallMsg('msg-agent', 'Agent', 'original prompt')
|
||||
const sidechainRoot = makeSidechainRootMsg('sc-root', 'different prompt')
|
||||
|
||||
const result = traceMessages([agentMsg, sidechainRoot])
|
||||
const sc = result.find(m => m.id === 'sc-root')
|
||||
expect(sc!.sidechainId).toBeUndefined()
|
||||
})
|
||||
|
||||
it('handles both Task and Agent in the same message list', () => {
|
||||
const taskPrompt = 'task prompt'
|
||||
const agentPrompt = 'agent prompt'
|
||||
const taskMsg = makeToolCallMsg('msg-task', 'Task', taskPrompt)
|
||||
const agentMsg = makeToolCallMsg('msg-agent', 'Agent', agentPrompt)
|
||||
const scForTask = makeSidechainRootMsg('sc-task', taskPrompt)
|
||||
const scForAgent = makeSidechainRootMsg('sc-agent', agentPrompt)
|
||||
|
||||
const result = traceMessages([taskMsg, agentMsg, scForTask, scForAgent])
|
||||
const scTaskResult = result.find(m => m.id === 'sc-task')
|
||||
const scAgentResult = result.find(m => m.id === 'sc-agent')
|
||||
expect(scTaskResult!.sidechainId).toBe('msg-task')
|
||||
expect(scAgentResult!.sidechainId).toBe('msg-agent')
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { NormalizedMessage } from '@/chat/types'
|
||||
import { isObject } from '@hapi/protocol'
|
||||
import { isSubagentToolName } from '@/chat/subagentTool'
|
||||
|
||||
export type TracedMessage = NormalizedMessage & {
|
||||
sidechainId?: string
|
||||
@@ -58,11 +59,11 @@ export function traceMessages(messages: NormalizedMessage[]): TracedMessage[] {
|
||||
|
||||
const results: TracedMessage[] = []
|
||||
|
||||
// Index Task prompts (including those inside sidechains).
|
||||
// Index Task/Agent prompts (including those inside sidechains).
|
||||
for (const message of messages) {
|
||||
if (message.role !== 'agent') continue
|
||||
for (const content of message.content) {
|
||||
if (content.type !== 'tool-call' || content.name !== 'Task') continue
|
||||
if (content.type !== 'tool-call' || !isSubagentToolName(content.name)) continue
|
||||
const input = content.input
|
||||
if (!isObject(input) || typeof input.prompt !== 'string') continue
|
||||
state.promptToTaskId.set(input.prompt, message.id)
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ToolCallMessagePartProps } from '@assistant-ui/react'
|
||||
import type { ChatBlock } from '@/chat/types'
|
||||
import type { ToolCallBlock } from '@/chat/types'
|
||||
import { isObject, safeStringify } from '@hapi/protocol'
|
||||
import { isSubagentToolName } from '@/chat/subagentTool'
|
||||
import { getEventPresentation } from '@/chat/presentation'
|
||||
import { CodeBlock } from '@/components/CodeBlock'
|
||||
import { MarkdownRenderer } from '@/components/MarkdownRenderer'
|
||||
@@ -109,7 +110,7 @@ function HappyNestedBlockList(props: {
|
||||
}
|
||||
|
||||
if (block.kind === 'tool-call') {
|
||||
const isTask = block.tool.name === 'Task'
|
||||
const isTask = isSubagentToolName(block.tool.name)
|
||||
const taskChildren = isTask ? splitTaskChildren(block) : null
|
||||
|
||||
return (
|
||||
@@ -199,7 +200,7 @@ export function HappyToolMessage(props: ToolCallMessagePartProps) {
|
||||
}
|
||||
|
||||
const block = artifact
|
||||
const isTask = block.tool.name === 'Task'
|
||||
const isTask = isSubagentToolName(block.tool.name)
|
||||
const taskChildren = isTask ? splitTaskChildren(block) : null
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,6 +22,7 @@ import { getInputString, getInputStringAny, truncate } from '@/lib/toolInputUtil
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useTranslation } from '@/lib/use-translation'
|
||||
import { TraceSection } from '@/components/ToolCard/trace'
|
||||
import { isSubagentToolName } from '@/chat/subagentTool'
|
||||
|
||||
const ELAPSED_INTERVAL_MS = 1000
|
||||
|
||||
@@ -47,7 +48,7 @@ function ElapsedView(props: { from: number; active: boolean }) {
|
||||
}
|
||||
|
||||
function getTaskSummaryChildren(block: ToolCallBlock): { visible: ToolCallBlock[]; remaining: number } | null {
|
||||
if (block.tool.name !== 'Task') return null
|
||||
if (!isSubagentToolName(block.tool.name)) return null
|
||||
|
||||
const children = block.children
|
||||
.filter((child): child is ToolCallBlock => child.kind === 'tool-call')
|
||||
@@ -126,7 +127,7 @@ function renderToolInput(block: ToolCallBlock, surface: 'inline' | 'dialog' = 'i
|
||||
const toolName = block.tool.name
|
||||
const input = block.tool.input
|
||||
|
||||
if (toolName === 'Task' && isObject(input) && typeof input.prompt === 'string') {
|
||||
if (isSubagentToolName(toolName) && isObject(input) && typeof input.prompt === 'string') {
|
||||
return <MarkdownRenderer content={input.prompt} />
|
||||
}
|
||||
|
||||
@@ -287,7 +288,7 @@ function ToolCardInner(props: ToolCardProps) {
|
||||
const subtitle = presentation.subtitle ?? props.block.tool.description
|
||||
const taskSummary = renderTaskSummary(props.block, props.metadata, t)
|
||||
const runningFrom = props.block.tool.startedAt ?? props.block.tool.createdAt
|
||||
const showInline = !presentation.minimal && toolName !== 'Task'
|
||||
const showInline = !presentation.minimal && !isSubagentToolName(toolName)
|
||||
const CompactToolView = showInline ? getToolViewComponent(toolName) : null
|
||||
const FullToolView = getToolFullViewComponent(toolName)
|
||||
const ResultToolView = getToolResultViewComponent(toolName)
|
||||
|
||||
@@ -294,13 +294,16 @@ export const knownTools: Record<string, {
|
||||
icon: () => <RocketIcon className={DEFAULT_ICON_CLASS} />,
|
||||
title: (opts) => {
|
||||
const description = getInputStringAny(opts.input, ['description'])
|
||||
return description ?? 'Agent'
|
||||
return description ?? 'Launch Agent'
|
||||
},
|
||||
subtitle: (opts) => {
|
||||
// Subagent invocation: show prompt preview (same as Task)
|
||||
const prompt = getInputStringAny(opts.input, ['prompt'])
|
||||
if (prompt) return truncate(prompt, 120)
|
||||
const model = getInputStringAny(opts.input, ['subagent_type'])
|
||||
return model ?? null
|
||||
},
|
||||
minimal: true
|
||||
minimal: (opts) => opts.childrenCount === 0
|
||||
},
|
||||
CodexReasoning: {
|
||||
icon: () => <BulbIcon className={DEFAULT_ICON_CLASS} />,
|
||||
|
||||
@@ -95,6 +95,31 @@ function makeTaskBlock(
|
||||
}
|
||||
}
|
||||
|
||||
function makeAgentBlock(
|
||||
children: ToolCallBlock[],
|
||||
state: ToolCallBlock['tool']['state'] = 'completed',
|
||||
result: unknown = null,
|
||||
): ToolCallBlock {
|
||||
return {
|
||||
kind: 'tool-call',
|
||||
id: 'agent-1',
|
||||
localId: null,
|
||||
createdAt: 1000,
|
||||
tool: {
|
||||
id: 'agent-1',
|
||||
name: 'Agent',
|
||||
state,
|
||||
input: { prompt: 'do stuff', subagent_type: 'general-purpose' },
|
||||
createdAt: 1000,
|
||||
startedAt: 1000,
|
||||
completedAt: 2000,
|
||||
description: null,
|
||||
result,
|
||||
},
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getTaskTraceChildren
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -139,6 +164,22 @@ describe('getTaskTraceChildren', () => {
|
||||
}
|
||||
expect(getTaskTraceChildren(block)).toBeNull()
|
||||
})
|
||||
|
||||
// Agent tool name (new SDK name for subagent invocations)
|
||||
it('returns children for Agent blocks (same as Task)', () => {
|
||||
const block = makeAgentBlock([
|
||||
makeChild('c1', 'Glob'),
|
||||
makeChild('c2', 'Grep'),
|
||||
])
|
||||
const result = getTaskTraceChildren(block)
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.length).toBe(2)
|
||||
})
|
||||
|
||||
it('returns null for Agent block with no tool-call children', () => {
|
||||
const block = makeAgentBlock([])
|
||||
expect(getTaskTraceChildren(block)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -267,4 +308,29 @@ describe('TraceSection', () => {
|
||||
// Result section label must also be present
|
||||
expect(screen.getByText('Result')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Agent tool name — same Trace UX as Task
|
||||
it('renders Trace header for Agent blocks', () => {
|
||||
const block = makeAgentBlock([makeChild('c1', 'Glob'), makeChild('c2', 'Grep')])
|
||||
const { container } = render(<TraceSection block={block} metadata={null} />)
|
||||
// TraceSection must mount (non-null) for Agent blocks
|
||||
expect(container.firstChild).not.toBeNull()
|
||||
// header button with aria-expanded attribute must exist
|
||||
const btn = container.querySelector('button[aria-expanded]')
|
||||
expect(btn).not.toBeNull()
|
||||
})
|
||||
|
||||
it('renders nothing for Agent block with no children', () => {
|
||||
const block = makeAgentBlock([])
|
||||
const { container } = render(<TraceSection block={block} metadata={null} />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('expands Agent trace by default when running', () => {
|
||||
const block = makeAgentBlock([makeChild('c1', 'Read')], 'running')
|
||||
const { container } = render(<TraceSection block={block} metadata={null} />)
|
||||
const headerBtn = container.querySelector('button[aria-expanded="true"]')
|
||||
expect(headerBtn).not.toBeNull()
|
||||
expect(container.querySelector('.border-l')).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* TraceSection — shows child tool calls inside a Task tool dialog.
|
||||
* TraceSection — shows child tool calls inside a Task/Agent tool dialog.
|
||||
* Placed between Input and Result sections.
|
||||
*/
|
||||
import { useState } from 'react'
|
||||
@@ -11,6 +11,7 @@ import { getToolResultViewComponent } from '@/components/ToolCard/views/_results
|
||||
import { formatTaskChildLabel, TaskStateIcon } from '@/components/ToolCard/helpers'
|
||||
import { CodeBlock } from '@/components/CodeBlock'
|
||||
import { useTranslation } from '@/lib/use-translation'
|
||||
import { isSubagentToolName } from '@/chat/subagentTool'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Result type narrowing (trace.tsx-internal; do NOT move to shared protocol)
|
||||
@@ -46,10 +47,10 @@ type _TaskToolResultSummary = TaskToolResultSummary
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns tool-call children of the given Task block, or null if none exist.
|
||||
* Returns tool-call children of the given Task/Agent block, or null if none exist.
|
||||
*/
|
||||
export function getTaskTraceChildren(block: ToolCallBlock): ToolCallBlock[] | null {
|
||||
if (block.tool.name !== 'Task') return null
|
||||
if (!isSubagentToolName(block.tool.name)) return null
|
||||
const children = block.children.filter(
|
||||
(c): c is ToolCallBlock => c.kind === 'tool-call',
|
||||
)
|
||||
|
||||
@@ -11,7 +11,6 @@ import { MultiEditFullView, MultiEditView } from '@/components/ToolCard/views/Mu
|
||||
import { TodoWriteView } from '@/components/ToolCard/views/TodoWriteView'
|
||||
import { UpdatePlanView } from '@/components/ToolCard/views/UpdatePlanView'
|
||||
import { WriteView } from '@/components/ToolCard/views/WriteView'
|
||||
import { isObject } from '@hapi/protocol'
|
||||
import { getInputStringAny } from '@/lib/toolInputUtils'
|
||||
|
||||
export type ToolViewProps = {
|
||||
@@ -31,25 +30,6 @@ const SkillFullView: ToolViewComponent = ({ block }: ToolViewProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
const AgentFullView: ToolViewComponent = ({ block }: ToolViewProps) => {
|
||||
const input = block.tool.input
|
||||
const description = getInputStringAny(input, ['description'])
|
||||
const subagentType = getInputStringAny(input, ['subagent_type'])
|
||||
const runInBackground = isObject(input) && input.run_in_background === true
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 text-sm">
|
||||
{description && (
|
||||
<div className="text-[var(--app-fg)]">{description}</div>
|
||||
)}
|
||||
<div className="flex gap-3 text-[var(--app-hint)]">
|
||||
{subagentType && <span>Type: {subagentType}</span>}
|
||||
{runInBackground && <span>Background</span>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const toolViewRegistry: Record<string, ToolViewComponent> = {
|
||||
Edit: EditView,
|
||||
MultiEdit: MultiEditView,
|
||||
@@ -71,7 +51,6 @@ export const toolFullViewRegistry: Record<string, ToolViewComponent> = {
|
||||
CodexDiff: CodexDiffFullView,
|
||||
CodexPatch: CodexPatchView,
|
||||
Skill: SkillFullView,
|
||||
Agent: AgentFullView,
|
||||
AskUserQuestion: AskUserQuestionView,
|
||||
ExitPlanMode: ExitPlanModeView,
|
||||
ask_user_question: AskUserQuestionView,
|
||||
|
||||
@@ -138,6 +138,12 @@ describe('getToolResultViewComponent registry', () => {
|
||||
it('uses a dedicated result view for CodexBash', () => {
|
||||
expect(getToolResultViewComponent('CodexBash')).not.toBe(getToolResultViewComponent('SomeUnknownTool'))
|
||||
})
|
||||
|
||||
it('Agent falls back to GenericResultView (no dedicated view — view layer must not filter content)', () => {
|
||||
const agentView = getToolResultViewComponent('Agent')
|
||||
const genericView = getToolResultViewComponent('SomeUnknownTool')
|
||||
expect(agentView).toBe(genericView)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dialog result formatting', () => {
|
||||
|
||||
@@ -751,46 +751,6 @@ const TodoWriteResultView: ToolViewComponent = (props: ToolViewProps) => {
|
||||
return <ChecklistList items={todos} />
|
||||
}
|
||||
|
||||
const AgentResultView: ToolViewComponent = (props: ToolViewProps) => {
|
||||
const { state, result } = props.block.tool
|
||||
|
||||
if (result === undefined || result === null) {
|
||||
return <ResultStatusPill text={placeholderForState(state)} />
|
||||
}
|
||||
|
||||
// For errors, show the error text
|
||||
if (state === 'error') {
|
||||
const text = extractTextFromResult(result)
|
||||
return (
|
||||
<div className="text-sm text-red-600">
|
||||
{text?.trim() ? text : 'Agent failed'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const text = extractTextFromResult(result)
|
||||
if (!text) {
|
||||
return <ResultStatusPill text={state === 'completed' ? 'Done' : placeholderForState(state)} />
|
||||
}
|
||||
|
||||
// Detect internal launch metadata. Check structurally first (result object
|
||||
// may carry agentId/output_file keys), then fall back to a strict text
|
||||
// pattern that is unlikely to appear in normal agent prose.
|
||||
const isInternalMeta = isObject(result) && ('agentId' in result || 'output_file' in result)
|
||||
|| (text.startsWith('Async agent launched successfully.') && text.includes('agentId:'))
|
||||
|
||||
if (isInternalMeta) {
|
||||
return <ResultStatusPill text="Agent launched" />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderResultBody(renderMarkdown(text, props.surface), props.surface)}
|
||||
<RawJsonDevOnly value={result} surface={props.surface} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const SkillResultView: ToolViewComponent = (props: ToolViewProps) => {
|
||||
const { state, result, input } = props.block.tool
|
||||
|
||||
@@ -889,7 +849,6 @@ export const toolResultViewRegistry: Record<string, ToolViewComponent> = {
|
||||
CodexPatch: CodexPatchResultView,
|
||||
CodexDiff: CodexDiffResultView,
|
||||
Skill: SkillResultView,
|
||||
Agent: AgentResultView,
|
||||
AskUserQuestion: AskUserQuestionResultView,
|
||||
ExitPlanMode: MarkdownResultView,
|
||||
ask_user_question: AskUserQuestionResultView,
|
||||
|
||||
Reference in New Issue
Block a user