@mars-sea/dsh-commandcode-provider 0.4.2 → 0.6.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,60 @@ 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
+ accountsRemoving: [...this.removedRefs],
220
+ dirty: plan.length > 0 || this.accountsDirty(),
210
221
  invalid: plan.some((item) => item.run === void 0),
211
222
  saving: this.saving,
212
223
  failed: this.failed
213
224
  };
214
225
  }
226
+ /** Stage a new extra account (saved on the next `save()`). */
227
+ addAccount() {
228
+ const used = /* @__PURE__ */ new Set([
229
+ this.credentialRef,
230
+ ...this.storedExtras().map((extra) => extra.ref),
231
+ ...this.addedAccounts.map((extra) => extra.ref)
232
+ ]);
233
+ let n = 2;
234
+ while (used.has(`COMMANDCODE_API_KEY_${n}`)) n += 1;
235
+ const index = this.storedExtras().length + this.addedAccounts.length + 2;
236
+ this.addedAccounts.push({
237
+ label: `Account ${index}`,
238
+ ref: `COMMANDCODE_API_KEY_${n}`
239
+ });
240
+ this.failed = false;
241
+ this.describeAll();
242
+ this.publish();
243
+ }
244
+ /** Stage one extra account's removal (or drop an unsaved addition). */
245
+ removeAccount(id) {
246
+ const addedIndex = this.addedAccounts.findIndex((extra) => extra.ref === id);
247
+ if (addedIndex >= 0) this.addedAccounts.splice(addedIndex, 1);
248
+ else this.removedRefs.add(id);
249
+ this.labelDrafts.delete(id);
250
+ this.keyDrafts.delete(id);
251
+ const stagedActive = this.staged.get("activeAccount");
252
+ if ((stagedActive !== void 0 ? stagedActive.clear ? "" : stagedActive.text : typeof this.sectionValue("activeAccount") === "string" ? this.sectionValue("activeAccount") : "") === id) this.staged.set("activeAccount", {
253
+ text: "",
254
+ clear: true
255
+ });
256
+ this.failed = false;
257
+ this.publish();
258
+ }
259
+ /** Stage one extra account's label draft. */
260
+ editAccountLabel(id, text) {
261
+ this.labelDrafts.set(id, text);
262
+ this.failed = false;
263
+ this.publish();
264
+ }
265
+ /** Stage one extra account's key draft (blank keeps the stored key). */
266
+ editAccountKey(id, text) {
267
+ this.keyDrafts.set(id, text);
268
+ this.failed = false;
269
+ this.publish();
270
+ }
215
271
  /** Stage one field's draft text. */
216
272
  edit(field, text) {
217
273
  this.staged.set(field, {
@@ -239,15 +295,17 @@ window.__ModuleLoader__.load({
239
295
  }
240
296
  /** Discard every staged edit. */
241
297
  discard() {
242
- if (this.staged.size === 0 && !this.failed) return;
298
+ if (this.staged.size === 0 && !this.accountsStaged() && !this.failed) return;
243
299
  this.staged.clear();
300
+ this.clearAccountStaging();
244
301
  this.failed = false;
245
302
  this.publish();
246
303
  }
247
304
  /** Write every staged edit, then re-read the Host's accepted state. */
248
305
  async save() {
249
306
  const plan = this.plan();
250
- if (plan.length === 0 || this.saving) return;
307
+ const accountRuns = this.accountPlan();
308
+ if (plan.length === 0 && accountRuns.length === 0 || this.saving) return;
251
309
  const runs = [];
252
310
  for (const item of plan) {
253
311
  if (item.run === void 0) return;
@@ -257,12 +315,34 @@ window.__ModuleLoader__.load({
257
315
  this.failed = false;
258
316
  this.publish();
259
317
  let landed = true;
260
- for (const run of runs) landed = await run() && landed;
318
+ for (const run of [...runs, ...accountRuns]) if (!await run()) {
319
+ landed = false;
320
+ break;
321
+ }
261
322
  this.saving = false;
262
323
  this.failed = !landed;
263
- if (landed) this.staged.clear();
324
+ if (landed) {
325
+ this.staged.clear();
326
+ this.clearAccountStaging();
327
+ } else this.reconcileAccountStaging();
264
328
  this.publish();
265
329
  }
330
+ /**
331
+ * Drop account staging the stored section already reflects: additions whose
332
+ * ref is now stored, removals whose ref is gone, and label drafts matching
333
+ * the stored label. Key drafts are kept — a landed key write is idempotent
334
+ * on retry, and the draft carries the user's intent when it was the
335
+ * accounts write that failed.
336
+ */
337
+ reconcileAccountStaging() {
338
+ const stored = new Set(this.storedExtras().map((extra) => extra.ref));
339
+ this.addedAccounts = this.addedAccounts.filter((extra) => !stored.has(extra.ref));
340
+ for (const ref of [...this.removedRefs]) if (!stored.has(ref)) this.removedRefs.delete(ref);
341
+ for (const [ref, text] of [...this.labelDrafts]) {
342
+ const storedLabel = this.storedExtras().find((extra) => extra.ref === ref)?.label;
343
+ if (storedLabel === void 0 || storedLabel === text.trim()) this.labelDrafts.delete(ref);
344
+ }
345
+ }
266
346
  spec(field) {
267
347
  const spec = this.specs.get(field);
268
348
  if (spec === void 0) throw new Error(`commandcode settings page has no field ${field}`);
@@ -349,38 +429,136 @@ window.__ModuleLoader__.load({
349
429
  await this.scope.set(field, value);
350
430
  return this.userLayer()?.[field] === value;
351
431
  }
352
- /** Write the staged key, then re-read whether the Host now holds one. */
432
+ /** Write the staged default key, then re-read whether the Host holds it. */
353
433
  async writeKey(value) {
434
+ return this.writeKeyTo(this.credentialRef, value);
435
+ }
436
+ /** Write one account's key, then re-read the Host's credential states. */
437
+ async writeKeyTo(ref, value) {
354
438
  try {
355
439
  if (!(await this.api.credentials.set({
356
- ref: this.credential.ref,
440
+ ref,
357
441
  value
358
442
  })).result.ok) return false;
359
443
  } catch {
360
444
  return false;
361
445
  }
362
- await this.readCredential();
363
- return this.credential.configured;
446
+ await this.describeAll();
447
+ return this.credentialStates.get(ref)?.configured ?? false;
364
448
  }
365
- /** Ask the credentials domain about the reference this page writes. */
366
- async readCredential() {
367
- const ref = this.credential.ref;
449
+ /** Ask the credentials domain about every reference this page writes. */
450
+ async describeAll() {
451
+ const refs = [
452
+ this.credentialRef,
453
+ ...this.storedExtras().map((extra) => extra.ref),
454
+ ...this.addedAccounts.map((extra) => extra.ref)
455
+ ];
368
456
  let response;
369
457
  try {
370
- response = await this.api.credentials.describe({ refs: [ref] });
458
+ response = await this.api.credentials.describe({ refs });
371
459
  } catch {
372
460
  return;
373
461
  }
374
462
  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();
463
+ let changed = false;
464
+ for (const ref of refs) {
465
+ const view = response.result.value.credentials[ref];
466
+ const next = {
467
+ configured: view?.configured ?? false,
468
+ writable: view?.writable ?? true
469
+ };
470
+ const prev = this.credentialStates.get(ref);
471
+ if (prev === void 0 || prev.configured !== next.configured || prev.writable !== next.writable) {
472
+ this.credentialStates.set(ref, next);
473
+ changed = true;
474
+ }
475
+ }
476
+ if (changed) this.publish();
477
+ }
478
+ /** The stored extra accounts from the settings section (`accounts`). */
479
+ storedExtras() {
480
+ const raw = this.scope.getSnapshot().value?.accounts;
481
+ if (!Array.isArray(raw)) return [];
482
+ const out = [];
483
+ for (const entry of raw) {
484
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
485
+ const record = entry;
486
+ const ref = record.apiKeyEnv;
487
+ if (typeof ref !== "string" || ref === "") continue;
488
+ const label = record.label;
489
+ out.push({
490
+ label: typeof label === "string" && label !== "" ? label : ref,
491
+ ref
492
+ });
493
+ }
494
+ return out;
495
+ }
496
+ /** Every extra account row: stored (minus staged removals) + staged adds. */
497
+ effectiveAccounts() {
498
+ const stored = this.storedExtras().filter((extra) => !this.removedRefs.has(extra.ref)).map((extra) => ({
499
+ ...extra,
500
+ added: false
501
+ }));
502
+ const added = this.addedAccounts.map((extra) => ({
503
+ ...extra,
504
+ added: true
505
+ }));
506
+ return [...stored, ...added].map((extra) => ({
507
+ id: extra.ref,
508
+ ref: extra.ref,
509
+ label: this.labelDrafts.get(extra.ref) ?? extra.label,
510
+ keyText: this.keyDrafts.get(extra.ref) ?? "",
511
+ configured: this.credentialStates.get(extra.ref)?.configured ?? false,
512
+ writable: this.credentialStates.get(extra.ref)?.writable ?? true,
513
+ added: extra.added
514
+ }));
515
+ }
516
+ /** Whether any account-level staging (add/remove/label/key) exists. */
517
+ accountsStaged() {
518
+ return this.addedAccounts.length > 0 || this.removedRefs.size > 0 || this.labelDrafts.size > 0 || this.keyDrafts.size > 0;
519
+ }
520
+ /** Whether the staged account edits differ from the stored section. */
521
+ accountsDirty() {
522
+ if (this.addedAccounts.length > 0 || this.removedRefs.size > 0) return true;
523
+ for (const [ref, text] of this.labelDrafts) {
524
+ const base = this.storedExtras().find((extra) => extra.ref === ref)?.label;
525
+ if (base !== void 0 && text.trim() !== "" && text !== base) return true;
526
+ }
527
+ for (const text of this.keyDrafts.values()) if (text.trim() !== "") return true;
528
+ return false;
529
+ }
530
+ /** Reset every account-level staged edit. */
531
+ clearAccountStaging() {
532
+ this.addedAccounts = [];
533
+ this.removedRefs.clear();
534
+ this.labelDrafts.clear();
535
+ this.keyDrafts.clear();
536
+ }
537
+ /** The account-level writes a save performs (empty when nothing staged). */
538
+ accountPlan() {
539
+ if (!this.accountsDirty()) return [];
540
+ const runs = [];
541
+ for (const [ref, text] of this.keyDrafts) {
542
+ const value = text.trim();
543
+ if (value !== "" && !this.removedRefs.has(ref)) runs.push(() => this.writeKeyTo(ref, value));
544
+ }
545
+ runs.push(() => this.writeAccounts());
546
+ return runs;
547
+ }
548
+ /** Persist the staged accounts list into the settings section. */
549
+ async writeAccounts() {
550
+ const base = [...this.storedExtras().filter((extra) => !this.removedRefs.has(extra.ref)), ...this.addedAccounts];
551
+ const seen = /* @__PURE__ */ new Set();
552
+ const list = base.filter((extra) => !seen.has(extra.ref) && (seen.add(extra.ref), true)).map((extra) => {
553
+ const draft = this.labelDrafts.get(extra.ref)?.trim();
554
+ return {
555
+ label: draft !== void 0 && draft !== "" ? draft : extra.label,
556
+ apiKeyEnv: extra.ref
557
+ };
558
+ });
559
+ await this.scope.set("accounts", list);
560
+ const after = this.storedExtras();
561
+ return after.length === list.length && list.every((item, index) => after[index]?.ref === item.apiKeyEnv);
384
562
  }
385
563
  publish() {
386
564
  if (this.disposed) return;
@@ -594,6 +772,25 @@ window.__ModuleLoader__.load({
594
772
  }
595
773
  return report;
596
774
  }
775
+ /** Parse one untrusted boundary value into a {@link CommandCodeAccountUsage}. */
776
+ function parseAccountUsage(value) {
777
+ const source = record(value, "account");
778
+ return {
779
+ id: stringField(source, "id", "account.id"),
780
+ label: stringField(source, "label", "account.label"),
781
+ configured: booleanField(source, "configured", "account.configured"),
782
+ active: booleanField(source, "active", "account.active"),
783
+ mark: stringField(source, "mark", "account.mark"),
784
+ cooldownUntil: numberField(source, "cooldownUntil", "account.cooldownUntil"),
785
+ report: parseUsageReport(source.report)
786
+ };
787
+ }
788
+ /** Parse the wire result into a {@link CommandCodeAccountsReport}. */
789
+ function parseAccountsReport(value) {
790
+ const accounts = record(value, "result").accounts;
791
+ if (!Array.isArray(accounts)) reject("accounts");
792
+ return { accounts: accounts.map(parseAccountUsage) };
793
+ }
597
794
  /** The Client-face contribution mounted on `ctx.remote`. */
598
795
  const USAGE_REMOTE_CONTRIBUTION = {
599
796
  package: USAGE_REMOTE_PACKAGE,
@@ -606,8 +803,8 @@ window.__ModuleLoader__.load({
606
803
  parameters: [],
607
804
  result: {
608
805
  mode: "strict",
609
- typeSymbol: `${USAGE_REMOTE_PACKAGE}#CommandCodeUsageReport`,
610
- schema: { parse: parseUsageReport }
806
+ typeSymbol: `${USAGE_REMOTE_PACKAGE}#CommandCodeAccountsReport`,
807
+ schema: { parse: parseAccountsReport }
611
808
  }
612
809
  }]
613
810
  };
@@ -815,38 +1012,55 @@ window.__ModuleLoader__.load({
815
1012
  ]
816
1013
  });
817
1014
  }
1015
+ /** One account's rotation state as a short badge next to its label. */
1016
+ function AccountMark({ entry, t }) {
1017
+ if (entry.active) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1018
+ className: "cc-usagePlan",
1019
+ children: t("usageActive")
1020
+ });
1021
+ if (entry.mark === "invalid-credential") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1022
+ className: "cc-usagePlanStatus",
1023
+ children: t("usageInvalidKey")
1024
+ });
1025
+ if (entry.cooldownUntil > 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1026
+ className: "cc-usagePlanStatus",
1027
+ children: [
1028
+ t("usageCooldown"),
1029
+ " ",
1030
+ formatResetAt(entry.cooldownUntil)
1031
+ ]
1032
+ });
1033
+ if (entry.mark === "rate-limit") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1034
+ className: "cc-usagePlanStatus",
1035
+ children: t("usageCooldown")
1036
+ });
1037
+ return null;
1038
+ }
818
1039
  /**
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.
1040
+ * One pool account's facts (identity, totals, credits, window limits)
1041
+ * rendered inside the account-usage card.
823
1042
  */
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;
1043
+ function AccountReport({ entry, t, onRemove }) {
1044
+ const report = entry.report;
1045
+ const account = report.account;
835
1046
  const accountName = account === void 0 ? "" : account.userName || account.name;
836
- const credits = report?.credits;
837
- const plan = report?.plan;
1047
+ const credits = report.credits;
1048
+ const plan = report.plan;
838
1049
  const planName = plan?.name ?? "";
839
1050
  const planStatus = plan !== void 0 && plan.status !== "" && plan.status !== "active" ? plan.status : "";
840
1051
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
841
- className: "cc-usageCard",
842
- "aria-label": t("usageTitle"),
1052
+ className: "cc-accountReport",
843
1053
  children: [
844
1054
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
845
1055
  className: "cc-usageHead",
846
1056
  children: [
847
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1057
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
848
1058
  className: "cc-usageTitle",
849
- children: t("usageTitle")
1059
+ children: entry.label
1060
+ }),
1061
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountMark, {
1062
+ entry,
1063
+ t
850
1064
  }),
851
1065
  accountName !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
852
1066
  className: "cc-usageAccount",
@@ -860,29 +1074,20 @@ window.__ModuleLoader__.load({
860
1074
  className: "cc-usagePlanStatus",
861
1075
  children: planStatus
862
1076
  }) : null,
863
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1077
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }),
1078
+ onRemove !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
864
1079
  type: "button",
865
1080
  className: "cc-usageRefresh",
866
- disabled: loading || !apiKeyConfigured,
867
- onClick: onRefresh,
868
- children: loading ? t("usageRefreshing") : t("usageRefresh")
869
- })
1081
+ onClick: onRemove,
1082
+ children: t("accountRemove")
1083
+ }) : null
870
1084
  ]
871
1085
  }),
872
- !apiKeyConfigured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1086
+ !entry.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
873
1087
  className: "cc-usageHint",
874
- children: t("usageNoKey")
1088
+ children: t("usageUnconfigured")
875
1089
  }) : 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", {
1090
+ report.usage !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
886
1091
  className: "cc-usageStats",
887
1092
  children: [
888
1093
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageStat, {
@@ -935,25 +1140,17 @@ window.__ModuleLoader__.load({
935
1140
  t
936
1141
  })]
937
1142
  }) : null,
938
- report !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1143
+ plan !== void 0 && plan.currentPeriodEnd > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
939
1144
  className: "cc-usageMeta",
940
1145
  children: [
941
- plan !== void 0 && plan.currentPeriodEnd > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1146
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
942
1147
  className: "cc-usageUpdated",
943
1148
  children: [
944
1149
  t("usagePeriodEnd"),
945
1150
  " ",
946
1151
  new Date(plan.currentPeriodEnd).toLocaleDateString()
947
1152
  ]
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,
1153
+ }),
957
1154
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }),
958
1155
  report.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
959
1156
  className: "cc-usagePartial",
@@ -961,10 +1158,280 @@ window.__ModuleLoader__.load({
961
1158
  children: t("usagePartial")
962
1159
  }) : null
963
1160
  ]
1161
+ }) : report.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1162
+ className: "cc-usageMeta",
1163
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1164
+ className: "cc-usagePartial",
1165
+ title: report.failures.join("; "),
1166
+ children: t("usagePartial")
1167
+ })]
964
1168
  }) : null
965
1169
  ]
966
1170
  });
967
1171
  }
1172
+ /** The status dot on an account tab: cooling/invalid warn, everything else ok. */
1173
+ function AccountTabDot({ entry }) {
1174
+ const cls = entry.mark === "invalid-credential" ? "cc-tabDot cc-tabDotError" : entry.mark !== "" || entry.cooldownUntil > 0 ? "cc-tabDot cc-tabDotWarn" : "cc-tabDot cc-tabDotOk";
1175
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cls });
1176
+ }
1177
+ /**
1178
+ * The account-usage card: the `/commandcode` dashboard's facts rendered as
1179
+ * a native settings card. With several accounts the card is a carousel — a
1180
+ * tab strip (label + status dot) switches between accounts so the page stays
1181
+ * short; each account's report carries its own remove affordance (the
1182
+ * default account is not removable). Accounts staged for removal in the
1183
+ * management card are hidden here immediately. Data arrives through the
1184
+ * `commandcode/report` Remote; the API keys never leave the Host.
1185
+ */
1186
+ function UsageCard({ t, usage, apiKeyConfigured, removingIds, removableIds, canManage, onRefresh, onRemoveAccount }) {
1187
+ (0, react.useEffect)(() => {
1188
+ if (apiKeyConfigured && usage.status === "idle") onRefresh();
1189
+ }, [
1190
+ apiKeyConfigured,
1191
+ usage.status,
1192
+ onRefresh
1193
+ ]);
1194
+ const loading = usage.status === "loading";
1195
+ const report = usage.report;
1196
+ const [locallyRemoved, setLocallyRemoved] = (0, react.useState)([]);
1197
+ (0, react.useEffect)(() => {
1198
+ setLocallyRemoved([]);
1199
+ }, [usage.fetchedAt]);
1200
+ const hidden = /* @__PURE__ */ new Set([...removingIds, ...locallyRemoved]);
1201
+ const seenIds = /* @__PURE__ */ new Set();
1202
+ const entries = (report?.accounts ?? []).filter((entry) => {
1203
+ if (hidden.has(entry.id)) return false;
1204
+ if (seenIds.has(entry.id)) return false;
1205
+ seenIds.add(entry.id);
1206
+ return true;
1207
+ });
1208
+ const [selectedId, setSelectedId] = (0, react.useState)(void 0);
1209
+ const selected = entries.find((entry) => entry.id === selectedId) ?? entries.find((entry) => entry.active) ?? entries[0];
1210
+ const removeSelected = canManage && selected !== void 0 && removableIds.includes(selected.id) ? () => {
1211
+ const id = selected.id;
1212
+ setLocallyRemoved((prev) => [...prev, id]);
1213
+ onRemoveAccount(id);
1214
+ } : void 0;
1215
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1216
+ className: "cc-usageCard",
1217
+ "aria-label": t("usageTitle"),
1218
+ children: [
1219
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1220
+ className: "cc-usageHead",
1221
+ children: [
1222
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1223
+ className: "cc-usageTitle",
1224
+ children: t("usageTitle")
1225
+ }),
1226
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }),
1227
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1228
+ type: "button",
1229
+ className: "cc-usageRefresh",
1230
+ disabled: loading || !apiKeyConfigured,
1231
+ onClick: onRefresh,
1232
+ children: loading ? t("usageRefreshing") : t("usageRefresh")
1233
+ })
1234
+ ]
1235
+ }),
1236
+ !apiKeyConfigured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1237
+ className: "cc-usageHint",
1238
+ children: t("usageNoKey")
1239
+ }) : null,
1240
+ apiKeyConfigured && report === void 0 && loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1241
+ className: "cc-usageHint",
1242
+ children: t("usageLoading")
1243
+ }) : null,
1244
+ usage.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1245
+ className: "cc-usageError",
1246
+ role: "status",
1247
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [t("usageError"), usage.error !== void 0 && usage.error !== "" ? ` — ${usage.error}` : ""] })
1248
+ }) : null,
1249
+ entries.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1250
+ className: "cc-tabs",
1251
+ role: "tablist",
1252
+ "aria-label": t("accountsTitle"),
1253
+ children: entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1254
+ type: "button",
1255
+ role: "tab",
1256
+ "aria-selected": selected?.id === entry.id,
1257
+ className: selected?.id === entry.id ? "cc-tab cc-tabActive" : "cc-tab",
1258
+ onClick: () => setSelectedId(entry.id),
1259
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountTabDot, { entry }), entry.label]
1260
+ }, entry.id))
1261
+ }) : null,
1262
+ selected !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountReport, {
1263
+ entry: selected,
1264
+ t,
1265
+ onRemove: removeSelected
1266
+ }, selected.id) : null,
1267
+ report !== void 0 && usage.fetchedAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1268
+ className: "cc-usageMeta",
1269
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1270
+ className: "cc-usageUpdated",
1271
+ children: [
1272
+ t("usageUpdated"),
1273
+ " ",
1274
+ new Date(usage.fetchedAt).toLocaleTimeString()
1275
+ ]
1276
+ })]
1277
+ }) : null
1278
+ ]
1279
+ });
1280
+ }
1281
+ /**
1282
+ * One extra account row: label, key, configured badge. Saved accounts are
1283
+ * removed from the usage card above; a NOT-YET-SAVED addition never appears
1284
+ * there (the usage report is Host-side), so it keeps its own remove button —
1285
+ * otherwise the only way to undo a mistaken Add would be discarding every
1286
+ * other staged edit.
1287
+ */
1288
+ function AccountRow({ account, disabled, t, onLabel, onKey, onRemove }) {
1289
+ const locked = !account.writable;
1290
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1291
+ className: "cc-field",
1292
+ children: [
1293
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1294
+ className: "cc-fieldHead",
1295
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1296
+ className: "cc-label",
1297
+ htmlFor: `cc-account-label-${account.id}`,
1298
+ children: t("accountLabel")
1299
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1300
+ className: "cc-badges",
1301
+ children: [
1302
+ account.added ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1303
+ className: "cc-badge",
1304
+ children: t("unsaved")
1305
+ }) : null,
1306
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1307
+ className: account.configured ? "cc-badge" : "cc-badgeMuted",
1308
+ children: account.configured ? t("apiKeySet") : t("apiKeyUnset")
1309
+ }),
1310
+ account.added ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1311
+ type: "button",
1312
+ className: "cc-reset",
1313
+ disabled,
1314
+ onClick: onRemove,
1315
+ children: t("accountRemove")
1316
+ }) : null
1317
+ ]
1318
+ })]
1319
+ }),
1320
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1321
+ id: `cc-account-label-${account.id}`,
1322
+ className: "cc-input",
1323
+ type: "text",
1324
+ value: account.label,
1325
+ disabled,
1326
+ onChange: (event) => onLabel(event.target.value)
1327
+ }),
1328
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1329
+ id: `cc-account-key-${account.id}`,
1330
+ className: "cc-input",
1331
+ type: "password",
1332
+ autoComplete: "off",
1333
+ placeholder: t("accountKey"),
1334
+ value: account.keyText,
1335
+ disabled: disabled || locked,
1336
+ onChange: (event) => onKey(event.target.value)
1337
+ }),
1338
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1339
+ className: "cc-hint",
1340
+ children: locked ? t("apiKeyLocked") : t("accountKeyHint")
1341
+ })
1342
+ ]
1343
+ });
1344
+ }
1345
+ /** The multi-account card: the active-account selector + extra accounts in rotation order + add button. */
1346
+ function AccountsCard({ t, state, disabled, onAdd, onRemove, onLabel, onKey, onActive, onActiveReset }) {
1347
+ const active = state.activeAccount;
1348
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1349
+ className: "cc-card",
1350
+ "aria-label": t("accountsTitle"),
1351
+ children: [
1352
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1353
+ className: "cc-field",
1354
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1355
+ className: "cc-fieldHead",
1356
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1357
+ className: "cc-label",
1358
+ children: t("accountsTitle")
1359
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1360
+ className: "cc-badges",
1361
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1362
+ type: "button",
1363
+ className: "cc-reset",
1364
+ disabled,
1365
+ onClick: onAdd,
1366
+ children: t("accountAdd")
1367
+ })
1368
+ })]
1369
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1370
+ className: "cc-hint",
1371
+ children: t("accountsHint")
1372
+ })]
1373
+ }),
1374
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1375
+ className: "cc-field",
1376
+ children: [
1377
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1378
+ className: "cc-fieldHead",
1379
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1380
+ className: "cc-label",
1381
+ htmlFor: "cc-active-account",
1382
+ children: t("activeAccount")
1383
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1384
+ className: "cc-badges",
1385
+ children: [active.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1386
+ className: "cc-badge",
1387
+ children: t("overridden")
1388
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1389
+ type: "button",
1390
+ className: "cc-reset",
1391
+ disabled,
1392
+ onClick: onActiveReset,
1393
+ children: t("reset")
1394
+ })]
1395
+ })]
1396
+ }),
1397
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1398
+ id: "cc-active-account",
1399
+ className: "cc-input",
1400
+ value: active.text,
1401
+ disabled,
1402
+ onChange: (event) => onActive(event.target.value),
1403
+ children: [
1404
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1405
+ value: "",
1406
+ children: t("activeAccountAuto")
1407
+ }),
1408
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1409
+ value: "default",
1410
+ children: t("accountDefault")
1411
+ }),
1412
+ state.accounts.filter((account) => !account.added).map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1413
+ value: account.ref,
1414
+ children: account.label
1415
+ }, account.id))
1416
+ ]
1417
+ }),
1418
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1419
+ className: "cc-hint",
1420
+ children: t("activeAccountHint")
1421
+ })
1422
+ ]
1423
+ }),
1424
+ state.accounts.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountRow, {
1425
+ account,
1426
+ disabled,
1427
+ t,
1428
+ onLabel: (text) => onLabel(account.id, text),
1429
+ onKey: (text) => onKey(account.id, text),
1430
+ onRemove: () => onRemove(account.id)
1431
+ }, account.id))
1432
+ ]
1433
+ });
1434
+ }
968
1435
  /** The settings page body: connection facts for the Command Code provider. */
969
1436
  function CommandCodeSettingsPage(props) {
970
1437
  const { t } = props;
@@ -992,8 +1459,23 @@ window.__ModuleLoader__.load({
992
1459
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageCard, {
993
1460
  t,
994
1461
  usage,
995
- apiKeyConfigured: state.apiKeyConfigured,
996
- onRefresh: props.refreshUsage
1462
+ apiKeyConfigured: state.anyAccountConfigured,
1463
+ removingIds: state.accountsRemoving,
1464
+ removableIds: state.accounts.map((account) => account.id),
1465
+ canManage: state.writable,
1466
+ onRefresh: props.refreshUsage,
1467
+ onRemoveAccount: props.removeAccount
1468
+ }),
1469
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountsCard, {
1470
+ t,
1471
+ state,
1472
+ disabled,
1473
+ onAdd: props.addAccount,
1474
+ onRemove: props.removeAccount,
1475
+ onLabel: props.editAccountLabel,
1476
+ onKey: props.editAccountKey,
1477
+ onActive: (text) => props.edit("activeAccount", text),
1478
+ onActiveReset: () => props.resetField("activeAccount")
997
1479
  }),
998
1480
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
999
1481
  className: "cc-card",
@@ -1112,6 +1594,17 @@ window.__ModuleLoader__.load({
1112
1594
  streamIdleTimeoutMsHint: "生成流停滞多久视为断连;默认 300000(长思考模型可静默数分钟,默认值刻意放宽)。",
1113
1595
  filterModelsByPlan: "隐藏套餐外模型",
1114
1596
  filterModelsByPlanHint: "开启后,模型选择器只列出当前套餐可用的模型;账户持有按需余额时会显示全部。",
1597
+ accountsTitle: "多账户轮换",
1598
+ accountsHint: "当前账户达到用量限额(429)或密钥失效(401)时,请求自动切换到下一个账户;全部耗尽时会提示最早的重置时间。",
1599
+ accountAdd: "添加账户",
1600
+ accountRemove: "移除",
1601
+ accountLabel: "账户备注名",
1602
+ accountKey: "API 密钥",
1603
+ accountKeyHint: "该账户的 API 密钥。留空保存不会覆盖已存储的密钥。",
1604
+ accountDefault: "默认账户",
1605
+ activeAccount: "当前使用账户",
1606
+ activeAccountAuto: "自动(第一个可用账户)",
1607
+ activeAccountHint: "手动指定优先使用的账户,保存后下次请求即生效;所选账户耗尽时仍会自动切换到其他可用账户。",
1115
1608
  overridden: "已覆盖",
1116
1609
  reset: "重置",
1117
1610
  invalidNumber: "无效数字",
@@ -1144,7 +1637,11 @@ window.__ModuleLoader__.load({
1144
1637
  usageReset: "重置于",
1145
1638
  usagePartial: "部分端点数据不可用",
1146
1639
  usageUpdated: "更新于",
1147
- usagePeriodEnd: "账期截止"
1640
+ usagePeriodEnd: "账期截止",
1641
+ usageActive: "当前使用",
1642
+ usageCooldown: "限额冷却中",
1643
+ usageInvalidKey: "密钥无效",
1644
+ usageUnconfigured: "该账户尚未配置 API 密钥。"
1148
1645
  };
1149
1646
  const en = {
1150
1647
  nav: "Command Code",
@@ -1165,6 +1662,17 @@ window.__ModuleLoader__.load({
1165
1662
  streamIdleTimeoutMsHint: "How long a stalled stream is treated as dead; default 300000 (deliberately generous — long-thinking models can stay silent for minutes).",
1166
1663
  filterModelsByPlan: "Hide out-of-plan models",
1167
1664
  filterModelsByPlanHint: "When on, the model picker lists only models your subscription includes; any on-demand credit balance shows the full catalog.",
1665
+ accountsTitle: "Account rotation",
1666
+ 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.",
1667
+ accountAdd: "Add account",
1668
+ accountRemove: "Remove",
1669
+ accountLabel: "Account label",
1670
+ accountKey: "API key",
1671
+ accountKeyHint: "This account’s API key. Saving with the field blank keeps the stored key.",
1672
+ accountDefault: "Default account",
1673
+ activeAccount: "Active account",
1674
+ activeAccountAuto: "Auto (first usable account)",
1675
+ 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
1676
  overridden: "Overridden",
1169
1677
  reset: "Reset",
1170
1678
  invalidNumber: "Invalid number",
@@ -1197,7 +1705,11 @@ window.__ModuleLoader__.load({
1197
1705
  usageReset: "Resets",
1198
1706
  usagePartial: "Some endpoint data unavailable",
1199
1707
  usageUpdated: "Updated",
1200
- usagePeriodEnd: "Period ends"
1708
+ usagePeriodEnd: "Period ends",
1709
+ usageActive: "Active",
1710
+ usageCooldown: "Cooling down",
1711
+ usageInvalidKey: "Invalid key",
1712
+ usageUnconfigured: "No API key configured for this account yet."
1201
1713
  };
1202
1714
  //#endregion
1203
1715
  //#region src/client/index.ts
@@ -1260,6 +1772,14 @@ window.__ModuleLoader__.load({
1260
1772
  .cc-usageBarFillWarn{background:var(--dsw-alias-label-error)}
1261
1773
  .cc-usageWindowReset{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}
1262
1774
  .cc-usageMeta{align-items:center;gap:8px;display:flex}
1775
+ .cc-accountReport{flex-direction:column;gap:12px;display:flex}
1776
+ .cc-tabs{flex-wrap:wrap;gap:6px;display:flex}
1777
+ .cc-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}
1778
+ .cc-tab:hover:not(.cc-tabActive){color:var(--dsw-alias-label-primary)}
1779
+ .cc-tabActive{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-brand-primary)}
1780
+ .cc-tabDotOk{background:var(--dsw-alias-brand-primary);border-radius:50%;width:6px;height:6px}
1781
+ .cc-tabDotWarn{background:#d97706;border-radius:50%;width:6px;height:6px}
1782
+ .cc-tabDotError{background:var(--dsw-alias-label-error);border-radius:50%;width:6px;height:6px}
1263
1783
  .cc-usageMetaSpacer{flex:1}
1264
1784
  .cc-usageUpdated{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}
1265
1785
  .cc-usagePartial{color:var(--dsw-alias-label-error);margin:0;font-size:11px;line-height:1.5}
@@ -1341,10 +1861,14 @@ window.__ModuleLoader__.load({
1341
1861
  resetField: (field) => controller.resetField(field),
1342
1862
  save: () => void controller.save().then(() => {
1343
1863
  const settled = controller.state();
1344
- if (!settled.failed && settled.apiKeyConfigured) usageController.refresh();
1864
+ if (!settled.failed && settled.anyAccountConfigured) usageController.refresh();
1345
1865
  }),
1346
1866
  discard: () => controller.discard(),
1347
- refreshUsage: () => void usageController.refresh()
1867
+ refreshUsage: () => void usageController.refresh(),
1868
+ addAccount: () => controller.addAccount(),
1869
+ removeAccount: (id) => controller.removeAccount(id),
1870
+ editAccountLabel: (id, text) => controller.editAccountLabel(id, text),
1871
+ editAccountKey: (id, text) => controller.editAccountKey(id, text)
1348
1872
  });
1349
1873
  ctx.slots.inject("settings.section", () => ctx.slots.register({
1350
1874
  name: "settings.section",