From e7816fc5ad133684cf5c22432ba740091ef607cc Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 27 Mar 2026 08:37:12 -0600 Subject: [PATCH] feat: add $D gallery command for design history timeline Generates a self-contained HTML page showing all prior design explorations for a project: every variant (approved or not), feedback notes, organized by date (newest first). Images embedded as base64. Handles corrupted approved.json gracefully (skips, still shows the session). Empty state shows "No history yet" with /design-shotgun prompt. Co-Authored-By: Claude Opus 4.6 (1M context) --- design/src/cli.ts | 8 ++ design/src/commands.ts | 5 + design/src/gallery.ts | 251 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 264 insertions(+) create mode 100644 design/src/gallery.ts diff --git a/design/src/cli.ts b/design/src/cli.ts index 1c72b816..481eb29d 100644 --- a/design/src/cli.ts +++ b/design/src/cli.ts @@ -24,6 +24,7 @@ import { diffMockups, verifyAgainstMockup } from "./diff"; import { evolve } from "./evolve"; import { generateDesignToCodePrompt } from "./design-to-code"; import { serve } from "./serve"; +import { gallery } from "./gallery"; function parseArgs(argv: string[]): { command: string; flags: Record } { const args = argv.slice(2); // skip bun/node and script path @@ -237,6 +238,13 @@ async function main(): Promise { }); break; + case "gallery": + gallery({ + designsDir: flags["designs-dir"] as string, + output: (flags.output as string) || "/tmp/gstack-design-gallery.html", + }); + break; + case "serve": await serve({ html: flags.html as string, diff --git a/design/src/commands.ts b/design/src/commands.ts index 70c174e3..c8331e97 100644 --- a/design/src/commands.ts +++ b/design/src/commands.ts @@ -64,6 +64,11 @@ export const COMMANDS = new Map b.date.localeCompare(a.date)); + + const sessionCards = sessions.map(session => { + const variantImgs = session.variants.map((vPath, i) => { + try { + const imgData = fs.readFileSync(vPath).toString("base64"); + const ext = path.extname(vPath).slice(1) || "png"; + const label = path.basename(vPath, `.${ext}`).replace("variant-", ""); + const isApproved = session.approved?.approved_variant === label; + return ` + `; + } catch { + return ""; // Skip unreadable images + } + }).filter(Boolean).join("\n"); + + const feedbackNote = session.approved?.feedback + ? `` + : ""; + + return ` + `; + }).join("\n"); + + return ` + + + + +Design History + + + +
+

Design History

+
${sessions.length} exploration${sessions.length === 1 ? "" : "s"}
+
+ + +`; +} + +function generateEmptyGallery(): string { + return ` + + + + +Design History + + + +
+

No design history yet

+

Run /design-shotgun to start exploring design directions.

+
+ +`; +} + +function escapeHtml(str: string): string { + return str.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); +} + +/** + * Gallery command: generate HTML timeline from design explorations. + */ +export function gallery(options: GalleryOptions): void { + const html = generateGalleryHtml(options.designsDir); + const outputDir = path.dirname(options.output); + fs.mkdirSync(outputDir, { recursive: true }); + fs.writeFileSync(options.output, html); + console.log(JSON.stringify({ outputPath: options.output })); +}