import { Code2, Database, FileText, Globe, Image, KeyRound, Mic, Rocket, Search, Terminal, Wrench, Zap, type LucideIcon, } from 'lucide-react-native'; import { getMessageToolCalls } from './messageUtils'; import type { SuperagentMessage, SuperagentToolCall } from '../../types'; /** * Native port of the builder chat's deterministic agent-phase status * (apper PR #10554, web flag `agent-phase-indicator`). While the agent works, * the thinking status reads what it's *doing* ("Editing your code...", * "Researching...") based on the active tool's resource group, with a fallback * to "Thinking...". Pure + read-only — derived from the tool calls already in * the conversation; no extra network/LLM call. * * Resource groups + labels mirror the web `tools-ui/tool-groups.ts`; the * per-tool icon mirrors `getTimelineIconForTool`. */ interface ToolGroup { statusActive: string; toolNames: string[]; } const TOOL_GROUPS: ReadonlyArray = [ { statusActive: 'Editing your files...', toolNames: [ 'read_file', 'write_file', 'edit_file', 'str_replace_based_edit_tool', 'find_replace', 'anchor_replace', 'delete_file', 'read_uploaded_file', 'list_files', 'grep', 'install_npm_package', ], }, { statusActive: 'Working with app data...', toolNames: [ 'read_entities', 'create_entities', 'create_entity_records', 'update_entities', 'delete_entities', 'manage_entity_schemas', ], }, { statusActive: 'Working on your backend...', toolNames: [ 'deploy_backend_function', 'deploy_backend_functions', 'delete_backend_function', 'test_backend_function', 'get_backend_function_logs', 'get_runtime_logs', ], }, { statusActive: 'Researching...', toolNames: ['search_web', 'web_search', 'fetch_website', 'search_base44_docs'], }, { statusActive: 'Configuring your agent...', toolNames: [ 'set_secrets', 'get_connectors_info', 'get_connector_token', 'request_oauth_authorization', 'set_app_user_connector', 'request_agent_tool_permissions', 'setup_telegram_connection', 'setup_slack_connection', 'create_whatsapp_group', ], }, { statusActive: 'Setting up payments...', toolNames: [ 'stripe_create_product', 'stripe_list_products', 'stripe_create_price', 'stripe_list_prices', 'stripe_register_webhook', 'stripe_create_checkout_session', 'wix_payments_register_webhook', 'suggest_payments_installation', ], }, { statusActive: 'Setting up automations...', toolNames: ['list_automations', 'create_automation', 'manage_automation'], }, { statusActive: 'Migrating data...', toolNames: [ 'import_data', 'discover_source_schemas', 'fetch_source_data', 'fetch_source_code', 'migrate_data_sample', 'migrate_data_batch', 'start_full_migration', ], }, { statusActive: 'Generating media...', toolNames: [ 'generate_image', 'generate_video', 'transcribe_audio', 'send_image', 'upload_file', 'upload_private_file', 'create_file_signed_url', ], }, { statusActive: 'Working with memory...', toolNames: ['update_identity', 'list_sessions', 'search_sessions', 'read_session_log'], }, { statusActive: 'Using a skill...', toolNames: [ 'activate_workspace_skill', 'activate_platform_skill', 'run_skill', 'suggest_skill_installation', 'mcp_figma_get_file', 'bash', 'execute_code', ], }, { statusActive: 'Working on the Base44 platform...', toolNames: [ 'list_user_apps', 'list_base44_apps', 'create_base44_app', 'send_message_to_builder', 'get_base44_app_status', 'call_base44_backend_function', 'broadcast_message', 'vent_send_feedback', ], }, { statusActive: 'Planning...', toolNames: ['ask_clarifying_questions', 'generate_prd', 'create_test_flow'], }, ]; const TOOL_TO_GROUP = new Map(); for (const group of TOOL_GROUPS) { for (const name of group.toolNames) TOOL_TO_GROUP.set(name, group); } const TOOL_FALLBACK_LABEL = 'Working...'; // Pattern group (not in TOOL_GROUPS): any tool whose name contains "browser". const BROWSING_LABEL = 'Browsing the internet...'; /** Tool name → icon, mirroring the web `getTimelineIconForTool` (sans active/error). */ export function getToolTypeIcon(name: string): LucideIcon { if (name.includes('browser')) return Globe; if (name === 'bash') return Terminal; if (name.includes('search') || name === 'grep' || name === 'fetch_website' || name === 'get_connectors_info') return Search; if (name.includes('secret') || name.includes('oauth')) return KeyRound; if (name.includes('entities') || name.includes('data')) return Database; if (name.includes('automation') || name.includes('scheduled_task')) return Zap; if (name.includes('deploy') || name.includes('backend')) return Rocket; if (name.includes('image') || name.includes('video') || name.includes('screenshot')) return Image; if (name.includes('audio') || name.includes('transcribe')) return Mic; if (name.includes('replace') || name === 'edit_file' || name === 'write_file') return Code2; if (name.includes('file') || name === 'read_agent_context_file') return FileText; return Wrench; } export interface ThinkingPhase { label: string; toolName: string; } /** Lowercased tool-call status (shared with ToolCallSummary's status predicates). */ export function normalizeStatus(status?: string): string { return (status || '').toLowerCase(); } /** * Single source of truth for "is this tool currently working" — used by both the * bottom thinking-phase (resolveThinkingPhase) and the in-bubble timeline * (ToolCallSummary's isRunningStatus) so they can't drift on a status value. */ export function isActiveToolStatus(status?: string): boolean { const normalized = normalizeStatus(status); return normalized === 'running' || normalized === 'in_progress' || normalized === 'pending'; } /** Expand a grouped tool call into its children, else the call itself. */ export function flattenToolCall(toolCall: SuperagentToolCall): SuperagentToolCall[] { if (toolCall.grouped && Array.isArray(toolCall.toolCalls)) return toolCall.toolCalls; return [toolCall]; } /** * Resolve the active-tool phase from the current assistant turn. Returns `null` * when no tool is active, so the caller falls back to the "Thinking..." state. * The first active tool (running/in_progress/pending, per isActiveToolStatus) * across the whole turn wins — a turn can span several assistant rows (e.g. * consecutive tool-only messages, or text on a later row), so we scan all of * them, not just the newest. */ export function resolveThinkingPhase(messages: SuperagentMessage[]): ThinkingPhase | null { // Gather the current turn's assistant rows: walk back from the end, stopping // at the newest user message. Otherwise a just-sent question (no assistant row // yet) would reuse the previous turn's tool calls and show a stale phase. const turnToolCalls: SuperagentToolCall[] = []; for (let index = messages.length - 1; index >= 0; index -= 1) { const role = messages[index].role; if (role === 'user') break; if (role === 'assistant') { turnToolCalls.push(...getMessageToolCalls(messages[index]).flatMap(flattenToolCall)); } } if (turnToolCalls.length === 0) return null; // Shared predicate with the in-bubble timeline — missing status is NOT active // here either, so the footer and the bubble never disagree on a tool. const active = turnToolCalls.find((tool) => !!tool.name && isActiveToolStatus(tool.status)); if (!active?.name) return null; const group = TOOL_TO_GROUP.get(active.name); if (group) return { label: group.statusActive, toolName: active.name }; if (active.name.includes('browser')) return { label: BROWSING_LABEL, toolName: active.name }; return { label: TOOL_FALLBACK_LABEL, toolName: active.name }; }