pi-design-deck 0.3.2 → 0.3.4
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 +3 -2
- package/form/css/controls.css +10 -0
- package/form/css/layout.css +3 -3
- package/index.ts +107 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# Design Deck
|
|
6
6
|
|
|
7
|
-
A tool for [Pi coding agent](https://github.com/badlogic/pi-mono/) that presents multi-slide visual decision decks in
|
|
7
|
+
A tool for [Pi coding agent](https://github.com/badlogic/pi-mono/) that presents multi-slide visual decision decks. On macOS, uses [Glimpse](https://github.com/nicobailon/glimpseui) to render in a native WKWebView window; falls back to a browser tab on other platforms. 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
9
|
<img width="1340" alt="Design Deck screenshot" src="https://github.com/user-attachments/assets/20864ac6-9223-4e2e-ba3c-db3eaae0abd8" />
|
|
10
10
|
|
|
@@ -41,6 +41,7 @@ Restart pi to load the extension and the bundled `design-deck` skill.
|
|
|
41
41
|
|
|
42
42
|
**Requirements:**
|
|
43
43
|
- pi-agent v0.35.0 or later (extensions API)
|
|
44
|
+
- For native macOS window: `pi install npm:glimpseui` (optional, falls back to browser if not installed)
|
|
44
45
|
|
|
45
46
|
https://github.com/user-attachments/assets/aff1bac6-8bc2-461a-8828-f588ce655f7f
|
|
46
47
|
|
|
@@ -110,7 +111,7 @@ The browser opens, the user picks "JWT + Refresh Tokens", and the agent receives
|
|
|
110
111
|
|
|
111
112
|
## How It Works
|
|
112
113
|
|
|
113
|
-
1. Agent calls `design_deck()` with slides JSON — local HTTP server starts,
|
|
114
|
+
1. Agent calls `design_deck()` with slides JSON — local HTTP server starts, opens in Glimpse on macOS when available, otherwise opens in the browser
|
|
114
115
|
2. User navigates slides, picks one option per slide
|
|
115
116
|
3. Optionally clicks "Generate N options" — agent generates and pushes via `add-options`, deck stays open
|
|
116
117
|
4. User submits — selections returned to agent as `{ slideId: "selected label" }`
|
package/form/css/controls.css
CHANGED
|
@@ -579,6 +579,10 @@
|
|
|
579
579
|
.options.cols-3 { grid-template-columns: repeat(2, 1fr); }
|
|
580
580
|
}
|
|
581
581
|
|
|
582
|
+
@media (max-width: 1100px) {
|
|
583
|
+
.deck-key { display: none; }
|
|
584
|
+
}
|
|
585
|
+
|
|
582
586
|
@media (max-width: 900px) {
|
|
583
587
|
.options.cols-3 { grid-template-columns: 1fr; }
|
|
584
588
|
.options.cols-2 { grid-template-columns: 1fr; }
|
|
@@ -586,6 +590,12 @@
|
|
|
586
590
|
.summary-grid { grid-template-columns: 1fr; }
|
|
587
591
|
.pv-layout-sidebar { grid-template-columns: 120px 1fr; }
|
|
588
592
|
.deck-save-status { display: none; }
|
|
593
|
+
.deck-footer { padding: 10px 16px; }
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
@media (max-width: 600px) {
|
|
597
|
+
.layout-toggle { display: none; }
|
|
598
|
+
.deck-header { padding: 0 16px; }
|
|
589
599
|
}
|
|
590
600
|
|
|
591
601
|
/* ─────────────────────────────────────────────────────────────
|
package/form/css/layout.css
CHANGED
|
@@ -367,12 +367,12 @@ body {
|
|
|
367
367
|
|
|
368
368
|
.deck-footer {
|
|
369
369
|
display: flex; align-items: center; justify-content: space-between;
|
|
370
|
-
padding: 12px 32px;
|
|
370
|
+
gap: 12px; padding: 12px 32px;
|
|
371
371
|
border-top: 1px solid rgba(var(--dk-ink),0.12);
|
|
372
372
|
flex-shrink: 0;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
.deck-keys { display: flex; gap: 14px; }
|
|
375
|
+
.deck-keys { display: flex; align-items: center; gap: 14px; flex: 1; min-width: 0; justify-content: center; overflow: hidden; }
|
|
376
376
|
.deck-key { display: flex; align-items: center; gap: 4px; font-size: 11px; color: var(--dk-text-hint); }
|
|
377
377
|
.deck-key kbd {
|
|
378
378
|
display: inline-flex; align-items: center; justify-content: center;
|
|
@@ -385,7 +385,7 @@ body {
|
|
|
385
385
|
.btn-nav {
|
|
386
386
|
padding: 8px 20px; border-radius: 6px; font-size: 12px; font-weight: 600;
|
|
387
387
|
border: 1px solid rgba(var(--dk-ink),0.14); background: var(--dk-elevated);
|
|
388
|
-
color: var(--dk-text); transition: all 0.15s;
|
|
388
|
+
color: var(--dk-text); transition: all 0.15s; flex-shrink: 0; white-space: nowrap;
|
|
389
389
|
}
|
|
390
390
|
.btn-nav:hover { background: var(--dk-elevated-hover); color: var(--dk-text-label); border-color: rgba(var(--dk-ink),0.12); }
|
|
391
391
|
.btn-nav:disabled { opacity: 0.3; cursor: not-allowed; }
|
package/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ import * as path from "node:path";
|
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
8
10
|
import { loadSettings } from "./settings.js";
|
|
9
11
|
import { getDefaultSnapshotDir, startDeckServer, type DeckServerHandle, type ModelInfo } from "./deck-server.js";
|
|
10
12
|
import { deriveDeckStatusFromFolderName, isDeckOption, validateDeckConfig, validateSavedDeck, type SavedDeckData, type SavedDeckStatus } from "./deck-schema.js";
|
|
@@ -12,6 +14,15 @@ import { buildGenerateMoreResult, buildRegenerateResult } from "./generate-promp
|
|
|
12
14
|
import { generateWithModel } from "./model-runner.js";
|
|
13
15
|
import { buildStandaloneDeckHtml } from "./export-html.js";
|
|
14
16
|
|
|
17
|
+
interface GlimpseWindow {
|
|
18
|
+
on(event: "closed", handler: () => void): void;
|
|
19
|
+
on(event: "error", handler: (err: Error) => void): void;
|
|
20
|
+
close(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let glimpseOpen: ((html: string, opts: Record<string, unknown>) => GlimpseWindow) | null | undefined;
|
|
24
|
+
let activeGlimpseWin: GlimpseWindow | null = null;
|
|
25
|
+
|
|
15
26
|
async function openUrl(pi: ExtensionAPI, url: string, browser?: string): Promise<void> {
|
|
16
27
|
const platform = os.platform();
|
|
17
28
|
let result;
|
|
@@ -144,6 +155,68 @@ function resolveSnapshotDir(snapshotDir?: string): string {
|
|
|
144
155
|
return snapshotDir ? expandHome(snapshotDir) : getDefaultSnapshotDir();
|
|
145
156
|
}
|
|
146
157
|
|
|
158
|
+
function escapeHtml(str: string): string {
|
|
159
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function findGlimpseMjs(): string | null {
|
|
163
|
+
// Local node_modules
|
|
164
|
+
try {
|
|
165
|
+
const req = createRequire(import.meta.url);
|
|
166
|
+
return req.resolve("glimpseui");
|
|
167
|
+
} catch {}
|
|
168
|
+
// Global npm install
|
|
169
|
+
try {
|
|
170
|
+
const globalRoot = execFileSync("npm", ["root", "-g"], { encoding: "utf-8" }).trim();
|
|
171
|
+
const entry = path.join(globalRoot, "glimpseui", "src", "glimpse.mjs");
|
|
172
|
+
if (fs.existsSync(entry)) return entry;
|
|
173
|
+
} catch {}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function getGlimpseOpen() {
|
|
178
|
+
if (glimpseOpen !== undefined) return glimpseOpen;
|
|
179
|
+
const resolved = findGlimpseMjs();
|
|
180
|
+
if (resolved) {
|
|
181
|
+
try {
|
|
182
|
+
glimpseOpen = (await import(resolved)).open;
|
|
183
|
+
return glimpseOpen;
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
186
|
+
glimpseOpen = null;
|
|
187
|
+
return glimpseOpen;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function openInGlimpse(
|
|
191
|
+
open: (html: string, opts: Record<string, unknown>) => GlimpseWindow,
|
|
192
|
+
url: string,
|
|
193
|
+
title?: string,
|
|
194
|
+
): GlimpseWindow {
|
|
195
|
+
const safeTitle = escapeHtml(title || "Design Deck");
|
|
196
|
+
const shellHtml = `<!DOCTYPE html>
|
|
197
|
+
<html>
|
|
198
|
+
<head><meta charset="UTF-8"><title>${safeTitle}</title></head>
|
|
199
|
+
<body style="margin:0; background:#18181e;">
|
|
200
|
+
<script>window.location.replace(${JSON.stringify(url)});</script>
|
|
201
|
+
</body>
|
|
202
|
+
</html>`;
|
|
203
|
+
|
|
204
|
+
return open(shellHtml, {
|
|
205
|
+
width: 1100,
|
|
206
|
+
height: 800,
|
|
207
|
+
title: title || "Design Deck",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function closeActiveGlimpseWindow(): void {
|
|
212
|
+
if (!activeGlimpseWin) return;
|
|
213
|
+
const win = activeGlimpseWin;
|
|
214
|
+
activeGlimpseWin = null;
|
|
215
|
+
try {
|
|
216
|
+
win.close();
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
|
|
147
220
|
function listSavedDecks(snapshotDir: string): { decks: SavedDeckListItem[]; warnings: string[] } {
|
|
148
221
|
if (!fs.existsSync(snapshotDir)) {
|
|
149
222
|
return { decks: [], warnings: [] };
|
|
@@ -253,6 +326,7 @@ function cleanupActiveDeck(reason?: string): void {
|
|
|
253
326
|
restoreDeckThinking();
|
|
254
327
|
restoreDeckThinking = null;
|
|
255
328
|
}
|
|
329
|
+
closeActiveGlimpseWindow();
|
|
256
330
|
if (!activeDeckServer) return;
|
|
257
331
|
try {
|
|
258
332
|
activeDeckServer.handle.close(reason);
|
|
@@ -332,6 +406,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
332
406
|
label: "Design Deck",
|
|
333
407
|
description:
|
|
334
408
|
"Present a multi-slide design deck with visual options for decisions. " +
|
|
409
|
+
"On macOS, opens in a native window (Glimpse); falls back to a browser tab elsewhere. " +
|
|
335
410
|
"Slides JSON: { title?, slides: [{ id, title, context?, columns?, options }] }. " +
|
|
336
411
|
"When the user requests more options, tool returns generate-more instructions — " +
|
|
337
412
|
'call design_deck with action:"add-options" to push all new options at once. ' +
|
|
@@ -341,6 +416,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
341
416
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
342
417
|
const p = params as Record<string, unknown>;
|
|
343
418
|
|
|
419
|
+
if (!p.action && typeof p.slides === "string") {
|
|
420
|
+
try {
|
|
421
|
+
const parsed = JSON.parse(p.slides);
|
|
422
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.action === "string") {
|
|
423
|
+
Object.assign(p, parsed);
|
|
424
|
+
delete p.slides;
|
|
425
|
+
}
|
|
426
|
+
} catch {}
|
|
427
|
+
}
|
|
428
|
+
|
|
344
429
|
if (!ctx.hasUI && p.action !== "list" && p.action !== "export") {
|
|
345
430
|
throw new Error(
|
|
346
431
|
"design_deck requires interactive mode with browser support. " +
|
|
@@ -889,12 +974,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
889
974
|
});
|
|
890
975
|
}
|
|
891
976
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
977
|
+
const glimpseOpenFn = os.platform() === "darwin" ? await getGlimpseOpen() : null;
|
|
978
|
+
if (glimpseOpenFn) {
|
|
979
|
+
try {
|
|
980
|
+
const thisWindow = openInGlimpse(glimpseOpenFn, serverHandle.url, config.title || "Design Deck");
|
|
981
|
+
activeGlimpseWin = thisWindow;
|
|
982
|
+
thisWindow.on("error", () => {});
|
|
983
|
+
thisWindow.on("closed", () => {
|
|
984
|
+
if (activeGlimpseWin !== thisWindow) return;
|
|
985
|
+
activeGlimpseWin = null;
|
|
986
|
+
handleCancel("user");
|
|
987
|
+
});
|
|
988
|
+
} catch {}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (!activeGlimpseWin) {
|
|
992
|
+
try {
|
|
993
|
+
await openUrl(pi, serverHandle.url, settings.browser);
|
|
994
|
+
} catch (err) {
|
|
995
|
+
cleanupActiveDeck();
|
|
996
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
997
|
+
throw new Error(`Failed to open browser: ${message}`);
|
|
998
|
+
}
|
|
898
999
|
}
|
|
899
1000
|
|
|
900
1001
|
return blockOnDeck();
|