* fix: open menu bar Settings centered on the active screen
The window was positioned at a 520x380 placeholder while SwiftUI's own
minimum is 880x620, so the grown window kept its top-left corner and ran
off the bottom-right edge. Size the window from the view's minimum, place
it with a tested pure function, and honour a saved frame only while it
still fits on one of today's screens.
* fix: say where the telemetry toggle is managed when the desktop app owns it
A desktop telemetry.v1.json with enabled:false locks the menu bar toggle
off, so clicking it does nothing. Name the desktop app's Privacy & data
pane in the caption, and pin the four desktop/veto combinations plus the
standalone case in tests.
* fix(desktop): export the provider id, and never swallow a rejected setter
The export pane built its provider list from `current.providers`, which is
keyed on the lowercased display name. Five of the seventeen providers on a
real corpus ("cursor agent", "grok build", "kimi code", "hermes agent",
"grok bot") are not valid ids, so vProvider rejected them as bad-args.
`exportNow` had a try/finally and no catch, so the rejection produced no
toast and no file: the button did nothing. Use detectedProviders, which
already resolves ids from providerDetails, and toast on rejection.
The telemetry consent toggle swallowed its rejection the same way.
* fix(desktop): warm only the selected period, and only while visible
The post-paint sweep warmed all six standard periods x seven reports plus
the provider overviews: up to 48 serve-routed queries, ~18 CPU-seconds a
minute for five minutes and 2.79 GB peak RSS on a real corpus, and it ran
with the window hidden. It now warms the selected period's reports (its
own overview is already under that memo key from the visible poll) and
the other providers' headlines for that same period, holding before every
request while a user fetch is in flight or the window is hidden. Other
periods take the ordinary on-demand path.
Also corrects two comments that claimed background priority lets a click
jump ahead: spawnCli takes the serve fast path before priority is read,
and the resident child answers FIFO.
* fix(desktop): answer the click, report the write, tell the truth about the Claude login
Nine bridge setters in Settings attached only .then(), so a rejected
envelope produced no toast and the control read as if it had ignored the
click. One shared helper toasts the rejection, per pane fallback wording.
Telemetry save() swallowed write errors while setEnabled returned the
in-memory status, so the UI could show ON while the file the macOS menu
bar app reads still said OFF. The setters now carry persisted:boolean and
the renderer toasts when it is false; the write goes through the existing
atomic writeFileAtomic (temp file, rename) so a crash cannot tear it.
The Claude card called a logged-in Claude Code 2.x user "disconnected"
forever, because the credential lives only in the keychain and the first
poll never reads it. A new darwin-only keychainUnchecked state says we
have not looked yet and offers the forced read, keeping the login
instructions for the genuinely-logged-out case. ConnectAffordance's
toggle is renamed to what it does: it discloses instructions.
* fix: retry tax is the cost of retried edit turns, not a multiple of spend
* stop the change guard watching a Warp path it can never stat
* apply a menu bar language change in place instead of relaunching
* keep honoring WARP_DB_PATH in the change guard
* restart a menu bar too old to watch AppleLanguages
* one rebuild per language change, and no global bundle in tests
* fix(desktop): a forced quota refresh must not join the poll it just aborted
Check now / Refresh / ⌘R invalidated the quota service and then handed the
caller the background fetch it had just aborted, so every provider failed
its generation check and resolved disconnected: the click repainted every
card as logged out without ever reading the keychain, and Claude showed
"Not connected. Log in with the Claude CLI." A call now joins an in-flight
run only when that run is at least as privileged, so a keychain-allowed
call starts its own; a second forced click still joins the first, which
keeps the single macOS prompt. A superseded run hands its caller the run
that replaced it instead of its own abort artefacts.
retainOnFailure also holds accessDenied through a keychainUnchecked poll,
so the "Allow in keychain" guidance is not replaced by "Check now" one
cycle after the user denied the prompt.
* fix(desktop): stop the sweep churning, and never spend a query on an abandoned one
The prefetch effect depended on the detected-provider entries, whose costs
move on every overview poll, so it was torn down and restarted each cadence
tick: a warm slower than the interval was discarded and re-issued forever.
It now depends on the provider ids alone, and a finished warm is kept for
its own period/provider key even if the effect was torn down meanwhile —
only a config-generation change makes a result wrong.
Both loops re-test cancellation and the memo after the hold, which has no
bound, so an abandoned sweep issues nothing and a section the user visited
meanwhile is not parsed again. The hidden half of the hold waits on
visibilitychange instead of waking the renderer every two seconds for a
window nobody is looking at.
* fix(desktop): never offer a provider id the CLI will reject, and keep the keychain note
detectedProviders applied the round-trip filter only to the legacy
providers map; a providerDetails id with a space reached the picker and the
export as a --provider value the main process rejects.
The Plans settings row showed the "not read your login yet" line without
the note that macOS may ask once for keychain access, which the Plans card
does show.
The desktop app and the macOS menu bar can now run in English, French, Japanese, Korean, Simplified Chinese and Traditional Chinese. A Language setting in the desktop Settings picks the language or follows the system, and the choice is shared so the menu bar switches with it, relaunching to pick up the language with a retry so a switch never leaves it down.
Every user-facing string in the desktop renderer goes through a local lookup table, 1,249 keys per language, no framework and no network. The menu bar gains four .lproj to join English and Simplified Chinese, 626 keys each. A completeness gate fails the build if any language is missing a key, in both the desktop catalogs and the menu bar coverage suite. Fonts fall back per language so Japanese, Korean and both Chinese render their correct glyph forms, and dates and numbers format for the locale with values unchanged. English output is byte-identical.
Strings produced by the CLI engine stay English until the CLI is localized, a separate effort. Validated live in all six languages: no missing text, no clipping, correct fonts, numbers unchanged, and the menu bar switching for every language.
The Codex early-reset detector dropped additional-limit windows that were at 0 percent used before checking them, so a per-model window that reset early to 0 percent was never recorded or notified; those windows now reach the detector while the popover stays compact. The Capacity Dock reconnect text was a fixed size while its reserved height scaled with the dock, so it clipped at the 0.9 minimum scale; the text now scales with the dock and the reserve is measured at the scaled font.
The reconnect and disconnected blocks fell through an unpadded branch and ran to the panel edge, the action button sat in the corner with no inset, and a worst-case height reserve parked about 40pt of dead space above it. The connection block is now measured from the strings it wraps, every state goes through the padded notice section, the Today amount and its token column share a baseline, and a plan label that only repeats the provider name is dropped.
The catalog comparison matched specifier lists, so a key whose
placeholders are all %@ passed even when the Chinese value bound the
arguments into the wrong slots — the drift that shipped in three
early-reset keys and was only caught by rendering real data bilingually
(#1331).
All 42 multi-argument keys in both tables now use positional specifiers
(%1$@, %2$lld, ...); the parity check compares slot to verb so Chinese
word order stays free while each slot keeps its type; a new test fails
on any multi-argument key that is not positional.
The source scanner closes its three expression gaps: ternary branches,
concatenated literals, and let-bound literals passed by name.
Text(verbatim:) stays the explicit opt-out; the four provider-name
fallbacks the new rules surfaced in HeatmapSection route through the
catalog as verbatim entries.
Closes#1331
A Capacity Dock row that vanishes while hovered fires no hover exit, so the
pushed pointing-hand cursor stuck system-wide; the row now pops it on the way
out. Claude Code writes a fresh sessions/<pid>.json on every resume, so several
files name one session and the first listed was taken; the newest is taken now,
and a newest that fails the recycled-pid guard leaves the row inert.
collectKimicodeInputs recursed the whole Kimi session store to find wire files,
listing ~18k entries for ~2k wires at ~250ms per payload build. It now walks
work dir, session and agent levels only: ~23ms, and a decoy wire.jsonl under
blobs/ or file-history/ no longer reports itself as a session.
Clicking a session row finds the row's process — Claude Code writes its
pid into ~/.claude/sessions/<pid>.json, Kimi Code is found by the one
`kimi` working in the directory its state.json records — walks the
process tree to the application that owns it, selects the tab by tty in
Terminal.app and iTerm2, and raises the app everywhere else. The
AppleScript runs off the main actor with a 3s cap.
A row whose process cannot be found keeps its layout and stays inert: no
pointer change, no tooltip.
The notification, the Plan-tab detail and the hover-card line #725 added were
composed in a presentation type and shipped English to a zh-Hans user. Each is
one key now. reset_type is vendor data with no fixed vocabulary, so the four
cadence words OpenAI sends are translated and anything else rides through
verbatim inside a translated frame. compactAge stays character-identical with
the TypeScript side in English, which is what the contract was about: en is an
identity table. The dock's cache-read tooltip was bound to a let before its
call site, which is why the scan never saw it.
Rebase resolution for #1289 onto main (#1306, #1313, #1314, #1315, #1328,
#1329). Conflicts kept both intents: main's second-row title composition,
glance-window accessibility actions, cache-read line, host field and merged
Notifications section keep their behaviour and now resolve through L(…).
Adds 14 keys to both tables, drops 4 that no longer have a call site, and
retranslates the two Copilot explanations main rewrote.
Groundwork for #1219. The menubar app hardcoded ~530 English strings across
Views/, the presentation structs, the status-item menu and the update alerts,
so there was nothing for a translator to translate.
Every user-facing literal now goes through `L(_:)` / `L(_:_:)`, which look the
string up in `Localizable.strings` in the SwiftPM target resource bundle. The
key *is* the English copy, so English stays the development language and a
missing translation degrades to correct English instead of a dotted
identifier. `en.lproj` is therefore an identity table; it exists so the bundle
advertises `en` and so a translation can be diffed against it.
Why the lookups are explicit rather than relying on SwiftUI's implicit
`LocalizedStringKey`: SwiftPM emits target resources into a sibling bundle
(`CodeBurnMenubar_CodeBurnMenubar.bundle`) that the packaging scripts copy into
`Contents/Resources`. `Bundle.main` has no `.lproj` at all, so `Text("literal")`
would always miss. Naming `Bundle.module` is the one form that resolves the
same way in `swift run`, in `swift test` and in the packaged `.app`.
Enum raw values that double as identity (`Period`, `MenubarScope`,
`InsightMode`, `AccentPreset`, `ProviderFilter`) keep their raw value and gain
a `displayLabel`, so persistence and cache keys are untouched by translation.
Three display-only date formatters that were pinned to `en_US_POSIX` with fixed
patterns now use `setLocalizedDateFormatFromTemplate`, and the calendar
popover's weekday row comes from the locale's own symbols. The `yyyy-MM-dd`
formatter stays POSIX: it parses and builds data keys, not display text.
Not extracted, deliberately: provider, model and plan names; units; currency
codes; shell commands and paths; quota window labels that policy code matches
on by English substring (`QuotaSummary.headlineWindow`); and anything the
`codeburn` CLI produces.
Also copies the SwiftPM resource bundle into the app in build-local.sh, using
the named-bundle + `[[ -d ]]` pattern package-app.sh already uses. Without it
the assembled app finds no strings table (and already trapped on first icon
load, the resource-bundle half of #1262).
Detection rides the refresh lifecycle that already runs: it happens where
each successful Claude fetch is captured into the snapshot store, before
the store is written, so it costs no request. Issue #725 names "no new
network polling loops" a hard non-goal, and this adds none.
Three surfaces, because the user can be looking at any of them. A system
notification names the provider, the window and the lead. The Capacity
Dock carries a band saying the same for twelve hours after the reset was
seen, drawn and reserved exactly like the staleness notice, because the
panel's frame is computed rather than fitted. The quota hover card gains
this Mac's own pattern next to the pace captions.
The state before the fetch decides whether the stored baseline can be
compared against it: a provider coming back from a terminal failure, a
fresh bootstrap or no credentials has a baseline from the far side of a
gap, where stale state looks like a jump. A window the API did not report
this time is passed as absent rather than omitted, so a window that comes
and goes is never read as a reset. Disconnect drops the baseline along
with the snapshots, so a reconnect under another account starts clean.
The Settings switch is on by default, and turning it off withholds only
the notification — the dock band still appears, since it is a caption on
data the user is already looking at rather than an interruption.
Reserving the pace slot only while connected made the panel relayout on
every refresh: the slot collapsed and the staleness band appeared. The
reservation is a property of the window shape, so it no longer asks the
connection. Windows block on the real Claude shapes, connected vs stale:
91/77 -> 91/91 at one and two windows, 164/136 -> 164/164 at three.
A Claude or Codex summary now reads stale on sample age alone, and the
manual cadence never refreshes, so "Last known usage - refreshing" and
the Settings "Refreshing..." became permanent lies. Both now ask whether
a fetch is actually in flight.
Main took render revision 6 for `sessionCountBasis` and changed the
provider-slice session fold from a sum to a per-day max. Both had to
survive the merge, so the resolutions are unions rather than sides:
- `addProviderSlice` keeps main's `Math.max` for `sessions` (per-day
ticks are not distinct identities) and adds the cache-read sum beside
it, with a comment saying why one is additive and the other is not.
- `ProviderSliceTotal`, `ProviderCost`, `providerDetails` and the Swift
`ProviderDetail` carry both `sessionCountBasis` and `cacheReadTokens`.
- The snapshot revision stays 7, which is still the next free number
above main's 6. Its doc comment said v6 was PR1265's per-model counts;
it is main's `sessionCountBasis`, so the comment now says that.
The status-menubar suite hard-codes render revisions. Main's assertion
that a rejected v5 snapshot is rewritten as `:render-6:` auto-merged
cleanly while this branch moved the current revision to 7, which would
have failed silently on the next bump too. Those assertions now derive
the tag from `STATUS_SNAPSHOT_RENDER_VERSION`, so the current and
previous revisions follow the constant; the deliberately historical
`:render-5:` and `:render-2:` plants stay literal.
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
Nil-quota Capacity Dock used generic OAuth retry copy. Explicit
Disconnect leaves credentials, so reuse the existing disconnected
Settings detail for Copilot only.
The Capacity Dock's glance popover rendered the machine-wide Today block in
every provider's card, so hovering Claude showed the total across every
provider as if it were Claude's.
CLI (add-only): ProviderCost and providerDetails now carry optional
inputTokens, outputTokens and sessions, summed per provider in all three
branches that build the provider list. They stay ABSENT when no day in the
period reported a breakdown, so a consumer can tell "no data" from zero.
STATUS_SNAPSHOT_RENDER_VERSION 4 -> 5, since a v4 snapshot's payload predates
the new fields; the daily cache already holds the tokens, so its version is
untouched.
App: the Today row reads the hovered provider's row out of providerDetails
(matched on the CLI provider id, which the dock's kimi entry bridges to
kimicode). Against a CLI that omits per-provider tokens the arrows drop out
rather than falling back to the global pair; a provider absent from a
breakdown that IS present reads as a real $0.00. The row keeps its fixed
frame, so the panel's computed height is unchanged either way.
The line reading "Last known usage · refreshing" sat crammed against the
header with the blocks below pushed into it. Its height was reserved, but
only as 16 points: a bare 10pt line box with no padding, no side inset of
its own and no separator, while every other block in the glance carries
8 or 10 points of padding and a 16 point inset.
It is a section now, with the same inset, its own padding and a divider,
and the reservation grows from 16 to 27 to match so nothing below is
squeezed. Only the two in-progress states change; disconnected and
reconnect keep their own blocks with the action button.
The new test pins both halves: the reservation equals the band's height
for every in-progress connection, and the total stays whole at every
scale, since a fractional panel height re-runs the window's layout at
display cadence for as long as the dock is up.
The glance popover replaced the row-counted one while this branch was
parked, so `detailHeight` grew three parameters and this branch's copy of
the old body went with main's. The branch's assertion that the popover's
height is whole is now covered better by CapacityDockGlanceTests, across
every scale and against the new signature, so it goes rather than being
ported.
What the branch's remaining test caught is a live regression on main. Every
dock metric must be a whole number of points: a fractional panel length
re-runs the window's layout at display cadence for as long as the dock is
up, which is the drain #1210 fixed. `points` still rounds each base metric,
but the eased flare compensation added afterwards put the fraction straight
back, so the rail padding measured 30.6 and the panel 111.2 and 225.2
through the whole attach animation. Round the sum, not the base.
Nothing on main tests this; the parked branch is the only guard, which is
how the regression reached the release candidate unseen.
The Capacity Dock hover panel becomes a glance: a provider header, the
sessions running right now as pills with their context rings, a Today
strip, and a row of quota-window percentages, separated by hairlines.
Window percentages stay plain text until 70 percent so a healthy row
reads as text rather than a wall of colour. The panel stays 350pt wide;
every block height is a whole point and is summed into the panel frame,
since the hosting view does not size itself.
Running sessions are new CLI surface. `liveSessions` is add-only on the
menubar payload: sessions whose transcript was appended inside a 120s
window, carrying the branch, the model that answered last, and the
context that turn reported. Absence means "unknown", so the section
hides rather than claiming nothing is running. The block names projects
and branches, so sharing and MCP output strip it.
Today's totals needed no CLI change; the app simply was not decoding the
cache token counts the payload already carried.
At 60 percent the docked pad was 12 + 18.6 points, putting the rail body at
111.2 against a 111 point panel; the same fractional mismatch as #1210,
hidden inside the attachment compensation instead of the base metrics.
The detail panel is created on the first hover and kept alive hidden, and
its height was the one dock dimension still fractional (186 at 60 percent
is 111.6), so from the first hover on it re-laid itself out at display
cadence forever: 0.1 percent idle CPU before the hover, 5.7 to 6.1 percent
after, measured on the release build. Same bug as #1210, second window.
At any scale except 100% the dock's dimensions were fractional (85% of 88
is 74.8), so SwiftUI's fitted content never agreed with the pixel-aligned
panel frame and the hosting view re-laid itself out at display cadence
forever: 5 to 7 percent idle CPU and hundreds of wakeups a second once
quota data existed, on every release since the dock shipped. Measured on
the same machine: 60% and 85% scale held 5 to 6 percent for an hour;
100% scale idled at 0.1 to 0.2 percent. Rounding every scaled metric to a
whole point makes all scales behave like 100%.
SwiftUI onHover rides on NSTrackingAreas that only deliver while the
app is active. The dock lives in a nonactivating panel inside an
accessory app, so the first click into another app killed every hover
interaction until relaunch.
Hover enter/exit is now synthesized in CapacityDockController from the
existing local and global mouse monitors, which observe the pointer
regardless of activation. Provider rows are resolved by inverting the
layoutDetail row-midpoint math (CapacityDockPlacement.providerRowIndex,
unit-tested), and the SwiftUI onHover plumbing is removed so the
controller is the single hover source. Row exits no longer cancel the
rail's pending collapse, and a drag drop re-derives hover so a
stationary pointer resting on a row still opens its bubble.
* 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>