@jcy2387/dsh-models-input-modalities 0.1.0 → 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.
package/lib/client.cjs CHANGED
@@ -24,8 +24,10 @@ window.__ModuleLoader__.load({
24
24
  return Array.isArray(value) ? value.map((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry) ? entry : {}) : [];
25
25
  }
26
26
  /** Joins the settings Remote's document view and fenced writes for one card. */
27
- var ImageInputController = class {
27
+ var ModelCapabilityController = class {
28
28
  ctx;
29
+ /** The cards waiting to hear that this namespace's stored section changed. */
30
+ listeners = /* @__PURE__ */ new Set();
29
31
  /**
30
32
  * @param ctx - the plugin's client context, which declares `remote.settings`
31
33
  * in its own `inject`.
@@ -34,6 +36,29 @@ window.__ModuleLoader__.load({
34
36
  this.ctx = ctx;
35
37
  }
36
38
  /**
39
+ * Start forwarding this namespace's pushed document invalidations to the
40
+ * cards. The Host emits one per committed write — including the Models page's
41
+ * own model-list edits — so a card never has to poll or wait for a remount.
42
+ * @returns the disposer that withdraws the Remote subscription.
43
+ */
44
+ watch() {
45
+ return this.ctx.remote.$on("settings/document-updated", (ns, revision) => {
46
+ if (String(ns) !== NS$1) return;
47
+ for (const listener of [...this.listeners]) listener(revision);
48
+ });
49
+ }
50
+ /**
51
+ * Subscribe one card to namespace invalidations.
52
+ * @param listener - called with the namespace's new revision on each change.
53
+ * @returns the disposer for this one subscription.
54
+ */
55
+ subscribe(listener) {
56
+ this.listeners.add(listener);
57
+ return () => {
58
+ this.listeners.delete(listener);
59
+ };
60
+ }
61
+ /**
37
62
  * Read one provider's model rows and the revision fence for writing them.
38
63
  * @param entry - the card's directory row (its settings address names the profile).
39
64
  * @returns the view, or undefined when the settings face or namespace is unavailable.
@@ -123,6 +148,8 @@ window.__ModuleLoader__.load({
123
148
  function parseImageInputChoice(value) {
124
149
  return value === "inherit" || value === "text" || value === "image" ? value : void 0;
125
150
  }
151
+ //#endregion
152
+ //#region src/model-row.ts
126
153
  /**
127
154
  * The row's model id for labels.
128
155
  * @param row - one stored model row.
@@ -134,8 +161,197 @@ window.__ModuleLoader__.load({
134
161
  return typeof id === "string" && id.length > 0 ? id : `#${String(index + 1)}`;
135
162
  }
136
163
  //#endregion
164
+ //#region src/reasoning-efforts.ts
165
+ /** Every level a row may declare, in escalation order — the adapter's own key set. */
166
+ const THINKING_LEVELS = [
167
+ "off",
168
+ "minimal",
169
+ "low",
170
+ "medium",
171
+ "high",
172
+ "xhigh",
173
+ "max"
174
+ ];
175
+ /**
176
+ * The spelling a newly offered level starts from: its own name, except for
177
+ * `off`, whose absence is the spelling most providers read as "do not think".
178
+ * @param level - the level being offered.
179
+ * @returns its default wire spelling.
180
+ */
181
+ function defaultWire(level) {
182
+ return level === "off" ? "" : level;
183
+ }
184
+ /**
185
+ * A level set offering exactly `offered`, each at its default spelling.
186
+ * @param offered - the levels to declare.
187
+ * @returns the seven level drafts.
188
+ */
189
+ function declaring(offered) {
190
+ const drafts = {};
191
+ for (const level of THINKING_LEVELS) {
192
+ const on = offered.includes(level);
193
+ drafts[level] = {
194
+ offered: on,
195
+ wire: on ? defaultWire(level) : ""
196
+ };
197
+ }
198
+ return drafts;
199
+ }
200
+ /**
201
+ * The levels a row with no dict of its own starts from when it is switched to a
202
+ * declared set: the three efforts an OpenAI-compatible gateway is most likely
203
+ * to serve, each spelled as its own name. `off` is deliberately left unticked:
204
+ * on the plain `reasoning_effort` wire an empty `off` is the very same request
205
+ * as naming no effort at all, so pre-offering it would promise a "stop
206
+ * thinking" choice the endpoint may not honour — the author ticks it, and
207
+ * spells it, once their endpoint says how.
208
+ */
209
+ const DEFAULT_LEVELS = declaring([
210
+ "low",
211
+ "medium",
212
+ "high"
213
+ ]);
214
+ /**
215
+ * The choice a row's stored `reasoningEfforts` displays. Anything that is not
216
+ * `false` and not a plain object states no claim this card understands, and
217
+ * reads as the inheritance it leaves in place; an empty object reads as a
218
+ * declared set offering nothing, which the validator then names.
219
+ * @param row - one stored model row.
220
+ * @returns the choice the row's select shows.
221
+ */
222
+ function reasoningChoice(row) {
223
+ const value = row["reasoningEfforts"];
224
+ if (value === false) return "none";
225
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) return "custom";
226
+ return "inherit";
227
+ }
228
+ /**
229
+ * The row's declared levels as editor state. A level the dict omits is not
230
+ * offered; one it carries is, with its wire spelling — an empty value, which is
231
+ * what a valueless `off:` stores as, and a value of a type the schema refuses
232
+ * both read as no spelling yet. A row declaring nothing shows the defaults a
233
+ * fresh declaration starts from.
234
+ * @param row - one stored model row.
235
+ * @returns the seven level drafts.
236
+ */
237
+ function reasoningLevels(row) {
238
+ const value = row["reasoningEfforts"];
239
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return DEFAULT_LEVELS;
240
+ const stored = value;
241
+ const drafts = {};
242
+ for (const level of THINKING_LEVELS) {
243
+ if (!(level in stored)) {
244
+ drafts[level] = {
245
+ offered: false,
246
+ wire: ""
247
+ };
248
+ continue;
249
+ }
250
+ const wire = stored[level];
251
+ drafts[level] = {
252
+ offered: true,
253
+ wire: typeof wire === "string" ? wire : ""
254
+ };
255
+ }
256
+ return drafts;
257
+ }
258
+ /**
259
+ * The row with one reasoning choice applied: `inherit` removes the field,
260
+ * `none` stores `false`, and `custom` stores exactly the levels offered — in
261
+ * escalation order, a valueless `off` as `null`, spellings trimmed so a stray
262
+ * space cannot reach the wire. Every other field, including ones this card
263
+ * never shows, survives.
264
+ * @param row - the row to patch.
265
+ * @param choice - the selected state.
266
+ * @param levels - the editor's level drafts; read only for `custom`.
267
+ * @returns a new row carrying the choice.
268
+ */
269
+ function withReasoning(row, choice, levels) {
270
+ const next = { ...row };
271
+ delete next["reasoningEfforts"];
272
+ if (choice === "none") {
273
+ next["reasoningEfforts"] = false;
274
+ return next;
275
+ }
276
+ if (choice !== "custom") return next;
277
+ const declared = {};
278
+ for (const level of THINKING_LEVELS) {
279
+ const draft = levels[level];
280
+ if (!draft.offered) continue;
281
+ const wire = draft.wire.trim();
282
+ declared[level] = wire.length === 0 ? null : wire;
283
+ }
284
+ next["reasoningEfforts"] = declared;
285
+ return next;
286
+ }
287
+ /**
288
+ * The level set with one level offered or withdrawn. Offering a level always
289
+ * starts its spelling from the default, so a tick is never a blank the save
290
+ * then refuses, and what a withdrawn level carried is not what a reticked one
291
+ * silently revives; levels left alone keep theirs.
292
+ * @param levels - the drafts to patch.
293
+ * @param level - the level toggled.
294
+ * @param offered - whether the row should declare it.
295
+ * @returns the patched drafts, or the same object when nothing changes.
296
+ */
297
+ function toggleLevel(levels, level, offered) {
298
+ if (levels[level].offered === offered) return levels;
299
+ return {
300
+ ...levels,
301
+ [level]: {
302
+ offered,
303
+ wire: offered ? defaultWire(level) : ""
304
+ }
305
+ };
306
+ }
307
+ /**
308
+ * The level set with one level's wire spelling retyped.
309
+ * @param levels - the drafts to patch.
310
+ * @param level - the level whose spelling changed.
311
+ * @param wire - the text the field now carries.
312
+ * @returns the patched drafts.
313
+ */
314
+ function setWire(levels, level, wire) {
315
+ return {
316
+ ...levels,
317
+ [level]: {
318
+ ...levels[level],
319
+ wire
320
+ }
321
+ };
322
+ }
323
+ /**
324
+ * Why a row's declared levels would be refused, checked before the write so the
325
+ * card can name the row instead of answering a rejected settings mutation. The
326
+ * rules are the adapter's: every declared level but `off` needs a wire spelling,
327
+ * and a set offering no level beyond `off` declares nothing worth declaring.
328
+ * @param row - one stored model row.
329
+ * @returns the failure, or undefined for a row that can be saved.
330
+ */
331
+ function reasoningFailure(row) {
332
+ if (reasoningChoice(row) !== "custom") return void 0;
333
+ const levels = reasoningLevels(row);
334
+ let thinks = false;
335
+ for (const level of THINKING_LEVELS) {
336
+ const draft = levels[level];
337
+ if (!draft.offered || level === "off") continue;
338
+ if (draft.wire.trim().length === 0) return "needsWire";
339
+ thinks = true;
340
+ }
341
+ return thinks ? void 0 : "needsLevel";
342
+ }
343
+ /**
344
+ * Read a select's submitted value. The DOM hands over a bare string, so an
345
+ * unrecognized one is refused rather than cast into the union.
346
+ * @param value - the submitted option value.
347
+ * @returns the choice, or undefined for anything else.
348
+ */
349
+ function parseReasoningChoice(value) {
350
+ return value === "inherit" || value === "none" || value === "custom" ? value : void 0;
351
+ }
352
+ //#endregion
137
353
  //#region \0dsh-css:src/client/styles.module.css.mjs
138
- const css = ".nU9wWG_fold{border:1px solid var(--dsw-alias-border-l2,#0000001f);background:var(--dsw-alias-bg-layer-3,transparent);border-radius:10px;margin:8px 0 0}.nU9wWG_summary{cursor:pointer;color:var(--dsw-alias-label-primary,inherit);user-select:none;padding:10px 14px;font-size:13px;font-weight:600}.nU9wWG_body{flex-direction:column;gap:10px;padding:2px 14px 14px;display:flex}.nU9wWG_row{align-items:center;gap:10px;display:flex}.nU9wWG_rowId{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary,inherit);flex:auto;font-size:13px;overflow:hidden}.nU9wWG_select{min-width:9em;color:var(--dsw-alias-label-primary,inherit);border:1px solid var(--dsw-alias-border-l2,#0000001f);border-radius:8px;flex:none;padding:6px 8px;font-size:13px}.nU9wWG_hint,.nU9wWG_status{color:var(--dsw-alias-label-tertiary,inherit);margin:0;font-size:12px;line-height:1.5}.nU9wWG_error{color:var(--dsw-alias-status-danger,#d64545);margin:0;font-size:12px;line-height:1.5}.nU9wWG_footer{align-items:center;gap:10px;display:flex}";
354
+ const css = ".q49TuG_fold{border:1px solid var(--dsw-alias-border-l2,#0000001f);background:var(--dsw-alias-bg-layer-3,transparent);border-radius:10px;margin:8px 0 0}.q49TuG_summary{cursor:pointer;color:var(--dsw-alias-label-primary,inherit);user-select:none;padding:10px 14px;font-size:13px;font-weight:600}.q49TuG_body{flex-direction:column;gap:10px;padding:2px 14px 14px;display:flex}.q49TuG_model{flex-direction:column;gap:6px;display:flex}.q49TuG_row{flex-wrap:wrap;align-items:flex-end;gap:10px;display:flex}.q49TuG_rowId{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary,inherit);flex:auto;padding-bottom:6px;font-size:13px;overflow:hidden}.q49TuG_field{flex-direction:column;gap:3px;display:flex}.q49TuG_fieldLabel{color:var(--dsw-alias-label-tertiary,inherit);font-size:11px;line-height:1.2}.q49TuG_select{min-width:9em;color:var(--dsw-alias-label-primary,inherit);border:1px solid var(--dsw-alias-border-l2,#0000001f);border-radius:8px;flex:none;padding:6px 8px;font-size:13px}.q49TuG_levels{border:1px dashed var(--dsw-alias-border-l2,#0000001f);border-radius:8px;flex-direction:column;gap:6px;padding:8px 10px;display:flex}.q49TuG_levelGrid{grid-template-columns:repeat(auto-fill,minmax(15em,1fr));gap:6px 12px;display:grid}.q49TuG_levelRow{align-items:center;gap:8px;min-width:0;display:flex}.q49TuG_levelOffered{color:var(--dsw-alias-label-primary,inherit);cursor:pointer;flex:0 0 6.5em;align-items:center;gap:6px;font-size:12px;display:flex}.q49TuG_wire{min-width:0;color:var(--dsw-alias-label-primary,inherit);border:1px solid var(--dsw-alias-border-l2,#0000001f);background:0 0;border-radius:8px;flex:auto;padding:4px 8px;font-size:12px}.q49TuG_wire:disabled{opacity:.55}.q49TuG_hint,.q49TuG_status{color:var(--dsw-alias-label-tertiary,inherit);margin:0;font-size:12px;line-height:1.5}.q49TuG_error{color:var(--dsw-alias-status-danger,#d64545);margin:0;font-size:12px;line-height:1.5}.q49TuG_footer{align-items:center;gap:10px;display:flex}";
139
355
  const tagId = "@jcy2387/dsh-models-input-modalities/styles.module.css";
140
356
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
141
357
  const tag = document.createElement("style");
@@ -145,67 +361,156 @@ window.__ModuleLoader__.load({
145
361
  document.head.appendChild(tag);
146
362
  }
147
363
  var styles_module_css_default = {
148
- "footer": "nU9wWG_footer",
149
- "body": "nU9wWG_body",
150
- "fold": "nU9wWG_fold",
151
- "rowId": "nU9wWG_rowId",
152
- "select": "nU9wWG_select",
153
- "hint": "nU9wWG_hint",
154
- "summary": "nU9wWG_summary",
155
- "error": "nU9wWG_error",
156
- "status": "nU9wWG_status",
157
- "row": "nU9wWG_row"
364
+ "field": "q49TuG_field",
365
+ "status": "q49TuG_status",
366
+ "body": "q49TuG_body",
367
+ "fold": "q49TuG_fold",
368
+ "row": "q49TuG_row",
369
+ "fieldLabel": "q49TuG_fieldLabel",
370
+ "levels": "q49TuG_levels",
371
+ "hint": "q49TuG_hint",
372
+ "levelOffered": "q49TuG_levelOffered",
373
+ "summary": "q49TuG_summary",
374
+ "rowId": "q49TuG_rowId",
375
+ "select": "q49TuG_select",
376
+ "levelRow": "q49TuG_levelRow",
377
+ "footer": "q49TuG_footer",
378
+ "levelGrid": "q49TuG_levelGrid",
379
+ "error": "q49TuG_error",
380
+ "wire": "q49TuG_wire",
381
+ "model": "q49TuG_model"
158
382
  };
159
383
  //#endregion
160
- //#region src/client/ImageInputCard.tsx
384
+ //#region src/client/ModelCapabilityCard.tsx
161
385
  /**
162
- * One pi-ai provider card's image-input fold: the per-model modality claim the
163
- * Models page's own form does not carry. The fold loads the provider's stored
164
- * rows when first opened, edits them locally, and writes the whole `models`
165
- * array back under the revision fence the load answered the same array
166
- * semantics the page's own cards use.
386
+ * One pi-ai provider card's model-capability fold: the per-model claims the
387
+ * Models page's own form does not carry which inputs a model accepts, and
388
+ * which reasoning levels it offers. The fold loads the provider's stored rows
389
+ * when first opened, edits them locally, and writes the whole `models` array
390
+ * back under the revision fence the load answered — the same array semantics
391
+ * the page's own cards use, and the reason both claims live in one fold: two
392
+ * folds would write the same array and fence each other into conflicts. A
393
+ * stored change elsewhere on the page (a model added or removed in the catalog
394
+ * above) reaches the fold through the pushed settings invalidation, so the list
395
+ * it shows never waits for the section to remount.
167
396
  */
168
397
  /**
169
- * Render the image-input fold of one provider card.
398
+ * Render the model-capability fold of one provider card.
170
399
  * @param props - the card's directory row plus the bound face and copy.
171
400
  * @returns the fold, or nothing while the provider is still a dormant row.
172
401
  */
173
- function ImageInputCard(props) {
174
- const { provider, configured, t, loadModels, saveModels } = props;
402
+ function ModelCapabilityCard(props) {
403
+ const { provider, configured, t, loadModels, saveModels, subscribeChanges } = props;
175
404
  const [status, setStatus] = (0, react.useState)("idle");
176
405
  const [view, setView] = (0, react.useState)(void 0);
177
406
  const [rows, setRows] = (0, react.useState)([]);
178
407
  const [failure, setFailure] = (0, react.useState)(void 0);
179
408
  const [saved, setSaved] = (0, react.useState)(false);
180
- const reload = (0, react.useCallback)(async () => {
181
- setStatus("loading");
182
- setFailure(void 0);
183
- setSaved(false);
409
+ const [open, setOpen] = (0, react.useState)(false);
410
+ const [stale, setStale] = (0, react.useState)(false);
411
+ /** Latest read wins: an older response never overwrites a newer one. */
412
+ const generation = (0, react.useRef)(0);
413
+ /**
414
+ * The newest revision this fold holds adopted data for. An announcement at
415
+ * or below it is old news — the card's own committed write's echo included.
416
+ */
417
+ const seen = (0, react.useRef)(0);
418
+ /**
419
+ * The newest revision the namespace has announced. A completed read or
420
+ * write compares against it to tell whether a commit outran it mid-flight.
421
+ */
422
+ const noticed = (0, react.useRef)(0);
423
+ const dirtyRef = (0, react.useRef)(false);
424
+ const savingRef = (0, react.useRef)(false);
425
+ const dirty = view !== void 0 && JSON.stringify(rows) !== JSON.stringify(view.models);
426
+ (0, react.useEffect)(() => {
427
+ dirtyRef.current = dirty;
428
+ }, [dirty]);
429
+ (0, react.useEffect)(() => {
430
+ savingRef.current = status === "saving";
431
+ }, [status]);
432
+ /**
433
+ * Re-read the provider's rows.
434
+ * @param silent - keep the rendered list and copy in place while reading, for
435
+ * a background refresh the user never asked for.
436
+ */
437
+ const reload = (0, react.useCallback)(async (silent) => {
438
+ const ticket = ++generation.current;
439
+ if (!silent) {
440
+ setStatus("loading");
441
+ setFailure(void 0);
442
+ setSaved(false);
443
+ }
184
444
  const loaded = await loadModels(provider);
445
+ if (ticket !== generation.current) return;
446
+ if (silent && (dirtyRef.current || savingRef.current)) {
447
+ setStale(true);
448
+ return;
449
+ }
185
450
  if (loaded === void 0) {
451
+ if (silent) {
452
+ setStale(true);
453
+ return;
454
+ }
186
455
  setView(void 0);
187
456
  setFailure(t("loadFailed"));
188
457
  setStatus("ready");
189
458
  return;
190
459
  }
460
+ seen.current = loaded.revision;
191
461
  setView(loaded);
192
462
  setRows(loaded.models.map((row) => ({ ...row })));
463
+ setFailure(void 0);
464
+ setStale(loaded.revision < noticed.current);
193
465
  setStatus("ready");
194
466
  }, [
195
467
  loadModels,
196
468
  provider,
197
469
  t
198
470
  ]);
471
+ (0, react.useEffect)(() => {
472
+ if (configured !== true) return void 0;
473
+ return subscribeChanges((revision) => {
474
+ if (revision > noticed.current) noticed.current = revision;
475
+ if (revision <= seen.current) return;
476
+ setStale(true);
477
+ });
478
+ }, [configured, subscribeChanges]);
479
+ (0, react.useEffect)(() => {
480
+ if (stale && open && status === "ready" && !dirty) reload(true);
481
+ }, [
482
+ stale,
483
+ open,
484
+ status,
485
+ dirty,
486
+ view,
487
+ reload
488
+ ]);
199
489
  if (configured !== true) return null;
200
- const dirty = view !== void 0 && JSON.stringify(rows) !== JSON.stringify(view.models);
201
- const choose = (index, choice) => {
490
+ const chooseInput = (index, choice) => {
202
491
  setRows((current) => current.map((row, at) => at === index ? withImageInput(row, choice) : row));
203
492
  };
493
+ const chooseReasoning = (index, choice) => {
494
+ setRows((current) => current.map((row, at) => at === index ? withReasoning(row, choice, reasoningLevels(row)) : row));
495
+ };
496
+ /**
497
+ * Patch one row's declared levels. The edit runs inside the state updater and
498
+ * re-reads the row there, so two controls changed in one batch each see what
499
+ * the previous one wrote.
500
+ * @param index - the row to patch.
501
+ * @param edit - the level-set transformation.
502
+ */
503
+ const patchLevels = (index, edit) => {
504
+ setRows((current) => current.map((row, at) => at === index ? withReasoning(row, "custom", edit(reasoningLevels(row))) : row));
505
+ };
506
+ const unsavable = rows.some((row) => reasoningFailure(row) !== void 0);
507
+ const locked = view === void 0 || !view.writable || status === "saving";
204
508
  const submit = async () => {
205
- if (view === void 0) return;
509
+ if (view === void 0 || unsavable) return;
206
510
  setStatus("saving");
207
511
  const outcome = await saveModels(provider, rows, view.revision);
208
512
  if (outcome.kind === "written") {
513
+ seen.current = outcome.revision;
209
514
  setView({
210
515
  ...view,
211
516
  revision: outcome.revision,
@@ -213,17 +518,21 @@ window.__ModuleLoader__.load({
213
518
  fromUser: true
214
519
  });
215
520
  setSaved(true);
521
+ setFailure(void 0);
522
+ setStale(noticed.current > outcome.revision);
216
523
  setStatus("ready");
217
524
  return;
218
525
  }
219
526
  setFailure(outcome.kind === "conflict" ? t("conflict") : outcome.message);
220
- if (outcome.kind === "conflict") await reload();
527
+ if (outcome.kind === "conflict") await reload(false);
221
528
  else setStatus("ready");
222
529
  };
223
530
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
224
531
  className: styles_module_css_default["fold"],
225
532
  onToggle: (event) => {
226
- if (event.currentTarget.open && status === "idle") reload();
533
+ const opened = event.currentTarget.open;
534
+ setOpen(opened);
535
+ if (opened && (status === "idle" || stale && !dirty)) reload(false);
227
536
  },
228
537
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", {
229
538
  className: styles_module_css_default["summary"],
@@ -244,7 +553,7 @@ window.__ModuleLoader__.load({
244
553
  variant: "ghost",
245
554
  size: "sm",
246
555
  onClick: () => {
247
- reload();
556
+ reload(false);
248
557
  },
249
558
  children: t("retry")
250
559
  })
@@ -257,36 +566,130 @@ window.__ModuleLoader__.load({
257
566
  className: styles_module_css_default["hint"],
258
567
  children: t("inheritsHint")
259
568
  }),
260
- rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
261
- className: styles_module_css_default["row"],
262
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
263
- className: styles_module_css_default["rowId"],
264
- children: rowId(row, index)
265
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
266
- className: styles_module_css_default["select"],
267
- value: imageInputChoice(row),
268
- "aria-label": `${t("title")} ${rowId(row, index)}`,
269
- disabled: !view.writable || status === "saving",
270
- onChange: (event) => {
271
- const next = parseImageInputChoice(event.target.value);
272
- if (next !== void 0) choose(index, next);
273
- },
274
- children: [
275
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
276
- value: "inherit",
277
- children: t("choiceDefault")
278
- }),
279
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
280
- value: "text",
281
- children: t("choiceText")
282
- }),
283
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
284
- value: "image",
285
- children: t("choiceImage")
286
- })
287
- ]
288
- })]
289
- }, index)),
569
+ rows.map((row, index) => {
570
+ const id = rowId(row, index);
571
+ const reasoning = reasoningChoice(row);
572
+ const levels = reasoningLevels(row);
573
+ const invalid = reasoningFailure(row);
574
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
575
+ className: styles_module_css_default["model"],
576
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
577
+ className: styles_module_css_default["row"],
578
+ children: [
579
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
580
+ className: styles_module_css_default["rowId"],
581
+ children: id
582
+ }),
583
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
584
+ className: styles_module_css_default["field"],
585
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
586
+ className: styles_module_css_default["fieldLabel"],
587
+ children: t("inputLabel")
588
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
589
+ className: styles_module_css_default["select"],
590
+ value: imageInputChoice(row),
591
+ "aria-label": `${t("inputLabel")} ${id}`,
592
+ disabled: locked,
593
+ onChange: (event) => {
594
+ const next = parseImageInputChoice(event.target.value);
595
+ if (next !== void 0) chooseInput(index, next);
596
+ },
597
+ children: [
598
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
599
+ value: "inherit",
600
+ children: t("choiceDefault")
601
+ }),
602
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
603
+ value: "text",
604
+ children: t("choiceText")
605
+ }),
606
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
607
+ value: "image",
608
+ children: t("choiceImage")
609
+ })
610
+ ]
611
+ })]
612
+ }),
613
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
614
+ className: styles_module_css_default["field"],
615
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
616
+ className: styles_module_css_default["fieldLabel"],
617
+ children: t("reasoningLabel")
618
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
619
+ className: styles_module_css_default["select"],
620
+ value: reasoning,
621
+ "aria-label": `${t("reasoningLabel")} ${id}`,
622
+ disabled: locked,
623
+ onChange: (event) => {
624
+ const next = parseReasoningChoice(event.target.value);
625
+ if (next !== void 0) chooseReasoning(index, next);
626
+ },
627
+ children: [
628
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
629
+ value: "inherit",
630
+ children: t("reasoningInherit")
631
+ }),
632
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
633
+ value: "none",
634
+ children: t("reasoningNone")
635
+ }),
636
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
637
+ value: "custom",
638
+ children: t("reasoningCustom")
639
+ })
640
+ ]
641
+ })]
642
+ })
643
+ ]
644
+ }), reasoning === "custom" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
645
+ className: styles_module_css_default["levels"],
646
+ children: [
647
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
648
+ className: styles_module_css_default["levelGrid"],
649
+ children: THINKING_LEVELS.map((level) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
650
+ className: styles_module_css_default["levelRow"],
651
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
652
+ className: styles_module_css_default["levelOffered"],
653
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
654
+ type: "checkbox",
655
+ checked: levels[level].offered,
656
+ disabled: locked,
657
+ onChange: (event) => {
658
+ const offered = event.target.checked;
659
+ patchLevels(index, (current) => toggleLevel(current, level, offered));
660
+ }
661
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: level })]
662
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
663
+ className: styles_module_css_default["wire"],
664
+ type: "text",
665
+ value: levels[level].wire,
666
+ placeholder: level === "off" ? t("wireNothing") : level,
667
+ "aria-label": `${t("wireLabel")} ${level}`,
668
+ spellCheck: false,
669
+ disabled: locked || !levels[level].offered,
670
+ onChange: (event) => {
671
+ const wire = event.target.value;
672
+ patchLevels(index, (current) => setWire(current, level, wire));
673
+ }
674
+ })]
675
+ }, level))
676
+ }),
677
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
678
+ className: styles_module_css_default["hint"],
679
+ children: t("reasoningHint")
680
+ }),
681
+ invalid === "needsWire" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
682
+ className: styles_module_css_default["error"],
683
+ children: `${id}: ${t("needsWire")}`
684
+ }) : null,
685
+ invalid === "needsLevel" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
686
+ className: styles_module_css_default["error"],
687
+ children: `${id}: ${t("needsLevel")}`
688
+ }) : null
689
+ ]
690
+ }) : null]
691
+ }, index);
692
+ }),
290
693
  failure !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
291
694
  className: styles_module_css_default["error"],
292
695
  children: failure
@@ -297,7 +700,7 @@ window.__ModuleLoader__.load({
297
700
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
298
701
  variant: "outline",
299
702
  size: "sm",
300
- disabled: !view.writable || status === "saving" || !dirty,
703
+ disabled: locked || !dirty || unsavable,
301
704
  onClick: () => {
302
705
  submit();
303
706
  },
@@ -308,7 +711,7 @@ window.__ModuleLoader__.load({
308
711
  size: "sm",
309
712
  disabled: status === "saving",
310
713
  onClick: () => {
311
- reload();
714
+ reload(false);
312
715
  },
313
716
  children: t("retry")
314
717
  }) : null,
@@ -329,19 +732,29 @@ window.__ModuleLoader__.load({
329
732
  }
330
733
  //#endregion
331
734
  //#region src/client/locales.ts
332
- /** Copy dictionaries for the image-input card. */
735
+ /** Copy dictionaries for the model-capability card. */
333
736
  /** English strings (the key-set source of truth for this pair). */
334
737
  const en = {
335
- title: "Input modalities",
738
+ title: "Model capabilities",
336
739
  loading: "Loading the model list…",
337
740
  loadFailed: "Loading the model configuration failed.",
338
741
  retry: "Retry",
339
- empty: "No explicit model list yet — add models in the catalog above, then declare their input modalities here.",
742
+ empty: "No explicit model list yet — add models in the catalog above, then declare what each one accepts and reasons with here.",
340
743
  inheritsHint: "Showing the inherited model list; saving copies it into your user settings.",
341
744
  readOnly: "The settings document is read-only in this deployment.",
745
+ inputLabel: "Input modalities",
342
746
  choiceDefault: "Provider default",
343
747
  choiceText: "Text only",
344
748
  choiceImage: "Text and image",
749
+ reasoningLabel: "Reasoning levels",
750
+ reasoningInherit: "Catalog default",
751
+ reasoningNone: "Not a reasoning model",
752
+ reasoningCustom: "Declare levels",
753
+ reasoningHint: "A ticked level is one the model picker offers; the value beside it is the spelling sent on the wire, which a gateway may name its own way. Only off may stay empty — offered, and sent as no parameter at all. A level left unticked is not offered.",
754
+ wireLabel: "Wire value for",
755
+ wireNothing: "send nothing",
756
+ needsLevel: "declare at least one level beyond off, or choose “Not a reasoning model”.",
757
+ needsWire: "every level except off needs the wire value to send.",
345
758
  save: "Save",
346
759
  saving: "Saving…",
347
760
  saved: "Saved. The adapter picks it up on its next request.",
@@ -349,16 +762,26 @@ window.__ModuleLoader__.load({
349
762
  };
350
763
  /** Chinese strings (same keys as {@link en}). */
351
764
  const zh = {
352
- title: "输入模态",
765
+ title: "模型能力",
353
766
  loading: "正在读取模型列表…",
354
767
  loadFailed: "读取模型配置失败。",
355
768
  retry: "重试",
356
- empty: "还没有显式模型列表——请先在上方模型目录中添加模型,再回到这里声明输入模态。",
769
+ empty: "还没有显式模型列表——请先在上方模型目录中添加模型,再回到这里声明每个模型接受的输入与推理等级。",
357
770
  inheritsHint: "当前显示的是继承的模型列表;保存会将其复制到你的用户设置层。",
358
771
  readOnly: "当前部署的设置文档为只读。",
772
+ inputLabel: "输入模态",
359
773
  choiceDefault: "提供方默认",
360
774
  choiceText: "仅文本",
361
775
  choiceImage: "文本和图片",
776
+ reasoningLabel: "推理等级",
777
+ reasoningInherit: "目录默认",
778
+ reasoningNone: "非推理模型",
779
+ reasoningCustom: "声明等级",
780
+ reasoningHint: "勾选的等级就是模型选择器会提供的选项;旁边的值是实际发到网关的拼写,网关可以有自己的叫法。只有 off 可以留空——表示提供该等级但完全不发送参数。未勾选的等级不会提供。",
781
+ wireLabel: "发送值:",
782
+ wireNothing: "不发送",
783
+ needsLevel: "至少声明一个 off 以外的等级,或选择「非推理模型」。",
784
+ needsWire: "除 off 外,每个勾选的等级都要填写发送值。",
362
785
  save: "保存",
363
786
  saving: "保存中…",
364
787
  saved: "已保存。适配器会在下一次请求时生效。",
@@ -367,7 +790,7 @@ window.__ModuleLoader__.load({
367
790
  //#endregion
368
791
  //#region src/client/index.ts
369
792
  /** Dictionary namespace owned by this plugin. */
370
- const NS = "settings.models.imageInput";
793
+ const NS = "settings.models.modelCapabilities";
371
794
  /** The effect label prefix. */
372
795
  const PKG = "@jcy2387/dsh-models-input-modalities";
373
796
  /** Required browser services. */
@@ -378,7 +801,7 @@ window.__ModuleLoader__.load({
378
801
  "remote.settings"
379
802
  ];
380
803
  /**
381
- * Register the image-input fold on every llm-pi-ai provider card once the
804
+ * Register the model-capability fold on every llm-pi-ai provider card once the
382
805
  * Models section has declared the seat.
383
806
  * @param ctx - the plugin's client context.
384
807
  */
@@ -387,17 +810,19 @@ window.__ModuleLoader__.load({
387
810
  zh,
388
811
  en
389
812
  }), `${PKG}: dictionaries`);
390
- const controller = new ImageInputController(ctx);
813
+ const controller = new ModelCapabilityController(ctx);
814
+ ctx.effect(() => controller.watch(), `${PKG}: settings invalidations`);
391
815
  const face = {
392
816
  loadModels: (entry) => controller.load(entry),
393
- saveModels: (entry, models, revision) => controller.save(entry, models, revision)
817
+ saveModels: (entry, models, revision) => controller.save(entry, models, revision),
818
+ subscribeChanges: (listener) => controller.subscribe(listener)
394
819
  };
395
820
  ctx.slots.inject("settings.models.provider-card", () => ctx.slots.register({
396
821
  name: "settings.models.provider-card",
397
822
  key: "llm-pi-ai",
398
823
  locale: NS,
399
824
  inject: () => face
400
- }, ImageInputCard));
825
+ }, ModelCapabilityCard));
401
826
  }
402
827
  //#endregion
403
828
  exports.apply = apply;