The KPI strip duplicated the hero (Spend = hero total, Calls/Sessions =
hero subline). Drop the strip; fold the unique KPIs — One-shot, Cache
hit, Saved — into the hero as a secondary row (keeps the a11y label).
- Models table: vertical + horizontal grid lines, sticky header, and a
244px max-height with an internal scrollbar for long model lists.
- Tightened Overview padding/gaps (not fonts) and widened content cap to
1180px so the layout uses the window rather than floating narrow.
.body is a flex column; its children defaulted to flex-shrink:1, so they
compressed to fit (no scroll, 'window gets smaller'). Pin .body > * to
flex-shrink:0 so content keeps its natural height and overflow-y scrolls.
Also cap children to max-width 940px (centered) so wide windows don't
stretch the layout edge-to-edge.
- Hero now reflects the SELECTED period (current.cost + current.label +
calls/sessions), not just today — 30D no longer shows today's number.
- New Models table: per-model Input/Output tokens + Cost + Calls,
aggregated from history.daily[].topModels[] (real per-day data),
rebuilt with a robust bar → left-name → right-numbers layout (fixes
the offset/misaligned columns).
- Daily chart: weekly date ticks (MMM d), Avg/Peak/Yesterday chips, and
the hover tooltip now portals to document.body (position:fixed,
edge-clamped) so it is never clipped.
- Removed the meaningless decorative sparklines. Empty fuel-ring card no
longer reserves a tall empty box.
- typecheck clean, 87 vitest pass.
- 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.
Flame logo + orange CodeBurn wordmark, an outline icon on every nav item
(active icon in accent), and an About + GitHub/Discord/X/YouTube social
footer replacing the price status line (per the approved design). Bundles
the flame asset. typecheck + 82 tests green.
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.
`npm run dev` hung: Vite bound to [::1]:5173 (IPv6) while `wait-on tcp:127.0.0.1:5173`
polled IPv4 — never satisfied, so Electron never launched. Pin Vite host to 127.0.0.1
and point Electron's VITE_DEV_SERVER_URL at 127.0.0.1 too; all three now agree on IPv4.
Caught by the live-run smoke (the one thing the no-display build env couldn't verify).
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 fix: periodEnd is exclusive (next reset), so the cycle end now renders periodEnd-1 day
with totalDays=diff (was Jul 15/31, now Jul 14/30); dates format in local time (not UTC);
test mock re-derived from the real emitter. Minor: en/em dashes, CLI provider order, non-
contradictory near-status copy, +near-status and plan-singular tests.
Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
One track per plan from getPlans() (StatusJson/JsonPlanSummary): fill = percentUsed, red
`.over` past 100% with overage shown, amber `.pace.hot` when over/near pace vs mint `.ok`.
Cycle caption from the plan period; pay-as-you-go rows use `.mut`. Add plan… is M2 (visual).
Honest empty state when no plan is configured.
Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
Critical review fix: the unpriced predicate misclassified every Codex row (all have credits)
and every local saved-only model, hiding real cost/savings. Now unpriced = costUSD===0 &&
savingsUSD===0. Fixtures made realistic (codex priced, local saved-only priced, genuine proxy
dimmed); proxy token cols show "—"; sub-1M tokens use the wireframe's "0.4M" style.
Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
Table from getModels(): calls/input/output/cache-read/cost/saved with a model-family series
dot (reuses the ListRow helper). Unpriced/proxy rows show the dim "add alias" affordance +
dashes. By-model / By-task SegTabs re-fetch (byTask). Compare… is a visual affordance (M2).
Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
Reverts/Abandoned totals + bodies show "—" while yield is loading/errored instead of a fake
$0.00; adds positive-path Abandoned and Fixes-tab tests (guards category filtering); reverted/
abandoned costs use plain .val (mint reserved for savings); simplified FixesRows copy.
Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
Segment tabs carry their totals (Waste=optimize.savingsUSD, Reverts/Abandoned from
getYield summary, Fixes=findingCount). Waste findings from optimize.topFindings; reverts
and abandoned lists from yield.details by category. Evidence lines + copy-fix chips are
honestly omitted (not present in MenubarPayload.optimize) — a richer `optimize --format
json` emitter is flagged as a follow-up.
Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
Addresses T3 review: full test coverage for all lenses + empty/error states, segment
color mapping, Sankey ribbon properties, and lib/period.ts across all windows; Sankey
ribbon widths now truly proportional (removed the 28px cap) and labels are pretty/basename
+ truncated so real ids don't clip; legend reflects present models; honest "top N" project count.
Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
Period-sliced StackedBars (history.daily[].topModels within the selected window via
a new shared lib/period.ts), the By-project list, and a Sankey rendered dynamically
from getSpendFlow (per-model hue ribbons). Lens tabs; series palette shared with the
legend. Adds a neutral .s-other class for the rollup segment.
Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
Payload now spans a 30-day backfill so slicing is provable. Adds: chart slices
to the week window (7 bars, 23 excluded); which bar is peak/runner-up (idx
10/20); corrected Waste/wk ($5.51 @30d, $23.60 @week); deterministic MTD/
Projected/rest-of-month and "-3% vs June pace" (May days prepended to prove the
prev-calendar-month comparator); matched model-join dot+sub-line and a
same-project/day/calls collision resolving by cost ($10 opus / $2 haiku).
history.daily is unbounded CLI backfill (up to 365d, zero-days absent), not
period-scoped. Slice it to the selected period's date window before the chart
(new periodWindowStart/sliceDailyToPeriod), and normalize Waste/wk by the
selected period length (periodLengthDays), not the backfill length.
Also: pace comparator = previous calendar month only (label/math now agree);
session sub-line unit turns->calls; model-join key gains cost to avoid
same-project/day/calls collisions (sessionModelKey); Panel rightLink "See all ›"
on the sessions panel; hide fabricated $0-window gridline labels; correct the
projection comment to the active-days approximation.
Addresses T1 review: discovery.ts logs mDNS bind errors to stderr and returns
already-found devices instead of masking failures as empty; spend-flow uses a
collision-proof "other" sentinel and a local-date period label; the spend/date
flags get friendly error handling; devices-json tests are deterministic (mocked
discovery) and assert the read-only mutation guards. 13 tests green, tsc clean.
Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
Real Overview driven by usePolled(getOverview): 4 stat cards (Today, Month
to date +% vs pace, Projected month est, Waste found /wk), the daily-spend
CapsuleChart, and the most-expensive-sessions list. Every rendered number is
derived from the single MenubarPayload (formulas documented in-code, mirroring
plan-usage projectMonthEnd); topSessions' missing model is recovered from
topProjects sessionDetails. App renders <Overview/> for the overview section,
replacing the T0 smoke view; first-run/error/loading states retained. TDD
spec added (test written first, red→green).
CapsuleChart renders one .c bar per history.daily entry with the peak day
gradient-glowed (.c.hi) and runner-up plain blue (.c.hi2); axis top is the
peak day's spend. ListRow is the reusable .li row (rank, model .mdot,
title+sub, value, chevron) plus seriesColorForModel() for the palette dots.
New `codeburn spend --format flow-json` emits the model x project cost matrix
(uncapped, then top-8 each side with an "other" rollup) for the desktop Spend
Sankey. Adds --format json to devices / devices scan / share status / identity,
matching the web /api/* shapes. Read-only; text output unchanged when --format
is omitted. discovery.ts degrades mDNS bind errors to an empty scan result.
Implemented by Codex gpt-5.5 (high); committed by Fable (git was blocked in the 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.
Ports the wireframe window shell into React with exact markup/classes:
- Shared components: Window, Sidebar (traffic lights, gradient mark, six
nav items with ⌘ keycaps + active `on` rail, status line), TopBar
(title, scope, period SegTabs Today/7D/30D/Month/6M/Custom, ProviderPop),
Panel, Stat, SegTabs, ProviderPop, Hint.
- usePolled(fetcher, deps): generic 30s poll + loading/error state,
errors normalized to CliError.
- App.tsx: local section state switches the content outlet; the Overview
placeholder round-trips getOverview('30days','all') and renders the raw
current.cost, with an honest first-run "locate CLI" state when the
codeburn binary isn't found (never a crash).
- Sidebar.test.tsx: six nav items, click routes onNavigate, active `on`.
- 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.
Dependency-ordered plan for the standalone Electron app. Corrects spec data
gaps: yield/plan already emit JSON; only spend flow-json (Sankey matrix) and
devices/share --format json are new. Subagent-driven execution: Opus 4.8 +
Codex 5.6-high implement, Fable reviews each task.
Standalone Electron app rendering the v6 "indigo instrument" wireframes,
fed by the codeburn CLI via the menubar JSON contract (spawn codeburn --json,
decode, poll). Six sections: Overview, Spend, Optimize, Models, Plans,
Settings/Devices. Data aggregation stays CLI-side; renderer never fakes data.
extractBashCommands recorded the wrapper (sudo, npx, rtk, and friends)
instead of the command it delegates to, so any agent that prefixes its
shell calls collapsed its whole bash breakdown into one meaningless
bucket and the optimize detectors lost the actual tool.
Skip a known set of command wrappers when a real command follows, and
interleave that skip with the existing VAR=value env-assignment skip so
forms like 'sudo NODE_ENV=prod node x' resolve to the real tool. A
wrapper followed by a flag or a quoted token is kept as-is so we never
emit a garbage key.
Closes#657
Users who run MCP tools through a CLI wrapper (e.g. philschmid/mcp-cli)
instead of registering servers natively don't produce Codex
mcp_tool_call_end events; Codex logs a plain exec_command. So their MCP
usage showed only as a shell command and was absent from the MCP
breakdown, even after #513 fixed the native path.
- Recognize `mcp-cli [options] call <server> <tool>` in the exec command
and also attribute it as mcp__<server>__<tool>. The exec still counts as
Bash since it genuinely is a shell exec. Flag-tolerant, quote/path/bash-lc
tolerant, and scoped to the mcp-cli binary (not foo-mcp-cli); only the
`call` subcommand (a real execution) matches, not info/grep/listing.
- Register `codex` in PROVIDER_PARSE_VERSIONS. session-cache.json serves
unchanged session files without invoking the provider parser, so the
codex-cache version bump alone would never re-attribute already-cached
sessions. This also retroactively repairs #513's native-MCP fix for
users whose files were cached before it shipped.
- Bump CODEX_CACHE_VERSION 4->5 for the provider-internal layer.
Tested: parse cases (bash -lc wrapper, flags-before-call, argv array,
plus info/grep/foo-mcp-cli negatives), and a mutation-verified cache
regression test that fails without the PROVIDER_PARSE_VERSIONS entry.
ReDoS-checked. Reviewed with a Fable 5 adversarial pass (Codex is down);
its two must-fixes (the cache gap and the flags-before-call miss that
would have missed the reporter's own sessions) are folded in.
Fixes#478
Review follow-up on the Sonoma deployment-target fix:
- The release minos guard was fail-open: if vtool returned nothing (missing
binary, tool change) the check passed vacuously and could green-light a bad
release. Now it fails closed when no minos is reported.
- The libswift_errno check now excludes weak links, so it only fails on a
breaking STRONG link. dyld tolerates a missing weakly-linked dylib, so a
macOS 15.x SDK runner that weak-links errno at the .v14 target no longer
causes a spurious release failure.
- Removed em-dashes from the changed files per repo style.
Verified: fresh .v14 build is minos 14.0 with no strong errno link; the guard
now fails closed on an unreadable binary and passes on the real one; 71 Swift
tests pass.
- README.md / build-local.sh: the -10825 fix is the Package.swift deployment
target, not the SDK used to build (ld64's $ld$previous drops
libswift_errno.dylib based on minos, so it already applies to the
CI-distributed release too). build-local.sh exists only for building on a
Sonoma machine with just the Command Line Tools.
- build-local.sh: build arm64 and x86_64 separately and lipo them into a
universal binary (`--arch arm64 --arch x86_64` together needs xcbuild,
which CLT doesn't ship); assert the active SDK is actually 14.x instead of
silently trusting `xcrun`; use `pkill -x` instead of `-f` to avoid matching
unrelated processes; broaden the @MainActor patch regex to slurp mode so it
also covers multi-line struct headers and `extension X: View`.
- package-app.sh: fail the build if the packaged binary's minos isn't 14.0
for every arch slice, or if it links libswift_errno.dylib, so a future
deployment-target regression is caught in CI instead of a user's crash report.
The packaged app set Package.swift to .macOS(.v15), producing a binary with
minos 15.0 that LaunchServices refuses on macOS 14.x with
kLSIncompatibleSystemVersionErr (-10825) — even though Info.plist, the CLI
install guard (MIN_MACOS_MAJOR=14), and the README all advertise macOS 14+.
The .v15 bump was attributed to NSAttributedString(attachment:), which is
actually AppKit since macOS 10.0, so the floor must not exclude Sonoma.
Separately, a stock macOS-15-SDK CI build hard-links libswift_errno.dylib
(macOS 15 only), which dyld cannot resolve on Sonoma even once minos passes.
Building against the macOS 14 SDK avoids that dependency.
- Package.swift: .macOS(.v15) -> .macOS(.v14)
- Scripts/build-local.sh: build on a Sonoma machine (which only has the
macOS 14 SDK, lacking the SwiftUI @MainActor inference the macOS 15 SDK
adds to the View protocol) using a standalone swift.org Swift 6.x toolchain,
patching @MainActor onto views in a scratch copy so repo sources stay clean.
- README: document the Sonoma local-build path.
Fable review found that a stale/invalid --claude-config-source with
--provider all served the Claude-only scan labelled as All, so a direct
CLI/MCP consumer holding a stale id got wrong All totals (the menubar
self-heals via reconcile, so users never saw it). Only take the scoped
path when the id validates; otherwise fall through to the normal
all-provider path, keeping the selector (selectedId null) so it still
renders.
Scopes the menubar to one Claude config for multi-config (CLAUDE_CONFIG_DIRS)
setups, with All as the default. Rebased onto main and fixed the review
findings from the original #635:
- Fix a TS2206 build break (a 'type' modifier inside an import type block).
- Reject --claude-config-source with a non-Claude --provider, and scan Claude
only in the scoped branch (a config is Claude-only): fixes provider data
leaking into a scoped query and avoids parsing every provider's corpus.
- Scope the macOS menu-bar figure to the selected config (badge matched the
popover), clear the selection when switching to a non-Claude provider tab,
and stop the on-disk badge fallback from showing an unscoped number while
scoped.
- Tag Claude Desktop / Cowork sessions as their own 'claude-desktop' source so
they are a selectable bucket instead of silently vanishing from per-config
views (sum of options now equals All).
- Skip the redundant Claude discovery walk for plain single-config users while
keeping idle configs and Claude Desktop selectable.
Reviewed by Codex 5.6; all findings addressed. Full suite: 1581 TS tests, 76
Swift tests, tsc clean.
The cold-scan progress line was gated only on stderr.isTTY, which is also
true when the interactive dashboard and `codeburn compare` render Ink to
the same terminal — so the \r progress line printed over their frame and
garbled the screen (confirmed under a PTY). isTTY alone can't tell an Ink
UI apart from a plain CLI command.
Gate on an explicit 'interactive Ink UI is live' flag instead: the two
interactive entrypoints call setInteractiveScanUI() before they render, so
their scans (and the dashboard's 30s auto-refresh, including the
getPlanUsages -> parseAllSessions path) stay silent, while plain CLI
commands (overview, export, status) still show progress. Verified under a
PTY: dashboard and compare silent, overview shows progress, piped/non-TTY
silent. Adds a gate unit test covering the interactive-active, plain-TTY,
non-TTY, threshold, and finish()-clear cases.