dsh-hypatia-auto-memory 1.1.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -13,6 +13,108 @@ window.__ModuleLoader__.load({
13
13
  return `${route.provider}\0${route.model}`;
14
14
  }
15
15
  /**
16
+ * Move one entry of the priority list, returning a new array.
17
+ *
18
+ * The stored `consolidation.models` array IS the priority order, so this is the
19
+ * only thing a reorder has to do: reorder the array. `to` is clamped instead of
20
+ * rejected, so a click that cannot move anything (the first row's "up") is a
21
+ * no-op rather than an error. A no-op returns the ORIGINAL reference, which lets
22
+ * a caller use identity to skip the state update.
23
+ *
24
+ * @param models - Priority-ordered routes; not mutated.
25
+ * @param from - Index to move.
26
+ * @param to - Target index, clamped into `models`.
27
+ */
28
+ function moveConsolidationModel(models, from, to) {
29
+ if (!Number.isInteger(from) || from < 0 || from >= models.length) return models;
30
+ if (!Number.isFinite(to)) return models;
31
+ const target = Math.min(Math.max(0, Math.trunc(to)), models.length - 1);
32
+ if (target === from) return models;
33
+ const next = [...models];
34
+ const [moved] = next.splice(from, 1);
35
+ next.splice(target, 0, moved);
36
+ return next;
37
+ }
38
+ /**
39
+ * The stored routes in their configured (priority) order, each resolved to its
40
+ * catalog entry for a display name, an availability flag and the reasoning
41
+ * levels the route declares.
42
+ *
43
+ * `consolidationModelCandidates` already guarantees every stored route appears —
44
+ * taken from the catalog, or appended as unavailable — so the candidate list is
45
+ * a lookup table for this. A route that somehow is not in it is still shown,
46
+ * named by its raw ids and marked unavailable, because dropping it from the
47
+ * priority list would hide a configured route from the only UI that can remove
48
+ * it.
49
+ *
50
+ * `reasoningEffort` is taken from the STORED route, never from the catalog: the
51
+ * catalog describes what a model can do, the stored route is what the user chose.
52
+ */
53
+ function orderedConsolidationModels(stored, candidates) {
54
+ const byKey = new Map(candidates.map((candidate) => [candidate.key, candidate]));
55
+ return stored.map((route) => {
56
+ const key = consolidationModelKey(route);
57
+ return {
58
+ ...byKey.get(key) ?? {
59
+ ...route,
60
+ key,
61
+ providerName: route.provider,
62
+ modelName: route.model,
63
+ available: false,
64
+ selected: true
65
+ },
66
+ reasoningEffort: route.reasoningEffort
67
+ };
68
+ });
69
+ }
70
+ /**
71
+ * The thinking-level options for one route, in the order they are offered.
72
+ *
73
+ * Two rules, both about not inventing a vocabulary this plugin does not own:
74
+ *
75
+ * - The list comes from the route's own declared `efforts`, in the adapter's
76
+ * preferred order, labelled with the adapter's own names. Nothing is
77
+ * hard-coded — the ids are opaque, and a level one adapter accepts is not one
78
+ * another does.
79
+ * - "Follow the purpose policy" (`undefined`) is always first and is what an
80
+ * absent field stores. It is NOT the same as the adapter's `defaultEffort`:
81
+ * omitting the field lets the plugin's own per-purpose policy apply, which for
82
+ * extraction and adjudication means asking for no reasoning at all.
83
+ *
84
+ * A stored value the route no longer declares is appended and flagged rather
85
+ * than dropped, so the card can show what is actually configured instead of
86
+ * silently rewriting it — and so a route whose catalog entry disappeared still
87
+ * displays its setting.
88
+ *
89
+ * @param reasoning - The route's catalog metadata; absent when the catalog does
90
+ * not advertise the route, or the model declares no reasoning control.
91
+ * @param stored - The configured value, if any.
92
+ */
93
+ function reasoningEffortChoices(reasoning, stored) {
94
+ const efforts = Array.isArray(reasoning?.efforts) ? reasoning.efforts : [];
95
+ const choices = [{
96
+ key: "purpose",
97
+ effort: void 0
98
+ }];
99
+ for (const effort of efforts) {
100
+ const id = typeof effort?.id === "string" ? effort.id : "";
101
+ if (id === "") continue;
102
+ const name = typeof effort.name === "string" && effort.name !== "" ? effort.name : id;
103
+ choices.push({
104
+ key: `effort:${id}`,
105
+ effort: id,
106
+ name
107
+ });
108
+ }
109
+ const value = typeof stored === "string" ? stored.trim() : "";
110
+ if (value !== "" && !efforts.some((effort) => effort?.id === value)) choices.push({
111
+ key: `stale:${value}`,
112
+ effort: value,
113
+ stale: true
114
+ });
115
+ return choices;
116
+ }
117
+ /**
16
118
  * Join the live model directory with selected routes that disappeared from it.
17
119
  * Disappeared routes remain visible so users can remove them deliberately.
18
120
  */
@@ -31,7 +133,8 @@ window.__ModuleLoader__.load({
31
133
  providerName: group.name,
32
134
  modelName: model.name,
33
135
  available: true,
34
- selected: selected.has(key)
136
+ selected: selected.has(key),
137
+ ...model.reasoning === void 0 ? {} : { reasoning: model.reasoning }
35
138
  };
36
139
  }));
37
140
  for (const route of storedByKey.values()) {
@@ -73,7 +176,7 @@ window.__ModuleLoader__.load({
73
176
  }
74
177
  //#endregion
75
178
  //#region \0dsh-hypatia-css:/home/runner/work/dsh-hypatia-auto-memory/dsh-hypatia-auto-memory/src/client/SettingsCard.module.css.mjs
76
- const css$1 = ".YVNYya_card{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-2);border-radius:16px}.YVNYya_header{align-items:center;gap:12px;padding:14px 16px;display:flex}.YVNYya_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.YVNYya_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.YVNYya_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.YVNYya_pending{corner-shape:round;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.YVNYya_body{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.YVNYya_status,.YVNYya_readOnly,.YVNYya_hint,.YVNYya_advancedHint,.YVNYya_error{margin:0;font-size:12px;line-height:1.5}.YVNYya_status,.YVNYya_readOnly,.YVNYya_hint,.YVNYya_advancedHint{color:var(--dsw-alias-label-tertiary)}.YVNYya_status{padding:12px 16px}.YVNYya_readOnly{padding-top:12px}.YVNYya_field{gap:6px;padding:12px 0;display:grid}.YVNYya_body>.YVNYya_field+.YVNYya_group,.YVNYya_body>.YVNYya_group+.YVNYya_field{border-top:.5px solid var(--dsw-alias-border-l2)}.YVNYya_group{padding:12px 0}.YVNYya_groupTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:13px;font-weight:500;line-height:1.5}.YVNYya_limitsTitle{border-top:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);margin:16px 0 0;padding-top:12px;font-size:13px;font-weight:500;line-height:1.5}.YVNYya_limitsTitle+.YVNYya_hint{margin-top:4px}.YVNYya_group>.YVNYya_field:first-of-type{padding-top:12px}.YVNYya_group>.YVNYya_field+.YVNYya_field,.YVNYya_pairedFields{border-top:.5px solid var(--dsw-alias-border-l2)}.YVNYya_fieldHead{align-items:center;gap:8px;display:flex}.YVNYya_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.YVNYya_reset{color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;background:0 0;border:0;padding:0;font-size:12px;line-height:1.5}.YVNYya_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.YVNYya_reset:disabled{cursor:default}.YVNYya_input{box-sizing:border-box;width:100%;min-width:0;display:flex}.YVNYya_select{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l3);width:100%;min-width:0;height:32px;color:var(--dsw-alias-label-primary);font:inherit;background:0 0;border-radius:8px;padding:0 8px;font-size:13px}.YVNYya_select:disabled{opacity:.5}.YVNYya_select:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.YVNYya_warning{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.YVNYya_pairedFields{grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin:4px 0;padding:4px 0;display:grid}.YVNYya_pairedFields .YVNYya_field{min-width:0}.YVNYya_switch{box-sizing:border-box;background:var(--dsw-alias-border-l3);cursor:pointer;border:0;border-radius:10px;width:36px;height:20px;padding:2px;position:relative}.YVNYya_switchOn{background:var(--dsw-alias-brand-primary)}.YVNYya_switch:disabled{cursor:default;opacity:.5}.YVNYya_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.YVNYya_switchThumb{corner-shape:round;background:var(--dsw-alias-label-primary-foreground);border-radius:50%;width:16px;height:16px;transition:transform .12s;display:block}.YVNYya_switchOn .YVNYya_switchThumb{transform:translate(16px)}.YVNYya_modelSelection{gap:10px;padding:12px 0;display:grid}.YVNYya_notice{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.YVNYya_catalogError{color:var(--dsw-alias-label-error);justify-content:space-between;align-items:center;gap:12px;font-size:12px;line-height:1.5;display:flex}.YVNYya_catalogError button{color:var(--dsw-alias-brand-primary);cursor:pointer;font:inherit;background:0 0;border:0;padding:0}.YVNYya_models{border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;gap:6px;min-width:0;max-height:280px;margin:0;padding:10px;display:grid;overflow:auto}.YVNYya_models legend{color:var(--dsw-alias-label-secondary);padding:0 4px;font-size:12px}.YVNYya_modelGroup{gap:6px;display:grid}.YVNYya_modelGroup+.YVNYya_modelGroup{border-top:.5px solid var(--dsw-alias-border-l3);margin-top:4px;padding-top:10px}.YVNYya_providerName{color:var(--dsw-alias-label-tertiary);padding:0 6px;font-size:11px;font-weight:500}.YVNYya_model{cursor:pointer;border-radius:6px;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:8px;min-width:0;padding:6px;display:grid}.YVNYya_model:hover{background:var(--dsw-alias-bg-layer-4)}.YVNYya_modelName,.YVNYya_route{text-overflow:ellipsis;white-space:nowrap;display:block;overflow:hidden}.YVNYya_modelName{color:var(--dsw-alias-label-primary);font-size:13px}.YVNYya_route{color:var(--dsw-alias-label-tertiary);margin-top:2px;font-size:11px}.YVNYya_unavailable{color:var(--dsw-alias-label-tertiary);font-size:11px}.YVNYya_error{color:var(--dsw-alias-label-error)}.YVNYya_advancedHint{border-top:.5px solid var(--dsw-alias-border-l2);padding-top:12px}.YVNYya_footer{border-top:.5px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}@media (width<=480px){.YVNYya_pairedFields{grid-template-columns:minmax(0,1fr);gap:0}.YVNYya_pairedFields .YVNYya_field+.YVNYya_field{border-top:.5px solid var(--dsw-alias-border-l2)}}";
179
+ const css$1 = ".YVNYya_card{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-2);border-radius:16px}.YVNYya_header{align-items:center;gap:12px;padding:14px 16px;display:flex}.YVNYya_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.YVNYya_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.YVNYya_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.YVNYya_pending{corner-shape:round;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.YVNYya_body{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.YVNYya_status,.YVNYya_readOnly,.YVNYya_hint,.YVNYya_advancedHint,.YVNYya_error{margin:0;font-size:12px;line-height:1.5}.YVNYya_status,.YVNYya_readOnly,.YVNYya_hint,.YVNYya_advancedHint{color:var(--dsw-alias-label-tertiary)}.YVNYya_status{padding:12px 16px}.YVNYya_readOnly{padding-top:12px}.YVNYya_field{gap:6px;padding:12px 0;display:grid}.YVNYya_body>.YVNYya_field+.YVNYya_group,.YVNYya_body>.YVNYya_group+.YVNYya_field{border-top:.5px solid var(--dsw-alias-border-l2)}.YVNYya_group{padding:12px 0}.YVNYya_groupTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:13px;font-weight:500;line-height:1.5}.YVNYya_limitsTitle{border-top:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);margin:16px 0 0;padding-top:12px;font-size:13px;font-weight:500;line-height:1.5}.YVNYya_limitsTitle+.YVNYya_hint{margin-top:4px}.YVNYya_group>.YVNYya_field:first-of-type{padding-top:12px}.YVNYya_group>.YVNYya_field+.YVNYya_field,.YVNYya_pairedFields{border-top:.5px solid var(--dsw-alias-border-l2)}.YVNYya_fieldHead{align-items:center;gap:8px;display:flex}.YVNYya_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.YVNYya_reset{color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;background:0 0;border:0;padding:0;font-size:12px;line-height:1.5}.YVNYya_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.YVNYya_reset:disabled{cursor:default}.YVNYya_input{box-sizing:border-box;width:100%;min-width:0;display:flex}.YVNYya_select{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l3);width:100%;min-width:0;height:32px;color:var(--dsw-alias-label-primary);font:inherit;background:0 0;border-radius:8px;padding:0 8px;font-size:13px}.YVNYya_select:disabled{opacity:.5}.YVNYya_select:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.YVNYya_warning{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.YVNYya_pairedFields{grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin:4px 0;padding:4px 0;display:grid}.YVNYya_pairedFields .YVNYya_field{min-width:0}.YVNYya_switch{box-sizing:border-box;background:var(--dsw-alias-border-l3);cursor:pointer;border:0;border-radius:10px;width:36px;height:20px;padding:2px;position:relative}.YVNYya_switchOn{background:var(--dsw-alias-brand-primary)}.YVNYya_switch:disabled{cursor:default;opacity:.5}.YVNYya_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.YVNYya_switchThumb{corner-shape:round;background:var(--dsw-alias-label-primary-foreground);border-radius:50%;width:16px;height:16px;transition:transform .12s;display:block}.YVNYya_switchOn .YVNYya_switchThumb{transform:translate(16px)}.YVNYya_modelSelection{gap:10px;padding:12px 0;display:grid}.YVNYya_notice{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.YVNYya_catalogError{color:var(--dsw-alias-label-error);justify-content:space-between;align-items:center;gap:12px;font-size:12px;line-height:1.5;display:flex}.YVNYya_catalogError button{color:var(--dsw-alias-brand-primary);cursor:pointer;font:inherit;background:0 0;border:0;padding:0}.YVNYya_models{border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;gap:6px;min-width:0;max-height:280px;margin:0;padding:10px;display:grid;overflow:auto}.YVNYya_models legend{color:var(--dsw-alias-label-secondary);padding:0 4px;font-size:12px}.YVNYya_modelGroup{gap:6px;display:grid}.YVNYya_modelGroup+.YVNYya_modelGroup{border-top:.5px solid var(--dsw-alias-border-l3);margin-top:4px;padding-top:10px}.YVNYya_providerName{color:var(--dsw-alias-label-tertiary);padding:0 6px;font-size:11px;font-weight:500}.YVNYya_model{cursor:pointer;border-radius:6px;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:8px;min-width:0;padding:6px;display:grid}.YVNYya_model:hover{background:var(--dsw-alias-bg-layer-4)}.YVNYya_selectedRow{border-radius:6px;align-items:center;gap:8px;min-width:0;padding:6px;display:flex}.YVNYya_selectedRow:hover{background:var(--dsw-alias-bg-layer-4)}.YVNYya_order{min-width:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;text-align:right;font-size:11px}.YVNYya_selectedText{flex:auto;min-width:0}.YVNYya_effort{border:.5px solid var(--dsw-alias-border-l4);max-width:132px;color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;background:0 0;border-radius:5px;flex:none;padding:2px 4px;font-size:12px}.YVNYya_effort:disabled{cursor:default;opacity:.5}.YVNYya_effort:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.YVNYya_rowActions{align-items:center;gap:2px;display:flex}.YVNYya_move{border:.5px solid var(--dsw-alias-border-l4);width:22px;height:22px;color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;background:0 0;border-radius:5px;padding:0;font-size:12px;line-height:1}.YVNYya_move:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4);color:var(--dsw-alias-label-primary)}.YVNYya_move:disabled{cursor:default;opacity:.4}.YVNYya_modelName,.YVNYya_route{text-overflow:ellipsis;white-space:nowrap;display:block;overflow:hidden}.YVNYya_modelName{color:var(--dsw-alias-label-primary);font-size:13px}.YVNYya_route{color:var(--dsw-alias-label-tertiary);margin-top:2px;font-size:11px}.YVNYya_unavailable{color:var(--dsw-alias-label-tertiary);font-size:11px}.YVNYya_error{color:var(--dsw-alias-label-error)}.YVNYya_advancedHint{border-top:.5px solid var(--dsw-alias-border-l2);padding-top:12px}.YVNYya_footer{border-top:.5px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}@media (width<=480px){.YVNYya_pairedFields{grid-template-columns:minmax(0,1fr);gap:0}.YVNYya_pairedFields .YVNYya_field+.YVNYya_field{border-top:.5px solid var(--dsw-alias-border-l2)}}";
77
180
  const tagId$1 = "dsh-hypatia-auto-memory/SettingsCard.module.css";
78
181
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
79
182
  const tag = document.createElement("style");
@@ -88,6 +191,7 @@ window.__ModuleLoader__.load({
88
191
  "card": "YVNYya_card",
89
192
  "catalogError": "YVNYya_catalogError",
90
193
  "description": "YVNYya_description",
194
+ "effort": "YVNYya_effort",
91
195
  "error": "YVNYya_error",
92
196
  "field": "YVNYya_field",
93
197
  "fieldHead": "YVNYya_fieldHead",
@@ -105,15 +209,20 @@ window.__ModuleLoader__.load({
105
209
  "modelName": "YVNYya_modelName",
106
210
  "modelSelection": "YVNYya_modelSelection",
107
211
  "models": "YVNYya_models",
212
+ "move": "YVNYya_move",
108
213
  "name": "YVNYya_name",
109
214
  "notice": "YVNYya_notice",
215
+ "order": "YVNYya_order",
110
216
  "pairedFields": "YVNYya_pairedFields",
111
217
  "pending": "YVNYya_pending",
112
218
  "providerName": "YVNYya_providerName",
113
219
  "readOnly": "YVNYya_readOnly",
114
220
  "reset": "YVNYya_reset",
115
221
  "route": "YVNYya_route",
222
+ "rowActions": "YVNYya_rowActions",
116
223
  "select": "YVNYya_select",
224
+ "selectedRow": "YVNYya_selectedRow",
225
+ "selectedText": "YVNYya_selectedText",
117
226
  "status": "YVNYya_status",
118
227
  "switch": "YVNYya_switch",
119
228
  "switchOn": "YVNYya_switchOn",
@@ -146,14 +255,25 @@ window.__ModuleLoader__.load({
146
255
  shelfDisconnectedWarning: "shelf {shelf} 已注册但未连接,写入会失败。",
147
256
  shelfNotListedWarning: "hypatia 没有注册 shelf {shelf},写入会失败;请先执行 hypatia connect <目录> --name {shelf}。",
148
257
  consolidationTitle: "整合候选模型",
149
- modelSelectionHint: "选择一个或多个模型。每次整合尝试(含重试)会按所选顺序轮转;未选择时只记录对话,不执行整合。",
258
+ modelSelectionHint: "选择一个或多个模型并决定优先级。列表自上而下即优先级:每次整合先用最上面那条,该次尝试失败时,重试会依次降级到下一条(可用的备选条数受队列重试次数限制)。未选择时只记录对话,不执行整合。",
150
259
  modelCatalogLoading: "正在读取可用模型…",
151
260
  modelCatalogFailed: "无法读取可用模型。",
152
261
  modelCatalogPartial: "部分提供方未能返回模型目录。",
153
262
  modelCatalogEmpty: "当前没有可选择的模型。",
154
- selectedModels: "用于轮转的模型",
263
+ selectedModels: "优先级顺序(自上而下)",
264
+ selectedModelsEmpty: "还没有选择模型。",
265
+ availableModels: "可选模型",
155
266
  unavailableModels: "不可用或未再声明的模型",
156
267
  modelUnavailable: "当前不可用",
268
+ moveUp: "上移",
269
+ moveDown: "下移",
270
+ remove: "移除",
271
+ reasoningEffort: "思考程度",
272
+ reasoningFollowPurpose: "跟随用途策略",
273
+ reasoningUnavailable: "该路由未声明思考档位,无法设置",
274
+ reasoningStale: "该路由已不再声明",
275
+ reasoningDefault: "未指定时该路由默认为:{effort}",
276
+ reasoningBudgetHint: "思考程度决定该模型思考多久。选「跟随用途策略」时,摘要与裁决会尽量关闭思考(两者都是机械变换,思考只花掉输出预算),归档则沿用模型自己的默认档位。显式选了档位就会覆盖上述策略;非关闭档位与正文共用输出预算,插件会自动追加余量,但高强度档位仍建议同步调大输出预算。档位由模型自己声明,未声明的值会在调用时被忽略并记一条警告。",
157
277
  retry: "重试",
158
278
  checkEveryTurns: "两次整合至少间隔 N 轮",
159
279
  minNewTokens: "最小新增 token 数",
@@ -235,14 +355,25 @@ window.__ModuleLoader__.load({
235
355
  shelfDisconnectedWarning: "Shelf {shelf} is registered but not connected; writes will fail.",
236
356
  shelfNotListedWarning: "hypatia has no shelf named {shelf}; writes will fail until you run hypatia connect <dir> --name {shelf}.",
237
357
  consolidationTitle: "Consolidation candidate models",
238
- modelSelectionHint: "Choose one or more models. Every consolidation attempt, including a retry, rotates through this order; without a selection, conversations continue logging but consolidation stays idle.",
358
+ modelSelectionHint: "Choose one or more models and set their priority. The list is read top-down: every consolidation starts on the first route, and each failed attempt degrades to the next one (how many fallbacks a task can reach is bounded by the queue's retry limit). Without a selection, conversations continue logging but consolidation stays idle.",
239
359
  modelCatalogLoading: "Loading available models…",
240
360
  modelCatalogFailed: "Unable to load available models.",
241
361
  modelCatalogPartial: "Some providers did not return a model catalog.",
242
362
  modelCatalogEmpty: "No models are currently available.",
243
- selectedModels: "Models used for rotation",
363
+ selectedModels: "Priority order (top first)",
364
+ selectedModelsEmpty: "No model selected yet.",
365
+ availableModels: "Available models",
244
366
  unavailableModels: "Unavailable or no longer advertised",
245
367
  modelUnavailable: "Unavailable",
368
+ moveUp: "Move up",
369
+ moveDown: "Move down",
370
+ remove: "Remove",
371
+ reasoningEffort: "Reasoning effort",
372
+ reasoningFollowPurpose: "Follow the purpose policy",
373
+ reasoningUnavailable: "This route declares no reasoning levels",
374
+ reasoningStale: "no longer declared by this route",
375
+ reasoningDefault: "Without one, this route defaults to: {effort}",
376
+ reasoningBudgetHint: "Reasoning effort is how long the model may think. On \"follow the purpose policy\", summaries and adjudication ask for no reasoning where the route allows it — both are mechanical transforms, so thinking only spends the output budget — while the archive call keeps the model's own default. Choosing a level explicitly overrides that policy; any level other than off shares the output budget with the answer, so the plugin adds its thinking allowance, and a high level is still worth pairing with a larger output budget. Levels are declared by the model itself, and one it does not declare is dropped at call time with a warning.",
246
377
  retry: "Retry",
247
378
  checkEveryTurns: "Minimum turns between consolidations",
248
379
  minNewTokens: "Min new tokens",
@@ -447,6 +578,7 @@ window.__ModuleLoader__.load({
447
578
  const models = consolidation.models ?? [];
448
579
  return consolidationModelCandidates(catalogGroups, models, new Set(models.map(consolidationModelKey)));
449
580
  }, [catalogGroups, consolidation.models]);
581
+ const selectedModels = (0, react.useMemo)(() => orderedConsolidationModels(consolidation.models ?? [], candidates), [consolidation.models, candidates]);
450
582
  const toggleModel = (0, react.useCallback)((candidate) => {
451
583
  if (disabled || saving) return;
452
584
  setDraft((current) => {
@@ -466,6 +598,77 @@ window.__ModuleLoader__.load({
466
598
  };
467
599
  });
468
600
  }, [disabled, saving]);
601
+ /**
602
+ * Reorder one entry of the priority list. A no-op move leaves the draft
603
+ * untouched (the mover returns the original array), so the "unsaved" badge
604
+ * does not light up for a click that changed nothing.
605
+ */
606
+ const moveModel = (0, react.useCallback)((from, to) => {
607
+ if (disabled || saving) return;
608
+ setDraft((current) => {
609
+ const currentConsolidation = current.consolidation ?? DEFAULT_CONSOLIDATION;
610
+ const currentModels = currentConsolidation.models ?? [];
611
+ const models = moveConsolidationModel(currentModels, from, to);
612
+ if (models === currentModels) return current;
613
+ return {
614
+ ...current,
615
+ consolidation: {
616
+ ...currentConsolidation,
617
+ models
618
+ }
619
+ };
620
+ });
621
+ }, [disabled, saving]);
622
+ /**
623
+ * Set or clear one route's thinking level. Clearing REMOVES the key rather
624
+ * than storing `undefined`, so the saved patch carries only what the user
625
+ * chose and nothing reading the profile can confuse "not configured" with
626
+ * "configured as nothing".
627
+ */
628
+ const setModelEffort = (0, react.useCallback)((index, effort) => {
629
+ if (disabled || saving) return;
630
+ setDraft((current) => {
631
+ const currentConsolidation = current.consolidation ?? DEFAULT_CONSOLIDATION;
632
+ const currentModels = currentConsolidation.models ?? [];
633
+ if (index < 0 || index >= currentModels.length) return current;
634
+ const models = currentModels.map((route, at) => {
635
+ if (at !== index) return route;
636
+ if (effort === void 0) {
637
+ const cleared = { ...route };
638
+ delete cleared.reasoningEffort;
639
+ return cleared;
640
+ }
641
+ return {
642
+ ...route,
643
+ reasoningEffort: effort
644
+ };
645
+ });
646
+ return {
647
+ ...current,
648
+ consolidation: {
649
+ ...currentConsolidation,
650
+ models
651
+ }
652
+ };
653
+ });
654
+ }, [disabled, saving]);
655
+ /** Display label for one thinking-level option, in the card's own words. */
656
+ const effortChoiceLabel = (0, react.useCallback)((choice) => {
657
+ if (choice.effort === void 0) return t("reasoningFollowPurpose");
658
+ const name = choice.name ?? choice.effort;
659
+ return choice.stale === true ? `${name} · ${t("reasoningStale")}` : name;
660
+ }, [t]);
661
+ /**
662
+ * What the adapter does when no effort is sent. Only a hint: this plugin
663
+ * cannot pin it, because omitting the field is what lets its own per-purpose
664
+ * policy apply.
665
+ */
666
+ const routeDefaultLabel = (0, react.useCallback)((candidate) => {
667
+ const reasoning = candidate.reasoning;
668
+ const fallback = reasoning?.defaultEffort;
669
+ if (reasoning === void 0 || fallback === void 0) return void 0;
670
+ return t("reasoningDefault", { effort: reasoning.efforts.find((effort) => effort.id === fallback)?.name ?? fallback });
671
+ }, [t]);
469
672
  const discard = (0, react.useCallback)(() => {
470
673
  setDraft(resolved);
471
674
  setError(null);
@@ -756,10 +959,82 @@ window.__ModuleLoader__.load({
756
959
  className: SettingsCard_module_css_default.notice,
757
960
  children: t("modelCatalogPartial")
758
961
  }) : null,
759
- candidates.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
962
+ candidates.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
963
+ className: SettingsCard_module_css_default.models,
964
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", { children: t("selectedModels") }), selectedModels.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
965
+ className: SettingsCard_module_css_default.notice,
966
+ children: t("selectedModelsEmpty")
967
+ }) : selectedModels.map((candidate, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
968
+ className: SettingsCard_module_css_default.selectedRow,
969
+ children: [
970
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
971
+ className: SettingsCard_module_css_default.order,
972
+ children: index + 1
973
+ }),
974
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
975
+ className: SettingsCard_module_css_default.selectedText,
976
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
977
+ className: SettingsCard_module_css_default.modelName,
978
+ children: candidate.modelName
979
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
980
+ className: SettingsCard_module_css_default.route,
981
+ children: `${candidate.providerName} · ${candidate.provider}/${candidate.model}`
982
+ })]
983
+ }),
984
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
985
+ className: SettingsCard_module_css_default.effort,
986
+ "aria-label": `${t("reasoningEffort")}: ${candidate.modelName}`,
987
+ title: candidate.reasoning === void 0 ? t("reasoningUnavailable") : routeDefaultLabel(candidate) ?? t("reasoningFollowPurpose"),
988
+ value: candidate.reasoningEffort ?? "",
989
+ disabled: disabled || saving || candidate.reasoning === void 0,
990
+ onChange: (event) => setModelEffort(index, event.target.value === "" ? void 0 : event.target.value),
991
+ children: reasoningEffortChoices(candidate.reasoning, candidate.reasoningEffort).map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
992
+ value: choice.effort ?? "",
993
+ children: effortChoiceLabel(choice)
994
+ }, choice.key))
995
+ }),
996
+ !candidate.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
997
+ className: SettingsCard_module_css_default.unavailable,
998
+ children: t("modelUnavailable")
999
+ }) : null,
1000
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1001
+ className: SettingsCard_module_css_default.rowActions,
1002
+ children: [
1003
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1004
+ type: "button",
1005
+ className: SettingsCard_module_css_default.move,
1006
+ title: t("moveUp"),
1007
+ "aria-label": `${t("moveUp")}: ${candidate.modelName}`,
1008
+ disabled: disabled || saving || index === 0,
1009
+ onClick: () => moveModel(index, index - 1),
1010
+ children: "↑"
1011
+ }),
1012
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1013
+ type: "button",
1014
+ className: SettingsCard_module_css_default.move,
1015
+ title: t("moveDown"),
1016
+ "aria-label": `${t("moveDown")}: ${candidate.modelName}`,
1017
+ disabled: disabled || saving || index === selectedModels.length - 1,
1018
+ onClick: () => moveModel(index, index + 1),
1019
+ children: "↓"
1020
+ }),
1021
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1022
+ type: "button",
1023
+ className: SettingsCard_module_css_default.move,
1024
+ title: t("remove"),
1025
+ "aria-label": `${t("remove")}: ${candidate.modelName}`,
1026
+ disabled: disabled || saving,
1027
+ onClick: () => toggleModel(candidate),
1028
+ children: "×"
1029
+ })
1030
+ ]
1031
+ })
1032
+ ]
1033
+ }, candidate.key))]
1034
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
760
1035
  className: SettingsCard_module_css_default.models,
761
1036
  children: [
762
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", { children: t("selectedModels") }),
1037
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", { children: t("availableModels") }),
763
1038
  [...availableGroups].map(([provider, group]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
764
1039
  className: SettingsCard_module_css_default.modelGroup,
765
1040
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -775,10 +1050,14 @@ window.__ModuleLoader__.load({
775
1050
  }), unavailable.map(renderCandidate)]
776
1051
  }) : null
777
1052
  ]
778
- }) : catalogStatus === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1053
+ })] }) : catalogStatus === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
779
1054
  className: SettingsCard_module_css_default.notice,
780
1055
  children: t("modelCatalogEmpty")
781
- }) : null
1056
+ }) : null,
1057
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1058
+ className: SettingsCard_module_css_default.hint,
1059
+ children: t("reasoningBudgetHint")
1060
+ })
782
1061
  ]
783
1062
  }),
784
1063
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {