import type { ToolCallMessagePartComponent } from "@assistant-ui/react"; import { useAtomValue, useSetAtom } from "jotai"; import { CheckIcon, ChevronDownIcon, ChevronUpIcon, RotateCcw, XCircleIcon } from "lucide-react"; import { useMemo, useState } from "react"; import { toast } from "sonner"; import { agentActionByToolCallIdAtom, markAgentActionRevertedAtom, } from "@/atoms/chat/agent-actions.atom"; import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom"; import { DoomLoopApprovalToolUI, isDoomLoopInterrupt, } from "@/components/tool-ui/doom-loop-approval"; import { GenericHitlApprovalToolUI } from "@/components/tool-ui/generic-hitl-approval"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { getToolDisplayName, getToolIcon } from "@/contracts/enums/toolIcons"; import { agentActionsApiService } from "@/lib/apis/agent-actions-api.service"; import { AppError } from "@/lib/error"; import { isInterruptResult } from "@/lib/hitl"; import { cn } from "@/lib/utils"; /** * Inline Revert button rendered on a tool card when the matching * ``AgentActionLog`` row is reversible and hasn't been reverted yet. * Reads from the SSE side-channel atom keyed by the synthetic * ``toolCallId`` so it lights up even when ``GET /threads/.../actions`` * is gated behind ``SURFSENSE_ENABLE_ACTION_LOG=False`` (503). */ function ToolCardRevertButton({ toolCallId }: { toolCallId: string }) { const session = useAtomValue(chatSessionStateAtom); const actionMap = useAtomValue(agentActionByToolCallIdAtom); const markReverted = useSetAtom(markAgentActionRevertedAtom); const action = actionMap.get(toolCallId); const [isReverting, setIsReverting] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false); if (!action) return null; if (!action.reversible) return null; if (action.revertedByActionId !== null) return null; if (action.isRevertAction) return null; if (action.error) return null; const threadId = session?.threadId; if (!threadId) return null; const handleRevert = async () => { setIsReverting(true); try { const response = await agentActionsApiService.revert(threadId, action.id); markReverted({ id: action.id, newActionId: response.new_action_id ?? null }); toast.success(response.message || "Action reverted."); } catch (err) { // 503 means revert is gated off on this deployment — hide the // button silently rather than nagging the user. Any other error // is surfaced as a toast so the operator can investigate. if (err instanceof AppError && err.status === 503) { return; } const message = err instanceof AppError ? err.message : err instanceof Error ? err.message : "Failed to revert action."; toast.error(message); } finally { setIsReverting(false); setConfirmOpen(false); } }; return ( Revert this action? This will undo{" "} {getToolDisplayName(action.toolName)} and add a new entry to the history. Your chat is preserved — only the changes the agent made to your knowledge base or connected apps will be rolled back where possible. Cancel { e.preventDefault(); handleRevert(); }} disabled={isReverting} > {isReverting ? "Reverting…" : "Revert"} ); } const DefaultToolFallbackInner: ToolCallMessagePartComponent = ({ toolCallId, toolName, argsText, result, status, }) => { const [isExpanded, setIsExpanded] = useState(false); const isCancelled = status?.type === "incomplete" && status.reason === "cancelled"; const isError = status?.type === "incomplete" && status.reason === "error"; const isRunning = status?.type === "running" || status?.type === "requires-action"; const errorData = status?.type === "incomplete" ? status.error : undefined; const serializedError = useMemo( () => (errorData && typeof errorData !== "string" ? JSON.stringify(errorData) : null), [errorData] ); const serializedResult = useMemo( () => result !== undefined && typeof result !== "string" ? JSON.stringify(result, null, 2) : null, [result] ); const cancelledReason = isCancelled && status.error ? typeof status.error === "string" ? status.error : serializedError : null; const errorReason = isError && status.error ? typeof status.error === "string" ? status.error : serializedError : null; const Icon = getToolIcon(toolName); const displayName = getToolDisplayName(toolName); return (
{isExpanded && !isRunning && ( <>
{argsText && (

Inputs

									{argsText}
								
)} {!isCancelled && result !== undefined && ( <>

Result

										{typeof result === "string" ? result : serializedResult}
									
)}
)}
); }; export const ToolFallback: ToolCallMessagePartComponent = (props) => { if (isInterruptResult(props.result)) { if (isDoomLoopInterrupt(props.result)) { return ; } return ; } return ; };