Render Assistant tool commands in compact rows

This commit is contained in:
rcourtman
2026-06-06 06:26:07 +01:00
parent 0d8b90ce00
commit b3958005ce
3 changed files with 121 additions and 10 deletions
@@ -325,9 +325,9 @@ runtime cost control, and shared AI transport surfaces.
`packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx`.
Pulse's compact tool rows must follow that operator-language model: the row
should summarize the actual governed action (`search "prowlarr"`, `list
active alerts`, `topology summary`) instead of exposing only internal
action names such as `QUERY search` or raw JSON; raw input and output stay
available behind Details.
active alerts`, `topology summary`, `$ ls /dev | wc -l on current resource`)
instead of exposing only internal action names such as `QUERY search`,
`exec`, or raw JSON; raw input and output stay available behind Details.
The referenced OpenCode source at fetched `origin/dev` commit
`fa2b63f850fc0a23bec2bdff9e660450d3fe7913` also keeps assistant text,
reasoning, and tool invocation as typed message parts in
@@ -46,7 +46,58 @@ const stringField = (record: Record<string, unknown>, keys: string[]) => {
const booleanField = (record: Record<string, unknown>, key: string) => record[key] === true;
const inlineValue = (value: string, maxLength = 24) =>
value.replace(/["\r\n]+/g, ' ').trim().substring(0, maxLength);
value
.replace(/["\r\n]+/g, ' ')
.trim()
.substring(0, maxLength);
const targetSuffix = (record: Record<string, unknown>) => {
const target = stringField(record, ['target_host', 'targetHost', 'resource_id', 'resourceId']);
if (!target) return '';
return ` on ${formatIdentifierLabel(target, { maxLength: 18 })}`;
};
const formatCommandSummary = (record: Record<string, unknown>) => {
const command = stringField(record, ['command', 'cmd']);
if (!command) return '';
return `$ ${inlineValue(command, 64)}${targetSuffix(record)}`;
};
const formatPulseReadInputSummary = (record: Record<string, unknown>) => {
const action = stringField(record, ['action', 'type']).toLowerCase();
const path = inlineValue(stringField(record, ['path', 'file', 'file_path', 'filePath']), 36);
const pattern = inlineValue(
stringField(record, ['pattern', 'grep', 'grep_pattern', 'grepPattern']),
28,
);
const container = inlineValue(stringField(record, ['container', 'service', 'unit']), 24);
const source = stringField(record, ['source']).toLowerCase();
if (action === 'exec') {
return formatCommandSummary(record) || `run read-only command${targetSuffix(record)}`;
}
if (action === 'file') {
return path ? `read ${path}${targetSuffix(record)}` : `read file${targetSuffix(record)}`;
}
if (action === 'tail') {
return path ? `tail ${path}${targetSuffix(record)}` : `tail file${targetSuffix(record)}`;
}
if (action === 'find') {
if (pattern && path) return `find "${pattern}" in ${path}${targetSuffix(record)}`;
return pattern
? `find "${pattern}"${targetSuffix(record)}`
: `find files${targetSuffix(record)}`;
}
if (action === 'logs') {
if (container) return `logs ${container}${targetSuffix(record)}`;
return source
? `${formatIdentifierLabel(source, { maxLength: 18 })} logs${targetSuffix(record)}`
: `read logs${targetSuffix(record)}`;
}
return (
formatCommandSummary(record) || (action ? formatIdentifierLabel(action, { maxLength: 28 }) : '')
);
};
const formatQueryInputSummary = (record: Record<string, unknown>) => {
const action = stringField(record, ['action', 'type']).toLowerCase();
@@ -105,6 +156,12 @@ const parseToolInputSummary = (input: string, toolName?: string) => {
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
const record = parsed as Record<string, unknown>;
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);
}
@@ -150,9 +207,7 @@ export const ToolExecutionBlock: Component<ToolExecutionBlockProps> = (props) =>
const toolLabel = createMemo(() => getToolLabel(props.tool.name));
const inputText = createMemo(() => toolValueText(props.tool.input));
const outputText = createMemo(() => toolValueText(props.tool.output));
const inputSummary = createMemo(() =>
parseToolInputSummary(inputText(), props.tool.name),
);
const inputSummary = createMemo(() => parseToolInputSummary(inputText(), props.tool.name));
const hasInput = createMemo(() => inputText().trim().length > 0);
const hasOutput = createMemo(() => hasReadableToolOutput(outputText()));
const hasDetails = createMemo(() => hasInput() || hasOutput());
@@ -232,9 +287,7 @@ interface PendingToolBlockProps {
export const PendingToolBlock: Component<PendingToolBlockProps> = (props) => {
const toolLabel = createMemo(() => getToolLabel(props.tool.name));
const inputText = createMemo(() => toolValueText(props.tool.input));
const inputSummary = createMemo(() =>
parseToolInputSummary(inputText(), props.tool.name),
);
const inputSummary = createMemo(() => parseToolInputSummary(inputText(), props.tool.name));
const status = createMemo(() => props.tool.status || 'pending');
const [now, setNow] = createSignal(Date.now());
const statusLabel = createMemo(() => {
@@ -180,6 +180,49 @@ describe('ToolExecutionBlock', () => {
expect(screen.getByText('list active alerts')).toBeInTheDocument();
});
it('renders Pulse read exec input as the actual command', () => {
render(() => (
<ToolExecutionBlock
tool={makeTool({
name: 'pulse_read',
input: '{"action":"exec","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(/"target_host"/)).not.toBeInTheDocument();
});
it('renders Pulse read log input as a readable log action', () => {
render(() => (
<ToolExecutionBlock
tool={makeTool({
name: 'pulse_read',
input: '{"action":"logs","target_host":"jellyfin","source":"systemd","unit":"jellyfin"}',
output: 'log output',
})}
/>
));
expect(screen.getByText('logs jellyfin on jellyfin')).toBeInTheDocument();
});
it('renders governed command input as the command being run', () => {
render(() => (
<ToolExecutionBlock
tool={makeTool({
name: 'pulse_run_command',
input: '{"target_host":"tower","command":"systemctl restart nginx"}',
output: 'queued',
})}
/>
));
expect(screen.getByText('$ systemctl restart nginx on tower')).toBeInTheDocument();
});
// --- Output display ---
it('does not show output by default when non-empty', () => {
@@ -357,6 +400,21 @@ describe('PendingToolBlock', () => {
expect(screen.queryByText(/"type"/)).not.toBeInTheDocument();
});
it('renders pending Pulse read exec input as the command being prepared', () => {
render(() => (
<PendingToolBlock
tool={makePending({
name: 'pulse_read',
input:
'{"action":"exec","target_host":"current_resource","command":"lsblk -o NAME,SIZE"}',
})}
/>
));
expect(screen.getByText('$ lsblk -o NAME,SIZE on current resource')).toBeInTheDocument();
expect(screen.queryByText(/"command"/)).not.toBeInTheDocument();
});
// --- Activity state ---
it('renders a spinner SVG with animate-spin class while pending', () => {