Skip to content

feat(quota): show Claude plan limits (5h / weekly / weekly per model) in the island - #338

Open
mutoe wants to merge 7 commits into
wxtsky:mainfrom
mutoe:feat/claude-quota
Open

feat(quota): show Claude plan limits (5h / weekly / weekly per model) in the island#338
mutoe wants to merge 7 commits into
wxtsky:mainfrom
mutoe:feat/claude-quota

Conversation

@mutoe

@mutoe mutoe commented Sep 3, 2026

Copy link
Copy Markdown

Problem

Claude Code's /usage shows three rate-limit windows — 5-hour, weekly (all models), weekly (current model) — and they are the thing that actually stops a long session. Nothing in the island tells you how close you are until the CLI refuses to continue. The existing token-usage stats are a different question (how much did I spend), not this one (how much is left before I'm cut off).

Change

An opt-in Show Claude plan limits setting, off by default because it reads the Claude Code login.

Data. The Claude Code OAuth token from Keychain (Claude Code-credentials), with ~/.claude/.credentials.json as fallback, sent to GET /api/oauth/usage with the oauth-2025-04-20 beta header — the same call /usage makes. The response's limits[] (session / weekly_all / weekly_scoped with percent, resets_at, severity, the scoped model's display name) is parsed with a fallback to the legacy five_hour / seven_day shape. The token is only ever read; refreshing it would invalidate the one Claude Code holds, so an expired token surfaces as "run Claude Code once".

The Keychain item is read by running security find-generic-password -s "Claude Code-credentials" -w rather than SecItemCopyMatching. Claude Code writes the item through security, so that binary is on the item's ACL and the read is silent; an in-process read from an ad-hoc-signed build prompts on every rebuild because the designated requirement changes. (The ccusage Raycast extension does the same thing.)

Expanded panel. A footer line with every window: mini bar, percent, reset countdown (↻1h56m / ↻6h06m), coloured by severity, refreshed each minute.

Collapsed island. A chip in the left wing next to the mascot — window label, 9pt ring, percent (Fable ◑58%). If the tool-name display is on and a tool is running, the tool name has priority; the chip stays laid out as an invisible placeholder so the bar doesn't change width when they swap. The chip mode is a picker: off / auto / 5h / weekly / weekly (current model).

Auto mode. A window is pressing when its used share exceeds the elapsed share of the window (ahead of pace) or it is at ≥80%. The 5-hour window takes the chip while pressing; otherwise a weekly window — if either weekly is pressing, the one further ahead of pace, else the one with more used. No weekly reported → 5-hour.

Refresh policy. Event-driven on Stop hooks from local Claude sessions: 15s coalesce, at most one fetch a minute, a trailing fetch for stops that landed during the throttle. A 10-minute tick while the setting is on, a fetch on expand if the snapshot is over a minute old, exponential backoff (60s → 15m) on errors, polling stops entirely on 401/403, and nothing runs while the mascot gate says the machine is asleep. The policy is a pure struct (ClaudeQuotaScheduler) so it's tested without timers.

Collapsed-bar layout fix

Putting a ~67pt chip in the left wing exposed a pre-existing layout flaw on notched screens. The collapsed bar was a flexible row centred on the notch with a Spacer(minLength: notchW) between the wings. That guarantees a gap at least as wide as the notch, not that the gap lines up with the cutout — whichever wing is wider pushes the gap sideways and its tail ends up under the physical notch. Screenshots show those pixels; the display doesn't. A long tool name already did this; the chip made it obvious.

Now each wing reports its ideal content width (rigid via fixedSize, the tool name still truncating at its 120pt cap), the row between them is exactly the notch, and the whole bar is offset by half the wing difference so the gap sits on the cutout. The left wing keeps the classic reserve or its content; the right wing is trimmed to its content (count plus badges) so the bar stays clear of menu-bar icons. Width changes ease in and only shrink back after 5s, so a tool name that comes and goes doesn't make the bar breathe. Non-notch screens keep the flexible row and just add the measured overflow. A geometry test pins the shift (it caught the sign inverted on the first attempt).

Tests

  • ClaudeQuotaTests: parse of a real response, legacy fallback, garbage rejection, elapsed/pace maths, auto-mode rules (weekly default, 5h takeover at pace/80%, weekly ranked by pace), fixed modes, countdown formats, severity levels, credential parse, request headers, HTTP status mapping.
  • ClaudeQuotaSchedulerTests: coalescing, trailing fetch, remembered stops, backoff cap, rejected token, expand staleness.
  • ClaudeQuotaMonitorTests: expand fetch, disabled never fetches, burst coalesces, unauthorized stops polling — with an injected UserDefaults suite so no test touches the real setting (a monitor on standard defaults would make every other test's AppState do real credential reads).
  • NotchPanelViewTests: wing slot and notch-shift geometry.

Full suite green locally on macOS 26.4 (arm64): 960 tests, 2 skipped, 0 failures. Verified on a 14" notched display by measuring the rendered bar against NSScreen.auxiliaryTopLeftArea: chip right edge 6pt clear of the cutout, bar extent identical across chip ↔ tool-name swaps, no Keychain prompt on a fresh ad-hoc build.

🤖 Generated with Claude Code

Opt-in setting reads the Claude Code OAuth login from Keychain and fetches
Anthropic's subscription windows — the numbers /usage shows. Expanded
panel: footer line with every window (bar, percent, reset countdown).
Collapsed island: ring chip beside the session count for one window;
Auto ranks by pace (used share minus elapsed share), or pin a window.

Refresh is event-driven off Stop hooks: 15s coalesce, 60s throttle with
a trailing fetch, 10-minute idle tick, exponential backoff, and no token
refresh — an expired token just asks to run Claude Code once. Keychain
reads hop to GCD so the macOS access prompt never blocks the main actor.

Tests cover parsing (real response fixture + legacy fallback), the pace
selector, countdown formatting, HTTP status mapping, the scheduler's
debounce/throttle/backoff rules, and the monitor's fetch behaviour on a
private UserDefaults suite.
Auto now defaults to the tighter of the two weekly windows and only hands
the chip to the 5-hour window while that one is pressing — ahead of pace
or past 70%. The chip carries its window label (5h / week / model name)
before the ring so the number is never ambiguous; the collapsed bar
reserves width for the label.
The collapsed chip moves next to the mascot and only shows while no tool
name is displayed — a running tool is the more urgent signal, and the
right wing keeps the session count alone. The chip shares the tool slot's
width reserve.
SecItemCopyMatching from our own process raised the Keychain access
prompt on every ad-hoc rebuild. Claude Code stores the item with
/usr/bin/security, which is therefore on the item's ACL and reads it
silently — the same route the ccusage Raycast extension takes — so the
credential is now read through a security(1) subprocess with a timeout.

The collapsed chip's reserve was a label-length estimate and clipped
"Fable 53%"; the bar now measures the laid-out chip through a
PreferenceKey and reserves exactly that.
…under the notch

The collapsed bar was a flexible row centred on the notch with a spacer of
at least the notch width between the wings. That guarantees a gap, not that
the gap lines up with the cutout: whichever wing is wider pushes the gap
sideways, and its tail ends up under the physical notch. Screenshots show
those pixels, the display doesn't — which is how the plan-limit chip read
'Fable ◑5' on screen while every capture looked fine. Widening the total
never helps either, since half of any extra goes to the far edge.

Each wing now reports its ideal content width (rigid via fixedSize; the tool
name still truncates at its own cap) and both wings get the same slot: the
reserve, or the wider wing plus a 6pt gap to the notch. The row between
them is exactly the notch. Non-notch screens keep the flexible row and just
add the measured overflow. The chip's label-length estimate is gone.
…dth, trimmed right wing

Sizing the wings from their content made the bar jump on every tool
change: the tool name and the plan-limit chip take turns in the same slot
and are different widths. Now the chip stays laid out as an invisible
placeholder while a tool name shows, so that switch never moves anything;
only a tool name wider than the chip widens the bar, eased in, and the
width shrinks back 5s after the content got narrower.

Wings are no longer forced equal: the left keeps the classic reserve or
its content, the right is trimmed to its content (count plus badges), and
the whole bar shifts by half the difference so the gap between the wings
still lands exactly on the notch — a test checks the shift geometry, which
caught the sign being inverted on the first try.
…pace

The 5-hour takeover line moves from 70% to 80%. The two weekly windows
(all models, current model) now compete on the same terms as the session:
if either is pressing — ahead of pace or past the line — the one further
ahead of pace shows; only when neither is pressing does the higher
percentage decide.
nguyenvanduocit pushed a commit to nguyenvanduocit/CodeIsland that referenced this pull request Sep 4, 2026
Upstream PR wxtsky#338 (open) adds live Claude rate-limit quota display
to the island: 5h / weekly / weekly-per-model windows with progress
bars, reset countdowns, and a collapsed chip showing the pressing
window. Tracked as T-089 (medium priority, M effort, gate: merge).

Also notes PR wxtsky#337 (configurable animation speed, watch alongside T-084).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEPtxTR2PTGXUhaXt1VEPj
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.

1 participant