import React, { useEffect, useMemo, useState } from 'react'; import { themedColor } from '../../theme'; import { Pressable, ScrollView, Text, View } from 'react-native'; import { AlertCircle, CheckCircle2, ChevronRight, Loader2, type LucideIcon } from 'lucide-react-native'; import { flattenToolCall, getToolTypeIcon, isActiveToolStatus, normalizeStatus } from '../conversation/agentPhase'; import { getPackageActionMode, type PackageActionMode } from './packageUtils'; // Muted monochrome palette, matching the web tool timeline (gray for normal / // active, red only for errors). See agent-timeline-tool-row-utils.ts + TimelineRow.tsx. const TOOL_ICON_MUTED = '#A1A1AA'; const TOOL_ICON_ERROR = '#FCA5A5'; import { conversationStyles } from '../conversation/conversationStyles'; import { SendImageGallery } from '../attachments/SendImageGallery'; import { isSendImageTool, parseSentImages } from '../attachments/sendImage'; import { SpinningLoader } from '../../components/SpinningLoader'; import { ToolApprovalCard } from './ToolApprovalCard'; import { approvalCapableToolNames, groupedToolNames, resolveDefaultToolRenderer, } from '../../toolWidgets/defaultToolRenderers'; import { EntityApprovalBatchCard } from '../../toolWidgets/EntityApprovalBatchCard'; import { isPendingEntityApproval } from '../../toolWidgets/entityWidgetUtils'; import { getStringArg, groupConsecutiveToolCalls, parseToolArgs, truncateSingleLine, } from '../../toolWidgets/toolWidgetUtils'; import type { SuperagentAgent, SuperagentConnector, SuperagentConnectorActionInput, SuperagentConversation, SuperagentMessage, SuperagentToolCall, SuperagentToolRenderers, } from '../../types'; // Low-level tools hidden in the timeline — mirror of the web's HIDDEN_IN_BOTH // (activity-groups.ts). The screenshot-producing browser tools // (browserbase_screenshot / local_browser_screenshot) are NOT hidden: they // render via their widget in defaultToolRenderers. const HIDDEN_TOOLS = new Set([ 'browserbase_get_session', 'browserbase_stop_session', 'browserbase_get_live_urls', 'browserbase_reset_context', 'browserbase_navigate', 'browserbase_click', 'browserbase_type', 'browserbase_get_content', 'edit_file', // Internal agent→Base44 feedback signal — never user-facing on the web either. 'vent_send_feedback', ]); // Labels only for tools that exist in superagent flows (create_user_agent_tools // in backend/.../user_agent/tools.py) or that older conversations can still // carry (legacy channel/connector approval tools, the Anthropic-native // str_replace editor). Builder-only names from the original builder-era port // were pruned. const FRIENDLY_TOOL_LABELS: Record = { ask_clarifying_questions: { active: 'Asking a clarifying question...', completed: 'Asked a clarifying question' }, bash: { active: 'Running commands...', completed: 'Ran commands' }, browserbase_screenshot: { active: 'Taking screenshots...', completed: 'Took screenshots' }, build_mode: { active: 'Designing your agent...', completed: 'Designed your agent' }, // Neutral completed copy: the PSP tools resolve success=True on "Skip" too, so a // connected/selected label would falsely claim payments are active (web parity). configure_psp_credentials: { active: 'Connecting payment provider...', completed: 'Payment processor setup' }, create_automation: { active: 'Creating automation...', completed: 'Created automation' }, create_or_update_workflow: { active: 'Saving workflow...', completed: 'Saved workflow' }, delete_entities: { active: 'Deleting data...', completed: 'Deleted data' }, delete_file: { active: 'Deleting files...', completed: 'Deleted files' }, fetch_website: { active: 'Reading websites...', completed: 'Read websites' }, find_replace: { active: 'Editing code...', completed: 'Edited code' }, get_connectors_info: { active: 'Checking connectors...', completed: 'Checked connectors' }, grep: { active: 'Searching files...', completed: 'Searched files' }, install_npm_package: { active: 'Installing packages...', completed: 'Installed packages' }, list_automations: { active: 'Listing automations...', completed: 'Listed automations' }, manage_automation: { active: 'Managing automation...', completed: 'Managed automation' }, request_oauth_authorization: { active: 'Requesting connector access...', completed: 'Requested connector access' }, run_skill: { active: 'Running a skill...', completed: 'Ran a skill' }, search_base44_docs: { active: 'Checking docs...', completed: 'Checked docs' }, search_web: { active: 'Searching the web...', completed: 'Searched the web' }, select_payment_provider: { active: 'Selecting payment provider...', completed: 'Payment provider selection' }, sequence_connect: { active: 'Connecting banking...', completed: 'Bank connection' }, sequence_create_account: { active: 'Creating account...', completed: 'Account setup' }, sequence_create_rule: { active: 'Creating automation rule...', completed: 'Automation rule setup' }, sequence_create_transfer: { active: 'Creating transfer...', completed: 'Transfer' }, sequence_get_account: { active: 'Getting account details...', completed: 'Got account details' }, sequence_get_rule: { active: 'Getting automation rule...', completed: 'Got automation rule' }, sequence_get_rule_execution: { active: 'Getting rule run...', completed: 'Got rule run' }, sequence_get_transfer: { active: 'Getting transfer...', completed: 'Got transfer' }, sequence_list_accounts: { active: 'Listing accounts...', completed: 'Listed accounts' }, sequence_list_audit_logs: { active: 'Listing audit logs...', completed: 'Listed audit logs' }, sequence_list_beneficiaries: { active: 'Listing beneficiaries...', completed: 'Listed beneficiaries' }, sequence_list_card_transactions: { active: 'Listing card transactions...', completed: 'Listed card transactions' }, sequence_list_external_transactions: { active: 'Listing external transactions...', completed: 'Listed external transactions' }, sequence_list_rule_executions: { active: 'Listing rule runs...', completed: 'Listed rule runs' }, sequence_list_rules: { active: 'Listing automation rules...', completed: 'Listed automation rules' }, sequence_list_transfers: { active: 'Listing transfers...', completed: 'Listed transfers' }, sequence_list_transfers_by_account: { active: 'Listing account transfers...', completed: 'Listed account transfers' }, sequence_trigger_rule: { active: 'Running automation rule...', completed: 'Automation run' }, sequence_update_rule: { active: 'Updating automation rule...', completed: 'Automation rule update' }, set_app_user_connector: { active: 'Requesting connector credentials...', completed: 'Requested connector credentials' }, set_secrets: { active: 'Setting a secret...', completed: 'Set a secret' }, setup_slack_connection: { active: 'Connecting Slack...', completed: 'Connected Slack' }, setup_telegram_connection: { active: 'Connecting Telegram...', completed: 'Connected Telegram' }, suggest_payments_installation: { active: 'Setting up payments...', completed: 'Requested payment setup' }, str_replace_based_edit_tool: { active: 'Editing code...', completed: 'Edited code' }, test_backend_function: { active: 'Testing backend...', completed: 'Tested backend' }, update_entities: { active: 'Updating data...', completed: 'Updated data' }, update_identity: { active: 'Updating memory...', completed: 'Updated memory' }, web_search: { active: 'Searching the web...', completed: 'Searched the web' }, }; type SubmitToolCallInput = ( toolCallId: string, approve: boolean, extraUserInput?: unknown, originRequestId?: string, ) => Promise; export function ToolCallSummary({ agent, availableConnectors, conversationId, isLastAssistantMessage, message, onConnectConnector, showDebugPayloads, submitToolCallInput, toolCalls, toolRenderers, }: { agent: SuperagentAgent; availableConnectors?: SuperagentConnector[]; conversationId: string | null; isLastAssistantMessage: boolean; message: SuperagentMessage; onConnectConnector?: (input: SuperagentConnectorActionInput) => Promise | boolean | string | void; showDebugPayloads?: boolean; submitToolCallInput?: SubmitToolCallInput; toolCalls: SuperagentToolCall[]; toolRenderers?: SuperagentToolRenderers; }) { const chunks = useMemo(() => createToolChunks(toolCalls, toolRenderers), [toolCalls, toolRenderers]); if (chunks.length === 0) return null; return ( {chunks.map((chunk) => { if (chunk.type === 'timeline') { return ( ); } if (chunk.type === 'gallery') { return ( ); } if (chunk.type === 'entityApprovalBatch') { return ( ); } const Renderer = chunk.Renderer; return ( ); })} ); } function DefaultToolTimeline({ agent, availableConnectors, isLastAssistantMessage, onConnectConnector, submitToolCallInput, toolCalls, }: { agent: SuperagentAgent; availableConnectors?: SuperagentConnector[]; isLastAssistantMessage: boolean; onConnectConnector?: (input: SuperagentConnectorActionInput) => Promise | boolean | string | void; submitToolCallInput?: SubmitToolCallInput; toolCalls: SuperagentToolCall[]; }) { const visibleToolCalls = useMemo( // Hidden tools (browserbase_*, ...) stay hidden during normal execution, // but a pre-execution guard can pause one as waiting_for_user_input. // Keep those visible so the approval card renders and the agent can resume. () => toolCalls .flatMap(flattenToolCall) .filter((toolCall) => !HIDDEN_TOOLS.has(toolCall.name || '') || isApprovalRequest(toolCall)), [toolCalls], ); const hasWaitingTool = visibleToolCalls.some(isApprovalRequest); const isRunning = visibleToolCalls.some(isRunningStatus); const [expanded, setExpanded] = useState(hasWaitingTool); const [openRows, setOpenRows] = useState>({}); useEffect(() => { if (hasWaitingTool) setExpanded(true); }, [hasWaitingTool]); if (visibleToolCalls.length === 0) return null; const label = getCollapsedLabel(visibleToolCalls, isRunning); // While tools are still running (and nothing needs the user's input), the live // status is carried by the bottom TypingIndicator — don't render a duplicate // in-bubble indicator here. The timeline renders once the tools settle. If an // approval/waiting tool is present alongside a running one, fall through to the // expanded timeline so the approval card stays reachable. if (isRunning && !hasWaitingTool) return null; return ( setExpanded((current) => !current)} style={conversationStyles.toolTimelineHeader} > {label} {expanded ? ( {visibleToolCalls.map((toolCall, index) => { const rowKey = getToolCallKey(toolCall, index); const isOpen = Boolean(openRows[rowKey]); return ( setOpenRows((current) => ({ ...current, [rowKey]: !current[rowKey] }))} onConnectConnector={onConnectConnector} submitToolCallInput={submitToolCallInput} toolCall={toolCall} /> ); })} {!hasWaitingTool ? : null} ) : null} ); } function ToolTimelineRow({ agent, availableConnectors, isLastAssistantMessage, isLast, isOpen, onConnectConnector, onToggle, submitToolCallInput, toolCall, }: { agent: SuperagentAgent; availableConnectors?: SuperagentConnector[]; isLastAssistantMessage: boolean; isLast: boolean; isOpen: boolean; onConnectConnector?: (input: SuperagentConnectorActionInput) => Promise | boolean | string | void; onToggle: () => void; submitToolCallInput?: SubmitToolCallInput; toolCall: SuperagentToolCall; }) { const preview = getToolPreview(toolCall); const detail = getToolDetail(toolCall); const showsApproval = isApprovalRequest(toolCall); const canOpen = Boolean(detail) && !showsApproval; return ( {!isLast ? : null} {getToolTitle(toolCall)} {preview ? {preview} : null} {shouldShowStatusLine(toolCall, preview) ? ( {formatStatus(toolCall.status || 'done')} ) : null} {showsApproval ? ( ) : null} {!showsApproval && isOpen && detail ? : null} ); } function ToolDetailBox({ detail }: { detail: string }) { return ( {detail} ); } function DoneRow() { return ( Done ); } // active → spinner, error → alert, otherwise the tool-type icon (shared with the // thinking-status phase indicator in agentPhase.ts). function getToolIcon(name: string, isActive: boolean, isError: boolean): LucideIcon { if (isActive) return Loader2; if (isError) return AlertCircle; return getToolTypeIcon(name); } function ToolStatusIcon({ name, status }: { name?: string; status?: string }) { const isActive = isRunningStatus({ status }); const isError = isErrorStatus({ status }); const color = isError ? themedColor(TOOL_ICON_ERROR) : themedColor(TOOL_ICON_MUTED); const Icon = getToolIcon(name || '', isActive, isError); return ( {isActive ? : } ); } // True when every image in a send_image gallery resolved AND its URL already // appears in the agent's message text — used to collapse (not hide) the gallery // so it doesn't visually duplicate what the text already shows, while staying // reachable (the text may render the URL as a link, not an inline image). A // still-loading/failed tile has no text equivalent, so it returns false. function galleryUrlsAllInText(toolCalls: SuperagentToolCall[], content: string | undefined): boolean { if (!content) return false; const images = parseSentImages(toolCalls); return images.length > 0 && images.every((img) => img.url != null && content.includes(img.url)); } function createToolChunks(toolCalls: SuperagentToolCall[], toolRenderers?: SuperagentToolRenderers) { const chunks: Array< | { key: string; toolCalls: SuperagentToolCall[]; type: 'timeline' } | { key: string; toolCalls: SuperagentToolCall[]; type: 'gallery' } | { key: string; toolCalls: SuperagentToolCall[]; type: 'entityApprovalBatch' } | { key: string; Renderer: NonNullable[string]; toolCall: SuperagentToolCall; type: 'custom' } > = []; let timelineBuffer: SuperagentToolCall[] = []; let galleryBuffer: SuperagentToolCall[] = []; let timelineIndex = 0; let galleryIndex = 0; // Runs of consecutive calls to a groupable tool (grep, generate_image) // collapse into one synthetic grouped call, so their renderer shows a // single combined card — the native mirror of the web's groupConsecutive // preprocessing. Non-groupable calls pass through untouched. const groupedToolCalls = groupConsecutiveToolCalls(toolCalls, groupedToolNames); const flushTimeline = () => { if (timelineBuffer.length === 0) return; chunks.push({ key: `timeline-${timelineIndex}`, toolCalls: timelineBuffer, type: 'timeline', }); timelineIndex += 1; timelineBuffer = []; }; // Consecutive send_image calls collapse into one gallery widget so a batch of // images (e.g. a deck of slides) renders as a grid rather than N timeline rows. const flushGallery = () => { if (galleryBuffer.length === 0) return; chunks.push({ key: `gallery-${galleryIndex}`, toolCalls: galleryBuffer, type: 'gallery', }); galleryIndex += 1; galleryBuffer = []; }; // Two or more pending update/delete entity approvals collapse into one batch // card (per-call checkboxes) rendered after everything else, mirroring the // web ToolCallsContainer. A single one stays inline via its own widget. // Host-overridden tools are excluded — their renderer keeps the call. // Entity tools are never in groupedToolNames, so grouping passes them // through untouched and this filter sees the original calls. const batchApprovalCalls = groupedToolCalls.filter( (toolCall) => isPendingEntityApproval(toolCall) && !toolRenderers?.[toolCall.name], ); const batchedIds = batchApprovalCalls.length > 1 ? new Set(batchApprovalCalls.map((toolCall) => toolCall.id)) : null; groupedToolCalls.forEach((toolCall, index) => { if (batchedIds?.has(toolCall.id)) return; const flattened = flattenToolCall(toolCall); // Host renderers (the toolRenderers prop) win over the built-in widget // registry. Built-in widgets additionally step aside for approval-paused // calls so the timeline's ToolApprovalCard keeps handling those — except // the approval-capable widgets, which render their own approval UI. const Renderer = toolRenderers?.[toolCall.name] ?? (flattened.some(isApprovalRequest) && !approvalCapableToolNames.has(toolCall.name) ? undefined : resolveDefaultToolRenderer(toolCall)); if (Renderer) { // A renderer for the tool always wins over the gallery. flushTimeline(); flushGallery(); chunks.push({ key: toolCall.id ?? `${toolCall.name}-${index}`, Renderer, toolCall, type: 'custom', }); return; } // send_image (incl. a group consisting only of send_image calls) is shown as // an image gallery instead of generic timeline rows. if (flattened.length > 0 && flattened.every(isSendImageTool)) { flushTimeline(); galleryBuffer.push(...flattened); return; } flushGallery(); timelineBuffer.push(toolCall); }); flushTimeline(); flushGallery(); if (batchedIds) { chunks.push({ key: 'entity-approval-batch', toolCalls: batchApprovalCalls, type: 'entityApprovalBatch' }); } return chunks; } function getCollapsedLabel(toolCalls: SuperagentToolCall[], isRunning: boolean) { if (toolCalls.some(isApprovalRequest)) return 'Waiting for input'; const firstName = toolCalls[0]?.name || ''; if (toolCalls.length > 0 && toolCalls.every((toolCall) => toolCall.name === firstName)) { if (firstName === 'bash') { if (isRunning) return 'Running commands...'; return toolCalls.length === 1 ? 'Ran a command' : `Ran ${toolCalls.length} commands`; } if (firstName === 'install_npm_package') { // Collapsed rows use the static FRIENDLY_TOOL_LABELS by default, which is // install-centric; make the collapsed label reflect the actual action(s) // AND the terminal status so a failed (un)install isn't shown as done. const modes = toolCalls.map((tc) => getPackageActionModeFromArgs(parseToolArgs(tc))); const hasInstall = modes.some((m) => m === 'install' || m === 'mixed'); const hasUninstall = modes.some((m) => m === 'uninstall' || m === 'mixed'); const kind = hasInstall && hasUninstall ? 'update' : hasUninstall ? 'uninstall' : 'install'; // Waiting/approval rows are handled by the isApprovalRequest guard above, // so here we only distinguish success from not-done (error or rejection). const notDone = !isRunning && toolCalls.some(isNotDoneStatus); const base = isRunning ? { update: 'Updating packages...', uninstall: 'Uninstalling packages...', install: 'Installing packages...' }[kind] : notDone ? { update: 'Failed to update packages', uninstall: 'Failed to uninstall packages', install: 'Failed to install packages' }[kind] : { update: 'Updated packages', uninstall: 'Uninstalled packages', install: 'Installed packages' }[kind]; return !isRunning && toolCalls.length > 1 ? `${base} (${toolCalls.length})` : base; } const label = FRIENDLY_TOOL_LABELS[firstName]; if (label) { const base = isRunning ? label.active : label.completed; return !isRunning && toolCalls.length > 1 ? `${base} (${toolCalls.length})` : base; } } if (isRunning) return 'Working...'; return `${toolCalls.length} ${toolCalls.length === 1 ? 'action' : 'actions'}`; } function getPackageActionModeFromArgs(args: Record | string | null): PackageActionMode { const packages = args && typeof args !== 'string' && Array.isArray(args.packages) ? args.packages : []; return getPackageActionMode(packages as Array<{ action?: string }>); } function getToolTitle(toolCall: SuperagentToolCall) { if (toolCall.name === 'install_npm_package') { const mode = getPackageActionModeFromArgs(parseToolArgs(toolCall)); const kind = mode === 'mixed' ? 'update' : mode; // Waiting-for-approval is not terminal — show the in-progress wording so the // header never implies a destructive removal happened before the user // approved. A rejected (stopped) or failed op shows a non-success label. const active = isRunningStatus(toolCall) || isWaitingStatus(toolCall); const notDone = !active && isNotDoneStatus(toolCall); const verbs = { install: { active: 'Installing packages', failed: 'Failed to install packages', done: 'Installed packages' }, uninstall: { active: 'Uninstalling packages', failed: 'Failed to uninstall packages', done: 'Uninstalled packages' }, update: { active: 'Updating packages', failed: 'Failed to update packages', done: 'Updated packages' }, }[kind]; return active ? verbs.active : notDone ? verbs.failed : verbs.done; } const label = FRIENDLY_TOOL_LABELS[toolCall.name || '']; if (label) return isRunningStatus(toolCall) ? label.active.replace(/\.\.\.$/, '') : label.completed; return formatToolName(toolCall.name || 'Tool'); } function getToolPreview(toolCall: SuperagentToolCall) { const args = parseToolArgs(toolCall); const rawArgs = getRawArgs(toolCall); if (toolCall.name === 'bash') { const command = getStringArg(args, ['command', 'cmd', 'script']) || (typeof rawArgs === 'string' ? rawArgs : ''); return command ? truncateSingleLine(command, 48) : null; } if (toolCall.name === 'grep') { const pattern = getStringArg(args, ['pattern', 'query']); const path = getStringArg(args, ['path', 'include']); return truncateSingleLine([pattern, path].filter(Boolean).join(' in '), 48); } if (toolCall.name === 'search_web' || toolCall.name === 'web_search') { return truncateSingleLine(getStringArg(args, ['query', 'q']), 48); } if (toolCall.name === 'request_oauth_authorization') { return 'waiting for user input / approval'; } if (toolCall.name === 'get_connectors_info') { return getResultPreview(toolCall.results); } return getStringArg(args, ['name', 'title', 'path', 'filename', 'integration_type', 'connector_id']).trim() || null; } function getToolDetail(toolCall: SuperagentToolCall) { const sections: string[] = []; const args = getRawArgs(toolCall); const results = toolCall.results; if (args) sections.push(`Input:\n${formatValue(args)}`); if (results) sections.push(`Output:\n${formatValue(results)}`); return sections.length > 0 ? sections.join('\n\n') : null; } function getRawArgs(toolCall: SuperagentToolCall) { return toolCall.arguments_string ?? toolCall.arguments ?? null; } function getResultPreview(result: unknown) { if (!result) return null; const value = typeof result === 'string' ? result : formatValue(result); const firstLine = value.split('\n').find((line) => line.trim()); return firstLine ? truncateSingleLine(firstLine, 52) : null; } function shouldShowStatusLine(toolCall: SuperagentToolCall, preview: string | null) { if (!toolCall.status) return false; if (toolCall.name === 'bash' || toolCall.name === 'grep') return false; return !preview || isWaitingStatus(toolCall) || isErrorStatus(toolCall); } function isRunningStatus(toolCall: Pick) { // Shared with resolveThinkingPhase so the in-bubble timeline and the bottom // status agree on what "active" means (notably: in_progress counts). return isActiveToolStatus(toolCall.status); } function isWaitingStatus(toolCall: Pick) { return normalizeStatus(toolCall.status).includes('waiting'); } function isApprovalRequest(toolCall: SuperagentToolCall) { // Web parity: every web tool-UI keys the approval form strictly on // `status === 'waiting_for_user_input'`, with no results-text fallback. The // old `results.includes('waiting for user input')` heuristic re-triggered the // approval UI on settled/declined calls whose results still carried the // backend's waiting message (set_waiting_for_user_input overwrites results), // making a dismissed suggestion render its approval card again. return isWaitingStatus(toolCall); } function isErrorStatus(toolCall: Pick) { const normalized = normalizeStatus(toolCall.status); return normalized.includes('error') || normalized.includes('failed') || normalized.includes('rejected'); } // A terminal status that is NOT a successful completion: an error/failure, or a // user-rejected approval (the backend sets `stopped` on rejection). Used so a // rejected/failed (un)install never renders the success ("Uninstalled") wording. function isNotDoneStatus(toolCall: Pick) { return isErrorStatus(toolCall) || normalizeStatus(toolCall.status).includes('stopped'); } function formatToolName(name: string) { return name .split('_') .filter(Boolean) .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)) .join(' '); } function formatStatus(status: string) { return status .split('_') .filter(Boolean) .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)) .join(' '); } function formatValue(value: unknown) { if (typeof value === 'string') return value.length > 900 ? `${value.slice(0, 900)}...` : value; try { const stringified = JSON.stringify(value, null, 2); return stringified.length > 900 ? `${stringified.slice(0, 900)}...` : stringified; } catch { return String(value); } } function getToolCallKey(toolCall: SuperagentToolCall, index: number) { return toolCall.id ?? `${toolCall.name || 'tool'}-${index}`; }