mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-14 08:10:26 +00:00
Show Assistant active command status
This commit is contained in:
@@ -360,6 +360,11 @@ runtime cost control, and shared AI transport surfaces.
|
|||||||
`session.next.tool.input.*` mutations in
|
`session.next.tool.input.*` mutations in
|
||||||
`packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx`: typed tool state
|
`packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx`: typed tool state
|
||||||
owns the action row, while raw invocation syntax is inspection detail.
|
owns the action row, while raw invocation syntax is inspection detail.
|
||||||
|
The live active-turn footer is part of the same contract: pending tools must
|
||||||
|
reuse the tool-row action/command summary instead of falling back to generic
|
||||||
|
tool names such as `read` or `query`. This keeps the always-visible progress
|
||||||
|
surface aligned with OpenCode's visible `ToolPart` state rather than hiding
|
||||||
|
the concrete command until the transcript row catches up.
|
||||||
Streaming thinking rows follow the same source-anchored reasoning-display
|
Streaming thinking rows follow the same source-anchored reasoning-display
|
||||||
contract: OpenCode commit
|
contract: OpenCode commit
|
||||||
`9ed17da55ab1f7360cc0e01075f763e27fa899e9` renders reasoning through
|
`9ed17da55ab1f7360cc0e01075f763e27fa899e9` renders reasoning through
|
||||||
|
|||||||
@@ -5,402 +5,19 @@ import LoaderCircleIcon from 'lucide-solid/icons/loader-circle';
|
|||||||
import XCircleIcon from 'lucide-solid/icons/x-circle';
|
import XCircleIcon from 'lucide-solid/icons/x-circle';
|
||||||
import type { ToolExecution, PendingTool } from './types';
|
import type { ToolExecution, PendingTool } from './types';
|
||||||
import { getToolCallResultTextClass } from '@/utils/patrolRunPresentation';
|
import { getToolCallResultTextClass } from '@/utils/patrolRunPresentation';
|
||||||
import { formatIdentifierLabel } from '@/utils/textPresentation';
|
import {
|
||||||
|
getToolLabel,
|
||||||
|
isPlaceholderToolInputSummary,
|
||||||
|
parseToolInputSummary,
|
||||||
|
pendingToolActionLabel,
|
||||||
|
pendingToolActionState,
|
||||||
|
toolValueText,
|
||||||
|
} from './toolPresentation';
|
||||||
|
|
||||||
interface ToolExecutionBlockProps {
|
interface ToolExecutionBlockProps {
|
||||||
tool: ToolExecution;
|
tool: ToolExecution;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getToolLabel = (name: string) => {
|
|
||||||
if (name === 'run_command' || name === 'pulse_run_command') return 'cmd';
|
|
||||||
if (name === 'fetch_url' || name === 'pulse_fetch_url') return 'fetch';
|
|
||||||
if (name === 'get_infrastructure_state' || name === 'pulse_get_infrastructure_state')
|
|
||||||
return 'infra';
|
|
||||||
if (name === 'get_active_alerts' || name === 'pulse_get_active_alerts') return 'alerts';
|
|
||||||
if (name === 'get_metrics_history' || name === 'pulse_get_metrics_history') return 'metrics';
|
|
||||||
if (name === 'get_baselines' || name === 'pulse_get_baselines') return 'baselines';
|
|
||||||
if (name === 'get_patterns' || name === 'pulse_get_patterns') return 'patterns';
|
|
||||||
if (name === 'get_disk_health' || name === 'pulse_get_disk_health') return 'disks';
|
|
||||||
if (name === 'get_storage' || name === 'pulse_get_storage') return 'storage';
|
|
||||||
if (name === 'pulse_get_storage_config') return 'storage cfg';
|
|
||||||
if (name === 'get_resource_details' || name === 'pulse_get_resource_details') return 'resource';
|
|
||||||
if (name.includes('finding')) return 'finding';
|
|
||||||
return formatIdentifierLabel(name, { stripPrefix: 'pulse_', maxLength: 12 });
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizedToolName = (name?: string) => (name || '').trim().replace(/^pulse_/, '');
|
|
||||||
|
|
||||||
const pendingToolActionLabel = (name?: string) => {
|
|
||||||
const tool = normalizedToolName(name);
|
|
||||||
if (tool === 'run_command' || tool === 'control') return 'Writing command...';
|
|
||||||
if (tool === 'read') return 'Preparing read...';
|
|
||||||
if (tool === 'query') return 'Preparing query...';
|
|
||||||
if (tool === 'fetch_url') return 'Fetching URL...';
|
|
||||||
if (tool === 'get_infrastructure_state') return 'Reading infrastructure...';
|
|
||||||
if (tool === 'get_active_alerts' || tool === 'alerts') return 'Reading alerts...';
|
|
||||||
if (tool === 'get_metrics_history') return 'Reading metrics...';
|
|
||||||
if (tool === 'get_baselines') return 'Reading baselines...';
|
|
||||||
if (tool === 'get_patterns') return 'Reading patterns...';
|
|
||||||
if (tool === 'get_disk_health') return 'Checking disks...';
|
|
||||||
if (tool === 'get_storage' || tool === 'get_storage_config') return 'Reading storage...';
|
|
||||||
if (tool === 'get_resource_details') return 'Reading resource...';
|
|
||||||
if ((name || '').includes('finding')) return 'Preparing finding...';
|
|
||||||
return 'Preparing tool...';
|
|
||||||
};
|
|
||||||
|
|
||||||
const pendingToolActionState = (name?: string) => {
|
|
||||||
const tool = normalizedToolName(name);
|
|
||||||
if (tool === 'run_command' || tool === 'control') return 'writing';
|
|
||||||
if (tool === 'query') return 'preparing';
|
|
||||||
if (tool === 'fetch_url') return 'fetching';
|
|
||||||
if (tool === 'get_disk_health') return 'checking';
|
|
||||||
if (
|
|
||||||
tool === 'read' ||
|
|
||||||
tool === 'get_infrastructure_state' ||
|
|
||||||
tool === 'get_active_alerts' ||
|
|
||||||
tool === 'alerts' ||
|
|
||||||
tool === 'get_metrics_history' ||
|
|
||||||
tool === 'get_baselines' ||
|
|
||||||
tool === 'get_patterns' ||
|
|
||||||
tool === 'get_storage' ||
|
|
||||||
tool === 'get_storage_config' ||
|
|
||||||
tool === 'get_resource_details'
|
|
||||||
) {
|
|
||||||
return 'reading';
|
|
||||||
}
|
|
||||||
return 'preparing';
|
|
||||||
};
|
|
||||||
|
|
||||||
const isPlaceholderToolInputSummary = (summary: string) => {
|
|
||||||
const normalized = summary.trim().toLowerCase();
|
|
||||||
return !normalized || normalized === 'request' || normalized === 'receiving input';
|
|
||||||
};
|
|
||||||
|
|
||||||
const escapePartialJSONKey = (key: string) => key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
|
|
||||||
const extractPartialJSONStringField = (rawInput: string, keys: string[]) => {
|
|
||||||
const raw = rawInput.trim();
|
|
||||||
if (!raw) return '';
|
|
||||||
|
|
||||||
for (const key of keys) {
|
|
||||||
const escapedKey = escapePartialJSONKey(key);
|
|
||||||
const match = raw.match(new RegExp(`"${escapedKey}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)`));
|
|
||||||
if (match?.[1]) {
|
|
||||||
return match[1].replace(/\\"/g, '"').replace(/\\\\/g, '\\').trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const stringField = (record: Record<string, unknown>, keys: string[]) => {
|
|
||||||
for (const key of keys) {
|
|
||||||
const value = record[key];
|
|
||||||
if (typeof value === 'string' && value.trim()) {
|
|
||||||
return value.trim();
|
|
||||||
}
|
|
||||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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 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 '';
|
|
||||||
|
|
||||||
const action = extractPartialJSONStringField(rawInput, ['action', 'type']).toLowerCase();
|
|
||||||
const command = extractPartialJSONStringField(rawInput, ['command', 'cmd']);
|
|
||||||
const path = extractPartialJSONStringField(rawInput, ['path', 'file', 'file_path', 'filePath']);
|
|
||||||
const pattern = extractPartialJSONStringField(rawInput, ['pattern', 'grep', 'grep_pattern', 'grepPattern']);
|
|
||||||
const query = extractPartialJSONStringField(rawInput, ['query', 'search', 'name', 'resource_name']);
|
|
||||||
const target = extractPartialJSONStringField(rawInput, [
|
|
||||||
'target_host',
|
|
||||||
'targetHost',
|
|
||||||
'resource_id',
|
|
||||||
'resourceId',
|
|
||||||
]);
|
|
||||||
const record: Record<string, unknown> = {
|
|
||||||
...(action ? { action } : {}),
|
|
||||||
...(command ? { command } : {}),
|
|
||||||
...(path ? { path } : {}),
|
|
||||||
...(pattern ? { pattern } : {}),
|
|
||||||
...(query ? { query } : {}),
|
|
||||||
...(target ? { target_host: target } : {}),
|
|
||||||
};
|
|
||||||
const tool = normalizedToolName(toolName);
|
|
||||||
|
|
||||||
if ((tool === 'read' || tool === 'run_command' || tool === 'control') && command) {
|
|
||||||
return `$ ${inlineValue(command, 64)}${targetSuffix(record)}`;
|
|
||||||
}
|
|
||||||
if (tool === 'read') {
|
|
||||||
if (action === 'file' && path) return `read ${inlineValue(path, 36)}${targetSuffix(record)}`;
|
|
||||||
if (action === 'tail' && path) return `tail ${inlineValue(path, 36)}${targetSuffix(record)}`;
|
|
||||||
if (action === 'find' && pattern) return `find "${inlineValue(pattern, 28)}"${targetSuffix(record)}`;
|
|
||||||
if (action === 'logs') return `read logs${targetSuffix(record)}`;
|
|
||||||
}
|
|
||||||
if (tool === 'query') {
|
|
||||||
if (action === 'search' && query) return `search "${inlineValue(query, 28)}"`;
|
|
||||||
if (action === 'list') return 'list resources';
|
|
||||||
if (action) return formatIdentifierLabel(action, { maxLength: 28 });
|
|
||||||
}
|
|
||||||
if (action) return formatIdentifierLabel(action, { maxLength: 28 });
|
|
||||||
return 'receiving input';
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
|
||||||
const resourceType = stringField(record, ['resource_type', 'resourceType', 'kind', 'include']);
|
|
||||||
const listType = stringField(record, ['type', 'resource_type', 'resourceType', 'kind']);
|
|
||||||
const query = inlineValue(stringField(record, ['query', 'search', 'name', 'resource_name']));
|
|
||||||
const resourceId = inlineValue(stringField(record, ['resource_id', 'resourceId', 'id', 'vmid']));
|
|
||||||
const node = inlineValue(stringField(record, ['node', 'host']));
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case 'search':
|
|
||||||
return query ? `search "${query}"` : 'search resources';
|
|
||||||
case 'list':
|
|
||||||
return listType
|
|
||||||
? `list ${formatIdentifierLabel(listType, { maxLength: 22 })}`
|
|
||||||
: 'list resources';
|
|
||||||
case 'get':
|
|
||||||
return resourceId ? `get ${resourceId}` : 'get resource';
|
|
||||||
case 'config':
|
|
||||||
if (resourceId && node) return `config ${resourceId} on ${node}`;
|
|
||||||
return resourceId ? `config ${resourceId}` : 'resource config';
|
|
||||||
case 'topology':
|
|
||||||
return booleanField(record, 'summary_only') ? 'topology summary' : 'topology';
|
|
||||||
case 'health':
|
|
||||||
return 'health summary';
|
|
||||||
default:
|
|
||||||
if (action) return formatIdentifierLabel(action, { maxLength: 28 });
|
|
||||||
return resourceType
|
|
||||||
? `query ${formatIdentifierLabel(resourceType, { maxLength: 22 })}`
|
|
||||||
: 'query resources';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatAlertsInputSummary = (record: Record<string, unknown>) => {
|
|
||||||
const action = stringField(record, ['action']).toLowerCase();
|
|
||||||
const severity = stringField(record, ['severity', 'level']);
|
|
||||||
const resource = stringField(record, ['resource', 'resource_id', 'resourceId', 'name']);
|
|
||||||
|
|
||||||
if (action === 'list') {
|
|
||||||
return severity
|
|
||||||
? `list ${formatIdentifierLabel(severity, { maxLength: 18 })} alerts`
|
|
||||||
: 'list active alerts';
|
|
||||||
}
|
|
||||||
if (action === 'get') {
|
|
||||||
return resource ? `get alert for ${inlineValue(resource, 18)}` : 'get alert';
|
|
||||||
}
|
|
||||||
return action ? formatIdentifierLabel(action, { maxLength: 28 }) : 'read alerts';
|
|
||||||
};
|
|
||||||
|
|
||||||
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 null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(trimmed) as unknown;
|
|
||||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
||||||
return formatStructuredInputSummary(parsed as Record<string, unknown>, toolName, rawInput);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Keep trying provider-style function-call input below.
|
|
||||||
}
|
|
||||||
|
|
||||||
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) => {
|
const hasReadableToolOutput = (output: string) => {
|
||||||
const trimmed = output.trim();
|
const trimmed = output.trim();
|
||||||
return trimmed.length > 0 && !trimmed.toLowerCase().includes('not available');
|
return trimmed.length > 0 && !trimmed.toLowerCase().includes('not available');
|
||||||
@@ -438,9 +55,7 @@ const formatOutputPreview = (output: string) => {
|
|||||||
const maxLines = 4;
|
const maxLines = 4;
|
||||||
const maxLineLength = 120;
|
const maxLineLength = 120;
|
||||||
const lines = normalized.split('\n');
|
const lines = normalized.split('\n');
|
||||||
const previewLines = lines
|
const previewLines = lines.slice(0, maxLines).map((line) => trimPreviewLine(line, maxLineLength));
|
||||||
.slice(0, maxLines)
|
|
||||||
.map((line) => trimPreviewLine(line, maxLineLength));
|
|
||||||
const fullPreview = previewLines.join('\n').trim();
|
const fullPreview = previewLines.join('\n').trim();
|
||||||
if (!fullPreview) return '';
|
if (!fullPreview) return '';
|
||||||
|
|
||||||
@@ -451,16 +66,6 @@ const formatOutputPreview = (output: string) => {
|
|||||||
return linesTruncated || charsTruncated ? `${fullPreview}\n...` : fullPreview;
|
return linesTruncated || charsTruncated ? `${fullPreview}\n...` : fullPreview;
|
||||||
};
|
};
|
||||||
|
|
||||||
const toolValueText = (value: unknown) => {
|
|
||||||
if (typeof value === 'string') return value;
|
|
||||||
if (value === null || value === undefined) return '';
|
|
||||||
try {
|
|
||||||
return JSON.stringify(value);
|
|
||||||
} catch {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ToolExecutionBlock - Displays completed tool executions in a compact terminal-like style.
|
* ToolExecutionBlock - Displays completed tool executions in a compact terminal-like style.
|
||||||
*/
|
*/
|
||||||
@@ -567,14 +172,17 @@ export const PendingToolBlock: Component<PendingToolBlockProps> = (props) => {
|
|||||||
);
|
);
|
||||||
const inputSummary = createMemo(() => {
|
const inputSummary = createMemo(() => {
|
||||||
const summary = parsedInputSummary();
|
const summary = parsedInputSummary();
|
||||||
return isPlaceholderToolInputSummary(summary) ? pendingToolActionLabel(props.tool.name) : summary;
|
return isPlaceholderToolInputSummary(summary)
|
||||||
|
? pendingToolActionLabel(props.tool.name)
|
||||||
|
: summary;
|
||||||
});
|
});
|
||||||
const status = createMemo(() => props.tool.status || 'pending');
|
const status = createMemo(() => props.tool.status || 'pending');
|
||||||
const [now, setNow] = createSignal(Date.now());
|
const [now, setNow] = createSignal(Date.now());
|
||||||
const statusLabel = createMemo(() => {
|
const statusLabel = createMemo(() => {
|
||||||
if (status() === 'waiting') return 'waiting';
|
if (status() === 'waiting') return 'waiting';
|
||||||
if (status() === 'running') return 'running';
|
if (status() === 'running') return 'running';
|
||||||
if (isPlaceholderToolInputSummary(parsedInputSummary())) return pendingToolActionState(props.tool.name);
|
if (isPlaceholderToolInputSummary(parsedInputSummary()))
|
||||||
|
return pendingToolActionState(props.tool.name);
|
||||||
return 'pending';
|
return 'pending';
|
||||||
});
|
});
|
||||||
const progressText = createMemo(() => (props.tool.progress || '').trim());
|
const progressText = createMemo(() => (props.tool.progress || '').trim());
|
||||||
|
|||||||
@@ -40,6 +40,60 @@ describe('getAssistantActiveTurnStatus', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('surfaces the active pending command instead of the generic tool name', () => {
|
||||||
|
const startedAt = Date.now() - 5_000;
|
||||||
|
expect(
|
||||||
|
getAssistantActiveTurnStatus(
|
||||||
|
[
|
||||||
|
assistantMessage({
|
||||||
|
pendingTools: [
|
||||||
|
{
|
||||||
|
id: 'tool-1',
|
||||||
|
name: 'pulse_read',
|
||||||
|
input:
|
||||||
|
'{"action":"exec","target_host":"current_resource","command":"ls /dev | wc -l"}',
|
||||||
|
status: 'running',
|
||||||
|
startedAt,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
type: 'tool',
|
||||||
|
text: 'Running $ ls /dev | wc -l on current resource',
|
||||||
|
startedAt,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('surfaces provider-style pending command input in the active turn status', () => {
|
||||||
|
expect(
|
||||||
|
getAssistantActiveTurnStatus(
|
||||||
|
[
|
||||||
|
assistantMessage({
|
||||||
|
streamEvents: [
|
||||||
|
{
|
||||||
|
type: 'pending_tool',
|
||||||
|
pendingTool: {
|
||||||
|
id: 'tool-1',
|
||||||
|
name: 'pulse_read',
|
||||||
|
input: 'pulse_read(target_host="current_resource", command="ls /dev | wc -l")',
|
||||||
|
status: 'running',
|
||||||
|
},
|
||||||
|
toolId: 'tool-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
type: 'tool',
|
||||||
|
text: 'Running $ ls /dev | wc -l on current resource',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('carries workflow start time so the live footer can show elapsed wait', () => {
|
it('carries workflow start time so the live footer can show elapsed wait', () => {
|
||||||
const startedAt = Date.now() - 8_000;
|
const startedAt = Date.now() - 8_000;
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import type { ChatMessage, PendingTool, StreamDisplayEvent, WorkflowStatus } fro
|
|||||||
import { formatIdentifierLabel } from '@/utils/textPresentation';
|
import { formatIdentifierLabel } from '@/utils/textPresentation';
|
||||||
import { formatAIModelRouteLabel } from '@/utils/aiProviderPresentation';
|
import { formatAIModelRouteLabel } from '@/utils/aiProviderPresentation';
|
||||||
import { extractReasoningSummaryTitle } from './reasoningSummary';
|
import { extractReasoningSummaryTitle } from './reasoningSummary';
|
||||||
|
import {
|
||||||
|
isPlaceholderToolInputSummary,
|
||||||
|
parseToolInputSummary,
|
||||||
|
toolValueText,
|
||||||
|
} from './toolPresentation';
|
||||||
|
|
||||||
export type AssistantActiveTurnStatusKind = 'thinking' | 'tool' | 'generating';
|
export type AssistantActiveTurnStatusKind = 'thinking' | 'tool' | 'generating';
|
||||||
|
|
||||||
@@ -34,8 +39,8 @@ export const formatAssistantWorkflowStatus = (status?: WorkflowStatus): string =
|
|||||||
const toolLabel = tool ? formatToolName(tool) : '';
|
const toolLabel = tool ? formatToolName(tool) : '';
|
||||||
const toolSuffix =
|
const toolSuffix =
|
||||||
toolLabel && !message.includes(tool || '') && !messageContainsToolLabel(message, toolLabel)
|
toolLabel && !message.includes(tool || '') && !messageContainsToolLabel(message, toolLabel)
|
||||||
? ` · ${toolLabel}`
|
? ` · ${toolLabel}`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return `${message}${toolSuffix}`;
|
return `${message}${toolSuffix}`;
|
||||||
};
|
};
|
||||||
@@ -46,15 +51,20 @@ const formatPendingToolStatus = (tool?: PendingTool): string => {
|
|||||||
const progress = tool.progress?.trim();
|
const progress = tool.progress?.trim();
|
||||||
if (progress) return progress;
|
if (progress) return progress;
|
||||||
|
|
||||||
|
const inputSummary = parseToolInputSummary(toolValueText(tool.input), tool.name, tool.rawInput);
|
||||||
|
const toolActivity = isPlaceholderToolInputSummary(inputSummary) ? '' : inputSummary;
|
||||||
|
if (toolActivity) {
|
||||||
|
if (tool.status === 'waiting') return `Waiting on ${toolActivity}`;
|
||||||
|
return `Running ${toolActivity}`;
|
||||||
|
}
|
||||||
|
|
||||||
const toolLabel = formatToolName(tool.name);
|
const toolLabel = formatToolName(tool.name);
|
||||||
if (tool.status === 'waiting') return `Waiting on ${toolLabel}`;
|
if (tool.status === 'waiting') return `Waiting on ${toolLabel}`;
|
||||||
|
|
||||||
return `Running ${toolLabel}`;
|
return `Running ${toolLabel}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const activePendingToolFromEvents = (
|
const activePendingToolFromEvents = (events?: StreamDisplayEvent[]): PendingTool | undefined => {
|
||||||
events?: StreamDisplayEvent[],
|
|
||||||
): PendingTool | undefined => {
|
|
||||||
const completedToolKeys = new Set<string>();
|
const completedToolKeys = new Set<string>();
|
||||||
|
|
||||||
for (let index = (events?.length || 0) - 1; index >= 0; index -= 1) {
|
for (let index = (events?.length || 0) - 1; index >= 0; index -= 1) {
|
||||||
@@ -296,7 +306,9 @@ export const getAssistantActiveTurnStatus = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const statusCandidates: AssistantActiveTurnStatusCandidate[] = [];
|
const statusCandidates: AssistantActiveTurnStatusCandidate[] = [];
|
||||||
const statePendingToolCandidate = activePendingToolCandidateFromState(assistantMessage.pendingTools);
|
const statePendingToolCandidate = activePendingToolCandidateFromState(
|
||||||
|
assistantMessage.pendingTools,
|
||||||
|
);
|
||||||
if (statePendingToolCandidate) {
|
if (statePendingToolCandidate) {
|
||||||
statusCandidates.push(statePendingToolCandidate);
|
statusCandidates.push(statePendingToolCandidate);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,412 @@
|
|||||||
|
import { formatIdentifierLabel } from '@/utils/textPresentation';
|
||||||
|
|
||||||
|
export const getToolLabel = (name: string) => {
|
||||||
|
if (name === 'run_command' || name === 'pulse_run_command') return 'cmd';
|
||||||
|
if (name === 'fetch_url' || name === 'pulse_fetch_url') return 'fetch';
|
||||||
|
if (name === 'get_infrastructure_state' || name === 'pulse_get_infrastructure_state')
|
||||||
|
return 'infra';
|
||||||
|
if (name === 'get_active_alerts' || name === 'pulse_get_active_alerts') return 'alerts';
|
||||||
|
if (name === 'get_metrics_history' || name === 'pulse_get_metrics_history') return 'metrics';
|
||||||
|
if (name === 'get_baselines' || name === 'pulse_get_baselines') return 'baselines';
|
||||||
|
if (name === 'get_patterns' || name === 'pulse_get_patterns') return 'patterns';
|
||||||
|
if (name === 'get_disk_health' || name === 'pulse_get_disk_health') return 'disks';
|
||||||
|
if (name === 'get_storage' || name === 'pulse_get_storage') return 'storage';
|
||||||
|
if (name === 'pulse_get_storage_config') return 'storage cfg';
|
||||||
|
if (name === 'get_resource_details' || name === 'pulse_get_resource_details') return 'resource';
|
||||||
|
if (name.includes('finding')) return 'finding';
|
||||||
|
return formatIdentifierLabel(name, { stripPrefix: 'pulse_', maxLength: 12 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizedToolName = (name?: string) => (name || '').trim().replace(/^pulse_/, '');
|
||||||
|
|
||||||
|
export const pendingToolActionLabel = (name?: string) => {
|
||||||
|
const tool = normalizedToolName(name);
|
||||||
|
if (tool === 'run_command' || tool === 'control') return 'Writing command...';
|
||||||
|
if (tool === 'read') return 'Preparing read...';
|
||||||
|
if (tool === 'query') return 'Preparing query...';
|
||||||
|
if (tool === 'fetch_url') return 'Fetching URL...';
|
||||||
|
if (tool === 'get_infrastructure_state') return 'Reading infrastructure...';
|
||||||
|
if (tool === 'get_active_alerts' || tool === 'alerts') return 'Reading alerts...';
|
||||||
|
if (tool === 'get_metrics_history') return 'Reading metrics...';
|
||||||
|
if (tool === 'get_baselines') return 'Reading baselines...';
|
||||||
|
if (tool === 'get_patterns') return 'Reading patterns...';
|
||||||
|
if (tool === 'get_disk_health') return 'Checking disks...';
|
||||||
|
if (tool === 'get_storage' || tool === 'get_storage_config') return 'Reading storage...';
|
||||||
|
if (tool === 'get_resource_details') return 'Reading resource...';
|
||||||
|
if ((name || '').includes('finding')) return 'Preparing finding...';
|
||||||
|
return 'Preparing tool...';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pendingToolActionState = (name?: string) => {
|
||||||
|
const tool = normalizedToolName(name);
|
||||||
|
if (tool === 'run_command' || tool === 'control') return 'writing';
|
||||||
|
if (tool === 'query') return 'preparing';
|
||||||
|
if (tool === 'fetch_url') return 'fetching';
|
||||||
|
if (tool === 'get_disk_health') return 'checking';
|
||||||
|
if (
|
||||||
|
tool === 'read' ||
|
||||||
|
tool === 'get_infrastructure_state' ||
|
||||||
|
tool === 'get_active_alerts' ||
|
||||||
|
tool === 'alerts' ||
|
||||||
|
tool === 'get_metrics_history' ||
|
||||||
|
tool === 'get_baselines' ||
|
||||||
|
tool === 'get_patterns' ||
|
||||||
|
tool === 'get_storage' ||
|
||||||
|
tool === 'get_storage_config' ||
|
||||||
|
tool === 'get_resource_details'
|
||||||
|
) {
|
||||||
|
return 'reading';
|
||||||
|
}
|
||||||
|
return 'preparing';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isPlaceholderToolInputSummary = (summary: string) => {
|
||||||
|
const normalized = summary.trim().toLowerCase();
|
||||||
|
return !normalized || normalized === 'request' || normalized === 'receiving input';
|
||||||
|
};
|
||||||
|
|
||||||
|
const escapePartialJSONKey = (key: string) => key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
|
||||||
|
const extractPartialJSONStringField = (rawInput: string, keys: string[]) => {
|
||||||
|
const raw = rawInput.trim();
|
||||||
|
if (!raw) return '';
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const escapedKey = escapePartialJSONKey(key);
|
||||||
|
const match = raw.match(new RegExp(`"${escapedKey}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)`));
|
||||||
|
if (match?.[1]) {
|
||||||
|
return match[1].replace(/\\"/g, '"').replace(/\\\\/g, '\\').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const stringField = (record: Record<string, unknown>, keys: string[]) => {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = record[key];
|
||||||
|
if (typeof value === 'string' && value.trim()) {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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 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 '';
|
||||||
|
|
||||||
|
const action = extractPartialJSONStringField(rawInput, ['action', 'type']).toLowerCase();
|
||||||
|
const command = extractPartialJSONStringField(rawInput, ['command', 'cmd']);
|
||||||
|
const path = extractPartialJSONStringField(rawInput, ['path', 'file', 'file_path', 'filePath']);
|
||||||
|
const pattern = extractPartialJSONStringField(rawInput, [
|
||||||
|
'pattern',
|
||||||
|
'grep',
|
||||||
|
'grep_pattern',
|
||||||
|
'grepPattern',
|
||||||
|
]);
|
||||||
|
const query = extractPartialJSONStringField(rawInput, [
|
||||||
|
'query',
|
||||||
|
'search',
|
||||||
|
'name',
|
||||||
|
'resource_name',
|
||||||
|
]);
|
||||||
|
const target = extractPartialJSONStringField(rawInput, [
|
||||||
|
'target_host',
|
||||||
|
'targetHost',
|
||||||
|
'resource_id',
|
||||||
|
'resourceId',
|
||||||
|
]);
|
||||||
|
const record: Record<string, unknown> = {
|
||||||
|
...(action ? { action } : {}),
|
||||||
|
...(command ? { command } : {}),
|
||||||
|
...(path ? { path } : {}),
|
||||||
|
...(pattern ? { pattern } : {}),
|
||||||
|
...(query ? { query } : {}),
|
||||||
|
...(target ? { target_host: target } : {}),
|
||||||
|
};
|
||||||
|
const tool = normalizedToolName(toolName);
|
||||||
|
|
||||||
|
if ((tool === 'read' || tool === 'run_command' || tool === 'control') && command) {
|
||||||
|
return `$ ${inlineValue(command, 64)}${targetSuffix(record)}`;
|
||||||
|
}
|
||||||
|
if (tool === 'read') {
|
||||||
|
if (action === 'file' && path) return `read ${inlineValue(path, 36)}${targetSuffix(record)}`;
|
||||||
|
if (action === 'tail' && path) return `tail ${inlineValue(path, 36)}${targetSuffix(record)}`;
|
||||||
|
if (action === 'find' && pattern)
|
||||||
|
return `find "${inlineValue(pattern, 28)}"${targetSuffix(record)}`;
|
||||||
|
if (action === 'logs') return `read logs${targetSuffix(record)}`;
|
||||||
|
}
|
||||||
|
if (tool === 'query') {
|
||||||
|
if (action === 'search' && query) return `search "${inlineValue(query, 28)}"`;
|
||||||
|
if (action === 'list') return 'list resources';
|
||||||
|
if (action) return formatIdentifierLabel(action, { maxLength: 28 });
|
||||||
|
}
|
||||||
|
if (action) return formatIdentifierLabel(action, { maxLength: 28 });
|
||||||
|
return 'receiving input';
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
const resourceType = stringField(record, ['resource_type', 'resourceType', 'kind', 'include']);
|
||||||
|
const listType = stringField(record, ['type', 'resource_type', 'resourceType', 'kind']);
|
||||||
|
const query = inlineValue(stringField(record, ['query', 'search', 'name', 'resource_name']));
|
||||||
|
const resourceId = inlineValue(stringField(record, ['resource_id', 'resourceId', 'id', 'vmid']));
|
||||||
|
const node = inlineValue(stringField(record, ['node', 'host']));
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'search':
|
||||||
|
return query ? `search "${query}"` : 'search resources';
|
||||||
|
case 'list':
|
||||||
|
return listType
|
||||||
|
? `list ${formatIdentifierLabel(listType, { maxLength: 22 })}`
|
||||||
|
: 'list resources';
|
||||||
|
case 'get':
|
||||||
|
return resourceId ? `get ${resourceId}` : 'get resource';
|
||||||
|
case 'config':
|
||||||
|
if (resourceId && node) return `config ${resourceId} on ${node}`;
|
||||||
|
return resourceId ? `config ${resourceId}` : 'resource config';
|
||||||
|
case 'topology':
|
||||||
|
return booleanField(record, 'summary_only') ? 'topology summary' : 'topology';
|
||||||
|
case 'health':
|
||||||
|
return 'health summary';
|
||||||
|
default:
|
||||||
|
if (action) return formatIdentifierLabel(action, { maxLength: 28 });
|
||||||
|
return resourceType
|
||||||
|
? `query ${formatIdentifierLabel(resourceType, { maxLength: 22 })}`
|
||||||
|
: 'query resources';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatAlertsInputSummary = (record: Record<string, unknown>) => {
|
||||||
|
const action = stringField(record, ['action']).toLowerCase();
|
||||||
|
const severity = stringField(record, ['severity', 'level']);
|
||||||
|
const resource = stringField(record, ['resource', 'resource_id', 'resourceId', 'name']);
|
||||||
|
|
||||||
|
if (action === 'list') {
|
||||||
|
return severity
|
||||||
|
? `list ${formatIdentifierLabel(severity, { maxLength: 18 })} alerts`
|
||||||
|
: 'list active alerts';
|
||||||
|
}
|
||||||
|
if (action === 'get') {
|
||||||
|
return resource ? `get alert for ${inlineValue(resource, 18)}` : 'get alert';
|
||||||
|
}
|
||||||
|
return action ? formatIdentifierLabel(action, { maxLength: 28 }) : 'read alerts';
|
||||||
|
};
|
||||||
|
|
||||||
|
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 null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed) as unknown;
|
||||||
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
|
return formatStructuredInputSummary(parsed as Record<string, unknown>, toolName, rawInput);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Keep trying provider-style function-call input below.
|
||||||
|
}
|
||||||
|
|
||||||
|
const functionCall = parseFunctionStyleToolInput(trimmed);
|
||||||
|
if (functionCall) {
|
||||||
|
return formatStructuredInputSummary(functionCall.args, functionCall.name, rawInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export 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 });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toolValueText = (value: unknown) => {
|
||||||
|
if (typeof value === 'string') return value;
|
||||||
|
if (value === null || value === undefined) return '';
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user