@sqdr2/dsh-fusion 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -15,6 +15,35 @@ own tags and releases.
15
15
  Entry format: what changed, then what was actually validated. "Not claimed" lines mark what a release did *not* test —
16
16
  read them before assuming a platform is covered.
17
17
 
18
+ ## 0.3.4 — 2026-11-07
19
+
20
+ ### Changes
21
+
22
+ - A combo that names a model the catalog no longer offers now says **which** route and **which** combo went stale.
23
+ Reading a saved combo deliberately tolerates a retired route so the row stays visible and editable, but saving the
24
+ list rejects it — and the refusal read only `lead 模型已不可用,请刷新后重选`, naming neither the route nor which combo
25
+ of several. After a provider is renamed the model itself usually still exists under the new provider id, so the
26
+ message left nothing to act on; and when the user had just edited the other role, it read like a complaint about the
27
+ role they had just changed. The Host now names the route (`lead 模型已不可用:aihub-openai/gpt-6.1-sol。请为该角色重新
28
+ 选择模型`), `validatePairs` passes the combo name through so a multi-row save says which row failed, and the page
29
+ checks the same condition before sending: the save names the combo, the role and the route, the combo list marks the
30
+ affected row, and the role's picker keeps the dropped model listed and disabled (`已不可用:provider/model`) instead
31
+ of silently falling back to the `选择已连接的模型` placeholder.
32
+
33
+ ### Validation
34
+
35
+ - `pnpm test` (54 files, 401 tests: 399 passed, 1 skipped, 1 failed) and `tsc -p tsconfig.json --noEmit`. The one
36
+ failing case is `tests/check-programs.spec.ts`, whose probe returns `undefined` on this Windows host before and after
37
+ the change, so it is not counted as a regression. The native Host suite was not re-run: it needs a harness checkout
38
+ (`DSHX_HARNESS`), which this machine does not have.
39
+ - New `tests/settings-stale-route.spec.ts` covers the route naming, the combo naming through `validatePairs`, and
40
+ `missingRoutes` ordering; a new `tests/fusion-combos.spec.ts` case covers the row marker and the save refusal. Both
41
+ were confirmed to fail against the previous source.
42
+ - Rebuilt `lib/` from the merged source and loaded both artifacts in Node: the Host module still exports
43
+ `apply`/`Config`/`name`, and the client bundle registers under `@sqdr2/dsh-fusion` and still exports
44
+ `apply`/`inject`/`name`/`FUSION_PANEL_ID`/`FusionPanelIcon`. The client bundle inlines only `missingRoutes`; it pulls
45
+ in no `node:` module.
46
+
18
47
  ## 0.3.3 — 2026-11-07
19
48
 
20
49
  ### Changes
package/lib/client.js CHANGED
@@ -685,6 +685,29 @@ window.__ModuleLoader__.load({
685
685
  });
686
686
  }
687
687
  //#endregion
688
+ //#region src/host/settings.ts
689
+ /**
690
+ * The routes a saved choice still names that the current catalog no longer
691
+ * offers: a provider that was renamed or removed, or a model that was retired.
692
+ *
693
+ * Reading a saved combo tolerates a stale route so the row stays visible and
694
+ * editable, but saving one does not. Without this, the page can only say that
695
+ * *some* model is gone, which is unusable when the stale route is not the one
696
+ * being edited — the user then cannot tell which combo to fix.
697
+ */
698
+ function missingRoutes(value, catalog) {
699
+ if (!value) return [];
700
+ const stale = (role, route) => route !== void 0 && !catalog.groups.find((group) => group.id === route.provider)?.models.find((model) => model.id === route.model) ? [{
701
+ role,
702
+ route
703
+ }] : [];
704
+ return [
705
+ ...stale("lead", value.lead),
706
+ ...stale("worker", value.worker),
707
+ ...stale("compactor", value.compactor?.route)
708
+ ];
709
+ }
710
+ //#endregion
688
711
  //#region src/client/settings-events.ts
689
712
  const listeners = /* @__PURE__ */ new Set();
690
713
  const onSettingsChanged = (listener) => {
@@ -735,6 +758,7 @@ window.__ModuleLoader__.load({
735
758
  .fusion-settings .fusion-combo-text{display:flex;flex-direction:column;gap:2px;min-width:0}
736
759
  .fusion-settings .fusion-combo-name{font-weight:600;overflow-wrap:anywhere}
737
760
  .fusion-settings .fusion-combo-routes{color:var(--dsw-alias-label-secondary);font-size:12px;overflow-wrap:anywhere}
761
+ .fusion-settings .fusion-combo-stale{color:var(--dsw-alias-state-error-primary);font-size:12px;overflow-wrap:anywhere}
738
762
  .fusion-settings .fusion-combo-actions{flex:none;align-self:stretch;display:flex;align-items:center;padding:0 14px;border:0;border-left:1px solid var(--dsw-alias-border-l1);border-radius:0;background:none;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer;transition:color .15s ease,background-color .15s ease}
739
763
  .fusion-settings .fusion-combo-actions:hover:not(:disabled){color:var(--dsw-alias-state-error-primary);background-color:var(--dsw-alias-interactive-bg-hover-danger)}
740
764
  .fusion-settings .fusion-combo-actions:disabled{opacity:.4;cursor:default}
@@ -801,6 +825,7 @@ window.__ModuleLoader__.load({
801
825
  const lang = useLang();
802
826
  const name = role === "compactor" ? tr(lang, "压缩", "Compaction") : role;
803
827
  const model = catalog.groups.find((group) => group.id === route?.provider)?.models.find((model) => model.id === route?.model);
828
+ const stale = route !== void 0 && model === void 0;
804
829
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", { children: [
805
830
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", { children: name }),
806
831
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: role === "Lead" ? tr(lang, "回答问题、制定方案并审查结果。", "Answers, plans and reviews.") : role === "Sidekick" ? tr(lang, "执行命令和修改,接收反馈后继续。", "Runs commands and makes changes, then continues with feedback.") : tr(lang, "整理较早的上下文,供 Lead 或 Worker 继续工作。", "Summarises older context so the Lead or Sidekick can continue.") }),
@@ -818,21 +843,33 @@ window.__ModuleLoader__.load({
818
843
  model
819
844
  });
820
845
  },
821
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
822
- value: "",
823
- disabled: role !== "compactor",
824
- children: role === "compactor" ? tr(lang, "使用当前角色模型", "Use the role's own model") : tr(lang, "选择已连接的模型", "Choose a connected model")
825
- }), catalog.groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("optgroup", {
826
- label: group.name,
827
- children: group.models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
828
- value: key({
829
- provider: group.id,
830
- model: model.id
831
- }),
832
- children: model.name
833
- }, model.id))
834
- }, group.id))]
846
+ children: [
847
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
848
+ value: "",
849
+ disabled: role !== "compactor",
850
+ children: role === "compactor" ? tr(lang, "使用当前角色模型", "Use the role's own model") : tr(lang, "选择已连接的模型", "Choose a connected model")
851
+ }),
852
+ stale && route && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
853
+ value: key(route),
854
+ disabled: true,
855
+ children: tr(lang, `已不可用:${route.provider}/${route.model}`, `No longer available: ${route.provider}/${route.model}`)
856
+ }),
857
+ catalog.groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("optgroup", {
858
+ label: group.name,
859
+ children: group.models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
860
+ value: key({
861
+ provider: group.id,
862
+ model: model.id
863
+ }),
864
+ children: model.name
865
+ }, model.id))
866
+ }, group.id))
867
+ ]
835
868
  })] }),
869
+ stale && route && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
870
+ role: "alert",
871
+ children: tr(lang, `这个角色保存的模型 ${route.provider}/${route.model} 已不可用,请重新选择。`, `This role's saved model ${route.provider}/${route.model} is no longer available; choose another.`)
872
+ }),
836
873
  model?.reasoning && route && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [tr(lang, "推理强度", "Reasoning"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
837
874
  "aria-label": tr(lang, `${name} 推理强度`, `${name} reasoning`),
838
875
  value: route.reasoningEffort ?? "",
@@ -870,6 +907,7 @@ window.__ModuleLoader__.load({
870
907
  const [saving, setSaving] = (0, react.useState)(false);
871
908
  const current = draft[Math.min(active, draft.length - 1)];
872
909
  const routeName = (route) => route ? state?.catalog.groups.find((group) => group.id === route.provider)?.models.find((model) => model.id === route.model)?.name ?? `${route.provider}/${route.model}` : "—";
910
+ const roleLabel = (role) => role === "lead" ? tr(lang, "Lead", "Lead") : role === "worker" ? tr(lang, "Sidekick", "Sidekick") : tr(lang, "压缩模型", "Compaction model");
873
911
  const update = (change) => setDraft((value) => value.map((combo, index) => combo.key === current?.key ? change(combo) : combo));
874
912
  const chooseModel = (role, route) => update((combo) => ({
875
913
  ...combo,
@@ -920,6 +958,11 @@ window.__ModuleLoader__.load({
920
958
  setError(tr(lang, `第 ${invalid + 1} 个组合还不完整:需要名称、Lead 和 Sidekick。`, `Combo ${invalid + 1} is incomplete: it needs a name, a Lead and a Sidekick.`));
921
959
  return;
922
960
  }
961
+ const stale = draft.flatMap((combo, index) => missingRoutes(combo.pair, state.catalog).map(({ role, route }) => tr(lang, `第 ${index + 1} 个组合「${combo.name.trim() || tr(lang, "未命名组合", "Untitled combo")}」的 ${roleLabel(role)} 模型已不可用:${route.provider}/${route.model}。请为该角色重新选择模型。`, `Combo ${index + 1} "${combo.name.trim() || "Untitled combo"}" still names a ${roleLabel(role)} model that is no longer available: ${route.provider}/${route.model}. Choose a model for that role again.`)));
962
+ if (stale.length) {
963
+ setError(stale.join(tr(lang, " ", " ")));
964
+ return;
965
+ }
923
966
  setSaving(true);
924
967
  setError(void 0);
925
968
  setNotice(void 0);
@@ -1024,13 +1067,20 @@ window.__ModuleLoader__.load({
1024
1067
  "aria-hidden": "true"
1025
1068
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1026
1069
  className: "fusion-combo-text",
1027
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1028
- className: "fusion-combo-name",
1029
- children: label
1030
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1031
- className: "fusion-combo-routes",
1032
- children: `${routeName(combo.pair.lead)} → ${routeName(combo.pair.worker)}`
1033
- })]
1070
+ children: [
1071
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1072
+ className: "fusion-combo-name",
1073
+ children: label
1074
+ }),
1075
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1076
+ className: "fusion-combo-routes",
1077
+ children: `${routeName(combo.pair.lead)} → ${routeName(combo.pair.worker)}`
1078
+ }),
1079
+ missingRoutes(combo.pair, state.catalog).length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1080
+ className: "fusion-combo-stale",
1081
+ children: tr(lang, "有模型已不可用,需要重新选择", "A model is no longer available; choose it again")
1082
+ })
1083
+ ]
1034
1084
  })]
1035
1085
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1036
1086
  type: "button",