Repository navigation
feat: add an Extra Usage Today widget - #811
Merged
Merged
Conversation
On usage-based plans (Claude Enterprise) the usage API reports every
rate-limit window as null and no limits[] entries: there's no plan limit
to go over, and extra usage is the account's whole spend. The Extra
Usage widgets still called it overage ("Overage Used: $123.45"), and
their picker descriptions only mentioned pay-as-you-go overage, so
Enterprise users had no reason to think these widgets were theirs.
Searching the picker for "enterprise" or "spend" didn't find them.
The parser now flags a response that explicitly reports no plan limits
(`noPlanLimits`). The flag is kept in the usage cache and through the
merge with statusline rate_limits, and the three widgets label their
value "Spend Used", "Spend Left" and "Spend" when it's set. Only an
explicit null counts and any limits[] entry counts as a limit, so every
uncertain case keeps the "Overage" label. A cache written before this
change keeps the old label until its next refresh.
The widget descriptions now mention both Pro/Max overage and Enterprise
spend, which also makes the picker's search find them.
Move the terminal width cache's best-effort O_EXCL lock into its own module so other cache files can use the same read-modify-write protection instead of copying it. No behavior change.
Some usage figures are per UTC day, but ccstatusline only ever sees the current month-to-date totals. Keep a small state file next to the other caches that records what was seen and when, modeled on the terminal width cache: one JSON file, the shared lock around each read-modify-write, an atomic temp-file rename, pruning to today and yesterday, and injected file and clock dependencies.
The usage API reports extra usage spent month to date only. Record the total per login in the daily state file and work out today's spend from where it stood when the UTC day began: the last value seen the previous day, zero on the 1st (extra usage resets at 00:00 UTC), or otherwise the first value seen today. A total that drops mid-day never makes today's spend negative, and an observation that loses the state lock to another process still reports today's spend.
fetchUsageData now remembers the token fingerprint of the login it used. When a line shows today's spend, the prefetch records the month-to-date total for that login in the daily state file and adds extraUsageUsedToday (cents) to the usage data. It's worked out on every render, cache hits included, and never stored in the usage cache.
Shows the extra usage spent since 00:00 UTC, including claude.ai and other machines, labeled like its siblings: "Spend Today" on accounts without plan limits, "Overage Today" otherwise. It shows nothing until a usage fetch has run since the UTC day began, and n/a when extra usage is off, with the same hide options, currency and cost number format as the other Extra Usage widgets.
Today's spend recorded every usage prefetch as an observation at render time, cache hits and stale fallbacks included. A total fetched at 23:58 UTC on the last day of the month and replayed from the cache at 00:01 became the 1st's first observation, so Spend Today showed the whole previous month for up to three minutes, or for as long as the API stayed unreachable. fetchUsageData now reports when its data came from the API (getUsageFetchedAt: now for a fresh fetch, the cache file's write time for a cache hit or stale fallback), and the observation is recorded at that time. A total fetched before midnight updates the previous day's record, so today's spend shows nothing until a total fetched today arrives, and an older fetch replayed after a newer one is ignored.
Today's extra usage spend depends on fetchUsageData recording which login and which fetch its data came from, and every other test replaces that link with a spy. Run the real fetch in the probe harness and check it reports the login fingerprint the usage cache is written with and the API fetch time, and neither when there are no credentials.
# Conflicts: # src/widgets/ExtraUsageRemaining.ts # src/widgets/ExtraUsageUsed.ts # src/widgets/ExtraUsageUtilization.ts
Upstream's label editor (sirmalloc#601) gives each widget one default label, while Extra Usage Used, Remaining and Utilization pick "Overage" or "Spend" by account. The editor offers the Overage label, since it has no usage data, and an edited label replaces whichever one the account would show.
With the label editor (sirmalloc#601) in main, anyone on a usage-based Enterprise plan can relabel the Extra Usage widgets' "Overage" to "Spend" in a few keys, so detecting those accounts from the usage API's response shape isn't worth its cache field and heuristics. What stays is the wording: the three widgets' descriptions say they cover Pro/Max overage and Enterprise spend, the docs say the same and point at the label editor, and picker searches for "enterprise" or "spend" list the three widgets first, which a new test checks.
…to feat/extra-usage-today #32 no longer detects accounts without plan limits, so Extra Usage Today keeps a fixed "Overage Today: " label, as the other Extra Usage widgets do; the label editor can change it.
…to feat/extra-usage-today
The two widgets were the same class apart from how the amount is worked out, and their label, name, description and preview sample. They now extend ExtraUsageAmountWidget, the way the Tokens widgets share TokenCountWidget. Rendering is unchanged.
Extra Usage Today ignores a fetch older than the one it last recorded, so a cached fetch a concurrent render replays late can't move the day back. A single render made while the system clock ran ahead defeats that: it records a fetch stamped at a time that hasn't come yet. Once the clock is put right, every real fetch looks older and is ignored, so Today disappears for the rest of the day; and when the clock reaches the stamped time, that record is taken as the day's, with the previous day's total as its start. With $10 seen at 09:00, one render a day ahead, then $25 and $40 later that day, Today was hidden for the rest of the day and showed $40 the next day for at most $10 of real spend. The usage cache that render wrote is stamped by the same clock and is served as fresh until the clock catches up with it, so its fetch keeps arriving with the future time and would store the same record again. A record last seen more than a minute ahead of now is now dropped and the day starts afresh, and a fetch stamped more than a minute ahead isn't recorded. The minute's leeway keeps a small step back of the clock, such as an NTP correction just after a fetch, from dropping the day's starting point. Now is read after the state file, so a fetch another render has just recorded is never taken for one from the future and the replay guard still holds.
# Conflicts: # docs/USAGE.md # src/utils/usage-prefetch.ts
updateDailyState writes daily.json through a temp file named daily.json.<pid>.tmp and renames it into place. When the write or the rename failed, for example a write cut short by a full disk or a rename refused with EPERM while a Windows virus scanner or sync client holds daily.json, the error went to the catch-all and the temp file stayed behind. Every status line render is a new process with a new pid, so each failing render left another orphan in ~/.cache/ccstatusline, and nothing ever deleted them. A failed write or rename now unlinks the temp file. A failed unlink is ignored; as before, the state stays best-effort and the render goes on. The same handle can be held on the temp file itself, and then it refuses the unlink as well as the rename. So each write also removes the other daily.json.<digits>.tmp files next to the state file once they are a minute or more old, which no running write can still own. While a handle stays held, only the last minute's temp files remain, and orphans an older build left behind are cleared as well. The temp name stays per process, so two renders never write through the same file.
Extra Usage Today keeps each login's month-to-date total at the start of the UTC day in daily.json, keyed by the usage cache's token fingerprint, a hash of the refresh token. Claude Code saves a new refresh token each time it refreshes the access token, which happens several times on a working day. The key then changed mid-day, the new key had no record, and today's spend started over from that moment: $15.00 at 13:00, then $0.00 at 17:00 instead of $35.00 and $10.00 at 18:00 instead of $45.00. The old record stayed behind until it was pruned. The record is now kept under the account and organization the credentials belong to. While a widget shows today's spend, ccstatusline asks /api/oauth/profile through the usage fetch's own request, proxy and credential code, once per token fingerprint, and keeps the answers in ~/.cache/ccstatusline/usage-account.json. A failed lookup holds off that fingerprint alone, for 30 seconds or a 429's Retry-After (at most a day), and the hold-off is written before the request goes out, so renders running side by side send one request between them. Until the lookup answers, the record stays under the fingerprint as before. Once the account is known, the records under its fingerprints, written by an earlier build or while the lookup was failing, are folded into the account's record once, so the day keeps its starting total. A record seen while the clock ran ahead is dropped instead, as it would be under its own key. A fingerprint that was never looked up can't be placed with an account, so today's spend still starts over once when an earlier build kept the record under a refresh token that has since changed, or when a lookup outage spans a refresh. The organization is part of the key because one account can sign in to a personal organization and a work one, each with its own extra usage total. The key is a hash, so neither cache file names the account.
mergeSpendRecord goes through an arrow, so reduce's index and array arguments never reach it, and the account id is built before it's fingerprinted instead of inside a nested template literal.
Upstream sirmalloc#699 added getUsageFetchedAtMs, which records the same fetch time as this branch's getUsageFetchedAt, so Extra Usage Today now reads that one. Upstream sirmalloc#724's account email lookup also asks the profile endpoint; this branch's account lookup keeps its own schema (ProfileAccountIdsSchema) and cache next to it.
# Conflicts: # docs/USAGE.md
# Conflicts: # docs/USAGE.md
…and the account key The Claude Account Email widget (sirmalloc#724) and Extra Usage Today each asked /api/oauth/profile about the same login, each with its own cache and backoff. One request now answers both. lookUpProfile runs under the email lookup's per-login lease and backoff files, and each caller keeps its part of the answer in its own cache: the email in account-email-<hash>.json, the account key in usage-account.json, which now holds only named logins. An answer that leaves out the caller's part holds that login off as before, and the other part is still kept. A rate-limited lookup's Retry-After is kept within the day the backoff reader trusts, as the account lookup did, so the email lookup no longer reads a Retry-After further off as no backoff at all.
The account key nested the id's template inside its own on one line. Build the id first, as the account lookup did before it shared the profile request.
This was referenced Nov 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A new Extra Usage Today widget shows the extra usage spent since the UTC day began, including claude.ai and other machines:
Overage Today: $23.45.Why
The usage API reports extra usage spent month to date only, so there's no way to see what today cost. That's the number that matters day to day, especially on usage-based plans where every request is billed.
How
~/.cache/ccstatusline/daily.jsonkeeps each login's month-to-date total at the start of the UTC day. It's modeled on the terminal width cache (terminal-width-cache.ts): one JSON file, a best-effortO_EXCLlock around each read-modify-write (a writer that finds it held skips that write; a lock older than 2s is stolen), an atomic temp-file rename, pruning to today and yesterday, and injected file and clock dependencies so tests never touch the real home directory. That lock moves into a sharedcache-lock.tsused by both caches instead of being copied (first commit, no behavior change). A temp file is deleted when its write or rename fails (for example when Windows antivirus holds the new file open), and each write also removes the state file's other temp files once they are a minute old, while a write in progress in another process is left alone./api/oauth/profilenames for the login. That's the request Claude Account Email (fix(account-email): show the account the live credentials belong to #724) already makes, and one request per login now answers both: it runs under that widget's per-login lease and backoff, and each keeps its part of the answer in its own cache (the hashed account key in~/.cache/ccstatusline/usage-account.json). The lookup only runs while a widget that shows today's spend is on a line. Until the account is known, the record stays under the token fingerprint; once it is, records under the account's fingerprints are folded into the account's record, keeping the day's starting total. A record under a token that was never looked up can't be tied to an account, so on that day the figure starts over once.getUsageFetchedAtMs, feat(usage): forecast the 5-hour block (Block Forecast, Block Limit Timer) #699), not the time it was rendered. A total fetched at 23:58 and replayed at 00:01 updates the previous day, so today's spend shows nothing until a total fetched today arrives. In particular, the 1st never shows last month's total. A fetch older than the one already recorded is ignored. A time more than a minute later than now was stamped while the system clock ran ahead: a record last seen then is dropped and the day starts again, and such a fetch isn't recorded, so one render with a wrong clock can't hide Today for the rest of the day or carry yesterday's spend into the next.extraUsageUsedToday(cents) to the usage data, only when a line shows the widget. It's worked out on every render, cache hits included, and never stored in the usage cache. It's derived, so it's not a fetchableUsageDataField.disabled/no-datahide options. It shows nothing until a usage fetch has run since the UTC day began, andn/awhen extra usage is off. The preview sample is$46.10.Retry-Afteris now kept within the day the backoff reader trusts. A longer one used to be read back as no backoff at all, so every render asked again.Demo
Rendered through the real pipeline from a scratch home: a fake login whose fresh usage cache is tagged with its fingerprint (so no API call), and a state file holding yesterday's last total. Enterprise: $123.45 this month, $100.00 by the end of yesterday. Max: $12.50 of overage, $10.00 by the end of yesterday.
Powerline
Plain
Why the record is keyed by the account: one fake login's day, with Extra Usage Used and Remaining next to it. It starts at $100.00 this month and is $15.00 at $115.00, then Claude Code saves a new refresh token. Keyed by the token fingerprint (first), the next renders show $0.00 and $10.00. Keyed by the account (second), they show $35.00 and $45.00. The data is fake, and the usage cache and the profile lookup's answer are seeded, so nothing is fetched.
Powerline, by fingerprint and by account:
Plain, by fingerprint and by account:
Testing
mainas of fix(powerline): keep the Custom theme when Powerline is enabled again #793 (a0bedc0), which this branch already includes:bun run lintis clean, andbun testgives 4210 pass, 0 fail (4147 onmain). The built CLI under Bun 1.4.4 and Node 26.13.0 agrees across runtimes in plain and Powerline mode and differs frommainonly by the new widget; the TUI opens and exits cleanly under both. It adds no JavaScript or Node API newer than Node 14, the build's target.fetchUsageDataand the profile lookup in the probe harness (a child process with a scratchHOME, the network mocked): the login fingerprint and fetch time the usage cache is written with; one profile request per fingerprint; the account key kept across a refresh-token rotation; other organizations and other accounts told apart; the 30-second hold-off after a failure, a 401 or a response with no organization, per login; the 429 hold-off, at most a day; the lookup lease taken before the request; a poisoned far-future backoff ignored; one profile request answering both the account key and Claude Account Email; the account looked up when only the email is cached; an email with no organization kept, holding off only the account lookup. Claude Account Email's existing tests pass unchanged.extraUsageEnabledandextraUsageUsed.n/aand its hidden state, errors andno-data, the preview.diston Node v26.10.0, a refresh-token rotation kept Extra Usage Today at $35.00 and $45.00 with one profile request per token, and withRetry-After: 172800four renders 5 seconds apart sent 1 profile request. Under Node (Vitest) thedaily-state,daily-spendandusage-prefetchtests passed 86/86.