isSessionHydrationComplete() (parser.ts, PR #937) already reports when a
read-only serve skipped or staled real on-disk changes; thread it through
buildMenubarPayload as an optional stale field, present and true only on a
stale serve, always absent otherwise, so older/newer CLI-app pairs stay
compatible. Mirrors the field into the desktop renderer types and the macOS
menubar's Codable payload model (data layer only, no view change).
Drafted with minimax/MiniMax-M3 via local gateway.
Today/week/month often have no session-linked PRs, so the tab
looked broken. Name the selected period and, when All has rows,
point at that count. Do not invent gh links. Do not change
attribution or the default period.
F1 - `codeburn serve` lost an in-flight request's response when stdin closed
mid-flight: runStdioServe returned before its queue drained, and the explicit
process.exit(0) then hit runCaptured's monkeypatched exit, throwing ExitSignal
and exiting 1 with the frame never written. The finally now awaits the queue.
F2 - the SIGTERM rationale was false. armSignalCleanup unlinks the refresh lock
and re-raises; it publishes no partial cache, and a lock left by SIGKILL already
self-heals through the stale-pid takeover. SIGTERM-first is kept for the real
(smaller) benefit - a clean lock release instead of a takeover - and every
comment plus the CHANGELOG now says only that.
F3 - the cold gate had no exit. overviewWarmed only flips on success, so an
install that can never hydrate sat behind an indexing splash forever with no
error and no route to the CLI recovery. The cold claim now expires with the cold
window itself.
F4 - the real CLI does not heartbeat the way the demo did: a cold parse's
inter-provider cache save measured 31.6s of total silence, which a 45s window
survives only until the machine is 1.5x slower. Under CODEBURN_PROGRESS a
running parse now emits a keepalive every 10s regardless of phase, so silence
genuinely means stopped. Consumers that do not know the event ignore it.
F5 - the orphan-reap identity check matched any `cli.js` running `serve`. The
pidfile now records the exact argv and `ps -ww` must match it exactly.
F6 - bump() re-armed the watchdog after settle, leaving a timer finish() never
clears when a killed child's buffered output landed.
F7 - kill paths dropped the child from activeChildren before SIGTERM and the
SIGKILL backstop was unref'd, so a quit inside the 5s grace orphaned a child
that ignores SIGTERM. It now stays registered until it actually dies.
N8 - the silence test wrote its only byte at t~0, so it passed without the
re-arm. The byte now lands mid-window and the kill is asserted from it.
N9 - documented why mutations keep a plain total cap.
The desktop app capped every read at 45s of TOTAL runtime, so a slow machine's
warm `optimize` (52.5s on 0.9.20, 39.6s on main) was SIGKILLed mid-parse and the
panel painted a red "timed out" that a 60s poll then reproduced forever.
- cli.ts: the read timeout is now a no-output watchdog. The window restarts on
every byte the child writes (serve: every frame carrying that request's id),
so only a genuinely silent child times out. A 15-minute absolute ceiling still
reaps a livelocked child. Every read spawn sets CODEBURN_PROGRESS=1 so a long
parse heartbeats through it; progress lines are stripped from any surfaced
stderr so they can never become the error message.
- cli.ts: a timed-out child gets SIGTERM first and SIGKILL only after a 5s
grace, on both the spawn and the serve branch. That lets the CLI's signal
cleanup publish its partial parse and release the cross-process refresh lock
instead of leaving both stale.
- main.ts: the 10-minute cold floor now covers EVERY read while the first
hydration is still running, not just the overview. Sections start polling the
moment `ready` flips (which an overview error also does), and a 45s section
spawn queued behind the cold parse was killed on arrival.
- main.ts/renderer: a timeout raised while the hydration is still running is
flagged `cold`, and the renderer keeps its indexing splash (and keeps the
sections gated) instead of latching ready and painting CliErrorPanel. Only
timeouts are flagged; a permission or nonzero failure still surfaces.
- cli.ts/main.ts: the resident serve child's pid is recorded under userData and
a serve orphaned by a previous crash is reaped at launch, after `ps` confirms
the pid still belongs to a codeburn serve.
- src/main.ts: `codeburn serve` exits explicitly once its stdin closes, so a
handle outliving the transport cannot keep the child alive as an orphan.
The budget window comes from computePeriodFromResetDay, which builds an
anniversary period from plan.resetDay (1-28, settable per plan with
`codeburn plan set --reset-day`). "Calendar-month budget" and "Next
calendar reset" are therefore wrong for anyone who moved the reset day,
which is the same class of inaccuracy this change set exists to remove.
Say "budget" and "Next budget reset" instead, and use one wording across
the TUI and the desktop cards.
Both TUI lines truncate end-first at the terminal width. The headline had
grown past the point where an 80-column terminal still showed the
percentage, so it drops "vs ... /mo" for "/ $300.00 budget", and the
status line drops the clause repeating "budget" from the headline. At 80
columns the longest label (custom plans carry their provider) now fits
the percentage, and the status line still shows the projection.
SuperGrok Heavy is a $300/mo preset compared to parsed API-equivalent
spend on a calendar month. CodexBar's live weekly window is a different
class. TUI and desktop copy now say budget / calendar month and deny
a live provider window. No Grok Connect is invented.
A mixed local + claude.ai connector finding is class `fix`, but `--apply`
only mutates the local servers. classTotals now credits the `fix` group
with `applyTokensSaved` when present, so the "Fix now (apply-able)"
subtotal, the "apply-able: ~$X" headline and `summary.byClass.fix` (CLI,
TUI and desktop all read these) describe what apply can actually recover.
The finding keeps the whole opportunity in its own `tokensSaved`.
Also fixes the desktop connector fixture, which predated the class/basis
fields, and adds class-level coverage: connector-only findings resolve to
`nudge` (no apply payload), a local server named like a connector stays
manual-only, and local-only findings keep their full subtotal.
Same section, compact: one line per still-applied fix with the verdict
glyph, and the undo command for the ones that measured nothing. The app
reads appliedFixes[] off the optimize JSON, tolerating its absence from
an older CLI.
Each class header now carries its own token/dollar subtotal and finding
count, so the apply-able slice is never mistaken for the whole board; the
headline savings line names that slice explicitly. CLI and TUI share one
classHeaderLine helper, the desktop app reads the same numbers from the
new summary.byClass in --format json (add-only; the three subtotals sum to
findingCount and potentialSavingsTokens).
Also scopes the SHELL_PROFILE_SCOPE comment to what is actually true: the
MCP deferral plans refuse to rewrite a shell profile, but bash-output-cap
appends its own marker block to one.
Every finding now resolves to a class (apply-able fix, habit nudge, or
informational keep) and a basis (measured from provider-counted usage, or
estimated from a schema/heuristic model), both from one table next to the
FindingId union. The class follows the plan layer: an id is 'fix' only when
buildPlan routes it, and an instance drops to 'nudge' when it lacks the
payload or cause its builder needs.
CLI, TUI and the desktop app group findings under Fix now / Habits / FYI
with continuous numbering; the CLI header reports 'N measured · M
estimated' in place of the blanket 'Estimates only.' footer. The JSON
report gains class + basis per finding and summary.measuredSavingsUSD;
existing fields are unchanged. The menubar's top three follow the same
order, since every surface reads the sorted findings list.
Sessions whose cost the provider never reported leave the cost-outliers
peer math; when nothing else is priced the comparison falls back to them
and the finding reports itself as estimated instead of disappearing.
- Punchcard tooltips flipped below the cursor on the top rows (the
overflow container clips anything above its own edge) and clamp
horizontally near the strip edges. Applied to both the app and dash
copies of the component.
- The menubar Workflow strip is removed (view, strip model, tests):
the popover is the compact surface and the workflow metrics live in
the desktop app, dash and TUI. The payload keeps emitting the block
(add-only contract; the other surfaces read it).
The release's new capabilities were unevenly surfaced: the Spend
punchcard existed only on the web dashboard, and the menubar decoded
nothing of the PR-attribution block the payload already carries.
- Desktop app: new Punchcard component (hour-of-day x weekday spend
matrix, ported from the dash and restyled to the app's tokens) on the
Spend page. Fed by a dedicated getTimeline bridge channel that fetches
the payload WITH history.timeline; every other fetch keeps the lean
--no-timeline path, and the serve child makes the extra fetch cheap.
Hides gracefully when the payload has no timeline (older CLI).
- Menubar: PullRequestsSection renders the top three PRs by attributed
spend under the Workflow strip; MenubarPayload now decodes the
pullRequests block (decodeIfPresent, so older payloads are unchanged).
Hidden when absent or empty.
Deliberately NOT ported: codex Tok/s (reads rollout files per session -
too heavy for payload cadence; stays a TUI/report analytics view) and
the punchcard in the menubar (a 7x24 matrix has no legible place in a
compact popover).
App suite 468 green (bridge channel pinned, mocks extended), swift
build + 156 tests green, CLI suite green.
Closes#918.
The renderer's keydown handler required event.metaKey and explicitly
rejected event.ctrlKey, so every shortcut was dead outside macOS: on
Windows and Linux metaKey is the Super key, which the OS shell takes.
Navigation (1-8), Settings (,) and Refresh (R) all did nothing. The
sidebar and footer hints also hardcoded the Cmd glyph, so a Windows
user was shown chords that could not fire.
Add app/renderer/lib/platform.ts as the single source of truth for
platform-aware shortcuts, reading the platform the preload already
exposes (window.codeburn.platform) with a user-agent fallback for the
non-Electron cases. isModifierChord accepts Cmd-without-Ctrl on darwin
and Ctrl-without-Cmd elsewhere; altKey stays rejected on both, because
AltGr on European Windows layouts arrives as Ctrl+Alt and must not
hijack a typed character. Every visible shortcut label now resolves
through shortcutLabel() at render time, so the sidebar shows Ctrl+1
where macOS shows the Cmd glyph.
The mac chord condition is unchanged: the old guard admitted
metaKey && !altKey && !ctrlKey && !shiftKey, and the new one admits
exactly the same set on darwin.
The Electron application menu is deliberately left alone. It ships no
reload/forceReload role and no CmdOrCtrl+R accelerator, which is what
leaves Ctrl+R free for the renderer to handle on Windows.
Also corrects the Settings navigation hint, which read 1-7 while the
sidebar has eight numbered destinations.
Tests cover both platforms for labels and dispatch, including the
negatives: Meta on win32, Ctrl on darwin, and the Ctrl+Alt AltGr shape.
Fixes#795: the desktop Dashboard showed only local-device cost while the
menubar and web GUI aggregated across paired devices.
Desktop app:
- Add a Local/Combined Scope setting (Settings > General), mirroring the
macOS menubar. Combined passes `--scope combined` to the CLI, forces the
provider filter to all, and persists to localStorage.
- Overview hero shows the paired-device aggregate (cost/calls/sessions) with
a "Combined · N devices" caption and per-device breakdown when Combined is
selected; detailed panels remain local (the combined payload carries totals
only).
Menubar:
- The badge figure now reflects Combined scope instead of always showing the
local total: refreshMenubarBadge fetches the combined payload for the badge
period and the badge renders the cross-device aggregate, falling back to
local when no combined payload is available.
* sessions: fold subagent runs into PR attribution
Sidechain (subagent) session cost never reached the by-PR view, so a
session orchestrated on one model with subagent lanes on another showed
only the parent model on every PR row. Fold each sidechain's cost, calls,
models, and categories into the parent turn that spawned it, so it
inherits that turn's PR set under the existing turn-level state machine.
Linkage, in priority order: the spawn result's toolUseResult.agentId
pairs the child's agent id with the Agent/Task tool_use id that launched
it (recorded per turn), which is the true launch point and wins even when
the child's first activity landed during a later turn; else the child's
first-activity timestamp is bucketed into the containing turn span; else
the child folds into the parent's unattributed spend. Children of parents
that referenced no PR, and orphans whose parent is absent from the scan,
contribute nothing, unchanged.
Cache v6 to v7 (neither shipped, so one combined bump from v5): per-turn
spawnToolUseIds, per-file parentSessionId and agentSpawnLinks; the
validator and the append/compact paths thread them like prRefs. By-PR
footers now count parents plus folded subagent runs and the payload gains
an additive subagentSessions field. distinctCost now includes folded
subagent spend, documented in the payload comment.
* sessions: address adversarial review of subagent PR attribution
Rework child attribution to resolve each subagent to the PR its launching
turn was working on, using the parent's UNFILTERED turn data, and enforce
that every dollar is counted exactly once.
- Mutual exclusion: a child that referenced its own PR attributes
standalone and is never folded; a child with no links is folded only.
Fixes a double-charge where a self-linking child was both folded and
self-attributed.
- Recursion: a fold aggregates a child plus its non-self-linking
descendants (depth-first, cycle-guarded), so grandchildren spawned by
subagents reach the PR report.
- Global linkage: the subagent index keys by parentSessionId alone
(UUIDs are globally unique), so a child whose worktree resolves to a
different project still links.
- Date-range correctness: spawn-to-PR sets are built at assembly from the
full turn list, so a spawn in a pre-range turn attributes to the right
PR; a PR-linked parent whose own turns fall out of range is kept as a
0-cost fold anchor so its in-range child is not lost.
- Timestamp fallback compares epoch ms (mixed UTC offsets order right) and
is end-bounded: a child active after the parent's last turn is unlinked
(contributes nothing), matching orphan semantics.
- Cache adoption tries the newest prior versioned file (v6 then v5) so the
preceding build's expired-PR history survives the v7 bump; an invariant
note requires the list to cover every version that can exist on disk.
- Spawn-result pairing matches the tool_result block that carries the
agentId, not the first block, when a record batches several results.
- resolveSubagentAttribution is computed once and shared by aggregateByPr
and prLinkedTotals.
subagentSessions now counts folded subtrees (children plus descendants).
Verified on real data: attributed + unattributed reconciles to cost, and
parent-only cost plus folded-children cost equals the folded total to the
cent (no double-count).
* sessions: round-2 hardening of subagent PR attribution
Address a second adversarial review of the new machinery.
- ID collision: parents and a child's parent reference are keyed by
provider + sessionId, not bare sessionId. When two distinct parents
still share a key (true duplicate/imported data), the child folds into
NEITHER (deterministic skip, stays standalone): correctness over
coverage.
- Recursion dedup is global: one claimed-set spans all of a parent's
direct children, so a descendant reachable through two paths (a diamond
or duplicate id) folds exactly once and a parent-link cycle terminates.
- Cache adoption migrates every prior version oldest-to-newest and MERGES
per source path (newer wins per entry), so a sparse or partial newer
file no longer masks older-only expired-PR orphans.
- Fold anchors (0-cost PR-linked parents kept only for attribution) live
in a new ProjectSummary.subagentAnchors, never in `sessions`, so they no
longer contaminate session counts, averages, or any per-session report.
Folded PR rows take their date span from the contributing child activity
rather than the anchor's empty timestamps.
- One-pass buildPrAttribution computes rows and totals together; the
payload builder and CLI call it once. Drops the identity-keyed
memoization, which could return stale folds if the array was mutated.
- Ambiguous multi-block spawn-result pairing leaves the spawn link unset
on purpose; the child then folds via the timestamp fallback rather than
pairing with the wrong id or disappearing.
Every fix is mutation-verified. A fresh real-data drive re-proves the
no-double-count identity to the cent (parent-only cost plus folded cost
equals the folded total) and that the PR rows sum to attributedCost.
* sessions: round-3 hardening of subagent PR attribution
Third adversarial review pass.
- Ambiguity counts ALL candidate parents (and anchors) sharing a
provider+sessionId key, not just PR-bearing ones, and uses a
per-record fingerprint: a key carried by more than one DISTINCT record
folds its child/subtree into NEITHER (identical duplicates still fold
once). This unifies the parent-collision and duplicate-descendant rules
and is deterministic across input order.
- Project-rebuilding filters (by day, by date range, by config source)
now carry subagentAnchors through, and a date filter CONVERTS a spawn
parent whose in-range turns are all filtered out into an anchor so a
surviving in-range child still folds. Rebuilt sessions also keep their
PR + subagent-linkage metadata (prLinks, parentSessionId, spawnPrSets,
...), which buildSessionSummary otherwise drops, so by-PR and folding
work on a filtered slice (menubar/dashboard flow).
- Fold anchors leave ProjectSummary.sessions entirely and folded PR rows
take their span from the child, so 0-cost anchors never touch session
counts or averages.
- Ambiguous spawn pairing (parent named the agent but its exact launching
tool_use could not be paired) is recorded per parent; a late child of
such a pairing folds to the parent's last turn within a 30 minute grace
window, else stays unlinked. A truly-absent pairing gets no grace.
- Row session key is NUL-delimited and provider-prefixed, so a project
name or session id containing a space no longer collides and
undercounts distinct sessions.
Every fix mutation-verified. A fresh real-data drive re-proves the
no-double-count identity to the cent, and a day-filtered drive proves the
filter fix end to end (anchors created, subagents fold, identity holds).
* sessions: round-4 hardening of subagent PR attribution
Fourth adversarial review pass.
- sessionFingerprint now covers the COMPLETE linkage-relevant payload,
not just headline stats: a canonical (sorted-key) serialization of
agentSpawnLinks, spawnPrSets, prRefsAtRangeStart, ambiguousSpawnAgentIds,
parent/agent identity, and the per-turn prRefs timeline. Two records
that share an id and headline stats but map the child to different
spawns/PRs now fingerprint DISTINCT, so the ambiguity rule fires and
they fold into neither, deterministically rather than order-dependent
first-wins.
- A date/day filter recomputes prRefsAtRangeStart at the new slice
boundary by replaying the original full turn sequence, instead of
copying the wide range's value. A PR switch between the wide start and
the slice start (July 1 A, July 10 B, slice July 20) now carries B, not
a stale A; a turn exactly on the boundary stays in-slice and applies its
own refs. The recompute selects by timestamp, so it is order-independent.
Non-contiguous day selections are documented as treated contiguous from
the earliest selected day (a single session-level seed cannot represent
multiple segments; the menubar selection is a single day or a run).
- The anchor-carry path drops an anchor that duplicates a surviving
session id, so malformed merged input cannot double-count.
Also: rebuilt filtered sessions were losing their PR/subagent-linkage
metadata (a child its parentSessionId, a parent its prLinks), which
carryLinkageFields now restores, so by-PR and folding work on any filtered
slice.
Every fix mutation-verified. A fresh real-data drive re-proves the
no-double-count identity and reconciliation to the cent for both a
lifetime scan and a day-filtered slice (anchors created, subagents fold
through the filter).
* sessions: round-5 hardening of subagent PR attribution
Fifth adversarial review pass; closed-form fixes.
- sessionFingerprint serializes the COMPLETE fold-determining state via a
real recursive canonical encoder: session-level linkage AND, per turn in
sequence, timestamp, prRefs, cost, calls, savings, and per-model cost.
Object keys are sorted recursively and set-semantic arrays (PR-ref lists,
ambiguous ids, spawnPrSets values) are sorted, while the turn list keeps
order; the structure is emitted through JSON.stringify (no delimiter
concatenation). Two same-id parents that differ only in a turn timestamp
now fingerprint DISTINCT (fold neither), and records differing only in
set-array order fingerprint EQUAL (no false ambiguity).
- recomputeRangeStartPrRefs breaks an exact-same-millisecond tie
deterministically by the lexicographically-last sorted-ref key, so the
recomputed seed is stable regardless of turn order.
- A day filter seeds EACH selected day's first ref-less turn by replaying
the original full turn sequence up to that day's start (per-day seeding),
so a PR switch on an UNSELECTED day between two selected days carries to
the later day. Contiguous and non-contiguous selections are both correct.
- The anchor dedupe drops an anchor only when a surviving session shares
the full provider-aware, fingerprint-qualified identity (a proven
duplicate): a different-provider or different-record same-id session no
longer wrongly drops the anchor.
Also fixed a double-count the fingerprint test exposed: two duplicate
parent sessions share a key and the SAME resolved children, so folding is
now done once per parent key.
Every fix mutation-verified. Fresh real-data drives (lifetime, single-day,
and a NON-CONTIGUOUS day selection) re-prove no-double-count and
reconciliation to the cent.
---------
Co-authored-by: reviewer <review@local>
* sessions: attribute PR spend per turn instead of per session
The by-PR surfaces attributed a session's full cost to every PR it
referenced, so one orchestration session that touched many PRs rendered
identical full-session rows. Capture per-turn PR references during
transcript parsing and attribute each turn's cost to the PR set active at
that turn (split evenly across a multi-PR merge-sweep turn), carrying the
most recent PR set forward across turns that reference nothing. Turns
before the first reference form an unattributed bucket.
A session whose transcript expired before per-turn capture keeps its
session-level prLinks but has no per-turn refs; it falls back to an even
whole-session split, and any row carrying such a portion is flagged
approximate.
Rows are now summable: the CLI and app footers report the attributed sum
plus the unattributed remainder instead of a distinct-session total.
Bumps the session cache version so surviving transcripts re-parse and
populate the new per-turn field. Daily-cache versioning is untouched.
* sessions: harden PR attribution and add models + category breakdown
Addresses the review findings on the per-turn PR attribution and adds the
model and task-category surfaces.
Correctness:
- Cache migration: the 5 -> 6 session-cache bump now adopts the prior v5
file's expired-source PR entries instead of abandoning them, and the claude
scan preserves and surfaces PR-bearing orphans, so a session whose transcript
was deleted still appears as a legacy even-split instead of vanishing. The
daily cache is untouched.
- Date-range carry: the parser captures the PR set active at the start of the
in-range turn slice and seeds the state machine with it, so a PR referenced
before the window still owns its later, in-range, ref-less turns instead of
the session falling back to a whole-session approx split.
- Calls are split across a multi-PR turn by largest-remainder, keeping per-PR
counts whole (a 1-call, 2-PR turn no longer renders as 2 calls).
- The CLI and app footers reconcile to the rounded row values actually shown.
- Distinct sessions are keyed by project + sessionId, not sessionId alone.
- The app tolerates an older by-reference payload (no attributedCost): it keeps
the old non-summable footer and never renders NaN.
Features:
- Each PR contribution records the models of its calls and the turn's task
category (split by the same share on a multi-PR turn); legacy even-split rows
carry the model union but no category breakdown.
- Payload rows gain models (short names, cost-desc) and categories (label + cost,
cost-desc, omitted when empty); the payload gains otherPrCount/otherPrCost for
the PRs beyond the sent top 20.
- CLI --by-pr gains a Models column. The desktop table is now full-width with a
Models column and click-to-expand rows showing a per-category cost breakdown
with proportional bars, keyboard accessible, with an "Other (N more PRs)" row
when capped.
Tests: state-machine seed/models/categories/largest-remainder/dedup, the prRefs
round-trip through the real incremental-append path (continuation + straddle),
v5 adoption of an expired PR session, payload round-trip, and the desktop
expansion/models/old-payload cases.
* sessions: reconcile mixed PR rows, harden v5 adoption, bound models
Round-2 review follow-ups.
- Mixed legacy/live category breakdown: a PR row that combines an expired
legacy contribution (even-split, no turn data) with a live per-turn
contribution now emits an explicit "Legacy estimate (no per-turn detail)"
category carrying the legacy share, so the expansion reconciles to the row
cost instead of silently dropping it. A legacy-only row still shows no
breakdown.
- v5 adoption is now per cached file: one malformed entry is skipped instead
of rejecting the whole v5 cache and dropping every valid expired PR session.
- The desktop "Other (N more PRs)" line moved to the table footer as a muted,
separated summary rather than a sorted row.
- Row expansion resets when the PR set changes (period/provider/data), so a
stale expansion cannot linger on a row that is gone.
- Payload models per row are capped to the top 4 by attributed cost, with a
name-ascending tie-break; categories get the same stable tie-break.
Tests: mixed live+legacy reconciliation, legacy-only omission, model cap and
tie-break, corrupt-plus-valid v5 adoption, and the desktop expansion-reset.
* session-cache: validate optional agentType and failed during v5 adoption; app: reset PR expansion on period switch
A v5 entry that was valid except for a malformed optional field (agentType,
failed) passed per-file validation and flowed downstream as a non-string.
The expansion reset keyed only on the row URL set, so a period switch that
returned the same PRs kept a stale expansion open over changed numbers.
---------
Co-authored-by: reviewer <review@local>
Part 1 (src): expose two add-only, optional aggregations on the menubar
payload's `current`, computed on the unscoped all-provider path from the
surviving-session parse (carried history cannot contribute, as expected).
- current.pullRequests: aggregateByPr rows (top 20 by cost) plus the
multi-link-safe distinctCost/distinctSessions. Rows are by-reference, so
they are never summed.
- current.byBranch: new aggregateByBranch, per-branch spend (top 15 by cost)
that carries each session's last-seen git branch forward across its turns.
The cache stores a turn's branch only when it changes, so the parser now
resolves the branch at reconstruction (before the date slice) and records
SessionSummary.everHadBranch from the full transcript. That lets the report
keep a branch-bearing session's pre-branch spend in an explicit null row
even when the range clipped the anchor turn, while a provider that never
captures a branch still contributes nothing.
Part 2 (app): new "Pull requests" sidebar entry between Sessions and Spend
(shortcuts reflow to number 8 for Plans). It renders the pullRequests rows as
a refined table with the PR label linking out via openExternal, cost,
sessions, calls and an active-date span, plus a footnote stating the distinct
total and the by-reference attribution. A quiet explanatory line shows when no
PR links exist, never a fake table.
Tests: aggregateByBranch unit tests (carry-forward, null bucket, clipped
anchor, by-reference session counts) and the Pull requests component tests
(table, external link, footnote, both empty states). Nav-reflow assertions in
Sidebar and App tests updated.
Co-authored-by: reviewer <review@local>
Two Phase-1 desktop additions over the renderer's existing card grammar.
Overview workflow card: a panel below the daily chart with two stat tiles
(correction rate with count, median time to first edit) plus a top-rework
line and one coaching caption. The caption is derived locally with the CLI's
buildCoachingNotes thresholds (corrections, then file churn, then time to
first edit; first that fires). The card hides entirely when no real signal is
present, never renders zeros, and shows a "N% priced" chip only when pricing
coverage is a number below 1. Payload types gain optional workflow,
topReworkedFiles, and pricingCoverage fields so older payloads render as before.
Sessions view: the captured session title becomes the row headline with the id
demoted to the mono secondary line; untitled rows keep the project as the
headline (unchanged). Title is also searchable and added as an optional
SessionRow field so older CLIs render unchanged.
Co-authored-by: reviewer <review@local>
The update Download button opened the GitHub release page; it now
downloads the right artifact for the running platform directly: arm64
or x64 dmg on macOS (preload newly exposes process.arch), the Setup exe
on Windows. Linux keeps the release page since it ships three formats
and the user picks. Unknown platforms and preloads without arch fall
back to the page. URL mapping is unit-tested per platform.
Co-authored-by: reviewer <review@local>
* fix(app,dash): render pre-history days as no data, not a currency zero
Days before the first recorded day in history.daily were painted as a real
0.00/0 calls in the daily activity heatmap and the daily spend charts. That
zero is unknown, not measured, so those cells and bars now read "No data
recorded" instead of a currency zero. Genuinely idle days within recorded
history stay as real zeros.
- app: heatmap cells, the daily spend bars, and the daily-by-model columns
get a distinct no-data style plus a "No data recorded" hover for days before
the first recorded day.
- dash: the granular line chart drops buckets before the first recorded day so
no flat zero line is drawn before any history exists.
- data-start is derived in the UI layer from history.daily; payload shape is
unchanged.
Verified: app vitest 418 pass (7 new for the no-data behavior); app, dash, and
root typecheck clean; dash and app renderer vite builds succeed.
* no-data: cap guard, timezone-free dash trim, StackedBars aria
Three hardening fixes from adversarial review of the no-data rendering:
dataStartKey returns null at the payload's 365-entry history cap, where
the oldest retained entry is no longer the true data start; classifying
past it would label real aged-out history as no data on long custom
ranges. Documented that the install-to-first-use gap reading as no data
is literally accurate: nothing was recorded then either.
The dash granular chart now trims leading zero-only buckets by value
instead of comparing bucket timestamps against date keys, so producer
and viewer timezone skew can never drop a real first-day bucket, and an
all-zero series lands in the established empty state.
StackedBars no-data columns expose their state via aria-label, not only
a title on a non-focusable div.
- build stamp ignores the pricing-snapshot files the build regenerates,
so a release checkout no longer shows a false -dirty
- splash shows just the flame, CodeBurn, and the version (dropped the
commit-hash line); the clean build id stays in About only
- About 'Check for updates' uses the in-app checker and reports inline
('You're on the latest' / 'Update available: X · Download') instead
of blindly opening the releases page
392/392.
The once-daily usage_snapshot now includes each top model's primary
task category (which model for what purpose, one cheap by-model call
per day), plus MCP servers and skills used (names + coarse usage
buckets). Names + buckets only, no prompts/paths/dollars; graceful
degradation if the by-model fetch fails. /telemetry page updated.
Per owner decision: the binary 'burning code' flame is the mark
everywhere, matching the app's identity as a code-cost tool. Desktop
app icon + FlameMark (splash/sidebar/About/dock), menubar dock icon,
brand logo, and local dashboard all use the binary flame.
Retires the binary-digit flame (which muddied below ~256px) in favor
of the original solid flame everywhere: desktop app icon + FlameMark
(splash, sidebar, About, dock), menubar dock icon, brand logo. One
consistent mark that stays crisp at every size, matching the website
favicon and local dashboard.
Checks the GitHub releases for newer desktop-v tags once per launch
and every 24h (15s timeout, no identifying headers, offline = silent
retry). A dismissible banner and a Settings About row surface the
newer version and link the release page; dismissal persists per
version so each release nags at most once. No auto-install: unsigned
builds cannot, by platform constraint; the checker only notifies.
App 364/364 (+24), typechecks + build green.
Energy (field report: ~3x Chrome drain):
- polls skip entirely while the window is hidden/minimized, with one
catch-up refresh on return when data is stale; visible-but-unfocused
keeps polling (second-monitor case)
- all looping animations pause under html.page-hidden (the sidebar
flame flicker was a perpetual compositor drain)
- default cadence 60s; an explicit stored choice is always honored
Currency (field report: set USD, still saw EUR):
- memo-served payloads re-applied their embedded stale currency; config
mutations now purge the renderer memo and force-refresh, and a
switching payload can never overwrite the applied currency
- USD default verified end to end
Measured: hidden window = 0 new CLI spawns over 5 cadences (was: full
polling forever). App 340/340, build + package green.
- prefetch fires exactly once per provider per session; memo sized to
detected providers so base keys never evict (was: 12 full-history
parses every 30s forever from LRU thrash re-triggering the effect)
- day-aggregator buckets whole turns by user-message timestamp,
matching the report/headline path: trend bars and provider breakdown
now reconcile exactly (midnight-straddling turns were split);
DAILY_CACHE_VERSION 13 with an IANA tz guard that rehydrates when
the cache crosses timezones
- overview's redundant second all-provider scan collapsed (~11% faster
warm); numbers parity-guarded
- lock takeover cleans stale hydrating.lock; SIGINT/SIGTERM release it
- scanner skips EPERM/EACCES provider dirs (progress shows skipped)
instead of aborting; onboarding hints Full Disk Access
- rateLimited quota copy render tests; export -f json emits valid
empty JSON when no data
Root 2023 (+4), app 327 (+3), typechecks clean.
P0: an interrupted cold hydration left partial caches that the
emptiness check read as warm — the daily backfill froze (missing older
days) while session parsing healed gradually (drifting totals),
poisoning every surface sharing the cache. Both caches now carry an
explicit complete marker: partial resumes cold under the hydration
lock; unmarked caches self-heal with one re-hydration. Regression test
seeds the exact frozen artifact and asserts bit-for-bit convergence
with a never-interrupted run.
- splash indexing reveals only on genuinely cold scans (explicit cold
flag in the progress protocol), never on warm incremental re-parses
- uncached filter switches clear to skeleton instead of showing the
previous filter's numbers; cached switches paint same-commit;
background prefetch warms every detected provider after idle
- build stamp (git sha + date) in splash and About ends build ambiguity
- payload parity test: identical payloads on identical cache, and
payload totals equal the CLI report path
- Sessions empty state keeps the provider filter row; zero savings
line hidden; TCC usage-description strings + Full Disk Access docs
- warm profile documented: optimize scan ~1.47s dominant (safe
refactor deferred), parse ~1s, aggregation ~30ms
Root 1848, app 320, typechecks clean.
Performance and coherence:
- Settings > General 'Refresh every' (Manual/30s/1m/3m/5m/10m), live via
RefreshCadenceContext; Manual polls only on demand
- usePolled memoKey LRU: provider/period switches paint the last-good
result instantly with a switching hairline while refreshing quietly
- quota TTL 5min + honest rate-limited copy on 429 backoff
- version-suffixed cache files (session-cache.v5.json, daily-cache.v12,
auto-minted on future bumps); legacy files never written or deleted,
adopted once when versions match: old and new binaries coexist
without clobbering (field-observed menubar-vs-desktop ping-pong)
- advisory hydration lock: concurrent cold starts share one scan
(wait-then-read-warm), stale/dead locks self-heal, never a
correctness gate
Telemetry v1 + onboarding (desktop only, per owner decisions):
- first-launch onboarding (3 feature screens + consent); region-split
default (EU/EEA/UK/CH off, elsewhere on, unknown off); nothing sends
before consent completion or while off; dev builds never send
- anonymous install UUID, rotated on opt-out; day-granularity events,
cost buckets only; whitelisted names; 200-event queue, 5min flush
- Settings > Privacy live toggle replaces the static claim
Zero computed-number changes. App 316/316, root 1805.
Wire contract targets api.codeburn.app/v1/telemetry (Worker follows).
The per-provider ingest list rendered a dozen stacked text rows on
machines with many detected providers. Now one status line with live
tabular-numeral counts (Indexing Claude · 240/699), a single strip of
15px provider logos (dim pending, pulse active, settled done), and a
muted one-time-scan note. ~3 lines of vertical footprint, 340px max
width; reduced motion drops the pulse; warm launches never see it.
Root cause of the field-reported 45s timeout + perpetual slowness: on
a cold cache all six sections spawned different CLI subcommands at
once, each running its own full-history parse (~11-31s, ~3GB RSS),
contending past the 45s kill so the rebuilt cache never persisted and
every poll restarted from zero.
- the first overview fetch runs as a warmup: 10-minute timeout,
re-arms until it succeeds, reverts to 45s after; section polls gate
on that first resolution (usePolled gains enabled), so cold
hydration happens exactly once
- warmup streams the CLI's progress protocol; the splash shows 'First
run: indexing your usage history' with a per-provider ingest list
(logo, live counts, check on done), static under reduced motion,
generic fallback without events; warm launches unchanged
- overview spawns pass --no-timeline (desktop never renders it)
Measured on real data: cold 31s once (was: killed at 45s forever),
warm 2.6s. 295/295 app, 1797 root.
Same model via different providers (MiniMax M3 x3) was
indistinguishable: rows now carry providerDisplayName in both lenses.
The provider dropdown and Sessions quick-filter no longer hide detected
providers with zero spend in the current period (Hermes was invisible
while Settings listed it); entries sort by cost, zero-spend last.
Settings Providers pane keyed logos by lowercased display names
('grok build' never matched THEMED_LOGOS 'grok'); rows now come from
providerDetails ids like the rest of the app. 20 official marks added
from the repo's own assets (hermes, openclaw, antigravity, kiro, devin,
cline, roo-code, ...) and unknown providers render a first-letter
monogram badge so no detected provider is ever logoless.
The h264 loader baked a flat dark background that read as a rectangle
against the splash's warm radial tint. Re-encoded from the original as
VP9 WebM with an alpha channel (checker keyed to transparency), so the
burning flame composites directly onto the gradient. Verified over a
contrasting test canvas.
273/273, typecheck + build green.
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.
New owner-approved art: the binary-digit flame. Checkerboard keyed out,
largest-component masking to drop glow-zone checker fringe, clean
synthetic ember glow rebuilt; composited to the Big Sur squircle as
app/build/icon.png and squared with alpha as the in-app FlameMark
raster (sidebar, About, static splash).
The supplied burn animation becomes the splash loader: checker keyed
per-frame and the splash's fixed dark canvas (#171310) baked in (h264
has no alpha), audio stripped, 944KB looping video shown while the
first scan runs. Splash canvas and text colors are now theme-fixed so
video and page never diverge; reduced motion/tests keep the static
mark, verified by test.
Flame/splash/motion suites green in isolation (full suite re-verified
when the in-flight quota batch commits); typecheck + build green.
The owner supplied the final flame render; the hand-retrace is retired.
Checkerboard keyed out to real alpha, composited to the Big Sur squircle
(824/1024, r185, charcoal #231613) as app/build/icon.png (the
electron-builder master, verified legible at 16px), and the flame-only
alpha PNG becomes the in-app mark: FlameMark now renders the raster so
splash, sidebar, About and dock are pixel-identical. Flicker applies to
the whole mark (raster has no separable core). Stale icon.svg removed.
Flame suites green in isolation; full suite re-verified before the
in-flight quota batch commits.