mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Stream forming tool args into the pending tool row
While the model writes tool arguments the pending row showed only a generic placeholder plus 'Receiving tool input.' for what can be tens of seconds on slow routes. OpenCode streams the forming call into the mutable tool part; Pulse now shows the live tail of the raw args in the pending row whenever they are still unparsable, and drops it the moment the input parses into a real summary or command preview. The raw input was already carried by tool_progress events; this only renders it.
This commit is contained in:
@@ -608,6 +608,19 @@ export const PendingToolBlock: Component<PendingToolBlockProps> = (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<PendingToolBlockProps> = (props) => {
|
||||
<Show when={commandPreview()}>
|
||||
<ToolCommandPreview preview={commandPreview()} />
|
||||
</Show>
|
||||
<Show when={formingArgsPreview()}>
|
||||
<div
|
||||
class="mt-1 min-w-0 font-mono text-[10px] leading-snug text-muted"
|
||||
aria-label="Tool input streaming"
|
||||
>
|
||||
<span class="block truncate">{formingArgsPreview()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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(() => (
|
||||
<PendingToolBlock
|
||||
tool={makePending({
|
||||
name: 'pulse_query',
|
||||
input: '',
|
||||
rawInput: '{"action":"topology","summary_on',
|
||||
status: 'pending',
|
||||
})}
|
||||
/>
|
||||
));
|
||||
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(() => (
|
||||
<PendingToolBlock
|
||||
tool={makePending({ name: 'pulse_query', input: '', rawInput: head + tail, status: 'pending' })}
|
||||
/>
|
||||
));
|
||||
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(() => (
|
||||
<PendingToolBlock
|
||||
tool={makePending({
|
||||
name: 'pulse_query',
|
||||
input: '{"action":"topology"}',
|
||||
rawInput: '{"action":"topology"}',
|
||||
status: 'running',
|
||||
})}
|
||||
/>
|
||||
));
|
||||
expect(screen.queryByLabelText('Tool input streaming')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('truncates input summaries longer than 28 chars', () => {
|
||||
const longInput = 'B'.repeat(55);
|
||||
render(() => <PendingToolBlock tool={makePending({ input: longInput })} />);
|
||||
|
||||
Reference in New Issue
Block a user