@mars-sea/dsh-commandcode-provider 0.10.6 → 0.11.1
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/CHANGELOG.md +37 -0
- package/README.md +8 -4
- package/README.zh-CN.md +8 -4
- package/lib/client.js +2372 -197
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +117 -7
- package/lib/index.js +1820 -505
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -303,6 +303,7 @@ window.__ModuleLoader__.load({
|
|
|
303
303
|
}),
|
|
304
304
|
booleanField$1("filterModelsByPlan"),
|
|
305
305
|
booleanField$1("webSearch"),
|
|
306
|
+
booleanField$1("showSidebarQuota"),
|
|
306
307
|
textField("activeAccount")
|
|
307
308
|
];
|
|
308
309
|
/** Whether two model-id lists are equal as sets (order-insensitive). */
|
|
@@ -455,6 +456,8 @@ window.__ModuleLoader__.load({
|
|
|
455
456
|
streamIdleTimeoutMs: this.field("streamIdleTimeoutMs"),
|
|
456
457
|
filterModelsByPlan: this.field("filterModelsByPlan"),
|
|
457
458
|
webSearch: this.field("webSearch"),
|
|
459
|
+
showSidebarQuota: this.field("showSidebarQuota"),
|
|
460
|
+
sidebarQuota: this.sectionValue("showSidebarQuota") === true,
|
|
458
461
|
activeAccount: this.field("activeAccount"),
|
|
459
462
|
accounts,
|
|
460
463
|
accountsRemoving: [...this.removedRefs],
|
|
@@ -1225,7 +1228,7 @@ window.__ModuleLoader__.load({
|
|
|
1225
1228
|
}
|
|
1226
1229
|
//#endregion
|
|
1227
1230
|
//#region src/client/usage.ts
|
|
1228
|
-
const IDLE = {
|
|
1231
|
+
const IDLE$1 = {
|
|
1229
1232
|
status: "idle",
|
|
1230
1233
|
report: void 0,
|
|
1231
1234
|
error: void 0,
|
|
@@ -1239,7 +1242,7 @@ window.__ModuleLoader__.load({
|
|
|
1239
1242
|
var CommandCodeUsageController = class {
|
|
1240
1243
|
remote;
|
|
1241
1244
|
listeners = /* @__PURE__ */ new Set();
|
|
1242
|
-
current = IDLE;
|
|
1245
|
+
current = IDLE$1;
|
|
1243
1246
|
generation = 0;
|
|
1244
1247
|
inFlight = false;
|
|
1245
1248
|
disposed = false;
|
|
@@ -1341,6 +1344,166 @@ window.__ModuleLoader__.load({
|
|
|
1341
1344
|
if (ms <= 0) return "";
|
|
1342
1345
|
return new Date(ms).toLocaleString();
|
|
1343
1346
|
}
|
|
1347
|
+
/** Host facts drive the usage card, including keys invisible to browser credentials. */
|
|
1348
|
+
function usageCardState(usage) {
|
|
1349
|
+
const accounts = usage.report?.accounts ?? [];
|
|
1350
|
+
return {
|
|
1351
|
+
loading: usage.status === "loading",
|
|
1352
|
+
shouldRefresh: usage.status === "idle",
|
|
1353
|
+
noKey: accounts.length > 0 && !accounts.some((account) => account.configured)
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
//#endregion
|
|
1357
|
+
//#region src/client/prices.ts
|
|
1358
|
+
const IDLE = {
|
|
1359
|
+
status: "idle",
|
|
1360
|
+
table: void 0,
|
|
1361
|
+
error: void 0
|
|
1362
|
+
};
|
|
1363
|
+
const RETRY_TIMER = {
|
|
1364
|
+
set(callback, ms) {
|
|
1365
|
+
const handle = setTimeout(callback, ms);
|
|
1366
|
+
handle.unref?.();
|
|
1367
|
+
return handle;
|
|
1368
|
+
},
|
|
1369
|
+
clear(handle) {
|
|
1370
|
+
clearTimeout(handle);
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
/**
|
|
1374
|
+
* Cache over the `commandcode/prices` Remote, with a bounded transient-retry
|
|
1375
|
+
* budget. Public API mirrors {@link CommandCodeUsageController}: `state()`,
|
|
1376
|
+
* `subscribe`, and `ensure()`; `reload()` additionally drops the cache and
|
|
1377
|
+
* restarts the budget when the Host namespace rebinds.
|
|
1378
|
+
*
|
|
1379
|
+
* Not a one-shot: the last fetch is cached until `reload()`, a transient
|
|
1380
|
+
* failure is retried three times at 1/2/4 s (the delays are bounded, not a
|
|
1381
|
+
* poll), and a MANUAL `ensure()` still fetches after that budget is spent. A
|
|
1382
|
+
* PERMANENT failure — the Host serving no such endpoint — is terminal for the
|
|
1383
|
+
* binding and is only cleared by `reload()`.
|
|
1384
|
+
*/
|
|
1385
|
+
var CommandCodePricesController = class {
|
|
1386
|
+
remote;
|
|
1387
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1388
|
+
current = IDLE;
|
|
1389
|
+
generation = 0;
|
|
1390
|
+
inFlight = false;
|
|
1391
|
+
disposed = false;
|
|
1392
|
+
retryHandle;
|
|
1393
|
+
attempts = 0;
|
|
1394
|
+
permanent = false;
|
|
1395
|
+
timer;
|
|
1396
|
+
constructor(remote, timer = RETRY_TIMER) {
|
|
1397
|
+
this.timer = timer;
|
|
1398
|
+
this.remote = remote;
|
|
1399
|
+
}
|
|
1400
|
+
/** Release every subscription. Idempotent; in-flight results are dropped. */
|
|
1401
|
+
dispose() {
|
|
1402
|
+
this.disposed = true;
|
|
1403
|
+
this.clearRetry();
|
|
1404
|
+
this.listeners.clear();
|
|
1405
|
+
}
|
|
1406
|
+
/** Subscribe to state projections. @returns the disposer. */
|
|
1407
|
+
subscribe(listener) {
|
|
1408
|
+
this.listeners.add(listener);
|
|
1409
|
+
return () => this.listeners.delete(listener);
|
|
1410
|
+
}
|
|
1411
|
+
/** The current state face. */
|
|
1412
|
+
state() {
|
|
1413
|
+
return this.current;
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Fetch the table unless it is already loaded, permanent, or in flight — and
|
|
1417
|
+
* unless a bounded retry already owns the next attempt.
|
|
1418
|
+
*
|
|
1419
|
+
* The only callers are the client entry: `reload()` when the Remote namespace
|
|
1420
|
+
* lands or rebinds, and a manual refresh. Nothing in the composer calls it, so
|
|
1421
|
+
* a mounted readout does not trigger a fetch of its own — it renders whatever
|
|
1422
|
+
* the table's state currently is.
|
|
1423
|
+
*/
|
|
1424
|
+
ensure() {
|
|
1425
|
+
if (this.disposed || this.permanent || this.inFlight || this.current.status === "ready") return;
|
|
1426
|
+
this.clearRetry();
|
|
1427
|
+
const call = this.remote.prices;
|
|
1428
|
+
if (typeof call !== "function") {
|
|
1429
|
+
this.permanent = true;
|
|
1430
|
+
this.publish({
|
|
1431
|
+
status: "error",
|
|
1432
|
+
table: void 0,
|
|
1433
|
+
error: "the Host serves no commandcode/prices endpoint"
|
|
1434
|
+
});
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
const generation = this.generation;
|
|
1438
|
+
this.inFlight = true;
|
|
1439
|
+
this.attempts += 1;
|
|
1440
|
+
this.publish({
|
|
1441
|
+
status: "loading",
|
|
1442
|
+
table: this.current.table,
|
|
1443
|
+
error: void 0
|
|
1444
|
+
});
|
|
1445
|
+
let request;
|
|
1446
|
+
try {
|
|
1447
|
+
request = call.call(this.remote);
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
request = Promise.reject(error);
|
|
1450
|
+
}
|
|
1451
|
+
request.then((result) => {
|
|
1452
|
+
if (this.disposed || generation !== this.generation) return;
|
|
1453
|
+
this.inFlight = false;
|
|
1454
|
+
if (result.ok) {
|
|
1455
|
+
this.publish({
|
|
1456
|
+
status: "ready",
|
|
1457
|
+
table: result.value,
|
|
1458
|
+
error: void 0
|
|
1459
|
+
});
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
this.permanent = result.error.permanent === true;
|
|
1463
|
+
this.publish({
|
|
1464
|
+
status: "error",
|
|
1465
|
+
table: void 0,
|
|
1466
|
+
error: result.error.message
|
|
1467
|
+
});
|
|
1468
|
+
this.scheduleRetry();
|
|
1469
|
+
}, (error) => {
|
|
1470
|
+
if (this.disposed || generation !== this.generation) return;
|
|
1471
|
+
this.inFlight = false;
|
|
1472
|
+
this.publish({
|
|
1473
|
+
status: "error",
|
|
1474
|
+
table: void 0,
|
|
1475
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1476
|
+
});
|
|
1477
|
+
this.scheduleRetry();
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
/** Rebinding a Host invalidates cached prices and restarts the bounded retry budget. */
|
|
1481
|
+
reload() {
|
|
1482
|
+
if (this.disposed) return;
|
|
1483
|
+
this.generation += 1;
|
|
1484
|
+
this.inFlight = false;
|
|
1485
|
+
this.clearRetry();
|
|
1486
|
+
this.permanent = false;
|
|
1487
|
+
this.attempts = 0;
|
|
1488
|
+
this.current = IDLE;
|
|
1489
|
+
this.ensure();
|
|
1490
|
+
}
|
|
1491
|
+
clearRetry() {
|
|
1492
|
+
if (this.retryHandle !== void 0) this.timer.clear(this.retryHandle);
|
|
1493
|
+
this.retryHandle = void 0;
|
|
1494
|
+
}
|
|
1495
|
+
scheduleRetry() {
|
|
1496
|
+
if (this.disposed || this.permanent || this.attempts >= 4) return;
|
|
1497
|
+
this.retryHandle = this.timer.set(() => {
|
|
1498
|
+
this.retryHandle = void 0;
|
|
1499
|
+
this.ensure();
|
|
1500
|
+
}, 1e3 * 2 ** (this.attempts - 1));
|
|
1501
|
+
}
|
|
1502
|
+
publish(next) {
|
|
1503
|
+
this.current = next;
|
|
1504
|
+
for (const listener of this.listeners) listener();
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1344
1507
|
//#endregion
|
|
1345
1508
|
//#region src/client/login.ts
|
|
1346
1509
|
/** How often a live attempt is polled. */
|
|
@@ -1680,7 +1843,16 @@ window.__ModuleLoader__.load({
|
|
|
1680
1843
|
*/
|
|
1681
1844
|
const { reject: reject$1, record: record$1, stringField: stringField$1, numberField, booleanField } = makeBoundaryValidator("commandcode/report result:");
|
|
1682
1845
|
/** Validate one window-limit block (`fiveHour` / `weekly`). */
|
|
1846
|
+
/**
|
|
1847
|
+
* Parse one quota window, or undefined when the frame carries no such window.
|
|
1848
|
+
*
|
|
1849
|
+
* The distinction is load-bearing at this boundary: an ABSENT window means the
|
|
1850
|
+
* billing endpoint reported none (an unlimited plan), while a present block with
|
|
1851
|
+
* `cap: 0` means uncapped spend that was really reported. Collapsing the two
|
|
1852
|
+
* would draw a zeroed quota row for an account that has no such limit.
|
|
1853
|
+
*/
|
|
1683
1854
|
function windowLimit(value, field) {
|
|
1855
|
+
if (value === void 0) return void 0;
|
|
1684
1856
|
const source = record$1(value, field);
|
|
1685
1857
|
return {
|
|
1686
1858
|
used: numberField(source, "used", `${field}.used`),
|
|
@@ -1728,13 +1900,18 @@ window.__ModuleLoader__.load({
|
|
|
1728
1900
|
}
|
|
1729
1901
|
if (source.credits !== void 0) {
|
|
1730
1902
|
const credits = record$1(source.credits, "credits");
|
|
1731
|
-
|
|
1903
|
+
const parsed = {
|
|
1732
1904
|
monthlyCredits: numberField(credits, "monthlyCredits", "credits.monthlyCredits"),
|
|
1733
1905
|
purchasedCredits: numberField(credits, "purchasedCredits", "credits.purchasedCredits"),
|
|
1734
|
-
freeCredits: numberField(credits, "freeCredits", "credits.freeCredits")
|
|
1735
|
-
fiveHour: windowLimit(credits.fiveHour, "credits.fiveHour"),
|
|
1736
|
-
weekly: windowLimit(credits.weekly, "credits.weekly")
|
|
1906
|
+
freeCredits: numberField(credits, "freeCredits", "credits.freeCredits")
|
|
1737
1907
|
};
|
|
1908
|
+
if (credits.monthlyReported !== void 0) parsed.monthlyReported = booleanField(credits, "monthlyReported", "credits.monthlyReported");
|
|
1909
|
+
for (const flag of ["purchasedReported", "freeReported"]) if (credits[flag] !== void 0) parsed[flag] = booleanField(credits, flag, `credits.${flag}`);
|
|
1910
|
+
const fiveHour = windowLimit(credits.fiveHour, "credits.fiveHour");
|
|
1911
|
+
const weekly = windowLimit(credits.weekly, "credits.weekly");
|
|
1912
|
+
if (fiveHour !== void 0) parsed.fiveHour = fiveHour;
|
|
1913
|
+
if (weekly !== void 0) parsed.weekly = weekly;
|
|
1914
|
+
report.credits = parsed;
|
|
1738
1915
|
}
|
|
1739
1916
|
if (source.plan !== void 0) {
|
|
1740
1917
|
const plan = record$1(source.plan, "plan");
|
|
@@ -1803,6 +1980,72 @@ window.__ModuleLoader__.load({
|
|
|
1803
1980
|
package: USAGE_REMOTE_PACKAGE,
|
|
1804
1981
|
descriptors: [makeRemoteDescriptor(MODELS_ENDPOINT, "models", `${USAGE_REMOTE_PACKAGE}#CommandCodeCatalog`, { parse: parseCatalog })]
|
|
1805
1982
|
};
|
|
1983
|
+
/** Canonical `<namespace>/<method>` endpoint of the price-table Remote. */
|
|
1984
|
+
const PRICES_ENDPOINT = "commandcode/prices";
|
|
1985
|
+
/**
|
|
1986
|
+
* The shared read/validate helpers for the price-table endpoint — its own
|
|
1987
|
+
* instance so price boundary errors name `commandcode/prices`.
|
|
1988
|
+
*/
|
|
1989
|
+
const { reject: priceReject, record: priceRecord, stringField: priceString, numberField: priceNumber, booleanField: priceBoolean } = makeBoundaryValidator("commandcode/prices result:");
|
|
1990
|
+
/** Parse one rate block (`rates`, or a model's `peak` override). */
|
|
1991
|
+
function parseRates(source, field) {
|
|
1992
|
+
const rates = {
|
|
1993
|
+
inputCost: priceNumber(source, "inputCost", `${field}.inputCost`),
|
|
1994
|
+
outputCost: priceNumber(source, "outputCost", `${field}.outputCost`),
|
|
1995
|
+
cacheReadCost: priceNumber(source, "cacheReadCost", `${field}.cacheReadCost`)
|
|
1996
|
+
};
|
|
1997
|
+
if (source.cacheWriteCost !== void 0) rates.cacheWriteCost = priceNumber(source, "cacheWriteCost", `${field}.cacheWriteCost`);
|
|
1998
|
+
return rates;
|
|
1999
|
+
}
|
|
2000
|
+
/** Parse one untrusted boundary value into a {@link CommandCodeModelPrice}. */
|
|
2001
|
+
function parseModelPrice(value) {
|
|
2002
|
+
const source = priceRecord(value, "model");
|
|
2003
|
+
const price = {
|
|
2004
|
+
id: priceString(source, "id", "model.id"),
|
|
2005
|
+
slug: priceString(source, "slug", "model.slug"),
|
|
2006
|
+
...parseRates(source, "model")
|
|
2007
|
+
};
|
|
2008
|
+
if (source.peak !== void 0) price.peak = parseRates(priceRecord(source.peak, "model.peak"), "model.peak");
|
|
2009
|
+
if (source.contextTiers !== void 0) {
|
|
2010
|
+
if (!Array.isArray(source.contextTiers) || source.contextTiers.length === 0) priceReject("contextTiers");
|
|
2011
|
+
let previous = 0;
|
|
2012
|
+
price.contextTiers = source.contextTiers.map((value, index, tiers) => {
|
|
2013
|
+
const tier = priceRecord(value, "contextTier");
|
|
2014
|
+
const out = parseRates(tier, "contextTier");
|
|
2015
|
+
if (tier.maxContext !== void 0) {
|
|
2016
|
+
const max = priceNumber(tier, "maxContext", "contextTier.maxContext");
|
|
2017
|
+
if (!Number.isSafeInteger(max) || max <= previous || index === tiers.length - 1) priceReject("contextTier.maxContext");
|
|
2018
|
+
previous = max;
|
|
2019
|
+
out.maxContext = max;
|
|
2020
|
+
} else if (index !== tiers.length - 1) priceReject("contextTier.maxContext");
|
|
2021
|
+
return out;
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
if (source.free !== void 0) price.free = priceBoolean(source, "free", "model.free");
|
|
2025
|
+
return price;
|
|
2026
|
+
}
|
|
2027
|
+
/** Parse the wire result into a {@link CommandCodePriceTable}. */
|
|
2028
|
+
function parsePriceTable(value) {
|
|
2029
|
+
const source = priceRecord(value, "result");
|
|
2030
|
+
const models = source.models;
|
|
2031
|
+
if (!Array.isArray(models)) priceReject("models");
|
|
2032
|
+
const peakHours = source.peakHours;
|
|
2033
|
+
if (!Array.isArray(peakHours)) priceReject("peakHours");
|
|
2034
|
+
return {
|
|
2035
|
+
models: models.map(parseModelPrice),
|
|
2036
|
+
peakHours: peakHours.map((window) => {
|
|
2037
|
+
if (!Array.isArray(window) || window.length !== 2) priceReject("peakHours[]");
|
|
2038
|
+
const [start, end] = window;
|
|
2039
|
+
if (typeof start !== "number" || typeof end !== "number") priceReject("peakHours[]");
|
|
2040
|
+
return [start, end];
|
|
2041
|
+
})
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
/** The Client-face contribution for the price-table endpoint. */
|
|
2045
|
+
const PRICES_REMOTE_CONTRIBUTION = {
|
|
2046
|
+
package: USAGE_REMOTE_PACKAGE,
|
|
2047
|
+
descriptors: [makeRemoteDescriptor(PRICES_ENDPOINT, "prices", `${USAGE_REMOTE_PACKAGE}#CommandCodePriceTable`, { parse: parsePriceTable })]
|
|
2048
|
+
};
|
|
1806
2049
|
//#endregion
|
|
1807
2050
|
//#region src/login-wire.ts
|
|
1808
2051
|
/** The canonical endpoint paths of the three login Remotes. */
|
|
@@ -2051,7 +2294,7 @@ window.__ModuleLoader__.load({
|
|
|
2051
2294
|
}
|
|
2052
2295
|
//#endregion
|
|
2053
2296
|
//#region package.json
|
|
2054
|
-
var version = "0.
|
|
2297
|
+
var version = "0.11.1";
|
|
2055
2298
|
var repository = {
|
|
2056
2299
|
"type": "git",
|
|
2057
2300
|
"url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
|
|
@@ -2251,7 +2494,8 @@ window.__ModuleLoader__.load({
|
|
|
2251
2494
|
"requestTimeoutMs",
|
|
2252
2495
|
"streamIdleTimeoutMs",
|
|
2253
2496
|
"filterModelsByPlan",
|
|
2254
|
-
"webSearch"
|
|
2497
|
+
"webSearch",
|
|
2498
|
+
"showSidebarQuota"
|
|
2255
2499
|
];
|
|
2256
2500
|
/** One labelled field row in the page body. */
|
|
2257
2501
|
function Field({ id, label, hint, state, disabled, numeric, placeholder, onEdit, onReset, t }) {
|
|
@@ -2303,11 +2547,12 @@ window.__ModuleLoader__.load({
|
|
|
2303
2547
|
return t("invalidNumber");
|
|
2304
2548
|
}
|
|
2305
2549
|
/**
|
|
2306
|
-
* The collapsed "Advanced" card: API base, working dir, both timeouts,
|
|
2307
|
-
*
|
|
2308
|
-
*
|
|
2309
|
-
*
|
|
2310
|
-
* visible (a number field's error
|
|
2550
|
+
* The collapsed "Advanced" card: API base, working dir, both timeouts, the
|
|
2551
|
+
* plan filter, the web-search backend and the sidebar quota card live here so
|
|
2552
|
+
* the page leads with the facts a user actually touches. Starts collapsed on
|
|
2553
|
+
* every visit; expands on demand. While collapsed, a badge names the
|
|
2554
|
+
* customized count so a nonzero override stays visible (a number field's error
|
|
2555
|
+
* blocks save and must be reachable).
|
|
2311
2556
|
*/
|
|
2312
2557
|
function AdvancedSection({ state, disabled, t, onEdit, onReset }) {
|
|
2313
2558
|
const [expanded, setExpanded] = (0, react.useState)(false);
|
|
@@ -2414,6 +2659,17 @@ window.__ModuleLoader__.load({
|
|
|
2414
2659
|
onEdit: (text) => onEdit("webSearch", text),
|
|
2415
2660
|
onReset: () => onReset("webSearch"),
|
|
2416
2661
|
t
|
|
2662
|
+
}),
|
|
2663
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleField, {
|
|
2664
|
+
id: "cc-show-sidebar-quota",
|
|
2665
|
+
label: t("showSidebarQuota"),
|
|
2666
|
+
hint: t("showSidebarQuotaHint"),
|
|
2667
|
+
state: state.showSidebarQuota,
|
|
2668
|
+
disabled,
|
|
2669
|
+
defaultChecked: false,
|
|
2670
|
+
onEdit: (text) => onEdit("showSidebarQuota", text),
|
|
2671
|
+
onReset: () => onReset("showSidebarQuota"),
|
|
2672
|
+
t
|
|
2417
2673
|
})
|
|
2418
2674
|
]
|
|
2419
2675
|
}) : null,
|
|
@@ -2554,7 +2810,8 @@ window.__ModuleLoader__.load({
|
|
|
2554
2810
|
});
|
|
2555
2811
|
}
|
|
2556
2812
|
/** One window-limit row: label, used/cap, a fill bar, and the reset time. */
|
|
2557
|
-
function UsageWindow({ label, limit
|
|
2813
|
+
function UsageWindow({ label, limit, t }) {
|
|
2814
|
+
const { used, cap, exceeded, resetAt } = limit;
|
|
2558
2815
|
const ratio = windowRatio(used, cap);
|
|
2559
2816
|
const reset = formatResetAt(resetAt);
|
|
2560
2817
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -2717,29 +2974,29 @@ window.__ModuleLoader__.load({
|
|
|
2717
2974
|
children: [
|
|
2718
2975
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageStat, {
|
|
2719
2976
|
label: t("usageMonthly"),
|
|
2720
|
-
value: formatMoney(credits.monthlyCredits)
|
|
2977
|
+
value: credits.monthlyReported === false ? "—" : formatMoney(credits.monthlyCredits)
|
|
2721
2978
|
}),
|
|
2722
2979
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageStat, {
|
|
2723
2980
|
label: t("usagePurchased"),
|
|
2724
|
-
value: formatMoney(credits.purchasedCredits)
|
|
2981
|
+
value: credits.purchasedReported === false ? "—" : formatMoney(credits.purchasedCredits)
|
|
2725
2982
|
}),
|
|
2726
2983
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageStat, {
|
|
2727
2984
|
label: t("usageFree"),
|
|
2728
|
-
value: formatMoney(credits.freeCredits)
|
|
2985
|
+
value: credits.freeReported === false ? "—" : formatMoney(credits.freeCredits)
|
|
2729
2986
|
})
|
|
2730
2987
|
]
|
|
2731
2988
|
}) : null,
|
|
2732
|
-
credits !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2989
|
+
credits?.fiveHour !== void 0 || credits?.weekly !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2733
2990
|
className: "cc-usageWindows",
|
|
2734
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageWindow, {
|
|
2991
|
+
children: [credits.fiveHour !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageWindow, {
|
|
2735
2992
|
label: t("usageFiveHour"),
|
|
2736
2993
|
limit: credits.fiveHour,
|
|
2737
2994
|
t
|
|
2738
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageWindow, {
|
|
2995
|
+
}) : null, credits.weekly !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageWindow, {
|
|
2739
2996
|
label: t("usageWeekly"),
|
|
2740
2997
|
limit: credits.weekly,
|
|
2741
2998
|
t
|
|
2742
|
-
})]
|
|
2999
|
+
}) : null]
|
|
2743
3000
|
}) : null,
|
|
2744
3001
|
showPeriod || showPartial || fetchedAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2745
3002
|
className: "cc-usageMeta",
|
|
@@ -2800,15 +3057,11 @@ window.__ModuleLoader__.load({
|
|
|
2800
3057
|
* management card are hidden here immediately. Data arrives through the
|
|
2801
3058
|
* `commandcode/report` Remote; the API keys never leave the Host.
|
|
2802
3059
|
*/
|
|
2803
|
-
function UsageCard({ t, usage,
|
|
3060
|
+
function UsageCard({ t, usage, removingIds, removableIds, canManage, onRefresh, onRemoveAccount }) {
|
|
3061
|
+
const { loading, shouldRefresh, noKey } = usageCardState(usage);
|
|
2804
3062
|
(0, react.useEffect)(() => {
|
|
2805
|
-
if (
|
|
2806
|
-
}, [
|
|
2807
|
-
apiKeyConfigured,
|
|
2808
|
-
usage.status,
|
|
2809
|
-
onRefresh
|
|
2810
|
-
]);
|
|
2811
|
-
const loading = usage.status === "loading";
|
|
3063
|
+
if (shouldRefresh) onRefresh();
|
|
3064
|
+
}, [shouldRefresh, onRefresh]);
|
|
2812
3065
|
const report = usage.report;
|
|
2813
3066
|
const [locallyRemoved, setLocallyRemoved] = (0, react.useState)([]);
|
|
2814
3067
|
(0, react.useEffect)(() => {
|
|
@@ -2844,17 +3097,17 @@ window.__ModuleLoader__.load({
|
|
|
2844
3097
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2845
3098
|
type: "button",
|
|
2846
3099
|
className: "cc-usageRefresh",
|
|
2847
|
-
disabled: loading
|
|
3100
|
+
disabled: loading,
|
|
2848
3101
|
onClick: onRefresh,
|
|
2849
3102
|
children: loading ? t("usageRefreshing") : t("usageRefresh")
|
|
2850
3103
|
})
|
|
2851
3104
|
]
|
|
2852
3105
|
}),
|
|
2853
|
-
|
|
3106
|
+
noKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2854
3107
|
className: "cc-usageHint",
|
|
2855
3108
|
children: t("usageNoKey")
|
|
2856
3109
|
}) : null,
|
|
2857
|
-
|
|
3110
|
+
report === void 0 && loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2858
3111
|
className: "cc-usageHint",
|
|
2859
3112
|
children: t("usageLoading")
|
|
2860
3113
|
}) : null,
|
|
@@ -3399,7 +3652,6 @@ window.__ModuleLoader__.load({
|
|
|
3399
3652
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageCard, {
|
|
3400
3653
|
t,
|
|
3401
3654
|
usage,
|
|
3402
|
-
apiKeyConfigured: state.anyAccountConfigured,
|
|
3403
3655
|
removingIds: state.accountsRemoving,
|
|
3404
3656
|
removableIds: state.accounts.map((account) => account.id),
|
|
3405
3657
|
canManage: state.writable,
|
|
@@ -3535,7 +3787,7 @@ window.__ModuleLoader__.load({
|
|
|
3535
3787
|
return null;
|
|
3536
3788
|
}
|
|
3537
3789
|
/** The closed-panel style: the outlet stays mounted as the detection anchor. */
|
|
3538
|
-
const HIDDEN_STYLE = { display: "none" };
|
|
3790
|
+
const HIDDEN_STYLE$1 = { display: "none" };
|
|
3539
3791
|
/**
|
|
3540
3792
|
* Decide the card's posture from one settings snapshot. Pure: the component
|
|
3541
3793
|
* subscribes once and passes the snapshot in, so hook order never depends
|
|
@@ -3655,7 +3907,7 @@ window.__ModuleLoader__.load({
|
|
|
3655
3907
|
ref: rootRef,
|
|
3656
3908
|
className: "cc-providerCard",
|
|
3657
3909
|
"data-cc-models-card": "true",
|
|
3658
|
-
style: editorOpen ? void 0 : HIDDEN_STYLE,
|
|
3910
|
+
style: editorOpen ? void 0 : HIDDEN_STYLE$1,
|
|
3659
3911
|
children: [
|
|
3660
3912
|
editorOpen && mode.kind === "registration" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3661
3913
|
className: "cc-hint",
|
|
@@ -3728,170 +3980,1990 @@ window.__ModuleLoader__.load({
|
|
|
3728
3980
|
});
|
|
3729
3981
|
}
|
|
3730
3982
|
//#endregion
|
|
3731
|
-
//#region src/client/
|
|
3732
|
-
|
|
3983
|
+
//#region src/client/panel-copy.ts
|
|
3984
|
+
/** The panel's literal string table. */
|
|
3985
|
+
const PANEL_COPY = {
|
|
3733
3986
|
nav: "Command Code",
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
modelSearchPlaceholder: "搜索模型…",
|
|
3781
|
-
modelSearchEmpty: "没有匹配的模型。",
|
|
3782
|
-
modelStale: "已下架",
|
|
3783
|
-
visibleModelsTitle: "模型白名单",
|
|
3784
|
-
visibleModelsHint: "勾选要保留的模型,模型选择器就只列出这些;一个都不勾选时则显示全部模型。保存后,下次打开模型选择器生效。",
|
|
3785
|
-
visibleModelsPick: "选择要保留的模型…",
|
|
3786
|
-
visibleModelsCount: "已选 {count} 个模型",
|
|
3787
|
-
visibleModelsShowAll: "显示全部",
|
|
3788
|
-
visibleModelsStaleHint: "有 {count} 个已选模型在目录中找不到了(可能已下架),不影响其他模型;可清理或保留。",
|
|
3789
|
-
visibleModelsCleanStale: "清理失效({count})",
|
|
3790
|
-
overridden: "已覆盖",
|
|
3791
|
-
reset: "重置",
|
|
3792
|
-
invalidNumber: "无效数字",
|
|
3793
|
-
numberTooSmall: "不能小于 1(毫秒)",
|
|
3794
|
-
numberTooLarge: "超出允许上限(2147483647 毫秒)",
|
|
3795
|
-
readOnly: "当前配置为只读。",
|
|
3796
|
-
unsaved: "未保存",
|
|
3797
|
-
save: "保存",
|
|
3798
|
-
saving: "保存中",
|
|
3799
|
-
saved: "已保存 ✓",
|
|
3800
|
-
saveFailed: "保存失败,请重试。",
|
|
3801
|
-
discard: "放弃",
|
|
3802
|
-
cancel: "取消",
|
|
3803
|
-
show: "显示",
|
|
3804
|
-
hide: "隐藏",
|
|
3805
|
-
usageTitle: "账户用量",
|
|
3806
|
-
usageRefresh: "刷新",
|
|
3807
|
-
usageRefreshing: "刷新中…",
|
|
3808
|
-
usageLoading: "正在获取账户用量…",
|
|
3809
|
-
usageNoKey: "配置 API 密钥后,这里会显示账户的用量与额度状态。",
|
|
3810
|
-
usageError: "用量获取失败",
|
|
3811
|
-
usageRequests: "请求",
|
|
3812
|
-
usageFailed: "失败",
|
|
3813
|
-
usageSuccessRate: "成功率",
|
|
3814
|
-
usageCost: "花费",
|
|
3815
|
-
usageTokens: "Token",
|
|
3816
|
-
usageTokensIn: "入",
|
|
3817
|
-
usageTokensOut: "出",
|
|
3818
|
-
usageMonthly: "月额度",
|
|
3819
|
-
usagePurchased: "已购",
|
|
3820
|
-
usageFree: "赠送",
|
|
3821
|
-
usageFiveHour: "5 小时窗口",
|
|
3822
|
-
usageWeekly: "每周窗口",
|
|
3823
|
-
usageExceeded: "已超限",
|
|
3824
|
-
usageReset: "重置于",
|
|
3825
|
-
usagePartial: "部分端点数据不可用",
|
|
3826
|
-
usageKeyClear: "清除已存密钥",
|
|
3827
|
-
usageKeyClearStaged: "将清除(保存后生效)",
|
|
3828
|
-
usageUndoKeyClear: "撤销清除",
|
|
3829
|
-
usageKeyInvalid: "API 密钥无效或已过期",
|
|
3830
|
-
usageKeyInvalidHint: "服务端拒绝了全部请求(401)。请检查该账户配置的密钥,或到 commandcode.ai 控制台重新生成。",
|
|
3831
|
-
usageServiceUnavailable: "Command Code 服务暂时不可用",
|
|
3832
|
-
usageServiceUnavailableHint: "服务端返回了错误(5xx),稍后点击刷新重试。",
|
|
3833
|
-
usageNetworkError: "无法连接 Command Code 服务",
|
|
3834
|
-
usageNetworkHint: "所有请求都没有到达服务端。请检查网络连接或 API 地址设置。",
|
|
3835
|
-
usageUpdated: "更新于",
|
|
3836
|
-
usagePeriodEnd: "账期截止",
|
|
3837
|
-
usageActive: "当前使用",
|
|
3838
|
-
usageCooldown: "限额冷却中",
|
|
3839
|
-
usageInvalidKey: "密钥无效",
|
|
3840
|
-
usageUnconfigured: "该账户尚未配置 API 密钥。",
|
|
3841
|
-
updateAvailable: "可更新",
|
|
3842
|
-
updateHint: "已发布新版本,点击查看发布说明;更新插件后刷新本页,提示会自动消失。",
|
|
3843
|
-
loginTitle: "通过官方登录获取密钥",
|
|
3844
|
-
loginHintIdle: "不想手动创建密钥?点击登录后浏览器会打开 commandcode.ai 授权页,完成后密钥自动写入本机凭据服务,下次请求即生效。",
|
|
3845
|
-
loginButton: "登录 Command Code",
|
|
3846
|
-
loginStarting: "正在启动本地回调服务…",
|
|
3847
|
-
loginWaiting: "等待在浏览器中完成授权…",
|
|
3848
|
-
loginOpenLink: "打开授权页面 ↗",
|
|
3849
|
-
loginCancel: "取消登录",
|
|
3850
|
-
loginSuccess: "已登录为",
|
|
3851
|
-
loginUnavailable: "此环境暂不支持登录流程,请手动粘贴密钥。",
|
|
3852
|
-
loginDenied: "授权被拒绝。可重试,或手动粘贴密钥。",
|
|
3853
|
-
loginTimeout: "等待超时:未在窗口期内收到授权回调,请重试。",
|
|
3854
|
-
loginInvalidKey: "获取到的密钥未通过校验(401),请重试或手动粘贴。",
|
|
3855
|
-
loginNetwork: "无法连接 Command Code 服务校验密钥,请检查网络后重试。",
|
|
3856
|
-
loginStoreFailed: "密钥无法写入本机凭据服务,请手动粘贴。",
|
|
3857
|
-
loginCancelled: "登录已取消。",
|
|
3858
|
-
loginFailedGeneric: "登录失败,请重试或手动粘贴密钥。",
|
|
3859
|
-
cardTitle: "Command Code",
|
|
3860
|
-
cardRouteActive: "已启用",
|
|
3861
|
-
cardLoadingHint: "正在读取 Command Code 配置…",
|
|
3862
|
-
cardRegistrationHint: "此卡片随 Command Code 插件注册,需要较新版本的 DeepSeek Harness 才会显示完整内容。"
|
|
3987
|
+
subtitle: "Plans, credits and quota windows",
|
|
3988
|
+
refresh: "Refresh",
|
|
3989
|
+
refreshing: "Refreshing…",
|
|
3990
|
+
loading: "Loading account usage…",
|
|
3991
|
+
noKey: "No API key configured",
|
|
3992
|
+
noKeyHint: "Paste a key — or sign in — under Settings → Command Code, then refresh.",
|
|
3993
|
+
plan: "Plan",
|
|
3994
|
+
credits: "Credits",
|
|
3995
|
+
limits: "Quota windows",
|
|
3996
|
+
usage: "Usage",
|
|
3997
|
+
monthly: "Monthly",
|
|
3998
|
+
monthlyLimit: "Monthly limit",
|
|
3999
|
+
monthlyUsed: "Monthly used",
|
|
4000
|
+
remaining: "Remaining",
|
|
4001
|
+
purchased: "Purchased",
|
|
4002
|
+
free: "Free",
|
|
4003
|
+
fiveHour: "5-hour window",
|
|
4004
|
+
weekly: "Weekly window",
|
|
4005
|
+
fiveHourShort: "5-hour",
|
|
4006
|
+
weeklyShort: "Weekly",
|
|
4007
|
+
windowUnlimited: "unlimited",
|
|
4008
|
+
exceeded: "Exceeded",
|
|
4009
|
+
exhausted: "Used up",
|
|
4010
|
+
resets: "Resets",
|
|
4011
|
+
requests: "Requests",
|
|
4012
|
+
failed: "failed",
|
|
4013
|
+
successRate: "Success rate",
|
|
4014
|
+
spend: "Spend",
|
|
4015
|
+
tokens: "Tokens",
|
|
4016
|
+
tokensIn: "in",
|
|
4017
|
+
tokensOut: "out",
|
|
4018
|
+
periodEnds: "Period ends",
|
|
4019
|
+
updated: "Updated",
|
|
4020
|
+
partial: "Some endpoint data unavailable",
|
|
4021
|
+
active: "Active",
|
|
4022
|
+
coolingDown: "Cooling down",
|
|
4023
|
+
invalidKey: "Invalid key",
|
|
4024
|
+
unconfigured: "Not configured",
|
|
4025
|
+
unavailable: "No data",
|
|
4026
|
+
errorInvalidKey: "API key invalid or expired",
|
|
4027
|
+
errorInvalidKeyHint: "The server rejected every request (401). Check the key for this account, or generate a new one in the commandcode.ai console.",
|
|
4028
|
+
errorServiceUnavailable: "The Command Code service is temporarily unavailable",
|
|
4029
|
+
errorServiceUnavailableHint: "The server returned errors (5xx). Try Refresh again in a moment.",
|
|
4030
|
+
errorNetwork: "Could not reach the Command Code service",
|
|
4031
|
+
errorNetworkHint: "No request reached the server. Check your network connection or the API base setting.",
|
|
4032
|
+
errorGeneric: "Could not fetch account usage"
|
|
3863
4033
|
};
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
4034
|
+
/** Look up one panel string. The fallback key keeps a bad call visible, never blank. */
|
|
4035
|
+
function panelText(key) {
|
|
4036
|
+
return PANEL_COPY[key] ?? key;
|
|
4037
|
+
}
|
|
4038
|
+
//#endregion
|
|
4039
|
+
//#region src/client/panel.ts
|
|
4040
|
+
/** `$1.23`, and `$0.0123` only when the amount is too small for cents to show it. */
|
|
4041
|
+
function money(value) {
|
|
4042
|
+
if (value === 0) return formatMoney(0);
|
|
4043
|
+
return Math.abs(value) < .01 ? formatMoneyExact(value) : formatMoney(value);
|
|
4044
|
+
}
|
|
4045
|
+
/**
|
|
4046
|
+
* What a figure shows when the endpoint that carries it did not report one.
|
|
4047
|
+
* A dash, never a zero: the panel exists to state what the account has spent,
|
|
4048
|
+
* so "we were not told" has to look different from "nothing".
|
|
4049
|
+
*/
|
|
4050
|
+
const UNREPORTED = "—";
|
|
4051
|
+
/** Local reset time; empty when the endpoint reported none. */
|
|
4052
|
+
function resetText(ms) {
|
|
4053
|
+
if (ms <= 0) return "";
|
|
4054
|
+
return new Date(ms).toLocaleString();
|
|
4055
|
+
}
|
|
4056
|
+
/** Local short date; empty when unset. */
|
|
4057
|
+
function dateText(ms) {
|
|
4058
|
+
if (ms <= 0) return "";
|
|
4059
|
+
return new Date(ms).toLocaleDateString();
|
|
4060
|
+
}
|
|
4061
|
+
/** Local time-of-day; empty when unset. */
|
|
4062
|
+
function timeText(ms) {
|
|
4063
|
+
if (ms <= 0) return "";
|
|
4064
|
+
return new Date(ms).toLocaleTimeString();
|
|
4065
|
+
}
|
|
4066
|
+
/**
|
|
4067
|
+
* Consumption as a percentage, NOT clamped at 100 — an over-quota window
|
|
4068
|
+
* reports its real overshoot (150%), which is what the printed figure should
|
|
4069
|
+
* say. Bar widths clamp separately via {@link barPercent}.
|
|
4070
|
+
*/
|
|
4071
|
+
function rawPercent(used, cap) {
|
|
4072
|
+
if (cap <= 0) return 0;
|
|
4073
|
+
return Math.round(used / cap * 100);
|
|
4074
|
+
}
|
|
4075
|
+
/** A percentage usable as a CSS width, clamped into [0, 100]. */
|
|
4076
|
+
function barPercent(percent) {
|
|
4077
|
+
return Math.min(100, Math.max(0, percent));
|
|
4078
|
+
}
|
|
4079
|
+
/** Build one quota-window view. */
|
|
4080
|
+
function windowView(label, limit) {
|
|
4081
|
+
const percent = rawPercent(limit.used, limit.cap);
|
|
4082
|
+
const capped = limit.cap > 0;
|
|
4083
|
+
return {
|
|
4084
|
+
label,
|
|
4085
|
+
value: capped ? `${money(limit.used)} / ${money(limit.cap)}` : money(limit.used),
|
|
4086
|
+
capped,
|
|
4087
|
+
percent,
|
|
4088
|
+
barPercent: barPercent(percent),
|
|
4089
|
+
exceeded: limit.exceeded,
|
|
4090
|
+
resetsAt: resetText(limit.resetAt)
|
|
4091
|
+
};
|
|
4092
|
+
}
|
|
4093
|
+
/**
|
|
4094
|
+
* Build the monthly credit view from the two endpoints that carry it.
|
|
4095
|
+
*
|
|
4096
|
+
* The limit is the PLAN's credit total, not the billing endpoint's
|
|
4097
|
+
* `monthlyCredits` — that field is a remaining balance (see
|
|
4098
|
+
* {@link PanelMonthlyView}). Unknown plans (no `monthlyCredits` on the plan
|
|
4099
|
+
* record) therefore still show their balances, just without a ratio.
|
|
4100
|
+
*
|
|
4101
|
+
* The trap this guards: a MISSING balance is not a consumed one. The billing
|
|
4102
|
+
* endpoint is one of four the report fetches in parallel, so it fails on its
|
|
4103
|
+
* own while the plan still arrives — and reading the absent balance as 0 would
|
|
4104
|
+
* turn that partial failure into `limit - 0 = limit`, i.e. a confident "100%
|
|
4105
|
+
* used, quota exhausted" for an account that may have spent nothing. The
|
|
4106
|
+
* official CLI draws no meter unless the credits payload is present at all
|
|
4107
|
+
* (`hasCreditsInfo`), so "unreported" is the upstream-faithful reading here
|
|
4108
|
+
* too. Every derived figure stays a placeholder until the balance reported.
|
|
4109
|
+
*/
|
|
4110
|
+
function monthlyView(report) {
|
|
4111
|
+
const credits = report.credits;
|
|
4112
|
+
const plan = report.plan;
|
|
4113
|
+
if (credits === void 0 && plan === void 0) return void 0;
|
|
4114
|
+
const limitValue = plan?.monthlyCredits ?? null;
|
|
4115
|
+
const remainingReported = credits !== void 0 && credits.monthlyReported !== false;
|
|
4116
|
+
const remaining = remainingReported ? Math.max(0, credits.monthlyCredits) : 0;
|
|
4117
|
+
const known = limitValue !== null && limitValue > 0 && remainingReported;
|
|
4118
|
+
const used = known ? Math.max(0, limitValue - remaining) : 0;
|
|
4119
|
+
const percent = known ? rawPercent(used, limitValue) : 0;
|
|
4120
|
+
return {
|
|
4121
|
+
known,
|
|
4122
|
+
limit: limitValue !== null ? money(limitValue) : UNREPORTED,
|
|
4123
|
+
used: known ? money(used) : UNREPORTED,
|
|
4124
|
+
remaining: remainingReported ? money(remaining) : UNREPORTED,
|
|
4125
|
+
purchased: credits !== void 0 && credits.purchasedReported !== false ? money(Math.max(0, credits.purchasedCredits)) : UNREPORTED,
|
|
4126
|
+
free: credits !== void 0 && credits.freeReported !== false ? money(Math.max(0, credits.freeCredits)) : UNREPORTED,
|
|
4127
|
+
percent,
|
|
4128
|
+
barPercent: barPercent(percent),
|
|
4129
|
+
exhausted: known && remaining <= 0,
|
|
4130
|
+
periodEnds: dateText(plan?.currentPeriodEnd ?? 0)
|
|
4131
|
+
};
|
|
4132
|
+
}
|
|
4133
|
+
/** Build one account's view. */
|
|
4134
|
+
function accountView(entry) {
|
|
4135
|
+
const { report } = entry;
|
|
4136
|
+
const account = report.account;
|
|
4137
|
+
const plan = report.plan;
|
|
4138
|
+
const credits = report.credits;
|
|
4139
|
+
const usage = report.usage;
|
|
4140
|
+
const stats = [];
|
|
4141
|
+
if (usage !== void 0) {
|
|
4142
|
+
stats.push({
|
|
4143
|
+
label: "requests",
|
|
4144
|
+
value: String(usage.completedCount),
|
|
4145
|
+
sub: `${usage.failedCount} ${panelText("failed")}`
|
|
4146
|
+
});
|
|
4147
|
+
stats.push({
|
|
4148
|
+
label: "successRate",
|
|
4149
|
+
value: `${formatSuccessRate(usage.successRate)}%`,
|
|
4150
|
+
sub: ""
|
|
4151
|
+
});
|
|
4152
|
+
stats.push({
|
|
4153
|
+
label: "spend",
|
|
4154
|
+
value: formatMoneyExact(usage.totalCost),
|
|
4155
|
+
sub: `${formatMoney(usage.totalCredits)} credits`
|
|
4156
|
+
});
|
|
4157
|
+
stats.push({
|
|
4158
|
+
label: "tokens",
|
|
4159
|
+
value: formatTokensCompact(usage.totalTokensIn + usage.totalTokensOut),
|
|
4160
|
+
sub: `${formatTokensCompact(usage.totalTokensIn)} ${panelText("tokensIn")} / ${formatTokensCompact(usage.totalTokensOut)} ${panelText("tokensOut")}`
|
|
4161
|
+
});
|
|
4162
|
+
}
|
|
4163
|
+
const windows = [];
|
|
4164
|
+
if (credits?.fiveHour !== void 0) windows.push(windowView("fiveHour", credits.fiveHour));
|
|
4165
|
+
if (credits?.weekly !== void 0) windows.push(windowView("weekly", credits.weekly));
|
|
4166
|
+
let mark;
|
|
4167
|
+
if (entry.active) mark = "active";
|
|
4168
|
+
else if (entry.mark === "invalid-credential") mark = "invalidKey";
|
|
4169
|
+
else if (entry.cooldownUntil > 0 || entry.mark === "rate-limit") mark = "coolingDown";
|
|
4170
|
+
return {
|
|
4171
|
+
id: entry.id,
|
|
4172
|
+
label: entry.label,
|
|
4173
|
+
owner: account === void 0 ? "" : account.userName || account.name,
|
|
4174
|
+
planName: plan?.name ?? "",
|
|
4175
|
+
planStatus: plan !== void 0 && plan.status !== "" && plan.status !== "active" ? plan.status : "",
|
|
4176
|
+
mark,
|
|
4177
|
+
cooldownUntil: entry.cooldownUntil > 0 ? resetText(entry.cooldownUntil) : "",
|
|
4178
|
+
unconfigured: !entry.configured,
|
|
4179
|
+
periodEnds: dateText(plan?.currentPeriodEnd ?? 0),
|
|
4180
|
+
active: entry.active,
|
|
4181
|
+
monthly: monthlyView(report),
|
|
4182
|
+
windows,
|
|
4183
|
+
stats
|
|
4184
|
+
};
|
|
4185
|
+
}
|
|
4186
|
+
/** The report-level failure box, or undefined when the report is usable. */
|
|
4187
|
+
function failureView(state) {
|
|
4188
|
+
const blocked = state.report?.accounts.find((entry) => entry.report.blocked !== void 0)?.report.blocked;
|
|
4189
|
+
if (blocked === "invalid-key") return {
|
|
4190
|
+
title: "errorInvalidKey",
|
|
4191
|
+
hint: "errorInvalidKeyHint",
|
|
4192
|
+
detail: ""
|
|
4193
|
+
};
|
|
4194
|
+
if (blocked === "service-unavailable") return {
|
|
4195
|
+
title: "errorServiceUnavailable",
|
|
4196
|
+
hint: "errorServiceUnavailableHint",
|
|
4197
|
+
detail: ""
|
|
4198
|
+
};
|
|
4199
|
+
if (blocked === "network") return {
|
|
4200
|
+
title: "errorNetwork",
|
|
4201
|
+
hint: "errorNetworkHint",
|
|
4202
|
+
detail: ""
|
|
4203
|
+
};
|
|
4204
|
+
if (state.status === "error" && state.report === void 0) return {
|
|
4205
|
+
title: "errorGeneric",
|
|
4206
|
+
hint: "errorGeneric",
|
|
4207
|
+
detail: state.error ?? ""
|
|
4208
|
+
};
|
|
4209
|
+
}
|
|
4210
|
+
/**
|
|
4211
|
+
* Every panel string, resolved once per projection. One object with all keys
|
|
4212
|
+
* (rather than per-field lookups in the components) keeps the copy table and
|
|
4213
|
+
* the render sites in lockstep: a key cannot be read from `text` unless
|
|
4214
|
+
* {@link PANEL_COPY} declares it.
|
|
4215
|
+
*/
|
|
4216
|
+
function panelStrings() {
|
|
4217
|
+
const keys = Object.keys(PANEL_COPY);
|
|
4218
|
+
const out = {};
|
|
4219
|
+
for (const key of keys) out[key] = panelText(key);
|
|
4220
|
+
return out;
|
|
4221
|
+
}
|
|
4222
|
+
/**
|
|
4223
|
+
* Project the shared usage snapshot into the panel's render tree.
|
|
4224
|
+
*
|
|
4225
|
+
* Deduplication matches the settings card: hand-edited settings can name one
|
|
4226
|
+
* credential twice, and removal staging hides an account before the post-save
|
|
4227
|
+
* refresh lands.
|
|
4228
|
+
*/
|
|
4229
|
+
function buildPanelView(input) {
|
|
4230
|
+
const { usage } = input;
|
|
4231
|
+
const hidden = new Set(input.removingIds ?? []);
|
|
4232
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4233
|
+
const entries = (usage.report?.accounts ?? []).filter((entry) => {
|
|
4234
|
+
if (hidden.has(entry.id)) return false;
|
|
4235
|
+
if (seen.has(entry.id)) return false;
|
|
4236
|
+
seen.add(entry.id);
|
|
4237
|
+
return true;
|
|
4238
|
+
});
|
|
4239
|
+
const accounts = entries.map(accountView);
|
|
4240
|
+
const selectedEntry = entries.find((entry) => entry.active) ?? entries[0];
|
|
4241
|
+
const selectedView = accounts.find((view) => view.id === selectedEntry?.id);
|
|
4242
|
+
const footerBars = [];
|
|
4243
|
+
const windowBars = [["fiveHourShort", selectedEntry?.report.credits?.fiveHour], ["weeklyShort", selectedEntry?.report.credits?.weekly]];
|
|
4244
|
+
for (const [label, window] of windowBars) {
|
|
4245
|
+
if (window === void 0 || window.cap <= 0) continue;
|
|
4246
|
+
const percent = rawPercent(window.used, window.cap);
|
|
4247
|
+
footerBars.push({
|
|
4248
|
+
label,
|
|
4249
|
+
percent: `${percent}%`,
|
|
4250
|
+
barPercent: barPercent(percent),
|
|
4251
|
+
warn: window.exceeded,
|
|
4252
|
+
detail: `${money(window.used)} / ${money(window.cap)}`
|
|
4253
|
+
});
|
|
4254
|
+
}
|
|
4255
|
+
const totalCost = selectedEntry?.report.usage?.totalCost;
|
|
4256
|
+
const cost = totalCost === void 0 ? "" : money(totalCost);
|
|
4257
|
+
let status = "";
|
|
4258
|
+
if (entries.length > 0 && !entries.some((entry) => entry.configured)) status = panelText("unconfigured");
|
|
4259
|
+
else if (selectedView?.mark !== void 0 && selectedView.mark !== "active") status = panelText(selectedView.mark);
|
|
4260
|
+
else if (selectedEntry === void 0) status = panelText("unavailable");
|
|
4261
|
+
const planName = selectedView !== void 0 && selectedView.planName !== "" ? selectedView.planName : panelText("nav");
|
|
4262
|
+
const titleParts = planName === panelText("nav") ? [planName] : [panelText("nav"), planName];
|
|
4263
|
+
for (const bar of footerBars) {
|
|
4264
|
+
const figures = bar.detail === "" ? "" : ` ${bar.detail}`;
|
|
4265
|
+
titleParts.push(`${panelText(bar.label)}${figures} (${bar.percent})`);
|
|
4266
|
+
}
|
|
4267
|
+
if (cost !== "") titleParts.push(`${panelText("spend")} ${cost}`);
|
|
4268
|
+
return {
|
|
4269
|
+
text: panelStrings(),
|
|
4270
|
+
planName,
|
|
4271
|
+
status,
|
|
4272
|
+
footerBars,
|
|
4273
|
+
cost,
|
|
4274
|
+
footTitle: titleParts.join(" · "),
|
|
4275
|
+
accounts,
|
|
4276
|
+
selectedId: selectedEntry?.id,
|
|
4277
|
+
selected: selectedView,
|
|
4278
|
+
loading: usage.status === "loading" && usage.report === void 0,
|
|
4279
|
+
noKey: entries.length > 0 && !entries.some((entry) => entry.configured),
|
|
4280
|
+
failure: failureView(usage),
|
|
4281
|
+
staleError: usage.status === "error" && usage.report !== void 0 ? usage.error ?? "" : void 0,
|
|
4282
|
+
partial: usage.report?.accounts.some((entry) => entry.report.failures.length > 0 && entry.report.blocked === void 0) ?? false ? "partial" : void 0,
|
|
4283
|
+
updatedAt: timeText(usage.fetchedAt ?? 0)
|
|
4284
|
+
};
|
|
4285
|
+
}
|
|
4286
|
+
/**
|
|
4287
|
+
* How often the panel re-reads the report while a surface is mounted. The
|
|
4288
|
+
* quota windows move slowly and one report costs four upstream calls, so this
|
|
4289
|
+
* is a background freshness tick, not a live meter.
|
|
4290
|
+
*/
|
|
4291
|
+
const PANEL_AUTO_REFRESH_MS = 12e4;
|
|
4292
|
+
/** The default timer (the client bundle runs in a browser; node tests inject one). */
|
|
4293
|
+
const REAL_TIMER = {
|
|
4294
|
+
set: (callback, ms) => setTimeout(callback, ms),
|
|
4295
|
+
clear: (handle) => clearTimeout(handle)
|
|
4296
|
+
};
|
|
4297
|
+
/** Live mount count of {@link startPanelAutoRefresh}. */
|
|
4298
|
+
let references = 0;
|
|
4299
|
+
/** Owner ticket of the running loop, if any. */
|
|
4300
|
+
let activeTicket;
|
|
4301
|
+
/** Ticket handed to the next starter. */
|
|
4302
|
+
let ticketSeq = 0;
|
|
4303
|
+
/** Pending timer handle of the running loop. */
|
|
4304
|
+
let handle;
|
|
4305
|
+
/**
|
|
4306
|
+
* Start the shared auto refresh. One fetch when the surface appears (the
|
|
4307
|
+
* sidebar row is the point of the panel — it must be current, not wait for a
|
|
4308
|
+
* click), then a tick every {@link PANEL_AUTO_REFRESH_MS} while a surface
|
|
4309
|
+
* stays mounted.
|
|
4310
|
+
*
|
|
4311
|
+
* Reference-counted: the sidebar entry and the dashboard can be mounted at
|
|
4312
|
+
* once, so only the first start fetches and only the last stop halts the loop.
|
|
4313
|
+
* Every tick goes through `usage.refresh()`, which already collapses a
|
|
4314
|
+
* concurrent fetch onto the in-flight one — a tick never double-fetches
|
|
4315
|
+
* against the settings page's own refresh.
|
|
4316
|
+
*
|
|
4317
|
+
* @param usage - the shared usage controller.
|
|
4318
|
+
* @param isConfigured - whether a credential exists right now (re-read per tick).
|
|
4319
|
+
* @param timer - timer seam for tests.
|
|
4320
|
+
* @returns the disposer that drops this surface's reference.
|
|
4321
|
+
*/
|
|
4322
|
+
function startPanelAutoRefresh(usage, isConfigured, timer = REAL_TIMER) {
|
|
4323
|
+
references += 1;
|
|
4324
|
+
const ticket = ++ticketSeq;
|
|
4325
|
+
const tick = () => {
|
|
4326
|
+
if (activeTicket !== ticket) return;
|
|
4327
|
+
if (isConfigured()) usage.refresh();
|
|
4328
|
+
handle = timer.set(tick, PANEL_AUTO_REFRESH_MS);
|
|
4329
|
+
};
|
|
4330
|
+
if (activeTicket === void 0) {
|
|
4331
|
+
activeTicket = ticket;
|
|
4332
|
+
handle = timer.set(tick, PANEL_AUTO_REFRESH_MS);
|
|
4333
|
+
if (isConfigured()) usage.refresh();
|
|
4334
|
+
}
|
|
4335
|
+
let stopped = false;
|
|
4336
|
+
return () => {
|
|
4337
|
+
if (stopped) return;
|
|
4338
|
+
stopped = true;
|
|
4339
|
+
references -= 1;
|
|
4340
|
+
if (references > 0) return;
|
|
4341
|
+
activeTicket = void 0;
|
|
4342
|
+
if (handle !== void 0) {
|
|
4343
|
+
timer.clear(handle);
|
|
4344
|
+
handle = void 0;
|
|
4345
|
+
}
|
|
4346
|
+
};
|
|
4347
|
+
}
|
|
4348
|
+
//#endregion
|
|
4349
|
+
//#region src/client/panel-view.tsx
|
|
4350
|
+
/**
|
|
4351
|
+
* React components for the Command Code plans & quota panel (browser half):
|
|
4352
|
+
* the sidebar footer card and the dashboard it opens in the center column.
|
|
4353
|
+
*
|
|
4354
|
+
* Both render one {@link PanelView} projected by `./panel.ts` — no fact is
|
|
4355
|
+
* derived here. Strings arrive as keys into `view.text`, and every one of them
|
|
4356
|
+
* is English, because the view is built from `./panel-copy.ts` rather than the
|
|
4357
|
+
* harness `ctx.locale` namespace (which follows the user's language and would
|
|
4358
|
+
* render this panel in Chinese on a Chinese harness — the thing this surface
|
|
4359
|
+
* exists to avoid).
|
|
4360
|
+
*
|
|
4361
|
+
* The footer card is the panel's home: the sidebar shell renders it in the foot
|
|
4362
|
+
* area directly above the Settings seat, so each quota window's own spend and
|
|
4363
|
+
* limit — the 5-hour and the weekly one — are on screen without opening
|
|
4364
|
+
* anything. Clicking it selects the `main` panel this file also renders — and
|
|
4365
|
+
* unlike a `sidebar.panellist` row, whose button chrome and label the SHELL
|
|
4366
|
+
* owns, this entry owns its whole surface and therefore calls `open()` itself.
|
|
4367
|
+
*
|
|
4368
|
+
* Styles ride the stylesheet `./panel-styles.ts` returns, injected once by
|
|
4369
|
+
* the client entry; classes are `ccp-` prefixed to stay clear of the settings
|
|
4370
|
+
* page's `cc-` set.
|
|
4371
|
+
*
|
|
4372
|
+
* @module dsh-commandcode-provider/client/panel-view
|
|
4373
|
+
*/
|
|
4374
|
+
/** The panel's view, recomputed from both seats on every notification. */
|
|
4375
|
+
function usePanelView(props) {
|
|
4376
|
+
const usage = props.useCommandCodeUsage((state) => state);
|
|
4377
|
+
const settings = props.useCommandCodeSettings((state) => state);
|
|
4378
|
+
return buildPanelView({
|
|
4379
|
+
usage,
|
|
4380
|
+
apiKeyConfigured: settings.anyAccountConfigured,
|
|
4381
|
+
removingIds: settings.accountsRemoving
|
|
4382
|
+
});
|
|
4383
|
+
}
|
|
4384
|
+
/**
|
|
4385
|
+
* The quota ring. One glyph serves the rail button, the footer card's top row
|
|
4386
|
+
* and the dashboard header: a faint track plus an arc whose sweep is the
|
|
4387
|
+
* consumption, drawn from 12 o'clock. Circumference 2πr = 45.55 at r = 7.25.
|
|
4388
|
+
*/
|
|
4389
|
+
function Ring({ percent, warn, size }) {
|
|
4390
|
+
const clamped = Math.min(100, Math.max(0, percent));
|
|
4391
|
+
const circumference = 45.55;
|
|
4392
|
+
const dashoffset = Math.round(circumference * (1 - clamped / 100) * 1e3) / 1e3;
|
|
4393
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4394
|
+
className: "ccp-glyph",
|
|
4395
|
+
"aria-hidden": "true",
|
|
4396
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
4397
|
+
viewBox: "0 0 20 20",
|
|
4398
|
+
width: size,
|
|
4399
|
+
height: size,
|
|
4400
|
+
focusable: "false",
|
|
4401
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
4402
|
+
cx: "10",
|
|
4403
|
+
cy: "10",
|
|
4404
|
+
r: "7.25",
|
|
4405
|
+
fill: "none",
|
|
4406
|
+
stroke: "currentColor",
|
|
4407
|
+
strokeWidth: "1.5",
|
|
4408
|
+
opacity: "0.4"
|
|
4409
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
4410
|
+
cx: "10",
|
|
4411
|
+
cy: "10",
|
|
4412
|
+
r: "7.25",
|
|
4413
|
+
fill: "none",
|
|
4414
|
+
stroke: warn ? "var(--dsw-alias-state-error-primary)" : "currentColor",
|
|
4415
|
+
strokeWidth: "2.5",
|
|
4416
|
+
strokeLinecap: "round",
|
|
4417
|
+
strokeDasharray: String(circumference),
|
|
4418
|
+
strokeDashoffset: String(dashoffset),
|
|
4419
|
+
transform: "rotate(-90 10 10)"
|
|
4420
|
+
})]
|
|
4421
|
+
})
|
|
4422
|
+
});
|
|
4423
|
+
}
|
|
4424
|
+
/** One labelled bar. `compact` drops the reset line; the footer draws its own. */
|
|
4425
|
+
function QuotaBar({ label, value, percent, barPercent, warn, resetsAt, resetsLabel }) {
|
|
4426
|
+
const clamped = Math.min(100, Math.max(0, barPercent));
|
|
4427
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4428
|
+
className: "ccp-window",
|
|
4429
|
+
children: [
|
|
4430
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4431
|
+
className: "ccp-windowHead",
|
|
4432
|
+
children: [
|
|
4433
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4434
|
+
className: "ccp-windowLabel",
|
|
4435
|
+
children: label
|
|
4436
|
+
}),
|
|
4437
|
+
warn !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4438
|
+
className: "ccp-warnTag",
|
|
4439
|
+
children: warn
|
|
4440
|
+
}) : null,
|
|
4441
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "ccp-spacer" }),
|
|
4442
|
+
value !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4443
|
+
className: "ccp-windowValue",
|
|
4444
|
+
children: value
|
|
4445
|
+
}) : null,
|
|
4446
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4447
|
+
className: "ccp-windowPct",
|
|
4448
|
+
children: percent
|
|
4449
|
+
})
|
|
4450
|
+
]
|
|
4451
|
+
}),
|
|
4452
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4453
|
+
className: "ccp-bar",
|
|
4454
|
+
role: "progressbar",
|
|
4455
|
+
"aria-label": label,
|
|
4456
|
+
"aria-valuemin": 0,
|
|
4457
|
+
"aria-valuemax": 100,
|
|
4458
|
+
"aria-valuenow": clamped,
|
|
4459
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4460
|
+
className: warn !== "" ? "ccp-barFill ccp-barFillWarn" : "ccp-barFill",
|
|
4461
|
+
style: { width: `${clamped}%` }
|
|
4462
|
+
})
|
|
4463
|
+
}),
|
|
4464
|
+
resetsAt !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
4465
|
+
className: "ccp-windowReset",
|
|
4466
|
+
children: [
|
|
4467
|
+
resetsLabel,
|
|
4468
|
+
" ",
|
|
4469
|
+
resetsAt
|
|
4470
|
+
]
|
|
4471
|
+
}) : null
|
|
4472
|
+
]
|
|
4473
|
+
});
|
|
4474
|
+
}
|
|
4475
|
+
/** A labelled figure. */
|
|
4476
|
+
function Tile({ label, value, sub }) {
|
|
4477
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4478
|
+
className: "ccp-tile",
|
|
4479
|
+
children: [
|
|
4480
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4481
|
+
className: "ccp-tileLabel",
|
|
4482
|
+
children: label
|
|
4483
|
+
}),
|
|
4484
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4485
|
+
className: "ccp-tileValue",
|
|
4486
|
+
children: value
|
|
4487
|
+
}),
|
|
4488
|
+
sub !== void 0 && sub !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4489
|
+
className: "ccp-tileSub",
|
|
4490
|
+
children: sub
|
|
4491
|
+
}) : null
|
|
4492
|
+
]
|
|
4493
|
+
});
|
|
4494
|
+
}
|
|
4495
|
+
/** One usage tile. */
|
|
4496
|
+
function StatTile({ stat, label }) {
|
|
4497
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tile, {
|
|
4498
|
+
label,
|
|
4499
|
+
value: stat.value,
|
|
4500
|
+
sub: stat.sub
|
|
4501
|
+
});
|
|
4502
|
+
}
|
|
4503
|
+
/** The rotation/credential badge line for one account. */
|
|
4504
|
+
function markText(account, text) {
|
|
4505
|
+
if (account.mark === void 0) return "";
|
|
4506
|
+
if (account.mark === "coolingDown" && account.cooldownUntil !== "") return `${text("coolingDown")} · ${account.cooldownUntil}`;
|
|
4507
|
+
return text(account.mark);
|
|
4508
|
+
}
|
|
4509
|
+
/** `Default account` → `D`; used for the card's monogram chip. */
|
|
4510
|
+
function initial(label) {
|
|
4511
|
+
const trimmed = label.trim();
|
|
4512
|
+
return trimmed === "" ? "?" : trimmed[0].toUpperCase();
|
|
4513
|
+
}
|
|
4514
|
+
/**
|
|
4515
|
+
* One account's full report: the monthly limit/usage bar, the two quota
|
|
4516
|
+
* windows, then the credit and usage totals.
|
|
4517
|
+
*/
|
|
4518
|
+
function AccountCard({ account, view }) {
|
|
4519
|
+
const text = (key) => view.text[key] ?? key;
|
|
4520
|
+
const mark = markText(account, text);
|
|
4521
|
+
const monthly = account.monthly;
|
|
4522
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4523
|
+
className: "ccp-card",
|
|
4524
|
+
"aria-label": account.label,
|
|
4525
|
+
children: [
|
|
4526
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
4527
|
+
className: "ccp-cardHead",
|
|
4528
|
+
children: [
|
|
4529
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4530
|
+
className: "ccp-avatar",
|
|
4531
|
+
"aria-hidden": "true",
|
|
4532
|
+
children: initial(account.label)
|
|
4533
|
+
}),
|
|
4534
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4535
|
+
className: "ccp-cardIdentity",
|
|
4536
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4537
|
+
className: "ccp-cardTitle",
|
|
4538
|
+
children: account.label
|
|
4539
|
+
}), account.owner !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4540
|
+
className: "ccp-cardOwner",
|
|
4541
|
+
children: account.owner
|
|
4542
|
+
}) : null]
|
|
4543
|
+
}),
|
|
4544
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "ccp-spacer" }),
|
|
4545
|
+
account.planName !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4546
|
+
className: "ccp-badge",
|
|
4547
|
+
children: account.planName
|
|
4548
|
+
}) : null,
|
|
4549
|
+
account.planStatus !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4550
|
+
className: "ccp-badge ccp-badgeWarn",
|
|
4551
|
+
children: account.planStatus
|
|
4552
|
+
}) : null,
|
|
4553
|
+
mark !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4554
|
+
className: account.mark === "invalidKey" ? "ccp-badge ccp-badgeError" : "ccp-badgeMuted",
|
|
4555
|
+
children: mark
|
|
4556
|
+
}) : null,
|
|
4557
|
+
account.periodEnds !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4558
|
+
className: "ccp-meta",
|
|
4559
|
+
children: [
|
|
4560
|
+
text("periodEnds"),
|
|
4561
|
+
" ",
|
|
4562
|
+
account.periodEnds
|
|
4563
|
+
]
|
|
4564
|
+
}) : null
|
|
4565
|
+
]
|
|
4566
|
+
}),
|
|
4567
|
+
account.unconfigured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4568
|
+
className: "ccp-hint",
|
|
4569
|
+
children: text("unconfigured")
|
|
4570
|
+
}) : null,
|
|
4571
|
+
monthly !== void 0 && monthly.known ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuotaBar, {
|
|
4572
|
+
label: text("monthly"),
|
|
4573
|
+
value: `${monthly.used} / ${monthly.limit}`,
|
|
4574
|
+
percent: `${monthly.percent}%`,
|
|
4575
|
+
barPercent: monthly.barPercent,
|
|
4576
|
+
warn: monthly.exhausted ? text("exhausted") : "",
|
|
4577
|
+
resetsAt: "",
|
|
4578
|
+
resetsLabel: ""
|
|
4579
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4580
|
+
className: "ccp-planRow",
|
|
4581
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4582
|
+
className: "ccp-fieldLabel",
|
|
4583
|
+
children: text("plan")
|
|
4584
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4585
|
+
className: "ccp-planName",
|
|
4586
|
+
children: account.planName !== "" ? account.planName : text("unavailable")
|
|
4587
|
+
})]
|
|
4588
|
+
}),
|
|
4589
|
+
monthly !== void 0 && monthly.remaining !== "—" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4590
|
+
className: "ccp-planRow",
|
|
4591
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4592
|
+
className: "ccp-fieldLabel",
|
|
4593
|
+
children: text("remaining")
|
|
4594
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4595
|
+
className: "ccp-planName",
|
|
4596
|
+
children: monthly.remaining
|
|
4597
|
+
})]
|
|
4598
|
+
}) : null,
|
|
4599
|
+
account.windows.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4600
|
+
className: "ccp-windows",
|
|
4601
|
+
children: account.windows.map((window) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuotaBar, {
|
|
4602
|
+
label: text(window.label),
|
|
4603
|
+
value: window.value,
|
|
4604
|
+
percent: window.capped ? `${window.percent}%` : text("windowUnlimited"),
|
|
4605
|
+
barPercent: window.barPercent,
|
|
4606
|
+
warn: window.exceeded ? text("exceeded") : "",
|
|
4607
|
+
resetsAt: window.resetsAt,
|
|
4608
|
+
resetsLabel: text("resets")
|
|
4609
|
+
}, window.label))
|
|
4610
|
+
}) : null,
|
|
4611
|
+
monthly !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4612
|
+
className: "ccp-block",
|
|
4613
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
4614
|
+
className: "ccp-blockTitle",
|
|
4615
|
+
children: text("credits")
|
|
4616
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4617
|
+
className: "ccp-tiles",
|
|
4618
|
+
children: [
|
|
4619
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tile, {
|
|
4620
|
+
label: text("monthlyLimit"),
|
|
4621
|
+
value: monthly.limit
|
|
4622
|
+
}),
|
|
4623
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tile, {
|
|
4624
|
+
label: text("monthlyUsed"),
|
|
4625
|
+
value: monthly.used
|
|
4626
|
+
}),
|
|
4627
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tile, {
|
|
4628
|
+
label: text("remaining"),
|
|
4629
|
+
value: monthly.remaining
|
|
4630
|
+
}),
|
|
4631
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tile, {
|
|
4632
|
+
label: text("purchased"),
|
|
4633
|
+
value: monthly.purchased
|
|
4634
|
+
}),
|
|
4635
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tile, {
|
|
4636
|
+
label: text("free"),
|
|
4637
|
+
value: monthly.free
|
|
4638
|
+
})
|
|
4639
|
+
]
|
|
4640
|
+
})]
|
|
4641
|
+
}) : null,
|
|
4642
|
+
account.stats.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4643
|
+
className: "ccp-block",
|
|
4644
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
4645
|
+
className: "ccp-blockTitle",
|
|
4646
|
+
children: text("usage")
|
|
4647
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4648
|
+
className: "ccp-tiles",
|
|
4649
|
+
children: account.stats.map((stat) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatTile, {
|
|
4650
|
+
stat,
|
|
4651
|
+
label: text(stat.label)
|
|
4652
|
+
}, stat.label))
|
|
4653
|
+
})]
|
|
4654
|
+
}) : null
|
|
4655
|
+
]
|
|
4656
|
+
});
|
|
4657
|
+
}
|
|
4658
|
+
/**
|
|
4659
|
+
* The center-column dashboard, registered into the layout's keyed `main` slot
|
|
4660
|
+
* under the same id the footer card selects, so the two are one navigation
|
|
4661
|
+
* entry: the card shows the plan, the monthly bar and the quota window; the
|
|
4662
|
+
* panel shows everything, including the per-account breakdown.
|
|
4663
|
+
*/
|
|
4664
|
+
function CommandCodePanel(props) {
|
|
4665
|
+
const view = usePanelView(props);
|
|
4666
|
+
const text = (key) => view.text[key] ?? key;
|
|
4667
|
+
const [selectedId, setSelectedId] = (0, react.useState)(void 0);
|
|
4668
|
+
const startAutoRefresh = props.startAutoRefresh;
|
|
4669
|
+
(0, react.useEffect)(() => startAutoRefresh(), [startAutoRefresh]);
|
|
4670
|
+
const accounts = view.accounts;
|
|
4671
|
+
const selected = accounts.find((account) => account.id === selectedId) ?? view.selected ?? accounts[0];
|
|
4672
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4673
|
+
className: "ccp-main",
|
|
4674
|
+
role: "region",
|
|
4675
|
+
"aria-label": text("nav"),
|
|
4676
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4677
|
+
className: "ccp-mainInner",
|
|
4678
|
+
children: [
|
|
4679
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
4680
|
+
className: "ccp-header",
|
|
4681
|
+
children: [
|
|
4682
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4683
|
+
className: "ccp-headerText",
|
|
4684
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
4685
|
+
className: "ccp-title",
|
|
4686
|
+
children: text("nav")
|
|
4687
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4688
|
+
className: "ccp-subtitle",
|
|
4689
|
+
children: text("subtitle")
|
|
4690
|
+
})]
|
|
4691
|
+
}),
|
|
4692
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "ccp-spacer" }),
|
|
4693
|
+
view.updatedAt !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4694
|
+
className: "ccp-meta",
|
|
4695
|
+
children: [
|
|
4696
|
+
text("updated"),
|
|
4697
|
+
" ",
|
|
4698
|
+
view.updatedAt
|
|
4699
|
+
]
|
|
4700
|
+
}) : null,
|
|
4701
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
4702
|
+
variant: "ghost",
|
|
4703
|
+
size: "sm",
|
|
4704
|
+
disabled: view.loading,
|
|
4705
|
+
onClick: () => props.refresh(),
|
|
4706
|
+
children: view.loading ? text("refreshing") : text("refresh")
|
|
4707
|
+
})
|
|
4708
|
+
]
|
|
4709
|
+
}),
|
|
4710
|
+
view.noKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4711
|
+
className: "ccp-notice",
|
|
4712
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4713
|
+
className: "ccp-noticeTitle",
|
|
4714
|
+
children: text("noKey")
|
|
4715
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4716
|
+
className: "ccp-noticeHint",
|
|
4717
|
+
children: text("noKeyHint")
|
|
4718
|
+
})]
|
|
4719
|
+
}) : null,
|
|
4720
|
+
view.failure !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4721
|
+
className: "ccp-notice ccp-noticeError",
|
|
4722
|
+
role: "alert",
|
|
4723
|
+
children: [
|
|
4724
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4725
|
+
className: "ccp-noticeTitle",
|
|
4726
|
+
children: text(view.failure.title)
|
|
4727
|
+
}),
|
|
4728
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4729
|
+
className: "ccp-noticeHint",
|
|
4730
|
+
children: text(view.failure.hint)
|
|
4731
|
+
}),
|
|
4732
|
+
view.failure.detail !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4733
|
+
className: "ccp-noticeDetail",
|
|
4734
|
+
children: view.failure.detail
|
|
4735
|
+
}) : null
|
|
4736
|
+
]
|
|
4737
|
+
}) : null,
|
|
4738
|
+
!view.noKey && view.failure === void 0 && accounts.length === 0 && view.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4739
|
+
className: "ccp-hint",
|
|
4740
|
+
children: text("loading")
|
|
4741
|
+
}) : null,
|
|
4742
|
+
view.staleError !== void 0 && view.staleError !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
4743
|
+
className: "ccp-hint",
|
|
4744
|
+
role: "status",
|
|
4745
|
+
children: [
|
|
4746
|
+
text("errorGeneric"),
|
|
4747
|
+
" — ",
|
|
4748
|
+
view.staleError
|
|
4749
|
+
]
|
|
4750
|
+
}) : null,
|
|
4751
|
+
accounts.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4752
|
+
className: "ccp-tabs",
|
|
4753
|
+
role: "tablist",
|
|
4754
|
+
"aria-label": text("nav"),
|
|
4755
|
+
children: accounts.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4756
|
+
type: "button",
|
|
4757
|
+
role: "tab",
|
|
4758
|
+
"aria-selected": selected?.id === account.id,
|
|
4759
|
+
className: selected?.id === account.id ? "ccp-tab ccp-tabActive" : "ccp-tab",
|
|
4760
|
+
onClick: () => setSelectedId(account.id),
|
|
4761
|
+
children: account.label
|
|
4762
|
+
}, account.id))
|
|
4763
|
+
}) : null,
|
|
4764
|
+
selected !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountCard, {
|
|
4765
|
+
account: selected,
|
|
4766
|
+
view
|
|
4767
|
+
}) : null,
|
|
4768
|
+
view.partial !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4769
|
+
className: "ccp-hint",
|
|
4770
|
+
children: text(view.partial)
|
|
4771
|
+
}) : null
|
|
4772
|
+
]
|
|
4773
|
+
})
|
|
4774
|
+
});
|
|
4775
|
+
}
|
|
4776
|
+
/**
|
|
4777
|
+
* The sidebar footer card, registered into `sidebar.footer.action` — the list
|
|
4778
|
+
* the shell renders in the sidebar's foot area directly ABOVE the Settings
|
|
4779
|
+
* seat, so the panel reads as a bottom-pinned sibling of Settings rather than
|
|
4780
|
+
* a global panel icon at the top of the column.
|
|
4781
|
+
*
|
|
4782
|
+
* The shell wraps nothing here, so this component owns the surface: the
|
|
4783
|
+
* button, its chrome and its accessible name. In the expanded column it draws
|
|
4784
|
+
* the title row, then one block per quota window (5-hour, then weekly) — the
|
|
4785
|
+
* window's own spend and limit (`$1.32 / $6.00`), its percentage and its bar —
|
|
4786
|
+
* and nothing else: the card's figures are the two windows the account runs
|
|
4787
|
+
* into, so the period total stays in the tooltip rather than taking a third
|
|
4788
|
+
* line. In the 56px rail it collapses to a 36px icon button carrying the ring,
|
|
4789
|
+
* matching the shell's own rail geometry. `wide` comes from the shell as an
|
|
4790
|
+
* owner prop — unlike the old `sidebar.panellist` row, this slot really does
|
|
4791
|
+
* supply it.
|
|
4792
|
+
*
|
|
4793
|
+
* Rendering is gated on the settings page's `showSidebarQuota` toggle, which
|
|
4794
|
+
* defaults OFF: a fresh install shows no quota surface in the sidebar and runs
|
|
4795
|
+
* no background poll. The gate reads the STORED document (the settings
|
|
4796
|
+
* controller's `sidebarQuota` fact), so an unsaved draft cannot show the card,
|
|
4797
|
+
* while a landed save flips it live. The entry is still REGISTERED — this is a
|
|
4798
|
+
* render decision, not a slot registration, so nothing churns the slot ledger.
|
|
4799
|
+
*/
|
|
4800
|
+
function CommandCodeFooterEntry(props) {
|
|
4801
|
+
const visible = props.useCommandCodeSettings((snapshot) => snapshot.sidebarQuota);
|
|
4802
|
+
const view = usePanelView(props);
|
|
4803
|
+
const startAutoRefresh = props.startAutoRefresh;
|
|
4804
|
+
(0, react.useEffect)(() => {
|
|
4805
|
+
if (!visible) return void 0;
|
|
4806
|
+
return startAutoRefresh();
|
|
4807
|
+
}, [visible, startAutoRefresh]);
|
|
4808
|
+
if (!visible) return null;
|
|
4809
|
+
const text = (key) => view.text[key] ?? key;
|
|
4810
|
+
const headline = view.footerBars[0];
|
|
4811
|
+
const title = view.footTitle;
|
|
4812
|
+
if (!props.wide) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4813
|
+
type: "button",
|
|
4814
|
+
className: "ccp-railButton",
|
|
4815
|
+
"aria-label": title,
|
|
4816
|
+
title,
|
|
4817
|
+
onClick: () => props.open(),
|
|
4818
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Ring, {
|
|
4819
|
+
percent: headline?.barPercent ?? 0,
|
|
4820
|
+
warn: headline?.warn ?? false,
|
|
4821
|
+
size: 18
|
|
4822
|
+
})
|
|
4823
|
+
});
|
|
4824
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4825
|
+
type: "button",
|
|
4826
|
+
className: "ccp-foot",
|
|
4827
|
+
"aria-label": title,
|
|
4828
|
+
title,
|
|
4829
|
+
onClick: () => props.open(),
|
|
4830
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4831
|
+
className: "ccp-footTop",
|
|
4832
|
+
children: [
|
|
4833
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Ring, {
|
|
4834
|
+
percent: headline?.barPercent ?? 0,
|
|
4835
|
+
warn: headline?.warn ?? false,
|
|
4836
|
+
size: 16
|
|
4837
|
+
}),
|
|
4838
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4839
|
+
className: "ccp-footName",
|
|
4840
|
+
children: text("nav")
|
|
4841
|
+
}),
|
|
4842
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "ccp-spacer" }),
|
|
4843
|
+
view.status !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4844
|
+
className: "ccp-badgeMuted",
|
|
4845
|
+
children: view.status
|
|
4846
|
+
}) : view.planName !== text("nav") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4847
|
+
className: "ccp-badge",
|
|
4848
|
+
children: view.planName
|
|
4849
|
+
}) : null
|
|
4850
|
+
]
|
|
4851
|
+
}), view.footerBars.map((bar) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4852
|
+
className: "ccp-footRow",
|
|
4853
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4854
|
+
className: "ccp-footHead",
|
|
4855
|
+
children: [
|
|
4856
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4857
|
+
className: "ccp-footLabel",
|
|
4858
|
+
children: text(bar.label)
|
|
4859
|
+
}),
|
|
4860
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "ccp-spacer" }),
|
|
4861
|
+
bar.detail !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4862
|
+
className: "ccp-footAmount",
|
|
4863
|
+
children: bar.detail
|
|
4864
|
+
}) : null,
|
|
4865
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4866
|
+
className: "ccp-footPct",
|
|
4867
|
+
children: bar.percent
|
|
4868
|
+
})
|
|
4869
|
+
]
|
|
4870
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4871
|
+
className: "ccp-footBar",
|
|
4872
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4873
|
+
className: bar.warn ? "ccp-footFill ccp-footFillWarn" : "ccp-footFill",
|
|
4874
|
+
style: { width: `${bar.barPercent}%` }
|
|
4875
|
+
})
|
|
4876
|
+
})]
|
|
4877
|
+
}, bar.label))]
|
|
4878
|
+
});
|
|
4879
|
+
}
|
|
4880
|
+
//#endregion
|
|
4881
|
+
//#region src/cost-facts.ts
|
|
4882
|
+
const zeroCostTokens = () => ({
|
|
4883
|
+
uncachedInputTokens: 0,
|
|
4884
|
+
outputTokens: 0,
|
|
4885
|
+
cacheReadTokens: 0,
|
|
4886
|
+
cacheWriteTokens: 0
|
|
4887
|
+
});
|
|
4888
|
+
const COST_TOKEN_KEYS = [
|
|
4889
|
+
"uncachedInputTokens",
|
|
4890
|
+
"outputTokens",
|
|
4891
|
+
"cacheReadTokens",
|
|
4892
|
+
"cacheWriteTokens"
|
|
4893
|
+
];
|
|
4894
|
+
function peakHour(at, windows) {
|
|
4895
|
+
const time = new Date(at);
|
|
4896
|
+
return time.getUTCDay() !== 0 && time.getUTCDay() !== 6 && windows.some(([start, end]) => time.getUTCHours() >= start && time.getUTCHours() < end);
|
|
4897
|
+
}
|
|
4898
|
+
/**
|
|
4899
|
+
* All prompt billing buckets determine a request's band, never cumulative session input.
|
|
4900
|
+
*
|
|
4901
|
+
* A tiered model returns the matching band and does NOT then apply `peak`: the
|
|
4902
|
+
* page publishes those two dimensions independently, no row carries both today,
|
|
4903
|
+
* and the generator would report a tiered row on every sync — so the combination
|
|
4904
|
+
* becoming real is visible before it reaches a user. This ordering is a latent
|
|
4905
|
+
* choice, not a verified upstream rule.
|
|
4906
|
+
*/
|
|
4907
|
+
function requestRates(price, at, contextTokens, table) {
|
|
4908
|
+
const tier = price.contextTiers?.find((tier) => tier.maxContext === void 0 || contextTokens <= tier.maxContext);
|
|
4909
|
+
if (tier !== void 0) return tier;
|
|
4910
|
+
return price.peak !== void 0 && at !== null && peakHour(at, table.peakHours) ? price.peak : price;
|
|
4911
|
+
}
|
|
4912
|
+
/** Cache version and wire guard: changing bands/rates must refold historical groups. */
|
|
4913
|
+
function pricingKey(table) {
|
|
4914
|
+
const rates = (r) => [
|
|
4915
|
+
r.inputCost,
|
|
4916
|
+
r.outputCost,
|
|
4917
|
+
r.cacheReadCost,
|
|
4918
|
+
r.cacheWriteCost ?? null
|
|
4919
|
+
];
|
|
4920
|
+
const canonical = JSON.stringify([
|
|
4921
|
+
"commandCodeCost-v1",
|
|
4922
|
+
table.peakHours,
|
|
4923
|
+
table.models.map((p) => [
|
|
4924
|
+
p.id,
|
|
4925
|
+
p.slug,
|
|
4926
|
+
p.free === true,
|
|
4927
|
+
rates(p),
|
|
4928
|
+
p.peak ? rates(p.peak) : null,
|
|
4929
|
+
p.contextTiers?.map((t) => [t.maxContext ?? null, rates(t)]) ?? null
|
|
4930
|
+
])
|
|
4931
|
+
]);
|
|
4932
|
+
let hash = 2166136261;
|
|
4933
|
+
for (const c of canonical) hash = Math.imul(hash ^ c.charCodeAt(0), 16777619) >>> 0;
|
|
4934
|
+
return hash;
|
|
4935
|
+
}
|
|
4936
|
+
//#endregion
|
|
4937
|
+
//#region src/client/session-cost.ts
|
|
4938
|
+
/**
|
|
4939
|
+
* Session-cost view model for the composer readout.
|
|
4940
|
+
*
|
|
4941
|
+
* Deliberately JSX-free and React-free, mirroring `./panel.ts`: it turns the
|
|
4942
|
+
* session's token accounting plus the Host's price table into one presentation
|
|
4943
|
+
* value AND the exact text the two surfaces of the harness's token-usage UI
|
|
4944
|
+
* receive — the amount appended to the shipped pill, and the rows appended to
|
|
4945
|
+
* the shipped usage dialog. Node tests therefore drive the whole calculation,
|
|
4946
|
+
* and every user-visible string, without a DOM.
|
|
4947
|
+
*
|
|
4948
|
+
* The figures it prices are the session's DURABLE cumulative buckets, which is
|
|
4949
|
+
* what the composer's own "N tokens" pill reads too (`tokenUsage`), so the two
|
|
4950
|
+
* surfaces can never disagree about how much was used. Dollars are computed
|
|
4951
|
+
* here because Command Code publishes per-token rates and bills against
|
|
4952
|
+
* dollar-denominated windows; the account's own reported `totalCost` is a
|
|
4953
|
+
* billing PERIOD figure, not this session's.
|
|
4954
|
+
*
|
|
4955
|
+
* Three rules are load-bearing:
|
|
4956
|
+
*
|
|
4957
|
+
* 1. **Only Command Code usage is priced.** A session served by another
|
|
4958
|
+
* provider must render nothing, never a Command Code estimate.
|
|
4959
|
+
* 2. **A missing rate is never invented.** The pricing page publishes
|
|
4960
|
+
* input/output/cache-read rates for every model but a cache-WRITE rate for
|
|
4961
|
+
* only some, so unpriced cache-write tokens are surfaced as such rather than
|
|
4962
|
+
* charged at a guessed multiple of the input rate.
|
|
4963
|
+
* 3. **Unpriceable means invisible.** No usage, no model, no table, an unknown
|
|
4964
|
+
* model, or all-zero buckets renders nothing at all — a confident `$0.00`
|
|
4965
|
+
* would be a lie, and this module never returns one.
|
|
4966
|
+
*
|
|
4967
|
+
* @module dsh-commandcode-provider/client/session-cost
|
|
4968
|
+
*/
|
|
4969
|
+
/** Tokens per published rate unit — the pricing page quotes USD per million. */
|
|
4970
|
+
const TOKENS_PER_RATE_UNIT = 1e6;
|
|
4971
|
+
/** The provider route whose usage this surface prices. */
|
|
4972
|
+
const COMMANDCODE_PROVIDER = "commandcode";
|
|
4973
|
+
/**
|
|
4974
|
+
* The English copy for this surface.
|
|
4975
|
+
*
|
|
4976
|
+
* A plain constant, NOT the `settings.commandcode` locale namespace: like the
|
|
4977
|
+
* plans & quota panel, the readout stays English on a Chinese harness. Do not
|
|
4978
|
+
* route it through `ctx.locale`.
|
|
4979
|
+
*/
|
|
4980
|
+
const SESSION_COST_COPY = {
|
|
4981
|
+
/** Shown instead of an amount when the model costs nothing on every plan. */
|
|
4982
|
+
free: "Free",
|
|
4983
|
+
/**
|
|
4984
|
+
* The separator the readout prefixes itself with, so the cost reads as the
|
|
4985
|
+
* last item of the token-usage pill's text run rather than a control beside
|
|
4986
|
+
* it. Rendered with the same colour and margins the shipped pill uses
|
|
4987
|
+
* between its own items.
|
|
4988
|
+
*/
|
|
4989
|
+
separator: "·",
|
|
4990
|
+
/** The heading the tooltip leads with. */
|
|
4991
|
+
panelTitle: "Session cost",
|
|
4992
|
+
/** Marks an estimate from published rates rather than an invoice. */
|
|
4993
|
+
approximate: "≈",
|
|
4994
|
+
/** Tooltip line for the unpriced cache-write tokens. */
|
|
4995
|
+
unpricedCacheWrite: "cache write tokens have no published rate",
|
|
4996
|
+
/** Tooltip line naming the rate half in force. */
|
|
4997
|
+
peakRates: "peak rates",
|
|
4998
|
+
/** Tooltip line naming the rate half in force. */
|
|
4999
|
+
offPeakRates: "off-peak rates",
|
|
5000
|
+
/** Tooltip line explaining the approximate marker. */
|
|
5001
|
+
approximateNote: "estimate from published rates, not the provider invoice",
|
|
5002
|
+
/** Row/tooltip label for uncached prompt tokens. */
|
|
5003
|
+
uncachedInput: "uncached input",
|
|
5004
|
+
/** Row/tooltip label for completion tokens. */
|
|
5005
|
+
output: "output",
|
|
5006
|
+
/** Row/tooltip label for cache-served input tokens. */
|
|
5007
|
+
cacheRead: "cache read",
|
|
5008
|
+
/** Row/tooltip label for cache-written input tokens. */
|
|
5009
|
+
cacheWrite: "cache write"
|
|
5010
|
+
};
|
|
5011
|
+
/** A finite, non-negative count — anything else reads as absent. */
|
|
5012
|
+
function count(value) {
|
|
5013
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
5014
|
+
}
|
|
5015
|
+
/**
|
|
5016
|
+
* Index a price table for lookup. Rows are keyed by catalog id and by pricing
|
|
5017
|
+
* slug, both exact and lowercased, because a session reports a catalog id while
|
|
5018
|
+
* a row no catalog model claims is served under the page's slug.
|
|
5019
|
+
*/
|
|
5020
|
+
function indexTable(table) {
|
|
5021
|
+
const index = /* @__PURE__ */ new Map();
|
|
5022
|
+
for (const price of table.models) for (const key of [price.id, price.slug]) {
|
|
5023
|
+
if (typeof key !== "string" || key === "") continue;
|
|
5024
|
+
if (!index.has(key)) index.set(key, price);
|
|
5025
|
+
const lower = key.toLowerCase();
|
|
5026
|
+
if (!index.has(lower)) index.set(lower, price);
|
|
5027
|
+
}
|
|
5028
|
+
return index;
|
|
5029
|
+
}
|
|
5030
|
+
function costOf(usage, rates, free) {
|
|
5031
|
+
if (free) return {
|
|
5032
|
+
uncachedInput: 0,
|
|
5033
|
+
cacheRead: 0,
|
|
5034
|
+
cacheWrite: 0,
|
|
5035
|
+
output: 0,
|
|
5036
|
+
total: 0,
|
|
5037
|
+
unpricedCacheWriteTokens: 0
|
|
5038
|
+
};
|
|
5039
|
+
const perUnit = (tokens, rate) => tokens * rate / TOKENS_PER_RATE_UNIT;
|
|
5040
|
+
const uncachedInput = perUnit(count(usage.uncachedInputTokens), rates.inputCost);
|
|
5041
|
+
const output = perUnit(count(usage.outputTokens), rates.outputCost);
|
|
5042
|
+
const cacheRead = perUnit(count(usage.cacheReadTokens), rates.cacheReadCost);
|
|
5043
|
+
const cacheWriteTokens = count(usage.cacheWriteTokens);
|
|
5044
|
+
const cacheWrite = rates.cacheWriteCost === void 0 ? void 0 : perUnit(cacheWriteTokens, rates.cacheWriteCost);
|
|
5045
|
+
return {
|
|
5046
|
+
uncachedInput,
|
|
5047
|
+
cacheRead,
|
|
5048
|
+
cacheWrite,
|
|
5049
|
+
output,
|
|
5050
|
+
total: uncachedInput + output + cacheRead + (cacheWrite ?? 0),
|
|
5051
|
+
unpricedCacheWriteTokens: rates.cacheWriteCost === void 0 ? cacheWriteTokens : 0
|
|
5052
|
+
};
|
|
5053
|
+
}
|
|
5054
|
+
/**
|
|
5055
|
+
* Amount text for a session cost.
|
|
5056
|
+
*
|
|
5057
|
+
* The panel's `money()` convention (2 decimals, 4 below a cent) is right for
|
|
5058
|
+
* billing windows, but a live session total starts far below a cent, where
|
|
5059
|
+
* `toFixed(4)` would print a flat `$0.0000` — which reads as broken rather than
|
|
5060
|
+
* as small. So a total under $0.0001 is stated as a bound instead.
|
|
5061
|
+
*/
|
|
5062
|
+
function sessionCostAmount(total) {
|
|
5063
|
+
if (!Number.isFinite(total) || total <= 0) return formatMoney(0);
|
|
5064
|
+
if (total < 1e-4) return `<$0.0001`;
|
|
5065
|
+
return total < .01 ? formatMoneyExact(total) : formatMoney(total);
|
|
5066
|
+
}
|
|
5067
|
+
/** One `label value` clause of the tooltip. */
|
|
5068
|
+
function clause(label, tokens) {
|
|
5069
|
+
return tokens > 0 ? `${label} ${formatTokensCompact(tokens)}` : void 0;
|
|
5070
|
+
}
|
|
5071
|
+
/**
|
|
5072
|
+
* Build the composer readout, or undefined when there is nothing honest to show.
|
|
5073
|
+
*
|
|
5074
|
+
* Undefined is the correct answer for a session with no usage yet, a session
|
|
5075
|
+
* another provider served, a model the price table does not know, and a table
|
|
5076
|
+
* that has not landed — the pill simply is not there.
|
|
5077
|
+
*/
|
|
5078
|
+
function buildSessionCostView(input) {
|
|
5079
|
+
const { usage, table, facts } = input;
|
|
5080
|
+
if (!usage || !table || !facts || facts.pricingKey !== pricingKey(table)) return void 0;
|
|
5081
|
+
const totals = zeroCostTokens();
|
|
5082
|
+
const index = indexTable(table);
|
|
5083
|
+
const breakdown = {
|
|
5084
|
+
uncachedInput: 0,
|
|
5085
|
+
cacheRead: 0,
|
|
5086
|
+
cacheWrite: 0,
|
|
5087
|
+
output: 0,
|
|
5088
|
+
total: 0,
|
|
5089
|
+
unpricedCacheWriteTokens: 0
|
|
5090
|
+
};
|
|
5091
|
+
let free = true;
|
|
5092
|
+
let peak = false;
|
|
5093
|
+
let partial = false;
|
|
5094
|
+
let priced = false;
|
|
5095
|
+
const notes = [];
|
|
5096
|
+
for (const group of facts.groups) {
|
|
5097
|
+
for (const key of COST_TOKEN_KEYS) totals[key] += group.tokens[key];
|
|
5098
|
+
if (group.provider !== COMMANDCODE_PROVIDER) {
|
|
5099
|
+
partial = true;
|
|
5100
|
+
continue;
|
|
5101
|
+
}
|
|
5102
|
+
const price = index.get(group.model);
|
|
5103
|
+
if (!price) {
|
|
5104
|
+
partial = true;
|
|
5105
|
+
continue;
|
|
5106
|
+
}
|
|
5107
|
+
const isFree = price.free === true;
|
|
5108
|
+
free &&= isFree;
|
|
5109
|
+
if (price.peak && group.at === null) {
|
|
5110
|
+
partial = true;
|
|
5111
|
+
notes.push("request time unavailable; off-peak lower bound");
|
|
5112
|
+
}
|
|
5113
|
+
const rates = requestRates(price, group.at, group.contextTokens, table);
|
|
5114
|
+
const part = costOf(group.tokens, rates, isFree);
|
|
5115
|
+
priced ||= isFree || part.total > 0;
|
|
5116
|
+
for (const key of [
|
|
5117
|
+
"uncachedInput",
|
|
5118
|
+
"cacheRead",
|
|
5119
|
+
"output",
|
|
5120
|
+
"total",
|
|
5121
|
+
"unpricedCacheWriteTokens"
|
|
5122
|
+
]) breakdown[key] += part[key];
|
|
5123
|
+
breakdown.cacheWrite = (breakdown.cacheWrite ?? 0) + (part.cacheWrite ?? 0);
|
|
5124
|
+
if (price.peak) {
|
|
5125
|
+
const isPeak = group.at !== null && peakHour(group.at, table.peakHours);
|
|
5126
|
+
peak ||= isPeak;
|
|
5127
|
+
notes.push(isPeak ? SESSION_COST_COPY.peakRates : SESSION_COST_COPY.offPeakRates);
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
if (COST_TOKEN_KEYS.some((key) => totals[key] !== count(usage[key])) || !priced) return void 0;
|
|
5131
|
+
const { total, unpricedCacheWriteTokens } = breakdown;
|
|
5132
|
+
partial ||= unpricedCacheWriteTokens > 0;
|
|
5133
|
+
free &&= !partial;
|
|
5134
|
+
if (partial) notes.push("Command Code priced subtotal only; unpriced or other-provider usage excluded");
|
|
5135
|
+
notes.push(SESSION_COST_COPY.approximateNote);
|
|
5136
|
+
const uncachedInput = totals.uncachedInputTokens;
|
|
5137
|
+
const output = totals.outputTokens;
|
|
5138
|
+
const cacheRead = totals.cacheReadTokens;
|
|
5139
|
+
const cacheWrite = totals.cacheWriteTokens;
|
|
5140
|
+
if (uncachedInput === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0) return void 0;
|
|
5141
|
+
if (!free && total <= 0) return void 0;
|
|
5142
|
+
const value = free ? SESSION_COST_COPY.free : `${partial ? "≥" : SESSION_COST_COPY.approximate}${sessionCostAmount(total)}`;
|
|
5143
|
+
const money = (amount) => amount === void 0 ? void 0 : sessionCostAmount(amount);
|
|
5144
|
+
const rows = [
|
|
5145
|
+
{
|
|
5146
|
+
key: "uncachedInput",
|
|
5147
|
+
label: SESSION_COST_COPY.uncachedInput,
|
|
5148
|
+
tokens: uncachedInput,
|
|
5149
|
+
costText: money(breakdown.uncachedInput)
|
|
5150
|
+
},
|
|
5151
|
+
{
|
|
5152
|
+
key: "cacheRead",
|
|
5153
|
+
label: SESSION_COST_COPY.cacheRead,
|
|
5154
|
+
tokens: cacheRead,
|
|
5155
|
+
costText: money(breakdown.cacheRead)
|
|
5156
|
+
},
|
|
5157
|
+
{
|
|
5158
|
+
key: "cacheWrite",
|
|
5159
|
+
label: SESSION_COST_COPY.cacheWrite,
|
|
5160
|
+
tokens: cacheWrite,
|
|
5161
|
+
costText: unpricedCacheWriteTokens === cacheWrite && cacheWrite > 0 ? void 0 : money(breakdown.cacheWrite)
|
|
5162
|
+
},
|
|
5163
|
+
{
|
|
5164
|
+
key: "output",
|
|
5165
|
+
label: SESSION_COST_COPY.output,
|
|
5166
|
+
tokens: output,
|
|
5167
|
+
costText: money(breakdown.output)
|
|
5168
|
+
}
|
|
5169
|
+
];
|
|
5170
|
+
if (unpricedCacheWriteTokens > 0) notes.push(SESSION_COST_COPY.unpricedCacheWrite);
|
|
5171
|
+
return {
|
|
5172
|
+
total,
|
|
5173
|
+
value,
|
|
5174
|
+
title: [
|
|
5175
|
+
`${SESSION_COST_COPY.panelTitle} ${free ? SESSION_COST_COPY.free : sessionCostAmount(total)}`,
|
|
5176
|
+
clause(SESSION_COST_COPY.uncachedInput, uncachedInput),
|
|
5177
|
+
clause(SESSION_COST_COPY.output, output),
|
|
5178
|
+
clause(SESSION_COST_COPY.cacheRead, cacheRead),
|
|
5179
|
+
clause(SESSION_COST_COPY.cacheWrite, cacheWrite),
|
|
5180
|
+
...notes
|
|
5181
|
+
].filter((part) => part !== void 0).join(" · "),
|
|
5182
|
+
rows,
|
|
5183
|
+
notes: [...new Set(notes)],
|
|
5184
|
+
free,
|
|
5185
|
+
peak,
|
|
5186
|
+
unpricedCacheWriteTokens,
|
|
5187
|
+
approximate: !free
|
|
5188
|
+
};
|
|
5189
|
+
}
|
|
5190
|
+
/** Split the appended run into the two nodes the display creates. */
|
|
5191
|
+
function sessionCostPillRun(view) {
|
|
5192
|
+
return {
|
|
5193
|
+
separator: SESSION_COST_COPY.separator,
|
|
5194
|
+
value: view.value
|
|
5195
|
+
};
|
|
5196
|
+
}
|
|
5197
|
+
/**
|
|
5198
|
+
* Decorate the harness's usage dialog, row by row.
|
|
5199
|
+
*
|
|
5200
|
+
* The row sequence mirrors the shipped component's conditions exactly — the
|
|
5201
|
+
* cache-hit row exists while there is billed prompt input, the cache-write row
|
|
5202
|
+
* while those tokens are non-zero — because the display layer matches what it
|
|
5203
|
+
* finds positionally. A shape it cannot confirm is a shape it does not touch.
|
|
5204
|
+
*
|
|
5205
|
+
* A free model decorates nothing: every row's cost is zero by definition, the
|
|
5206
|
+
* pill already says `Free`, and a column of `$0.00` would be noise. A bucket
|
|
5207
|
+
* whose rate the page does not publish is likewise left unfilled rather than
|
|
5208
|
+
* filled with an invented number.
|
|
5209
|
+
*/
|
|
5210
|
+
function sessionCostRowDecorations(view) {
|
|
5211
|
+
const bucket = (key) => view.rows.find((row) => row.key === key)?.tokens ?? 0;
|
|
5212
|
+
const price = (key) => {
|
|
5213
|
+
if (view.free) return void 0;
|
|
5214
|
+
return view.rows.find((row) => row.key === key)?.costText;
|
|
5215
|
+
};
|
|
5216
|
+
const uncachedInput = bucket("uncachedInput");
|
|
5217
|
+
const cacheRead = bucket("cacheRead");
|
|
5218
|
+
const cacheWrite = bucket("cacheWrite");
|
|
5219
|
+
const output = bucket("output");
|
|
5220
|
+
const plan = [];
|
|
5221
|
+
if (uncachedInput + cacheRead + cacheWrite > 0) plan.push({
|
|
5222
|
+
row: "cacheHit",
|
|
5223
|
+
tokens: void 0,
|
|
5224
|
+
amount: void 0,
|
|
5225
|
+
hidden: false
|
|
5226
|
+
});
|
|
5227
|
+
plan.push({
|
|
5228
|
+
row: "uncachedInput",
|
|
5229
|
+
tokens: uncachedInput,
|
|
5230
|
+
amount: price("uncachedInput"),
|
|
5231
|
+
hidden: false
|
|
5232
|
+
});
|
|
5233
|
+
plan.push({
|
|
5234
|
+
row: "cacheRead",
|
|
5235
|
+
tokens: cacheRead,
|
|
5236
|
+
amount: price("cacheRead"),
|
|
5237
|
+
hidden: false
|
|
5238
|
+
});
|
|
5239
|
+
if (cacheWrite !== 0) {
|
|
5240
|
+
const amount = price("cacheWrite");
|
|
5241
|
+
plan.push({
|
|
5242
|
+
row: "cacheWrite",
|
|
5243
|
+
tokens: cacheWrite,
|
|
5244
|
+
amount,
|
|
5245
|
+
hidden: amount === void 0
|
|
5246
|
+
});
|
|
5247
|
+
}
|
|
5248
|
+
plan.push({
|
|
5249
|
+
row: "output",
|
|
5250
|
+
tokens: output,
|
|
5251
|
+
amount: price("output"),
|
|
5252
|
+
hidden: false
|
|
5253
|
+
});
|
|
5254
|
+
return plan;
|
|
5255
|
+
}
|
|
5256
|
+
//#endregion
|
|
5257
|
+
//#region src/client/session-cost-display.ts
|
|
5258
|
+
/** The shipped composer stats row (dsh-client-ui-chat `StatsPills`). */
|
|
5259
|
+
const STATS_ROOT = "[data-composer-stats]";
|
|
5260
|
+
/**
|
|
5261
|
+
* The shipped token-usage dialog's `<dl>`. Unique in the whole of
|
|
5262
|
+
* `dsh-client-ui-chat`: the per-message turn-usage panel has its own dialog with
|
|
5263
|
+
* different markup, so the composer's is unambiguous.
|
|
5264
|
+
*/
|
|
5265
|
+
const USAGE_DIALOG = "[data-session-stats-usage]";
|
|
5266
|
+
/**
|
|
5267
|
+
* The usage dialog's trigger. Both pills announce a dialog, but the token one is
|
|
5268
|
+
* rendered LAST — and when no step carries timing the time pill is not a button
|
|
5269
|
+
* at all, so "last" still selects the token pill in both cases.
|
|
5270
|
+
*/
|
|
5271
|
+
const DIALOG_TRIGGER = "button[aria-haspopup=\"dialog\"]";
|
|
5272
|
+
/**
|
|
5273
|
+
* Stable id for the hidden node the appended cost is described by. The pill's
|
|
5274
|
+
* own `aria-label` is computed by the harness on every render and cannot be
|
|
5275
|
+
* extended, so a description is how the cost reaches assistive technology.
|
|
5276
|
+
*/
|
|
5277
|
+
const A11Y_ID = "dsh-commandcode-session-cost";
|
|
5278
|
+
/** How many consecutive misses before the missing anchor is worth a console line. */
|
|
5279
|
+
const MISSES_BEFORE_WARNING = 3;
|
|
5280
|
+
/**
|
|
5281
|
+
* The default observer: `childList` on `body`, and deliberately NOT `subtree`, so
|
|
5282
|
+
* streaming text inside the page never wakes it while the panel the harness
|
|
5283
|
+
* portals straight onto `body` does.
|
|
5284
|
+
*/
|
|
5285
|
+
const observeBodyChildren = (target, listener) => {
|
|
5286
|
+
if (typeof MutationObserver === "undefined") return void 0;
|
|
5287
|
+
const observer = new MutationObserver(listener);
|
|
5288
|
+
observer.observe(target, { childList: true });
|
|
5289
|
+
return () => observer.disconnect();
|
|
5290
|
+
};
|
|
5291
|
+
/**
|
|
5292
|
+
* Owns the nodes injected into the shipped token-usage UI.
|
|
5293
|
+
*
|
|
5294
|
+
* One instance per mounted entry. `sync()` is cheap and idempotent: it re-reads
|
|
5295
|
+
* both targets on every call, which is what lets it self-heal when React
|
|
5296
|
+
* remounts the row or replaces the dialog, and it rewrites a node only when its
|
|
5297
|
+
* text actually changed.
|
|
5298
|
+
*/
|
|
5299
|
+
var SessionCostDisplay = class {
|
|
5300
|
+
doc;
|
|
5301
|
+
scope;
|
|
5302
|
+
observe;
|
|
5303
|
+
view = void 0;
|
|
5304
|
+
/** Detaches the observer installed by `start()`. */
|
|
5305
|
+
detach = void 0;
|
|
5306
|
+
/** The button we appended into, while it is still connected. */
|
|
5307
|
+
pillHost = void 0;
|
|
5308
|
+
/** Our appended root, and the two nodes whose text changes. */
|
|
5309
|
+
pillRoot = void 0;
|
|
5310
|
+
pillValue = void 0;
|
|
5311
|
+
pillA11y = void 0;
|
|
5312
|
+
/** Whether WE set `aria-describedby` on the button (so only we take it back). */
|
|
5313
|
+
described = false;
|
|
5314
|
+
/** The dialog we decorated, the price nodes we own, and the cells we hid. */
|
|
5315
|
+
dialogHost = void 0;
|
|
5316
|
+
dialogPrices = /* @__PURE__ */ new Map();
|
|
5317
|
+
dialogHidden = /* @__PURE__ */ new Set();
|
|
5318
|
+
pillMisses = 0;
|
|
5319
|
+
constructor(options) {
|
|
5320
|
+
this.doc = options.doc;
|
|
5321
|
+
this.scope = options.scope;
|
|
5322
|
+
this.observe = options.observe ?? observeBodyChildren;
|
|
5323
|
+
}
|
|
5324
|
+
/** Begin watching for the shipped usage dialog opening and closing. */
|
|
5325
|
+
start() {
|
|
5326
|
+
if (this.detach !== void 0) return;
|
|
5327
|
+
const body = this.doc.body;
|
|
5328
|
+
if (body === null || body === void 0) return;
|
|
5329
|
+
this.detach = this.observe(body, () => this.apply());
|
|
5330
|
+
}
|
|
5331
|
+
/**
|
|
5332
|
+
* Hand the display the current figure.
|
|
5333
|
+
*
|
|
5334
|
+
* Undefined — nothing priceable about this session — removes everything this
|
|
5335
|
+
* module injected, leaving both shipped surfaces exactly as they ship.
|
|
5336
|
+
*/
|
|
5337
|
+
sync(view) {
|
|
5338
|
+
this.view = view;
|
|
5339
|
+
this.apply();
|
|
5340
|
+
}
|
|
5341
|
+
/** Remove every injected node and stop observing. Safe to call twice. */
|
|
5342
|
+
dispose() {
|
|
5343
|
+
this.detach?.();
|
|
5344
|
+
this.detach = void 0;
|
|
5345
|
+
this.removePill();
|
|
5346
|
+
this.clearDialog();
|
|
5347
|
+
this.view = void 0;
|
|
5348
|
+
}
|
|
5349
|
+
/** Re-apply the last known view to whatever both targets are right now. */
|
|
5350
|
+
apply() {
|
|
5351
|
+
this.applyPill();
|
|
5352
|
+
this.applyDialog();
|
|
5353
|
+
}
|
|
5354
|
+
applyPill() {
|
|
5355
|
+
const view = this.view;
|
|
5356
|
+
if (view === void 0) {
|
|
5357
|
+
this.removePill();
|
|
5358
|
+
return;
|
|
5359
|
+
}
|
|
5360
|
+
if (this.pillHost?.isConnected !== true) {
|
|
5361
|
+
const button = this.resolvePillButton();
|
|
5362
|
+
if (button === null) {
|
|
5363
|
+
this.pillMisses += 1;
|
|
5364
|
+
if (this.pillMisses === MISSES_BEFORE_WARNING) console.warn(`[dsh-commandcode-provider] no ${STATS_ROOT} row to append the session cost to; the figure stays in the usage dialog only`);
|
|
5365
|
+
return;
|
|
5366
|
+
}
|
|
5367
|
+
this.pillMisses = 0;
|
|
5368
|
+
this.removePill();
|
|
5369
|
+
this.buildPill(button, sessionCostPillRun(view));
|
|
5370
|
+
}
|
|
5371
|
+
if (this.pillValue !== void 0) {
|
|
5372
|
+
const amount = sessionCostPillRun(view).value;
|
|
5373
|
+
if (this.pillValue.textContent !== amount) this.pillValue.textContent = amount;
|
|
5374
|
+
}
|
|
5375
|
+
if (this.pillRoot !== void 0) {
|
|
5376
|
+
if (this.pillRoot.title !== view.title) this.pillRoot.title = view.title;
|
|
5377
|
+
const approximate = view.approximate ? "true" : null;
|
|
5378
|
+
if (this.pillRoot.getAttribute("data-approximate") !== approximate) {
|
|
5379
|
+
if (approximate === null) this.pillRoot.removeAttribute("data-approximate");
|
|
5380
|
+
else this.pillRoot.setAttribute("data-approximate", approximate);
|
|
5381
|
+
}
|
|
5382
|
+
}
|
|
5383
|
+
if (this.pillA11y !== void 0) {
|
|
5384
|
+
const described = `${SESSION_COST_COPY.panelTitle} ${view.value}`;
|
|
5385
|
+
if (this.pillA11y.textContent !== described) this.pillA11y.textContent = described;
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
/** The shipped token pill, scoped to this entry's own composer. */
|
|
5389
|
+
resolvePillButton() {
|
|
5390
|
+
const root = (this.scope() ?? this.doc).querySelector(STATS_ROOT);
|
|
5391
|
+
if (root === null) return null;
|
|
5392
|
+
const triggers = root.querySelectorAll(DIALOG_TRIGGER);
|
|
5393
|
+
return triggers.length === 0 ? null : triggers[triggers.length - 1] ?? null;
|
|
5394
|
+
}
|
|
5395
|
+
/**
|
|
5396
|
+
* Create and append the cost run. The button's child list is static
|
|
5397
|
+
* (`[svg, label]`), so appending once is enough for it to stay last.
|
|
5398
|
+
*/
|
|
5399
|
+
buildPill(button, run) {
|
|
5400
|
+
const doc = this.doc;
|
|
5401
|
+
const root = doc.createElement("span");
|
|
5402
|
+
root.setAttribute("data-composer-session-cost", "");
|
|
5403
|
+
const separator = doc.createElement("span");
|
|
5404
|
+
separator.textContent = run.separator;
|
|
5405
|
+
separator.setAttribute("aria-hidden", "true");
|
|
5406
|
+
separator.style.color = "var(--dsw-alias-separator-primary)";
|
|
5407
|
+
separator.style.margin = "0 6px 0 0";
|
|
5408
|
+
const value = doc.createElement("span");
|
|
5409
|
+
value.textContent = run.value;
|
|
5410
|
+
value.style.fontWeight = "500";
|
|
5411
|
+
const a11y = doc.createElement("span");
|
|
5412
|
+
a11y.id = A11Y_ID;
|
|
5413
|
+
a11y.style.display = "none";
|
|
5414
|
+
root.appendChild(separator);
|
|
5415
|
+
root.appendChild(value);
|
|
5416
|
+
root.appendChild(a11y);
|
|
5417
|
+
button.appendChild(root);
|
|
5418
|
+
if (!button.hasAttribute("aria-describedby")) {
|
|
5419
|
+
button.setAttribute("aria-describedby", A11Y_ID);
|
|
5420
|
+
this.described = true;
|
|
5421
|
+
}
|
|
5422
|
+
this.pillHost = button;
|
|
5423
|
+
this.pillRoot = root;
|
|
5424
|
+
this.pillValue = value;
|
|
5425
|
+
this.pillA11y = a11y;
|
|
5426
|
+
}
|
|
5427
|
+
removePill() {
|
|
5428
|
+
if (this.pillRoot !== void 0 && this.pillRoot.parentNode !== null) this.pillRoot.parentNode.removeChild(this.pillRoot);
|
|
5429
|
+
if (this.described) {
|
|
5430
|
+
this.pillHost?.removeAttribute("aria-describedby");
|
|
5431
|
+
this.described = false;
|
|
5432
|
+
}
|
|
5433
|
+
this.pillHost = void 0;
|
|
5434
|
+
this.pillRoot = void 0;
|
|
5435
|
+
this.pillValue = void 0;
|
|
5436
|
+
this.pillA11y = void 0;
|
|
5437
|
+
}
|
|
5438
|
+
applyDialog() {
|
|
5439
|
+
const view = this.view;
|
|
5440
|
+
const host = view === void 0 ? null : this.resolveDialog();
|
|
5441
|
+
if (host === null || view === void 0) {
|
|
5442
|
+
this.clearDialog();
|
|
5443
|
+
return;
|
|
5444
|
+
}
|
|
5445
|
+
this.pruneDialog(host);
|
|
5446
|
+
const plan = sessionCostRowDecorations(view);
|
|
5447
|
+
const pairs = dialogPairs(host);
|
|
5448
|
+
if (!dialogShapeMatches(pairs, plan, this.dialogPrices)) {
|
|
5449
|
+
this.clearDialog();
|
|
5450
|
+
return;
|
|
5451
|
+
}
|
|
5452
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
5453
|
+
for (const [index, row] of plan.entries()) {
|
|
5454
|
+
const pair = pairs[index];
|
|
5455
|
+
if (pair === void 0) continue;
|
|
5456
|
+
const span = this.dialogPrices.get(pair.dd);
|
|
5457
|
+
if (row.hidden) {
|
|
5458
|
+
if (pair.dt.style.display !== "none") pair.dt.style.display = "none";
|
|
5459
|
+
if (pair.dd.style.display !== "none") pair.dd.style.display = "none";
|
|
5460
|
+
hidden.add(pair.dt);
|
|
5461
|
+
hidden.add(pair.dd);
|
|
5462
|
+
this.removeDialogPrice(pair.dd, span);
|
|
5463
|
+
continue;
|
|
5464
|
+
}
|
|
5465
|
+
if (pair.dt.style.display === "none") pair.dt.style.display = "";
|
|
5466
|
+
if (pair.dd.style.display === "none") pair.dd.style.display = "";
|
|
5467
|
+
if (row.amount === void 0) {
|
|
5468
|
+
this.removeDialogPrice(pair.dd, span);
|
|
5469
|
+
continue;
|
|
5470
|
+
}
|
|
5471
|
+
const price = span ?? this.createDialogPrice(pair.dd, row.row);
|
|
5472
|
+
if (price.parentNode !== pair.dd) pair.dd.appendChild(price);
|
|
5473
|
+
if (price.textContent !== row.amount) price.textContent = row.amount;
|
|
5474
|
+
}
|
|
5475
|
+
for (const cell of [...this.dialogHidden]) {
|
|
5476
|
+
if (hidden.has(cell)) continue;
|
|
5477
|
+
if (cell.style.display === "none") cell.style.display = "";
|
|
5478
|
+
this.dialogHidden.delete(cell);
|
|
5479
|
+
}
|
|
5480
|
+
for (const cell of hidden) this.dialogHidden.add(cell);
|
|
5481
|
+
this.dialogHost = host;
|
|
5482
|
+
}
|
|
5483
|
+
/** Forget the cells and prices a re-rendered dialog took with it. */
|
|
5484
|
+
pruneDialog(host) {
|
|
5485
|
+
for (const [dd, price] of [...this.dialogPrices]) {
|
|
5486
|
+
if (dd.parentNode === host) continue;
|
|
5487
|
+
if (price.parentNode !== null) price.parentNode.removeChild(price);
|
|
5488
|
+
this.dialogPrices.delete(dd);
|
|
5489
|
+
}
|
|
5490
|
+
for (const cell of [...this.dialogHidden]) {
|
|
5491
|
+
if (cell.parentNode === host) continue;
|
|
5492
|
+
if (cell.style.display === "none") cell.style.display = "";
|
|
5493
|
+
this.dialogHidden.delete(cell);
|
|
5494
|
+
}
|
|
5495
|
+
}
|
|
5496
|
+
/** The price node for one shipped value cell, appended as its last child. */
|
|
5497
|
+
createDialogPrice(dd, row) {
|
|
5498
|
+
const price = this.doc.createElement("span");
|
|
5499
|
+
price.setAttribute("data-session-cost-price", row);
|
|
5500
|
+
price.style.marginLeft = "6px";
|
|
5501
|
+
price.style.display = "inline-block";
|
|
5502
|
+
price.style.minWidth = "56px";
|
|
5503
|
+
price.style.textAlign = "right";
|
|
5504
|
+
price.style.fontWeight = "500";
|
|
5505
|
+
dd.appendChild(price);
|
|
5506
|
+
this.dialogPrices.set(dd, price);
|
|
5507
|
+
return price;
|
|
5508
|
+
}
|
|
5509
|
+
removeDialogPrice(dd, price) {
|
|
5510
|
+
if (price === void 0) return;
|
|
5511
|
+
if (price.parentNode !== null) price.parentNode.removeChild(price);
|
|
5512
|
+
this.dialogPrices.delete(dd);
|
|
5513
|
+
}
|
|
5514
|
+
/**
|
|
5515
|
+
* The shipped usage dialog, or null while it is closed.
|
|
5516
|
+
*
|
|
5517
|
+
* The dialog is portaled onto `body`, so unlike the pill it cannot be scoped
|
|
5518
|
+
* from this entry; DSH renders one composer, and the attribute is unique in
|
|
5519
|
+
* the chat client, so a document-level lookup is exact. Should a future build
|
|
5520
|
+
* mount two composers at once, both dialogs would describe whichever session
|
|
5521
|
+
* this entry belongs to — noted rather than defended against.
|
|
5522
|
+
*/
|
|
5523
|
+
resolveDialog() {
|
|
5524
|
+
if (this.dialogHost?.isConnected === true) return this.dialogHost;
|
|
5525
|
+
this.clearDialog();
|
|
5526
|
+
return this.doc.querySelector(USAGE_DIALOG);
|
|
5527
|
+
}
|
|
5528
|
+
/** Give the dialog back: every price removed, every hidden row restored. */
|
|
5529
|
+
clearDialog() {
|
|
5530
|
+
for (const price of this.dialogPrices.values()) if (price.parentNode !== null) price.parentNode.removeChild(price);
|
|
5531
|
+
this.dialogPrices.clear();
|
|
5532
|
+
for (const cell of this.dialogHidden) if (cell.style.display === "none") cell.style.display = "";
|
|
5533
|
+
this.dialogHidden.clear();
|
|
5534
|
+
this.dialogHost = void 0;
|
|
5535
|
+
}
|
|
5536
|
+
};
|
|
5537
|
+
/** The shipped dialog's `dt`/`dd` pairs, in document order. */
|
|
5538
|
+
function dialogPairs(host) {
|
|
5539
|
+
const pairs = [];
|
|
5540
|
+
let label = null;
|
|
5541
|
+
for (const node of Array.from(host.childNodes)) {
|
|
5542
|
+
const tag = tagNameOf(node);
|
|
5543
|
+
if (tag === "DT") label = node;
|
|
5544
|
+
else if (tag === "DD" && label !== null) {
|
|
5545
|
+
pairs.push({
|
|
5546
|
+
dt: label,
|
|
5547
|
+
dd: node
|
|
5548
|
+
});
|
|
5549
|
+
label = null;
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
5552
|
+
return pairs;
|
|
5553
|
+
}
|
|
5554
|
+
/** `tagName` upper-cased, or an empty string for a non-element node. */
|
|
5555
|
+
function tagNameOf(node) {
|
|
5556
|
+
const tag = node.tagName;
|
|
5557
|
+
return typeof tag === "string" ? tag.toUpperCase() : "";
|
|
5558
|
+
}
|
|
5559
|
+
/**
|
|
5560
|
+
* Whether the dialog really holds the rows this session's buckets predict.
|
|
5561
|
+
*
|
|
5562
|
+
* The shipped labels belong to the `chat` locale — they are `Uncached input` in
|
|
5563
|
+
* English and something else entirely in Chinese — so rows are matched by
|
|
5564
|
+
* POSITION, and this is what makes that safe: the row count must agree, the
|
|
5565
|
+
* cache-hit row must be the percentage it is, and every other value must carry
|
|
5566
|
+
* exactly the token count of the bucket predicted for it. A mismatch means the
|
|
5567
|
+
* dialog is not what this view describes, and nothing is decorated.
|
|
5568
|
+
*/
|
|
5569
|
+
function dialogShapeMatches(pairs, plan, prices) {
|
|
5570
|
+
if (pairs.length !== plan.length) return false;
|
|
5571
|
+
return plan.every((row, index) => {
|
|
5572
|
+
const pair = pairs[index];
|
|
5573
|
+
if (pair === void 0) return false;
|
|
5574
|
+
const text = hostValueText(pair.dd, prices.get(pair.dd));
|
|
5575
|
+
if (row.tokens === void 0) return text.includes("%");
|
|
5576
|
+
return digitsOf(text) === String(row.tokens);
|
|
5577
|
+
});
|
|
5578
|
+
}
|
|
5579
|
+
/**
|
|
5580
|
+
* A value cell's own text, without the price this module appended to it — the
|
|
5581
|
+
* digits of `3,206,544 tok` are the host's, the digits of `$0.07` are ours.
|
|
5582
|
+
*/
|
|
5583
|
+
function hostValueText(dd, price) {
|
|
5584
|
+
let text = "";
|
|
5585
|
+
for (const node of Array.from(dd.childNodes)) {
|
|
5586
|
+
if (node === price) continue;
|
|
5587
|
+
text += node.textContent ?? "";
|
|
5588
|
+
}
|
|
5589
|
+
return text;
|
|
5590
|
+
}
|
|
5591
|
+
/** Every digit of a display string, so locale grouping cannot break the match. */
|
|
5592
|
+
function digitsOf(text) {
|
|
5593
|
+
return text.replace(/\D/g, "");
|
|
5594
|
+
}
|
|
5595
|
+
//#endregion
|
|
5596
|
+
//#region src/client/session-cost-view.tsx
|
|
5597
|
+
/** The dock outlet this entry renders inside, i.e. what it can scope itself from. */
|
|
5598
|
+
const DOCK_ANCHOR = "[data-slot=\"conversation.composer.dock\"]";
|
|
5599
|
+
/** Module-level constant so the marker's `style` prop never diffs. */
|
|
5600
|
+
const HIDDEN_STYLE = { display: "none" };
|
|
5601
|
+
/** Log a missing seat once per page, so a silent no-op stays diagnosable. */
|
|
5602
|
+
let warnedMissingSeat = false;
|
|
5603
|
+
/**
|
|
5604
|
+
* The composer's session-cost entry. Guards the two seats before rendering the
|
|
5605
|
+
* mount: a dsh that does not supply the projection seat to dock occupants gets
|
|
5606
|
+
* an absent cost and one console line, never a render crash — which the renderer
|
|
5607
|
+
* would answer by abdicating the entry with no visible trace.
|
|
5608
|
+
*/
|
|
5609
|
+
function CommandCodeSessionCost(props) {
|
|
5610
|
+
if (typeof props.useProjection !== "function" || typeof props.useCommandCodePrices !== "function") {
|
|
5611
|
+
if (!warnedMissingSeat) {
|
|
5612
|
+
warnedMissingSeat = true;
|
|
5613
|
+
console.error("[dsh-commandcode-provider] the composer does not supply the projection/hook seats the session-cost readout needs; the readout stays hidden");
|
|
5614
|
+
}
|
|
5615
|
+
return null;
|
|
5616
|
+
}
|
|
5617
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionCostEntry, { ...props });
|
|
5618
|
+
}
|
|
5619
|
+
/** The mount: one injection lifetime, one hidden marker. Every hook lives here. */
|
|
5620
|
+
function SessionCostEntry(props) {
|
|
5621
|
+
const markerRef = (0, react.useRef)(null);
|
|
5622
|
+
const displayRef = (0, react.useRef)(null);
|
|
5623
|
+
const prices = props.useCommandCodePrices((state) => state);
|
|
5624
|
+
const view = buildSessionCostView({
|
|
5625
|
+
usage: props.useProjection("tokenUsage"),
|
|
5626
|
+
selection: props.useProjection("modelSelection"),
|
|
5627
|
+
facts: props.useProjection("commandCodeCost"),
|
|
5628
|
+
table: prices.table,
|
|
5629
|
+
now: Date.now()
|
|
5630
|
+
});
|
|
5631
|
+
(0, react.useEffect)(() => {
|
|
5632
|
+
if (typeof document === "undefined") return;
|
|
5633
|
+
const display = new SessionCostDisplay({
|
|
5634
|
+
doc: document,
|
|
5635
|
+
scope: () => markerRef.current?.closest(DOCK_ANCHOR)?.parentElement ?? null
|
|
5636
|
+
});
|
|
5637
|
+
displayRef.current = display;
|
|
5638
|
+
display.start();
|
|
5639
|
+
return () => {
|
|
5640
|
+
displayRef.current = null;
|
|
5641
|
+
display.dispose();
|
|
5642
|
+
};
|
|
5643
|
+
}, []);
|
|
5644
|
+
(0, react.useEffect)(() => {
|
|
5645
|
+
displayRef.current?.sync(view);
|
|
5646
|
+
});
|
|
5647
|
+
if (view === void 0) return null;
|
|
5648
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5649
|
+
ref: markerRef,
|
|
5650
|
+
style: HIDDEN_STYLE,
|
|
5651
|
+
"data-ccp-session-cost-anchor": ""
|
|
5652
|
+
});
|
|
5653
|
+
}
|
|
5654
|
+
//#endregion
|
|
5655
|
+
//#region src/client/panel-styles.ts
|
|
5656
|
+
/**
|
|
5657
|
+
* Stylesheet for the Command Code plans & quota panel (the sidebar footer card
|
|
5658
|
+
* and the dashboard it opens).
|
|
5659
|
+
*
|
|
5660
|
+
* Returned as a string rather than injected here so the modules stay free of
|
|
5661
|
+
* DOM side effects at import time — the client entry installs it once, keyed
|
|
5662
|
+
* by the same `data-plugin-css` attribute the settings-page stylesheet uses,
|
|
5663
|
+
* and removes it again when the plugin's fiber unwinds.
|
|
5664
|
+
*
|
|
5665
|
+
* Every colour comes from a harness theme alias with a neutral fallback, so
|
|
5666
|
+
* the panel follows the active theme (light/dark and any brand pack) without
|
|
5667
|
+
* hardcoded values. Classes are `ccp-` prefixed to stay clear of the settings
|
|
5668
|
+
* page's `cc-` set.
|
|
5669
|
+
*
|
|
5670
|
+
* @module dsh-commandcode-provider/client/panel-styles
|
|
5671
|
+
*/
|
|
5672
|
+
/**
|
|
5673
|
+
* Stylesheet id (the `data-plugin-css` value that makes injection idempotent).
|
|
5674
|
+
*
|
|
5675
|
+
* The package prefix must match the one the settings-page stylesheet uses
|
|
5676
|
+
* (`injectPageCss` in `./index.ts`) and this package's real name: an id is the
|
|
5677
|
+
* injection's identity, so a stale fork prefix would let a second copy of the
|
|
5678
|
+
* plugin inject the same rules twice and would misreport the owner in the DOM.
|
|
5679
|
+
*/
|
|
5680
|
+
const PANEL_CSS_ID = "@mars-sea/dsh-commandcode-provider/CommandCodePanel.module.css";
|
|
5681
|
+
/** The panel stylesheet. */
|
|
5682
|
+
const PANEL_CSS = `
|
|
5683
|
+
/* ------------------------------------------------- sidebar footer card */
|
|
5684
|
+
/* The shell's foot area renders this list ABOVE the Settings seat, so the card
|
|
5685
|
+
is the sidebar's bottom-most content. The shell supplies no chrome: the entry
|
|
5686
|
+
is the button. It is deliberately quiet — a surface that sits beside Settings
|
|
5687
|
+
should read as part of the column, not as a call to action — with one hover
|
|
5688
|
+
step and a hairline border.
|
|
5689
|
+
|
|
5690
|
+
The shell's container is a flex ROW whose occupants (this card and ui-cordis's
|
|
5691
|
+
footer chip) each declare a full-width line and shrink-proof flex, so as a row
|
|
5692
|
+
it would overflow the column. Both were written for a full-width line, which
|
|
5693
|
+
is exactly what a column gives them. Matched by the CSS-module class STEM —
|
|
5694
|
+
never a hashed name — so a dsh that renames it degrades to the shell's own
|
|
5695
|
+
row rather than breaking. */
|
|
5696
|
+
[class*="_footerActions"]{flex-direction:column}
|
|
5697
|
+
.ccp-foot{box-sizing:border-box;flex:0 0 auto;width:100%;min-width:0;font:inherit;color:var(--dsw-alias-label-secondary);text-align:left;cursor:pointer;background:0 0;border:1px solid transparent;border-radius:10px;flex-direction:column;gap:6px;margin:0 0 4px;padding:8px;display:flex}
|
|
5698
|
+
.ccp-foot:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l2)}
|
|
5699
|
+
.ccp-foot:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
5700
|
+
.ccp-footTop{align-items:center;gap:8px;min-width:0;display:flex}
|
|
5701
|
+
.ccp-footName{white-space:nowrap;text-overflow:ellipsis;color:var(--dsw-alias-label-primary);min-width:0;overflow:hidden;font-size:13px;font-weight:500;line-height:20px}
|
|
5702
|
+
/* One block per quota window: a head line carrying the window's own spend and
|
|
5703
|
+
limit, then the FULL-WIDTH bar under it. Stacking the two lets the card show
|
|
5704
|
+
the dollar figures — the reason this surface exists — without squeezing the
|
|
5705
|
+
bar into what is left beside them. Mirrors the dashboard's own window block. */
|
|
5706
|
+
.ccp-footRow{flex-direction:column;gap:4px;min-width:0;display:flex}
|
|
5707
|
+
.ccp-footHead{align-items:baseline;gap:8px;min-width:0;display:flex}
|
|
5708
|
+
.ccp-footLabel{flex:1;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
|
|
5709
|
+
.ccp-footAmount{flex:none;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
5710
|
+
/* The card's markup must stay PHRASING content — it renders inside the shell's
|
|
5711
|
+
own button — so these bars are spans, not divs. That makes display:block
|
|
5712
|
+
load-bearing on BOTH: an inline box ignores width and height outright, so
|
|
5713
|
+
without it the 5px track still painted (a flex item is blockified by its
|
|
5714
|
+
container) while the fill collapsed to 0x0 and the bar showed no usage. */
|
|
5715
|
+
.ccp-footBar{display:block;background:var(--dsw-alias-bg-layer-2);border-radius:999px;height:5px;overflow:hidden}
|
|
5716
|
+
.ccp-footFill{display:block;background:var(--dsw-alias-brand-primary);border-radius:999px;height:100%;transition:width .3s ease}
|
|
5717
|
+
.ccp-footFillWarn{background:var(--dsw-alias-state-error-primary)}
|
|
5718
|
+
.ccp-footPct{flex:none;width:34px;color:var(--dsw-alias-label-secondary);text-align:right;font-size:11px;line-height:16px;font-variant-numeric:tabular-nums}
|
|
5719
|
+
|
|
5720
|
+
/* The 56px rail: one icon button on the shell's own rail geometry (36px cell),
|
|
5721
|
+
so the collapsed column keeps a single 18px glyph like its siblings. */
|
|
5722
|
+
.ccp-railButton{box-sizing:border-box;width:36px;height:36px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:1px solid transparent;border-radius:8px;flex:none;justify-content:center;align-items:center;margin:0 0 4px;padding:0;display:inline-flex}
|
|
5723
|
+
.ccp-railButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
5724
|
+
.ccp-railButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
5725
|
+
|
|
5726
|
+
/* The ring glyph. Sized entirely by its own width/height attribute, so the
|
|
5727
|
+
footer row, the rail button and the dashboard can each ask for their own. */
|
|
5728
|
+
.ccp-glyph{flex:none;justify-content:center;align-items:center;display:inline-flex;color:var(--dsw-alias-brand-primary)}
|
|
5729
|
+
|
|
5730
|
+
/* ------------------------------------------------------------ dashboard */
|
|
5731
|
+
/* The center column in the layout frame: fill it, scroll the content column,
|
|
5732
|
+
and cap the reading width like the harness's own panels. */
|
|
5733
|
+
.ccp-main{background:var(--dsw-alias-bg-layer-1);width:100%;height:100%;overflow:auto;display:block}
|
|
5734
|
+
.ccp-mainInner{max-width:760px;margin:0 auto;padding:24px 20px 40px;flex-direction:column;gap:14px;display:flex;color:var(--dsw-alias-label-primary)}
|
|
5735
|
+
.ccp-header{align-items:center;gap:10px;display:flex;flex-wrap:wrap}
|
|
5736
|
+
.ccp-headerText{flex-direction:column;gap:2px;display:flex;min-width:0}
|
|
5737
|
+
.ccp-title{margin:0;font-size:18px;font-weight:600;line-height:1.4}
|
|
5738
|
+
.ccp-subtitle{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
|
|
5739
|
+
.ccp-spacer{flex:1}
|
|
5740
|
+
.ccp-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;font-variant-numeric:tabular-nums}
|
|
5741
|
+
.ccp-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
|
|
5742
|
+
|
|
5743
|
+
/* Notices: the no-key guidance, a blocked report, and a stale-data error. */
|
|
5744
|
+
.ccp-notice{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:12px 14px;flex-direction:column;gap:4px;display:flex}
|
|
5745
|
+
.ccp-noticeError{border-color:var(--dsw-alias-state-error-primary)}
|
|
5746
|
+
.ccp-noticeTitle{margin:0;font-size:13px;font-weight:600;line-height:1.5}
|
|
5747
|
+
.ccp-noticeError .ccp-noticeTitle{color:var(--dsw-alias-state-error-primary)}
|
|
5748
|
+
.ccp-noticeHint{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.55}
|
|
5749
|
+
.ccp-noticeDetail{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;word-break:break-word}
|
|
5750
|
+
|
|
5751
|
+
.ccp-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:14px;padding:16px 18px;flex-direction:column;gap:16px;display:flex}
|
|
5752
|
+
.ccp-cardHead{align-items:center;gap:10px;display:flex;flex-wrap:wrap}
|
|
5753
|
+
.ccp-avatar{flex:none;width:28px;height:28px;color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-module-platform);border-radius:50%;justify-content:center;align-items:center;font-size:12px;font-weight:600;line-height:1;display:inline-flex}
|
|
5754
|
+
.ccp-cardIdentity{flex-direction:column;gap:1px;min-width:0;display:flex}
|
|
5755
|
+
.ccp-cardTitle{font-size:13px;font-weight:600;line-height:1.4}
|
|
5756
|
+
.ccp-cardOwner{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px}
|
|
5757
|
+
.ccp-block{flex-direction:column;gap:8px;display:flex}
|
|
5758
|
+
.ccp-blockTitle{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:600;line-height:1.5;text-transform:uppercase;letter-spacing:.04em}
|
|
5759
|
+
.ccp-planRow{align-items:center;gap:8px;display:flex;flex-wrap:wrap}
|
|
5760
|
+
.ccp-fieldLabel{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
|
|
5761
|
+
.ccp-planName{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:1.5}
|
|
5762
|
+
|
|
5763
|
+
/* Stat tiles: the monthly credits and the usage totals share one grid. */
|
|
5764
|
+
.ccp-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px}
|
|
5765
|
+
.ccp-tile{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:8px;padding:8px 10px;flex-direction:column;gap:2px;display:flex;min-width:0}
|
|
5766
|
+
.ccp-tileLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5}
|
|
5767
|
+
.ccp-tileValue{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4;font-variant-numeric:tabular-nums}
|
|
5768
|
+
.ccp-tileSub{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
5769
|
+
|
|
5770
|
+
/* Quota bars: the monthly bar and the two windows stack in one column, each a
|
|
5771
|
+
label row plus the track. */
|
|
5772
|
+
.ccp-windows{flex-direction:column;gap:14px;display:flex}
|
|
5773
|
+
.ccp-window{flex-direction:column;gap:6px;display:flex}
|
|
5774
|
+
.ccp-windowHead{align-items:baseline;gap:8px;display:flex}
|
|
5775
|
+
.ccp-windowLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:1.5}
|
|
5776
|
+
.ccp-windowValue{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
5777
|
+
.ccp-windowPct{color:var(--dsw-alias-label-primary);min-width:38px;text-align:right;font-size:12px;font-weight:600;line-height:1.5;font-variant-numeric:tabular-nums}
|
|
5778
|
+
.ccp-warnTag{white-space:nowrap;background:var(--dsw-alias-state-warn-tertiary,var(--dsw-alias-bg-module-platform));color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-label-secondary));border-radius:999px;padding:0 8px;font-size:11px;font-weight:600;line-height:17px}
|
|
5779
|
+
.ccp-bar{overflow:hidden;background:var(--dsw-alias-bg-layer-1);border-radius:999px;height:8px}
|
|
5780
|
+
.ccp-barFill{background:var(--dsw-alias-brand-primary);border-radius:999px;height:100%;transition:width .3s ease}
|
|
5781
|
+
.ccp-barFillWarn{background:var(--dsw-alias-state-error-primary)}
|
|
5782
|
+
.ccp-windowReset{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}
|
|
5783
|
+
|
|
5784
|
+
/* Badges. */
|
|
5785
|
+
.ccp-badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-brand-primary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:600;line-height:17px}
|
|
5786
|
+
.ccp-badgeError{background:transparent;color:var(--dsw-alias-state-error-primary)}
|
|
5787
|
+
.ccp-badgeWarn{background:var(--dsw-alias-state-warn-tertiary,var(--dsw-alias-bg-module-platform));color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-label-secondary))}
|
|
5788
|
+
.ccp-badgeMuted{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;max-width:220px;overflow:hidden;text-overflow:ellipsis}
|
|
5789
|
+
|
|
5790
|
+
/* Account switch: plain buttons, like the settings page's usage carousel. */
|
|
5791
|
+
.ccp-tabs{flex-wrap:wrap;gap:6px;display:flex}
|
|
5792
|
+
.ccp-tab{align-items:center;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex;gap:6px}
|
|
5793
|
+
.ccp-tab:hover:not(.ccp-tabActive){color:var(--dsw-alias-label-primary)}
|
|
5794
|
+
.ccp-tabActive{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-brand-primary)}
|
|
5795
|
+
|
|
5796
|
+
@media (prefers-reduced-motion:reduce){.ccp-footFill,.ccp-barFill{transition:none}}
|
|
5797
|
+
`;
|
|
5798
|
+
//#endregion
|
|
5799
|
+
//#region src/client/locales.ts
|
|
5800
|
+
const zh = {
|
|
5801
|
+
nav: "Command Code",
|
|
5802
|
+
title: "Command Code",
|
|
5803
|
+
intro: "配置 Command Code Provider 连接。API 密钥仅保存在本机凭据服务中,不会回显;其他字段写入用户设置,下次请求即生效。",
|
|
5804
|
+
apiKey: "API 密钥",
|
|
5805
|
+
apiKeyHint: "在 commandcode.ai 控制台创建。留空保存不会覆盖已存储的密钥。",
|
|
5806
|
+
apiKeySet: "已配置",
|
|
5807
|
+
apiKeyUnset: "未配置",
|
|
5808
|
+
apiKeyLocked: "密钥由只读来源提供",
|
|
5809
|
+
apiBase: "API 地址",
|
|
5810
|
+
apiBaseHint: "默认 https://api.commandcode.ai,一般无需修改。",
|
|
5811
|
+
workingDir: "工作目录",
|
|
5812
|
+
workingDirHint: "可选。留空时使用占位符显示的进程工作目录;仅在需要固定路径时填写。",
|
|
5813
|
+
requestTimeoutMs: "请求超时(毫秒)",
|
|
5814
|
+
requestTimeoutMsHint: "等待响应首个字节的超时;默认 60000。",
|
|
5815
|
+
streamIdleTimeoutMs: "流空闲超时(毫秒)",
|
|
5816
|
+
streamIdleTimeoutMsHint: "生成流停滞多久视为断连;默认 300000(长思考模型可静默数分钟,默认值刻意放宽)。",
|
|
5817
|
+
advancedSettings: "高级设置",
|
|
5818
|
+
advancedSettingsHint: "API 地址、工作目录、超时、模型过滤、联网搜索与侧边栏额度卡片等不常修改的选项。",
|
|
5819
|
+
advancedOverriddenOne: "已自定义 1 项",
|
|
5820
|
+
advancedOverriddenMany: "已自定义 {count} 项",
|
|
5821
|
+
advancedInvalid: "高级设置中有未填好的数字,请展开修正后再保存。",
|
|
5822
|
+
filterModelsByPlan: "隐藏套餐外模型",
|
|
5823
|
+
filterModelsByPlanHint: "开启后,模型选择器只列出当前套餐可用的模型;账户持有按需余额时会显示全部。",
|
|
5824
|
+
webSearch: "用 Command Code 承载联网搜索",
|
|
5825
|
+
webSearchHint: "开启后,dsh 的 web_search 工具由 Command Code 承担(复用同一个 API key 与地址),并优先于其他搜索后端;关闭则把选择权交还给之前的后端(如 modsearch),而不是强制回退到 DeepSeek 搜索。",
|
|
5826
|
+
showSidebarQuota: "在侧边栏显示额度卡片",
|
|
5827
|
+
showSidebarQuotaHint: "开启后,侧边栏底部(Settings 上方)显示 Command Code 套餐与配额卡片,点击可在中间栏打开完整仪表盘。默认关闭:左侧不显示任何额度信息,也不会为其后台刷新用量。保存后立即生效。",
|
|
5828
|
+
accountsTitle: "多账户轮换",
|
|
5829
|
+
accountsHint: "当前账户达到用量限额(429)或密钥失效(401)时,请求自动切换到下一个账户;全部耗尽时会提示最早的重置时间。",
|
|
5830
|
+
accountAdd: "添加账户",
|
|
5831
|
+
accountRemove: "移除",
|
|
5832
|
+
accountLabel: "账户备注名",
|
|
5833
|
+
accountKey: "API 密钥",
|
|
5834
|
+
accountKeyHint: "该账户的 API 密钥。留空保存不会覆盖已存储的密钥。",
|
|
5835
|
+
accountDefault: "默认账户",
|
|
5836
|
+
activeAccount: "当前使用账户",
|
|
5837
|
+
activeAccountAuto: "自动(第一个可用账户)",
|
|
5838
|
+
activeAccountHint: "手动指定优先使用的账户,保存后下次请求即生效;所选账户耗尽时仍会自动切换到其他可用账户。",
|
|
5839
|
+
rulesTitle: "按模型切换账户",
|
|
5840
|
+
rulesHint: "选择模型并路由到某个账户(可多选)。命中规则的模型且该账户可用时优先使用;账户耗尽或密钥失效时仍自动回落到其他账户。规则按列表顺序匹配,第一条命中生效。",
|
|
5841
|
+
rulesEmpty: "尚未配置规则。",
|
|
5842
|
+
rulesCatalogFailed: "模型目录获取失败,暂时无法选择模型;已保存的规则仍会生效。",
|
|
5843
|
+
ruleAdd: "添加规则",
|
|
5844
|
+
ruleRemove: "移除",
|
|
5845
|
+
ruleModel: "模型",
|
|
5846
|
+
ruleModelPick: "选择模型…",
|
|
5847
|
+
ruleModelCount: "已选 {count} 个模型",
|
|
5848
|
+
ruleAccount: "目标账户",
|
|
5849
|
+
ruleHint: "从下拉列表勾选要路由的模型(可多选),再选择目标账户。",
|
|
5850
|
+
modelSearchPlaceholder: "搜索模型…",
|
|
5851
|
+
modelSearchEmpty: "没有匹配的模型。",
|
|
5852
|
+
modelStale: "已下架",
|
|
5853
|
+
visibleModelsTitle: "模型白名单",
|
|
5854
|
+
visibleModelsHint: "勾选要保留的模型,模型选择器就只列出这些;一个都不勾选时则显示全部模型。保存后,下次打开模型选择器生效。",
|
|
5855
|
+
visibleModelsPick: "选择要保留的模型…",
|
|
5856
|
+
visibleModelsCount: "已选 {count} 个模型",
|
|
5857
|
+
visibleModelsShowAll: "显示全部",
|
|
5858
|
+
visibleModelsStaleHint: "有 {count} 个已选模型在目录中找不到了(可能已下架),不影响其他模型;可清理或保留。",
|
|
5859
|
+
visibleModelsCleanStale: "清理失效({count})",
|
|
5860
|
+
overridden: "已覆盖",
|
|
5861
|
+
reset: "重置",
|
|
5862
|
+
invalidNumber: "无效数字",
|
|
5863
|
+
numberTooSmall: "不能小于 1(毫秒)",
|
|
5864
|
+
numberTooLarge: "超出允许上限(2147483647 毫秒)",
|
|
5865
|
+
readOnly: "当前配置为只读。",
|
|
5866
|
+
unsaved: "未保存",
|
|
5867
|
+
save: "保存",
|
|
5868
|
+
saving: "保存中",
|
|
5869
|
+
saved: "已保存 ✓",
|
|
5870
|
+
saveFailed: "保存失败,请重试。",
|
|
5871
|
+
discard: "放弃",
|
|
5872
|
+
cancel: "取消",
|
|
5873
|
+
show: "显示",
|
|
5874
|
+
hide: "隐藏",
|
|
5875
|
+
usageTitle: "账户用量",
|
|
5876
|
+
usageRefresh: "刷新",
|
|
5877
|
+
usageRefreshing: "刷新中…",
|
|
5878
|
+
usageLoading: "正在获取账户用量…",
|
|
5879
|
+
usageNoKey: "配置 API 密钥后,这里会显示账户的用量与额度状态。",
|
|
5880
|
+
usageError: "用量获取失败",
|
|
5881
|
+
usageRequests: "请求",
|
|
5882
|
+
usageFailed: "失败",
|
|
5883
|
+
usageSuccessRate: "成功率",
|
|
5884
|
+
usageCost: "花费",
|
|
5885
|
+
usageTokens: "Token",
|
|
5886
|
+
usageTokensIn: "入",
|
|
5887
|
+
usageTokensOut: "出",
|
|
5888
|
+
usageMonthly: "月额度",
|
|
5889
|
+
usagePurchased: "已购",
|
|
5890
|
+
usageFree: "赠送",
|
|
5891
|
+
usageFiveHour: "5 小时窗口",
|
|
5892
|
+
usageWeekly: "每周窗口",
|
|
5893
|
+
usageExceeded: "已超限",
|
|
5894
|
+
usageReset: "重置于",
|
|
5895
|
+
usagePartial: "部分端点数据不可用",
|
|
5896
|
+
usageKeyClear: "清除已存密钥",
|
|
5897
|
+
usageKeyClearStaged: "将清除(保存后生效)",
|
|
5898
|
+
usageUndoKeyClear: "撤销清除",
|
|
5899
|
+
usageKeyInvalid: "API 密钥无效或已过期",
|
|
5900
|
+
usageKeyInvalidHint: "服务端拒绝了全部请求(401)。请检查该账户配置的密钥,或到 commandcode.ai 控制台重新生成。",
|
|
5901
|
+
usageServiceUnavailable: "Command Code 服务暂时不可用",
|
|
5902
|
+
usageServiceUnavailableHint: "服务端返回了错误(5xx),稍后点击刷新重试。",
|
|
5903
|
+
usageNetworkError: "无法连接 Command Code 服务",
|
|
5904
|
+
usageNetworkHint: "所有请求都没有到达服务端。请检查网络连接或 API 地址设置。",
|
|
5905
|
+
usageUpdated: "更新于",
|
|
5906
|
+
usagePeriodEnd: "账期截止",
|
|
5907
|
+
usageActive: "当前使用",
|
|
5908
|
+
usageCooldown: "限额冷却中",
|
|
5909
|
+
usageInvalidKey: "密钥无效",
|
|
5910
|
+
usageUnconfigured: "该账户尚未配置 API 密钥。",
|
|
5911
|
+
updateAvailable: "可更新",
|
|
5912
|
+
updateHint: "已发布新版本,点击查看发布说明;更新插件后刷新本页,提示会自动消失。",
|
|
5913
|
+
loginTitle: "通过官方登录获取密钥",
|
|
5914
|
+
loginHintIdle: "不想手动创建密钥?点击登录后浏览器会打开 commandcode.ai 授权页,完成后密钥自动写入本机凭据服务,下次请求即生效。",
|
|
5915
|
+
loginButton: "登录 Command Code",
|
|
5916
|
+
loginStarting: "正在启动本地回调服务…",
|
|
5917
|
+
loginWaiting: "等待在浏览器中完成授权…",
|
|
5918
|
+
loginOpenLink: "打开授权页面 ↗",
|
|
5919
|
+
loginCancel: "取消登录",
|
|
5920
|
+
loginSuccess: "已登录为",
|
|
5921
|
+
loginUnavailable: "此环境暂不支持登录流程,请手动粘贴密钥。",
|
|
5922
|
+
loginDenied: "授权被拒绝。可重试,或手动粘贴密钥。",
|
|
5923
|
+
loginTimeout: "等待超时:未在窗口期内收到授权回调,请重试。",
|
|
5924
|
+
loginInvalidKey: "获取到的密钥未通过校验(401),请重试或手动粘贴。",
|
|
5925
|
+
loginNetwork: "无法连接 Command Code 服务校验密钥,请检查网络后重试。",
|
|
5926
|
+
loginStoreFailed: "密钥无法写入本机凭据服务,请手动粘贴。",
|
|
5927
|
+
loginCancelled: "登录已取消。",
|
|
5928
|
+
loginFailedGeneric: "登录失败,请重试或手动粘贴密钥。",
|
|
5929
|
+
cardTitle: "Command Code",
|
|
5930
|
+
cardRouteActive: "已启用",
|
|
5931
|
+
cardLoadingHint: "正在读取 Command Code 配置…",
|
|
5932
|
+
cardRegistrationHint: "此卡片随 Command Code 插件注册,需要较新版本的 DeepSeek Harness 才会显示完整内容。"
|
|
5933
|
+
};
|
|
5934
|
+
const en = {
|
|
5935
|
+
nav: "Command Code",
|
|
5936
|
+
title: "Command Code",
|
|
5937
|
+
intro: "Configure the Command Code Provider connection. The API key is stored only in the local credential service and never echoed; other fields are written to user settings and take effect on the next request.",
|
|
5938
|
+
apiKey: "API key",
|
|
5939
|
+
apiKeyHint: "Create one in the commandcode.ai console. Saving with this field blank keeps the stored key.",
|
|
5940
|
+
apiKeySet: "Configured",
|
|
5941
|
+
apiKeyUnset: "Not configured",
|
|
5942
|
+
apiKeyLocked: "Key provided by a read-only source",
|
|
5943
|
+
apiBase: "API base URL",
|
|
5944
|
+
apiBaseHint: "Defaults to https://api.commandcode.ai; usually leave as-is.",
|
|
5945
|
+
workingDir: "Working directory",
|
|
5946
|
+
workingDirHint: "Optional. Leave blank to use the process cwd shown as the placeholder; fill in only to pin a specific path.",
|
|
5947
|
+
requestTimeoutMs: "Request timeout (ms)",
|
|
5948
|
+
requestTimeoutMsHint: "Time to wait for the first response byte; default 60000.",
|
|
5949
|
+
streamIdleTimeoutMs: "Stream idle timeout (ms)",
|
|
5950
|
+
streamIdleTimeoutMsHint: "How long a stalled stream is treated as dead; default 300000 (deliberately generous — long-thinking models can stay silent for minutes).",
|
|
5951
|
+
advancedSettings: "Advanced",
|
|
5952
|
+
advancedSettingsHint: "Rarely touched options: API base URL, working directory, timeouts, model filtering, web search, and the sidebar quota card.",
|
|
5953
|
+
advancedOverriddenOne: "1 customized",
|
|
5954
|
+
advancedOverriddenMany: "{count} customized",
|
|
5955
|
+
advancedInvalid: "A number in Advanced settings is not ready to save; expand to fix it.",
|
|
5956
|
+
filterModelsByPlan: "Hide out-of-plan models",
|
|
5957
|
+
filterModelsByPlanHint: "When on, the model picker lists only models your subscription includes; any on-demand credit balance shows the full catalog.",
|
|
5958
|
+
webSearch: "Serve dsh web search with Command Code",
|
|
5959
|
+
webSearchHint: "When on, the model-facing web_search tool is backed by Command Code (same API key and base URL as chat), winning over other search backends. Off hands the selection back to the previous backend (e.g. modsearch) instead of forcing the shipped DeepSeek search.",
|
|
5960
|
+
showSidebarQuota: "Show the quota card in the sidebar",
|
|
5961
|
+
showSidebarQuotaHint: "When on, a Command Code plan & quota card sits at the bottom of the sidebar (above Settings) and opens the full dashboard in the centre column. Off by default: nothing is shown on the left, and no background usage poll runs for it. Applies on save.",
|
|
5962
|
+
accountsTitle: "Account rotation",
|
|
5963
|
+
accountsHint: "When the active account hits its usage limit (429) or its key fails (401), requests switch to the next account; when every account is exhausted the error names the earliest window reset.",
|
|
5964
|
+
accountAdd: "Add account",
|
|
5965
|
+
accountRemove: "Remove",
|
|
5966
|
+
accountLabel: "Account label",
|
|
3895
5967
|
accountKey: "API key",
|
|
3896
5968
|
accountKeyHint: "This account’s API key. Saving with the field blank keeps the stored key.",
|
|
3897
5969
|
accountDefault: "Default account",
|
|
@@ -4134,6 +6206,38 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
|
|
|
4134
6206
|
document.head.appendChild(tag);
|
|
4135
6207
|
}
|
|
4136
6208
|
/**
|
|
6209
|
+
* Install the plans & quota panel's stylesheet and return its disposer, for
|
|
6210
|
+
* `ctx.effect` to own. Keyed by its own `data-plugin-css` id, so the injection
|
|
6211
|
+
* is idempotent even if a second surface asks for it later.
|
|
6212
|
+
*/
|
|
6213
|
+
function injectPanelCss() {
|
|
6214
|
+
if (typeof document === "undefined") return () => {};
|
|
6215
|
+
if (document.querySelector(`style[data-plugin-css="@mars-sea/dsh-commandcode-provider/CommandCodePanel.module.css"]`) !== null) return () => {};
|
|
6216
|
+
const tag = document.createElement("style");
|
|
6217
|
+
tag.dataset.plugin = "@mars-sea/dsh-commandcode-provider";
|
|
6218
|
+
tag.dataset.pluginCss = PANEL_CSS_ID;
|
|
6219
|
+
tag.textContent = PANEL_CSS;
|
|
6220
|
+
document.head.appendChild(tag);
|
|
6221
|
+
return () => {
|
|
6222
|
+
tag.remove();
|
|
6223
|
+
};
|
|
6224
|
+
}
|
|
6225
|
+
/**
|
|
6226
|
+
* Plans & quota panel id. It is the layout's `MainPanelId`: one string shared
|
|
6227
|
+
* by the `sidebar.footer.action` card and the `main` slot cell, so the card
|
|
6228
|
+
* selects this panel and nothing else. `dsh-client-ui-layout` is not a
|
|
6229
|
+
* dependency of this bundle (its type is only a brand over `string`), so the
|
|
6230
|
+
* brand is applied at the call site instead of importing the package.
|
|
6231
|
+
*/
|
|
6232
|
+
const PANEL_ID = "commandcode-panel";
|
|
6233
|
+
/**
|
|
6234
|
+
* The composer figure's entry id in `conversation.composer.dock`. Its own id,
|
|
6235
|
+
* not the shipped `stats` cell's: reusing `stats` would REPLACE the tokens /
|
|
6236
|
+
* cache-hit / throughput readout rather than inject into it, and that readout
|
|
6237
|
+
* is the harness's to format (see `./session-cost-display.ts`).
|
|
6238
|
+
*/
|
|
6239
|
+
const SESSION_COST_ID = "commandcode-session-cost";
|
|
6240
|
+
/**
|
|
4137
6241
|
* Client plugin body. Gates on the services shared by both client generations
|
|
4138
6242
|
* (`slots`, `locale`, `connection`, `remote`, `settingsScope`). Current clients
|
|
4139
6243
|
* mount the page after `remote.credentials` appears; legacy clients mount it
|
|
@@ -4176,11 +6280,13 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
|
|
|
4176
6280
|
}), "dsh-commandcode-provider: credential invalidations");
|
|
4177
6281
|
let usageNamespace;
|
|
4178
6282
|
let usageMountError;
|
|
6283
|
+
let pricesController;
|
|
4179
6284
|
const contribution = {
|
|
4180
6285
|
package: USAGE_REMOTE_CONTRIBUTION.package,
|
|
4181
6286
|
descriptors: [
|
|
4182
6287
|
...USAGE_REMOTE_CONTRIBUTION.descriptors,
|
|
4183
6288
|
...MODELS_REMOTE_CONTRIBUTION.descriptors,
|
|
6289
|
+
...PRICES_REMOTE_CONTRIBUTION.descriptors,
|
|
4184
6290
|
...LOGIN_REMOTE_CONTRIBUTION.descriptors
|
|
4185
6291
|
]
|
|
4186
6292
|
};
|
|
@@ -4196,6 +6302,7 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
|
|
|
4196
6302
|
ctx.inject(["remote.commandcode"], (namespaceCtx) => {
|
|
4197
6303
|
usageNamespace = namespaceCtx.remote.commandcode;
|
|
4198
6304
|
controller.refreshCatalog();
|
|
6305
|
+
pricesController?.reload();
|
|
4199
6306
|
namespaceCtx.effect(() => () => {
|
|
4200
6307
|
usageNamespace = void 0;
|
|
4201
6308
|
}, "dsh-commandcode-provider: usage namespace");
|
|
@@ -4225,6 +6332,22 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
|
|
|
4225
6332
|
error: { message: usageMountError ?? "commandcode/models remote is not mounted" }
|
|
4226
6333
|
};
|
|
4227
6334
|
return namespace.models();
|
|
6335
|
+
},
|
|
6336
|
+
prices: async () => {
|
|
6337
|
+
const namespace = usageNamespace;
|
|
6338
|
+
if (namespace === void 0) return {
|
|
6339
|
+
ok: false,
|
|
6340
|
+
error: { message: usageMountError ?? "commandcode/prices remote is not mounted" }
|
|
6341
|
+
};
|
|
6342
|
+
const call = namespace.prices;
|
|
6343
|
+
if (typeof call !== "function") return {
|
|
6344
|
+
ok: false,
|
|
6345
|
+
error: {
|
|
6346
|
+
message: "the Host serves no commandcode/prices endpoint",
|
|
6347
|
+
permanent: true
|
|
6348
|
+
}
|
|
6349
|
+
};
|
|
6350
|
+
return call.call(namespace);
|
|
4228
6351
|
}
|
|
4229
6352
|
};
|
|
4230
6353
|
modelsRemote = () => usageRemote.models();
|
|
@@ -4232,6 +6355,10 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
|
|
|
4232
6355
|
ctx.effect(() => () => usageController.dispose(), "dsh-commandcode-provider: usage controller");
|
|
4233
6356
|
const usageStore = createSnapshotStore(usageController.state());
|
|
4234
6357
|
usageController.subscribe(() => usageStore.set(usageController.state()));
|
|
6358
|
+
pricesController = new CommandCodePricesController(usageRemote);
|
|
6359
|
+
ctx.effect(() => () => pricesController?.dispose(), "dsh-commandcode-provider: price table");
|
|
6360
|
+
const pricesStore = createSnapshotStore(pricesController.state());
|
|
6361
|
+
pricesController.subscribe(() => pricesStore.set(pricesController.state()));
|
|
4235
6362
|
const loginRemote = {
|
|
4236
6363
|
loginBegin: async () => {
|
|
4237
6364
|
if (usageNamespace === void 0) return {
|
|
@@ -4323,6 +6450,54 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
|
|
|
4323
6450
|
cancelLogin: () => void loginController.cancel()
|
|
4324
6451
|
})
|
|
4325
6452
|
}, CommandCodeProviderCard));
|
|
6453
|
+
const panelFace = () => ({
|
|
6454
|
+
hooks: {
|
|
6455
|
+
commandCodeUsage: usageStore,
|
|
6456
|
+
commandCodeSettings: store
|
|
6457
|
+
},
|
|
6458
|
+
refresh: () => {
|
|
6459
|
+
pricesController?.ensure();
|
|
6460
|
+
usageController.refresh();
|
|
6461
|
+
},
|
|
6462
|
+
startAutoRefresh: () => startPanelAutoRefresh(usageController, () => true),
|
|
6463
|
+
open: () => {
|
|
6464
|
+
const layout = ctx.get("layout");
|
|
6465
|
+
if (typeof layout?.selectPanel === "function") layout.selectPanel(PANEL_ID);
|
|
6466
|
+
}
|
|
6467
|
+
});
|
|
6468
|
+
ctx.effect(() => injectPanelCss(), "dsh-commandcode-provider: panel styles");
|
|
6469
|
+
try {
|
|
6470
|
+
ctx.slots.inject("main", () => ctx.slots.register({
|
|
6471
|
+
name: "main",
|
|
6472
|
+
key: PANEL_ID,
|
|
6473
|
+
inject: panelFace
|
|
6474
|
+
}, CommandCodePanel));
|
|
6475
|
+
} catch (error) {
|
|
6476
|
+
console.error("[dsh-commandcode-provider] could not register the plans & quota panel:", error);
|
|
6477
|
+
}
|
|
6478
|
+
ctx.inject(["layout"], (layoutCtx) => {
|
|
6479
|
+
if (typeof layoutCtx.get("layout")?.selectPanel !== "function") return;
|
|
6480
|
+
try {
|
|
6481
|
+
layoutCtx.slots.inject("sidebar.footer.action", () => layoutCtx.slots.register({
|
|
6482
|
+
name: "sidebar.footer.action",
|
|
6483
|
+
id: PANEL_ID,
|
|
6484
|
+
order: 1,
|
|
6485
|
+
inject: panelFace
|
|
6486
|
+
}, CommandCodeFooterEntry));
|
|
6487
|
+
} catch (error) {
|
|
6488
|
+
console.error("[dsh-commandcode-provider] could not register the sidebar footer card:", error);
|
|
6489
|
+
}
|
|
6490
|
+
});
|
|
6491
|
+
const sessionCostFace = () => ({ hooks: { commandCodePrices: pricesStore } });
|
|
6492
|
+
try {
|
|
6493
|
+
ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
|
|
6494
|
+
name: "conversation.composer.dock",
|
|
6495
|
+
id: SESSION_COST_ID,
|
|
6496
|
+
inject: sessionCostFace
|
|
6497
|
+
}, CommandCodeSessionCost));
|
|
6498
|
+
} catch (error) {
|
|
6499
|
+
console.error("[dsh-commandcode-provider] could not register the composer session-cost readout:", error);
|
|
6500
|
+
}
|
|
4326
6501
|
}
|
|
4327
6502
|
const inject = [
|
|
4328
6503
|
"slots",
|