From 9e98119145fa0600575f79cfa13e2915706b30ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Sat, 9 May 2026 10:01:27 +0000 Subject: [PATCH] feat: dashboard multi-agent support + CF Pages deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase C of #164: - Dashboard fetches agents from gateway /endpoints - Sidebar shows agent selector with online/offline status - All API calls routed through gateway /api/:agent/* - Hash routing: #agent/threads/id format - SSE live streaming via gateway proxy - VITE_GATEWAY_URL env var for gateway configuration - Deployed to CF Pages: workflow-dashboard-54r.pages.dev - Custom domain: workflow.shazhou.work (pending SSL) Ref: #164, closes #167 小橘 🍊(NEKO TeamοΌ‰ --- packages/workflow-dashboard/src/api.ts | 71 +++++++++++++------ packages/workflow-dashboard/src/app.tsx | 26 ++++--- .../src/components/run-dialog.tsx | 9 +-- .../src/components/sidebar.tsx | 65 ++++++++++++++++- .../src/components/status-bar.tsx | 26 +++++-- .../src/components/thread-detail.tsx | 9 +-- .../src/components/thread-list.tsx | 5 +- .../src/components/workflow-list.tsx | 8 ++- .../workflow-dashboard/src/use-hash-route.ts | 57 ++++++++++----- packages/workflow-dashboard/src/use-sse.ts | 18 +++-- 10 files changed, 221 insertions(+), 73 deletions(-) diff --git a/packages/workflow-dashboard/src/api.ts b/packages/workflow-dashboard/src/api.ts index 4afe307..4310989 100644 --- a/packages/workflow-dashboard/src/api.ts +++ b/packages/workflow-dashboard/src/api.ts @@ -1,7 +1,15 @@ -const BASE = "/api"; +const GATEWAY_URL = import.meta.env.VITE_GATEWAY_URL || ""; -async function postJson(path: string, body: unknown): Promise { - const res = await fetch(`${BASE}${path}`, { +function agentBase(agent: string): string { + if (GATEWAY_URL) { + return `${GATEWAY_URL}/api/${agent}`; + } + // Local dev: proxy via vite, no agent prefix + return "/api"; +} + +async function postJson(base: string, path: string, body: unknown): Promise { + const res = await fetch(`${base}${path}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), @@ -13,14 +21,23 @@ async function postJson(path: string, body: unknown): Promise { return res.json() as Promise; } -async function fetchJson(path: string): Promise { - const res = await fetch(`${BASE}${path}`); +async function fetchJson(base: string, path: string): Promise { + const res = await fetch(`${base}${path}`); if (!res.ok) { throw new Error(`API ${res.status}: ${path}`); } return res.json() as Promise; } +// ── Endpoint types ────────────────────────────────────────────────── + +export type AgentEndpoint = { + name: string; + url: string; + status: string; + lastHeartbeat: number; +}; + export type WorkflowSummary = { name: string; currentHash: string; @@ -43,42 +60,52 @@ export type ThreadRecord = { [key: string]: unknown; }; -export function listWorkflows(): Promise<{ workflows: WorkflowSummary[] }> { - return fetchJson("/workflows"); +// ── Gateway endpoints ─────────────────────────────────────────────── + +export function listAgents(): Promise { + const url = GATEWAY_URL || ""; + return fetchJson(url, "/endpoints"); } -export function listThreads(): Promise<{ threads: ThreadSummary[] }> { - return fetchJson("/threads"); +// ── Agent-scoped endpoints ────────────────────────────────────────── + +export function listWorkflows(agent: string): Promise<{ workflows: WorkflowSummary[] }> { + return fetchJson(agentBase(agent), "/workflows"); } -export function listRunningThreads(): Promise<{ threads: ThreadSummary[] }> { - return fetchJson("/threads/running"); +export function listThreads(agent: string): Promise<{ threads: ThreadSummary[] }> { + return fetchJson(agentBase(agent), "/threads"); } -export function getThread(id: string): Promise<{ records: ThreadRecord[] }> { - return fetchJson(`/threads/${id}`); +export function listRunningThreads(agent: string): Promise<{ threads: ThreadSummary[] }> { + return fetchJson(agentBase(agent), "/threads/running"); +} + +export function getThread(agent: string, id: string): Promise<{ records: ThreadRecord[] }> { + return fetchJson(agentBase(agent), `/threads/${id}`); } export function runThread( + agent: string, workflow: string, prompt: string, maxRounds: number = 10, ): Promise<{ threadId: string }> { - return postJson("/threads", { workflow, prompt, maxRounds }); + return postJson(agentBase(agent), "/threads", { workflow, prompt, maxRounds }); } -export function killThread(threadId: string): Promise<{ ok: boolean }> { - return postJson(`/threads/${threadId}/kill`, {}); +export function killThread(agent: string, threadId: string): Promise<{ ok: boolean }> { + return postJson(agentBase(agent), `/threads/${threadId}/kill`, {}); } -export function pauseThread(threadId: string): Promise<{ ok: boolean }> { - return postJson(`/threads/${threadId}/pause`, {}); +export function pauseThread(agent: string, threadId: string): Promise<{ ok: boolean }> { + return postJson(agentBase(agent), `/threads/${threadId}/pause`, {}); } -export function resumeThread(threadId: string): Promise<{ ok: boolean }> { - return postJson(`/threads/${threadId}/resume`, {}); +export function resumeThread(agent: string, threadId: string): Promise<{ ok: boolean }> { + return postJson(agentBase(agent), `/threads/${threadId}/resume`, {}); } -export function getHealth(): Promise<{ ok: boolean }> { - return fetchJson("/healthz"); +export function getAgentHealth(agent: string): Promise<{ ok: boolean }> { + return fetchJson(agentBase(agent), "/healthz"); } diff --git a/packages/workflow-dashboard/src/app.tsx b/packages/workflow-dashboard/src/app.tsx index 5ee4013..77ad746 100644 --- a/packages/workflow-dashboard/src/app.tsx +++ b/packages/workflow-dashboard/src/app.tsx @@ -8,24 +8,34 @@ import { WorkflowList } from "./components/workflow-list.tsx"; import { useHashRoute } from "./use-hash-route.ts"; export function App() { - const { view, threadId, setView, setThreadId } = useHashRoute(); + const { view, agent, threadId, setView, setAgent, setThreadId } = useHashRoute(); const [showRun, setShowRun] = useState(false); return (
- +
- setShowRun(true)} /> + setShowRun(true)} />
- {view === "threads" && threadId === null && } - {view === "threads" && threadId !== null && ( - setThreadId(null)} /> + {!agent && ( +
+

+ Select an agent from the sidebar to get started. +

+
)} - {view === "workflows" && } + {agent && view === "threads" && threadId === null && ( + + )} + {agent && view === "threads" && threadId !== null && ( + setThreadId(null)} /> + )} + {agent && view === "workflows" && }
- {showRun && ( + {showRun && agent && ( setShowRun(false)} onCreated={(id) => { setShowRun(false); diff --git a/packages/workflow-dashboard/src/components/run-dialog.tsx b/packages/workflow-dashboard/src/components/run-dialog.tsx index 84a79d5..9a53dc3 100644 --- a/packages/workflow-dashboard/src/components/run-dialog.tsx +++ b/packages/workflow-dashboard/src/components/run-dialog.tsx @@ -3,12 +3,13 @@ import { listWorkflows, runThread } from "../api.ts"; import { useFetch } from "../hooks.ts"; type Props = { + agent: string; onClose: () => void; onCreated: (threadId: string) => void; }; -export function RunDialog({ onClose, onCreated }: Props) { - const workflows = useFetch(() => listWorkflows(), []); +export function RunDialog({ agent, onClose, onCreated }: Props) { + const workflows = useFetch(() => listWorkflows(agent), [agent]); const [workflow, setWorkflow] = useState(""); const [prompt, setPrompt] = useState(""); const [maxRounds, setMaxRounds] = useState(10); @@ -21,7 +22,7 @@ export function RunDialog({ onClose, onCreated }: Props) { setSubmitting(true); setError(null); try { - const result = await runThread(workflow, prompt, maxRounds); + const result = await runThread(agent, workflow, prompt, maxRounds); onCreated(result.threadId); } catch (err) { setError(err instanceof Error ? err.message : String(err)); @@ -38,7 +39,7 @@ export function RunDialog({ onClose, onCreated }: Props) { className="w-full max-w-lg p-6 rounded-lg border" style={{ background: "var(--color-surface)", borderColor: "var(--color-border)" }} > -

Run Thread

+

Run Thread on {agent}

+ + {/* Agent selector */} +
+ + {expanded && ( +
+ {agents.length === 0 && ( +

+ {status === "loading" ? "Loading..." : "No agents online"} +

+ )} + {agents.map((a) => ( + + ))} +
+ )} +
+ + {/* View navigation */}