@mars-sea/dsh-commandcode-provider 0.9.1 → 0.10.0-alpha.2

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
@@ -4,10 +4,38 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
8
7
  let react = require("react");
9
8
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
+ //#region src/client/snapshot-store.ts
11
+ /** Notify every subscriber without letting one faulty UI consumer suppress the rest. */
12
+ function notifyListeners(listeners) {
13
+ for (const listener of listeners) try {
14
+ listener();
15
+ } catch (error) {
16
+ console.error("[dsh-commandcode-provider] snapshot subscriber failed:", error);
17
+ }
18
+ }
19
+ /** Create one observable snapshot store. */
20
+ function createSnapshotStore(initial) {
21
+ let snapshot = initial;
22
+ const listeners = /* @__PURE__ */ new Set();
23
+ return {
24
+ getSnapshot: () => snapshot,
25
+ subscribe(listener) {
26
+ listeners.add(listener);
27
+ return () => {
28
+ listeners.delete(listener);
29
+ };
30
+ },
31
+ set(value) {
32
+ if (Object.is(value, snapshot)) return;
33
+ snapshot = value;
34
+ notifyListeners(listeners);
35
+ }
36
+ };
37
+ }
38
+ //#endregion
11
39
  //#region src/command-locales.ts
12
40
  const commandcodeCommand = {
13
41
  zh: {
@@ -137,12 +165,49 @@ window.__ModuleLoader__.load({
137
165
  }
138
166
  };
139
167
  }
140
- /** Install the wrapper on a connection's shared sessions face. */
168
+ /**
169
+ * Install the friendly-error wrapper when a legacy sessions face is present.
170
+ *
171
+ * @returns whether the wrapper was installed. The rewrite is only UX polish;
172
+ * a 0.1.2 connection has no `api.sessions`, and its absence must never block
173
+ * the plugin from mounting its settings, credential, or usage surfaces.
174
+ */
141
175
  function installFriendlyImageError(connection, getLocale) {
142
- connection.api.sessions = withFriendlyImageError(connection.api.sessions, getLocale);
176
+ const api = connection.api;
177
+ const sessions = api?.sessions;
178
+ if (api === void 0 || sessions === void 0 || typeof sessions.selectModel !== "function") return false;
179
+ api.sessions = withFriendlyImageError(sessions, getLocale);
180
+ return true;
143
181
  }
144
182
  //#endregion
145
183
  //#region src/client/settings.ts
184
+ /**
185
+ * Browser controller for the "Command Code" settings page.
186
+ *
187
+ * The page lives at the same settings-nav level as General / Models / Plugins
188
+ * (a `settings.section` entry, id `commandcode`). It exists because the
189
+ * Models page renders an unknown-adapter-family card for the `commandcode`
190
+ * provider and deliberately disables its submit — the API key cannot be
191
+ * configured there. This page owns the connection facts the plugin resolves
192
+ * per request:
193
+ *
194
+ * - API key -> written through the credentials domain under the reference
195
+ * the plugin resolves (`apiKeyEnv`, default
196
+ * `COMMANDCODE_API_KEY`). The literal never rides a response,
197
+ * so the control only reports whether one is configured.
198
+ * - API base -> the `llm-commandcode` settings namespace (`apiBase`), same
199
+ * namespace the Models page card addresses.
200
+ * - Working dir, request/stream timeouts -> the same namespace.
201
+ *
202
+ * The controller mirrors the plugin-card pattern from the harness's own
203
+ * settings UI: it binds the `llm-commandcode` namespace through the
204
+ * `settingsScope` service, keeps a staged draft of edits, and writes them on
205
+ * save through `scope.set` / the credentials domain. The Host stays the
206
+ * single fact source; the snapshot is republished after each accepted write.
207
+ *
208
+ * This module is deliberately free of JSX — it only produces the state face
209
+ * the React component renders.
210
+ */
146
211
  /** The settings namespace the plugin registers (host half, src/index.ts). */
147
212
  const COMMANDCODE_NS = "llm-commandcode";
148
213
  /** Default credential reference the plugin resolves when none is named. */
@@ -239,6 +304,12 @@ window.__ModuleLoader__.load({
239
304
  booleanField$1("filterModelsByPlan"),
240
305
  textField("activeAccount")
241
306
  ];
307
+ /** Whether two model-id lists are equal as sets (order-insensitive). */
308
+ function sameModels(a, b) {
309
+ if (a.length !== b.length) return false;
310
+ const set = new Set(a);
311
+ return b.every((id) => set.has(id));
312
+ }
242
313
  /**
243
314
  * Controller bridging the `llm-commandcode` scope and the credentials domain
244
315
  * onto the page. Public API mirrors the harness's CardForm actions, so the
@@ -267,6 +338,15 @@ window.__ModuleLoader__.load({
267
338
  keyDrafts = /* @__PURE__ */ new Map();
268
339
  /** Credential references staged for removal on the next save. */
269
340
  keyClears = /* @__PURE__ */ new Set();
341
+ /** Staged model→account routing rules (not yet saved). */
342
+ addedRules = [];
343
+ /** Staged edits to stored routing rules, by stored row id. */
344
+ ruleDrafts = /* @__PURE__ */ new Map();
345
+ /** Stored routing rule rows staged for removal. */
346
+ removedRuleIds = /* @__PURE__ */ new Set();
347
+ /** The catalog the rule editor offers (Host-side). */
348
+ catalogModels = [];
349
+ catalogFailed = false;
270
350
  saving = false;
271
351
  failed = false;
272
352
  savedCount = 0;
@@ -297,6 +377,7 @@ window.__ModuleLoader__.load({
297
377
  }
298
378
  this.recomputeCredentialRef();
299
379
  this.describeAll();
380
+ this.refreshCatalog();
300
381
  }
301
382
  /** Release every subscription held on external sources. Idempotent. */
302
383
  dispose() {
@@ -353,7 +434,10 @@ window.__ModuleLoader__.load({
353
434
  activeAccount: this.field("activeAccount"),
354
435
  accounts,
355
436
  accountsRemoving: [...this.removedRefs],
356
- dirty: plan.length > 0 || this.accountsDirty(),
437
+ rules: this.effectiveRules(),
438
+ catalogModels: this.catalogModels,
439
+ catalogFailed: this.catalogFailed,
440
+ dirty: plan.length > 0 || this.accountsDirty() || this.rulesDirty(),
357
441
  invalid: plan.some((item) => item.run === void 0),
358
442
  saving: this.saving,
359
443
  failed: this.failed,
@@ -426,6 +510,64 @@ window.__ModuleLoader__.load({
426
510
  this.describeAll();
427
511
  this.publish();
428
512
  }
513
+ /** Stage a new model → account routing rule (saved on the next `save()`). */
514
+ addRule() {
515
+ this.addedRules.push({
516
+ models: [],
517
+ account: "default"
518
+ });
519
+ this.failed = false;
520
+ this.publish();
521
+ }
522
+ /** Stage one routing rule's removal (or drop an unsaved addition). */
523
+ removeRule(id) {
524
+ const addedIndex = this.addedRules.findIndex((_, index) => `new-${index}` === id);
525
+ if (addedIndex >= 0) this.addedRules.splice(addedIndex, 1);
526
+ else this.removedRuleIds.add(id);
527
+ this.ruleDrafts.delete(id);
528
+ this.failed = false;
529
+ this.publish();
530
+ }
531
+ /** Stage one routing rule's selected model ids (multi-select). */
532
+ editRuleModels(id, models) {
533
+ const addedIndex = this.addedRules.findIndex((_, index) => `new-${index}` === id);
534
+ if (addedIndex >= 0) this.addedRules[addedIndex] = {
535
+ ...this.addedRules[addedIndex],
536
+ models
537
+ };
538
+ else {
539
+ const current = this.ruleDrafts.get(id) ?? this.storedRules().find((rule) => rule.id === id) ?? {
540
+ models: [],
541
+ account: "default"
542
+ };
543
+ this.ruleDrafts.set(id, {
544
+ ...current,
545
+ models
546
+ });
547
+ }
548
+ this.failed = false;
549
+ this.publish();
550
+ }
551
+ /** Stage one routing rule's target account draft. */
552
+ editRuleAccount(id, text) {
553
+ const addedIndex = this.addedRules.findIndex((_, index) => `new-${index}` === id);
554
+ if (addedIndex >= 0) this.addedRules[addedIndex] = {
555
+ ...this.addedRules[addedIndex],
556
+ account: text
557
+ };
558
+ else {
559
+ const current = this.ruleDrafts.get(id) ?? this.storedRules().find((rule) => rule.id === id) ?? {
560
+ models: [],
561
+ account: "default"
562
+ };
563
+ this.ruleDrafts.set(id, {
564
+ ...current,
565
+ account: text
566
+ });
567
+ }
568
+ this.failed = false;
569
+ this.publish();
570
+ }
429
571
  /** Stage one field's draft text. */
430
572
  edit(field, text) {
431
573
  this.staged.set(field, {
@@ -454,9 +596,10 @@ window.__ModuleLoader__.load({
454
596
  }
455
597
  /** Discard every staged edit. */
456
598
  discard() {
457
- if (this.staged.size === 0 && !this.accountsStaged() && !this.failed) return;
599
+ if (this.staged.size === 0 && !this.accountsStaged() && !this.rulesStaged() && !this.failed) return;
458
600
  this.staged.clear();
459
601
  this.clearAccountStaging();
602
+ this.clearRuleStaging();
460
603
  this.failed = false;
461
604
  this.publish();
462
605
  }
@@ -472,7 +615,8 @@ window.__ModuleLoader__.load({
472
615
  async save() {
473
616
  const plan = this.plan();
474
617
  const accountRuns = this.accountPlan();
475
- if (plan.length === 0 && accountRuns.length === 0 || this.saving) return;
618
+ const ruleRuns = this.rulesPlan();
619
+ if (plan.length === 0 && accountRuns.length === 0 && ruleRuns.length === 0 || this.saving) return;
476
620
  const runs = [];
477
621
  for (const item of plan) {
478
622
  if (item.run === void 0) return;
@@ -482,7 +626,11 @@ window.__ModuleLoader__.load({
482
626
  this.failed = false;
483
627
  this.publish();
484
628
  let landed = true;
485
- for (const run of [...runs, ...accountRuns]) if (!await run()) {
629
+ for (const run of [
630
+ ...runs,
631
+ ...accountRuns,
632
+ ...ruleRuns
633
+ ]) if (!await run()) {
486
634
  landed = false;
487
635
  break;
488
636
  }
@@ -492,7 +640,11 @@ window.__ModuleLoader__.load({
492
640
  this.savedCount += 1;
493
641
  this.staged.clear();
494
642
  this.clearAccountStaging();
495
- } else this.reconcileAccountStaging();
643
+ this.clearRuleStaging();
644
+ } else {
645
+ this.reconcileAccountStaging();
646
+ this.reconcileRuleStaging();
647
+ }
496
648
  this.publish();
497
649
  }
498
650
  /**
@@ -607,10 +759,7 @@ window.__ModuleLoader__.load({
607
759
  /** Write one account's key, then re-read the Host's credential states. */
608
760
  async writeKeyTo(ref, value) {
609
761
  try {
610
- if (!(await this.api.credentials.set({
611
- ref,
612
- value
613
- })).result.ok) return false;
762
+ if (!(await this.api.credentials.set(ref, value)).ok) return false;
614
763
  } catch {
615
764
  return false;
616
765
  }
@@ -626,14 +775,14 @@ window.__ModuleLoader__.load({
626
775
  ];
627
776
  let response;
628
777
  try {
629
- response = await this.api.credentials.describe({ refs });
778
+ response = await this.api.credentials.describe(refs);
630
779
  } catch {
631
780
  return;
632
781
  }
633
- if (!response.result.ok) return;
782
+ if (!response.ok) return;
634
783
  let changed = false;
635
784
  for (const ref of refs) {
636
- const view = response.result.value.credentials[ref];
785
+ const view = response.value?.[ref];
637
786
  const next = {
638
787
  configured: view?.configured ?? false,
639
788
  writable: view?.writable ?? true
@@ -646,6 +795,28 @@ window.__ModuleLoader__.load({
646
795
  }
647
796
  if (changed) this.publish();
648
797
  }
798
+ /**
799
+ * Fetch the model catalog for the routing-rule editor through the Host
800
+ * Remote. Runs once at construction; call again (e.g. from the client entry
801
+ * once the Remote mount lands) to (re)try — a later success clears a prior
802
+ * failure flag so the editor recovers without a page reload.
803
+ */
804
+ refreshCatalog() {
805
+ const models = this.api.models;
806
+ if (models === void 0) {
807
+ this.catalogFailed = true;
808
+ this.publish();
809
+ return;
810
+ }
811
+ models().then((response) => {
812
+ if (response.ok && Array.isArray(response.value?.models)) {
813
+ this.catalogModels = response.value.models;
814
+ this.catalogFailed = false;
815
+ } else this.catalogFailed = true;
816
+ }, () => {
817
+ this.catalogFailed = true;
818
+ }).then(() => this.publish());
819
+ }
649
820
  /** The stored extra accounts from the settings section (`accounts`). */
650
821
  storedExtras() {
651
822
  const raw = this.scope.getSnapshot().value?.accounts;
@@ -711,7 +882,7 @@ window.__ModuleLoader__.load({
711
882
  /** Unset one stored credential, then re-read the Host's credential states. */
712
883
  async unsetKey(ref) {
713
884
  try {
714
- if (!(await this.api.credentials.unset({ ref })).result.ok) return false;
885
+ if (!(await this.api.credentials.unset(ref)).ok) return false;
715
886
  } catch {
716
887
  return false;
717
888
  }
@@ -745,12 +916,140 @@ window.__ModuleLoader__.load({
745
916
  const after = this.storedExtras();
746
917
  return after.length === list.length && list.every((item, index) => after[index]?.ref === item.apiKeyEnv);
747
918
  }
919
+ /** The stored routing rules from the settings section (`modelAccountRules`). */
920
+ storedRules() {
921
+ const raw = this.scope.getSnapshot().value?.modelAccountRules;
922
+ if (!Array.isArray(raw)) return [];
923
+ const out = [];
924
+ for (const [index, entry] of raw.entries()) {
925
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
926
+ const record = entry;
927
+ const models = record.models;
928
+ const account = record.account;
929
+ const modelsList = Array.isArray(models) && models.every((m) => typeof m === "string") ? models.filter((m) => m !== "") : [];
930
+ if (modelsList.length === 0) continue;
931
+ out.push({
932
+ id: `rule-${index}`,
933
+ models: modelsList,
934
+ account: typeof account === "string" && account !== "" ? account : "default"
935
+ });
936
+ }
937
+ return out;
938
+ }
939
+ /** Every routing rule row: stored (minus staged removals, with drafts) + staged adds. */
940
+ effectiveRules() {
941
+ const stored = this.storedRules().filter((rule) => !this.removedRuleIds.has(rule.id)).map((rule) => {
942
+ const draft = this.ruleDrafts.get(rule.id);
943
+ return {
944
+ id: rule.id,
945
+ models: draft?.models ?? rule.models,
946
+ account: draft?.account ?? rule.account,
947
+ added: false
948
+ };
949
+ });
950
+ const added = this.addedRules.map((rule, index) => ({
951
+ id: `new-${index}`,
952
+ models: rule.models,
953
+ account: rule.account,
954
+ added: true
955
+ }));
956
+ return [...stored, ...added];
957
+ }
958
+ /** Whether any routing-rule staging (add/remove/edit) exists. */
959
+ rulesStaged() {
960
+ return this.addedRules.length > 0 || this.removedRuleIds.size > 0 || this.ruleDrafts.size > 0;
961
+ }
962
+ /** Whether the staged routing rules differ from the stored section. */
963
+ rulesDirty() {
964
+ if (this.addedRules.length > 0 || this.removedRuleIds.size > 0) return true;
965
+ for (const [id, draft] of this.ruleDrafts) {
966
+ const base = this.storedRules().find((rule) => rule.id === id);
967
+ if (base === void 0) continue;
968
+ if (draft.models.length > 0 && !sameModels(draft.models, base.models)) return true;
969
+ if (draft.account !== base.account) return true;
970
+ }
971
+ return false;
972
+ }
973
+ /** Reset every routing-rule staged edit. */
974
+ clearRuleStaging() {
975
+ this.addedRules = [];
976
+ this.ruleDrafts.clear();
977
+ this.removedRuleIds.clear();
978
+ }
979
+ /** Drop rule staging the stored section already reflects (partial-save retry). */
980
+ reconcileRuleStaging() {
981
+ const storedIds = new Set(this.storedRules().map((rule) => rule.id));
982
+ for (const id of [...this.removedRuleIds]) if (!storedIds.has(id)) this.removedRuleIds.delete(id);
983
+ for (const id of [...this.ruleDrafts.keys()]) if (!storedIds.has(id)) this.ruleDrafts.delete(id);
984
+ }
985
+ /** The routing-rule writes a save performs (empty when nothing staged). */
986
+ rulesPlan() {
987
+ if (!this.rulesDirty()) return [];
988
+ return [() => this.writeRules()];
989
+ }
990
+ /** Persist the staged routing rules into the settings section. */
991
+ async writeRules() {
992
+ const list = [...this.storedRules().filter((rule) => !this.removedRuleIds.has(rule.id)).map((rule) => {
993
+ const draft = this.ruleDrafts.get(rule.id);
994
+ return {
995
+ models: draft !== void 0 && draft.models.length > 0 ? draft.models : rule.models,
996
+ account: draft?.account !== void 0 && draft.account !== "" ? draft.account : rule.account
997
+ };
998
+ }), ...this.addedRules.map((rule) => ({
999
+ models: rule.models,
1000
+ account: rule.account
1001
+ }))].filter((rule) => rule.models.length > 0);
1002
+ await this.scope.set("modelAccountRules", list);
1003
+ const after = this.storedRules();
1004
+ return after.length === list.length && list.every((item, index) => after[index] !== void 0 && sameModels(after[index].models, item.models) && after[index].account === item.account);
1005
+ }
748
1006
  publish() {
749
1007
  if (this.disposed) return;
750
1008
  for (const listener of this.listeners) listener();
751
1009
  }
752
1010
  };
753
1011
  //#endregion
1012
+ //#region src/client/legacy-credentials.ts
1013
+ /** Convert a legacy credentials ApiProxy into the current settings-page face. */
1014
+ function adaptLegacyCredentials(legacy) {
1015
+ if (legacy === void 0) return void 0;
1016
+ return { credentials: {
1017
+ describe: async (refs) => {
1018
+ const response = await legacy.describe({ refs });
1019
+ return response.result.ok ? {
1020
+ ok: true,
1021
+ value: response.result.value.credentials
1022
+ } : {
1023
+ ok: false,
1024
+ error: response.result.error
1025
+ };
1026
+ },
1027
+ set: async (ref, value) => {
1028
+ const response = await legacy.set({
1029
+ ref,
1030
+ value
1031
+ });
1032
+ return response.result.ok ? {
1033
+ ok: true,
1034
+ value: void 0
1035
+ } : {
1036
+ ok: false,
1037
+ error: response.result.error
1038
+ };
1039
+ },
1040
+ unset: async (ref) => {
1041
+ const response = await legacy.unset({ ref });
1042
+ return response.result.ok ? {
1043
+ ok: true,
1044
+ value: void 0
1045
+ } : {
1046
+ ok: false,
1047
+ error: response.result.error
1048
+ };
1049
+ }
1050
+ } };
1051
+ }
1052
+ //#endregion
754
1053
  //#region src/client/usage.ts
755
1054
  const IDLE = {
756
1055
  status: "idle",
@@ -1218,6 +1517,39 @@ window.__ModuleLoader__.load({
1218
1517
  }
1219
1518
  }]
1220
1519
  };
1520
+ /** Canonical `<namespace>/<method>` endpoint of the model-catalog Remote. */
1521
+ const MODELS_ENDPOINT = "commandcode/models";
1522
+ /** Parse one untrusted boundary value into a {@link CommandCodeCatalogModel}. */
1523
+ function parseCatalogModel(value) {
1524
+ const source = record(value, "model");
1525
+ return {
1526
+ id: stringField(source, "id", "model.id"),
1527
+ name: stringField(source, "name", "model.name")
1528
+ };
1529
+ }
1530
+ /** Parse the wire result into a {@link CommandCodeCatalog}. */
1531
+ function parseCatalog(value) {
1532
+ const models = record(value, "result").models;
1533
+ if (!Array.isArray(models)) reject$1("models");
1534
+ return { models: models.map(parseCatalogModel) };
1535
+ }
1536
+ /** The Client-face contribution for the model-catalog endpoint. */
1537
+ const MODELS_REMOTE_CONTRIBUTION = {
1538
+ package: USAGE_REMOTE_PACKAGE,
1539
+ descriptors: [{
1540
+ id: `${USAGE_REMOTE_PACKAGE}#${MODELS_ENDPOINT}`,
1541
+ service: "commandcodeUsage",
1542
+ namespace: "commandcode",
1543
+ method: "models",
1544
+ invocation: { kind: "direct" },
1545
+ parameters: [],
1546
+ result: {
1547
+ mode: "strict",
1548
+ typeSymbol: `${USAGE_REMOTE_PACKAGE}#CommandCodeCatalog`,
1549
+ schema: { parse: parseCatalog }
1550
+ }
1551
+ }]
1552
+ };
1221
1553
  //#endregion
1222
1554
  //#region src/login-wire.ts
1223
1555
  /** The canonical endpoint paths of the three login Remotes. */
@@ -1299,7 +1631,7 @@ window.__ModuleLoader__.load({
1299
1631
  };
1300
1632
  //#endregion
1301
1633
  //#region package.json
1302
- var version = "0.9.1";
1634
+ var version = "0.10.0-alpha.2";
1303
1635
  var repository = {
1304
1636
  "type": "git",
1305
1637
  "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
@@ -2361,6 +2693,173 @@ window.__ModuleLoader__.load({
2361
2693
  ]
2362
2694
  });
2363
2695
  }
2696
+ /** One model → account routing rule row. */
2697
+ function RuleRow({ rule, accounts, catalog, disabled, t, onModels, onAccount, onRemove }) {
2698
+ const targets = [{
2699
+ value: "default",
2700
+ label: t("accountDefault")
2701
+ }, ...accounts.filter((account) => !account.added).map((account) => ({
2702
+ value: account.ref,
2703
+ label: account.label
2704
+ }))];
2705
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2706
+ className: "cc-field",
2707
+ children: [
2708
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2709
+ className: "cc-fieldHead",
2710
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2711
+ className: "cc-label",
2712
+ htmlFor: `cc-rule-model-${rule.id}`,
2713
+ children: t("ruleModel")
2714
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2715
+ className: "cc-badges",
2716
+ children: [rule.added ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2717
+ className: "cc-badge",
2718
+ children: t("unsaved")
2719
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2720
+ type: "button",
2721
+ className: "cc-reset",
2722
+ disabled,
2723
+ onClick: onRemove,
2724
+ children: t("ruleRemove")
2725
+ })]
2726
+ })]
2727
+ }),
2728
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMultiSelect, {
2729
+ id: `cc-rule-model-${rule.id}`,
2730
+ selected: rule.models,
2731
+ catalog,
2732
+ disabled,
2733
+ t,
2734
+ onSelect: onModels
2735
+ }),
2736
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
2737
+ id: `cc-rule-account-${rule.id}`,
2738
+ className: "cc-input",
2739
+ value: rule.account,
2740
+ disabled,
2741
+ onChange: (event) => onAccount(event.target.value),
2742
+ children: targets.map((target) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2743
+ value: target.value,
2744
+ children: target.label
2745
+ }, target.value))
2746
+ }),
2747
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2748
+ className: "cc-hint",
2749
+ children: t("ruleHint")
2750
+ })
2751
+ ]
2752
+ });
2753
+ }
2754
+ /**
2755
+ * A checkbox multi-select dropdown for routing-rule models. The trigger shows
2756
+ * the selection count; the anchored Menu lists every catalog model with a
2757
+ * checkbox, toggled by clicking the row. Selected ids the catalog no longer
2758
+ * carries still render so a saved rule never silently loses a selection.
2759
+ */
2760
+ function ModelMultiSelect({ id, selected, catalog, disabled, t, onSelect }) {
2761
+ const [open, setOpen] = (0, react.useState)(false);
2762
+ const options = [...catalog.map((model) => ({
2763
+ value: model.id,
2764
+ label: model.name
2765
+ })), ...selected.filter((id) => !catalog.some((model) => model.id === id)).map((id) => ({
2766
+ value: id,
2767
+ label: id
2768
+ }))];
2769
+ const selectedSet = new Set(selected);
2770
+ const items = options.map((option) => ({
2771
+ id: option.value,
2772
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2773
+ className: "cc-checkRow",
2774
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2775
+ type: "checkbox",
2776
+ className: "cc-check",
2777
+ checked: selectedSet.has(option.value),
2778
+ readOnly: true,
2779
+ tabIndex: -1
2780
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2781
+ className: "cc-checkName",
2782
+ children: option.label
2783
+ })]
2784
+ })
2785
+ }));
2786
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
2787
+ open,
2788
+ onClose: () => setOpen(false),
2789
+ onSelect: (modelId) => {
2790
+ onSelect(selectedSet.has(modelId) ? selected.filter((value) => value !== modelId) : [...selected, modelId]);
2791
+ },
2792
+ selectedIds: selected,
2793
+ items,
2794
+ portal: true,
2795
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2796
+ id,
2797
+ type: "button",
2798
+ className: "cc-input cc-ruleTrigger",
2799
+ disabled: disabled || catalog.length === 0,
2800
+ onClick: () => setOpen((value) => !value),
2801
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2802
+ className: "cc-ruleTriggerText",
2803
+ children: selected.length === 0 ? t("ruleModelPick") : t("ruleModelCount", { count: selected.length })
2804
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2805
+ className: "cc-ruleCaret",
2806
+ "aria-hidden": "true"
2807
+ })]
2808
+ })
2809
+ });
2810
+ }
2811
+ /** The model → account routing card: rules in list order (first match wins). */
2812
+ function RulesCard({ t, state, disabled, onAdd, onRemove, onModels, onAccount }) {
2813
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2814
+ className: "cc-card",
2815
+ "aria-label": t("rulesTitle"),
2816
+ children: [
2817
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2818
+ className: "cc-field",
2819
+ children: [
2820
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2821
+ className: "cc-fieldHead",
2822
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2823
+ className: "cc-label",
2824
+ children: t("rulesTitle")
2825
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2826
+ className: "cc-badges",
2827
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2828
+ type: "button",
2829
+ className: "cc-reset",
2830
+ disabled,
2831
+ onClick: onAdd,
2832
+ children: t("ruleAdd")
2833
+ })
2834
+ })]
2835
+ }),
2836
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2837
+ className: "cc-hint",
2838
+ children: t("rulesHint")
2839
+ }),
2840
+ state.catalogFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2841
+ className: "cc-invalid",
2842
+ children: t("rulesCatalogFailed")
2843
+ }) : null
2844
+ ]
2845
+ }),
2846
+ state.rules.map((rule) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuleRow, {
2847
+ rule,
2848
+ accounts: state.accounts,
2849
+ catalog: state.catalogModels,
2850
+ disabled,
2851
+ t,
2852
+ onModels: (ids) => onModels(rule.id, ids),
2853
+ onAccount: (text) => onAccount(rule.id, text),
2854
+ onRemove: () => onRemove(rule.id)
2855
+ }, rule.id)),
2856
+ state.rules.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2857
+ className: "cc-hint",
2858
+ children: t("rulesEmpty")
2859
+ }) : null
2860
+ ]
2861
+ });
2862
+ }
2364
2863
  /**
2365
2864
  * Show the "Saved ✓" affordance for a short window after each accepted save.
2366
2865
  * The controller only counts saves (`savedCount`); the flash timing lives
@@ -2448,6 +2947,15 @@ window.__ModuleLoader__.load({
2448
2947
  onActive: (text) => props.edit("activeAccount", text),
2449
2948
  onActiveReset: () => props.resetField("activeAccount")
2450
2949
  }),
2950
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RulesCard, {
2951
+ t,
2952
+ state,
2953
+ disabled,
2954
+ onAdd: props.addRule,
2955
+ onRemove: props.removeRule,
2956
+ onModels: props.editRuleModels,
2957
+ onAccount: props.editRuleAccount
2958
+ }),
2451
2959
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2452
2960
  className: "cc-card",
2453
2961
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretKeyField, {
@@ -2533,35 +3041,23 @@ window.__ModuleLoader__.load({
2533
3041
  ]
2534
3042
  });
2535
3043
  }
2536
- //#endregion
2537
- //#region src/client/card.tsx
2538
3044
  /**
2539
- * The Command Code provider card inside the harness Models settings page
2540
- * (browser half). Rendered through the `settings.models.provider-card` keyed
2541
- * slot introduced in dsh 0.1.2-alpha.1, registered with
2542
- * `entryKey = 'llm-commandcode'` (the plugin's settings namespace, the key the
2543
- * Models page dispatches for every Command Code provider row).
2544
- *
2545
- * The slot's owner props (`configured`, `keyConfigured`) mirror what the
2546
- * Models page already knows; the authoritative credential facts still come
2547
- * from this plugin's `CommandCodeSettingsController` shared with the dedicated
2548
- * settings page, so the two surfaces can never disagree about whether a key
2549
- * is stored.
2550
- *
2551
- * Not configured: an "Unconfigured" badge, a paste-a-key field, and the
2552
- * official sign-in button — the whole flow a first-run user needs, inline.
2553
- * Configured: a green "Configured" badge plus a pointer to the full
2554
- * Command Code settings page for rotation, usage, and connection facts.
2555
- *
2556
- * The card renders nothing until the shared controller's first snapshot is
2557
- * ready (`available`), which also keeps the stale-facts window of the older
2558
- * join from ever being visible. A controller-less render (card mounted before
2559
- * the section registered its inject face — the composition runs one apply)
2560
- * degrades to the stateless registration-notice form.
2561
- *
2562
- * Styles ride the page stylesheet the client entry injects once (`cc-`
2563
- * prefixed classes); the card adds no CSS of its own.
3045
+ * Find the official editor card among the slot outlet's siblings, or null
3046
+ * while it is closed. The Models page renders the editor as an immediate
3047
+ * sibling of the outlet wrapper — after it in a provider row (the target of
3048
+ * the row's 编辑 toggle), before it in the first-run setup card and the
3049
+ * add-provider card, where it is always open. The editor is the only such
3050
+ * sibling whose CSS module class carries the `editor` stem
3051
+ * (`<hash>_editor`); the row header and the add card's provider select
3052
+ * never do, so the lookup needs no hash knowledge.
2564
3053
  */
3054
+ function adjacentEditorCard(wrapper) {
3055
+ if (wrapper === null) return null;
3056
+ for (const sibling of [wrapper.previousElementSibling, wrapper.nextElementSibling]) if (sibling !== null && typeof sibling.className === "string" && sibling.className.includes("editor")) return sibling;
3057
+ return null;
3058
+ }
3059
+ /** The closed-panel style: the outlet stays mounted as the detection anchor. */
3060
+ const HIDDEN_STYLE = { display: "none" };
2565
3061
  /** Decide the card's posture from the injected face and the owner facts. */
2566
3062
  function cardMode(props) {
2567
3063
  if (props.useCommandCodeSettings === void 0) return { kind: "registration" };
@@ -2692,20 +3188,18 @@ window.__ModuleLoader__.load({
2692
3188
  ]
2693
3189
  });
2694
3190
  }
2695
- /** One "Configured" badge + pointer to the full settings page (footer area). */
2696
- function CardConfiguredBody({ t }) {
2697
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2698
- className: "cc-field",
2699
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2700
- className: "cc-hint",
2701
- children: t("cardConfiguredHint")
2702
- })
2703
- });
2704
- }
2705
3191
  /**
2706
3192
  * The slot component body. Dispatched on every Command Code provider card of
2707
3193
  * the Models page (saved row, first-run setup posture, and add-provider
2708
3194
  * draft).
3195
+ *
3196
+ * Closed (the official 编辑 toggle off) the panel renders nothing: the row
3197
+ * head the Models page owns already names the provider and shows the
3198
+ * credential dot, so a page full of providers stays compact. Opening the
3199
+ * official editor mounts the editor shell as the outlet's sibling; the panel
3200
+ * watches for it, hides the shell (it carries only the settings.yaml hint and
3201
+ * a disabled apply for this namespace), and shows the real controls — badges,
3202
+ * API-key field, sign-in, discard/save.
2709
3203
  */
2710
3204
  function CommandCodeProviderCard(props) {
2711
3205
  const { t } = props;
@@ -2720,14 +3214,50 @@ window.__ModuleLoader__.load({
2720
3214
  const configured = mode.kind === "live" && mode.ready ? mode.controllerConfigured : props.keyConfigured;
2721
3215
  const disabled = mode.kind === "live" && (!mode.writable || state !== void 0 && !mode.apiKeyWritable);
2722
3216
  const showBody = mode.kind === "live" && mode.ready && state !== void 0;
3217
+ const rootRef = (0, react.useRef)(null);
3218
+ const [editorOpen, setEditorOpen] = (0, react.useState)(false);
3219
+ (0, react.useEffect)(() => {
3220
+ const root = rootRef.current;
3221
+ if (root === null || typeof MutationObserver === "undefined") return;
3222
+ const wrapper = root.closest(`[data-slot="settings.models.provider-card"]`) ?? root.parentElement;
3223
+ if (wrapper === null) return;
3224
+ const row = wrapper.parentElement;
3225
+ if (row === null) return;
3226
+ let hiddenEditor = null;
3227
+ const sync = () => {
3228
+ const editor = adjacentEditorCard(wrapper);
3229
+ setEditorOpen(editor !== null);
3230
+ if (editor !== null) {
3231
+ editor.style.display = "none";
3232
+ hiddenEditor = editor;
3233
+ }
3234
+ };
3235
+ sync();
3236
+ const observer = new MutationObserver(sync);
3237
+ observer.observe(row, { childList: true });
3238
+ return () => {
3239
+ observer.disconnect();
3240
+ if (hiddenEditor !== null) hiddenEditor.style.display = "";
3241
+ };
3242
+ }, []);
2723
3243
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3244
+ ref: rootRef,
2724
3245
  className: "cc-providerCard",
2725
3246
  "data-cc-models-card": "true",
3247
+ style: editorOpen ? void 0 : HIDDEN_STYLE,
2726
3248
  children: [
2727
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2728
- className: "cc-field",
2729
- children: [
2730
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3249
+ editorOpen && mode.kind === "registration" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3250
+ className: "cc-hint",
3251
+ children: t("cardRegistrationHint")
3252
+ }) : null,
3253
+ editorOpen && mode.kind === "live" && !mode.ready ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3254
+ className: "cc-hint",
3255
+ children: t("cardLoadingHint")
3256
+ }) : null,
3257
+ editorOpen && showBody ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3258
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3259
+ className: "cc-field",
3260
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2731
3261
  className: "cc-fieldHead",
2732
3262
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2733
3263
  className: "cc-label",
@@ -2743,18 +3273,8 @@ window.__ModuleLoader__.load({
2743
3273
  children: t("cardRouteActive")
2744
3274
  }) : null]
2745
3275
  })]
2746
- }),
2747
- mode.kind === "registration" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2748
- className: "cc-hint",
2749
- children: t("cardRegistrationHint")
2750
- }) : null,
2751
- mode.kind === "live" && !mode.ready ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2752
- className: "cc-hint",
2753
- children: t("cardLoadingHint")
2754
- }) : null
2755
- ]
2756
- }),
2757
- showBody && !configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3276
+ })
3277
+ }),
2758
3278
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardKeyField, {
2759
3279
  state: state.apiKey,
2760
3280
  disabled,
@@ -2770,20 +3290,29 @@ window.__ModuleLoader__.load({
2770
3290
  }) : null,
2771
3291
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2772
3292
  className: "cc-footer",
2773
- children: [failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2774
- className: "cc-failed",
2775
- role: "status",
2776
- children: t("saveFailed")
2777
- }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2778
- type: "button",
2779
- className: "cc-reset",
2780
- disabled: savingBlocked || saving,
2781
- onClick: props.save,
2782
- children: t(saving ? "saving" : "save")
2783
- })]
3293
+ children: [
3294
+ failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3295
+ className: "cc-failed",
3296
+ role: "status",
3297
+ children: t("saveFailed")
3298
+ }) : null,
3299
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3300
+ type: "button",
3301
+ className: "cc-reset",
3302
+ disabled: !dirty || saving,
3303
+ onClick: props.discard,
3304
+ children: t("discard")
3305
+ }),
3306
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3307
+ type: "button",
3308
+ className: "cc-reset",
3309
+ disabled: savingBlocked || saving,
3310
+ onClick: props.save,
3311
+ children: t(saving ? "saving" : "save")
3312
+ })
3313
+ ]
2784
3314
  })
2785
- ] }) : null,
2786
- showBody && configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardConfiguredBody, { t }) : null
3315
+ ] }) : null
2787
3316
  ]
2788
3317
  });
2789
3318
  }
@@ -2824,6 +3353,17 @@ window.__ModuleLoader__.load({
2824
3353
  activeAccount: "当前使用账户",
2825
3354
  activeAccountAuto: "自动(第一个可用账户)",
2826
3355
  activeAccountHint: "手动指定优先使用的账户,保存后下次请求即生效;所选账户耗尽时仍会自动切换到其他可用账户。",
3356
+ rulesTitle: "按模型切换账户",
3357
+ rulesHint: "选择模型并路由到某个账户(可多选)。命中规则的模型且该账户可用时优先使用;账户耗尽或密钥失效时仍自动回落到其他账户。规则按列表顺序匹配,第一条命中生效。",
3358
+ rulesEmpty: "尚未配置规则。",
3359
+ rulesCatalogFailed: "模型目录获取失败,暂时无法选择模型;已保存的规则仍会生效。",
3360
+ ruleAdd: "添加规则",
3361
+ ruleRemove: "移除",
3362
+ ruleModel: "模型",
3363
+ ruleModelPick: "选择模型…",
3364
+ ruleModelCount: "已选 {count} 个模型",
3365
+ ruleAccount: "目标账户",
3366
+ ruleHint: "从下拉列表勾选要路由的模型(可多选),再选择目标账户。",
2827
3367
  overridden: "已覆盖",
2828
3368
  reset: "重置",
2829
3369
  invalidNumber: "无效数字",
@@ -2893,11 +3433,10 @@ window.__ModuleLoader__.load({
2893
3433
  loginStoreFailed: "密钥无法写入本机凭据服务,请手动粘贴。",
2894
3434
  loginCancelled: "登录已取消。",
2895
3435
  loginFailedGeneric: "登录失败,请重试或手动粘贴密钥。",
2896
- cardTitle: "Command Code 连接",
3436
+ cardTitle: "Command Code",
2897
3437
  cardRouteActive: "已启用",
2898
3438
  cardLoadingHint: "正在读取 Command Code 配置…",
2899
- cardRegistrationHint: "此卡片随 Command Code 插件注册,需要较新版本的 DeepSeek Harness 才会显示完整内容。",
2900
- cardConfiguredHint: "API 密钥已就绪。如需更换密钥、添加多账户轮换、查看用量或修改 API 地址,请前往设置中的「Command Code」页。"
3439
+ cardRegistrationHint: "此卡片随 Command Code 插件注册,需要较新版本的 DeepSeek Harness 才会显示完整内容。"
2901
3440
  };
2902
3441
  const en = {
2903
3442
  nav: "Command Code",
@@ -2934,6 +3473,17 @@ window.__ModuleLoader__.load({
2934
3473
  activeAccount: "Active account",
2935
3474
  activeAccountAuto: "Auto (first usable account)",
2936
3475
  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.",
3476
+ rulesTitle: "Route models to accounts",
3477
+ rulesHint: "Pick models (multi-select) and route them to an account. When the request’s model is in a rule and that account is usable, it serves; an exhausted or invalid routed account falls back to the normal rotation. Rules match in list order — the first hit wins.",
3478
+ rulesEmpty: "No rules yet.",
3479
+ rulesCatalogFailed: "Could not load the model catalog — selecting models is unavailable; saved rules still apply.",
3480
+ ruleAdd: "Add rule",
3481
+ ruleRemove: "Remove",
3482
+ ruleModel: "Models",
3483
+ ruleModelPick: "Select models…",
3484
+ ruleModelCount: "{count} model(s) selected",
3485
+ ruleAccount: "Target account",
3486
+ ruleHint: "Check the models to route from the dropdown (multi-select), then pick the target account.",
2937
3487
  overridden: "Overridden",
2938
3488
  reset: "Reset",
2939
3489
  invalidNumber: "Invalid number",
@@ -3003,11 +3553,10 @@ window.__ModuleLoader__.load({
3003
3553
  loginStoreFailed: "The key could not be stored in the local credential service; paste it manually.",
3004
3554
  loginCancelled: "Sign-in cancelled.",
3005
3555
  loginFailedGeneric: "Sign-in failed; try again or paste the key manually.",
3006
- cardTitle: "Command Code connection",
3556
+ cardTitle: "Command Code",
3007
3557
  cardRouteActive: "Active",
3008
3558
  cardLoadingHint: "Loading the Command Code configuration…",
3009
- cardRegistrationHint: "This card is contributed by the Command Code plugin; a newer DeepSeek Harness is needed to show the full controls.",
3010
- cardConfiguredHint: "The API key is ready. To replace it, add account rotation, review usage, or change the API base, open the \"Command Code\" page in Settings."
3559
+ cardRegistrationHint: "This card is contributed by the Command Code plugin; a newer DeepSeek Harness is needed to show the full controls."
3011
3560
  };
3012
3561
  //#endregion
3013
3562
  //#region src/client/index.ts
@@ -3031,6 +3580,19 @@ window.__ModuleLoader__.load({
3031
3580
  .cc-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}
3032
3581
  .cc-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
3033
3582
  .cc-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
3583
+ /* The routing-rule model multi-select: a button trigger that opens an
3584
+ * anchored Menu of checkbox rows. The trigger mirrors .cc-input sizing so it
3585
+ * sits flush with the sibling account select. */
3586
+ .cc-ruleTrigger{align-items:center;gap:8px;display:flex;width:100%;text-align:left;cursor:pointer}
3587
+ .cc-ruleTrigger:disabled{cursor:default}
3588
+ .cc-ruleTriggerText{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3589
+ .cc-ruleCaret{flex-shrink:0;border-right:1.5px solid var(--dsw-alias-label-tertiary);border-bottom:1.5px solid var(--dsw-alias-label-tertiary);width:6px;height:6px;margin-right:4px;margin-bottom:2px;transform:rotate(45deg)}
3590
+ .cc-checkRow{align-items:center;gap:8px;display:inline-flex;min-width:0}
3591
+ .cc-checkRow:hover{cursor:pointer}
3592
+ .cc-check{appearance:none;flex-shrink:0;width:15px;height:15px;margin:0;border:1px solid var(--dsw-alias-border-l2);border-radius:4px;background:var(--dsw-alias-bg-layer-1);position:relative}
3593
+ .cc-check:checked{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary)}
3594
+ .cc-check:checked::after{content:'';position:absolute;top:2px;left:5px;width:3px;height:7px;border:solid #fff;border-width:0 1.5px 1.5px 0;transform:rotate(45deg)}
3595
+ .cc-checkName{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3034
3596
  /* Selects need their own treatment to sit flush with the text inputs:
3035
3597
  * the UA stylesheet renders <select> border-box (34px total vs the inputs'
3036
3598
  * 36px) and forces its own menulist text metrics, so drop the native
@@ -3128,30 +3690,55 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3128
3690
  document.head.appendChild(tag);
3129
3691
  }
3130
3692
  /**
3131
- * Client plugin body. Gates on the services the settings page needs
3132
- * (`slots`, `locale`, `connection`, `remote`, `settingsScope`) plus the
3133
- * `connection` used by the friendly-error wrapper the same inject list the
3134
- * harness's own settings-surface plugins declare.
3693
+ * Client plugin body. Gates on the services shared by both client generations
3694
+ * (`slots`, `locale`, `connection`, `remote`, `settingsScope`). Current clients
3695
+ * mount the page after `remote.credentials` appears; legacy clients mount it
3696
+ * from the connection ApiProxy credential face.
3135
3697
  */
3136
3698
  function apply(ctx) {
3137
3699
  injectPageCss();
3138
3700
  const connection = ctx.get("connection");
3139
- if (connection !== void 0) installFriendlyImageError(connection, () => ctx.locale.getLocale().active);
3701
+ if (connection !== void 0) installFriendlyImageError(connection, () => ctx.locale.getLocale().active === "zh" ? "zh" : "en");
3140
3702
  ctx.effect(() => ctx.locale.register("settings.commandcode", {
3141
3703
  zh,
3142
3704
  en
3143
3705
  }), "dsh-commandcode-provider: page copy");
3144
- const api = ctx.get("connection").api;
3145
- const hostDescription = ctx.get("connection").hostDescription;
3146
- const controller = new CommandCodeSettingsController(ctx.settingsScope.bind({ namespace: COMMANDCODE_NS }), { credentials: api.credentials }, hostDescription);
3706
+ const legacyApi = adaptLegacyCredentials(connection?.api?.credentials);
3707
+ if (legacyApi !== void 0) {
3708
+ applyClientSurfaces(ctx, legacyApi, connection?.hostDescription);
3709
+ return;
3710
+ }
3711
+ ctx.inject(["remote.credentials"], (remoteCtx) => {
3712
+ const credentials = remoteCtx.remote.credentials;
3713
+ applyClientSurfaces(remoteCtx, { credentials });
3714
+ });
3715
+ }
3716
+ /** Mount the one shared UI implementation over either credential transport. */
3717
+ function applyClientSurfaces(ctx, api, hostDescription) {
3718
+ const scope = ctx.settingsScope.bind({ namespace: COMMANDCODE_NS });
3719
+ let modelsRemote;
3720
+ const controller = new CommandCodeSettingsController(scope, {
3721
+ ...api,
3722
+ models: () => modelsRemote?.() ?? Promise.resolve({
3723
+ ok: false,
3724
+ error: { message: "commandcode/models remote is not mounted" }
3725
+ })
3726
+ }, hostDescription);
3147
3727
  ctx.effect(() => () => controller.dispose(), "dsh-commandcode-provider: settings controller");
3148
- const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(controller.state());
3728
+ const store = createSnapshotStore(controller.state());
3149
3729
  controller.subscribe(() => store.set(controller.state()));
3730
+ ctx.effect(() => ctx.remote.$on("credentials/reference-updated", () => {
3731
+ controller.refreshCredentials();
3732
+ }), "dsh-commandcode-provider: credential invalidations");
3150
3733
  let usageNamespace;
3151
3734
  let usageMountError;
3152
3735
  const contribution = {
3153
3736
  package: USAGE_REMOTE_CONTRIBUTION.package,
3154
- descriptors: [...USAGE_REMOTE_CONTRIBUTION.descriptors, ...LOGIN_REMOTE_CONTRIBUTION.descriptors]
3737
+ descriptors: [
3738
+ ...USAGE_REMOTE_CONTRIBUTION.descriptors,
3739
+ ...MODELS_REMOTE_CONTRIBUTION.descriptors,
3740
+ ...LOGIN_REMOTE_CONTRIBUTION.descriptors
3741
+ ]
3155
3742
  };
3156
3743
  ctx.effect(() => {
3157
3744
  let cancelled = false;
@@ -3164,6 +3751,7 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3164
3751
  unmount = dispose;
3165
3752
  ctx.inject(["remote.commandcode"], (namespaceCtx) => {
3166
3753
  usageNamespace = namespaceCtx.remote.commandcode;
3754
+ controller.refreshCatalog();
3167
3755
  namespaceCtx.effect(() => () => {
3168
3756
  usageNamespace = void 0;
3169
3757
  }, "dsh-commandcode-provider: usage namespace");
@@ -3177,16 +3765,28 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3177
3765
  if (unmount !== void 0) unmount();
3178
3766
  };
3179
3767
  }, "dsh-commandcode-provider: usage remote");
3180
- const usageController = new CommandCodeUsageController({ report: async () => {
3181
- const namespace = usageNamespace;
3182
- if (namespace === void 0) return {
3183
- ok: false,
3184
- error: { message: usageMountError ?? "commandcode/report remote is not mounted" }
3185
- };
3186
- return namespace.report();
3187
- } });
3768
+ const usageRemote = {
3769
+ report: async () => {
3770
+ const namespace = usageNamespace;
3771
+ if (namespace === void 0) return {
3772
+ ok: false,
3773
+ error: { message: usageMountError ?? "commandcode/report remote is not mounted" }
3774
+ };
3775
+ return namespace.report();
3776
+ },
3777
+ models: async () => {
3778
+ const namespace = usageNamespace;
3779
+ if (namespace === void 0) return {
3780
+ ok: false,
3781
+ error: { message: usageMountError ?? "commandcode/models remote is not mounted" }
3782
+ };
3783
+ return namespace.models();
3784
+ }
3785
+ };
3786
+ modelsRemote = () => usageRemote.models();
3787
+ const usageController = new CommandCodeUsageController(usageRemote);
3188
3788
  ctx.effect(() => () => usageController.dispose(), "dsh-commandcode-provider: usage controller");
3189
- const usageStore = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(usageController.state());
3789
+ const usageStore = createSnapshotStore(usageController.state());
3190
3790
  usageController.subscribe(() => usageStore.set(usageController.state()));
3191
3791
  const loginRemote = {
3192
3792
  loginBegin: async () => {
@@ -3213,7 +3813,7 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3213
3813
  };
3214
3814
  const loginController = new CommandCodeLoginController(() => loginRemote);
3215
3815
  ctx.effect(() => () => loginController.dispose(), "dsh-commandcode-provider: login controller");
3216
- const loginStore = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(loginController.state());
3816
+ const loginStore = createSnapshotStore(loginController.state());
3217
3817
  let lastLoginPhase = loginController.state().phase;
3218
3818
  loginController.subscribe(() => {
3219
3819
  const phase = loginController.state().phase;
@@ -3244,7 +3844,11 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3244
3844
  removeAccount: (id) => controller.removeAccount(id),
3245
3845
  editAccountLabel: (id, text) => controller.editAccountLabel(id, text),
3246
3846
  editAccountKey: (id, text) => controller.editAccountKey(id, text),
3247
- toggleKeyClear: (id) => controller.toggleKeyClear(id)
3847
+ toggleKeyClear: (id) => controller.toggleKeyClear(id),
3848
+ addRule: () => controller.addRule(),
3849
+ removeRule: (id) => controller.removeRule(id),
3850
+ editRuleModels: (id, ids) => controller.editRuleModels(id, ids),
3851
+ editRuleAccount: (id, text) => controller.editRuleAccount(id, text)
3248
3852
  });
3249
3853
  ctx.slots.inject("settings.section", () => ctx.slots.register({
3250
3854
  name: "settings.section",
@@ -3268,6 +3872,7 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3268
3872
  const settled = controller.state();
3269
3873
  if (!settled.failed && settled.anyAccountConfigured) usageController.refresh();
3270
3874
  }),
3875
+ discard: () => controller.discard(),
3271
3876
  beginLogin: () => void loginController.begin(),
3272
3877
  cancelLogin: () => void loginController.cancel()
3273
3878
  })