quotacap 0.0.50 → 0.0.52

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 (69) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/agy.js +3 -0
  3. package/dist/adapters/claude.js +3 -0
  4. package/dist/adapters/codex.js +4 -2
  5. package/dist/adapters/kimi.js +14 -0
  6. package/dist/adapters/manual.js +4 -0
  7. package/dist/adapters/muse.js +19 -5
  8. package/dist/adapters/opencode-go.d.ts +12 -2
  9. package/dist/adapters/opencode-go.js +156 -31
  10. package/dist/adapters/types.d.ts +2 -0
  11. package/dist/advisory/snapshot.d.ts +1 -0
  12. package/dist/advisory/snapshot.js +7 -1
  13. package/dist/advisory/types.d.ts +3 -0
  14. package/dist/cli/models.js +5 -4
  15. package/dist/format/rows.d.ts +30 -1
  16. package/dist/format/rows.js +109 -24
  17. package/dist/format/terminal.js +5 -3
  18. package/dist/http/server.d.ts +3 -0
  19. package/dist/http/server.js +7 -3
  20. package/dist/runtime/service.js +14 -3
  21. package/dist/service/macos.d.ts +5 -0
  22. package/dist/service/macos.js +6 -2
  23. package/dist/store/db.js +2 -1
  24. package/dist/store/quotas.d.ts +1 -1
  25. package/dist/store/quotas.js +65 -2
  26. package/dist/version.js +1 -1
  27. package/dist/web/src/App.js +10 -4
  28. package/dist/web/src/components/AdviceDrawer.d.ts +2 -1
  29. package/dist/web/src/components/AdviceDrawer.js +27 -16
  30. package/dist/web/src/components/BurnDown.d.ts +37 -0
  31. package/dist/web/src/components/BurnDown.js +182 -0
  32. package/dist/web/src/components/ClosedWeeks.d.ts +6 -5
  33. package/dist/web/src/components/ClosedWeeks.js +25 -37
  34. package/dist/web/src/components/CopyButton.d.ts +4 -0
  35. package/dist/web/src/components/CopyButton.js +27 -0
  36. package/dist/web/src/components/ErrorBoundary.d.ts +37 -0
  37. package/dist/web/src/components/ErrorBoundary.js +32 -0
  38. package/dist/web/src/components/FaultBanner.d.ts +3 -2
  39. package/dist/web/src/components/FaultBanner.js +7 -4
  40. package/dist/web/src/components/OpencodeGoBanner.d.ts +3 -1
  41. package/dist/web/src/components/OpencodeGoBanner.js +16 -10
  42. package/dist/web/src/components/PaceBar.d.ts +80 -3
  43. package/dist/web/src/components/PaceBar.js +225 -34
  44. package/dist/web/src/components/PacingLegend.js +1 -1
  45. package/dist/web/src/components/ProviderCard.js +6 -9
  46. package/dist/web/src/components/ProviderDrawer.d.ts +56 -10
  47. package/dist/web/src/components/ProviderDrawer.js +263 -138
  48. package/dist/web/src/components/ProviderRow.js +3 -3
  49. package/dist/web/src/components/Recommendation.js +11 -7
  50. package/dist/web/src/components/ResetRail.js +13 -9
  51. package/dist/web/src/components/SubscriptionList.js +11 -4
  52. package/dist/web/src/components/WindowsRow.d.ts +13 -3
  53. package/dist/web/src/components/WindowsRow.js +27 -5
  54. package/dist/web/src/components/settings/CliIntegrationPanel.js +7 -45
  55. package/dist/web/src/components/settings/DaemonPanel.js +1 -28
  56. package/dist/web/src/components/settings/ProvidersPanel.d.ts +2 -2
  57. package/dist/web/src/components/settings/ProvidersPanel.js +10 -24
  58. package/dist/web/src/components/settings/SettingsDrawer.d.ts +1 -2
  59. package/dist/web/src/components/settings/SettingsDrawer.js +2 -2
  60. package/dist/web/src/state.d.ts +6 -1
  61. package/dist/web/src/state.js +19 -10
  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-Co33DTl5.css +1 -0
  66. package/web/dist/assets/index-DqK6QVaw.js +13 -0
  67. package/web/dist/index.html +2 -2
  68. package/web/dist/assets/index-BSQW0C_t.css +0 -1
  69. package/web/dist/assets/index-Dq9ZvTUe.js +0 -13
@@ -1,9 +1,10 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef } from "react";
3
- import { paceBadge, timeLeft } from "./PaceBar.js";
3
+ import { quotaResetsAt, quotaUsedPct, timeLeft } from "./PaceBar.js";
4
4
  import { ProviderIcon, providerTint } from "./ProviderIcon.js";
5
5
  import { lanesFor } from "./Recommendation.js";
6
- export function AdviceDrawer({ open, recommendation, providers, asOf, onClose, }) {
6
+ import { CopyButton } from "./CopyButton.js";
7
+ export function AdviceDrawer({ open, recommendation, providers, asOf, onClose, onSelectProvider, }) {
7
8
  const dialogRef = useRef(null);
8
9
  const previouslyFocused = useRef(null);
9
10
  useEffect(() => {
@@ -47,8 +48,10 @@ export function AdviceDrawer({ open, recommendation, providers, asOf, onClose, }
47
48
  const pickId = recommendation.use !== "none" ? recommendation.use : useMore[0]?.provider;
48
49
  const pick = pickId ? providers.find((p) => p.id === pickId) : undefined;
49
50
  const pickName = pick ? pick.displayName : null;
50
- const remaining = pick?.advisory?.remaining !== null && pick?.advisory?.remaining !== undefined
51
- ? Math.round(pick.advisory.remaining)
51
+ const remaining = pick?.advisory
52
+ ? Math.round(pick.advisory.bindingWindow === "monthly"
53
+ ? pick.advisory.bindingRemaining
54
+ : pick.advisory.remaining)
52
55
  : null;
53
56
  // A monthly-bound pick's leftover is unusable until the month resets, so
54
57
  // the weekly expiry sentence would restate issue 109; show the reason.
@@ -60,23 +63,31 @@ export function AdviceDrawer({ open, recommendation, providers, asOf, onClose, }
60
63
  : pick?.advisory?.wastePct !== null && pick?.advisory?.wastePct !== undefined
61
64
  ? Math.round(pick.advisory.wastePct)
62
65
  : null;
63
- const left = pick?.quota ? timeLeft(pick.quota.resetsAt, asOfMs) : null;
66
+ const pickReset = pick ? quotaResetsAt(pick) : null;
67
+ const left = pickReset ? timeLeft(pickReset, asOfMs) : null;
64
68
  const basis = pick?.advisory?.paceSource === "window-average" ? "window-average pace" : "current 24h pace";
65
69
  const othersMore = useMore.filter((a) => a.provider !== pickId);
66
70
  const titleId = "advice-drawer-title";
67
- return (_jsxs(_Fragment, { children: [_jsx("div", { "data-testid": "advice-scrim", className: "scrim is-open", "aria-hidden": "true", onClick: onClose }), _jsxs("aside", { ref: dialogRef, tabIndex: -1, "data-testid": "advice-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 rec-picon", "aria-hidden": "true", children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", 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" }) }) }), _jsxs("div", { style: { flex: 1 }, children: [_jsx("h2", { id: titleId, children: "Quota Advice & Pacing" }), _jsx("span", { className: "sub", children: "Actionable recommendations based on quota pacing & reset schedules" })] }), _jsx("button", { className: "drawer-close", type: "button", "aria-label": "Close advice", onClick: onClose, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) }) })] }), _jsxs("div", { className: "dsec", children: [_jsx("h3", { children: "Recommended for next session" }), pick && pickName ? (_jsxs("div", { className: "advice-card", style: { marginTop: 8 }, children: [_jsxs("div", { className: "advice-card-main", children: [_jsxs("div", { className: "advice-card-lead", children: [_jsx("span", { className: "sq", style: providerTint(pick.id), children: _jsx(ProviderIcon, { id: pick.id, title: pickName, size: 16 }) }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("span", { className: "name", children: pickName }), _jsxs("span", { className: "recommended-badge", children: [_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", 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" }) }), "Recommended"] })] })] }), waste !== null && left && (_jsxs("div", { className: "advice-card-stats", children: [_jsxs("b", { children: [waste, "%"] }), " unused quota expires in ", _jsx("b", { children: left })] }))] }), _jsx("p", { className: "advice-card-body", children: remaining !== null && left && waste !== null ? (_jsxs(_Fragment, { children: [_jsx("b", { children: pickName }), " has ", remaining, "% quota remaining with its reset window closing in ", left, ". At the ", basis, ", ", waste, "% unused quota is projected at reset. Using this subscription now avoids unused quota expiration."] })) : (recommendation.reason) })] })) : (_jsx("p", { className: "advice-card-body", children: recommendation.reason }))] }), _jsxs("div", { className: "dsec", children: [_jsx("h3", { children: "Fleet Pacing Breakdown" }), _jsxs("dl", { className: "dkv", children: [_jsx("dt", { children: "Use more" }), _jsx("dd", { children: othersMore.length
68
- ? othersMore.map((a, i) => {
71
+ return (_jsxs(_Fragment, { children: [_jsx("div", { "data-testid": "advice-scrim", className: "scrim is-open", "aria-hidden": "true", onClick: onClose }), _jsxs("aside", { ref: dialogRef, tabIndex: -1, "data-testid": "advice-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 rec-picon", "aria-hidden": "true", children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", 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" }) }) }), _jsxs("div", { style: { flex: 1 }, children: [_jsx("h2", { id: titleId, children: "Quota Advice & Pacing" }), _jsx("span", { className: "sub", children: "Which quota to burn next, and why" })] }), _jsx("button", { className: "drawer-close", type: "button", "aria-label": "Close advice", onClick: onClose, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) }) })] }), _jsxs("div", { className: "dsec", children: [_jsx("h3", { children: "Recommended for next session" }), pick && pickName ? (_jsxs("div", { className: "advice-card", style: { marginTop: 8 }, children: [_jsxs("div", { className: "advice-card-main", children: [_jsxs("div", { className: "advice-card-lead", children: [_jsx("span", { className: "sq", style: providerTint(pick.id), children: _jsx(ProviderIcon, { id: pick.id, title: pickName, size: 16 }) }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("span", { className: "name", children: pickName }), _jsxs("span", { className: "recommended-badge", children: [_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", 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" }) }), "Recommended"] })] })] }), waste !== null && left && (_jsxs("div", { className: "advice-card-stats", children: [_jsxs("b", { children: [waste, "%"] }), " unused quota expires in ", _jsx("b", { children: left })] }))] }), _jsx("p", { className: "advice-card-body", children: remaining !== null && left && !monthlyBound ? (_jsxs(_Fragment, { children: [_jsx("b", { children: pickName }), " has ", remaining, "% quota remaining with ", left, " to reset, forecast at the ", basis, "."] })) : (recommendation.reason) }), onSelectProvider && (_jsx("div", { children: _jsxs("button", { type: "button", className: "goto-link", onClick: () => onSelectProvider(pick.id), children: ["View ", pickName, " details \u2192"] }) }))] })) : (_jsx("p", { className: "advice-card-body", children: recommendation.reason }))] }), _jsxs("div", { className: "dsec", children: [_jsx("h3", { children: "Fleet Pacing Breakdown" }), _jsxs("dl", { className: "lane-group", children: [_jsxs("dt", { className: "um", children: [_jsx("span", { "aria-hidden": "true", children: "\u2191" }), " Use more"] }), _jsx("dd", { children: othersMore.length
72
+ ? othersMore.map((a) => {
69
73
  const p = providers.find((x) => x.id === a.provider);
70
- const w = a.wastePct !== null ? `${Math.round(a.wastePct)}% unused` : null;
71
- const t = p?.quota ? timeLeft(p.quota.resetsAt, asOfMs) : null;
72
- const bits = [w, t ? `resets in ${t}` : null].filter(Boolean).join(" · ");
73
- return (_jsxs("span", { children: [i > 0 ? ", " : "", _jsx("b", { children: p?.displayName ?? a.provider }), bits ? ` (${bits})` : ""] }, a.provider));
74
+ const name = p?.displayName ?? a.provider;
75
+ const unused = a.wastePct !== null ? `${Math.round(a.wastePct)}% unused` : null;
76
+ const resetIso = p ? quotaResetsAt(p) : null;
77
+ const t = resetIso ? timeLeft(resetIso, asOfMs) : null;
78
+ const meta = [unused, t ? `resets in ${t}` : null].filter(Boolean).join(" · ");
79
+ return (_jsxs("div", { className: "lane-row", children: [_jsx("span", { className: "nm", children: name }), _jsx("span", { className: "meta", children: meta })] }, a.provider));
74
80
  })
75
- : "None" }), _jsx("dt", { children: "Ease off" }), _jsx("dd", { children: easeOff.length
76
- ? easeOff.map((a, i) => {
81
+ : "None" }), _jsxs("dt", { className: "eo", children: [_jsx("span", { "aria-hidden": "true", children: "\u2193" }), " Ease off"] }), _jsx("dd", { children: easeOff.length
82
+ ? easeOff.map((a) => {
77
83
  const p = providers.find((x) => x.id === a.provider);
78
- const badge = p ? paceBadge(p) : a.urgency;
79
- return (_jsxs("span", { children: [i > 0 ? ", " : "", _jsx("b", { children: p?.displayName ?? a.provider }), " (", badge, ")"] }, a.provider));
84
+ const name = p?.displayName ?? a.provider;
85
+ const usedRaw = p ? quotaUsedPct(p) : null;
86
+ const used = usedRaw !== null ? `${Math.round(usedRaw)}% used` : null;
87
+ const resetIso2 = p ? quotaResetsAt(p) : null;
88
+ const t = resetIso2 ? timeLeft(resetIso2, asOfMs) : null;
89
+ const meta = [used, t ? `resets in ${t}` : null].filter(Boolean).join(" · ");
90
+ return (_jsxs("div", { className: "lane-row", children: [_jsx("span", { className: "nm", children: name }), _jsx("span", { className: "meta", children: meta })] }, a.provider));
80
91
  })
81
- : "None" })] })] }), _jsxs("div", { className: "dsec", children: [_jsx("h3", { children: "CLI Command" }), _jsx("p", { style: { fontSize: "var(--t-2)", color: "var(--ink-soft)", marginBottom: 8 }, children: "Equivalent terminal command:" }), _jsx("pre", { className: "advice-cli", children: _jsx("code", { children: "$ quotacap advise" }) })] })] })] }));
92
+ : "None" })] })] }), _jsxs("div", { className: "dsec", children: [_jsx("h3", { children: "CLI Command" }), _jsxs("div", { className: "cli-wrap", children: [_jsx("pre", { className: "advice-cli", children: _jsx("code", { children: "$ quotacap advise" }) }), _jsx(CopyButton, { text: "quotacap advise" })] })] })] })] }));
82
93
  }
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+ import type { ProviderView } from "../state.js";
3
+ export type BurnTone = "cap" | "behind" | "ontrack";
4
+ /** Chart tone from the shared badge: cap risk paints cap, behind-pace paints
5
+ * behind, and on-track/ahead share the ontrack tone on the ahead token. */
6
+ export declare function burnTone(provider: ProviderView): BurnTone;
7
+ export interface BurnGeometry {
8
+ usedPct: number;
9
+ elapsedPct: number;
10
+ projXPct: number;
11
+ projYPct: number;
12
+ exhaustsBeforeReset: boolean;
13
+ tone: BurnTone;
14
+ }
15
+ /**
16
+ * Burn-down geometry in quota-window data space (x = elapsed %, y = used %).
17
+ * Actual runs (0,0) to (elapsed, used); projection ends at 100% when
18
+ * exhaustion precedes the quota reset, else at (100, used + rate × days
19
+ * left). Weekly projects at the server burn rate; monthly projects at the
20
+ * monthly window average (no monthly burn history is tracked). Null when
21
+ * the quota pace is unknown.
22
+ */
23
+ export declare function burnGeometry(provider: ProviderView, asOfMs: number): BurnGeometry | null;
24
+ export declare function burnAriaLabel(provider: ProviderView, geo: BurnGeometry): string;
25
+ /**
26
+ * Pace section: burn-down SVG, the shared four-cell pace grid, and the
27
+ * basis line. Returns null (omitting the whole section) when pace is
28
+ * unknown, burnRate or elapsed is missing, or the provider is excluded.
29
+ * The drawer may pass the basis line derived from paceBasis; standalone
30
+ * use falls back to the same basis wording from period timestamps and
31
+ * PACE_TIPS.
32
+ */
33
+ export declare function BurnDown({ provider, asOf, basis, }: {
34
+ provider: ProviderView;
35
+ asOf: string;
36
+ basis?: string;
37
+ }): React.JSX.Element | null;
@@ -0,0 +1,182 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PACE_TIPS, PaceStats, barGeometry, isMonthlyBound, paceBadge, paceFigures, quotaAvgPace, quotaResetsAt, quotaWindowName, quotaWindowStartIso, } from "./PaceBar.js";
3
+ /** Chart tone from the shared badge: cap risk paints cap, behind-pace paints
4
+ * behind, and on-track/ahead share the ontrack tone on the ahead token. */
5
+ export function burnTone(provider) {
6
+ const badge = paceBadge(provider);
7
+ switch (badge) {
8
+ case "Cap risk":
9
+ case "Monthly exhausted":
10
+ case "Watch":
11
+ return "cap";
12
+ case "Behind pace":
13
+ return "behind";
14
+ default:
15
+ return "ontrack";
16
+ }
17
+ }
18
+ /**
19
+ * Burn-down geometry in quota-window data space (x = elapsed %, y = used %).
20
+ * Actual runs (0,0) to (elapsed, used); projection ends at 100% when
21
+ * exhaustion precedes the quota reset, else at (100, used + rate × days
22
+ * left). Weekly projects at the server burn rate; monthly projects at the
23
+ * monthly window average (no monthly burn history is tracked). Null when
24
+ * the quota pace is unknown.
25
+ */
26
+ export function burnGeometry(provider, asOfMs) {
27
+ if (!provider.quota || !provider.advisory)
28
+ return null;
29
+ if (provider.exclusionReason !== null)
30
+ return null;
31
+ const advisory = provider.advisory;
32
+ const monthly = isMonthlyBound(provider);
33
+ let burnRate;
34
+ let daysLeft;
35
+ let daysToExhaust;
36
+ if (monthly) {
37
+ burnRate = quotaAvgPace(provider, asOfMs);
38
+ daysLeft = advisory.bindingDaysLeft;
39
+ daysToExhaust =
40
+ burnRate !== null && burnRate > 0 ? advisory.bindingRemaining / burnRate : Infinity;
41
+ }
42
+ else {
43
+ if (advisory.paceSource === "unknown")
44
+ return null;
45
+ burnRate = advisory.burnRate;
46
+ daysLeft = advisory.daysLeft;
47
+ daysToExhaust = advisory.daysToExhaust;
48
+ }
49
+ if (burnRate === null || !Number.isFinite(burnRate))
50
+ return null;
51
+ const { usedPct, elapsedPct } = barGeometry(provider, asOfMs);
52
+ if (elapsedPct === null || !Number.isFinite(elapsedPct))
53
+ return null;
54
+ let projXPct;
55
+ let projYPct;
56
+ let exhaustsBeforeReset = false;
57
+ if (daysToExhaust !== null &&
58
+ Number.isFinite(daysToExhaust) &&
59
+ daysToExhaust < daysLeft) {
60
+ exhaustsBeforeReset = true;
61
+ projYPct = 100;
62
+ if (usedPct >= 100) {
63
+ projXPct = elapsedPct;
64
+ }
65
+ else {
66
+ const startIso = quotaWindowStartIso(provider);
67
+ const resetIso = quotaResetsAt(provider);
68
+ const startMs = startIso ? Date.parse(startIso) : NaN;
69
+ const resetMs = resetIso ? Date.parse(resetIso) : NaN;
70
+ if (Number.isFinite(startMs) &&
71
+ Number.isFinite(resetMs) &&
72
+ resetMs > startMs &&
73
+ Number.isFinite(asOfMs)) {
74
+ const totalDays = (resetMs - startMs) / 86_400_000;
75
+ const elapsedDays = (asOfMs - startMs) / 86_400_000;
76
+ projXPct = ((elapsedDays + daysToExhaust) / totalDays) * 100;
77
+ }
78
+ else if (elapsedPct < 100 && daysLeft > 0) {
79
+ projXPct = elapsedPct + (daysToExhaust * (100 - elapsedPct)) / daysLeft;
80
+ }
81
+ else {
82
+ projXPct = 100;
83
+ }
84
+ projXPct = Math.min(100, Math.max(elapsedPct, projXPct));
85
+ }
86
+ }
87
+ else {
88
+ projXPct = 100;
89
+ projYPct = usedPct + burnRate * daysLeft;
90
+ if (!Number.isFinite(projYPct))
91
+ projYPct = usedPct;
92
+ projYPct = Math.min(100, Math.max(usedPct, projYPct));
93
+ }
94
+ return {
95
+ usedPct,
96
+ elapsedPct,
97
+ projXPct,
98
+ projYPct,
99
+ exhaustsBeforeReset,
100
+ tone: burnTone(provider),
101
+ };
102
+ }
103
+ export function burnAriaLabel(provider, geo) {
104
+ const used = Math.round(geo.usedPct);
105
+ const elapsed = Math.round(geo.elapsedPct);
106
+ const window = `${quotaWindowName(provider).toLowerCase()} quota window`;
107
+ if (geo.exhaustsBeforeReset) {
108
+ return `Burn-down: ${used} percent used at ${elapsed} percent elapsed, forecast exhausts the quota before reset (${window})`;
109
+ }
110
+ const lands = Math.round(geo.projYPct);
111
+ const waste = Math.round(100 - geo.projYPct);
112
+ if (waste > 0) {
113
+ return `Burn-down: ${used} percent used at ${elapsed} percent elapsed, forecast lands at ${lands} percent with ${waste} percent waste (${window})`;
114
+ }
115
+ return `Burn-down: ${used} percent used at ${elapsed} percent elapsed, forecast lands at ${lands} percent (${window})`;
116
+ }
117
+ function fallbackBasis(provider, asOfMs) {
118
+ const advisory = provider.advisory;
119
+ if (!advisory)
120
+ return "No current reading";
121
+ if (!isMonthlyBound(provider) && advisory.paceSource === "unknown")
122
+ return "No current reading";
123
+ const { avg, recent } = paceFigures(advisory);
124
+ if (!isMonthlyBound(provider) && avg === null && recent !== null)
125
+ return PACE_TIPS.recent;
126
+ const startIso = quotaWindowStartIso(provider);
127
+ const start = startIso ? Date.parse(startIso) : NaN;
128
+ if (Number.isFinite(start) && Number.isFinite(asOfMs) && asOfMs > start) {
129
+ const days = (asOfMs - start) / 86_400_000;
130
+ if (days >= 0.1)
131
+ return `averaged over ${days.toFixed(1)} days of this window`;
132
+ }
133
+ if (provider.evidence.includes("measured") || advisory.burnMeasured)
134
+ return "Measured";
135
+ if (advisory.paceSource === "window-average" || provider.evidence.includes("window-average")) {
136
+ return "Window avg";
137
+ }
138
+ return PACE_TIPS.avg;
139
+ }
140
+ const PLOT = { left: 48, right: 388, top: 28, bottom: 142 };
141
+ const PLOT_MID_X = (PLOT.left + PLOT.right) / 2;
142
+ const PLOT_MID_Y = (PLOT.top + PLOT.bottom) / 2;
143
+ function sx(pct) {
144
+ return PLOT.left + (pct / 100) * (PLOT.right - PLOT.left);
145
+ }
146
+ function sy(pct) {
147
+ return PLOT.bottom - (pct / 100) * (PLOT.bottom - PLOT.top);
148
+ }
149
+ function n(v) {
150
+ return String(Math.round(v * 10) / 10);
151
+ }
152
+ /**
153
+ * Pace section: burn-down SVG, the shared four-cell pace grid, and the
154
+ * basis line. Returns null (omitting the whole section) when pace is
155
+ * unknown, burnRate or elapsed is missing, or the provider is excluded.
156
+ * The drawer may pass the basis line derived from paceBasis; standalone
157
+ * use falls back to the same basis wording from period timestamps and
158
+ * PACE_TIPS.
159
+ */
160
+ export function BurnDown({ provider, asOf, basis, }) {
161
+ const asOfMs = Date.parse(asOf);
162
+ const geo = burnGeometry(provider, asOfMs);
163
+ if (!geo)
164
+ return null;
165
+ const basisLine = basis ?? fallbackBasis(provider, asOfMs);
166
+ const tone = geo.tone;
167
+ const nowX = sx(geo.elapsedPct);
168
+ const nowY = sy(geo.usedPct);
169
+ const projX = sx(geo.projXPct);
170
+ const projY = sy(geo.projYPct);
171
+ const showProjDot = Math.hypot(projX - nowX, projY - nowY) >= 3;
172
+ const label = burnAriaLabel(provider, geo);
173
+ const windowName = quotaWindowName(provider);
174
+ const elapsedLabel = `${Math.round(geo.elapsedPct)}% elapsed`;
175
+ // Center the now readout in the top callout band above PLOT.top, clamped to plot edges.
176
+ const nowTextX = Math.min(PLOT.right - 46, Math.max(PLOT.left + 46, nowX));
177
+ // Place "ideal pace" on whichever half of the diagonal is clear of the vertical now-line.
178
+ const idealPct = geo.elapsedPct > 52 ? 35 : 66;
179
+ const idealX = sx(idealPct);
180
+ const idealY = sy(idealPct) - 5;
181
+ return (_jsxs("section", { className: "dsec", "aria-label": "Pace", "data-testid": "burn-down-section", children: [_jsxs("div", { className: "burn-head", children: [_jsx("h3", { children: "Pace" }), _jsxs("span", { className: "burn-win", children: [windowName, " quota window"] })] }), _jsxs("svg", { className: "burn", viewBox: "0 0 400 184", role: "img", "aria-label": label, "data-testid": "burn-down", children: [_jsx("text", { x: 11, y: PLOT_MID_Y, className: "axis-y", transform: `rotate(-90 11 ${PLOT_MID_Y})`, children: "Quota used (%)" }), _jsx("line", { x1: PLOT.left, y1: PLOT.bottom, x2: PLOT.right, y2: PLOT.bottom, className: "grid" }), _jsx("line", { x1: PLOT.left, y1: PLOT_MID_Y, x2: PLOT.right, y2: PLOT_MID_Y, className: "grid" }), _jsx("line", { x1: PLOT.left, y1: PLOT.top, x2: PLOT.right, y2: PLOT.top, className: "grid" }), _jsx("line", { x1: PLOT.left, y1: PLOT.bottom, x2: PLOT.left, y2: PLOT.bottom + 4, className: "grid-tick" }), _jsx("line", { x1: PLOT_MID_X, y1: PLOT.bottom, x2: PLOT_MID_X, y2: PLOT.bottom + 4, className: "grid-tick" }), _jsx("line", { x1: PLOT.right, y1: PLOT.bottom, x2: PLOT.right, y2: PLOT.bottom + 4, className: "grid-tick" }), _jsx("text", { x: 40, y: PLOT.bottom + 3.5, className: "ylab", children: "0%" }), _jsx("text", { x: 40, y: PLOT_MID_Y + 3.5, className: "ylab", children: "50%" }), _jsx("text", { x: 40, y: PLOT.top + 3.5, className: "ylab", children: "100%" }), _jsx("line", { x1: PLOT.left, y1: PLOT.bottom, x2: PLOT.right, y2: PLOT.top, className: "ideal" }), _jsx("text", { x: n(idealX), y: n(idealY), className: "ideal-lab", textAnchor: "middle", transform: `rotate(-18.5 ${n(idealX)} ${n(idealY)})`, children: "ideal pace" }), _jsx("line", { x1: n(nowX), y1: PLOT.top - 8, x2: n(nowX), y2: PLOT.bottom, className: "now" }), _jsxs("text", { x: n(nowTextX), y: 12, className: "now-lab", textAnchor: "middle", children: ["now \u00B7 ", elapsedLabel] }), _jsx("line", { x1: PLOT.left, y1: PLOT.bottom, x2: n(nowX), y2: n(nowY), className: `actual ${tone}`, "data-testid": "burn-actual" }), _jsx("line", { x1: n(nowX), y1: n(nowY), x2: n(projX), y2: n(projY), className: `proj ${tone}`, "data-testid": "burn-proj", "data-proj-x": n(geo.projXPct), "data-proj-y": n(geo.projYPct) }), _jsx("circle", { cx: n(nowX), cy: n(nowY), r: 4.5, className: `dot ${tone}` }), showProjDot && (_jsx("circle", { cx: n(projX), cy: n(projY), r: 3.5, className: `dot ${tone} hollow` })), _jsx("text", { x: PLOT.left, y: 160, className: "xlab", children: "start \u00B7 0%" }), _jsx("text", { x: PLOT_MID_X, y: 160, className: "xlab mid", children: "50%" }), _jsx("text", { x: PLOT.right, y: 160, className: "xlab end", children: "quota reset \u00B7 100%" }), _jsx("text", { x: PLOT_MID_X, y: 178, className: "axis-x", children: "Time elapsed in window (%)" })] }), _jsxs("p", { className: "burn-legend", children: [_jsx("span", { className: "legend-label", children: "Legend" }), _jsxs("span", { className: "legend-item", children: [_jsx("span", { className: `burn-swatch-dot ${tone}`, "aria-hidden": "true" }), "Dot is now"] }), _jsxs("span", { className: "legend-item", children: [_jsx("span", { className: `burn-swatch-proj ${tone}`, "aria-hidden": "true" }), "Dashed is forecast"] })] }), _jsx(PaceStats, { provider: provider, asOf: asOf }), _jsx("p", { className: "pacecap", children: _jsx("span", { className: "basis", children: basisLine }) })] }));
182
+ }
@@ -7,10 +7,11 @@ export declare function closeDateLabel(resetsAt: string | null): string | null;
7
7
  export declare function closeIsStale(c: WindowCloseView): boolean;
8
8
  export declare function closeSampleStamp(sampledAt: string): string | null;
9
9
  export declare function closeResetStamp(resetsAt: string): string | null;
10
- /** Four-cell strip of closed weekly windows, oldest left. Fill is used at
11
- * close, the hatch is the stored leftover. One cell per real close, never a
12
- * placeholder. Stale samples name their reading on the cell and under the
13
- * strip. */
14
- export declare function ClosedWeeks({ closes }: {
10
+ /** Bar chart of closed quota windows, oldest left. The solid segment is
11
+ * used at close, the hatched cap is the stored leftover; a cap under 3% is
12
+ * label-only. One bar per real close, never a placeholder. Stale samples
13
+ * name their earliness on the label; exact stamps stay in the tooltip. */
14
+ export declare function ClosedWeeks({ closes, windowName, }: {
15
15
  closes: WindowCloseView[];
16
+ windowName?: "Weekly" | "Monthly";
16
17
  }): React.JSX.Element | null;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { format } from "date-fns";
3
3
  const STALE_EARLY_MS = 15 * 60 * 1000;
4
4
  export function lastClose(provider) {
@@ -34,7 +34,7 @@ export function closeResetStamp(resetsAt) {
34
34
  return null;
35
35
  return format(new Date(ms), "d MMM HH:mm");
36
36
  }
37
- /** One floored duration for both the compact cell line and the strip note. */
37
+ /** One floored duration for both the compact label and the word form. */
38
38
  function earlyParts(ms) {
39
39
  if (ms < 60_000)
40
40
  return { n: Math.max(1, Math.floor(ms / 1000)), unit: "s" };
@@ -60,43 +60,31 @@ function earlyWords(ms) {
60
60
  const { n, unit } = earlyParts(ms);
61
61
  return `${n} ${EARLY_UNIT_WORDS[unit]}${n === 1 ? "" : "s"}`;
62
62
  }
63
- /** Four-cell strip of closed weekly windows, oldest left. Fill is used at
64
- * close, the hatch is the stored leftover. One cell per real close, never a
65
- * placeholder. Stale samples name their reading on the cell and under the
66
- * strip. */
67
- export function ClosedWeeks({ closes }) {
63
+ /** Bar chart of closed quota windows, oldest left. The solid segment is
64
+ * used at close, the hatched cap is the stored leftover; a cap under 3% is
65
+ * label-only. One bar per real close, never a placeholder. Stale samples
66
+ * name their earliness on the label; exact stamps stay in the tooltip. */
67
+ export function ClosedWeeks({ closes, windowName, }) {
68
68
  if (!Array.isArray(closes) || closes.length === 0)
69
69
  return null;
70
70
  const cells = [...closes].reverse();
71
71
  const labels = cells.map((c) => closeDateLabel(c.resetsAt));
72
- return (_jsxs("section", { className: "dsec", "aria-label": "Closed weeks", children: [_jsx("h3", { children: "Closed weeks" }), _jsx("div", { className: "weeks", "data-testid": "closed-weeks", children: cells.map((c, i) => {
73
- const used = Math.min(100, Math.max(0, c.usedPct));
74
- const leftover = Math.min(100, Math.max(0, c.leftoverPct));
75
- const stale = closeIsStale(c);
76
- const sampled = closeSampleStamp(c.sampledAt);
77
- const reset = c.resetsAt ? closeResetStamp(c.resetsAt) : null;
78
- const earlyMs = c.resetsAt ? Date.parse(c.resetsAt) - Date.parse(c.sampledAt) : NaN;
79
- const est = c.resetsAtEstimated ? " (est.)" : "";
80
- const title = [
81
- `${c.usedPct}% used`,
82
- `${c.leftoverPct}% leftover`,
83
- reset ? `reset ${reset}${est}` : "reset unknown",
84
- stale && sampled ? `last reading ${sampled}` : null,
85
- ]
86
- .filter(Boolean)
87
- .join(", ");
88
- return (_jsxs("div", { className: "week", title: title, children: [_jsxs("div", { className: "track", role: "img", "aria-label": title, children: [_jsx("div", { className: "fill", style: { width: `${used}%` } }), leftover > 0 && (_jsx("div", { className: "band band-waste", style: { left: `${used}%`, width: `${leftover}%` } }))] }), _jsxs("div", { className: "lab", children: [_jsx("span", { children: labels[i] ?? "date unknown" }), _jsxs("b", { children: [c.leftoverPct, "% leftover", est] }), stale && Number.isFinite(earlyMs) && earlyMs > 0 && (_jsxs("span", { className: "stale-note", children: ["read ", earlyCompact(earlyMs), " early"] }))] })] }, `${c.detectedAt}-${c.sampledAt}`));
89
- }) }), cells.map((c, i) => {
90
- const resetMs = c.resetsAt ? Date.parse(c.resetsAt) : NaN;
91
- const sampledMs = Date.parse(c.sampledAt);
92
- const sampled = closeSampleStamp(c.sampledAt);
93
- if (!closeIsStale(c) ||
94
- !sampled ||
95
- !Number.isFinite(resetMs) ||
96
- !Number.isFinite(sampledMs) ||
97
- resetMs <= sampledMs) {
98
- return null;
99
- }
100
- return (_jsxs("p", { className: "cell-s stale-note", children: [labels[i] ?? "Date unknown", " leftover is from the ", sampled, " reading, ", earlyWords(resetMs - sampledMs), " ", "before that reset."] }, `note-${c.detectedAt}-${c.sampledAt}`));
101
- })] }));
72
+ return (_jsxs("section", { className: "dsec", "aria-label": "History", children: [windowName ? (_jsxs("div", { className: "burn-head", children: [_jsx("h3", { children: "History" }), _jsxs("span", { className: "burn-win", children: [windowName, " quota window"] })] })) : (_jsx("h3", { children: "History" })), _jsxs("div", { className: "hchart", "data-testid": "closed-weeks", children: [_jsx("div", { className: "hgrid", children: _jsx("span", { children: "100%" }) }), cells.map((c, i) => {
73
+ const used = Math.min(100, Math.max(0, c.usedPct));
74
+ const leftover = Math.min(100, Math.max(0, c.leftoverPct));
75
+ const stale = closeIsStale(c);
76
+ const sampled = closeSampleStamp(c.sampledAt);
77
+ const reset = c.resetsAt ? closeResetStamp(c.resetsAt) : null;
78
+ const earlyMs = c.resetsAt ? Date.parse(c.resetsAt) - Date.parse(c.sampledAt) : NaN;
79
+ const est = c.resetsAtEstimated ? " (est.)" : "";
80
+ const title = [
81
+ `${c.usedPct}% used`,
82
+ `${c.leftoverPct}% leftover`,
83
+ reset ? `reset ${reset}${est}` : "reset unknown",
84
+ stale && sampled ? `last reading ${sampled}` : null,
85
+ ]
86
+ .filter(Boolean)
87
+ .join(", ");
88
+ return (_jsxs("div", { className: "hcol", title: title, children: [_jsx("div", { className: "hbars", children: _jsxs("div", { className: "hbar", children: [leftover >= 3 && _jsx("span", { className: "hatch", style: { height: `${leftover}%` } }), _jsx("span", { className: "solid", style: { height: `${used}%` } })] }) }), _jsxs("div", { className: "hlab", children: [labels[i] ?? "date unknown", _jsxs("b", { children: [c.leftoverPct, "% leftover", est] }), stale && Number.isFinite(earlyMs) && earlyMs > 0 && _jsxs(_Fragment, { children: ["read ", earlyCompact(earlyMs), " early"] })] })] }, `${c.detectedAt}-${c.sampledAt}`));
89
+ })] })] }));
102
90
  }
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ export declare function CopyButton({ text }: {
3
+ text: string;
4
+ }): React.JSX.Element;
@@ -0,0 +1,27 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ export function CopyButton({ text }) {
4
+ const [copied, setCopied] = useState(false);
5
+ const timer = useRef(null);
6
+ useEffect(() => {
7
+ return () => {
8
+ if (timer.current)
9
+ clearTimeout(timer.current);
10
+ };
11
+ }, []);
12
+ const handleCopy = async () => {
13
+ try {
14
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
15
+ await navigator.clipboard.writeText(text);
16
+ }
17
+ }
18
+ catch {
19
+ // Clipboard may be unavailable; still give feedback like the mock.
20
+ }
21
+ setCopied(true);
22
+ if (timer.current)
23
+ clearTimeout(timer.current);
24
+ timer.current = setTimeout(() => setCopied(false), 1500);
25
+ };
26
+ return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "btn btn-quiet btn-sm", onClick: handleCopy, children: copied ? "Copied" : "Copy" }), _jsx("span", { className: "sr-live", role: "status", "aria-live": "polite", children: copied ? "Copied to clipboard." : "" })] }));
27
+ }
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+ interface BoundaryProps {
3
+ fallback: React.ReactNode;
4
+ /** A changed key clears a shown fallback and retries the render. */
5
+ resetKey?: unknown;
6
+ children?: React.ReactNode;
7
+ }
8
+ /**
9
+ * A render crash inside shows fallback UI; the rest of the dashboard
10
+ * survives. Without this, one bad provider unmounts the whole tree into
11
+ * a blank page.
12
+ */
13
+ export declare class ErrorBoundary extends React.Component<BoundaryProps, {
14
+ error: unknown;
15
+ }> {
16
+ state: {
17
+ error: unknown;
18
+ };
19
+ static getDerivedStateFromError(error: unknown): {
20
+ error: unknown;
21
+ };
22
+ componentDidUpdate(prevProps: BoundaryProps): void;
23
+ render(): React.ReactNode;
24
+ }
25
+ export declare function CardErrorFallback({ id, name }: {
26
+ id: string;
27
+ name: string;
28
+ }): React.JSX.Element;
29
+ export declare function RowErrorFallback({ id, name }: {
30
+ id: string;
31
+ name: string;
32
+ }): React.JSX.Element;
33
+ export declare function DrawerErrorFallback({ name, onClose }: {
34
+ name: string;
35
+ onClose: () => void;
36
+ }): React.JSX.Element;
37
+ export {};
@@ -0,0 +1,32 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ /**
4
+ * A render crash inside shows fallback UI; the rest of the dashboard
5
+ * survives. Without this, one bad provider unmounts the whole tree into
6
+ * a blank page.
7
+ */
8
+ export class ErrorBoundary extends React.Component {
9
+ state = { error: null };
10
+ static getDerivedStateFromError(error) {
11
+ return { error };
12
+ }
13
+ componentDidUpdate(prevProps) {
14
+ if (prevProps.resetKey !== this.props.resetKey && this.state.error !== null) {
15
+ this.setState({ error: null });
16
+ }
17
+ }
18
+ render() {
19
+ if (this.state.error !== null)
20
+ return this.props.fallback;
21
+ return this.props.children;
22
+ }
23
+ }
24
+ export function CardErrorFallback({ id, name }) {
25
+ return (_jsxs("article", { "data-testid": `provider-card-${id}-error`, className: "pcard", role: "alert", children: [_jsx("div", { className: "pcard-head", children: _jsx("div", { className: "who", children: _jsx("b", { children: name }) }) }), _jsx("div", { className: "pcard-body", children: _jsx("p", { className: "cell-s", children: "Couldn't show this subscription." }) })] }));
26
+ }
27
+ export function RowErrorFallback({ id, name }) {
28
+ return (_jsxs("div", { "data-testid": `provider-row-${id}-error`, className: "lrow", role: "alert", children: [_jsx("div", { className: "prov", "data-label": "Provider", children: _jsx("span", { children: _jsx("span", { className: "nm", children: name }) }) }), _jsx("div", { "data-label": "Used vs elapsed", children: _jsx("span", { className: "cell-s", children: "Couldn't show this subscription." }) })] }));
29
+ }
30
+ export function DrawerErrorFallback({ name, onClose }) {
31
+ return (_jsxs(_Fragment, { children: [_jsx("div", { "data-testid": "drawer-scrim", className: "scrim is-open", "aria-hidden": "true", onClick: onClose }), _jsxs("aside", { "data-testid": "provider-drawer-error", role: "alert", className: "drawer is-open", children: [_jsxs("div", { className: "drawer-head", children: [_jsx("div", { style: { flex: 1, minWidth: 0 }, children: _jsx("h2", { children: name }) }), _jsx("button", { className: "drawer-close", type: "button", "aria-label": "Close provider details", onClick: onClose, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) }) })] }), _jsx("section", { className: "dsec", "aria-label": "Details unavailable", children: _jsx("p", { className: "cell-s", children: "Couldn't show these details." }) })] })] }));
32
+ }
@@ -15,10 +15,11 @@ export declare function authInstruction(name: string, attempt: {
15
15
  * a re-poll. Service start/install is covered by the service-unavailable
16
16
  * panel when the daemon itself is down.
17
17
  */
18
- export declare function repairFor(provider: ProviderView): Repair;
19
- export declare function FaultBanner({ providers, onRepoll, repolling, polling, }: {
18
+ export declare function repairFor(provider: ProviderView, opencodeLoginCommand?: string | null): Repair;
19
+ export declare function FaultBanner({ providers, onRepoll, repolling, polling, opencodeLoginCommand, }: {
20
20
  providers: ProviderView[];
21
21
  onRepoll: () => void;
22
22
  repolling: boolean;
23
23
  polling?: "idle" | "in-progress" | "degraded" | string;
24
+ opencodeLoginCommand?: string | null;
24
25
  }): React.JSX.Element | null;
@@ -16,12 +16,15 @@ export function authInstruction(name, attempt) {
16
16
  * a re-poll. Service start/install is covered by the service-unavailable
17
17
  * panel when the daemon itself is down.
18
18
  */
19
- export function repairFor(provider) {
19
+ export function repairFor(provider, opencodeLoginCommand) {
20
20
  const name = provider.displayName;
21
21
  if (provider.lastAttempt?.failureCategory === "auth") {
22
22
  if (provider.id === "opencode-go") {
23
+ const run = opencodeLoginCommand
24
+ ? `Run \`${opencodeLoginCommand}\`, then re-poll.`
25
+ : "Run the login command for your OpenCode CLI version — v2 `opencode auth login opencode` or v1 `opencode console login` — then re-poll.";
23
26
  return {
24
- text: "Run `opencode auth login -p opencode-go`, then re-poll. QuotaCap reads that key only because you enabled OpenCode Go — `quotacap providers disable opencode-go` stops it.",
27
+ text: `${run} QuotaCap reads that key only because you enabled OpenCode Go — \`quotacap providers disable opencode-go\` stops it.`,
25
28
  };
26
29
  }
27
30
  return { text: authInstruction(name, provider.lastAttempt) };
@@ -65,7 +68,7 @@ function bannerVerb(provider) {
65
68
  return "needs attention";
66
69
  }
67
70
  }
68
- export function FaultBanner({ providers, onRepoll, repolling, polling, }) {
71
+ export function FaultBanner({ providers, onRepoll, repolling, polling, opencodeLoginCommand, }) {
69
72
  const isPolling = repolling || polling === "in-progress";
70
73
  const faulted = providers.filter((p) => {
71
74
  if (!p.enabled || p.exclusionReason === null)
@@ -79,7 +82,7 @@ export function FaultBanner({ providers, onRepoll, repolling, polling, }) {
79
82
  return null;
80
83
  return (_jsx("div", { children: faulted.map((p) => {
81
84
  const age = ageDuration(p.ageMs);
82
- const repair = repairFor(p);
85
+ const repair = repairFor(p, opencodeLoginCommand);
83
86
  return (_jsxs("div", { "data-testid": "fault-banner", role: "alert", className: "alert", children: [_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.3", strokeLinecap: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M12 8v5" }), _jsx("path", { d: "M12 16.5v.01" }), _jsx("circle", { cx: "12", cy: "12", r: "9" })] }), _jsxs("span", { children: [_jsxs("strong", { children: [p.displayName, " ", bannerVerb(p), "."] }), " ", age ? `Last read ${age} ago; ` : "", "excluded from ranking. ", repair.text] }), _jsx("button", { type: "button", className: "linkbtn", onClick: onRepoll, disabled: repolling, style: { flex: "none", marginLeft: "auto" }, children: repolling
84
87
  ? "Polling…"
85
88
  : (p.lastAttempt?.failureCategory === "auth"
@@ -1,12 +1,14 @@
1
1
  import React from "react";
2
2
  import type { ProviderView, RuntimeView } from "../state.js";
3
+ export declare function openCodeGoConsentCopy(loginCommand?: string | null): string;
3
4
  export declare const OPENCODE_GO_CONSENT_COPY: string;
4
5
  export declare function showOpenCodeGoBanner(runtime: Pick<RuntimeView, "detectedProviders"> | undefined, providers: Array<Pick<ProviderView, "id" | "enabled">>, suppressed: boolean): boolean;
5
6
  export declare function OpencodeGoBanner({ onEnable }: {
6
7
  onEnable: () => void;
7
8
  }): React.JSX.Element;
8
- export declare function ConsentModal({ onConfirm, onCancel, busy, }: {
9
+ export declare function ConsentModal({ onConfirm, onCancel, busy, loginCommand, }: {
9
10
  onConfirm: () => void;
10
11
  onCancel: () => void;
11
12
  busy?: boolean;
13
+ loginCommand?: string | null;
12
14
  }): React.JSX.Element;