@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/CHANGELOG.md +49 -2
- package/README.md +633 -400
- package/dist/{chunk-VFZI3SEL.js → chunk-27XM6EVX.js} +8 -8
- package/dist/chunk-G6YPSE6J.cjs +520 -0
- package/dist/chunk-GFCROSGQ.cjs +4399 -0
- package/dist/chunk-JSRO5CRO.js +4399 -0
- package/dist/{document-engine-RKD5SLXR.js → document-engine-BUTK73DW.js} +1 -1
- package/dist/document-engine-GXZH2LNW.cjs +12 -0
- package/dist/entry-core.cjs +148 -0
- package/dist/entry-core.d.cts +1005 -0
- package/dist/entry-core.d.ts +1005 -0
- package/dist/entry-core.js +149 -0
- package/dist/index.cjs +182 -4238
- package/dist/index.d.cts +80 -525
- package/dist/index.d.ts +80 -525
- package/dist/index.js +168 -3599
- package/dist/june.css +94 -0
- package/docs/architecture.md +95 -0
- package/docs/component-registration.md +115 -0
- package/docs/custom-features.md +68 -0
- package/docs/streaming.md +76 -0
- package/docs/theming.md +65 -0
- package/docs/troubleshooting.md +59 -0
- package/package.json +19 -5
package/README.md
CHANGED
|
@@ -1,79 +1,140 @@
|
|
|
1
|
-
#
|
|
1
|
+
# June UI — `@june-ai/june-ui`
|
|
2
2
|
|
|
3
|
-
React SDK for rendering [OpenUI Lang](https://openui.com/docs/openui-lang)
|
|
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}
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
+
## Quick start
|
|
63
129
|
|
|
64
|
-
|
|
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 = `
|
|
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}
|
|
165
|
+
return <JuneUI response={response} textFormat="markdown" />;
|
|
121
166
|
}
|
|
122
167
|
```
|
|
123
168
|
|
|
124
|
-
|
|
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
|
-
|
|
171
|
+
During development, proxy `/api` to your backend in `vite.config.ts`: `server: { proxy: { "/api": "http://localhost:8000" } }`.
|
|
131
172
|
|
|
132
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
190
|
+
Render `<JunePreview response={...} />` from any server or client page.
|
|
158
191
|
|
|
159
|
-
|
|
160
|
-
import { JunePreview } from "./june-preview";
|
|
192
|
+
## Basic chat integration
|
|
161
193
|
|
|
162
|
-
|
|
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
|
-
|
|
196
|
+
```tsx
|
|
197
|
+
import { useState } from "react";
|
|
198
|
+
import { createJuneUI, JuneUI, JuneUIProvider, useJuneStream, type OpenUIActionEvent } from "@june-ai/june-ui";
|
|
168
199
|
|
|
169
|
-
|
|
200
|
+
const june = createJuneUI({
|
|
201
|
+
defaults: { theme: "system", textFormat: "markdown", onJuneError: (e) => console.warn(e.code, e.message) },
|
|
202
|
+
});
|
|
170
203
|
|
|
171
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
+
<JuneUI response={"## Summary\n\n- **Revenue** up 12%\n- Churn down"} textFormat="markdown" />
|
|
250
|
+
```
|
|
195
251
|
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
300
|
+
## Streaming integration
|
|
218
301
|
|
|
219
|
-
|
|
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("
|
|
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
|
-
|
|
242
|
-
<
|
|
243
|
-
|
|
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
|
|
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
|
-
|
|
329
|
+
</>
|
|
257
330
|
);
|
|
258
331
|
}
|
|
259
332
|
```
|
|
260
333
|
|
|
261
|
-
`stream.status` is `"idle"
|
|
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
|
-
|
|
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
|
-
|
|
344
|
+
**LangGraph** (server API, `stream_mode: ["messages-tuple"]` or `"messages"`):
|
|
266
345
|
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
361
|
+
More: [docs/streaming.md](docs/streaming.md) · [examples/streaming](examples/streaming/App.tsx).
|
|
278
362
|
|
|
279
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
381
|
+
**2. Define it: name, description for the model, Zod schema for the props.**
|
|
291
382
|
|
|
292
383
|
```tsx
|
|
293
|
-
import {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
399
|
+
**3. Register it** — on an instance (recommended), or on the default registry:
|
|
304
400
|
|
|
305
|
-
|
|
401
|
+
```tsx
|
|
402
|
+
import { createJuneUI, JuneUI, registerComponent } from "@june-ai/june-ui";
|
|
306
403
|
|
|
307
|
-
|
|
308
|
-
|
|
404
|
+
// a) isolated instance: <JuneUI june={june} … /> or <JuneUIProvider june={june}>
|
|
405
|
+
const june = createJuneUI({ components: [KpiCard] });
|
|
309
406
|
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
|
414
|
+
<JuneUI
|
|
415
|
+
june={june}
|
|
416
|
+
response={`root = Stack([revenue])
|
|
417
|
+
revenue = KpiCard("Revenue", "$43,000", "up")`}
|
|
418
|
+
/>
|
|
322
419
|
```
|
|
323
420
|
|
|
324
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
334
|
-
|
|
425
|
+
```ts
|
|
426
|
+
const system = june.prompt({ preamble: "You are an analytics assistant." }); // includes KpiCard
|
|
335
427
|
```
|
|
336
428
|
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
492
|
+
More: [docs/theming.md](docs/theming.md) · [examples/custom-theme](examples/custom-theme/App.tsx).
|
|
359
493
|
|
|
360
|
-
|
|
494
|
+
## Document editor integration
|
|
361
495
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
522
|
+
Teach the model when to use it, and give it the user's current text on the next turn:
|
|
385
523
|
|
|
386
|
-
|
|
524
|
+
```ts
|
|
525
|
+
import { getDocumentEditorInstructions, documentPromptOptions, serializeDocumentsForAgent } from "@june-ai/june-ui";
|
|
387
526
|
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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
|
-
|
|
533
|
+
Example: [examples/document-editor](examples/document-editor/App.tsx).
|
|
399
534
|
|
|
400
|
-
|
|
535
|
+
## Actions and tool approval
|
|
401
536
|
|
|
402
|
-
|
|
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
|
|
418
|
-
|
|
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
|
-
|
|
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
|
-
|
|
430
|
-
response={response}
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
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
|
-
|
|
588
|
+
Example: [examples/tool-approval](examples/tool-approval/App.tsx).
|
|
442
589
|
|
|
443
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
602
|
+
More: [docs/custom-features.md](docs/custom-features.md).
|
|
455
603
|
|
|
456
|
-
|
|
604
|
+
## API reference
|
|
457
605
|
|
|
458
|
-
|
|
459
|
-
import { createJuneLibrary, defineJuneComponent, JuneUI } from "@june-ai/june-ui";
|
|
460
|
-
import { z } from "zod/v4";
|
|
606
|
+
### Entry points
|
|
461
607
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
472
|
-
root: "Greeting",
|
|
473
|
-
components: [Greeting],
|
|
474
|
-
});
|
|
614
|
+
### Core
|
|
475
615
|
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
624
|
+
### `JuneUI` / `JuneRenderer` props
|
|
482
625
|
|
|
483
|
-
|
|
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
|
-
|
|
486
|
-
|
|
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
|
-
|
|
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
|
-
|
|
696
|
+
The package ships ESM and CommonJS builds with `.d.ts` / `.d.cts` declarations for both entries. Notable types:
|
|
492
697
|
|
|
493
|
-
|
|
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
|
-
|
|
703
|
+
`moduleResolution: "bundler"` or `"node16"` is recommended so the `/core` subpath resolves.
|
|
496
704
|
|
|
497
|
-
|
|
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
|
-
| `
|
|
500
|
-
| `
|
|
501
|
-
| `
|
|
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
|
-
|
|
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
|
-
`
|
|
740
|
+
`isJuneError(value)` narrows a `catch`; `toJuneError(error, fallbackCode)` wraps anything. Without `onError`/`onJuneError`, parser errors are logged with `console.warn`.
|
|
524
741
|
|
|
525
|
-
|
|
742
|
+
## Troubleshooting
|
|
526
743
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
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
|
-
|
|
790
|
+
```markdown
|
|
791
|
+
# CLAUDE.md
|
|
545
792
|
|
|
546
|
-
|
|
793
|
+
## Project
|
|
794
|
+
Customer-support dashboard (Vite + React 19). Agent backend: FastAPI + LangGraph at /api.
|
|
547
795
|
|
|
548
|
-
|
|
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
|
-
##
|
|
571
|
-
|
|
572
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
814
|
+
## Migrating from 0.2.x
|
|
588
815
|
|
|
589
|
-
|
|
816
|
+
**No breaking changes.** Every 0.2.7 export and prop still works (a test enforces this). New, optional:
|
|
590
817
|
|
|
591
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
834
|
+
```bash
|
|
835
|
+
git clone <repository-url>
|
|
836
|
+
cd june-ui
|
|
837
|
+
npm install
|
|
838
|
+
npm test
|
|
839
|
+
```
|
|
606
840
|
|
|
607
|
-
|
|
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
|
-
|
|
843
|
+
## Build and test commands
|
|
610
844
|
|
|
611
|
-
|
|
|
845
|
+
| Command | Does |
|
|
612
846
|
| :--- | :--- |
|
|
613
|
-
| `
|
|
614
|
-
| `
|
|
615
|
-
| `
|
|
616
|
-
| `
|
|
617
|
-
| `
|
|
618
|
-
| `
|
|
619
|
-
| `
|
|
620
|
-
| `
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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)
|