* feat(dock): one Claude ring per config directory, with its own sessions and today (#1523)
Rate limits are per account, so a machine with a work and a personal
~/.claude cannot be served by one Claude ring. When the CLI knows two or
more Claude config directories:
- `codeburn quota --format json` adds a top-level `claudeProfiles` array,
one entry per directory (`providers[]` unchanged). Each entry carries
the directory's own windows, read from its own `<dir>/.credentials.json`
(the macOS Keychain fallback stays reserved for the default ~/.claude),
plus that directory's own today totals from a single Claude-only,
cache-backed parse of today's range, filtered per source id exactly like
the menubar's per-config payloads.
- Live sessions carry `claudeConfigSourceId`, so a per-profile ring claims
only its own sessions. Claude Desktop sessions keep their own bucket id
and belong to no profile.
- The Windows Capacity Dock gains a `claudeProfiles` preference (combined,
default; separate draws one captioned ring per directory). Profile rings
keep addressing the `claude` provider id for the provider set, glyphs,
colors and the connect button; the settings row appears only when the
CLI reports more than one profile.
- Captioned rows are taller by two new metrics (caption line + gap),
mirrored in TS and Rust: the page tells Rust the rail is captioned in
the layout request, and the frame carries the effective row extent the
hit-test uses. Non-captioned rails are pixel-identical to before.
- The desktop app's shared dock-prefs schema parses and sanitizes the new
key, so its pane cannot strip it from windows-dock.json.
Closes#1523
* review: opt-in profile reads, today timeout, missing-dir filter (#1570)
- `quota --claude-profiles` gates the per-directory reads: without it the
payload lists profiles ({id,label,path}) only, so a combined rail and
WSL users pay nothing; the Windows dock passes the flag only while its
claudeProfiles preference is separate, and an unknown option from an
older CLI reads as CliOutdated.
- The default ~/.claude profile reuses the claude provider row's result —
no second request, no second macOS Keychain read.
- claudeTodayBySource races the same timeoutMs and answers an empty map
on loss, so a silent child cannot cost the dock every provider's quota.
- Missing config directories are filtered before the >1 check.
- `captioned` applies only while a Claude ring is in the selected set;
profile rows tolerate a list-mode answer; comments trimmed; merge with
#1560 keeps both keepExpanded and claudeProfiles.
* test(quota): set USERPROFILE for the default-dir check; drop issue refs
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
Updates, Codex banked resets and early quota resets each had their own
one-toggle Section in General. Three sibling headers for three checkboxes
is not an information architecture. One Notifications section, three
toggles, each keeping its own help text and defaults key.
Also assert the early-reset notification identifier does not carry
UpdateChecker's prefix, so the delegate's tap guard cannot install an
update from it.
The Capacity Dock design doc gains the band, the switch and the two new
code boundaries; the Claude provider doc explains what the detection
reads, how the local history is derived from stored reset times, and why
it stays quiet, so the next person changing the snapshot store knows what
depends on it.
Refs #725, rather than Closes: that issue is a five-part epic and this is
one slice of its surfaces, so it should not close on this PR. No issue
tracks early-reset detection on its own.
Ports the two parts of #1287 the maintainer asked to keep, expressed
through this branch's `QuotaPacePresentation` rather than through
`QuotaPace.Verdict`:
- Placement: `QuotaDetailRow` in the agent-tab quota hover card becomes
a VStack and draws the pace line under the bar, indented past the
label column, with the three tones the dock already uses. The card is
fitted, so a silent caption collapses there instead of reserving a
slot the way the dock's computed frame must.
- Wording: the long-window caption drops "est. N% at reset" /
"est. out in X" for the plain "Lasts until reset" / "Runs out in X",
and the exhausted state becomes "Limit reached". The projected
percentage it came from stays in the hover text, which has room for
the reasoning. Short windows keep the on-pace / deficit / reserve
stage (#726), and the freshness, connection and duration gates are
untouched.
`QuotaPace.inferredWindowSeconds(label:)` is deliberately NOT ported.
Grok Build chooses its window label from the distance to the reset, so
a monthly cycle passing through the 4-12 day band is labeled "Weekly"
and a label round trip paces a month's budget against seven days. The
adapter's real `windowSeconds` is the only source here, and a new test
pins that shape: 72% used with six days to a monthly reset reads
"Lasts until reset", while the same sample against an inferred 7-day
window overflows and would have printed "Runs out in 9h 20m". The same
test asserts a window with no duration metadata stays silent rather
than falling back to its label.
Verified with a standalone swiftc harness over QuotaPace,
QuotaPacePresentation and QuotaSummary: this host has Command Line
Tools only, so `swift test` cannot load the Testing module.
Closes#1267
The resting rail shows one number per provider, and that number was always
the weekly (else monthly) billing window. A user watching Claude's 5-hour
limit had to expand the dock and read the glance card every time, which
defeats the point of an always-visible usage monitor.
Clicking the provider that already rests in the rail now switches its gauge
to the provider's short rolling window and back. The preference is stored per
provider rather than as a Claude-only switch: the dock's collapsed row is
provider-agnostic, and Codex publishes a 5-hour and a weekly window too, so
the question is "which horizon for this provider", stored under
CodeBurnCapacityDockGlanceWindows alongside the other dock preferences.
The two horizons are named rather than labelled, so the policy works for any
adapter: billing reuses QuotaSummary.headlineWindow, the app's single
definition of the weekly/monthly glance, and burst matches the short rolling
rows the adapters emit (5-hour, Hourly, Daily, session) while excluding
anything weekly or monthly, so Claude's per-model "Weekly · Opus" row can
never be read as a short window. CapacityDockGlanceWindow keeps that policy
pure and testable, including the fallbacks: a provider with one window shows
it whichever horizon is stored, and a stored horizon the provider stops
reporting resolves to the one it does report instead of blanking the gauge.
The click cannot fight the hover expansion: this branch only ever pins, never
collapses, and it is reached only when the provider actually has two windows,
so a single-window provider keeps the existing click-to-pin toggle. Escape
and an outside click still unpin. Keyboard and VoiceOver users get the same
switch as a named accessibility action on the provider cell, and the cell's
accessibility value now names the window feeding the ring. The rail's
geometry is untouched.
Closes#1243
* feat(menubar): add native capacity dock
* fix(menubar): harden dock interaction boundaries
* feat(menubar): polish native Capacity Dock for launch
- Flush top-edge docking via NSHostingView safe-area opt-out; shielding
window level so the rail necks into the physical top like the system notch
- Liquid Glass legibility scrim so labels stay readable on light wallpapers
- Weekly-else-monthly headline window; unified severity colour bands
(green <50 / yellow / orange / red) across rings and detail bars
- Squircle gauge + graphite theme defaults; auto-show connected
subscriptions (up to 5) until the user edits the set, then respect it
- Capacity Dock Settings menu item; re-centre the settings window; info
glyph for the About row
- Trim dead provider-credential override fields down to apiKey only
- Reconcile and extend tests for all of the above
---------
Co-authored-by: iamtoruk <hello@agentseal.org>
Round 3 adversarial-panel fixes for PR #999:
- Snapshot writes now go through a per-queryKey file plus a CAS re-read
guard before rename, closing the last-writer-wins race where a slower
recompute against an older corpus could clobber a fresher save, and
where distinct queryKeys used to evict each other's single shared slot
(B-G1).
- cline-cli's discoverSessions now fingerprints the growing
<sessionId>.messages.json sibling instead of the static
<sessionId>.json metadata file, so new turns in a live session are no
longer invisible to computeCorpusFingerprint (C-G1).
- computeCorpusFingerprint folds in computeEnvFingerprint per discovered
provider, so non-discovery env vars like CODEBURN_CURSOR_MAX_BUBBLES
and KIMI_MODEL_NAME can no longer serve a stale-forever snapshot (A-G1).
- collectFilesRecursive now resolves symlinked subdirectories and
recurses into them (with a visited-inode guard against cycles) instead
of misclassifying them as leaf files (C-G2).
- Added tests/session-cache-status-snapshot.test.ts covering concurrent
writers against the snapshot file, mirroring session-cache-shards.test.ts's
existing coverage for the main cache (D-G9).
Responds to ozymandiashh's CHANGES_REQUESTED review on #999.
Structural: kept the disk status-snapshot, scoped to serve's documented
CLI-spawn-fallback path, after bringing its invalidation to parity with
serve's own in-process memo (see docs/design/perf-cache-fix.md for the full
reconciliation reasoning against #972's serve.ts/watcher invalidation,
already merged into this branch's ancestry).
Correctness fixes:
1. queryKey now includes the same four pricing-config hashes parser.ts's
own cacheKey deliberately includes (proxyPaths/modelAliases/
priceOverrides/localModelSavings), so an edited pricing config
invalidates the snapshot instead of waiting for an unrelated corpus
change.
2. computeCorpusFingerprint now gives network providers (e.g.
vercel-gateway) a now-stamped synthetic fingerprint entry every call,
mirroring parseAllSessions' own treatment of the same sources, instead
of letting them contribute nothing to the hash.
3. computeCorpusFingerprint generically expands any directory-shaped
non-Claude source (e.g. mistral-vibe) via a new collectFilesRecursive
helper, instead of only Claude getting leaf-file expansion.
4. saveStatusSnapshot is now gated on isSessionHydrationComplete(), so a
degraded read-only parse can no longer be checkpointed as authoritative.
5. The settle-window defer in loadStatusSnapshot now anchors on wall-clock
time since the fingerprint FIRST mismatched (persisted via a new
mismatchFirstSeenAt field) instead of the corpus-wide newest mtime,
which fix#2 can otherwise keep perpetually "recent."
Also fixes a 6th instance of the same defect class as #1, found while
root-causing a test failure: the query key omitted the active display
currency, so switching currencies kept serving stale numbers. Updates
tests/parse-workers.test.ts, whose "warm" assertion assumed every
menubar-json call does a full parse — no longer true within the settle
window after this PR's own snapshot feature.
vitest's default glob reached the Electron app's specs under app/, which carry their
own vitest config and their own jsdom in app/node_modules. From a root install that
fails with ERR_MODULE_NOT_FOUND: jsdom, so the command CONTRIBUTING documents and the
one RELEASING.md names as the pre-release gate both error out.
Move the scoping CI already applies into package.json: test runs tests/ minus the
parallelism-sensitive cache-refresh-lock suites, test:locks runs those three serially,
test:watch keeps watch mode at the same scope. The first two are byte-identical to the
invocations .github/workflows/tests.yml spells out, so the workflow can be pointed at
the scripts to stop the two drifting apart again; that edit is left out of this PR so it
needs no workflow permissions. test plus test:locks together still cover all 192 files
under tests/.
Scoping the script changes what a trailing path argument means: vitest ORs positional
filters, so 'npm test -- tests/providers/hermes.test.ts' would no longer narrow to that
file, it would run the whole suite. Rewrite those to 'npx vitest run <path>' everywhere
they appear - four provider guides and the MCP design plan, thirteen lines in all.
Also refresh the stale test docs: 42 files/568 tests (now 192 under tests/), the
per-directory counts, the line claiming vitest does not run in CI which stopped being
true when tests.yml landed, and the provider test-gap list, which still named
antigravity and gemini after both gained test files.
Record the cache-refresh-lock naming convention in CONTRIBUTING, since the split makes
it load-bearing: a lock test that misses the prefix runs under the full worker pool and
flakes, and one that matches it but is absent from test:locks never runs at all.
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.