diff --git a/app/src/renderer/src/session-timeline-presentation.mjs b/app/src/renderer/src/session-timeline-presentation.mjs index fee7471..976fe57 100644 --- a/app/src/renderer/src/session-timeline-presentation.mjs +++ b/app/src/renderer/src/session-timeline-presentation.mjs @@ -21,7 +21,7 @@ function formatToolInput(toolCall) { } } -function renderFileContent(text) { +function renderFileContent(text, label = 'File contents') { let lines = text.split('\n'); const hasLineNums = lines.length > 1 && lines.slice(0, 5).every(line => /^\s*\d+\t/.test(line) || line === ''); let gutter; @@ -38,7 +38,7 @@ function renderFileContent(text) { const total = lines.length; const collapsed = total > 12; return `
-
File contents${total} lines
+
${escapeHtml(label)}${total} lines
${gutter}
${escapeHtml(lines.join('\n'))}
${collapsed ? `` : ''}
`; @@ -226,7 +226,10 @@ export function renderPrettyTool(toolCall) { const terminal = renderTerminalTool(toolCall.name, args, output, isError); if (terminal !== null) return terminal; - return `
Input
${renderFieldGrid(args)}
` + const input = typeof args === 'string' + ? renderFileContent(args, 'Input') + : `
Input
${renderFieldGrid(args)}
`; + return input + (output ? `
Output
${renderOutput(output, isError)}
` : ''); } diff --git a/tests/app-tool-renderer.test.mjs b/tests/app-tool-renderer.test.mjs index d72657a..abd4ba4 100644 --- a/tests/app-tool-renderer.test.mjs +++ b/tests/app-tool-renderer.test.mjs @@ -2,6 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { getArgPreview, getToolIcon, renderTerminalTool } from '../app/src/renderer/src/tool-renderer.js'; +import { renderPrettyTool } from '../app/src/renderer/src/session-timeline-presentation.mjs'; test('Codex exec renders source and decoded result instead of a Bash terminal', () => { const output = JSON.stringify([ @@ -110,3 +111,38 @@ test('Codex exec preview uses its string input', () => { test('Codex exec uses the Claude Bash terminal icon', () => { assert.equal(getToolIcon('exec'), getToolIcon('Bash')); }); + +test('Codex apply_patch renders its string input as one multiline block', () => { + const patch = [ + '*** Begin Patch', + '*** Update File: app.css', + '@@', + '-old', + '+new ', + '*** End Patch', + ].join('\n'); + const html = renderPrettyTool({ + name: 'apply_patch', + input_json: JSON.stringify(patch), + result: { content: 'Done!', is_error: 0 }, + }); + + assert.match(html, /class="file-content"/); + assert.match(html, /Input<\/span>6 lines<\/span>/); + assert.match(html, /\*\*\* Begin Patch\n\*\*\* Update File: app\.css\n@@\n-old\n\+new <value>\n\*\*\* End Patch/); + assert.doesNotMatch(html, /
/); + assert.doesNotMatch(html, //); +}); + +test('generic object tool input continues to render as a field grid', () => { + const html = renderPrettyTool({ + name: 'custom_tool', + input_json: JSON.stringify({ path: 'app.css', recursive: true }), + result: {}, + }); + + assert.match(html, /class="field-grid"/); + assert.match(html, /class="field-key">pathrecursive