/** * Assistant — Client * * Left sidebar: orchestrator + spawned agents hierarchy. * Main area: chat for the active agent. * * Data sources: * - Agent list: from Agent state sync (useAgent onStateUpdate) * - Chat messages & streaming: useChat with custom AgentChatTransport * - Agent CRUD + navigation: via agent.call() RPC + WS messages * * The AgentChatTransport bridges the AI SDK's useChat hook with the Agent * WebSocket connection: sendMessages() triggers the server-side RPC, then * pipes WS stream-event messages into a ReadableStream * that useChat consumes and renders. */ import "./styles.css"; import { createRoot } from "react-dom/client"; import { ThemeProvider } from "@cloudflare/agents-ui/hooks"; import { Suspense, useCallback, useState, useEffect, useRef, useMemo } from "react"; import { useAgent } from "agents/react"; import { applyChunkToParts } from "@cloudflare/think/message-builder"; import { AgentChatTransport } from "@cloudflare/think/transport"; import { useChat } from "@ai-sdk/react"; import type { UIMessage } from "ai"; import type { MCPServersState } from "agents"; import { Button, Badge, InputArea, Empty, Surface, Text } from "@cloudflare/kumo"; import { ConnectionIndicator, ModeToggle, PoweredByAgents, type ConnectionStatus } from "@cloudflare/agents-ui"; import { PaperPlaneRightIcon, ChatTextIcon, BroomIcon, InfoIcon, FolderIcon, GearIcon, PlugsConnectedIcon, WrenchIcon, SignInIcon, TrashIcon, XIcon, RobotIcon, ArrowLeftIcon, CircleNotchIcon, CheckCircleIcon, WarningCircleIcon, ArrowSquareOutIcon, FileIcon, CaretRightIcon } from "@phosphor-icons/react"; import { Streamdown } from "streamdown"; import type { AppState, AgentInfo, FileInfo } from "./server"; const ORCHESTRATOR_ID = "orchestrator"; // ─── Helpers ────────────────────────────────────────────────────────────── function getMessageText(msg: UIMessage): string { return msg.parts .filter((p): p is { type: "text"; text: string } => p.type === "text") .map((p) => p.text) .join(""); } // ─── Agent status helpers ───────────────────────────────────────────────── function AgentStatusIcon({ status }: { status: AgentInfo["status"] }) { switch (status) { case "working": return ( ); case "done": return ; case "error": return ; default: return null; } } // ─── Agent Sidebar ──────────────────────────────────────────────────────── function AgentSidebar({ agents, activeAgentId, onSwitch, onDelete, onClear, onRename }: { agents: AgentInfo[]; activeAgentId: string | null; onSwitch: (id: string) => void; onDelete: (id: string) => void; onClear: (id: string) => void; onRename: (id: string, name: string) => void; }) { const [editingId, setEditingId] = useState(null); const [editName, setEditName] = useState(""); const orchestrator = agents.find((a) => a.id === ORCHESTRATOR_ID); const subAgents = agents.filter((a) => a.id !== ORCHESTRATOR_ID); return (
Agents
{/* Orchestrator — always at top */} {orchestrator && (
onSwitch(orchestrator.id)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onSwitch(orchestrator.id); } }} >
Orchestrator
{orchestrator.messageCount > 0 && ( {orchestrator.messageCount} )}
{orchestrator.id === activeAgentId && (
)}
)} {/* Sub-agents section */} {subAgents.length > 0 && ( <>
Spawned Agents
{subAgents.map((ag) => { const isActive = ag.id === activeAgentId; return (
onSwitch(ag.id)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onSwitch(ag.id); } }} >
{editingId === ag.id ? ( setEditName(e.target.value)} onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Enter") { onRename(ag.id, editName); setEditingId(null); } if (e.key === "Escape") { setEditingId(null); } }} onBlur={() => { onRename(ag.id, editName); setEditingId(null); }} onClick={(e) => e.stopPropagation()} /> ) : ( {ag.name} )}
{ag.messageCount > 0 && editingId !== ag.id && ( {ag.messageCount} )}
{ag.lastTaskDescription && ( {ag.lastTaskDescription} )}
); })} )}
); } // ─── Messages ────────────────────────────────────────────────────────────── // ─── Orchestrator tool names for special rendering ──────────────────────── const ORCHESTRATOR_TOOLS = new Set([ "spawn_agent", "delegate_task", "hand_off" ]); function DelegationCard({ toolName, input, output, state, onNavigate }: { toolName: string; input: Record; output: unknown; state: string; onNavigate: (agentId: string) => void; }) { const out = output as Record | null | undefined; const agentId = (out?.agentId ?? input?.agent_id) as string | undefined; const isRunning = state !== "output-available"; if (toolName === "spawn_agent") { return (
Spawning: {(input.name as string) ?? "agent"} {isRunning ? ( ) : agentId ? ( ) : null}
); } if (toolName === "delegate_task") { const isDelegated = !isRunning && out?.status === "delegated"; return (
{isDelegated ? "Delegated to agent" : "Delegating to agent"} {isRunning && sending} {isDelegated && background}
{isDelegated && out?.task != null && ( {String(out.task)} )} {isDelegated && agentId && ( )} {!isRunning && out?.error != null && ( {String(out.error)} )}
); } if (toolName === "hand_off") { return (
Handing off to {(out?.name as string) ?? "agent"} {agentId && ( )}
); } // list_agents, list_available_tools — generic rendering return null; } function Messages({ messages, status, onNavigate, activeAgent }: { messages: UIMessage[]; status: string; onNavigate: (agentId: string) => void; activeAgent: AgentInfo | undefined; }) { const endRef = useRef(null); const isBusy = status === "submitted" || status === "streaming"; const isOrch = activeAgent?.id === ORCHESTRATOR_ID; useEffect(() => { endRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, isBusy]); if (messages.length === 0 && !isBusy) { return ( <>
{isOrch ? "Orchestrator Assistant" : (activeAgent?.name ?? "Agent")} {isOrch ? "An AI orchestrator that can spawn specialized agents, delegate tasks, or hand off conversations. It has workspace tools, MCP server support, and model routing (fast vs capable)." : `A specialized agent (${activeAgent?.config.modelTier ?? "fast"} model, ${activeAgent?.config.toolAccess ?? "workspace"} tools). Chat with it directly or navigate back to the orchestrator.`}
: } title="Start a conversation" description={ isOrch ? 'Try "Create a researcher agent to find info about X" or "Write a package.json for a Node.js project"' : "Send a message to start chatting with this agent" } /> ); } return (
{messages.map((msg) => (
{msg.role === "user" ? (
{getMessageText(msg)}
) : (
{msg.parts.map((part, i) => { if (part.type === "reasoning") { return (
Reasoning
{part.text}
); } if (part.type.startsWith("tool-") && "toolCallId" in part) { const tp = part as unknown as { type: string; toolCallId: string; state: string; input: unknown; output?: unknown; }; const toolName = tp.type.split("-").slice(1).join("-"); // Orchestrator tools get special delegation card rendering if (ORCHESTRATOR_TOOLS.has(toolName)) { return (
} output={tp.output} state={tp.state} onNavigate={onNavigate} />
); } return (
{toolName} {tp.state}
{tp.input != null && Object.keys(tp.input as Record) .length > 0 && (
                              {JSON.stringify(tp.input, null, 2)}
                            
)} {tp.state === "output-available" && tp.output != null && (
                              {formatToolOutput(tp.output)}
                            
)}
); } if (part.type === "text") { return ( {part.text} ); } return null; })}
)}
))} {status === "submitted" && (
Thinking...
)} {!isBusy && activeAgent?.status === "working" && (
Working on background task...
)}
); } // ─── Workspace Panel ───────────────────────────────────────────────────── function formatFileSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } interface AgentRpc { call(method: string, args?: unknown[]): Promise; } function WorkspacePanel({ agent, agentId, onClose }: { agent: AgentRpc; agentId: string; onClose: () => void; }) { const [which, setWhich] = useState<"private" | "shared">("private"); const [currentPath, setCurrentPath] = useState("/"); const [files, setFiles] = useState([]); const [selectedFile, setSelectedFile] = useState<{ path: string; content: string; } | null>(null); const [loading, setLoading] = useState(false); const [fileLoading, setFileLoading] = useState(false); // Load directory listing const loadDir = useCallback( async (path: string) => { setLoading(true); setSelectedFile(null); try { const result = (await agent.call("listWorkspaceFiles", [ agentId, which, path ])) as FileInfo[]; setFiles(result ?? []); setCurrentPath(path); } catch { setFiles([]); } finally { setLoading(false); } }, [agent, agentId, which] ); // Load file content const loadFile = useCallback( async (path: string) => { setFileLoading(true); try { const content = (await agent.call("readWorkspaceFile", [ agentId, which, path ])) as string | null; setSelectedFile({ path, content: content ?? "(empty)" }); } catch { setSelectedFile({ path, content: "(error reading file)" }); } finally { setFileLoading(false); } }, [agent, agentId, which] ); // Reload on agent/tab change useEffect(() => { loadDir("/"); }, [loadDir]); // Breadcrumb segments const segments = currentPath === "/" ? [] : currentPath.split("/").filter(Boolean); const sorted = useMemo(() => { const dirs = files.filter((f) => f.type === "directory"); const rest = files.filter((f) => f.type !== "directory"); return [...dirs, ...rest]; }, [files]); return (
{/* Header */}
Workspace
{/* Tabs */}
{/* Breadcrumbs */}
{segments.map((seg, i) => { const path = "/" + segments.slice(0, i + 1).join("/"); const isLast = i === segments.length - 1; return ( {isLast ? ( {seg} ) : ( )} ); })}
{/* File list */}
{loading ? (
) : sorted.length === 0 ? (
Empty directory
) : (
{sorted.map((file) => ( ))}
)}
{/* File viewer */} {selectedFile && (
{selectedFile.path.split("/").pop()}
{fileLoading ? (
) : (
                {selectedFile.content}
              
)}
)}
); } // ─── Messages (continued) ──────────────────────────────────────────────────── function formatToolOutput(output: unknown): string { if (typeof output === "string") return output; try { return JSON.stringify(output, null, 2); } catch { return String(output); } } // ─── Main ────────────────────────────────────────────────────────────────── function App() { const [connectionStatus, setConnectionStatus] = useState("connecting"); const [agents, setAgents] = useState([]); const [activeAgentId, setActiveAgentId] = useState(null); const [input, setInput] = useState(""); const [mcpState, setMcpState] = useState({ prompts: [], resources: [], servers: {}, tools: [] }); const [showMcpPanel, setShowMcpPanel] = useState(false); const [showWorkspace, setShowWorkspace] = useState(false); const [mcpName, setMcpName] = useState(""); const [mcpUrl, setMcpUrl] = useState(""); const [isAddingServer, setIsAddingServer] = useState(false); const mcpPanelRef = useRef(null); const setChatMessagesRef = useRef<((messages: UIMessage[]) => void) | null>( null ); // Ref for handleSwitch so navigate handler can call it without stale closure const handleSwitchRef = useRef<(id: string) => Promise>(undefined); // Track active delegation stream (server-initiated, not from useChat transport) const delegationRef = useRef<{ requestId: string; baseMessages: UIMessage[]; assistantMsg: UIMessage; } | null>(null); // Last messages received from server — used as base when a delegation stream starts const lastMessagesRef = useRef([]); const handleServerMessage = useCallback((event: MessageEvent) => { if (typeof event.data !== "string") return; try { const msg = JSON.parse(event.data); if (msg.type === "messages") { setActiveAgentId(msg.agentId); setChatMessagesRef.current?.(msg.messages); lastMessagesRef.current = msg.messages; delegationRef.current = null; } else if (msg.type === "navigate") { handleSwitchRef.current?.(msg.agentId); } else if (msg.type === "stream-start" && msg.delegation) { delegationRef.current = { requestId: msg.requestId, baseMessages: lastMessagesRef.current, assistantMsg: { id: `deleg-${msg.requestId}`, role: "assistant", parts: [] } }; } else if ( msg.type === "stream-event" && delegationRef.current?.requestId === msg.requestId ) { const d = delegationRef.current; if (!d) return; const chunk = JSON.parse(msg.event); applyChunkToParts(d.assistantMsg.parts, chunk); setChatMessagesRef.current?.([ ...d.baseMessages, { ...d.assistantMsg, parts: [...d.assistantMsg.parts] } ]); } else if ( msg.type === "stream-done" && delegationRef.current?.requestId === msg.requestId ) { if (msg.error && delegationRef.current) { const d = delegationRef.current; d.assistantMsg.parts.push({ type: "text", text: `\n\nError: ${msg.error}` } as UIMessage["parts"][number]); setChatMessagesRef.current?.([ ...d.baseMessages, { ...d.assistantMsg, parts: [...d.assistantMsg.parts] } ]); } delegationRef.current = null; } } catch (err) { console.error(`[CLIENT] handleServerMessage error`, err); } }, []); const agent = useAgent({ agent: "MyAssistant", onOpen: useCallback(() => setConnectionStatus("connected"), []), onClose: useCallback(() => setConnectionStatus("disconnected"), []), onError: useCallback( (error: Event) => console.error("WebSocket error:", error), [] ), onStateUpdate: useCallback( (state: AppState) => setAgents(state.agents), [] ), onMessage: handleServerMessage, onMcpUpdate: useCallback((state: MCPServersState) => { setMcpState(state); }, []) }); // Auto-select orchestrator on first connect useEffect(() => { if (connectionStatus === "connected" && !activeAgentId) { agent.call("switchAgent", [ORCHESTRATOR_ID]).catch(() => {}); } }, [connectionStatus, activeAgentId, agent]); // Close MCP panel when clicking outside useEffect(() => { if (!showMcpPanel) return; function handleClickOutside(e: MouseEvent) { if ( mcpPanelRef.current && !mcpPanelRef.current.contains(e.target as Node) ) { setShowMcpPanel(false); } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [showMcpPanel]); const handleAddServer = useCallback(async () => { if (!mcpName.trim() || !mcpUrl.trim()) return; setIsAddingServer(true); try { await agent.call("addServer", [ mcpName.trim(), mcpUrl.trim(), window.location.origin ]); setMcpName(""); setMcpUrl(""); } catch (e) { console.error("Failed to add MCP server:", e); } finally { setIsAddingServer(false); } }, [agent, mcpName, mcpUrl]); const handleRemoveServer = useCallback( async (serverId: string) => { try { await agent.call("removeServer", [serverId]); } catch (e) { console.error("Failed to remove MCP server:", e); } }, [agent] ); const serverEntries = Object.entries(mcpState.servers); const mcpToolCount = mcpState.tools.length; const transport = useMemo(() => new AgentChatTransport(agent), [agent]); const { messages, setMessages: setChatMessages, sendMessage, resumeStream, status } = useChat({ transport }); setChatMessagesRef.current = setChatMessages; const isConnected = connectionStatus === "connected"; const isBusy = status === "submitted" || status === "streaming"; const handleDelete = useCallback( async (id: string) => { transport.detach(); await agent.call("deleteAgent", [id]); if (activeAgentId === id) { setActiveAgentId(ORCHESTRATOR_ID); setChatMessages([]); } }, [agent, activeAgentId, setChatMessages, transport] ); const handleClear = useCallback( async (id: string) => agent.call("clearAgent", [id]), [agent] ); const handleRename = useCallback( async (id: string, name: string) => agent.call("renameAgent", [id, name]), [agent] ); const handleSwitch = useCallback( async (id: string) => { transport.detach(); await agent.call("switchAgent", [id]); // Skip resumeStream when viewing a delegation stream — resumeStream // finds no user-initiated stream and resets useChat messages, wiping // the delegation text we're already displaying. if (!delegationRef.current) { resumeStream(); } }, [agent, transport, resumeStream] ); handleSwitchRef.current = handleSwitch; const send = useCallback(() => { const text = input.trim(); if (!text || isBusy || !activeAgentId) return; setInput(""); sendMessage({ text }); }, [input, isBusy, activeAgentId, sendMessage]); const activeAgent = agents.find((a) => a.id === activeAgentId); const isOrchestrator = activeAgentId === ORCHESTRATOR_ID; return (
{/* Left: Agent sidebar */}
{/* Main: Chat */}
{activeAgent ? ( <> {isOrchestrator ? ( ) : ( )} {activeAgent.name} {activeAgent.messageCount > 0 && ( {activeAgent.messageCount} messages )} {!isOrchestrator && ( <> {activeAgent.config.modelTier} {activeAgent.config.toolAccess} )} {!isOrchestrator && ( )} ) : ( Connecting... )}
{showMcpPanel && (
MCP Servers {serverEntries.length > 0 && ( {serverEntries.length} )}
{ e.preventDefault(); handleAddServer(); }} className="space-y-2" > setMcpName(e.target.value)} placeholder="Server name" className="w-full px-3 py-1.5 text-sm rounded-lg border border-kumo-line bg-kumo-base text-kumo-default placeholder:text-kumo-inactive focus:outline-none focus:ring-1 focus:ring-kumo-accent" />
setMcpUrl(e.target.value)} placeholder="https://mcp.example.com" className="flex-1 px-3 py-1.5 text-sm rounded-lg border border-kumo-line bg-kumo-base text-kumo-default placeholder:text-kumo-inactive focus:outline-none focus:ring-1 focus:ring-kumo-accent font-mono" />
{serverEntries.length > 0 && (
{serverEntries.map(([id, server]) => (
{server.name} {server.state}
{server.server_url} {server.state === "failed" && server.error && ( {server.error} )}
{server.state === "authenticating" && server.auth_url && ( )}
))}
)} {mcpToolCount > 0 && (
{mcpToolCount} tool {mcpToolCount !== 1 ? "s" : ""} available from MCP servers
)}
)}
{activeAgent && ( )} {activeAgent && ( )}
{activeAgentId ? ( ) : ( } title="Connecting..." description="Waiting for connection to the orchestrator" /> )}
{ e.preventDefault(); send(); }} className="max-w-3xl mx-auto px-5 py-4" >
{ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} placeholder={ activeAgentId ? isOrchestrator ? "Ask me anything, or tell me to spawn a specialist agent..." : "Chat with this agent..." : "Connecting..." } disabled={!isConnected || isBusy || !activeAgentId} rows={2} className="flex-1 ring-0! focus:ring-0! shadow-none! bg-transparent! outline-none!" />
{/* Right: Workspace panel */} {showWorkspace && activeAgentId && ( setShowWorkspace(false)} /> )}
); } export default function AppRoot() { return ( Loading...
} > ); } const root = document.getElementById("root")!; createRoot(root).render( );