pi-design-deck 0.3.1 → 0.3.3

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
@@ -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
+ - Optional on macOS: `glimpseui` for native window launch (`npm install -g glimpseui`)
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" }`
@@ -221,9 +221,8 @@
221
221
  /* Spinner inside skeleton cards */
222
222
  .option-skeleton .spinner {
223
223
  position: absolute;
224
- top: 50%;
225
- left: 50%;
226
- transform: translate(-50%, -50%);
224
+ inset: 0;
225
+ margin: auto;
227
226
  z-index: 2;
228
227
  }
229
228
 
@@ -580,6 +579,10 @@
580
579
  .options.cols-3 { grid-template-columns: repeat(2, 1fr); }
581
580
  }
582
581
 
582
+ @media (max-width: 1100px) {
583
+ .deck-key { display: none; }
584
+ }
585
+
583
586
  @media (max-width: 900px) {
584
587
  .options.cols-3 { grid-template-columns: 1fr; }
585
588
  .options.cols-2 { grid-template-columns: 1fr; }
@@ -587,6 +590,12 @@
587
590
  .summary-grid { grid-template-columns: 1fr; }
588
591
  .pv-layout-sidebar { grid-template-columns: 120px 1fr; }
589
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; }
590
599
  }
591
600
 
592
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; }
@@ -285,8 +285,8 @@ function initLayoutToggle() {
285
285
  const btn = event.target.closest(".layout-btn");
286
286
  if (!btn) return;
287
287
  const cols = btn.dataset.cols;
288
- const current = getStoredLayout();
289
- if (cols === current) {
288
+ const stored = getStoredLayout();
289
+ if (cols === stored) {
290
290
  // Clicking active button toggles back to auto
291
291
  setStoredLayout(null);
292
292
  applyLayout(null);
@@ -403,10 +403,10 @@ function connectEvents() {
403
403
  // Reset timeout for next option
404
404
  if (pending.timeoutId) clearTimeout(pending.timeoutId);
405
405
  pending.timeoutId = setTimeout(() => {
406
- const current = pendingGenerate.get(payload.slideId);
407
- if (!current || current.isRegen) return;
408
- const received = current.receivedCount || 0;
409
- const expected = current.expectedCount || 1;
406
+ const entry = pendingGenerate.get(payload.slideId);
407
+ if (!entry || entry.isRegen) return;
408
+ const received = entry.receivedCount || 0;
409
+ const expected = entry.expectedCount || 1;
410
410
  restoreGenerateButton(payload.slideId);
411
411
  if (received < expected) {
412
412
  showSaveToast(`Generated ${received} of ${expected} options`, true);
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. ' +
@@ -889,12 +964,28 @@ export default function (pi: ExtensionAPI) {
889
964
  });
890
965
  }
891
966
 
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}`);
967
+ const glimpseOpenFn = os.platform() === "darwin" ? await getGlimpseOpen() : null;
968
+ if (glimpseOpenFn) {
969
+ try {
970
+ const thisWindow = openInGlimpse(glimpseOpenFn, serverHandle.url, config.title || "Design Deck");
971
+ activeGlimpseWin = thisWindow;
972
+ thisWindow.on("error", () => {});
973
+ thisWindow.on("closed", () => {
974
+ if (activeGlimpseWin !== thisWindow) return;
975
+ activeGlimpseWin = null;
976
+ handleCancel("user");
977
+ });
978
+ } catch {}
979
+ }
980
+
981
+ if (!activeGlimpseWin) {
982
+ try {
983
+ await openUrl(pi, serverHandle.url, settings.browser);
984
+ } catch (err) {
985
+ cleanupActiveDeck();
986
+ const message = err instanceof Error ? err.message : String(err);
987
+ throw new Error(`Failed to open browser: ${message}`);
988
+ }
898
989
  }
899
990
 
900
991
  return blockOnDeck();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-design-deck",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Visual design deck for presenting multi-slide options with high-fidelity previews",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",