quotacap 0.0.39 → 0.0.41

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.
Files changed (68) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/agy.js +2 -2
  3. package/dist/adapters/claude.js +1 -1
  4. package/dist/adapters/codex.js +29 -20
  5. package/dist/adapters/grok.js +1 -1
  6. package/dist/adapters/kimi.d.ts +41 -0
  7. package/dist/adapters/kimi.js +453 -21
  8. package/dist/adapters/manual.js +2 -2
  9. package/dist/adapters/muse.js +3 -3
  10. package/dist/adapters/opencode-go.js +30 -2
  11. package/dist/adapters/pty.d.ts +9 -0
  12. package/dist/adapters/pty.js +16 -0
  13. package/dist/adapters/types.d.ts +17 -2
  14. package/dist/advisory/engine.d.ts +4 -2
  15. package/dist/advisory/engine.js +55 -14
  16. package/dist/advisory/snapshot.js +11 -3
  17. package/dist/advisory/types.d.ts +11 -3
  18. package/dist/cli/advise.js +1 -0
  19. package/dist/cli/clients.d.ts +4 -0
  20. package/dist/cli/models.js +5 -1
  21. package/dist/cli/progress.d.ts +32 -0
  22. package/dist/cli/progress.js +70 -0
  23. package/dist/cli/runtime.d.ts +5 -0
  24. package/dist/cli/runtime.js +18 -8
  25. package/dist/cli/status.js +1 -0
  26. package/dist/cli/takeover.d.ts +3 -0
  27. package/dist/cli/takeover.js +7 -1
  28. package/dist/cli/update.d.ts +4 -0
  29. package/dist/cli/update.js +8 -1
  30. package/dist/diagnostics/failure.js +46 -11
  31. package/dist/format/rows.d.ts +6 -1
  32. package/dist/format/rows.js +27 -4
  33. package/dist/format/terminal.js +5 -4
  34. package/dist/http/server.js +3 -3
  35. package/dist/runtime/poll.js +1 -1
  36. package/dist/service/index.js +3 -1
  37. package/dist/service/macos.d.ts +2 -0
  38. package/dist/service/macos.js +4 -1
  39. package/dist/service/systemd.d.ts +2 -0
  40. package/dist/service/systemd.js +5 -3
  41. package/dist/store/db.js +12 -1
  42. package/dist/store/quotas.js +33 -8
  43. package/dist/version.js +1 -1
  44. package/dist/web/src/components/AdviceDrawer.js +10 -5
  45. package/dist/web/src/components/FaultBanner.d.ts +10 -5
  46. package/dist/web/src/components/FaultBanner.js +19 -9
  47. package/dist/web/src/components/PaceBar.d.ts +9 -5
  48. package/dist/web/src/components/PaceBar.js +32 -10
  49. package/dist/web/src/components/ProviderCard.js +4 -2
  50. package/dist/web/src/components/ProviderDrawer.d.ts +7 -1
  51. package/dist/web/src/components/ProviderDrawer.js +46 -9
  52. package/dist/web/src/components/ProviderRow.js +4 -4
  53. package/dist/web/src/components/Recommendation.d.ts +5 -2
  54. package/dist/web/src/components/Recommendation.js +8 -4
  55. package/dist/web/src/components/ResetRail.js +3 -2
  56. package/dist/web/src/components/SubscriptionList.d.ts +2 -1
  57. package/dist/web/src/components/SubscriptionList.js +8 -5
  58. package/dist/web/src/components/WindowsRow.d.ts +30 -0
  59. package/dist/web/src/components/WindowsRow.js +68 -0
  60. package/dist/web/src/pages/Onboarding.js +2 -1
  61. package/dist/web/src/state.d.ts +12 -0
  62. package/dist/webAssets.js +1 -1
  63. package/dist/webHtml.js +1 -1
  64. package/package.json +1 -1
  65. package/web/dist/assets/index-BUK8xUHm.js +12 -0
  66. package/web/dist/assets/{index-NtT9XTrm.css → index-Dk6l5h27.css} +1 -1
  67. package/web/dist/index.html +2 -2
  68. package/web/dist/assets/index-CD8jaN87.js +0 -12
@@ -3,12 +3,16 @@ import { ageLabel } from "../state.js";
3
3
  import { timeLeft } from "./PaceBar.js";
4
4
  import { ProviderIcon, providerTint } from "./ProviderIcon.js";
5
5
  /**
6
- * Lane membership from server urgency only. Any exclusionReason keeps the
7
- * provider out of both lanes; "on track" belongs to neither lane.
6
+ * Lane membership from server urgency only. Any exclusionReason, or an
7
+ * exhausted binding month, keeps the provider out of both lanes; "on track"
8
+ * belongs to neither lane.
8
9
  */
9
10
  export function laneOf(advisory, exclusionReason) {
10
11
  if (exclusionReason !== null)
11
12
  return null;
13
+ // A blocked month vetoes weekly leftover: no lane, exactly like an exclusion.
14
+ if (advisory.bindingWindow === "monthly" && advisory.bindingRemaining === 0)
15
+ return null;
12
16
  switch (advisory.urgency) {
13
17
  case "burn now":
14
18
  case "use soon":
@@ -45,7 +49,7 @@ function LaneSection({ testId, label, tintClass, laneClass, icon, members, provi
45
49
  const quota = byId.get(m.provider)?.quota;
46
50
  const left = quota ? timeLeft(quota.resetsAt, asOfMs) : null;
47
51
  const name = byId.get(m.provider)?.displayName ?? m.provider;
48
- return (_jsxs("div", { className: "rsub", children: [_jsx("span", { className: "sq", style: providerTint(m.provider), "aria-hidden": "true", children: _jsx(ProviderIcon, { id: m.provider, title: name, size: 13 }) }), _jsx("div", { className: "rnm-group", children: _jsx("button", { type: "button", className: "rnm", onClick: () => onSelectProvider?.(m.provider), "aria-label": `View ${name} details`, children: name }) }), _jsx("span", { className: "rnum", children: quota ? (_jsxs(_Fragment, { children: [_jsxs("b", { children: [quota.usedPct, "%"] }), " used", left ? (_jsxs(_Fragment, { children: [" · ", _jsx("b", { children: left }), " left"] })) : null] })) : null })] }, m.provider));
52
+ return (_jsxs("div", { className: "rsub", children: [_jsx("span", { className: "sq", style: providerTint(m.provider), "aria-hidden": "true", children: _jsx(ProviderIcon, { id: m.provider, title: name, size: 13 }) }), _jsx("div", { className: "rnm-group", children: _jsx("button", { type: "button", className: "rnm", onClick: () => onSelectProvider?.(m.provider), "aria-label": `View ${name} details`, children: name }) }), _jsx("span", { className: "rnum", children: quota ? (_jsxs(_Fragment, { children: [_jsxs("b", { children: [quota.weeklyPct, "%"] }), " used", left ? (_jsxs(_Fragment, { children: [" · ", _jsx("b", { children: left }), " left"] })) : null] })) : null })] }, m.provider));
49
53
  }) })] }));
50
54
  }
51
55
  /**
@@ -77,7 +81,7 @@ export function Recommendation({ recommendation, providers, asOf, onSelectProvid
77
81
  if (isClear) {
78
82
  return (_jsxs("section", { "aria-label": "Quota advice and pacing", children: [head, _jsxs("section", { "data-testid": "recommendation", "aria-label": "Recommendation", className: "recwrap is-clear", role: "status", children: [_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z" }), _jsx("path", { d: "M8 12.5l2.8 2.8L16 10" })] }), _jsxs("div", { children: [_jsx("b", { children: "No subscription needs priority" }), _jsx("span", { "data-testid": "rec-prose", children: recommendation.reason })] })] })] }));
79
83
  }
80
- return (_jsxs("section", { "aria-label": "Quota advice and pacing", children: [head, _jsxs("section", { "data-testid": "recommendation", "aria-label": "Recommendation", className: "recwrap", role: "status", children: [_jsxs("div", { className: "rec-top", children: [_jsxs("div", { className: "rec-lead", children: [_jsxs("span", { className: "rec-badge", children: [_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z" }) }), "Recommendation"] }), recommendation.use === "none" ? (_jsx("p", { className: "rec-advice-prose", "data-testid": "rec-prose", children: recommendation.reason })) : (_jsxs("p", { className: "rec-advice-prose", "data-testid": "rec-prose", children: ["Switch to ", _jsx("strong", { children: pick?.displayName ?? recommendation.use }), " next", recommendation.wastePct !== null ? (_jsxs(_Fragment, { children: [" — ", _jsxs("strong", { children: [Math.round(recommendation.wastePct), "%"] }), " unused quota forecast", pickLeft ? _jsxs(_Fragment, { children: [" to expire in ", _jsx("strong", { children: pickLeft })] }) : null, "."] })) : (_jsxs(_Fragment, { children: [" \u2014 ", recommendation.reason] }))] })), measuring && (_jsx("span", { className: "rec-badge", "data-testid": "rec-measuring", style: {
84
+ return (_jsxs("section", { "aria-label": "Quota advice and pacing", children: [head, _jsxs("section", { "data-testid": "recommendation", "aria-label": "Recommendation", className: "recwrap", role: "status", children: [_jsxs("div", { className: "rec-top", children: [_jsxs("div", { className: "rec-lead", children: [_jsxs("span", { className: "rec-badge", children: [_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z" }) }), "Recommendation"] }), recommendation.use === "none" ? (_jsx("p", { className: "rec-advice-prose", "data-testid": "rec-prose", children: recommendation.reason })) : (_jsxs("p", { className: "rec-advice-prose", "data-testid": "rec-prose", children: ["Switch to ", _jsx("strong", { children: pick?.displayName ?? recommendation.use }), " next", recommendation.wastePct !== null && recommendation.bindingWindow !== "monthly" ? (_jsxs(_Fragment, { children: [" — ", _jsxs("strong", { children: [Math.round(recommendation.wastePct), "%"] }), " unused quota forecast", pickLeft ? _jsxs(_Fragment, { children: [" to expire in ", _jsx("strong", { children: pickLeft })] }) : null, "."] })) : (_jsxs(_Fragment, { children: [" \u2014 ", recommendation.reason] }))] })), measuring && (_jsx("span", { className: "rec-badge", "data-testid": "rec-measuring", style: {
81
85
  background: "var(--ahead-soft)",
82
86
  color: "var(--ahead)",
83
87
  borderColor: "var(--ahead)",
@@ -112,7 +112,7 @@ function ClusterPopover({ cluster, providers, asOfMs, onSelect, onClose, }) {
112
112
  return (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => {
113
113
  onSelect(id);
114
114
  onClose();
115
- }, children: [_jsx("b", { children: name }), _jsxs("span", { children: [provider?.quota ? `${provider.quota.usedPct}% · ` : "", provider ? resetCountdown(provider, asOfMs) : ""] })] }) }, id));
115
+ }, children: [_jsx("b", { children: name }), _jsxs("span", { children: [provider?.quota ? `${provider.quota.weeklyPct}% · ` : "", provider ? resetCountdown(provider, asOfMs) : ""] })] }) }, id));
116
116
  }) }), _jsx("button", { type: "button", className: "btn btn-sm btn-quiet", style: { marginTop: "var(--s2)", width: "100%" }, onClick: onClose, children: "Close" })] }));
117
117
  }
118
118
  function tzAbbrev() {
@@ -136,6 +136,7 @@ function pinPace(provider) {
136
136
  return { cls: "pace-ahead", color: "var(--ahead)" };
137
137
  case "Watch":
138
138
  return { cls: "pace-watch", color: "var(--watch)" };
139
+ case "Monthly exhausted":
139
140
  case "Cap risk":
140
141
  return { cls: "pace-cap", color: "var(--danger)" };
141
142
  default:
@@ -177,7 +178,7 @@ export function ResetRail({ providers, asOf, onSelectProvider, }) {
177
178
  return (_jsxs("button", { "data-testid": "pin", type: "button", className: `pin ${pin.band} ${tierClass} ${alignClass}`, style: {
178
179
  left: `${pin.positionPct}%`,
179
180
  "--dot-color": paceColor,
180
- }, "aria-label": `${name}: ${pace}, resets ${when}`, onClick: () => onSelectProvider(pin.id), children: [pin.band === "above" ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "pin-when", children: pin.estimated ? `${when} (est.)` : when }), _jsx("span", { className: `pin-label ${paceCls}`, style: pinBorder, children: name }), _jsx("span", { className: "pin-stem" }), _jsx("span", { className: "pin-dot" })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "pin-dot" }), _jsx("span", { className: "pin-stem" }), _jsx("span", { className: `pin-label ${paceCls}`, style: pinBorder, children: name }), _jsx("span", { className: "pin-when", children: pin.estimated ? `${when} (est.)` : when })] })), _jsxs("span", { className: "pin-tooltip", role: "tooltip", children: [_jsx("b", { children: name }), _jsx("span", { className: "pt-sep", children: "\u00B7" }), provider?.quota ? `${provider.quota.usedPct}% used` : "no readings", _jsx("span", { className: "pt-sep", children: "\u00B7" }), left ? `${left} left` : when, pin.estimated ? " (est.)" : ""] })] }, pin.id));
181
+ }, "aria-label": `${name}: ${pace}, resets ${when}`, onClick: () => onSelectProvider(pin.id), children: [pin.band === "above" ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "pin-when", children: pin.estimated ? `${when} (est.)` : when }), _jsx("span", { className: `pin-label ${paceCls}`, style: pinBorder, children: name }), _jsx("span", { className: "pin-stem" }), _jsx("span", { className: "pin-dot" })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "pin-dot" }), _jsx("span", { className: "pin-stem" }), _jsx("span", { className: `pin-label ${paceCls}`, style: pinBorder, children: name }), _jsx("span", { className: "pin-when", children: pin.estimated ? `${when} (est.)` : when })] })), _jsxs("span", { className: "pin-tooltip", role: "tooltip", children: [_jsx("b", { children: name }), _jsx("span", { className: "pt-sep", children: "\u00B7" }), provider?.quota ? `${provider.quota.weeklyPct}% used` : "no readings", _jsx("span", { className: "pt-sep", children: "\u00B7" }), left ? `${left} left` : when, pin.estimated ? " (est.)" : ""] })] }, pin.id));
181
182
  }), placement.clusters.map((cluster, index) => {
182
183
  const alignClass = cluster.positionPct < 12 ? "pin-start" : cluster.positionPct > 88 ? "pin-end" : "";
183
184
  const tierClass = cluster.tier > 0 ? `tier${cluster.tier + 1}` : "tier1";
@@ -4,7 +4,8 @@ export type SortKey = "recommended" | "reset-asc" | "reset-desc" | "used-desc" |
4
4
  export type Density = "cards" | "table";
5
5
  /**
6
6
  * Sort over server fields only. Recommended puts the server pick first, then
7
- * lane members, then the rest in server order, excluded last. Sort is stable.
7
+ * lane members, then the rest in server order, excluded last. A blocked month
8
+ * sorts with the excluded rows. Sort is stable.
8
9
  */
9
10
  export declare function sortProviders(providers: ProviderView[], key: SortKey, recommendation: RecommendationView): ProviderView[];
10
11
  /** Density follows the control at every width — the prototype keeps Cards and Table visible on mobile. */
@@ -7,7 +7,8 @@ import { ProviderRow } from "./ProviderRow.js";
7
7
  import { resetCountdown } from "./PaceBar.js";
8
8
  /**
9
9
  * Sort over server fields only. Recommended puts the server pick first, then
10
- * lane members, then the rest in server order, excluded last. Sort is stable.
10
+ * lane members, then the rest in server order, excluded last. A blocked month
11
+ * sorts with the excluded rows. Sort is stable.
11
12
  */
12
13
  export function sortProviders(providers, key, recommendation) {
13
14
  const list = [...providers];
@@ -21,15 +22,17 @@ export function sortProviders(providers, key, recommendation) {
21
22
  if (!order.has(a.provider))
22
23
  order.set(a.provider, rank++);
23
24
  }
25
+ const blocked = (p) => p.exclusionReason !== null ||
26
+ (p.advisory?.bindingWindow === "monthly" && p.advisory.bindingRemaining === 0);
24
27
  const ranked = list
25
- .filter((p) => p.exclusionReason === null && order.has(p.id))
28
+ .filter((p) => !blocked(p) && order.has(p.id))
26
29
  .sort((a, b) => order.get(a.id) - order.get(b.id));
27
- const rest = list.filter((p) => p.exclusionReason === null && !order.has(p.id));
28
- const excluded = list.filter((p) => p.exclusionReason !== null);
30
+ const rest = list.filter((p) => !blocked(p) && !order.has(p.id));
31
+ const excluded = list.filter(blocked);
29
32
  return [...ranked, ...rest, ...excluded];
30
33
  }
31
34
  const resetMs = (p) => (p.quota ? Date.parse(p.quota.resetsAt) : NaN);
32
- const used = (p) => (p.quota ? p.quota.usedPct : NaN);
35
+ const used = (p) => (p.quota ? p.quota.weeklyPct : NaN);
33
36
  const withInvalidLast = (cmp, value) => list.sort((a, b) => {
34
37
  const va = value(a);
35
38
  const vb = value(b);
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import type { ProviderView } from "../state.js";
3
+ /** One-unit countdown for a window tag: "7d", "4h" or "40m". Null when passed or unparseable. */
4
+ export declare function windowCountdown(resetsAt: string, asOfMs: number): string | null;
5
+ export interface WindowUnit {
6
+ win: "five" | "month";
7
+ tag: "5h" | "Mth";
8
+ cardLabel: string;
9
+ rowLabel: string;
10
+ pct: number;
11
+ /** Presentation-only heat: < 80 none, 80–99 hot, 100 or exhausted full. */
12
+ heat: "" | "hot" | "full";
13
+ /** Tag time outside the pill: "in 7d", or the weekday alone when the month is exhausted. */
14
+ time: string | null;
15
+ /** Expanded card track's reset line. */
16
+ reset: string | null;
17
+ }
18
+ /** Present windows only; a unit with no data is not rendered at all. */
19
+ export declare function windowUnits(provider: ProviderView, asOfMs: number): WindowUnit[];
20
+ /**
21
+ * Collapsed windows line under the weekly bar, with a per-card chevron that
22
+ * expands thin tracks. A real button inside the card's role="button": click
23
+ * stops propagation so it never opens the drawer, and the card's own keydown
24
+ * ignores keys aimed at this button.
25
+ */
26
+ export declare function WindowsRow({ provider, asOf, variant }: {
27
+ provider: ProviderView;
28
+ asOf: string;
29
+ variant: "card" | "row";
30
+ }): React.JSX.Element | null;
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { resetClock, resetWeekday } from "./PaceBar.js";
4
+ const MIN_MS = 60_000;
5
+ const HOUR_MS = 60 * MIN_MS;
6
+ const DAY_MS = 24 * HOUR_MS;
7
+ /** One-unit countdown for a window tag: "7d", "4h" or "40m". Null when passed or unparseable. */
8
+ export function windowCountdown(resetsAt, asOfMs) {
9
+ const diff = Date.parse(resetsAt) - asOfMs;
10
+ if (!Number.isFinite(diff) || diff <= 0)
11
+ return null;
12
+ if (diff >= DAY_MS)
13
+ return `${Math.floor(diff / DAY_MS)}d`;
14
+ if (diff >= HOUR_MS)
15
+ return `${Math.floor(diff / HOUR_MS)}h`;
16
+ return `${Math.max(1, Math.floor(diff / MIN_MS))}m`;
17
+ }
18
+ function heatOf(pct, exhausted) {
19
+ if (exhausted || pct >= 100)
20
+ return "full";
21
+ return pct >= 80 ? "hot" : "";
22
+ }
23
+ /** Present windows only; a unit with no data is not rendered at all. */
24
+ export function windowUnits(provider, asOfMs) {
25
+ const q = provider.quota;
26
+ if (!q)
27
+ return [];
28
+ const units = [];
29
+ if (typeof q.fiveHourPct === "number" && Number.isFinite(q.fiveHourPct)) {
30
+ // No adapter reports a 5h reset, so the 5h tag carries no time.
31
+ units.push({ win: "five", tag: "5h", cardLabel: "5h limit", rowLabel: "5h",
32
+ pct: q.fiveHourPct, heat: heatOf(q.fiveHourPct, false), time: null, reset: null });
33
+ }
34
+ if (q.monthlyKind === "included" && typeof q.monthlyPct === "number" && q.monthlyResetsAt) {
35
+ const exhausted = q.monthlyStatus === "exhausted";
36
+ const left = windowCountdown(q.monthlyResetsAt, asOfMs);
37
+ const clock = resetClock(q.monthlyResetsAt);
38
+ units.push({ win: "month", tag: "Mth", cardLabel: "Monthly", rowLabel: "Month",
39
+ pct: q.monthlyPct, heat: heatOf(q.monthlyPct, exhausted),
40
+ time: exhausted ? resetWeekday(q.monthlyResetsAt) : left ? `in ${left}` : null,
41
+ reset: exhausted ? (clock ? `resets ${clock}` : null) : left ? `in ${left}` : null });
42
+ }
43
+ return units;
44
+ }
45
+ function trackFill(u) {
46
+ const cap = u.pct >= 80 || u.heat === "full";
47
+ return {
48
+ width: `${Math.min(100, Math.max(0, u.pct))}%`,
49
+ background: cap ? "var(--fill-cap)" : "var(--fill-ontrack)",
50
+ ["--fill-edge"]: cap ? "var(--edge-cap)" : "var(--edge-ontrack)",
51
+ };
52
+ }
53
+ /**
54
+ * Collapsed windows line under the weekly bar, with a per-card chevron that
55
+ * expands thin tracks. A real button inside the card's role="button": click
56
+ * stops propagation so it never opens the drawer, and the card's own keydown
57
+ * ignores keys aimed at this button.
58
+ */
59
+ export function WindowsRow({ provider, asOf, variant }) {
60
+ const [open, setOpen] = React.useState(false);
61
+ const units = windowUnits(provider, Date.parse(asOf));
62
+ if (units.length === 0)
63
+ return null;
64
+ return (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "wline", "data-testid": `wline-${provider.id}`, "aria-expanded": open, "aria-label": `${provider.displayName} other windows: ${units.map((u) => `${u.tag} ${u.pct}%${u.time ? ` ${u.time}` : ""}`).join(", ")}`, onClick: (e) => {
65
+ e.stopPropagation();
66
+ setOpen((v) => !v);
67
+ }, children: [units.map((u) => (_jsxs("span", { className: "unit", "data-win": u.win, children: [_jsxs("span", { className: u.heat ? `seg ${u.heat}` : "seg", children: [_jsx("span", { className: "k", children: u.tag }), _jsxs("span", { className: "n", children: [u.pct, "%"] })] }), u.time && _jsx("span", { className: "t", children: u.time })] }, u.win))), _jsx("span", { className: "chev", "aria-hidden": "true", children: _jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", children: _jsx("path", { d: "M4 6l4 4 4-4" }) }) })] }), open && (_jsx("div", { className: "windows", "data-testid": `windows-${provider.id}`, children: units.map((u) => (_jsxs("div", { className: "win", "data-win": u.win, children: [_jsx("span", { className: "lab", children: variant === "card" ? u.cardLabel : u.rowLabel }), _jsx("div", { className: "track", children: _jsx("div", { className: "fill", style: trackFill(u) }) }), _jsxs("span", { className: "pct", children: [u.pct, "%"] }), variant === "card" && u.reset && _jsx("span", { className: "reset", children: u.reset })] }, u.win))) }))] }));
68
+ }
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import { ageDuration } from "../state.js";
4
4
  import { failureWords } from "../components/ProviderDrawer.js";
5
+ import { authInstruction } from "../components/FaultBanner.js";
5
6
  /**
6
7
  * Provider readiness from server state only. The browser cannot probe local
7
8
  * paths, so this reports what the daemon reported — never a local scan.
@@ -27,7 +28,7 @@ export function readiness(provider) {
27
28
  if (provider.lastAttempt?.failureCategory === "auth") {
28
29
  return {
29
30
  status: "Sign in first",
30
- detail: `Sign in to the ${cli} CLI, then re-poll. ${credentialNote}`,
31
+ detail: authInstruction(cli, provider.lastAttempt),
31
32
  };
32
33
  }
33
34
  if (!provider.lastAttempt) {
@@ -23,6 +23,9 @@ export interface AdvisoryView {
23
23
  status: BurnStatus;
24
24
  wastePct: number | null;
25
25
  urgency: Urgency;
26
+ bindingWindow: "weekly" | "monthly";
27
+ bindingRemaining: number;
28
+ bindingDaysLeft: number;
26
29
  }
27
30
  export interface RecommendationView {
28
31
  use: string;
@@ -30,6 +33,7 @@ export interface RecommendationView {
30
33
  wastePct: number | null;
31
34
  idealRate: number;
32
35
  recommendationBasis: RecommendationBasis;
36
+ bindingWindow: "weekly" | "monthly";
33
37
  alternatives: unknown[];
34
38
  advisories: AdvisoryView[];
35
39
  }
@@ -48,7 +52,15 @@ export interface WindowCloseView {
48
52
  export interface QuotaView {
49
53
  provider: string;
50
54
  plan: string;
55
+ weeklyPct: number;
56
+ fiveHourPct?: number;
57
+ monthlyPct?: number;
58
+ monthlyResetsAt?: string;
59
+ monthlyStatus?: "ok" | "exhausted";
60
+ monthlyKind?: "included";
61
+ /** @deprecated wire alias for weeklyPct. */
51
62
  usedPct: number;
63
+ /** @deprecated wire alias for fiveHourPct. */
52
64
  sessionPct?: number;
53
65
  resetsAt: string;
54
66
  periodStart: string;