組み込みの開発UIでも十分だが、独自のフロントエンドを作りたい場合はnimbus-reactパッケージが提供するReactフックを使う。
ReactのUIはViteでビルドし、nimbus devで起動したエージェントのバックエンドにWebSocketで接続する。
Terminal
pnpm add @yuheinakasaka/nimbus-react react react-dom
pnpm add -D vite @vitejs/plugin-react @types/react @types/react-dom
このチュートリアルでは、エージェントのバックエンド(nimbus dev、ポート2525)と、
ReactのUIを配信するViteの開発サーバー(ポート5173)の2つのプロセスを並行して動かす。
UIはフックを通じてバックエンドに接続する。まずVite用の設定ファイルを追加する。
vite.config.ts
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react()],
server: { port: 5173 },
});
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>My Agent</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
src/main.tsx
import { createRoot } from "react-dom/client";
import App from "./App";
const container = document.getElementById("root");
if (!container) throw new Error("Root element #root not found");
createRoot(container).render(<App />);
あわせてpackage.jsonのscriptsにUI起動用のコマンドを追加する。
package.json
{
"scripts": {
"dev": "nimbus dev",
"dev:ui": "vite",
"deploy": "nimbus deploy"
}
}
useAgent / useAgentChat
useAgentがWebSocket接続の管理を担い、useAgentChatがメッセージ履歴・入力状態・ローディング状態・ツール承認フローを一括で提供する。
src/App.tsx
import { useEffect, useRef } from "react";
import { useAgent, useAgentChat } from "@yuheinakasaka/nimbus-react";
export default function App() {
// nimbus dev のバックエンド(:2525)に接続する。
// 本番では同一オリジンになるため host は省略できる。
const agent = useAgent({
agent: "chat",
host: import.meta.env.DEV ? "localhost:2525" : undefined,
});
const {
messages, input, handleInputChange,
handleSubmit, isLoading, error,
} = useAgentChat({ agent });
const endRef = useRef<HTMLDivElement>(null);
useEffect(() => {
endRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
return (
<div style={{ maxWidth: 640, margin: "0 auto", padding: 24 }}>
<h1>My Agent</h1>
<p>{agent.connected ? "接続中" : "接続待ち..."}</p>
<div style={{
minHeight: 300, border: "1px solid #ddd",
borderRadius: 8, padding: 16, marginBottom: 16,
}}>
{messages.map((msg) => (
<div key={msg.id} style={{
marginBottom: 12,
textAlign: msg.role === "user" ? "right" : "left",
}}>
{msg.toolCalls?.map((tc) => (
<div key={tc.toolCallId} style={{
fontFamily: "monospace", fontSize: 12,
color: "#6d5a00", background: "#fff8e1",
border: "1px solid #f0e0a0", borderRadius: 6,
padding: "4px 8px", marginBottom: 4,
}}>
🔧 {tc.toolName}({JSON.stringify(tc.args)})
{tc.result ? ` → ${JSON.stringify(tc.result)}` : ""}
</div>
))}
<div style={{
display: "inline-block",
padding: "8px 12px",
borderRadius: 12,
background: msg.role === "user"
? "#3968A8" : "#f0f0f0",
color: msg.role === "user"
? "#fff" : "#000",
maxWidth: "80%", whiteSpace: "pre-wrap",
}}>
{msg.content || (isLoading ? "考え中..." : "")}
</div>
</div>
))}
<div ref={endRef} />
</div>
{error && (
<p style={{ color: "red" }}>{error.message}</p>
)}
<form onSubmit={handleSubmit}
style={{ display: "flex", gap: 8 }}>
<input
value={input}
onChange={handleInputChange}
placeholder="メッセージを入力..."
disabled={!agent.connected}
style={{
flex: 1, padding: 8,
borderRadius: 6,
border: "1px solid #ccc",
}}
/>
<button type="submit"
disabled={isLoading || !agent.connected}>
{isLoading ? "..." : "送信"}
</button>
</form>
</div>
);
}
Workerのエントリポイントsrc/index.tsは@yuheinakasaka/create-nimbusが生成した状態のままで動作する。
nimbus devを使う場合、ツールの登録は自動で行われるためこのファイルの変更は不要である。
src/index.ts
import { createNimbusAgent, createWorkerHandler } from "@yuheinakasaka/nimbus";
import { createWorkersAI } from "workers-ai-provider";
import agentDef from "../agent/agent.js";
// nimbus dev 使用時はagent/tools/のツールが自動登録される。
// wrangler deploy(本番)ではここでツールをimportしてtoolsに追加する。
type Env = { AI: Ai; Chat: DurableObjectNamespace };
export const Chat = createNimbusAgent<Env>({
definition: agentDef as any,
tools: [],
instructions: "You are a helpful AI assistant.",
createModel: (env: Env) =>
createWorkersAI({ binding: env.AI })("@cf/meta/llama-4-scout-17b-16e-instruct"),
});
export default createWorkerHandler({
agents: { Chat },
routes: [],
});
あとは2つのターミナルでバックエンドとUIをそれぞれ起動する。
Terminal 1 — エージェント (:2525)
pnpm dev
Terminal 2 — React UI (:5173)
pnpm dev:ui
ブラウザでhttp://localhost:5173を開く。ヘッダーに「接続中」と表示されれば、
UIがWebSocket経由でエージェントに接続できている。メッセージを送ると、Chapter 4と同じく
天気・計算ツールが呼び出され、結果が画面にストリーミング表示される。
承認ワークフロー。
useAgentChatはpendingApprovals、approve、rejectも返す。
ツール定義でneedsApproval: "always"を設定すれば、実行前にUIで承認を求めるHuman-in-the-Loopフローを追加できる。
完成したプロジェクトの全体像
my-agent/
├── agent/
│ ├── agent.ts
│ ├── instructions.md
│ └── tools/
│ ├── get-weather.ts
│ └── calculator.ts
├── src/
│ ├── App.tsx # React チャットUI
│ ├── main.tsx # React エントリポイント
│ └── index.ts # Worker エントリポイント
├── evals/
│ ├── eval.config.ts
│ └── basic-qa.ts
├── index.html # Vite HTML エントリ
├── vite.config.ts # React UI 用 Vite 設定
├── nimbus.config.ts
├── wrangler.jsonc
├── tsconfig.json
└── package.json