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 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 the browser. Each slide shows 2-4 high-fidelity previews — code diffs, architecture diagrams, UI mockups — and you pick one per slide. The agent gets back a clean selection map and moves on to implementation.
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, browser opens
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" }`
@@ -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
  /* ─────────────────────────────────────────────────────────────
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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
- try {
893
- await openUrl(pi, serverHandle.url, settings.browser);
894
- } catch (err) {
895
- cleanupActiveDeck();
896
- const message = err instanceof Error ? err.message : String(err);
897
- throw new Error(`Failed to open browser: ${message}`);
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-design-deck",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Visual design deck for presenting multi-slide options with high-fidelity previews",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",