// Dev-only review fixture. Imported lazily behind `import.meta.env.DEV` from // main.ts (`?mock`), so it never ships in a production bundle. It pushes a // synthetic Snapshot through the store's normal event path. import type { CatalogStore } from "./events.js"; import type { ActivityState, Session } from "./types.js"; // Rotate inferred activity across the fixture so the debug HUD (#20) shows all // three states. Keyed off the id's last hex digit for stable variety. const ACTIVITIES: ActivityState[] = ["Active", "Idle", "AwaitingInput"]; function sess( id: string, command: string, status: "Running" | "Dead", cwd: string, name?: string, blocked_work?: Session["blocked_work"], ): Session { const seed = parseInt(id.slice(-1), 16) || 0; return { id, cwd, command, env: {}, metadata: name ? { name } : {}, status, created_at: "2026-06-15T12:00:00Z", // Inferred fields (#18) so the HUD has real data to render in review. foreground: command.split(" ")[0], activity: ACTIVITIES[seed % ACTIVITIES.length], last_activity_at: new Date(Date.now() - seed * 11_000).toISOString(), // The session tree nests by current_cwd; default it to the spawn dir. current_cwd: cwd, blocked_work, }; } /** Stub the spawn-form backend endpoints (#26/#59) so `?mock&spawn` renders the * full flow — presets, projects, gitroot — with no daemon running. Dev-only; * delegates every other request to the real `fetch`. */ function stubSpawnApi(): void { const real = globalThis.fetch.bind(globalThis); const json = (body: unknown) => new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" }, }); globalThis.fetch = (input: RequestInfo | URL, init?: RequestInit) => { const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; const path = url.startsWith("http") ? new URL(url).pathname + new URL(url).search : url; if (path.startsWith("/api/info")) { return Promise.resolve( json({ hostname: "thinkpad.lan", version: "0.1.0", home_dir: "/home/dev", presets: [ { id: "claude", label: "Claude", command: "claude" }, { id: "opencode", label: "opencode", command: "opencode" }, { id: "zsh", label: "Shell", command: "zsh" }, { id: "custom", label: "Custom…", command: "" }, ], }), ); } if (path.includes("/projects")) { const now = Date.now(); const iso = (daysAgo: number) => new Date(now - daysAgo * 86_400_000).toISOString(); return Promise.resolve( json([ { path: "/home/dev/git/maestro", name: "maestro", is_worktree: false, last_activity: iso(0), last_command: "claude", }, { path: "/home/dev/worktrees/maestro-59", name: "maestro-59", is_worktree: true, base_repo: "/home/dev/git/maestro", last_activity: iso(1), last_command: "opencode", }, { path: "/home/dev/git/dotfiles", name: "dotfiles", is_worktree: false, last_activity: iso(30), last_command: "zsh", }, { path: "/home/dev/work/acme/api", name: "api", is_worktree: false, }, ]), ); } if (path.includes("/gitroot")) { const root = path.includes("maestro") || path.includes("dotfiles") ? { root: "/home/dev/git/maestro" } : {}; return Promise.resolve(json(root)); } return real(input, init); }; } export function seedMock(store: CatalogStore): void { const sessions: Session[] = [ // ~/git/maestro/web/* — deep dir chain; "~/git/maestro" collapses to one node. sess("a1b2c3d4-0000-0000-0000-000000000001", "claude", "Running", "/home/dev/git/maestro/web", "redesign", { uncommitted_files: 4, untracked_files: 2, unpushed_commits: 1, insertions: 312, deletions: 47, }), sess("a1b2c3d4-0000-0000-0000-000000000002", "vite dev", "Running", "/home/dev/git/maestro/web", "dev-server"), sess("a1b2c3d4-0000-0000-0000-000000000003", "bash", "Dead", "/home/dev/git/maestro/web", "scratch"), sess("a1b2c3d4-0000-0000-0000-000000000004", "codex --resume", "Running", "/home/dev/git/maestro/web/tests", "e2e-fixes", { uncommitted_files: 1, }), // ~/git/maestro/crates/* — sibling subtree under the same repo. sess("a1b2c3d4-0000-0000-0000-000000000005", "claude", "Running", "/home/dev/git/maestro/crates/daemon", "daemon-work"), sess("a1b2c3d4-0000-0000-0000-000000000006", "cargo watch -x test", "Running", "/home/dev/git/maestro/crates/daemon", "test-loop"), sess("a1b2c3d4-0000-0000-0000-000000000007", "cargo run -- daemon", "Running", "/home/dev/git/maestro/crates/daemon", "daemon"), // ~/work/acme/* — a separate top-level dir on the same host. sess("a1b2c3d4-0000-0000-0000-000000000008", "gemini", "Running", "/home/dev/work/acme/api", "billing-api", { untracked_files: 3, }), sess("a1b2c3d4-0000-0000-0000-000000000009", "pnpm dev", "Dead", "/home/dev/work/acme/web", "dashboard"), // ~/git/dotfiles — single session, fully collapsed chain. sess("a1b2c3d4-0000-0000-0000-000000000010", "opencode", "Running", "/home/dev/git/dotfiles", "dotfiles"), sess("a1b2c3d4-0000-0000-0000-000000000011", "btop", "Running", "/home/dev/git/infra", "monitor"), // outside any home dir → shown verbatim (no ~ collapse). sess("a1b2c3d4-0000-0000-0000-000000000012", "ssh gateway", "Dead", "/etc"), // home root — a leaf directly under ~ (name from command). sess("a1b2c3d4-0000-0000-0000-000000000013", "htop", "Running", "/home/dev"), ]; // The store applies CatalogEvents internally; feed a Snapshot the same way // the WS would. `apply` is private, so reach it through an index cast. (store as unknown as { apply: (e: unknown) => void }).apply({ type: "Snapshot", sessions, }); // `?mock&spawn` auto-opens a draft pane for visual review, with the spawn // backend endpoints stubbed so presets/projects/gitroot all render. if (location.search.includes("spawn")) { stubSpawnApi(); document.dispatchEvent(new CustomEvent("catalog-open-spawn")); } }