[aidan] ui: tool calling desc/naming

This commit is contained in:
abccodes
2026-06-17 18:41:11 -07:00
parent 4c253e182e
commit 233c1648c1
7 changed files with 213 additions and 33 deletions
@@ -63,6 +63,7 @@ import { ContextPath } from '@/app/components/editor/DirectoryBrowser';
import { setGlowingBrowserCards, fadeGlowingBrowserCards, clearGlowingBrowserCards, removeCard } from '@/shared/state/dashboardLayoutSlice';
import { setCardSidecar, commitDraft, updateWorkflowCard } from '@/shared/state/workflowsSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { parseMcpToolName, getMcpInputSummary } from '@/shared/mcpToolMeta';
const CONTEXT_WINDOWS: Record<string, number> = {
'opus-4-8': 1_000_000,
@@ -1285,7 +1286,9 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
const c = p.call.content;
const tool = typeof c === 'object' ? c.tool || '' : '';
const input = typeof c === 'object' ? c.input : '';
const summary = typeof input === 'string' ? input.slice(0, 120) : JSON.stringify(input).slice(0, 120);
const mcp = parseMcpToolName(tool);
const friendly = mcp.isMcp ? getMcpInputSummary(input, mcp.action, mcp.serverSlug) : '';
const summary = friendly || (typeof input === 'string' ? input.slice(0, 120) : JSON.stringify(input).slice(0, 120));
return { tool, input_summary: summary };
});
dispatch(generateGroupMeta({ sessionId: id, groupId: group.id, toolCalls }));
@@ -1297,7 +1300,9 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
const c = p.call.content;
const tool = typeof c === 'object' ? c.tool || '' : '';
const input = typeof c === 'object' ? c.input : '';
const summary = typeof input === 'string' ? input.slice(0, 120) : JSON.stringify(input).slice(0, 120);
const mcp = parseMcpToolName(tool);
const friendly = mcp.isMcp ? getMcpInputSummary(input, mcp.action, mcp.serverSlug) : '';
const summary = friendly || (typeof input === 'string' ? input.slice(0, 120) : JSON.stringify(input).slice(0, 120));
return { tool, input_summary: summary };
});
const resultsSummary = group.pairs
@@ -1,6 +1,6 @@
import { AgentMessage } from '@/shared/state/agentsSlice';
import { prettyPath, prettyUrl, quoteQuery, bashCommandDetail } from './toolLabels';
import { parseMcpToolName, getMcpInputSummary, getGmailHeader } from '@/shared/mcpToolMeta';
import { parseMcpToolName, getMcpInputSummary, getGmailHeader, getWorkflowToolInputDisplay } from '@/shared/mcpToolMeta';
export function getToolData(call: AgentMessage) {
const content = typeof call.content === 'object' ? call.content : {};
@@ -19,7 +19,7 @@ export function isBashTool(name: string) {
export function getInputSummary(toolName: string, input: any): string {
try {
const mcp = parseMcpToolName(toolName);
if (mcp.isMcp) return getMcpInputSummary(input);
if (mcp.isMcp) return getMcpInputSummary(input, mcp.action, mcp.serverSlug);
const n = toolName.toLowerCase();
if (isBashTool(toolName)) {
@@ -61,7 +61,10 @@ function formatMcpInputDisplay(input: any): string {
export function formatInputDisplay(toolName: string, input: any): string {
try {
const mcp = parseMcpToolName(toolName);
if (mcp.isMcp) return formatMcpInputDisplay(input);
if (mcp.isMcp) {
const workflowDisplay = getWorkflowToolInputDisplay(input, mcp.action, mcp.serverSlug);
return workflowDisplay || formatMcpInputDisplay(input);
}
const n = toolName.toLowerCase();
if (isBashTool(toolName)) return input.command || '';
@@ -25,6 +25,7 @@ import { ApprovalRequest } from '@/shared/state/agentsSlice';
import { useAppSelector } from '@/shared/hooks';
import { ToolDefinition } from '@/shared/state/toolsSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { getMcpInputSummary as getSharedMcpInputSummary, getWorkflowToolInputDisplay, getWorkflowToolLabel } from '@/shared/mcpToolMeta';
interface IntegrationMeta {
label: string;
@@ -96,6 +97,9 @@ export function useMcpToolMeta(parsed: ParsedTool): McpToolMeta {
);
if (!toolDef) {
if (parsed.serverSlug === 'openswarm-schedule') {
return { integration: null, description: '', serverLabel: 'Workflows' };
}
return { integration: null, description: '', serverLabel: parsed.serverSlug };
}
@@ -110,6 +114,10 @@ export function useMcpToolMeta(parsed: ParsedTool): McpToolMeta {
function getMcpInputSummary(actionName: string, toolInput: Record<string, any>): string {
const lower = actionName.toLowerCase();
if (getWorkflowToolLabel(actionName)) {
return getSharedMcpInputSummary(toolInput, actionName, 'openswarm-schedule');
}
if (lower.includes('gmail') || lower.includes('email') || lower.includes('mail')) {
const query = toolInput.query || toolInput.search_query || toolInput.q || '';
const to = toolInput.to || toolInput.recipient || '';
@@ -569,7 +577,12 @@ const GenericApprovalBar: React.FC<Props> = ({ request, onApprove, onDeny }) =>
const meta = useMcpToolMeta(parsed);
const accentColor = meta.integration?.color || c.status.warning;
const displayName = getWorkflowToolLabel(parsed.actionName) || parsed.displayName;
const summary = parsed.isMcp ? getMcpInputSummary(parsed.actionName, request.tool_input) : '';
const workflowDetails = parsed.isMcp
? getWorkflowToolInputDisplay(request.tool_input, parsed.actionName, parsed.serverSlug)
: '';
const detailText = workflowDetails || JSON.stringify(request.tool_input, null, 2);
const isSensitive = !!request.sensitive_pattern;
if (!parsed.isMcp) {
@@ -744,7 +757,7 @@ const GenericApprovalBar: React.FC<Props> = ({ request, onApprove, onDeny }) =>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography sx={{ color: c.text.primary, fontWeight: 600, fontSize: '0.9rem' }}>
{parsed.displayName}
{displayName}
</Typography>
<Chip
label={meta.serverLabel || parsed.serverSlug}
@@ -794,7 +807,6 @@ const GenericApprovalBar: React.FC<Props> = ({ request, onApprove, onDeny }) =>
sx={{
color: c.text.secondary,
fontSize: '0.82rem',
fontFamily: c.font.mono,
flex: 1,
minWidth: 0,
overflow: 'hidden',
@@ -812,7 +824,7 @@ const GenericApprovalBar: React.FC<Props> = ({ request, onApprove, onDeny }) =>
<Collapse in={detailsExpanded || !summary}>
<Box sx={{ mt: summary ? 0.75 : 0 }}>
<CodeBlock tokens={c}>
{JSON.stringify(request.tool_input, null, 2)}
{detailText}
</CodeBlock>
</Box>
</Collapse>
@@ -15,7 +15,7 @@ import { GoogleServiceIcon } from '../mcp-cards/GoogleServiceIcon';
import { ElapsedTimer, formatElapsed } from '../parsing/toolBubbleChrome';
import { useTermColors } from '../parsing/toolColorize';
import { ParsedResult } from '../parsing/toolResultParsing';
import { McpToolInfo, getMcpShortAction } from '@/shared/mcpToolMeta';
import { McpToolInfo, getMcpShortAction, getMcpInputSummary, getWorkflowToolLabel } from '@/shared/mcpToolMeta';
import { McpResultCard } from '../mcp-cards/McpResultCard';
interface CompactMcpBubbleProps {
@@ -47,12 +47,17 @@ export const CompactMcpBubble: React.FC<CompactMcpBubbleProps> = ({
const c = useClaudeTokens();
const tc = useTermColors();
const shortAction = mcpInfo.isMcp ? getMcpShortAction(mcpInfo) : toolName;
const workflowLabel = mcpInfo.isMcp ? getWorkflowToolLabel(mcpInfo.action) : null;
const shortAction = workflowLabel || (mcpInfo.isMcp ? getMcpShortAction(mcpInfo) : toolName);
const mcpVerbLabel = (() => {
if (workflowLabel) return workflowLabel;
const lbl = getToolLabel(toolName, call.id);
return result && !isDenied ? lbl.past : lbl.present;
})();
const serviceLabel = mcpInfo.isMcp ? mcpVerbLabel : shortAction;
const inputSummary = mcpInfo.isMcp ? getMcpInputSummary(input, mcpInfo.action, mcpInfo.serverSlug) : '';
const visibleSummary = resultSummary || inputSummary;
const canToggleDetails = !!visibleSummary;
const ServiceIcon = mcpInfo.isMcp && mcpInfo.service
? <GoogleServiceIcon service={mcpInfo.service} size={14} />
: null;
@@ -60,16 +65,16 @@ export const CompactMcpBubble: React.FC<CompactMcpBubbleProps> = ({
return (
<Box {...selectAttrs} sx={{ my: 0 }}>
<Box
onClick={toggle}
onClick={canToggleDetails ? toggle : undefined}
sx={{
display: 'flex',
alignItems: showBody ? 'flex-start' : 'center',
alignItems: showBody && canToggleDetails ? 'flex-start' : 'center',
gap: 0.75,
px: 1.5,
py: 0.6,
cursor: 'pointer',
borderBottom: showBody ? `1px solid ${c.border.subtle}` : 'none',
'&:hover': { bgcolor: 'rgba(0,0,0,0.02)' },
cursor: canToggleDetails ? 'pointer' : 'default',
borderBottom: showBody && canToggleDetails ? `1px solid ${c.border.subtle}` : 'none',
'&:hover': canToggleDetails ? { bgcolor: 'rgba(0,0,0,0.02)' } : undefined,
}}
>
{ServiceIcon}
@@ -83,22 +88,22 @@ export const CompactMcpBubble: React.FC<CompactMcpBubbleProps> = ({
>
{serviceLabel}
</Typography>
{resultSummary && !isError && (
{visibleSummary && !isError && (
<Typography
sx={{
color: c.text.secondary,
fontSize: '0.74rem',
flex: 1,
minWidth: 0,
...(showBody
...(showBody && canToggleDetails
? { whiteSpace: 'normal', wordBreak: 'break-word' }
: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }),
}}
>
{resultSummary}
{visibleSummary}
</Typography>
)}
{!resultSummary && !showTimer && <Box sx={{ flex: 1 }} />}
{!visibleSummary && !showTimer && <Box sx={{ flex: 1 }} />}
{showTimer && (
<>
<Box sx={{ flex: 1 }} />
@@ -123,12 +128,14 @@ export const CompactMcpBubble: React.FC<CompactMcpBubbleProps> = ({
)}
</Box>
)}
{canToggleDetails && (
<IconButton size="small" sx={{ color: c.text.tertiary, p: 0.15, flexShrink: 0 }}>
{showBody ? <ExpandLessIcon sx={{ fontSize: 16 }} /> : <ExpandMoreIcon sx={{ fontSize: 16 }} />}
</IconButton>
)}
</Box>
<Collapse in={showBody}>
<Collapse in={showBody && canToggleDetails}>
<Box
sx={{
bgcolor: tc.TERM_BG,
@@ -61,6 +61,7 @@ export const DefaultToolBubble: React.FC<DefaultToolBubbleProps> = ({
// already on screen. mcpCompact rows opt out (the group's row-fade handles them).
const reveal = useMountReveal();
const enterStyle = (!mcpCompact && !suppressReveal) ? reveal : {};
const canToggleDetails = !!inputSummary && !isStreaming;
return (
<Box
@@ -89,16 +90,16 @@ export const DefaultToolBubble: React.FC<DefaultToolBubbleProps> = ({
} as any}
>
<Box
onClick={toggle}
onClick={canToggleDetails ? toggle : undefined}
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.75,
px: 1.5,
py: mcpCompact ? 0.6 : 0.75,
cursor: isStreaming ? 'default' : 'pointer',
borderBottom: mcpCompact && showBody ? `1px solid ${c.border.subtle}` : 'none',
'&:hover': isStreaming ? {} : { bgcolor: 'rgba(0,0,0,0.02)' },
cursor: canToggleDetails ? 'pointer' : 'default',
borderBottom: mcpCompact && showBody && canToggleDetails ? `1px solid ${c.border.subtle}` : 'none',
'&:hover': canToggleDetails ? { bgcolor: 'rgba(0,0,0,0.02)' } : {},
}}
>
{mcpInfo.isMcp && mcpInfo.service
@@ -186,7 +187,7 @@ export const DefaultToolBubble: React.FC<DefaultToolBubbleProps> = ({
)}
{showTimer && <ElapsedTimer startTime={call.timestamp} />}
{!isStreaming && (
{canToggleDetails && (
<IconButton size="small" sx={{ color: c.text.tertiary, p: mcpCompact ? 0.15 : 0.25, flexShrink: 0 }}>
{showBody ? (
<ExpandLessIcon sx={{ fontSize: mcpCompact ? 16 : 18 }} />
@@ -197,7 +198,7 @@ export const DefaultToolBubble: React.FC<DefaultToolBubbleProps> = ({
)}
</Box>
<Collapse in={showBody}>
<Collapse in={showBody && canToggleDetails}>
<Box
sx={{
bgcolor: tc.TERM_BG,
@@ -10,6 +10,7 @@ import { AgentMessage, ToolGroupMeta } from '@/shared/state/agentsSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { useMountReveal } from './useMountReveal';
import { sanitizeSvgString } from '@/shared/sanitizeSvg';
import { parseMcpToolName, getWorkflowToolLabel } from '@/shared/mcpToolMeta';
import ToolCallBubble, { ToolPair } from './ToolCallBubble';
export interface ToolGroup {
@@ -83,13 +84,21 @@ const ToolGroupBubble: React.FC<Props> = React.memo(({ group, isSessionRunning =
).length;
const allDone = pendingCount === 0 || !isSessionRunning;
const displayName = meta?.name || group.label;
const hasSvg = !!meta?.svg;
const toolNames = group.pairs.map((p) => {
const c2 = typeof p.call.content === 'object' ? p.call.content : {};
return c2.tool || 'unknown';
});
const workflowGroupLabel = (() => {
if (group.mcpServer !== 'openswarm-schedule') return null;
const parsedLabels = Array.from(new Set(toolNames.map((name) => {
const parsed = parseMcpToolName(name);
return parsed.isMcp ? getWorkflowToolLabel(parsed.action) : null;
}).filter(Boolean))) as string[];
return parsedLabels.length === 1 ? parsedLabels[0] : 'Workflow actions';
})();
const displayName = workflowGroupLabel || meta?.name || group.label;
const hasSvg = !!meta?.svg && !workflowGroupLabel;
const canToggleGroup = group.pairs.length > 1;
return (
<Box
@@ -116,15 +125,15 @@ const ToolGroupBubble: React.FC<Props> = React.memo(({ group, isSessionRunning =
}}
>
<Box
onClick={() => setExpanded(!expanded)}
onClick={canToggleGroup ? () => setExpanded(!expanded) : undefined}
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.75,
px: 1.5,
py: 0.7,
cursor: 'pointer',
'&:hover': { bgcolor: 'rgba(0,0,0,0.02)' },
cursor: canToggleGroup ? 'pointer' : 'default',
'&:hover': canToggleGroup ? { bgcolor: 'rgba(0,0,0,0.02)' } : undefined,
}}
>
{!meta ? (
@@ -171,9 +180,11 @@ const ToolGroupBubble: React.FC<Props> = React.memo(({ group, isSessionRunning =
{completedCount}/{group.callCount}
</Typography>
)}
{canToggleGroup && (
<IconButton size="small" sx={{ color: c.text.tertiary, p: 0.15 }}>
{expanded ? <ExpandLessIcon sx={{ fontSize: 16 }} /> : <ExpandMoreIcon sx={{ fontSize: 16 }} />}
</IconButton>
)}
</Box>
<Collapse in={expanded}>
+142 -1
View File
@@ -26,8 +26,149 @@ export function parseMcpToolName(rawName: string): McpToolInfo {
return { isMcp: true, serverSlug, action, service, displayName: display };
}
export function getMcpInputSummary(input: any): string {
function formatTime(hour: unknown, minute: unknown): string {
const h = typeof hour === 'number' ? hour : Number(hour);
const m = typeof minute === 'number' ? minute : Number(minute || 0);
if (!Number.isFinite(h)) return '';
const h12 = ((h + 11) % 12) + 1;
const suffix = h < 12 ? 'am' : 'pm';
return Number.isFinite(m) && m > 0 ? `${h12}:${String(m).padStart(2, '0')}${suffix}` : `${h12}${suffix}`;
}
function weekdayLabel(day: number): string {
return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][day] || '';
}
function compactWorkflowSchedule(input: any): string {
if (!input || typeof input !== 'object') return '';
const enabled = input.schedule_enabled ?? input.enabled;
if (enabled === false) return 'Turn schedule off';
const unit = input.repeat_unit || input.unit;
const every = Math.max(1, Number(input.repeat_every || 1));
const time = formatTime(input.hour, input.minute);
const days = Array.isArray(input.on_days) ? input.on_days.filter((d: any) => Number.isInteger(d) && d >= 0 && d <= 6) : [];
if (unit === 'minute') return `Run every ${Math.max(15, Number(input.repeat_every || 15))} minutes`;
if (unit === 'hour') {
const minute = Number(input.minute || 0);
const suffix = minute > 0 ? ` at :${String(minute).padStart(2, '0')}` : '';
return every === 1 ? `Run every hour${suffix}` : `Run every ${every} hours${suffix}`;
}
if (unit === 'day') return `Run ${every === 1 ? 'daily' : `every ${every} days`}${time ? ` at ${time}` : ''}`;
if (unit === 'month') return `Run ${every === 1 ? 'monthly' : `every ${every} months`}${time ? ` at ${time}` : ''}`;
if (unit === 'week') {
const dayText = days.length === 1
? weekdayLabel(days[0])
: days.length === 5 && [1, 2, 3, 4, 5].every((d) => days.includes(d))
? 'weekdays'
: days.length > 1
? days.map(weekdayLabel).filter(Boolean).join(', ')
: '';
if (!dayText) return time ? `Choose weekly days at ${time}` : 'Choose weekly days';
return `Run ${every === 1 ? `every ${dayText}` : `every ${every} weeks on ${dayText}`}${time ? ` at ${time}` : ''}`;
}
if (input.title) return `Update "${input.title}"`;
return '';
}
function firstText(input: any, keys: string[]): string {
for (const key of keys) {
const value = input?.[key];
if (typeof value === 'string' && value.trim()) return value.trim();
}
return '';
}
function stepNumber(input: any): string {
const raw = input?.step_idx ?? input?.step_index ?? input?.index;
const idx = typeof raw === 'number' ? raw : Number(raw);
return Number.isInteger(idx) && idx >= 0 ? String(idx + 1) : '';
}
function compactWorkflowAction(input: any, action: string): string {
const lower = action.toLowerCase();
const step = stepNumber(input);
const text = firstText(input, ['new_text', 'text', 'prompt', 'description']);
const label = firstText(input, ['new_label', 'label', 'title', 'name']);
const preview = text || label;
const shortPreview = preview.length > 80 ? preview.slice(0, 77) + '...' : preview;
if (lower === 'addworkflowstep') return shortPreview ? `Add step: ${shortPreview}` : 'Add a workflow step';
if (lower === 'editworkflowstep') return step ? `Edit step ${step}` : 'Edit workflow step';
if (lower === 'deleteworkflowstep') return step ? `Delete step ${step}` : 'Delete workflow step';
if (lower === 'runworkflow') return 'Run this workflow now';
if (lower === 'testworkflow') return 'Test this workflow';
if (lower === 'readtesttranscript') return 'Read latest test results';
if (lower === 'deletescheduledworkflow') return 'Delete this workflow';
if (lower === 'pauseallworkflows') return 'Pause all scheduled workflows';
if (lower === 'resumeallworkflows') return 'Resume scheduled workflows';
if (lower === 'listworkflows') return 'Show workflows';
return '';
}
export function getWorkflowToolLabel(action: string): string | null {
const lower = action.toLowerCase();
if (lower === 'scheduleworkflow') return 'Schedule workflow';
if (lower === 'updatescheduledworkflow') return 'Update schedule';
if (lower === 'deletescheduledworkflow') return 'Delete workflow';
if (lower === 'pauseallworkflows') return 'Pause workflows';
if (lower === 'resumeallworkflows') return 'Resume workflows';
if (lower === 'runworkflow') return 'Run workflow';
if (lower === 'editworkflowstep') return 'Edit workflow step';
if (lower === 'addworkflowstep') return 'Add workflow step';
if (lower === 'deleteworkflowstep') return 'Delete workflow step';
if (lower === 'testworkflow') return 'Test workflow';
if (lower === 'readtesttranscript') return 'Read test results';
if (lower === 'listworkflows') return 'List workflows';
return null;
}
export function getWorkflowToolInputDisplay(input: any, action?: string, serverSlug?: string): string {
if (!input || typeof input !== 'object') return '';
const isWorkflowTool = serverSlug === 'openswarm-schedule' || (action && getWorkflowToolLabel(action));
if (!isWorkflowTool || !action) return '';
const lower = action.toLowerCase();
const lines: string[] = [];
const schedule = compactWorkflowSchedule(input);
const step = stepNumber(input);
const text = firstText(input, ['new_text', 'text', 'prompt', 'description']);
const label = firstText(input, ['new_label', 'label', 'title', 'name']);
if (lower === 'scheduleworkflow' || lower === 'updatescheduledworkflow') {
if (schedule) lines.push(`Schedule: ${schedule}`);
if (label) lines.push(`Name: ${label}`);
return lines.join('\n') || getWorkflowToolLabel(action) || 'Workflow action';
}
if (lower === 'addworkflowstep') {
if (text) lines.push(`Step: ${text}`);
if (label && label !== text) lines.push(`Label: ${label}`);
return lines.join('\n') || compactWorkflowAction(input, action);
}
if (lower === 'editworkflowstep') {
if (step) lines.push(`Step: ${step}`);
if (text) lines.push(`Prompt: ${text}`);
if (label && label !== text) lines.push(`Label: ${label}`);
return lines.join('\n') || compactWorkflowAction(input, action);
}
return compactWorkflowAction(input, action) || getWorkflowToolLabel(action) || 'Workflow action';
}
export function getMcpInputSummary(input: any, action?: string, serverSlug?: string): string {
if (!input || typeof input !== 'object') return '';
if (serverSlug === 'openswarm-schedule' || (action && getWorkflowToolLabel(action))) {
const workflowSummary = compactWorkflowSchedule(input);
if (workflowSummary) return workflowSummary;
if (action) {
const actionSummary = compactWorkflowAction(input, action);
if (actionSummary) return actionSummary;
}
return '';
}
const keys = Object.keys(input);
if (keys.length === 0) return '';
if (keys.length === 1) {