@dialogent/agentic-ui
v0.1.2
Published
React UI library for chat agents: headless primitives, themeable styled components, OpenAI-compatible SSE runtime, and a typed tool-call widget system.
Maintainers
Readme
@dialogent/agentic-ui
React UI library for chat agents: headless primitives, a themeable styled layer, an OpenAI-compatible SSE runtime, and a typed tool-call widget system with vendor-agnostic booking/scheduling widgets.
- Headless-first — radix-style primitives (
asChild,data-*state attributes); the styled layer is a thin shell you can theme or discard. - Runtime-agnostic — UI depends on one 8-member
ChatRuntimeinterface. OpenAI-compatible SSE ships in the box; anything else is an adapter away. - Streaming-first — every component renders correctly mid-stream: partial text, partial tool args (progressive JSON parsing), cancel/retry, graceful error states.
- One package — subpath exports keep unused layers out of your bundle (core is ~3 kB min+brotli; markdown/styled/widgets only load if you import them).
Install
pnpm add @dialogent/agentic-ui
# optional, only for the ./markdown entry:
pnpm add react-markdown remark-gfmQuickstart
"use client";
import { useMemo } from "react";
import { AgenticUIProvider } from "@dialogent/agentic-ui";
import { createOpenAIChatRuntime } from "@dialogent/agentic-ui/openai";
import { MarkdownText } from "@dialogent/agentic-ui/markdown";
import { Thread } from "@dialogent/agentic-ui/styled";
import "@dialogent/agentic-ui/styled/styles.css";
export function Chat() {
const runtime = useMemo(
() => createOpenAIChatRuntime({ api: "/api/chat" }), // any OpenAI-compatible endpoint
[],
);
return (
<AgenticUIProvider runtime={runtime}>
<Thread components={{ Text: MarkdownText }} welcome="How can I help?" />
</AgenticUIProvider>
);
}Works with any backend speaking the OpenAI chat/completions streaming dialect — OpenAI,
vLLM, LiteLLM, OpenRouter, Ollama, or your own.
Entries
| Import | Contents |
|---|---|
| @dialogent/agentic-ui | Core: types, ChatRuntime interface, createChatStore, tool UI registry (defineToolUI, useToolUI), provider + hooks |
| …/primitives | Headless Thread, Message, Composer, Suggestions |
| …/styled + …/styled/styles.css | Drop-in styled components, themed by --aui-* CSS variables (no Tailwind required) |
| …/markdown | MarkdownText (GFM) — needs the optional peers above |
| …/widgets | Vendor-agnostic BookingCard, AvailabilityPicker, ConfirmationPrompt |
| …/openai | createOpenAIChatRuntime (SSE, tool calls, reasoning deltas, cancel/retry) |
| …/test | createInMemoryRuntime for demos, tests, stories — zero network |
Tool-call UI
Register a component per tool name; it renders through the call's whole lifecycle — args streaming in (progressively parsed), running, result or error. Schemas are Standard Schema, so zod/valibot/arktype all work:
import { defineToolUI } from "@dialogent/agentic-ui";
import { AvailabilityPicker } from "@dialogent/agentic-ui/widgets";
import { z } from "zod";
const proposeSlots = defineToolUI({
toolName: "propose_booking_slots",
argsSchema: z.object({ serviceName: z.string(), slots: z.array(SlotSchema) }),
render: ({ args, status, respond }) => (
<AvailabilityPicker
slots={args?.slots ?? []} // renders while args stream
resolved={status.type !== "requires-action"}
onConfirm={(slot) => void respond?.({ selectedSlotId: slot.id })}
/>
),
});
<AgenticUIProvider runtime={runtime} tools={[proposeSlots]}>…</AgenticUIProvider>;respond(result)resolves human-in-the-loop tool calls and continues the run.- Unregistered tools get a collapsible default renderer; override it via
fallbackTool. useToolUI(def)registers for a component's lifetime and overrides static config.- A throwing tool UI degrades to the fallback and retries on the next stream update — it never takes the thread down.
The widgets are vendor-agnostic by design: map your backend's payload (SquareUp,
Calendly, anything) into the generic Booking/AvailabilitySlot shapes at the app
boundary — or better, go schema-first (below) and skip client mappers entirely.
Schema-first booking tools (recommended)
Every widget payload ships a colocated runtime schema (Standard Schema + safeParse,
zero dependencies) and a pre-wired tool UI — patterns adapted from
tool-ui (MIT). Your backend emits payloads matching the widget
schema (validate with the same schema object server-side), and registration is one line
per tool:
import {
createProposeSlotsToolUI, // "propose_booking_slots" → AvailabilityPicker
createConfirmBookingToolUI, // "confirm_booking" → BookingCard + prompt
createOrderSummaryToolUI, // "order_summary" → OrderSummary → receipt
createOptionListToolUI, // "choose_option" → OptionList
slotsProposalSchema, // …the same schemas, usable on the server
} from "@dialogent/agentic-ui/widgets";
<AgenticUIProvider runtime={runtime} tools={[createProposeSlotsToolUI(), createOrderSummaryToolUI()]}>Consequential choices are submitted as decision envelopes —
{ type: "tool-decision", decisionId, action, payload?, decidedAt } via the
respondWithDecision render prop (single-submission guaranteed; rapid double-clicks
submit once). Widgets derive their dimmed receipt state from the recorded decision,
including after history restoration. If a settled tool call's args fail the registered
schema, the fallback renderer shows instead of a broken widget.
See examples/booking-demo: the mock backend keeps SquareUp-shaped data internally, maps
it to widget schemas at the server boundary, and validates outbound payloads with the
shipped schemas — the client has zero mappers and zero custom render code.
Theming
Everything visual routes through documented --aui-* CSS custom properties (colors,
typography, radii, spacing, layout). Dark mode via data-aui-theme="dark" or
prefers-color-scheme. Slot overrides (components={{ Text, ToolCall, Avatar, Message }})
swap any part of the styled layer; for full control, compose the headless primitives —
streaming, auto-scroll, and composer behavior come along for free.
Custom runtimes
Implement the 8-member ChatRuntime interface (build on createChatStore to get
getState/subscribe for free) and the entire UI runs on it unchanged:
interface ChatRuntime {
getState(): ChatState;
subscribe(listener: () => void): () => void;
sendMessage(input: { text: string; metadata?: Record<string, unknown> }): Promise<void>;
cancel(): void;
retry(): Promise<void>;
addToolResult(input: { toolCallId: string; result: unknown; isError?: boolean }): Promise<void>;
setMessages(messages: ChatMessage[]): void;
readonly capabilities: { cancel: boolean; retry: boolean; toolResults: boolean };
}See examples/quickstart/src/scripted-runtime.ts for a complete ~60-line implementation.
License
MIT. Inspired by and partially derived from assistant-ui (MIT) — see the repository NOTICE.
