feat(chat): a usage limit pauses the queue and offers to resume at the reset
Stacked on chat-shell-intent-rows → chat-github-work-item-chip.
What
When the provider refuses a turn for the account's usage, a banner sits on the composer: an amber gauge, 5-hour limit reached (or Weekly / Weekly Opus / Weekly Sonnet / Extra usage, or plain "Usage limit reached" when the window is unknown), one line "Sonnet 5.5 · avi@… · Resets in 2m · 12:15 AM", and the composer's pill controls on the right — Account ▾ (the host's logins for the agent with plan and tightest window, the current one checked; picking another makes it the default and reopens the chat on it; plus "Usage settings"), Resume / Resume at reset (→ "Resuming at reset", click again to cancel), and a dismiss. Prompts sent while limited queue instead of hitting the wall again. In the transcript the failed turn is one card ("Stopped by the usage limit" + the agent's words) instead of the same error twice in red.
How
- Protocol (
@superset/chat):SessionState.usageLimit?: { hitAtMs, resetsAtMs?, resumeAtReset?, window?, model? }and a newrespondToUsageLimit({ action })command withresume | dismiss | resume_at_reset | cancel_resume_at_reset. Both additive; released clients are unaffected. - ACP adapter (the path every new chat uses): reads the agent's
usage_update._meta—_claude/rateLimit(arejectedwindow not on overage is remembered, withresetsAtseconds → ms andrateLimitType) and_claude/modeland reports the limit when the prompt is refused. A refused prompt's turn error now carries the agent's message without thesession/prompt:prefix. - Claude stream translator (legacy
claude-codesessions):rate_limit_eventis no longer ignored; same rule. Sharedharness/usageLimithelper for both. - Live session: on a limit, the queue pauses and new prompts queue.
resumecontinues the cut-short work — the next queued prompt, or a plain "Continue" when nothing is queued.resume_at_resetarms a host-side timer (reset + 30 s grace), so it happens with the window closed.dismissdrops the notice and leaves the queue paused. A plain queue resume also clears the limit. Any harness whose turn fails with the limit in its error text gets the fallback without a reset time. - Renderer:
UsageLimitNoticein the Composer (window/model/account facts,formatResetLabelcountdown ticking every 5 s inside the last two minutes, account switcher overusage.quota+usage.setDefaultAccount, thenAcpChatPane's crash-recovery restart so the chat reopens on the new login;modelDisplayNamemaps the id through the agent's model list or reads it as a name);TurnStatusRowbecomes a card with icon + title + message;transcriptRowsdrops the error notice that duplicates the turn's own error. - Eighteen new strings in all 16 locales.
Verified
- Unit: chat-runtime 280+ (5 live-session tests: pause + resume delivers the queue, resume with an empty queue sends "Continue", dismiss keeps the pause, resume-at-reset fires and can be cancelled, error-text fallback; 4 translator tests; 2 ACP adapter tests), chat 69, desktop ChatSession 249 (notice ×3, status row ×3, row dedupe); tsc for desktop/chat/chat-runtime; Biome;
check:i18n. - End to end in the dev app with a stand-in ACP agent that refuses the first prompt the way Claude does (a
usage_updatecarrying_claude/rateLimit: rejected, then the prompt failing with "You've hit your usage limit"): the notice appeared with the reset time, "Resume at reset" armed and flipped to "Resuming at reset", the notice turned into "Limit has reset · Resume" at the reset, and 30 s later the host sent "Continue" on its own and the agent replied. Screenshots in/tmp/ul-shot-{3,4,6,9}.png. - Live with the stand-in agent reporting
rateLimitType+_claude/model: the notice read "5-hour limit · Sonnet 5.5 · avi@superset.sh", and Switch account listed both logins with plan and tightest window (/tmp/ul-shot-10.png,-11.png). Not exercised: actually picking another account — that changes the machine's default login, so it was left for a hand test. - Not verified against a real Anthropic rate limit; the
rate_limit_infoshape is the one the CLI emits in the recorded fixtures (status,resetsAtin seconds,isUsingOverage).
Summary by cubic
When the provider refuses a turn for the account's usage, the chat session now pauses its queue and shows an amber notice on the composer instead of dropping the refusal and failing the turn twice in red.
- The notice names the limit window, model, and account — the account button shows the current login's email rather than always "Account" — with Resume, Resume at reset, dismiss, and a Switch account menu that reopens the chat on the new login.
- The queue pauses before the refused turn, so prompts already queued no longer hit the wall; queued work also survives an account switch, and resuming continues the cut-short work or sends a plain "Continue" when nothing is queued.
- Resume at reset fires from a host-side timer 30 s past the reset and is cancelled when the session ends.
- Both Claude paths report the limit; a failed turn renders as one card with the agent's own words.
respondToUsageLimitandSessionState.usageLimitare additive, so released clients are unaffected.