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 +2 -1
- package/form/css/controls.css +12 -3
- package/form/css/layout.css +3 -3
- package/form/js/deck-core.js +2 -2
- package/form/js/deck-session.js +4 -4
- package/index.ts +97 -6
- package/package.json +1 -1
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,
|
|
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
|
@@ -221,9 +221,8 @@
|
|
|
221
221
|
/* Spinner inside skeleton cards */
|
|
222
222
|
.option-skeleton .spinner {
|
|
223
223
|
position: absolute;
|
|
224
|
-
|
|
225
|
-
|
|
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
|
/* ─────────────────────────────────────────────────────────────
|
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/form/js/deck-core.js
CHANGED
|
@@ -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
|
|
289
|
-
if (cols ===
|
|
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);
|
package/form/js/deck-session.js
CHANGED
|
@@ -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
|
|
407
|
-
if (!
|
|
408
|
-
const received =
|
|
409
|
-
const expected =
|
|
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, "&").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. ' +
|
|
@@ -889,12 +964,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
889
964
|
});
|
|
890
965
|
}
|
|
891
966
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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();
|