mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-21 10:43:36 +00:00
Render Assistant tool commands in compact rows
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user