@mars-sea/dsh-commandcode-provider 0.4.2 → 0.5.0

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/lib/client.js CHANGED
@@ -105,7 +105,8 @@ window.__ModuleLoader__.load({
105
105
  textField("workingDir"),
106
106
  numberField$1("requestTimeoutMs"),
107
107
  numberField$1("streamIdleTimeoutMs"),
108
- booleanField$1("filterModelsByPlan")
108
+ booleanField$1("filterModelsByPlan"),
109
+ textField("activeAccount")
109
110
  ];
110
111
  /**
111
112
  * Controller bridging the `llm-commandcode` scope and the credentials domain
@@ -121,11 +122,18 @@ window.__ModuleLoader__.load({
121
122
  disposers = [];
122
123
  disposed = false;
123
124
  defaultWorkingDir;
124
- credential = {
125
- ref: DEFAULT_API_KEY_REF,
126
- configured: false,
127
- writable: true
128
- };
125
+ /** The credential reference the default account resolves. */
126
+ credentialRef = DEFAULT_API_KEY_REF;
127
+ /** Host-reported configured/writable state per credential reference. */
128
+ credentialStates = /* @__PURE__ */ new Map();
129
+ /** Staged account additions (not yet saved). */
130
+ addedAccounts = [];
131
+ /** Staged removals of stored extra accounts, by credential reference. */
132
+ removedRefs = /* @__PURE__ */ new Set();
133
+ /** Staged label drafts, by credential reference. */
134
+ labelDrafts = /* @__PURE__ */ new Map();
135
+ /** Staged key drafts, by credential reference (blank = keep stored key). */
136
+ keyDrafts = /* @__PURE__ */ new Map();
129
137
  saving = false;
130
138
  failed = false;
131
139
  /**
@@ -139,6 +147,7 @@ window.__ModuleLoader__.load({
139
147
  this.api = api;
140
148
  this.disposers.push(scope.subscribe(() => {
141
149
  this.recomputeCredentialRef();
150
+ this.describeAll();
142
151
  this.publish();
143
152
  }));
144
153
  if (hostDescription !== void 0) {
@@ -153,7 +162,7 @@ window.__ModuleLoader__.load({
153
162
  }));
154
163
  }
155
164
  this.recomputeCredentialRef();
156
- this.readCredential();
165
+ this.describeAll();
157
166
  }
158
167
  /** Release every subscription held on external sources. Idempotent. */
159
168
  dispose() {
@@ -172,13 +181,9 @@ window.__ModuleLoader__.load({
172
181
  recomputeCredentialRef() {
173
182
  const snapshot = this.scope.getSnapshot();
174
183
  const named = typeof snapshot.value?.apiKeyEnv === "string" && snapshot.value.apiKeyEnv.length > 0 ? snapshot.value.apiKeyEnv : DEFAULT_API_KEY_REF;
175
- if (named === this.credential.ref) return;
176
- this.credential = {
177
- ref: named,
178
- configured: false,
179
- writable: true
180
- };
181
- this.readCredential();
184
+ if (named === this.credentialRef) return;
185
+ this.credentialRef = named;
186
+ this.credentialStates.delete(named);
182
187
  }
183
188
  /** Subscribe to state projections. @returns the disposer. */
184
189
  subscribe(listener) {
@@ -189,11 +194,14 @@ window.__ModuleLoader__.load({
189
194
  state() {
190
195
  const snapshot = this.scope.getSnapshot();
191
196
  const plan = this.plan();
197
+ const credential = this.credentialStates.get(this.credentialRef);
198
+ const accounts = this.effectiveAccounts();
192
199
  return {
193
200
  available: snapshot.status === "ready",
194
201
  writable: snapshot.writable,
195
- apiKeyConfigured: this.credential.configured,
196
- apiKeyWritable: this.credential.writable,
202
+ apiKeyConfigured: credential?.configured ?? false,
203
+ anyAccountConfigured: (credential?.configured ?? false) || accounts.some((account) => account.configured),
204
+ apiKeyWritable: credential?.writable ?? true,
197
205
  apiKey: {
198
206
  text: this.staged.get("apiKey")?.text ?? "",
199
207
  clear: false,
@@ -206,12 +214,59 @@ window.__ModuleLoader__.load({
206
214
  requestTimeoutMs: this.field("requestTimeoutMs"),
207
215
  streamIdleTimeoutMs: this.field("streamIdleTimeoutMs"),
208
216
  filterModelsByPlan: this.field("filterModelsByPlan"),
209
- dirty: plan.length > 0,
217
+ activeAccount: this.field("activeAccount"),
218
+ accounts,
219
+ dirty: plan.length > 0 || this.accountsDirty(),
210
220
  invalid: plan.some((item) => item.run === void 0),
211
221
  saving: this.saving,
212
222
  failed: this.failed
213
223
  };
214
224
  }
225
+ /** Stage a new extra account (saved on the next `save()`). */
226
+ addAccount() {
227
+ const used = /* @__PURE__ */ new Set([
228
+ this.credentialRef,
229
+ ...this.storedExtras().map((extra) => extra.ref),
230
+ ...this.addedAccounts.map((extra) => extra.ref)
231
+ ]);
232
+ let n = 2;
233
+ while (used.has(`COMMANDCODE_API_KEY_${n}`)) n += 1;
234
+ const index = this.storedExtras().length + this.addedAccounts.length + 2;
235
+ this.addedAccounts.push({
236
+ label: `Account ${index}`,
237
+ ref: `COMMANDCODE_API_KEY_${n}`
238
+ });
239
+ this.failed = false;
240
+ this.describeAll();
241
+ this.publish();
242
+ }
243
+ /** Stage one extra account's removal (or drop an unsaved addition). */
244
+ removeAccount(id) {
245
+ const addedIndex = this.addedAccounts.findIndex((extra) => extra.ref === id);
246
+ if (addedIndex >= 0) this.addedAccounts.splice(addedIndex, 1);
247
+ else this.removedRefs.add(id);
248
+ this.labelDrafts.delete(id);
249
+ this.keyDrafts.delete(id);
250
+ const stagedActive = this.staged.get("activeAccount");
251
+ if ((stagedActive !== void 0 ? stagedActive.clear ? "" : stagedActive.text : typeof this.sectionValue("activeAccount") === "string" ? this.sectionValue("activeAccount") : "") === id) this.staged.set("activeAccount", {
252
+ text: "",
253
+ clear: true
254
+ });
255
+ this.failed = false;
256
+ this.publish();
257
+ }
258
+ /** Stage one extra account's label draft. */
259
+ editAccountLabel(id, text) {
260
+ this.labelDrafts.set(id, text);
261
+ this.failed = false;
262
+ this.publish();
263
+ }
264
+ /** Stage one extra account's key draft (blank keeps the stored key). */
265
+ editAccountKey(id, text) {
266
+ this.keyDrafts.set(id, text);
267
+ this.failed = false;
268
+ this.publish();
269
+ }
215
270
  /** Stage one field's draft text. */
216
271
  edit(field, text) {
217
272
  this.staged.set(field, {
@@ -239,15 +294,17 @@ window.__ModuleLoader__.load({
239
294
  }
240
295
  /** Discard every staged edit. */
241
296
  discard() {
242
- if (this.staged.size === 0 && !this.failed) return;
297
+ if (this.staged.size === 0 && !this.accountsStaged() && !this.failed) return;
243
298
  this.staged.clear();
299
+ this.clearAccountStaging();
244
300
  this.failed = false;
245
301
  this.publish();
246
302
  }
247
303
  /** Write every staged edit, then re-read the Host's accepted state. */
248
304
  async save() {
249
305
  const plan = this.plan();
250
- if (plan.length === 0 || this.saving) return;
306
+ const accountRuns = this.accountPlan();
307
+ if (plan.length === 0 && accountRuns.length === 0 || this.saving) return;
251
308
  const runs = [];
252
309
  for (const item of plan) {
253
310
  if (item.run === void 0) return;
@@ -257,12 +314,34 @@ window.__ModuleLoader__.load({
257
314
  this.failed = false;
258
315
  this.publish();
259
316
  let landed = true;
260
- for (const run of runs) landed = await run() && landed;
317
+ for (const run of [...runs, ...accountRuns]) if (!await run()) {
318
+ landed = false;
319
+ break;
320
+ }
261
321
  this.saving = false;
262
322
  this.failed = !landed;
263
- if (landed) this.staged.clear();
323
+ if (landed) {
324
+ this.staged.clear();
325
+ this.clearAccountStaging();
326
+ } else this.reconcileAccountStaging();
264
327
  this.publish();
265
328
  }
329
+ /**
330
+ * Drop account staging the stored section already reflects: additions whose
331
+ * ref is now stored, removals whose ref is gone, and label drafts matching
332
+ * the stored label. Key drafts are kept — a landed key write is idempotent
333
+ * on retry, and the draft carries the user's intent when it was the
334
+ * accounts write that failed.
335
+ */
336
+ reconcileAccountStaging() {
337
+ const stored = new Set(this.storedExtras().map((extra) => extra.ref));
338
+ this.addedAccounts = this.addedAccounts.filter((extra) => !stored.has(extra.ref));
339
+ for (const ref of [...this.removedRefs]) if (!stored.has(ref)) this.removedRefs.delete(ref);
340
+ for (const [ref, text] of [...this.labelDrafts]) {
341
+ const storedLabel = this.storedExtras().find((extra) => extra.ref === ref)?.label;
342
+ if (storedLabel === void 0 || storedLabel === text.trim()) this.labelDrafts.delete(ref);
343
+ }
344
+ }
266
345
  spec(field) {
267
346
  const spec = this.specs.get(field);
268
347
  if (spec === void 0) throw new Error(`commandcode settings page has no field ${field}`);
@@ -349,38 +428,136 @@ window.__ModuleLoader__.load({
349
428
  await this.scope.set(field, value);
350
429
  return this.userLayer()?.[field] === value;
351
430
  }
352
- /** Write the staged key, then re-read whether the Host now holds one. */
431
+ /** Write the staged default key, then re-read whether the Host holds it. */
353
432
  async writeKey(value) {
433
+ return this.writeKeyTo(this.credentialRef, value);
434
+ }
435
+ /** Write one account's key, then re-read the Host's credential states. */
436
+ async writeKeyTo(ref, value) {
354
437
  try {
355
438
  if (!(await this.api.credentials.set({
356
- ref: this.credential.ref,
439
+ ref,
357
440
  value
358
441
  })).result.ok) return false;
359
442
  } catch {
360
443
  return false;
361
444
  }
362
- await this.readCredential();
363
- return this.credential.configured;
445
+ await this.describeAll();
446
+ return this.credentialStates.get(ref)?.configured ?? false;
364
447
  }
365
- /** Ask the credentials domain about the reference this page writes. */
366
- async readCredential() {
367
- const ref = this.credential.ref;
448
+ /** Ask the credentials domain about every reference this page writes. */
449
+ async describeAll() {
450
+ const refs = [
451
+ this.credentialRef,
452
+ ...this.storedExtras().map((extra) => extra.ref),
453
+ ...this.addedAccounts.map((extra) => extra.ref)
454
+ ];
368
455
  let response;
369
456
  try {
370
- response = await this.api.credentials.describe({ refs: [ref] });
457
+ response = await this.api.credentials.describe({ refs });
371
458
  } catch {
372
459
  return;
373
460
  }
374
461
  if (!response.result.ok) return;
375
- const view = response.result.value.credentials[ref];
376
- const next = {
377
- ref,
378
- configured: view?.configured ?? false,
379
- writable: view?.writable ?? true
380
- };
381
- if (next.configured === this.credential.configured && next.writable === this.credential.writable) return;
382
- this.credential = next;
383
- this.publish();
462
+ let changed = false;
463
+ for (const ref of refs) {
464
+ const view = response.result.value.credentials[ref];
465
+ const next = {
466
+ configured: view?.configured ?? false,
467
+ writable: view?.writable ?? true
468
+ };
469
+ const prev = this.credentialStates.get(ref);
470
+ if (prev === void 0 || prev.configured !== next.configured || prev.writable !== next.writable) {
471
+ this.credentialStates.set(ref, next);
472
+ changed = true;
473
+ }
474
+ }
475
+ if (changed) this.publish();
476
+ }
477
+ /** The stored extra accounts from the settings section (`accounts`). */
478
+ storedExtras() {
479
+ const raw = this.scope.getSnapshot().value?.accounts;
480
+ if (!Array.isArray(raw)) return [];
481
+ const out = [];
482
+ for (const entry of raw) {
483
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
484
+ const record = entry;
485
+ const ref = record.apiKeyEnv;
486
+ if (typeof ref !== "string" || ref === "") continue;
487
+ const label = record.label;
488
+ out.push({
489
+ label: typeof label === "string" && label !== "" ? label : ref,
490
+ ref
491
+ });
492
+ }
493
+ return out;
494
+ }
495
+ /** Every extra account row: stored (minus staged removals) + staged adds. */
496
+ effectiveAccounts() {
497
+ const stored = this.storedExtras().filter((extra) => !this.removedRefs.has(extra.ref)).map((extra) => ({
498
+ ...extra,
499
+ added: false
500
+ }));
501
+ const added = this.addedAccounts.map((extra) => ({
502
+ ...extra,
503
+ added: true
504
+ }));
505
+ return [...stored, ...added].map((extra) => ({
506
+ id: extra.ref,
507
+ ref: extra.ref,
508
+ label: this.labelDrafts.get(extra.ref) ?? extra.label,
509
+ keyText: this.keyDrafts.get(extra.ref) ?? "",
510
+ configured: this.credentialStates.get(extra.ref)?.configured ?? false,
511
+ writable: this.credentialStates.get(extra.ref)?.writable ?? true,
512
+ added: extra.added
513
+ }));
514
+ }
515
+ /** Whether any account-level staging (add/remove/label/key) exists. */
516
+ accountsStaged() {
517
+ return this.addedAccounts.length > 0 || this.removedRefs.size > 0 || this.labelDrafts.size > 0 || this.keyDrafts.size > 0;
518
+ }
519
+ /** Whether the staged account edits differ from the stored section. */
520
+ accountsDirty() {
521
+ if (this.addedAccounts.length > 0 || this.removedRefs.size > 0) return true;
522
+ for (const [ref, text] of this.labelDrafts) {
523
+ const base = this.storedExtras().find((extra) => extra.ref === ref)?.label;
524
+ if (base !== void 0 && text.trim() !== "" && text !== base) return true;
525
+ }
526
+ for (const text of this.keyDrafts.values()) if (text.trim() !== "") return true;
527
+ return false;
528
+ }
529
+ /** Reset every account-level staged edit. */
530
+ clearAccountStaging() {
531
+ this.addedAccounts = [];
532
+ this.removedRefs.clear();
533
+ this.labelDrafts.clear();
534
+ this.keyDrafts.clear();
535
+ }
536
+ /** The account-level writes a save performs (empty when nothing staged). */
537
+ accountPlan() {
538
+ if (!this.accountsDirty()) return [];
539
+ const runs = [];
540
+ for (const [ref, text] of this.keyDrafts) {
541
+ const value = text.trim();
542
+ if (value !== "" && !this.removedRefs.has(ref)) runs.push(() => this.writeKeyTo(ref, value));
543
+ }
544
+ runs.push(() => this.writeAccounts());
545
+ return runs;
546
+ }
547
+ /** Persist the staged accounts list into the settings section. */
548
+ async writeAccounts() {
549
+ const base = [...this.storedExtras().filter((extra) => !this.removedRefs.has(extra.ref)), ...this.addedAccounts];
550
+ const seen = /* @__PURE__ */ new Set();
551
+ const list = base.filter((extra) => !seen.has(extra.ref) && (seen.add(extra.ref), true)).map((extra) => {
552
+ const draft = this.labelDrafts.get(extra.ref)?.trim();
553
+ return {
554
+ label: draft !== void 0 && draft !== "" ? draft : extra.label,
555
+ apiKeyEnv: extra.ref
556
+ };
557
+ });
558
+ await this.scope.set("accounts", list);
559
+ const after = this.storedExtras();
560
+ return after.length === list.length && list.every((item, index) => after[index]?.ref === item.apiKeyEnv);
384
561
  }
385
562
  publish() {
386
563
  if (this.disposed) return;
@@ -594,6 +771,25 @@ window.__ModuleLoader__.load({
594
771
  }
595
772
  return report;
596
773
  }
774
+ /** Parse one untrusted boundary value into a {@link CommandCodeAccountUsage}. */
775
+ function parseAccountUsage(value) {
776
+ const source = record(value, "account");
777
+ return {
778
+ id: stringField(source, "id", "account.id"),
779
+ label: stringField(source, "label", "account.label"),
780
+ configured: booleanField(source, "configured", "account.configured"),
781
+ active: booleanField(source, "active", "account.active"),
782
+ mark: stringField(source, "mark", "account.mark"),
783
+ cooldownUntil: numberField(source, "cooldownUntil", "account.cooldownUntil"),
784
+ report: parseUsageReport(source.report)
785
+ };
786
+ }
787
+ /** Parse the wire result into a {@link CommandCodeAccountsReport}. */
788
+ function parseAccountsReport(value) {
789
+ const accounts = record(value, "result").accounts;
790
+ if (!Array.isArray(accounts)) reject("accounts");
791
+ return { accounts: accounts.map(parseAccountUsage) };
792
+ }
597
793
  /** The Client-face contribution mounted on `ctx.remote`. */
598
794
  const USAGE_REMOTE_CONTRIBUTION = {
599
795
  package: USAGE_REMOTE_PACKAGE,
@@ -606,8 +802,8 @@ window.__ModuleLoader__.load({
606
802
  parameters: [],
607
803
  result: {
608
804
  mode: "strict",
609
- typeSymbol: `${USAGE_REMOTE_PACKAGE}#CommandCodeUsageReport`,
610
- schema: { parse: parseUsageReport }
805
+ typeSymbol: `${USAGE_REMOTE_PACKAGE}#CommandCodeAccountsReport`,
806
+ schema: { parse: parseAccountsReport }
611
807
  }
612
808
  }]
613
809
  };
@@ -815,38 +1011,55 @@ window.__ModuleLoader__.load({
815
1011
  ]
816
1012
  });
817
1013
  }
1014
+ /** One account's rotation state as a short badge next to its label. */
1015
+ function AccountMark({ entry, t }) {
1016
+ if (entry.active) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1017
+ className: "cc-usagePlan",
1018
+ children: t("usageActive")
1019
+ });
1020
+ if (entry.mark === "invalid-credential") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1021
+ className: "cc-usagePlanStatus",
1022
+ children: t("usageInvalidKey")
1023
+ });
1024
+ if (entry.cooldownUntil > 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1025
+ className: "cc-usagePlanStatus",
1026
+ children: [
1027
+ t("usageCooldown"),
1028
+ " ",
1029
+ formatResetAt(entry.cooldownUntil)
1030
+ ]
1031
+ });
1032
+ if (entry.mark === "rate-limit") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1033
+ className: "cc-usagePlanStatus",
1034
+ children: t("usageCooldown")
1035
+ });
1036
+ return null;
1037
+ }
818
1038
  /**
819
- * The account-usage card: the `/commandcode` dashboard's facts (account,
820
- * totals, credits, window limits) rendered as a native settings card. Data
821
- * arrives through the `commandcode/report` Remote; the API key never leaves
822
- * the Host.
1039
+ * One pool account's facts (identity, totals, credits, window limits)
1040
+ * rendered inside the account-usage card.
823
1041
  */
824
- function UsageCard({ t, usage, apiKeyConfigured, onRefresh }) {
825
- (0, react.useEffect)(() => {
826
- if (apiKeyConfigured && usage.status === "idle") onRefresh();
827
- }, [
828
- apiKeyConfigured,
829
- usage.status,
830
- onRefresh
831
- ]);
832
- const loading = usage.status === "loading";
833
- const report = usage.report;
834
- const account = report?.account;
1042
+ function AccountReport({ entry, t }) {
1043
+ const report = entry.report;
1044
+ const account = report.account;
835
1045
  const accountName = account === void 0 ? "" : account.userName || account.name;
836
- const credits = report?.credits;
837
- const plan = report?.plan;
1046
+ const credits = report.credits;
1047
+ const plan = report.plan;
838
1048
  const planName = plan?.name ?? "";
839
1049
  const planStatus = plan !== void 0 && plan.status !== "" && plan.status !== "active" ? plan.status : "";
840
1050
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
841
- className: "cc-usageCard",
842
- "aria-label": t("usageTitle"),
1051
+ className: "cc-accountReport",
843
1052
  children: [
844
1053
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
845
1054
  className: "cc-usageHead",
846
1055
  children: [
847
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
848
1057
  className: "cc-usageTitle",
849
- children: t("usageTitle")
1058
+ children: entry.label
1059
+ }),
1060
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountMark, {
1061
+ entry,
1062
+ t
850
1063
  }),
851
1064
  accountName !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
852
1065
  className: "cc-usageAccount",
@@ -859,30 +1072,14 @@ window.__ModuleLoader__.load({
859
1072
  planStatus !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
860
1073
  className: "cc-usagePlanStatus",
861
1074
  children: planStatus
862
- }) : null,
863
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
864
- type: "button",
865
- className: "cc-usageRefresh",
866
- disabled: loading || !apiKeyConfigured,
867
- onClick: onRefresh,
868
- children: loading ? t("usageRefreshing") : t("usageRefresh")
869
- })
1075
+ }) : null
870
1076
  ]
871
1077
  }),
872
- !apiKeyConfigured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1078
+ !entry.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
873
1079
  className: "cc-usageHint",
874
- children: t("usageNoKey")
1080
+ children: t("usageUnconfigured")
875
1081
  }) : null,
876
- apiKeyConfigured && report === void 0 && loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
877
- className: "cc-usageHint",
878
- children: t("usageLoading")
879
- }) : null,
880
- usage.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
881
- className: "cc-usageError",
882
- role: "status",
883
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [t("usageError"), usage.error !== void 0 && usage.error !== "" ? ` — ${usage.error}` : ""] })
884
- }) : null,
885
- report?.usage !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1082
+ report.usage !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
886
1083
  className: "cc-usageStats",
887
1084
  children: [
888
1085
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageStat, {
@@ -935,25 +1132,17 @@ window.__ModuleLoader__.load({
935
1132
  t
936
1133
  })]
937
1134
  }) : null,
938
- report !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1135
+ plan !== void 0 && plan.currentPeriodEnd > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
939
1136
  className: "cc-usageMeta",
940
1137
  children: [
941
- plan !== void 0 && plan.currentPeriodEnd > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1138
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
942
1139
  className: "cc-usageUpdated",
943
1140
  children: [
944
1141
  t("usagePeriodEnd"),
945
1142
  " ",
946
1143
  new Date(plan.currentPeriodEnd).toLocaleDateString()
947
1144
  ]
948
- }) : null,
949
- usage.fetchedAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
950
- className: "cc-usageUpdated",
951
- children: [
952
- t("usageUpdated"),
953
- " ",
954
- new Date(usage.fetchedAt).toLocaleTimeString()
955
- ]
956
- }) : null,
1145
+ }),
957
1146
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }),
958
1147
  report.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
959
1148
  className: "cc-usagePartial",
@@ -961,10 +1150,233 @@ window.__ModuleLoader__.load({
961
1150
  children: t("usagePartial")
962
1151
  }) : null
963
1152
  ]
1153
+ }) : report.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1154
+ className: "cc-usageMeta",
1155
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1156
+ className: "cc-usagePartial",
1157
+ title: report.failures.join("; "),
1158
+ children: t("usagePartial")
1159
+ })]
964
1160
  }) : null
965
1161
  ]
966
1162
  });
967
1163
  }
1164
+ /**
1165
+ * The account-usage card: the `/commandcode` dashboard's facts rendered as
1166
+ * a native settings card — one section per pool account. Data arrives
1167
+ * through the `commandcode/report` Remote; the API keys never leave the
1168
+ * Host.
1169
+ */
1170
+ function UsageCard({ t, usage, apiKeyConfigured, onRefresh }) {
1171
+ (0, react.useEffect)(() => {
1172
+ if (apiKeyConfigured && usage.status === "idle") onRefresh();
1173
+ }, [
1174
+ apiKeyConfigured,
1175
+ usage.status,
1176
+ onRefresh
1177
+ ]);
1178
+ const loading = usage.status === "loading";
1179
+ const report = usage.report;
1180
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1181
+ className: "cc-usageCard",
1182
+ "aria-label": t("usageTitle"),
1183
+ children: [
1184
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1185
+ className: "cc-usageHead",
1186
+ children: [
1187
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1188
+ className: "cc-usageTitle",
1189
+ children: t("usageTitle")
1190
+ }),
1191
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }),
1192
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1193
+ type: "button",
1194
+ className: "cc-usageRefresh",
1195
+ disabled: loading || !apiKeyConfigured,
1196
+ onClick: onRefresh,
1197
+ children: loading ? t("usageRefreshing") : t("usageRefresh")
1198
+ })
1199
+ ]
1200
+ }),
1201
+ !apiKeyConfigured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1202
+ className: "cc-usageHint",
1203
+ children: t("usageNoKey")
1204
+ }) : null,
1205
+ apiKeyConfigured && report === void 0 && loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1206
+ className: "cc-usageHint",
1207
+ children: t("usageLoading")
1208
+ }) : null,
1209
+ usage.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1210
+ className: "cc-usageError",
1211
+ role: "status",
1212
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [t("usageError"), usage.error !== void 0 && usage.error !== "" ? ` — ${usage.error}` : ""] })
1213
+ }) : null,
1214
+ report?.accounts.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountReport, {
1215
+ entry,
1216
+ t
1217
+ }, entry.id)),
1218
+ report !== void 0 && usage.fetchedAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1219
+ className: "cc-usageMeta",
1220
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1221
+ className: "cc-usageUpdated",
1222
+ children: [
1223
+ t("usageUpdated"),
1224
+ " ",
1225
+ new Date(usage.fetchedAt).toLocaleTimeString()
1226
+ ]
1227
+ })]
1228
+ }) : null
1229
+ ]
1230
+ });
1231
+ }
1232
+ /** One extra account row: label, key, configured badge, remove affordance. */
1233
+ function AccountRow({ account, disabled, t, onLabel, onKey, onRemove }) {
1234
+ const locked = !account.writable;
1235
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1236
+ className: "cc-field",
1237
+ children: [
1238
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1239
+ className: "cc-fieldHead",
1240
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1241
+ className: "cc-label",
1242
+ htmlFor: `cc-account-label-${account.id}`,
1243
+ children: t("accountLabel")
1244
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1245
+ className: "cc-badges",
1246
+ children: [
1247
+ account.added ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1248
+ className: "cc-badge",
1249
+ children: t("unsaved")
1250
+ }) : null,
1251
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1252
+ className: account.configured ? "cc-badge" : "cc-badgeMuted",
1253
+ children: account.configured ? t("apiKeySet") : t("apiKeyUnset")
1254
+ }),
1255
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1256
+ type: "button",
1257
+ className: "cc-reset",
1258
+ disabled,
1259
+ onClick: onRemove,
1260
+ children: t("accountRemove")
1261
+ })
1262
+ ]
1263
+ })]
1264
+ }),
1265
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1266
+ id: `cc-account-label-${account.id}`,
1267
+ className: "cc-input",
1268
+ type: "text",
1269
+ value: account.label,
1270
+ disabled,
1271
+ onChange: (event) => onLabel(event.target.value)
1272
+ }),
1273
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1274
+ id: `cc-account-key-${account.id}`,
1275
+ className: "cc-input",
1276
+ type: "password",
1277
+ autoComplete: "off",
1278
+ placeholder: t("accountKey"),
1279
+ value: account.keyText,
1280
+ disabled: disabled || locked,
1281
+ onChange: (event) => onKey(event.target.value)
1282
+ }),
1283
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1284
+ className: "cc-hint",
1285
+ children: locked ? t("apiKeyLocked") : t("accountKeyHint")
1286
+ })
1287
+ ]
1288
+ });
1289
+ }
1290
+ /** The multi-account card: extra accounts in rotation order + add button. */
1291
+ function AccountsCard({ t, state, disabled, onAdd, onRemove, onLabel, onKey, onActive, onActiveReset }) {
1292
+ const active = state.activeAccount;
1293
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1294
+ className: "cc-card",
1295
+ "aria-label": t("accountsTitle"),
1296
+ children: [
1297
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1298
+ className: "cc-field",
1299
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1300
+ className: "cc-fieldHead",
1301
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1302
+ className: "cc-label",
1303
+ children: t("accountsTitle")
1304
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1305
+ className: "cc-badges",
1306
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1307
+ type: "button",
1308
+ className: "cc-reset",
1309
+ disabled,
1310
+ onClick: onAdd,
1311
+ children: t("accountAdd")
1312
+ })
1313
+ })]
1314
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1315
+ className: "cc-hint",
1316
+ children: t("accountsHint")
1317
+ })]
1318
+ }),
1319
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1320
+ className: "cc-field",
1321
+ children: [
1322
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1323
+ className: "cc-fieldHead",
1324
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1325
+ className: "cc-label",
1326
+ htmlFor: "cc-active-account",
1327
+ children: t("activeAccount")
1328
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1329
+ className: "cc-badges",
1330
+ children: [active.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1331
+ className: "cc-badge",
1332
+ children: t("overridden")
1333
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1334
+ type: "button",
1335
+ className: "cc-reset",
1336
+ disabled,
1337
+ onClick: onActiveReset,
1338
+ children: t("reset")
1339
+ })]
1340
+ })]
1341
+ }),
1342
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1343
+ id: "cc-active-account",
1344
+ className: "cc-input",
1345
+ value: active.text,
1346
+ disabled,
1347
+ onChange: (event) => onActive(event.target.value),
1348
+ children: [
1349
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1350
+ value: "",
1351
+ children: t("activeAccountAuto")
1352
+ }),
1353
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1354
+ value: "default",
1355
+ children: t("accountDefault")
1356
+ }),
1357
+ state.accounts.filter((account) => !account.added).map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1358
+ value: account.ref,
1359
+ children: account.label
1360
+ }, account.id))
1361
+ ]
1362
+ }),
1363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1364
+ className: "cc-hint",
1365
+ children: t("activeAccountHint")
1366
+ })
1367
+ ]
1368
+ }),
1369
+ state.accounts.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountRow, {
1370
+ account,
1371
+ disabled,
1372
+ t,
1373
+ onLabel: (text) => onLabel(account.id, text),
1374
+ onKey: (text) => onKey(account.id, text),
1375
+ onRemove: () => onRemove(account.id)
1376
+ }, account.id))
1377
+ ]
1378
+ });
1379
+ }
968
1380
  /** The settings page body: connection facts for the Command Code provider. */
969
1381
  function CommandCodeSettingsPage(props) {
970
1382
  const { t } = props;
@@ -992,9 +1404,20 @@ window.__ModuleLoader__.load({
992
1404
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageCard, {
993
1405
  t,
994
1406
  usage,
995
- apiKeyConfigured: state.apiKeyConfigured,
1407
+ apiKeyConfigured: state.anyAccountConfigured,
996
1408
  onRefresh: props.refreshUsage
997
1409
  }),
1410
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountsCard, {
1411
+ t,
1412
+ state,
1413
+ disabled,
1414
+ onAdd: props.addAccount,
1415
+ onRemove: props.removeAccount,
1416
+ onLabel: props.editAccountLabel,
1417
+ onKey: props.editAccountKey,
1418
+ onActive: (text) => props.edit("activeAccount", text),
1419
+ onActiveReset: () => props.resetField("activeAccount")
1420
+ }),
998
1421
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
999
1422
  className: "cc-card",
1000
1423
  children: [
@@ -1112,6 +1535,17 @@ window.__ModuleLoader__.load({
1112
1535
  streamIdleTimeoutMsHint: "生成流停滞多久视为断连;默认 300000(长思考模型可静默数分钟,默认值刻意放宽)。",
1113
1536
  filterModelsByPlan: "隐藏套餐外模型",
1114
1537
  filterModelsByPlanHint: "开启后,模型选择器只列出当前套餐可用的模型;账户持有按需余额时会显示全部。",
1538
+ accountsTitle: "多账户轮换",
1539
+ accountsHint: "当前账户达到用量限额(429)或密钥失效(401)时,请求自动切换到下一个账户;全部耗尽时会提示最早的重置时间。",
1540
+ accountAdd: "添加账户",
1541
+ accountRemove: "移除",
1542
+ accountLabel: "账户备注名",
1543
+ accountKey: "API 密钥",
1544
+ accountKeyHint: "该账户的 API 密钥。留空保存不会覆盖已存储的密钥。",
1545
+ accountDefault: "默认账户",
1546
+ activeAccount: "当前使用账户",
1547
+ activeAccountAuto: "自动(第一个可用账户)",
1548
+ activeAccountHint: "手动指定优先使用的账户,保存后下次请求即生效;所选账户耗尽时仍会自动切换到其他可用账户。",
1115
1549
  overridden: "已覆盖",
1116
1550
  reset: "重置",
1117
1551
  invalidNumber: "无效数字",
@@ -1144,7 +1578,11 @@ window.__ModuleLoader__.load({
1144
1578
  usageReset: "重置于",
1145
1579
  usagePartial: "部分端点数据不可用",
1146
1580
  usageUpdated: "更新于",
1147
- usagePeriodEnd: "账期截止"
1581
+ usagePeriodEnd: "账期截止",
1582
+ usageActive: "当前使用",
1583
+ usageCooldown: "限额冷却中",
1584
+ usageInvalidKey: "密钥无效",
1585
+ usageUnconfigured: "该账户尚未配置 API 密钥。"
1148
1586
  };
1149
1587
  const en = {
1150
1588
  nav: "Command Code",
@@ -1165,6 +1603,17 @@ window.__ModuleLoader__.load({
1165
1603
  streamIdleTimeoutMsHint: "How long a stalled stream is treated as dead; default 300000 (deliberately generous — long-thinking models can stay silent for minutes).",
1166
1604
  filterModelsByPlan: "Hide out-of-plan models",
1167
1605
  filterModelsByPlanHint: "When on, the model picker lists only models your subscription includes; any on-demand credit balance shows the full catalog.",
1606
+ accountsTitle: "Account rotation",
1607
+ 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.",
1608
+ accountAdd: "Add account",
1609
+ accountRemove: "Remove",
1610
+ accountLabel: "Account label",
1611
+ accountKey: "API key",
1612
+ accountKeyHint: "This account’s API key. Saving with the field blank keeps the stored key.",
1613
+ accountDefault: "Default account",
1614
+ activeAccount: "Active account",
1615
+ activeAccountAuto: "Auto (first usable account)",
1616
+ activeAccountHint: "Pin the preferred account; applies to the next request after saving. If the selected account is exhausted, requests still rotate to another usable account.",
1168
1617
  overridden: "Overridden",
1169
1618
  reset: "Reset",
1170
1619
  invalidNumber: "Invalid number",
@@ -1197,7 +1646,11 @@ window.__ModuleLoader__.load({
1197
1646
  usageReset: "Resets",
1198
1647
  usagePartial: "Some endpoint data unavailable",
1199
1648
  usageUpdated: "Updated",
1200
- usagePeriodEnd: "Period ends"
1649
+ usagePeriodEnd: "Period ends",
1650
+ usageActive: "Active",
1651
+ usageCooldown: "Cooling down",
1652
+ usageInvalidKey: "Invalid key",
1653
+ usageUnconfigured: "No API key configured for this account yet."
1201
1654
  };
1202
1655
  //#endregion
1203
1656
  //#region src/client/index.ts
@@ -1260,6 +1713,8 @@ window.__ModuleLoader__.load({
1260
1713
  .cc-usageBarFillWarn{background:var(--dsw-alias-label-error)}
1261
1714
  .cc-usageWindowReset{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}
1262
1715
  .cc-usageMeta{align-items:center;gap:8px;display:flex}
1716
+ .cc-accountReport{border-top:1px solid var(--dsw-alias-border-l2);padding-top:12px;flex-direction:column;gap:12px;display:flex}
1717
+ .cc-accountReport:first-of-type{border-top:none;padding-top:0}
1263
1718
  .cc-usageMetaSpacer{flex:1}
1264
1719
  .cc-usageUpdated{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}
1265
1720
  .cc-usagePartial{color:var(--dsw-alias-label-error);margin:0;font-size:11px;line-height:1.5}
@@ -1341,10 +1796,14 @@ window.__ModuleLoader__.load({
1341
1796
  resetField: (field) => controller.resetField(field),
1342
1797
  save: () => void controller.save().then(() => {
1343
1798
  const settled = controller.state();
1344
- if (!settled.failed && settled.apiKeyConfigured) usageController.refresh();
1799
+ if (!settled.failed && settled.anyAccountConfigured) usageController.refresh();
1345
1800
  }),
1346
1801
  discard: () => controller.discard(),
1347
- refreshUsage: () => void usageController.refresh()
1802
+ refreshUsage: () => void usageController.refresh(),
1803
+ addAccount: () => controller.addAccount(),
1804
+ removeAccount: (id) => controller.removeAccount(id),
1805
+ editAccountLabel: (id, text) => controller.editAccountLabel(id, text),
1806
+ editAccountKey: (id, text) => controller.editAccountKey(id, text)
1348
1807
  });
1349
1808
  ctx.slots.inject("settings.section", () => ctx.slots.register({
1350
1809
  name: "settings.section",