diff --git a/frontend/src/app/pages/AgentChat/AgentChat.tsx b/frontend/src/app/pages/AgentChat/AgentChat.tsx index 079ad6f3..5b17583a 100644 --- a/frontend/src/app/pages/AgentChat/AgentChat.tsx +++ b/frontend/src/app/pages/AgentChat/AgentChat.tsx @@ -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 = { 'opus-4-8': 1_000_000, @@ -1285,7 +1286,9 @@ const AgentChat: React.FC = ({ 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 = ({ 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 diff --git a/frontend/src/app/pages/AgentChat/parsing/toolResultParsing.ts b/frontend/src/app/pages/AgentChat/parsing/toolResultParsing.ts index 8413c5cf..02d53675 100644 --- a/frontend/src/app/pages/AgentChat/parsing/toolResultParsing.ts +++ b/frontend/src/app/pages/AgentChat/parsing/toolResultParsing.ts @@ -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 || ''; diff --git a/frontend/src/app/pages/AgentChat/shell/ApprovalBar.tsx b/frontend/src/app/pages/AgentChat/shell/ApprovalBar.tsx index 09daf483..911cb8ea 100644 --- a/frontend/src/app/pages/AgentChat/shell/ApprovalBar.tsx +++ b/frontend/src/app/pages/AgentChat/shell/ApprovalBar.tsx @@ -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 { 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 = ({ 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 = ({ request, onApprove, onDeny }) => - {parsed.displayName} + {displayName} = ({ 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 = ({ request, onApprove, onDeny }) => - {JSON.stringify(request.tool_input, null, 2)} + {detailText} diff --git a/frontend/src/app/pages/AgentChat/tool-bubbles/CompactMcpBubble.tsx b/frontend/src/app/pages/AgentChat/tool-bubbles/CompactMcpBubble.tsx index fb1c6bb5..700fd138 100644 --- a/frontend/src/app/pages/AgentChat/tool-bubbles/CompactMcpBubble.tsx +++ b/frontend/src/app/pages/AgentChat/tool-bubbles/CompactMcpBubble.tsx @@ -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 = ({ 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 ? : null; @@ -60,16 +65,16 @@ export const CompactMcpBubble: React.FC = ({ return ( {ServiceIcon} @@ -83,22 +88,22 @@ export const CompactMcpBubble: React.FC = ({ > {serviceLabel} - {resultSummary && !isError && ( + {visibleSummary && !isError && ( - {resultSummary} + {visibleSummary} )} - {!resultSummary && !showTimer && } + {!visibleSummary && !showTimer && } {showTimer && ( <> @@ -123,12 +128,14 @@ export const CompactMcpBubble: React.FC = ({ )} )} + {canToggleDetails && ( {showBody ? : } + )} - + = ({ // 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 ( = ({ } as any} > {mcpInfo.isMcp && mcpInfo.service @@ -186,7 +187,7 @@ export const DefaultToolBubble: React.FC = ({ )} {showTimer && } - {!isStreaming && ( + {canToggleDetails && ( {showBody ? ( @@ -197,7 +198,7 @@ export const DefaultToolBubble: React.FC = ({ )} - + = 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 ( = React.memo(({ group, isSessionRunning = }} > 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 = React.memo(({ group, isSessionRunning = {completedCount}/{group.callCount} )} + {canToggleGroup && ( {expanded ? : } + )} diff --git a/frontend/src/shared/mcpToolMeta.ts b/frontend/src/shared/mcpToolMeta.ts index 0bb911d8..89b6f122 100644 --- a/frontend/src/shared/mcpToolMeta.ts +++ b/frontend/src/shared/mcpToolMeta.ts @@ -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) {