@jcy2387/dsh-models-input-modalities 0.1.0 → 0.1.1
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/README.md +3 -0
- package/README.zh.md +3 -0
- package/lib/client.cjs +118 -23
- package/lib/client.cjs.map +1 -1
- package/lib/types/client/ImageInputCard.d.ts +6 -1
- package/lib/types/client/controller.d.ts +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ The providers themselves (provider ID, API base URL, protocol, API key, model li
|
|
|
17
17
|
- **Exact adapter semantics** — *Text only* writes `input: [text]`, *Text and image* writes `input: [text, image]`, and *Provider default* removes the field so the row inherits the installed catalog's modalities, then the route's `defaultInput`.
|
|
18
18
|
- **Revision-fenced writes** — the whole `models` array is written back under the revision read when the fold was opened, with the same array semantics and conflict handling as the Models page's own cards: a concurrent edit surfaces a conflict notice and reloads instead of silently clobbering.
|
|
19
19
|
- **Field preservation** — every other field of every row survives verbatim, including fields this card never shows.
|
|
20
|
+
- **Follows the page live** — add or remove a model in the catalog above and the modality rows follow it in place: the fold listens for the pushed `settings/document-updated` invalidation of its own namespace, so nothing has to be closed and reopened. A commit that outruns a running read is caught by revision comparison and re-read until the fold is level. An unsaved draft is never overwritten, not even by a reopen — a change that lands mid-draft parks until the draft settles: revert the edits and the fold re-reads silently, or save and the revision fence refuses the raced write and reloads behind a conflict notice.
|
|
20
21
|
- **Localized UI** — English and Chinese follow the Web UI's locale.
|
|
21
22
|
|
|
22
23
|
## Usage
|
|
@@ -30,6 +31,8 @@ The providers themselves (provider ID, API base URL, protocol, API key, model li
|
|
|
30
31
|
|
|
31
32
|
The plugin registers its component into the `settings.models.provider-card` extension seat exposed by the Models page (key `llm-pi-ai`, i.e. the cards of the whole pi-ai adapter family). On first expansion the fold reads the provider's stored `models` rows through the settings Remote, edits them locally, and writes the whole array back under the revision captured at read time — the same array semantics and conflict handling as the page's own cards (a concurrent edit prompts a conflict notice and a reload). All fields other than `input` are preserved verbatim in every row.
|
|
32
33
|
|
|
34
|
+
The fold also subscribes to the Host's forwarded `settings/document-updated` event and filters it to `llm-pi-ai`, so the page's own model-list writes reach it without a remount. An open, clean fold re-reads silently, and keeps re-reading until its data is level with the newest announced revision; a closed one parks the notice and re-reads on the next expansion; one holding an unsaved draft parks the notice until the draft settles, so a reopen never costs edits. The card's own write is recognised by the revision it just committed and does not echo back as a refresh.
|
|
35
|
+
|
|
33
36
|
## Installation
|
|
34
37
|
|
|
35
38
|
Prerequisites: DeepSeek Harness (`dsh`) `>=0.1.5-alpha.1 <0.2.0` with the `web` profile.
|
package/README.zh.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
- **与适配器语义一致** —— *仅文本*写入 `input: [text]`,*文本和图片*写入 `input: [text, image]`;*提供方默认*删除该字段,让这一行继承已安装目录的模态,再回退路由 `defaultInput`。
|
|
18
18
|
- **revision 围栏写入** —— 整组 `models` 数组按折叠区打开时读到的 revision 写回,与模型页自身卡片相同的数组语义与冲突处理:他人同时改动时提示冲突并重新加载,而不是静默覆盖。
|
|
19
19
|
- **字段保留** —— 行内除 `input` 外的所有字段原样保留,包括本卡片从不展示的字段。
|
|
20
|
+
- **与模型页实时联动** —— 在上方模型目录里增删模型,模态行会就地跟上:折叠区订阅了本命名空间的 `settings/document-updated` 推送失效事件,无需关掉设置页再重开。读取途中被新提交反超时,按 revision 对比识别落差并自动追读,直到追平。未保存的草稿永不被覆盖,重新展开也不会——草稿期间落下的改动会一直挂起到草稿落定为止:撤回编辑,折叠区静默重读;直接保存,则由 revision 围栏拦下撞车的写入,提示冲突后重载。
|
|
20
21
|
- **本地化界面** —— 中英文跟随 Web UI 语言。
|
|
21
22
|
|
|
22
23
|
## 使用
|
|
@@ -33,6 +34,8 @@
|
|
|
33
34
|
|
|
34
35
|
插件把组件注册进模型页对外开放的 `settings.models.provider-card` 扩展位(key 为 `llm-pi-ai`,即整个 pi-ai 适配器家族的卡片)。折叠区首次展开时通过 settings Remote 读取该提供方存储的 `models` 行,本地编辑后按读取时的 revision 围栏整组写回——与模型页自身卡片相同的数组语义与冲突处理(他人同时改动时提示冲突并重新加载)。行内除 `input` 外的所有字段原样保留。
|
|
35
36
|
|
|
37
|
+
折叠区同时订阅 Host 转发的 `settings/document-updated` 事件,并只认 `llm-pi-ai`,所以模型页自己写模型列表时能无需重挂载地传到折叠区:已展开且没有草稿的折叠区静默重读,并一直追读到与最新通报的 revision 持平;收起的把通知挂起,等下次展开再重读;正握着未保存草稿的把通知挂起到草稿落定为止,因此重新展开永远不会丢掉编辑。卡片自己那次写入按刚提交的 revision 认出回声,不会再触发一次刷新。
|
|
38
|
+
|
|
36
39
|
## 安装
|
|
37
40
|
|
|
38
41
|
前置条件:DeepSeek Harness(`dsh`)`>=0.1.5-alpha.1 <0.2.0`(装有 `web` profile)。
|
package/lib/client.cjs
CHANGED
|
@@ -26,6 +26,8 @@ window.__ModuleLoader__.load({
|
|
|
26
26
|
/** Joins the settings Remote's document view and fenced writes for one card. */
|
|
27
27
|
var ImageInputController = 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.
|
|
@@ -135,7 +160,7 @@ window.__ModuleLoader__.load({
|
|
|
135
160
|
}
|
|
136
161
|
//#endregion
|
|
137
162
|
//#region \0dsh-css:src/client/styles.module.css.mjs
|
|
138
|
-
const css = ".
|
|
163
|
+
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_row{align-items:center;gap:10px;display:flex}.q49TuG_rowId{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary,inherit);flex:auto;font-size:13px;overflow:hidden}.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_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
164
|
const tagId = "@jcy2387/dsh-models-input-modalities/styles.module.css";
|
|
140
165
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
141
166
|
const tag = document.createElement("style");
|
|
@@ -145,16 +170,16 @@ window.__ModuleLoader__.load({
|
|
|
145
170
|
document.head.appendChild(tag);
|
|
146
171
|
}
|
|
147
172
|
var styles_module_css_default = {
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"rowId": "
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"
|
|
173
|
+
"hint": "q49TuG_hint",
|
|
174
|
+
"row": "q49TuG_row",
|
|
175
|
+
"body": "q49TuG_body",
|
|
176
|
+
"rowId": "q49TuG_rowId",
|
|
177
|
+
"status": "q49TuG_status",
|
|
178
|
+
"error": "q49TuG_error",
|
|
179
|
+
"footer": "q49TuG_footer",
|
|
180
|
+
"summary": "q49TuG_summary",
|
|
181
|
+
"fold": "q49TuG_fold",
|
|
182
|
+
"select": "q49TuG_select"
|
|
158
183
|
};
|
|
159
184
|
//#endregion
|
|
160
185
|
//#region src/client/ImageInputCard.tsx
|
|
@@ -163,7 +188,10 @@ window.__ModuleLoader__.load({
|
|
|
163
188
|
* Models page's own form does not carry. The fold loads the provider's stored
|
|
164
189
|
* rows when first opened, edits them locally, and writes the whole `models`
|
|
165
190
|
* array back under the revision fence the load answered — the same array
|
|
166
|
-
* semantics the page's own cards use.
|
|
191
|
+
* semantics the page's own cards use. A stored change elsewhere on the page (a
|
|
192
|
+
* model added or removed in the catalog above) reaches the fold through the
|
|
193
|
+
* pushed settings invalidation, so the list it shows never waits for the
|
|
194
|
+
* section to remount.
|
|
167
195
|
*/
|
|
168
196
|
/**
|
|
169
197
|
* Render the image-input fold of one provider card.
|
|
@@ -171,33 +199,93 @@ window.__ModuleLoader__.load({
|
|
|
171
199
|
* @returns the fold, or nothing while the provider is still a dormant row.
|
|
172
200
|
*/
|
|
173
201
|
function ImageInputCard(props) {
|
|
174
|
-
const { provider, configured, t, loadModels, saveModels } = props;
|
|
202
|
+
const { provider, configured, t, loadModels, saveModels, subscribeChanges } = props;
|
|
175
203
|
const [status, setStatus] = (0, react.useState)("idle");
|
|
176
204
|
const [view, setView] = (0, react.useState)(void 0);
|
|
177
205
|
const [rows, setRows] = (0, react.useState)([]);
|
|
178
206
|
const [failure, setFailure] = (0, react.useState)(void 0);
|
|
179
207
|
const [saved, setSaved] = (0, react.useState)(false);
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
208
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
209
|
+
const [stale, setStale] = (0, react.useState)(false);
|
|
210
|
+
/** Latest read wins: an older response never overwrites a newer one. */
|
|
211
|
+
const generation = (0, react.useRef)(0);
|
|
212
|
+
/**
|
|
213
|
+
* The newest revision this fold holds adopted data for. An announcement at
|
|
214
|
+
* or below it is old news — the card's own committed write's echo included.
|
|
215
|
+
*/
|
|
216
|
+
const seen = (0, react.useRef)(0);
|
|
217
|
+
/**
|
|
218
|
+
* The newest revision the namespace has announced. A completed read or
|
|
219
|
+
* write compares against it to tell whether a commit outran it mid-flight.
|
|
220
|
+
*/
|
|
221
|
+
const noticed = (0, react.useRef)(0);
|
|
222
|
+
const dirtyRef = (0, react.useRef)(false);
|
|
223
|
+
const savingRef = (0, react.useRef)(false);
|
|
224
|
+
const dirty = view !== void 0 && JSON.stringify(rows) !== JSON.stringify(view.models);
|
|
225
|
+
(0, react.useEffect)(() => {
|
|
226
|
+
dirtyRef.current = dirty;
|
|
227
|
+
}, [dirty]);
|
|
228
|
+
(0, react.useEffect)(() => {
|
|
229
|
+
savingRef.current = status === "saving";
|
|
230
|
+
}, [status]);
|
|
231
|
+
/**
|
|
232
|
+
* Re-read the provider's rows.
|
|
233
|
+
* @param silent - keep the rendered list and copy in place while reading, for
|
|
234
|
+
* a background refresh the user never asked for.
|
|
235
|
+
*/
|
|
236
|
+
const reload = (0, react.useCallback)(async (silent) => {
|
|
237
|
+
const ticket = ++generation.current;
|
|
238
|
+
if (!silent) {
|
|
239
|
+
setStatus("loading");
|
|
240
|
+
setFailure(void 0);
|
|
241
|
+
setSaved(false);
|
|
242
|
+
}
|
|
184
243
|
const loaded = await loadModels(provider);
|
|
244
|
+
if (ticket !== generation.current) return;
|
|
245
|
+
if (silent && (dirtyRef.current || savingRef.current)) {
|
|
246
|
+
setStale(true);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
185
249
|
if (loaded === void 0) {
|
|
250
|
+
if (silent) {
|
|
251
|
+
setStale(true);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
186
254
|
setView(void 0);
|
|
187
255
|
setFailure(t("loadFailed"));
|
|
188
256
|
setStatus("ready");
|
|
189
257
|
return;
|
|
190
258
|
}
|
|
259
|
+
seen.current = loaded.revision;
|
|
191
260
|
setView(loaded);
|
|
192
261
|
setRows(loaded.models.map((row) => ({ ...row })));
|
|
262
|
+
setFailure(void 0);
|
|
263
|
+
setStale(loaded.revision < noticed.current);
|
|
193
264
|
setStatus("ready");
|
|
194
265
|
}, [
|
|
195
266
|
loadModels,
|
|
196
267
|
provider,
|
|
197
268
|
t
|
|
198
269
|
]);
|
|
270
|
+
(0, react.useEffect)(() => {
|
|
271
|
+
if (configured !== true) return void 0;
|
|
272
|
+
return subscribeChanges((revision) => {
|
|
273
|
+
if (revision > noticed.current) noticed.current = revision;
|
|
274
|
+
if (revision <= seen.current) return;
|
|
275
|
+
setStale(true);
|
|
276
|
+
});
|
|
277
|
+
}, [configured, subscribeChanges]);
|
|
278
|
+
(0, react.useEffect)(() => {
|
|
279
|
+
if (stale && open && status === "ready" && !dirty) reload(true);
|
|
280
|
+
}, [
|
|
281
|
+
stale,
|
|
282
|
+
open,
|
|
283
|
+
status,
|
|
284
|
+
dirty,
|
|
285
|
+
view,
|
|
286
|
+
reload
|
|
287
|
+
]);
|
|
199
288
|
if (configured !== true) return null;
|
|
200
|
-
const dirty = view !== void 0 && JSON.stringify(rows) !== JSON.stringify(view.models);
|
|
201
289
|
const choose = (index, choice) => {
|
|
202
290
|
setRows((current) => current.map((row, at) => at === index ? withImageInput(row, choice) : row));
|
|
203
291
|
};
|
|
@@ -206,6 +294,7 @@ window.__ModuleLoader__.load({
|
|
|
206
294
|
setStatus("saving");
|
|
207
295
|
const outcome = await saveModels(provider, rows, view.revision);
|
|
208
296
|
if (outcome.kind === "written") {
|
|
297
|
+
seen.current = outcome.revision;
|
|
209
298
|
setView({
|
|
210
299
|
...view,
|
|
211
300
|
revision: outcome.revision,
|
|
@@ -213,17 +302,21 @@ window.__ModuleLoader__.load({
|
|
|
213
302
|
fromUser: true
|
|
214
303
|
});
|
|
215
304
|
setSaved(true);
|
|
305
|
+
setFailure(void 0);
|
|
306
|
+
setStale(noticed.current > outcome.revision);
|
|
216
307
|
setStatus("ready");
|
|
217
308
|
return;
|
|
218
309
|
}
|
|
219
310
|
setFailure(outcome.kind === "conflict" ? t("conflict") : outcome.message);
|
|
220
|
-
if (outcome.kind === "conflict") await reload();
|
|
311
|
+
if (outcome.kind === "conflict") await reload(false);
|
|
221
312
|
else setStatus("ready");
|
|
222
313
|
};
|
|
223
314
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
224
315
|
className: styles_module_css_default["fold"],
|
|
225
316
|
onToggle: (event) => {
|
|
226
|
-
|
|
317
|
+
const opened = event.currentTarget.open;
|
|
318
|
+
setOpen(opened);
|
|
319
|
+
if (opened && (status === "idle" || stale && !dirty)) reload(false);
|
|
227
320
|
},
|
|
228
321
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", {
|
|
229
322
|
className: styles_module_css_default["summary"],
|
|
@@ -244,7 +337,7 @@ window.__ModuleLoader__.load({
|
|
|
244
337
|
variant: "ghost",
|
|
245
338
|
size: "sm",
|
|
246
339
|
onClick: () => {
|
|
247
|
-
reload();
|
|
340
|
+
reload(false);
|
|
248
341
|
},
|
|
249
342
|
children: t("retry")
|
|
250
343
|
})
|
|
@@ -308,7 +401,7 @@ window.__ModuleLoader__.load({
|
|
|
308
401
|
size: "sm",
|
|
309
402
|
disabled: status === "saving",
|
|
310
403
|
onClick: () => {
|
|
311
|
-
reload();
|
|
404
|
+
reload(false);
|
|
312
405
|
},
|
|
313
406
|
children: t("retry")
|
|
314
407
|
}) : null,
|
|
@@ -388,9 +481,11 @@ window.__ModuleLoader__.load({
|
|
|
388
481
|
en
|
|
389
482
|
}), `${PKG}: dictionaries`);
|
|
390
483
|
const controller = new ImageInputController(ctx);
|
|
484
|
+
ctx.effect(() => controller.watch(), `${PKG}: settings invalidations`);
|
|
391
485
|
const face = {
|
|
392
486
|
loadModels: (entry) => controller.load(entry),
|
|
393
|
-
saveModels: (entry, models, revision) => controller.save(entry, models, revision)
|
|
487
|
+
saveModels: (entry, models, revision) => controller.save(entry, models, revision),
|
|
488
|
+
subscribeChanges: (listener) => controller.subscribe(listener)
|
|
394
489
|
};
|
|
395
490
|
ctx.slots.inject("settings.models.provider-card", () => ctx.slots.register({
|
|
396
491
|
name: "settings.models.provider-card",
|
package/lib/client.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.cjs","names":["NS","useState","useCallback","css","Button"],"sources":["../src/client/controller.ts","../src/image-input.ts","../src/client/ImageInputCard.tsx","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/** Settings reads and writes for the image-input card, over the settings Remote. */\n\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\nimport type { SettingsPathOpView } from '@deepseek-ai/dsh-api-remotes/client'\nimport type { ProviderDirectoryEntry } from '@deepseek-ai/dsh-client-ui-settings-models/client'\nimport type { JsonValue } from '@deepseek-ai/dsh-util-values'\nimport type { ModelRow } from '../image-input.ts'\n\n/** The settings namespace every pi-ai provider card addresses. */\nconst NS = 'llm-pi-ai'\n\n/** What one load answers for a provider card. */\nexport interface ImageInputView {\n /** Whether the deployment accepts settings writes at all. */\n writable: boolean\n /** Revision fence the next save must carry. */\n revision: number\n /** The rows to show: the user layer's when it owns the list, else the effective ones. */\n models: readonly ModelRow[]\n /** Whether the shown rows already live in the user layer. */\n fromUser: boolean\n}\n\n/** What one save answered. */\nexport type ImageInputSaveOutcome =\n | { readonly kind: 'written'; readonly revision: number }\n | { readonly kind: 'conflict'; readonly message: string }\n | { readonly kind: 'refused'; readonly message: string }\n\n/** Read one plain-object path; anything off-path answers undefined. */\nfunction at(source: unknown, path: readonly string[]): unknown {\n let current: unknown = source\n for (const key of path) {\n if (typeof current !== 'object' || current === null || Array.isArray(current)) return undefined\n current = (current as Record<string, unknown>)[key]\n }\n return current\n}\n\n/** Coerce a stored models value into open row records. */\nfunction rows(value: unknown): ModelRow[] {\n return Array.isArray(value)\n ? value.map(entry =>\n typeof entry === 'object' && entry !== null && !Array.isArray(entry) ? entry as ModelRow : {})\n : []\n}\n\n/** Joins the settings Remote's document view and fenced writes for one card. */\nexport class ImageInputController {\n /**\n * @param ctx - the plugin's client context, which declares `remote.settings`\n * in its own `inject`.\n */\n constructor(private readonly ctx: ClientContext) {}\n\n /**\n * Read one provider's model rows and the revision fence for writing them.\n * @param entry - the card's directory row (its settings address names the profile).\n * @returns the view, or undefined when the settings face or namespace is unavailable.\n */\n async load(entry: ProviderDirectoryEntry): Promise<ImageInputView | undefined> {\n const response = await this.ctx.remote.settings.describe()\n if (!response.ok) return undefined\n const namespace = response.value.namespaces.find(view => view.ns === NS)\n if (namespace === undefined) return undefined\n const path = [...entry.settingsPath, 'models']\n const userModel = at(namespace.user, path)\n const fromUser = Array.isArray(userModel)\n return {\n writable: response.value.writable,\n revision: namespace.revision,\n models: rows(fromUser ? userModel : at(namespace.value, path)),\n fromUser,\n }\n }\n\n /**\n * Write the rows back as the profile's whole `models` array, under the fence\n * the load answered. The array is replaced by value — the adapter's own\n * semantics — so untouched rows ride along exactly as stored.\n * @param entry - the card's directory row.\n * @param models - the edited rows.\n * @param revision - the fence from the load this draft was opened at.\n * @returns the write outcome the card renders from.\n */\n async save(\n entry: ProviderDirectoryEntry,\n models: readonly ModelRow[],\n revision: number,\n ): Promise<ImageInputSaveOutcome> {\n // The rows came out of a stored JSON document and the edit only ever sets\n // string arrays, so the array is JSON by construction.\n const value = models as unknown as JsonValue\n const ops: SettingsPathOpView[] = [{ op: 'set', path: [...entry.settingsPath, 'models'], value }]\n const response = await this.ctx.remote.settings.mutate(NS, ops, revision)\n if (response.ok) return { kind: 'written', revision: response.value.revision }\n const { code, message } = response.error\n return code === 'settings/conflict' ? { kind: 'conflict', message } : { kind: 'refused', message }\n }\n}\n","/** Pure row helpers for the per-model image-input claim. */\n\n/** One configured model row, structurally open so hidden fields survive an edit. */\nexport type ModelRow = Record<string, unknown>\n\n/** The three image-input states a row's select offers. */\nexport type ImageInputChoice = 'inherit' | 'text' | 'image'\n\n/**\n * The choice a row's stored `input` displays. Absent and empty mean the same\n * inheritance — the installed catalog's modalities, then the route's\n * `defaultInput` — and any list naming `image` is the image-capable claim\n * however else it is spelled.\n * @param row - one stored model row.\n * @returns the choice the row's select shows.\n */\nexport function imageInputChoice(row: ModelRow): ImageInputChoice {\n const value = row['input']\n if (!Array.isArray(value) || value.length === 0) return 'inherit'\n return value.includes('image') ? 'image' : 'text'\n}\n\n/**\n * The row with one choice applied: `inherit` removes the field, the others\n * store exactly the modality list the adapter reads. Every other field,\n * including ones this card never shows, survives.\n * @param row - the row to patch.\n * @param choice - the selected state.\n * @returns a new row carrying the choice.\n */\nexport function withImageInput(row: ModelRow, choice: ImageInputChoice): ModelRow {\n const next = { ...row }\n delete next['input']\n if (choice === 'text') next['input'] = ['text']\n else if (choice === 'image') next['input'] = ['text', 'image']\n return next\n}\n\n/**\n * Read a select's submitted value. The DOM hands over a bare string, so an\n * unrecognized one is refused rather than cast into the union.\n * @param value - the submitted option value.\n * @returns the choice, or undefined for anything else.\n */\nexport function parseImageInputChoice(value: string): ImageInputChoice | undefined {\n return value === 'inherit' || value === 'text' || value === 'image' ? value : undefined\n}\n\n/**\n * The row's model id for labels.\n * @param row - one stored model row.\n * @param index - the row's zero-based position.\n * @returns the id, or a positional name for an id-less row.\n */\nexport function rowId(row: ModelRow, index: number): string {\n const id = row['id']\n return typeof id === 'string' && id.length > 0 ? id : `#${String(index + 1)}`\n}\n","/**\n * One pi-ai provider card's image-input fold: the per-model modality claim the\n * Models page's own form does not carry. The fold loads the provider's stored\n * rows when first opened, edits them locally, and writes the whole `models`\n * array back under the revision fence the load answered — the same array\n * semantics the page's own cards use.\n */\n\nimport { useCallback, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport { Button } from '@deepseek-ai/dsh-client-ui-primitives'\nimport type { ProviderDirectoryEntry } from '@deepseek-ai/dsh-client-ui-settings-models/client'\nimport type { ImageInputSaveOutcome, ImageInputView } from './controller.ts'\nimport { imageInputChoice, parseImageInputChoice, rowId, withImageInput } from '../image-input.ts'\nimport type { ImageInputChoice, ModelRow } from '../image-input.ts'\nimport css from './styles.module.css'\n\n/** The registration-side face this card receives. */\nexport interface ImageInputFace {\n /** Read the provider's rows and revision fence; undefined when the settings face is unavailable. */\n loadModels(entry: ProviderDirectoryEntry): Promise<ImageInputView | undefined>\n /** Write the rows back under the fence the load answered. */\n saveModels(\n entry: ProviderDirectoryEntry,\n models: readonly ModelRow[],\n revision: number,\n ): Promise<ImageInputSaveOutcome>\n}\n\n/** Props the provider-card slot binds. */\nexport type ImageInputCardProps =\n PropsRuntime<'settings.models.provider-card'>\n & PropsLocale<'settings.models.imageInput'>\n & InjectFace<ImageInputFace>\n\n/** Lifecycle of one fold: closed, loading, editable, or writing. */\ntype Status = 'idle' | 'loading' | 'ready' | 'saving'\n\n/**\n * Render the image-input fold of one provider card.\n * @param props - the card's directory row plus the bound face and copy.\n * @returns the fold, or nothing while the provider is still a dormant row.\n */\nexport function ImageInputCard(props: ImageInputCardProps): ReactNode {\n const { provider, configured, t, loadModels, saveModels } = props\n const [status, setStatus] = useState<Status>('idle')\n const [view, setView] = useState<ImageInputView | undefined>(undefined)\n const [rows, setRows] = useState<readonly ModelRow[]>([])\n const [failure, setFailure] = useState<string | undefined>(undefined)\n const [saved, setSaved] = useState(false)\n\n const reload = useCallback(async (): Promise<void> => {\n setStatus('loading')\n setFailure(undefined)\n setSaved(false)\n const loaded = await loadModels(provider)\n if (loaded === undefined) {\n setView(undefined)\n setFailure(t('loadFailed'))\n setStatus('ready')\n return\n }\n setView(loaded)\n setRows(loaded.models.map(row => ({ ...row })))\n setStatus('ready')\n }, [loadModels, provider, t])\n\n // A dormant directory row has no profile to read models from; the create\n // card dispatches this seat only after the provider is saved anyway.\n if (configured !== true) return null\n\n const dirty = view !== undefined && JSON.stringify(rows) !== JSON.stringify(view.models)\n\n const choose = (index: number, choice: ImageInputChoice): void => {\n setRows(current => current.map((row, at) => at === index ? withImageInput(row, choice) : row))\n }\n\n const submit = async (): Promise<void> => {\n if (view === undefined) return\n setStatus('saving')\n const outcome = await saveModels(provider, rows, view.revision)\n if (outcome.kind === 'written') {\n setView({ ...view, revision: outcome.revision, models: rows.map(row => ({ ...row })), fromUser: true })\n setSaved(true)\n setStatus('ready')\n return\n }\n setFailure(outcome.kind === 'conflict' ? t('conflict') : outcome.message)\n if (outcome.kind === 'conflict') await reload()\n else setStatus('ready')\n }\n\n return (\n <details\n className={css['fold']}\n onToggle={(event) => {\n // Load on first open only: a reopened fold keeps the loaded (possibly\n // just-saved) view, and every write is revision-fenced regardless.\n if (event.currentTarget.open && status === 'idle') void reload()\n }}\n >\n <summary className={css['summary']}>{t('title')}</summary>\n <div className={css['body']}>\n {status === 'loading' ? <p className={css['status']}>{t('loading')}</p> : null}\n {status !== 'loading' && view === undefined\n ? (\n <>\n <p className={css['error']}>{failure ?? t('loadFailed')}</p>\n <div className={css['footer']}>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => { void reload() }}>{t('retry')}</Button>\n </div>\n </>\n )\n : null}\n {status !== 'loading' && view !== undefined\n ? (view.models.length === 0\n ? <p className={css['hint']}>{t('empty')}</p>\n : (\n <>\n {view.fromUser ? null : <p className={css['hint']}>{t('inheritsHint')}</p>}\n {rows.map((row, index) => (\n <label key={index} className={css['row']}>\n <span className={css['rowId']}>{rowId(row, index)}</span>\n <select\n className={css['select']}\n value={imageInputChoice(row)}\n aria-label={`${t('title')} ${rowId(row, index)}`}\n disabled={!view.writable || status === 'saving'}\n onChange={(event) => {\n const next = parseImageInputChoice(event.target.value)\n if (next !== undefined) choose(index, next)\n }}\n >\n <option value=\"inherit\">{t('choiceDefault')}</option>\n <option value=\"text\">{t('choiceText')}</option>\n <option value=\"image\">{t('choiceImage')}</option>\n </select>\n </label>\n ))}\n {failure !== undefined ? <p className={css['error']}>{failure}</p> : null}\n <div className={css['footer']}>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={!view.writable || status === 'saving' || !dirty}\n onClick={() => { void submit() }}\n >\n {status === 'saving' ? t('saving') : t('save')}\n </Button>\n {failure !== undefined\n ? (\n <Button variant=\"ghost\" size=\"sm\" disabled={status === 'saving'} onClick={() => { void reload() }}>\n {t('retry')}\n </Button>\n )\n : null}\n {saved ? <p className={css['status']}>{t('saved')}</p> : null}\n {view.writable ? null : <p className={css['hint']}>{t('readOnly')}</p>}\n </div>\n </>\n ))\n : null}\n </div>\n </details>\n )\n}\n","/** Copy dictionaries for the image-input card. */\n\n/** English strings (the key-set source of truth for this pair). */\nexport const en = {\n title: 'Input modalities',\n loading: 'Loading the model list…',\n loadFailed: 'Loading the model configuration failed.',\n retry: 'Retry',\n empty: 'No explicit model list yet — add models in the catalog above, then declare their input modalities here.',\n inheritsHint: 'Showing the inherited model list; saving copies it into your user settings.',\n readOnly: 'The settings document is read-only in this deployment.',\n choiceDefault: 'Provider default',\n choiceText: 'Text only',\n choiceImage: 'Text and image',\n save: 'Save',\n saving: 'Saving…',\n saved: 'Saved. The adapter picks it up on its next request.',\n conflict: 'These settings changed elsewhere while this card was open; the latest values were reloaded.',\n}\n\n/** The settings.models.imageInput namespace key union. */\nexport type ImageInputKey = keyof typeof en\n\n/** Chinese strings (same keys as {@link en}). */\nexport const zh: { [Key in keyof typeof en]: string } = {\n title: '输入模态',\n loading: '正在读取模型列表…',\n loadFailed: '读取模型配置失败。',\n retry: '重试',\n empty: '还没有显式模型列表——请先在上方模型目录中添加模型,再回到这里声明输入模态。',\n inheritsHint: '当前显示的是继承的模型列表;保存会将其复制到你的用户设置层。',\n readOnly: '当前部署的设置文档为只读。',\n choiceDefault: '提供方默认',\n choiceText: '仅文本',\n choiceImage: '文本和图片',\n save: '保存',\n saving: '保存中…',\n saved: '已保存。适配器会在下一次请求时生效。',\n conflict: '这张卡片打开期间,设置已被其他地方改动;已重新加载最新值。',\n}\n","/**\n * Browser half: the per-model image-input fold inside every llm-pi-ai provider\n * card of the Models settings page. The Host half is empty; provider routes\n * are created and edited through the page's own forms, and this plugin only\n * adds the one field those forms do not carry.\n */\n\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\n// Type-only: pulls the ctx.slots service merge (SlotRegistry).\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\n// Type-only: pulls the ctx.locale merge.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the ctx.remote merge (the settings describe/mutate face).\nimport type {} from '@deepseek-ai/dsh-api-remotes/client'\n// Type-only: pulls the 'settings.models.provider-card' SlotMap entry and the\n// ProviderDirectoryEntry owner data.\nimport type {} from '@deepseek-ai/dsh-client-ui-settings-models/client'\nimport { ImageInputController } from './controller.ts'\nimport { ImageInputCard } from './ImageInputCard.tsx'\nimport type { ImageInputFace } from './ImageInputCard.tsx'\nimport { en, zh } from './locales.ts'\nimport type { ImageInputKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** Per-model image-input copy on the Models page. */\n 'settings.models.imageInput': ImageInputKey\n }\n}\n\n/** Dictionary namespace owned by this plugin. */\nconst NS = 'settings.models.imageInput'\n\n/** The effect label prefix. */\nconst PKG = '@jcy2387/dsh-models-input-modalities'\n\n/** Required browser services. */\nexport const inject = ['slots', 'locale', 'remote', 'remote.settings']\n\n/**\n * Register the image-input fold on every llm-pi-ai provider card once the\n * Models section has declared the seat.\n * @param ctx - the plugin's client context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), `${PKG}: dictionaries`)\n const controller = new ImageInputController(ctx)\n const face: ImageInputFace = {\n loadModels: entry => controller.load(entry),\n saveModels: (entry, models, revision) => controller.save(entry, models, revision),\n }\n ctx.slots.inject('settings.models.provider-card', () => ctx.slots.register({\n name: 'settings.models.provider-card',\n key: 'llm-pi-ai',\n locale: NS,\n inject: () => face,\n }, ImageInputCard))\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.cjs","names":["NS","useState","useRef","useCallback","css","Button"],"sources":["../src/client/controller.ts","../src/image-input.ts","../src/client/ImageInputCard.tsx","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/** Settings reads and writes for the image-input card, over the settings Remote. */\n\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\nimport type { SettingsPathOpView } from '@deepseek-ai/dsh-api-remotes/client'\nimport type { ProviderDirectoryEntry } from '@deepseek-ai/dsh-client-ui-settings-models/client'\nimport type { JsonValue } from '@deepseek-ai/dsh-util-values'\nimport type { ModelRow } from '../image-input.ts'\n\n/** The settings namespace every pi-ai provider card addresses. */\nconst NS = 'llm-pi-ai'\n\n/** What one load answers for a provider card. */\nexport interface ImageInputView {\n /** Whether the deployment accepts settings writes at all. */\n writable: boolean\n /** Revision fence the next save must carry. */\n revision: number\n /** The rows to show: the user layer's when it owns the list, else the effective ones. */\n models: readonly ModelRow[]\n /** Whether the shown rows already live in the user layer. */\n fromUser: boolean\n}\n\n/** What one save answered. */\nexport type ImageInputSaveOutcome =\n | { readonly kind: 'written'; readonly revision: number }\n | { readonly kind: 'conflict'; readonly message: string }\n | { readonly kind: 'refused'; readonly message: string }\n\n/** Read one plain-object path; anything off-path answers undefined. */\nfunction at(source: unknown, path: readonly string[]): unknown {\n let current: unknown = source\n for (const key of path) {\n if (typeof current !== 'object' || current === null || Array.isArray(current)) return undefined\n current = (current as Record<string, unknown>)[key]\n }\n return current\n}\n\n/** Coerce a stored models value into open row records. */\nfunction rows(value: unknown): ModelRow[] {\n return Array.isArray(value)\n ? value.map(entry =>\n typeof entry === 'object' && entry !== null && !Array.isArray(entry) ? entry as ModelRow : {})\n : []\n}\n\n/** Joins the settings Remote's document view and fenced writes for one card. */\nexport class ImageInputController {\n /** The cards waiting to hear that this namespace's stored section changed. */\n private readonly listeners = new Set<(revision: number) => void>()\n\n /**\n * @param ctx - the plugin's client context, which declares `remote.settings`\n * in its own `inject`.\n */\n constructor(private readonly ctx: ClientContext) {}\n\n /**\n * Start forwarding this namespace's pushed document invalidations to the\n * cards. The Host emits one per committed write — including the Models page's\n * own model-list edits — so a card never has to poll or wait for a remount.\n * @returns the disposer that withdraws the Remote subscription.\n */\n watch(): () => void {\n return this.ctx.remote.$on('settings/document-updated', (ns, revision) => {\n if (String(ns) !== NS) return\n for (const listener of [...this.listeners]) listener(revision)\n })\n }\n\n /**\n * Subscribe one card to namespace invalidations.\n * @param listener - called with the namespace's new revision on each change.\n * @returns the disposer for this one subscription.\n */\n subscribe(listener: (revision: number) => void): () => void {\n this.listeners.add(listener)\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n /**\n * Read one provider's model rows and the revision fence for writing them.\n * @param entry - the card's directory row (its settings address names the profile).\n * @returns the view, or undefined when the settings face or namespace is unavailable.\n */\n async load(entry: ProviderDirectoryEntry): Promise<ImageInputView | undefined> {\n const response = await this.ctx.remote.settings.describe()\n if (!response.ok) return undefined\n const namespace = response.value.namespaces.find(view => view.ns === NS)\n if (namespace === undefined) return undefined\n const path = [...entry.settingsPath, 'models']\n const userModel = at(namespace.user, path)\n const fromUser = Array.isArray(userModel)\n return {\n writable: response.value.writable,\n revision: namespace.revision,\n models: rows(fromUser ? userModel : at(namespace.value, path)),\n fromUser,\n }\n }\n\n /**\n * Write the rows back as the profile's whole `models` array, under the fence\n * the load answered. The array is replaced by value — the adapter's own\n * semantics — so untouched rows ride along exactly as stored.\n * @param entry - the card's directory row.\n * @param models - the edited rows.\n * @param revision - the fence from the load this draft was opened at.\n * @returns the write outcome the card renders from.\n */\n async save(\n entry: ProviderDirectoryEntry,\n models: readonly ModelRow[],\n revision: number,\n ): Promise<ImageInputSaveOutcome> {\n // The rows came out of a stored JSON document and the edit only ever sets\n // string arrays, so the array is JSON by construction.\n const value = models as unknown as JsonValue\n const ops: SettingsPathOpView[] = [{ op: 'set', path: [...entry.settingsPath, 'models'], value }]\n const response = await this.ctx.remote.settings.mutate(NS, ops, revision)\n if (response.ok) return { kind: 'written', revision: response.value.revision }\n const { code, message } = response.error\n return code === 'settings/conflict' ? { kind: 'conflict', message } : { kind: 'refused', message }\n }\n}\n","/** Pure row helpers for the per-model image-input claim. */\n\n/** One configured model row, structurally open so hidden fields survive an edit. */\nexport type ModelRow = Record<string, unknown>\n\n/** The three image-input states a row's select offers. */\nexport type ImageInputChoice = 'inherit' | 'text' | 'image'\n\n/**\n * The choice a row's stored `input` displays. Absent and empty mean the same\n * inheritance — the installed catalog's modalities, then the route's\n * `defaultInput` — and any list naming `image` is the image-capable claim\n * however else it is spelled.\n * @param row - one stored model row.\n * @returns the choice the row's select shows.\n */\nexport function imageInputChoice(row: ModelRow): ImageInputChoice {\n const value = row['input']\n if (!Array.isArray(value) || value.length === 0) return 'inherit'\n return value.includes('image') ? 'image' : 'text'\n}\n\n/**\n * The row with one choice applied: `inherit` removes the field, the others\n * store exactly the modality list the adapter reads. Every other field,\n * including ones this card never shows, survives.\n * @param row - the row to patch.\n * @param choice - the selected state.\n * @returns a new row carrying the choice.\n */\nexport function withImageInput(row: ModelRow, choice: ImageInputChoice): ModelRow {\n const next = { ...row }\n delete next['input']\n if (choice === 'text') next['input'] = ['text']\n else if (choice === 'image') next['input'] = ['text', 'image']\n return next\n}\n\n/**\n * Read a select's submitted value. The DOM hands over a bare string, so an\n * unrecognized one is refused rather than cast into the union.\n * @param value - the submitted option value.\n * @returns the choice, or undefined for anything else.\n */\nexport function parseImageInputChoice(value: string): ImageInputChoice | undefined {\n return value === 'inherit' || value === 'text' || value === 'image' ? value : undefined\n}\n\n/**\n * The row's model id for labels.\n * @param row - one stored model row.\n * @param index - the row's zero-based position.\n * @returns the id, or a positional name for an id-less row.\n */\nexport function rowId(row: ModelRow, index: number): string {\n const id = row['id']\n return typeof id === 'string' && id.length > 0 ? id : `#${String(index + 1)}`\n}\n","/**\n * One pi-ai provider card's image-input fold: the per-model modality claim the\n * Models page's own form does not carry. The fold loads the provider's stored\n * rows when first opened, edits them locally, and writes the whole `models`\n * array back under the revision fence the load answered — the same array\n * semantics the page's own cards use. A stored change elsewhere on the page (a\n * model added or removed in the catalog above) reaches the fold through the\n * pushed settings invalidation, so the list it shows never waits for the\n * section to remount.\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport { Button } from '@deepseek-ai/dsh-client-ui-primitives'\nimport type { ProviderDirectoryEntry } from '@deepseek-ai/dsh-client-ui-settings-models/client'\nimport type { ImageInputSaveOutcome, ImageInputView } from './controller.ts'\nimport { imageInputChoice, parseImageInputChoice, rowId, withImageInput } from '../image-input.ts'\nimport type { ImageInputChoice, ModelRow } from '../image-input.ts'\nimport css from './styles.module.css'\n\n/** The registration-side face this card receives. */\nexport interface ImageInputFace {\n /** Read the provider's rows and revision fence; undefined when the settings face is unavailable. */\n loadModels(entry: ProviderDirectoryEntry): Promise<ImageInputView | undefined>\n /** Write the rows back under the fence the load answered. */\n saveModels(\n entry: ProviderDirectoryEntry,\n models: readonly ModelRow[],\n revision: number,\n ): Promise<ImageInputSaveOutcome>\n /** Listen for stored changes in the provider namespace; receives its new revision. */\n subscribeChanges(listener: (revision: number) => void): () => void\n}\n\n/** Props the provider-card slot binds. */\nexport type ImageInputCardProps =\n PropsRuntime<'settings.models.provider-card'>\n & PropsLocale<'settings.models.imageInput'>\n & InjectFace<ImageInputFace>\n\n/** Lifecycle of one fold: closed, loading, editable, or writing. */\ntype Status = 'idle' | 'loading' | 'ready' | 'saving'\n\n/**\n * Render the image-input fold of one provider card.\n * @param props - the card's directory row plus the bound face and copy.\n * @returns the fold, or nothing while the provider is still a dormant row.\n */\nexport function ImageInputCard(props: ImageInputCardProps): ReactNode {\n const { provider, configured, t, loadModels, saveModels, subscribeChanges } = props\n const [status, setStatus] = useState<Status>('idle')\n const [view, setView] = useState<ImageInputView | undefined>(undefined)\n const [rows, setRows] = useState<readonly ModelRow[]>([])\n const [failure, setFailure] = useState<string | undefined>(undefined)\n const [saved, setSaved] = useState(false)\n const [open, setOpen] = useState(false)\n // Whether the stored section moved past what this fold shows. A change that\n // cannot be adopted yet — the fold is closed, it holds an unsaved draft, or\n // a newer commit outran the read in flight — parks here until a safe moment\n // takes it.\n const [stale, setStale] = useState(false)\n\n /** Latest read wins: an older response never overwrites a newer one. */\n const generation = useRef(0)\n /**\n * The newest revision this fold holds adopted data for. An announcement at\n * or below it is old news — the card's own committed write's echo included.\n */\n const seen = useRef(0)\n /**\n * The newest revision the namespace has announced. A completed read or\n * write compares against it to tell whether a commit outran it mid-flight.\n */\n const noticed = useRef(0)\n const dirtyRef = useRef(false)\n const savingRef = useRef(false)\n\n const dirty = view !== undefined && JSON.stringify(rows) !== JSON.stringify(view.models)\n useEffect(() => {\n dirtyRef.current = dirty\n }, [dirty])\n useEffect(() => {\n savingRef.current = status === 'saving'\n }, [status])\n\n /**\n * Re-read the provider's rows.\n * @param silent - keep the rendered list and copy in place while reading, for\n * a background refresh the user never asked for.\n */\n const reload = useCallback(async (silent: boolean): Promise<void> => {\n const ticket = ++generation.current\n if (!silent) {\n setStatus('loading')\n setFailure(undefined)\n setSaved(false)\n }\n const loaded = await loadModels(provider)\n if (ticket !== generation.current) return\n if (silent && (dirtyRef.current || savingRef.current)) {\n setStale(true)\n return\n }\n if (loaded === undefined) {\n // A background read that found nothing keeps the last good list and\n // retries at the fold's next open; only an asked-for read may fail visibly.\n if (silent) {\n setStale(true)\n return\n }\n setView(undefined)\n setFailure(t('loadFailed'))\n setStatus('ready')\n return\n }\n seen.current = loaded.revision\n setView(loaded)\n setRows(loaded.models.map(row => ({ ...row })))\n setFailure(undefined)\n // A commit that outran this read mid-flight keeps the notice parked; the\n // adoption effect sees the fresh view and reads again until level.\n setStale(loaded.revision < noticed.current)\n setStatus('ready')\n }, [loadModels, provider, t])\n\n // The page writes the same namespace this fold reads, so every pushed\n // invalidation is the fold's notice that its list moved underneath it.\n useEffect(() => {\n if (configured !== true) return undefined\n return subscribeChanges((revision) => {\n if (revision > noticed.current) noticed.current = revision\n // Data already in hand covers this revision: the card's own write's\n // echo, or a notice whose commit the last adopted read already caught.\n if (revision <= seen.current) return\n setStale(true)\n })\n }, [configured, subscribeChanges])\n\n // An open fold with nothing at stake adopts the change immediately; one that\n // is closed or holds a draft waits for the toggle handler or the fence. The\n // view dep re-runs the check after every completed read, so a read a newer\n // commit outran is followed by another until the fold is level.\n useEffect(() => {\n if (stale && open && status === 'ready' && !dirty) void reload(true)\n }, [stale, open, status, dirty, view, reload])\n\n // A dormant directory row has no profile to read models from; the create\n // card dispatches this seat only after the provider is saved anyway.\n if (configured !== true) return null\n\n const choose = (index: number, choice: ImageInputChoice): void => {\n setRows(current => current.map((row, at) => at === index ? withImageInput(row, choice) : row))\n }\n\n const submit = async (): Promise<void> => {\n if (view === undefined) return\n setStatus('saving')\n const outcome = await saveModels(provider, rows, view.revision)\n if (outcome.kind === 'written') {\n // The fold now holds data for the committed revision, so its own echo\n // is old news; a notice that raced the write stays parked for the\n // adoption effect, which the just-settled draft unlocks.\n seen.current = outcome.revision\n setView({ ...view, revision: outcome.revision, models: rows.map(row => ({ ...row })), fromUser: true })\n setSaved(true)\n setFailure(undefined)\n setStale(noticed.current > outcome.revision)\n setStatus('ready')\n return\n }\n setFailure(outcome.kind === 'conflict' ? t('conflict') : outcome.message)\n if (outcome.kind === 'conflict') await reload(false)\n else setStatus('ready')\n }\n\n return (\n <details\n className={css['fold']}\n onToggle={(event) => {\n const opened = event.currentTarget.open\n setOpen(opened)\n // First open reads; a clean reopen adopts whatever the parked notice\n // held. A reopen over an unsaved draft keeps it: the notice stays\n // parked until the draft settles — saved into a fence conflict, or\n // reverted into a silent re-read.\n if (opened && (status === 'idle' || (stale && !dirty))) void reload(false)\n }}\n >\n <summary className={css['summary']}>{t('title')}</summary>\n <div className={css['body']}>\n {status === 'loading' ? <p className={css['status']}>{t('loading')}</p> : null}\n {status !== 'loading' && view === undefined\n ? (\n <>\n <p className={css['error']}>{failure ?? t('loadFailed')}</p>\n <div className={css['footer']}>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => { void reload(false) }}>{t('retry')}</Button>\n </div>\n </>\n )\n : null}\n {status !== 'loading' && view !== undefined\n ? (view.models.length === 0\n ? <p className={css['hint']}>{t('empty')}</p>\n : (\n <>\n {view.fromUser ? null : <p className={css['hint']}>{t('inheritsHint')}</p>}\n {rows.map((row, index) => (\n <label key={index} className={css['row']}>\n <span className={css['rowId']}>{rowId(row, index)}</span>\n <select\n className={css['select']}\n value={imageInputChoice(row)}\n aria-label={`${t('title')} ${rowId(row, index)}`}\n disabled={!view.writable || status === 'saving'}\n onChange={(event) => {\n const next = parseImageInputChoice(event.target.value)\n if (next !== undefined) choose(index, next)\n }}\n >\n <option value=\"inherit\">{t('choiceDefault')}</option>\n <option value=\"text\">{t('choiceText')}</option>\n <option value=\"image\">{t('choiceImage')}</option>\n </select>\n </label>\n ))}\n {failure !== undefined ? <p className={css['error']}>{failure}</p> : null}\n <div className={css['footer']}>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={!view.writable || status === 'saving' || !dirty}\n onClick={() => { void submit() }}\n >\n {status === 'saving' ? t('saving') : t('save')}\n </Button>\n {failure !== undefined\n ? (\n <Button variant=\"ghost\" size=\"sm\" disabled={status === 'saving'} onClick={() => { void reload(false) }}>\n {t('retry')}\n </Button>\n )\n : null}\n {saved ? <p className={css['status']}>{t('saved')}</p> : null}\n {view.writable ? null : <p className={css['hint']}>{t('readOnly')}</p>}\n </div>\n </>\n ))\n : null}\n </div>\n </details>\n )\n}\n","/** Copy dictionaries for the image-input card. */\n\n/** English strings (the key-set source of truth for this pair). */\nexport const en = {\n title: 'Input modalities',\n loading: 'Loading the model list…',\n loadFailed: 'Loading the model configuration failed.',\n retry: 'Retry',\n empty: 'No explicit model list yet — add models in the catalog above, then declare their input modalities here.',\n inheritsHint: 'Showing the inherited model list; saving copies it into your user settings.',\n readOnly: 'The settings document is read-only in this deployment.',\n choiceDefault: 'Provider default',\n choiceText: 'Text only',\n choiceImage: 'Text and image',\n save: 'Save',\n saving: 'Saving…',\n saved: 'Saved. The adapter picks it up on its next request.',\n conflict: 'These settings changed elsewhere while this card was open; the latest values were reloaded.',\n}\n\n/** The settings.models.imageInput namespace key union. */\nexport type ImageInputKey = keyof typeof en\n\n/** Chinese strings (same keys as {@link en}). */\nexport const zh: { [Key in keyof typeof en]: string } = {\n title: '输入模态',\n loading: '正在读取模型列表…',\n loadFailed: '读取模型配置失败。',\n retry: '重试',\n empty: '还没有显式模型列表——请先在上方模型目录中添加模型,再回到这里声明输入模态。',\n inheritsHint: '当前显示的是继承的模型列表;保存会将其复制到你的用户设置层。',\n readOnly: '当前部署的设置文档为只读。',\n choiceDefault: '提供方默认',\n choiceText: '仅文本',\n choiceImage: '文本和图片',\n save: '保存',\n saving: '保存中…',\n saved: '已保存。适配器会在下一次请求时生效。',\n conflict: '这张卡片打开期间,设置已被其他地方改动;已重新加载最新值。',\n}\n","/**\n * Browser half: the per-model image-input fold inside every llm-pi-ai provider\n * card of the Models settings page. The Host half is empty; provider routes\n * are created and edited through the page's own forms, and this plugin only\n * adds the one field those forms do not carry.\n */\n\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\n// Type-only: pulls the ctx.slots service merge (SlotRegistry).\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\n// Type-only: pulls the ctx.locale merge.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the ctx.remote merge (the settings describe/mutate face).\nimport type {} from '@deepseek-ai/dsh-api-remotes/client'\n// Type-only: pulls the 'settings.models.provider-card' SlotMap entry and the\n// ProviderDirectoryEntry owner data.\nimport type {} from '@deepseek-ai/dsh-client-ui-settings-models/client'\nimport { ImageInputController } from './controller.ts'\nimport { ImageInputCard } from './ImageInputCard.tsx'\nimport type { ImageInputFace } from './ImageInputCard.tsx'\nimport { en, zh } from './locales.ts'\nimport type { ImageInputKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** Per-model image-input copy on the Models page. */\n 'settings.models.imageInput': ImageInputKey\n }\n}\n\n/** Dictionary namespace owned by this plugin. */\nconst NS = 'settings.models.imageInput'\n\n/** The effect label prefix. */\nconst PKG = '@jcy2387/dsh-models-input-modalities'\n\n/** Required browser services. */\nexport const inject = ['slots', 'locale', 'remote', 'remote.settings']\n\n/**\n * Register the image-input fold on every llm-pi-ai provider card once the\n * Models section has declared the seat.\n * @param ctx - the plugin's client context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), `${PKG}: dictionaries`)\n const controller = new ImageInputController(ctx)\n ctx.effect(() => controller.watch(), `${PKG}: settings invalidations`)\n const face: ImageInputFace = {\n loadModels: entry => controller.load(entry),\n saveModels: (entry, models, revision) => controller.save(entry, models, revision),\n subscribeChanges: listener => controller.subscribe(listener),\n }\n ctx.slots.inject('settings.models.provider-card', () => ctx.slots.register({\n name: 'settings.models.provider-card',\n key: 'llm-pi-ai',\n locale: NS,\n inject: () => face,\n }, ImageInputCard))\n}\n"],"mappings":"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"}
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
* Models page's own form does not carry. The fold loads the provider's stored
|
|
4
4
|
* rows when first opened, edits them locally, and writes the whole `models`
|
|
5
5
|
* array back under the revision fence the load answered — the same array
|
|
6
|
-
* semantics the page's own cards use.
|
|
6
|
+
* semantics the page's own cards use. A stored change elsewhere on the page (a
|
|
7
|
+
* model added or removed in the catalog above) reaches the fold through the
|
|
8
|
+
* pushed settings invalidation, so the list it shows never waits for the
|
|
9
|
+
* section to remount.
|
|
7
10
|
*/
|
|
8
11
|
import type { ReactNode } from 'react';
|
|
9
12
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
@@ -16,6 +19,8 @@ export interface ImageInputFace {
|
|
|
16
19
|
loadModels(entry: ProviderDirectoryEntry): Promise<ImageInputView | undefined>;
|
|
17
20
|
/** Write the rows back under the fence the load answered. */
|
|
18
21
|
saveModels(entry: ProviderDirectoryEntry, models: readonly ModelRow[], revision: number): Promise<ImageInputSaveOutcome>;
|
|
22
|
+
/** Listen for stored changes in the provider namespace; receives its new revision. */
|
|
23
|
+
subscribeChanges(listener: (revision: number) => void): () => void;
|
|
19
24
|
}
|
|
20
25
|
/** Props the provider-card slot binds. */
|
|
21
26
|
export type ImageInputCardProps = PropsRuntime<'settings.models.provider-card'> & PropsLocale<'settings.models.imageInput'> & InjectFace<ImageInputFace>;
|
|
@@ -27,11 +27,26 @@ export type ImageInputSaveOutcome = {
|
|
|
27
27
|
/** Joins the settings Remote's document view and fenced writes for one card. */
|
|
28
28
|
export declare class ImageInputController {
|
|
29
29
|
private readonly ctx;
|
|
30
|
+
/** The cards waiting to hear that this namespace's stored section changed. */
|
|
31
|
+
private readonly listeners;
|
|
30
32
|
/**
|
|
31
33
|
* @param ctx - the plugin's client context, which declares `remote.settings`
|
|
32
34
|
* in its own `inject`.
|
|
33
35
|
*/
|
|
34
36
|
constructor(ctx: ClientContext);
|
|
37
|
+
/**
|
|
38
|
+
* Start forwarding this namespace's pushed document invalidations to the
|
|
39
|
+
* cards. The Host emits one per committed write — including the Models page's
|
|
40
|
+
* own model-list edits — so a card never has to poll or wait for a remount.
|
|
41
|
+
* @returns the disposer that withdraws the Remote subscription.
|
|
42
|
+
*/
|
|
43
|
+
watch(): () => void;
|
|
44
|
+
/**
|
|
45
|
+
* Subscribe one card to namespace invalidations.
|
|
46
|
+
* @param listener - called with the namespace's new revision on each change.
|
|
47
|
+
* @returns the disposer for this one subscription.
|
|
48
|
+
*/
|
|
49
|
+
subscribe(listener: (revision: number) => void): () => void;
|
|
35
50
|
/**
|
|
36
51
|
* Read one provider's model rows and the revision fence for writing them.
|
|
37
52
|
* @param entry - the card's directory row (its settings address names the profile).
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@jcy2387/dsh-models-input-modalities",
|
|
3
3
|
"displayName": "Models input modalities",
|
|
4
4
|
"description": "DeepSeek Harness Web plugin: per-model input-modality selector on the Models settings page for third-party (pi-ai) providers",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.1",
|
|
6
6
|
"author": "jcy2387",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|