@june-ai/june-ui 0.2.7 → 0.2.9

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
@@ -1,79 +1,140 @@
1
- # @june-ai/june-ui
1
+ # June UI — `@june-ai/june-ui`
2
2
 
3
- React SDK for rendering [OpenUI Lang](https://openui.com/docs/openui-lang) as the model streams it. June supplies the React component, streaming, themes, registered actions, and error recovery. [OpenUI](https://github.com/thesysdev/openui) parses the language and draws the components.
4
-
5
- A response can be plain OpenUI Lang, or prose with OpenUI Lang in the middle (including inside ` ``` ` fences). June separates the prose and hands the program to OpenUI.
3
+ **June UI is a React SDK for rendering AI agent responses as real user interface.** An agent answers with Markdown, with [OpenUI Lang](https://openui.com/docs/openui-lang) (a small language for describing UI), or with both mixed together. June turns that answer into React components — while it is still streaming.
6
4
 
7
5
  ```tsx
8
- <JuneUI response={response} isStreaming={isStreaming} onJuneError={console.error} />
6
+ <JuneUI response={response} isStreaming={isStreaming} />
9
7
  ```
10
8
 
9
+ June is an independent framework around [OpenUI](https://github.com/thesysdev/openui): OpenUI parses the language and draws its built-in components; June adds the parts an application needs — streaming, Markdown, a component registry, themes, actions, tool approval, a document editor, agent adapters and structured errors — behind one public API. You never touch OpenUI internals.
10
+
11
11
  ## Contents
12
12
 
13
- - [Requirements](#requirements)
14
- - [Install](#install)
15
- - [Set up a Vite app](#set-up-a-vite-app)
16
- - [Set up a Next.js app](#set-up-a-nextjs-app)
17
- - [Render a static response](#render-a-static-response)
18
- - [Stream from your backend](#stream-from-your-backend)
19
- - [Server-sent events](#server-sent-events)
20
- - [Themes](#themes)
21
- - [Chat actions](#chat-actions)
22
- - [Registered actions](#registered-actions)
23
- - [Your own component library](#your-own-component-library)
24
- - [Errors and fallbacks](#errors-and-fallbacks)
25
- - [Smaller bundle](#smaller-bundle)
26
- - [Props](#props)
27
- - [Streaming reference](#streaming-reference)
28
- - [Theme tokens](#theme-tokens)
29
- - [Errors](#errors)
30
- - [Security](#security)
31
-
32
- ## Requirements
33
-
34
- | Package | Range |
13
+ 1. [What the SDK does](#what-the-sdk-does)
14
+ 2. [Main features](#main-features)
15
+ 3. [Architecture overview](#architecture-overview)
16
+ 4. [Requirements and peer dependencies](#requirements-and-peer-dependencies)
17
+ 5. [Installation](#installation)
18
+ 6. [Quick start](#quick-start)
19
+ 7. [Basic chat integration](#basic-chat-integration)
20
+ 8. [Markdown rendering](#markdown-rendering)
21
+ 9. [OpenUI Lang rendering](#openui-lang-rendering)
22
+ 10. [Mixed Markdown + OpenUI responses](#mixed-markdown--openui-responses)
23
+ 11. [Streaming integration](#streaming-integration)
24
+ 12. [Custom component registration](#custom-component-registration)
25
+ 13. [Zod schema definitions](#zod-schema-definitions)
26
+ 14. [Custom theme configuration](#custom-theme-configuration)
27
+ 15. [Document editor integration](#document-editor-integration)
28
+ 16. [Actions and tool approval](#actions-and-tool-approval)
29
+ 17. [Custom feature development](#custom-feature-development)
30
+ 18. [API reference](#api-reference)
31
+ 19. [TypeScript support](#typescript-support)
32
+ 20. [Error handling](#error-handling)
33
+ 21. [Troubleshooting](#troubleshooting)
34
+ 22. [Using June UI with Claude Code and AI Coding Agents](#using-june-ui-with-claude-code-and-ai-coding-agents)
35
+ 23. [Security](#security)
36
+ 24. [Migrating from 0.2.x](#migrating-from-02x)
37
+ 25. [Development setup](#development-setup)
38
+ 26. [Build and test commands](#build-and-test-commands)
39
+ 27. [Contribution guidelines](#contribution-guidelines)
40
+ 28. [License](#license)
41
+
42
+ ## What the SDK does
43
+
44
+ Your backend asks a model for an answer. The model writes text such as:
45
+
46
+ ````text
47
+ Revenue grew **12%** this quarter.
48
+
49
+ root = Stack([title, chart])
50
+ title = TextContent("Q4 Revenue", "large-heavy")
51
+ chart = BarChart(["Oct", "Nov", "Dec"], [Series("Product A", [120, 150, 180])])
52
+ ````
53
+
54
+ June receives that text chunk by chunk, separates the prose from the OpenUI program, renders the prose (as plain text or Markdown), validates every component call against its schema, and draws the components — updating as more text arrives. Buttons and forms in the generated UI call back into your app through whitelisted actions.
55
+
56
+ ## Main features
57
+
58
+ | Feature | What you get |
35
59
  | :--- | :--- |
36
- | `react` | `^18.3.1` or `^19.0.0` |
37
- | `react-dom` | `^18.0.0` or `^19.0.0` |
38
- | `@openuidev/react-lang` | `>=1.0.0 <2.0.0` |
39
- | `@openuidev/react-headless` | `>=0.14.0 <0.19.0` |
40
- | `zod` | `^3.25.0` or `^4.0.0` |
41
- | `zustand` | `^4.5.5` |
60
+ | **Rendering** | One `<JuneUI>` component for Markdown, OpenUI Lang, or both mixed. |
61
+ | **Streaming** | `useJuneStream` for fetch, SSE, OpenAI-style deltas and LangGraph runs; frame-batched updates, abort, cleanup. |
62
+ | **Component registry** | `defineComponent` + Zod schema → the model can use your React component. Add, override or remove components, even at runtime. |
63
+ | **Prompt generation** | `june.prompt()` builds the model's system prompt from exactly the components you render. |
64
+ | **Themes** | CSS-variable tokens, light/dark/system, per-mode overrides. Inherits your app's look by default. |
65
+ | **Actions** | Whitelisted, schema-validated handlers for buttons and forms; confirmation for sensitive actions. |
66
+ | **Tool approval** | `<ToolApproval>` + `useToolApproval` for human-in-the-loop approve / reject / edit. |
67
+ | **Document editor** | A Tiptap editor the model can open with `DocumentEditor(...)`: autosave, AI merge, DOCX/PDF/HTML/Markdown export. |
68
+ | **Adapters** | LangGraph stream and interrupt helpers. |
69
+ | **Errors** | One `JuneError` type with stable codes, an error boundary, and safe fallbacks. |
70
+
71
+ ## Architecture overview
72
+
73
+ ```mermaid
74
+ flowchart LR
75
+ A[Agent / model] -->|text stream| S[Streaming<br/>features/streaming]
76
+ S -->|accumulated response| P[Parser<br/>engine/parser]
77
+ P -->|prose segments| M[Markdown / text]
78
+ P -->|OpenUI program| R[OpenUI Renderer<br/>engine/renderer]
79
+ G[Component registry<br/>engine/registry] -->|library| R
80
+ R --> UI[React UI]
81
+ M --> UI
82
+ UI -->|actions| X[Actions + tool approval<br/>features]
83
+ ```
42
84
 
43
- `@openuidev/react-ui` is a direct dependency. You do not install it yourself. Component styles still have to be imported once (see below).
85
+ **Data flow:** agent response → **streaming** (bytes → text, batched per frame) → **parser** (split prose from statements, hold back half-typed lines, repair common model mistakes once complete) → **component registry** (the OpenUI library: names, schemas, React components) → **renderer** (OpenUI parses incrementally and validates props; June adds theme, runtime context, documents, error boundary) → **React UI**.
44
86
 
45
- The browser calls your backend. The backend calls the model. Do not put model API keys in the client.
87
+ | Folder | Responsibility | Depends on |
88
+ | :--- | :--- | :--- |
89
+ | `src/core/` | Public composition: `JuneUI`, `createJuneUI`, `JuneUIProvider`, the default library and registry. | engine, features, themes |
90
+ | `src/engine/parser/` | Pure text functions: `splitResponse`, `resolveMixedContent`, `normalizeProgram`. No React. | — |
91
+ | `src/engine/renderer/` | `JuneRenderer`, error boundary, Markdown text. Renders; never fetches. | parser, themes, features (actions, document bridge) |
92
+ | `src/engine/registry/` | `defineComponent`, `ComponentRegistry`, `createJuneLibrary`. Knows no app components. | OpenUI |
93
+ | `src/features/` | Self-contained features: `streaming`, `actions`, `document-editor`, `tool-approval`. | utils |
94
+ | `src/adapters/` | External agent frameworks (`langgraph`). Pure functions. | feature types |
95
+ | `src/themes/` | Tokens, palettes, `JuneThemeProvider`, `createTheme`. | — |
96
+ | `src/utils/` | `JuneError`. | — |
46
97
 
47
- ## Install
98
+ More: [docs/architecture.md](docs/architecture.md).
99
+
100
+ ## Requirements and peer dependencies
101
+
102
+ | Package | Range | Why |
103
+ | :--- | :--- | :--- |
104
+ | `react` | `^18.3.1` or `^19.0.0` | |
105
+ | `react-dom` | `^18.0.0` or `^19.0.0` | |
106
+ | `@openuidev/react-lang` | `>=1.0.0 <2.0.0` | OpenUI Lang parser and renderer (one shared copy). |
107
+ | `@openuidev/react-headless` | `>=0.14.0 <0.19.0` | Used by OpenUI's components. |
108
+ | `zod` | `^3.25.0` or `^4.0.0` | Component and action schemas. |
109
+ | `zustand` | `^4.5.5` | Used by OpenUI's components. |
110
+
111
+ `@openuidev/react-ui` (OpenUI's component library), Tiptap and `docx` are regular dependencies and install automatically.
112
+
113
+ The browser calls **your backend**; your backend calls the model. Never put model API keys in client code.
114
+
115
+ ## Installation
48
116
 
49
117
  ```bash
50
118
  npm install @june-ai/june-ui @openuidev/react-lang @openuidev/react-headless zod zustand
51
119
  ```
52
120
 
53
- `react` and `react-dom` are already in a Vite or Next.js app. pnpm and yarn use the same package names.
54
-
55
- Import both stylesheets once, at the app entry:
121
+ pnpm and yarn use the same package names. Then import the two stylesheets **once**, at your app entry:
56
122
 
57
123
  ```ts
58
- import "@openuidev/react-ui/components.css";
59
- import "@june-ai/june-ui/styles.css";
124
+ import "@openuidev/react-ui/components.css"; // OpenUI components: cards, forms, charts, tables
125
+ import "@june-ai/june-ui/styles.css"; // June: root layout, prose, document editor, approval cards
60
126
  ```
61
127
 
62
- `components.css` is the OpenUI component library (cards, forms, charts, tables). `styles.css` is June's root layout and, when you opt into a theme, its text color.
128
+ ## Quick start
63
129
 
64
- ## Set up a Vite app
65
-
66
- ### 1. Create the project
130
+ ### Vite
67
131
 
68
132
  ```bash
69
133
  npm create vite@latest my-june-app -- --template react-ts
70
134
  cd my-june-app
71
- npm install
72
135
  npm install @june-ai/june-ui @openuidev/react-lang @openuidev/react-headless zod zustand
73
136
  ```
74
137
 
75
- ### 2. Import styles
76
-
77
138
  `src/main.tsx`:
78
139
 
79
140
  ```tsx
@@ -90,544 +151,716 @@ createRoot(document.getElementById("root")!).render(
90
151
  );
91
152
  ```
92
153
 
93
- ### 3. Point `/api` at your backend during development
94
-
95
- `vite.config.ts`:
96
-
97
- ```ts
98
- import { defineConfig } from "vite";
99
- import react from "@vitejs/plugin-react";
100
-
101
- export default defineConfig({
102
- plugins: [react()],
103
- server: {
104
- proxy: { "/api": "http://localhost:8000" },
105
- },
106
- });
107
- ```
108
-
109
- ### 4. Render
110
-
111
154
  `src/App.tsx`:
112
155
 
113
156
  ```tsx
114
157
  import { JuneUI } from "@june-ai/june-ui";
115
158
 
116
- const response = `root = Stack([title])
159
+ const response = `Hello from **June**.
160
+
161
+ root = Stack([title])
117
162
  title = TextContent("Hello", "large-heavy")`;
118
163
 
119
164
  export default function App() {
120
- return <JuneUI response={response} isStreaming={false} />;
165
+ return <JuneUI response={response} textFormat="markdown" />;
121
166
  }
122
167
  ```
123
168
 
124
- ```bash
125
- npm run dev
126
- ```
127
-
128
- Open the local URL Vite prints. The page shows the text "Hello".
169
+ Run `npm run dev` and open the URL Vite prints: you see the Markdown sentence and a large "Hello".
129
170
 
130
- ## Set up a Next.js app
171
+ During development, proxy `/api` to your backend in `vite.config.ts`: `server: { proxy: { "/api": "http://localhost:8000" } }`.
131
172
 
132
- June uses hooks and the browser, so the component that renders `JuneUI` is a Client Component.
133
-
134
- ```bash
135
- npx create-next-app@latest my-june-app --ts --app --eslint
136
- cd my-june-app
137
- npm install @june-ai/june-ui @openuidev/react-lang @openuidev/react-headless zod zustand
138
- ```
173
+ ### Next.js (App Router)
139
174
 
140
- `app/june-preview.tsx`:
175
+ June uses hooks and the browser, so the component that renders `JuneUI` is a Client Component:
141
176
 
142
177
  ```tsx
178
+ // app/june-preview.tsx
143
179
  "use client";
144
180
 
145
181
  import "@openuidev/react-ui/components.css";
146
182
  import "@june-ai/june-ui/styles.css";
147
183
  import { JuneUI } from "@june-ai/june-ui";
148
184
 
149
- const response = `root = Stack([title])
150
- title = TextContent("Hello", "large-heavy")`;
151
-
152
- export function JunePreview() {
153
- return <JuneUI response={response} isStreaming={false} />;
185
+ export function JunePreview({ response }: { response: string }) {
186
+ return <JuneUI response={response} />;
154
187
  }
155
188
  ```
156
189
 
157
- `app/page.tsx`:
190
+ Render `<JunePreview response={...} />` from any server or client page.
158
191
 
159
- ```tsx
160
- import { JunePreview } from "./june-preview";
192
+ ## Basic chat integration
161
193
 
162
- export default function Page() {
163
- return <JunePreview />;
164
- }
165
- ```
194
+ One `<JuneUI>` per assistant message. Share configuration with `createJuneUI` + `JuneUIProvider` instead of repeating props on every message:
166
195
 
167
- Import the two CSS files from one client module. Importing them again from other routes is harmless, but one import is enough for the app.
196
+ ```tsx
197
+ import { useState } from "react";
198
+ import { createJuneUI, JuneUI, JuneUIProvider, useJuneStream, type OpenUIActionEvent } from "@june-ai/june-ui";
168
199
 
169
- ## Render a static response
200
+ const june = createJuneUI({
201
+ defaults: { theme: "system", textFormat: "markdown", onJuneError: (e) => console.warn(e.code, e.message) },
202
+ });
170
203
 
171
- `response` is the full string. `null` or `""` renders nothing unless you pass `emptyFallback`.
204
+ export function Chat() {
205
+ const [messages, setMessages] = useState<{ role: "user" | "assistant"; content: string }[]>([]);
206
+ const stream = useJuneStream({
207
+ onComplete: (text) => setMessages((m) => [...m, { role: "assistant", content: text }]),
208
+ });
172
209
 
173
- ```tsx
174
- import { JuneUI } from "@june-ai/june-ui";
210
+ const send = (text: string) => {
211
+ const next = [...messages, { role: "user" as const, content: text }];
212
+ setMessages(next);
213
+ void stream.startFetch("/api/chat", {
214
+ method: "POST",
215
+ headers: { "Content-Type": "application/json" },
216
+ body: JSON.stringify({ messages: next }),
217
+ });
218
+ };
219
+
220
+ const onAction = (event: OpenUIActionEvent) => {
221
+ if (event.type === "continue_conversation" && event.humanFriendlyMessage) send(event.humanFriendlyMessage);
222
+ };
175
223
 
176
- export function Dashboard() {
177
224
  return (
178
- <JuneUI
179
- response={`root = Stack([title, chart])
180
- title = TextContent("Q4 Revenue", "large-heavy")
181
- chart = BarChart(labels, [series])
182
- labels = ["Oct", "Nov", "Dec"]
183
- series = Series("Product A", [120, 150, 180])`}
184
- isStreaming={false}
185
- />
225
+ <JuneUIProvider june={june}>
226
+ {messages.map((m, i) =>
227
+ m.role === "user" ? <p key={i}>{m.content}</p> : <JuneUI key={i} response={m.content} onAction={onAction} />,
228
+ )}
229
+ {stream.isStreaming && <JuneUI response={stream.response} isStreaming loadingFallback={<p>Thinking…</p>} />}
230
+ </JuneUIProvider>
186
231
  );
187
232
  }
188
233
  ```
189
234
 
190
- Prose around the program is rendered as paragraphs. This is the default (`mixedContent`):
235
+ Generated buttons produce two built-in action types a chat should handle:
236
+
237
+ | `event.type` | Produced by | What to do |
238
+ | :--- | :--- | :--- |
239
+ | `"continue_conversation"` | `Button("Submit", @ToAssistant("Submit"))` | Send `event.humanFriendlyMessage` as the next user turn. `event.formState` holds form fields. |
240
+ | `"open_url"` | `Button("Docs", @OpenUrl("https://example.com"))` | Open `event.params.url` after checking the scheme. |
241
+
242
+ Full example: [examples/basic-chat](examples/basic-chat/App.tsx).
243
+
244
+ ## Markdown rendering
245
+
246
+ Prose is drawn as plain paragraphs by default (`<p class="june-text">`). Set `textFormat="markdown"` to render it as Markdown — headings, emphasis, lists, links, tables and code blocks:
191
247
 
192
248
  ```tsx
193
- <JuneUI
194
- response={`Sales rose this quarter.
249
+ <JuneUI response={"## Summary\n\n- **Revenue** up 12%\n- Churn down"} textFormat="markdown" />
250
+ ```
195
251
 
196
- root = Stack([title])
197
- title = TextContent("Sales", "large-heavy")
252
+ Markdown is rendered with OpenUI's `MarkDownRenderer` (react-markdown), loaded on first use. Raw HTML in model output is **not** rendered. In Markdown mode line breaks are kept, and fenced code blocks in other languages (```` ```python ````) stay intact as prose.
253
+
254
+ Need a different renderer? `renderText={(text, index) => <MyMarkdown source={text} />}` overrides both formats.
255
+
256
+ ## OpenUI Lang rendering
257
+
258
+ OpenUI Lang is a list of statements. `root` is what gets drawn; other names are referenced from it. Arguments are positional, in the order of the component's schema.
198
259
 
199
- Compared with last year.`}
260
+ ```tsx
261
+ <JuneUI
262
+ response={`root = Stack([title, chart])
263
+ title = TextContent("Q4 Revenue", "large-heavy")
264
+ chart = BarChart(labels, [series])
265
+ labels = ["Oct", "Nov", "Dec"]
266
+ series = Series("Product A", [120, 150, 180])`}
200
267
  />
201
268
  ```
202
269
 
203
- Pass `mixedContent={false}` when the string is already pure OpenUI Lang and must be given to OpenUI unchanged. A sentence with no `name = ...` statement will not render in that mode.
204
-
205
- `isStreaming` tells OpenUI the program is still arriving. Leave it `true` until the stream finishes so a half-written statement is not treated as the final UI. Forms stay non-interactive while it is `true`.
270
+ The default library contains OpenUI's built-in components (`Stack`, `Card`, `TextContent`, `Form`, `Table`, `BarChart`, …) plus June's `DocumentEditor`, plus anything you register. Pass `mixedContent={false}` when the string is pure OpenUI Lang that must reach OpenUI unchanged.
206
271
 
207
- A few shapes the built-in library does not draw directly are rewritten before render, and only when `mixedContent` is on and the stream has finished:
272
+ Once a response has finished streaming, June repairs a few shapes models often get wrong (only with `mixedContent` on; disable with `repair={false}`):
208
273
 
209
274
  - `Grid([a, b])` becomes a stack of those children.
210
275
  - `Card("Total Revenue", "$43,000")` becomes a title-and-value card.
211
- - A chart or table that was declared but left off `root` is attached so it still appears.
276
+ - A chart or table declared but left off `root` is attached so it still appears.
212
277
 
213
278
  A program that is already valid is left as it was.
214
279
 
215
- ## Stream from your backend
280
+ ## Mixed Markdown + OpenUI responses
281
+
282
+ Prose before, between and after statements is rendered around the UI. Statements may also sit inside ```` ``` ```` or ```` ```openui ```` fences:
283
+
284
+ ````tsx
285
+ <JuneUI
286
+ textFormat="markdown"
287
+ response={`Sales rose this quarter.
288
+
289
+ ```openui
290
+ root = Stack([title])
291
+ title = TextContent("Sales", "large-heavy")
292
+ ```
293
+
294
+ Compared with **last year**.`}
295
+ />
296
+ ````
297
+
298
+ All statements are joined into one program so references resolve across the whole answer. To teach a model this format, generate the prompt with OpenUI's `inlineMode`: `june.prompt({ inlineMode: true })`.
216
299
 
217
- `useJuneStream` collects chunks into one string, about once per frame. Starting a new stream cancels the one in progress. Unmount cancels it too.
300
+ ## Streaming integration
218
301
 
219
- The backend should write the OpenUI Lang (or prose plus OpenUI Lang) as a plain text body. Chunked `fetch` is enough. No special framing.
302
+ `useJuneStream` reads a stream into one string, batching React updates to about one per frame. Starting a new stream cancels the current one; unmounting cancels too. Keep `isStreaming` true until the end so half-written statements never count as final (forms stay non-interactive meanwhile).
220
303
 
221
304
  ```tsx
222
305
  import { JuneUI, useJuneStream } from "@june-ai/june-ui";
223
- import { useState } from "react";
224
306
 
225
307
  export function Generator() {
226
- const [prompt, setPrompt] = useState("Show a signup form");
227
308
  const stream = useJuneStream({
228
- onComplete: (text) => console.log("finished", text.length),
309
+ onComplete: (text) => console.log("done", text.length),
229
310
  onError: (error) => console.error(error.code, error.message),
230
311
  });
231
312
 
232
- const send = () => {
233
- void stream.startFetch("/api/generate", {
234
- method: "POST",
235
- headers: { "Content-Type": "application/json" },
236
- body: JSON.stringify({ prompt }),
237
- });
238
- };
239
-
240
313
  return (
241
- <div>
242
- <input value={prompt} onChange={(e) => setPrompt(e.target.value)} />
243
- <button type="button" onClick={send} disabled={stream.isStreaming}>
314
+ <>
315
+ <button
316
+ onClick={() =>
317
+ void stream.startFetch("/api/generate", {
318
+ method: "POST",
319
+ headers: { "Content-Type": "application/json" },
320
+ body: JSON.stringify({ prompt: "Show a signup form" }),
321
+ })
322
+ }
323
+ disabled={stream.isStreaming}
324
+ >
244
325
  Generate
245
326
  </button>
246
- <button type="button" onClick={stream.abort} disabled={!stream.isStreaming}>
247
- Stop
248
- </button>
249
- <p>status: {stream.status}</p>
250
- {stream.error && (
251
- <p role="alert">
252
- {stream.error.code}: {stream.error.message}
253
- </p>
254
- )}
327
+ <button onClick={stream.abort} disabled={!stream.isStreaming}>Stop</button>
255
328
  <JuneUI response={stream.response} isStreaming={stream.isStreaming} />
256
- </div>
329
+ </>
257
330
  );
258
331
  }
259
332
  ```
260
333
 
261
- `stream.status` is `"idle"`, `"streaming"`, `"done"`, `"error"`, or `"aborted"`. Stop keeps the text received so far and sets `error` to `null`. A network or HTTP failure sets `error` and also keeps the partial text.
334
+ `stream.status` is `"idle" | "streaming" | "done" | "error" | "aborted"`. Stop keeps the partial text with `error: null`; network and HTTP failures set `error` and keep the partial text. `startFetch` returns the full text, or `null` when aborted, superseded or failed.
262
335
 
263
- `startFetch` returns the full string, or `null` if the run was aborted, superseded, or failed.
336
+ | Backend sends | Option |
337
+ | :--- | :--- |
338
+ | Plain chunked text | none |
339
+ | SSE, `data:` = next text, `data: [DONE]` ends | `sse: true` |
340
+ | OpenAI-style SSE JSON | `sse: { extract: jsonFieldExtractor("choices.0.delta.content") }` |
341
+ | LangGraph run stream | `...langGraphStreamOptions({ nodes: ["agent"] })` |
342
+ | Your own `ReadableStream` / async iterable | `stream.start(async (signal) => body)` |
264
343
 
265
- If you already have a `ReadableStream` or an async iterable, use `start` instead of `startFetch`:
344
+ **LangGraph** (server API, `stream_mode: ["messages-tuple"]` or `"messages"`):
266
345
 
267
- ```tsx
268
- await stream.start(async (signal) => {
269
- const response = await fetch("/api/generate", { method: "POST", body, signal });
270
- if (!response.ok || !response.body) throw new Error(String(response.status));
271
- return response.body;
346
+ ```ts
347
+ import { langGraphStreamOptions } from "@june-ai/june-ui";
348
+
349
+ void stream.startFetch(`${API}/threads/${threadId}/runs/stream`, {
350
+ method: "POST",
351
+ headers: { "Content-Type": "application/json" },
352
+ body: JSON.stringify({ assistant_id: "agent", input, stream_mode: ["messages-tuple"] }),
353
+ ...langGraphStreamOptions({ nodes: ["agent"] }), // only text from the "agent" node; call per run
272
354
  });
273
355
  ```
274
356
 
275
- Prefer `startFetch` unless you need that control. It maps network failures, non-2xx responses, and an empty body to a `JuneError`.
357
+ Only AI message text is kept (tool and human messages are skipped); cumulative `messages/partial` events are turned into deltas. `readLangGraphInterrupts` and `interruptToApprovalRequests` help with human-in-the-loop interrupts — see [Actions and tool approval](#actions-and-tool-approval).
358
+
359
+ Outside React: `const text = await consumeStream(await fetchStream(url, { method: "POST", body, sse: true }))`.
276
360
 
277
- ## Server-sent events
361
+ More: [docs/streaming.md](docs/streaming.md) · [examples/streaming](examples/streaming/App.tsx).
278
362
 
279
- Pass `sse: true` when each event's `data` field is the next piece of text. A `data: [DONE]` frame ends the stream. An `event: error` frame fails it.
363
+ ## Custom component registration
364
+
365
+ Register a React component and the model can use it like a built-in. Four steps.
366
+
367
+ **1. Write a normal React component.**
280
368
 
281
369
  ```tsx
282
- void stream.startFetch("/api/generate", {
283
- method: "POST",
284
- headers: { "Content-Type": "application/json" },
285
- body: JSON.stringify({ prompt }),
286
- sse: true,
287
- });
370
+ function KpiCardView({ label, value, trend }: { label: string; value: string; trend?: "up" | "down" }) {
371
+ return (
372
+ <section className="kpi-card">
373
+ <small>{label}</small>
374
+ <strong>{value}</strong>
375
+ {trend && <span>{trend === "up" ? "▲" : "▼"}</span>}
376
+ </section>
377
+ );
378
+ }
288
379
  ```
289
380
 
290
- OpenAI-style streams put the token under a JSON path. `jsonFieldExtractor` reads that path and skips frames that do not have a string there:
381
+ **2. Define it: name, description for the model, Zod schema for the props.**
291
382
 
292
383
  ```tsx
293
- import { jsonFieldExtractor, useJuneStream } from "@june-ai/june-ui";
294
-
295
- void stream.startFetch("/api/generate", {
296
- method: "POST",
297
- headers: { "Content-Type": "application/json" },
298
- body: JSON.stringify({ prompt }),
299
- sse: { extract: jsonFieldExtractor("choices.0.delta.content") },
384
+ import { z } from "zod/v4";
385
+ import { defineComponent } from "@june-ai/june-ui";
386
+
387
+ export const KpiCard = defineComponent({
388
+ name: "KpiCard",
389
+ description: "One key metric with an optional trend arrow. Use for headline numbers.",
390
+ schema: z.object({
391
+ label: z.string().describe("What the number measures"),
392
+ value: z.string().describe("Formatted value, e.g. $43,000"),
393
+ trend: z.enum(["up", "down"]).optional(),
394
+ }),
395
+ component: KpiCardView,
300
396
  });
301
397
  ```
302
398
 
303
- `extract` receives the raw `data` string and the event name. Return `""` or `null` to ignore a frame. Set `doneSentinel: null` if the server never sends `[DONE]` and just closes the response.
399
+ **3. Register it** — on an instance (recommended), or on the default registry:
304
400
 
305
- Outside React, the same helpers work in a script or a test:
401
+ ```tsx
402
+ import { createJuneUI, JuneUI, registerComponent } from "@june-ai/june-ui";
306
403
 
307
- ```ts
308
- import { consumeStream, fetchStream } from "@june-ai/june-ui";
404
+ // a) isolated instance: <JuneUI june={june} … /> or <JuneUIProvider june={june}>
405
+ const june = createJuneUI({ components: [KpiCard] });
309
406
 
310
- const source = await fetchStream("/api/generate", { method: "POST", body, sse: true });
311
- const text = await consumeStream(source, {
312
- onText: (_delta, full) => process.stdout.write(full),
313
- });
407
+ // b) or globally, once at startup: every plain <JuneUI> can draw it
408
+ registerComponent(KpiCard);
314
409
  ```
315
410
 
316
- ## Themes
317
-
318
- June does not paint a background, a font, or a radius. With no `theme` prop it also does not set a text color, so the UI inherits the page.
411
+ **4. The model refers to it by name**, with arguments in schema key order:
319
412
 
320
413
  ```tsx
321
- <JuneUI response={response} />
414
+ <JuneUI
415
+ june={june}
416
+ response={`root = Stack([revenue])
417
+ revenue = KpiCard("Revenue", "$43,000", "up")`}
418
+ />
322
419
  ```
323
420
 
324
- Pass `theme` when you want June's own text colors:
421
+ **How it is validated and rendered.** OpenUI parses each statement and checks the arguments against your schema. A valid call renders your component with typed props. An invalid call (wrong type, missing required argument) is **not rendered**; OpenUI reports it and June raises `onJuneError` with `code: "PARSE_ERROR"` and the details. Built-in components keep working.
325
422
 
326
- | Value | Result |
327
- | :--- | :--- |
328
- | omitted | Host color, font, and `color-scheme` are left alone. |
329
- | `"light"` | Dark text on the assumption of a light page. |
330
- | `"dark"` | Light text on the assumption of a dark page. |
331
- | `"system"` | Follows `prefers-color-scheme`, and updates when the OS changes. |
423
+ **Teach the model.** Generate the system prompt from the same instance, on your server:
332
424
 
333
- ```tsx
334
- <JuneUI response={response} theme="system" />
425
+ ```ts
426
+ const system = june.prompt({ preamble: "You are an analytics assistant." }); // includes KpiCard
335
427
  ```
336
428
 
337
- `theme` colors June prose (`.june-text`) and the root. Cards, charts, inputs, and tables still use OpenUI's own tokens from `components.css`. To restyle those, wrap the tree in OpenUI's `ThemeProvider` (`@openuidev/react-ui`) and point its tokens at your CSS variables. June's `theme` does not replace that.
429
+ **More options:**
430
+
431
+ | Need | How |
432
+ | :--- | :--- |
433
+ | Child nodes / statement id | Use `render: ({ props, renderNode, statementId }) => …` instead of `component`. |
434
+ | Allow it inside a parent | `slots: ["Card"]` (needs Zod 4.1+; skipped with a dev warning otherwise). |
435
+ | Replace a built-in | `createJuneUI({ overrides: [MyTextContent] })` or `registry.override(c)`. |
436
+ | Remove a built-in | `createJuneUI({ remove: ["Modal"] })` or `registry.unregister("Modal")`. |
437
+ | Add at runtime | `june.register(c)` / `registerComponent(c)` — mounted `<JuneUI>`s re-render. |
438
+ | Your own library from scratch | `createComponentRegistry({ root: "Stack", components: [...] })` or `createJuneLibrary`. |
439
+ | Tooling metadata | `metadata: {...}` → `registry.getMeta(name)`. |
440
+
441
+ Names are PascalCase and unique; duplicates throw `JuneError("LIBRARY_ERROR")` with a fix in the message, and the registry is left unchanged. Create definitions and instances at module scope — the library reference must stay stable between renders.
338
442
 
339
- Override individual tokens with `themeVars`. They are written as inline custom properties and win over the built-in palette:
443
+ More: [docs/component-registration.md](docs/component-registration.md) · [examples/custom-component](examples/custom-component/App.tsx).
444
+
445
+ ## Zod schema definitions
446
+
447
+ Component schemas are **Zod 4** objects (OpenUI requires them). On Zod 4, `import { z } from "zod"`; on Zod 3.25, `import { z } from "zod/v4"` (works on both).
448
+
449
+ - **Key order is argument order:** `z.object({ title, body })` ↔ `Card("Title", "Body")`.
450
+ - **`.describe()` every field** — descriptions go into the generated prompt.
451
+ - **`.optional()`** for trailing optional arguments; required ones first.
452
+ - **Enums** (`z.enum([...])`) constrain the model to valid values.
453
+ - **Child components:** `z.array(OtherComponent.ref)` (with a `render` function that calls `renderNode`).
454
+ - Props types are inferred: `type Props = ComponentProps<typeof KpiCard>`.
455
+
456
+ Action payload schemas (`defineJuneAction({ schema })`) accept any object with `safeParse` — Zod 3 or 4.
457
+
458
+ ## Custom theme configuration
459
+
460
+ June never sets fonts, backgrounds or radii. Without `theme`, it doesn't set colors either: the UI inherits your page.
461
+
462
+ | `theme` | Result |
463
+ | :--- | :--- |
464
+ | omitted | Host color, font and `color-scheme` are left alone. |
465
+ | `"light"` / `"dark"` | June's palette for that mode; `color-scheme` is set. |
466
+ | `"system"` | Follows `prefers-color-scheme`, live. |
467
+
468
+ Override tokens with `themeVars` — one map for both modes, or one per mode:
340
469
 
341
470
  ```tsx
342
- <JuneUI
343
- response={response}
344
- theme="system"
345
- themeVars={{
346
- "--june-foreground": "var(--text)",
347
- "--june-accent": "var(--brand)",
348
- "--june-danger": "#b91c1c",
349
- }}
350
- className="assistant-ui"
351
- />
471
+ import { createTheme, JuneUI } from "@june-ai/june-ui";
472
+
473
+ const brand = createTheme({
474
+ light: { "--june-accent": "#0ea5e9" },
475
+ dark: { "--june-accent": "#38bdf8" },
476
+ });
477
+
478
+ <JuneUI response={response} theme="system" themeVars={brand} className="assistant" />;
479
+ <JuneUI response={response} themeVars={{ "--june-accent": "var(--brand)" }} />; // flat map, both modes
352
480
  ```
353
481
 
354
- `className` and `style` land on the `.june-ui-root` element. `useJuneTheme()` inside that tree returns `{ mode, theme }` where `theme` is the resolved `"light"` or `"dark"`.
482
+ | Token | Light | Dark | Used by |
483
+ | :--- | :--- | :--- | :--- |
484
+ | `--june-foreground` | `#111827` | `#f3f4f6` | root text (when a theme is set) |
485
+ | `--june-muted` | `#6b7280` | `#9ca3af` | secondary text |
486
+ | `--june-border` | `#e5e7eb` | `#1f2937` | borders, approval cards |
487
+ | `--june-accent` | `#4f46e5` | `#818cf8` | primary buttons |
488
+ | `--june-danger` | `#b91c1c` | `#f87171` | errors, high-risk approvals |
355
489
 
356
- `lightTheme` and `darkTheme` are the palettes June applies. Read them if you need the same colors elsewhere. Do not mutate them.
490
+ `className` and `style` land on `.june-ui-root`. Inside, `useJuneTheme()` returns `{ mode, theme }` (`theme` resolved to `"light"`/`"dark"`). OpenUI's own components (cards, charts, inputs) use OpenUI's tokens from `components.css`; restyle them with OpenUI's `ThemeProvider` from `@openuidev/react-ui`.
357
491
 
358
- ## Chat actions
492
+ More: [docs/theming.md](docs/theming.md) · [examples/custom-theme](examples/custom-theme/App.tsx).
359
493
 
360
- Buttons and forms call `onAction` with OpenUI's event. June does not run those for you. Two built-in types matter for a chat:
494
+ ## Document editor integration
361
495
 
362
- | `event.type` | Produced by | What to do |
363
- | :--- | :--- | :--- |
364
- | `"continue_conversation"` | `Button("Submit", @ToAssistant("Submit"))` | Send `event.humanFriendlyMessage` as the next user turn. `event.formState` holds the form fields. |
365
- | `"open_url"` | `Button("Docs", @OpenUrl("https://example.com"))` | Open `event.params.url` after you check the scheme. |
496
+ The default library includes `DocumentEditor`. When the model writes
497
+
498
+ ```text
499
+ root = Stack([doc])
500
+ doc = DocumentEditor("proposal-1", "Website Proposal", "## Summary\n\nWe propose…", "proposal", "draft")
501
+ ```
502
+
503
+ June shows an editable document (Tiptap): toolbar, find & replace, pagination, autosave, and export to DOCX, PDF, HTML, Markdown and text. While the response streams it shows a lightweight draft placeholder; the editor mounts when the stream ends. A new AI version with the same `documentId` is merged with the user's edits (conflicts are reported).
366
504
 
367
505
  ```tsx
368
- <JuneUI
369
- response={stream.response}
370
- isStreaming={stream.isStreaming}
371
- onAction={(event) => {
372
- if (event.type === "open_url") {
373
- const url = String(event.params?.url ?? "");
374
- if (/^https?:\/\//i.test(url)) window.open(url, "_blank", "noopener,noreferrer");
375
- return;
376
- }
377
- if (event.type === "continue_conversation" && event.humanFriendlyMessage) {
378
- send(event.humanFriendlyMessage);
379
- }
380
- }}
381
- />
506
+ import { useMemo, useState } from "react";
507
+ import { createDocumentStore, JuneUI, type JuneDocumentOptions } from "@june-ai/june-ui";
508
+
509
+ const store = createDocumentStore();
510
+
511
+ export function Reply({ initial }: { initial: string }) {
512
+ const [response, setResponse] = useState(initial);
513
+ const documents = useMemo<JuneDocumentOptions>(
514
+ () => ({ store, onSave: async (doc) => save(doc.documentId, doc.markdown), onConflict: console.warn }),
515
+ [],
516
+ );
517
+ // User edits are written back into the DocumentEditor(...) call: keep the program in sync.
518
+ return <JuneUI response={response} documents={documents} onResponseChange={setResponse} />;
519
+ }
382
520
  ```
383
521
 
384
- `onAction` runs for every event, including ones you also handle with the `actions` map below.
522
+ Teach the model when to use it, and give it the user's current text on the next turn:
385
523
 
386
- Form values can be restored on the next render with `initialState`, and read as the user types with `onStateUpdate`. Both are forwarded to OpenUI:
524
+ ```ts
525
+ import { getDocumentEditorInstructions, documentPromptOptions, serializeDocumentsForAgent } from "@june-ai/june-ui";
387
526
 
388
- ```tsx
389
- <JuneUI
390
- response={response}
391
- initialState={saved}
392
- onStateUpdate={(state) => setSaved(state)}
393
- />
527
+ const system = june.prompt({ ...documentPromptOptions() }); // or append getDocumentEditorInstructions()
528
+ const context = await serializeDocumentsForAgent({ store }); // attach to the next user message
394
529
  ```
395
530
 
396
- `Query()` and `Mutation()` in a program call `toolProvider`. Pass `toolProvider={null}` to disable them. If you supply a provider, it must call your backend. A tool that talks to a model or a database from the browser will leak credentials.
531
+ The editor is also a standalone component: `<DocumentEditor documentId="x" title="Notes" content="# Hi" />`. Custom export formats: `registerDocumentExporter(...)`. Remove it from the library with `createJuneUI({ remove: ["DocumentEditor"] })`.
397
532
 
398
- ## Registered actions
533
+ Example: [examples/document-editor](examples/document-editor/App.tsx).
399
534
 
400
- `onAction` is a callback. The `actions` prop is a whitelist. A handler runs only when `event.type` matches a name you registered. Model text is never evaluated as code.
535
+ ## Actions and tool approval
401
536
 
402
- A custom OpenUI action step arrives as `event.type` equal to the action name and `event.params` equal to its parameters. Register that name:
537
+ **Registered actions.** `onAction` sees every event; the `actions` prop is a **whitelist** — a handler runs only when `event.type` matches a registered name. Model text is never evaluated as code.
403
538
 
404
539
  ```tsx
405
- import { defineJuneAction, JuneUI } from "@june-ai/june-ui";
406
540
  import { z } from "zod";
541
+ import { defineJuneAction, JuneUI } from "@june-ai/june-ui";
407
542
 
408
543
  const actions = {
409
544
  place_order: defineJuneAction({
410
545
  description: "Place an order",
411
- sensitive: true,
412
- schema: z.object({
413
- sku: z.string(),
414
- qty: z.number().int().positive(),
415
- }),
546
+ sensitive: true, // needs approval first
547
+ schema: z.object({ sku: z.string(), qty: z.number().int().positive() }),
416
548
  handler: async ({ sku, qty }) => {
417
- const response = await fetch("/api/orders", {
418
- method: "POST",
419
- headers: { "Content-Type": "application/json" },
420
- body: JSON.stringify({ sku, qty }),
421
- });
422
- if (!response.ok) throw new Error(`Order failed (${response.status})`);
549
+ const res = await fetch("/api/orders", { method: "POST", body: JSON.stringify({ sku, qty }) });
550
+ if (!res.ok) throw new Error(`Order failed (${res.status})`);
423
551
  },
424
552
  }),
425
553
  };
426
554
 
427
- export function Checkout({ response }: { response: string }) {
555
+ <JuneUI response={response} actions={actions} confirmAction={({ description }) => window.confirm(description ?? "")} />;
556
+ ```
557
+
558
+ The runner: unknown names are ignored → `schema.safeParse` (failure: `ACTION_VALIDATION_ERROR`) → sensitive actions need `confirm`/`confirmAction` (none: `ACTION_REJECTED`; a user's "no" is not an error) → handler (a throw: `ACTION_ERROR`). Create the map once at module scope.
559
+
560
+ **Tool approval.** Replace `window.confirm` with an approval card, and reuse it for agent tool calls:
561
+
562
+ ```tsx
563
+ import { interruptToApprovalRequests, JuneUI, ToolApprovalList, useToolApproval } from "@june-ai/june-ui";
564
+
565
+ // `actions` as above; `resumeRun` is your call that resumes the agent with the decision.
566
+ function Assistant({ response }: { response: string }) {
567
+ const approvals = useToolApproval();
568
+
569
+ // Agent tool calls (e.g. a LangGraph interrupt) use the same UI:
570
+ async function onInterrupt(value: unknown) {
571
+ for (const request of interruptToApprovalRequests(value)) {
572
+ const decision = await approvals.request(request); // { choice: "approve" | "reject" | "edit", editedArguments? }
573
+ await resumeRun(decision);
574
+ }
575
+ }
576
+
428
577
  return (
429
- <JuneUI
430
- response={response}
431
- actions={actions}
432
- confirmAction={({ name, description }) => window.confirm(description ?? name)}
433
- onJuneError={(error) => console.error(error.code, error.message)}
434
- />
578
+ <>
579
+ <JuneUI response={response} actions={actions} confirmAction={approvals.confirmAction} />
580
+ <ToolApprovalList approvals={approvals} />
581
+ </>
435
582
  );
436
583
  }
437
584
  ```
438
585
 
439
- Create the map once, at module scope, so its identity stays stable.
586
+ `<ToolApproval request status onDecide />` shows tool name, risk, side effect, description and arguments, with Approve / Reject / Edit (JSON, validated). Use `approvals.setStatus(id, "running" | "completed" | "failed")` to show progress. Customize with `labels`, `renderArguments`, CSS (`.june-approval*`), or your own UI on top of `useToolApproval`.
440
587
 
441
- What the runner does, in order:
588
+ Example: [examples/tool-approval](examples/tool-approval/App.tsx).
442
589
 
443
- 1. Unknown names, including `"constructor"`, are ignored. Outcome `"unhandled"`.
444
- 2. `schema.safeParse` runs when you passed a schema. Zod objects work. A failed parse reports `ACTION_VALIDATION_ERROR` and does not call the handler.
445
- 3. `sensitive: true`, or a per-action `confirm`, must approve the call. With neither a per-action `confirm` nor the `confirmAction` prop, a sensitive action is rejected (`ACTION_REJECTED`) and the handler does not run. A user declining is also `"rejected"` and is not an error.
446
- 4. The handler receives the parsed payload and `{ name, event }`. A throw becomes `ACTION_ERROR`.
590
+ ## Custom feature development
447
591
 
448
- `defineJuneAction` throws `LIBRARY_ERROR`'s sibling `ACTION_VALIDATION_ERROR` immediately if `handler` is not a function. That is a setup bug, not a runtime action failure.
592
+ A feature is a folder in `src/features/<name>/` with its own components, hooks, types, CSS and `index.ts`. It must not import from other features' internals or from `core/`. To ship one:
449
593
 
450
- The default parser reads the name from `type`, then `name`, then `action`, and the payload from `params`, then `payload`, then `data`. Pass `parseActionEvent` if your event shape differs.
594
+ 1. Implement it in `src/features/<name>/`; export its public API from `index.ts`.
595
+ 2. If it contributes OpenUI components, define them with `defineComponent` and add them in `src/core/default-library.ts` (the composition root) — the engine stays untouched.
596
+ 3. If it has CSS, add the file to `scripts/copy-css.mjs`.
597
+ 4. Re-export it from `src/entry-core.ts` (`export * from "./features/<name>"`).
598
+ 5. Add tests in `tests/features/<name>/` and docs.
451
599
 
452
- ## Your own component library
600
+ In an **application** (not the SDK), a "feature" is usually just components + `defineComponent` + `createJuneUI({ components })`, and hooks that read `useJuneRuntime()` / `useJuneTheme()`. You never edit the SDK.
453
601
 
454
- `JuneUI` uses OpenUI's built-in library (`Stack`, `TextContent`, `Form`, `Table`, `BarChart`, and the rest). Pass `library` to replace it.
602
+ More: [docs/custom-features.md](docs/custom-features.md).
455
603
 
456
- Build the library once at module scope. `createJuneLibrary` checks that every component has a unique non-empty name, then applies `allow` and `deny`. `allow` lists the only names to keep. `deny` removes names after that. The `root` component must still be present. A bad library throws `LIBRARY_ERROR` when the module loads, which is what you want.
604
+ ## API reference
457
605
 
458
- ```tsx
459
- import { createJuneLibrary, defineJuneComponent, JuneUI } from "@june-ai/june-ui";
460
- import { z } from "zod/v4";
606
+ ### Entry points
461
607
 
462
- const Greeting = defineJuneComponent({
463
- name: "Greeting",
464
- description: "Greets a person by name",
465
- props: z.object({
466
- name: z.string().describe("The person's name"),
467
- }),
468
- component: ({ props }) => <p>Hello, {props.name}</p>,
469
- });
608
+ | Import | Contents |
609
+ | :--- | :--- |
610
+ | `@june-ai/june-ui` | Everything below, including `JuneUI` and the default library. |
611
+ | `@june-ai/june-ui/core` | Everything except the default library (`JuneUI`, `juneLibrary`, `createJuneUI`, `JuneUIProvider`, `registerComponent`, `defaultRegistry`). Pair `JuneRenderer` with your own library for a smaller bundle. |
612
+ | `@june-ai/june-ui/styles.css` | June's stylesheet. |
470
613
 
471
- const library = createJuneLibrary({
472
- root: "Greeting",
473
- components: [Greeting],
474
- });
614
+ ### Core
475
615
 
476
- export function Custom() {
477
- return <JuneUI library={library} response={'root = Greeting("Ada")'} />;
478
- }
479
- ```
616
+ | Export | Description |
617
+ | :--- | :--- |
618
+ | `JuneUI` | Renders a response. All `JuneRenderer` props, plus optional `library` and `june`. |
619
+ | `JuneRenderer` | Same, with a required `library`. Available from `/core`. |
620
+ | `createJuneUI(options)` | Instance: `{ registry, library, defaults, register(), prompt() }`. Options: `components`, `overrides`, `remove`, `baseLibrary`, `defaults`. |
621
+ | `JuneUIProvider` | `{ june?, defaults? }` for every `<JuneUI>` below. Props on `<JuneUI>` win. |
622
+ | `juneLibrary` | Default OpenUI library: OpenUI components + `DocumentEditor`. |
480
623
 
481
- `defineJuneComponent` is OpenUI's `defineComponent`. Component schemas use Zod 4 (`zod/v4`). Props, functions, and actions follow [OpenUI's library guide](https://openui.com/docs/openui-lang). `createJuneLibrary` adds the name checks and the allow/deny lists on top of `createLibrary`. The `root` name has to be one of the components you pass.
624
+ ### `JuneUI` / `JuneRenderer` props
482
625
 
483
- To teach the model the built-in components, generate the system prompt on the server from the same library the client renders:
626
+ Any other prop of OpenUI's `Renderer` is forwarded and typed from your installed `@openuidev/react-lang` (`initialState`, `onStateUpdate`, `toolProvider`, `queryLoader`, `onParseResult`, …).
484
627
 
485
- ```ts
486
- import { openuiLibrary } from "@openuidev/react-ui";
628
+ | Prop | Type | Default | |
629
+ | :--- | :--- | :--- | :--- |
630
+ | `response` | `string \| null` | — | Accumulated model output. |
631
+ | `isStreaming` | `boolean` | `false` | The model is still writing. |
632
+ | `library` | OpenUI library | instance / provider / default | Required on `JuneRenderer`. |
633
+ | `june` | `JuneUIInstance` | provider | `JuneUI` only. |
634
+ | `mixedContent` | `boolean` | `true` | Render prose around the program. |
635
+ | `textFormat` | `"plain" \| "markdown"` | `"plain"` | How prose is drawn. |
636
+ | `renderText` | `(text, index) => ReactNode` | — | Custom prose rendering. |
637
+ | `repair` | `boolean` | `true` | Fix common model mistakes after streaming. |
638
+ | `theme` | `"light" \| "dark" \| "system"` | omitted | Opt in to June colors. |
639
+ | `themeVars` | vars or `{ light, dark }` | — | CSS variable overrides. |
640
+ | `className` / `style` | | — | On `.june-ui-root`. |
641
+ | `actions` | `JuneActionMap` | — | Whitelisted handlers. |
642
+ | `confirmAction` | `(args) => boolean \| Promise<boolean>` | — | Approval for sensitive actions. |
643
+ | `parseActionEvent` | `(event) => { name, payload } \| null` | built-in | Custom event shape. |
644
+ | `onAction` | `(event) => void` | — | Every OpenUI action. |
645
+ | `onError` | `(errors: unknown[]) => void` | — | Raw OpenUI errors (`[]` when cleared). |
646
+ | `onJuneError` | `(error: JuneError) => void` | — | Structured errors. |
647
+ | `documents` | `JuneDocumentOptions` | — | Document store and callbacks. Memoize. |
648
+ | `onResponseChange` | `(response, info) => void` | — | Program rewritten with user document edits. |
649
+ | `responseChangeDelay` | `number` | `300` | Debounce in ms. |
650
+ | `loadingFallback` | `ReactNode` | `null` | Streaming, nothing to draw yet. |
651
+ | `emptyFallback` | `ReactNode` | `null` | Finished, nothing to draw. |
652
+ | `errorFallback` | node or `({ error, reset }) => node` | generic alert | Render-crash UI. |
653
+ | `resetKeys` | `unknown[]` | `[response]` | Changing clears a caught error. |
654
+
655
+ ### Registry
656
+
657
+ | Export | Description |
658
+ | :--- | :--- |
659
+ | `defineComponent({ name, description, schema, component \| render, slots?, metadata? })` | A component definition (also a valid OpenUI `DefinedComponent`). |
660
+ | `createComponentRegistry({ base?, root?, components?, overrides?, remove? })` | A `ComponentRegistry`. |
661
+ | `ComponentRegistry` | `.library`, `.register(c, { override })`, `.override(c)`, `.unregister(name)`, `.reset()`, `.has()`, `.get()`, `.getMeta()`, `.names()`, `.customNames()`, `.prompt()`, `.subscribe()`. |
662
+ | `registerComponent(c, opts?)` / `unregisterComponent(name)` / `defaultRegistry` | The registry plain `<JuneUI>` uses. |
663
+ | `useRegistryLibrary(registry)` | Hook: current library, re-renders on change. |
664
+ | `createJuneLibrary({ components, root, allow?, deny? })`, `defineJuneComponent` | 0.2 APIs, unchanged. |
487
665
 
488
- const system = openuiLibrary.prompt({});
489
- ```
666
+ ### Parser (pure functions)
667
+
668
+ `splitResponse(text, { streaming?, markdown? })` → `{ before, ui, after, pure }` · `resolveMixedContent(response, { mixedContent?, isStreaming?, repair?, markdown? })` → `{ before, ui, after, hasUi, hasText }` · `normalizeProgram(program)`.
669
+
670
+ ### Streaming
671
+
672
+ | Export | Description |
673
+ | :--- | :--- |
674
+ | `useJuneStream({ allowEmpty?, onComplete?, onError? })` | `{ response, isStreaming, status, error, start, startFetch, abort, reset }`. |
675
+ | `fetchStream(input, init & { sse? })` | `fetch` → stream source with `JuneError`s. |
676
+ | `consumeStream(source, { signal?, onText?, allowEmpty? })` | Reads a source to one string. |
677
+ | `sseToText(source, { extract?, doneSentinel?, signal? })` | SSE → text chunks. `doneSentinel` default `"[DONE]"`. |
678
+ | `jsonFieldExtractor("a.0.b")` | `extract` for a JSON path. |
679
+ | `createLangGraphExtractor({ nodes? })`, `langGraphStreamOptions({ nodes? })` | LangGraph stream text. |
680
+ | `readLangGraphInterrupts(data, event)`, `interruptToApprovalRequests(value)` | LangGraph interrupts → approval requests. |
681
+
682
+ ### Actions, approval, themes, documents, runtime
683
+
684
+ | Export | Description |
685
+ | :--- | :--- |
686
+ | `defineJuneAction`, `runAction`, `createActionHandler`, `defaultParseActionEvent` | Actions. |
687
+ | `ToolApproval`, `ToolApprovalList`, `useToolApproval`, `defaultToolApprovalLabels` | Tool approval. |
688
+ | `JuneThemeProvider`, `useResolvedTheme`, `useJuneTheme`, `createTheme`, `lightTheme`, `darkTheme` | Themes. |
689
+ | `DocumentEditor`, `createDocumentStore`, `getDefaultDocumentStore`, `exportDocument`, `registerDocumentExporter`, `serializeDocumentsForAgent`, `getDocumentEditorInstructions`, `documentPromptOptions`, `updateDocumentInProgram`, `readDocumentFromProgram`, … | Documents. |
690
+ | `useJuneRuntime()` | `{ isStreaming }` for custom components. |
691
+ | `MarkdownText`, `JuneErrorBoundary` | Building blocks. |
692
+ | `JuneError`, `isJuneError`, `toJuneError` | Errors. |
693
+
694
+ ## TypeScript support
490
695
 
491
- Send `system` with the user prompt from your backend. Keep that call off the client if the prompt is large or if you add private rules to it.
696
+ The package ships ESM and CommonJS builds with `.d.ts` / `.d.cts` declarations for both entries. Notable types:
492
697
 
493
- ## Errors and fallbacks
698
+ - `JuneUIProps`, `JuneRendererProps` — OpenUI's renderer props are **derived** from your installed `@openuidev/react-lang`, never copied.
699
+ - `OpenUIActionEvent`, `OpenUILibrary` — OpenUI's own types, re-exported.
700
+ - `ComponentProps<typeof MyComponent>` — props inferred from a component's schema.
701
+ - `JuneComponent`, `DefineComponentOptions`, `JuneThemeVars` (with token autocompletion), `JuneThemeSet`, `ToolApprovalRequest`, `JuneErrorCode`.
494
702
 
495
- Three channels, for three kinds of failure:
703
+ `moduleResolution: "bundler"` or `"node16"` is recommended so the `/core` subpath resolves.
496
704
 
497
- | Prop | When |
705
+ ## Error handling
706
+
707
+ | Channel | When |
708
+ | :--- | :--- |
709
+ | `onJuneError` | A `JuneError`: parse failures (warnings excluded), render crashes, action, document and export failures. |
710
+ | `onError` | OpenUI's raw error list; `[]` when cleared. |
711
+ | `errorFallback` | The render boundary. The default alert never echoes model output. |
712
+ | `useJuneStream().error` | Stream failures. |
713
+
714
+ | Code | Meaning |
498
715
  | :--- | :--- |
499
- | `onError` | OpenUI's own list. Called with `[]` when the errors clear. Warnings stay in the list; filter `severity !== "warning"` if you only want failures. |
500
- | `onJuneError` | A `JuneError`: parse failures (warnings excluded), a render crash, or an action failure. |
501
- | `errorFallback` | The render crash boundary. The default is a short alert that does not echo the model output. |
716
+ | `PARSE_ERROR` | OpenUI reported errors (bad syntax, props failing a schema). `details` is the list. |
717
+ | `RENDER_ERROR` | A component threw. `details.componentStack`. |
718
+ | `STREAM_ERROR` | The stream failed, including an SSE `event: error`. |
719
+ | `STREAM_ABORTED` | Aborted. `useJuneStream` treats it as a stop. |
720
+ | `STREAM_EMPTY` | No body, or nothing decoded. |
721
+ | `NETWORK_ERROR` | `fetch` failed or non-2xx (`details.status`). |
722
+ | `ACTION_ERROR` | A handler threw. |
723
+ | `ACTION_VALIDATION_ERROR` | Payload failed the schema, or `defineJuneAction` had no handler. |
724
+ | `ACTION_REJECTED` | Sensitive action with no confirm function. |
725
+ | `LIBRARY_ERROR` | A component definition or registration was rejected. |
726
+ | `DOCUMENT_ERROR` | Document content or sync failed. |
727
+ | `EXPORT_ERROR` | A document export failed. |
502
728
 
503
729
  ```tsx
504
730
  <JuneUI
505
731
  response={stream.response}
506
732
  isStreaming={stream.isStreaming}
507
- loadingFallback={<p>Drawing…</p>}
508
- emptyFallback={<p>Nothing to show yet.</p>}
509
- errorFallback={({ error, reset }) => (
510
- <div role="alert">
511
- <p>This reply could not be drawn.</p>
512
- <button type="button" onClick={reset}>
513
- Try again
514
- </button>
515
- </div>
516
- )}
733
+ errorFallback={({ reset }) => <button onClick={reset}>This reply could not be drawn. Retry</button>}
517
734
  onJuneError={(error) => {
518
735
  if (error.code === "PARSE_ERROR") console.table(error.details);
519
736
  }}
520
737
  />
521
738
  ```
522
739
 
523
- `loadingFallback` shows while `isStreaming` is true and there is no UI yet. `emptyFallback` shows when the stream is finished and there is still no UI and no prose. `resetKeys` clears a caught render error when any value changes. The default key is the `response` string, so the next chunk retries the render.
740
+ `isJuneError(value)` narrows a `catch`; `toJuneError(error, fallbackCode)` wraps anything. Without `onError`/`onJuneError`, parser errors are logged with `console.warn`.
524
741
 
525
- If you pass neither `onError` nor `onJuneError`, parser errors are written with `console.warn`.
742
+ ## Troubleshooting
526
743
 
527
- ## Smaller bundle
528
-
529
- The main entry imports OpenUI's built-in component library, which is large. `@june-ai/june-ui/core` exports the same streaming, actions, themes, and `JuneRenderer`, and does not import that library. You pass `library` yourself.
530
-
531
- ```tsx
532
- import { JuneRenderer } from "@june-ai/june-ui/core";
533
- import { library } from "./library";
534
- import "@openuidev/react-ui/components.css";
535
- import "@june-ai/june-ui/styles.css";
536
-
537
- export function View({ response, isStreaming }: { response: string | null; isStreaming: boolean }) {
538
- return <JuneRenderer library={library} response={response} isStreaming={isStreaming} />;
539
- }
744
+ | Symptom | Fix |
745
+ | :--- | :--- |
746
+ | Components render unstyled | Import `@openuidev/react-ui/components.css` and `@june-ai/june-ui/styles.css` once. |
747
+ | My component doesn't render | Is it registered on the instance/registry `<JuneUI>` uses? Check `june.registry.has("Name")`, and `onJuneError` for `PARSE_ERROR` (arguments vs. schema order). |
748
+ | `LIBRARY_ERROR … Zod 4 object schema` | Use `import { z } from "zod/v4"` (Zod 3.25) or Zod 4. |
749
+ | `LIBRARY_ERROR … already registered` | Pass `{ override: true }` or use `overrides`. Registering in a component body re-runs every render — do it at module scope. |
750
+ | The model never uses my component | Generate the prompt from the same instance: `june.prompt()`. Improve `description` and `.describe()`. |
751
+ | UI flickers or re-parses constantly | `library`, `actions`, `documents` and instances must be stable (module scope or `useMemo`). |
752
+ | Prose shows `**stars**` | Set `textFormat="markdown"`. |
753
+ | Forms do nothing | `isStreaming` must become `false` when the stream ends. |
754
+ | `/core` import not found | Use `moduleResolution: "bundler"` or `"node16"`. |
755
+ | Next.js: hooks error | Render June from a `"use client"` component. |
756
+
757
+ More: [docs/troubleshooting.md](docs/troubleshooting.md).
758
+
759
+ ## Using June UI with Claude Code and AI Coding Agents
760
+
761
+ Give your AI coding assistant this instruction so it integrates June correctly instead of reinventing or patching the SDK:
762
+
763
+ ```markdown
764
+ ## June UI (@june-ai/june-ui)
765
+
766
+ This project renders AI agent responses with June UI, a React SDK for Markdown + OpenUI Lang
767
+ (generative UI) with streaming, a component registry, themes, actions, tool approval and a
768
+ document editor. Docs: node_modules/@june-ai/june-ui/README.md and docs/.
769
+
770
+ - Install: `npm install @june-ai/june-ui @openuidev/react-lang @openuidev/react-headless zod zustand`.
771
+ Import `@openuidev/react-ui/components.css` and `@june-ai/june-ui/styles.css` once at the app entry.
772
+ - Import only from `@june-ai/june-ui` (or `@june-ai/june-ui/core`). Never import from its `dist/`
773
+ or `src/` paths, and never edit files in node_modules.
774
+ - Render: `<JuneUI response={text} isStreaming={bool} />`. Stream with `useJuneStream().startFetch(url, init)`
775
+ (`sse: true`, `jsonFieldExtractor(...)` or `langGraphStreamOptions()` for SSE / OpenAI / LangGraph).
776
+ - Custom components: `defineComponent({ name, description, schema: z.object({...}), component })`
777
+ with `import { z } from "zod/v4"`; register via `createJuneUI({ components: [...] })` at module scope
778
+ and render `<JuneUI june={june} … />` (or wrap in `<JuneUIProvider june={june}>`).
779
+ Schema key order = OpenUI Lang argument order. Generate the model prompt with `june.prompt()` on the server.
780
+ - Buttons/forms: whitelist handlers with `defineJuneAction` + the `actions` prop; mark risky ones
781
+ `sensitive: true` and use `useToolApproval().confirmAction`.
782
+ - Theme with `theme` + `themeVars`/`createTheme`; never override June CSS classes by default.
783
+ - Keep `library`, `actions`, `documents` and `createJuneUI(...)` results stable (module scope / useMemo).
784
+ - Handle failures through `onJuneError` (`JuneError.code`). Keep model API keys on the server.
785
+ - If something seems missing, check the README API reference before writing a workaround.
540
786
  ```
541
787
 
542
- `JuneUI` is `JuneRenderer` with the built-in library as the default. Props are the same except `library` is required on `JuneRenderer`.
788
+ Put it in your project's `CLAUDE.md` (Claude Code reads it automatically), or in the equivalent instruction file of your agent (`AGENTS.md`, `.cursor/rules`, …):
543
789
 
544
- ## Props
790
+ ```markdown
791
+ # CLAUDE.md
545
792
 
546
- `JuneUI` and `JuneRenderer` accept the props below. Any other prop of OpenUI's `Renderer` is forwarded and typed from the installed `@openuidev/react-lang`. That includes `onStateUpdate`, `initialState`, `toolProvider`, `queryLoader`, and `onParseResult`.
793
+ ## Project
794
+ Customer-support dashboard (Vite + React 19). Agent backend: FastAPI + LangGraph at /api.
547
795
 
548
- | Prop | Type | Default | |
549
- | :--- | :--- | :--- | :--- |
550
- | `response` | `string \| null` | — | Accumulated model output. |
551
- | `isStreaming` | `boolean` | `false` | The model is still writing. |
552
- | `library` | OpenUI library | built-in (`JuneUI` only) | Required on `JuneRenderer`. |
553
- | `mixedContent` | `boolean` | `true` | Draw prose around the program. `false` passes `response` through. |
554
- | `renderText` | `(text, index) => ReactNode` | `<p className="june-text">` | How a prose segment is drawn. |
555
- | `theme` | `"light" \| "dark" \| "system"` | omitted | Opt in to June text colors. |
556
- | `themeVars` | `Record<string, string>` | — | CSS variable overrides. |
557
- | `className` | `string` | — | Added on `.june-ui-root`. |
558
- | `style` | `CSSProperties` | — | Inline style on `.june-ui-root`. |
559
- | `actions` | `JuneActionMap` | — | Whitelist of handlers, keyed by action name. |
560
- | `confirmAction` | `(args) => boolean \| Promise<boolean>` | — | Approval for sensitive actions. |
561
- | `parseActionEvent` | `(event) => { name, payload } \| null` | built-in | Override how an event is read. |
562
- | `onAction` | `(event) => void` | — | Every OpenUI action, before the whitelist. |
563
- | `onError` | `(errors: unknown[]) => void` | — | Raw OpenUI errors. |
564
- | `onJuneError` | `(error: JuneError) => void` | — | Parse, render, and action failures. |
565
- | `loadingFallback` | `ReactNode` | `null` | Shown while streaming with nothing to draw. |
566
- | `emptyFallback` | `ReactNode` | `null` | Shown when finished with nothing to draw. |
567
- | `errorFallback` | `ReactNode` or `({ error, reset }) => ReactNode` | generic alert | Render-crash UI. |
568
- | `resetKeys` | `readonly unknown[]` | `[response]` | Change clears a caught render error. |
796
+ ## June UI (@june-ai/june-ui)
797
+ <paste the block above>
569
798
 
570
- ## Streaming reference
571
-
572
- `useJuneStream(options?)` returns:
799
+ ## Our conventions
800
+ - June components live in src/june/components/*.tsx and are registered in src/june/index.ts
801
+ (`export const june = createJuneUI({ components: [...] })`).
802
+ - The system prompt is generated in the backend from the same component list.
803
+ ```
573
804
 
574
- | Field | |
575
- | :--- | :--- |
576
- | `response` | Text so far. Kept after an error or an abort. |
577
- | `isStreaming` | `true` only while a run is in progress. |
578
- | `status` | `"idle" \| "streaming" \| "done" \| "error" \| "aborted"`. |
579
- | `error` | `JuneError` or `null`. `null` after a clean stop. |
580
- | `start(source)` | `source` is a `ReadableStream`, an async iterable, or `(signal) =>` either of those. Returns the full text, or `null`. |
581
- | `startFetch(input, init)` | `fetch` plus the same return. `init.sse` enables SSE parsing. |
582
- | `abort()` | Cancels the current run. |
583
- | `reset()` | Cancels and clears back to idle. |
805
+ ## Security
584
806
 
585
- Options: `allowEmpty` resolves an empty body to `""` instead of `STREAM_EMPTY`. `onComplete(text)` and `onError(error)` fire for that hook instance.
807
+ - The model chooses components and arguments, never which JavaScript runs. Only names in your `actions` map have handlers.
808
+ - Mark anything that spends money, deletes data or leaves the account `sensitive: true`, and pass `confirmAction` (or `useToolApproval`).
809
+ - Validate payloads with `schema`; treat `event.params` and tool arguments as untrusted even then.
810
+ - Open URLs only for `https:` (or `http:` in local development).
811
+ - Call the model, databases and MCP tools from the server. A client `toolProvider` should be a thin proxy.
812
+ - Markdown rendering does not execute raw HTML. The error boundary never prints model text.
586
813
 
587
- `fetchStream(input, init)` is the non-hook version of the request. It throws `NETWORK_ERROR` when `fetch` rejects or the status is not OK, `STREAM_ABORTED` when the signal already fired, and `STREAM_EMPTY` when the body is missing.
814
+ ## Migrating from 0.2.x
588
815
 
589
- `consumeStream(source, { signal, onText, allowEmpty })` decodes the source to one string. `onText(delta, accumulated)` sees every chunk.
816
+ **No breaking changes.** Every 0.2.7 export and prop still works (a test enforces this). New, optional:
590
817
 
591
- `sseToText(source, { extract, doneSentinel, signal })` turns an SSE byte stream into an async iterable of strings. The default `doneSentinel` is `"[DONE]"`.
818
+ | Before | Now (optional) |
819
+ | :--- | :--- |
820
+ | `createJuneLibrary({ root, components: [...all, Mine] })` + `<JuneUI library={lib}>` | `createJuneUI({ components: [Mine] })` + `<JuneUI june={june}>` — keeps the built-ins. |
821
+ | `defineJuneComponent({ props, component: ({ props }) => … })` | `defineComponent({ schema, component: (props) => … })` — plain React props, validation, metadata. Old style still accepted everywhere. |
822
+ | `withDocumentEditor(openuiLibrary)` | Already in the default library; `createJuneUI({ baseLibrary })` to start elsewhere. |
823
+ | Same props on every `<JuneUI>` | `createJuneUI({ defaults })` or `<JuneUIProvider defaults>`. |
824
+ | `renderText` for Markdown | `textFormat="markdown"`. |
825
+ | `themeVars` flat map | Also accepts `{ light, dark }`; `createTheme()`. |
826
+ | `confirmAction={window.confirm…}` | `useToolApproval().confirmAction` + `<ToolApprovalList>`. |
592
827
 
593
- ## Theme tokens
828
+ **Fixed:** `@june-ai/june-ui/core` was declared in `package.json` but not built in 0.2.7; it now ships (ESM + CJS + types), sharing state with the root entry.
594
829
 
595
- Set on `.june-ui-root` when `theme` is `"light"`, `"dark"`, or `"system"`:
830
+ Internal file paths changed (see [CHANGELOG.md](CHANGELOG.md)). They were never public; if you deep-imported `dist/` or `src/` files, switch to the package exports.
596
831
 
597
- | Variable | Light | Dark |
598
- | :--- | :--- | :--- |
599
- | `--june-foreground` | `#111827` | `#f3f4f6` |
600
- | `--june-muted` | `#6b7280` | `#9ca3af` |
601
- | `--june-border` | `#e5e7eb` | `#1f2937` |
602
- | `--june-accent` | `#4f46e5` | `#818cf8` |
603
- | `--june-danger` | `#b91c1c` | `#f87171` |
832
+ ## Development setup
604
833
 
605
- `color` on the root uses `--june-foreground` only when a theme was requested (the element has `data-june-theme="light"` or `"dark"`). `color-scheme` is set to the same value. Fonts and backgrounds stay yours.
834
+ ```bash
835
+ git clone <repository-url>
836
+ cd june-ui
837
+ npm install
838
+ npm test
839
+ ```
606
840
 
607
- ## Errors
841
+ Node 18+ is required. Source is in `src/`, tests in `tests/` (mirroring `src/`), runnable examples in `examples/` (type-checked against the source), docs in `docs/`.
608
842
 
609
- Every structured failure is a `JuneError`. `error.code` is one of:
843
+ ## Build and test commands
610
844
 
611
- | Code | |
845
+ | Command | Does |
612
846
  | :--- | :--- |
613
- | `PARSE_ERROR` | OpenUI reported one or more non-warning errors. `details` is that list. |
614
- | `RENDER_ERROR` | A component threw. `details.componentStack` is the React stack. |
615
- | `STREAM_ERROR` | The stream failed, including an SSE `event: error` frame. |
616
- | `STREAM_ABORTED` | The request was aborted. `useJuneStream` treats this as a stop, not `error`. |
617
- | `STREAM_EMPTY` | The response had no body, or the body decoded to nothing. |
618
- | `NETWORK_ERROR` | `fetch` failed, or the status was not OK. `details.status` is set for HTTP errors. |
619
- | `ACTION_ERROR` | A registered handler threw. |
620
- | `ACTION_VALIDATION_ERROR` | The payload failed the schema, or `defineJuneAction` was called without a handler. |
621
- | `ACTION_REJECTED` | A sensitive action had no confirm function. A user's "no" is not this code. |
622
- | `LIBRARY_ERROR` | `createJuneLibrary` rejected the library. |
623
-
624
- `isJuneError(value)` narrows an unknown catch. `toJuneError(error, fallbackCode)` wraps anything else.
625
-
626
- ## Security
627
-
628
- - The model chooses components and arguments. It does not choose which JavaScript runs. Only names in your `actions` map have handlers.
629
- - Mark anything that spends money, deletes data, or leaves the account as `sensitive: true`, and pass `confirmAction`.
630
- - Validate payloads with `schema`. Treat `event.params` as untrusted input even after that.
631
- - Open URLs only when the scheme is `https:` (or `http:` in local development).
632
- - Call the model, your database, and MCP tools from the server. `toolProvider` on the client should be a thin proxy to that server.
633
- - The render boundary does not print the thrown message by default. Keep raw model text out of error UI.
847
+ | `npm run typecheck` | TypeScript check of `src` and `tests`. |
848
+ | `npm run typecheck:examples` | Type-checks `examples/` against the public API. |
849
+ | `npm test` / `npm run test:watch` | Vitest (jsdom). |
850
+ | `npm run check:cycles` | Fails on circular imports in `src/`. |
851
+ | `npm run build` | tsup → `dist/` (ESM, CJS, `.d.ts`), plus `dist/june.css`. |
852
+ | `npm run check:exports` | Loads the built package (ESM + CJS), checks every export target, shared state across entries, and that `/core` stays free of OpenUI's component bundle. |
853
+ | `npm run validate` | All of the above, in order (also runs on `prepublishOnly`). |
854
+ | `npm run pack:check` | Lists what `npm publish` would include. |
855
+
856
+ ## Contribution guidelines
857
+
858
+ 1. Open an issue describing the change first for anything beyond a small fix.
859
+ 2. Keep module boundaries (see [docs/architecture.md](docs/architecture.md)): parser has no React, registry knows no app components, features don't import each other's internals, everything public is exported from `src/entry-core.ts` or `src/index.ts`.
860
+ 3. Add or update tests next to the matching folder in `tests/`, and docs/examples for public API changes.
861
+ 4. Don't remove or change public exports without a deprecation path; `tests/core/public-api.test.ts` guards them.
862
+ 5. Run `npm run validate` before opening a pull request, and add a line to `CHANGELOG.md`.
863
+
864
+ ## License
865
+
866
+ [MIT](LICENSE)