pi-design-deck 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +54 -13
  2. package/deck-schema.ts +30 -0
  3. package/deck-server.ts +76 -19
  4. package/export-html.ts +329 -0
  5. package/form/css/controls.css +171 -0
  6. package/form/css/layout.css +56 -0
  7. package/form/css/preview.css +60 -6
  8. package/form/deck.html +2 -0
  9. package/form/js/deck-core.js +60 -2
  10. package/form/js/deck-interact.js +63 -19
  11. package/form/js/deck-render.js +95 -6
  12. package/form/js/deck-session.js +140 -27
  13. package/generate-prompts.ts +18 -12
  14. package/index.ts +364 -66
  15. package/package.json +2 -1
  16. package/prompts/deck-discover.md +3 -1
  17. package/prompts/deck-plan.md +3 -1
  18. package/prompts/deck.md +3 -1
  19. package/skills/design-deck/SKILL.md +44 -8
  20. package/skills/design-deck/references/component-gallery/INDEX.md +88 -0
  21. package/skills/design-deck/references/component-gallery/LOOKUP.md +592 -0
  22. package/skills/design-deck/references/component-gallery/components/INDEX.md +106 -0
  23. package/skills/design-deck/references/component-gallery/components/actions.md +354 -0
  24. package/skills/design-deck/references/component-gallery/components/data-display.md +812 -0
  25. package/skills/design-deck/references/component-gallery/components/feedback.md +513 -0
  26. package/skills/design-deck/references/component-gallery/components/inputs.md +921 -0
  27. package/skills/design-deck/references/component-gallery/components/layout.md +167 -0
  28. package/skills/design-deck/references/component-gallery/components/navigation.md +350 -0
  29. package/skills/design-deck/references/component-gallery/components/overlays.md +208 -0
  30. package/skills/design-deck/references/component-gallery/components/utilities.md +29 -0
  31. package/skills/design-deck/references/component-gallery/components.md +1383 -0
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  A tool for [Pi coding agent](https://github.com/badlogic/pi-mono/) that presents multi-slide visual decision decks in the browser. Each slide shows 2-4 high-fidelity previews — code diffs, architecture diagrams, UI mockups — and you pick one per slide. The agent gets back a clean selection map and moves on to implementation.
8
8
 
9
- https://github.com/user-attachments/assets/aff1bac6-8bc2-461a-8828-f588ce655f7f
9
+ <img width="1340" alt="Design Deck screenshot" src="https://github.com/user-attachments/assets/20864ac6-9223-4e2e-ba3c-db3eaae0abd8" />
10
10
 
11
11
  ## Usage
12
12
 
@@ -42,6 +42,8 @@ Restart pi to load the extension and the bundled `design-deck` skill.
42
42
  **Requirements:**
43
43
  - pi-agent v0.35.0 or later (extensions API)
44
44
 
45
+ https://github.com/user-attachments/assets/aff1bac6-8bc2-461a-8828-f588ce655f7f
46
+
45
47
  ## Quick Start
46
48
 
47
49
  The agent builds slides as JSON. Each slide is one decision, each option is one approach:
@@ -97,7 +99,9 @@ The browser opens, the user picks "JWT + Refresh Tokens", and the agent receives
97
99
  - **Slide columns**: `columns` property (1, 2, or 3) per slide. Auto-detected from option count if omitted.
98
100
  - **Smart rebalancing**: Grid layout recalculates after generate-more adds options to minimize orphans.
99
101
  - **Option aside**: Explanatory text rendered below the preview. Supports `\n` for line breaks.
100
- - **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.
101
105
  - **Light/dark/auto theme**: Full theme toggle with `Cmd+Shift+L` (configurable). Persists in localStorage.
102
106
  - **Heartbeat watchdog**: Server detects lost browser connections (60s grace) and cleans up.
103
107
  - **Idle timeout**: 5-minute inactivity timer after generate-more. Closes the deck if the agent doesn't respond.
@@ -108,7 +112,7 @@ The browser opens, the user picks "JWT + Refresh Tokens", and the agent receives
108
112
 
109
113
  1. Agent calls `design_deck()` with slides JSON — local HTTP server starts, browser opens
110
114
  2. User navigates slides, picks one option per slide
111
- 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
112
116
  4. User submits — selections returned to agent as `{ slideId: "selected label" }`
113
117
 
114
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.
@@ -164,21 +168,21 @@ The slide ID `"summary"` is reserved for the built-in summary slide that appears
164
168
 
165
169
  ## Generate-More Loop
166
170
 
167
- 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:
168
172
 
169
173
  ```typescript
170
174
  design_deck({
171
- action: "add-option",
175
+ action: "add-options",
172
176
  slideId: "arch",
173
- option: '{"label": "Serverless", "previewBlocks": [{"type": "code", "code": "...", "lang": "ts"}]}'
177
+ options: '[{"label": "Serverless", "previewBlocks": [...]}, {"label": "Edge", "previewBlocks": [...]}]'
174
178
  })
175
179
  ```
176
180
 
177
- 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).
178
182
 
179
183
  ### Model Override
180
184
 
181
- The deck shows a model dropdown when 2+ models are available. Users pick which model generates new options. When a model other than the current one is selected, the generate-more result instructs the agent to delegate to a subagent with that model.
185
+ The deck shows a model dropdown when 2+ models are available. Users pick which model generates new options. When a model other than the current one is selected, the generate-more result instructs the agent to use the built-in `deck_generate` tool, which spawns pi headlessly with that model.
182
186
 
183
187
  The default model can be set in the UI (saved to settings) or in `settings.json`:
184
188
 
@@ -211,6 +215,21 @@ design_deck({ slides: "~/.pi/deck-snapshots/api-design-myapp-main-2026-02-22-143
211
215
 
212
216
  The deck opens with selections pre-populated and image paths resolved relative to the snapshot directory.
213
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
+
214
233
  **Snapshot structure:**
215
234
  ```
216
235
  ~/.pi/deck-snapshots/
@@ -270,15 +289,20 @@ The agent handles these when you use the slash commands or ask in natural langua
270
289
  | Parameter | Type | Description |
271
290
  |-----------|------|-------------|
272
291
  | `slides` | string | JSON string of deck config, or file path to a saved deck |
273
- | `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 |
274
293
  | `slideId` | string | Target slide ID (required with actions) |
275
294
  | `option` | string | JSON string of one option (required with `add-option`) |
276
- | `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) |
277
298
 
278
- Three modes of invocation:
299
+ Six modes of invocation:
279
300
  1. **Start a new deck:** `design_deck({ slides: "<JSON>" })`
280
- 2. **Add option to running deck:** `design_deck({ action: "add-option", slideId: "...", option: "<JSON>" })`
281
- 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: "..." })`
282
306
 
283
307
  ## File Structure
284
308
 
@@ -286,6 +310,7 @@ Three modes of invocation:
286
310
  pi-design-deck/
287
311
  ├── index.ts # Tool registration, module-level state, lifecycle
288
312
  ├── generate-prompts.ts # Prompt builders for generate-more / regenerate
313
+ ├── model-runner.ts # Headless pi spawner for deck_generate tool
289
314
  ├── deck-schema.ts # TypeScript types and validation (no dependencies)
290
315
  ├── deck-server.ts # HTTP server, SSE, asset serving, snapshots
291
316
  ├── server-utils.ts # Shared HTTP/session utilities
@@ -310,6 +335,18 @@ The extension includes a `design-deck` skill at `skills/design-deck/SKILL.md` th
310
335
 
311
336
  The skill is declared in `package.json` under `pi.skills` and is automatically discovered when the extension is installed. No manual copying needed.
312
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
+
313
350
  ## Limitations
314
351
 
315
352
  - Only one deck can be active at a time. Complete or cancel before starting another.
@@ -317,3 +354,7 @@ The skill is declared in `package.json` under `pi.skills` and is automatically d
317
354
  - The `summary` slide ID is reserved and cannot be used for custom slides.
318
355
  - Mermaid diagrams load from CDN — requires internet on first load.
319
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
@@ -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,22 +189,24 @@ 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;
173
197
  }
174
198
 
175
199
  export interface DeckServerCallbacks {
176
- onSubmit: (selections: Record<string, string>) => void;
200
+ onSubmit: (selections: Record<string, string>, notes?: Record<string, string>, finalNotes?: string) => void;
177
201
  onCancel: (reason?: "user" | "stale" | "aborted") => void;
178
- onGenerateMore: (slideId: string, prompt?: string, model?: string, thinking?: string) => void;
202
+ onGenerateMore: (slideId: string, prompt?: string, model?: string, thinking?: string, count?: number) => void;
179
203
  onRegenerateSlide: (slideId: string, prompt?: string, model?: string, thinking?: string) => void;
180
204
  }
181
205
 
182
206
  export interface DeckServerHandle {
183
207
  url: string;
184
208
  port: number;
185
- close: () => void;
209
+ close: (reason?: string) => void;
186
210
  pushOption: (slideId: string, option: DeckOption) => void;
187
211
  cancelGenerate: () => void;
188
212
  replaceSlideOptions: (slideId: string, options: DeckOption[]) => void;
@@ -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
@@ -414,24 +452,30 @@ export async function startDeckServer(
414
452
  return;
415
453
  }
416
454
 
417
- const payload = body as { selections?: unknown };
455
+ const payload = body as { selections?: unknown; notes?: unknown; finalNotes?: unknown };
418
456
  const selections = toStringMap(payload.selections);
419
457
  if (!selections) {
420
458
  sendJson(res, 400, { ok: false, error: "Invalid selections payload" });
421
459
  return;
422
460
  }
461
+ const notes = toStringMap(payload.notes) ?? undefined;
462
+ const finalNotes = typeof payload.finalNotes === "string" ? payload.finalNotes.trim() : undefined;
423
463
 
424
464
  touchHeartbeat();
425
465
  if (autoSaveOnSubmit !== false) {
426
466
  try {
427
- 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
+ });
428
472
  } catch {}
429
473
  }
430
474
  markCompleted();
431
475
  unregisterSession(sessionId);
432
476
  pushEvent("deck-close", { reason: "submitted" });
433
477
  sendJson(res, 200, { ok: true });
434
- setImmediate(() => callbacks.onSubmit(selections));
478
+ setImmediate(() => callbacks.onSubmit(selections, notes, finalNotes || undefined));
435
479
  return;
436
480
  }
437
481
 
@@ -439,12 +483,22 @@ export async function startDeckServer(
439
483
  const body = await safeParseBody(req, res);
440
484
  if (!body) return;
441
485
  if (!validateTokenBody(body, sessionToken, res)) return;
486
+ if (completed) {
487
+ sendJson(res, 409, { ok: false, error: "Session closed" });
488
+ return;
489
+ }
442
490
 
443
- const payload = body as { selections?: unknown };
491
+ const payload = body as { selections?: unknown; notes?: unknown; finalNotes?: unknown };
444
492
  const selections = toStringMap(payload.selections) ?? {};
493
+ const notes = toStringMap(payload.notes) ?? undefined;
494
+ const finalNotes = typeof payload.finalNotes === "string" ? payload.finalNotes.trim() : undefined;
445
495
 
446
496
  try {
447
- 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
+ });
448
502
  sendJson(res, 200, { ok: true, path: result.path, relativePath: result.relativePath });
449
503
  } catch (err) {
450
504
  const message = err instanceof Error ? err.message : "Save failed";
@@ -471,7 +525,9 @@ export async function startDeckServer(
471
525
  const cancelSelections = toStringMap(payload.selections);
472
526
  if (cancelSelections && Object.keys(cancelSelections).length > 0) {
473
527
  try {
474
- 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
+ });
475
531
  } catch {}
476
532
  }
477
533
 
@@ -492,7 +548,7 @@ export async function startDeckServer(
492
548
  return;
493
549
  }
494
550
 
495
- const payload = body as { slideId?: string; prompt?: string; model?: string; thinking?: string };
551
+ const payload = body as { slideId?: string; prompt?: string; model?: string; thinking?: string; count?: number };
496
552
  if (typeof payload.slideId !== "string" || payload.slideId.trim() === "") {
497
553
  sendJson(res, 400, { ok: false, error: "slideId is required" });
498
554
  return;
@@ -509,12 +565,13 @@ export async function startDeckServer(
509
565
  const prompt = typeof payload.prompt === "string" ? payload.prompt.trim() || undefined : undefined;
510
566
  const model = typeof payload.model === "string" ? (payload.model.trim() || "") : undefined;
511
567
  const thinking = typeof payload.thinking === "string" ? payload.thinking.trim() || undefined : undefined;
568
+ const count = typeof payload.count === "number" && payload.count >= 1 && payload.count <= 5 ? payload.count : 1;
512
569
 
513
570
  setPendingGenerate(payload.slideId as string, false);
514
571
  touchHeartbeat();
515
572
  sendJson(res, 200, { ok: true });
516
573
  setImmediate(() => {
517
- callbacks.onGenerateMore(payload.slideId as string, prompt, model, thinking);
574
+ callbacks.onGenerateMore(payload.slideId as string, prompt, model, thinking, count);
518
575
  });
519
576
  return;
520
577
  }
@@ -603,11 +660,11 @@ export async function startDeckServer(
603
660
  resolve({
604
661
  url,
605
662
  port: addr.port,
606
- close: () => {
663
+ close: (reason?: string) => {
607
664
  if (!completed) {
608
665
  markCompleted();
609
666
  unregisterSession(sessionId);
610
- pushEvent("deck-close", { reason: "closed" });
667
+ pushEvent("deck-close", { reason: reason || "closed" });
611
668
  }
612
669
  try {
613
670
  server.close();