@logictan/dsh-desktop-agent 0.1.6 → 0.1.7

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