quotacap 0.0.52 → 0.0.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,6 +29,8 @@ export interface RecoveryDeps {
29
29
  */
30
30
  export declare function recoverSubscription(deps: RecoveryDeps): Promise<ParsedQuota>;
31
31
  export declare function museProbeDir(): string;
32
+ export declare function parseMusePlan(cleaned: string): string;
33
+ export declare function parseMuseQuotaExhausted(text: string, plan?: string, now?: Date): ParsedQuota | null;
32
34
  export declare function parseMuseTui(text: string, now?: Date, dims?: Partial<GridDims>): ParsedQuota;
33
35
  export declare const museAdapter: {
34
36
  id: string;
@@ -5,7 +5,7 @@ import { parseResetText } from "./parse.js";
5
5
  import { runPty } from "./pty.js";
6
6
  import { parseScreens } from "./vt.js";
7
7
  import { adapterSignal, trackedExecFile } from "../runtime/spawn.js";
8
- import { attachEvidence } from "../diagnostics/failure.js";
8
+ import { attachEvidence, getErrorEvidence } from "../diagnostics/failure.js";
9
9
  /** Canonical unavailability error: parseMuseTui throws it, poll() recognises it
10
10
  * to start recovery, and failure.ts classifies its text as service_unavailable. */
11
11
  export const MUSE_UNAVAILABLE_MESSAGE = "muse: subscription currently unavailable in TUI output";
@@ -48,6 +48,19 @@ export async function recoverSubscription(deps) {
48
48
  catch (warmError) {
49
49
  if (aborted())
50
50
  throw warmError;
51
+ const ev = getErrorEvidence(warmError);
52
+ const text = [
53
+ warmError?.message,
54
+ ev?.stdout,
55
+ ev?.stderr,
56
+ ].filter(Boolean).join("\n");
57
+ const unavailEv = getErrorEvidence(unavailable);
58
+ const plan = parseMusePlan(unavailEv?.stdout ?? "");
59
+ const quota = parseMuseQuotaExhausted(text, plan, new Date(now()));
60
+ if (quota) {
61
+ log(`subscription exhausted; parsed 100% quota from warm turn (resets ${quota.resetsAt})`);
62
+ return quota;
63
+ }
51
64
  log(`warm attempt ${attempt} failed: ${warmError?.message}`);
52
65
  continue;
53
66
  }
@@ -76,6 +89,32 @@ export async function recoverSubscription(deps) {
76
89
  export function museProbeDir() {
77
90
  return path.join(process.env.QUOTACAP_HOME ?? os.homedir(), ".quotacap", "muse-probe");
78
91
  }
92
+ export function parseMusePlan(cleaned) {
93
+ const planMatch = cleaned.match(/Subscription\s*·\s*Muse Code\s+(.+?)(?:\s{2,}|\r?\n|$)/i);
94
+ return planMatch ? planMatch[1].trim() : "unknown";
95
+ }
96
+ export function parseMuseQuotaExhausted(text, plan = "unknown", now = new Date()) {
97
+ const match = text.match(/Subscription quota exhausted\.?\s+Your usage window resets at\s+(\S+?)(?:\.|\s|$)/i);
98
+ if (!match)
99
+ return null;
100
+ const resetIso = match[1];
101
+ const resetMs = Date.parse(resetIso);
102
+ if (!Number.isFinite(resetMs) || resetMs <= now.getTime())
103
+ return null;
104
+ const resetsAt = new Date(resetMs).toISOString();
105
+ const periodStart = new Date(resetMs - 7 * 86400000).toISOString();
106
+ return {
107
+ provider: "muse",
108
+ plan,
109
+ weeklyPct: 100,
110
+ fiveHourPct: 0,
111
+ resetsAt,
112
+ periodStart,
113
+ source: "tui",
114
+ fetchedAt: now.toISOString(),
115
+ raw: text.slice(0, 4096),
116
+ };
117
+ }
79
118
  export function parseMuseTui(text, now = new Date(), dims) {
80
119
  return parseScreens(text, dims, (cleaned) => parseMuseCleaned(cleaned, now));
81
120
  }
@@ -101,10 +140,7 @@ function parseMuseCleaned(cleaned, now) {
101
140
  // First match wins; overdrawn transcripts carry two identical copies.
102
141
  // Vendor casing preserved (grok precedent: SuperGrok); the Muse Code
103
142
  // product prefix is stripped because the product is already the row label.
104
- let plan = "unknown";
105
- const planMatch = cleaned.match(/Subscription\s*·\s*Muse Code\s+(.+?)\s{2,}/i);
106
- if (planMatch)
107
- plan = planMatch[1].trim();
143
+ const plan = parseMusePlan(cleaned);
108
144
  // The Current-window reset ("Resets at 3:51 PM") is a bare clock time
109
145
  // with no date: resolve it against the panel's Brisbane date, rolling to
110
146
  // tomorrow once today's time has passed. Display-only; a missing or
@@ -157,18 +157,18 @@ function matchPrecedence(evidence, errorName, errorCode, text) {
157
157
  if (/\btrust this folder\b/i.test(text)) {
158
158
  return { code: "trust_prompt", phrase: "trust prompt" };
159
159
  }
160
- // Order 5: rate_limit — rate limit, rate_limit, too many requests, quota exceeded, or explicit HTTP status 429
160
+ // Order 5: rate_limit — rate limit, rate_limit, rate_limit_error, too many requests, quota exceeded/exhausted, or explicit HTTP/API status 429
161
161
  // A bare number 429 is insufficient
162
- if (/\brate[ _]limit\b/i.test(text)) {
162
+ if (/\brate[ _]limit(?:_error)?\b/i.test(text)) {
163
163
  return { code: "rate_limit", phrase: "rate limit" };
164
164
  }
165
165
  if (/\btoo many requests\b/i.test(text)) {
166
166
  return { code: "rate_limit", phrase: "too many requests" };
167
167
  }
168
- if (/\bquota exceeded\b/i.test(text)) {
168
+ if (/\bquota (?:exceeded|exhausted)\b/i.test(text)) {
169
169
  return { code: "rate_limit", phrase: "quota exceeded" };
170
170
  }
171
- if (/\b(?:status|code|http)\s*:?\s*429\b/i.test(text) || /\b429\s+too many requests\b/i.test(text)) {
171
+ if (/\b(?:status|code|http|api error)\s*:?\s*429\b/i.test(text) || /\b429\s+too many requests\b/i.test(text)) {
172
172
  return { code: "rate_limit", phrase: "HTTP 429" };
173
173
  }
174
174
  // Order 6: auth — login required, not logged in, logged out, unauthorized, unauthorised, forbidden,
@@ -1,5 +1,5 @@
1
1
  import type { ProviderSnapshot } from "../advisory/types.js";
2
- export type StateWord = "On track" | "Behind pace" | "Ahead of pace" | "Cap risk" | "Watch" | "Measuring" | "Not reporting" | "Monthly exhausted";
2
+ export type StateWord = "On track" | "Behind pace" | "Ahead of pace" | "Cap risk" | "Watch" | "Measuring" | "Not reporting" | "Monthly exhausted" | "Quota exhausted";
3
3
  export type RailCell = "used" | "tick" | "unused";
4
4
  export type SortKey = "recommended" | "reset-asc" | "reset-desc" | "used-desc" | "used-asc";
5
5
  export type CompactSuffix = "" | "!" | "~" | "?";
@@ -141,9 +141,10 @@ export function usedPctOf(ps) {
141
141
  }
142
142
  export function railCells(ps, now) {
143
143
  const cells = new Array(RAIL_CELLS).fill("unused");
144
- if (ps.exclusionReason !== null)
145
- return cells;
146
144
  const u = bindingUsedPct(ps);
145
+ const is100Exhausted = typeof u === "number" && u >= 100 && !ps.resetPassed;
146
+ if (ps.exclusionReason !== null && !is100Exhausted)
147
+ return cells;
147
148
  const used = typeof u === "number" && Number.isFinite(u)
148
149
  ? Math.min(RAIL_CELLS, Math.max(0, Math.round((u / 100) * RAIL_CELLS)))
149
150
  : 0;
@@ -170,6 +171,12 @@ export function stateWord(ps) {
170
171
  // spent until the month resets.
171
172
  if (adv.bindingWindow === "monthly" && adv.bindingRemaining === 0)
172
173
  return "Monthly exhausted";
174
+ const u = bindingUsedPct(ps);
175
+ const isExhausted = (typeof adv.bindingRemaining === "number" && adv.bindingRemaining <= 0) ||
176
+ (typeof adv.remaining === "number" && adv.remaining <= 0) ||
177
+ (typeof u === "number" && u >= 100);
178
+ if (isExhausted)
179
+ return "Quota exhausted";
173
180
  if (adv.status === "at risk")
174
181
  return "Cap risk";
175
182
  if (adv.status === "watch")
@@ -30,6 +30,7 @@ const STATE_COLORS = {
30
30
  "Ahead of pace": "\x1b[93m",
31
31
  "Watch": "\x1b[38;5;214m",
32
32
  "Cap risk": "\x1b[31m",
33
+ "Quota exhausted": "\x1b[31m",
33
34
  "Measuring": "\x1b[90m",
34
35
  "Not reporting": "\x1b[90m",
35
36
  "Monthly exhausted": "\x1b[31m",
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // generated by scripts/build-embed.mjs — do not edit
2
- export const VERSION = "0.0.52";
2
+ export const VERSION = "0.0.54";
@@ -5,6 +5,7 @@ import { PACE_TIPS, PaceStats, barGeometry, isMonthlyBound, paceBadge, paceFigur
5
5
  export function burnTone(provider) {
6
6
  const badge = paceBadge(provider);
7
7
  switch (badge) {
8
+ case "Quota exhausted":
8
9
  case "Cap risk":
9
10
  case "Monthly exhausted":
10
11
  case "Watch":
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import type { ExclusionReason, ProviderView } from "../state.js";
3
- export type PaceBadge = "Not reporting" | "Cap risk" | "Watch" | "Behind pace" | "Ahead of pace" | "On track" | "Measuring" | "Monthly exhausted";
3
+ export type PaceBadge = "Not reporting" | "Cap risk" | "Watch" | "Behind pace" | "Ahead of pace" | "On track" | "Measuring" | "Monthly exhausted" | "Quota exhausted";
4
4
  /**
5
5
  * Pace-state badge from the mapping table. Exclusion always wins; a blocked
6
6
  * month next; otherwise status "at risk" wins, then "watch", then "unknown";
@@ -190,12 +190,13 @@ export interface PaceHeadline {
190
190
  /** True when the projected-unused hatch belongs on the bar. */
191
191
  hatch: boolean;
192
192
  }
193
+ export declare function isQuotaExhaustedBeforeReset(provider: ProviderView, asOfMs?: number): boolean;
193
194
  /**
194
195
  * Headline forecast shared by the card bar and the drawer quota row:
195
196
  * "expires unused" wording when a waste hatch applies, otherwise the
196
197
  * one-line forecast. Null when there is nothing to forecast.
197
198
  */
198
- export declare function paceHeadline(provider: ProviderView): PaceHeadline | null;
199
+ export declare function paceHeadline(provider: ProviderView, asOfMs?: number): PaceHeadline | null;
199
200
  /**
200
201
  * Two-tier pacing rail: used fill, elapsed tick, projected-unused hatch.
201
202
  * Unknown pace shows fill and tick but no hatch and no numeric rate.
@@ -14,6 +14,12 @@ export function paceBadge(provider) {
14
14
  return "Not reporting";
15
15
  if (advisory.bindingWindow === "monthly" && advisory.bindingRemaining === 0)
16
16
  return "Monthly exhausted";
17
+ const quotaUsed = quotaUsedPct(provider);
18
+ const isExhausted = (typeof advisory.bindingRemaining === "number" && advisory.bindingRemaining <= 0) ||
19
+ (typeof advisory.remaining === "number" && advisory.remaining <= 0) ||
20
+ (quotaUsed !== null && quotaUsed >= 100);
21
+ if (isExhausted)
22
+ return "Quota exhausted";
17
23
  if (advisory.status === "at risk")
18
24
  return "Cap risk";
19
25
  if (advisory.status === "watch")
@@ -399,6 +405,7 @@ export function badgeColor(badge) {
399
405
  return "var(--ahead)";
400
406
  case "Watch":
401
407
  return "var(--watch)";
408
+ case "Quota exhausted":
402
409
  case "Monthly exhausted":
403
410
  case "Cap risk":
404
411
  return "var(--danger)";
@@ -418,6 +425,7 @@ export function fillToken(badge) {
418
425
  return "var(--fill-ahead)";
419
426
  case "Watch":
420
427
  return "var(--fill-watch)";
428
+ case "Quota exhausted":
421
429
  case "Monthly exhausted":
422
430
  case "Cap risk":
423
431
  return "var(--fill-cap)";
@@ -437,6 +445,7 @@ export function edgeToken(badge) {
437
445
  return "var(--edge-ahead)";
438
446
  case "Watch":
439
447
  return "var(--edge-watch)";
448
+ case "Quota exhausted":
440
449
  case "Monthly exhausted":
441
450
  case "Cap risk":
442
451
  return "var(--edge-cap)";
@@ -452,6 +461,7 @@ export function outKind(badge) {
452
461
  return "behind";
453
462
  case "Watch":
454
463
  return "watch";
464
+ case "Quota exhausted":
455
465
  case "Monthly exhausted":
456
466
  case "Cap risk":
457
467
  return "cap";
@@ -474,7 +484,13 @@ export function hatchGradient(_badge) {
474
484
  */
475
485
  export function badgeTitle(badge, estimated) {
476
486
  let title;
477
- if (badge === "Cap risk") {
487
+ if (badge === "Quota exhausted") {
488
+ title = "Quota fully used until reset";
489
+ }
490
+ else if (badge === "Monthly exhausted") {
491
+ title = "Monthly quota fully used until reset";
492
+ }
493
+ else if (badge === "Cap risk") {
478
494
  title = "Burning faster than the window allows and ahead of elapsed time";
479
495
  }
480
496
  else if (badge === "Watch") {
@@ -495,7 +511,7 @@ export function Badge({ provider, compact = false, }) {
495
511
  ? "pace-ahead"
496
512
  : badge === "Watch"
497
513
  ? "pace-watch"
498
- : badge === "Cap risk" || badge === "Monthly exhausted"
514
+ : badge === "Cap risk" || badge === "Monthly exhausted" || badge === "Quota exhausted"
499
515
  ? "pace-cap"
500
516
  : "pace-out"; // Not reporting and Measuring share the neutral style
501
517
  const title = compact && detail
@@ -503,16 +519,41 @@ export function Badge({ provider, compact = false, }) {
503
519
  : badgeTitle(badge, quotaResetEstimated(provider));
504
520
  return (_jsxs("span", { "data-testid": "pace-badge", className: `pace ${badgeClass}`, title: title, children: [badge, !compact && detail ? ` · ${detail}` : ""] }));
505
521
  }
522
+ export function isQuotaExhaustedBeforeReset(provider, asOfMs) {
523
+ if (provider.resetPassed)
524
+ return false;
525
+ const resetIso = quotaResetsAt(provider);
526
+ if (!resetIso)
527
+ return false;
528
+ const resetMs = Date.parse(resetIso);
529
+ if (Number.isFinite(resetMs) && asOfMs !== undefined && resetMs <= asOfMs)
530
+ return false;
531
+ const used = quotaUsedPct(provider);
532
+ if (used !== null && used >= 100)
533
+ return true;
534
+ if (isMonthlyBound(provider) && provider.quota?.monthlyStatus === "exhausted")
535
+ return true;
536
+ return false;
537
+ }
506
538
  /**
507
539
  * Headline forecast shared by the card bar and the drawer quota row:
508
540
  * "expires unused" wording when a waste hatch applies, otherwise the
509
541
  * one-line forecast. Null when there is nothing to forecast.
510
542
  */
511
- export function paceHeadline(provider) {
543
+ export function paceHeadline(provider, asOfMs) {
512
544
  const badge = paceBadge(provider);
513
545
  const monthlyBinds = provider.advisory?.bindingWindow === "monthly";
514
546
  const wastePct = provider.advisory?.wastePct;
515
547
  const hatch = !monthlyBinds && wastePct !== null && wastePct !== undefined && Number.isFinite(wastePct) && wastePct >= 3;
548
+ const is100Exhausted = isQuotaExhaustedBeforeReset(provider, asOfMs);
549
+ if (provider.exclusionReason !== null && is100Exhausted) {
550
+ const line = forecastLine(provider);
551
+ return {
552
+ text: line ? `Exhausted · ${line}` : "Exhausted · quota fully used",
553
+ kind: "cap",
554
+ hatch: false,
555
+ };
556
+ }
516
557
  const text = hatch && badge === "Behind pace" ? `${Math.round(wastePct)}% expires unused` : forecastLine(provider);
517
558
  if (!text)
518
559
  return null;
@@ -532,17 +573,20 @@ export function PaceBar({ provider, asOf, showHeadline = true, }) {
532
573
  const asOfMs = Date.parse(asOf);
533
574
  const { usedPct, elapsedPct } = barGeometry(provider, asOfMs);
534
575
  const wastePct = provider.advisory?.wastePct;
535
- const headline = paceHeadline(provider);
576
+ const headline = paceHeadline(provider, asOfMs);
536
577
  // A weekly-sized hatch under a month-bound card restates issue 109.
537
578
  const showHatch = headline?.hatch ?? false;
538
579
  const quotaUsed = quotaUsedPct(provider);
539
580
  const label = provider.quota
540
581
  ? `${provider.id}: ${quotaUsed ?? provider.quota.weeklyPct}% used, ${badge}`
541
582
  : `${provider.id}: no readings, ${badge}`;
583
+ const is100Exhausted = isQuotaExhaustedBeforeReset(provider, asOfMs);
584
+ const fillBg = is100Exhausted ? "var(--fill-cap)" : fillToken(badge);
585
+ const edgeBg = is100Exhausted ? "var(--edge-cap)" : edgeToken(badge);
542
586
  return (_jsxs("div", { "data-testid": "pace-bar", role: "img", "aria-label": label, className: "trackwrap", children: [showHeadline && provider.quota && (_jsxs("div", { className: "tline", children: [_jsxs("span", { className: "used", children: [quotaUsed ?? provider.quota.weeklyPct, "% used"] }), headline && headline.kind !== "none" ? (_jsxs("span", { className: `out out-${headline.kind}`, children: ["\u2192 ", headline.text] })) : headline ? (_jsxs("span", { className: "out out-none", children: ["\u2192 ", headline.text] })) : null] })), _jsxs("div", { className: "track", children: [_jsx("div", { "aria-hidden": "true", className: "fill", style: {
543
587
  width: `${usedPct}%`,
544
- background: fillToken(badge),
545
- ["--fill-edge"]: edgeToken(badge),
588
+ background: fillBg,
589
+ ["--fill-edge"]: edgeBg,
546
590
  } }), showHatch && (_jsx("div", { "aria-hidden": "true", "data-testid": "pace-hatch", className: "band band-waste", style: {
547
591
  left: `${100 - wastePct}%`,
548
592
  width: `${wastePct}%`,
@@ -5,6 +5,7 @@ const STATES = [
5
5
  { label: "Behind pace", cls: "pace-behind", meaning: "Burning slow; risk of unused quota waste" },
6
6
  { label: "Ahead of pace", cls: "pace-ahead", meaning: "Burning faster than time; monitor usage" },
7
7
  { label: "Cap risk", cls: "pace-cap", meaning: "Will hit cap before reset window closes" },
8
+ { label: "Quota exhausted", cls: "pace-cap", meaning: "Quota is 100% consumed until next reset" },
8
9
  { label: "Watch", cls: "pace-watch", meaning: "Pace points at the cap, but position or the reset clock is uncertain" },
9
10
  { label: "Measuring", cls: "pace-out", meaning: "Not enough data yet; pace appears after 6h" },
10
11
  { label: "Not reporting", cls: "pace-out", meaning: "No usable reading; excluded from ranking" },
@@ -67,7 +67,7 @@ export function drawerModel(provider) {
67
67
  lastSuccessAt: provider.lastSuccessAt,
68
68
  ageMs: provider.ageMs,
69
69
  failureCategory: provider.lastAttempt?.failureCategory ?? null,
70
- failureWords: failureWords(provider.lastAttempt?.failureCategory ?? null),
70
+ failureWords: provider.lastAttempt?.summary ?? failureWords(provider.lastAttempt?.failureCategory ?? null),
71
71
  evidence: [...provider.evidence],
72
72
  exclusionReason: provider.exclusionReason,
73
73
  exclusionWords: exclusionWords(provider.exclusionReason),
@@ -484,21 +484,35 @@ export function ProviderDrawer({ provider, asOf, recommendation, onClose, onRena
484
484
  const limitForecast = compactLimitForecast(provider);
485
485
  const wastePct = provider.advisory?.wastePct ?? null;
486
486
  const weeklyHatchPct = !monthlyPrimary && headline?.hatch ? wastePct : null;
487
+ const isWeekly100 = provider.quota &&
488
+ !provider.resetPassed &&
489
+ provider.quota.weeklyPct >= 100 &&
490
+ (!provider.quota.resetsAt || Date.parse(provider.quota.resetsAt) > asOfMs);
487
491
  const weeklyRow = provider.quota ? (_jsx(LimitRow, { name: "Weekly limit", pct: provider.quota.weeklyPct, quota: !monthlyPrimary, resetLine: weeklyResetLine, forecast: monthlyPrimary ? null : limitForecast, hatchPct: weeklyHatchPct, fillStyle: monthlyPrimary
488
492
  ? limitFill(provider.quota.weeklyPct)
489
- : {
490
- width: `${Math.min(100, Math.max(0, provider.quota.weeklyPct))}%`,
491
- background: fillToken(model.badge),
492
- ["--fill-edge"]: edgeToken(model.badge),
493
- }, elapsedPct: weeklyElapsed })) : null;
493
+ : isWeekly100
494
+ ? {
495
+ width: `${Math.min(100, Math.max(0, provider.quota.weeklyPct))}%`,
496
+ background: "var(--fill-cap)",
497
+ ["--fill-edge"]: "var(--edge-cap)",
498
+ }
499
+ : {
500
+ width: `${Math.min(100, Math.max(0, provider.quota.weeklyPct))}%`,
501
+ background: fillToken(model.badge),
502
+ ["--fill-edge"]: edgeToken(model.badge),
503
+ }, elapsedPct: weeklyElapsed })) : null;
494
504
  const monthlyRow = model.monthlyKind === "included" &&
495
505
  model.monthlyPct !== undefined &&
496
506
  Number.isFinite(model.monthlyPct) ? (_jsx(LimitRow, { name: "Monthly limit", pct: model.monthlyPct, quota: monthlyPrimary, resetLine: monthlyResetLine, forecast: monthlyPrimary ? limitForecast : null, fillStyle: monthlyPrimary
497
507
  ? trackFillForPct(model.monthlyPct, model.monthlyStatus === "exhausted")
498
508
  : limitFill(model.monthlyPct, model.monthlyStatus === "exhausted"), elapsedPct: monthlyElapsed, testId: "drawer-monthly" })) : null;
509
+ const fiveResetPassed = model.fiveHourResetsAt !== undefined &&
510
+ Number.isFinite(Date.parse(model.fiveHourResetsAt)) &&
511
+ Date.parse(model.fiveHourResetsAt) <= asOfMs;
512
+ const effectiveFivePct = fiveResetPassed ? 0 : model.fiveHourPct;
499
513
  const fiveRow = model.fiveHourPct !== undefined &&
500
514
  model.fiveHourPct !== null &&
501
- Number.isFinite(model.fiveHourPct) ? (_jsx(LimitRow, { name: shortWindowLabel(provider.id) ?? "5h limit", pct: model.fiveHourPct, resetLine: fiveResetLineNode, fillStyle: limitFill(model.fiveHourPct), elapsedPct: fiveElapsed })) : null;
515
+ Number.isFinite(model.fiveHourPct) ? (_jsx(LimitRow, { name: shortWindowLabel(provider.id) ?? "5h limit", pct: effectiveFivePct ?? 0, resetLine: fiveResetLineNode, fillStyle: limitFill(effectiveFivePct ?? 0), elapsedPct: fiveResetPassed ? null : fiveElapsed })) : null;
502
516
  const anyElapsed = [weeklyElapsed, monthlyElapsed, fiveElapsed].some((v) => v !== null);
503
517
  return (_jsxs(_Fragment, { children: [_jsx("div", { "data-testid": "drawer-scrim", className: "scrim is-open", "aria-hidden": "true", onClick: onClose }), _jsxs("aside", { ref: dialogRef, tabIndex: -1, "data-testid": "provider-drawer", role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, className: "drawer is-open", onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "drawer-head", children: [_jsx("span", { className: "picon", style: providerTint(provider.id), children: _jsx(ProviderIcon, { id: provider.id, title: model.name }) }), _jsxs("div", { className: "who", style: { flex: 1, minWidth: 0 }, children: [_jsxs("h2", { id: titleId, style: isEditing
504
518
  ? {
@@ -239,6 +239,7 @@ function pinPace(provider) {
239
239
  return { cls: "pace-ahead", color: "var(--ahead)" };
240
240
  case "Watch":
241
241
  return { cls: "pace-watch", color: "var(--watch)" };
242
+ case "Quota exhausted":
242
243
  case "Monthly exhausted":
243
244
  case "Cap risk":
244
245
  return { cls: "pace-cap", color: "var(--danger)" };
@@ -43,9 +43,10 @@ export function windowUnits(provider, asOfMs) {
43
43
  reset: left ? `in ${left}` : null });
44
44
  }
45
45
  if (typeof q.fiveHourPct === "number" && Number.isFinite(q.fiveHourPct)) {
46
- // No adapter reports a 5h reset, so the 5h tag carries no time.
46
+ const passed = q.fiveHourResetsAt ? Date.parse(q.fiveHourResetsAt) <= asOfMs : false;
47
+ const effectivePct = passed ? 0 : q.fiveHourPct;
47
48
  units.push({ win: "five", tag: "5h", cardLabel: "5h limit", rowLabel: "5h",
48
- pct: q.fiveHourPct, heat: heatOf(q.fiveHourPct, false), time: null, reset: null });
49
+ pct: effectivePct, heat: heatOf(effectivePct, false), time: null, reset: null });
49
50
  }
50
51
  if (!monthlyBound && q.monthlyKind === "included" && typeof q.monthlyPct === "number" && q.monthlyResetsAt) {
51
52
  const exhausted = q.monthlyStatus === "exhausted";