AgentsHerald UI

Guide

Wire it to your agents

Herald ships the surface, not the backend. Every component is prop-driven and endpoint-agnostic — it takes generic typed data and injected callbacks, and owns zero assumptions about where that data comes from. That's what lets it drop onto any stack — and it's what an agent wires when it builds you a dashboard. This page is the map from Herald's seams to a live agent gateway.

The mental model

Self-hosted agents — OpenClaw, Hermes, or your own — run as a gateway process: a long-running service that streams replies, reports fleet status, and runs scheduled work. Herald is the control plane on top. You point its callbacks at your gateway's HTTP/SSE endpoints, and map the responses onto Herald's shapes. No SDK, no lock-in.

1. Streaming chat

useStream turns any SSE/fetch stream into live parts[]. Hand it a request(signal) that hits your gateway; it owns the parsing, state, and abort. Feed the result into ChatPanel or InlineAgentChat.

import { useStream } from "@/hooks/use-stream"

const stream = useStream()

async function send(text: string) {
  const next = [...messages, { role: "user", content: text }]
  setMessages([...next, { role: "assistant", content: "", parts: [] }])

  await stream.start((signal) =>
    fetch(`${GATEWAY_URL}/agents/${agentId}/chat`, {   // ← your gateway
      method: "POST",
      headers: { Authorization: `Bearer ${GATEWAY_TOKEN}` },
      body: JSON.stringify({ messages: next }),
      signal,
    }),
  )
}

<InlineAgentChat messages={messages} draft={draft} onDraftChange={setDraft}
  onSend={send} isStreaming={stream.isStreaming} onStop={stream.stop} />

The parser is swappable — the default speaks OpenAI-compatible SSE; pass your own for any wire format your gateway emits (including tool-call and reasoning events, which render as tool cards).

2. The fleet

The Dashboard and AgentStatusCard are pure — you feed them your gateway's fleet-status response, mapped to their shapes. Refresh on an interval (or a websocket) and they stay live.

const res = await fetch(`${GATEWAY_URL}/fleet`, {
  headers: { Authorization: `Bearer ${GATEWAY_TOKEN}` },
}).then((r) => r.json())

// map your gateway's shape → Herald's
const agents = res.agents.map((a) => ({
  id: a.id,
  name: a.name,
  model: a.model,
  status: a.online ? "online" : a.error ? "error" : "idle",
  description: a.lastActivity,
  stats: [{ label: "Tasks", value: a.taskCount }, { label: "Uptime", value: a.uptime }],
}))

<Dashboard agents={agents} activity={events} stats={kpis} usage={byAgent} />

3. Multi-agent rooms

useTurnRunner orchestrates who speaks next; you inject run — one call to your gateway per turn. Render it with RoundtableTranscript.

const room = useTurnRunner({
  participants,
  run: async ({ speaker, prompt, signal }) => {
    const res = await stream.start((sig) =>
      fetch(`${GATEWAY_URL}/agents/${speaker.id}/chat`, {
        method: "POST",
        body: JSON.stringify({ messages: [{ role: "user", content: prompt }] }),
        signal: signal ?? sig,
      }),
    )
    return res.acc.text
  },
})

4. Generation jobs

For non-streaming work — image, render, any submit→poll→result — useGeneration owns the lifecycle. Inject generate, progress, and interrupt.

const gen = useGeneration({
  generate: ({ input, signal }) =>
    fetch(`${GATEWAY_URL}/txt2img`, { method: "POST", body: JSON.stringify(input), signal }).then((r) => r.json()),
  progress: (signal) => fetch(`${GATEWAY_URL}/txt2img/progress`, { signal }).then((r) => r.json()),
  interrupt: () => fetch(`${GATEWAY_URL}/txt2img/interrupt`, { method: "POST" }).then(() => undefined),
})

5. Config & the rest

The same pattern covers everything else. Each surface takes callbacks; you back them with your own store or gateway endpoints:

  • Providers:  ProviderManager's onAdd/onDelete/onSetKey → your provider store (the key stays write-only).
  • Personas: PersonaBuilder's onChange/onSave → your persona records.
  • Notifications: NotificationsBell ← your scheduler's cron results (Hermes and OpenClaw both deliver scheduled output you can ingest).
  • Deletes: useUndoableDelete's onCommit → your DELETE, fired only if the Undo window lapses.

Building with an agent?

Point your coding agent at /llms.txt (the full catalog in one file) or install the Herald Agent Skill from the repo. Then just ask it to build the dashboard you want — it knows how to add the components and wire them to your gateway using the patterns above.