/**
* Gatekeeper Example — Client
*
* Split-panel layout:
* Left: AI chat (talk to the agent, ask it to query/modify the database)
* Right: Approval queue + customer data (see pending actions, approve/reject/revert)
*
* The chat and the approval queue are connected through the Agent's state sync.
* When the agent proposes an action, it appears in the queue in real-time.
* When a human approves/rejects, the database updates and the state syncs back.
*
* This demonstrates the Gatekeeper pattern's key UX property: the human always
* has a clear view of what the agent wants to do, and full control over whether
* it happens.
*/
import { Suspense, useCallback, useState, useEffect, useRef } from "react";
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
import { isToolUIPart, getToolName } from "ai";
import type { UIMessage } from "ai";
import {
Button,
Badge,
InputArea,
Empty,
Surface,
Text
} from "@cloudflare/kumo";
import {
ConnectionIndicator,
ModeToggle,
PoweredByAgents,
type ConnectionStatus
} from "@cloudflare/agents-ui";
import {
PaperPlaneRightIcon,
TrashIcon,
CheckCircleIcon,
XCircleIcon,
GearIcon,
ShieldCheckIcon,
ClockIcon,
ArrowCounterClockwiseIcon,
DatabaseIcon,
EyeIcon,
WarningIcon,
MagnifyingGlassIcon
} from "@phosphor-icons/react";
import { Streamdown } from "streamdown";
import type { GatekeeperState, ActionEntry, CustomerRecord } from "./server";
// ─── Helpers ───────────────────────────────────────────────────────────────
function getMessageText(message: UIMessage): string {
return message.parts
.filter((part) => part.type === "text")
.map((part) => (part as { type: "text"; text: string }).text)
.join("");
}
function timeAgo(dateStr: string): string {
const seconds = Math.floor(
(Date.now() - new Date(dateStr + "Z").getTime()) / 1000
);
if (seconds < 60) return "just now";
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
return `${Math.floor(seconds / 86400)}d ago`;
}
// ─── Action Queue Panel ────────────────────────────────────────────────────
/**
* The approval queue UI. This is the human's view into the Gatekeeper.
*
* Each pending action shows:
* - What it does (title + description)
* - The exact SQL that will run (full transparency)
* - Approve / Reject buttons
*
* Approved actions show a Revert button if the action is revertable.
* The entire history serves as an audit log.
*/
function ActionQueue({
actions,
onApprove,
onReject,
onRevert
}: {
actions: ActionEntry[];
onApprove: (id: number) => void;
onReject: (id: number) => void;
onRevert: (id: number) => void;
}) {
const pending = actions.filter((a) => a.state === "pending");
const resolved = actions.filter((a) => a.state !== "pending");
return (
{/* Pending actions — these need human attention */}
{pending.length > 0 && (
{/* Show revert button for approved, revertable actions */}
{action.state === "approved" &&
action.canRevert &&
action.type === "action" && (
}
onClick={() => onRevert(action.id)}
>
Revert
)}
{action.resolvedAt && (
{timeAgo(action.resolvedAt)}
)}
))}
)}
{actions.length === 0 && (
}
title="No actions yet"
description="Ask the agent to modify the database — actions will appear here for approval"
/>
)}
);
}
// ─── Customer Table ────────────────────────────────────────────────────────
/**
* Shows the current state of the customer database.
* Updates in real-time as actions are approved/reverted.
*/
function CustomerTable({ customers }: { customers: CustomerRecord[] }) {
if (customers.length === 0) {
return (
}
title="No customers"
description="The database is empty"
/>
);
}
return (