import React, { useEffect, useMemo, useState } from 'react'; import { Pressable, Text, View } from 'react-native'; import { Check } from 'lucide-react-native'; import { useAgentBi } from '../analytics/mixpanelContext'; import { themedColor } from '../theme'; import type { SuperagentToolCall, SuperagentToolRendererProps } from '../types'; import { buildBatchApprovalDecisions, getEntitySummary, PREVIOUS_APPROVAL_NOTE, type BatchApprovalDecision, } from './entityWidgetUtils'; import { ApprovalCard } from './primitives/ApprovalCard'; import { ExpandableSection } from './primitives/ExpandableSection'; import { toolWidgetStyles } from './primitives/toolWidgetStyles'; import { isActionableTurn, parseToolArgs } from './toolWidgetUtils'; import { EntityWriteDetails, hasEntityWriteDetails } from './widgets/EntityWriteWidget'; /** * Batch approval for 2+ pending update_entities/delete_entities calls — * native port of the web ToolCallsContainer batch UI: one checkbox per * pending call, "Approve (n)" approves the checked calls AND rejects the * unchecked ones, "Reject All" rejects everything. The web submits through a * batch endpoint that resumes each call individually; native has no batch * route, so the same per-call approved/rejected submissions go out * sequentially (approvals first — the turn resumes after the last one). */ export function EntityApprovalBatchCard({ isLastAssistantMessage, submitToolCallInput, toolCalls, }: { isLastAssistantMessage?: boolean; submitToolCallInput?: SuperagentToolRendererProps['submitToolCallInput']; toolCalls: SuperagentToolCall[]; }) { const bi = useAgentBi(); const pendingIds = toolCalls.map((toolCall) => toolCall.id).filter((id): id is string => Boolean(id)); const pendingIdsKey = pendingIds.join(','); const [selectedIds, setSelectedIds] = useState>(() => new Set(pendingIds)); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); // Keep only selections that still exist; re-select all once none remain // (web useBatchApproval parity). useEffect(() => { const currentIds = new Set(pendingIdsKey.split(',').filter(Boolean)); setSelectedIds((previous) => { const kept = new Set([...previous].filter((id) => currentIds.has(id))); return kept.size > 0 ? kept : currentIds; }); }, [pendingIdsKey]); const toggleSelection = (id: string) => { setSelectedIds((previous) => { const next = new Set(previous); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const submitDecisions = async (decisions: BatchApprovalDecision[]) => { if (!submitToolCallInput) return; setIsSubmitting(true); setError(null); let failed = 0; for (const decision of decisions) { try { const result = await submitToolCallInput(decision.toolCallId, decision.approve, {}); if (result === null) failed += 1; } catch { failed += 1; } } if (failed > 0) { setError(`Failed to process ${failed} of ${decisions.length} changes.`); } else { const approvedCount = decisions.filter((decision) => decision.approve).length; void bi.trackEditor('Entity Write Approval', { batch: true, approved_count: approvedCount, rejected_count: decisions.length - approvedCount }); } setIsSubmitting(false); }; // Stale turn (web useBatchApproval gates batching on isLastAssistantMessage): // keep the summary list, drop the buttons. const actionableTurn = isActionableTurn(isLastAssistantMessage); const canAct = Boolean(submitToolCallInput) && pendingIds.length > 0 && actionableTurn; return ( submitDecisions(buildBatchApprovalDecisions(pendingIds, selectedIds)) : undefined} onReject={canAct ? () => submitDecisions(pendingIds.map((toolCallId) => ({ toolCallId, approve: false }))) : undefined} rejectLabel="Reject All" title={`${toolCalls.length} changes pending`} unavailableNote={actionableTurn ? undefined : PREVIOUS_APPROVAL_NOTE} > {toolCalls.map((toolCall, index) => ( toggleSelection(toolCall.id!) : undefined} toolCall={toolCall} /> ))} ); } function BatchApprovalItem({ disabled, isSelected, onToggleSelect, toolCall, }: { disabled: boolean; isSelected: boolean; onToggleSelect?: () => void; toolCall: SuperagentToolCall; }) { const args = useMemo(() => parseToolArgs(toolCall), [toolCall]); const toolName = toolCall.name || ''; const summary = getEntitySummary(toolName, args); return ( {isSelected ? : null} {hasEntityWriteDetails(toolName, args) ? ( ) : ( {summary} )} ); }