pi-generative-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/.pi/extensions/generative-ui/claude-guidelines/CORE.md +89 -0
  2. package/.pi/extensions/generative-ui/claude-guidelines/art.md +175 -0
  3. package/.pi/extensions/generative-ui/claude-guidelines/art_interactive.md +297 -0
  4. package/.pi/extensions/generative-ui/claude-guidelines/chart.md +255 -0
  5. package/.pi/extensions/generative-ui/claude-guidelines/chart_interactive.md +255 -0
  6. package/.pi/extensions/generative-ui/claude-guidelines/diagram.md +624 -0
  7. package/.pi/extensions/generative-ui/claude-guidelines/interactive.md +209 -0
  8. package/.pi/extensions/generative-ui/claude-guidelines/mockup.md +209 -0
  9. package/.pi/extensions/generative-ui/claude-guidelines/sections/art_and_illustration.md +11 -0
  10. package/.pi/extensions/generative-ui/claude-guidelines/sections/charts_chart_js.md +43 -0
  11. package/.pi/extensions/generative-ui/claude-guidelines/sections/color_palette.md +31 -0
  12. package/.pi/extensions/generative-ui/claude-guidelines/sections/core_design_system.md +60 -0
  13. package/.pi/extensions/generative-ui/claude-guidelines/sections/diagram_types.md +427 -0
  14. package/.pi/extensions/generative-ui/claude-guidelines/sections/mapping.json +44 -0
  15. package/.pi/extensions/generative-ui/claude-guidelines/sections/modules.md +17 -0
  16. package/.pi/extensions/generative-ui/claude-guidelines/sections/preamble.md +1 -0
  17. package/.pi/extensions/generative-ui/claude-guidelines/sections/svg_setup.md +73 -0
  18. package/.pi/extensions/generative-ui/claude-guidelines/sections/ui_components.md +87 -0
  19. package/.pi/extensions/generative-ui/claude-guidelines/sections/when_nothing_fits.md +6 -0
  20. package/.pi/extensions/generative-ui/guidelines.ts +795 -0
  21. package/.pi/extensions/generative-ui/index.ts +401 -0
  22. package/README.md +124 -0
  23. package/package.json +22 -0
@@ -0,0 +1,401 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { StringEnum } from "@mariozechner/pi-ai";
4
+ import { Text } from "@mariozechner/pi-tui";
5
+ import { join, dirname } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { getGuidelines, AVAILABLE_MODULES } from "./guidelines.js";
8
+
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const GLIMPSE_PATH = join(__dirname, "../../../node_modules/glimpseui/src/glimpse.mjs");
12
+
13
+ // Shell HTML with a root container — used for streaming.
14
+ // Content is injected via win.send() JS eval, not setHTML(), to avoid full-page flashes.
15
+ function shellHTML(): string {
16
+ return `<!DOCTYPE html><html><head><meta charset="utf-8">
17
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
18
+ <style>
19
+ *{box-sizing:border-box}
20
+ body{margin:0;padding:1rem;font-family:system-ui,-apple-system,sans-serif;background:#1a1a1a;color:#e0e0e0;}
21
+ @keyframes _fadeIn{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:none;}}
22
+ </style>
23
+ </head><body><div id="root"></div>
24
+ <script>
25
+ window._morphReady = false;
26
+ window._pending = null;
27
+ window._setContent = function(html) {
28
+ if (!window._morphReady) { window._pending = html; return; }
29
+ var root = document.getElementById('root');
30
+ var target = document.createElement('div');
31
+ target.id = 'root';
32
+ target.innerHTML = html;
33
+ morphdom(root, target, {
34
+ onBeforeElUpdated: function(from, to) {
35
+ if (from.isEqualNode(to)) return false;
36
+ return true;
37
+ },
38
+ onNodeAdded: function(node) {
39
+ if (node.nodeType === 1 && node.tagName !== 'STYLE' && node.tagName !== 'SCRIPT') {
40
+ node.style.animation = '_fadeIn 0.3s ease both';
41
+ }
42
+ return node;
43
+ }
44
+ });
45
+ };
46
+ window._runScripts = function() {
47
+ document.querySelectorAll('#root script').forEach(function(old) {
48
+ var s = document.createElement('script');
49
+ if (old.src) { s.src = old.src; } else { s.textContent = old.textContent; }
50
+ old.parentNode.replaceChild(s, old);
51
+ });
52
+ };
53
+ </script>
54
+ <script src="https://cdn.jsdelivr.net/npm/morphdom@2.7.4/dist/morphdom-umd.min.js"
55
+ onload="window._morphReady=true;if(window._pending){window._setContent(window._pending);window._pending=null;}"></script>
56
+ </body></html>`;
57
+ }
58
+
59
+ // Wrap HTML fragment into a full document for Glimpse (non-streaming fallback)
60
+ function wrapHTML(code: string, isSVG = false): string {
61
+ if (isSVG) {
62
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"></head>
63
+ <body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#1a1a1a;color:#e0e0e0;">
64
+ ${code}</body></html>`;
65
+ }
66
+ return `<!DOCTYPE html><html><head><meta charset="utf-8">
67
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
68
+ <style>*{box-sizing:border-box}body{margin:0;padding:1rem;font-family:system-ui,-apple-system,sans-serif;background:#1a1a1a;color:#e0e0e0}</style>
69
+ </head><body>${code}</body></html>`;
70
+ }
71
+
72
+ // Escape a string for safe injection into a JS string literal
73
+ function escapeJS(s: string): string {
74
+ return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/<\/script>/gi, '<\\/script>');
75
+ }
76
+
77
+ export default function (pi: ExtensionAPI) {
78
+ let hasSeenReadMe = false;
79
+ let activeWindows: any[] = [];
80
+ let glimpseModule: any = null;
81
+
82
+ // Lazy-load glimpse module
83
+ async function getGlimpse() {
84
+ if (!glimpseModule) {
85
+ glimpseModule = await import(GLIMPSE_PATH);
86
+ }
87
+ return glimpseModule;
88
+ }
89
+
90
+ // ── Streaming state ─────────────────────────────────────────────────────
91
+
92
+ // Tracks in-flight show_widget tool calls being streamed
93
+ interface StreamingWidget {
94
+ contentIndex: number;
95
+ window: any | null;
96
+ lastHTML: string;
97
+ updateTimer: any;
98
+ ready: boolean;
99
+ }
100
+
101
+ let streaming: StreamingWidget | null = null;
102
+
103
+ // ── message_update: intercept streaming tool calls ────────────────────
104
+
105
+ pi.on("message_update", async (event) => {
106
+ const raw: any = event.assistantMessageEvent;
107
+ if (!raw) return;
108
+
109
+ // Tool call starts streaming
110
+ if (raw.type === "toolcall_start") {
111
+ const partial: any = raw.partial;
112
+ const block = partial?.content?.[raw.contentIndex];
113
+ if (block?.type === "toolCall" && block?.name === "show_widget") {
114
+ streaming = {
115
+ contentIndex: raw.contentIndex,
116
+ window: null,
117
+ lastHTML: "",
118
+ updateTimer: null,
119
+ ready: false,
120
+ };
121
+ }
122
+ return;
123
+ }
124
+
125
+ // Tool call input JSON delta — arguments already parsed by pi-ai
126
+ if (raw.type === "toolcall_delta" && streaming && raw.contentIndex === streaming.contentIndex) {
127
+ const partial: any = raw.partial;
128
+ const block = partial?.content?.[raw.contentIndex];
129
+ const html = block?.arguments?.widget_code;
130
+ if (!html || html.length < 20 || html === streaming.lastHTML) return;
131
+
132
+ streaming.lastHTML = html;
133
+
134
+ // Debounce updates to ~150ms for smooth rendering
135
+ if (streaming.updateTimer) return;
136
+ streaming.updateTimer = setTimeout(async () => {
137
+ if (!streaming) return;
138
+ streaming.updateTimer = null;
139
+
140
+ try {
141
+ if (!streaming.window) {
142
+ // Open window with empty shell — content will be injected via JS eval
143
+ const args = block?.arguments ?? {};
144
+ const title = (args.title ?? "Widget").replace(/_/g, " ");
145
+ const width = args.width ?? 800;
146
+ const height = args.height ?? 600;
147
+
148
+ const { open } = await getGlimpse();
149
+ streaming.window = open(shellHTML(), { width, height, title });
150
+ activeWindows.push(streaming.window);
151
+
152
+ streaming.window.on("ready", () => {
153
+ if (!streaming) return;
154
+ streaming.ready = true;
155
+ // Inject the content we've accumulated so far
156
+ const escaped = escapeJS(streaming.lastHTML);
157
+ streaming.window.send(`window._setContent('${escaped}')`);
158
+ });
159
+ } else if (streaming.ready) {
160
+ // Update content via JS — no full page replace
161
+ const escaped = escapeJS(streaming.lastHTML);
162
+ streaming.window.send(`window._setContent('${escaped}')`);
163
+ }
164
+ } catch {}
165
+ }, 150);
166
+ return;
167
+ }
168
+
169
+ // Tool call complete — final update with complete HTML + execute scripts
170
+ if (raw.type === "toolcall_end" && streaming && raw.contentIndex === streaming.contentIndex) {
171
+ if (streaming.updateTimer) {
172
+ clearTimeout(streaming.updateTimer);
173
+ streaming.updateTimer = null;
174
+ }
175
+
176
+ const toolCall = raw.toolCall;
177
+ if (toolCall?.arguments?.widget_code && streaming.window && streaming.ready) {
178
+ const escaped = escapeJS(toolCall.arguments.widget_code);
179
+ streaming.window.send(`window._setContent('${escaped}'); window._runScripts();`);
180
+ }
181
+ // Don't clear streaming — execute() will pick up the window
182
+ return;
183
+ }
184
+ });
185
+
186
+ // ── read_me tool ──────────────────────────────────────────────────────────
187
+
188
+ pi.registerTool({
189
+ name: "visualize_read_me",
190
+ label: "Read Guidelines",
191
+ description:
192
+ "Returns design guidelines for show_widget (CSS patterns, colors, typography, layout rules, examples). " +
193
+ "Call once before your first show_widget call. Do NOT mention this call to the user — it is an internal setup step.",
194
+ promptSnippet: "Load design guidelines before creating widgets. Call silently before first show_widget use.",
195
+ promptGuidelines: [
196
+ "Call visualize_read_me once before your first show_widget call to load design guidelines.",
197
+ "Do NOT mention the read_me call to the user — call it silently, then proceed directly to building the widget.",
198
+ "Pick the modules that match your use case: interactive, chart, mockup, art, diagram.",
199
+ ],
200
+ parameters: Type.Object({
201
+ modules: Type.Array(
202
+ StringEnum(AVAILABLE_MODULES as readonly string[]),
203
+ { description: "Which module(s) to load. Pick all that fit." }
204
+ ),
205
+ }),
206
+
207
+ async execute(_toolCallId, params) {
208
+ hasSeenReadMe = true;
209
+ const content = getGuidelines(params.modules);
210
+ return {
211
+ content: [{ type: "text" as const, text: content }],
212
+ details: { modules: params.modules },
213
+ };
214
+ },
215
+
216
+ renderCall(args: any, theme: any) {
217
+ const mods = (args.modules ?? []).join(", ");
218
+ return new Text(
219
+ theme.fg("toolTitle", theme.bold("read_me ")) + theme.fg("muted", mods),
220
+ 0, 0
221
+ );
222
+ },
223
+
224
+ renderResult(_result: any, { isPartial }: any, theme: any) {
225
+ if (isPartial) return new Text(theme.fg("warning", "Loading guidelines..."), 0, 0);
226
+ return new Text(theme.fg("dim", "Guidelines loaded"), 0, 0);
227
+ },
228
+ });
229
+
230
+ // ── show_widget tool ──────────────────────────────────────────────────────
231
+
232
+ pi.registerTool({
233
+ name: "show_widget",
234
+ label: "Show Widget",
235
+ description:
236
+ "Show visual content — SVG graphics, diagrams, charts, or interactive HTML widgets — in a native macOS window. " +
237
+ "Use for flowcharts, dashboards, forms, calculators, data tables, games, illustrations, or any visual content. " +
238
+ "The HTML is rendered in a native WKWebView with full CSS/JS support including Canvas and CDN libraries. " +
239
+ "The page gets a window.glimpse.send(data) bridge to send JSON data back to the agent. " +
240
+ "IMPORTANT: Call visualize_read_me once before your first show_widget call.",
241
+ promptSnippet: "Render interactive HTML/SVG widgets in a native macOS window (WKWebView). Supports full CSS, JS, Canvas, Chart.js.",
242
+ promptGuidelines: [
243
+ "Use show_widget when the user asks for visual content: charts, diagrams, interactive explainers, UI mockups, art.",
244
+ "Always call visualize_read_me first to load design guidelines, then set i_have_seen_read_me: true.",
245
+ "The widget opens in a native macOS window — it has full browser capabilities (Canvas, JS, CDN libraries).",
246
+ "Structure HTML as fragments: no DOCTYPE/<html>/<head>/<body>. Style first, then HTML, then scripts.",
247
+ "The page has window.glimpse.send(data) to send data back. Use it for user choices and interactions.",
248
+ "Keep widgets focused and appropriately sized. Default is 800x600 but adjust to fit content.",
249
+ "For interactive explainers: sliders, live calculations, Chart.js charts.",
250
+ "For SVG: start code with <svg> tag, it will be auto-detected.",
251
+ "Be concise in your responses",
252
+ ],
253
+ parameters: Type.Object({
254
+ i_have_seen_read_me: Type.Boolean({
255
+ description: "Confirm you have already called visualize_read_me in this conversation.",
256
+ }),
257
+ title: Type.String({
258
+ description: "Short snake_case identifier for this widget (used as window title).",
259
+ }),
260
+ widget_code: Type.String({
261
+ description:
262
+ "HTML or SVG code to render. For SVG: raw SVG starting with <svg>. " +
263
+ "For HTML: raw content fragment, no DOCTYPE/<html>/<head>/<body>.",
264
+ }),
265
+ width: Type.Optional(Type.Number({ description: "Window width in pixels. Default: 800." })),
266
+ height: Type.Optional(Type.Number({ description: "Window height in pixels. Default: 600." })),
267
+ floating: Type.Optional(Type.Boolean({ description: "Keep window always on top. Default: false." })),
268
+ }),
269
+
270
+ async execute(_toolCallId, params, signal) {
271
+ if (!params.i_have_seen_read_me) {
272
+ throw new Error("You must call visualize_read_me before show_widget. Set i_have_seen_read_me: true after doing so.");
273
+ }
274
+
275
+ const code = params.widget_code;
276
+ const isSVG = code.trimStart().startsWith("<svg");
277
+ const title = params.title.replace(/_/g, " ");
278
+ const width = params.width ?? 800;
279
+ const height = params.height ?? 600;
280
+
281
+ // Check if we already have a streaming window from message_update
282
+ let win: any = null;
283
+
284
+ if (streaming?.window) {
285
+ win = streaming.window;
286
+ // Send final complete HTML + run scripts via JS eval (no full page replace)
287
+ if (streaming.ready) {
288
+ const escaped = escapeJS(code);
289
+ win.send(`window._setContent('${escaped}'); window._runScripts();`);
290
+ }
291
+ streaming = null;
292
+ } else {
293
+ // No streaming window — open fresh (fallback for non-streaming providers)
294
+ const { open } = await getGlimpse();
295
+ win = open(wrapHTML(code, isSVG), {
296
+ width,
297
+ height,
298
+ title,
299
+ floating: params.floating ?? false,
300
+ });
301
+ activeWindows.push(win);
302
+ }
303
+
304
+ return new Promise<any>((resolve) => {
305
+ let messageData: any = null;
306
+ let resolved = false;
307
+
308
+ const finish = (reason: string) => {
309
+ if (resolved) return;
310
+ resolved = true;
311
+ activeWindows = activeWindows.filter((w) => w !== win);
312
+ resolve({
313
+ content: [
314
+ {
315
+ type: "text" as const,
316
+ text: messageData
317
+ ? `Widget rendered. User interaction data: ${JSON.stringify(messageData)}`
318
+ : `Widget "${title}" rendered and shown to the user (${width}×${height}). ${reason}`,
319
+ },
320
+ ],
321
+ details: {
322
+ title: params.title,
323
+ width,
324
+ height,
325
+ isSVG,
326
+ messageData,
327
+ closedReason: reason,
328
+ },
329
+ });
330
+ };
331
+
332
+ win.on("message", (data: any) => {
333
+ messageData = data;
334
+ finish("User sent data from widget.");
335
+ });
336
+
337
+ win.on("closed", () => {
338
+ finish("Window closed by user.");
339
+ });
340
+
341
+ win.on("error", (err: Error) => {
342
+ finish(`Error: ${err.message}`);
343
+ });
344
+
345
+ if (signal) {
346
+ signal.addEventListener("abort", () => {
347
+ try { win.close(); } catch {}
348
+ finish("Aborted.");
349
+ }, { once: true });
350
+ }
351
+
352
+ // Auto-resolve after 120s if no interaction
353
+ setTimeout(() => {
354
+ finish("Widget still open (timed out waiting for interaction).");
355
+ }, 120_000);
356
+ });
357
+ },
358
+
359
+ renderCall(args: any, theme: any) {
360
+ const title = (args.title ?? "widget").replace(/_/g, " ");
361
+ const size = args.width && args.height ? ` ${args.width}×${args.height}` : "";
362
+ let text = theme.fg("toolTitle", theme.bold("show_widget "));
363
+ text += theme.fg("accent", title);
364
+ if (size) text += theme.fg("dim", size);
365
+ return new Text(text, 0, 0);
366
+ },
367
+
368
+ renderResult(result: any, { isPartial, expanded }: any, theme: any) {
369
+ if (isPartial) {
370
+ return new Text(theme.fg("warning", "⟳ Widget rendering..."), 0, 0);
371
+ }
372
+
373
+ const details = result.details ?? {};
374
+ const title = (details.title ?? "widget").replace(/_/g, " ");
375
+ let text = theme.fg("success", "✓ ") + theme.fg("accent", title);
376
+ text += theme.fg("dim", ` ${details.width ?? 800}×${details.height ?? 600}`);
377
+ if (details.isSVG) text += theme.fg("dim", " (SVG)");
378
+
379
+ if (details.closedReason) {
380
+ text += "\n" + theme.fg("muted", ` ${details.closedReason}`);
381
+ }
382
+
383
+ if (expanded && details.messageData) {
384
+ text += "\n" + theme.fg("dim", ` Data: ${JSON.stringify(details.messageData, null, 2)}`);
385
+ }
386
+
387
+ return new Text(text, 0, 0);
388
+ },
389
+ });
390
+
391
+ // ── cleanup on shutdown ───────────────────────────────────────────────────
392
+
393
+ pi.on("session_shutdown", async () => {
394
+ if (streaming?.updateTimer) clearTimeout(streaming.updateTimer);
395
+ streaming = null;
396
+ for (const win of activeWindows) {
397
+ try { win.close(); } catch {}
398
+ }
399
+ activeWindows = [];
400
+ });
401
+ }
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # pi-generative-ui
2
+
3
+ Claude.ai's generative UI - reverse-engineered, rebuilt for [pi](https://github.com/badlogic/pi).
4
+
5
+ Ask pi to "show me how compound interest works" and get a live interactive widget - sliders, charts, animations - rendered in a native macOS window. Not a screenshot. Not a code block. A real HTML application with JavaScript, streaming live as the LLM generates it.
6
+
7
+ https://github.com/user-attachments/assets/placeholder-demo-video
8
+
9
+ ## How it works
10
+
11
+ On claude.ai, when you ask Claude to visualize something, it calls a tool called `show_widget` that renders HTML inline in the conversation. The HTML streams live - you see cards, charts, and sliders appear as tokens arrive.
12
+
13
+ This extension replicates that system for pi:
14
+
15
+ 1. **LLM calls `visualize_read_me`** - loads design guidelines (lazy, only the relevant modules)
16
+ 2. **LLM calls `show_widget`** - generates an HTML fragment as a tool call parameter
17
+ 3. **Extension intercepts the stream** - opens a native macOS window via [Glimpse](https://github.com/hazat/glimpse) and feeds partial HTML as tokens arrive
18
+ 4. **[morphdom](https://github.com/patrick-steele-idem/morphdom) diffs the DOM** - new elements fade in smoothly, unchanged elements stay untouched
19
+ 5. **Scripts execute on completion** - Chart.js, D3, Three.js, anything from CDN
20
+
21
+ The widget window has full browser capabilities (WKWebView) and a bidirectional bridge - `window.glimpse.send(data)` sends data back to the agent.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pi install git:github.com/user/pi-generative-ui
27
+ ```
28
+
29
+ > macOS only. Requires Swift toolchain (ships with Xcode or Xcode Command Line Tools).
30
+
31
+ ## Usage
32
+
33
+ Just ask pi to visualize things. The extension adds two tools that the LLM calls automatically:
34
+
35
+ - **"Show me how compound interest works"** → interactive explainer with sliders and Chart.js
36
+ - **"Visualize the architecture of a transformer"** → SVG diagram with labeled components
37
+ - **"Create a dashboard for this data"** → metric cards, charts, tables
38
+ - **"Draw a particle system"** → Canvas animation
39
+
40
+ The LLM decides when to use widgets vs text based on the request. Explanatory/visual requests trigger widgets; code/text requests stay in the terminal.
41
+
42
+ ## What's inside
43
+
44
+ ### The guidelines - extracted from Claude
45
+
46
+ The design guidelines aren't hand-written. They're **extracted verbatim from claude.ai**.
47
+
48
+ Here's the trick: you can export any claude.ai conversation as JSON. The export includes full tool call payloads - including the complete `read_me` tool results containing Anthropic's actual design system. 72K of production rules covering typography, color palettes, streaming-safe CSS patterns, Chart.js configuration, SVG diagram engineering, and more.
49
+
50
+ We triggered `read_me` with each module combination, exported the conversation, parsed the JSON, split the responses into deduplicated sections, and verified byte-level accuracy against the originals. The result: our LLM gets the exact same instructions Claude gets on claude.ai.
51
+
52
+ Five modules, loaded on demand:
53
+
54
+ | Module | Size | What it covers |
55
+ |---|---|---|
56
+ | `interactive` | 19KB | Sliders, metric cards, live calculations |
57
+ | `chart` | 22KB | Chart.js setup, custom legends, number formatting |
58
+ | `mockup` | 19KB | UI component tokens, cards, forms, skeleton loading |
59
+ | `art` | 17KB | SVG illustration, Canvas animation, creative patterns |
60
+ | `diagram` | 59KB | Flowcharts, architecture diagrams, SVG arrow systems |
61
+
62
+ ### Streaming architecture
63
+
64
+ The extension intercepts pi's streaming events (`toolcall_start` / `toolcall_delta` / `toolcall_end`) to render the widget live as tokens arrive:
65
+
66
+ ```
67
+ toolcall_start → initialize streaming state
68
+ toolcall_delta → debounce 150ms, open window, morphdom diff
69
+ toolcall_end → final diff + execute <script> tags
70
+ execute() → reuse window, wait for interaction or close
71
+ ```
72
+
73
+ Key details:
74
+ - **Shell HTML + JS eval** - window opens with an empty shell; content injected via `win.send()`, not `setHTML()`, to avoid full-page flashes
75
+ - **morphdom DOM diffing** - only changed nodes update; new nodes get a 0.3s fade-in animation
76
+ - **pi-ai's `parseStreamingJson`** - no need for a partial JSON parser; pi already provides parsed `arguments` on every delta
77
+ - **150ms debounce** - batches rapid token updates for smooth visual rendering
78
+ - **Dark mode by default** - `#1a1a1a` background, designed for macOS WKWebView
79
+
80
+ ### Glimpse
81
+
82
+ [Glimpse](https://github.com/hazat/glimpse) is a native macOS micro-UI library. It opens a WKWebView window in under 50ms via a tiny Swift binary. No Electron, no browser tab, no runtime dependencies beyond the system WebKit.
83
+
84
+ The Swift source compiles automatically on `npm install` via `postinstall`.
85
+
86
+ ## Project structure
87
+
88
+ ```
89
+ pi-generative-ui/
90
+ ├── .pi/extensions/generative-ui/
91
+ │ ├── index.ts # Extension: tools, streaming, Glimpse integration
92
+ │ ├── guidelines.ts # 72K of verbatim claude.ai design guidelines
93
+ │ └── claude-guidelines/ # Raw extracted markdown (reference)
94
+ │ ├── art.md
95
+ │ ├── chart.md
96
+ │ ├── diagram.md
97
+ │ ├── interactive.md
98
+ │ ├── mockup.md
99
+ │ └── sections/ # Deduplicated sections
100
+ └── package.json # pi-package manifest
101
+ ```
102
+
103
+ ## How the guidelines were extracted
104
+
105
+ 1. Start a conversation on claude.ai that triggers `show_widget`
106
+ 2. Call `read_me` with each module combination (`art`, `chart`, `diagram`, `interactive`, `mockup`)
107
+ 3. Export the conversation as JSON from claude.ai settings
108
+ 4. Parse the JSON - every `tool_result` for `visualize:read_me` contains the complete guidelines
109
+ 5. Split each response at `##` heading boundaries
110
+ 6. Deduplicate shared sections (e.g., "Color palette" appears in chart, mockup, interactive, diagram)
111
+ 7. Verify reconstruction matches the originals (4/5 exact, 1 has a single whitespace char difference)
112
+
113
+ The raw `read_me` responses are preserved in [`claude-guidelines/`](.pi/extensions/generative-ui/claude-guidelines/) - the original markdown exactly as claude.ai returned it, before splitting and deduplication. The conversation export JSON is not included in this repo.
114
+
115
+ ## Credits
116
+
117
+ - [pi](https://github.com/badlogic/pi) - the extensible coding agent that makes this possible
118
+ - [Glimpse](https://github.com/hazat/glimpse) - native macOS WKWebView windows
119
+ - [morphdom](https://github.com/patrick-steele-idem/morphdom) - DOM diffing for smooth streaming
120
+ - Anthropic - for building the generative UI system we reverse-engineered
121
+
122
+ ## License
123
+
124
+ MIT
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "pi-generative-ui",
3
+ "version": "0.1.0",
4
+ "description": "Generative UI for pi — render interactive HTML/SVG widgets in native macOS windows via Glimpse",
5
+ "type": "commonjs",
6
+ "keywords": ["pi-package"],
7
+ "pi": {
8
+ "extensions": [".pi/extensions/generative-ui"]
9
+ },
10
+ "dependencies": {
11
+ "glimpseui": "^0.3.5"
12
+ },
13
+ "peerDependencies": {
14
+ "@mariozechner/pi-ai": "*",
15
+ "@mariozechner/pi-coding-agent": "*",
16
+ "@mariozechner/pi-tui": "*",
17
+ "@sinclair/typebox": "*"
18
+ },
19
+ "os": ["darwin"],
20
+ "author": "michaelliv",
21
+ "license": "MIT"
22
+ }