@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 +29 -0
- package/lib/client.js +71 -21
- package/lib/client.js.map +1 -1
- package/lib/dsh-fusion.js +9 -8
- package/lib/types/host/settings.d.ts +16 -1
- package/package.json +1 -1
- package/src/client/FusionSettings.tsx +20 -0
- package/src/host/pairs.ts +3 -1
- package/src/host/settings.ts +36 -7
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: [
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
value: key(
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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: [
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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",
|