Skip to content

Commit 7a03779

Browse files
IM.codesclaude
andcommitted
fix(web): tool call args missing — show input from result detail
Transport SDK tool.call events arrive at content_block_start before input is streamed, so payload.input is undefined. The complete input only appears in the tool.result's detail.input (from content_block_stop). The consolidation and render code only checked the call's input, resulting in "> Agent ✓" with no args visible. Fix: fall back to result detail.input when the call has no input, in both the consolidation merge and the render path. Also default tool detail <details> to open so args are always visible. Add 3 tests: - Transport SDK tool.call with no input → args from result detail - Normal tool.call with input → args shown directly - Agent tool → prompt from result detail.input Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7233734 commit 7a03779

2 files changed

Lines changed: 104 additions & 5 deletions

File tree

web/src/components/ChatView.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,10 @@ function buildViewItems(events: TimelineEvent[]): ViewItem[] {
232232
const next = visible[resultIdx];
233233
consumedIds.add(next.eventId); // mark tool.result as consumed
234234
const toolName = String(ev.payload.tool ?? 'tool');
235-
const inputText = summarizeToolInput(ev.payload.input, ev.payload.detail);
235+
// tool.call from transport SDK may have no input yet (streamed incrementally).
236+
// Fall back to the result's detail.input which has the complete args.
237+
const inputText = summarizeToolInput(ev.payload.input, ev.payload.detail)
238+
|| summarizeToolInput((next.payload.detail as any)?.input, next.payload.detail);
236239
const input = inputText ? ` ${inputText}` : '';
237240
const status = next.payload.error ? `✗ ${String(next.payload.error)}` : '✓';
238241
const output = !next.payload.error && next.payload.output ? String(next.payload.output) : undefined;
@@ -1193,9 +1196,11 @@ const ChatEvent = memo(function ChatEvent({
11931196

11941197
case 'tool.call': {
11951198
const callDetail = event.payload._callDetail ?? event.payload.detail;
1196-
const toolInput = summarizeToolInput(event.payload.input, callDetail);
1197-
const toolOutput = event.payload._output ? String(event.payload._output) : undefined;
11981199
const resultDetail = event.payload._resultDetail;
1200+
// Fall back to result detail for input — transport SDK tool.call may arrive without input
1201+
const toolInput = summarizeToolInput(event.payload.input, callDetail)
1202+
|| summarizeToolInput((resultDetail as any)?.input, resultDetail);
1203+
const toolOutput = event.payload._output ? String(event.payload._output) : undefined;
11991204
return (
12001205
<ToolBlockFold>
12011206
<div class="chat-event chat-tool">
@@ -1209,7 +1214,7 @@ const ChatEvent = memo(function ChatEvent({
12091214
</div>
12101215
)}
12111216
{(callDetail || resultDetail) && (
1212-
<details class="chat-tool-detail">
1217+
<details class="chat-tool-detail" open>
12131218
<summary class="chat-tool-detail-summary">{t('chat.tool_detail_toggle')}</summary>
12141219
<ToolDetailSection label={t('chat.tool_detail_input')} value={(callDetail as any)?.input} />
12151220
<ToolDetailSection label={t('chat.tool_detail_output')} value={(resultDetail as any)?.output} />
@@ -1239,7 +1244,7 @@ const ChatEvent = memo(function ChatEvent({
12391244
)}
12401245
</div>
12411246
{detail && (
1242-
<details class="chat-tool-detail">
1247+
<details class="chat-tool-detail" open>
12431248
<summary class="chat-tool-detail-summary">{t('chat.tool_detail_toggle')}</summary>
12441249
<ToolDetailSection label={t('chat.tool_detail_output')} value={(detail as any).output} />
12451250
<ToolDetailSection label={t('chat.tool_detail_meta')} value={(detail as any).meta} />

web/test/components/ChatView.test.tsx

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,4 +404,98 @@ describe('ChatView', () => {
404404
});
405405
expect(onLoadOlder).not.toHaveBeenCalled();
406406
});
407+
408+
it('shows tool input args from result detail when tool.call has no input (transport SDK)', async () => {
409+
// Transport SDK: tool.call arrives at content_block_start with NO input,
410+
// tool.result arrives at content_block_stop with input in detail.input.
411+
const events = [
412+
{
413+
eventId: 'tc-1',
414+
type: 'tool.call',
415+
ts: 1000,
416+
payload: { tool: 'Read' },
417+
},
418+
{
419+
eventId: 'tr-1',
420+
type: 'tool.result',
421+
ts: 1001,
422+
payload: {
423+
output: 'file contents...',
424+
detail: {
425+
kind: 'tool_use_complete',
426+
summary: 'Read',
427+
input: { file_path: '/home/user/project/src/index.ts' },
428+
},
429+
},
430+
},
431+
] as any;
432+
433+
const { container } = render(
434+
<ChatView events={events} loading={false} sessionId="test" />,
435+
);
436+
437+
const toolEl = container.querySelector('.chat-tool');
438+
expect(toolEl).not.toBeNull();
439+
const text = toolEl!.textContent ?? '';
440+
// Must contain the file path from result detail, not just "Read ✓"
441+
expect(text).toContain('/home/user/project/src/index.ts');
442+
expect(text).toContain('✓');
443+
});
444+
445+
it('shows tool input inline when tool.call already has input (tmux agent)', async () => {
446+
const events = [
447+
{
448+
eventId: 'tc-2',
449+
type: 'tool.call',
450+
ts: 2000,
451+
payload: { tool: 'Bash', input: 'npm test' },
452+
},
453+
{
454+
eventId: 'tr-2',
455+
type: 'tool.result',
456+
ts: 2001,
457+
payload: { output: 'all tests passed' },
458+
},
459+
] as any;
460+
461+
const { container } = render(
462+
<ChatView events={events} loading={false} sessionId="test" />,
463+
);
464+
465+
const toolEl = container.querySelector('.chat-tool');
466+
const text = toolEl!.textContent ?? '';
467+
expect(text).toContain('npm test');
468+
expect(text).toContain('✓');
469+
});
470+
471+
it('shows Agent tool prompt from result detail.input', async () => {
472+
const events = [
473+
{
474+
eventId: 'tc-3',
475+
type: 'tool.call',
476+
ts: 3000,
477+
payload: { tool: 'Agent' },
478+
},
479+
{
480+
eventId: 'tr-3',
481+
type: 'tool.result',
482+
ts: 3001,
483+
payload: {
484+
detail: {
485+
kind: 'tool_use_complete',
486+
input: { prompt: 'Find the bug in auth module', description: 'Auth bug search' },
487+
},
488+
},
489+
},
490+
] as any;
491+
492+
const { container } = render(
493+
<ChatView events={events} loading={false} sessionId="test" />,
494+
);
495+
496+
const toolEl = container.querySelector('.chat-tool');
497+
const text = toolEl!.textContent ?? '';
498+
// Must show the prompt, not just "Agent ✓"
499+
expect(text).toContain('Find the bug in auth module');
500+
});
407501
});

0 commit comments

Comments
 (0)