pi-design-deck 0.1.0 → 0.2.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 +7 -14
- package/deck-server.ts +12 -9
- package/form/css/controls.css +121 -0
- package/form/css/layout.css +56 -8
- package/form/css/preview.css +60 -6
- package/form/js/deck-core.js +14 -2
- package/form/js/deck-interact.js +36 -15
- package/form/js/deck-render.js +99 -10
- package/form/js/deck-session.js +128 -30
- package/generate-prompts.ts +17 -9
- package/index.ts +74 -32
- package/model-runner.ts +50 -0
- package/package.json +2 -1
- package/skills/design-deck/SKILL.md +3 -3
package/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { loadSettings } from "./settings.js";
|
|
|
9
9
|
import { startDeckServer, type DeckServerHandle, type ModelInfo } from "./deck-server.js";
|
|
10
10
|
import { isDeckOption, validateDeckConfig, validateSavedDeck } from "./deck-schema.js";
|
|
11
11
|
import { buildGenerateMoreResult, buildRegenerateResult } from "./generate-prompts.js";
|
|
12
|
+
import { generateWithModel } from "./model-runner.js";
|
|
12
13
|
|
|
13
14
|
async function openUrl(pi: ExtensionAPI, url: string, browser?: string): Promise<void> {
|
|
14
15
|
const platform = os.platform();
|
|
@@ -41,6 +42,8 @@ interface DeckDetails {
|
|
|
41
42
|
status: "completed" | "cancelled" | "generate-more" | "aborted" | "error";
|
|
42
43
|
url: string;
|
|
43
44
|
selections?: Record<string, string>;
|
|
45
|
+
notes?: Record<string, string>;
|
|
46
|
+
finalNotes?: string;
|
|
44
47
|
slideId?: string;
|
|
45
48
|
reason?: string;
|
|
46
49
|
}
|
|
@@ -117,7 +120,7 @@ function clearDeckIdleTimer(): void {
|
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
function cleanupActiveDeck(): void {
|
|
123
|
+
function cleanupActiveDeck(reason?: string): void {
|
|
121
124
|
clearDeckIdleTimer();
|
|
122
125
|
if (restoreDeckThinking) {
|
|
123
126
|
restoreDeckThinking();
|
|
@@ -125,20 +128,23 @@ function cleanupActiveDeck(): void {
|
|
|
125
128
|
}
|
|
126
129
|
if (!activeDeckServer) return;
|
|
127
130
|
try {
|
|
128
|
-
activeDeckServer.handle.close();
|
|
131
|
+
activeDeckServer.handle.close(reason);
|
|
129
132
|
} catch {}
|
|
130
133
|
activeDeckServer = null;
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
function cleanupActiveDeckAndStoreResult(result: DeckToolResult): void {
|
|
134
137
|
if (!activeDeckServer) return;
|
|
138
|
+
// Extract close reason from result details
|
|
139
|
+
const details = result.details as DeckDetails | undefined;
|
|
140
|
+
const closeReason = details?.status === "aborted" ? "aborted" : details?.reason;
|
|
135
141
|
if (activeDeckServer.currentResolve) {
|
|
136
142
|
const resolve = activeDeckServer.currentResolve;
|
|
137
|
-
cleanupActiveDeck();
|
|
143
|
+
cleanupActiveDeck(closeReason);
|
|
138
144
|
resolve(result);
|
|
139
145
|
} else {
|
|
140
146
|
pendingDeckResult = result;
|
|
141
|
-
cleanupActiveDeck();
|
|
147
|
+
cleanupActiveDeck(closeReason);
|
|
142
148
|
}
|
|
143
149
|
}
|
|
144
150
|
|
|
@@ -180,10 +186,16 @@ function attachDeckAbortHandler(signal: AbortSignal | undefined): void {
|
|
|
180
186
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
181
187
|
}
|
|
182
188
|
|
|
183
|
-
function formatDeckSelections(selections: Record<string, string>): string {
|
|
189
|
+
function formatDeckSelections(selections: Record<string, string>, notes?: Record<string, string>): string {
|
|
184
190
|
const entries = Object.entries(selections);
|
|
185
191
|
if (entries.length === 0) return "(none)";
|
|
186
|
-
return entries.map(([key, value]) =>
|
|
192
|
+
return entries.map(([key, value]) => {
|
|
193
|
+
const note = notes?.[key];
|
|
194
|
+
if (note) {
|
|
195
|
+
return `- ${key}: ${value}\n Notes: ${note}`;
|
|
196
|
+
}
|
|
197
|
+
return `- ${key}: ${value}`;
|
|
198
|
+
}).join("\n");
|
|
187
199
|
}
|
|
188
200
|
|
|
189
201
|
export default function (pi: ExtensionAPI) {
|
|
@@ -273,17 +285,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
273
285
|
};
|
|
274
286
|
}
|
|
275
287
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
content: [
|
|
279
|
-
{
|
|
280
|
-
type: "text",
|
|
281
|
-
text: "Design deck is not waiting for a generated option right now.",
|
|
282
|
-
},
|
|
283
|
-
],
|
|
284
|
-
details: { status: "error", url: activeDeckServer.handle.url },
|
|
285
|
-
};
|
|
286
|
-
}
|
|
288
|
+
// Note: We don't check currentResolve here because multiple parallel
|
|
289
|
+
// add-option calls are valid (e.g., user requests 3 options at once)
|
|
287
290
|
|
|
288
291
|
const slideId = p.slideId as string;
|
|
289
292
|
const option = p.option as string;
|
|
@@ -319,14 +322,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
319
322
|
};
|
|
320
323
|
}
|
|
321
324
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
325
|
+
// For add-option, return immediately without blocking.
|
|
326
|
+
// This allows parallel add-option calls to all succeed.
|
|
327
|
+
// The deck stays open and will send a new prompt when the user
|
|
328
|
+
// clicks generate-more again or submits.
|
|
329
|
+
return {
|
|
330
|
+
content: [{ type: "text", text: `Pushed option "${parsedOption.label}" to slide ${slideId}.` }],
|
|
331
|
+
details: { status: "generate-more", url: activeDeckServer.handle.url, slideId },
|
|
332
|
+
};
|
|
330
333
|
}
|
|
331
334
|
|
|
332
335
|
if (p.action === "replace-options") {
|
|
@@ -471,17 +474,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
471
474
|
};
|
|
472
475
|
}
|
|
473
476
|
|
|
474
|
-
const handleSubmit = (selections: Record<string, string
|
|
477
|
+
const handleSubmit = (selections: Record<string, string>, notes?: Record<string, string>, finalNotes?: string) => {
|
|
475
478
|
if (!activeDeckServer) return;
|
|
476
479
|
const url = activeDeckServer.handle.url;
|
|
480
|
+
const hasNotes = notes && Object.keys(notes).length > 0;
|
|
481
|
+
const textParts = [`Design deck completed.\n\nSelections:\n${formatDeckSelections(selections, notes)}`];
|
|
482
|
+
if (finalNotes) {
|
|
483
|
+
textParts.push(`\nAdditional instructions:\n${finalNotes}`);
|
|
484
|
+
}
|
|
477
485
|
cleanupActiveDeckAndStoreResult({
|
|
478
486
|
content: [
|
|
479
487
|
{
|
|
480
488
|
type: "text",
|
|
481
|
-
text:
|
|
489
|
+
text: textParts.join(""),
|
|
482
490
|
},
|
|
483
491
|
],
|
|
484
|
-
details: {
|
|
492
|
+
details: {
|
|
493
|
+
status: "completed",
|
|
494
|
+
url,
|
|
495
|
+
selections,
|
|
496
|
+
...(hasNotes ? { notes } : {}),
|
|
497
|
+
...(finalNotes ? { finalNotes } : {}),
|
|
498
|
+
},
|
|
485
499
|
});
|
|
486
500
|
};
|
|
487
501
|
|
|
@@ -502,8 +516,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
502
516
|
});
|
|
503
517
|
};
|
|
504
518
|
|
|
505
|
-
const handleGenerateMore = (slideId: string, prompt?: string, model?: string, thinking?: string) => {
|
|
506
|
-
if (!activeDeckServer?.currentResolve)
|
|
519
|
+
const handleGenerateMore = (slideId: string, prompt?: string, model?: string, thinking?: string, count?: number) => {
|
|
520
|
+
if (!activeDeckServer?.currentResolve) {
|
|
521
|
+
// Agent is no longer listening - close the deck
|
|
522
|
+
cleanupActiveDeck("stale");
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
507
525
|
const resolve = activeDeckServer.currentResolve;
|
|
508
526
|
activeDeckServer.currentResolve = null;
|
|
509
527
|
armDeckIdleTimer();
|
|
@@ -512,8 +530,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
512
530
|
if (thinking && !effectiveModel) {
|
|
513
531
|
pi.setThinkingLevel(thinking as "off" | "minimal" | "low" | "medium" | "high" | "xhigh");
|
|
514
532
|
}
|
|
533
|
+
const effectiveCount = count && count >= 1 && count <= 5 ? count : 1;
|
|
515
534
|
resolve({
|
|
516
|
-
content: [{ type: "text", text: buildGenerateMoreResult(slideId, slide, prompt, effectiveModel, thinking) }],
|
|
535
|
+
content: [{ type: "text", text: buildGenerateMoreResult(slideId, slide, prompt, effectiveModel, thinking, effectiveCount) }],
|
|
517
536
|
details: {
|
|
518
537
|
status: "generate-more",
|
|
519
538
|
url: activeDeckServer.handle.url,
|
|
@@ -523,7 +542,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
523
542
|
};
|
|
524
543
|
|
|
525
544
|
const handleRegenerateSlide = (slideId: string, prompt?: string, model?: string, thinking?: string) => {
|
|
526
|
-
if (!activeDeckServer?.currentResolve)
|
|
545
|
+
if (!activeDeckServer?.currentResolve) {
|
|
546
|
+
// Agent is no longer listening - close the deck
|
|
547
|
+
cleanupActiveDeck("stale");
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
527
550
|
const resolve = activeDeckServer.currentResolve;
|
|
528
551
|
activeDeckServer.currentResolve = null;
|
|
529
552
|
armDeckIdleTimer();
|
|
@@ -664,6 +687,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
664
687
|
},
|
|
665
688
|
});
|
|
666
689
|
|
|
690
|
+
pi.registerTool({
|
|
691
|
+
name: "deck_generate",
|
|
692
|
+
description: "Generate text using a specific model (for design deck option generation). Use this when the generate-more prompt specifies a model override.",
|
|
693
|
+
parameters: Type.Object({
|
|
694
|
+
model: Type.String({ description: "Full model ID in 'provider/model-id' format (e.g., 'anthropic/claude-haiku-4-5')" }),
|
|
695
|
+
task: Type.String({ description: "The generation task/prompt" }),
|
|
696
|
+
}),
|
|
697
|
+
async execute(_toolCallId, params) {
|
|
698
|
+
const { model, task } = params as { model: string; task: string };
|
|
699
|
+
try {
|
|
700
|
+
const result = await generateWithModel(model, task);
|
|
701
|
+
return { content: [{ type: "text", text: result }] };
|
|
702
|
+
} catch (err) {
|
|
703
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
704
|
+
return { content: [{ type: "text", text: `Generation failed: ${message}` }], isError: true };
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
|
|
667
709
|
pi.on("session_shutdown", () => {
|
|
668
710
|
pendingDeckResult = null;
|
|
669
711
|
cleanupActiveDeck();
|
package/model-runner.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const TIMEOUT_MS = 120_000;
|
|
4
|
+
|
|
5
|
+
export async function generateWithModel(fullModel: string, task: string): Promise<string> {
|
|
6
|
+
const parts = fullModel.split("/");
|
|
7
|
+
if (parts.length < 2) {
|
|
8
|
+
throw new Error(`Invalid model format: ${fullModel}. Expected "provider/model-id"`);
|
|
9
|
+
}
|
|
10
|
+
const provider = parts[0];
|
|
11
|
+
const modelId = parts.slice(1).join("/");
|
|
12
|
+
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const args = ["--provider", provider, "--model", modelId, "--no-tools", "--no-session", "-p", task];
|
|
15
|
+
|
|
16
|
+
const proc = spawn("pi", args, {
|
|
17
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
18
|
+
env: process.env,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
let stdout = "";
|
|
22
|
+
let stderr = "";
|
|
23
|
+
|
|
24
|
+
proc.stdout.on("data", (chunk) => {
|
|
25
|
+
stdout += chunk.toString();
|
|
26
|
+
});
|
|
27
|
+
proc.stderr.on("data", (chunk) => {
|
|
28
|
+
stderr += chunk.toString();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const timer = setTimeout(() => {
|
|
32
|
+
proc.kill("SIGTERM");
|
|
33
|
+
reject(new Error("Generation timed out after 2 minutes"));
|
|
34
|
+
}, TIMEOUT_MS);
|
|
35
|
+
|
|
36
|
+
proc.on("error", (err) => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
reject(err);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
proc.on("close", (code) => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
if (code === 0) {
|
|
44
|
+
resolve(stdout.trim());
|
|
45
|
+
} else {
|
|
46
|
+
reject(new Error(`pi exited with code ${code}: ${stderr.slice(0, 500)}`));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-design-deck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"deck-schema.ts",
|
|
14
14
|
"deck-server.ts",
|
|
15
15
|
"generate-prompts.ts",
|
|
16
|
+
"model-runner.ts",
|
|
16
17
|
"server-utils.ts",
|
|
17
18
|
"settings.ts",
|
|
18
19
|
"index.ts"
|
|
@@ -234,13 +234,13 @@ The `add-option` call pushes the new option into the live deck and blocks again
|
|
|
234
234
|
|
|
235
235
|
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
236
|
|
|
237
|
-
When the generate-more result includes a model instruction (e.g. "Generate using model X via
|
|
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 option with that model:
|
|
238
238
|
|
|
239
239
|
```
|
|
240
|
-
|
|
240
|
+
deck_generate({ model: "google/gemini-3.1-pro", task: "Generate a JSON deck option..." })
|
|
241
241
|
```
|
|
242
242
|
|
|
243
|
-
Parse the
|
|
243
|
+
Parse the output as the option JSON and pass it to `add-option`.
|
|
244
244
|
|
|
245
245
|
The default model can also be set in `~/.pi/agent/settings.json`:
|
|
246
246
|
|