mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-03 12:42:43 +00:00
Parse Assistant tool input summaries
This commit is contained in:
@@ -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: '{}' })} />);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user