Widgets
A widget is anything you want to render in your UI when the agent emits an event. AgentNavaKit ships the event protocol — your agent emits widget events from tool handlers, your UI dispatches on kind and renders. There is no built-in widget catalog: every widget kind is yours.
What lives where
Widgets cross a clean boundary. The SDK ships the protocol; you ship the components.
| Piece | Owned by | Notes |
|---|---|---|
Widget event protocol (widget-update / widget-remove SSE) |
@agentnava/kit |
The typed contract between agent and UI. Lives with the SDK. |
Agent emit API (ctx.emitWidget({ kind, props }), ctx.removeWidget(id)) |
@agentnava/kit |
Usable from any tool handler or workflow step. |
Vanilla subscribe helper (subscribeWidgets(...)) |
@agentnava/kit |
Typed event stream you read from any JS stack. |
Thin React drop-in (<AgentWidgets renderers={{ ... }} />) |
@agentnava/widgets-react · sibling MIT package · optional soon |
Wires subscribe + dispatch + mount in three lines. Stripe pattern — separate from the core SDK. |
| Your actual widget components (the JSX/HTML) | Your code | Your UI, your stack, your design system. You write the components; we deliver the data. |
| Widget starter templates (gallery of copy-paste components) | agentnava.com | Content offering, not an SDK dependency. Grab a starter and customize. |
| Workspace renderer (canvas pane on workspace.agentnava.com) | AgentNava platform | Renders any widget kind a workspace renderer is registered for. Automatic — install nothing. |
How a widget flows from agent to screen
ctx.emitWidget({ kind: 'listings-grid', props: { rows } })
widget-update event
Same stream as chat tokens, tool calls, and phase events. The protocol is the contract.
<AgentWidgets renderers=…> or subscribeWidgets(...)
Dispatches each event on kind. Filters by renderIn for the current surface.
Emitting a widget from the agent
A tool handler (or workflow step) calls ctx.emitWidget with a kind name you control and props you define. Re-emitting with the same widgetId updates the widget; removeWidget unmounts it.
// agents/<agent-name>/tools/search-listings.ts
import { defineTool, t } from '@agentnava/kit';
export const searchListings = defineTool({
name: 'search_listings',
description: 'Search MLS for listings in a city',
input: t.object({ city: t.string(), beds: t.number().optional() }),
handler: async ({ city, beds }, ctx) => {
const rows = await fetchMls(city, beds);
// Tell the UI: render a 'listings-grid' widget with these props.
ctx.emitWidget({
widgetId: 'main-listings', // optional; stable ID lets the same widget update in place
kind: 'listings-grid', // a name you invented, shared with your UI
props: { rows, columns: ['address', 'price', 'beds'] },
renderIn: ['workspace', 'embed', 'own-ui'], // optional; default = everywhere
});
return { rows };
},
});
The shape of props is yours. The SDK doesn't validate it — your UI does. Type safety comes from a small shared file you keep in your project:
// shared/widgets.ts — imported by both your agent and your UI
export type WidgetKinds = {
'listings-grid': { rows: Listing[]; columns: string[] };
'mortgage-calc': { homePrice: number; down: number; rate: number };
'status-banner': { message: string; tone: 'info' | 'warning' };
};
Pass that to the generic version of emitWidget for compile-time checks against the kind + props:
import type { WidgetKinds } from '../shared/widgets';
ctx.emitWidget<WidgetKinds>({
kind: 'listings-grid',
props: { rows, columns: ['address', 'price', 'beds'] }, // type-checked against WidgetKinds['listings-grid']
});
Rendering in your UI
Two paths. Both subscribe to the same typed event stream.
React — @agentnava/widgets-react
One component. Pass a renderers object mapping each kind to a React component:
import { AgentWidgets } from '@agentnava/widgets-react';
import type { WidgetKinds } from './shared/widgets';
import { ListingsGrid } from './components/ListingsGrid';
import { MortgageCalc } from './components/MortgageCalc';
import { StatusBanner } from './components/StatusBanner';
<AgentWidgets<WidgetKinds>
agent="agt_2b8e"
sessionId={sid}
renderers={{
'listings-grid': ListingsGrid,
'mortgage-calc': MortgageCalc,
'status-banner': StatusBanner,
}}
/>
The drop-in handles SSE subscription, lifecycle (mount on first event, update on same widgetId, unmount on widget-remove), and renderIn filtering for whichever surface you're on.
Any other stack — subscribeWidgets
Subscribe to the typed stream yourself. Dispatch on kind:
import { subscribeWidgets } from '@agentnava/kit';
import type { WidgetKinds } from './shared/widgets';
const unsub = subscribeWidgets<WidgetKinds>(
{ agent: 'agt_2b8e', sessionId },
(event) => {
if (event.type === 'widget-update') {
switch (event.kind) {
case 'listings-grid': mountOrUpdate(event.widgetId, ListingsGridVue, event.props); break;
case 'mortgage-calc': mountOrUpdate(event.widgetId, MortgageCalcVue, event.props); break;
case 'status-banner': mountOrUpdate(event.widgetId, StatusBannerVue, event.props); break;
}
} else if (event.type === 'widget-remove') {
unmount(event.widgetId);
}
},
);
Works in Vue, Svelte, Solid, server-rendered HTML — anywhere you can read SSE and call a function.
Widget event protocol
The full contract. kind is any string you choose; props is any JSON-serializable object. Type safety is layered on top by your shared types.
type Surface = 'workspace' | 'embed' | 'own-ui';
type WidgetEvent<K = Record<string, unknown>> =
| {
type: 'widget-update';
widgetId: string; // stable per widget instance; re-emit to update in place
kind: keyof K & string; // your kind name
props: K[keyof K]; // your props shape
renderIn: Surface[]; // default: all surfaces
}
| {
type: 'widget-remove';
widgetId: string;
};
These events appear alongside chat events in the same SSE stream — message-delta, tool-start, tool-end, phase, widget-update, widget-remove, done. The host UI dispatches on type; the widget renderer dispatches on kind.
renderIn — controlling surfaces
Every emitWidget call can declare which surfaces should receive the event:
ctx.emitWidget({
kind: 'admin-stats',
props: { totals, errors },
renderIn: ['own-ui'], // hide from the AgentNava workspace and the embed widget
});
Renderers receive only events whose renderIn includes their surface. Default is all three surfaces.
Discovery (optional)
If you want operators of the workspace or other UIs to know what kinds an agent might emit, declare them in meta:
await an.agents.configure({
// ...
meta: {
widgetKinds: ['listings-grid', 'mortgage-calc', 'status-banner'],
},
});
This is metadata only — purely for discovery. The runtime does not enforce it; you can emit any kind at any time.