# CodeBurn Desktop Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. Each task runs its own TDD cycle (write failing test → confirm red → implement → confirm green → commit). **Goal:** A standalone Electron desktop app that renders the approved v6 "indigo instrument" wireframes, fed entirely by spawning the `codeburn` CLI for JSON. **Architecture:** New `app/` workspace. Electron **main** process resolves + spawns `codeburn --format json|menubar-json` (plain argv, no shell), decodes typed payloads, pushes to the **renderer** (React 19 + Vite) over a typed `contextBridge` IPC surface, and re-polls on a 30s timer + on demand. All aggregation stays CLI-side; the renderer is a pure view and never fabricates data. Wireframe CSS is ported verbatim as the design system. **Tech Stack:** Electron, TypeScript, React 19, Vite 6, Vitest + React Testing Library. Data via `codeburn` CLI subprocess. No new runtime deps in the CLI beyond the two small JSON emitters (T1). **Design source of truth:** `codeburn-desktop-wireframes.html` (repo root). Each renderer section is a near-verbatim port of the matching `
` block; do not redesign. **Spec:** `docs/design/codeburn-desktop.md`. ## Global Constraints - **Do not modify** `dash/`, `mac/`, `gnome/`, `appstore/`, or the gitignored `desktop/` Tauri experiment. New code lives in `app/` (renderer/main) and `src/` (only the two T1 emitters + their tests). - **Electron security:** `contextIsolation: true`, `nodeIntegration: false`, `sandbox: true` where feasible; renderer gets Node access ONLY through the typed `preload` bridge. No remote content loaded — renderer is local files only. - **Data contract = spawn CLI, decode JSON, poll.** No HTTP server, no daemon, no importing `src/` into the app. Never render fabricated numbers; missing/failed data → the wireframe's honest loading/empty/permission states. - **Binary resolution** mirrors the mac app: resolve `codeburn` via a persisted path file, then `PATH` search (brew/nvm/volta/asdf), then a first-run "locate CLI" state. Reference: `mac/Sources/CodeBurnMenubar/Security/CodeburnCLI.swift`. - **Poll cadence:** 30s timer + immediate refresh on period/provider change and manual refresh; hard per-spawn timeout ~45s; cap concurrent spawns. - **Theme:** dark only this milestone (light is M2). - **Packaging:** none this milestone — app runs via `npm --prefix app run dev` (Vite + Electron). Signing/notarization/`codeburn app` launcher are M2. - **Repo pattern:** `app/` is a standalone package (own `package.json`), like `dash/`. No npm workspaces exist. Hook into root `build` via a `build:app` script only in M2; M1 stays self-contained. - **CLI subcommand flags are `--format json`, not `--json`.** Exact commands are in the Data Contract table below. --- ## Shared Interface Contracts These are defined once and referenced by every task. Implementers must use these exact names/types. ### CLI data contract (what main spawns) | Need | Command (argv) | Payload type | Source | New? | |---|---|---|---|---| | Overview + Spend bars | `codeburn status --format menubar-json --period

[--provider ]` | `MenubarPayload` | `src/menubar-json.ts:80` | existing | | Plans pacing | `codeburn status --format json --period

` | JSON incl. `plan` summaries (`PlanUsage`) | `src/plan-usage.ts:110`, wired `src/main.ts:773` | existing | | Models table | `codeburn models --format json --period

[--provider ] [--by-task]` | `ModelReportRow[]` | `src/models-report.ts:9,551` | existing | | Optimize reverts/abandoned | `codeburn yield --format json --period

` | `YieldJsonReport` | `src/yield.ts:247` | existing | | Spend Sankey (model×project) | `codeburn spend --format flow-json --period

[--provider ]` | `SpendFlow` (T1a) | new — `src/spend-flow.ts` | **T1a** | | Settings/Devices | `codeburn devices --format json --period

` / `codeburn share status --format json` | `CombinedUsage` / `ShareStatus` / `Identity` / scan list | `src/menubar-json.ts:99`, `src/sharing/*` | **T1b** | `

` ∈ `today | week | 30days | month | all` (wireframe "Today/7D/30D/Month/6M/Custom" maps to these; `6M`/`Custom` use `--from/--to`). Types to mirror verbatim into `app/renderer/lib/types.ts` (copy from the cited files — do not invent): - `MenubarPayload`, `DailyHistoryEntry`, `DailyModelBreakdown`, `CombinedUsage`, `DeviceSummary` — `src/menubar-json.ts`. - `ModelReportRow` — `src/models-report.ts:9`. - `YieldJsonReport` — `src/yield.ts:247`. - `PlanUsage` — `src/plan-usage.ts` (only the fields the Plans section renders: `plan, periodStart, periodEnd, spentApiEquivalentUsd, budgetUsd, percentUsed, status, projectedMonthUsd, daysUntilReset`). - `SpendFlow`, `ShareStatus`, `Identity`, scan entry — defined by T1a/T1b below. ### IPC surface (preload `contextBridge`) `app/electron/preload.ts` exposes exactly this on `window.codeburn`; `app/renderer/lib/ipc.ts` wraps it 1:1 with the mirrored types: ```ts export interface CodeburnBridge { getOverview(period: Period, provider: string): Promise // status --format menubar-json getPlans(period: Period): Promise // status --format json (plan summaries) getModels(period: Period, provider: string, byTask: boolean): Promise getYield(period: Period): Promise getSpendFlow(period: Period, provider: string): Promise // T1a getDevices(period: Period): Promise // T1b getShareStatus(): Promise // T1b getIdentity(): Promise // T1b cliStatus(): Promise<{ found: boolean; path: string | null; error?: string }> } export type Period = 'today' | 'week' | '30days' | 'month' | 'all' ``` Each method rejects with a structured `CliError { kind: 'not-found' | 'nonzero' | 'bad-json' | 'timeout'; message: string }` so sections can render the right empty/permission state. Channel names: `codeburn:getOverview`, etc. (one per method). ### Section → data map (for the six section tasks) | Section (wireframe `

`) | Bridge call(s) | |---|---| | 01 Overview | `getOverview` | | 02 Spend | `getOverview` (bars, projects) + `getSpendFlow` (Sankey) | | 03 Optimize | `getOverview` (`optimize` waste) + `getYield` (reverts/abandoned) | | 04 Models | `getModels` | | 05 Plans | `getPlans` | | 06 Settings/Devices | `getIdentity` + `getDevices` + `getShareStatus` | --- ## File Structure ``` app/ ├── package.json # electron, vite, @vitejs/plugin-react, react, react-dom, typescript, vitest, @testing-library/react, jsdom ├── tsconfig.json ├── vite.config.ts # base './', react plugin, build → app/dist/renderer ├── electron/ │ ├── main.ts # BrowserWindow, 30s timer, ipcMain handlers → spawnCli │ ├── cli.ts # resolveCodeburnPath() + spawnCli(args): argv, no shell, timeout │ └── preload.ts # contextBridge → window.codeburn (CodeburnBridge) ├── renderer/ │ ├── index.html │ ├── main.tsx │ ├── App.tsx # sidebar nav + window chrome + section switch (local state) │ ├── styles/indigo.css # ported wireframe CSS (tokens + classes) │ ├── lib/{ipc.ts,types.ts} │ ├── hooks/usePolled.ts # generic 30s poll + loading/error state │ ├── components/ # Window, Sidebar, TopBar, Panel, Stat, CapsuleChart, │ │ # StackedBars, Sankey, ListRow, Track, SegTabs, ProviderPop, Hint │ └── sections/ # Overview, Spend, Optimize, Models, Plans, Settings (+ tests co-located) │ └── test/setup.ts # RTL + jsdom └── (no changes to root package.json this milestone) src/ # CLI — T1 only ├── spend-flow.ts # T1a: computeSpendFlow() + SpendFlow type ├── main.ts # T1a: register `spend` command; T1b: add --format json to devices/share └── sharing/… # T1b: json serializers (thin) tests/ ├── spend-flow.test.ts # T1a └── devices-json.test.ts # T1b ``` --- ## Task 0 — Scaffold + shell (BLOCKS ALL) · **Opus 4.8** **Files:** - Create: `app/package.json`, `app/tsconfig.json`, `app/vite.config.ts`, `app/renderer/index.html`, `app/renderer/main.tsx`, `app/renderer/App.tsx`, `app/renderer/styles/indigo.css`, `app/electron/{main,cli,preload}.ts`, `app/renderer/lib/{ipc,types}.ts`, `app/renderer/hooks/usePolled.ts`, `app/renderer/test/setup.ts` - Create components: `app/renderer/components/{Window,Sidebar,TopBar,Panel,Stat,SegTabs,ProviderPop,Hint}.tsx` - Test: `app/electron/cli.test.ts`, `app/renderer/components/Sidebar.test.tsx` **Interfaces:** - Produces: the full `CodeburnBridge` (stubbed handlers that actually spawn for `getOverview`/`cliStatus`; other methods wired but may throw `not-found` until their emitter lands), `resolveCodeburnPath()`, `spawnCli(args, {timeoutMs}): Promise`, `usePolled(fetcher, deps)`, and all shared `components/` + `styles/indigo.css`. Every later task consumes these. **Steps:** - [ ] **1. Port CSS.** Copy the `