diff --git a/frontend-modern/src/components/AI/Chat/ToolExecutionBlock.tsx b/frontend-modern/src/components/AI/Chat/ToolExecutionBlock.tsx index 4dd23087c9..097b21cda8 100644 --- a/frontend-modern/src/components/AI/Chat/ToolExecutionBlock.tsx +++ b/frontend-modern/src/components/AI/Chat/ToolExecutionBlock.tsx @@ -608,6 +608,19 @@ export const PendingToolBlock: Component = (props) => { if (!preview || summary.startsWith('$ ') || preview === summary) return ''; return preview; }); + // While the model is still streaming the tool arguments the raw input is + // partial JSON that no parser can summarize yet. Show its live tail (the + // OpenCode mutable tool-part pattern: you watch the call being written) + // instead of leaving only the generic placeholder. + const formingArgsPreview = createMemo(() => { + if (commandPreview()) return ''; + if (!isPlaceholderToolInputSummary(parsedInputSummary())) return ''; + const raw = (props.tool.rawInput || '').replace(/\s+/g, ' ').trim(); + if (!raw) return ''; + const maxChars = 120; + if (raw.length <= maxChars) return raw; + return '…' + raw.slice(raw.length - maxChars); + }); const status = createMemo(() => props.tool.status || 'pending'); const [now, setNow] = createSignal(Date.now()); const statusLabel = createMemo(() => { @@ -696,6 +709,14 @@ export const PendingToolBlock: Component = (props) => { + +
+ {formingArgsPreview()} +
+
diff --git a/frontend-modern/src/components/AI/Chat/__tests__/ToolExecutionBlock.test.tsx b/frontend-modern/src/components/AI/Chat/__tests__/ToolExecutionBlock.test.tsx index 8feac4ba37..e137c4044a 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/ToolExecutionBlock.test.tsx +++ b/frontend-modern/src/components/AI/Chat/__tests__/ToolExecutionBlock.test.tsx @@ -818,6 +818,53 @@ describe('PendingToolBlock', () => { expect(screen.getByText('df -h')).toBeInTheDocument(); }); + it('streams forming raw args into the pending row while they are unparsable', () => { + // Mid-stream tool args are partial JSON: no summary or command preview + // can parse yet, so the row must show the live raw tail instead of only + // the generic placeholder. + render(() => ( + + )); + expect(screen.getByLabelText('Tool input streaming')).toHaveTextContent( + '{"action":"topology","summary_on', + ); + }); + + it('shows the tail of long forming raw args', () => { + const head = '{"action":"exec","command":"'; + const tail = 'x'.repeat(130); + render(() => ( + + )); + const preview = screen.getByLabelText('Tool input streaming'); + expect(preview.textContent).toMatch(/^…/); + expect(preview.textContent).toContain('xxx'); + expect(preview.textContent!.length).toBeLessThanOrEqual(121); + }); + + it('does not show the forming-args preview once the input parses to a summary', () => { + render(() => ( + + )); + expect(screen.queryByLabelText('Tool input streaming')).not.toBeInTheDocument(); + }); + it('truncates input summaries longer than 28 chars', () => { const longInput = 'B'.repeat(55); render(() => );