quotacap 0.0.37 → 0.0.39
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.
- package/README.md +5 -3
- package/dist/adapters/claude.js +4 -1
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/opencode-go.d.ts +11 -0
- package/dist/adapters/opencode-go.js +122 -0
- package/dist/advisory/provider-names.js +9 -0
- package/dist/advisory/snapshot.d.ts +1 -0
- package/dist/advisory/types.d.ts +3 -0
- package/dist/catalog/claude.js +2 -1
- package/dist/cli/providers.js +121 -1
- package/dist/cli/status.js +6 -0
- package/dist/config.d.ts +11 -1
- package/dist/config.js +52 -3
- package/dist/diagnostics/failure.js +11 -0
- package/dist/http/server.d.ts +2 -0
- package/dist/http/server.js +37 -1
- package/dist/runtime/service.js +2 -0
- package/dist/version.js +1 -1
- package/dist/web/src/App.js +53 -4
- package/dist/web/src/api.d.ts +2 -0
- package/dist/web/src/api.js +20 -0
- package/dist/web/src/components/FaultBanner.d.ts +2 -1
- package/dist/web/src/components/FaultBanner.js +8 -2
- package/dist/web/src/components/OpencodeGoBanner.d.ts +12 -0
- package/dist/web/src/components/OpencodeGoBanner.js +36 -0
- package/dist/web/src/components/ProviderDrawer.js +3 -0
- package/dist/web/src/components/settings/ProvidersPanel.d.ts +2 -1
- package/dist/web/src/components/settings/ProvidersPanel.js +12 -10
- package/dist/web/src/components/settings/SettingsDrawer.d.ts +2 -1
- package/dist/web/src/components/settings/SettingsDrawer.js +2 -2
- package/dist/web/src/pages/Onboarding.js +2 -2
- package/dist/web/src/state.d.ts +3 -1
- package/dist/web/src/state.js +10 -2
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +1 -1
- package/web/dist/assets/index-CD8jaN87.js +12 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-gDYktkri.js +0 -12
package/dist/http/server.js
CHANGED
|
@@ -10,12 +10,13 @@ import { validateTask } from "../advisory/validate.js";
|
|
|
10
10
|
import { readUpdateCache } from "../runtime/updates.js";
|
|
11
11
|
import { compareVersions } from "../runtime/versions.js";
|
|
12
12
|
import { parseManualUsage } from "../adapters/manual.js";
|
|
13
|
+
import { adapters } from "../adapters/index.js";
|
|
13
14
|
import { upsertQuota } from "../store/quotas.js";
|
|
14
15
|
import { createCoordinator, ServiceClosing, } from "../runtime/poll.js";
|
|
15
16
|
import { classifyFailure } from "../diagnostics/failure.js";
|
|
16
17
|
import { validateDisplayName } from "../advisory/validation.js";
|
|
17
18
|
import { providerIdentity } from "../advisory/provider-names.js";
|
|
18
|
-
import { setProviderNameOverride } from "../config.js";
|
|
19
|
+
import { setProviderNameOverride, setProviderEnabled } from "../config.js";
|
|
19
20
|
import { ensureCatalogs, catalogProviders, CATALOG_FAIL_COOLDOWN_MS } from "../catalog/index.js";
|
|
20
21
|
import { getAllLatest } from "../store/quotas.js";
|
|
21
22
|
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
|
|
@@ -91,6 +92,7 @@ export function testCtx(db, overrides) {
|
|
|
91
92
|
coordinator: createCoordinator({ db, enabledProviders: [] }),
|
|
92
93
|
enabledProviders: [],
|
|
93
94
|
providerNames: {},
|
|
95
|
+
detectedProviders: [],
|
|
94
96
|
version: "test",
|
|
95
97
|
exec: "test",
|
|
96
98
|
ingestEnabled: false,
|
|
@@ -119,6 +121,7 @@ function snapshotOf(ctx) {
|
|
|
119
121
|
upToDate: !cache || compareVersions(cache.latest, ctx.version) <= 0,
|
|
120
122
|
checkedAt: cache?.checkedAt ?? null,
|
|
121
123
|
},
|
|
124
|
+
detectedProviders: ctx.detectedProviders ?? [],
|
|
122
125
|
},
|
|
123
126
|
});
|
|
124
127
|
}
|
|
@@ -363,6 +366,39 @@ export function buildApp(ctx) {
|
|
|
363
366
|
override: validatedName,
|
|
364
367
|
};
|
|
365
368
|
});
|
|
369
|
+
// Enable/disable a provider. Token-gated like refresh. Consent is recorded
|
|
370
|
+
// protocol-level for the one provider whose key QuotaCap reads. The
|
|
371
|
+
// coordinator's enabled set is a start-time snapshot, so the response
|
|
372
|
+
// tells every caller a restart applies it — no live mutation.
|
|
373
|
+
app.post("/api/providers/:id/enabled", async (req, reply) => {
|
|
374
|
+
const headerToken = req.headers["x-quotacap-token"];
|
|
375
|
+
if (!isValidToken(headerToken, ctx.token)) {
|
|
376
|
+
return reply.status(401).send({ error: "unauthorized: missing or invalid X-QuotaCap-Token header" });
|
|
377
|
+
}
|
|
378
|
+
const { id } = req.params;
|
|
379
|
+
if (typeof id !== "string" || !id || !Object.hasOwn(adapters, id) || id === "manual") {
|
|
380
|
+
return reply.status(400).send({ error: `unknown provider id: ${id}` });
|
|
381
|
+
}
|
|
382
|
+
const body = (req.body ?? {});
|
|
383
|
+
if (typeof body !== "object" || body === null || typeof body.enabled !== "boolean") {
|
|
384
|
+
return reply.status(400).send({ error: "invalid-argument: body must contain enabled: boolean" });
|
|
385
|
+
}
|
|
386
|
+
const enabling = body.enabled === true;
|
|
387
|
+
if (id === "opencode-go" && enabling && body.consent !== true) {
|
|
388
|
+
return reply.status(400).send({ error: "consent required: enabling opencode-go requires consent: true" });
|
|
389
|
+
}
|
|
390
|
+
if (ctx.configPath) {
|
|
391
|
+
try {
|
|
392
|
+
await setProviderEnabled(id, enabling, ctx.configPath);
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
return reply.status(500).send({
|
|
396
|
+
error: `failed to persist configuration: ${err?.message ?? String(err)}`,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { ok: true, id, enabled: enabling, restartRequired: true };
|
|
401
|
+
});
|
|
366
402
|
// Graceful-stop request for CLI-driven takeovers. The endpoint restarts
|
|
367
403
|
// nothing by itself: it responds 202, then the service follows its normal
|
|
368
404
|
// SIGTERM path while the CLI spawns the successor after the port releases.
|
package/dist/runtime/service.js
CHANGED
|
@@ -16,6 +16,7 @@ import { buildApp } from "../http/server.js";
|
|
|
16
16
|
import { detectChannel, refreshUpdateCache } from "./updates.js";
|
|
17
17
|
import { VERSION } from "../version.js";
|
|
18
18
|
import { claudeAdapter } from "../adapters/claude.js";
|
|
19
|
+
import { opencodeGoDetected } from "../adapters/opencode-go.js";
|
|
19
20
|
import { classifyFailure } from "../diagnostics/failure.js";
|
|
20
21
|
import { ensureCatalogs, catalogProviders } from "../catalog/index.js";
|
|
21
22
|
export function resolveClaudeExecPath() {
|
|
@@ -198,6 +199,7 @@ export async function startService(opts) {
|
|
|
198
199
|
token,
|
|
199
200
|
coordinator,
|
|
200
201
|
enabledProviders: config.enabledProviders,
|
|
202
|
+
detectedProviders: opencodeGoDetected() ? ["opencode-go"] : [],
|
|
201
203
|
providerNames: config.providerNames,
|
|
202
204
|
catalogTtlHours: config.catalogTtlHours,
|
|
203
205
|
version: VERSION,
|
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.
|
|
2
|
+
export const VERSION = "0.0.39";
|
package/dist/web/src/App.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useCallback, useEffect, useState } from "react";
|
|
3
3
|
import { createRoot } from "react-dom/client";
|
|
4
4
|
import "./theme.css";
|
|
5
|
-
import { daemonEndpoint, loadState, triggerRefresh, StateHttpError, StateNetworkError, } from "./api.js";
|
|
5
|
+
import { daemonEndpoint, loadState, triggerRefresh, setProviderEnabled, StateHttpError, StateNetworkError, } from "./api.js";
|
|
6
6
|
import { ageLabel, firstRun, toViewModel } from "./state.js";
|
|
7
7
|
import { Recommendation } from "./components/Recommendation.js";
|
|
8
8
|
import { SubscriptionList } from "./components/SubscriptionList.js";
|
|
@@ -14,6 +14,7 @@ import { navigate, routeFor, useRoute } from "./router.js";
|
|
|
14
14
|
import { Onboarding } from "./pages/Onboarding.js";
|
|
15
15
|
import { Header } from "./components/Header.js";
|
|
16
16
|
import { FaultBanner } from "./components/FaultBanner.js";
|
|
17
|
+
import { ConsentModal, OpencodeGoBanner, showOpenCodeGoBanner } from "./components/OpencodeGoBanner.js";
|
|
17
18
|
import { SiteFooter } from "./components/PacingLegend.js";
|
|
18
19
|
function UnavailablePanel({ endpoint, lastGood, onRetry, }) {
|
|
19
20
|
return (_jsxs("div", { "data-testid": "state-unavailable", role: "alert", children: [_jsx("h2", { children: "Service unavailable" }), _jsxs("p", { children: ["The QuotaCap daemon did not answer at ", _jsx("code", { children: endpoint }), ". Start it with", " ", _jsx("code", { children: "quotacap daemon" }), ", then retry."] }), _jsx("button", { type: "button", onClick: onRetry, children: "Retry" }), lastGood && _jsxs("p", { children: ["Showing last readings (", ageLabel(lastGood.asOf), ")."] })] }));
|
|
@@ -34,6 +35,9 @@ function App() {
|
|
|
34
35
|
const [selectedProvider, setSelectedProvider] = useState(null);
|
|
35
36
|
const [adviceOpen, setAdviceOpen] = useState(false);
|
|
36
37
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
38
|
+
const [consentOpen, setConsentOpen] = useState(false);
|
|
39
|
+
const [consentBusy, setConsentBusy] = useState(false);
|
|
40
|
+
const [goSuppressed, setGoSuppressed] = useState(false);
|
|
37
41
|
const route = useRoute();
|
|
38
42
|
const target = snapshot ? routeFor(route, firstRun(snapshot)) : null;
|
|
39
43
|
useEffect(() => {
|
|
@@ -98,6 +102,23 @@ function App() {
|
|
|
98
102
|
setRefreshing(false);
|
|
99
103
|
}
|
|
100
104
|
}, [load]);
|
|
105
|
+
const handleSetProviderEnabled = (id, enabled) => {
|
|
106
|
+
if (id === "opencode-go" && enabled) {
|
|
107
|
+
setConsentOpen(true);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
void (async () => {
|
|
111
|
+
try {
|
|
112
|
+
await setProviderEnabled(id, enabled);
|
|
113
|
+
if (!enabled)
|
|
114
|
+
setGoSuppressed(false);
|
|
115
|
+
setNotice(`${id === "opencode-go" ? "OpenCode Go" : id} ${enabled ? "enabled" : "disabled"} — restart the daemon to apply it.`);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
setNotice(`Could not update ${id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
121
|
+
};
|
|
101
122
|
const setupMode = target === "/setup";
|
|
102
123
|
const pollErrorText = error
|
|
103
124
|
? error.kind === "http-error"
|
|
@@ -106,7 +127,21 @@ function App() {
|
|
|
106
127
|
: null;
|
|
107
128
|
const header = (_jsx(Header, { runtime: snapshot?.runtime ?? null, unreachable: error?.kind === "service-unavailable", refreshing: refreshing, onRefresh: () => void refresh(), onSettings: () => setSettingsOpen(true) }));
|
|
108
129
|
if (snapshot && setupMode) {
|
|
109
|
-
return (_jsxs(_Fragment, { children: [header,
|
|
130
|
+
return (_jsxs(_Fragment, { children: [header, _jsxs("main", { children: [notice && (_jsx("div", { "data-testid": "refresh-notice", role: "status", children: notice })), showOpenCodeGoBanner(snapshot.runtime, snapshot.providers, goSuppressed) && (_jsx(OpencodeGoBanner, { onEnable: () => setConsentOpen(true) })), _jsx(Onboarding, { snapshot: snapshot, onFirstPoll: () => void refresh(), polling: refreshing, pollError: pollErrorText })] }), _jsx(SettingsDrawer, { open: settingsOpen, providers: snapshot.providers, onRefresh: () => void refresh(), refreshing: refreshing, onSetProviderEnabled: handleSetProviderEnabled, onClose: () => setSettingsOpen(false) }), consentOpen && (_jsx(ConsentModal, { busy: consentBusy, onCancel: () => setConsentOpen(false), onConfirm: () => void (async () => {
|
|
131
|
+
setConsentBusy(true);
|
|
132
|
+
try {
|
|
133
|
+
await setProviderEnabled("opencode-go", true, true);
|
|
134
|
+
setGoSuppressed(true);
|
|
135
|
+
setConsentOpen(false);
|
|
136
|
+
setNotice("OpenCode Go enabled — restart the daemon to apply it.");
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
setNotice(`Could not enable OpenCode Go: ${e instanceof Error ? e.message : String(e)}`);
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
setConsentBusy(false);
|
|
143
|
+
}
|
|
144
|
+
})() })), _jsx(SiteFooter, { version: snapshot.runtime.version })] }));
|
|
110
145
|
}
|
|
111
146
|
return (_jsxs(_Fragment, { children: [header, _jsxs("main", { children: [notice && (_jsx("div", { "data-testid": "refresh-notice", role: "status", children: notice })), loading && !snapshot && (_jsx("div", { "data-testid": "state-loading", "aria-busy": "true", "aria-label": "Loading quota state", children: [0, 1, 2].map((i) => (_jsx("div", { "aria-hidden": "true", className: "skel", style: {
|
|
112
147
|
height: 64,
|
|
@@ -114,13 +149,27 @@ function App() {
|
|
|
114
149
|
border: i === 0 ? "1px solid var(--rec)" : "1px solid var(--line)",
|
|
115
150
|
borderRadius: 8,
|
|
116
151
|
marginBottom: 8,
|
|
117
|
-
} }, i))) })), error?.kind === "service-unavailable" && !snapshot && (_jsx(UnavailablePanel, { endpoint: daemonEndpoint(), lastGood: snapshot, onRetry: () => void load() })), error?.kind === "http-error" && !snapshot && (_jsx(HttpErrorPanel, { status: error.status, message: error.message, lastGood: snapshot, onRetry: () => void load() })), error && snapshot && (_jsxs("div", { "data-testid": "state-stale-error", role: "alert", children: [error.kind === "http-error" ? `Request failed (HTTP ${error.status}): ` : "Service unavailable: ", error.message, " Showing last readings (", ageLabel(snapshot.asOf), ").", " ", _jsx("button", { type: "button", onClick: () => void load(), children: "Retry" })] })), snapshot && (_jsx(FaultBanner, { providers: snapshot.providers, onRepoll: () => void refresh(), repolling: refreshing, polling: snapshot.runtime.polling })), snapshot && (_jsx(ReadyView, { snapshot: snapshot, onSelectProvider: (id) => {
|
|
152
|
+
} }, i))) })), error?.kind === "service-unavailable" && !snapshot && (_jsx(UnavailablePanel, { endpoint: daemonEndpoint(), lastGood: snapshot, onRetry: () => void load() })), error?.kind === "http-error" && !snapshot && (_jsx(HttpErrorPanel, { status: error.status, message: error.message, lastGood: snapshot, onRetry: () => void load() })), error && snapshot && (_jsxs("div", { "data-testid": "state-stale-error", role: "alert", children: [error.kind === "http-error" ? `Request failed (HTTP ${error.status}): ` : "Service unavailable: ", error.message, " Showing last readings (", ageLabel(snapshot.asOf), ").", " ", _jsx("button", { type: "button", onClick: () => void load(), children: "Retry" })] })), snapshot && (_jsx(FaultBanner, { providers: snapshot.providers, onRepoll: () => void refresh(), repolling: refreshing, polling: snapshot.runtime.polling })), snapshot && showOpenCodeGoBanner(snapshot.runtime, snapshot.providers, goSuppressed) && (_jsx(OpencodeGoBanner, { onEnable: () => setConsentOpen(true) })), snapshot && (_jsx(ReadyView, { snapshot: snapshot, onSelectProvider: (id) => {
|
|
118
153
|
setAdviceOpen(false);
|
|
119
154
|
setSelectedProvider(id);
|
|
120
155
|
}, onViewAdvice: () => {
|
|
121
156
|
setSelectedProvider(null);
|
|
122
157
|
setAdviceOpen(true);
|
|
123
|
-
} }))] }), _jsx(ProviderDrawer, { provider: snapshot?.providers.find((p) => p.id === selectedProvider) ?? null, asOf: snapshot?.asOf ?? "", recommendation: snapshot?.recommendation ?? null, onClose: () => setSelectedProvider(null), onRenamed: () => void load() }),
|
|
158
|
+
} }))] }), _jsx(ProviderDrawer, { provider: snapshot?.providers.find((p) => p.id === selectedProvider) ?? null, asOf: snapshot?.asOf ?? "", recommendation: snapshot?.recommendation ?? null, onClose: () => setSelectedProvider(null), onRenamed: () => void load() }), consentOpen && (_jsx(ConsentModal, { busy: consentBusy, onCancel: () => setConsentOpen(false), onConfirm: () => void (async () => {
|
|
159
|
+
setConsentBusy(true);
|
|
160
|
+
try {
|
|
161
|
+
await setProviderEnabled("opencode-go", true, true);
|
|
162
|
+
setGoSuppressed(true);
|
|
163
|
+
setConsentOpen(false);
|
|
164
|
+
setNotice("OpenCode Go enabled — restart the daemon to apply it.");
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
setNotice(`Could not enable OpenCode Go: ${e instanceof Error ? e.message : String(e)}`);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
setConsentBusy(false);
|
|
171
|
+
}
|
|
172
|
+
})() })), snapshot && (_jsx(AdviceDrawer, { open: adviceOpen, recommendation: snapshot.recommendation, providers: snapshot.providers, asOf: snapshot.asOf, onClose: () => setAdviceOpen(false) })), _jsx(SettingsDrawer, { open: settingsOpen, providers: snapshot?.providers ?? [], onRefresh: () => void refresh(), refreshing: refreshing, onSetProviderEnabled: handleSetProviderEnabled, onClose: () => setSettingsOpen(false) }), _jsx(SiteFooter, { version: snapshot?.runtime.version })] }));
|
|
124
173
|
}
|
|
125
174
|
const el = typeof document === "undefined" ? null : document.getElementById("app");
|
|
126
175
|
if (el)
|
package/dist/web/src/api.d.ts
CHANGED
|
@@ -27,3 +27,5 @@ export declare function refreshMessage(body: unknown): string | null;
|
|
|
27
27
|
export declare function triggerRefresh(): Promise<RefreshResult>;
|
|
28
28
|
/** Rename provider or restore built-in default with null. */
|
|
29
29
|
export declare function renameProvider(id: string, displayName: string | null): Promise<void>;
|
|
30
|
+
/** Enable or disable a provider. Consent is required by the server for opencode-go. */
|
|
31
|
+
export declare function setProviderEnabled(id: string, enabled: boolean, consent?: boolean): Promise<void>;
|
package/dist/web/src/api.js
CHANGED
|
@@ -122,3 +122,23 @@ export async function renameProvider(id, displayName) {
|
|
|
122
122
|
if (!r.ok)
|
|
123
123
|
throw new StateHttpError(r.status, await serverMessage(r));
|
|
124
124
|
}
|
|
125
|
+
/** Enable or disable a provider. Consent is required by the server for opencode-go. */
|
|
126
|
+
export async function setProviderEnabled(id, enabled, consent) {
|
|
127
|
+
const token = await fetchToken();
|
|
128
|
+
let r;
|
|
129
|
+
try {
|
|
130
|
+
r = await fetch(`/api/providers/${encodeURIComponent(id)}/enabled`, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: {
|
|
133
|
+
"Content-Type": "application/json",
|
|
134
|
+
"X-QuotaCap-Token": token,
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify(consent === undefined ? { enabled } : { enabled, consent }),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
throw new StateNetworkError(`/api/providers/${id}/enabled`, e);
|
|
141
|
+
}
|
|
142
|
+
if (!r.ok)
|
|
143
|
+
throw new StateHttpError(r.status, await serverMessage(r));
|
|
144
|
+
}
|
|
@@ -5,7 +5,8 @@ export interface Repair {
|
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Repair routing per excluded provider. Auth failures point at signing into
|
|
8
|
-
* the provider CLI (QuotaCap never
|
|
8
|
+
* the provider CLI (by default QuotaCap never reads credentials; the only
|
|
9
|
+
* exception is OpenCode Go, which the user enables explicitly); every other
|
|
9
10
|
* exclusion points at a re-poll. Service start/install is covered by the
|
|
10
11
|
* service-unavailable panel when the daemon itself is down.
|
|
11
12
|
*/
|
|
@@ -3,15 +3,21 @@ import { ageDuration } from "../state.js";
|
|
|
3
3
|
import { failureWords } from "./ProviderDrawer.js";
|
|
4
4
|
/**
|
|
5
5
|
* Repair routing per excluded provider. Auth failures point at signing into
|
|
6
|
-
* the provider CLI (QuotaCap never
|
|
6
|
+
* the provider CLI (by default QuotaCap never reads credentials; the only
|
|
7
|
+
* exception is OpenCode Go, which the user enables explicitly); every other
|
|
7
8
|
* exclusion points at a re-poll. Service start/install is covered by the
|
|
8
9
|
* service-unavailable panel when the daemon itself is down.
|
|
9
10
|
*/
|
|
10
11
|
export function repairFor(provider) {
|
|
11
12
|
const name = provider.displayName;
|
|
12
13
|
if (provider.lastAttempt?.failureCategory === "auth") {
|
|
14
|
+
if (provider.id === "opencode-go") {
|
|
15
|
+
return {
|
|
16
|
+
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.",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
13
19
|
return {
|
|
14
|
-
text: `Sign in to the ${name} CLI, then re-poll. QuotaCap never
|
|
20
|
+
text: `Sign in to the ${name} CLI, then re-poll. By default QuotaCap never reads credentials; the only exception is OpenCode Go, which you enable explicitly.`,
|
|
15
21
|
};
|
|
16
22
|
}
|
|
17
23
|
switch (provider.exclusionReason) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ProviderView, RuntimeView } from "../state.js";
|
|
3
|
+
export declare const OPENCODE_GO_CONSENT_COPY = "QuotaCap will read the API key OpenCode already stored when you ran `opencode auth login -p opencode-go` (`~/.local/share/opencode/auth.json`, `opencode-go` \u2192 `opencode` fallback). It is read in-memory, read-only once per poll (every 15 minutes by default). It is sent only as `Authorization: Bearer` to `https://opencode.ai/zen/go/v1/usage` to fetch `rolling/weekly/monthly` `percent` + `resetsAt`. It is never used to make chat or model requests, never written or rotated, never stored in the `quotacap` DB, never returned by the API or MCP, and never logged (errors show `[redacted]`). Revoke at `https://opencode.ai/auth` or `opencode providers logout opencode-go` and run `quotacap providers disable opencode-go` to stop polling. If you prefer no file access, set `OPENCODE_API_KEY` instead.";
|
|
4
|
+
export declare function showOpenCodeGoBanner(runtime: Pick<RuntimeView, "detectedProviders"> | undefined, providers: Array<Pick<ProviderView, "id" | "enabled">>, suppressed: boolean): boolean;
|
|
5
|
+
export declare function OpencodeGoBanner({ onEnable }: {
|
|
6
|
+
onEnable: () => void;
|
|
7
|
+
}): React.JSX.Element;
|
|
8
|
+
export declare function ConsentModal({ onConfirm, onCancel, busy, }: {
|
|
9
|
+
onConfirm: () => void;
|
|
10
|
+
onCancel: () => void;
|
|
11
|
+
busy?: boolean;
|
|
12
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
export const OPENCODE_GO_CONSENT_COPY = "QuotaCap will read the API key OpenCode already stored when you ran `opencode auth login -p opencode-go` (`~/.local/share/opencode/auth.json`, `opencode-go` → `opencode` fallback). It is read in-memory, read-only once per poll (every 15 minutes by default). It is sent only as `Authorization: Bearer` to `https://opencode.ai/zen/go/v1/usage` to fetch `rolling/weekly/monthly` `percent` + `resetsAt`. It is never used to make chat or model requests, never written or rotated, never stored in the `quotacap` DB, never returned by the API or MCP, and never logged (errors show `[redacted]`). Revoke at `https://opencode.ai/auth` or `opencode providers logout opencode-go` and run `quotacap providers disable opencode-go` to stop polling. If you prefer no file access, set `OPENCODE_API_KEY` instead.";
|
|
4
|
+
export function showOpenCodeGoBanner(runtime, providers, suppressed) {
|
|
5
|
+
const detected = runtime?.detectedProviders?.includes("opencode-go") ?? false;
|
|
6
|
+
const row = providers.find((p) => p.id === "opencode-go");
|
|
7
|
+
return detected && !row?.enabled && !suppressed;
|
|
8
|
+
}
|
|
9
|
+
export function OpencodeGoBanner({ onEnable }) {
|
|
10
|
+
return (_jsxs("div", { "data-testid": "opencode-go-banner", role: "status", className: "alert", children: [_jsxs("span", { children: [_jsx("strong", { children: "OpenCode Go detected." }), " Enable to track 5h/weekly usage? The API key stays with OpenCode until you enable tracking."] }), _jsx("button", { type: "button", className: "linkbtn", onClick: onEnable, style: { flex: "none", marginLeft: "auto" }, children: "Enable OpenCode Go" })] }));
|
|
11
|
+
}
|
|
12
|
+
export function ConsentModal({ onConfirm, onCancel, busy, }) {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const onKey = (e) => {
|
|
15
|
+
if (e.key === "Escape")
|
|
16
|
+
onCancel();
|
|
17
|
+
};
|
|
18
|
+
window.addEventListener("keydown", onKey);
|
|
19
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
20
|
+
}, [onCancel]);
|
|
21
|
+
return (_jsx("div", { role: "presentation", style: {
|
|
22
|
+
position: "fixed",
|
|
23
|
+
inset: 0,
|
|
24
|
+
display: "grid",
|
|
25
|
+
placeItems: "center",
|
|
26
|
+
background: "var(--scrim, rgba(0,0,0,0.5))",
|
|
27
|
+
zIndex: 60,
|
|
28
|
+
}, children: _jsxs("div", { role: "dialog", "aria-modal": "true", "aria-labelledby": "opencode-go-consent-title", style: {
|
|
29
|
+
background: "var(--surface)",
|
|
30
|
+
border: "1px solid var(--line)",
|
|
31
|
+
borderRadius: "var(--r-md)",
|
|
32
|
+
padding: "var(--s4) var(--s5)",
|
|
33
|
+
maxWidth: 560,
|
|
34
|
+
margin: "var(--s4)",
|
|
35
|
+
}, children: [_jsx("h2", { id: "opencode-go-consent-title", style: { fontSize: "var(--t-4)", margin: "0 0 var(--s3)" }, children: "Enable OpenCode Go?" }), _jsx("p", { style: { fontSize: "var(--t-2)", color: "var(--ink-soft)", margin: "0 0 var(--s4)" }, children: OPENCODE_GO_CONSENT_COPY }), _jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: "var(--s2)" }, children: [_jsx("button", { type: "button", className: "btn btn-quiet", onClick: onCancel, disabled: busy, children: "Cancel" }), _jsx("button", { type: "button", className: "btn btn-primary", onClick: onConfirm, disabled: busy, children: busy ? "Enabling…" : "Enable" })] })] }) }));
|
|
36
|
+
}
|
|
@@ -109,6 +109,9 @@ export function paceBasis(provider, asOfMs) {
|
|
|
109
109
|
}
|
|
110
110
|
export function credentialLabel(provider) {
|
|
111
111
|
const source = provider.quota?.source;
|
|
112
|
+
if (provider.id === "opencode-go" && source === "api") {
|
|
113
|
+
return "read in-memory from OpenCode auth (opt-in), never stored";
|
|
114
|
+
}
|
|
112
115
|
if (source === "cli" || source === "tui")
|
|
113
116
|
return `handled by ${provider.displayName}`;
|
|
114
117
|
return "not stored by QuotaCap";
|
|
@@ -7,8 +7,9 @@ export declare function connectionBadge(provider: ProviderView): string;
|
|
|
7
7
|
export type SettingsStatus = "Ready" | "Stale" | "Failed" | "Invalid" | "Reset passed" | "Not reporting";
|
|
8
8
|
/** Short status from server exclusion and attempt fields. */
|
|
9
9
|
export declare function providerStatus(provider: ProviderView): SettingsStatus;
|
|
10
|
-
export declare function ProvidersPanel({ providers, onRefresh, refreshing, }: {
|
|
10
|
+
export declare function ProvidersPanel({ providers, onRefresh, refreshing, onSetProviderEnabled, }: {
|
|
11
11
|
providers: ProviderView[];
|
|
12
12
|
onRefresh: () => void;
|
|
13
13
|
refreshing: boolean;
|
|
14
|
+
onSetProviderEnabled?: (id: string, enabled: boolean) => void;
|
|
14
15
|
}): React.JSX.Element;
|
|
@@ -12,6 +12,8 @@ const CONNECTION_NAMES = new Map([
|
|
|
12
12
|
["grok", "Grok"],
|
|
13
13
|
["agy", "Antigravity"],
|
|
14
14
|
["agy:3p", "Antigravity 3P"],
|
|
15
|
+
["muse", "Muse Code"],
|
|
16
|
+
["opencode-go", "OpenCode Go"],
|
|
15
17
|
]);
|
|
16
18
|
export function connectionName(id) {
|
|
17
19
|
return CONNECTION_NAMES.get(id) ?? id;
|
|
@@ -38,7 +40,7 @@ export function providerStatus(provider) {
|
|
|
38
40
|
return provider.lastAttempt && !provider.lastAttempt.success ? "Failed" : "Ready";
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
|
-
export function ProvidersPanel({ providers, onRefresh, refreshing, }) {
|
|
43
|
+
export function ProvidersPanel({ providers, onRefresh, refreshing, onSetProviderEnabled, }) {
|
|
42
44
|
const listed = adapterProviders(providers);
|
|
43
45
|
return (_jsxs("div", { children: [_jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "var(--s3)" }, children: [_jsx("h3", { style: { fontSize: "var(--t-3)", fontWeight: 700, margin: 0 }, children: "Active Tool Connections" }), _jsx("button", { type: "button", className: "btn btn-quiet", onClick: onRefresh, disabled: refreshing, style: { fontSize: "var(--t-1)", padding: "4px 8px" }, children: refreshing ? "Polling…" : "Re-poll adapters" })] }), _jsx("div", { style: {
|
|
44
46
|
border: "1px solid var(--line)",
|
|
@@ -55,14 +57,14 @@ export function ProvidersPanel({ providers, onRefresh, refreshing, }) {
|
|
|
55
57
|
display: "flex",
|
|
56
58
|
justifyContent: "space-between",
|
|
57
59
|
alignItems: "center",
|
|
58
|
-
}, children: [_jsxs("div", { children: [_jsx("b", { style: { fontSize: "var(--t-3)" }, children: connectionName(p.id) }), _jsx("span", { style: { display: "block", fontSize: "var(--t-1)", color: "var(--ink-soft)" }, children: sourceLabel(p.quota?.source) })] }), _jsx("span", { className: isReady ? "st-ready" : "st-todo", style: {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
}, children: [_jsxs("div", { children: [_jsx("b", { style: { fontSize: "var(--t-3)" }, children: connectionName(p.id) }), _jsx("span", { style: { display: "block", fontSize: "var(--t-1)", color: "var(--ink-soft)" }, children: sourceLabel(p.quota?.source) })] }), _jsxs("div", { style: { display: "flex", alignItems: "center", flex: "none" }, children: [_jsx("span", { className: isReady ? "st-ready" : "st-todo", style: {
|
|
61
|
+
fontSize: "var(--t-1)",
|
|
62
|
+
padding: "2px 8px",
|
|
63
|
+
borderRadius: 999,
|
|
64
|
+
background: isReady ? "var(--good-soft)" : "var(--warn-soft)",
|
|
65
|
+
border: `1px solid ${isReady ? "var(--good)" : "var(--warn)"}`,
|
|
66
|
+
color: isReady ? "var(--good)" : "var(--warn)",
|
|
67
|
+
fontWeight: 600,
|
|
68
|
+
}, children: connectionBadge(p) }), onSetProviderEnabled && p.id !== "agy:3p" && (_jsx("button", { type: "button", className: "btn btn-quiet", "aria-label": `${p.enabled ? "Disable" : "Enable"} ${connectionName(p.id)}`, onClick: () => onSetProviderEnabled(p.id, !p.enabled), disabled: refreshing, style: { fontSize: "var(--t-1)", padding: "4px 8px", flex: "none", marginLeft: "8px" }, children: p.enabled ? "Disable" : "Enable" }))] })] }, p.id));
|
|
67
69
|
}) })] }));
|
|
68
70
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { ProviderView } from "../../state.js";
|
|
3
|
-
export declare function SettingsDrawer({ open, providers, onRefresh, refreshing, onClose, }: {
|
|
3
|
+
export declare function SettingsDrawer({ open, providers, onRefresh, refreshing, onSetProviderEnabled, onClose, }: {
|
|
4
4
|
open: boolean;
|
|
5
5
|
providers: ProviderView[];
|
|
6
6
|
onRefresh: () => void;
|
|
7
7
|
refreshing: boolean;
|
|
8
|
+
onSetProviderEnabled?: (id: string, enabled: boolean) => void;
|
|
8
9
|
onClose: () => void;
|
|
9
10
|
}): React.JSX.Element | null;
|
|
@@ -8,7 +8,7 @@ const TABS = [
|
|
|
8
8
|
{ id: "cli", label: "CLI & Shell" },
|
|
9
9
|
{ id: "daemon", label: "Daemon" },
|
|
10
10
|
];
|
|
11
|
-
export function SettingsDrawer({ open, providers, onRefresh, refreshing, onClose, }) {
|
|
11
|
+
export function SettingsDrawer({ open, providers, onRefresh, refreshing, onSetProviderEnabled, onClose, }) {
|
|
12
12
|
const [tab, setTab] = useState("providers");
|
|
13
13
|
const dialogRef = useRef(null);
|
|
14
14
|
const previouslyFocused = useRef(null);
|
|
@@ -51,5 +51,5 @@ export function SettingsDrawer({ open, providers, onRefresh, refreshing, onClose
|
|
|
51
51
|
if (!open)
|
|
52
52
|
return null;
|
|
53
53
|
const titleId = "settings-drawer-title";
|
|
54
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { "data-testid": "settings-scrim", className: "scrim is-open", "aria-hidden": "true", onClick: onClose }), _jsxs("aside", { ref: dialogRef, tabIndex: -1, "data-testid": "settings-drawer", role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, className: "drawer is-open", style: { width: "min(580px, 100vw)" }, onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "drawer-head", children: [_jsxs("div", { style: { flex: 1 }, children: [_jsx("h2", { id: titleId, style: { margin: "0 0 2px" }, children: "Settings & Configuration" }), _jsx("span", { className: "sub", children: "Connected provider adapters and CLI prompt integration." })] }), _jsx("button", { className: "drawer-close", type: "button", "aria-label": "Close settings", 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("div", { className: "seg", role: "tablist", "aria-label": "Settings sections", style: { margin: "var(--s4) var(--s5) 0", display: "flex", height: 32 }, children: TABS.map((t) => (_jsx("button", { type: "button", role: "tab", className: `settings-tab${tab === t.id ? " is-active" : ""}`, "aria-selected": tab === t.id, onClick: () => setTab(t.id), style: { flex: 1, justifyContent: "center" }, children: t.label }, t.id))) }), _jsxs("div", { role: "tabpanel", style: { padding: "var(--s4) var(--s5) var(--s6)", overflowY: "auto" }, children: [tab === "providers" && (_jsx(ProvidersPanel, { providers: providers, onRefresh: onRefresh, refreshing: refreshing })), tab === "cli" && _jsx(CliIntegrationPanel, {}), tab === "daemon" && _jsx(DaemonPanel, {})] })] })] }));
|
|
54
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { "data-testid": "settings-scrim", className: "scrim is-open", "aria-hidden": "true", onClick: onClose }), _jsxs("aside", { ref: dialogRef, tabIndex: -1, "data-testid": "settings-drawer", role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, className: "drawer is-open", style: { width: "min(580px, 100vw)" }, onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "drawer-head", children: [_jsxs("div", { style: { flex: 1 }, children: [_jsx("h2", { id: titleId, style: { margin: "0 0 2px" }, children: "Settings & Configuration" }), _jsx("span", { className: "sub", children: "Connected provider adapters and CLI prompt integration." })] }), _jsx("button", { className: "drawer-close", type: "button", "aria-label": "Close settings", 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("div", { className: "seg", role: "tablist", "aria-label": "Settings sections", style: { margin: "var(--s4) var(--s5) 0", display: "flex", height: 32 }, children: TABS.map((t) => (_jsx("button", { type: "button", role: "tab", className: `settings-tab${tab === t.id ? " is-active" : ""}`, "aria-selected": tab === t.id, onClick: () => setTab(t.id), style: { flex: 1, justifyContent: "center" }, children: t.label }, t.id))) }), _jsxs("div", { role: "tabpanel", style: { padding: "var(--s4) var(--s5) var(--s6)", overflowY: "auto" }, children: [tab === "providers" && (_jsx(ProvidersPanel, { providers: providers, onRefresh: onRefresh, refreshing: refreshing, onSetProviderEnabled: onSetProviderEnabled })), tab === "cli" && _jsx(CliIntegrationPanel, {}), tab === "daemon" && _jsx(DaemonPanel, {})] })] })] }));
|
|
55
55
|
}
|
|
@@ -17,7 +17,7 @@ export function readiness(provider) {
|
|
|
17
17
|
if (provider.lastAttempt?.success && provider.lastAttempt.succeededAt) {
|
|
18
18
|
return { status: "Ready", detail: "Connected — no readings yet." };
|
|
19
19
|
}
|
|
20
|
-
const credentialNote = "QuotaCap never
|
|
20
|
+
const credentialNote = "By default QuotaCap never reads credentials; the only exception is OpenCode Go, which you enable explicitly.";
|
|
21
21
|
if (provider.harness === null) {
|
|
22
22
|
return {
|
|
23
23
|
status: "Sign in first",
|
|
@@ -91,5 +91,5 @@ export function Onboarding({ snapshot, onFirstPoll, polling, pollError, }) {
|
|
|
91
91
|
letterSpacing: "-0.024em",
|
|
92
92
|
margin: "0 0 var(--s5)",
|
|
93
93
|
fontWeight: 700,
|
|
94
|
-
}, children: "Connect the subscriptions you already use" }), _jsxs("div", { className: "setup", children: [_jsxs("ol", { className: "steps", children: [_jsxs("li", { className: step === 1 ? "is-now" : "", "aria-current": step === 1 ? "step" : undefined, children: [_jsx("span", { className: "num", children: "1" }), _jsxs("span", { children: [_jsx("span", { className: "t", children: "Detect" }), _jsx("span", { className: "d", children: "Find installed coding tools" })] })] }), _jsxs("li", { className: step === 2 ? "is-now" : "", "aria-current": step === 2 ? "step" : undefined, children: [_jsx("span", { className: "num", children: "2" }), _jsxs("span", { children: [_jsx("span", { className: "t", children: "Review" }), _jsx("span", { className: "d", children: "Check what each adapter reads" })] })] }), _jsxs("li", { className: step === 3 ? "is-now" : "", "aria-current": step === 3 ? "step" : undefined, children: [_jsx("span", { className: "num", children: "3" }), _jsxs("span", { children: [_jsx("span", { className: "t", children: "Start" }), _jsx("span", { className: "d", children: "Read current usage once" })] })] })] }), _jsxs("div", { className: "panel", children: [step === 1 && (_jsxs("section", { "data-testid": "setup-step-1", "aria-label": "Step 1: Detect", children: [_jsxs("div", { className: "panel-head", children: [_jsx("p", { className: "eyebrow", children: "Step 1 of 3" }), _jsx("h2", { children: "What the daemon can see" }), _jsx("p", { children: "QuotaCap asked the running daemon what it knows. The dashboard cannot scan this machine from the browser, so anything missing here needs the provider CLI installed and signed in first." }), _jsxs("div", { className: "privacy", children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "var(--good)", strokeWidth: "2.2", style: { flex: "none" }, "aria-hidden": "true", children: [_jsx("path", { d: "M12 3l7 3v5c0 5-3 8-7 10-4-2-7-5-7-10V6z" }), _jsx("path", { d: "M9 12l2 2 4-4" })] }), _jsxs("div", { children: [_jsx("b", { children: "Usage history stays on this machine" }), _jsx("span", { children: "QuotaCap reads quota percentages and reset times only. It never accesses code
|
|
94
|
+
}, children: "Connect the subscriptions you already use" }), _jsxs("div", { className: "setup", children: [_jsxs("ol", { className: "steps", children: [_jsxs("li", { className: step === 1 ? "is-now" : "", "aria-current": step === 1 ? "step" : undefined, children: [_jsx("span", { className: "num", children: "1" }), _jsxs("span", { children: [_jsx("span", { className: "t", children: "Detect" }), _jsx("span", { className: "d", children: "Find installed coding tools" })] })] }), _jsxs("li", { className: step === 2 ? "is-now" : "", "aria-current": step === 2 ? "step" : undefined, children: [_jsx("span", { className: "num", children: "2" }), _jsxs("span", { children: [_jsx("span", { className: "t", children: "Review" }), _jsx("span", { className: "d", children: "Check what each adapter reads" })] })] }), _jsxs("li", { className: step === 3 ? "is-now" : "", "aria-current": step === 3 ? "step" : undefined, children: [_jsx("span", { className: "num", children: "3" }), _jsxs("span", { children: [_jsx("span", { className: "t", children: "Start" }), _jsx("span", { className: "d", children: "Read current usage once" })] })] })] }), _jsxs("div", { className: "panel", children: [step === 1 && (_jsxs("section", { "data-testid": "setup-step-1", "aria-label": "Step 1: Detect", children: [_jsxs("div", { className: "panel-head", children: [_jsx("p", { className: "eyebrow", children: "Step 1 of 3" }), _jsx("h2", { children: "What the daemon can see" }), _jsx("p", { children: "QuotaCap asked the running daemon what it knows. The dashboard cannot scan this machine from the browser, so anything missing here needs the provider CLI installed and signed in first." }), _jsxs("div", { className: "privacy", children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "var(--good)", strokeWidth: "2.2", style: { flex: "none" }, "aria-hidden": "true", children: [_jsx("path", { d: "M12 3l7 3v5c0 5-3 8-7 10-4-2-7-5-7-10V6z" }), _jsx("path", { d: "M9 12l2 2 4-4" })] }), _jsxs("div", { children: [_jsx("b", { children: "Usage history stays on this machine" }), _jsx("span", { children: "QuotaCap reads quota percentages and reset times only. It never accesses code or prompts." })] })] })] }), _jsx(ProviderReadinessList, { providers: snapshot.providers }), _jsxs("div", { className: "panel-foot", children: [_jsx("span", { style: { fontSize: "var(--t-2)", color: "var(--ink-soft)" }, children: "You can add, disable, or repair providers at any time." }), _jsx("button", { className: "btn btn-primary", type: "button", onClick: () => setStep(2), children: "Continue to review" })] })] })), step === 2 && (_jsxs("section", { "data-testid": "setup-step-2", "aria-label": "Step 2: Review", children: [_jsxs("div", { className: "panel-head", children: [_jsx("p", { className: "eyebrow", children: "Step 2 of 3" }), _jsx("h2", { children: "Review accounts" }), _jsx("p", { children: "QuotaCap reads usage through your own provider CLIs. It never asks for, stores, or transmits your provider credentials \u2014 sign-in happens in the provider CLI itself, not here." }), _jsxs("div", { className: "privacy", children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "var(--good)", strokeWidth: "2.2", style: { flex: "none" }, "aria-hidden": "true", children: [_jsx("path", { d: "M12 3l7 3v5c0 5-3 8-7 10-4-2-7-5-7-10V6z" }), _jsx("path", { d: "M9 12l2 2 4-4" })] }), _jsxs("div", { children: [_jsx("b", { children: "Zero credential storage" }), _jsx("span", { children: "Authentication stays in official CLI keychains. QuotaCap never stores tokens, and reads one only if you explicitly enable OpenCode Go." })] })] })] }), _jsx(ProviderReadinessList, { providers: snapshot.providers }), _jsxs("div", { className: "panel-foot", children: [_jsx("button", { className: "btn btn-quiet", type: "button", onClick: () => setStep(1), children: "Back" }), _jsx("button", { className: "btn btn-primary", type: "button", onClick: () => setStep(3), children: "Continue to first poll" })] })] })), step === 3 && (_jsxs("section", { "data-testid": "setup-step-3", "aria-label": "Step 3: Start", children: [_jsxs("div", { className: "panel-head", children: [_jsx("p", { className: "eyebrow", children: "Step 3 of 3" }), _jsx("h2", { children: "Take the first readings" }), _jsx("p", { children: "Run the first poll. The daemon will ask each provider CLI for current usage and the dashboard will open when readings land." })] }), _jsx(SamplePreview, {}), _jsxs("div", { className: "panel-foot", children: [_jsx("button", { className: "btn btn-quiet", type: "button", onClick: () => setStep(2), children: "Back" }), _jsx("button", { "data-testid": "setup-poll-button", className: "btn btn-primary", type: "button", onClick: onFirstPoll, disabled: polling, children: polling ? "Polling…" : "Run first poll" })] }), pollError && (_jsxs("div", { "data-testid": "setup-poll-error", className: "alert", role: "alert", style: { margin: "var(--s4) var(--s5)" }, children: [_jsx("span", { children: pollError }), _jsx("button", { className: "btn btn-quiet btn-sm", type: "button", onClick: onFirstPoll, children: "Retry" })] }))] }))] })] })] }));
|
|
95
95
|
}
|
package/dist/web/src/state.d.ts
CHANGED
|
@@ -103,6 +103,7 @@ export interface RuntimeView {
|
|
|
103
103
|
polling: "idle" | "in-progress" | "cooldown";
|
|
104
104
|
lastCompletedPollAt: string | null;
|
|
105
105
|
version: string;
|
|
106
|
+
detectedProviders?: string[];
|
|
106
107
|
update?: UpdateView;
|
|
107
108
|
}
|
|
108
109
|
export interface StateSnapshot {
|
|
@@ -136,7 +137,8 @@ export declare function displayPlan(plan: string | null | undefined): string | n
|
|
|
136
137
|
/**
|
|
137
138
|
* Short-window display name ("5h Limit"). Codex, Kimi, and Agy report a 5h limit;
|
|
138
139
|
* Claude (current session) and Muse (current window) report an equivalent 5-hour
|
|
139
|
-
* rolling window. Returns null when the provider reports no
|
|
140
|
+
* rolling window, as does OpenCode Go. Returns null when the provider reports no
|
|
141
|
+
* short window (grok, manual).
|
|
140
142
|
*/
|
|
141
143
|
export declare function shortWindowLabel(id: string): string | null;
|
|
142
144
|
/** "read Xm ago" from the last good snapshot's server timestamp. */
|
package/dist/web/src/state.js
CHANGED
|
@@ -24,7 +24,13 @@ export function toViewModel(s) {
|
|
|
24
24
|
: [];
|
|
25
25
|
return {
|
|
26
26
|
asOf: s.asOf,
|
|
27
|
-
runtime: {
|
|
27
|
+
runtime: {
|
|
28
|
+
...s.runtime,
|
|
29
|
+
// A service older than opencode-go support omits this field.
|
|
30
|
+
detectedProviders: Array.isArray(s.runtime?.detectedProviders)
|
|
31
|
+
? [...s.runtime.detectedProviders]
|
|
32
|
+
: [],
|
|
33
|
+
},
|
|
28
34
|
providers,
|
|
29
35
|
recommendation: { ...s.recommendation, advisories },
|
|
30
36
|
};
|
|
@@ -54,7 +60,8 @@ export function displayPlan(plan) {
|
|
|
54
60
|
/**
|
|
55
61
|
* Short-window display name ("5h Limit"). Codex, Kimi, and Agy report a 5h limit;
|
|
56
62
|
* Claude (current session) and Muse (current window) report an equivalent 5-hour
|
|
57
|
-
* rolling window. Returns null when the provider reports no
|
|
63
|
+
* rolling window, as does OpenCode Go. Returns null when the provider reports no
|
|
64
|
+
* short window (grok, manual).
|
|
58
65
|
*/
|
|
59
66
|
export function shortWindowLabel(id) {
|
|
60
67
|
switch (id.split(":")[0]) {
|
|
@@ -63,6 +70,7 @@ export function shortWindowLabel(id) {
|
|
|
63
70
|
case "agy":
|
|
64
71
|
case "claude":
|
|
65
72
|
case "muse":
|
|
73
|
+
case "opencode-go":
|
|
66
74
|
return "5h Limit";
|
|
67
75
|
default:
|
|
68
76
|
return null;
|