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/index.ts
CHANGED
|
@@ -6,10 +6,11 @@ import * as os from "node:os";
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { loadSettings } from "./settings.js";
|
|
9
|
-
import { startDeckServer, type DeckServerHandle, type ModelInfo } from "./deck-server.js";
|
|
10
|
-
import { isDeckOption, validateDeckConfig, validateSavedDeck } from "./deck-schema.js";
|
|
9
|
+
import { getDefaultSnapshotDir, startDeckServer, type DeckServerHandle, type ModelInfo } from "./deck-server.js";
|
|
10
|
+
import { deriveDeckStatusFromFolderName, isDeckOption, validateDeckConfig, validateSavedDeck, type SavedDeckData, type SavedDeckStatus } from "./deck-schema.js";
|
|
11
11
|
import { buildGenerateMoreResult, buildRegenerateResult } from "./generate-prompts.js";
|
|
12
12
|
import { generateWithModel } from "./model-runner.js";
|
|
13
|
+
import { buildStandaloneDeckHtml } from "./export-html.js";
|
|
13
14
|
|
|
14
15
|
async function openUrl(pi: ExtensionAPI, url: string, browser?: string): Promise<void> {
|
|
15
16
|
const platform = os.platform();
|
|
@@ -42,6 +43,8 @@ interface DeckDetails {
|
|
|
42
43
|
status: "completed" | "cancelled" | "generate-more" | "aborted" | "error";
|
|
43
44
|
url: string;
|
|
44
45
|
selections?: Record<string, string>;
|
|
46
|
+
notes?: Record<string, string>;
|
|
47
|
+
finalNotes?: string;
|
|
45
48
|
slideId?: string;
|
|
46
49
|
reason?: string;
|
|
47
50
|
}
|
|
@@ -62,6 +65,24 @@ let restoreDeckThinking: (() => void) | null = null;
|
|
|
62
65
|
|
|
63
66
|
const DECK_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
64
67
|
|
|
68
|
+
interface SavedDeckListItem {
|
|
69
|
+
id: string;
|
|
70
|
+
title: string;
|
|
71
|
+
savedAt: string;
|
|
72
|
+
modifiedAt: string;
|
|
73
|
+
status: SavedDeckStatus;
|
|
74
|
+
cwd?: string;
|
|
75
|
+
branch?: string | null;
|
|
76
|
+
slideCount: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface LoadedDeckSource {
|
|
80
|
+
configData: unknown;
|
|
81
|
+
savedSelections?: Record<string, string>;
|
|
82
|
+
savedNotes?: Record<string, { label: string; notes: string }>;
|
|
83
|
+
savedFinalNotes?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
const DeckParams = Type.Object(
|
|
66
87
|
{
|
|
67
88
|
slides: Type.Optional(
|
|
@@ -76,11 +97,15 @@ const DeckParams = Type.Object(
|
|
|
76
97
|
action: Type.Optional(
|
|
77
98
|
Type.Union([
|
|
78
99
|
Type.Literal("add-option", { description: "Push a single generated option into a running deck session" }),
|
|
100
|
+
Type.Literal("add-options", { description: "Push multiple generated options into a running deck session (blocks until next user action)" }),
|
|
79
101
|
Type.Literal("replace-options", { description: "Replace all options for a slide with fresh alternatives" }),
|
|
102
|
+
Type.Literal("list", { description: "List saved decks from the snapshot directory" }),
|
|
103
|
+
Type.Literal("open", { description: "Open a saved deck by deck ID" }),
|
|
104
|
+
Type.Literal("export", { description: "Export a saved deck as standalone HTML" }),
|
|
80
105
|
])
|
|
81
106
|
),
|
|
82
107
|
slideId: Type.Optional(
|
|
83
|
-
Type.String({ description: "Target slide ID (required with action: 'add-option' or 'replace-options')" })
|
|
108
|
+
Type.String({ description: "Target slide ID (required with action: 'add-option', 'add-options', or 'replace-options')" })
|
|
84
109
|
),
|
|
85
110
|
option: Type.Optional(
|
|
86
111
|
Type.String({
|
|
@@ -91,9 +116,15 @@ const DeckParams = Type.Object(
|
|
|
91
116
|
options: Type.Optional(
|
|
92
117
|
Type.String({
|
|
93
118
|
description:
|
|
94
|
-
"JSON string of array of deck options (required with action: 'replace-options')",
|
|
119
|
+
"JSON string of array of deck options (required with action: 'add-options' or 'replace-options')",
|
|
95
120
|
})
|
|
96
121
|
),
|
|
122
|
+
deckId: Type.Optional(
|
|
123
|
+
Type.String({ description: "Deck ID for open/export actions (folder name from list)" })
|
|
124
|
+
),
|
|
125
|
+
format: Type.Optional(
|
|
126
|
+
Type.String({ description: "Export format: 'html' (default)" })
|
|
127
|
+
),
|
|
97
128
|
},
|
|
98
129
|
{ additionalProperties: false }
|
|
99
130
|
);
|
|
@@ -109,6 +140,104 @@ function expandHome(value: string): string {
|
|
|
109
140
|
return value;
|
|
110
141
|
}
|
|
111
142
|
|
|
143
|
+
function resolveSnapshotDir(snapshotDir?: string): string {
|
|
144
|
+
return snapshotDir ? expandHome(snapshotDir) : getDefaultSnapshotDir();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function listSavedDecks(snapshotDir: string): { decks: SavedDeckListItem[]; warnings: string[] } {
|
|
148
|
+
if (!fs.existsSync(snapshotDir)) {
|
|
149
|
+
return { decks: [], warnings: [] };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const entries = fs.readdirSync(snapshotDir, { withFileTypes: true });
|
|
153
|
+
const decks: SavedDeckListItem[] = [];
|
|
154
|
+
const warnings: string[] = [];
|
|
155
|
+
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
if (!entry.isDirectory()) continue;
|
|
158
|
+
const deckJsonPath = path.join(snapshotDir, entry.name, "deck.json");
|
|
159
|
+
if (!fs.existsSync(deckJsonPath)) continue;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const raw = JSON.parse(fs.readFileSync(deckJsonPath, "utf-8"));
|
|
163
|
+
const saved = validateSavedDeck(raw);
|
|
164
|
+
decks.push({
|
|
165
|
+
id: saved.id ?? entry.name,
|
|
166
|
+
title: saved.config.title || "Design Deck",
|
|
167
|
+
savedAt: saved.savedAt,
|
|
168
|
+
modifiedAt: saved.modifiedAt ?? saved.savedAt,
|
|
169
|
+
status: saved.status ?? deriveDeckStatusFromFolderName(entry.name),
|
|
170
|
+
cwd: saved.savedFrom?.cwd,
|
|
171
|
+
branch: saved.savedFrom?.branch,
|
|
172
|
+
slideCount: saved.config.slides.length,
|
|
173
|
+
});
|
|
174
|
+
} catch (err) {
|
|
175
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
176
|
+
warnings.push(`${entry.name}: ${message}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { decks, warnings };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function resolveDeckFilePath(snapshotDir: string, deckId: string): string | null {
|
|
184
|
+
if (!deckId || deckId === "." || deckId === ".." || deckId.includes("/") || deckId.includes("\\")) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const root = path.resolve(snapshotDir);
|
|
188
|
+
const deckDir = path.resolve(snapshotDir, deckId);
|
|
189
|
+
if (deckDir !== root && !deckDir.startsWith(root + path.sep)) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
return path.join(deckDir, "deck.json");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildSavedNotesForClient(saved: SavedDeckData): Record<string, { label: string; notes: string }> | undefined {
|
|
196
|
+
const savedNotesForClient: Record<string, { label: string; notes: string }> = {};
|
|
197
|
+
for (const [slideId, noteString] of Object.entries(saved.notes ?? {})) {
|
|
198
|
+
const label = saved.selections[slideId];
|
|
199
|
+
if (label && noteString) {
|
|
200
|
+
savedNotesForClient[slideId] = { label, notes: noteString };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return Object.keys(savedNotesForClient).length > 0 ? savedNotesForClient : undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function loadDeckFile(absolutePath: string): LoadedDeckSource {
|
|
207
|
+
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
208
|
+
let fileData: unknown;
|
|
209
|
+
try {
|
|
210
|
+
fileData = JSON.parse(content);
|
|
211
|
+
} catch (parseErr) {
|
|
212
|
+
const message = parseErr instanceof Error ? parseErr.message : String(parseErr);
|
|
213
|
+
throw new Error(`Invalid JSON in saved deck file: ${message}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const raw = fileData as Record<string, unknown>;
|
|
217
|
+
if (raw.config && typeof raw.config === "object") {
|
|
218
|
+
const saved = validateSavedDeck(fileData);
|
|
219
|
+
const snapshotDir = path.dirname(absolutePath);
|
|
220
|
+
for (const slide of saved.config.slides) {
|
|
221
|
+
for (const option of slide.options) {
|
|
222
|
+
if (!option.previewBlocks) continue;
|
|
223
|
+
for (const block of option.previewBlocks) {
|
|
224
|
+
if (block.type === "image" && !path.isAbsolute(block.src)) {
|
|
225
|
+
block.src = path.join(snapshotDir, block.src);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
configData: saved.config,
|
|
232
|
+
savedSelections: Object.keys(saved.selections).length > 0 ? saved.selections : undefined,
|
|
233
|
+
savedNotes: buildSavedNotesForClient(saved),
|
|
234
|
+
savedFinalNotes: saved.finalNotes,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { configData: fileData };
|
|
239
|
+
}
|
|
240
|
+
|
|
112
241
|
const DEFAULT_THEME_HOTKEY = "mod+shift+l";
|
|
113
242
|
|
|
114
243
|
function clearDeckIdleTimer(): void {
|
|
@@ -118,7 +247,7 @@ function clearDeckIdleTimer(): void {
|
|
|
118
247
|
}
|
|
119
248
|
}
|
|
120
249
|
|
|
121
|
-
function cleanupActiveDeck(): void {
|
|
250
|
+
function cleanupActiveDeck(reason?: string): void {
|
|
122
251
|
clearDeckIdleTimer();
|
|
123
252
|
if (restoreDeckThinking) {
|
|
124
253
|
restoreDeckThinking();
|
|
@@ -126,20 +255,23 @@ function cleanupActiveDeck(): void {
|
|
|
126
255
|
}
|
|
127
256
|
if (!activeDeckServer) return;
|
|
128
257
|
try {
|
|
129
|
-
activeDeckServer.handle.close();
|
|
258
|
+
activeDeckServer.handle.close(reason);
|
|
130
259
|
} catch {}
|
|
131
260
|
activeDeckServer = null;
|
|
132
261
|
}
|
|
133
262
|
|
|
134
263
|
function cleanupActiveDeckAndStoreResult(result: DeckToolResult): void {
|
|
135
264
|
if (!activeDeckServer) return;
|
|
265
|
+
// Extract close reason from result details
|
|
266
|
+
const details = result.details as DeckDetails | undefined;
|
|
267
|
+
const closeReason = details?.status === "aborted" ? "aborted" : details?.reason;
|
|
136
268
|
if (activeDeckServer.currentResolve) {
|
|
137
269
|
const resolve = activeDeckServer.currentResolve;
|
|
138
|
-
cleanupActiveDeck();
|
|
270
|
+
cleanupActiveDeck(closeReason);
|
|
139
271
|
resolve(result);
|
|
140
272
|
} else {
|
|
141
273
|
pendingDeckResult = result;
|
|
142
|
-
cleanupActiveDeck();
|
|
274
|
+
cleanupActiveDeck(closeReason);
|
|
143
275
|
}
|
|
144
276
|
}
|
|
145
277
|
|
|
@@ -181,10 +313,16 @@ function attachDeckAbortHandler(signal: AbortSignal | undefined): void {
|
|
|
181
313
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
182
314
|
}
|
|
183
315
|
|
|
184
|
-
function formatDeckSelections(selections: Record<string, string>): string {
|
|
316
|
+
function formatDeckSelections(selections: Record<string, string>, notes?: Record<string, string>): string {
|
|
185
317
|
const entries = Object.entries(selections);
|
|
186
318
|
if (entries.length === 0) return "(none)";
|
|
187
|
-
return entries.map(([key, value]) =>
|
|
319
|
+
return entries.map(([key, value]) => {
|
|
320
|
+
const note = notes?.[key];
|
|
321
|
+
if (note) {
|
|
322
|
+
return `- ${key}: ${value}\n Notes: ${note}`;
|
|
323
|
+
}
|
|
324
|
+
return `- ${key}: ${value}`;
|
|
325
|
+
}).join("\n");
|
|
188
326
|
}
|
|
189
327
|
|
|
190
328
|
export default function (pi: ExtensionAPI) {
|
|
@@ -196,19 +334,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
196
334
|
"Present a multi-slide design deck with visual options for decisions. " +
|
|
197
335
|
"Slides JSON: { title?, slides: [{ id, title, context?, columns?, options }] }. " +
|
|
198
336
|
"When the user requests more options, tool returns generate-more instructions — " +
|
|
199
|
-
'call design_deck with action:"add-
|
|
337
|
+
'call design_deck with action:"add-options" to push all new options at once. ' +
|
|
200
338
|
"previewBlocks for code/architecture comparisons, previewHtml for custom UI mockups.",
|
|
201
339
|
parameters: DeckParams,
|
|
202
340
|
|
|
203
341
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
204
|
-
|
|
342
|
+
const p = params as Record<string, unknown>;
|
|
343
|
+
|
|
344
|
+
if (!ctx.hasUI && p.action !== "list" && p.action !== "export") {
|
|
205
345
|
throw new Error(
|
|
206
346
|
"design_deck requires interactive mode with browser support. " +
|
|
207
347
|
"Cannot run in headless/RPC/print mode."
|
|
208
348
|
);
|
|
209
349
|
}
|
|
210
350
|
|
|
211
|
-
|
|
351
|
+
if (p.action === "list") {
|
|
352
|
+
const settings = loadSettings();
|
|
353
|
+
const snapshotDir = resolveSnapshotDir(settings.snapshotDir);
|
|
354
|
+
const { decks, warnings } = listSavedDecks(snapshotDir);
|
|
355
|
+
const content: Array<{ type: "text"; text: string }> = [
|
|
356
|
+
{ type: "text", text: JSON.stringify(decks, null, 2) },
|
|
357
|
+
];
|
|
358
|
+
if (warnings.length > 0) {
|
|
359
|
+
content.push({ type: "text", text: `Warnings:\n${warnings.map((w) => `- ${w}`).join("\n")}` });
|
|
360
|
+
}
|
|
361
|
+
return { content };
|
|
362
|
+
}
|
|
212
363
|
|
|
213
364
|
if (p.action === "add-option") {
|
|
214
365
|
if (typeof p.slideId !== "string" || p.slideId.trim() === "") {
|
|
@@ -225,6 +376,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
225
376
|
details: { status: "error", url: activeDeckServer?.handle.url ?? "" },
|
|
226
377
|
};
|
|
227
378
|
}
|
|
379
|
+
} else if (p.action === "add-options") {
|
|
380
|
+
if (typeof p.slideId !== "string" || p.slideId.trim() === "") {
|
|
381
|
+
activeDeckServer?.handle.cancelGenerate();
|
|
382
|
+
return {
|
|
383
|
+
content: [{ type: "text", text: 'add-options requires slideId (string).' }],
|
|
384
|
+
details: { status: "error", url: activeDeckServer?.handle.url ?? "" },
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (typeof p.options !== "string" || p.options.trim() === "") {
|
|
388
|
+
activeDeckServer?.handle.cancelGenerate();
|
|
389
|
+
return {
|
|
390
|
+
content: [{ type: "text", text: 'add-options requires options (JSON array string).' }],
|
|
391
|
+
details: { status: "error", url: activeDeckServer?.handle.url ?? "" },
|
|
392
|
+
};
|
|
393
|
+
}
|
|
228
394
|
} else if (p.action === "replace-options") {
|
|
229
395
|
if (typeof p.slideId !== "string" || p.slideId.trim() === "") {
|
|
230
396
|
activeDeckServer?.handle.cancelGenerate();
|
|
@@ -240,6 +406,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
240
406
|
details: { status: "error", url: activeDeckServer?.handle.url ?? "" },
|
|
241
407
|
};
|
|
242
408
|
}
|
|
409
|
+
} else if (p.action === "open" || p.action === "export") {
|
|
410
|
+
if (typeof p.deckId !== "string" || p.deckId.trim() === "") {
|
|
411
|
+
return {
|
|
412
|
+
content: [{ type: "text", text: `${p.action} requires deckId (string). Example: { action: "${p.action}", deckId: "tabs-component-myapp-main-2026-03-01-103045-submitted" }` }],
|
|
413
|
+
details: { status: "error", url: activeDeckServer?.handle.url ?? "" },
|
|
414
|
+
};
|
|
415
|
+
}
|
|
243
416
|
} else if (typeof p.slides !== "string" || p.slides.trim() === "") {
|
|
244
417
|
return {
|
|
245
418
|
content: [{
|
|
@@ -247,7 +420,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
247
420
|
text:
|
|
248
421
|
"design_deck requires one of:\n\n" +
|
|
249
422
|
'1. Start a new deck: { slides: "<JSON string of { title?, slides: [{ id, title, options }] }>" }\n' +
|
|
250
|
-
'2.
|
|
423
|
+
'2. Open a saved deck: { action: "open", deckId: "..." }\n' +
|
|
424
|
+
'3. Export a saved deck: { action: "export", deckId: "...", format: "html" }\n' +
|
|
425
|
+
'4. Add options to running deck: { action: "add-options", slideId: "...", options: "[<JSON array>]" }\n' +
|
|
426
|
+
'5. Add single option: { action: "add-option", slideId: "...", option: "<JSON string>" }\n\n' +
|
|
251
427
|
"Each option needs label + either previewHtml (raw HTML) or previewBlocks (array of {type, ...} blocks).\n" +
|
|
252
428
|
"Block types: html, mermaid, code, image.",
|
|
253
429
|
}],
|
|
@@ -274,17 +450,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
274
450
|
};
|
|
275
451
|
}
|
|
276
452
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
content: [
|
|
280
|
-
{
|
|
281
|
-
type: "text",
|
|
282
|
-
text: "Design deck is not waiting for a generated option right now.",
|
|
283
|
-
},
|
|
284
|
-
],
|
|
285
|
-
details: { status: "error", url: activeDeckServer.handle.url },
|
|
286
|
-
};
|
|
287
|
-
}
|
|
453
|
+
// Note: We don't check currentResolve here because multiple parallel
|
|
454
|
+
// add-option calls are valid (e.g., user requests 3 options at once)
|
|
288
455
|
|
|
289
456
|
const slideId = p.slideId as string;
|
|
290
457
|
const option = p.option as string;
|
|
@@ -320,9 +487,86 @@ export default function (pi: ExtensionAPI) {
|
|
|
320
487
|
};
|
|
321
488
|
}
|
|
322
489
|
|
|
490
|
+
// For add-option, return immediately without blocking.
|
|
491
|
+
// This allows parallel add-option calls to all succeed.
|
|
492
|
+
// The deck stays open and will send a new prompt when the user
|
|
493
|
+
// clicks generate-more again or submits.
|
|
494
|
+
return {
|
|
495
|
+
content: [{ type: "text", text: `Pushed option "${parsedOption.label}" to slide ${slideId}.` }],
|
|
496
|
+
details: { status: "generate-more", url: activeDeckServer.handle.url, slideId },
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (p.action === "add-options") {
|
|
501
|
+
if (pendingDeckResult) {
|
|
502
|
+
const result = pendingDeckResult;
|
|
503
|
+
pendingDeckResult = null;
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (!activeDeckServer) {
|
|
508
|
+
return {
|
|
509
|
+
content: [{ type: "text", text: "No active design deck session. Start a new deck before adding options." }],
|
|
510
|
+
details: { status: "error", url: "" },
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (activeDeckServer.currentResolve !== null) {
|
|
515
|
+
return {
|
|
516
|
+
content: [{ type: "text", text: "Design deck is not waiting for new options right now." }],
|
|
517
|
+
details: { status: "error", url: activeDeckServer.handle.url },
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const slideId = p.slideId as string;
|
|
522
|
+
const optionsStr = p.options as string;
|
|
523
|
+
|
|
524
|
+
let parsedOptions: unknown;
|
|
525
|
+
try {
|
|
526
|
+
parsedOptions = JSON.parse(optionsStr);
|
|
527
|
+
} catch (err) {
|
|
528
|
+
activeDeckServer.handle.cancelGenerate();
|
|
529
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
530
|
+
const snippet = optionsStr.length > 300 ? optionsStr.slice(0, 300) + "..." : optionsStr;
|
|
531
|
+
return {
|
|
532
|
+
content: [{ type: "text", text: `Invalid options JSON: ${message}\n\nReceived:\n${snippet}\n\nFix the JSON and call design_deck add-options again.` }],
|
|
533
|
+
details: { status: "error", url: activeDeckServer.handle.url },
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (!Array.isArray(parsedOptions)) {
|
|
538
|
+
activeDeckServer.handle.cancelGenerate();
|
|
539
|
+
return {
|
|
540
|
+
content: [{ type: "text", text: "options must be a JSON array of deck options. Fix and call design_deck add-options again." }],
|
|
541
|
+
details: { status: "error", url: activeDeckServer.handle.url },
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
for (const opt of parsedOptions) {
|
|
546
|
+
if (!isDeckOption(opt)) {
|
|
547
|
+
activeDeckServer.handle.cancelGenerate();
|
|
548
|
+
return {
|
|
549
|
+
content: [{ type: "text", text: "One or more options in the array are invalid — each needs label and either previewHtml or previewBlocks. Fix and call design_deck add-options again." }],
|
|
550
|
+
details: { status: "error", url: activeDeckServer.handle.url },
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
for (const opt of parsedOptions) {
|
|
557
|
+
activeDeckServer.handle.pushOption(slideId, opt);
|
|
558
|
+
}
|
|
559
|
+
} catch (err) {
|
|
560
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
561
|
+
return {
|
|
562
|
+
content: [{ type: "text", text: `Failed to push options: ${message}` }],
|
|
563
|
+
details: { status: "error", url: activeDeckServer.handle.url },
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
323
567
|
if (onUpdate) {
|
|
324
568
|
onUpdate({
|
|
325
|
-
content: [{ type: "text", text: `Pushed
|
|
569
|
+
content: [{ type: "text", text: `Pushed ${parsedOptions.length} options to slide ${slideId}.` }],
|
|
326
570
|
details: { status: "generate-more", url: activeDeckServer.handle.url, slideId },
|
|
327
571
|
});
|
|
328
572
|
}
|
|
@@ -407,7 +651,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
407
651
|
|
|
408
652
|
pendingDeckResult = null;
|
|
409
653
|
|
|
410
|
-
if (activeDeckServer) {
|
|
654
|
+
if (activeDeckServer && p.action !== "export") {
|
|
411
655
|
return {
|
|
412
656
|
content: [
|
|
413
657
|
{
|
|
@@ -419,10 +663,61 @@ export default function (pi: ExtensionAPI) {
|
|
|
419
663
|
};
|
|
420
664
|
}
|
|
421
665
|
|
|
422
|
-
const
|
|
666
|
+
const settings = loadSettings();
|
|
667
|
+
const snapshotDir = resolveSnapshotDir(settings.snapshotDir);
|
|
668
|
+
let slides = p.slides as string;
|
|
669
|
+
if (p.action === "open" || p.action === "export") {
|
|
670
|
+
const deckId = (p.deckId as string).trim();
|
|
671
|
+
const deckPath = resolveDeckFilePath(snapshotDir, deckId);
|
|
672
|
+
if (!deckPath || !fs.existsSync(deckPath)) {
|
|
673
|
+
const { decks } = listSavedDecks(snapshotDir);
|
|
674
|
+
const availableHint = decks.length > 0
|
|
675
|
+
? ` Available deck IDs: ${decks.slice(0, 10).map((deck) => deck.id).join(", ")}`
|
|
676
|
+
: " Snapshot directory is empty.";
|
|
677
|
+
return {
|
|
678
|
+
content: [{ type: "text", text: `Saved deck "${deckId}" not found in ${snapshotDir}.${availableHint}` }],
|
|
679
|
+
details: { status: "error", url: "" },
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
if (p.action === "export") {
|
|
683
|
+
const format = typeof p.format === "string" && p.format.trim() !== "" ? p.format.trim().toLowerCase() : "html";
|
|
684
|
+
if (format !== "html") {
|
|
685
|
+
return {
|
|
686
|
+
content: [{ type: "text", text: `Unsupported export format: ${format}` }],
|
|
687
|
+
details: { status: "error", url: "" },
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
const saved = validateSavedDeck(JSON.parse(fs.readFileSync(deckPath, "utf-8")));
|
|
692
|
+
const enrichedSaved: SavedDeckData = {
|
|
693
|
+
...saved,
|
|
694
|
+
id: saved.id ?? deckId,
|
|
695
|
+
status: saved.status ?? deriveDeckStatusFromFolderName(deckId),
|
|
696
|
+
};
|
|
697
|
+
const html = buildStandaloneDeckHtml(deckPath, enrichedSaved);
|
|
698
|
+
const exportPath = path.join(path.dirname(deckPath), "export.html");
|
|
699
|
+
fs.writeFileSync(exportPath, html, "utf-8");
|
|
700
|
+
const relativePath = exportPath.startsWith(os.homedir())
|
|
701
|
+
? "~" + exportPath.slice(os.homedir().length)
|
|
702
|
+
: exportPath;
|
|
703
|
+
return {
|
|
704
|
+
content: [{ type: "text", text: `Exported HTML to ${relativePath}` }],
|
|
705
|
+
};
|
|
706
|
+
} catch (err) {
|
|
707
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
708
|
+
return {
|
|
709
|
+
content: [{ type: "text", text: `Failed to export deck "${deckId}": ${message}` }],
|
|
710
|
+
details: { status: "error", url: "" },
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
slides = deckPath;
|
|
715
|
+
}
|
|
423
716
|
|
|
424
717
|
let configData: unknown;
|
|
425
718
|
let savedSelections: Record<string, string> | undefined;
|
|
719
|
+
let savedNotes: Record<string, { label: string; notes: string }> | undefined;
|
|
720
|
+
let savedFinalNotes: string | undefined;
|
|
426
721
|
try {
|
|
427
722
|
configData = JSON.parse(slides);
|
|
428
723
|
} catch {
|
|
@@ -431,37 +726,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
431
726
|
if (!fs.existsSync(absolutePath)) {
|
|
432
727
|
throw new Error(`Invalid slides: not valid JSON and file not found at ${absolutePath}`);
|
|
433
728
|
}
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const message = parseErr instanceof Error ? parseErr.message : String(parseErr);
|
|
440
|
-
throw new Error(`Invalid JSON in saved deck file: ${message}`);
|
|
441
|
-
}
|
|
442
|
-
const raw = fileData as Record<string, unknown>;
|
|
443
|
-
if (raw.config && typeof raw.config === "object") {
|
|
444
|
-
const saved = validateSavedDeck(fileData);
|
|
445
|
-
configData = saved.config;
|
|
446
|
-
savedSelections = Object.keys(saved.selections).length > 0 ? saved.selections : undefined;
|
|
447
|
-
const snapshotDir = path.dirname(absolutePath);
|
|
448
|
-
for (const slide of saved.config.slides) {
|
|
449
|
-
for (const option of slide.options) {
|
|
450
|
-
if (!option.previewBlocks) continue;
|
|
451
|
-
for (const block of option.previewBlocks) {
|
|
452
|
-
if (block.type === "image" && !path.isAbsolute(block.src)) {
|
|
453
|
-
block.src = path.join(snapshotDir, block.src);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
} else {
|
|
459
|
-
configData = fileData;
|
|
460
|
-
}
|
|
729
|
+
const loaded = loadDeckFile(absolutePath);
|
|
730
|
+
configData = loaded.configData;
|
|
731
|
+
savedSelections = loaded.savedSelections;
|
|
732
|
+
savedNotes = loaded.savedNotes;
|
|
733
|
+
savedFinalNotes = loaded.savedFinalNotes;
|
|
461
734
|
}
|
|
462
735
|
const config = validateDeckConfig(configData);
|
|
463
|
-
|
|
464
|
-
const settings = loadSettings();
|
|
465
736
|
const sessionId = randomUUID();
|
|
466
737
|
const sessionToken = randomUUID();
|
|
467
738
|
|
|
@@ -472,17 +743,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
472
743
|
};
|
|
473
744
|
}
|
|
474
745
|
|
|
475
|
-
const handleSubmit = (selections: Record<string, string
|
|
746
|
+
const handleSubmit = (selections: Record<string, string>, notes?: Record<string, string>, finalNotes?: string) => {
|
|
476
747
|
if (!activeDeckServer) return;
|
|
477
748
|
const url = activeDeckServer.handle.url;
|
|
749
|
+
const hasNotes = notes && Object.keys(notes).length > 0;
|
|
750
|
+
const textParts = [`Design deck completed.\n\nSelections:\n${formatDeckSelections(selections, notes)}`];
|
|
751
|
+
if (finalNotes) {
|
|
752
|
+
textParts.push(`\nAdditional instructions:\n${finalNotes}`);
|
|
753
|
+
}
|
|
478
754
|
cleanupActiveDeckAndStoreResult({
|
|
479
755
|
content: [
|
|
480
756
|
{
|
|
481
757
|
type: "text",
|
|
482
|
-
text:
|
|
758
|
+
text: textParts.join(""),
|
|
483
759
|
},
|
|
484
760
|
],
|
|
485
|
-
details: {
|
|
761
|
+
details: {
|
|
762
|
+
status: "completed",
|
|
763
|
+
url,
|
|
764
|
+
selections,
|
|
765
|
+
...(hasNotes ? { notes } : {}),
|
|
766
|
+
...(finalNotes ? { finalNotes } : {}),
|
|
767
|
+
},
|
|
486
768
|
});
|
|
487
769
|
};
|
|
488
770
|
|
|
@@ -503,8 +785,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
503
785
|
});
|
|
504
786
|
};
|
|
505
787
|
|
|
506
|
-
const handleGenerateMore = (slideId: string, prompt?: string, model?: string, thinking?: string) => {
|
|
507
|
-
if (!activeDeckServer?.currentResolve)
|
|
788
|
+
const handleGenerateMore = (slideId: string, prompt?: string, model?: string, thinking?: string, count?: number) => {
|
|
789
|
+
if (!activeDeckServer?.currentResolve) {
|
|
790
|
+
// Agent is no longer listening - close the deck
|
|
791
|
+
cleanupActiveDeck("stale");
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
508
794
|
const resolve = activeDeckServer.currentResolve;
|
|
509
795
|
activeDeckServer.currentResolve = null;
|
|
510
796
|
armDeckIdleTimer();
|
|
@@ -513,8 +799,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
513
799
|
if (thinking && !effectiveModel) {
|
|
514
800
|
pi.setThinkingLevel(thinking as "off" | "minimal" | "low" | "medium" | "high" | "xhigh");
|
|
515
801
|
}
|
|
802
|
+
const effectiveCount = count && count >= 1 && count <= 5 ? count : 1;
|
|
516
803
|
resolve({
|
|
517
|
-
content: [{ type: "text", text: buildGenerateMoreResult(slideId, slide, prompt, effectiveModel, thinking) }],
|
|
804
|
+
content: [{ type: "text", text: buildGenerateMoreResult(slideId, slide, prompt, effectiveModel, thinking, effectiveCount) }],
|
|
518
805
|
details: {
|
|
519
806
|
status: "generate-more",
|
|
520
807
|
url: activeDeckServer.handle.url,
|
|
@@ -524,7 +811,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
524
811
|
};
|
|
525
812
|
|
|
526
813
|
const handleRegenerateSlide = (slideId: string, prompt?: string, model?: string, thinking?: string) => {
|
|
527
|
-
if (!activeDeckServer?.currentResolve)
|
|
814
|
+
if (!activeDeckServer?.currentResolve) {
|
|
815
|
+
// Agent is no longer listening - close the deck
|
|
816
|
+
cleanupActiveDeck("stale");
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
528
819
|
const resolve = activeDeckServer.currentResolve;
|
|
529
820
|
activeDeckServer.currentResolve = null;
|
|
530
821
|
armDeckIdleTimer();
|
|
@@ -545,8 +836,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
545
836
|
};
|
|
546
837
|
|
|
547
838
|
const themeConfig = settings.theme ?? {};
|
|
548
|
-
const snapshotDir = settings.snapshotDir ? expandHome(settings.snapshotDir) : undefined;
|
|
549
|
-
|
|
550
839
|
const currentModelStr = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null;
|
|
551
840
|
const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
|
|
552
841
|
provider: m.provider,
|
|
@@ -570,6 +859,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
570
859
|
toggleHotkey: themeConfig.toggleHotkey ?? DEFAULT_THEME_HOTKEY,
|
|
571
860
|
},
|
|
572
861
|
savedSelections,
|
|
862
|
+
savedNotes,
|
|
863
|
+
savedFinalNotes,
|
|
573
864
|
snapshotDir,
|
|
574
865
|
autoSaveOnSubmit: settings.autoSaveOnSubmit ?? true,
|
|
575
866
|
models: {
|
|
@@ -618,6 +909,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
618
909
|
0
|
|
619
910
|
);
|
|
620
911
|
}
|
|
912
|
+
if (data.action === "add-options") {
|
|
913
|
+
return new Text(
|
|
914
|
+
theme.fg("toolTitle", theme.bold(`Design Deck: add options (${data.slideId || "unknown"})`)),
|
|
915
|
+
0,
|
|
916
|
+
0
|
|
917
|
+
);
|
|
918
|
+
}
|
|
621
919
|
if (data.action === "replace-options") {
|
|
622
920
|
return new Text(
|
|
623
921
|
theme.fg("toolTitle", theme.bold(`Design Deck: replace options (${data.slideId || "unknown"})`)),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-design-deck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Visual design deck for presenting multi-slide options with high-fidelity previews",
|
|
5
5
|
"author": "Nico Bailon",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"model-runner.ts",
|
|
17
17
|
"server-utils.ts",
|
|
18
18
|
"settings.ts",
|
|
19
|
+
"export-html.ts",
|
|
19
20
|
"index.ts"
|
|
20
21
|
],
|
|
21
22
|
"scripts": {
|
package/prompts/deck-discover.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Interview to gather requirements, then present design options
|
|
3
3
|
---
|
|
4
|
-
Load the `design-deck` skill for the full format reference.
|
|
4
|
+
Load the `design-deck` skill for the full format reference. If presenting UI component options (tabs, trees, buttons, accordions, etc.), also read the relevant file from the skill's `references/component-gallery/` directory — it has visual vocabulary across design systems plus guidance on when to use distinct systems vs variations of the same approach.
|
|
5
|
+
|
|
6
|
+
This is a two-phase flow.
|
|
5
7
|
|
|
6
8
|
**Phase 1 — Discovery.** Before proposing anything, interview me in depth. Read relevant codebase context first so your questions are informed. Then hit me with detailed questions about goals, constraints, audience, aesthetics, technical requirements, trade-offs, edge cases, and anything else that would affect the design options. No obvious questions. Keep interviewing in multiple rounds if earlier answers surface new dimensions worth exploring — only move on when you have enough to generate genuinely distinct options.
|
|
7
9
|
|
package/prompts/deck-plan.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Analyze a plan/PRD and present design & architecture options
|
|
3
3
|
---
|
|
4
|
-
Load the `design-deck` skill for the full format reference.
|
|
4
|
+
Load the `design-deck` skill for the full format reference. If presenting UI component options (tabs, trees, buttons, accordions, etc.), also read the relevant file from the skill's `references/component-gallery/` directory — it has visual vocabulary across design systems plus guidance on when to use distinct systems vs variations of the same approach.
|
|
5
|
+
|
|
6
|
+
Then read and analyze the plan or PRD at `$1`. Also read the actual codebase files it references — in full — to understand the real state of the code, not just what the plan assumes.
|
|
5
7
|
|
|
6
8
|
Identify the key design and architecture decisions embedded in the plan. For each decision point, build a slide with 2-4 concrete options that are faithful to the plan's goals but offer genuinely different approaches. Mix preview types as appropriate: mermaid diagrams for architecture, code blocks for API design, HTML for UI mockups, images if available.
|
|
7
9
|
|