The packaged app resolved whatever codeburn was on PATH — for real
users the published npm release, which predates every JSON surface the
app calls. The app now ships its own CLI copy under resources/cli
(staged production node_modules tree; tsup output is not
self-contained) and spawns it with Electron's own binary via
ELECTRON_RUN_AS_NODE. No install prerequisite remains.
- resolution order: CODEBURN_BIN, dev repo CLI, bundled, persisted
path, PATH search
- launch.js shim strips the extra argv element commander mis-slices
under packaged Electron-as-node (process.versions.electron is set),
which reproduced the 'too many arguments' class of error
- afterPack hook copies the staged tree (extraResources runs
node_modules through the production-dep filter and ships it empty);
lands before signing, signature verified intact
- packaging always restages from src via root build:cli (tsup only,
no network); ~12MB compressed per artifact
Verified on the built app: Electron-as-node CLI emits current JSON
(providerDetails, currency), and a minimal-PATH GUI launch spawns the
bundled CLI with zero external dependencies. 290/290.
GUI-launched apps inherit a minimal PATH without the user's node
install, so spawning the codeburn npm shim (#!/usr/bin/env node) failed
with 'env: node: No such file or directory' in packaged builds. Spawn
now augments PATH with the resolved binary's own directory (node sits
beside the shim in nvm, Homebrew, and npm-prefix layouts) plus the
resolver's search dirs. Dev never hit this because the terminal PATH
was inherited.
Root causes (verified live): the Claude keychain branch was never
enabled (allowClaudeKeychain never passed), security -w returns
hex-encoded JSON the parser rejected, and Codex had no keychain
discovery at all.
- shared readKeychainPassword: Apple-signed /usr/bin/security, 90s
window so the user can answer the macOS dialog, hex decode,
notFound vs accessDenied classification; secrets never logged
- Claude: keychain fallback active (gated to explicit Refresh so
launch never ambushes with a dialog); connection retained across
keychain-less background polls
- Codex sources in order: the menubar's own cached OAuth (read-only,
401 = one re-read, never rotate), legacy ~/.codex/auth.json
(writable), com.openai.codex plaintext if usable; Safe Storage
never decrypted
- new accessDenied state (amber, 'locked') + ConnectAffordance on
Plans cards and Settings rows: login command, keychain-Allow note,
force Refresh
Live verification: Claude connected with its real tier and windows;
Codex correctly accessDenied until the keychain dialog is allowed.
273/273 (26 files), typecheck + build green.
- Models gains an Audit lens: raw provider token fields vs displayed
totals and cost derivation per model, est badge where pricing is
missing or recomputed cost diverges from attributed
- Settings Pricing pane: list/add/remove price overrides (USD per 1M),
inline confirm on remove, only provided rates sent; rates validated
finite and strictly positive on both sides of the bridge
- four new IPC channels with the established validation pattern
Root 1617/1617, app 235/235, build green.
- TopBar picker (shown only when >1 Claude config dir exists) threads
--claude-config-source through getOverview; synthetic 'All Claude
configs' entry returns to the aggregate default
- config id validation accepts the real <kind>:<16hex> id shape while
still rejecting flag-shaped input (leading char anchored)
- both incompatibility directions handled: selecting a config resets a
non-Claude provider filter, and picking a non-Claude provider clears
the config scope, so the CLI rejection is unreachable
- choice persisted (localStorage); active config shown in the scope
line and read-only in Settings/General
Verified live against 2 real config dirs. 205/205, build green.
- will-navigate/window-open denied; openExternal restricted to http(s);
production CSP drops 'unsafe-inline' scripts (dev-only Vite plugin
re-adds it for the Fast Refresh preamble)
- single-instance lock; before-quit reaps in-flight CLI children; 16MB
output cap rejects with CliError 'too-large'
- read-only CLI spawns coalesce: concurrent identical calls share one
child, a 5s result cache absorbs same-cadence pollers (six sections
polled getOverview independently); actions bypass and flush the cache
on completion so post-action refetches are fresh
- every renderer-supplied argv string validated (period/provider/range/
currency allowlists, no flag-shaped tokens, absolute export path) with
new CliError kind 'bad-args'; envelope + action stderr routed through
sanitizeError
- quota force-refresh: manual refresh (refreshToken) passes force which
invalidates QuotaService's cache; steady polls do not
- POSIX credential mode check gated off on win32
App suite 190/190, build green (production index.html verified strict).
- formatUsd applies the payload currency {code,symbol,rate} once at
display; formatConverted (symbol only) for CLI-preconverted plan
values so nothing converts twice
- provider picker built from providerDetails: label shown, internal id
sent as --provider (fixes filters for providers whose display name
differs, e.g. Grok Build); falls back to map keys on older CLIs
- getYield threads the active provider through preload/main/sections
App suite 170/170, root 1615/1615.
Port the menu bar's OAuth quota fetch to the Electron main:
- app/electron/quota/{claude,codex,index,security}.ts reads the local Claude/
Codex credentials, hits the usage APIs, normalizes to QuotaProvider (5h/weekly
windows, auto-detected tier, resets-at) with 429 backoff + single-flight
- getQuota IPC bridge (main + preload + renderer types); needs a full relaunch
Security: tokens are never returned or logged (redact Bearer/sk-ant/sk-/JWT,
strip NUL, truncate 240); credential reads refuse symlinks (O_NOFOLLOW/lstat) and
insecure modes; Anthropic refresh is never POSTed (single-use, shared with CLI).
typecheck clean; 147/147 tests pass; Electron build passes.
- add a shell.openExternal bridge (main ipc handler + preload + renderer type)
so links open in the default browser (requires a full relaunch)
- wire the sidebar social icons to real URLs and add LinkedIn
- AboutModal: flame + wordmark, version, tagline, the five social links, and a
"Check for updates" link to the GitHub releases page
typecheck clean; 128/128 tests pass.
- Plans tab: add/remove budget presets via `plan set`/`plan reset`, lists
configured plans, links to the full Plans screen
- Export tab: format + provider + native folder picker (electron dialog)
→ runs `codeburn export`, reports the result
- Devices: real Remove (`devices rm`) + Refresh; visibility is read-only
from `share status`; pairing shows an honest "pair from terminal" note
instead of a dead Approve button; combine row is read-only status
- Apply the persisted theme at app startup (not only when General mounts)
- Settings gets onNavigate for the "Open Plans" link
Only one-shot, non-interactive CLI commands are wired as buttons; the
interactive share daemon and pairing flow stay honestly read-only.
Settings was a dead shell (rail didn't switch, all buttons disabled).
Now the rail navigates and four tabs are wired to the real CLI:
- action bridge: spawnCliAction + runAction (text output, not JSON) with
getAliases/getProxyPaths reads and setCurrency/resetCurrency/addAlias/
removeAlias actions
- General: theme switcher (System/Light/Dark, persisted) + live currency
(reads status, changes via `currency`) + default-period preference
- Providers: real detected-tools list with spend
- Model aliases: full add/remove against real config
- Privacy: honest local-only info
Devices content preserved as-is; Plans/Export are placeholders for Part 2.
The dev-mode repo dist/cli.js fallback ran AFTER the persisted-path file,
so a stale globally-installed codeburn (e.g. a Homebrew build lacking the
new sessions/compare commands) shadowed the repo build and every Sessions
call errored. Move the Vite-dev fallback ahead of the persisted-path
lookup; CODEBURN_BIN still overrides, and production (no VITE_DEV_SERVER_URL)
is unchanged.
Adds getSessions / getCompareModels / getCompare IPC channels (argv for
the new CLI JSON emitters), mirrors SessionRow + compare-stats types into
the renderer, and — in Vite dev — resolves the repo's own dist/cli.js so
newly-added commands work without setting CODEBURN_BIN.
- Period seg now uses the 5 real CLI periods (Today/7D/30D/Month/6M);
6M maps to 'all' (matches the menubar).
- Provider control is a real dropdown listing only DETECTED providers
(accumulated from current.providers so it never shrinks) + All.
- New RangeCalendar: month grid with click-and-drag range selection
(+ two-click fallback, future-date disabling) behind a calendar button;
commits a custom {from,to} threaded to Overview/Spend/Models/Optimize
via new --from/--to CLI args (Plans/Settings stay period-only).
- typecheck clean, 87 vitest pass.
macOS uses titleBarStyle hiddenInset so the traffic lights float over
the sidebar (Linear/Hermes-style), with the sidebar top inset to clear
them and the top chrome marked -webkit-app-region: drag (interactive
controls stay no-drag). Windows/Linux keep their native frame + controls.
Exposes process.platform through the bridge and tags <html data-platform>
so CSS adapts per OS. typecheck + 82 tests green.
- Daily-spend chart always shows a contiguous >=30 calendar-day trend
(backfilling gaps with zero bars) instead of collapsing to the period
selector (e.g. a single bar on Today).
- Real app shell: viewport-height window with an internally scrolling
.body (was clipped/unscrollable).
- Drop the wireframe's fake traffic-light dots; rely on the real OS
window controls. Window background follows the system theme.
- Update Overview test to assert the 30-day chart window.
Install an app menu without the CmdOrCtrl+R reload accelerator so ⌘R reaches the renderer for
in-place refresh (keeps Edit/Window roles + dev DevTools). Model labels are now family-level
("Opus"/"Sonnet"/"Haiku"/"GPT / Codex") and the Sankey shows the real model id instead of the
wireframe's sample version names. Optimize retains last-good yield across 30s revalidation (no
flicker to "—"). Removed hints for unimplemented ⌘K/⌘E/esc; fixed stale TopBar comment.
Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
Settings rail (Devices active) with This device (getIdentity), Discovered nearby
(new getDevicesScan bridge → `codeburn devices scan --format json`), and Paired
(getDevices perDevice). Pairing/approve/pull/visibility/combine are M1 visual
affordances (mutations = M2); share status is process-local so not shown as
authoritative. Settings uses a title-only bar (no period/provider).
Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
Review of T0 (Electron scaffold) approved with one Important finding + cheap
Minors. All fixes scoped to app/.
- usePolled: replace per-call cancel closure with a generation/epoch counter so
an orphaned in-flight fetch (from refresh() or an interval tick, discarded
cancel handle) can't resolve after a newer fetch and clobber fresh data. New
TDD race test (slow deps-A resolves after fast deps-B; keeps B).
- cli: nvm resolution now scans version dirs descending and takes the first
whose bin actually contains codeburn (was lexicographic max, unverified),
matching CodeburnCLI.swift. Export nodeManagerDirs + hermetic nvm test.
- index.html: tighten CSP connect-src ws: -> ws://localhost:5173.
- preload: import type { Envelope } from main instead of redeclaring it.
- main.test: table-driven channel->argv assertion over all 9 codeburn:* channels
plus a keys check and a non-spawning cliStatus case.
- main.ts: 1200x820 BrowserWindow (contextIsolation:true,
nodeIntegration:false, sandbox:true), loads the Vite dev URL or the
built index.html, registers one ipcMain.handle per CodeburnBridge
channel, and starts a 30s tick that emits `codeburn:refresh`.
createBridgeHandlers() is a pure, injectable channel→argv map so the
spawn→IPC wiring is unit-testable without a GUI (main.test.ts).
- preload.ts: exposes window.codeburn (CodeburnBridge, exactly) and
window.codeburnEvents.onRefresh via contextBridge. Handlers return an
{ok,value|error} envelope so the structured CliError kind survives the
world boundary.
- types.ts: mirrors MenubarPayload/DailyHistoryEntry/DailyModelBreakdown
and the other CLI payload types verbatim, plus the exact CodeburnBridge
interface and Period union.
- ipc.ts: typed 1:1 wrapper + normalizeCliError.