agentic-ui-kit 0.2.1 → 0.3.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 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
- export { a as activityTokens, o as applyEvent, s as awaitingToolCalls, c as estimateTokens, t as formatEvent, r as isActivityPart, l as outputTokens, p as parsePartialJson, n as readEventStream, u as reduceEvents, d as resolveEffort, f as settleToolCall, i as toBlocks } from "./sse-BA_ebMcH.js";
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-6xbP-myu.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:\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 };