@mars-sea/dsh-commandcode-provider 0.10.0-alpha.1 → 0.10.0-alpha.3

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
@@ -304,6 +304,12 @@ window.__ModuleLoader__.load({
304
304
  booleanField$1("filterModelsByPlan"),
305
305
  textField("activeAccount")
306
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
+ }
307
313
  /**
308
314
  * Controller bridging the `llm-commandcode` scope and the credentials domain
309
315
  * onto the page. Public API mirrors the harness's CardForm actions, so the
@@ -332,6 +338,15 @@ window.__ModuleLoader__.load({
332
338
  keyDrafts = /* @__PURE__ */ new Map();
333
339
  /** Credential references staged for removal on the next save. */
334
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;
335
350
  saving = false;
336
351
  failed = false;
337
352
  savedCount = 0;
@@ -362,6 +377,7 @@ window.__ModuleLoader__.load({
362
377
  }
363
378
  this.recomputeCredentialRef();
364
379
  this.describeAll();
380
+ this.refreshCatalog();
365
381
  }
366
382
  /** Release every subscription held on external sources. Idempotent. */
367
383
  dispose() {
@@ -418,7 +434,10 @@ window.__ModuleLoader__.load({
418
434
  activeAccount: this.field("activeAccount"),
419
435
  accounts,
420
436
  accountsRemoving: [...this.removedRefs],
421
- 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(),
422
441
  invalid: plan.some((item) => item.run === void 0),
423
442
  saving: this.saving,
424
443
  failed: this.failed,
@@ -491,6 +510,64 @@ window.__ModuleLoader__.load({
491
510
  this.describeAll();
492
511
  this.publish();
493
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
+ }
494
571
  /** Stage one field's draft text. */
495
572
  edit(field, text) {
496
573
  this.staged.set(field, {
@@ -519,9 +596,10 @@ window.__ModuleLoader__.load({
519
596
  }
520
597
  /** Discard every staged edit. */
521
598
  discard() {
522
- if (this.staged.size === 0 && !this.accountsStaged() && !this.failed) return;
599
+ if (this.staged.size === 0 && !this.accountsStaged() && !this.rulesStaged() && !this.failed) return;
523
600
  this.staged.clear();
524
601
  this.clearAccountStaging();
602
+ this.clearRuleStaging();
525
603
  this.failed = false;
526
604
  this.publish();
527
605
  }
@@ -537,7 +615,8 @@ window.__ModuleLoader__.load({
537
615
  async save() {
538
616
  const plan = this.plan();
539
617
  const accountRuns = this.accountPlan();
540
- 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;
541
620
  const runs = [];
542
621
  for (const item of plan) {
543
622
  if (item.run === void 0) return;
@@ -547,7 +626,11 @@ window.__ModuleLoader__.load({
547
626
  this.failed = false;
548
627
  this.publish();
549
628
  let landed = true;
550
- for (const run of [...runs, ...accountRuns]) if (!await run()) {
629
+ for (const run of [
630
+ ...runs,
631
+ ...accountRuns,
632
+ ...ruleRuns
633
+ ]) if (!await run()) {
551
634
  landed = false;
552
635
  break;
553
636
  }
@@ -557,7 +640,11 @@ window.__ModuleLoader__.load({
557
640
  this.savedCount += 1;
558
641
  this.staged.clear();
559
642
  this.clearAccountStaging();
560
- } else this.reconcileAccountStaging();
643
+ this.clearRuleStaging();
644
+ } else {
645
+ this.reconcileAccountStaging();
646
+ this.reconcileRuleStaging();
647
+ }
561
648
  this.publish();
562
649
  }
563
650
  /**
@@ -708,6 +795,28 @@ window.__ModuleLoader__.load({
708
795
  }
709
796
  if (changed) this.publish();
710
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
+ }
711
820
  /** The stored extra accounts from the settings section (`accounts`). */
712
821
  storedExtras() {
713
822
  const raw = this.scope.getSnapshot().value?.accounts;
@@ -807,6 +916,93 @@ window.__ModuleLoader__.load({
807
916
  const after = this.storedExtras();
808
917
  return after.length === list.length && list.every((item, index) => after[index]?.ref === item.apiKeyEnv);
809
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
+ }
810
1006
  publish() {
811
1007
  if (this.disposed) return;
812
1008
  for (const listener of this.listeners) listener();
@@ -1321,6 +1517,39 @@ window.__ModuleLoader__.load({
1321
1517
  }
1322
1518
  }]
1323
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
+ };
1324
1553
  //#endregion
1325
1554
  //#region src/login-wire.ts
1326
1555
  /** The canonical endpoint paths of the three login Remotes. */
@@ -1402,7 +1631,7 @@ window.__ModuleLoader__.load({
1402
1631
  };
1403
1632
  //#endregion
1404
1633
  //#region package.json
1405
- var version = "0.10.0-alpha.1";
1634
+ var version = "0.10.0-alpha.3";
1406
1635
  var repository = {
1407
1636
  "type": "git",
1408
1637
  "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
@@ -2464,6 +2693,173 @@ window.__ModuleLoader__.load({
2464
2693
  ]
2465
2694
  });
2466
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
+ }
2467
2863
  /**
2468
2864
  * Show the "Saved ✓" affordance for a short window after each accepted save.
2469
2865
  * The controller only counts saves (`savedCount`); the flash timing lives
@@ -2551,6 +2947,15 @@ window.__ModuleLoader__.load({
2551
2947
  onActive: (text) => props.edit("activeAccount", text),
2552
2948
  onActiveReset: () => props.resetField("activeAccount")
2553
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
+ }),
2554
2959
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2555
2960
  className: "cc-card",
2556
2961
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretKeyField, {
@@ -2636,35 +3041,23 @@ window.__ModuleLoader__.load({
2636
3041
  ]
2637
3042
  });
2638
3043
  }
2639
- //#endregion
2640
- //#region src/client/card.tsx
2641
3044
  /**
2642
- * The Command Code provider card inside the harness Models settings page
2643
- * (browser half). Rendered through the `settings.models.provider-card` keyed
2644
- * slot available in dsh 0.1.2-alpha.2, registered with
2645
- * `entryKey = 'llm-commandcode'` (the plugin's settings namespace, the key the
2646
- * Models page dispatches for every Command Code provider row).
2647
- *
2648
- * The slot's owner props (`configured`, `keyConfigured`) mirror what the
2649
- * Models page already knows; the authoritative credential facts still come
2650
- * from this plugin's `CommandCodeSettingsController` shared with the dedicated
2651
- * settings page, so the two surfaces can never disagree about whether a key
2652
- * is stored.
2653
- *
2654
- * Not configured: an "Unconfigured" badge, a paste-a-key field, and the
2655
- * official sign-in button — the whole flow a first-run user needs, inline.
2656
- * Configured: a green "Configured" badge plus a pointer to the full
2657
- * Command Code settings page for rotation, usage, and connection facts.
2658
- *
2659
- * The card renders nothing until the shared controller's first snapshot is
2660
- * ready (`available`), which also keeps the stale-facts window of the older
2661
- * join from ever being visible. A controller-less render (card mounted before
2662
- * the section registered its inject face — the composition runs one apply)
2663
- * degrades to the stateless registration-notice form.
2664
- *
2665
- * Styles ride the page stylesheet the client entry injects once (`cc-`
2666
- * 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.
2667
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" };
2668
3061
  /** Decide the card's posture from the injected face and the owner facts. */
2669
3062
  function cardMode(props) {
2670
3063
  if (props.useCommandCodeSettings === void 0) return { kind: "registration" };
@@ -2795,20 +3188,18 @@ window.__ModuleLoader__.load({
2795
3188
  ]
2796
3189
  });
2797
3190
  }
2798
- /** One "Configured" badge + pointer to the full settings page (footer area). */
2799
- function CardConfiguredBody({ t }) {
2800
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2801
- className: "cc-field",
2802
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2803
- className: "cc-hint",
2804
- children: t("cardConfiguredHint")
2805
- })
2806
- });
2807
- }
2808
3191
  /**
2809
3192
  * The slot component body. Dispatched on every Command Code provider card of
2810
3193
  * the Models page (saved row, first-run setup posture, and add-provider
2811
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.
2812
3203
  */
2813
3204
  function CommandCodeProviderCard(props) {
2814
3205
  const { t } = props;
@@ -2823,14 +3214,50 @@ window.__ModuleLoader__.load({
2823
3214
  const configured = mode.kind === "live" && mode.ready ? mode.controllerConfigured : props.keyConfigured;
2824
3215
  const disabled = mode.kind === "live" && (!mode.writable || state !== void 0 && !mode.apiKeyWritable);
2825
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
+ }, []);
2826
3243
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3244
+ ref: rootRef,
2827
3245
  className: "cc-providerCard",
2828
3246
  "data-cc-models-card": "true",
3247
+ style: editorOpen ? void 0 : HIDDEN_STYLE,
2829
3248
  children: [
2830
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2831
- className: "cc-field",
2832
- children: [
2833
- /* @__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", {
2834
3261
  className: "cc-fieldHead",
2835
3262
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2836
3263
  className: "cc-label",
@@ -2846,18 +3273,8 @@ window.__ModuleLoader__.load({
2846
3273
  children: t("cardRouteActive")
2847
3274
  }) : null]
2848
3275
  })]
2849
- }),
2850
- mode.kind === "registration" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2851
- className: "cc-hint",
2852
- children: t("cardRegistrationHint")
2853
- }) : null,
2854
- mode.kind === "live" && !mode.ready ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2855
- className: "cc-hint",
2856
- children: t("cardLoadingHint")
2857
- }) : null
2858
- ]
2859
- }),
2860
- showBody && !configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3276
+ })
3277
+ }),
2861
3278
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardKeyField, {
2862
3279
  state: state.apiKey,
2863
3280
  disabled,
@@ -2873,20 +3290,29 @@ window.__ModuleLoader__.load({
2873
3290
  }) : null,
2874
3291
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2875
3292
  className: "cc-footer",
2876
- children: [failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2877
- className: "cc-failed",
2878
- role: "status",
2879
- children: t("saveFailed")
2880
- }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2881
- type: "button",
2882
- className: "cc-reset",
2883
- disabled: savingBlocked || saving,
2884
- onClick: props.save,
2885
- children: t(saving ? "saving" : "save")
2886
- })]
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
+ ]
2887
3314
  })
2888
- ] }) : null,
2889
- showBody && configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardConfiguredBody, { t }) : null
3315
+ ] }) : null
2890
3316
  ]
2891
3317
  });
2892
3318
  }
@@ -2927,6 +3353,17 @@ window.__ModuleLoader__.load({
2927
3353
  activeAccount: "当前使用账户",
2928
3354
  activeAccountAuto: "自动(第一个可用账户)",
2929
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: "从下拉列表勾选要路由的模型(可多选),再选择目标账户。",
2930
3367
  overridden: "已覆盖",
2931
3368
  reset: "重置",
2932
3369
  invalidNumber: "无效数字",
@@ -2996,11 +3433,10 @@ window.__ModuleLoader__.load({
2996
3433
  loginStoreFailed: "密钥无法写入本机凭据服务,请手动粘贴。",
2997
3434
  loginCancelled: "登录已取消。",
2998
3435
  loginFailedGeneric: "登录失败,请重试或手动粘贴密钥。",
2999
- cardTitle: "Command Code 连接",
3436
+ cardTitle: "Command Code",
3000
3437
  cardRouteActive: "已启用",
3001
3438
  cardLoadingHint: "正在读取 Command Code 配置…",
3002
- cardRegistrationHint: "此卡片随 Command Code 插件注册,需要较新版本的 DeepSeek Harness 才会显示完整内容。",
3003
- cardConfiguredHint: "API 密钥已就绪。如需更换密钥、添加多账户轮换、查看用量或修改 API 地址,请前往设置中的「Command Code」页。"
3439
+ cardRegistrationHint: "此卡片随 Command Code 插件注册,需要较新版本的 DeepSeek Harness 才会显示完整内容。"
3004
3440
  };
3005
3441
  const en = {
3006
3442
  nav: "Command Code",
@@ -3037,6 +3473,17 @@ window.__ModuleLoader__.load({
3037
3473
  activeAccount: "Active account",
3038
3474
  activeAccountAuto: "Auto (first usable account)",
3039
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.",
3040
3487
  overridden: "Overridden",
3041
3488
  reset: "Reset",
3042
3489
  invalidNumber: "Invalid number",
@@ -3106,11 +3553,10 @@ window.__ModuleLoader__.load({
3106
3553
  loginStoreFailed: "The key could not be stored in the local credential service; paste it manually.",
3107
3554
  loginCancelled: "Sign-in cancelled.",
3108
3555
  loginFailedGeneric: "Sign-in failed; try again or paste the key manually.",
3109
- cardTitle: "Command Code connection",
3556
+ cardTitle: "Command Code",
3110
3557
  cardRouteActive: "Active",
3111
3558
  cardLoadingHint: "Loading the Command Code configuration…",
3112
- cardRegistrationHint: "This card is contributed by the Command Code plugin; a newer DeepSeek Harness is needed to show the full controls.",
3113
- 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."
3114
3560
  };
3115
3561
  //#endregion
3116
3562
  //#region src/client/index.ts
@@ -3134,6 +3580,19 @@ window.__ModuleLoader__.load({
3134
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}
3135
3581
  .cc-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
3136
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}
3137
3596
  /* Selects need their own treatment to sit flush with the text inputs:
3138
3597
  * the UA stylesheet renders <select> border-box (34px total vs the inputs'
3139
3598
  * 36px) and forces its own menulist text metrics, so drop the native
@@ -3256,7 +3715,15 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3256
3715
  }
3257
3716
  /** Mount the one shared UI implementation over either credential transport. */
3258
3717
  function applyClientSurfaces(ctx, api, hostDescription) {
3259
- const controller = new CommandCodeSettingsController(ctx.settingsScope.bind({ namespace: COMMANDCODE_NS }), 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);
3260
3727
  ctx.effect(() => () => controller.dispose(), "dsh-commandcode-provider: settings controller");
3261
3728
  const store = createSnapshotStore(controller.state());
3262
3729
  controller.subscribe(() => store.set(controller.state()));
@@ -3267,7 +3734,11 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3267
3734
  let usageMountError;
3268
3735
  const contribution = {
3269
3736
  package: USAGE_REMOTE_CONTRIBUTION.package,
3270
- 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
+ ]
3271
3742
  };
3272
3743
  ctx.effect(() => {
3273
3744
  let cancelled = false;
@@ -3280,6 +3751,7 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3280
3751
  unmount = dispose;
3281
3752
  ctx.inject(["remote.commandcode"], (namespaceCtx) => {
3282
3753
  usageNamespace = namespaceCtx.remote.commandcode;
3754
+ controller.refreshCatalog();
3283
3755
  namespaceCtx.effect(() => () => {
3284
3756
  usageNamespace = void 0;
3285
3757
  }, "dsh-commandcode-provider: usage namespace");
@@ -3293,14 +3765,26 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3293
3765
  if (unmount !== void 0) unmount();
3294
3766
  };
3295
3767
  }, "dsh-commandcode-provider: usage remote");
3296
- const usageController = new CommandCodeUsageController({ report: async () => {
3297
- const namespace = usageNamespace;
3298
- if (namespace === void 0) return {
3299
- ok: false,
3300
- error: { message: usageMountError ?? "commandcode/report remote is not mounted" }
3301
- };
3302
- return namespace.report();
3303
- } });
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);
3304
3788
  ctx.effect(() => () => usageController.dispose(), "dsh-commandcode-provider: usage controller");
3305
3789
  const usageStore = createSnapshotStore(usageController.state());
3306
3790
  usageController.subscribe(() => usageStore.set(usageController.state()));
@@ -3360,7 +3844,11 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3360
3844
  removeAccount: (id) => controller.removeAccount(id),
3361
3845
  editAccountLabel: (id, text) => controller.editAccountLabel(id, text),
3362
3846
  editAccountKey: (id, text) => controller.editAccountKey(id, text),
3363
- 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)
3364
3852
  });
3365
3853
  ctx.slots.inject("settings.section", () => ctx.slots.register({
3366
3854
  name: "settings.section",
@@ -3384,6 +3872,7 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3384
3872
  const settled = controller.state();
3385
3873
  if (!settled.failed && settled.anyAccountConfigured) usageController.refresh();
3386
3874
  }),
3875
+ discard: () => controller.discard(),
3387
3876
  beginLogin: () => void loginController.begin(),
3388
3877
  cancelLogin: () => void loginController.cancel()
3389
3878
  })