From 374df87ee13b3e651c031b44b7ddb7de9f731ff2 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sat, 23 May 2026 05:20:58 -0700 Subject: [PATCH] [eric] split: extract invoke/create/compact/default tool bubbles + shell dispatch --- .../app/pages/AgentChat/AgentResponseBody.tsx | 71 + .../app/pages/AgentChat/CompactMcpBubble.tsx | 168 ++ .../app/pages/AgentChat/CreateAgentBubble.tsx | 176 ++ .../app/pages/AgentChat/DefaultToolBubble.tsx | 299 +++ .../app/pages/AgentChat/InvokeAgentBubble.tsx | 181 ++ .../app/pages/AgentChat/ToolCallBubble.tsx | 2099 +---------------- 6 files changed, 1000 insertions(+), 1994 deletions(-) create mode 100644 frontend/src/app/pages/AgentChat/AgentResponseBody.tsx create mode 100644 frontend/src/app/pages/AgentChat/CompactMcpBubble.tsx create mode 100644 frontend/src/app/pages/AgentChat/CreateAgentBubble.tsx create mode 100644 frontend/src/app/pages/AgentChat/DefaultToolBubble.tsx create mode 100644 frontend/src/app/pages/AgentChat/InvokeAgentBubble.tsx diff --git a/frontend/src/app/pages/AgentChat/AgentResponseBody.tsx b/frontend/src/app/pages/AgentChat/AgentResponseBody.tsx new file mode 100644 index 00000000..a2740e2c --- /dev/null +++ b/frontend/src/app/pages/AgentChat/AgentResponseBody.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Collapse from '@mui/material/Collapse'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { useClaudeTokens } from '@/shared/styles/ThemeContext'; + +export const AgentResponseBody: React.FC<{ open: boolean; markdown: string }> = ({ open, markdown }) => { + const c = useClaudeTokens(); + return ( + + + ( + {children} + ), + }} + > + {markdown} + + + + ); +}; diff --git a/frontend/src/app/pages/AgentChat/CompactMcpBubble.tsx b/frontend/src/app/pages/AgentChat/CompactMcpBubble.tsx new file mode 100644 index 00000000..01bbd64b --- /dev/null +++ b/frontend/src/app/pages/AgentChat/CompactMcpBubble.tsx @@ -0,0 +1,168 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Collapse from '@mui/material/Collapse'; +import IconButton from '@mui/material/IconButton'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import BlockIcon from '@mui/icons-material/Block'; +import { AgentMessage } from '@/shared/state/agentsSlice'; +import { useClaudeTokens } from '@/shared/styles/ThemeContext'; +import { getToolLabel } from './toolLabels'; +import BrowserAgentInlineFeed from './BrowserAgentInlineFeed'; +import { GoogleServiceIcon } from './GoogleServiceIcon'; +import { ElapsedTimer, formatElapsed } from './toolBubbleChrome'; +import { useTermColors } from './toolColorize'; +import { ParsedResult } from './toolResultParsing'; +import { McpToolInfo, getMcpShortAction } from './mcpToolName'; +import { McpResultCard } from './McpResultCard'; + +interface CompactMcpBubbleProps { + call: AgentMessage; + input: any; + sessionId?: string; + isPending: boolean; + isStreaming: boolean; + isDenied: boolean; + isError: boolean; + result: AgentMessage | null; + mcpInfo: McpToolInfo; + toolName: string; + resultSummary: string | null; + resultElapsedMs: number | null; + showTimer: boolean; + showBody: boolean; + toggle: () => void; + parsedResult: ParsedResult | null; + isBrowserAgent: boolean; + selectAttrs: Record; +} + +export const CompactMcpBubble: React.FC = ({ + call, input, sessionId, isPending, isStreaming, isDenied, isError, result, + mcpInfo, toolName, resultSummary, resultElapsedMs, showTimer, showBody, toggle, parsedResult, + isBrowserAgent, selectAttrs, +}) => { + const c = useClaudeTokens(); + const tc = useTermColors(); + + const shortAction = mcpInfo.isMcp ? getMcpShortAction(mcpInfo) : toolName; + const mcpVerbLabel = (() => { + const lbl = getToolLabel(toolName, call.id); + return result && !isDenied ? lbl.past : lbl.present; + })(); + const serviceLabel = mcpInfo.isMcp ? mcpVerbLabel : shortAction; + const ServiceIcon = mcpInfo.isMcp && mcpInfo.service + ? + : null; + + return ( + + + {ServiceIcon} + + {serviceLabel} + + {resultSummary && !isError && ( + + {resultSummary} + + )} + {!resultSummary && !showTimer && } + {showTimer && ( + <> + + + + )} + {isDenied && ( + + + denied + + )} + {result && !isDenied && ( + + {isError && ( + + )} + {resultElapsedMs != null && ( + + {formatElapsed(resultElapsedMs)} + + )} + + )} + + {showBody ? : } + + + + + + {isBrowserAgent && sessionId && ( + + )} + {parsedResult && parsedResult.type === 'mcp' ? ( + + ) : parsedResult ? ( +
+              {parsedResult.type === 'text' ? parsedResult.content : ''}
+            
+ ) : null} + {!parsedResult && isPending && !isStreaming && !isBrowserAgent && ( + + + + )} + +
+
+ ); +}; diff --git a/frontend/src/app/pages/AgentChat/CreateAgentBubble.tsx b/frontend/src/app/pages/AgentChat/CreateAgentBubble.tsx new file mode 100644 index 00000000..a8a2d06b --- /dev/null +++ b/frontend/src/app/pages/AgentChat/CreateAgentBubble.tsx @@ -0,0 +1,176 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import BlockIcon from '@mui/icons-material/Block'; +import CallSplitIcon from '@mui/icons-material/CallSplit'; +import { AgentMessage } from '@/shared/state/agentsSlice'; +import { useClaudeTokens } from '@/shared/styles/ThemeContext'; +import { ElapsedTimer, formatElapsed } from './toolBubbleChrome'; +import { AgentResponseBody } from './AgentResponseBody'; + +interface CreateAgentBubbleProps { + call: AgentMessage; + input: any; + isPending: boolean; + isDenied: boolean; + isError: boolean; + resultElapsedMs: number | null; + expanded: boolean; + showTimer: boolean; + toggle: () => void; + accentRgb: string; + createAgentResponse: string; + createAgentSessionId: string | null; + handleRevealAgent: (e: React.MouseEvent) => void; + bubbleRef: React.RefObject; + selectAttrs: Record; +} + +export const CreateAgentBubble: React.FC = ({ + call, input, isPending, isDenied, isError, resultElapsedMs, expanded, showTimer, + toggle, accentRgb, createAgentResponse, createAgentSessionId, handleRevealAgent, bubbleRef, selectAttrs, +}) => { + const c = useClaudeTokens(); + const taskPrompt = input?.prompt || input?.task || input?.message || ''; + const taskLabel = taskPrompt + ? taskPrompt.length > 40 ? taskPrompt.slice(0, 40) + '…' : taskPrompt + : 'Sub-agent'; + const hasResponse = !!createAgentResponse; + + return ( + + + + + + CreateAgent + + + + {taskLabel} + + + + {!hasResponse && !showTimer && } + + {hasResponse && createAgentResponse && !expanded && ( + + {createAgentResponse.slice(0, 100)}{createAgentResponse.length > 100 ? '…' : ''} + + )} + {expanded && } + + {isDenied && ( + + + denied + + )} + + {hasResponse && !isDenied && ( + + {isError && ( + + )} + {resultElapsedMs != null && ( + + {formatElapsed(resultElapsedMs)} + + )} + + )} + + {showTimer && } + + {createAgentSessionId && ( + + + + + + )} + + {hasResponse && ( + + {expanded ? : } + + )} + + + + + + ); +}; diff --git a/frontend/src/app/pages/AgentChat/DefaultToolBubble.tsx b/frontend/src/app/pages/AgentChat/DefaultToolBubble.tsx new file mode 100644 index 00000000..2bd44ed8 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/DefaultToolBubble.tsx @@ -0,0 +1,299 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Collapse from '@mui/material/Collapse'; +import IconButton from '@mui/material/IconButton'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import TerminalIcon from '@mui/icons-material/Terminal'; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import BlockIcon from '@mui/icons-material/Block'; +import SearchIcon from '@mui/icons-material/Search'; +import { AgentMessage } from '@/shared/state/agentsSlice'; +import { useClaudeTokens } from '@/shared/styles/ThemeContext'; +import { getToolLabelWithInput } from './toolLabels'; +import BrowserAgentInlineFeed from './BrowserAgentInlineFeed'; +import { GoogleServiceIcon } from './GoogleServiceIcon'; +import { ElapsedTimer, formatElapsed } from './toolBubbleChrome'; +import { useTermColors, colorizeInput, colorizeOutput } from './toolColorize'; +import { ParsedResult } from './toolResultParsing'; +import { McpToolInfo } from './mcpToolName'; +import { McpResultCard } from './McpResultCard'; + +interface DefaultToolBubbleProps { + call: AgentMessage; + input: any; + sessionId?: string; + mcpCompact: boolean; + isPending: boolean; + isStreaming: boolean; + isDenied: boolean; + isError: boolean; + result: AgentMessage | null; + mcpInfo: McpToolInfo; + toolName: string; + inputSummary: string; + formattedInput: string; + promptPrefix: string; + resultSummary: string | null; + resultElapsedMs: number | null; + showTimer: boolean; + showBody: boolean; + toggle: () => void; + parsedResult: ParsedResult | null; + isBrowserAgent: boolean; + accentRgb: string; + selectAttrs: Record; +} + +export const DefaultToolBubble: React.FC = ({ + call, input, sessionId, mcpCompact, isPending, isStreaming, isDenied, isError, result, + mcpInfo, toolName, inputSummary, formattedInput, promptPrefix, resultSummary, resultElapsedMs, + showTimer, showBody, toggle, parsedResult, isBrowserAgent, accentRgb, selectAttrs, +}) => { + const c = useClaudeTokens(); + const tc = useTermColors(); + + return ( + + + + {mcpInfo.isMcp && mcpInfo.service + ? + : (() => { + const n = toolName.toLowerCase(); + if (n.includes('search') || n === 'grep' || n === 'glob') + return ; + return ; + })() + } + + {(() => { + const { present, past } = getToolLabelWithInput(toolName, input, call.id); + return result && !isDenied ? past : present; + })()} + + {mcpInfo.isMcp && ( + + {mcpInfo.serverSlug} + + )} + {inputSummary && !isStreaming && ( + + {inputSummary} + + )} + {!inputSummary && } + {isStreaming && } + {isDenied && ( + + + + denied + + + )} + {result && !isDenied && ( + + {isError && ( + <> + + {resultSummary && ( + + {resultSummary} + + )} + + )} + {resultElapsedMs != null && ( + + {formatElapsed(resultElapsedMs)} + + )} + + )} + {showTimer && } + + {!isStreaming && ( + + {showBody ? ( + + ) : ( + + )} + + )} + + + + +
+              
+                {promptPrefix}
+              
+              {isStreaming ? (
+                {call.content?.input ?? ''}
+              ) : (
+                colorizeInput(toolName, formattedInput, tc)
+              )}
+              {isStreaming && (
+                
+              )}
+            
+ + {isBrowserAgent && sessionId && ( + + )} + + {parsedResult && parsedResult.type === 'mcp' ? ( + + ) : parsedResult ? ( +
+                {parsedResult.type === 'bash' ? (
+                  <>
+                    {parsedResult.stdout.trim() &&
+                      colorizeOutput(toolName, parsedResult.stdout, tc)}
+                    {parsedResult.stderr.trim() && (
+                      <>
+                        {parsedResult.stdout.trim() && '\n'}
+                        {parsedResult.stderr}
+                      
+                    )}
+                    {!parsedResult.stdout.trim() && !parsedResult.stderr.trim() && (
+                      (no output)
+                    )}
+                  
+                ) : (
+                  <>
+                    {parsedResult.isError ? (
+                      {parsedResult.content || '(empty)'}
+                    ) : (
+                      colorizeOutput(toolName, parsedResult.content, tc)
+                    )}
+                  
+                )}
+              
+ ) : null} + + {!parsedResult && isPending && !isStreaming && !isBrowserAgent && ( + + + + )} + +
+
+
+ ); +}; diff --git a/frontend/src/app/pages/AgentChat/InvokeAgentBubble.tsx b/frontend/src/app/pages/AgentChat/InvokeAgentBubble.tsx new file mode 100644 index 00000000..3094d5a3 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/InvokeAgentBubble.tsx @@ -0,0 +1,181 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import BlockIcon from '@mui/icons-material/Block'; +import CallSplitIcon from '@mui/icons-material/CallSplit'; +import { AgentMessage } from '@/shared/state/agentsSlice'; +import { useClaudeTokens } from '@/shared/styles/ThemeContext'; +import { ElapsedTimer, formatElapsed } from './toolBubbleChrome'; +import { AgentResponseBody } from './AgentResponseBody'; +import { InvokeAgentParsed } from './agentToolParsing'; + +interface InvokeAgentBubbleProps { + call: AgentMessage; + input: any; + isPending: boolean; + isDenied: boolean; + isError: boolean; + resultElapsedMs: number | null; + expanded: boolean; + showTimer: boolean; + toggle: () => void; + accentRgb: string; + invokeAgentParsed: InvokeAgentParsed | null; + invokedSessionId: string | null; + handleRevealAgent: (e: React.MouseEvent) => void; + bubbleRef: React.RefObject; + selectAttrs: Record; +} + +export const InvokeAgentBubble: React.FC = ({ + call, input, isPending, isDenied, isError, resultElapsedMs, expanded, showTimer, + toggle, accentRgb, invokeAgentParsed, invokedSessionId, handleRevealAgent, bubbleRef, selectAttrs, +}) => { + const c = useClaudeTokens(); + const agentName = invokeAgentParsed?.agentName || input?.session_id || 'Agent'; + const responsePreview = invokeAgentParsed?.response || ''; + const costLabel = invokeAgentParsed?.cost ? `$${invokeAgentParsed.cost}` : null; + const hasResponse = !!invokeAgentParsed; + + return ( + + + + + + InvokeAgent + + + + {agentName} + + + + {!hasResponse && !showTimer && } + + {hasResponse && responsePreview && !expanded && ( + + {responsePreview.slice(0, 100)}{responsePreview.length > 100 ? '…' : ''} + + )} + {expanded && } + + {isDenied && ( + + + denied + + )} + + {hasResponse && !isDenied && ( + + {isError && ( + + )} + {resultElapsedMs != null && ( + + {formatElapsed(resultElapsedMs)} + + )} + {costLabel && ( + + {costLabel} + + )} + + )} + + {showTimer && } + + {invokedSessionId && ( + + + + + + )} + + {hasResponse && ( + + {expanded ? : } + + )} + + + + + + ); +}; diff --git a/frontend/src/app/pages/AgentChat/ToolCallBubble.tsx b/frontend/src/app/pages/AgentChat/ToolCallBubble.tsx index 94a339af..8f6aca70 100644 --- a/frontend/src/app/pages/AgentChat/ToolCallBubble.tsx +++ b/frontend/src/app/pages/AgentChat/ToolCallBubble.tsx @@ -1,65 +1,33 @@ -import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import Collapse from '@mui/material/Collapse'; -import IconButton from '@mui/material/IconButton'; -import Tooltip from '@mui/material/Tooltip'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ExpandLessIcon from '@mui/icons-material/ExpandLess'; -import TerminalIcon from '@mui/icons-material/Terminal'; -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; -import BlockIcon from '@mui/icons-material/Block'; -import EmailIcon from '@mui/icons-material/Email'; -import EventIcon from '@mui/icons-material/Event'; -import FolderIcon from '@mui/icons-material/Folder'; -import AttachFileIcon from '@mui/icons-material/AttachFile'; -import SearchIcon from '@mui/icons-material/Search'; -import SendIcon from '@mui/icons-material/Send'; -import CallSplitIcon from '@mui/icons-material/CallSplit'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; +import React, { useState, useCallback, useMemo, useRef } from 'react'; import { AgentMessage, expandSession, collapseSession, fetchSession } from '@/shared/state/agentsSlice'; -import { getToolLabel, getToolLabelWithInput, prettyPath, prettyUrl, quoteQuery, bashCommandDetail } from './toolLabels'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import { placeCard, removeCard, setGlowingAgentCard, clearGlowingAgentCard, DEFAULT_CARD_W, DEFAULT_CARD_H, EXPANDED_CARD_MIN_H, GRID_GAP } from '@/shared/state/dashboardLayoutSlice'; -import { useClaudeTokens, useThemeMode } from '@/shared/styles/ThemeContext'; -import BrowserAgentInlineFeed from './BrowserAgentInlineFeed'; +import { useClaudeTokens } from '@/shared/styles/ThemeContext'; +import { ensureToolCallKeyframes } from './toolBubbleChrome'; +import { + getToolData, + getInputSummary, + formatInputDisplay, + parseToolResult, + getResultSummary, + getPromptPrefix, +} from './toolResultParsing'; +import { parseMcpToolName } from './mcpToolName'; +import { + isBrowserAgentTool, + isInvokeAgentTool, + isCreateAgentTool, + parseInvokedSessionId, + parseCreateAgentResult, + parseInvokeAgentResult, +} from './agentToolParsing'; +import { InvokeAgentBubble } from './InvokeAgentBubble'; +import { CreateAgentBubble } from './CreateAgentBubble'; +import { CompactMcpBubble } from './CompactMcpBubble'; +import { DefaultToolBubble } from './DefaultToolBubble'; -const GoogleServiceIcon: React.FC<{ service: string; size?: number }> = ({ service, size = 14 }) => { - if (service === 'gmail') { - return ( - - - - - - - - - ); - } - if (service === 'calendar') { - return ( - - - - 31 - - ); - } - if (service === 'drive' || service === 'sheets') { - return ( - - - - - - - ); - } - return null; -}; +export { parseMcpToolName, getMcpShortAction } from './mcpToolName'; +export type { McpToolInfo } from './mcpToolName'; export interface ToolPair { type: 'tool_pair'; @@ -68,399 +36,6 @@ export interface ToolPair { result: AgentMessage | null; } -let toolCallKeyframesInjected = false; -function ensureToolCallKeyframes() { - if (toolCallKeyframesInjected) return; - toolCallKeyframesInjected = true; - const style = document.createElement('style'); - style.setAttribute('data-tool-call-keyframes', ''); - style.textContent = ` -@keyframes tool-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } -} -@keyframes border-glow { - 0%, 100% { box-shadow: 0 0 0 0 rgba(var(--glow-rgb), 0); } - 50% { box-shadow: 0 0 10px 2px rgba(var(--glow-rgb), 0.12); } -} -@keyframes blink-cursor { - 0%, 100% { opacity: 1; } - 50% { opacity: 0; } -} -`; - document.head.appendChild(style); -} - -const ElapsedTimer: React.FC<{ startTime: string }> = ({ startTime }) => { - const c = useClaudeTokens(); - const [elapsed, setElapsed] = useState(0); - - useEffect(() => { - const start = new Date(startTime).getTime(); - const tick = () => setElapsed(Math.floor((Date.now() - start) / 1000)); - tick(); - const interval = setInterval(tick, 1000); - return () => clearInterval(interval); - }, [startTime]); - - const mins = Math.floor(elapsed / 60); - const secs = elapsed % 60; - const display = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`; - - return ( - - - - {display} - - - ); -}; - -function formatElapsed(ms: number): string { - if (ms >= 60000) return `${Math.floor(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`; - if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`; - return `${ms}ms`; -} - -function getToolData(call: AgentMessage) { - const content = typeof call.content === 'object' ? call.content : {}; - return { - toolName: content.tool || 'Unknown', - input: content.input || {}, - isDenied: content.approved === false, - toolId: content.id, - }; -} - -function isBashTool(name: string) { - return name === 'Bash' || name === 'bash'; -} - -export interface McpToolInfo { - isMcp: boolean; - serverSlug: string; - action: string; - service: string; - displayName: string; -} - -export function parseMcpToolName(rawName: string): McpToolInfo { - const m = rawName.match(/^mcp__([^_]+(?:-[^_]+)*)__(.+)$/); - if (!m) return { isMcp: false, serverSlug: '', action: '', service: '', displayName: rawName }; - const serverSlug = m[1]; - const action = m[2]; - const spaced = action.replace(/_/g, ' ').toLowerCase(); - const display = spaced.charAt(0).toUpperCase() + spaced.slice(1); - - const lower = action.toLowerCase(); - let service = ''; - if (lower.includes('gmail') || lower.includes('email') || lower.includes('mail')) service = 'gmail'; - else if (lower.includes('calendar') || lower.includes('event') || lower.includes('freebusy')) service = 'calendar'; - else if (lower.includes('drive') || lower.includes('file')) service = 'drive'; - else if (lower.includes('sheet') || lower.includes('spreadsheet')) service = 'sheets'; - else if (lower.includes('doc') || lower.includes('paragraph')) service = 'docs'; - else if (lower.includes('contact')) service = 'contacts'; - - return { isMcp: true, serverSlug, action, service, displayName: display }; -} - -function getMcpInputSummary(input: any): string { - if (!input || typeof input !== 'object') return ''; - const keys = Object.keys(input); - if (keys.length === 0) return ''; - if (keys.length === 1) { - const v = input[keys[0]]; - const s = typeof v === 'string' ? v : JSON.stringify(v); - return s.length > 60 ? s.slice(0, 60) + '…' : s; - } - return keys.slice(0, 3).map((k) => { - const v = input[k]; - const s = typeof v === 'string' ? v : JSON.stringify(v); - return `${k}: ${s.length > 30 ? s.slice(0, 30) + '…' : s}`; - }).join(' '); -} - -function getInputSummary(toolName: string, input: any): string { - try { - const mcp = parseMcpToolName(toolName); - if (mcp.isMcp) return getMcpInputSummary(input); - - const n = toolName.toLowerCase(); - if (isBashTool(toolName)) { - return bashCommandDetail(input.command || ''); - } - if (n === 'read' || n === 'write' || n === 'edit' || n === 'multiedit' || n === 'strreplace') - return prettyPath(input.file_path || input.path || ''); - if (n === 'glob') return input.pattern || input.glob || input.glob_pattern || ''; - if (n === 'grep' || n === 'ripgrep') { - const pat = input.pattern || input.regex || ''; - const path = input.path || input.directory || ''; - const q = quoteQuery(pat); - return path ? `${q} in ${prettyPath(path)}` : q; - } - if (n === 'websearch') return quoteQuery(input.query || input.search_term || ''); - if (n === 'webfetch') return prettyUrl(input.url || ''); - if (n === 'todoread' || n === 'todowrite') return ''; - if (n === 'ls') return prettyPath(input.path || '.'); - if (n === 'mcpactivate') return ''; - if (n === 'mcpsearch' || n === 'outputsearch') return quoteQuery(input.query || ''); - if (n === 'outputactivate') return input.output_id || ''; - if (n === 'renderoutput') return input.output_id || ''; - return ''; - } catch { - return ''; - } -} - -function formatMcpInputDisplay(input: any): string { - if (!input || typeof input !== 'object') return String(input ?? ''); - return Object.entries(input) - .map(([k, v]) => { - const s = typeof v === 'string' ? v : JSON.stringify(v, null, 2); - return `${k}: ${s}`; - }) - .join('\n'); -} - -function formatInputDisplay(toolName: string, input: any): string { - try { - const mcp = parseMcpToolName(toolName); - if (mcp.isMcp) return formatMcpInputDisplay(input); - - const n = toolName.toLowerCase(); - if (isBashTool(toolName)) return input.command || ''; - if (n === 'read') { - const p = input.file_path || input.path || ''; - const parts = [p]; - if (input.offset) parts.push(`offset: ${input.offset}`); - if (input.limit) parts.push(`limit: ${input.limit}`); - return parts.join(' '); - } - if (n === 'write') { - const p = input.file_path || input.path || ''; - const content = input.content || ''; - const preview = content.length > 300 ? content.slice(0, 300) + '\n…' : content; - return `${p}\n\n${preview}`; - } - if (n === 'edit' || n === 'strreplace') { - const p = input.file_path || input.path || ''; - const old = input.old_string || input.old_text || ''; - const nw = input.new_string || input.new_text || ''; - const lines = [p, '']; - if (old) { - const oldPreview = old.length > 200 ? old.slice(0, 200) + '…' : old; - lines.push(`- ${oldPreview.split('\n').join('\n- ')}`); - } - if (nw) { - const nwPreview = nw.length > 200 ? nw.slice(0, 200) + '…' : nw; - lines.push(`+ ${nwPreview.split('\n').join('\n+ ')}`); - } - return lines.join('\n'); - } - if (n === 'multiedit') { - const p = input.file_path || input.path || ''; - const edits = input.edits || []; - const lines = [p]; - for (const e of edits.slice(0, 3)) { - const old = e.old_string || e.old_text || ''; - lines.push(` - ${old.split('\n')[0].slice(0, 60)}…`); - } - if (edits.length > 3) lines.push(` … +${edits.length - 3} more edits`); - return lines.join('\n'); - } - if (n === 'glob') return input.pattern || input.glob || input.glob_pattern || ''; - if (n === 'grep' || n === 'ripgrep') { - const pat = input.pattern || input.regex || ''; - const path = input.path || input.directory || ''; - const parts = [`pattern: ${pat}`]; - if (path) parts.push(`path: ${path}`); - if (input.include) parts.push(`include: ${input.include}`); - return parts.join('\n'); - } - if (n === 'websearch') return input.query || input.search_term || ''; - if (n === 'webfetch') return input.url || ''; - } catch {} - if (typeof input === 'string') return input; - return JSON.stringify(input, null, 2); -} - -interface ParsedBashResult { - type: 'bash'; - stdout: string; - stderr: string; - exitCode: number | null; -} - -interface ParsedTextResult { - type: 'text'; - content: string; - isError?: boolean; -} - -interface ParsedMcpResult { - type: 'mcp'; - service: string; - action: string; - data: Record; - rawText: string; -} - -type ParsedResult = ParsedBashResult | ParsedTextResult | ParsedMcpResult; - -function parseToolResult(toolName: string, rawText: string): ParsedResult { - if (isBashTool(toolName)) { - try { - const parsed = JSON.parse(rawText); - if (typeof parsed === 'object' && parsed !== null && 'stdout' in parsed) { - const exitMatch = (parsed.stdout || '').match(/[Ee]xit code:\s*(\d+)/); - return { - type: 'bash', - stdout: parsed.stdout || '', - stderr: parsed.stderr || '', - exitCode: exitMatch ? parseInt(exitMatch[1], 10) : null, - }; - } - } catch {} - } - - const mcp = parseMcpToolName(toolName); - if (mcp.isMcp) { - try { - let parsed = JSON.parse(rawText); - - if (Array.isArray(parsed) && parsed.some((b: any) => b?.type === 'text' && typeof b?.text === 'string')) { - const textContent = parsed - .filter((b: any) => b?.type === 'text') - .map((b: any) => b.text) - .join('\n'); - try { - parsed = JSON.parse(textContent); - } catch { - return { type: 'mcp', service: mcp.service, action: mcp.action, data: {}, rawText: textContent }; - } - } - - if (typeof parsed === 'object' && parsed !== null) { - return { type: 'mcp', service: mcp.service, action: mcp.action, data: parsed, rawText }; - } - } catch {} - return { type: 'mcp', service: mcp.service, action: mcp.action, data: {}, rawText }; - } - - try { - const parsed = JSON.parse(rawText); - if (typeof parsed === 'object' && parsed !== null) { - if ('stdout' in parsed) { - return { type: 'text', content: parsed.stdout || '' }; - } - if ('content' in parsed && typeof parsed.content === 'string') { - return { type: 'text', content: parsed.content, isError: !!parsed.is_error }; - } - if ('result' in parsed && typeof parsed.result === 'string') { - return { type: 'text', content: parsed.result }; - } - if ('output' in parsed && typeof parsed.output === 'string') { - return { type: 'text', content: parsed.output }; - } - const n = toolName.toLowerCase(); - if (n === 'glob' && Array.isArray(parsed)) { - return { type: 'text', content: parsed.join('\n') }; - } - } - } catch {} - - return { type: 'text', content: rawText }; -} - -export function getMcpShortAction(mcpInfo: McpToolInfo): string { - const { action, service } = mcpInfo; - let short = action; - if (service && action.toLowerCase().startsWith(service.toLowerCase() + '_')) { - short = action.slice(service.length + 1); - } - const lower = short.replace(/_/g, ' ').toLowerCase(); - return lower.charAt(0).toUpperCase() + lower.slice(1); -} - -export function getResultSummary(toolName: string, rawText: string): string { - const parsed = parseToolResult(toolName, rawText); - - if (parsed.type === 'bash') { - const lines = parsed.stdout.split('\n').filter((l) => l.trim()).length; - if (parsed.exitCode !== null && parsed.exitCode !== 0) return `exit ${parsed.exitCode}`; - if (parsed.stderr && !parsed.stdout) return 'stderr'; - return `${lines} line${lines !== 1 ? 's' : ''}`; - } - - if (parsed.type === 'mcp') { - const d = parsed.data; - if (parsed.service === 'gmail') { - const subj = d.subject || getGmailHeader(d, 'Subject'); - if (subj) return subj; - if (Array.isArray(d.messages)) return `${d.messages.length} email${d.messages.length !== 1 ? 's' : ''}`; - if (d.id || d.messageId) return 'sent'; - } - if (parsed.service === 'calendar') { - if (d.summary) return d.summary.slice(0, 40); - if (Array.isArray(d.items)) return `${d.items.length} event${d.items.length !== 1 ? 's' : ''}`; - } - if (parsed.service === 'drive') { - if (d.name) return d.name; - if (Array.isArray(d.files)) return `${d.files.length} file${d.files.length !== 1 ? 's' : ''}`; - } - if (d.error || d.is_error) return 'error'; - return ''; - } - - const text = parsed.content; - const lines = text.split('\n'); - const lineCount = lines.length; - const n = toolName.toLowerCase(); - - try { - if (n === 'glob') { - const fileCount = lines.filter((l) => l.trim()).length; - return `${fileCount} file${fileCount !== 1 ? 's' : ''}`; - } - if (n === 'grep' || n === 'ripgrep') { - const matchCount = lines.filter((l) => l.trim()).length; - return `${matchCount} match${matchCount !== 1 ? 'es' : ''}`; - } - if (n === 'read') return `${lineCount} lines`; - if (n === 'write') return ''; - if (n === 'edit' || n === 'multiedit' || n === 'strreplace') return ''; - if (n === 'websearch') return 'results'; - if (n === 'webfetch') return `${lineCount} lines`; - if (parsed.isError) return 'error'; - } catch {} - - return `${lineCount} line${lineCount !== 1 ? 's' : ''}`; -} - -function getPromptPrefix(toolName: string): string { - if (isBashTool(toolName)) return '$ '; - const mcp = parseMcpToolName(toolName); - if (mcp.isMcp) return `❯ ${mcp.displayName} `; - return `❯ ${toolName} `; -} - interface ToolCallBubbleProps { call: AgentMessage; result?: AgentMessage | null; @@ -470,803 +45,11 @@ interface ToolCallBubbleProps { sessionId?: string; } -interface TermColors { - TERM_BG: string; - TERM_BORDER: string; - PROMPT_COLOR: string; - CMD_COLOR: string; - OUTPUT_COLOR: string; - PATH_COLOR: string; - ADD_COLOR: string; - DEL_COLOR: string; - STDERR_COLOR: string; - WARN_COLOR: string; - NUM_COLOR: string; - DIM_COLOR: string; - DIFF_HEADER_COLOR: string; - SCROLLBAR_THUMB: string; -} - -const darkTermColors: TermColors = { - TERM_BG: '#131520', - TERM_BORDER: '#1e2030', - PROMPT_COLOR: '#7ec699', - CMD_COLOR: '#e8ecf4', - OUTPUT_COLOR: '#a0aab8', - PATH_COLOR: '#82aaff', - ADD_COLOR: '#7ec699', - DEL_COLOR: '#ff8787', - STDERR_COLOR: '#ff8787', - WARN_COLOR: '#ffcb6b', - NUM_COLOR: '#f78c6c', - DIM_COLOR: '#555b6e', - DIFF_HEADER_COLOR: '#c792ea', - SCROLLBAR_THUMB: '#2a2d3e', -}; - -const lightTermColors: TermColors = { - TERM_BG: '#f4f3ee', - TERM_BORDER: '#e2e0d8', - PROMPT_COLOR: '#2d7a3e', - CMD_COLOR: '#2a2a28', - OUTPUT_COLOR: '#555550', - PATH_COLOR: '#3060a8', - ADD_COLOR: '#2d7a3e', - DEL_COLOR: '#c03030', - STDERR_COLOR: '#c03030', - WARN_COLOR: '#8a6518', - NUM_COLOR: '#c05020', - DIM_COLOR: '#9e9c95', - DIFF_HEADER_COLOR: '#7c4daa', - SCROLLBAR_THUMB: '#ccc9c0', -}; - -function useTermColors(): TermColors { - const { mode } = useThemeMode(); - return mode === 'dark' ? darkTermColors : lightTermColors; -} - -function colorizeInput(toolName: string, text: string, tc: TermColors): React.ReactNode { - const n = toolName.toLowerCase(); - const mcp = parseMcpToolName(toolName); - - if (mcp.isMcp) { - const lines = text.split('\n'); - return ( - <> - {lines.map((line, i) => { - const nl = i < lines.length - 1 ? '\n' : ''; - const colonIdx = line.indexOf(':'); - if (colonIdx > 0 && colonIdx < 30) { - return ( - - {line.slice(0, colonIdx + 1)} - {line.slice(colonIdx + 1)} - {nl} - - ); - } - return {line}{nl}; - })} - - ); - } - - if (isBashTool(toolName)) return {text}; - - if (n === 'edit' || n === 'strreplace' || n === 'multiedit') { - const lines = text.split('\n'); - return ( - <> - {lines.map((line, i) => { - const nl = i < lines.length - 1 ? '\n' : ''; - if (i === 0 && (line.startsWith('/') || line.includes('.'))) - return {line}{nl}; - if (line.startsWith('+ ')) - return {line}{nl}; - if (line.startsWith('- ')) - return {line}{nl}; - return {line}{nl}; - })} - - ); - } - - if (n === 'write') { - const lines = text.split('\n'); - return ( - <> - {lines.map((line, i) => { - const nl = i < lines.length - 1 ? '\n' : ''; - if (i === 0 && (line.startsWith('/') || line.includes('.'))) - return {line}{nl}; - return {line}{nl}; - })} - - ); - } - - if (n === 'read' || n === 'glob' || n === 'webfetch') { - if (/^\//.test(text) || text.includes('/')) - return {text}; - } - - if (n === 'grep' || n === 'ripgrep') { - const lines = text.split('\n'); - return ( - <> - {lines.map((line, i) => { - const nl = i < lines.length - 1 ? '\n' : ''; - if (line.startsWith('pattern:')) - return ( - - pattern: - {line.slice(9)} - {nl} - - ); - if (line.startsWith('path:')) - return ( - - path: - {line.slice(6)} - {nl} - - ); - return {line}{nl}; - })} - - ); - } - - return {text}; -} - -function colorizeOutput(toolName: string, text: string, tc: TermColors): React.ReactNode { - if (!text) return (empty); - - const lines = text.split('\n'); - const n = toolName.toLowerCase(); - - return ( - <> - {lines.map((line, i) => { - const nl = i < lines.length - 1 ? '\n' : ''; - const trimmed = line.trimStart(); - - if (/^\/\S+/.test(trimmed)) - return {line}{nl}; - - if (n === 'grep' || n === 'ripgrep') { - const grepMatch = line.match(/^(\S+?:\d+[:-])/); - if (grepMatch) { - return ( - - {grepMatch[1]} - {line.slice(grepMatch[1].length)} - {nl} - - ); - } - const fileHeader = line.match(/^(\S+\.\w+)$/); - if (fileHeader) - return {line}{nl}; - } - - if (line.startsWith('@@') && line.includes('@@')) - return {line}{nl}; - if (line.startsWith('+')) - return {line}{nl}; - if (line.startsWith('-')) - return {line}{nl}; - - if (/\b[Ee]rror\b/.test(line)) - return {line}{nl}; - if (/\b[Ww]arning\b/.test(line)) - return {line}{nl}; - - if (n === 'read') { - const lineNumMatch = line.match(/^(\s*\d+\s*[|:])/); - if (lineNumMatch) { - return ( - - {lineNumMatch[1]} - {line.slice(lineNumMatch[1].length)} - {nl} - - ); - } - } - - return {line}{nl}; - })} - - ); -} - - -function formatTimestamp(ts: string | number | undefined): string { - if (!ts) return ''; - try { - const d = typeof ts === 'number' ? new Date(ts) : new Date(ts); - if (isNaN(d.getTime())) return String(ts); - return d.toLocaleDateString('en-US', { - weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', - hour: 'numeric', minute: '2-digit', - }); - } catch { return String(ts); } -} - -function stripHtml(html: string): string { - const tmp = document.createElement('div'); - tmp.innerHTML = html; - return tmp.textContent || tmp.innerText || ''; -} - -interface CardColors { - TC_BG: string; - TC_BORDER: string; - TC_HOVER: string; - TC_HEADING: string; - TC_BODY: string; - TC_MUTED: string; - TC_DIM: string; - TC_ACCENT: string; - TC_SUCCESS: string; - TC_WARNING: string; -} - -const darkCardColors: CardColors = { - TC_BG: 'rgba(255,255,255,0.03)', - TC_BORDER: 'rgba(255,255,255,0.06)', - TC_HOVER: 'rgba(255,255,255,0.05)', - TC_HEADING: '#C2C0B6', - TC_BODY: '#9C9A92', - TC_MUTED: '#85837C', - TC_DIM: 'rgba(156,154,146,0.5)', - TC_ACCENT: '#c4633a', - TC_SUCCESS: '#7AB948', - TC_WARNING: '#D1A041', -}; - -const lightCardColors: CardColors = { - TC_BG: 'rgba(0,0,0,0.03)', - TC_BORDER: 'rgba(0,0,0,0.08)', - TC_HOVER: 'rgba(0,0,0,0.05)', - TC_HEADING: '#3D3D3A', - TC_BODY: '#555550', - TC_MUTED: '#73726C', - TC_DIM: 'rgba(115,114,108,0.5)', - TC_ACCENT: '#ae5630', - TC_SUCCESS: '#265B19', - TC_WARNING: '#805C1F', -}; - -function useCardColors(): CardColors { - const { mode } = useThemeMode(); - return mode === 'dark' ? darkCardColors : lightCardColors; -} - -function getGmailHeader(msg: any, name: string): string { - if (msg.payload?.headers && Array.isArray(msg.payload.headers)) { - const h = msg.payload.headers.find( - (hdr: any) => (hdr.name || '').toLowerCase() === name.toLowerCase() - ); - if (h) return h.value || ''; - } - if (msg.headers && typeof msg.headers === 'object' && !Array.isArray(msg.headers)) { - return msg.headers[name] || msg.headers[name.toLowerCase()] || ''; - } - return ''; -} - -function extractEmailFields(msg: any) { - const subject = msg.subject || getGmailHeader(msg, 'Subject') || '(no subject)'; - const from = msg.from || msg.sender || getGmailHeader(msg, 'From') || ''; - const to = msg.to || msg.recipient || getGmailHeader(msg, 'To') || ''; - const rawDate = msg.date || msg.internalDate || msg.receivedAt || getGmailHeader(msg, 'Date') || ''; - const date = formatTimestamp(rawDate); - const snippet = msg.snippet || ''; - const body = msg.body || msg.text || msg.textBody || ''; - const htmlBody = msg.htmlBody || msg.html || ''; - const bodyPreview = body || (htmlBody ? stripHtml(htmlBody) : ''); - return { subject, from, to, date, snippet, bodyPreview }; -} - -const GmailCard: React.FC<{ data: Record; action: string; hideSubjectHeader?: boolean }> = ({ data, action, hideSubjectHeader }) => { - const c = useClaudeTokens(); - const { TC_BG, TC_BORDER, TC_HOVER, TC_HEADING, TC_BODY, TC_MUTED, TC_DIM, TC_ACCENT, TC_SUCCESS, TC_WARNING } = useCardColors(); - const email = extractEmailFields(data); - const labels = data.labelIds || data.labels || []; - const attachments = data.attachments || []; - - const isSend = action.includes('send'); - const isSearch = action.includes('search') || action.includes('list'); - const messages: any[] = data.messages || (isSearch && data.results ? data.results : []); - - if (messages.length > 0) { - return ( - - {messages.slice(0, 5).map((msg: any, i: number) => { - const m = extractEmailFields(msg); - return ( - - - - {m.subject} - - {m.date && ( - - {m.date} - - )} - - {m.from && ( - - {m.from} - - )} - {(m.snippet || m.bodyPreview) && ( - - {(m.snippet || m.bodyPreview).slice(0, 120)} - {(m.snippet || m.bodyPreview).length > 120 ? '…' : ''} - - )} - - ); - })} - {messages.length > 5 && ( - - +{messages.length - 5} more - - )} - - ); - } - - return ( - - {!hideSubjectHeader && ( - - {isSend ? ( - - ) : ( - - )} - - {email.subject} - - - )} - - - {(email.from || email.to || email.date) && ( - - {email.from && ( - - From - {email.from} - - )} - {email.to && ( - - To - {email.to} - - )} - {email.date && ( - - Date - {email.date} - - )} - - )} - - {labels.length > 0 && ( - - {labels.map((l: string, i: number) => ( - - {l} - - ))} - - )} - - {(email.snippet || email.bodyPreview) && ( - - {children} }} - > - {email.bodyPreview || email.snippet} - - - )} - - {attachments.length > 0 && ( - - {attachments.map((a: any, i: number) => ( - - - - {a.filename || a.name || 'attachment'} - - - ))} - - )} - - - ); -}; - -const CalendarCard: React.FC<{ data: Record; hideHeader?: boolean }> = ({ data, hideHeader }) => { - const c = useClaudeTokens(); - const { TC_BG, TC_BORDER, TC_HOVER, TC_HEADING, TC_BODY, TC_DIM, TC_SUCCESS } = useCardColors(); - const items: any[] = data.items || (Array.isArray(data) ? data : []); - const single = !items.length ? data : null; - - if (single && (single.summary || single.start)) { - const start = single.start?.dateTime || single.start?.date || single.start || ''; - const end = single.end?.dateTime || single.end?.date || single.end || ''; - return ( - - {!hideHeader && ( - - - - {single.summary || '(no title)'} - - - )} - - {start && ( - - Start - {formatTimestamp(start)} - - )} - {end && ( - - End - {formatTimestamp(end)} - - )} - {single.location && ( - - Where - {single.location} - - )} - {single.description && ( - -
-                {single.description.slice(0, 300)}
-                {single.description.length > 300 ? '…' : ''}
-              
-
- )} -
-
- ); - } - - if (items.length > 0) { - return ( - - {items.slice(0, 6).map((item: any, i: number) => ( - - - {item.summary || '(no title)'} - - - {formatTimestamp(item.start?.dateTime || item.start?.date || item.start)} - - - ))} - {items.length > 6 && ( - - +{items.length - 6} more - - )} - - ); - } - - return null; -}; - -const DriveCard: React.FC<{ data: Record }> = ({ data }) => { - const c = useClaudeTokens(); - const { TC_BG, TC_BORDER, TC_HOVER, TC_HEADING, TC_DIM, TC_WARNING } = useCardColors(); - const files: any[] = data.files || (Array.isArray(data) ? data : []); - const single = !files.length && data.name ? data : null; - - if (single) { - return ( - - - - - {single.name} - - {single.mimeType && ( - {single.mimeType} - )} - - - ); - } - - if (files.length > 0) { - return ( - - {files.slice(0, 8).map((f: any, i: number) => ( - - - {f.name || f.id} - {f.mimeType && ( - - {f.mimeType.split('/').pop()} - - )} - - ))} - - ); - } - - return null; -}; - -const GenericMcpCard: React.FC<{ data: Record }> = ({ data }) => { - const c = useClaudeTokens(); - const { TC_DIM, TC_BODY } = useCardColors(); - const entries = Object.entries(data).filter(([, v]) => v != null); - - if (entries.length === 0) - return (empty response); - - return ( - - {entries.slice(0, 20).map(([key, val], i) => { - const isLong = typeof val === 'string' && val.length > 100; - const isObj = typeof val === 'object'; - return ( - - - {key} - - {isObj ? ( -
-                {JSON.stringify(val, null, 2).slice(0, 500)}
-              
- ) : isLong ? ( -
-                {String(val).slice(0, 500)}{String(val).length > 500 ? '…' : ''}
-              
- ) : ( - {String(val)} - )} -
- ); - })} - {entries.length > 20 && ( - - +{entries.length - 20} more fields - - )} -
- ); -}; - -const McpResultCard: React.FC<{ parsed: ParsedMcpResult; compact?: boolean }> = ({ parsed, compact }) => { - const c = useClaudeTokens(); - const tc = useTermColors(); - const { TC_BODY } = useCardColors(); - const { service, action, data, rawText } = parsed; - - if (data.error || data.is_error) { - return ( - - - {data.error || data.message || JSON.stringify(data, null, 2)} - - - ); - } - - if (service === 'gmail') return ; - if (service === 'calendar') return ; - if (service === 'drive' || service === 'sheets') return ; - - // Plain-text MCP results: render rawText capped at 6000 chars (model still sees full payload). - const hasData = data && Object.keys(data).length > 0; - if (!hasData && rawText && rawText.trim()) { - const DISPLAY_CAP = 6000; - const preview = rawText.length > DISPLAY_CAP - ? rawText.slice(0, DISPLAY_CAP) + `\n… (${rawText.length - DISPLAY_CAP} more chars; model received full output)` - : rawText; - return ( - - - {preview} - - - ); - } - - return ; -}; - -function isBrowserAgentTool(name: string): boolean { - if (name === 'CreateBrowserAgent' || name === 'BrowserAgent' || name === 'BrowserAgents') return true; - const mcp = parseMcpToolName(name); - return mcp.isMcp && mcp.serverSlug === 'openswarm-browser-agent'; -} - -function isInvokeAgentTool(name: string): boolean { - if (name === 'InvokeAgent') return true; - const mcp = parseMcpToolName(name); - return mcp.isMcp && mcp.serverSlug === 'openswarm-invoke-agent'; -} - -function isCreateAgentTool(name: string): boolean { - return name === 'Agent'; -} - -function parseInvokedSessionId(rawText: string): string | null { - const match = rawText.match(/\(forked session:\s*([a-f0-9]+)\)/); - return match ? match[1] : null; -} - -interface InvokeAgentParsed { - agentName: string; - sessionId: string | null; - cost: string | null; - response: string; -} - -function parseCreateAgentResult(rawText: string): string { - if (!rawText) return ''; - try { - const parsed = JSON.parse(rawText); - if (typeof parsed === 'string') return parsed; - if (typeof parsed === 'object' && parsed !== null) { - if (parsed.text) return parsed.text; - if (parsed.content) return typeof parsed.content === 'string' ? parsed.content : JSON.stringify(parsed.content); - if (parsed.result) return typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result); - } - } catch {} - return rawText; -} - -function parseInvokeAgentResult(rawText: string): InvokeAgentParsed | null { - const headerMatch = rawText.match( - /\*\*Invoked Agent Result\*\*(?:\s*;\s*(.+?))?\s*\(forked session:\s*([a-f0-9]+)\)/, - ); - if (!headerMatch) return null; - - const agentName = headerMatch[1]?.trim() || 'Agent'; - const sessionId = headerMatch[2]; - - const costMatch = rawText.match(/\*Cost:\s*\$([0-9.]+)\*/); - const cost = costMatch ? costMatch[1] : null; - - const bodyStart = rawText.indexOf('\n\n'); - let response = bodyStart >= 0 ? rawText.slice(bodyStart + 2).trim() : ''; - if (response.startsWith('*Cost:')) { - const afterCost = response.indexOf('\n'); - response = afterCost >= 0 ? response.slice(afterCost + 1).trim() : ''; - } - - return { agentName, sessionId, cost, response }; -} - const ToolCallBubble: React.FC = React.memo( ({ call, result = null, isPending = false, isStreaming = false, mcpCompact = false, sessionId }) => { ensureToolCallKeyframes(); const c = useClaudeTokens(); - const tc = useTermColors(); const dispatch = useAppDispatch(); const cards = useAppSelector((s) => s.dashboardLayout.cards); const [expanded, setExpanded] = useState(false); @@ -1312,17 +95,14 @@ const ToolCallBubble: React.FC = React.memo( () => (isInvokeAgent && result ? parseInvokedSessionId(resultRawText) : null), [isInvokeAgent, result, resultRawText], ); - const invokeAgentParsed = useMemo( () => (isInvokeAgent && result ? parseInvokeAgentResult(resultRawText) : null), [isInvokeAgent, result, resultRawText], ); - const createAgentResponse = useMemo( () => (isCreateAgent && result ? parseCreateAgentResult(resultRawText) : ''), [isCreateAgent, result, resultRawText], ); - const createAgentSessionId: string | null = useMemo( () => (isCreateAgent && hasStructuredResult && resultContent?.sub_session_id) ? resultContent.sub_session_id : null, [isCreateAgent, hasStructuredResult, resultContent], @@ -1410,17 +190,6 @@ const ToolCallBubble: React.FC = React.memo( .join(', ') || '189, 100, 57'; const promptPrefix = getPromptPrefix(toolName); - const shortAction = mcpInfo.isMcp ? getMcpShortAction(mcpInfo) : toolName; - - const mcpVerbLabel = (() => { - const lbl = getToolLabel(toolName, call.id); - return result && !isDenied ? lbl.past : lbl.present; - })(); - const serviceLabel = mcpInfo.isMcp ? mcpVerbLabel : shortAction; - - const ServiceIcon = mcpInfo.isMcp && mcpInfo.service - ? - : null; const selectAttrs = { 'data-select-type': 'tool-call' as const, @@ -1429,758 +198,100 @@ const ToolCallBubble: React.FC = React.memo( }; if (isInvokeAgent) { - const agentName = invokeAgentParsed?.agentName || input?.session_id || 'Agent'; - const responsePreview = invokeAgentParsed?.response || ''; - const costLabel = invokeAgentParsed?.cost ? `$${invokeAgentParsed.cost}` : null; - const hasResponse = !!invokeAgentParsed; - return ( - - - - - - InvokeAgent - - - - {agentName} - - - - {!hasResponse && !showTimer && } - - {hasResponse && responsePreview && !expanded && ( - - {responsePreview.slice(0, 100)}{responsePreview.length > 100 ? '…' : ''} - - )} - {expanded && } - - {isDenied && ( - - - denied - - )} - - {hasResponse && !isDenied && ( - - {isError && ( - - )} - {resultElapsedMs != null && ( - - {formatElapsed(resultElapsedMs)} - - )} - {costLabel && ( - - {costLabel} - - )} - - )} - - {showTimer && } - - {invokedSessionId && ( - - - - - - )} - - {hasResponse && ( - - {expanded ? : } - - )} - - - - - ( - {children} - ), - }} - > - {responsePreview} - - - - - + ); } if (isCreateAgent) { - const taskPrompt = input?.prompt || input?.task || input?.message || ''; - const taskLabel = taskPrompt - ? taskPrompt.length > 40 ? taskPrompt.slice(0, 40) + '…' : taskPrompt - : 'Sub-agent'; - const hasResponse = !!createAgentResponse; - return ( - - - - - - CreateAgent - - - - {taskLabel} - - - - {!hasResponse && !showTimer && } - - {hasResponse && createAgentResponse && !expanded && ( - - {createAgentResponse.slice(0, 100)}{createAgentResponse.length > 100 ? '…' : ''} - - )} - {expanded && } - - {isDenied && ( - - - denied - - )} - - {hasResponse && !isDenied && ( - - {isError && ( - - )} - {resultElapsedMs != null && ( - - {formatElapsed(resultElapsedMs)} - - )} - - )} - - {showTimer && } - - {createAgentSessionId && ( - - - - - - )} - - {hasResponse && ( - - {expanded ? : } - - )} - - - - - ( - {children} - ), - }} - > - {createAgentResponse} - - - - - + ); } if (mcpCompact && mcpInfo.isMcp) { return ( - - - {ServiceIcon} - - {serviceLabel} - - {resultSummary && !isError && ( - - {resultSummary} - - )} - {!resultSummary && !showTimer && } - {showTimer && ( - <> - - - - )} - {isDenied && ( - - - denied - - )} - {result && !isDenied && ( - - {isError && ( - - )} - {resultElapsedMs != null && ( - - {formatElapsed(resultElapsedMs)} - - )} - - )} - - {showBody ? : } - - - - - - {isBrowserAgent && sessionId && ( - - )} - {parsedResult && parsedResult.type === 'mcp' ? ( - - ) : parsedResult ? ( -
-                  {parsedResult.type === 'text' ? parsedResult.content : ''}
-                
- ) : null} - {!parsedResult && isPending && !isStreaming && !isBrowserAgent && ( - - - - )} - -
-
+ ); } return ( - - - - {mcpInfo.isMcp && mcpInfo.service - ? - : (() => { - const n = toolName.toLowerCase(); - if (n.includes('search') || n === 'grep' || n === 'glob') - return ; - return ; - })() - } - - {(() => { - const { present, past } = getToolLabelWithInput(toolName, input, call.id); - return result && !isDenied ? past : present; - })()} - - {mcpInfo.isMcp && ( - - {mcpInfo.serverSlug} - - )} - {inputSummary && !isStreaming && ( - - {inputSummary} - - )} - {!inputSummary && } - {isStreaming && } - - {isDenied && ( - - - - denied - - - )} - {result && !isDenied && ( - - {isError && ( - <> - - {resultSummary && ( - - {resultSummary} - - )} - - )} - {resultElapsedMs != null && ( - - {formatElapsed(resultElapsedMs)} - - )} - - )} - {showTimer && } - - {!isStreaming && ( - - {showBody ? ( - - ) : ( - - )} - - )} - - - - -
-                
-                  {promptPrefix}
-                
-                {isStreaming ? (
-                  {call.content?.input ?? ''}
-                ) : (
-                  colorizeInput(toolName, formattedInput, tc)
-                )}
-                {isStreaming && (
-                  
-                )}
-              
- - {isBrowserAgent && sessionId && ( - - )} - - {parsedResult && parsedResult.type === 'mcp' ? ( - - ) : parsedResult ? ( -
-                  {parsedResult.type === 'bash' ? (
-                    <>
-                      {parsedResult.stdout.trim() &&
-                        colorizeOutput(toolName, parsedResult.stdout, tc)}
-                      {parsedResult.stderr.trim() && (
-                        <>
-                          {parsedResult.stdout.trim() && '\n'}
-                          {parsedResult.stderr}
-                        
-                      )}
-                      {!parsedResult.stdout.trim() && !parsedResult.stderr.trim() && (
-                        (no output)
-                      )}
-                    
-                  ) : (
-                    <>
-                      {parsedResult.isError ? (
-                        {parsedResult.content || '(empty)'}
-                      ) : (
-                        colorizeOutput(toolName, parsedResult.content, tc)
-                      )}
-                    
-                  )}
-                
- ) : null} - - {!parsedResult && isPending && !isStreaming && !isBrowserAgent && ( - - - - )} - -
-
-
+ ); } );