Parse Assistant tool input summaries

This commit is contained in:
rcourtman
2026-06-06 13:47:33 +01:00
parent a7ca1da7ee
commit 02fdddf7f0
3 changed files with 209 additions and 28 deletions
@@ -323,6 +323,16 @@ runtime cost control, and shared AI transport surfaces.
useful, and keeping structured JSON, unavailable output, and the full raw
payload behind Details. This preserves evidence for inspection without
letting large command output dominate the Assistant answer flow.
Tool-row summaries are part of that same source-anchored contract, not
assistant prose cleanup: when provider or backend transport supplies a
function-style call such as `pulse_read(...)` or a friendly string backed by
structured raw input, Pulse must derive the visible row from the parsed tool
arguments and keep the raw transport form behind Details. This adapts
OpenCode's `ToolPart`/`InlineToolRow` rendering in
`packages/opencode/src/cli/cmd/tui/routes/session/index.tsx` and the
`session.next.tool.input.*` mutations in
`packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx`: typed tool state
owns the action row, while raw invocation syntax is inspection detail.
Streaming thinking rows follow the same source-anchored reasoning-display
contract: OpenCode commit
`9ed17da55ab1f7360cc0e01075f763e27fa899e9` renders reasoning through
@@ -119,6 +119,85 @@ const targetSuffix = (record: Record<string, unknown>) => {
return ` on ${formatIdentifierLabel(target, { maxLength: 18 })}`;
};
const parseFunctionStyleToolInput = (input: string) => {
const match = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([\s\S]*)\)\s*$/.exec(input.trim());
if (!match) return null;
const args: Record<string, unknown> = {};
const body = match[2] || '';
let index = 0;
const skipWhitespace = () => {
while (index < body.length && /\s/.test(body[index])) index += 1;
};
const parseQuotedValue = (quote: string) => {
index += 1;
let value = '';
while (index < body.length) {
const char = body[index];
if (char === '\\') {
if (index + 1 < body.length) {
value += body[index + 1];
index += 2;
continue;
}
return null;
}
if (char === quote) {
index += 1;
return value;
}
value += char;
index += 1;
}
return null;
};
while (index < body.length) {
skipWhitespace();
if (index >= body.length) break;
if (body[index] === ',') {
index += 1;
continue;
}
const keyMatch = /^[a-zA-Z_][a-zA-Z0-9_]*/.exec(body.slice(index));
if (!keyMatch) return null;
const key = keyMatch[0];
index += key.length;
skipWhitespace();
if (body[index] !== '=') return null;
index += 1;
skipWhitespace();
const quote = body[index];
let value: unknown;
if (quote === '"' || quote === "'") {
value = parseQuotedValue(quote);
if (value === null) return null;
} else {
const start = index;
while (index < body.length && body[index] !== ',') index += 1;
const rawValue = body.slice(start, index).trim();
if (!rawValue) return null;
if (rawValue === 'true') value = true;
else if (rawValue === 'false') value = false;
else if (rawValue === 'null') value = null;
else if (/^-?\d+(?:\.\d+)?$/.test(rawValue)) value = Number(rawValue);
else value = rawValue;
}
args[key] = value;
skipWhitespace();
if (index < body.length && body[index] !== ',') return null;
}
return { name: match[1], args };
};
const formatPartialRawInputSummary = (rawInput: string | undefined, toolName?: string) => {
if (!rawInput?.trim()) return '';
@@ -251,43 +330,75 @@ const formatAlertsInputSummary = (record: Record<string, unknown>) => {
return action ? formatIdentifierLabel(action, { maxLength: 28 }) : 'read alerts';
};
const parseToolInputSummary = (input: string, toolName?: string, rawInput?: string) => {
const formatStructuredInputSummary = (
record: Record<string, unknown>,
toolName?: string,
rawInput?: string,
) => {
if (Object.keys(record).length === 0) {
return formatPartialRawInputSummary(rawInput, toolName) || 'request';
}
const tool = normalizedToolName(toolName);
if (tool === 'read') {
return formatPulseReadInputSummary(record) || 'read resource';
}
if (tool === 'run_command' || tool === 'control') {
return formatCommandSummary(record) || 'run command';
}
if (tool === 'query') {
return formatQueryInputSummary(record);
}
if (tool === 'alerts') {
return formatAlertsInputSummary(record);
}
if (typeof record.action === 'string' && record.action.trim()) {
return formatIdentifierLabel(record.action, { maxLength: 28 });
}
if (typeof record.command === 'string' && record.command.trim()) {
return formatIdentifierLabel(record.command, { maxLength: 28 });
}
return 'request';
};
const parseStructuredInputSummary = (input: string, toolName?: string, rawInput?: string) => {
const trimmed = input.trim();
if (!trimmed) return '';
if (!trimmed) return null;
try {
const parsed = JSON.parse(trimmed) as unknown;
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
const record = parsed as Record<string, unknown>;
if (Object.keys(record).length === 0) {
return formatPartialRawInputSummary(rawInput, toolName) || 'request';
}
const tool = normalizedToolName(toolName);
if (tool === 'read') {
return formatPulseReadInputSummary(record) || 'read resource';
}
if (tool === 'run_command' || tool === 'control') {
return formatCommandSummary(record) || 'run command';
}
if (tool === 'query') {
return formatQueryInputSummary(record);
}
if (tool === 'alerts') {
return formatAlertsInputSummary(record);
}
if (typeof record.action === 'string' && record.action.trim()) {
return formatIdentifierLabel(record.action, { maxLength: 28 });
}
if (typeof record.command === 'string' && record.command.trim()) {
return formatIdentifierLabel(record.command, { maxLength: 28 });
}
return 'request';
return formatStructuredInputSummary(parsed as Record<string, unknown>, toolName, rawInput);
}
} catch {
return formatIdentifierLabel(trimmed, { maxLength: 28 });
// Keep trying provider-style function-call input below.
}
return 'request';
const functionCall = parseFunctionStyleToolInput(trimmed);
if (functionCall) {
return formatStructuredInputSummary(functionCall.args, functionCall.name, rawInput);
}
return null;
};
const parseToolInputSummary = (input: string, toolName?: string, rawInput?: string) => {
const trimmed = input.trim();
if (!trimmed) return '';
const directSummary = parseStructuredInputSummary(trimmed, toolName, rawInput);
if (directSummary) {
return directSummary;
}
if (rawInput && rawInput.trim() && rawInput.trim() !== trimmed) {
const rawSummary = parseStructuredInputSummary(rawInput, toolName);
if (rawSummary && !isPlaceholderToolInputSummary(rawSummary)) {
return rawSummary;
}
}
return formatIdentifierLabel(trimmed, { maxLength: 28 });
};
const hasReadableToolOutput = (output: string) => {
@@ -195,6 +195,21 @@ describe('ToolExecutionBlock', () => {
expect(screen.queryByText(/"target_host"/)).not.toBeInTheDocument();
});
it('renders provider-style Pulse read function input as the actual command', () => {
render(() => (
<ToolExecutionBlock
tool={makeTool({
name: 'pulse_read',
input: 'pulse_read(target_host="current_resource", command="ls /dev | wc -l")',
output: '42',
})}
/>
));
expect(screen.getByText('$ ls /dev | wc -l on current resource')).toBeInTheDocument();
expect(screen.queryByText(/pulse_read\(/)).not.toBeInTheDocument();
});
it('renders Pulse read log input as a readable log action', () => {
render(() => (
<ToolExecutionBlock
@@ -223,6 +238,22 @@ describe('ToolExecutionBlock', () => {
expect(screen.getByText('$ systemctl restart nginx on tower')).toBeInTheDocument();
});
it('uses structured raw input over backend command display strings', () => {
render(() => (
<ToolExecutionBlock
tool={makeTool({
name: 'pulse_run_command',
input: 'Running: systemctl restart nginx',
rawInput: '{"target_host":"tower","command":"systemctl restart nginx"}',
output: 'queued',
})}
/>
));
expect(screen.getByText('$ systemctl restart nginx on tower')).toBeInTheDocument();
expect(screen.queryByText('Running: systemctl restart nginx')).not.toBeInTheDocument();
});
// --- Output display ---
it('previews plain-text output while keeping full details available', () => {
@@ -477,6 +508,20 @@ describe('PendingToolBlock', () => {
expect(screen.queryByText(/"command"/)).not.toBeInTheDocument();
});
it('renders pending provider-style Pulse read function input as the command being prepared', () => {
render(() => (
<PendingToolBlock
tool={makePending({
name: 'pulse_read',
input: 'pulse_read(target_host="current_resource", command="lsblk -o NAME,SIZE")',
})}
/>
));
expect(screen.getByText('$ lsblk -o NAME,SIZE on current resource')).toBeInTheDocument();
expect(screen.queryByText(/pulse_read\(/)).not.toBeInTheDocument();
});
it('uses raw partial input while pending Pulse read command JSON is still streaming', () => {
render(() => (
<PendingToolBlock
@@ -507,6 +552,21 @@ describe('PendingToolBlock', () => {
expect(screen.getByText('$ ls /dev |')).toBeInTheDocument();
});
it('uses structured raw input over pending backend command display strings', () => {
render(() => (
<PendingToolBlock
tool={makePending({
name: 'pulse_run_command',
input: 'Running: systemctl restart nginx',
rawInput: '{"target_host":"tower","command":"systemctl restart nginx"}',
})}
/>
));
expect(screen.getByText('$ systemctl restart nginx on tower')).toBeInTheDocument();
expect(screen.queryByText('Running: systemctl restart nginx')).not.toBeInTheDocument();
});
it('renders a command-specific pending label before governed command arguments arrive', () => {
render(() => <PendingToolBlock tool={makePending({ name: 'pulse_run_command', input: '{}' })} />);