agentic-ui-kit 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -2
- package/dist/core.js +96 -1
- package/dist/index.js +1482 -304
- package/dist/styles.css +1 -1
- package/dist/tailwind.css +33 -3
- package/dist/types/core/src/index.d.ts +8 -1
- package/dist/types/core/src/partial-json.d.ts +8 -0
- package/dist/types/core/src/types.d.ts +5 -0
- package/dist/types/core/src/ui-markup.d.ts +92 -0
- package/dist/types/core/src/ui-tree.d.ts +110 -0
- package/dist/types/core/src/visualize.d.ts +60 -0
- package/dist/types/react/src/components/chat-notice.d.ts +11 -0
- package/dist/types/react/src/components/composer.d.ts +7 -1
- package/dist/types/react/src/components/markdown.d.ts +19 -1
- package/dist/types/react/src/components/message-list.d.ts +10 -2
- package/dist/types/react/src/components/messages.d.ts +25 -4
- package/dist/types/react/src/components/tools.d.ts +20 -7
- package/dist/types/react/src/components/ui-tree.d.ts +87 -0
- package/dist/types/react/src/components/widget-frame.d.ts +97 -0
- package/dist/types/react/src/index.d.ts +23 -4
- package/dist/types/react/src/primitives/button.d.ts +10 -1
- package/dist/types/react/src/primitives/chat-theme.d.ts +13 -1
- package/dist/types/react/src/primitives/icons.d.ts +95 -0
- package/dist/types/react/src/primitives/labels.d.ts +193 -0
- package/dist/types/react/src/primitives/menu.d.ts +3 -1
- package/dist/types/react/src/primitives/segmented-control.d.ts +22 -0
- package/dist/types/react/src/views/chat-transcript.d.ts +27 -0
- package/dist/types/react/src/views/chat-view.d.ts +20 -3
- package/dist/ui-markup-DtMYer7I.js +1082 -0
- package/package.json +1 -1
- package/dist/sse-BA_ebMcH.js +0 -408
package/README.md
CHANGED
|
@@ -174,7 +174,60 @@ edit into an open file. If the stream breaks or the arguments aren't valid JSON,
|
|
|
174
174
|
|
|
175
175
|
A tool's `view` sets how it looks in the chat: `label`, `subtitle`, `summary`, a `Detail` component for the
|
|
176
176
|
expanded row and the approval card, and `ask` for the approval question. `Block` renders a call on its own
|
|
177
|
-
instead of inside the activity group
|
|
177
|
+
instead of inside the activity group; `Card` draws a card under the group while the call keeps its row (job
|
|
178
|
+
cards, a diff with Apply/Undo). A backend can set `data` on a tool call (with `part.update` / `part.end`):
|
|
179
|
+
display data for these views, kept apart from the `result` the model sees. `MarkdownLinksProvider` draws
|
|
180
|
+
chosen links in replies your way, e.g. a job link as a job row.
|
|
181
|
+
|
|
182
|
+
## Inline visuals
|
|
183
|
+
|
|
184
|
+
The model can draw something interactive in its reply, and it fills in while the model writes. The kit is
|
|
185
|
+
only the UI: your harness names the tools and writes the prompt; the kit suggests both and draws the result.
|
|
186
|
+
Register the views under your tool names. Full guide: https://bushshrub.github.io/agentic-ui-kit/custom-harness/connecting-your-agent/
|
|
187
|
+
|
|
188
|
+
**Your components (recommended).** Register components with a JSON Schema for their props (with zod:
|
|
189
|
+
`z.toJSONSchema`). Give the model `showUiTool(specs)` from `agentic-ui-kit/core`: it passes JSX as a string,
|
|
190
|
+
which the chat draws element by element as it streams. Answer each call with
|
|
191
|
+
`checkUiMarkupCode(markup, specs).feedback`, so the model learns what to fix (line, component, allowed props).
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
const components: UiComponent[] = [
|
|
195
|
+
{ name: "Metric", description: "A key number with a label.", props: metricSchema, render: Metric },
|
|
196
|
+
{ name: "Stack", description: "Children in a column.", children: true, render: Stack },
|
|
197
|
+
];
|
|
198
|
+
// server: tools: [showUiTool(components.map(({ render, ...spec }) => spec)), ...]
|
|
199
|
+
<UiComponentsProvider components={components} persist={store}>
|
|
200
|
+
<ToolViewsProvider tools={{ ...defaultToolViews, show_ui: uiMarkupToolView() }}>
|
|
201
|
+
<ChatView {...chat} />
|
|
202
|
+
</ToolViewsProvider>
|
|
203
|
+
</UiComponentsProvider>;
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Inside a component, `useUiAction()` runs an action from its props (`uiActionSchema`: set a state key, send
|
|
207
|
+
a message, open a URL, copy), and `useUiState()` reads and writes the widget's state. `persist` keeps each
|
|
208
|
+
widget's state across reloads. Also available: ` ```ui ` blocks in reply text (`uiMarkupGuide`,
|
|
209
|
+
`checkUiMarkup`) and JSON trees in a tool call (`renderUiTool`, `uiTreeToolView`, `checkUiTree`).
|
|
210
|
+
|
|
211
|
+
**Sandboxed HTML.** For maps, charts and anything free-form: give the model `visualizeTool` and put
|
|
212
|
+
`visualizeGuide` in its instructions, register `show_widget: widgetToolView()`, and answer calls with
|
|
213
|
+
`checkWidgetCode(code, allow).feedback`. Widgets render in a `WidgetFrame`: an iframe with
|
|
214
|
+
`sandbox="allow-scripts"` only and a CSP that blocks every request, themed with your `--chat-*` variables.
|
|
215
|
+
The same frame hosts MCP App UIs (MCP Apps spec 2026-01-26: `ui/initialize`, tool input and result,
|
|
216
|
+
`tools/call`); the kit never fetches anything itself, your `readResource` does.
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
<ChatView
|
|
220
|
+
{...chat}
|
|
221
|
+
widgets={{
|
|
222
|
+
allow: { resourceDomains: ["https://unpkg.com", "https://tile.openstreetmap.org"] }, // the most any widget may load
|
|
223
|
+
onCallTool: (name, args) => mcp.callTool(name, args), // MCP App UIs; leave out to turn it off
|
|
224
|
+
onWidgetError: ({ partId, message }) => report(partId, message), // script errors, to send back to the model
|
|
225
|
+
}}
|
|
226
|
+
/>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
A widget's `sendPrompt(text)` sends through `onSend`, and its links open in a new tab; set either callback to
|
|
230
|
+
`undefined` to turn it off. `ChatTranscript` widgets cannot send messages or call tools.
|
|
178
231
|
|
|
179
232
|
## Theming
|
|
180
233
|
|
|
@@ -203,6 +256,21 @@ add `background: "theme"`, which uses the Shiki theme's own background and text
|
|
|
203
256
|
animations even when the OS asks for less). It sets `data-chat-motion` on the element; an app that does not use
|
|
204
257
|
`ChatTheme` can put `data-chat-motion="reduce"` on `<html>` instead.
|
|
205
258
|
|
|
259
|
+
Every string the kit shows (text, aria-labels, placeholders) can be reworded or translated with `labels` on
|
|
260
|
+
`ChatTheme`. Keys you leave out keep the English default; labels with values in them are functions, and `locale`
|
|
261
|
+
sets the language of relative times and dates (default: the browser's). The keys are listed in `ChatLabels`.
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
<ChatTheme labels={{ copy: "Copier", moreOptionsFor: (title) => `Plus d'options pour ${title}`, locale: "fr" }}>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Every icon is swappable the same way with `icons` (Lucide by default). Pass SVG elements; the kit sizes and
|
|
268
|
+
colors them with CSS. The keys are listed in `ChatIcons`.
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
<ChatTheme icons={{ copy: <MyCopy />, send: <MySend /> }}>
|
|
272
|
+
```
|
|
273
|
+
|
|
206
274
|
The full list of variables is at the top of
|
|
207
275
|
[`packages/react/src/styles.css`](packages/react/src/styles.css).
|
|
208
276
|
|
|
@@ -214,11 +282,50 @@ The React package has four layers, each built only from the ones before it:
|
|
|
214
282
|
| ---------- | --------------------------------------------------------------------------------------------- |
|
|
215
283
|
| Primitives | `Button`, `IconButton`, `Menu`, `ContextMenu`, `Modal`, `Drawer`, `Panel`, `Tooltip`, `CodeBlock`, `ChatTheme` |
|
|
216
284
|
| Hooks | `useChat`, `sseTransport`, `AgentProvider`, `useAgentTool`, `useAgentContext`, `useSmoothText` |
|
|
217
|
-
| Components | `Composer`, `ModelPicker`, `MessageList`, `ActivityGroup`, `ApprovalCard`, `Markdown`, `ChatHeader`, `ChatList`, `ContextPanel` |
|
|
285
|
+
| Components | `Composer`, `ModelPicker`, `MessageList`, `ActivityGroup`, `ApprovalCard`, `Markdown`, `ChatHeader`, `ChatList`, `ContextPanel`, `WidgetFrame`, `UiTree` |
|
|
218
286
|
| Views | `ChatView`, `AppShell`, `Sidebar` |
|
|
219
287
|
|
|
220
288
|
Use `ChatView` for the whole thing, or build your own layout from the components.
|
|
221
289
|
|
|
290
|
+
## What you can change, and what is fixed
|
|
291
|
+
|
|
292
|
+
The kit has opinions about how a chat behaves and is laid out, and stays out of the way of how it looks as
|
|
293
|
+
your brand and of the space it sits in.
|
|
294
|
+
|
|
295
|
+
**Yours to change**
|
|
296
|
+
|
|
297
|
+
- Colors, fonts, radii, shadows: `--chat-*` variables.
|
|
298
|
+
- Text sizes: variables for message text (`--chat-message-font-size`), code (`--chat-code-font-size`),
|
|
299
|
+
interface text (`--chat-ui-font-size`), small interface text (`--chat-ui-font-size-sm`), the user bubble
|
|
300
|
+
(`--chat-bubble-font-size`) and activity rows (`--chat-activity-font-size`). A theme can set them per
|
|
301
|
+
`[data-chat-density]`.
|
|
302
|
+
- Width and density: `contentWidth`, and `density="full" | "panel" | "inline"` for a page, a side panel or a
|
|
303
|
+
box in a form.
|
|
304
|
+
- Code themes and motion on or off: `ChatTheme` props.
|
|
305
|
+
- Wording and locale: `ChatTheme labels`, see Theming. Icons: `ChatTheme icons`, Lucide by default.
|
|
306
|
+
- Content: the `header`, `footer`, `notice`, `emptyState` and `disclaimer` slots, custom parts through
|
|
307
|
+
`renderPart`, and tool views through `useChatTool`.
|
|
308
|
+
- Inline visuals: which domains widgets may load and which of their abilities are on (`widgets`), and the
|
|
309
|
+
components the model may compose (`UiComponentsProvider`).
|
|
310
|
+
|
|
311
|
+
**Fixed on purpose**
|
|
312
|
+
|
|
313
|
+
- Streaming: the paced reveal, the word fade, following the reply while you are at the bottom and leaving you
|
|
314
|
+
alone when you scroll up. Small changes here make streaming feel janky.
|
|
315
|
+
- Animations and their timings. Only on or off, for reduced motion.
|
|
316
|
+
- The layout: user messages in bubbles on the right, replies as full-width text, thinking and tool calls
|
|
317
|
+
folded into one activity row with a summary.
|
|
318
|
+
- Message actions: which ones you turn on is yours; where they sit (under the message, on hover) is not.
|
|
319
|
+
- Spacing inside the chrome. `density` covers the cases that need it to be tighter.
|
|
320
|
+
- Accessibility and keyboard behavior: focus rings, roles, Enter to send, menus and dialogs from react-aria.
|
|
321
|
+
- Markdown: only safe HTML in replies, streaming-safe parsing.
|
|
322
|
+
- When long messages fold and how tool calls group.
|
|
323
|
+
- The widget sandbox: `allow-scripts` only, the CSP, messages only from the widget's own frame, the height cap,
|
|
324
|
+
scripts running once the markup is complete. The action set of your components' trees.
|
|
325
|
+
|
|
326
|
+
A new setting has to change the look or fit a different container. A setting that changes how the chat
|
|
327
|
+
behaves needs a strong case. If you need different behavior, build your own layout from the components.
|
|
328
|
+
|
|
222
329
|
Bundle size: Markdown, KaTeX and Shiki are most of it, about 345 KB gzipped for a chat page. Shiki's language
|
|
223
330
|
grammars load on demand.
|
|
224
331
|
|
package/dist/core.js
CHANGED
|
@@ -1 +1,96 @@
|
|
|
1
|
-
|
|
1
|
+
import { A as salvageJson, C as awaitingToolCalls, D as resolveEffort, E as reduceEvents, O as settleToolCall, S as applyEvent, T as outputTokens, _ as formatEvent, a as readUiValue, b as toBlocks, c as uiMarkupGuide, d as isStateRef, f as matchesSchema, g as uiActionSchema, h as schemaErrors, i as parseUiMarkup, k as parsePartialJson, l as uiMarkupLanguage, m as renderUiToolName, n as checkUiMarkupCode, o as showUiTool, p as renderUiTool, r as findUiBlocks, s as showUiToolName, t as checkUiMarkup, u as checkUiTree, v as readEventStream, w as estimateTokens, x as activityTokens, y as isActivityPart } from "./ui-markup-DtMYer7I.js";
|
|
2
|
+
// ../core/src/visualize.ts
|
|
3
|
+
/** The tool definition to give the model. Same shape as a page tool's `ToolSpec`. */
|
|
4
|
+
const visualizeTool = {
|
|
5
|
+
name: "show_widget",
|
|
6
|
+
description: "Draw an interactive visual inline in your reply: a chart, diagram, map, calendar, calculator, planner. Write an HTML fragment or an <svg> in widget_code; it runs in a sandboxed frame that matches the chat's theme. Follow the visual design guide in your instructions.",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
widget_code: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "An HTML fragment (no <html>, <head> or <body>) or an <svg>. Inline <style> and <script> are allowed."
|
|
13
|
+
},
|
|
14
|
+
title: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Short snake_case name for the visual, e.g. gym_week."
|
|
17
|
+
},
|
|
18
|
+
loading_messages: {
|
|
19
|
+
type: "array",
|
|
20
|
+
items: { type: "string" },
|
|
21
|
+
minItems: 1,
|
|
22
|
+
maxItems: 4,
|
|
23
|
+
description: "1-4 short lines shown while you write it, e.g. \"Finding free slots\"."
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
required: ["widget_code"]
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/** Design guide for the system prompt when the visualize tool is on. */
|
|
30
|
+
const visualizeGuide = `## Visuals (show_widget)
|
|
31
|
+
Use show_widget when a visual says it better than text: data to compare, a plan to adjust, a place to see, a
|
|
32
|
+
process to step through. Keep explaining in text around it; the widget is not the whole answer.
|
|
33
|
+
|
|
34
|
+
How the frame works
|
|
35
|
+
- widget_code is a fragment: no <!doctype>, <html>, <head> or <body>. Put <style> first, then markup, then <script>.
|
|
36
|
+
- It renders while you write it; scripts run once it is complete, in order. Put scripts last.
|
|
37
|
+
- No network unless the host allows it: no fetch, no external images or fonts. Inline your data.
|
|
38
|
+
- The background is transparent and the frame sizes itself to your content. Add no outer margin or padding,
|
|
39
|
+
no outer border or card around the whole widget, and no fixed height on the root.
|
|
40
|
+
- Two globals: sendPrompt(text) sends a message as the user (for "Add these to my calendar", "Explain this"),
|
|
41
|
+
openLink(url) opens an http(s) link. Prefer local state (JavaScript in the widget) over round trips: filter,
|
|
42
|
+
toggle, recalculate and animate in place; use sendPrompt only for what needs you.
|
|
43
|
+
|
|
44
|
+
Look
|
|
45
|
+
- Use the chat's theme variables, never hard-coded colors or fonts: var(--chat-fg) text, var(--chat-muted) and
|
|
46
|
+
var(--chat-subtle) secondary text, var(--chat-surface) and var(--chat-raised) fills, var(--chat-border) and
|
|
47
|
+
var(--chat-divider) lines, var(--chat-hover) hover, var(--chat-accent) with var(--chat-accent-fg) for the one
|
|
48
|
+
thing that matters most, var(--chat-check) good, var(--chat-warning), var(--chat-danger) bad;
|
|
49
|
+
var(--chat-font-sans), var(--chat-font-mono); var(--chat-ui-font-size), var(--chat-ui-font-size-sm);
|
|
50
|
+
var(--chat-radius-sm), var(--chat-radius-md), var(--chat-radius-lg).
|
|
51
|
+
- Quiet by default: thin borders, few fills, one accent. Readable labels, aligned numbers, no decoration.
|
|
52
|
+
- Set apart what you added or changed from what was already there: your proposals in the accent with a small
|
|
53
|
+
"New" (or "Changed") tag, existing data quiet. The user must see at a glance what is yours.
|
|
54
|
+
- Make it work on a narrow screen (down to 320px wide) and with a keyboard: real <button>s, visible focus.`;
|
|
55
|
+
const originOf = (url) => {
|
|
56
|
+
try {
|
|
57
|
+
return new URL(url).origin.toLowerCase();
|
|
58
|
+
} catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Checks widget code before it reaches the user, so a harness can send problems back to the model as the
|
|
64
|
+
* tool result. `allow` is what the host lets widgets load (the same `allow` as the chat's `widgets`).
|
|
65
|
+
*/
|
|
66
|
+
function checkWidgetCode(html, allow = {}, toolName = visualizeTool.name) {
|
|
67
|
+
const errors = [];
|
|
68
|
+
const warnings = [];
|
|
69
|
+
if (typeof html !== "string" || !html.trim()) errors.push("widget_code: required, an HTML fragment or an <svg>");
|
|
70
|
+
else {
|
|
71
|
+
if (/<!doctype|<html[\s>]|<head[\s>]|<body[\s>]/i.test(html)) errors.push("Send a fragment: no <!doctype>, <html>, <head> or <body>.");
|
|
72
|
+
const allowed = (allow.resourceDomains ?? []).map((d) => d.toLowerCase().replace(/\/$/, ""));
|
|
73
|
+
const fits = (origin) => allowed.some((a) => a === origin || a.includes("*.") && origin.endsWith(a.slice(a.indexOf("*.") + 1)) && origin.startsWith(a.slice(0, a.indexOf("*."))));
|
|
74
|
+
const blocked = /* @__PURE__ */ new Set();
|
|
75
|
+
for (const m of html.matchAll(/(?:src|href)\s*=\s*["']?(https?:\/\/[^"'\s>]+)|url\(\s*["']?(https?:\/\/[^"')\s]+)|@import\s+["'](https?:\/\/[^"']+)/gi)) {
|
|
76
|
+
const origin = originOf(m[1] ?? m[2] ?? m[3] ?? "");
|
|
77
|
+
if (origin && !fits(origin) && !/<a\s[^>]*$/i.test(html.slice(0, m.index))) blocked.add(origin);
|
|
78
|
+
}
|
|
79
|
+
if (blocked.size) errors.push(`Blocked by the sandbox: ${[...blocked].join(", ")}. ${allowed.length ? `Only ${allowed.join(", ")} may be loaded.` : "Nothing external may be loaded."} Inline it instead.`);
|
|
80
|
+
if (!(allow.connectDomains ?? []).length && /\bfetch\s*\(|XMLHttpRequest|new\s+WebSocket|EventSource\s*\(/.test(html)) errors.push("Network calls (fetch, XHR, WebSocket) are blocked: inline the data.");
|
|
81
|
+
if (/\b(localStorage|sessionStorage|document\.cookie|indexedDB)\b/.test(html)) errors.push("Storage (localStorage, cookies, indexedDB) is not available in the sandbox: keep state in variables.");
|
|
82
|
+
if (/\b(alert|confirm|prompt)\s*\(/.test(html)) errors.push("alert/confirm/prompt are blocked: show it in the widget, or use sendPrompt(text).");
|
|
83
|
+
if (/<form[\s>]/i.test(html) && /type\s*=\s*["']?submit/i.test(html)) warnings.push("Form submits are blocked: handle the button's click in a script.");
|
|
84
|
+
const colors = new Set([...html.matchAll(/(?<![\w-])(#[0-9a-f]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\))/gi)].map((m) => m[1].toLowerCase()));
|
|
85
|
+
if (colors.size) warnings.push(`Hard-coded colors (${[...colors].slice(0, 5).join(", ")}${colors.size > 5 ? ", ..." : ""}): use var(--chat-*) so it matches the theme.`);
|
|
86
|
+
}
|
|
87
|
+
const ok = errors.length === 0;
|
|
88
|
+
const lines = [...errors, ...warnings].map((e) => `- ${e}`).join("\n");
|
|
89
|
+
return {
|
|
90
|
+
ok,
|
|
91
|
+
errors,
|
|
92
|
+
warnings,
|
|
93
|
+
feedback: ok && !warnings.length ? "Shown to the user." : ok ? `Shown. To fit the chat better next time:\n${lines}` : `Shown, but it will not work as written. Fix these and call ${toolName} again, without remarking on the fix to the user:\n${lines}`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export { activityTokens, applyEvent, awaitingToolCalls, checkUiMarkup, checkUiMarkupCode, checkUiTree, checkWidgetCode, estimateTokens, findUiBlocks, formatEvent, isActivityPart, isStateRef, matchesSchema, outputTokens, parsePartialJson, parseUiMarkup, readEventStream, readUiValue, reduceEvents, renderUiTool, renderUiToolName, resolveEffort, salvageJson, schemaErrors, settleToolCall, showUiTool, showUiToolName, toBlocks, uiActionSchema, uiMarkupGuide, uiMarkupLanguage, visualizeGuide, visualizeTool };
|