Detected subscriptions card (Claude/Codex): auto-detected tier from the
live quota with a connect hint when logged out — like the menubar,
never a fixed preset. Manual budget presets now only for providers
without OAuth quota (Cursor/SuperGrok); configured claude/codex manual
plans stay listed for removal with a superseded note. The Plans
dashboard already excludes them next to the authoritative live quota.
Replaces the rejected hand-traced silhouette with Bootstrap Icons'
fire path (MIT, embedded with license note) — the flame.fill-style
teardrop with the inner tongue — filled with the menubar BurnFlame's
actual Ember gradient from the Swift theme (#f0a070 > #e8774a >
#c9521d > #8b3e13, bottom to top), not the brighter accent ramp.
Flicker hook moves to the whole mark (single compound path).
Also: .splash-word snapped to var(--fs-kpi).
The motion batch gave five sections shimmer skeletons on first load but
missed Compare and Models' Audit lens (both still showed bare loading
text in an empty panel). Same SectionSkeleton treatment now.
250/250.
Old-CLI fallback derived picker ids from providers-map keys (lowercased
display names); a key with a space ('grok build') passed to --provider
was rejected by the app's own validator, erroring the fetch and leaving
stale data under the new filter's header. Exclude keys that cannot
round-trip. (Primary fix is running the current CLI: rebuilt dist.)
- FlameMark: hand-traced inline SVG of the brand flame (~1.1KB, warm
3-stop gradients, useId-scoped), used at splash and sidebar sizes
- Splash: full-window first-load overlay echoing the menubar's
BurnLoadingOverlay ignite moment; 600ms floor so warm cache never
blinks, 250ms crossfade on first data, instant yield on error or
reduced motion, done-latch so it never reappears
- Sidebar logo replaced with the live FlameMark: 1-2% inner-tongue
flicker, 4.5s period, random phase; static under reduced motion
250/250, build green (+2.4KB JS, +1.8KB CSS).
- one gate (motionEnabled): off under reduced-motion, missing matchMedia,
and vitest; every path checks it first
- mount/filter-change only: count-up and bar grow-in key off the
period|provider|range key, so 30s poll refreshes snap values silently
instead of re-animating
- first-load skeleton shimmer replaces bare scanning text (kept sr-only
for screen readers); slide-in toast host for Settings/export feedback
(validation errors stay inline); CSS hover-lift + press micro-
interactions, all with a reduced-motion escape hatch
- gsap 3.15.0 + @gsap/react 2.1.2 (+74KB raw JS; G4 flame work shares it)
244/244, typecheck + build green.
Every var(--t1/--t2/--t3/--hair/--hair2) and secondary legacy color
(--lav/--amber/--red/--mint/--blue/--purple/--cyan) repointed to the
canonical family (--ink/--mut/--mut2/--line/--line2, --warn/--bad/--ok,
--s-*); all legacy definitions and the plain.css alias shim deleted.
The explicit dark/light theme blocks carried drifted legacy literals
(including a pre-contrast-fix --t3), so this also extends the AA
contrast values to all four theme modes, which previously disagreed
with system-light.
grep-proven zero legacy usages. 235/235, build green.
* feat(dashboard): add inline daily history paging
* refactor(dashboard): make daily activity directly scrollable
* fix(dashboard): keep the empty state for users with no history at all
Scrollable mode kept the dashboard shell up whenever full history exists,
but a truly-new user with zero data anywhere saw a zeroed skeleton instead
of the No usage data message. Extract the decision into showEmptyState so
the boundary is test-pinned: empty period with history renders the shell,
empty everything renders the empty state, and the loading pass never
flashes it.
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
- 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.
Replaces the three-rule anomaly card with a Wins/Improvements/Risks
Signals card mirroring the menubar's rule set and thresholds
(FindingsSection.swift): cache >=80% and one-shot >=0.75 and spend-down
>10% and streak >=5d as wins; top-3 optimize findings, low cache <50%,
low one-shot <0.5, retry tax >=25% of spend as improvements; weekday
spike >1.8x, week-over-week >+25%, projected month >1.3x prior month as
risks. Time-comparison rules suppressed under a custom range. Renders
nothing when empty; icons per group, never color-alone. Anomaly test
coverage ported, not deleted.
221/221, build green.
- Settings/General: Daily budget row (Off / USD amount / Tokens),
positive-finite validation, warns at 80% and alerts at 100%
- app-wide one-line banner above every section: --warn at 80%+, --bad
at 100%+, dismissible for the rest of the day
- USD caps compare raw USD against per-provider daily cost (works under
any filter); token caps evaluate only on the all-providers view since
provider-filtered history zeroes token fields (comparing against a
false zero would always pass)
212/212, 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.
* feat(dashboard): add granular usage timelines
* fix(dashboard): readable timeline legend and month-scale buckets
Session legend labels now use the real projectPath's last two segments
(app/web) instead of the sanitized project dir, which rendered as an
unreadable -Users-name-... dump and truncated in the legend.
Hourly buckets now cap at 8 days; longer ranges bucket daily. A 30-day
window rendered 720 overlapping hourly spikes, unreadable as a chart.
The browser no longer gives the backend session_other/model_other
aggregate a top-N slot, which rendered two identical Other legend
entries whenever the backend fold out-ranked a real series.
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
- Sessions: All + per-provider logo buttons (detected, cost > 0) that
drive the same app-level provider state as the top-bar dropdown, so
the two controls can never disagree; aria-pressed group semantics
- Spend By project: rows expand inline (single-open, aria-expanded,
chevron rotation) to the project's sessions: date, top model, calls,
currency-aware cost; date-only strings parsed at local noon so days
never roll across time zones
194/194, typecheck + build green.
* fix(claude): use APPDATA for Windows Desktop sessions
* test(claude): harden withPlatform mock against cross-file leak
The process.platform mock omitted configurable:true and never restored
the value when the property had no own descriptor, leaving win32 mocked
for later tests on the same Vitest worker. Add configurable + an else
branch that deletes the override to expose the real inherited value.
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
A project's first/bootstrap session is a different kind of work (one-off
scaffolding), not an expensive instance of routine work. While a project is
still young its peer sample is thin, so the leave-one-out average is dominated
by a few routine sessions and the founding session's legitimately high one-off
cost trips the >2x outlier multiplier. Exclude each young project's earliest
costed session from the outlier finding only; mature projects are unaffected so
genuine outliers still surface. Young = fewer than 2x MIN_SESSIONS_FOR_OUTLIER
costed sessions.
Fixes#664
- 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.
yield always analyzed all providers, so provider-filtered GUI panels
(cost per outcome, reverts, abandoned) showed all-provider numbers
under a filtered header.
ProviderCost now keeps the internal id alongside the display name. The
providers map keys stay lowercased display names (byte-identical, the
Swift menubar decodes them); new current.providerDetails exposes
{id,label,cost} so GUI clients can round-trip a valid --provider value.
New top-level currency {code,symbol,rate}: payload values remain raw
USD, clients convert at display time like the Swift menubar does.
- Provider filter: Models-this-period sources current.topModels (history
days carry empty topModels per provider); StackedBars falls back to a
cost-proportional single segment like the Swift menubar
- Chart windows align to the CLI (week -7, 30days -30, all = 6 months)
and zero-fill a contiguous calendar window; history.daily is sparse and
both sides key local YYYY-MM-DD, so lookups are safe (verified on real
CLI output)
- Custom range threads into Overview panels (chart, models table) and
suppresses MTD/projected and vs-last-week comparisons; Compare states
that custom dates fall back to the period
- usePolled clears errors per attempt and sections show a StaleBanner
instead of silently rendering last-good data after a failed refresh
- Cache-hit denominator matches the CLI; hero savings relabeled 'Saved by
applied fixes' + new 'Saved via local models' line; formatCompact
everywhere; Optimize 'Fixes' count matches the rendered list
App suite 163/163 (was 147), root 1613/1613.
Compare defined cache-hit as read/(input+read+write) while menubar-json
and every other surface use read/(input+read), so the same model showed
two different numbers depending on screen. Standardize on the latter;
TUI compare output changes intentionally.
The desktop app's getOptimizeReport handler already appends --from/--to
when a custom range is active; without these options commander rejects
the invocation and the Optimize screen errors under a custom range.
v1 modern execution files carry the same metered credits as v2
(usageSummary[].usage, unit "credit" — the predecessor of v2's
promptTurnSummaries), but the parser only harvested usedTools from
that array and priced executions from estimated tokens. Since v2 only
ships in brand-new IDE builds, v1 is the format nearly all Kiro IDE
users are on today. Sum usage across usageSummary entries and price at
the public overage rate.
Align all three credit parsers (CLI, v1, v2) on one fallback contract —
gate on summed credits > 0 and fall back to token-estimated pricing
with costIsEstimated: true:
- CLI turns without metering_usage were priced at a frozen $0
- CLI turns with an EMPTY metering_usage array (75 of 10,105 real turn
metadatas — meta written before metering lands) passed the truthy
presence check and froze $0 marked as real cost
- legacy .chat calls now carry costIsEstimated: true, which was always
the reality but never stated
Validated against a real machine: 221 of 277 v1/legacy calls now carry
metered cost ($20.11) instead of token estimates that had overstated
them ~5x; verified usageSummary values are per-turn amounts, not
cumulative counters (sequences fluctuate). Call counts unchanged before
and after — nothing gained, lost, or double-counted.
Also document the companion-file fingerprint blind spot at
fingerprintFile: kiro CLI credits and v2 modelId live in companion
files the single-file fingerprint never sees, so a parse racing the
companion write can cache fallback values that only self-heal while
the transcript keeps changing.
AI-Origin: ai-generated
AI-Tool: kiro
providerCallToCachedCall drops provider-computed costUSD unless the
provider is allowlisted, and cachedCallToApiCall then re-prices from
token counts. Kiro's cost now comes from metered credits, which token
pricing cannot reproduce — reports were understating real kiro spend
~18x (e.g. $130.72 of metered CLI credits reported as $7.20 of token
estimates). Add kiro to the pass-through allowlist, same as the other
platform-billed providers (mistral-vibe, devin, hermes, ...).
Both persistence layers froze pre-fix dollar values and neither
self-invalidates (historical session files never change), so bump:
- session cache: CACHE_VERSION 4 -> 5 (cached kiro entries carry
costUSD: undefined and would keep being token re-priced forever)
- daily cache: DAILY_CACHE_VERSION 10 -> 11 with MIN_SUPPORTED_VERSION
raised (finalized days carry token-estimated kiro costs off by up to
16x per model), same as the v10 cursor precedent
Test: end-to-end through parseAllSessions — a CLI fixture with 2.5
metered credits must report $0.10; fails with the token re-price
($0.000048) when the allowlist entry is removed.
Docs: document the pricing contract in docs/providers/kiro.md — metered
credit cost is frozen at parse time, so price-override / model-alias
do not affect kiro dollar amounts (shared tradeoff of all allowlisted
providers).
AI-Origin: ai-generated
AI-Tool: kiro
Kiro's new IDE builds write sessions to ~/.kiro/sessions/<hash>/sess_<id>/
(session.json + messages.jsonl event log) instead of globalStorage. Add a
v2 parser and discovery, plus fixes surfaced while validating against a
real upgraded machine:
- Parse v2 event-sourced turns (user/turn_start/assistant/tool_call/
tool_result/usage_summary/turn_end) with per-execution dedup keys
(kiro-v2:<session>:<execId>) and defensive flushes for out-of-order
events and in-progress sessions
- Price turns from real metered credits at the public overage rate
(USD_PER_KIRO_CREDIT = $0.04/credit, individual plan: $20/mo for
1,000 credits), falling back to token estimation only when a turn has
no usage_summary (aborted/in-flight); costIsEstimated distinguishes
the two
- Fix pre-existing CLI parser bug: metering_usage values are credits,
not dollars — costs were overstated 25x at the real rate
- Count v2 tool_result content as input context (matching the CLI
parser's ToolResults treatment); previously only the user prompt was
counted, undercounting input ~25x on agentic turns
- Keep reasoningTokens disjoint from outputTokens: downstream
aggregation (models-report, audit-report, parser) sums the two
fields, so folding reasoning into output would double-count. Combine
them only for the token-pricing fallback, matching codex/gemini
- Take the real modelId from session.json, so v2 sessions are not
mislabeled kiro-auto
- Extract parseWorkspaceSession from the inline createParser block for
parity with the other four format parsers
- Add v2SessionsRootOverride and stop deriving the v2 scan root from
the cliDir parent when only agentDirOverride is set (tests were
scanning the system tmpdir)
- Tests: v2 parsing/discovery/dedup/routing, credit vs fallback
pricing, tool_result turn scoping, reasoning disjointness, cli-dir
skip guard, and a mixed-format coexistence suite (legacy .chat + v1
executions + workspace-sessions + CLI + v2 on one machine) asserting
exact aggregate counts and no double counting
- Docs: v2 store layout, credit pricing, dedup namespaces, and the
disjoint-store verification (v1->v2 is a clean cutover, no
migration/dual-write observed)
AI-Origin: human
Before-merge items:
1. HTTPS enforced on every remote endpoint (RFC 8252 §8.3): baseUrl,
issuer, authorization/token/revocation endpoints, and the traces
endpoint all reject non-https, with a loopback (127.0.0.1/::1/
localhost) exception for offline tests and local dev. Enforcement is
central (assertHttps) — the browser-open guard is no longer the only
check whose failure was swallowed.
2. Credential store test isolation: CODEBURN_SYNC_TOKEN_STORE=file
forces the file store (honors HOME) so the offline suite never
touches the real macOS login keychain. Set in the e2e suite.
3. Golden pins for deriveSpanId/deriveTraceId/deriveDeviceId with fixed
inputs and expected hex — the idempotency contract depends on these
encodings being stable across releases; a green-tests encoding change
would silently double-count history on span-ID-keyed backends.
getDeviceId refactored over a pure deriveDeviceId(host, user).
Smaller review items:
- Callback server: ready promise resolves the actually-bound port from
the listening event (kills the 100ms-sleep race after port fallback);
Connection: close on all responses + closeAllConnections() on
shutdown (pooled keep-alive sockets from a closed server could
swallow requests aimed at a later server on the same port); error
handler guarded so a post-bind error can never rebind to a different
port than advertised; optional ports param ([0] = ephemeral) removes
fixed-port contention between parallel test workers.
- fetchOidcConfig verifies the issuer claim matches the fetch origin
(OIDC Discovery §4.3 mix-up defense).
- partialSuccess.rejectedSpans wrapped in Number() — proto3 int64 JSON
mapping sends strings from strict protojson servers; += would
concatenate.
- Ledger writes are atomic (temp + rename); corrupt-ledger recovery and
no-tmp-left-behind tests added; XDG_CACHE_HOME honored (ledger is
reconstructible state, not config).
- Mock IdP now implements /oauth2/authorize (registers PKCE challenge,
302s to redirect_uri) and verifies S256 code_verifier + single-use
codes at the token endpoint. The e2e drives the real redirect flow
and asserts wrong-verifier and code-reuse are rejected — PKCE binding
is now exercised end to end.
- sync reset calls clearLedger() instead of reimplementing the path.
- push sets exit code 1 on rate-limited/server-error outcomes so cron
and script callers can detect incomplete pushes.
Deferred (noted for fast-follow): macOS 'security -i' stdin mode
(untestable on this Linux box), ai.cost_estimated as a real
ParsedApiCall flag (touches core parser types).
Sync suite: 81 passing (5x stable), 5 developer-only.
AI-Origin: human
contiguousDailyWindow rebuilt a calendar window from the client clock and looked
up days by date key; on real CLI data every lookup missed and zero-filled, so the
Spend chart went empty and the Overview chart went flat. Use the real last-N
backfilled entries of history.daily directly (Spend last-15, Overview
last-max(30, periodDaily)) and remove the dead helper.
typecheck clean; 147/147 tests pass.
Replace the $-budget model as the primary view: per-provider cards (Claude/Codex)
from getQuota show the real 5-hour/weekly windows (% used, resets-in, severity
colors), the auto-detected subscription tier, connection states, and the credit
footer. Manual budget plans demoted to a secondary section.
typecheck clean; 148/148 tests pass. Needs a full relaunch for live quota.
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.
- extract contiguousDailyWindow + formatChartDate to lib/period (shared)
- Spend "Daily spend by model": fixed contiguous 15-day window (zero-filled)
with an .ov-xax date axis, matching the Overview chart
- remove the horizontal gridlines from both charts (they read like data lines)
typecheck clean; 129/129 tests pass.
By-project and the Activity/Tools/MCP/Subagents breakdown panels cap at 300px
with a slim internal scrollbar, so they stay a consistent compact size instead
of growing unbounded with the data.
typecheck clean; 126/126 tests pass.
Drop the 11px stacked-segment cap so the bars fill their columns like the
Overview .chart; render only the last 15 days.
typecheck clean; 126/126 tests pass.
Drop the Waste/Reverts/Abandoned/Fixes tabs. One scrolling page: findings
(count + potential in the header), then Reverted and Abandoned session sections;
empty sections hidden. The redundant Fixes tab folds into the findings header.
typecheck clean; 126/126 tests pass (removed 2 now-moot tab-isolation tests;
impact-badge + section coverage preserved).
Drop the Projects/Activity/Tools/MCP/Subagents lens tabs. Everything shows on
one scrolling page: chart + By-project, Cost-flow Sankey, then the breakdown
sections in a responsive grid. Empty sections are hidden. All reflects the
selected date range.
typecheck clean; 128/128 tests pass.
Drop the FuelRing (and its now-unused plans polling) from the ov-stats3 card;
the stats row is now Month-to-date + Projected month (2 equal columns).
typecheck clean; 128/128 tests pass.
- new Dropdown component (our .pop menu style, keyboard-accessible) replacing
every native <select> (currency, period, plan preset, export, Compare pickers)
- expand currencies to 30 common ISO codes (engine handles any valid currency)
- fix the Settings pane so rail + content anchor top-left and fill the area
typecheck clean; 128/128 tests pass.