Skip to content

feat(usage): add Spend Limit Usage and Spend Limit Amount widgets - #695

Merged
sirmalloc merged 2 commits into
sirmalloc:mainfrom
mertkayacs:feat/spend-limit-widgets
Oct 13, 2026
Merged

sirmalloc merged 2 commits into
sirmalloc:mainfrom
mertkayacs:feat/spend-limit-widgets

Conversation

@mertkayacs

Copy link
Copy Markdown
Contributor

Closes #572

Claude Code 2.1.251 added rate_limits.spend_limit to the status line payload for developers behind a Claude apps gateway with spend limits. This PR parses it and adds two widgets in the Usage category.

The payload shape comes from the status line docs: used_percentage and resets_at are always present when spend_limit is, while used_usd, limit_usd and period can be absent (they need Claude Code and the gateway on 2.1.284 or later).

Changes

  • StatusJSON.ts: rate_limits.spend_limit, with the optional dollar fields and period.
  • usage-prefetch.ts: extractUsageDataFromRateLimits maps the three values we use onto UsageData (spendLimitUsage, spendLimitUsedUsd, spendLimitLimitUsd). The new widget types are registered as usage widgets but have no entry in USAGE_WIDGET_REQUIREMENTS. The field only exists in the stdin payload, so a missing value must not trigger an OAuth usage fetch.
  • Spend Limit Usage (spend-limit-usage): used percentage, using the shared usage-percent-widget.ts as a new spend-limit kind. Percentage, bar and slider modes, remaining/used toggle, number format and raw value all come from the shared code. The kind sets ignoreApiError so a usage API error from another widget is not printed next to a value that came from stdin, and it has no time cursor because the payload does not give the period's start.
  • Spend Limit Amount (spend-limit-amount): $314.12 / $500.00, rendered only when both dollar fields are present. It follows the number format setting.
  • usage-types.ts: a StatuslineOnlyUsageField type so WeeklyModelUsageBucket keeps its previous field set. Without it the new fields widen UsageDataField and break the cache schema parity test at compile time.
  • Docs: docs/USAGE.md and docs/DEVELOPMENT.md. I left the README "What's New" section alone since that reads like release notes.

Sample output

Payload from the docs example (used_percentage 62.8, used_usd 314.12, limit_usd 500), piped through the built dist/ccstatusline.js on Node 22 and bun run src/ccstatusline.ts, ANSI colors stripped:

Spend Limit: 62.8% | Spend: $314.12 / $500.00
Spend Limit: [████████████████████░░░░░░░░░░░░] 62.8%
Spend Limit: ▓▓▓▓▓▓░░░░ 62.8%

With a payload that has no spend_limit, both widgets render nothing.

Testing

  • New tests for the schema, the extractor, prefetch behaviour (stdin only, no API call, spend limit kept when another widget triggers a fetch) and both widgets. I wrote them first and checked that the schema and prefetch ones fail without the change.
  • bun run lint and bun run build pass.
  • bun test: 2804 pass, 4 fail. The same 4 fail on a clean main in my environment (bun 1.4.2): three loadClaudeSettingsSync cases and one usage-fetch timeout. They pass when their files run on their own, so this looks like state leaking between test files in a full run, and it is not related to this change.

Notes

@eric-engberg

Copy link
Copy Markdown
Contributor

Nice addition. One thought, since this overlaps with the Extra Usage widgets: both families show spend against a cap, from different meters. This PR reads the gateway's spend_limit from stdin; Extra Usage Used / Remaining / Utilization / Daily Budget read the claude.ai account's extra usage from the usage API. One person only ever has one of them: a gateway user has no claude.ai login (the usage API widgets show [No credentials] for them), and a claude.ai user doesn't go through a gateway.

So instead of a separate pair, the Extra Usage widgets could take the gateway as a source. One source per render: spend_limit when stdin has it (no API call), otherwise the usage API, and never a mix of the two.

  • Utilization comes from used_percentage, which is always present.
  • Used and Remaining come from used_usd / limit_usd when present, and render nothing otherwise, as they do today without data.
  • Daily Budget (feat: add an Extra Usage Daily Budget widget #672) would count the days left in the cap's period from resets_at instead of in the month; with a daily cap it's just what's left.
  • The default label could follow the source (Spend Limit: vs Overage:).

Two differences would need a note in the docs: the gateway's dollars are an estimate in USD, not a billed amount, and caps can be daily or weekly.

Not a blocker, though: if separate widgets are preferred, that works too. I'm happy to help with a source-aware version as a follow-up either way.

Claude Code 2.1.251 added rate_limits.spend_limit to the status line payload for developers behind a Claude apps gateway. Parse it and show it with two widgets that read the payload only and never call the usage API.

Closes sirmalloc#572
@mertkayacs

Copy link
Copy Markdown
Contributor Author

Thanks, that is a sensible way to fold the two together. I rebased on main to clear the conflict with Extra Usage Daily Budget (lint, build and the new tests pass) and kept the separate widgets for now so this PR stays small. If @sirmalloc prefers the source-aware Extra Usage version, I can rework it that way here.

@mertkayacs
mertkayacs force-pushed the feat/spend-limit-widgets branch from a3768ba to be526ed Compare October 9, 2026 23:04
@mertkayacs

Copy link
Copy Markdown
Contributor Author

Hi, checking in on this. It has been quiet since the Extra Usage discussion and the branch still merges cleanly. Could you review it when you have time?

@sirmalloc
sirmalloc merged commit da0f231 into sirmalloc:main Oct 13, 2026
@sirmalloc

Copy link
Copy Markdown
Owner

Merged, will be published in the next release.

@EarlyFried

Copy link
Copy Markdown

Thanks!

axisrow added a commit to axisrow/ccstatusline that referenced this pull request Oct 15, 2026
…rs (#64)

* chore(deps-dev): bump vitest from 4.1.11 to 5.0.0 (sirmalloc#723)

Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 4.1.11 to 5.0.0.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.0/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 5.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump react-devtools-core from 7.0.1 to 8.0.0 (sirmalloc#722)

Bumps [react-devtools-core](https://github.com/react/react/tree/HEAD/packages/react-devtools-core) from 7.0.1 to 8.0.0.
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/[email protected]/packages/react-devtools-core)

---
updated-dependencies:
- dependency-name: react-devtools-core
  dependency-version: 8.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump actions/setup-node from 7.0.0 to 7.1.0 (sirmalloc#720)

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 7.0.0 to 7.1.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@8207627...949feb2)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump the dev-dependencies group across 1 directory with 6 updates (sirmalloc#733)

Bumps the dev-dependencies group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@remotion/cli](https://github.com/remotion-dev/remotion) | `4.0.532` | `4.0.534` |
| [eslint](https://github.com/eslint/eslint) | `10.11.0` | `10.12.0` |
| [globals](https://github.com/sindresorhus/globals) | `17.12.0` | `17.13.0` |
| [remotion](https://github.com/remotion-dev/remotion) | `4.0.532` | `4.0.534` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.70.1` | `8.71.1` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `5.0.0` | `5.0.3` |



Updates `@remotion/cli` from 4.0.532 to 4.0.534
- [Release notes](https://github.com/remotion-dev/remotion/releases)
- [Commits](remotion-dev/remotion@v4.0.532...v4.0.534)

Updates `eslint` from 10.11.0 to 10.12.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](eslint/eslint@v10.11.0...v10.12.0)

Updates `globals` from 17.12.0 to 17.13.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](sindresorhus/globals@v17.12.0...v17.13.0)

Updates `remotion` from 4.0.532 to 4.0.534
- [Release notes](https://github.com/remotion-dev/remotion/releases)
- [Commits](remotion-dev/remotion@v4.0.532...v4.0.534)

Updates `typescript-eslint` from 8.70.1 to 8.71.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.71.1/packages/typescript-eslint)

Updates `vitest` from 5.0.0 to 5.0.3
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.3/packages/vitest)

---
updated-dependencies:
- dependency-name: "@remotion/cli"
  dependency-version: 4.0.534
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: eslint
  dependency-version: 10.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: globals
  dependency-version: 17.13.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: remotion
  dependency-version: 4.0.534
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.71.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: vitest
  dependency-version: 5.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat(usage): add Spend Limit Usage and Spend Limit Amount widgets (sirmalloc#695)

Claude Code 2.1.251 added rate_limits.spend_limit to the status line payload for developers behind a Claude apps gateway. Parse it and show it with two widgets that read the payload only and never call the usage API.

Closes sirmalloc#572

Co-authored-by: Matthew Breedlove <[email protected]>

* fix: draw the text cursor as a bar so trailing spaces show (sirmalloc#710)

* fix: draw the text cursor as a bar so trailing spaces show

The label editor quotes the label so a trailing space stays visible, but
the cursor was a highlighted block: over a character, or a highlighted
space after the text when at the end. So a label ending in a space with
the cursor on it ("Weekly:" + block) looked the same as a label with no
space and the cursor past the end ("7d" + block), and "7d" read as "7d ".

Draw the cursor as a bar between characters instead. It never covers a
character, so a trailing space shows as a gap before it, and the end of
the text adds nothing that looks like a space. Custom Text shares the
cursor, so it gets the bar too.

* feat(tui): preview label edits live and blink editor cursors

---------

Co-authored-by: Matthew Breedlove <[email protected]>

* fix(usage): call Session Usage Block Usage, ending its "Session:" label clash (sirmalloc#708)

* fix(usage): call Session Usage Block Usage, ending its "Session:" label clash

Three widgets printed the label "Session: " and meant two different
things by it. Session Clock and Session Name mean the Claude Code
conversation; Session Usage meant the 5-hour usage window. On one line,
"Session: 42% | Session: 1hr 5m" reads as two facts about the same
session when they aren't.

The 5-hour window already has a name here: Block Timer and Block Reset
Timer call it the block. Session Usage becomes Block Usage, labeled
"Block: ", with the description "Shows the current 5-hour block's usage
percentage". Its type stays session-usage, so saved configs keep
working, and anyone who prefers the old label can set "Session: " back
with the label editor (e). The docs follow; past release notes keep the
name they shipped with.

* fix(usage): label Block Usage "5h: " rather than "Block: "

"Block: " is already Block Timer's label, for the block's elapsed time
("Block: 3hr 45m"), so Block Usage beside it would print two "Block:"
figures. Label it "5h: " instead, which says which window the percent
is of and pairs with Weekly Usage's "Weekly: ". The name stays Block
Usage, and a "Session: " override still sets the old label back.

---------

Co-authored-by: Matthew Breedlove <[email protected]>

* fix: keep 8-bit color codes, strip them, and drop other 8-bit sequences whole (sirmalloc#704)

* fix(ansi): strip 8-bit color codes as well as ESC [ ones

The ANSI parser reads the 8-bit CSI (0x9b) as an escape sequence, but
stripSgrCodes only matched color codes written with ESC [. Text colored
with \x9b31m kept its colors where they should go: a global foreground
override left a preserveColors widget's own colors in place, and a
Custom Command cut by its max width never got its closing reset, so its
color ran into the separators and widgets after it.

Match both introducers. restoreBackgroundAfterResets shares the pattern,
so an 8-bit reset now gets the background re-applied after it too; its
parameter parsing skips a one-character introducer for those.

* fix(sanitize): keep 8-bit color codes and drop other 8-bit sequences whole

The terminal sanitizer dropped 8-bit C1 controls one character at a time.
For a sequence started by one, that removed only the introducer and left
the rest as text: a command printing \x9b31mred\x9b39m showed "31mred39m",
and an 8-bit hyperlink or title string showed its URL or payload.

Read each 8-bit introducer as the ESC sequence it stands for: CSI (0x9b)
as ESC [, OSC (0x9d) as ESC ], and DCS, SOS, PM and APC likewise, with
ST (0x9c) ending strings. Color codes and safe hyperlinks are kept, in
their 7-bit form, which every terminal reads and the rest of the
renderer parses; any other sequence is dropped whole. Lone 8-bit
controls that start no sequence, such as NEL or a stray ST, are still
dropped.

* feat(claude-status): optionally link the widget to the status page (sirmalloc#701)

* feat(claude-status): optionally link the widget to the status page

Claude Status shows "minor" or "major" when something's wrong, but
the details are on status.claude.com, which you have to look up.
Git Branch and Git Root Dir can already link to the repo or IDE.

Press `l` on Claude Status to make the whole widget an OSC 8 hyperlink
to https://status.claude.com, the site it reads the status from:
label, status and the history strip, and an unknown "?" too, when the
page is most worth a look. It's off by default, like the other links,
and the editor row shows "status link". Terminals without OSC 8
support show the text as before.

* fix(preview): preserve enabled widget hyperlinks

---------

Co-authored-by: Matthew Breedlove <[email protected]>

* fix(powerline): detect Nerd Fonts v3 names and fonts in font subfolders

checkPowerlineFonts() scanned only the top level of the font folders and
required a spaced "nerd font" pattern, so common layouts were reported as
missing: Nerd Fonts v3 file names have no spaces
("JetBrainsMonoNerdFont-Regular.ttf"), the short "NF" form exists
("JetBrainsMonoNF-Regular.ttf"), and font managers nest fonts in
subfolders ("NerdFonts/", "TTF/", Debian's
/usr/share/fonts/opentype/PowerlineSymbols.otf).

Match "nerd ?font" and an "NF" token, and scan the font folders
recursively with a depth cap.

Co-Authored-By: Claude Code <[email protected]>

* chore: retrigger CI after merge

* chore: probe CI trigger

* chore: probe CI trigger after actions re-enable

* chore: probe CI trigger after fork workflows re-enable

* fix: align tests and compact labels with merged upstream changes

Three merge fallouts after merging origin/main into the PR branch:

- upstream-terminal-sanitize: upstream c08a59d drops an 8-bit C1 CSI
  sequence whole (terminals consume it as one unit), so \x9b2J no
  longer leaves 2J as text; expect 'okx'.
- usage-prefetch: the fork's async providers pass a cache handle in
  every fetchUsageData call; the upstream spend-limit test now uses
  the same anyCacheMatcher() as the neighboring assertions.
- compact labels: upstream 6e148cb renamed the session usage label
  from "Session:" to "5h:", so the compact-label probe dropped the
  widget; add the '5h: ' preset.

Co-Authored-By: Claude Code <[email protected]>

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mert Kaya <[email protected]>
Co-authored-by: Matthew Breedlove <[email protected]>
Co-authored-by: eric-engberg <[email protected]>
Co-authored-by: axisrow <[email protected]>
Co-authored-by: Claude Code <[email protected]>
axisrow added a commit to axisrow/ccstatusline that referenced this pull request Oct 20, 2026
…sirmalloc#704/sirmalloc#708/sirmalloc#710, dep bumps)

Upstream cursor-bar fix (sirmalloc#710) ported into fork lazy editors in src/widgets/editors/. Fork compaction-counter no-space glyph and usage-prefetch cache matcher kept. Co-Authored-By: Claude Code <[email protected]>
pcvelz added a commit to pcvelz/ccstatusline-usage that referenced this pull request Oct 21, 2026
Upstream:
- Spend Limit Usage and Spend Limit Amount widgets (sirmalloc#695)
- Session Usage renamed to Block Usage, ending its Session label clash (sirmalloc#708)
- keep 8-bit color codes, strip them, drop other 8-bit sequences whole (sirmalloc#704)
- text cursor drawn as a bar so trailing spaces show (sirmalloc#710)
- Claude Status widget optionally links to the status page (sirmalloc#701)
- dependency bumps: vitest major (sirmalloc#723), react-devtools-core major (sirmalloc#722), actions/setup-node (sirmalloc#720), dev-dependencies group (sirmalloc#733)

Conflict resolutions:
- .github/workflows/publish.yml, docs/DEVELOPMENT.md, docs/USAGE.md: kept deleted (.fork-keep-deleted)
- src/types/RenderContext.ts, src/utils/usage-types.ts: fork weeklyFableUsage fields kept, upstream spendLimit fields added; upstream fableUsage fields dropped (their FableWeeklyUsage widget is fork-deleted)
- src/utils/widget-manifest.ts, src/widgets/index.ts: spend-limit widgets added; upstream fable-weekly-usage dropped (fork-deleted), fork keeps its own reset-timer entry
- src/widgets/shared/usage-percent-widget.ts: fork fable-weekly kind kept, upstream spend-limit kind added
- src/utils/usage-prefetch.ts: fork per-item requirements and API-sourced weekly kept, upstream spend-limit extraction added
- src/utils/__tests__/usage-prefetch.test.ts: upstream test for weekly usage from rate_limits stays retired (docs/test-retirements.md, a56a58e); upstream spend-limit tests added

Hand edits outside conflicts:
- src/utils/__tests__/usage-prefetch.test.ts: compat fix - upstream spend-limit tests reference mockFetchUsageData, the fork's mock is mockFetchUsage
- docs/test-retirements.md: ledger entries for two tests upstream replaced (sirmalloc#704 8-bit sequences, sirmalloc#701 OSC links in the preview)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for rate_limits.spend_limit available in v2.1.251

4 participants