Skip to content
Merged
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
15 changes: 11 additions & 4 deletions src/renderer/components/WorkbenchControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -310,10 +310,17 @@ export const TranscriptRow = memo(function TranscriptRow({
{entry.running ? 'Running' : entry.ok ? 'Done' : 'Failed'}
</small>
</summary>
<pre>
{entry.output ||
(entry.running ? 'Waiting for output…' : 'No output.')}
</pre>
<div className="wb-tool-section">
<h3>Input</h3>
<pre>{entry.input || 'No input.'}</pre>
</div>
<div className="wb-tool-section">
<h3>Output</h3>
<pre>
{entry.output ||
(entry.running ? 'Waiting for output…' : 'No output.')}
</pre>
</div>
</details>
);
return (
Expand Down
30 changes: 30 additions & 0 deletions src/renderer/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type Entry =
toolId: string;
tool: string;
args: string;
input: string;
output: string;
running: boolean;
ok: boolean;
Expand Down Expand Up @@ -55,6 +56,33 @@ function toolArgsSummary(name: string, args: Record<string, unknown> | undefined
return JSON.stringify(args).slice(0, 120);
}

function toolInputText(name: string, args: Record<string, unknown> | undefined): string {
if (!args) return '';
if (name === 'bash') return String(args.command ?? '');
if (name === 'read' || name === 'write' || name === 'edit') {
const p = String(args.path ?? args.file ?? '');
const parts = [p];
if (name === 'write' && typeof args.content === 'string')
parts.push('', args.content);
if (name === 'edit') {
if (typeof args.oldText === 'string') parts.push('', 'Old:', args.oldText);
if (typeof args.newText === 'string') parts.push('', 'New:', args.newText);
}
if (name === 'read') {
const range: string[] = [];
if (typeof args.offset === 'number') range.push(`offset ${args.offset}`);
if (typeof args.limit === 'number') range.push(`limit ${args.limit}`);
if (range.length) parts.push(range.join(', '));
}
return parts.join('\n');
}
try {
return JSON.stringify(args, null, 2);
} catch {
return String(args);
}
}

function resultText(content: unknown): string {
if (!Array.isArray(content)) return '';
return content
Expand Down Expand Up @@ -92,6 +120,7 @@ function entriesFromMessages(messages: unknown[]): Entry[] {
toolId: p.id,
tool: p.name,
args: toolArgsSummary(p.name, p.arguments),
input: toolInputText(p.name, p.arguments),
output: '',
running: false,
ok: true,
Expand Down Expand Up @@ -177,6 +206,7 @@ function applyEvent(
toolId: toolCallId,
tool: toolName,
args: toolArgsSummary(toolName, event.args as Record<string, unknown>),
input: toolInputText(toolName, event.args as Record<string, unknown>),
output: '',
running: true,
ok: true,
Expand Down
15 changes: 13 additions & 2 deletions src/renderer/workbench.css
Original file line number Diff line number Diff line change
Expand Up @@ -706,11 +706,22 @@ html[data-reduced-motion='true'] .wb-shell *::after {
font-size: 12px;
color: var(--copy);
}
.wb-tool-section h3 {
margin: 0;
padding: 8px 12px 0;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
}
.wb-tool-section + .wb-tool-section {
border-top: 1px solid var(--line-soft);
}
.wb-tool pre {
padding: 12px;
padding: 8px 12px 12px;
max-height: 320px;
overflow: auto;
border-top: 1px solid var(--line-soft);
margin: 0;
white-space: pre-wrap;
overflow-wrap: anywhere;
Expand Down
53 changes: 53 additions & 0 deletions tests/integration/ToolCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, expect, it, beforeEach } from 'vitest';
import { render, cleanup } from '@testing-library/react';
import { TranscriptRow } from '../../src/renderer/components/WorkbenchControls';
import type { Entry } from '../../src/renderer/timeline';

const toolEntry: Extract<Entry, { kind: 'tool' }> = {
key: 'tool-1',
kind: 'tool',
toolId: 'call_1',
tool: 'edit',
args: '/repo/src/app.ts',
input: '/repo/src/app.ts\n\nOld:\nconst before = 1;\n\nNew:\nconst after = 2;',
output: 'Edited /repo/src/app.ts',
running: false,
ok: true,
};

describe('<TranscriptRow> tool card', () => {
beforeEach(() => cleanup());

it('shows input and output as separate labelled sections when expanded', () => {
const expanded = new Map<string, boolean>([[toolEntry.key, true]]);
const { container } = render(
<TranscriptRow entry={toolEntry} expanded={expanded} />,
);
const headings = [...container.querySelectorAll('.wb-tool-section h3')].map(
(h) => h.textContent,
);
expect(headings).toEqual(['Input', 'Output']);
expect(container.textContent).toContain('const before = 1;');
expect(container.textContent).toContain('const after = 2;');
expect(container.textContent).toContain('Edited /repo/src/app.ts');
expect(container.textContent).toContain('Done');
});

it('shows running state and waiting output while a tool runs', () => {
const running: Extract<Entry, { kind: 'tool' }> = {
...toolEntry,
key: 'tool-2',
tool: 'bash',
args: 'sleep 1',
input: 'sleep 1',
output: '',
running: true,
};
const expanded = new Map<string, boolean>([[running.key, true]]);
const { container } = render(
<TranscriptRow entry={running} expanded={expanded} />,
);
expect(container.textContent).toContain('Running');
expect(container.textContent).toContain('Waiting for output');
});
});
55 changes: 55 additions & 0 deletions tests/unit/timeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,58 @@ describe('entriesFromMessages', () => {
}
});

it('keeps full edit input with old and new text', () => {
const messages = [
{
role: 'assistant',
content: [
{
type: 'toolCall',
id: 'call_edit',
name: 'edit',
arguments: {
path: '/repo/src/app.ts',
oldText: 'const before = 1;',
newText: 'const after = 2;',
},
},
],
},
];
const [entry] = entriesFromMessages(messages);
if (entry?.kind === 'tool') {
expect(entry.args).toBe('/repo/src/app.ts');
expect(entry.input).toContain('/repo/src/app.ts');
expect(entry.input).toContain('const before = 1;');
expect(entry.input).toContain('const after = 2;');
} else {
expect.fail('expected tool entry');
}
});

it('does not truncate long input for other tools', () => {
const long = 'x'.repeat(300);
const messages = [
{
role: 'assistant',
content: [
{
type: 'toolCall',
id: 'call_long',
name: 'custom_lookup',
arguments: { query: long },
},
],
},
];
const [entry] = entriesFromMessages(messages);
if (entry?.kind === 'tool') {
expect(entry.input).toContain(long);
} else {
expect.fail('expected tool entry');
}
});

it('interleaves user, assistant, tool, toolResult entries', () => {
const messages = [
{ role: 'user', content: [{ type: 'text', text: 'do it' }] },
Expand Down Expand Up @@ -246,6 +298,7 @@ describe('applyEvent', () => {
if (entry?.kind === 'tool') {
expect(entry.tool).toBe('bash');
expect(entry.args).toBe('ls');
expect(entry.input).toBe('ls');
expect(entry.running).toBe(true);
} else {
expect.fail('expected tool');
Expand All @@ -260,6 +313,7 @@ describe('applyEvent', () => {
toolId: 'c1',
tool: 'bash',
args: 'ls',
input: 'ls',
output: '',
running: true,
ok: true,
Expand Down Expand Up @@ -287,6 +341,7 @@ describe('applyEvent', () => {
toolId: 'c1',
tool: 'bash',
args: 'ls',
input: 'ls',
output: '',
running: true,
ok: true,
Expand Down
Loading