@logictan/dsh-desktop-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,835 @@
1
+ /**
2
+ * Browser half: the desktop-agent configuration card inside the Plugins page.
3
+ *
4
+ * This file is already in the client module loader's protocol — it is not a
5
+ * module and imports nothing. `build.mjs` copies it to `lib/client.js`
6
+ * verbatim, which is why `package.json` exports `./client` at that path.
7
+ *
8
+ * The card is registered into the `plugins.row.config` slot under
9
+ * `<bundle>#desktop-agent`, the key the Plugins page derives from the bundle
10
+ * that declares the row.
11
+ *
12
+ * The field list mirrors the schema in `src/config.js`; the two are kept in step
13
+ * by hand because this half ships as plain browser code.
14
+ *
15
+ * Two catalogs feed this card, and they are not interchangeable:
16
+ *
17
+ * - The VISION list comes from this plugin's own loopback route
18
+ * (`/api/dsh-desktop-agent/vision-models`). It has to: the host's
19
+ * `session.modelCatalog` does not carry `inputModalities` at all — its
20
+ * builder maps a fixed set of fields off the resolved model info and drops
21
+ * that one — so the card cannot tell a seeing model from a blind one by
22
+ * asking it.
23
+ * - The HOST catalog still supplies each model's reasoning efforts, which it
24
+ * does carry, so the effort dropdown stays exactly as rich as the host makes
25
+ * it without this card inventing a second source for it.
26
+ *
27
+ * The settings service is `configForms`, addressed by loader ENTRY ID
28
+ * (`configForms.get(entryId)`) rather than bound by namespace. It is injected
29
+ * NESTED inside `apply` rather than declared in `inject`, so a host without the
30
+ * settings UI loses only this card instead of failing the page's boot audit.
31
+ */
32
+ window.__ModuleLoader__.load({
33
+ id: "@logictan/dsh-desktop-agent",
34
+ factory: function (require) {
35
+ var React = require("react");
36
+
37
+ /** Loader entry id this card's form is keyed by. */
38
+ var NS = "desktop-agent";
39
+ /** The patch row this card configures (cordis.patch.yml). */
40
+ var ROW_ID = "desktop-agent";
41
+ /**
42
+ * Bundle package names whose row `ROW_ID` this card configures.
43
+ *
44
+ * The page keys a row's configuration by the package that declares the row.
45
+ * This plugin reaches a profile in one of two shapes — as a dependency of
46
+ * this repository's aggregate bundle, or installed on its own — and the key
47
+ * differs between them. Both are registered; the key whose bundle is not
48
+ * installed never renders, because the page dispatches only the keys its own
49
+ * bundles declare.
50
+ */
51
+ var BUNDLE_NAMES = ["@logictan/dsh-plugins-all", "@logictan/dsh-desktop-agent"];
52
+ /** This plugin's own vision-catalog route. */
53
+ var VISION_MODELS_API = "/api/dsh-desktop-agent/vision-models";
54
+
55
+ var copy = {
56
+ summary: "桌面 Agent 的参数",
57
+ heading: "桌面 Agent",
58
+ intro:
59
+ "用截图作为主要感知、通过 Cua Driver 操作桌面窗口。留空表示继承默认值。需先在 profile 挂载 computer-use 的 cua-driver-native provider。",
60
+ inherit: "继承默认",
61
+ save: "保存",
62
+ saving: "保存中…",
63
+ reset: "恢复默认",
64
+ overridden: "已覆盖",
65
+ unwritable: "当前连接不写回本机设置,无法保存。",
66
+ catalogLoading: "正在读取视觉模型目录…",
67
+ catalogError: "视觉模型目录读取失败,可切换为手填。",
68
+ catalogPartial: "部分 provider 读取失败,列表可能不完整。",
69
+ catalogEmpty: "没有声明支持图像的模型,可切换为手填。",
70
+ catalogFallback: "无图像能力的模型会退回 AX 元素表通道。",
71
+ effortNone: "该模型没有 reasoning effort",
72
+ manual: "手填",
73
+ picker: "从目录选择",
74
+ routeMissing: "已存的路由不在当前目录中(provider 读取失败或模型已下线),已切换为手填。",
75
+ deliveryBackground: "后台投递(默认)",
76
+ deliveryForeground: "前台投递(画布/游戏)",
77
+ };
78
+
79
+ /**
80
+ * Editable fields, in render order. `kind` picks the control.
81
+ *
82
+ * The three vision-route keys are rendered together below the catalog picker
83
+ * and are listed here only so that override detection, save, and reset see
84
+ * them.
85
+ */
86
+ var FIELDS = [
87
+ {
88
+ key: "maxSteps",
89
+ label: "步数上限",
90
+ kind: "number",
91
+ min: 1,
92
+ step: 1,
93
+ hint: "一次任务最多执行多少个动作,默认 40。",
94
+ },
95
+ {
96
+ key: "maxImageDimension",
97
+ label: "截图长边上限",
98
+ kind: "number",
99
+ min: 200,
100
+ step: 1,
101
+ hint: "送给模型的截图最长边(像素),默认 1568。调小可省 token,但太小会看不清控件。",
102
+ },
103
+ {
104
+ key: "deliveryMode",
105
+ label: "投递方式",
106
+ kind: "select",
107
+ options: [
108
+ { value: "background", label: copy.deliveryBackground },
109
+ { value: "foreground", label: copy.deliveryForeground },
110
+ ],
111
+ hint: "后台投递不抢焦点;游戏等自绘表面会过滤按 pid 路由的事件,需要前台投递。",
112
+ },
113
+ ];
114
+
115
+ /** The three vision route fields, rendered together below the catalog. */
116
+ var ROUTE_KEYS = ["visionProvider", "visionModel", "visionReasoningEffort"];
117
+
118
+ var wrapStyle = {
119
+ display: "flex",
120
+ flexDirection: "column",
121
+ gap: "12px",
122
+ maxWidth: "620px",
123
+ color: "var(--dsw-alias-label-primary)",
124
+ fontSize: "13px",
125
+ lineHeight: 1.6,
126
+ };
127
+ var headingStyle = { fontSize: "14px", fontWeight: 600 };
128
+ var hintStyle = { color: "var(--dsw-alias-label-tertiary)" };
129
+ var rowStyle = { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" };
130
+ var fieldStyle = { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" };
131
+ var labelStyle = { minWidth: "140px" };
132
+ var controlStyle = {
133
+ padding: "6px 8px",
134
+ border: "1px solid var(--dsw-alias-border-l2)",
135
+ borderRadius: "6px",
136
+ background: "var(--dsw-alias-bg-layer-1)",
137
+ color: "inherit",
138
+ font: "inherit",
139
+ fontVariantNumeric: "tabular-nums",
140
+ };
141
+ var inputStyle = Object.assign({ width: "132px" }, controlStyle);
142
+ var textStyle = Object.assign({ width: "260px" }, controlStyle);
143
+ var selectStyle = Object.assign({ minWidth: "200px" }, controlStyle);
144
+ var buttonStyle = {
145
+ padding: "6px 12px",
146
+ border: "1px solid var(--dsw-alias-border-l2)",
147
+ borderRadius: "6px",
148
+ background: "var(--dsw-alias-bg-layer-1)",
149
+ color: "inherit",
150
+ font: "inherit",
151
+ cursor: "pointer",
152
+ };
153
+ var primaryButtonStyle = Object.assign({}, buttonStyle, {
154
+ border: "1px solid var(--dsw-alias-button-primary-fill)",
155
+ background: "var(--dsw-alias-button-primary-fill)",
156
+ color: "var(--dsw-alias-label-primary-foreground)",
157
+ });
158
+ var errorStyle = { color: "var(--dsw-alias-state-error-primary)" };
159
+ var badgeStyle = {
160
+ padding: "1px 6px",
161
+ borderRadius: "999px",
162
+ background: "var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.08))",
163
+ color: "var(--dsw-alias-label-secondary)",
164
+ fontSize: "12px",
165
+ };
166
+
167
+ function has(object, key) {
168
+ return Object.prototype.hasOwnProperty.call(object, key);
169
+ }
170
+
171
+ function objectLayer(snapshot, which) {
172
+ var layer = snapshot && snapshot[which];
173
+ return layer !== null && typeof layer === "object" ? layer : {};
174
+ }
175
+
176
+ /** The schema-resolved section the Host last accepted. */
177
+ function accepted(snapshot) {
178
+ return objectLayer(snapshot, "value");
179
+ }
180
+
181
+ /** The raw user layer; a key's presence here is what marks it overridden. */
182
+ function userLayer(snapshot) {
183
+ return objectLayer(snapshot, "user");
184
+ }
185
+
186
+ function overridden(snapshot, key) {
187
+ return has(userLayer(snapshot), key);
188
+ }
189
+
190
+ /** The effective value for a field, as the string a control holds. */
191
+ function stored(snapshot, key) {
192
+ var value = accepted(snapshot)[key];
193
+ return value === undefined || value === null ? "" : String(value);
194
+ }
195
+
196
+ /**
197
+ * Parse a draft string into the value to store.
198
+ * @throws {Error} when the draft does not satisfy the field's schema.
199
+ */
200
+ function parse(field, raw) {
201
+ if (field.kind === "text") return raw;
202
+ if (field.kind === "select") return raw;
203
+ var value = Number(raw);
204
+ if (!isFinite(value)) throw new Error(field.label + "必须是数字。");
205
+ if (field.step === 1 && !Number.isInteger(value)) throw new Error(field.label + "必须是整数。");
206
+ if (field.min !== undefined && value < field.min) throw new Error(field.label + "不能小于 " + field.min + "。");
207
+ if (field.max !== undefined && value > field.max) throw new Error(field.label + "不能大于 " + field.max + "。");
208
+ return value;
209
+ }
210
+
211
+ /**
212
+ * Load this plugin's own vision-capable model list.
213
+ *
214
+ * A failed load must not disable the card — the route can always be typed by
215
+ * hand — so a failure only changes the status line and the manual fallback.
216
+ */
217
+ function useVisionCatalog(ctx) {
218
+ var state = React.useState({ status: "loading", groups: [], failures: 0 });
219
+ var value = state[0];
220
+ var setValue = state[1];
221
+ var generation = React.useRef(0);
222
+
223
+ var load = React.useCallback(function () {
224
+ var mine = ++generation.current;
225
+ setValue(function (previous) {
226
+ return { status: "loading", groups: previous.groups, failures: previous.failures };
227
+ });
228
+ Promise.resolve()
229
+ .then(function () {
230
+ return fetch(VISION_MODELS_API, { method: "GET", headers: { accept: "application/json" } });
231
+ })
232
+ .then(function (response) {
233
+ if (!response.ok) throw new Error("HTTP " + response.status);
234
+ return response.json();
235
+ })
236
+ .then(
237
+ function (body) {
238
+ if (mine !== generation.current) return;
239
+ setValue({
240
+ status: "ready",
241
+ groups: (body && body.groups) || [],
242
+ failures: ((body && body.failures) || []).length,
243
+ });
244
+ },
245
+ function () {
246
+ if (mine !== generation.current) return;
247
+ setValue({ status: "error", groups: [], failures: 0 });
248
+ },
249
+ );
250
+ }, []);
251
+
252
+ React.useEffect(
253
+ function () {
254
+ load();
255
+ var offAdapters = ctx.remote.$on("llm/adapters-updated", load);
256
+ var offSettings = ctx.remote.$on("settings/document-updated", load);
257
+ return function () {
258
+ offAdapters();
259
+ offSettings();
260
+ };
261
+ },
262
+ [ctx, load],
263
+ );
264
+
265
+ return { value: value, reload: load };
266
+ }
267
+
268
+ /**
269
+ * Load the host's model directory, which is where reasoning efforts live.
270
+ *
271
+ * Only the effort list is taken from here: the vision filter comes from this
272
+ * plugin's own route, because this catalog cannot express it.
273
+ */
274
+ function useHostCatalog(ctx) {
275
+ var state = React.useState({ groups: [] });
276
+ var value = state[0];
277
+ var setValue = state[1];
278
+
279
+ React.useEffect(
280
+ function () {
281
+ var live = true;
282
+ function load() {
283
+ Promise.resolve()
284
+ .then(function () {
285
+ return ctx.remote.session.modelCatalog();
286
+ })
287
+ .then(
288
+ function (response) {
289
+ if (!live) return;
290
+ if (response && response.ok) setValue({ groups: response.value.groups || [] });
291
+ },
292
+ function () {},
293
+ );
294
+ }
295
+ load();
296
+ var offAdapters = ctx.remote.$on("llm/adapters-updated", load);
297
+ return function () {
298
+ live = false;
299
+ offAdapters();
300
+ };
301
+ },
302
+ [ctx],
303
+ );
304
+
305
+ return value;
306
+ }
307
+
308
+ /** The catalog entry matching a stored route, when it is still present. */
309
+ function matchModel(groups, provider, model) {
310
+ var found = null;
311
+ groups.forEach(function (group) {
312
+ if (group.id !== provider) return;
313
+ (group.models || []).forEach(function (entry) {
314
+ if (entry.id === model) found = entry;
315
+ });
316
+ });
317
+ return found;
318
+ }
319
+
320
+ /**
321
+ * The row's configuration card.
322
+ *
323
+ * `scope` is the configuration form `apply` obtained for this entry from
324
+ * `configForms.get(NS)`; `ctx` is the scoped context it was obtained on,
325
+ * which supplies both catalogs.
326
+ */
327
+ function DesktopAgentSettingsCard(props) {
328
+ var scope = props.scope;
329
+ var ctx = props.ctx;
330
+
331
+ var subscribe = React.useCallback(
332
+ function (onChange) {
333
+ return scope.subscribe(onChange);
334
+ },
335
+ [scope],
336
+ );
337
+ var getSnapshot = React.useCallback(
338
+ function () {
339
+ return scope.getSnapshot();
340
+ },
341
+ [scope],
342
+ );
343
+ var snapshot = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
344
+
345
+ var catalog = useVisionCatalog(ctx);
346
+ var host = useHostCatalog(ctx);
347
+
348
+ var draftState = React.useState({});
349
+ var draft = draftState[0];
350
+ var setDraft = draftState[1];
351
+ var busyState = React.useState(false);
352
+ var busy = busyState[0];
353
+ var setBusy = busyState[1];
354
+ var errorState = React.useState(null);
355
+ var error = errorState[0];
356
+ var setError = errorState[1];
357
+ /** Whether the user chose to type the route instead of picking it. */
358
+ var manualState = React.useState(false);
359
+ var manual = manualState[0];
360
+ var setManual = manualState[1];
361
+
362
+ if (props.view === "summary") return copy.summary;
363
+
364
+ var writable = snapshot.writable === true;
365
+ var current = accepted(snapshot);
366
+
367
+ /** Record one field's draft, leaving the others untouched. */
368
+ function edit(key, value) {
369
+ setDraft(function (previous) {
370
+ var next = {};
371
+ for (var name in previous) if (has(previous, name)) next[name] = previous[name];
372
+ next[key] = value;
373
+ return next;
374
+ });
375
+ }
376
+
377
+ /**
378
+ * Stage several fields at once.
379
+ *
380
+ * Picking a provider clears the model and the effort, because both belong
381
+ * to the previous provider and would otherwise be saved as a route the
382
+ * Host cannot resolve.
383
+ */
384
+ function editMany(patch) {
385
+ setDraft(function (previous) {
386
+ var next = {};
387
+ for (var name in previous) if (has(previous, name)) next[name] = previous[name];
388
+ for (var key in patch) if (has(patch, key)) next[key] = patch[key];
389
+ return next;
390
+ });
391
+ }
392
+
393
+ var allFields = FIELDS.concat([
394
+ { key: "visionProvider", label: "视觉 provider", kind: "text" },
395
+ { key: "visionModel", label: "视觉 model", kind: "text" },
396
+ { key: "visionReasoningEffort", label: "视觉 reasoning effort", kind: "text" },
397
+ ]);
398
+
399
+ /** Whether a field currently carries a user override. */
400
+ function isOverridden(field) {
401
+ return overridden(snapshot, field.key);
402
+ }
403
+
404
+ // Every field whose draft no longer matches what is stored. A draft that
405
+ // is an empty string means "clear the override", which is a real edit
406
+ // whenever a value is currently in effect.
407
+ var edits = [];
408
+ var invalid = null;
409
+ allFields.forEach(function (field) {
410
+ if (!has(draft, field.key)) return;
411
+ var raw = draft[field.key];
412
+ if (raw === "") {
413
+ if (isOverridden(field)) edits.push({ field: field, raw: raw });
414
+ return;
415
+ }
416
+ var value;
417
+ try {
418
+ value = parse(field, raw);
419
+ } catch (cause) {
420
+ if (invalid === null) invalid = String((cause && cause.message) || cause);
421
+ return;
422
+ }
423
+ if (String(value) !== String(current[field.key])) edits.push({ field: field, raw: raw, value: value });
424
+ });
425
+
426
+ var anyOverridden = allFields.some(isOverridden);
427
+ var dirty = edits.length > 0;
428
+ var blocked = busy || invalid !== null;
429
+
430
+ /** Persist every edit, in order, one write at a time. */
431
+ function save() {
432
+ if (!dirty || blocked) return;
433
+ setError(null);
434
+ setBusy(true);
435
+ var chain = Promise.resolve();
436
+ edits.forEach(function (item) {
437
+ chain = chain.then(function () {
438
+ return item.raw === "" ? scope.unset(item.field.key) : scope.set(item.field.key, item.value);
439
+ });
440
+ });
441
+ chain.then(
442
+ function () {
443
+ setBusy(false);
444
+ setDraft({});
445
+ },
446
+ function (cause) {
447
+ setBusy(false);
448
+ setError(String((cause && cause.message) || cause));
449
+ },
450
+ );
451
+ }
452
+
453
+ /** Drop every override so the fields re-inherit the composition values. */
454
+ function reset() {
455
+ if (busy) return;
456
+ var keys = allFields
457
+ .filter(function (field) {
458
+ return isOverridden(field);
459
+ })
460
+ .map(function (field) {
461
+ return field.key;
462
+ });
463
+ if (keys.length === 0) return;
464
+ setError(null);
465
+ setDraft({});
466
+ setBusy(true);
467
+ var chain = Promise.resolve();
468
+ keys.forEach(function (key) {
469
+ chain = chain.then(function () {
470
+ return scope.unset(key);
471
+ });
472
+ });
473
+ chain.then(
474
+ function () {
475
+ setBusy(false);
476
+ },
477
+ function (cause) {
478
+ setBusy(false);
479
+ setError(String((cause && cause.message) || cause));
480
+ },
481
+ );
482
+ }
483
+
484
+ /** The draft for a key, or the stored value. */
485
+ function rawOf(key) {
486
+ return has(draft, key) ? draft[key] : stored(snapshot, key);
487
+ }
488
+
489
+ var children = [
490
+ React.createElement("div", { key: "heading", style: headingStyle }, copy.heading),
491
+ React.createElement("div", { key: "intro" }, copy.intro),
492
+ ];
493
+
494
+ FIELDS.forEach(function (field) {
495
+ var disabled = !writable || busy;
496
+ var control;
497
+ if (field.kind === "select") {
498
+ control = React.createElement(
499
+ "select",
500
+ {
501
+ style: selectStyle,
502
+ value: rawOf(field.key),
503
+ disabled: disabled,
504
+ "aria-label": field.label,
505
+ onChange: function (event) {
506
+ edit(field.key, event.target.value);
507
+ },
508
+ },
509
+ field.options.map(function (option) {
510
+ return React.createElement("option", { key: option.value, value: option.value }, option.label);
511
+ }),
512
+ );
513
+ } else if (field.kind === "number") {
514
+ control = React.createElement("input", {
515
+ type: "number",
516
+ min: field.min,
517
+ max: field.max,
518
+ step: field.step,
519
+ style: inputStyle,
520
+ value: rawOf(field.key),
521
+ disabled: disabled,
522
+ placeholder: copy.inherit,
523
+ "aria-label": field.label,
524
+ onChange: function (event) {
525
+ edit(field.key, event.target.value);
526
+ },
527
+ onKeyDown: function (event) {
528
+ if (event.key === "Enter") save();
529
+ },
530
+ });
531
+ } else {
532
+ control = React.createElement("input", {
533
+ type: "text",
534
+ style: textStyle,
535
+ value: rawOf(field.key),
536
+ disabled: disabled,
537
+ placeholder: copy.inherit,
538
+ "aria-label": field.label,
539
+ onChange: function (event) {
540
+ edit(field.key, event.target.value);
541
+ },
542
+ onKeyDown: function (event) {
543
+ if (event.key === "Enter") save();
544
+ },
545
+ });
546
+ }
547
+
548
+ children.push(
549
+ React.createElement(
550
+ "div",
551
+ { key: field.key, style: fieldStyle },
552
+ React.createElement("span", { style: labelStyle }, field.label),
553
+ control,
554
+ isOverridden(field) ? React.createElement("span", { style: badgeStyle }, copy.overridden) : null,
555
+ ),
556
+ );
557
+ if (field.hint) {
558
+ children.push(React.createElement("div", { key: field.key + "-hint", style: hintStyle }, field.hint));
559
+ }
560
+ });
561
+
562
+ children.push(
563
+ React.createElement("hr", {
564
+ key: "route-sep",
565
+ style: { border: "none", borderTop: "1px solid var(--dsw-alias-border-l2)", margin: "4px 0" },
566
+ }),
567
+ );
568
+ children.push(React.createElement("div", { key: "route-heading", style: headingStyle }, "视觉模型路由"));
569
+ children.push(
570
+ React.createElement(
571
+ "div",
572
+ { key: "route-hint", style: hintStyle },
573
+ "每步决策用哪个模型看图。全空 = 用会话当前模型;会话模型不支持图像时自动走 AX 元素表。",
574
+ ),
575
+ );
576
+
577
+ var groups = catalog.value.groups || [];
578
+ var provider = rawOf("visionProvider");
579
+ var model = rawOf("visionModel");
580
+ var selected = matchModel(groups, provider, model);
581
+ var providers = [];
582
+ groups.forEach(function (group) {
583
+ if (providers.indexOf(group.id) === -1) providers.push(group.id);
584
+ });
585
+ var modelsForProvider = [];
586
+ groups.forEach(function (group) {
587
+ if (group.id !== provider) return;
588
+ (group.models || []).forEach(function (entry) {
589
+ modelsForProvider.push(entry);
590
+ });
591
+ });
592
+
593
+ // The dropdown is usable only when the catalog loaded AND a provider is
594
+ // available. A stored route the catalog no longer offers (a provider that
595
+ // failed to load, or a model removed upstream) is never silently
596
+ // rewritten: the card falls back to manual entry and says so.
597
+ var pickable = catalog.value.status === "ready" && providers.length > 0;
598
+ var usePicker = pickable && !manual;
599
+
600
+ if (catalog.value.status === "loading") {
601
+ children.push(React.createElement("div", { key: "catalog-status", style: hintStyle }, copy.catalogLoading));
602
+ } else if (catalog.value.status === "error") {
603
+ children.push(React.createElement("div", { key: "catalog-status", style: errorStyle }, copy.catalogError));
604
+ } else if (providers.length === 0) {
605
+ children.push(React.createElement("div", { key: "catalog-status", style: hintStyle }, copy.catalogEmpty));
606
+ } else if (catalog.value.failures > 0) {
607
+ children.push(React.createElement("div", { key: "catalog-status", style: hintStyle }, copy.catalogPartial));
608
+ }
609
+
610
+ if (usePicker) {
611
+ children.push(
612
+ React.createElement(
613
+ "div",
614
+ { key: "route-provider", style: fieldStyle },
615
+ React.createElement("span", { style: labelStyle }, "provider"),
616
+ React.createElement(
617
+ "select",
618
+ {
619
+ style: selectStyle,
620
+ value: provider,
621
+ disabled: !writable || busy,
622
+ "aria-label": "视觉 provider",
623
+ onChange: function (event) {
624
+ // The previous model and effort belong to the previous
625
+ // provider, so they are cleared together with it.
626
+ editMany({ visionProvider: event.target.value, visionModel: "", visionReasoningEffort: "" });
627
+ },
628
+ },
629
+ [React.createElement("option", { key: "", value: "" }, copy.inherit)].concat(
630
+ providers.map(function (id) {
631
+ return React.createElement("option", { key: id, value: id }, id);
632
+ }),
633
+ ),
634
+ ),
635
+ overridden(snapshot, "visionProvider")
636
+ ? React.createElement("span", { style: badgeStyle }, copy.overridden)
637
+ : null,
638
+ ),
639
+ );
640
+ children.push(
641
+ React.createElement(
642
+ "div",
643
+ { key: "route-model", style: fieldStyle },
644
+ React.createElement("span", { style: labelStyle }, "model"),
645
+ React.createElement(
646
+ "select",
647
+ {
648
+ style: selectStyle,
649
+ value: selected === null ? "" : model,
650
+ disabled: !writable || busy || provider === "",
651
+ "aria-label": "视觉 model",
652
+ onChange: function (event) {
653
+ editMany({ visionModel: event.target.value, visionReasoningEffort: "" });
654
+ },
655
+ },
656
+ [React.createElement("option", { key: "", value: "" }, copy.inherit)].concat(
657
+ modelsForProvider.map(function (entry) {
658
+ return React.createElement("option", { key: entry.id, value: entry.id }, entry.name || entry.id);
659
+ }),
660
+ ),
661
+ ),
662
+ overridden(snapshot, "visionModel") ? React.createElement("span", { style: badgeStyle }, copy.overridden) : null,
663
+ ),
664
+ );
665
+
666
+ // The effort list comes from the HOST catalog, which does carry it; the
667
+ // vision route above only decides which models may appear at all.
668
+ var hostEntry = matchModel(host.groups, provider, model);
669
+ var reasoning = (hostEntry && hostEntry.reasoning) || null;
670
+ var effortOptions = reasoning ? reasoning.efforts || [] : [];
671
+ if (effortOptions.length > 0) {
672
+ var effortValue = rawOf("visionReasoningEffort") || reasoning.defaultEffort || "";
673
+ children.push(
674
+ React.createElement(
675
+ "div",
676
+ { key: "route-effort", style: fieldStyle },
677
+ React.createElement("span", { style: labelStyle }, "reasoning effort"),
678
+ React.createElement(
679
+ "select",
680
+ {
681
+ style: selectStyle,
682
+ value: effortValue,
683
+ disabled: !writable || busy,
684
+ "aria-label": "视觉 reasoning effort",
685
+ onChange: function (event) {
686
+ edit("visionReasoningEffort", event.target.value);
687
+ },
688
+ },
689
+ [React.createElement("option", { key: "", value: "" }, copy.inherit)].concat(
690
+ effortOptions.map(function (effort) {
691
+ return React.createElement("option", { key: effort.id, value: effort.id }, effort.name || effort.id);
692
+ }),
693
+ ),
694
+ ),
695
+ overridden(snapshot, "visionReasoningEffort")
696
+ ? React.createElement("span", { style: badgeStyle }, copy.overridden)
697
+ : null,
698
+ ),
699
+ );
700
+ } else if (reasoning) {
701
+ children.push(
702
+ React.createElement("div", { key: "route-effort", style: hintStyle }, "reasoning effort:" + copy.effortNone),
703
+ );
704
+ }
705
+
706
+ if (selected === null && (provider !== "" || model !== "")) {
707
+ children.push(React.createElement("div", { key: "route-missing", style: hintStyle }, copy.routeMissing));
708
+ }
709
+ } else {
710
+ // Manual entry, also the fallback when the catalog failed: the card must
711
+ // stay usable when a provider is unreachable.
712
+ ROUTE_KEYS.forEach(function (key) {
713
+ var label = key === "visionProvider" ? "provider" : key === "visionModel" ? "model" : "reasoning effort";
714
+ children.push(
715
+ React.createElement(
716
+ "div",
717
+ { key: "route-" + key, style: fieldStyle },
718
+ React.createElement("span", { style: labelStyle }, label),
719
+ React.createElement("input", {
720
+ type: "text",
721
+ style: textStyle,
722
+ value: rawOf(key),
723
+ disabled: !writable || busy,
724
+ placeholder: copy.inherit,
725
+ "aria-label": "视觉 " + label,
726
+ onChange: function (event) {
727
+ edit(key, event.target.value);
728
+ },
729
+ onKeyDown: function (event) {
730
+ if (event.key === "Enter") save();
731
+ },
732
+ }),
733
+ overridden(snapshot, key) ? React.createElement("span", { style: badgeStyle }, copy.overridden) : null,
734
+ ),
735
+ );
736
+ });
737
+ }
738
+
739
+ if (pickable) {
740
+ children.push(
741
+ React.createElement(
742
+ "div",
743
+ { key: "route-mode", style: rowStyle },
744
+ React.createElement(
745
+ "button",
746
+ {
747
+ type: "button",
748
+ style: buttonStyle,
749
+ onClick: function () {
750
+ setManual(!manual);
751
+ },
752
+ },
753
+ manual ? copy.picker : copy.manual,
754
+ ),
755
+ ),
756
+ );
757
+ }
758
+
759
+ children.push(React.createElement("div", { key: "route-fallback", style: hintStyle }, copy.catalogFallback));
760
+
761
+ if (invalid !== null) children.push(React.createElement("div", { key: "invalid", style: errorStyle }, invalid));
762
+ if (error !== null) children.push(React.createElement("div", { key: "error", style: errorStyle }, error));
763
+ if (!writable && snapshot.status === "ready") {
764
+ children.push(React.createElement("div", { key: "unwritable", style: errorStyle }, copy.unwritable));
765
+ }
766
+
767
+ children.push(
768
+ React.createElement(
769
+ "div",
770
+ { key: "actions", style: rowStyle },
771
+ React.createElement(
772
+ "button",
773
+ {
774
+ type: "button",
775
+ style: dirty && writable && !blocked ? primaryButtonStyle : buttonStyle,
776
+ disabled: !dirty || !writable || blocked,
777
+ onClick: save,
778
+ },
779
+ busy ? copy.saving : copy.save,
780
+ ),
781
+ React.createElement(
782
+ "button",
783
+ {
784
+ type: "button",
785
+ style: buttonStyle,
786
+ disabled: !writable || busy || !anyOverridden,
787
+ onClick: reset,
788
+ },
789
+ copy.reset,
790
+ ),
791
+ ),
792
+ );
793
+
794
+ return React.createElement("div", { style: wrapStyle }, children);
795
+ }
796
+
797
+ /**
798
+ * Client plugin body: bind the configuration form and register the card.
799
+ *
800
+ * `configForms` is injected NESTED rather than declared in this plugin's
801
+ * `inject` list. A plugin that waits on a service the profile does not
802
+ * provide never activates, and the Web UI's boot audit turns that into a hard
803
+ * failure for the WHOLE page ("web boot: N entries did not activate"), not
804
+ * just for this card. Nested, a host without the settings UI loses only this
805
+ * card.
806
+ *
807
+ * @param ctx - client root context.
808
+ */
809
+ function apply(ctx) {
810
+ ctx.inject(["configForms"], function (scoped) {
811
+ var scope = scoped.configForms.get(NS);
812
+ BUNDLE_NAMES.forEach(function (bundle) {
813
+ scoped.slots.inject("plugins.row.config", function () {
814
+ return scoped.slots.register(
815
+ {
816
+ name: "plugins.row.config",
817
+ key: bundle + "#" + ROW_ID,
818
+ inject: function () {
819
+ return { scope: scope, ctx: scoped };
820
+ },
821
+ },
822
+ DesktopAgentSettingsCard,
823
+ );
824
+ });
825
+ });
826
+ });
827
+ }
828
+
829
+ return {
830
+ name: "dsh-desktop-agent",
831
+ inject: ["slots", "remote", "remote.session"],
832
+ apply: apply,
833
+ };
834
+ },
835
+ });