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.
- package/README.md +54 -13
- package/deck-schema.ts +30 -0
- package/deck-server.ts +76 -19
- package/export-html.ts +329 -0
- package/form/css/controls.css +171 -0
- package/form/css/layout.css +56 -0
- package/form/css/preview.css +60 -6
- package/form/deck.html +2 -0
- package/form/js/deck-core.js +60 -2
- package/form/js/deck-interact.js +63 -19
- package/form/js/deck-render.js +95 -6
- package/form/js/deck-session.js +140 -27
- package/generate-prompts.ts +18 -12
- package/index.ts +364 -66
- package/package.json +2 -1
- package/prompts/deck-discover.md +3 -1
- package/prompts/deck-plan.md +3 -1
- package/prompts/deck.md +3 -1
- package/skills/design-deck/SKILL.md +44 -8
- package/skills/design-deck/references/component-gallery/INDEX.md +88 -0
- package/skills/design-deck/references/component-gallery/LOOKUP.md +592 -0
- package/skills/design-deck/references/component-gallery/components/INDEX.md +106 -0
- package/skills/design-deck/references/component-gallery/components/actions.md +354 -0
- package/skills/design-deck/references/component-gallery/components/data-display.md +812 -0
- package/skills/design-deck/references/component-gallery/components/feedback.md +513 -0
- package/skills/design-deck/references/component-gallery/components/inputs.md +921 -0
- package/skills/design-deck/references/component-gallery/components/layout.md +167 -0
- package/skills/design-deck/references/component-gallery/components/navigation.md +350 -0
- package/skills/design-deck/references/component-gallery/components/overlays.md +208 -0
- package/skills/design-deck/references/component-gallery/components/utilities.md +29 -0
- 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/
|
|
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.
|
|
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
|
|
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
|
|
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-
|
|
175
|
+
action: "add-options",
|
|
172
176
|
slideId: "arch",
|
|
173
|
-
|
|
177
|
+
options: '[{"label": "Serverless", "previewBlocks": [...]}, {"label": "Edge", "previewBlocks": [...]}]'
|
|
174
178
|
})
|
|
175
179
|
```
|
|
176
180
|
|
|
177
|
-
The browser shows the new
|
|
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
|
|
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
|
|
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
|
-
|
|
299
|
+
Six modes of invocation:
|
|
279
300
|
1. **Start a new deck:** `design_deck({ slides: "<JSON>" })`
|
|
280
|
-
2. **Add
|
|
281
|
-
3. **
|
|
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 =
|
|
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
|
-
|
|
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
|
|
127
|
-
|
|
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:
|
|
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
|
|
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 {
|
|
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,
|
|
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,
|
|
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();
|