diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index f47bfb61..e4ae602b 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -1151,21 +1151,31 @@ describe('MessageList nested tool calls', () => { id: 'result-agent', type: 'tool_result', toolUseId: 'agent-1', - content: { - results: [ - { - file: 'git:v0.2.6..v0.2.7', - line: 0, - snippet: 'v0.2.7 tag = a4c92ec7', - context: '版本范围判断:release-notes/v0.2.7.md 明确相比 v0.2.6。', - }, - { - file: '/tmp/example/src/lib.rs', - line: 220, - context: '中风险:服务默认监听 0.0.0.0。', - }, - ], - }, + content: [ + { + type: 'text', + text: JSON.stringify({ + results: [ + { + file: 'git:v0.2.6..v0.2.7', + line: 0, + snippet: 'v0.2.7 tag = a4c92ec7', + context: '版本范围判断:release-notes/v0.2.7.md 明确相比 v0.2.6。', + }, + { + risk: 'medium', + items: [ + { + file: '/tmp/example/src/lib.rs', + line: 220, + context: '中风险:服务默认监听 0.0.0.0。', + }, + ], + }, + ], + }), + }, + ], isError: false, timestamp: 2, }, diff --git a/desktop/src/components/chat/ToolCallGroup.tsx b/desktop/src/components/chat/ToolCallGroup.tsx index 9df1b9a8..d35fd613 100644 --- a/desktop/src/components/chat/ToolCallGroup.tsx +++ b/desktop/src/components/chat/ToolCallGroup.tsx @@ -896,17 +896,32 @@ function formatAgentStructuredResult(content: unknown): string { function parseStructuredAgentContent(content: unknown): Record | unknown[] | null { if (typeof content === 'string') { - const trimmed = content.trim() - if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return null - try { - const parsed = JSON.parse(trimmed) as unknown - return typeof parsed === 'object' && parsed !== null ? parsed as Record | unknown[] : null - } catch { - return null - } + return parseStructuredAgentText(content) } - return typeof content === 'object' && content !== null ? content as Record | unknown[] : null + if (Array.isArray(content)) { + return parseStructuredAgentText(extractTextContent(content)) + } + + if (content && typeof content === 'object') { + if ('results' in content) return content as Record + + const extracted = extractTextContent(content) + return extracted ? parseStructuredAgentText(extracted) : null + } + + return null +} + +function parseStructuredAgentText(text: string): Record | unknown[] | null { + const trimmed = text.trim() + if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return null + try { + const parsed = JSON.parse(trimmed) as unknown + return typeof parsed === 'object' && parsed !== null ? parsed as Record | unknown[] : null + } catch { + return null + } } function formatAgentStructuredResultItem(result: unknown, index: number): string { @@ -920,6 +935,29 @@ function formatAgentStructuredResultItem(result: unknown, index: number): string const context = getStringField(record, 'context') const snippet = getStringField(record, 'snippet') const message = getStringField(record, 'message') || getStringField(record, 'text') || getStringField(record, 'summary') + const nestedItems = Array.isArray(record.items) ? record.items : [] + + if (nestedItems.length > 0) { + const label = getStringField(record, 'risk') || getStringField(record, 'title') || message || 'Grouped results' + const lines = [`${index + 1}. ${formatAgentGroupLabel(label)}`] + if (context) lines.push(` - ${context}`) + if (snippet) lines.push(` - ${snippet}`) + + nestedItems + .map(formatAgentStructuredNestedItem) + .filter(Boolean) + .forEach((item) => { + lines.push( + item + .split('\n') + .map((line, lineIndex) => `${lineIndex === 0 ? ' - ' : ' '}${line}`) + .join('\n'), + ) + }) + + return lines.join('\n') + } + const lines = [`${index + 1}. ${location ? formatInlineCode(location) : 'Result'}`] if (message) lines.push(` - ${message}`) @@ -929,6 +967,31 @@ function formatAgentStructuredResultItem(result: unknown, index: number): string return lines.join('\n') } +function formatAgentStructuredNestedItem(item: unknown): string { + if (!item || typeof item !== 'object' || Array.isArray(item)) { + return extractTextContent(item).trim() + } + + const record = item as Record + const location = formatAgentResultLocation(record) + const context = getStringField(record, 'context') + const snippet = getStringField(record, 'snippet') + const message = getStringField(record, 'message') || getStringField(record, 'text') || getStringField(record, 'summary') + const headingParts = [location ? formatInlineCode(location) : '', message].filter(Boolean) + const lines = [headingParts.join(' - ') || 'Result'] + + if (context) lines.push(context) + if (snippet) lines.push(snippet) + + return lines.join('\n') +} + +function formatAgentGroupLabel(label: string): string { + const normalized = label.trim() + if (!normalized) return 'Grouped results' + return normalized.length > 1 ? `${normalized[0].toUpperCase()}${normalized.slice(1)}` : normalized.toUpperCase() +} + function formatAgentResultLocation(record: Record): string { const file = getStringField(record, 'file') if (!file) return ''