import { useAgent } from "agents/react"; import { useState, useEffect, useCallback, useRef } from "react"; import { Button, Input, Badge, Text, Empty } from "@cloudflare/kumo"; import { ConnectionIndicator, ModeToggle, PoweredByAgents } from "@cloudflare/agents-ui"; import type { ConnectionStatus } from "@cloudflare/agents-ui"; import type { TaskAgent, WorkflowItem, WorkflowPage, WorkflowUpdate } from "./server"; // Local progress type without index signature for type-safe JSX rendering type ProgressInfo = { step?: string; status?: string; message?: string; percent?: number; }; // UI-safe workflow type with explicit result type for rendering type WorkflowCardData = Omit & { result?: Record; progress: ProgressInfo | null; }; // Client-side pagination state type PaginationState = { workflows: WorkflowItem[]; total: number; nextCursor: string | null; }; const initialPagination: PaginationState = { workflows: [], total: 0, nextCursor: null }; type Toast = { message: string; type: "error" | "info"; }; /** Format the "not supported in local dev" message, or fall back to a generic one. */ function localDevMessage(err: unknown, fallback: string): string { if ( err instanceof Error && err.message.includes("not supported in local development") ) { return `${fallback.replace("Failed to ", "")} is not supported in local dev. Deploy to Cloudflare to use this feature.`; } return fallback; } export default function App() { const [taskName, setTaskName] = useState(""); const [pagination, setPagination] = useState(initialPagination); const [connectionStatus, setConnectionStatus] = useState("connecting"); const [toast, setToast] = useState(null); const [loading, setLoading] = useState(false); const toastTimer = useRef | null>(null); const connected = connectionStatus === "connected"; const showToast = useCallback( (message: string, type: Toast["type"] = "error") => { if (toastTimer.current) clearTimeout(toastTimer.current); setToast({ message, type }); toastTimer.current = setTimeout(() => setToast(null), 5000); }, [] ); // Handle real-time updates from server const handleMessage = useCallback( (message: MessageEvent) => { try { const data = JSON.parse(message.data); if (data?.type === "warning" && data?.message) { showToast(data.message, "error"); return; } if (data?.type === "workflows:cleared") { setPagination((prev) => ({ ...prev, workflows: prev.workflows.filter( (w) => w.status !== "complete" && w.status !== "errored" ), total: prev.total - (data.count || 0) })); return; } const update = data as WorkflowUpdate; if (update?.type === "workflow:added") { setPagination((prev) => { const exists = prev.workflows.some( (w) => w.workflowId === update.workflow.workflowId ); if (exists) return prev; return { ...prev, workflows: [update.workflow, ...prev.workflows], total: prev.total + 1 }; }); } else if (update?.type === "workflow:updated") { setPagination((prev) => ({ ...prev, workflows: prev.workflows.map((w) => w.workflowId === update.workflowId ? { ...w, ...update.updates } : w ) })); } else if (update?.type === "workflow:removed") { setPagination((prev) => ({ ...prev, workflows: prev.workflows.filter( (w) => w.workflowId !== update.workflowId ), total: Math.max(0, prev.total - 1) })); } } catch { // Ignore non-JSON messages } }, [showToast] ); const agent = useAgent>({ agent: "TaskAgent", onMessage: handleMessage, onOpen: () => setConnectionStatus("connected"), onClose: () => setConnectionStatus("disconnected") }); /** Wrapper around agent.call with try/catch and local-dev error handling. */ const callAgent = useCallback( async (method: string, args: unknown[], errorLabel?: string) => { try { // @ts-expect-error - callable method typing return await agent.call(method, args); } catch (err) { if (errorLabel) { showToast(localDevMessage(err, errorLabel)); } else { console.error(`Failed to call ${method}:`, err); } } }, [agent, showToast] ); // Fetch initial page on connect useEffect(() => { if (!connected) return; const fetchInitial = async () => { const page = (await callAgent("listWorkflows", [])) as | WorkflowPage | undefined; if (page) { setPagination({ workflows: page.workflows, total: page.total, nextCursor: page.nextCursor }); } }; fetchInitial(); }, [connected, callAgent]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!taskName.trim()) return; await callAgent("submitTask", [taskName]); setTaskName(""); }; const handleLoadMore = async () => { if (!pagination.nextCursor || loading) return; setLoading(true); try { const page = (await callAgent("listWorkflows", [ pagination.nextCursor, 5 ])) as WorkflowPage | undefined; if (page) { // Silently deduplicate in case of pagination overlap const existingIds = new Set( pagination.workflows.map((w) => w.workflowId) ); const newWorkflows = page.workflows.filter( (w) => !existingIds.has(w.workflowId) ); setPagination((prev) => ({ workflows: [...prev.workflows, ...newWorkflows], total: page.total, nextCursor: page.nextCursor })); } } finally { setLoading(false); } }; const handleClearCompleted = () => callAgent("clearCompleted", []); const hasCompletedWorkflows = pagination.workflows.some( (w) => w.status === "complete" || w.status === "errored" ); return (
{/* Header */}
Workflow Demo

Multiple Concurrent Workflows with Human-in-the-Loop Approval

{/* Task submission form */}
setTaskName(e.target.value)} placeholder="Enter task name (e.g., 'Generate Report')" aria-label="Task name" className="flex-1" />
{/* Workflow list header */} {pagination.workflows.length > 0 && (
Workflows ({pagination.workflows.length} {pagination.total > pagination.workflows.length && ` of ${pagination.total}`} ) {hasCompletedWorkflows && ( )}
)} {/* Workflow list */}
{pagination.workflows.map((workflow) => ( ))}
{/* Load more button */} {pagination.nextCursor && (
)} {/* Empty state */} {pagination.workflows.length === 0 && ( )} {/* Feature list */}

This demo shows:

  • Multiple concurrent workflows with{" "} runWorkflow()
  • Paginated workflow list via{" "} getWorkflows()
  • Typed progress reporting with{" "} reportProgress()
  • Human-in-the-loop with{" "} waitForApproval()
  • Per-workflow approve/reject via{" "} approveWorkflow()
  • Workflow termination via{" "} terminateWorkflow()
{/* Toast notification */} {toast && (
{toast.message}
)}
); } // Status badge variant mapping const statusBadgeConfig: Record< WorkflowItem["status"], { label: string; variant: React.ComponentProps["variant"] } > = { queued: { label: "Queued", variant: "secondary" }, running: { label: "Running", variant: "primary" }, waiting: { label: "Awaiting Approval", variant: "beta" }, complete: { label: "Complete", variant: "outline" }, errored: { label: "Error", variant: "destructive" }, paused: { label: "Paused", variant: "secondary" } }; function StatusBadge({ status }: { status: WorkflowItem["status"] }) { const { label, variant } = statusBadgeConfig[status]; return {label}; } /** Restart + Dismiss buttons used for both completed and errored workflows. */ function WorkflowEndActions({ workflowId, callAgent }: { workflowId: string; callAgent: ( method: string, args: unknown[], errorLabel?: string ) => Promise; }) { return (
); } // Workflow card component function WorkflowCard({ workflow: rawWorkflow, callAgent }: { workflow: WorkflowItem; callAgent: ( method: string, args: unknown[], errorLabel?: string ) => Promise; }) { const [rejectReason, setRejectReason] = useState(""); // Transform WorkflowItem to UI-safe types for rendering const workflow: WorkflowCardData = { ...rawWorkflow, result: rawWorkflow.result != null && typeof rawWorkflow.result === "object" && !Array.isArray(rawWorkflow.result) ? (rawWorkflow.result as Record) : undefined, progress: rawWorkflow.progress as ProgressInfo | null }; const percent = workflow.progress?.percent ?? 0; const message = workflow.progress?.message ?? "Processing..."; const id = workflow.workflowId; const borderClass = workflow.status === "waiting" ? "border-kumo-brand/40" : workflow.status === "complete" ? "border-green-500/30" : workflow.status === "errored" ? "border-red-500/30" : "border-kumo-line"; return (
{/* Header with task name and status */}
{workflow.taskName} {id.slice(0, 8)}
{/* Progress bar for running/waiting/queued workflows */} {(workflow.status === "running" || workflow.status === "waiting" || workflow.status === "queued") && (
{Math.round(percent * 100)}% – {message}
)} {/* Action buttons for running/queued workflows */} {(workflow.status === "running" || workflow.status === "queued") && !workflow.waitingForApproval && (
)} {/* Resume button for paused workflows */} {workflow.status === "paused" && (
)} {/* Approval buttons */} {workflow.waitingForApproval && (
setRejectReason(e.target.value)} placeholder="Reason (optional)" aria-label="Rejection reason" className="min-w-[120px] flex-1" />
)} {/* Completed result */} {workflow.status === "complete" && workflow.result && (
            {JSON.stringify(workflow.result, null, 2)}
          
)} {/* Error display */} {workflow.status === "errored" && (

{workflow.error?.message}

)}
); }