branch:
client.tsx
16646 bytesRaw
/**
* Sandbox Example — Client
*
* Split layout: chat on the left, execution log + customer data on the right.
* The agent writes code that runs in a sandboxed isolate — the execution log
* shows the code and its captured console output.
*/
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,
GearIcon,
CodeIcon,
DatabaseIcon,
TerminalIcon,
PlayIcon
} from "@phosphor-icons/react";
import { Streamdown } from "streamdown";
import type { SandboxState, ExecutionRecord, CustomerRecord } from "./server";
function getMessageText(message: UIMessage): string {
return message.parts
.filter((part) => part.type === "text")
.map((part) => (part as { type: "text"; text: string }).text)
.join("");
}
// ─── Execution Log ─────────────────────────────────────────────────────────
function ExecutionLog({ executions }: { executions: ExecutionRecord[] }) {
if (executions.length === 0) {
return (
<Empty
icon={<TerminalIcon size={32} />}
title="No executions yet"
description='Ask the agent to write code — e.g. "List all Gold tier customers"'
/>
);
}
return (
<div className="space-y-3">
{executions.map((exec) => (
<Surface key={exec.id} className="rounded-lg overflow-hidden">
<div className="flex items-center gap-2 px-3 py-2 border-b border-kumo-line">
<CodeIcon size={14} className="text-kumo-brand" />
<Text size="xs" bold>
Code
</Text>
{exec.error ? (
<Badge variant="destructive">Error</Badge>
) : (
<Badge variant="primary">OK</Badge>
)}
<span className="ml-auto text-xs text-kumo-secondary">
{exec.timestamp}
</span>
</div>
{/* Code */}
<pre className="px-3 py-2 text-xs font-mono bg-kumo-elevated overflow-x-auto whitespace-pre-wrap border-b border-kumo-line">
{exec.code.length > 500
? exec.code.slice(0, 500) + "\n// ... truncated"
: exec.code}
</pre>
{/* Output */}
<div className="px-3 py-2">
<div className="flex items-center gap-2 mb-1">
<TerminalIcon size={12} className="text-kumo-inactive" />
<Text size="xs" variant="secondary" bold>
Output
</Text>
</div>
<pre className="text-xs font-mono whitespace-pre-wrap text-kumo-default">
{exec.output || "(no output)"}
</pre>
</div>
</Surface>
))}
</div>
);
}
// ─── Customer Table ────────────────────────────────────────────────────────
function CustomerTable({ customers }: { customers: CustomerRecord[] }) {
if (customers.length === 0) {
return (
<Empty
icon={<DatabaseIcon size={32} />}
title="No customers"
description="The database is empty"
/>
);
}
return (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-kumo-line">
<th className="text-left py-2 px-2 font-medium text-kumo-secondary">
Name
</th>
<th className="text-left py-2 px-2 font-medium text-kumo-secondary">
Email
</th>
<th className="text-left py-2 px-2 font-medium text-kumo-secondary">
Tier
</th>
<th className="text-left py-2 px-2 font-medium text-kumo-secondary">
Region
</th>
</tr>
</thead>
<tbody>
{customers.map((c) => (
<tr key={c.id} className="border-b border-kumo-line/50">
<td className="py-2 px-2">{c.name}</td>
<td className="py-2 px-2 text-kumo-secondary">{c.email}</td>
<td className="py-2 px-2">
<Badge variant={c.tier === "Gold" ? "primary" : "secondary"}>
{c.tier}
</Badge>
</td>
<td className="py-2 px-2 text-kumo-secondary">{c.region}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
// ─── Main ──────────────────────────────────────────────────────────────────
function ChatPanel() {
const [connectionStatus, setConnectionStatus] =
useState<ConnectionStatus>("connecting");
const [input, setInput] = useState("");
const [sandboxState, setSandboxState] = useState<SandboxState | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const agent = useAgent<SandboxState>({
agent: "SandboxAgent",
onOpen: useCallback(() => setConnectionStatus("connected"), []),
onClose: useCallback(() => setConnectionStatus("disconnected"), []),
onError: useCallback(
(error: Event) => console.error("WebSocket error:", error),
[]
),
onStateUpdate: useCallback(
(state: SandboxState) => setSandboxState(state),
[]
)
});
const { messages, sendMessage, clearHistory, status } = useAgentChat({
agent
});
const isStreaming = status === "streaming";
const isConnected = connectionStatus === "connected";
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const send = useCallback(() => {
const text = input.trim();
if (!text || isStreaming) return;
setInput("");
sendMessage({ role: "user", parts: [{ type: "text", text }] });
}, [input, isStreaming, sendMessage]);
const [rightTab, setRightTab] = useState<"executions" | "data">("executions");
return (
<div className="flex h-screen bg-kumo-elevated">
{/* Left: Chat */}
<div className="flex flex-col flex-1 min-w-0 border-r border-kumo-line">
<header className="px-5 py-4 bg-kumo-base border-b border-kumo-line">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Text size="lg" bold>
Sandbox
</Text>
<Badge variant="secondary">
<PlayIcon size={12} weight="bold" className="mr-1" />
Dynamic Code Execution
</Badge>
</div>
<div className="flex items-center gap-3">
<ConnectionIndicator status={connectionStatus} />
<ModeToggle />
<Button
variant="secondary"
icon={<TrashIcon size={16} />}
onClick={clearHistory}
>
Clear
</Button>
</div>
</div>
</header>
<div className="flex-1 overflow-y-auto">
<div className="max-w-2xl mx-auto px-5 py-6 space-y-5">
{messages.length === 0 && (
<Empty
icon={<CodeIcon size={32} />}
title="Talk to the sandbox agent"
description={`Try: "Write code to count customers by tier" or "Find customers whose email contains 'example'"`}
/>
)}
{messages.map((message, index) => {
const isUser = message.role === "user";
const isLastAssistant =
message.role === "assistant" && index === messages.length - 1;
return (
<div key={message.id} className="space-y-2">
{isUser ? (
<div className="flex justify-end">
<div className="max-w-[85%] px-4 py-2.5 rounded-2xl rounded-br-md bg-kumo-contrast text-kumo-inverse leading-relaxed">
{getMessageText(message)}
</div>
</div>
) : (
getMessageText(message) && (
<div className="flex justify-start">
<div className="max-w-[85%] rounded-2xl rounded-bl-md bg-kumo-base text-kumo-default leading-relaxed overflow-hidden">
<Streamdown
className="sd-theme px-4 py-2.5"
controls={false}
isAnimating={isLastAssistant && isStreaming}
>
{getMessageText(message)}
</Streamdown>
</div>
</div>
)
)}
{message.parts
.filter((part) => isToolUIPart(part))
.map((part) => {
if (!isToolUIPart(part)) return null;
const toolName = getToolName(part);
if (part.state === "output-available") {
return (
<div
key={part.toolCallId}
className="flex justify-start"
>
<Surface className="max-w-[85%] px-3 py-2 rounded-lg ring ring-kumo-line">
<div className="flex items-center gap-2">
{toolName === "executeCode" ? (
<PlayIcon
size={14}
className="text-kumo-brand"
/>
) : (
<DatabaseIcon
size={14}
className="text-kumo-positive"
/>
)}
<Text size="xs" variant="secondary">
{toolName === "executeCode"
? "Code executed in sandbox"
: "Query executed"}
</Text>
</div>
</Surface>
</div>
);
}
if (
part.state === "input-available" ||
part.state === "input-streaming"
) {
return (
<div
key={part.toolCallId}
className="flex justify-start"
>
<Surface className="max-w-[85%] px-3 py-2 rounded-lg ring ring-kumo-line">
<div className="flex items-center gap-2">
<GearIcon
size={14}
className="text-kumo-inactive animate-spin"
/>
<Text size="xs" variant="secondary">
{toolName === "executeCode"
? "Running code in sandbox..."
: `Running ${toolName}...`}
</Text>
</div>
</Surface>
</div>
);
}
return null;
})}
</div>
);
})}
<div ref={messagesEndRef} />
</div>
</div>
<div className="border-t border-kumo-line bg-kumo-base">
<form
onSubmit={(e) => {
e.preventDefault();
send();
}}
className="max-w-2xl mx-auto px-5 py-4"
>
<div className="flex items-end gap-3 rounded-xl border border-kumo-line bg-kumo-base p-3 shadow-sm focus-within:ring-2 focus-within:ring-kumo-ring focus-within:border-transparent transition-shadow">
<InputArea
value={input}
onValueChange={setInput}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
send();
}
}}
placeholder='Try: "Write code to count customers by region"'
disabled={!isConnected || isStreaming}
rows={2}
className="flex-1 !ring-0 focus:!ring-0 !shadow-none !bg-transparent !outline-none"
/>
<Button
type="submit"
variant="primary"
shape="square"
aria-label="Send message"
disabled={!input.trim() || !isConnected || isStreaming}
icon={<PaperPlaneRightIcon size={18} />}
loading={isStreaming}
className="mb-0.5"
/>
</div>
</form>
<div className="flex justify-center pb-3">
<PoweredByAgents />
</div>
</div>
</div>
{/* Right: Execution Log + Data */}
<div className="w-[420px] flex flex-col bg-kumo-base shrink-0">
<div className="flex border-b border-kumo-line">
<button
onClick={() => setRightTab("executions")}
className={`flex-1 py-3 px-4 text-sm font-medium transition-colors relative ${
rightTab === "executions"
? "text-kumo-default"
: "text-kumo-secondary hover:text-kumo-default"
}`}
>
<span className="flex items-center justify-center gap-2">
<TerminalIcon size={16} />
Executions
{sandboxState && sandboxState.executions.length > 0 && (
<Badge variant="secondary">
{sandboxState.executions.length}
</Badge>
)}
</span>
{rightTab === "executions" && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-kumo-brand" />
)}
</button>
<button
onClick={() => setRightTab("data")}
className={`flex-1 py-3 px-4 text-sm font-medium transition-colors relative ${
rightTab === "data"
? "text-kumo-default"
: "text-kumo-secondary hover:text-kumo-default"
}`}
>
<span className="flex items-center justify-center gap-2">
<DatabaseIcon size={16} />
Customers
{sandboxState && (
<Badge variant="secondary">
{sandboxState.customers.length}
</Badge>
)}
</span>
{rightTab === "data" && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-kumo-brand" />
)}
</button>
</div>
<div className="flex-1 overflow-y-auto p-4">
{rightTab === "executions" && sandboxState && (
<ExecutionLog executions={sandboxState.executions} />
)}
{rightTab === "data" && sandboxState && (
<CustomerTable customers={sandboxState.customers} />
)}
{!sandboxState && (
<div className="flex items-center justify-center h-32">
<Text variant="secondary">Connecting...</Text>
</div>
)}
</div>
</div>
</div>
);
}
export default function App() {
return (
<Suspense
fallback={
<div className="flex items-center justify-center h-screen text-kumo-inactive">
Loading...
</div>
}
>
<ChatPanel />
</Suspense>
);
}