mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
fix: Render structured agent fallbacks readably
Agent fallback output can arrive as structured result arrays when no terminal task report is available. The desktop card now formats that shape into a readable Markdown list instead of exposing raw transport JSON in the preview or result modal. Constraint: Some Agent executions still only provide structured tool_result content without a terminal Markdown report Rejected: Keep pretty-printed JSON as the fallback | still exposes implementation shape instead of user-facing results Confidence: high Scope-risk: narrow Directive: Structured Agent result arrays are display data; keep them readable before falling back to raw serialization Tested: cd desktop && bun run test -- run src/components/chat/MessageList.test.tsx -t "formats structured agent fallback" Tested: cd desktop && bun run test -- run src/components/chat/MessageList.test.tsx -t agent Not-tested: Full desktop gate per request to avoid broad gate/test runs
This commit is contained in:
parent
430edc436e
commit
3433be18dd
@ -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(<MessageList />)
|
||||
|
||||
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, {
|
||||
|
||||
@ -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<string, unknown> | 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<string, unknown> | unknown[] : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return typeof content === 'object' && content !== null ? content as Record<string, unknown> | 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<string, unknown>
|
||||
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, unknown>): 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<string, unknown>, 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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user