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 (
Inputs
{argsText}
Result
{typeof result === "string" ? result : serializedResult}