Skip to content

feat(usage): Block Reset Timer can hide until the 5-hour block hits 100% - #697

Merged
sirmalloc merged 12 commits into
sirmalloc:mainfrom
eric-engberg:feat/block-reset-hide-under-limit
Oct 26, 2026
Merged

sirmalloc merged 12 commits into
sirmalloc:mainfrom
eric-engberg:feat/block-reset-hide-under-limit

Conversation

@eric-engberg

@eric-engberg eric-engberg commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

What

Block Reset Timer's (h)ide… list has a new entry, "when the 5-hour block is under 100%" (under-limit). With it checked, the timer stays out of the status line until the 5-hour limit is hit, then shows the usual Reset: 1hr 43m so you know how long until you can work again.

Why

Until you hit the 5-hour limit, the time until the block resets doesn't change what you do, but it takes space on every line. Once you're at 100% it's the one number that matters. Today the timer is all or nothing; this lets it appear only when it's useful.

How

  • The raw percent decides. The timer shows once sessionUsage >= 100, not when Block Usage's rounded text reads 100%. With whole numbers, 99.7% reads as 100% but you can still work, so the timer stays hidden.
  • An unknown percent hides it. With the state on, no percent for the block (no payload rate_limits and no usage data) means the timer stays hidden, rather than showing a countdown it can't justify. A usage error is hidden the same way.
  • Data: the usage API's percent. With the state on, the prefetch asks for sessionUsage only for timers that have the state checked, the same way Block Usage's time cursor asks for the reset time. It fetches it from the usage API even when the status payload's rate_limits has one. The payload's percent comes from that session's last API response, so it stops moving while the session sits idle. With several sessions open, one left idle while another used up the limit would keep its timer hidden at the limit. After a reset it could also show a timer from the old block's 100%.
    • The API's answer is the account's. It's cached for 3 minutes and shared by every session, so this costs at most one request per 3 minutes.
    • It's kept apart from the merged usage data (apiSessionUsage), so Block Usage and every other widget still show the payload's percent as before.
    • When the fetch fails, the payload's percent stands in. The failure is suppressed, like the timer's own reset-time requirement, so it never turns into an error placeholder elsewhere on the line.
    • A cached answer from a block that has already reset is ignored. The cache is reused for up to 3 minutes, and kept for as long as the API keeps failing, so right after a reset it can still hold the old block's 100% and would show the countdown at the start of a fresh block. The API's percent counts only while its reset time is still ahead; otherwise the payload's percent stands in, as when the fetch fails.
  • Unchanged: the TUI preview still shows its sample (Reset: 4hr 30m), since the widget returns the preview before any hide check, as the existing no-data state does. With the state off, nothing changes. Combined with no-data, a timer at 100% with no reset window yet stays hidden instead of showing [Loading].

Update: Don't let a stale usage cache decide the under-limit state

  • While the usage API can't be reached (a 429 backoff or a failed request), fetchUsageData serves the last good cache however old it is, with no error set, so nothing told it apart from a fresh answer. The prefetch took that cache's 5-hour percent as the API's current answer, so the payload's percent never stood in. During a backoff, which a Retry-After can stretch to an hour, a cache written at 97% kept the reset countdown hidden after the payload had reached 100% in the same block.
  • fetchUsageData now marks the stale fallback it returns, including when a later call serves it again from memory. It keeps a WeakSet and exports isStaleUsageFallback. The prefetch doesn't take the block's API percent from a marked result. A stale fallback therefore counts as the API being unreachable, like a failed fetch, and the payload's percent decides, as the under-limit docs describe.
  • Only a cache that has outlived a refresh is marked: one older than the 3-minute cache lifetime plus the 30-second lock a fetch holds while it is in flight (3.5 minutes in all). A render that lands during another session's ordinary refresh is served the cache through that lock, seconds past its 3 minutes and as current as the shared cache ever is. That cache still decides, like a cache hit. The check is one statSync of the cache file in a small helper next to readStaleUsageCache, and it counts a failed stat as outlived.
  • The payload decides in both directions once the cache is stale. So if the last cached answer said 100% and an idle session's payload still says 97%, the countdown stays hidden until the API answers again. This matches the documented rule that the API's percent decides, and that the payload's stands in only when the API can't be reached.

Demo

A fake status payload with the 5-hour block at 60% and then 100%, with the reset 1hr 43m away. Before, the timer shows at both; after, only at 100%. The recording has no usage API credentials, so the payload's percent stands in, which is what the API would report at these percents. Reading the API changes which percent decides, not how the widget looks, so the media only changed for the new wording.

Powerline, before and after:

Block Reset Timer at 60% and 100% before the change, Powerline mode

Block Reset Timer at 60% and 100% with under-limit on, Powerline mode

Plain, before and after:

Block Reset Timer at 60% and 100% before the change, plain mode

Block Reset Timer at 60% and 100% with under-limit on, plain mode

A new block at 40% in the payload while the usage API cache (fake login and data) still holds the last block's answer: 100%, with a reset 20 minutes ago, as when the API kept failing across the reset. Before, the countdown shows at 40%; after, the cached answer is ignored and the timer stays hidden.

Powerline, before and after:

A finished block's cached 100% shows the countdown at 40%, Powerline mode

The finished block's cached answer is ignored, Powerline mode

Plain, before and after:

A finished block's cached 100% shows the countdown at 40%, plain mode

The finished block's cached answer is ignored, plain mode

Turning it on in the line editor: select the timer, h, check the new entry, Enter.

Powerline

Checking "when the 5-hour block is under 100%" in the hide list, Powerline mode

Plain

Checking "when the 5-hour block is under 100%" in the hide list, plain mode

Update

Block Usage and Block Reset Timer (hidden 'under-limit'), piped with fake data while the usage API is in a 429 backoff for the next hour. Its last cached answer is 40 minutes old and for the current 5-hour block, and nothing is fetched. Before, the stale cache decided: at 100% the countdown stayed hidden because the cache said 97%, and at 97% it showed because the cache said 100%. After, the payload's percent decides both ways, so the countdown shows at 100% and is hidden at 97%.

Powerline, before and after:

Before, Powerline

After, Powerline

Plain, before and after:

Before, Plain

After, Plain

Testing

  • Widget tests: hidden at 60%, at 99.7%, with no percent and with a usage error; shown at 100% and 100.4%; the API's percent decides over the payload's both ways (API at 100% with the payload at 97%, and API at 2% with the payload at 100%); unchanged with the state off; the preview sample kept; hidden rather than [Loading] at 100% with no reset window when no-data is also on; the declared states.
  • Prefetch tests: the block's percent is fetched for a timer with the state, also when rate_limits has one, and kept apart so Block Usage keeps the payload's; a failed fetch for it alone is suppressed, with the payload's percent standing in.
  • bun test: 2794 pass, 0 fail. bun run lint passes. The changed test files never write to HOME (run with HOME pointed at an empty directory).
  • Under Node (Vitest): both changed test files pass, 79 of 79 (67 of 67 on main).
  • Built CLI under Bun 1.4.2 and Node 26.10.0, payloads at 60% and 100%: all runtimes give identical output in plain and Powerline modes. At 60% it differs from main (the timer is hidden); at 100% it matches main. The TUI opens and exits cleanly under both.
  • After merging today's main: bun test 3067 pass, 0 fail, bun run lint passes, and the built CLI's plain and Powerline renders match main under Bun 1.4.2 and Node 26.10.0.
  • Reading the API's percent (latest commit): bun test 3070 pass, 0 fail; bun run lint passes; under Node (Vitest) both changed test files pass, 84 of 84. Built CLI under Bun 1.4.2 and Node 26.10.0 with payloads at 97% and 100% and no credentials: identical across runtimes and to the previous commit in plain and Powerline modes. Against the live API, with a made-up payload at 100% while the account was at 12%: the timer stays hidden, where the previous commit showed Reset: 1hr 42m.
  • Block wording (latest commit: the hide entry reads "when the 5-hour block is under 100%", since "session" also means the Claude Code conversation in Session Clock and Session Cost): bun test 3070 pass, 0 fail; bun run lint passes; under Node (Vitest) both changed test files pass, 84 of 84. The media is re-recorded for the new label.
  • Ignoring a finished block's answer (latest commit): two prefetch tests, one where an answer whose reset passed 20 minutes ago is ignored and the payload's 40% stands in, and one where an answer whose block is still running is kept. bun test 3185 pass, 0 fail; bun run lint passes; under Node (Vitest) the prefetch test file passes, 54 of 54. Built CLI under Bun 1.4.2 and Node 26.10.0, plain and Powerline, from a scratch HOME whose fresh cache holds the old block's 100%: before, 5h: 40.0% | Reset: 4hr 39m; after, 5h: 40.0%, identical across runtimes.

Update:

  • usage-fetch.test.ts: a new it.each case covers a render that lands while another render's refresh is in flight (a timeout lock with 28s left). A cache 185s old is not marked as a stale fallback; before the threshold this case failed with [true, true] against [false, false]. A cache 215s old is marked, which pins the other side of the threshold. The two cases that check a stale cache served through a backoff and through a failed request now use a 40-minute-old cache.
  • usage-prefetch.test.ts: the test 'falls back to rate_limits' block percent when the under-limit fetch only has a stale cache' now ties the stale mark to the exact object fetchUsageData returned. If the check reads the wrong variable it fails, receiving apiSessionUsage 97, and it also fails if the check is removed. The old mock passed in both cases.
  • bun test: 3191 pass, 0 fail, across 178 files (3189 before, plus the 2 new cases). bun run lint (tsc and ESLint) passes.
  • Node 26.10.0 (vitest): usage-prefetch.test.ts passes 55/55. usage-fetch.test.ts can't run its probe subprocess under plain Node, because Node can't resolve usage.ts's extensionless imports: 46 cases fail with ERR_MODULE_NOT_FOUND, the 44 known before plus the 2 new probe cases. To cover it on Node, I built the dist and ran it under Node 26.10.0 and Bun 1.4.2 in 9 scenarios: a cache hit after a reset, a stale fallback after a reset, a 429 backoff in both directions, a control, and an in-flight refresh at 185s and 215s in both directions. Each ran from a scratch HOME with fake credentials and a dead HTTPS_PROXY. Both runtimes gave the same output, and the cache and lock files were never touched.
  • Results from the built dist: during a 429 backoff, with the cache 40 minutes old at 97% and the payload at 100%, the line went from 5h: 100.0% to 5h: 100.0% | Reset: 1hr 59m. During another session's refresh (cache 185s old at 100%, payload 97%), it stays 5h: 97.0% | Reset: 1hr 59m, as before.

…ge API

The under-limit hide state read the session percent from the status
payload's rate_limits whenever it had one. That percent comes from the
session's own last API response, so it stops moving while the session
sits idle: with several sessions open, one left idle while another used
up the 5-hour limit kept its timer hidden at the limit. After a reset it
could also keep showing a timer from the old window's 100%.

The state now fetches the usage API's percent even when the payload has
one, and keeps it apart from the merged data as apiSessionUsage, so
Session Usage and every other widget still show the payload's percent.
The API's answer is the account's, cached for 3 minutes and shared by
every session, so this costs at most one request per 3 minutes. When the
fetch fails, the payload's percent stands in, without an error.
The under-limit state's checklist label read "when session usage is
under 100%", but ccstatusline also says "session" for the Claude Code
conversation (Session Clock, Session Cost), and the timers for the
5-hour window say "block". The label now reads "when the 5-hour block is
under 100%", and the docs, comments and test names say the block's
percent too. "Session" stays where it means a Claude Code session, such
as a session left idle. Code names that follow the usage data's
`sessionUsage` field keep it.
@eric-engberg eric-engberg changed the title feat(usage): Block Reset Timer can hide until session usage hits 100% feat(usage): Block Reset Timer can hide until the 5-hour block hits 100% Oct 9, 2026
Another change renames Session Usage to Block Usage. Say "usage
percent" where the under-limit state's docs and two test comments named
it, so they read right with either name.
eric-engberg and others added 4 commits October 15, 2026 11:13
The usage API's answer is cached for 3 minutes, and kept for as long as
the API keeps failing. Across a reset, under-limit took that answer's
100% for the new block's, so the reset countdown showed at the start of
a fresh block (at 40%, say) until a fetch succeeded. The API's percent
now counts only while its block hasn't reset; otherwise the payload's
percent stands in, as when the fetch fails.
While the usage API can't be reached (a 429 backoff, a failed request,
no credentials), fetchUsageData serves the last good cache however old
it is, and nothing tells it apart from a fresh answer: unlike a failed
fetch it carries no error. Block Reset Timer's under-limit state took
that cache's 5-hour percent as the API's current answer, so the
payload's percent never stood in. Through a backoff, which a Retry-After
can stretch to an hour, a cache written at 97% kept the reset countdown
hidden after the payload had reached 100% in the same block.

fetchUsageData now marks the stale fallback it returns, including when a
later call serves it again from memory. The prefetch takes the block's
API percent only from the API or a current cache, so a stale fallback
counts as the API being unreachable, like a failed fetch, and the
payload's percent decides, as the under-limit state's docs describe.

Only a cache that has outlived a refresh is marked: one older than the
3-minute cache lifetime plus the 30-second lock a fetch holds while in
flight. Another session's ordinary refresh serves the cache through
that lock too, seconds past its 3 minutes and as current as the shared
cache ever is, and marking it would let an idle session's frozen
payload percent override the API's for that render.
The under-limit hide state preferred apiSessionUsage and fell back to the
merged sessionUsage field. When the status payload omitted its percentage,
that merged field retained the API value even if prefetch rejected it as
stale or from a block that had already reset. An old cached 100% could
therefore show a new block's countdown from a reset-only payload.

Read the fallback percentage directly from the original status payload in
the render context. Continue preferring the API percentage accepted by
prefetch, and treat missing or null payload percentages as unknown so the
timer stays hidden. Keep general usage merging and other widgets unchanged.

Add prefetch-to-render regression coverage for expired and stale cached
100% readings with missing, null, zero, 97%, and 100% payload percentages,
plus an entirely absent payload. Verify that merged usage is preserved
while rejected API values cannot decide visibility. Update widget fixtures
to supply their fallback percentages through the original payload.

Validation:
- Both new regression cases reproduced the erroneous countdown before the fix.
- Targeted prefetch and Block Reset Timer tests: 96 passed.
- bun run lint: passed (TypeScript and ESLint).
- bun test: 3,191 passed; two local proxy listener tests failed because the
  sandbox blocked binding to 127.0.0.1. Both passed outside the sandbox
  (the status-service file: 30 passed; the usage proxy timeout test: 1 passed).
Bring in main through cf73531, including the dependency updates from sirmalloc#745.
The merge applies cleanly and preserves the Block Reset Timer under-limit
feature and the fix that keeps rejected API percentages out of its fallback.

Install the merged dependency versions with bun install --frozen-lockfile.
No conflict resolutions or additional source changes were required.

Validation:
- bun run lint: passed (TypeScript and ESLint).
- bun test: 3,193 passed, 0 failed across 178 files, with local proxy
  listeners permitted so the proxy timeout tests execute successfully.
- git diff --check and git diff --cached --check: passed.
@sirmalloc
sirmalloc merged commit bd6fcb1 into sirmalloc:main Oct 26, 2026
3 checks passed
eric-engberg added a commit to eric-engberg/ccstatusline that referenced this pull request Oct 27, 2026
Main's stale-cache check from sirmalloc#697 (hasUsageCacheOutlivedRefresh) reads
the usage cache through getCacheFile(), this branch's cache-dir
fallback. A usage error's errorTransient and sirmalloc#697's apiSessionUsage are
both left out of UsageDataField, and each side's prefetch tests stay.
axisrow added a commit to axisrow/ccstatusline that referenced this pull request Oct 27, 2026
…it names, sirmalloc#717 claude-settings test isolation, sirmalloc#705 jj detached HEAD, sirmalloc#703 hooks save warning, sirmalloc#700 dev reload, sirmalloc#698 renderer merge ends at hidden widget, sirmalloc#697 block reset timer under-limit hide, sirmalloc#745 dev-deps bumps

Conflict policy: upstream infra wins, fork features ported on top (fork-only
src/daemon/, src/widgets/editors/*, parity fixtures and fork widgets kept).
Notable joins: sirmalloc#703 returns SkippedWidgetHooks through the fork's
saveSettingsTo wrapper; sirmalloc#697 keeps the fork's zero hide state alongside
upstream's under-limit state; sirmalloc#698 replaces the fork's merge-glue palette
semantics with upstream's merge-ends-at-unmerged-empty-widget rule;
dev-reload (ctrl+r) composes with the fork's first-run preset picker.

Co-Authored-By: Claude Code <noreply@anthropic.com>
pcvelz added a commit to pcvelz/ccstatusline-usage that referenced this pull request Oct 29, 2026
Upstream: Extra Usage Limit widget (sirmalloc#756), live widget preview while picking (sirmalloc#755), Fast Mode widget (sirmalloc#754), transcript block fallback fixes (sirmalloc#752), editor keys on a global install (sirmalloc#751), git cache TTL (sirmalloc#750), npx cached copy skip (sirmalloc#749), Block Usage hide at 100% (sirmalloc#747), Block Reset Timer hide until 100% (sirmalloc#697), fish-style branch abbreviation (sirmalloc#614), jj detached HEAD (sirmalloc#705), hooks save warning (sirmalloc#703), dev reload (sirmalloc#700), merge ends at a hidden widget (sirmalloc#698), Block Forecast and Block Limit Timer widgets (sirmalloc#699), contributor guide (sirmalloc#706), POSIX path case fix (sirmalloc#707), whitespace-distinct conflict filenames (sirmalloc#709), Quick Start reorder (sirmalloc#715), bunx launch guidance (sirmalloc#714), jj diff summary counts (sirmalloc#711), powerline manual separators (sirmalloc#713), keychain credential by account (sirmalloc#532), Keychain probe timeout (sirmalloc#760), usage cache scoped per CLAUDE_CONFIG_DIR (sirmalloc#266), test isolation (sirmalloc#717, sirmalloc#753), dev-dependency bumps (sirmalloc#745)

Conflicts resolved per the standard-resolution table; fork deletions re-applied.

Hand edits outside conflicts:
- src/utils/global-command-resolution.ts: fork-rename (ccstatusline@ -> ccstatusline-usage@ in upstream's new bunx pattern)
- src/utils/__tests__/global-package-manager.test.ts: fork-rename (package dir ccstatusline-usage in upstream's new tests)
- src/utils/usage-fetch.ts: compat fix - cacheUsageData gained a fetchedAtMs arg (upstream sirmalloc#266); adapted the fork-only fetchFreshUsageData call site
- src/utils/__tests__/usage-fetch.test.ts: compat fixes - cache/lock now hashed per CLAUDE_CONFIG_DIR (usageFilePath helper) in four fork tests; upstream fableUsage renamed to fork's weeklyFableUsage; seedUsageCache takes TokenHome
- src/utils/__tests__/usage-prefetch.test.ts: compat fix - mockFetchUsageData renamed to upstream's mockFetchUsage in cleanly-merged tests
- README.md: took upstream's Quick Start move (sirmalloc#715) and dropped the fork's old copy; fork-rename of its ccstatusline@ specifiers; links to fork-deleted docs (docs/USAGE.md, docs/WINDOWS.md) replaced with the fork's inline text
- CONTRIBUTING.md: links to the fork-deleted docs/DEVELOPMENT.md pointed at the README Development section
- docs/test-retirements.md: ledger entries for the 12 upstream test renames (sirmalloc#697, sirmalloc#698, sirmalloc#700, sirmalloc#705, sirmalloc#713, sirmalloc#760)
eric-engberg added a commit to eric-engberg/ccstatusline that referenced this pull request Oct 30, 2026
…build

The branch was rebuilt on today's updated #115 and #80, which hold upstream
main; #71 and #89, which it also merged, are upstream (sirmalloc#697, sirmalloc#747) and in
#115 through main. This keeps the old history so the push is a
fast-forward; the tree is the rebuild's.
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.

2 participants