dsh-plugin-effort-declare 0.1.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.
Files changed (62) hide show
  1. package/CONTRIBUTING.en.md +57 -0
  2. package/CONTRIBUTING.md +57 -0
  3. package/INSTALL.en.md +62 -0
  4. package/INSTALL.md +62 -0
  5. package/LICENSE +21 -0
  6. package/README.en.md +64 -0
  7. package/README.md +64 -0
  8. package/cordis.patch.yml +5 -0
  9. package/lib/client.js +1237 -0
  10. package/lib/client.js.map +1 -0
  11. package/lib/index.js +6 -0
  12. package/lib/types/client/EffortDeclareSection.d.ts +24 -0
  13. package/lib/types/client/EffortDeclareSection.d.ts.map +1 -0
  14. package/lib/types/client/index.d.ts +15 -0
  15. package/lib/types/client/index.d.ts.map +1 -0
  16. package/lib/types/client/load-drafts.d.ts +20 -0
  17. package/lib/types/client/load-drafts.d.ts.map +1 -0
  18. package/lib/types/client/locales.d.ts +8 -0
  19. package/lib/types/client/locales.d.ts.map +1 -0
  20. package/lib/types/client/schema-ops.d.ts +13 -0
  21. package/lib/types/client/schema-ops.d.ts.map +1 -0
  22. package/lib/types/core/catalog.d.ts +31 -0
  23. package/lib/types/core/catalog.d.ts.map +1 -0
  24. package/lib/types/core/drafts.d.ts +53 -0
  25. package/lib/types/core/drafts.d.ts.map +1 -0
  26. package/lib/types/core/efforts.d.ts +32 -0
  27. package/lib/types/core/efforts.d.ts.map +1 -0
  28. package/lib/types/core/filter.d.ts +28 -0
  29. package/lib/types/core/filter.d.ts.map +1 -0
  30. package/lib/types/core/path-ops.d.ts +40 -0
  31. package/lib/types/core/path-ops.d.ts.map +1 -0
  32. package/lib/types/core/paths.d.ts +16 -0
  33. package/lib/types/core/paths.d.ts.map +1 -0
  34. package/lib/types/core/presets.d.ts +31 -0
  35. package/lib/types/core/presets.d.ts.map +1 -0
  36. package/lib/types/core/validate.d.ts +3 -0
  37. package/lib/types/core/validate.d.ts.map +1 -0
  38. package/lib/types/index.d.ts +10 -0
  39. package/lib/types/index.d.ts.map +1 -0
  40. package/package.json +87 -0
  41. package/src/README.en.md +23 -0
  42. package/src/README.md +23 -0
  43. package/src/client/EffortDeclareSection.tsx +496 -0
  44. package/src/client/README.en.md +27 -0
  45. package/src/client/README.md +27 -0
  46. package/src/client/effort-declare.module.css +278 -0
  47. package/src/client/index.ts +97 -0
  48. package/src/client/load-drafts.ts +83 -0
  49. package/src/client/locales.ts +133 -0
  50. package/src/client/schema-ops.ts +20 -0
  51. package/src/core/README.en.md +27 -0
  52. package/src/core/README.md +27 -0
  53. package/src/core/catalog.ts +61 -0
  54. package/src/core/drafts.ts +137 -0
  55. package/src/core/efforts.ts +119 -0
  56. package/src/core/filter.ts +82 -0
  57. package/src/core/path-ops.ts +65 -0
  58. package/src/core/paths.ts +45 -0
  59. package/src/core/presets.ts +104 -0
  60. package/src/core/validate.ts +7 -0
  61. package/src/css-modules.d.ts +6 -0
  62. package/src/index.ts +11 -0
package/lib/client.js ADDED
@@ -0,0 +1,1237 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-plugin-effort-declare",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/core/catalog.ts
10
+ /**
11
+ * Canonical thinking levels and openai-completions thinkingFormat names.
12
+ *
13
+ * Levels match `@deepseek-ai/dsh-llm-pi-ai` catalog.ts `THINKING_LEVELS`.
14
+ * Formats match `SUPPORTED_THINKING_FORMATS` in the same file (rc.8).
15
+ * Runtime UI prefers the live settings schema union; these lists are the
16
+ * fallback plus a test pin so a silent drift is a failing test, not a
17
+ * second hand-maintained copy nobody notices.
18
+ */
19
+ /** Selectable reasoning levels, in pi-ai escalation order. */
20
+ const THINKING_LEVELS = [
21
+ "off",
22
+ "minimal",
23
+ "low",
24
+ "medium",
25
+ "high",
26
+ "xhigh",
27
+ "max"
28
+ ];
29
+ /** Thinking levels other than Off; Off has its own tri-state control. */
30
+ const THINKING_LEVELS_WITHOUT_OFF = THINKING_LEVELS.filter((level) => level !== "off");
31
+ /**
32
+ * openai-completions thinkingFormat values from llm-pi-ai catalog.ts rc.8.
33
+ * Tests pin this list; the settings page prefers schema union choices.
34
+ */
35
+ const FALLBACK_THINKING_FORMATS = [
36
+ "openai",
37
+ "deepseek",
38
+ "openrouter",
39
+ "together",
40
+ "zai",
41
+ "qwen",
42
+ "chat-template",
43
+ "qwen-chat-template",
44
+ "string-thinking",
45
+ "ant-ling"
46
+ ];
47
+ /** Wire protocol this plugin's v1 editor supports. */
48
+ const OPENAI_COMPLETIONS = "openai-completions";
49
+ /** Settings namespace this plugin writes. */
50
+ const LLM_PI_AI_NS = "llm-pi-ai";
51
+ /** Any route key walks a dict schema to the same profile node. */
52
+ const SCHEMA_PROBE_ROUTE = "\0probe";
53
+ //#endregion
54
+ //#region src/core/paths.ts
55
+ /**
56
+ * Nested get/has over plain JSON, matching ui-settings SettingsSchemaService
57
+ * for the paths this plugin needs. Kept in-repo so tests do not import a
58
+ * Cordis service.
59
+ */
60
+ /** Read a nested value by string keys / array indexes. */
61
+ function getPath(value, path) {
62
+ let current = value;
63
+ for (const key of path) {
64
+ if (Array.isArray(current)) {
65
+ current = current[Number(key)];
66
+ continue;
67
+ }
68
+ if (typeof current !== "object" || current === null) return void 0;
69
+ current = current[key];
70
+ }
71
+ return current;
72
+ }
73
+ /** Plain object (not array, not null). */
74
+ function isPlainObject(value) {
75
+ return typeof value === "object" && value !== null && !Array.isArray(value);
76
+ }
77
+ /** Structured clone of a JSON object, or `{}` when the source is not one. */
78
+ function cloneObject(value) {
79
+ return isPlainObject(value) ? structuredClone(value) : {};
80
+ }
81
+ /** Structured clone of a models array; non-arrays and non-object rows are skipped. */
82
+ function cloneModels(value) {
83
+ if (!Array.isArray(value)) return [];
84
+ return value.flatMap((row) => isPlainObject(row) ? [structuredClone(row)] : []);
85
+ }
86
+ //#endregion
87
+ //#region src/core/filter.ts
88
+ /**
89
+ * Resolve the wire protocol for a route.
90
+ * User/effective `api` wins; then schema default; then openai-completions.
91
+ */
92
+ function resolveRouteApi(profile, schemaDefault) {
93
+ if (isPlainObject(profile) && typeof profile.api === "string" && profile.api.length > 0) return profile.api;
94
+ if (schemaDefault !== void 0 && schemaDefault.length > 0) return schemaDefault;
95
+ return OPENAI_COMPLETIONS;
96
+ }
97
+ /**
98
+ * v1 editable routes: hand-declared llm-pi-ai openai-completions.
99
+ * Catalog routes are excluded (writing `models` replaces the whole catalog).
100
+ */
101
+ function classifyRoute(entry, profile, schemaDefaultApi) {
102
+ if (entry.provider === "deepseek-official" || entry.settingsNs === "llm-deepseek") return {
103
+ editable: false,
104
+ reason: "official-deepseek"
105
+ };
106
+ if (entry.settingsNs !== "llm-pi-ai") return {
107
+ editable: false,
108
+ reason: "not-pi-ai"
109
+ };
110
+ if (entry.declared !== true) return {
111
+ editable: false,
112
+ reason: "catalog"
113
+ };
114
+ if (resolveRouteApi(profile, schemaDefaultApi) !== "openai-completions") return {
115
+ editable: false,
116
+ reason: "not-completions"
117
+ };
118
+ return { editable: true };
119
+ }
120
+ /** Profile object at `providers.<route>` from an llm-pi-ai section value. */
121
+ function profileAt(section, settingsPath, provider) {
122
+ return getPath(section, settingsPath !== void 0 && settingsPath.length > 0 ? settingsPath : ["providers", provider]);
123
+ }
124
+ /** String choices from a schemastery union node (`type: 'union'`). */
125
+ function unionStringChoices(node) {
126
+ if (!isPlainObject(node) || node.type !== "union" || !Array.isArray(node.list)) return [];
127
+ return node.list.map((entry) => isPlainObject(entry) ? entry.value : void 0).filter((value) => typeof value === "string");
128
+ }
129
+ //#endregion
130
+ //#region src/core/path-ops.ts
131
+ /**
132
+ * The minimal path ops carrying `after` over `before`.
133
+ * @param base - path of the edited subtree.
134
+ * @param before - subtree as loaded, or undefined when new.
135
+ * @param after - subtree as edited.
136
+ */
137
+ function pathOps(base, before, after) {
138
+ const previous = typeof before === "object" && before !== null && !Array.isArray(before) ? before : {};
139
+ const ops = [];
140
+ for (const [key, value] of Object.entries(after)) {
141
+ if (JSON.stringify(previous[key]) === JSON.stringify(value)) continue;
142
+ ops.push({
143
+ op: "set",
144
+ path: [...base, key],
145
+ value
146
+ });
147
+ }
148
+ for (const key of Object.keys(previous)) if (!(key in after)) ops.push({
149
+ op: "unset",
150
+ path: [...base, key]
151
+ });
152
+ return ops;
153
+ }
154
+ /**
155
+ * Ops for one route: whole-array `models` replace when the table changed,
156
+ * plus one-level compat path ops. Never replace the `llm-pi-ai` section.
157
+ */
158
+ function buildSaveOps(slices) {
159
+ const base = [...slices.settingsPath];
160
+ const ops = [];
161
+ if (JSON.stringify(slices.beforeModels) !== JSON.stringify(slices.afterModels)) ops.push({
162
+ op: "set",
163
+ path: [...base, "models"],
164
+ value: slices.afterModels
165
+ });
166
+ if (slices.afterCompat !== void 0) ops.push(...pathOps([...base, "compat"], slices.beforeCompat, slices.afterCompat));
167
+ return ops;
168
+ }
169
+ //#endregion
170
+ //#region src/core/drafts.ts
171
+ /**
172
+ * Route-card draft merge: user-layer slices, namespace revision, dirty preserve.
173
+ * No React — settings UI and tests share these helpers.
174
+ */
175
+ /** Build a draft from the stored user subtree (never from effective `value`). */
176
+ function routeDraftFromUserProfile(args) {
177
+ const { provider, displayName, settingsPath, revision, userProfile } = args;
178
+ const models = cloneModels(isPlainObject(userProfile) ? userProfile.models : []);
179
+ const compatPresent = isPlainObject(userProfile) && "compat" in userProfile && isPlainObject(userProfile.compat);
180
+ const compat = cloneObject(isPlainObject(userProfile) ? userProfile.compat : {});
181
+ return {
182
+ provider,
183
+ displayName,
184
+ settingsPath: [...settingsPath],
185
+ revision,
186
+ models,
187
+ originalModels: cloneModels(models),
188
+ compat,
189
+ originalCompat: cloneObject(compat),
190
+ compatPresent
191
+ };
192
+ }
193
+ /** Dirty iff pathOps would emit something (same comparison as save). */
194
+ function draftDirty(draft) {
195
+ return buildSaveOps({
196
+ settingsPath: draft.settingsPath,
197
+ beforeModels: draft.originalModels,
198
+ afterModels: draft.models,
199
+ beforeCompat: draft.compatPresent ? draft.originalCompat : void 0,
200
+ afterCompat: draft.compat
201
+ }).length > 0;
202
+ }
203
+ /** Treat the current models/compat as the committed originals (no-op save). */
204
+ function alignDraft(draft) {
205
+ return {
206
+ ...draft,
207
+ originalModels: cloneModels(draft.models),
208
+ originalCompat: cloneObject(draft.compat),
209
+ compatPresent: Object.keys(draft.compat).length > 0 ? true : draft.compatPresent
210
+ };
211
+ }
212
+ /** Fold a successful mutate view: saved card realigns; every card gets the new revision. */
213
+ function applySaveSuccess(drafts, savedProvider, slice) {
214
+ return drafts.map((draft) => {
215
+ if (draft.provider !== savedProvider) return {
216
+ ...draft,
217
+ revision: slice.revision
218
+ };
219
+ const userProfile = profileAt(slice.user, draft.settingsPath, draft.provider);
220
+ return routeDraftFromUserProfile({
221
+ provider: draft.provider,
222
+ displayName: draft.displayName,
223
+ settingsPath: draft.settingsPath,
224
+ revision: slice.revision,
225
+ userProfile
226
+ });
227
+ });
228
+ }
229
+ /**
230
+ * Apply a freshly loaded table. Dirty cards keep models/compat; originals and
231
+ * revision follow the incoming snapshot so a later save is against the new user layer.
232
+ */
233
+ function mergeLoadedDrafts(current, incoming, options) {
234
+ const currentByProvider = new Map(current.map((draft) => [draft.provider, draft]));
235
+ const conflicted = [];
236
+ return {
237
+ drafts: incoming.map((next) => {
238
+ const prev = currentByProvider.get(next.provider);
239
+ if (prev === void 0 || !options.preserveDirty || !draftDirty(prev)) return next;
240
+ conflicted.push(next.provider);
241
+ return {
242
+ ...prev,
243
+ displayName: next.displayName,
244
+ settingsPath: next.settingsPath,
245
+ revision: next.revision,
246
+ originalModels: cloneModels(next.originalModels),
247
+ originalCompat: cloneObject(next.originalCompat),
248
+ compatPresent: next.compatPresent
249
+ };
250
+ }),
251
+ conflicted
252
+ };
253
+ }
254
+ /** Increment a generation counter; callers discard stale async settlements. */
255
+ function nextGeneration(holder) {
256
+ holder.current += 1;
257
+ return holder.current;
258
+ }
259
+ /** Whether `generation` is still the latest issued token. */
260
+ function generationIsCurrent(holder, generation) {
261
+ return holder.current === generation;
262
+ }
263
+ /** Keep a stored thinkingFormat visible even if the schema union omitted it. */
264
+ function thinkingFormatChoices(formats, current) {
265
+ if (typeof current === "string" && current.length > 0 && !formats.includes(current)) return [current, ...formats];
266
+ return [...formats];
267
+ }
268
+ //#endregion
269
+ //#region src/core/efforts.ts
270
+ /** Read Off's three states from a stored dict. */
271
+ function readOff(efforts) {
272
+ if (efforts === void 0 || !("off" in efforts) || efforts.off === void 0) return {
273
+ mode: "absent",
274
+ value: "none"
275
+ };
276
+ if (efforts.off === null) return {
277
+ mode: "empty",
278
+ value: "none"
279
+ };
280
+ return {
281
+ mode: "value",
282
+ value: efforts.off
283
+ };
284
+ }
285
+ /** Write Off's three states onto a dict (does not mutate the input). */
286
+ function writeOff(efforts, mode, value) {
287
+ const next = { ...efforts };
288
+ delete next.off;
289
+ if (mode === "empty") next.off = null;
290
+ else if (mode === "value") next.off = value.trim().length > 0 ? value : "none";
291
+ return next;
292
+ }
293
+ /** Whether a thinking level (other than Off) is currently declared. */
294
+ function hasLevel(efforts, level) {
295
+ return efforts !== void 0 && efforts[level] !== void 0 && efforts[level] !== null;
296
+ }
297
+ /** Toggle a non-off level. New levels default the wire spelling to the key. */
298
+ function toggleLevel(efforts, level, enabled, wire) {
299
+ const next = { ...efforts };
300
+ if (enabled) next[level] = wire !== void 0 && wire.length > 0 ? wire : level;
301
+ else delete next[level];
302
+ return next;
303
+ }
304
+ /** Set the wire spelling for a declared non-off level. */
305
+ function setWireSpelling(efforts, level, wire) {
306
+ if (!hasLevel(efforts, level)) return efforts;
307
+ return {
308
+ ...efforts,
309
+ [level]: wire
310
+ };
311
+ }
312
+ /**
313
+ * Validate a reasoningEfforts field. Returns an error code; never throws.
314
+ * Absence / `false` are valid (default-off / catalog strip). Empty dict and
315
+ * off-only are rejected by the official resolver.
316
+ */
317
+ function validateReasoningEfforts(efforts) {
318
+ if (efforts === void 0 || efforts === false) return void 0;
319
+ if (efforts === null || typeof efforts === "object" && !Array.isArray(efforts) && Object.keys(efforts).length === 0) return "empty";
320
+ if (typeof efforts !== "object" || Array.isArray(efforts)) return "empty";
321
+ const record = efforts;
322
+ const known = new Set(THINKING_LEVELS);
323
+ for (const key of Object.keys(record)) if (!known.has(key)) return "bad-wire";
324
+ const declared = THINKING_LEVELS.flatMap((level) => {
325
+ if (!(level in record) || record[level] === void 0) return [];
326
+ return [[level, record[level]]];
327
+ });
328
+ for (const [level, wire] of declared) if (wire === null) {
329
+ if (level !== "off") return "bad-wire";
330
+ } else if (typeof wire !== "string" || wire.length === 0) return "bad-wire";
331
+ if (!declared.some(([level]) => level !== "off")) return "off-only";
332
+ }
333
+ /** Drop reasoningEfforts from a model row; keep every other field. */
334
+ function clearReasoningEfforts(row) {
335
+ const next = { ...row };
336
+ delete next.reasoningEfforts;
337
+ return next;
338
+ }
339
+ /** Read reasoningEfforts from a model row. `false` is treated as absent for the editor. */
340
+ function readEfforts(row) {
341
+ const value = row.reasoningEfforts;
342
+ if (value === void 0 || value === false || value === null) return void 0;
343
+ if (typeof value !== "object" || Array.isArray(value)) return void 0;
344
+ return value;
345
+ }
346
+ /** Put a dict (or omit the field) onto a spread row. */
347
+ function writeEfforts(row, efforts) {
348
+ const next = { ...row };
349
+ if (efforts === void 0 || Object.keys(efforts).length === 0) {
350
+ delete next.reasoningEfforts;
351
+ return next;
352
+ }
353
+ next.reasoningEfforts = efforts;
354
+ return next;
355
+ }
356
+ const PRESETS = {
357
+ deepseek: {
358
+ id: "deepseek",
359
+ efforts: {
360
+ off: null,
361
+ low: "low",
362
+ high: "high",
363
+ max: "max"
364
+ },
365
+ compat: {
366
+ thinkingFormat: "set",
367
+ thinkingFormatValue: "deepseek",
368
+ supportsDeveloperRole: "set-false",
369
+ supportsReasoningEffort: "unset"
370
+ },
371
+ warnSameWire: false
372
+ },
373
+ openai: {
374
+ id: "openai",
375
+ efforts: {
376
+ minimal: "minimal",
377
+ low: "low",
378
+ medium: "medium",
379
+ high: "high"
380
+ },
381
+ compat: {
382
+ thinkingFormat: "unset",
383
+ supportsDeveloperRole: "unset",
384
+ supportsReasoningEffort: "unset"
385
+ },
386
+ warnSameWire: false
387
+ },
388
+ toggle: {
389
+ id: "toggle",
390
+ efforts: {
391
+ off: null,
392
+ high: "high"
393
+ },
394
+ compat: {
395
+ thinkingFormat: "unset",
396
+ supportsDeveloperRole: "unset",
397
+ supportsReasoningEffort: "set-false"
398
+ },
399
+ warnSameWire: true
400
+ }
401
+ };
402
+ /** Apply a preset's efforts onto every model row (spread, keep other fields). */
403
+ function applyPresetEfforts(models, preset) {
404
+ return models.map((row) => ({
405
+ ...row,
406
+ reasoningEfforts: { ...preset.efforts }
407
+ }));
408
+ }
409
+ /** Merge a preset's compat patch onto the current route-level compat object. */
410
+ function applyPresetCompat(compat, preset) {
411
+ const next = { ...compat };
412
+ const { compat: patch } = preset;
413
+ if (patch.thinkingFormat === "unset") delete next.thinkingFormat;
414
+ else if (patch.thinkingFormat === "set" && patch.thinkingFormatValue !== void 0) next.thinkingFormat = patch.thinkingFormatValue;
415
+ if (patch.supportsDeveloperRole === "unset") delete next.supportsDeveloperRole;
416
+ else if (patch.supportsDeveloperRole === "set-false") next.supportsDeveloperRole = false;
417
+ if (patch.supportsReasoningEffort === "unset") delete next.supportsReasoningEffort;
418
+ else if (patch.supportsReasoningEffort === "set-false") next.supportsReasoningEffort = false;
419
+ return next;
420
+ }
421
+ //#endregion
422
+ //#region src/core/validate.ts
423
+ /** Per-model client-side check used to show errors without throwing. */
424
+ function modelEffortError(row) {
425
+ if (!("reasoningEfforts" in row) || row.reasoningEfforts === void 0) return void 0;
426
+ return validateReasoningEfforts(row.reasoningEfforts);
427
+ }
428
+ //#endregion
429
+ //#region src/client/load-drafts.ts
430
+ function schemaDefaultString(node) {
431
+ if (!isPlainObject(node) || !isPlainObject(node.meta)) return void 0;
432
+ return typeof node.meta.default === "string" ? node.meta.default : void 0;
433
+ }
434
+ /**
435
+ * First paint: `ensure()` (reads only from idle). Never treat ensure as refresh.
436
+ * Callers that must not apply a stale settlement compare generation themselves.
437
+ */
438
+ async function loadDrafts(api, describe, schema) {
439
+ await describe.ensure();
440
+ const mirrored = describe.getSnapshot();
441
+ if (mirrored.view === void 0) return {
442
+ writable: false,
443
+ formats: [...FALLBACK_THINKING_FORMATS],
444
+ drafts: [],
445
+ error: mirrored.error ?? void 0
446
+ };
447
+ const providersResponse = await api.llm.providers({});
448
+ if (!providersResponse.result.ok) return {
449
+ writable: mirrored.view.writable,
450
+ formats: [...FALLBACK_THINKING_FORMATS],
451
+ drafts: [],
452
+ error: providersResponse.result.error.message
453
+ };
454
+ const pi = new Map(mirrored.view.namespaces.map((view) => [view.ns, view])).get(LLM_PI_AI_NS);
455
+ let formats = [...FALLBACK_THINKING_FORMATS];
456
+ let schemaDefaultApi;
457
+ if (pi !== void 0) try {
458
+ const root = schema.rehydrate(pi.schema);
459
+ const fromSchema = unionStringChoices(schema.nodeAtPath(root, [
460
+ "providers",
461
+ SCHEMA_PROBE_ROUTE,
462
+ "compat",
463
+ "thinkingFormat"
464
+ ]));
465
+ if (fromSchema.length > 0) formats = fromSchema;
466
+ schemaDefaultApi = schemaDefaultString(schema.nodeAtPath(root, [
467
+ "providers",
468
+ SCHEMA_PROBE_ROUTE,
469
+ "api"
470
+ ]));
471
+ } catch {}
472
+ const drafts = [];
473
+ for (const entry of providersResponse.result.value.providers) {
474
+ const settingsPath = entry.settingsPath !== void 0 && entry.settingsPath.length > 0 ? [...entry.settingsPath] : ["providers", entry.provider];
475
+ if (!classifyRoute(entry, profileAt(pi?.value, settingsPath, entry.provider), schemaDefaultApi).editable) continue;
476
+ drafts.push(routeDraftFromUserProfile({
477
+ provider: entry.provider,
478
+ displayName: entry.displayName ?? entry.provider,
479
+ settingsPath,
480
+ revision: pi?.revision ?? 0,
481
+ userProfile: profileAt(pi?.user, settingsPath, entry.provider)
482
+ }));
483
+ }
484
+ return {
485
+ writable: mirrored.view.writable,
486
+ formats,
487
+ drafts
488
+ };
489
+ }
490
+ //#endregion
491
+ //#region \0dsh-css:C:\Users\zimo\AppData\Roaming\io.github.hairyf.deepseek-harness-desktop\data\dsh\临时目录\dsh-plugin-effort-declare\src\client\effort-declare.module.css.mjs
492
+ const cssText = ".hYEBUa_section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}.hYEBUa_title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}.hYEBUa_intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}.hYEBUa_notice{color:var(--dsw-alias-state-warn-label);margin:0;font-size:12px;line-height:18px}.hYEBUa_savedNotice{color:var(--dsw-alias-state-success-primary);margin:0;font-size:12px;line-height:18px}.hYEBUa_error{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}.hYEBUa_rows{flex-direction:column;gap:8px;margin:12px 0 0;padding:0;list-style:none;display:flex}.hYEBUa_rowCard{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;flex-direction:column;gap:12px;padding:12px 14px;display:flex}.hYEBUa_rowHead{align-items:baseline;gap:8px;display:flex}.hYEBUa_rowName{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}.hYEBUa_rowTag{border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);border-radius:4px;flex:none;padding:1px 6px;font-size:11px;line-height:16px}.hYEBUa_compatSummary{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}.hYEBUa_presetRow,.hYEBUa_actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.hYEBUa_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}.hYEBUa_primaryButton,.hYEBUa_secondaryButton{box-sizing:border-box;height:36px;font:inherit;cursor:pointer;border-radius:18px;justify-content:center;align-items:center;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.hYEBUa_primaryButton{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border:none}.hYEBUa_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.hYEBUa_secondaryButton{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0}.hYEBUa_secondaryButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.hYEBUa_primaryButton:disabled,.hYEBUa_secondaryButton:disabled,.hYEBUa_linkButton:disabled,.hYEBUa_input:disabled{opacity:.4;cursor:default}.hYEBUa_primaryButton:focus-visible,.hYEBUa_secondaryButton:focus-visible,.hYEBUa_linkButton:focus-visible,.hYEBUa_input:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}.hYEBUa_linkButton{box-sizing:border-box;height:28px;color:var(--dsw-alias-label-tertiary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:14px;align-items:center;padding:0 10px;font-size:12px;line-height:18px;display:inline-flex}.hYEBUa_linkButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}.hYEBUa_modelEntry{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:8px;padding:10px 0;display:flex}.hYEBUa_modelHead{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}.hYEBUa_modelId{font-size:13px;font-weight:500;line-height:20px}.hYEBUa_modelName{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.hYEBUa_levels{flex-wrap:wrap;gap:10px 14px;display:flex}.hYEBUa_level{align-items:center;gap:6px;font-size:12px;line-height:18px;display:inline-flex}.hYEBUa_wireRow{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.hYEBUa_input,.hYEBUa_selectInput{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);height:32px;color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;padding:0 10px;font-size:13px}.hYEBUa_wireInput{width:7em}.hYEBUa_offGroup{flex-direction:column;gap:6px;display:flex}.hYEBUa_advanced{border-top:1px solid var(--dsw-alias-border-l2);padding-top:8px}.hYEBUa_advanced summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}.hYEBUa_advancedBody{flex-direction:column;gap:10px;padding-top:10px;display:flex}.hYEBUa_check{align-items:flex-start;gap:8px;font-size:12px;line-height:18px;display:flex}";
493
+ const cssTagId = "dsh-plugin-effort-declare/effort-declare.module.css";
494
+ var effort_declare_module_css_default = {
495
+ "actions": "hYEBUa_actions",
496
+ "advanced": "hYEBUa_advanced",
497
+ "advancedBody": "hYEBUa_advancedBody",
498
+ "check": "hYEBUa_check",
499
+ "compatSummary": "hYEBUa_compatSummary",
500
+ "error": "hYEBUa_error",
501
+ "fieldLabel": "hYEBUa_fieldLabel",
502
+ "input": "hYEBUa_input",
503
+ "intro": "hYEBUa_intro",
504
+ "level": "hYEBUa_level",
505
+ "levels": "hYEBUa_levels",
506
+ "linkButton": "hYEBUa_linkButton",
507
+ "modelEntry": "hYEBUa_modelEntry",
508
+ "modelHead": "hYEBUa_modelHead",
509
+ "modelId": "hYEBUa_modelId",
510
+ "modelName": "hYEBUa_modelName",
511
+ "notice": "hYEBUa_notice",
512
+ "offGroup": "hYEBUa_offGroup",
513
+ "presetRow": "hYEBUa_presetRow",
514
+ "primaryButton": "hYEBUa_primaryButton",
515
+ "rowCard": "hYEBUa_rowCard",
516
+ "rowHead": "hYEBUa_rowHead",
517
+ "rowName": "hYEBUa_rowName",
518
+ "rowTag": "hYEBUa_rowTag",
519
+ "rows": "hYEBUa_rows",
520
+ "savedNotice": "hYEBUa_savedNotice",
521
+ "secondaryButton": "hYEBUa_secondaryButton",
522
+ "section": "hYEBUa_section",
523
+ "selectInput": "hYEBUa_selectInput",
524
+ "title": "hYEBUa_title",
525
+ "wireInput": "hYEBUa_wireInput",
526
+ "wireRow": "hYEBUa_wireRow"
527
+ };
528
+ //#endregion
529
+ //#region src/client/EffortDeclareSection.tsx
530
+ /**
531
+ * Settings section: per-model reasoningEfforts + openai-completions compat
532
+ * for hand-declared llm-pi-ai routes.
533
+ */
534
+ function compatSummary(compat) {
535
+ const parts = [];
536
+ if (typeof compat.thinkingFormat === "string") parts.push(`thinkingFormat=${compat.thinkingFormat}`);
537
+ if (compat.supportsDeveloperRole === false) parts.push("supportsDeveloperRole=false");
538
+ if (compat.supportsReasoningEffort === false) parts.push("supportsReasoningEffort=false");
539
+ return parts.join(" · ");
540
+ }
541
+ function errorText(code, t) {
542
+ if (code === "empty") return t("errorEmpty");
543
+ if (code === "off-only") return t("errorOffOnly");
544
+ if (code === "bad-wire") return t("errorBadWire");
545
+ }
546
+ function ModelRowEditor(props) {
547
+ const { row, disabled, t, onChange } = props;
548
+ const efforts = readEfforts(row) ?? {};
549
+ const off = readOff(readEfforts(row));
550
+ const id = typeof row.id === "string" ? row.id : "";
551
+ const name = typeof row.name === "string" ? row.name : "";
552
+ const patchEfforts = (next) => {
553
+ onChange(writeEfforts(row, Object.keys(next).length === 0 ? void 0 : next));
554
+ };
555
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
556
+ className: effort_declare_module_css_default.modelEntry,
557
+ children: [
558
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
559
+ className: effort_declare_module_css_default.modelHead,
560
+ children: [
561
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
562
+ className: effort_declare_module_css_default.modelId,
563
+ children: id || t("model")
564
+ }),
565
+ name !== "" && name !== id ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
566
+ className: effort_declare_module_css_default.modelName,
567
+ children: name
568
+ }) : null,
569
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
570
+ type: "button",
571
+ className: effort_declare_module_css_default.linkButton,
572
+ disabled: disabled || !("reasoningEfforts" in row),
573
+ onClick: () => {
574
+ onChange(clearReasoningEfforts(row));
575
+ },
576
+ children: t("clear")
577
+ })
578
+ ]
579
+ }),
580
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
581
+ className: effort_declare_module_css_default.fieldLabel,
582
+ children: t("levels")
583
+ }),
584
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
585
+ className: effort_declare_module_css_default.levels,
586
+ children: THINKING_LEVELS_WITHOUT_OFF.map((level) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
587
+ className: effort_declare_module_css_default.level,
588
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
589
+ type: "checkbox",
590
+ checked: hasLevel(efforts, level),
591
+ disabled,
592
+ onChange: (event) => {
593
+ patchEfforts(toggleLevel(efforts, level, event.target.checked));
594
+ }
595
+ }), level]
596
+ }, level))
597
+ }),
598
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
599
+ className: effort_declare_module_css_default.wireRow,
600
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
601
+ className: effort_declare_module_css_default.fieldLabel,
602
+ children: t("wire")
603
+ }), THINKING_LEVELS_WITHOUT_OFF.filter((level) => hasLevel(efforts, level)).map((level) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
604
+ className: effort_declare_module_css_default.level,
605
+ children: [level, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
606
+ className: `${effort_declare_module_css_default.input} ${effort_declare_module_css_default.wireInput}`,
607
+ type: "text",
608
+ value: efforts[level] ?? level,
609
+ disabled,
610
+ "aria-label": `${t("wire")} ${level}`,
611
+ onChange: (event) => {
612
+ patchEfforts(setWireSpelling(efforts, level, event.target.value));
613
+ }
614
+ })]
615
+ }, level))]
616
+ }),
617
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
618
+ className: effort_declare_module_css_default.offGroup,
619
+ children: [
620
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
621
+ className: effort_declare_module_css_default.fieldLabel,
622
+ children: t("offMode")
623
+ }),
624
+ [
625
+ "absent",
626
+ "empty",
627
+ "value"
628
+ ].map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
629
+ className: effort_declare_module_css_default.level,
630
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
631
+ type: "radio",
632
+ name: props.radioName,
633
+ checked: off.mode === mode,
634
+ disabled,
635
+ onChange: () => {
636
+ patchEfforts(writeOff(efforts, mode, off.value));
637
+ }
638
+ }), mode === "absent" ? t("offAbsent") : mode === "empty" ? t("offEmpty") : t("offValue")]
639
+ }, mode)),
640
+ off.mode === "value" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
641
+ className: `${effort_declare_module_css_default.input} ${effort_declare_module_css_default.wireInput}`,
642
+ type: "text",
643
+ value: off.value,
644
+ placeholder: t("offValuePlaceholder"),
645
+ disabled,
646
+ onChange: (event) => {
647
+ patchEfforts(writeOff(efforts, "value", event.target.value));
648
+ }
649
+ }) : null
650
+ ]
651
+ })
652
+ ]
653
+ });
654
+ }
655
+ function RouteCard(props) {
656
+ const { draft, writable, busy, reloading, t, onChange } = props;
657
+ const noModels = draft.models.length === 0;
658
+ const editDisabled = !writable || busy || noModels;
659
+ const saveLocked = editDisabled || reloading;
660
+ const formats = thinkingFormatChoices(props.formats, draft.compat.thinkingFormat);
661
+ const summary = compatSummary(draft.compat);
662
+ const sameWire = draft.compat.supportsReasoningEffort === false;
663
+ const clientError = draft.models.map((row) => errorText(modelEffortError(row), t)).find((text) => text !== void 0);
664
+ const dirty = draftDirty(draft);
665
+ const applyPreset = (id) => {
666
+ const preset = PRESETS[id];
667
+ onChange({
668
+ ...draft,
669
+ models: applyPresetEfforts(draft.models, preset),
670
+ compat: applyPresetCompat(draft.compat, preset)
671
+ });
672
+ };
673
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
674
+ className: effort_declare_module_css_default.rowCard,
675
+ children: [
676
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
677
+ className: effort_declare_module_css_default.rowHead,
678
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
679
+ className: effort_declare_module_css_default.rowName,
680
+ children: draft.displayName
681
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
682
+ className: effort_declare_module_css_default.rowTag,
683
+ children: draft.provider
684
+ })]
685
+ }),
686
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
687
+ className: effort_declare_module_css_default.compatSummary,
688
+ children: [t("compatSummary"), summary === "" ? "" : `: ${summary}`]
689
+ }),
690
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
691
+ className: effort_declare_module_css_default.presetRow,
692
+ children: [
693
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
694
+ className: effort_declare_module_css_default.fieldLabel,
695
+ children: t("presets")
696
+ }),
697
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
698
+ type: "button",
699
+ className: effort_declare_module_css_default.secondaryButton,
700
+ disabled: editDisabled,
701
+ onClick: () => {
702
+ applyPreset("deepseek");
703
+ },
704
+ children: t("presetDeepSeek")
705
+ }),
706
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
707
+ type: "button",
708
+ className: effort_declare_module_css_default.secondaryButton,
709
+ disabled: editDisabled,
710
+ onClick: () => {
711
+ applyPreset("openai");
712
+ },
713
+ children: t("presetOpenAI")
714
+ }),
715
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
716
+ type: "button",
717
+ className: effort_declare_module_css_default.secondaryButton,
718
+ disabled: editDisabled,
719
+ onClick: () => {
720
+ applyPreset("toggle");
721
+ },
722
+ children: t("presetToggle")
723
+ })
724
+ ]
725
+ }),
726
+ sameWire ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
727
+ className: effort_declare_module_css_default.notice,
728
+ children: t("presetToggleWarn")
729
+ }) : null,
730
+ noModels ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
731
+ className: effort_declare_module_css_default.notice,
732
+ children: t("noModels")
733
+ }) : null,
734
+ draft.models.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRowEditor, {
735
+ row,
736
+ index,
737
+ radioName: `off-${draft.provider}-${String(index)}`,
738
+ disabled: editDisabled,
739
+ t,
740
+ onChange: (next) => {
741
+ const models = draft.models.map((candidate, at) => at === index ? next : candidate);
742
+ onChange({
743
+ ...draft,
744
+ models
745
+ });
746
+ }
747
+ }, `${String(row.id)}-${String(index)}`)),
748
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
749
+ className: effort_declare_module_css_default.advanced,
750
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("advanced") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
751
+ className: effort_declare_module_css_default.advancedBody,
752
+ children: [
753
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
754
+ className: effort_declare_module_css_default.fieldLabel,
755
+ children: [t("thinkingFormat"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
756
+ className: `${effort_declare_module_css_default.input} ${effort_declare_module_css_default.selectInput}`,
757
+ value: typeof draft.compat.thinkingFormat === "string" ? draft.compat.thinkingFormat : "",
758
+ disabled: editDisabled,
759
+ onChange: (event) => {
760
+ const compat = { ...draft.compat };
761
+ if (event.target.value === "") delete compat.thinkingFormat;
762
+ else compat.thinkingFormat = event.target.value;
763
+ onChange({
764
+ ...draft,
765
+ compat
766
+ });
767
+ },
768
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
769
+ value: "",
770
+ children: t("thinkingFormatDefault")
771
+ }), formats.map((format) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
772
+ value: format,
773
+ children: format
774
+ }, format))]
775
+ })]
776
+ }),
777
+ draft.compat.supportsDeveloperRole === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
778
+ className: effort_declare_module_css_default.notice,
779
+ children: t("developerTrueHint")
780
+ }) : null,
781
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
782
+ className: effort_declare_module_css_default.check,
783
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
784
+ type: "checkbox",
785
+ checked: draft.compat.supportsDeveloperRole === false,
786
+ disabled: editDisabled,
787
+ onChange: (event) => {
788
+ const compat = { ...draft.compat };
789
+ if (event.target.checked) compat.supportsDeveloperRole = false;
790
+ else delete compat.supportsDeveloperRole;
791
+ onChange({
792
+ ...draft,
793
+ compat
794
+ });
795
+ }
796
+ }), t("supportsDeveloperRole")]
797
+ }),
798
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
799
+ className: effort_declare_module_css_default.check,
800
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
801
+ type: "checkbox",
802
+ checked: draft.compat.supportsReasoningEffort === false,
803
+ disabled: editDisabled,
804
+ onChange: (event) => {
805
+ const compat = { ...draft.compat };
806
+ if (event.target.checked) compat.supportsReasoningEffort = false;
807
+ else delete compat.supportsReasoningEffort;
808
+ onChange({
809
+ ...draft,
810
+ compat
811
+ });
812
+ }
813
+ }), t("supportsReasoningEffort")]
814
+ })
815
+ ]
816
+ })]
817
+ }),
818
+ clientError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
819
+ className: effort_declare_module_css_default.error,
820
+ children: clientError
821
+ }) : null,
822
+ props.notice?.kind === "saved" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
823
+ className: effort_declare_module_css_default.savedNotice,
824
+ children: props.notice.text
825
+ }) : null,
826
+ props.notice?.kind === "conflict" || props.notice?.kind === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
827
+ className: effort_declare_module_css_default.error,
828
+ children: props.notice.text
829
+ }) : null,
830
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
831
+ className: effort_declare_module_css_default.actions,
832
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
833
+ type: "button",
834
+ className: effort_declare_module_css_default.secondaryButton,
835
+ disabled: busy || !dirty,
836
+ onClick: () => {
837
+ props.onCancel(draft);
838
+ },
839
+ children: t("cancel")
840
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
841
+ type: "button",
842
+ className: effort_declare_module_css_default.primaryButton,
843
+ disabled: saveLocked || !dirty || clientError !== void 0,
844
+ onClick: () => {
845
+ props.onSave(draft);
846
+ },
847
+ children: busy ? t("saving") : t("save")
848
+ })]
849
+ })
850
+ ]
851
+ });
852
+ }
853
+ function EffortDeclareSection(props) {
854
+ const t = props.t;
855
+ const api = props.api;
856
+ const describe = props.describe;
857
+ const schema = props.schema;
858
+ const [status, setStatus] = (0, react.useState)("loading");
859
+ const [error, setError] = (0, react.useState)("");
860
+ const [writable, setWritable] = (0, react.useState)(false);
861
+ const [formats, setFormats] = (0, react.useState)([]);
862
+ const [drafts, setDrafts] = (0, react.useState)([]);
863
+ const [busyRoute, setBusyRoute] = (0, react.useState)(null);
864
+ const [notices, setNotices] = (0, react.useState)({});
865
+ const generationRef = (0, react.useRef)(0);
866
+ const draftsRef = (0, react.useRef)(drafts);
867
+ draftsRef.current = drafts;
868
+ const reload = (0, react.useCallback)((preserveDirty) => {
869
+ if (api === void 0 || describe === void 0 || schema === void 0) {
870
+ setStatus("error");
871
+ setError(t("loadError"));
872
+ return;
873
+ }
874
+ const generation = nextGeneration(generationRef);
875
+ setStatus("loading");
876
+ setError("");
877
+ loadDrafts(api, describe, schema).then((result) => {
878
+ if (!generationIsCurrent(generationRef, generation)) return;
879
+ setWritable(result.writable);
880
+ if (result.formats.length > 0) setFormats(result.formats);
881
+ if (result.error !== void 0) {
882
+ setStatus("error");
883
+ setError(result.error);
884
+ return;
885
+ }
886
+ const merged = mergeLoadedDrafts(draftsRef.current, result.drafts, { preserveDirty });
887
+ setDrafts(merged.drafts);
888
+ if (merged.conflicted.length > 0) setNotices(Object.fromEntries(merged.conflicted.map((provider) => [provider, {
889
+ kind: "conflict",
890
+ text: t("dirtyConflict")
891
+ }])));
892
+ setStatus("ready");
893
+ }, (failure) => {
894
+ if (!generationIsCurrent(generationRef, generation)) return;
895
+ setStatus("error");
896
+ setError(failure instanceof Error ? failure.message : t("loadError"));
897
+ });
898
+ }, [
899
+ api,
900
+ describe,
901
+ schema,
902
+ t
903
+ ]);
904
+ (0, react.useEffect)(() => {
905
+ reload(false);
906
+ }, [reload]);
907
+ (0, react.useEffect)(() => {
908
+ if (props.subscribeInvalidate === void 0) return void 0;
909
+ return props.subscribeInvalidate(() => {
910
+ reload(true);
911
+ });
912
+ }, [props.subscribeInvalidate, reload]);
913
+ const save = async (draft) => {
914
+ if (api === void 0 || describe === void 0) return;
915
+ if (status === "loading" || busyRoute !== null) return;
916
+ const blocking = draft.models.map((row) => errorText(modelEffortError(row), t)).find((text) => text !== void 0);
917
+ if (blocking !== void 0) {
918
+ setNotices({ [draft.provider]: {
919
+ kind: "error",
920
+ text: blocking
921
+ } });
922
+ return;
923
+ }
924
+ setBusyRoute(draft.provider);
925
+ setNotices({});
926
+ try {
927
+ const ops = buildSaveOps({
928
+ settingsPath: draft.settingsPath,
929
+ beforeModels: draft.originalModels,
930
+ afterModels: draft.models,
931
+ beforeCompat: draft.compatPresent ? draft.originalCompat : void 0,
932
+ afterCompat: draft.compat
933
+ });
934
+ if (ops.length === 0) {
935
+ setDrafts((current) => current.map((row) => row.provider === draft.provider ? alignDraft(row) : row));
936
+ return;
937
+ }
938
+ const response = await api.settings.mutate({
939
+ ns: LLM_PI_AI_NS,
940
+ ops,
941
+ expectedRevision: draft.revision
942
+ });
943
+ if (!response.result.ok) {
944
+ setNotices({ [draft.provider]: {
945
+ kind: "error",
946
+ text: response.result.error.code === "settings-conflict" ? t("conflict") : response.result.error.message
947
+ } });
948
+ return;
949
+ }
950
+ const view = response.result.value;
951
+ describe.acceptView(view);
952
+ setDrafts((current) => applySaveSuccess(current, draft.provider, {
953
+ user: view.user ?? {},
954
+ revision: view.revision
955
+ }));
956
+ setNotices({ [draft.provider]: {
957
+ kind: "saved",
958
+ text: t("saved")
959
+ } });
960
+ } catch (failure) {
961
+ setNotices({ [draft.provider]: {
962
+ kind: "error",
963
+ text: failure instanceof Error ? failure.message : t("loadError")
964
+ } });
965
+ } finally {
966
+ setBusyRoute(null);
967
+ }
968
+ };
969
+ const showLoading = status === "loading" && drafts.length === 0;
970
+ const showEmpty = status === "ready" && drafts.length === 0;
971
+ const showList = drafts.length > 0;
972
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
973
+ className: effort_declare_module_css_default.section,
974
+ children: [
975
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
976
+ className: effort_declare_module_css_default.title,
977
+ children: t("title")
978
+ }),
979
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
980
+ className: effort_declare_module_css_default.intro,
981
+ children: t("intro")
982
+ }),
983
+ !writable && status === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
984
+ className: effort_declare_module_css_default.notice,
985
+ children: t("readOnly")
986
+ }) : null,
987
+ showLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
988
+ className: effort_declare_module_css_default.intro,
989
+ children: t("loading")
990
+ }) : null,
991
+ status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
992
+ className: effort_declare_module_css_default.error,
993
+ children: error
994
+ }) : null,
995
+ status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
996
+ type: "button",
997
+ className: effort_declare_module_css_default.secondaryButton,
998
+ onClick: () => {
999
+ reload(true);
1000
+ },
1001
+ children: t("reload")
1002
+ }) : null,
1003
+ showEmpty ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1004
+ className: effort_declare_module_css_default.notice,
1005
+ children: t("empty")
1006
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1007
+ className: effort_declare_module_css_default.intro,
1008
+ children: t("emptyHint")
1009
+ })] }) : null,
1010
+ showList ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1011
+ className: effort_declare_module_css_default.rows,
1012
+ children: drafts.map((draft) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouteCard, {
1013
+ draft,
1014
+ formats: formats.length > 0 ? formats : [],
1015
+ writable,
1016
+ busy: busyRoute === draft.provider,
1017
+ reloading: status === "loading",
1018
+ notice: notices[draft.provider],
1019
+ t,
1020
+ onChange: (next) => {
1021
+ setNotices((current) => {
1022
+ const copy = { ...current };
1023
+ delete copy[next.provider];
1024
+ return copy;
1025
+ });
1026
+ setDrafts((current) => current.map((row) => row.provider === next.provider ? next : row));
1027
+ },
1028
+ onSave: (next) => {
1029
+ save(next);
1030
+ },
1031
+ onCancel: (next) => {
1032
+ setNotices((current) => {
1033
+ const copy = { ...current };
1034
+ delete copy[next.provider];
1035
+ return copy;
1036
+ });
1037
+ setDrafts((current) => current.map((row) => row.provider === next.provider ? {
1038
+ ...row,
1039
+ models: cloneModels(row.originalModels),
1040
+ compat: cloneObject(row.originalCompat)
1041
+ } : row));
1042
+ }
1043
+ }, draft.provider))
1044
+ }) : null
1045
+ ]
1046
+ });
1047
+ }
1048
+ //#endregion
1049
+ //#region src/client/schema-ops.ts
1050
+ /** Wrap a live settingsSchema service as plain callbacks. */
1051
+ function bindSchema(service) {
1052
+ return {
1053
+ rehydrate: (serialized) => service.rehydrate(serialized),
1054
+ nodeAtPath: (root, path) => service.nodeAtPath(root, path),
1055
+ getPath: (value, path) => service.getPath(value, path),
1056
+ hasPath: (value, path) => service.hasPath(value, path)
1057
+ };
1058
+ }
1059
+ //#endregion
1060
+ //#region src/client/locales.ts
1061
+ const NS = "plugin-effort-declare";
1062
+ const zh = {
1063
+ nav: "推理档位",
1064
+ title: "推理档位声明",
1065
+ intro: "本页声明「这模型能选哪些推理档」。对话里的档位选择仍在输入框模型菜单。没声明 = 和现在一样没有 Effort 行。不会改密钥、目录或当次选择。",
1066
+ empty: "没有可编辑的手工 openai-completions 路由。",
1067
+ emptyHint: "请先到「模型」页添加提供方(llm-pi-ai 手工路由)。官方 DeepSeek 与 catalog 路由不在本页编辑。需要 DSH ≥ 0.1.0-rc.8(providers.declared)。",
1068
+ loadError: "无法加载提供方或设置。",
1069
+ loading: "正在加载…",
1070
+ readOnly: "当前设置为只读,无法保存。",
1071
+ save: "保存",
1072
+ saving: "保存中…",
1073
+ cancel: "取消",
1074
+ saved: "已保存。对话选择器会按新的能力声明显示 Effort 行。",
1075
+ conflict: "设置已被其他地方改过,请重新加载后再保存。",
1076
+ dirtyConflict: "此路由有未保存修改,设置已在其他地方更新。保存会写到最新文档上,或先取消再改。",
1077
+ presets: "预设",
1078
+ presetDeepSeek: "DeepSeek 兼容",
1079
+ presetOpenAI: "OpenAI 兼容",
1080
+ presetToggle: "仅开/关",
1081
+ presetToggleWarn: "选择器里多档在线上没有区别:除 Off 外请求体相同。",
1082
+ model: "模型",
1083
+ levels: "可选档位",
1084
+ wire: "线上拼写",
1085
+ offMode: "Off",
1086
+ offAbsent: "无 Off",
1087
+ offEmpty: "Off 且不发参数",
1088
+ offValue: "Off 且发字符串",
1089
+ offValuePlaceholder: "none",
1090
+ clear: "清除本模型声明",
1091
+ advanced: "高级:协议方言",
1092
+ thinkingFormat: "thinkingFormat",
1093
+ thinkingFormatDefault: "默认(openai)",
1094
+ supportsDeveloperRole: "系统提示走 system 而不是 developer(supportsDeveloperRole: false)",
1095
+ supportsReasoningEffort: "不发 reasoning_effort,只发开关(supportsReasoningEffort: false)",
1096
+ developerTrueHint: "当前文档是 supportsDeveloperRole: true。v1 只能强制 false 或缺席;勾选会写成 false,取消勾选会删除该键。",
1097
+ compatSummary: "协议",
1098
+ errorEmpty: "不能保存空的 reasoningEfforts。请选择档位,或清除声明。",
1099
+ errorOffOnly: "不能只开 Off,必须再开一档思考档。",
1100
+ errorBadWire: "思考档必须填写线上拼写;只有 Off 可以留空。不能使用未知档位键。",
1101
+ noModels: "这条路由还没有 models 列表。请先到「模型」页添加模型。",
1102
+ reload: "重新加载"
1103
+ };
1104
+ const en = {
1105
+ nav: "Reasoning efforts",
1106
+ title: "Reasoning effort declarations",
1107
+ intro: "This page declares which reasoning levels a model can offer. The per-turn choice stays in the composer model menu. Undeclared models keep having no Effort row. Keys, catalogs, and the current selection are not edited here.",
1108
+ empty: "No editable hand-declared openai-completions routes.",
1109
+ emptyHint: "Add a provider on the Models page first (an llm-pi-ai hand-declared route). Official DeepSeek and catalog routes are not edited here. Requires DSH ≥ 0.1.0-rc.8 (providers.declared).",
1110
+ loadError: "Could not load providers or settings.",
1111
+ loading: "Loading…",
1112
+ readOnly: "Settings are read-only; saving is disabled.",
1113
+ save: "Save",
1114
+ saving: "Saving…",
1115
+ cancel: "Cancel",
1116
+ saved: "Saved. The composer Effort row follows this capability declaration.",
1117
+ conflict: "Settings changed elsewhere. Reload, then save again.",
1118
+ dirtyConflict: "This route has unsaved edits and settings changed elsewhere. Save writes onto the latest document, or cancel first.",
1119
+ presets: "Presets",
1120
+ presetDeepSeek: "DeepSeek compatible",
1121
+ presetOpenAI: "OpenAI compatible",
1122
+ presetToggle: "On/off only",
1123
+ presetToggleWarn: "Multiple selector levels are identical on the wire except Off.",
1124
+ model: "Model",
1125
+ levels: "Offered levels",
1126
+ wire: "Wire spelling",
1127
+ offMode: "Off",
1128
+ offAbsent: "No Off",
1129
+ offEmpty: "Off, send nothing",
1130
+ offValue: "Off, send a string",
1131
+ offValuePlaceholder: "none",
1132
+ clear: "Clear this model’s declaration",
1133
+ advanced: "Advanced: protocol dialect",
1134
+ thinkingFormat: "thinkingFormat",
1135
+ thinkingFormatDefault: "Default (openai)",
1136
+ supportsDeveloperRole: "Send system prompts as system, not developer (supportsDeveloperRole: false)",
1137
+ supportsReasoningEffort: "Do not send reasoning_effort; switch only (supportsReasoningEffort: false)",
1138
+ developerTrueHint: "The document has supportsDeveloperRole: true. v1 can only force false or omit the key; checking writes false, unchecking deletes the key.",
1139
+ compatSummary: "Protocol",
1140
+ errorEmpty: "Empty reasoningEfforts cannot be saved. Pick levels, or clear the declaration.",
1141
+ errorOffOnly: "Off alone is not enough; declare at least one thinking level.",
1142
+ errorBadWire: "Thinking levels need a wire spelling; only Off may be empty. Unknown effort keys are rejected.",
1143
+ noModels: "This route has no models list yet. Add models on the Models page first.",
1144
+ reload: "Reload"
1145
+ };
1146
+ //#endregion
1147
+ //#region src/client/index.ts
1148
+ const inject = [
1149
+ "slots",
1150
+ "locale",
1151
+ "connection",
1152
+ "remote",
1153
+ "settingsScope",
1154
+ "settingsSchema"
1155
+ ];
1156
+ const PLUGIN_ID = "dsh-plugin-effort-declare";
1157
+ function mountPluginCss() {
1158
+ if (typeof document === "undefined") return () => {};
1159
+ const selector = `style[data-plugin-css=${JSON.stringify(cssTagId)}]`;
1160
+ let tag = document.querySelector(selector);
1161
+ if (tag === null) {
1162
+ tag = document.createElement("style");
1163
+ tag.dataset.plugin = PLUGIN_ID;
1164
+ tag.dataset.pluginCss = cssTagId;
1165
+ document.head.appendChild(tag);
1166
+ }
1167
+ tag.textContent = cssText;
1168
+ return () => {
1169
+ tag?.remove();
1170
+ };
1171
+ }
1172
+ function apply(ctx) {
1173
+ ctx.effect(() => ctx.locale.register(NS, {
1174
+ zh,
1175
+ en
1176
+ }), `${PLUGIN_ID}: dictionaries`);
1177
+ ctx.effect(() => mountPluginCss(), `${PLUGIN_ID}: css`);
1178
+ const connection = ctx.get("connection");
1179
+ const settingsSchema = ctx.settingsSchema;
1180
+ const schema = bindSchema({
1181
+ rehydrate: (serialized) => settingsSchema.rehydrate(serialized),
1182
+ nodeAtPath: (root, path) => settingsSchema.nodeAtPath(root, path),
1183
+ getPath: (value, path) => settingsSchema.getPath(value, path),
1184
+ hasPath: (value, path) => settingsSchema.hasPath(value, path)
1185
+ });
1186
+ const t = ctx.locale.bind(NS);
1187
+ const describe = ctx.settingsScope.describe();
1188
+ const invalidation = /* @__PURE__ */ new Set();
1189
+ ctx.effect(() => {
1190
+ const emit = (source) => {
1191
+ for (const listener of invalidation) listener(source);
1192
+ };
1193
+ const disposers = [
1194
+ ctx.remote.$on("settings/document-updated", (ns) => {
1195
+ if (ns !== "llm-pi-ai") return;
1196
+ emit("settings");
1197
+ }),
1198
+ ctx.remote.$on("llm/adapters-updated", () => {
1199
+ emit("directory");
1200
+ }),
1201
+ ctx.on("connection/reset", () => {
1202
+ emit("directory");
1203
+ })
1204
+ ];
1205
+ return () => {
1206
+ for (const dispose of disposers) dispose();
1207
+ };
1208
+ }, `${PLUGIN_ID}: invalidations`);
1209
+ const subscribeInvalidate = (listener) => {
1210
+ invalidation.add(listener);
1211
+ return () => {
1212
+ invalidation.delete(listener);
1213
+ };
1214
+ };
1215
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
1216
+ name: "settings.section",
1217
+ id: "effort-declare",
1218
+ order: 15,
1219
+ label: () => t("nav"),
1220
+ locale: NS,
1221
+ inject: () => ({
1222
+ api: connection.api,
1223
+ describe,
1224
+ schema,
1225
+ t,
1226
+ subscribeInvalidate
1227
+ })
1228
+ }, EffortDeclareSection));
1229
+ }
1230
+ //#endregion
1231
+ exports.apply = apply;
1232
+ exports.inject = inject;
1233
+ return module.exports;
1234
+ }
1235
+ });
1236
+
1237
+ //# sourceMappingURL=client.js.map