diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx
index 75ca1308..f47bfb61 100644
--- a/desktop/src/components/chat/MessageList.test.tsx
+++ b/desktop/src/components/chat/MessageList.test.tsx
@@ -1134,6 +1134,60 @@ describe('MessageList nested tool calls', () => {
expect(within(dialog).queryByText(/raw structured JSON should not be shown/)).toBeNull()
})
+ it('formats structured agent fallback results as readable markdown', () => {
+ useChatStore.setState({
+ sessions: {
+ [ACTIVE_TAB]: makeSessionState({
+ messages: [
+ {
+ id: 'tool-agent',
+ type: 'tool_use',
+ toolName: 'Agent',
+ toolUseId: 'agent-1',
+ input: { description: '审查安全风险' },
+ timestamp: 1,
+ },
+ {
+ 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。',
+ },
+ ],
+ },
+ isError: false,
+ timestamp: 2,
+ },
+ ],
+ }),
+ },
+ })
+
+ render()
+
+ expect(screen.getByText(/git:v0\.2\.6\.\.v0\.2\.7:0/)).toBeTruthy()
+ expect(screen.queryByText(/\{"results"/)).toBeNull()
+
+ fireEvent.click(screen.getByRole('button', { name: 'View result' }))
+
+ const dialog = screen.getByRole('dialog')
+ expect(within(dialog).getByText('git:v0.2.6..v0.2.7:0')).toBeTruthy()
+ expect(within(dialog).getByText('/tmp/example/src/lib.rs:220')).toBeTruthy()
+ expect(within(dialog).getByText(/服务默认监听 0\.0\.0\.0/)).toBeTruthy()
+ expect(within(dialog).queryByText(/\{"results"/)).toBeNull()
+ })
+
it('renders copy controls for user messages and scopes assistant copy to a single reply', async () => {
const writeText = vi.fn().mockResolvedValue(undefined)
Object.assign(navigator, {
diff --git a/desktop/src/components/chat/ToolCallGroup.tsx b/desktop/src/components/chat/ToolCallGroup.tsx
index 3a2671ee..9df1b9a8 100644
--- a/desktop/src/components/chat/ToolCallGroup.tsx
+++ b/desktop/src/components/chat/ToolCallGroup.tsx
@@ -877,7 +877,72 @@ function getAgentOutputSummary(content: string): string {
}
function extractAgentDisplayText(content: unknown): string {
- return stripAgentResultMetadata(extractTextContent(content))
+ return stripAgentResultMetadata(formatAgentStructuredResult(content) || extractTextContent(content))
+}
+
+function formatAgentStructuredResult(content: unknown): string {
+ const structured = parseStructuredAgentContent(content)
+ if (!structured || Array.isArray(structured)) return ''
+
+ const results = structured.results
+ if (!Array.isArray(results) || results.length === 0) return ''
+
+ const items = results
+ .map((result, index) => formatAgentStructuredResultItem(result, index))
+ .filter(Boolean)
+
+ return items.join('\n')
+}
+
+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 typeof content === 'object' && content !== null ? content as Record | unknown[] : null
+}
+
+function formatAgentStructuredResultItem(result: unknown, index: number): string {
+ if (!result || typeof result !== 'object' || Array.isArray(result)) {
+ const text = extractTextContent(result).trim()
+ return text ? `${index + 1}. ${text}` : ''
+ }
+
+ const record = result 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 lines = [`${index + 1}. ${location ? formatInlineCode(location) : 'Result'}`]
+
+ if (message) lines.push(` - ${message}`)
+ if (context) lines.push(` - ${context}`)
+ if (snippet) lines.push(` - ${snippet}`)
+
+ return lines.join('\n')
+}
+
+function formatAgentResultLocation(record: Record): string {
+ const file = getStringField(record, 'file')
+ if (!file) return ''
+ const line = typeof record.line === 'number' ? record.line : null
+ return line !== null ? `${file}:${line}` : file
+}
+
+function getStringField(record: Record, key: string): string {
+ const value = record[key]
+ return typeof value === 'string' ? value.trim() : ''
+}
+
+function formatInlineCode(value: string): string {
+ return `\`${value.replace(/`/g, '\\`')}\``
}
function stripAgentResultMetadata(text: string): string {