pi-design-deck 0.2.0 → 0.3.1

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 (30) hide show
  1. package/README.md +51 -13
  2. package/deck-schema.ts +33 -3
  3. package/deck-server.ts +64 -10
  4. package/export-html.ts +329 -0
  5. package/form/css/controls.css +152 -16
  6. package/form/css/layout.css +7 -0
  7. package/form/deck.html +16 -0
  8. package/form/js/deck-core.js +118 -0
  9. package/form/js/deck-interact.js +30 -12
  10. package/form/js/deck-render.js +2 -0
  11. package/form/js/deck-session.js +31 -12
  12. package/generate-prompts.ts +8 -10
  13. package/index.ts +318 -42
  14. package/package.json +2 -1
  15. package/prompts/deck-discover.md +3 -1
  16. package/prompts/deck-plan.md +3 -1
  17. package/prompts/deck.md +3 -1
  18. package/skills/design-deck/SKILL.md +45 -9
  19. package/skills/design-deck/references/component-gallery/INDEX.md +88 -0
  20. package/skills/design-deck/references/component-gallery/LOOKUP.md +592 -0
  21. package/skills/design-deck/references/component-gallery/components/INDEX.md +106 -0
  22. package/skills/design-deck/references/component-gallery/components/actions.md +354 -0
  23. package/skills/design-deck/references/component-gallery/components/data-display.md +812 -0
  24. package/skills/design-deck/references/component-gallery/components/feedback.md +513 -0
  25. package/skills/design-deck/references/component-gallery/components/inputs.md +921 -0
  26. package/skills/design-deck/references/component-gallery/components/layout.md +167 -0
  27. package/skills/design-deck/references/component-gallery/components/navigation.md +350 -0
  28. package/skills/design-deck/references/component-gallery/components/overlays.md +208 -0
  29. package/skills/design-deck/references/component-gallery/components/utilities.md +29 -0
  30. package/skills/design-deck/references/component-gallery/components.md +1383 -0
package/README.md CHANGED
@@ -96,10 +96,12 @@ The browser opens, the user picks "JWT + Refresh Tokens", and the agent receives
96
96
  - **Generate-more loop**: Users click "Generate another option" and the agent pushes a new option into the live deck via SSE. No page reload.
97
97
  - **Model selector**: Dropdown to pick which model generates new options. Save as default, or override per-request.
98
98
  - **Thinking level**: Adjust reasoning effort for option generation when the selected model supports it.
99
- - **Slide columns**: `columns` property (1, 2, or 3) per slide. Auto-detected from option count if omitted.
99
+ - **Slide columns**: `columns` property (1, 2, 3, or 4) per slide. Auto-detected from option count if omitted.
100
100
  - **Smart rebalancing**: Grid layout recalculates after generate-more adds options to minimize orphans.
101
101
  - **Option aside**: Explanatory text rendered below the preview. Supports `\n` for line breaks.
102
- - **Save/load snapshots**: `Cmd+S` saves the deck to disk. Pass a file path to `slides` to reload a saved deck with selections pre-populated.
102
+ - **Save/load snapshots**: `Cmd+S` saves the deck to disk. Use `action: "list"` to enumerate saved decks, `action: "open"` to reopen one by deck ID, or pass a file path to `slides`.
103
+ - **Notes persistence**: Saved decks include selected-option notes and summary-slide final instructions, and reopening restores both from disk.
104
+ - **Standalone HTML export**: `action: "export"` writes a read-only `export.html` next to the saved deck snapshot.
103
105
  - **Light/dark/auto theme**: Full theme toggle with `Cmd+Shift+L` (configurable). Persists in localStorage.
104
106
  - **Heartbeat watchdog**: Server detects lost browser connections (60s grace) and cleans up.
105
107
  - **Idle timeout**: 5-minute inactivity timer after generate-more. Closes the deck if the agent doesn't respond.
@@ -110,7 +112,7 @@ The browser opens, the user picks "JWT + Refresh Tokens", and the agent receives
110
112
 
111
113
  1. Agent calls `design_deck()` with slides JSON — local HTTP server starts, browser opens
112
114
  2. User navigates slides, picks one option per slide
113
- 3. Optionally clicks "Generate another option" — agent generates and pushes via `add-option`, deck stays open
115
+ 3. Optionally clicks "Generate N options" — agent generates and pushes via `add-options`, deck stays open
114
116
  4. User submits — selections returned to agent as `{ slideId: "selected label" }`
115
117
 
116
118
  The server persists across tool re-invocations. When generate-more fires, the tool resolves with instructions for the agent to create a new option. The browser shows a skeleton placeholder with shimmer animation until the new option arrives via SSE.
@@ -154,7 +156,7 @@ Image blocks reference absolute file paths. The server copies each file into a t
154
156
 
155
157
  ### Columns
156
158
 
157
- Each slide supports `columns: 1 | 2 | 3` to control the grid layout. Omit it and the deck auto-detects based on option count. Use `columns: 1` for wide architecture diagrams, `columns: 2` for side-by-side comparisons.
159
+ Each slide supports `columns: 1 | 2 | 3 | 4` to control the grid layout. Omit it and the deck auto-detects based on option count. Use `columns: 1` for wide architecture diagrams, `columns: 2` for side-by-side comparisons, `columns: 4` for many small items.
158
160
 
159
161
  ### Aside
160
162
 
@@ -166,17 +168,17 @@ The slide ID `"summary"` is reserved for the built-in summary slide that appears
166
168
 
167
169
  ## Generate-More Loop
168
170
 
169
- When the user clicks "Generate another option," the tool resolves with a structured prompt telling the agent which slide needs a new option, what options already exist, and what format to use. The agent generates one new option and pushes it:
171
+ When the user clicks "Generate N options," the tool resolves with a structured prompt telling the agent which slide needs options, how many, what options already exist, and what format to use. The agent generates the requested options and pushes them all at once:
170
172
 
171
173
  ```typescript
172
174
  design_deck({
173
- action: "add-option",
175
+ action: "add-options",
174
176
  slideId: "arch",
175
- option: '{"label": "Serverless", "previewBlocks": [{"type": "code", "code": "...", "lang": "ts"}]}'
177
+ options: '[{"label": "Serverless", "previewBlocks": [...]}, {"label": "Edge", "previewBlocks": [...]}]'
176
178
  })
177
179
  ```
178
180
 
179
- The browser shows the new option with an entry animation. The tool blocks again, waiting for the next user action (submit, cancel, or another generate-more).
181
+ The browser shows the new options with entry animations. The `add-options` call blocks until the next user action (submit, cancel, or another generate-more).
180
182
 
181
183
  ### Model Override
182
184
 
@@ -213,6 +215,21 @@ design_deck({ slides: "~/.pi/deck-snapshots/api-design-myapp-main-2026-02-22-143
213
215
 
214
216
  The deck opens with selections pre-populated and image paths resolved relative to the snapshot directory.
215
217
 
218
+ **Listing saved decks:**
219
+ ```typescript
220
+ design_deck({ action: "list" })
221
+ ```
222
+
223
+ **Opening by deck ID:**
224
+ ```typescript
225
+ design_deck({ action: "open", deckId: "api-design-myapp-main-2026-02-22-143000-submitted" })
226
+ ```
227
+
228
+ **Exporting standalone HTML:**
229
+ ```typescript
230
+ design_deck({ action: "export", deckId: "api-design-myapp-main-2026-02-22-143000-submitted", format: "html" })
231
+ ```
232
+
216
233
  **Snapshot structure:**
217
234
  ```
218
235
  ~/.pi/deck-snapshots/
@@ -272,15 +289,20 @@ The agent handles these when you use the slash commands or ask in natural langua
272
289
  | Parameter | Type | Description |
273
290
  |-----------|------|-------------|
274
291
  | `slides` | string | JSON string of deck config, or file path to a saved deck |
275
- | `action` | `"add-option"` \| `"replace-options"` | Push or replace options in a running deck |
292
+ | `action` | `"add-options"` \| `"add-option"` \| `"replace-options"` \| `"list"` \| `"open"` \| `"export"` | Push/replace options, list saved decks, reopen a saved deck, or export one |
276
293
  | `slideId` | string | Target slide ID (required with actions) |
277
294
  | `option` | string | JSON string of one option (required with `add-option`) |
278
- | `options` | string | JSON string of option array (required with `replace-options`) |
295
+ | `options` | string | JSON string of option array (required with `add-options` or `replace-options`) |
296
+ | `deckId` | string | Saved deck ID from `action: "list"` (required with `open` / `export`) |
297
+ | `format` | string | Export format for `action: "export"` (`"html"` currently supported) |
279
298
 
280
- Three modes of invocation:
299
+ Six modes of invocation:
281
300
  1. **Start a new deck:** `design_deck({ slides: "<JSON>" })`
282
- 2. **Add option to running deck:** `design_deck({ action: "add-option", slideId: "...", option: "<JSON>" })`
283
- 3. **Replace all options on a slide:** `design_deck({ action: "replace-options", slideId: "...", options: "<JSON array>" })`
301
+ 2. **Add options to running deck:** `design_deck({ action: "add-options", slideId: "...", options: "<JSON array>" })` — blocks until next user action
302
+ 3. **Add single option (non-blocking):** `design_deck({ action: "add-option", slideId: "...", option: "<JSON>" })`
303
+ 4. **Replace all options on a slide:** `design_deck({ action: "replace-options", slideId: "...", options: "<JSON array>" })`
304
+ 5. **List saved decks:** `design_deck({ action: "list" })`
305
+ 6. **Open or export a saved deck:** `design_deck({ action: "open" | "export", deckId: "..." })`
284
306
 
285
307
  ## File Structure
286
308
 
@@ -313,6 +335,18 @@ The extension includes a `design-deck` skill at `skills/design-deck/SKILL.md` th
313
335
 
314
336
  The skill is declared in `package.json` under `pi.skills` and is automatically discovered when the extension is installed. No manual copying needed.
315
337
 
338
+ ### Component Gallery Reference
339
+
340
+ The skill includes a reference library for 60 UI components with best practices, common layouts, and aliases. Each component links to [component.gallery](https://component.gallery) where the agent can browse real screenshots when needed.
341
+
342
+ **Before:** "Show me collapse options" → agent might not connect that to accordion, or know what components are available for the use case.
343
+
344
+ **After:** Agent has 60 components to suggest from. Knows *collapse = accordion = disclosure = expander*. Knows *Blueprint = dense, dark-native; Ant = clean, blue primary.* Can browse [100+ real implementations](https://component.gallery/components/accordion/) when it needs concrete references.
345
+
346
+ The reference enables discovery (find/suggest components), cross-referencing (connect related terms), and design vocabulary (know what systems look like) — plus guidance on *when* to show distinct design systems vs variations of one style.
347
+
348
+ A separate vocabulary lookup (`LOOKUP.md`) resolves ambiguous user terms to canonical components. When a user says "dropdown" (Select? Combobox? Dropdown menu?) or "popup" (Modal? Popover? Tooltip?) or describes intent ("I need something that expands"), the agent can consult the lookup to understand what they mean and ask the right clarifying questions when needed.
349
+
316
350
  ## Limitations
317
351
 
318
352
  - Only one deck can be active at a time. Complete or cancel before starting another.
@@ -320,3 +354,7 @@ The skill is declared in `package.json` under `pi.skills` and is automatically d
320
354
  - The `summary` slide ID is reserved and cannot be used for custom slides.
321
355
  - Mermaid diagrams load from CDN — requires internet on first load.
322
356
  - macOS tested primarily; Linux and Windows support is best-effort.
357
+
358
+ ## Credits
359
+
360
+ UI component reference data sourced from [component.gallery](https://component.gallery) by Iain Bean.
package/deck-schema.ts CHANGED
@@ -17,7 +17,7 @@ export interface DeckSlide {
17
17
  id: string;
18
18
  title: string;
19
19
  context?: string;
20
- columns?: 1 | 2 | 3;
20
+ columns?: 1 | 2 | 3 | 4;
21
21
  options: DeckOption[];
22
22
  }
23
23
 
@@ -155,8 +155,8 @@ function validateDeckSlide(slide: unknown, index: number): DeckSlide {
155
155
  }
156
156
 
157
157
  if (obj.columns !== undefined) {
158
- if (obj.columns !== 1 && obj.columns !== 2 && obj.columns !== 3) {
159
- throw new Error(`Slide "${obj.id}": columns must be 1, 2, or 3`);
158
+ if (obj.columns !== 1 && obj.columns !== 2 && obj.columns !== 3 && obj.columns !== 4) {
159
+ throw new Error(`Slide "${obj.id}": columns must be 1, 2, 3, or 4`);
160
160
  }
161
161
  }
162
162
 
@@ -229,6 +229,11 @@ export interface SavedDeckData {
229
229
  config: DeckConfig;
230
230
  selections: Record<string, string>;
231
231
  savedAt: string;
232
+ id?: string;
233
+ status?: "submitted" | "in-progress" | "cancelled";
234
+ modifiedAt?: string;
235
+ notes?: Record<string, string>;
236
+ finalNotes?: string;
232
237
  savedFrom?: {
233
238
  cwd: string;
234
239
  branch: string | null;
@@ -236,6 +241,14 @@ export interface SavedDeckData {
236
241
  };
237
242
  }
238
243
 
244
+ export type SavedDeckStatus = NonNullable<SavedDeckData["status"]>;
245
+
246
+ export function deriveDeckStatusFromFolderName(folderName: string): SavedDeckStatus {
247
+ if (folderName.endsWith("-submitted")) return "submitted";
248
+ if (folderName.endsWith("-cancelled")) return "cancelled";
249
+ return "in-progress";
250
+ }
251
+
239
252
  export function validateSavedDeck(data: unknown): SavedDeckData {
240
253
  if (!data || typeof data !== "object" || Array.isArray(data)) {
241
254
  throw new Error("Invalid saved deck: must be an object");
@@ -251,10 +264,27 @@ export function validateSavedDeck(data: unknown): SavedDeckData {
251
264
  }
252
265
  }
253
266
 
267
+ const notes: Record<string, string> = {};
268
+ if (obj.notes && typeof obj.notes === "object" && !Array.isArray(obj.notes)) {
269
+ for (const [key, val] of Object.entries(obj.notes as Record<string, unknown>)) {
270
+ if (typeof val === "string") notes[key] = val;
271
+ }
272
+ }
273
+
274
+ const status =
275
+ obj.status === "submitted" || obj.status === "in-progress" || obj.status === "cancelled"
276
+ ? obj.status
277
+ : undefined;
278
+
254
279
  return {
255
280
  config,
256
281
  selections,
257
282
  savedAt: typeof obj.savedAt === "string" ? obj.savedAt : new Date().toISOString(),
283
+ id: typeof obj.id === "string" && obj.id.trim() !== "" ? obj.id : undefined,
284
+ status,
285
+ modifiedAt: typeof obj.modifiedAt === "string" ? obj.modifiedAt : undefined,
286
+ notes: Object.keys(notes).length > 0 ? notes : undefined,
287
+ finalNotes: typeof obj.finalNotes === "string" ? obj.finalNotes : undefined,
258
288
  savedFrom: obj.savedFrom && typeof obj.savedFrom === "object"
259
289
  ? obj.savedFrom as SavedDeckData["savedFrom"]
260
290
  : undefined,
package/deck-server.ts CHANGED
@@ -66,6 +66,10 @@ const ABANDONED_GRACE_MS = 60000;
66
66
  const WATCHDOG_INTERVAL_MS = 5000;
67
67
  const GENERATE_TIMEOUT_MS = 90_000;
68
68
 
69
+ export function getDefaultSnapshotDir(): string {
70
+ return join(homedir(), ".pi", "deck-snapshots");
71
+ }
72
+
69
73
  function toStringMap(value: unknown): Record<string, string> | null {
70
74
  if (!value || typeof value !== "object" || Array.isArray(value)) {
71
75
  return null;
@@ -100,7 +104,7 @@ function processOptionAssets(option: DeckOption, assetsDir: string): DeckOption
100
104
  return { ...option, previewBlocks: processImageBlocks(option.previewBlocks, assetsDir) };
101
105
  }
102
106
 
103
- const DECK_SNAPSHOTS_DIR = join(homedir(), ".pi", "deck-snapshots");
107
+ const DECK_SNAPSHOTS_DIR = getDefaultSnapshotDir();
104
108
 
105
109
  function sanitizeForFilename(value: string): string {
106
110
  return value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40).replace(/_+$/, "") || "unknown";
@@ -114,17 +118,30 @@ function saveDeckSnapshot(
114
118
  gitBranch: string | null,
115
119
  sessionId: string,
116
120
  baseDir: string,
117
- suffix?: string
121
+ options?: {
122
+ status?: "submitted" | "in-progress" | "cancelled";
123
+ notes?: Record<string, string>;
124
+ finalNotes?: string;
125
+ }
118
126
  ): { path: string; relativePath: string } {
119
127
  const now = new Date();
128
+ const nowIso = now.toISOString();
120
129
  const date = now.toISOString().slice(0, 10);
121
130
  const time = now.toTimeString().slice(0, 8).replace(/:/g, "");
122
131
  const titleSlug = sanitizeForFilename(config.title || "deck");
123
132
  const project = sanitizeForFilename(basename(normalizedCwd) || "unknown");
124
133
  const branch = sanitizeForFilename(gitBranch || "nogit");
134
+ const suffix = options?.status === "submitted" || options?.status === "cancelled" ? options.status : undefined;
125
135
  const safeSuffix = suffix ? `-${suffix}` : "";
126
- const folderName = `${titleSlug}-${project}-${branch}-${date}-${time}${safeSuffix}`;
127
- const snapshotPath = join(baseDir, folderName);
136
+ const baseFolderName = `${titleSlug}-${project}-${branch}-${date}-${time}${safeSuffix}`;
137
+ let folderName = baseFolderName;
138
+ let snapshotPath = join(baseDir, folderName);
139
+ let collisionIndex = 2;
140
+ while (existsSync(snapshotPath)) {
141
+ folderName = `${baseFolderName}-${collisionIndex}`;
142
+ snapshotPath = join(baseDir, folderName);
143
+ collisionIndex += 1;
144
+ }
128
145
  const imagesPath = join(snapshotPath, "images");
129
146
 
130
147
  mkdirSync(snapshotPath, { recursive: true });
@@ -149,7 +166,12 @@ function saveDeckSnapshot(
149
166
  const data = {
150
167
  config: saved,
151
168
  selections,
152
- savedAt: now.toISOString(),
169
+ savedAt: nowIso,
170
+ id: folderName,
171
+ status: options?.status,
172
+ modifiedAt: nowIso,
173
+ notes: options?.notes && Object.keys(options.notes).length > 0 ? options.notes : undefined,
174
+ finalNotes: options?.finalNotes ? options.finalNotes : undefined,
153
175
  savedFrom: { cwd: normalizedCwd, branch: gitBranch, sessionId },
154
176
  };
155
177
  writeFileSync(join(snapshotPath, "deck.json"), JSON.stringify(data, null, 2));
@@ -167,6 +189,8 @@ export interface DeckServerOptions {
167
189
  port?: number;
168
190
  theme?: { mode?: string; toggleHotkey?: string };
169
191
  savedSelections?: Record<string, string>;
192
+ savedNotes?: Record<string, { label: string; notes: string }>;
193
+ savedFinalNotes?: string;
170
194
  snapshotDir?: string;
171
195
  autoSaveOnSubmit?: boolean;
172
196
  models?: ModelsPayload;
@@ -192,7 +216,19 @@ export async function startDeckServer(
192
216
  options: DeckServerOptions,
193
217
  callbacks: DeckServerCallbacks
194
218
  ): Promise<DeckServerHandle> {
195
- const { config, sessionToken, sessionId, cwd, port, theme, savedSelections, snapshotDir, autoSaveOnSubmit } = options;
219
+ const {
220
+ config,
221
+ sessionToken,
222
+ sessionId,
223
+ cwd,
224
+ port,
225
+ theme,
226
+ savedSelections,
227
+ savedNotes,
228
+ savedFinalNotes,
229
+ snapshotDir,
230
+ autoSaveOnSubmit,
231
+ } = options;
196
232
  const normalizedCwd = normalizePath(cwd);
197
233
  const gitBranch = getGitBranch(cwd);
198
234
 
@@ -293,6 +329,8 @@ export async function startDeckServer(
293
329
  gitBranch,
294
330
  theme,
295
331
  savedSelections,
332
+ savedNotes,
333
+ savedFinalNotes,
296
334
  });
297
335
  const title = config.title ? `${config.title} — Design Deck` : "Design Deck";
298
336
  const html = DECK_TEMPLATE
@@ -426,7 +464,11 @@ export async function startDeckServer(
426
464
  touchHeartbeat();
427
465
  if (autoSaveOnSubmit !== false) {
428
466
  try {
429
- saveDeckSnapshot(config, selections, assetsDir, normalizedCwd, gitBranch, sessionId, snapshotDir || DECK_SNAPSHOTS_DIR, "submitted");
467
+ saveDeckSnapshot(config, selections, assetsDir, normalizedCwd, gitBranch, sessionId, snapshotDir || DECK_SNAPSHOTS_DIR, {
468
+ status: "submitted",
469
+ notes,
470
+ finalNotes: finalNotes || undefined,
471
+ });
430
472
  } catch {}
431
473
  }
432
474
  markCompleted();
@@ -441,12 +483,22 @@ export async function startDeckServer(
441
483
  const body = await safeParseBody(req, res);
442
484
  if (!body) return;
443
485
  if (!validateTokenBody(body, sessionToken, res)) return;
486
+ if (completed) {
487
+ sendJson(res, 409, { ok: false, error: "Session closed" });
488
+ return;
489
+ }
444
490
 
445
- const payload = body as { selections?: unknown };
491
+ const payload = body as { selections?: unknown; notes?: unknown; finalNotes?: unknown };
446
492
  const selections = toStringMap(payload.selections) ?? {};
493
+ const notes = toStringMap(payload.notes) ?? undefined;
494
+ const finalNotes = typeof payload.finalNotes === "string" ? payload.finalNotes.trim() : undefined;
447
495
 
448
496
  try {
449
- const result = saveDeckSnapshot(config, selections, assetsDir, normalizedCwd, gitBranch, sessionId, snapshotDir || DECK_SNAPSHOTS_DIR);
497
+ const result = saveDeckSnapshot(config, selections, assetsDir, normalizedCwd, gitBranch, sessionId, snapshotDir || DECK_SNAPSHOTS_DIR, {
498
+ status: "in-progress",
499
+ notes,
500
+ finalNotes: finalNotes || undefined,
501
+ });
450
502
  sendJson(res, 200, { ok: true, path: result.path, relativePath: result.relativePath });
451
503
  } catch (err) {
452
504
  const message = err instanceof Error ? err.message : "Save failed";
@@ -473,7 +525,9 @@ export async function startDeckServer(
473
525
  const cancelSelections = toStringMap(payload.selections);
474
526
  if (cancelSelections && Object.keys(cancelSelections).length > 0) {
475
527
  try {
476
- saveDeckSnapshot(config, cancelSelections, assetsDir, normalizedCwd, gitBranch, sessionId, snapshotDir || DECK_SNAPSHOTS_DIR, "cancelled");
528
+ saveDeckSnapshot(config, cancelSelections, assetsDir, normalizedCwd, gitBranch, sessionId, snapshotDir || DECK_SNAPSHOTS_DIR, {
529
+ status: "cancelled",
530
+ });
477
531
  } catch {}
478
532
  }
479
533