dsh-plugin-effort-declare 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,8 @@ Write `llm-pi-ai` settings only:
17
17
  - No fork of the official Models page
18
18
  - No parallel effort table
19
19
  - No per-model `reasoningEfforts` in the bundle layer (settings dict merge has no delete semantics)
20
+ - Do not auto-write efforts onto undeclared models at Host start (dict merge cannot delete; it would mutate models the user never saved)
21
+ - Do not hook `agent/request` to fill a default effort for subagents
20
22
  - Do not set route-level `reasoning` to `high`
21
23
 
22
24
  Any new model-visible input must land in a session-log-rebuildable mechanism. This plugin does not inject prompt text.
@@ -38,7 +40,7 @@ pnpm build
38
40
 
39
41
  Before sending a change, say whether it is a bugfix, UI improvement, or an alignment with a newer DSH / pi-ai schema. Update zh and en docs together. The root README and [INSTALL.en.md](./INSTALL.en.md) are for users; this file and the READMEs under `src/` and `tests/` are for people changing code.
40
42
 
41
- Canonical level names and `thinkingFormat` enums come from the installed `llm-pi-ai` schema / catalog. The repo keeps a whitelist pinned by tests; if the schema drifts, update the tests first, then UI copy. Do not offer that list as writable dropdown choices.
43
+ Canonical level names and `thinkingFormat` enums come from the installed `llm-pi-ai` schema / catalog. The live `thinkingFormat` schema contract is [`tests/fixtures/pi-ai-thinking-format-union.ts`](./tests/fixtures/pi-ai-thinking-format-union.ts). When upgrading DSH, refresh that fixture from `dsh --dump-config` or the settings describe schema, then update `src/core/catalog.ts` and UI copy. Do not value-import `@deepseek-ai/dsh-llm-pi-ai` into the client bundle. Effort keys stay a locally pinned whitelist; if the schema drifts, change the fixture / tests first. Do not offer that list as writable dropdown choices.
42
44
 
43
45
  ## Publishing to npm
44
46
 
package/CONTRIBUTING.md CHANGED
@@ -17,6 +17,8 @@
17
17
  - 不要 fork 官方模型页
18
18
  - 不要平行再存一份档位表
19
19
  - 不要在 bundle 层预置 per-model `reasoningEfforts`(settings 字典合并没有删除语义)
20
+ - 不要在 Host 启动时给未声明的模型自动写入档位(字典合并删不掉,会改用户没保存过的模型)
21
+ - 不要挂 `agent/request` 给 subagent 填默认档
20
22
  - 不要把路由级 `reasoning` 写成 `high`
21
23
 
22
24
  新增任何模型可见输入,必须走会话日志可重建的机制。本插件不向模型注入提示词。
@@ -38,7 +40,7 @@ pnpm build
38
40
 
39
41
  提交前请说明改动是修 bug、增强 UI,还是对齐新的 DSH / pi-ai schema。中英文档一起改。根 README 和 [INSTALL.md](./INSTALL.md) 面向使用者;本文件与 `src/`、`tests/` 的 README 面向改代码的人。
40
42
 
41
- 上游档位名与 `thinkingFormat` 枚举以 DSH 安装里 `llm-pi-ai` 的 schema / catalog 为准。仓库内有一份测试钉白名单;schema 漂移时应先改测试再改 UI 文案。不要把这份名单当作可写入下拉选项。
43
+ 上游档位名与 `thinkingFormat` 枚举以 DSH 安装里 `llm-pi-ai` 的 schema / catalog 为准。`thinkingFormat` 的现场 schema 契约在 [`tests/fixtures/pi-ai-thinking-format-union.ts`](./tests/fixtures/pi-ai-thinking-format-union.ts)。升 DSH 时用 `dsh --dump-config` 或设置 describe 的 schema 更新该 fixture,再改 `src/core/catalog.ts` 与 UI。不要把 `@deepseek-ai/dsh-llm-pi-ai` 打进 client bundle。档位键仍由测试钉本地白名单;schema 漂移时应先改 fixture / 测试再改 UI。不要把这份名单当作可写入下拉选项。
42
44
 
43
45
  ## 发布到 npm
44
46
 
package/README.en.md CHANGED
@@ -8,16 +8,16 @@
8
8
  </p>
9
9
 
10
10
  <p align="center">
11
- <a href="https://www.npmjs.com/package/dsh-plugin-effort-declare"><img alt="npm" src="https://img.shields.io/npm/v/dsh-plugin-effort-declare"></a>
12
- <a href="https://github.com/zimodzh/dsh-plugin-effort-declare/commits/master"><img alt="last commit" src="https://img.shields.io/github/last-commit/zimodzh/dsh-plugin-effort-declare"></a>
13
- <a href="https://github.com/zimodzh/dsh-plugin-effort-declare"><img alt="repo size" src="https://img.shields.io/github/repo-size/zimodzh/dsh-plugin-effort-declare"></a>
14
- <a href="./LICENSE"><img alt="license" src="https://img.shields.io/github/license/zimodzh/dsh-plugin-effort-declare"></a>
15
- <a href="https://github.com/deepseek-ai/deepseek-harness"><img alt="Agent" src="https://img.shields.io/badge/Agent-Deepseek%20Harness-000000"></a>
11
+ <a href="https://www.npmjs.com/package/dsh-plugin-effort-declare" target="_blank" rel="noopener noreferrer"><img alt="npm" src="https://img.shields.io/npm/v/dsh-plugin-effort-declare"></a>
12
+ <a href="https://github.com/zimodzh/dsh-plugin-effort-declare/commits/master" target="_blank" rel="noopener noreferrer"><img alt="last commit" src="https://img.shields.io/github/last-commit/zimodzh/dsh-plugin-effort-declare"></a>
13
+ <a href="https://github.com/zimodzh/dsh-plugin-effort-declare" target="_blank" rel="noopener noreferrer"><img alt="repo size" src="https://img.shields.io/github/repo-size/zimodzh/dsh-plugin-effort-declare"></a>
14
+ <a href="./LICENSE" target="_blank" rel="noopener noreferrer"><img alt="license" src="https://img.shields.io/github/license/zimodzh/dsh-plugin-effort-declare"></a>
15
+ <a href="https://github.com/deepseek-ai/deepseek-harness" target="_blank" rel="noopener noreferrer"><img alt="Agent" src="https://img.shields.io/badge/Agent-Deepseek%20Harness-000000"></a>
16
16
  </p>
17
17
 
18
18
  Add reasoning-effort options to OpenAI-compatible models you added yourself, so the Effort row shows up in the composer.
19
19
 
20
- > This is a community plugin. It is not an official DeepSeek project and is not endorsed by DeepSeek. Requires DeepSeek Harness **0.1.0-rc.8 or later**.
20
+ > This is a community-maintained third-party plugin. It is not an official DeepSeek project and is not endorsed by DeepSeek. Requires **DeepSeek Harness 0.1.0-rc.8** or later. Versions before **DeepSeek Harness 0.1.0-rc.8** have not been tested and are not guaranteed to work.
21
21
 
22
22
  ## Why
23
23
 
package/README.md CHANGED
@@ -8,16 +8,16 @@
8
8
  </p>
9
9
 
10
10
  <p align="center">
11
- <a href="https://www.npmjs.com/package/dsh-plugin-effort-declare"><img alt="npm" src="https://img.shields.io/npm/v/dsh-plugin-effort-declare"></a>
12
- <a href="https://github.com/zimodzh/dsh-plugin-effort-declare/commits/master"><img alt="last commit" src="https://img.shields.io/github/last-commit/zimodzh/dsh-plugin-effort-declare"></a>
13
- <a href="https://github.com/zimodzh/dsh-plugin-effort-declare"><img alt="repo size" src="https://img.shields.io/github/repo-size/zimodzh/dsh-plugin-effort-declare"></a>
14
- <a href="./LICENSE"><img alt="license" src="https://img.shields.io/github/license/zimodzh/dsh-plugin-effort-declare"></a>
15
- <a href="https://github.com/deepseek-ai/deepseek-harness"><img alt="Agent" src="https://img.shields.io/badge/Agent-Deepseek%20Harness-000000"></a>
11
+ <a href="https://www.npmjs.com/package/dsh-plugin-effort-declare" target="_blank" rel="noopener noreferrer"><img alt="npm" src="https://img.shields.io/npm/v/dsh-plugin-effort-declare"></a>
12
+ <a href="https://github.com/zimodzh/dsh-plugin-effort-declare/commits/master" target="_blank" rel="noopener noreferrer"><img alt="last commit" src="https://img.shields.io/github/last-commit/zimodzh/dsh-plugin-effort-declare"></a>
13
+ <a href="https://github.com/zimodzh/dsh-plugin-effort-declare" target="_blank" rel="noopener noreferrer"><img alt="repo size" src="https://img.shields.io/github/repo-size/zimodzh/dsh-plugin-effort-declare"></a>
14
+ <a href="./LICENSE" target="_blank" rel="noopener noreferrer"><img alt="license" src="https://img.shields.io/github/license/zimodzh/dsh-plugin-effort-declare"></a>
15
+ <a href="https://github.com/deepseek-ai/deepseek-harness" target="_blank" rel="noopener noreferrer"><img alt="Agent" src="https://img.shields.io/badge/Agent-Deepseek%20Harness-000000"></a>
16
16
  </p>
17
17
 
18
18
  给自己添加的 OpenAI 兼容模型补上「推理强度」选项,对话里的 Effort 行就会出现。
19
19
 
20
- > 这是社区插件,不是 DeepSeek 官方项目,也未获官方背书。需要 DeepSeek Harness **0.1.0-rc.8 及以上**。
20
+ > 这是社区维护的第三方插件,不是 DeepSeek 官方项目,也未获官方背书。需要 **DeepSeek Harness 0.1.0-rc.8** 及以上, **DeepSeek Harness 0.1.0-rc.8** 之前的版本没测试过,不保证可用。
21
21
 
22
22
  ## 它解决什么
23
23
 
package/lib/client.js CHANGED
@@ -12,9 +12,11 @@ window.__ModuleLoader__.load({
12
12
  *
13
13
  * Levels match `@deepseek-ai/dsh-llm-pi-ai` catalog.ts `THINKING_LEVELS`.
14
14
  * Formats match `SUPPORTED_THINKING_FORMATS` in the same file (rc.8).
15
- * Tests pin these lists against llm-pi-ai. The settings page never offers
16
- * the handwritten thinkingFormat list as writable choices — only the live
17
- * schema union, plus a stored value that the union omitted.
15
+ * Tests pin these lists against a checked-in schema fixture
16
+ * (`tests/fixtures/pi-ai-thinking-format-union.ts`) and the local level
17
+ * whitelist. The settings page never offers the handwritten thinkingFormat
18
+ * list as writable choices — only the live schema union, plus a stored
19
+ * value that the union omitted.
18
20
  */
19
21
  /** Selectable reasoning levels, in pi-ai escalation order. */
20
22
  const THINKING_LEVELS = [
@@ -156,6 +158,14 @@ window.__ModuleLoader__.load({
156
158
  * Route-card draft merge: user-layer slices, namespace revision, dirty preserve.
157
159
  * No React — settings UI and tests share these helpers.
158
160
  */
161
+ /** JSON-stable equality matching pathOps (key order included). */
162
+ function sliceEqual(left, right) {
163
+ return JSON.stringify(left) === JSON.stringify(right);
164
+ }
165
+ /** Whether two settings slices differ. */
166
+ function sliceChanged(before, after) {
167
+ return !sliceEqual(before, after);
168
+ }
159
169
  /** Build a draft from the stored user subtree (never from effective `value`). */
160
170
  function routeDraftFromUserProfile(args) {
161
171
  const { provider, displayName, settingsPath, revision, userProfile } = args;
@@ -210,9 +220,103 @@ window.__ModuleLoader__.load({
210
220
  });
211
221
  });
212
222
  }
223
+ function modelRowId(row) {
224
+ return String(row.id);
225
+ }
226
+ function indexById(rows) {
227
+ const map = /* @__PURE__ */ new Map();
228
+ for (const row of rows) {
229
+ const id = modelRowId(row);
230
+ if (!map.has(id)) map.set(id, row);
231
+ }
232
+ return map;
233
+ }
234
+ function effortsPresence(row) {
235
+ if (row === void 0 || !Object.hasOwn(row, "reasoningEfforts")) return {
236
+ present: false,
237
+ value: void 0
238
+ };
239
+ return {
240
+ present: true,
241
+ value: row.reasoningEfforts
242
+ };
243
+ }
244
+ function effortsEqual(left, right) {
245
+ if (left.present !== right.present) return false;
246
+ if (!left.present) return true;
247
+ return sliceEqual(left.value, right.value);
248
+ }
249
+ function overlayLocalEfforts(incomingRow, prevRow) {
250
+ const next = structuredClone(incomingRow);
251
+ if (Object.hasOwn(prevRow, "reasoningEfforts")) next.reasoningEfforts = structuredClone(prevRow.reasoningEfforts);
252
+ else delete next.reasoningEfforts;
253
+ return next;
254
+ }
255
+ function objectKeyChanged(left, right, key) {
256
+ const leftHas = Object.hasOwn(left, key);
257
+ if (leftHas !== Object.hasOwn(right, key)) return true;
258
+ if (!leftHas) return false;
259
+ return sliceChanged(left[key], right[key]);
260
+ }
261
+ /**
262
+ * Membership follows the latest user-layer models list (Models page add/delete).
263
+ * Local unsaved `reasoningEfforts` (including a cleared key) overlay by id.
264
+ */
265
+ function mergeModelsById(args) {
266
+ const prevById = indexById(args.prevModels);
267
+ const prevOrigById = indexById(args.prevOriginal);
268
+ const incomingOrigById = indexById(args.incomingOriginal);
269
+ const incomingIds = new Set(args.incomingModels.map(modelRowId));
270
+ let conflicted = false;
271
+ const models = args.incomingModels.map((incomingRow) => {
272
+ const id = modelRowId(incomingRow);
273
+ const prevRow = prevById.get(id);
274
+ if (prevRow === void 0) return structuredClone(incomingRow);
275
+ const prevOrig = prevOrigById.get(id);
276
+ if (!!effortsEqual(effortsPresence(prevRow), effortsPresence(prevOrig))) return structuredClone(incomingRow);
277
+ const incomingOrig = incomingOrigById.get(id);
278
+ if (!effortsEqual(effortsPresence(prevOrig), effortsPresence(incomingOrig))) conflicted = true;
279
+ return overlayLocalEfforts(incomingRow, prevRow);
280
+ });
281
+ for (const [id, prevRow] of prevById) {
282
+ if (incomingIds.has(id)) continue;
283
+ const prevOrig = prevOrigById.get(id);
284
+ if (effortsEqual(effortsPresence(prevRow), effortsPresence(prevOrig))) continue;
285
+ if (!effortsEqual(effortsPresence(prevOrig), effortsPresence(incomingOrigById.get(id)))) conflicted = true;
286
+ }
287
+ return {
288
+ models,
289
+ conflicted
290
+ };
291
+ }
213
292
  /**
214
- * Apply a freshly loaded table. Dirty cards keep models/compat; originals and
215
- * revision follow the incoming snapshot so a later save is against the new user layer.
293
+ * Three-way compat merge: locally changed keys stay local; everything else
294
+ * follows incoming. Conflict only when a locally dirty key also moved in originals.
295
+ */
296
+ function mergeCompat(args) {
297
+ if (!sliceChanged(args.prev, args.prevOriginal)) return {
298
+ compat: cloneObject(args.incoming),
299
+ conflicted: false
300
+ };
301
+ const compat = cloneObject(args.incoming);
302
+ let conflicted = false;
303
+ const keys = /* @__PURE__ */ new Set([...Object.keys(args.prev), ...Object.keys(args.prevOriginal)]);
304
+ for (const key of keys) {
305
+ if (!objectKeyChanged(args.prev, args.prevOriginal, key)) continue;
306
+ if (objectKeyChanged(args.prevOriginal, args.incomingOriginal, key)) conflicted = true;
307
+ if (Object.hasOwn(args.prev, key)) compat[key] = structuredClone(args.prev[key]);
308
+ else delete compat[key];
309
+ }
310
+ return {
311
+ compat,
312
+ conflicted
313
+ };
314
+ }
315
+ /**
316
+ * Apply a freshly loaded table. Membership and metadata follow incoming;
317
+ * unsaved reasoningEfforts / dirty compat keys overlay by id. Conflict only
318
+ * when a locally dirty field also changed in originals (revision-only bumps
319
+ * and sibling-card saves do not warn).
216
320
  */
217
321
  function mergeLoadedDrafts(current, incoming, options) {
218
322
  const currentByProvider = new Map(current.map((draft) => [draft.provider, draft]));
@@ -221,13 +325,27 @@ window.__ModuleLoader__.load({
221
325
  drafts: incoming.map((next) => {
222
326
  const prev = currentByProvider.get(next.provider);
223
327
  if (prev === void 0 || !options.preserveDirty || !draftDirty(prev)) return next;
224
- conflicted.push(next.provider);
328
+ const modelsMerge = mergeModelsById({
329
+ prevModels: prev.models,
330
+ prevOriginal: prev.originalModels,
331
+ incomingModels: next.models,
332
+ incomingOriginal: next.originalModels
333
+ });
334
+ const compatMerge = mergeCompat({
335
+ prev: prev.compat,
336
+ prevOriginal: prev.originalCompat,
337
+ incoming: next.compat,
338
+ incomingOriginal: next.originalCompat
339
+ });
340
+ if (modelsMerge.conflicted || compatMerge.conflicted) conflicted.push(next.provider);
225
341
  return {
226
- ...prev,
342
+ provider: next.provider,
227
343
  displayName: next.displayName,
228
344
  settingsPath: next.settingsPath,
229
345
  revision: next.revision,
346
+ models: modelsMerge.models,
230
347
  originalModels: cloneModels(next.originalModels),
348
+ compat: compatMerge.compat,
231
349
  originalCompat: cloneObject(next.originalCompat),
232
350
  compatPresent: next.compatPresent
233
351
  };
@@ -670,10 +788,10 @@ window.__ModuleLoader__.load({
670
788
  });
671
789
  }
672
790
  function RouteCard(props) {
673
- const { draft, writable, busy, reloading, t, onChange } = props;
791
+ const { draft, writable, busy, saveLocked, t, onChange } = props;
674
792
  const noModels = draft.models.length === 0;
675
793
  const editDisabled = !writable || busy || noModels;
676
- const saveLocked = editDisabled || reloading;
794
+ const saveDisabled = saveLocked || noModels;
677
795
  const formats = thinkingFormatChoices(props.formats, draft.compat.thinkingFormat);
678
796
  const summary = compatSummary(draft.compat);
679
797
  const sameWire = draft.compat.supportsReasoningEffort === false;
@@ -857,7 +975,7 @@ window.__ModuleLoader__.load({
857
975
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
858
976
  type: "button",
859
977
  className: effort_declare_module_css_default.primaryButton,
860
- disabled: saveLocked || !dirty || clientError !== void 0,
978
+ disabled: saveDisabled || !dirty || clientError !== void 0,
861
979
  onClick: () => {
862
980
  props.onSave(draft);
863
981
  },
@@ -889,7 +1007,7 @@ window.__ModuleLoader__.load({
889
1007
  return;
890
1008
  }
891
1009
  const generation = nextGeneration(generationRef);
892
- setStatus("loading");
1010
+ if (draftsRef.current.length === 0) setStatus("loading");
893
1011
  setError("");
894
1012
  loadDrafts(api, describe, schema).then((result) => {
895
1013
  if (!generationIsCurrent(generationRef, generation)) return;
@@ -928,9 +1046,18 @@ window.__ModuleLoader__.load({
928
1046
  (0, react.useEffect)(() => {
929
1047
  if (props.subscribeInvalidate === void 0) return void 0;
930
1048
  return props.subscribeInvalidate((source) => {
1049
+ if (source === "writable") {
1050
+ const view = describe?.getSnapshot().view;
1051
+ if (view !== void 0) setWritable(view.writable);
1052
+ return;
1053
+ }
931
1054
  if (source === "settings" || source === "directory") reload(true);
932
1055
  });
933
- }, [props.subscribeInvalidate, reload]);
1056
+ }, [
1057
+ describe,
1058
+ props.subscribeInvalidate,
1059
+ reload
1060
+ ]);
934
1061
  const patchNotice = (provider, notice) => {
935
1062
  setNotices((current) => {
936
1063
  const copy = { ...current };
@@ -941,7 +1068,13 @@ window.__ModuleLoader__.load({
941
1068
  };
942
1069
  const save = async (draft) => {
943
1070
  if (api === void 0 || describe === void 0 || schema === void 0) return;
944
- if (status === "loading" || busyRoute !== null) return;
1071
+ if (status === "loading" || busyRoute !== null) {
1072
+ patchNotice(draft.provider, {
1073
+ kind: "error",
1074
+ text: t("saveBusy")
1075
+ });
1076
+ return;
1077
+ }
945
1078
  const blocking = draft.models.map((row) => errorText(modelEffortError(row), t)).find((text) => text !== void 0);
946
1079
  if (blocking !== void 0) {
947
1080
  patchNotice(draft.provider, {
@@ -1067,7 +1200,7 @@ window.__ModuleLoader__.load({
1067
1200
  formats: formats.length > 0 ? formats : [],
1068
1201
  writable,
1069
1202
  busy: busyRoute === draft.provider,
1070
- reloading: status === "loading",
1203
+ saveLocked: !writable || busyRoute !== null || status === "loading",
1071
1204
  notice: notices[draft.provider],
1072
1205
  t,
1073
1206
  onChange: (next) => {
@@ -1104,6 +1237,7 @@ window.__ModuleLoader__.load({
1104
1237
  readOnly: "当前设置为只读,无法保存。",
1105
1238
  save: "保存",
1106
1239
  saving: "保存中…",
1240
+ saveBusy: "另有路由正在保存,请稍候。",
1107
1241
  cancel: "取消",
1108
1242
  saved: "已保存。对话选择器会按新的能力声明显示 Effort 行。",
1109
1243
  conflict: "设置已被其他地方改过,请重新加载后再保存。",
@@ -1146,6 +1280,7 @@ window.__ModuleLoader__.load({
1146
1280
  readOnly: "Settings are read-only; saving is disabled.",
1147
1281
  save: "Save",
1148
1282
  saving: "Saving…",
1283
+ saveBusy: "Another route is saving. Wait, then save this card.",
1149
1284
  cancel: "Cancel",
1150
1285
  saved: "Saved. The composer Effort row follows this capability declaration.",
1151
1286
  conflict: "Settings changed elsewhere. Reload, then save again.",
@@ -1227,7 +1362,7 @@ window.__ModuleLoader__.load({
1227
1362
  };
1228
1363
  const disposers = [
1229
1364
  describe.subscribe(() => {
1230
- emit("settings");
1365
+ emit("writable");
1231
1366
  }),
1232
1367
  ctx.remote.$on("settings/document-updated", (ns) => {
1233
1368
  if (ns !== "llm-pi-ai") return;