quotacap 0.0.36 → 0.0.38
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 +6 -4
- package/dist/adapters/index.js +4 -1
- package/dist/adapters/muse.d.ts +28 -0
- package/dist/adapters/muse.js +124 -33
- 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/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 +16 -3
- 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/config.js
CHANGED
|
@@ -37,6 +37,7 @@ const ConfigSchema = z.object({
|
|
|
37
37
|
enabledProviders: z.array(z.string()).default(["claude", "codex", "kimi", "grok", "agy", "muse"]),
|
|
38
38
|
knownProviders: z.array(z.string()).default(["claude", "codex", "kimi", "grok", "agy", "muse"]),
|
|
39
39
|
providerNames: z.record(z.string(), ProviderDisplayNameSchema).default({}),
|
|
40
|
+
opencodeGoConsentAt: z.string().nullable().default(null),
|
|
40
41
|
// Optional, omitted from defaults and `init` output. Manual ingest stays
|
|
41
42
|
// in-tree but is not a public surface until the product design lands.
|
|
42
43
|
experimentalIngest: z.boolean().optional(),
|
|
@@ -90,7 +91,7 @@ const ServiceConfigSchema = z.object({
|
|
|
90
91
|
enabledProviders: z
|
|
91
92
|
.array(z.string())
|
|
92
93
|
.superRefine((ids, ctx) => {
|
|
93
|
-
const bad = ids.filter((id) => !(id
|
|
94
|
+
const bad = ids.filter((id) => !Object.hasOwn(adapters, id));
|
|
94
95
|
if (bad.length > 0) {
|
|
95
96
|
ctx.addIssue({
|
|
96
97
|
code: z.ZodIssueCode.custom,
|
|
@@ -103,6 +104,7 @@ const ServiceConfigSchema = z.object({
|
|
|
103
104
|
// start. A non-array still fails naming the field.
|
|
104
105
|
knownProviders: z.array(z.string()).default(["claude", "codex", "kimi", "grok", "agy", "muse"]),
|
|
105
106
|
providerNames: z.record(z.string(), ProviderDisplayNameSchema).default({}),
|
|
107
|
+
opencodeGoConsentAt: z.string().nullable().default(null),
|
|
106
108
|
experimentalIngest: z.boolean().optional(),
|
|
107
109
|
});
|
|
108
110
|
export function defaultConfig() {
|
|
@@ -205,6 +207,49 @@ export async function setProviderNameOverride(id, displayName, p) {
|
|
|
205
207
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
206
208
|
await fs.writeFile(file, JSON.stringify(rawObj, null, 2) + "\n");
|
|
207
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Safe mutation of provider enablement in config.json: raw-JSON like
|
|
212
|
+
* setProviderNameOverride (unknown keys preserved). Enabling opencode-go
|
|
213
|
+
* records the consent timestamp; disabling it revokes the consent record.
|
|
214
|
+
*/
|
|
215
|
+
export async function setProviderEnabled(id, enabled, p) {
|
|
216
|
+
const file = getConfigPath(p);
|
|
217
|
+
let rawObj = {};
|
|
218
|
+
try {
|
|
219
|
+
const raw = await fs.readFile(file, "utf8");
|
|
220
|
+
const parsed = JSON.parse(raw);
|
|
221
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
222
|
+
throw new Error(`invalid config: ${file} must contain a JSON object`);
|
|
223
|
+
}
|
|
224
|
+
rawObj = parsed;
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
if (err?.code === "ENOENT") {
|
|
228
|
+
rawObj = {};
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
throw new Error(`cannot update provider enablement: failed to read ${file}: ${err?.message ?? err}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const list = Array.isArray(rawObj.enabledProviders)
|
|
235
|
+
? [...rawObj.enabledProviders]
|
|
236
|
+
: [...defaultConfig().enabledProviders];
|
|
237
|
+
if (enabled) {
|
|
238
|
+
if (!list.includes(id))
|
|
239
|
+
list.push(id);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const at = list.indexOf(id);
|
|
243
|
+
if (at >= 0)
|
|
244
|
+
list.splice(at, 1);
|
|
245
|
+
}
|
|
246
|
+
rawObj.enabledProviders = list;
|
|
247
|
+
if (id === "opencode-go") {
|
|
248
|
+
rawObj.opencodeGoConsentAt = enabled ? new Date().toISOString() : null;
|
|
249
|
+
}
|
|
250
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
251
|
+
await fs.writeFile(file, JSON.stringify(rawObj, null, 2) + "\n");
|
|
252
|
+
}
|
|
208
253
|
export async function resetAllProviderNameOverrides(p) {
|
|
209
254
|
const file = getConfigPath(p);
|
|
210
255
|
let rawObj = {};
|
|
@@ -233,6 +278,9 @@ export async function resetAllProviderNameOverrides(p) {
|
|
|
233
278
|
// reads as new. Never extend this list: seeding from the live registry would
|
|
234
279
|
// mark every adapter known and silently disable auto-enable.
|
|
235
280
|
export const LEGACY_KNOWN_PROVIDERS = ["claude", "codex", "kimi", "grok", "agy"];
|
|
281
|
+
// Opt-in adapters are never auto-enabled: a binary on PATH is not consent.
|
|
282
|
+
// opencode-go polls a stored credential, so only an explicit enable adds it.
|
|
283
|
+
const OPT_IN_ADAPTERS = new Set(["opencode-go"]);
|
|
236
284
|
// PATH lookup kept local: importing the sibling in src/service/macos.ts
|
|
237
285
|
// would create a config<->service import cycle.
|
|
238
286
|
function defaultWhich(bin) {
|
|
@@ -249,7 +297,8 @@ function defaultWhich(bin) {
|
|
|
249
297
|
}
|
|
250
298
|
/**
|
|
251
299
|
* Auto-enable newly shipped adapters. For each registered adapter (minus
|
|
252
|
-
* `manual`, which has no CLI binary
|
|
300
|
+
* `manual`, which has no CLI binary, and minus opt-in adapters, which an
|
|
301
|
+
* explicit enable must consent to) absent from knownProviders, resolve its
|
|
253
302
|
* binary on PATH and append it to both lists when found. An adapter whose
|
|
254
303
|
* binary is missing stays unknown so it is re-checked on the next start; a
|
|
255
304
|
* provider already known but disabled is never re-added. Raw-JSON mutation
|
|
@@ -285,7 +334,7 @@ export async function autoEnableNewProviders(deps) {
|
|
|
285
334
|
const enabled = [...prevEnabled];
|
|
286
335
|
const known = [...prevKnown];
|
|
287
336
|
for (const id of Object.keys(adapters)) {
|
|
288
|
-
if (id === "manual" || known.includes(id))
|
|
337
|
+
if (id === "manual" || OPT_IN_ADAPTERS.has(id) || known.includes(id))
|
|
289
338
|
continue;
|
|
290
339
|
let resolved = null;
|
|
291
340
|
try {
|
|
@@ -22,6 +22,8 @@ export function formatProviderName(provider) {
|
|
|
22
22
|
return "Antigravity (3rd-party)";
|
|
23
23
|
case "muse":
|
|
24
24
|
return "Muse";
|
|
25
|
+
case "opencode-go":
|
|
26
|
+
return "OpenCode Go";
|
|
25
27
|
case "all":
|
|
26
28
|
return "QuotaCap service";
|
|
27
29
|
default:
|
|
@@ -224,6 +226,15 @@ function matchPrecedence(evidence, errorName, errorCode, text) {
|
|
|
224
226
|
if (/\bno weekly bucket\b/i.test(text)) {
|
|
225
227
|
return { code: "parse_error", phrase: "no weekly bucket" };
|
|
226
228
|
}
|
|
229
|
+
if (/\brolling usage not found\b/i.test(text)) {
|
|
230
|
+
return { code: "parse_error", phrase: "rolling usage not found" };
|
|
231
|
+
}
|
|
232
|
+
if (/\bbad rolling pct\b/i.test(text)) {
|
|
233
|
+
return { code: "parse_error", phrase: "bad rolling pct" };
|
|
234
|
+
}
|
|
235
|
+
if (/\busage status not ok\b/i.test(text)) {
|
|
236
|
+
return { code: "parse_error", phrase: "usage status not ok" };
|
|
237
|
+
}
|
|
227
238
|
// Order 8b: service_unavailable — subscription or service currently unavailable
|
|
228
239
|
if (/\b(?:subscription|service|usage)\s+currently\s+unavailable\b/i.test(text) ||
|
|
229
240
|
/\bsubscriptions aren't currently available\b/i.test(text)) {
|
|
@@ -408,7 +419,7 @@ function mapCategory(code) {
|
|
|
408
419
|
return "unknown";
|
|
409
420
|
}
|
|
410
421
|
}
|
|
411
|
-
function getSummaryAndAction(code, providerName) {
|
|
422
|
+
function getSummaryAndAction(code, providerName, provider) {
|
|
412
423
|
switch (code) {
|
|
413
424
|
case "terminal_error":
|
|
414
425
|
return {
|
|
@@ -443,7 +454,9 @@ function getSummaryAndAction(code, providerName) {
|
|
|
443
454
|
case "service_unavailable":
|
|
444
455
|
return {
|
|
445
456
|
summary: `${providerName} usage currently unavailable`,
|
|
446
|
-
action:
|
|
457
|
+
action: provider === "muse"
|
|
458
|
+
? `${providerName} reported that subscription usage is currently unavailable. QuotaCap already retried automatically with a warm-up prompt. If this persists, send a prompt in ${providerName}, or wait for the next scheduled poll.`
|
|
459
|
+
: `${providerName} reported that subscription usage is currently unavailable. Send a prompt in ${providerName} to refresh its session limits, or wait for the next scheduled poll.`,
|
|
447
460
|
};
|
|
448
461
|
case "timeout":
|
|
449
462
|
return {
|
|
@@ -473,7 +486,7 @@ export function classifyFailure(provider, reason) {
|
|
|
473
486
|
}
|
|
474
487
|
const category = mapCategory(diag.diagnosticCode);
|
|
475
488
|
const pName = formatProviderName(provider);
|
|
476
|
-
const { summary, action } = getSummaryAndAction(diag.diagnosticCode, pName);
|
|
489
|
+
const { summary, action } = getSummaryAndAction(diag.diagnosticCode, pName, provider);
|
|
477
490
|
return {
|
|
478
491
|
diagnosticCode: diag.diagnosticCode,
|
|
479
492
|
errorDetail: diag.errorDetail,
|
package/dist/http/server.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface RuntimeContext {
|
|
|
10
10
|
coordinator: Coordinator;
|
|
11
11
|
enabledProviders: string[];
|
|
12
12
|
providerNames?: Record<string, string>;
|
|
13
|
+
/** Detected-but-not-enabled opt-in providers, computed once at start. */
|
|
14
|
+
detectedProviders?: string[];
|
|
13
15
|
version: string;
|
|
14
16
|
exec: string;
|
|
15
17
|
now?: () => Date;
|
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.38";
|
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
|
}
|