From 5a92f5e15d60146d4c379c031dd9d1e6969741b2 Mon Sep 17 00:00:00 2001
From: Johnny Huynh <27847622+johnnyhuy@users.noreply.github.com>
Date: Sun, 8 Nov 2026 00:45:14 +0900
Subject: [PATCH] feat: show full tool input on tool call cards
---
src/renderer/components/WorkbenchControls.tsx | 15 +++--
src/renderer/timeline.ts | 30 ++++++++++
src/renderer/workbench.css | 15 ++++-
tests/integration/ToolCard.test.tsx | 53 ++++++++++++++++++
tests/unit/timeline.test.ts | 55 +++++++++++++++++++
5 files changed, 162 insertions(+), 6 deletions(-)
create mode 100644 tests/integration/ToolCard.test.tsx
diff --git a/src/renderer/components/WorkbenchControls.tsx b/src/renderer/components/WorkbenchControls.tsx
index 282ce4f..5cf1ddf 100644
--- a/src/renderer/components/WorkbenchControls.tsx
+++ b/src/renderer/components/WorkbenchControls.tsx
@@ -310,10 +310,17 @@ export const TranscriptRow = memo(function TranscriptRow({
{entry.running ? 'Running' : entry.ok ? 'Done' : 'Failed'}
-
- {entry.output ||
- (entry.running ? 'Waiting for output…' : 'No output.')}
-
+
+
Input
+
{entry.input || 'No input.'}
+
+
+
Output
+
+ {entry.output ||
+ (entry.running ? 'Waiting for output…' : 'No output.')}
+
+
);
return (
diff --git a/src/renderer/timeline.ts b/src/renderer/timeline.ts
index 5522ce6..34064de 100644
--- a/src/renderer/timeline.ts
+++ b/src/renderer/timeline.ts
@@ -9,6 +9,7 @@ type Entry =
toolId: string;
tool: string;
args: string;
+ input: string;
output: string;
running: boolean;
ok: boolean;
@@ -55,6 +56,33 @@ function toolArgsSummary(name: string, args: Record | undefined
return JSON.stringify(args).slice(0, 120);
}
+function toolInputText(name: string, args: Record | 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
@@ -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,
@@ -177,6 +206,7 @@ function applyEvent(
toolId: toolCallId,
tool: toolName,
args: toolArgsSummary(toolName, event.args as Record),
+ input: toolInputText(toolName, event.args as Record),
output: '',
running: true,
ok: true,
diff --git a/src/renderer/workbench.css b/src/renderer/workbench.css
index e8c2e36..8123754 100644
--- a/src/renderer/workbench.css
+++ b/src/renderer/workbench.css
@@ -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;
diff --git a/tests/integration/ToolCard.test.tsx b/tests/integration/ToolCard.test.tsx
new file mode 100644
index 0000000..534ba36
--- /dev/null
+++ b/tests/integration/ToolCard.test.tsx
@@ -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 = {
+ 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(' tool card', () => {
+ beforeEach(() => cleanup());
+
+ it('shows input and output as separate labelled sections when expanded', () => {
+ const expanded = new Map([[toolEntry.key, true]]);
+ const { container } = render(
+ ,
+ );
+ 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 = {
+ ...toolEntry,
+ key: 'tool-2',
+ tool: 'bash',
+ args: 'sleep 1',
+ input: 'sleep 1',
+ output: '',
+ running: true,
+ };
+ const expanded = new Map([[running.key, true]]);
+ const { container } = render(
+ ,
+ );
+ expect(container.textContent).toContain('Running');
+ expect(container.textContent).toContain('Waiting for output');
+ });
+});
diff --git a/tests/unit/timeline.test.ts b/tests/unit/timeline.test.ts
index 1fe00ab..d42944a 100644
--- a/tests/unit/timeline.test.ts
+++ b/tests/unit/timeline.test.ts
@@ -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' }] },
@@ -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');
@@ -260,6 +313,7 @@ describe('applyEvent', () => {
toolId: 'c1',
tool: 'bash',
args: 'ls',
+ input: 'ls',
output: '',
running: true,
ok: true,
@@ -287,6 +341,7 @@ describe('applyEvent', () => {
toolId: 'c1',
tool: 'bash',
args: 'ls',
+ input: 'ls',
output: '',
running: true,
ok: true,