import { useRef, useState } from 'react'; import type { SuperagentToolCall, SuperagentToolRendererProps } from '../types'; /** * Approve/reject submission state shared by the approval-capable widgets. * `extraUserInput` defaults to {} (web useEntityApprovalSubmit parity — the * entity/import tools ignore the user_input contents); interactive widgets * pass their tool's payload (answers, approved keys, credentials, …). */ export function useApprovalSubmit( toolCall: SuperagentToolCall, submitToolCallInput: SuperagentToolRendererProps['submitToolCallInput'], ) { const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); // React state lands on the next render, so a fast second tap could fire // submitToolCallInput twice before `isSubmitting` disables the button. The // ref flips synchronously (web PSP-hook parity) and drops the duplicate; // the state stays for the disabled/spinner rendering. const inFlightRef = useRef(false); const canAct = Boolean(toolCall.id && submitToolCallInput); // Resolves true only when the submit reached the server, so callers can fire // analytics on confirmed success. const submit = async (approve: boolean, extraUserInput: unknown = {}): Promise => { if (!toolCall.id || !submitToolCallInput) return false; if (inFlightRef.current) return false; inFlightRef.current = true; setIsSubmitting(true); setError(null); try { const result = await submitToolCallInput(toolCall.id, approve, extraUserInput); if (result === null) { // The conversation client isn't ready yet, so nothing reached the // server — surface it instead of silently no-opping. throw new Error('The conversation is still loading. Try again in a moment.'); } return true; } catch (submitError) { setError(submitError instanceof Error ? submitError.message : 'Unable to submit this approval.'); return false; } finally { inFlightRef.current = false; setIsSubmitting(false); } }; return { canAct, error, isSubmitting, submit }; }