Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions app/src/renderer/src/session-timeline-presentation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,7 +38,7 @@ function renderFileContent(text) {
const total = lines.length;
const collapsed = total > 12;
return `<div class="file-content">
<div class="file-content-head"><span class="label">File contents</span><span class="meta">${total} lines</span></div>
<div class="file-content-head"><span class="label">${escapeHtml(label)}</span><span class="meta">${total} lines</span></div>
<div class="file-content-body ${collapsed ? 'collapsed' : ''}"><div class="gutter">${gutter}</div><div class="code">${escapeHtml(lines.join('\n'))}</div></div>
${collapsed ? `<button class="file-content-expand" onclick="this.previousElementSibling.classList.toggle('collapsed');this.textContent=this.previousElementSibling.classList.contains('collapsed')?'Show all ${total} lines':'Collapse'">Show all ${total} lines</button>` : ''}
</div>`;
Expand Down Expand Up @@ -226,7 +226,10 @@ export function renderPrettyTool(toolCall) {

const terminal = renderTerminalTool(toolCall.name, args, output, isError);
if (terminal !== null) return terminal;
return `<div class="body-section"><div class="body-label">Input</div>${renderFieldGrid(args)}</div>`
const input = typeof args === 'string'
? renderFileContent(args, 'Input')
: `<div class="body-section"><div class="body-label">Input</div>${renderFieldGrid(args)}</div>`;
return input
+ (output ? `<div class="body-section" style="margin-top:12px;"><div class="body-label">Output</div>${renderOutput(output, isError)}</div>` : '');
}

Expand Down
36 changes: 36 additions & 0 deletions tests/app-tool-renderer.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -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 <value>',
'*** 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, /<span class="label">Input<\/span><span class="meta">6 lines<\/span>/);
assert.match(html, /\*\*\* Begin Patch\n\*\*\* Update File: app\.css\n@@\n-old\n\+new &lt;value&gt;\n\*\*\* End Patch/);
assert.doesNotMatch(html, /<div class="field-(?:grid|key)">/);
assert.doesNotMatch(html, /<value>/);
});

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">path</);
assert.match(html, /class="field-key">recursive</);
assert.doesNotMatch(html, /class="file-content"/);
});