pi-design-deck 0.2.0 → 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 +49 -11
- package/deck-schema.ts +30 -0
- package/deck-server.ts +64 -10
- package/export-html.ts +329 -0
- package/form/css/controls.css +51 -0
- package/form/deck.html +2 -0
- package/form/js/deck-core.js +46 -0
- package/form/js/deck-interact.js +30 -12
- package/form/js/deck-render.js +2 -0
- package/form/js/deck-session.js +25 -9
- package/generate-prompts.ts +8 -10
- package/index.ts +317 -41
- 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();
|
|
@@ -64,6 +65,24 @@ let restoreDeckThinking: (() => void) | null = null;
|
|
|
64
65
|
|
|
65
66
|
const DECK_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
66
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
|
+
|
|
67
86
|
const DeckParams = Type.Object(
|
|
68
87
|
{
|
|
69
88
|
slides: Type.Optional(
|
|
@@ -78,11 +97,15 @@ const DeckParams = Type.Object(
|
|
|
78
97
|
action: Type.Optional(
|
|
79
98
|
Type.Union([
|
|
80
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)" }),
|
|
81
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" }),
|
|
82
105
|
])
|
|
83
106
|
),
|
|
84
107
|
slideId: Type.Optional(
|
|
85
|
-
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')" })
|
|
86
109
|
),
|
|
87
110
|
option: Type.Optional(
|
|
88
111
|
Type.String({
|
|
@@ -93,9 +116,15 @@ const DeckParams = Type.Object(
|
|
|
93
116
|
options: Type.Optional(
|
|
94
117
|
Type.String({
|
|
95
118
|
description:
|
|
96
|
-
"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')",
|
|
97
120
|
})
|
|
98
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
|
+
),
|
|
99
128
|
},
|
|
100
129
|
{ additionalProperties: false }
|
|
101
130
|
);
|
|
@@ -111,6 +140,104 @@ function expandHome(value: string): string {
|
|
|
111
140
|
return value;
|
|
112
141
|
}
|
|
113
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
|
+
|
|
114
241
|
const DEFAULT_THEME_HOTKEY = "mod+shift+l";
|
|
115
242
|
|
|
116
243
|
function clearDeckIdleTimer(): void {
|
|
@@ -207,19 +334,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
207
334
|
"Present a multi-slide design deck with visual options for decisions. " +
|
|
208
335
|
"Slides JSON: { title?, slides: [{ id, title, context?, columns?, options }] }. " +
|
|
209
336
|
"When the user requests more options, tool returns generate-more instructions — " +
|
|
210
|
-
'call design_deck with action:"add-
|
|
337
|
+
'call design_deck with action:"add-options" to push all new options at once. ' +
|
|
211
338
|
"previewBlocks for code/architecture comparisons, previewHtml for custom UI mockups.",
|
|
212
339
|
parameters: DeckParams,
|
|
213
340
|
|
|
214
341
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
215
|
-
|
|
342
|
+
const p = params as Record<string, unknown>;
|
|
343
|
+
|
|
344
|
+
if (!ctx.hasUI && p.action !== "list" && p.action !== "export") {
|
|
216
345
|
throw new Error(
|
|
217
346
|
"design_deck requires interactive mode with browser support. " +
|
|
218
347
|
"Cannot run in headless/RPC/print mode."
|
|
219
348
|
);
|
|
220
349
|
}
|
|
221
350
|
|
|
222
|
-
|
|
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
|
+
}
|
|
223
363
|
|
|
224
364
|
if (p.action === "add-option") {
|
|
225
365
|
if (typeof p.slideId !== "string" || p.slideId.trim() === "") {
|
|
@@ -236,6 +376,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
236
376
|
details: { status: "error", url: activeDeckServer?.handle.url ?? "" },
|
|
237
377
|
};
|
|
238
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
|
+
}
|
|
239
394
|
} else if (p.action === "replace-options") {
|
|
240
395
|
if (typeof p.slideId !== "string" || p.slideId.trim() === "") {
|
|
241
396
|
activeDeckServer?.handle.cancelGenerate();
|
|
@@ -251,6 +406,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
251
406
|
details: { status: "error", url: activeDeckServer?.handle.url ?? "" },
|
|
252
407
|
};
|
|
253
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
|
+
}
|
|
254
416
|
} else if (typeof p.slides !== "string" || p.slides.trim() === "") {
|
|
255
417
|
return {
|
|
256
418
|
content: [{
|
|
@@ -258,7 +420,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
258
420
|
text:
|
|
259
421
|
"design_deck requires one of:\n\n" +
|
|
260
422
|
'1. Start a new deck: { slides: "<JSON string of { title?, slides: [{ id, title, options }] }>" }\n' +
|
|
261
|
-
'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' +
|
|
262
427
|
"Each option needs label + either previewHtml (raw HTML) or previewBlocks (array of {type, ...} blocks).\n" +
|
|
263
428
|
"Block types: html, mermaid, code, image.",
|
|
264
429
|
}],
|
|
@@ -332,6 +497,83 @@ export default function (pi: ExtensionAPI) {
|
|
|
332
497
|
};
|
|
333
498
|
}
|
|
334
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
|
+
|
|
567
|
+
if (onUpdate) {
|
|
568
|
+
onUpdate({
|
|
569
|
+
content: [{ type: "text", text: `Pushed ${parsedOptions.length} options to slide ${slideId}.` }],
|
|
570
|
+
details: { status: "generate-more", url: activeDeckServer.handle.url, slideId },
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
attachDeckAbortHandler(signal);
|
|
574
|
+
return blockOnDeck();
|
|
575
|
+
}
|
|
576
|
+
|
|
335
577
|
if (p.action === "replace-options") {
|
|
336
578
|
if (pendingDeckResult) {
|
|
337
579
|
const result = pendingDeckResult;
|
|
@@ -409,7 +651,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
409
651
|
|
|
410
652
|
pendingDeckResult = null;
|
|
411
653
|
|
|
412
|
-
if (activeDeckServer) {
|
|
654
|
+
if (activeDeckServer && p.action !== "export") {
|
|
413
655
|
return {
|
|
414
656
|
content: [
|
|
415
657
|
{
|
|
@@ -421,10 +663,61 @@ export default function (pi: ExtensionAPI) {
|
|
|
421
663
|
};
|
|
422
664
|
}
|
|
423
665
|
|
|
424
|
-
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
|
+
}
|
|
425
716
|
|
|
426
717
|
let configData: unknown;
|
|
427
718
|
let savedSelections: Record<string, string> | undefined;
|
|
719
|
+
let savedNotes: Record<string, { label: string; notes: string }> | undefined;
|
|
720
|
+
let savedFinalNotes: string | undefined;
|
|
428
721
|
try {
|
|
429
722
|
configData = JSON.parse(slides);
|
|
430
723
|
} catch {
|
|
@@ -433,37 +726,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
433
726
|
if (!fs.existsSync(absolutePath)) {
|
|
434
727
|
throw new Error(`Invalid slides: not valid JSON and file not found at ${absolutePath}`);
|
|
435
728
|
}
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const message = parseErr instanceof Error ? parseErr.message : String(parseErr);
|
|
442
|
-
throw new Error(`Invalid JSON in saved deck file: ${message}`);
|
|
443
|
-
}
|
|
444
|
-
const raw = fileData as Record<string, unknown>;
|
|
445
|
-
if (raw.config && typeof raw.config === "object") {
|
|
446
|
-
const saved = validateSavedDeck(fileData);
|
|
447
|
-
configData = saved.config;
|
|
448
|
-
savedSelections = Object.keys(saved.selections).length > 0 ? saved.selections : undefined;
|
|
449
|
-
const snapshotDir = path.dirname(absolutePath);
|
|
450
|
-
for (const slide of saved.config.slides) {
|
|
451
|
-
for (const option of slide.options) {
|
|
452
|
-
if (!option.previewBlocks) continue;
|
|
453
|
-
for (const block of option.previewBlocks) {
|
|
454
|
-
if (block.type === "image" && !path.isAbsolute(block.src)) {
|
|
455
|
-
block.src = path.join(snapshotDir, block.src);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
} else {
|
|
461
|
-
configData = fileData;
|
|
462
|
-
}
|
|
729
|
+
const loaded = loadDeckFile(absolutePath);
|
|
730
|
+
configData = loaded.configData;
|
|
731
|
+
savedSelections = loaded.savedSelections;
|
|
732
|
+
savedNotes = loaded.savedNotes;
|
|
733
|
+
savedFinalNotes = loaded.savedFinalNotes;
|
|
463
734
|
}
|
|
464
735
|
const config = validateDeckConfig(configData);
|
|
465
|
-
|
|
466
|
-
const settings = loadSettings();
|
|
467
736
|
const sessionId = randomUUID();
|
|
468
737
|
const sessionToken = randomUUID();
|
|
469
738
|
|
|
@@ -567,8 +836,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
567
836
|
};
|
|
568
837
|
|
|
569
838
|
const themeConfig = settings.theme ?? {};
|
|
570
|
-
const snapshotDir = settings.snapshotDir ? expandHome(settings.snapshotDir) : undefined;
|
|
571
|
-
|
|
572
839
|
const currentModelStr = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null;
|
|
573
840
|
const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
|
|
574
841
|
provider: m.provider,
|
|
@@ -592,6 +859,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
592
859
|
toggleHotkey: themeConfig.toggleHotkey ?? DEFAULT_THEME_HOTKEY,
|
|
593
860
|
},
|
|
594
861
|
savedSelections,
|
|
862
|
+
savedNotes,
|
|
863
|
+
savedFinalNotes,
|
|
595
864
|
snapshotDir,
|
|
596
865
|
autoSaveOnSubmit: settings.autoSaveOnSubmit ?? true,
|
|
597
866
|
models: {
|
|
@@ -640,6 +909,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
640
909
|
0
|
|
641
910
|
);
|
|
642
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
|
+
}
|
|
643
919
|
if (data.action === "replace-options") {
|
|
644
920
|
return new Text(
|
|
645
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
|
|
package/prompts/deck.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Present visual design options via design deck
|
|
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
|
+
Analyze the current codebase and context, and present concrete visual options using `design_deck`.
|
|
5
7
|
|
|
6
8
|
Read relevant files aggressively first so you understand the real constraints before generating options. If there's a plan or PRD, read it in full along with every codebase file it references — understand the actual state of the code, not just what the document assumes. Then identify the key design and architecture decisions and present options for each.
|
|
7
9
|
|
|
@@ -3,6 +3,8 @@ name: design-deck
|
|
|
3
3
|
description: Present visual options for architecture, UI, and code decisions with high-fidelity side-by-side previews. For comparing approaches visually — code diffs, diagrams, UI mockups, images — not for gathering structured input (use interview for that). Supports previewBlocks (code, mermaid, image, html), previewHtml, generate-more loops, and plan/PRD-driven flows.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
> **`design_deck` is a direct tool — call it directly, not via MCP.**
|
|
7
|
+
|
|
6
8
|
# Design Deck Workflow
|
|
7
9
|
|
|
8
10
|
Use this skill when the task requires presenting multiple visual directions and collecting explicit user choices. Load this skill before building any deck to get the full format reference.
|
|
@@ -201,6 +203,40 @@ surf gemini "isometric database cluster, dark theme, blue nodes" \
|
|
|
201
203
|
**Combine images with other blocks:**
|
|
202
204
|
An image showing the visual direction paired with a code block showing the implementation approach, or a mermaid diagram showing data flow alongside a generated image showing the UI that flow produces.
|
|
203
205
|
|
|
206
|
+
### Component Gallery Reference
|
|
207
|
+
|
|
208
|
+
When generating UI component options (tabs, accordions, tree views, buttons, etc.), read `./references/component-gallery/components.md` for best practices, common layouts, and aliases for 60 UI components.
|
|
209
|
+
|
|
210
|
+
The reference enables three things:
|
|
211
|
+
- **Discovery** — list, find, and suggest components for a use case ("I need expandable content" → accordion, disclosure, details)
|
|
212
|
+
- **Cross-referencing** — connect related terms (collapse = accordion = disclosure = expander; notification = alert = banner)
|
|
213
|
+
- **Design vocabulary** — know what design systems look like (Blueprint = dense, dark-native; Ant = clean, blue primary)
|
|
214
|
+
|
|
215
|
+
The `INDEX.md` provides a design system vocabulary table (Ant Design, Blueprint, Carbon, Material, etc.) and guidance on when to use distinct systems vs variations.
|
|
216
|
+
|
|
217
|
+
**Decide based on context:**
|
|
218
|
+
- **Distinct systems** (Blueprint vs Ant vs 98.css) when exploring the design space with no established aesthetic
|
|
219
|
+
- **Variations within a system** when the project already has a design direction or the user specifies a style
|
|
220
|
+
|
|
221
|
+
See `./references/component-gallery/INDEX.md` for the decision table and examples.
|
|
222
|
+
|
|
223
|
+
**Proactively browse real examples:** When you need visual inspiration for a component, fetch the component.gallery page directly:
|
|
224
|
+
|
|
225
|
+
- https://component.gallery/components/tabs/ — 80+ real tab implementations
|
|
226
|
+
- https://component.gallery/components/accordion/ — 100+ accordion examples
|
|
227
|
+
- https://component.gallery/components/button/ — 120+ button styles
|
|
228
|
+
|
|
229
|
+
Each page shows real screenshots from Ant Design, Blueprint, Carbon, Material, Shopify Polaris, and 90+ other design systems. Useful when you need concrete visual references — not required for every deck.
|
|
230
|
+
|
|
231
|
+
**When user vocabulary is unclear or ambiguous:** Read `./references/component-gallery/LOOKUP.md` to resolve user terms to canonical component names. The lookup file provides:
|
|
232
|
+
|
|
233
|
+
1. **Alias Index** — Direct mappings like `collapse → Accordion`, `snackbar → Toast`
|
|
234
|
+
2. **Disambiguation** — Rules for ambiguous terms like `dropdown` (Select? Combobox? Dropdown menu?) or `popup` (Modal? Popover? Tooltip?)
|
|
235
|
+
3. **Intent Clusters** — When users describe what they're trying to do ("I need users to pick from options"), maps constraints to components
|
|
236
|
+
4. **Clarification Templates** — Suggested questions when disambiguation is needed
|
|
237
|
+
|
|
238
|
+
Use the lookup before generating options if the user's component vocabulary seems imprecise or you're unsure which component they mean.
|
|
239
|
+
|
|
204
240
|
### For Mixed Previews
|
|
205
241
|
|
|
206
242
|
Many slides benefit from combining block types — a mermaid diagram showing structure alongside a code block showing usage, or an HTML mockup with a code block showing the component API.
|
|
@@ -220,27 +256,27 @@ Use these selections as the implementation contract for the final build.
|
|
|
220
256
|
|
|
221
257
|
## Generate-More Loop
|
|
222
258
|
|
|
223
|
-
When the user clicks generate-more, `design_deck` returns a result instructing which slide needs
|
|
259
|
+
When the user clicks generate-more, `design_deck` returns a result instructing which slide needs options and how many, along with the existing option labels.
|
|
224
260
|
|
|
225
|
-
Generate
|
|
261
|
+
Generate the requested number of new options, each meaningfully distinct from the existing ones.
|
|
226
262
|
|
|
227
|
-
Re-invoke:
|
|
263
|
+
Re-invoke with all options in a single call:
|
|
228
264
|
|
|
229
|
-
`design_deck({ action: "add-
|
|
265
|
+
`design_deck({ action: "add-options", slideId: "...", options: "[{...}, {...}]" })`
|
|
230
266
|
|
|
231
|
-
The `add-
|
|
267
|
+
The `add-options` call pushes all options into the live deck and blocks for the next user action. Use `add-options` (plural) for generate-more requests — it takes an array and handles blocking automatically.
|
|
232
268
|
|
|
233
269
|
### Model Override
|
|
234
270
|
|
|
235
271
|
The deck shows a model dropdown below the header (when 2+ models are available). Users can pick which model generates new options and optionally save it as the default.
|
|
236
272
|
|
|
237
|
-
When the generate-more result includes a model instruction (e.g. "Generate using model X via deck_generate"), use the built-in `deck_generate` tool to generate the
|
|
273
|
+
When the generate-more result includes a model instruction (e.g. "Generate using model X via deck_generate"), use the built-in `deck_generate` tool to generate the options with that model:
|
|
238
274
|
|
|
239
275
|
```
|
|
240
|
-
deck_generate({ model: "google/gemini-3.1-pro", task: "Generate
|
|
276
|
+
deck_generate({ model: "google/gemini-3.1-pro", task: "Generate JSON deck options..." })
|
|
241
277
|
```
|
|
242
278
|
|
|
243
|
-
Parse the output as the
|
|
279
|
+
Parse the output as the options JSON array and pass it to `add-options`.
|
|
244
280
|
|
|
245
281
|
The default model can also be set in `~/.pi/agent/settings.json`:
|
|
246
282
|
|