@anweat/dsh-browser 0.1.17 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +266 -88
  2. package/assets/skills/dsh-browser/SKILL.md +120 -0
  3. package/assets/skills/dsh-browser/references/develop.md +48 -0
  4. package/assets/skills/dsh-browser/references/observe.md +91 -0
  5. package/assets/skills/dsh-browser/references/opencli.md +25 -0
  6. package/assets/skills/dsh-browser/references/recipes.md +149 -0
  7. package/assets/skills/dsh-browser/references/userscripts.md +44 -0
  8. package/cordis.patch.yml +11 -3
  9. package/lib/actions/act.d.ts +3 -0
  10. package/lib/actions/act.js +147 -0
  11. package/lib/actions/act.js.map +1 -0
  12. package/lib/actions/automation.d.ts +3 -0
  13. package/lib/actions/automation.js +356 -0
  14. package/lib/actions/automation.js.map +1 -0
  15. package/lib/actions/crawl.d.ts +3 -0
  16. package/lib/actions/crawl.js +28 -0
  17. package/lib/actions/crawl.js.map +1 -0
  18. package/lib/actions/errors.d.ts +53 -0
  19. package/lib/actions/errors.js +178 -0
  20. package/lib/actions/errors.js.map +1 -0
  21. package/lib/actions/index-view.d.ts +42 -0
  22. package/lib/actions/index-view.js +268 -0
  23. package/lib/actions/index-view.js.map +1 -0
  24. package/lib/actions/inspect.d.ts +3 -0
  25. package/lib/actions/inspect.js +35 -0
  26. package/lib/actions/inspect.js.map +1 -0
  27. package/lib/actions/observe.d.ts +3 -0
  28. package/lib/actions/observe.js +121 -0
  29. package/lib/actions/observe.js.map +1 -0
  30. package/lib/actions/opencli.d.ts +3 -0
  31. package/lib/actions/opencli.js +51 -0
  32. package/lib/actions/opencli.js.map +1 -0
  33. package/lib/actions/registry.d.ts +35 -0
  34. package/lib/actions/registry.js +89 -0
  35. package/lib/actions/registry.js.map +1 -0
  36. package/lib/actions/run.d.ts +34 -0
  37. package/lib/actions/run.js +226 -0
  38. package/lib/actions/run.js.map +1 -0
  39. package/lib/actions/runtime.d.ts +3 -0
  40. package/lib/actions/runtime.js +33 -0
  41. package/lib/actions/runtime.js.map +1 -0
  42. package/lib/actions/schema.d.ts +31 -0
  43. package/lib/actions/schema.js +309 -0
  44. package/lib/actions/schema.js.map +1 -0
  45. package/lib/actions/script.d.ts +3 -0
  46. package/lib/actions/script.js +97 -0
  47. package/lib/actions/script.js.map +1 -0
  48. package/lib/actions/shared.d.ts +31 -0
  49. package/lib/actions/shared.js +45 -0
  50. package/lib/actions/shared.js.map +1 -0
  51. package/lib/actions/surface.d.ts +22 -0
  52. package/lib/actions/surface.js +18 -0
  53. package/lib/actions/surface.js.map +1 -0
  54. package/lib/actions/target.d.ts +3 -0
  55. package/lib/actions/target.js +66 -0
  56. package/lib/actions/target.js.map +1 -0
  57. package/lib/actions/types.d.ts +181 -0
  58. package/lib/actions/types.js +48 -0
  59. package/lib/actions/types.js.map +1 -0
  60. package/lib/activation-rules.d.ts +29 -0
  61. package/lib/activation-rules.js +33 -0
  62. package/lib/activation-rules.js.map +1 -0
  63. package/lib/approval-policy.d.ts +40 -2
  64. package/lib/approval-policy.js +184 -97
  65. package/lib/approval-policy.js.map +1 -1
  66. package/lib/automation-assets-rpc.d.ts +8 -2
  67. package/lib/automation-assets-rpc.js +68 -10
  68. package/lib/automation-assets-rpc.js.map +1 -1
  69. package/lib/automation-assets.d.ts +152 -4
  70. package/lib/automation-assets.js +255 -15
  71. package/lib/automation-assets.js.map +1 -1
  72. package/lib/automation-convert.d.ts +23 -0
  73. package/lib/automation-convert.js +110 -0
  74. package/lib/automation-convert.js.map +1 -0
  75. package/lib/automation-development.d.ts +26 -0
  76. package/lib/automation-development.js +47 -0
  77. package/lib/automation-development.js.map +1 -1
  78. package/lib/automation-execution.d.ts +57 -1
  79. package/lib/automation-execution.js +167 -11
  80. package/lib/automation-execution.js.map +1 -1
  81. package/lib/automation-journal.d.ts +92 -0
  82. package/lib/automation-journal.js +449 -0
  83. package/lib/automation-journal.js.map +1 -0
  84. package/lib/automation-v2.d.ts +138 -0
  85. package/lib/automation-v2.js +631 -0
  86. package/lib/automation-v2.js.map +1 -0
  87. package/lib/automation.d.ts +82 -5
  88. package/lib/automation.js +239 -82
  89. package/lib/automation.js.map +1 -1
  90. package/lib/browser-service.d.ts +309 -32
  91. package/lib/browser-service.js +636 -182
  92. package/lib/browser-service.js.map +1 -1
  93. package/lib/client/SettingsCard.js +95 -57
  94. package/lib/client/SettingsCard.js.map +1 -1
  95. package/lib/client/asset-editor.d.ts +55 -0
  96. package/lib/client/asset-editor.js +101 -0
  97. package/lib/client/asset-editor.js.map +1 -0
  98. package/lib/client/automation-assets-client.d.ts +99 -2
  99. package/lib/client/automation-assets-client.js +243 -10
  100. package/lib/client/automation-assets-client.js.map +1 -1
  101. package/lib/client/form.d.ts +48 -4
  102. package/lib/client/form.js +142 -29
  103. package/lib/client/form.js.map +1 -1
  104. package/lib/client/index.d.ts +25 -2
  105. package/lib/client/index.js +5 -1
  106. package/lib/client/index.js.map +1 -1
  107. package/lib/client/locales.d.ts +85 -6
  108. package/lib/client/locales.js +64 -12
  109. package/lib/client/locales.js.map +1 -1
  110. package/lib/client/prompts-form.d.ts +67 -0
  111. package/lib/client/prompts-form.js +140 -0
  112. package/lib/client/prompts-form.js.map +1 -0
  113. package/lib/client/prompts-status-client.d.ts +34 -0
  114. package/lib/client/prompts-status-client.js +53 -0
  115. package/lib/client/prompts-status-client.js.map +1 -0
  116. package/lib/client/settings-namespace.d.ts +1 -1
  117. package/lib/client/settings-namespace.js +1 -1
  118. package/lib/client/settings-namespace.js.map +1 -1
  119. package/lib/client.js +1915 -330
  120. package/lib/client.js.map +1 -1
  121. package/lib/config.d.ts +50 -1
  122. package/lib/config.js +31 -1
  123. package/lib/config.js.map +1 -1
  124. package/lib/dialogs.d.ts +37 -0
  125. package/lib/dialogs.js +73 -0
  126. package/lib/dialogs.js.map +1 -0
  127. package/lib/freedom.d.ts +23 -7
  128. package/lib/freedom.js +48 -59
  129. package/lib/freedom.js.map +1 -1
  130. package/lib/index.d.ts +5 -13
  131. package/lib/index.js +51 -30
  132. package/lib/index.js.map +1 -1
  133. package/lib/journal.d.ts +109 -0
  134. package/lib/journal.js +238 -0
  135. package/lib/journal.js.map +1 -0
  136. package/lib/locator.d.ts +82 -0
  137. package/lib/locator.js +190 -0
  138. package/lib/locator.js.map +1 -0
  139. package/lib/observe-script.d.ts +27 -0
  140. package/lib/observe-script.js +565 -0
  141. package/lib/observe-script.js.map +1 -0
  142. package/lib/observe.d.ts +179 -0
  143. package/lib/observe.js +368 -0
  144. package/lib/observe.js.map +1 -0
  145. package/lib/policy-keys.d.ts +30 -0
  146. package/lib/policy-keys.js +34 -0
  147. package/lib/policy-keys.js.map +1 -0
  148. package/lib/prompt-limits.d.ts +30 -0
  149. package/lib/prompt-limits.js +31 -0
  150. package/lib/prompt-limits.js.map +1 -0
  151. package/lib/prompts.d.ts +131 -0
  152. package/lib/prompts.js +295 -0
  153. package/lib/prompts.js.map +1 -0
  154. package/lib/skill.d.ts +100 -0
  155. package/lib/skill.js +181 -0
  156. package/lib/skill.js.map +1 -0
  157. package/lib/tool-defs.d.ts +72 -0
  158. package/lib/tool-defs.js +67 -0
  159. package/lib/tool-defs.js.map +1 -0
  160. package/lib/tools.d.ts +19 -4
  161. package/lib/tools.js +124 -922
  162. package/lib/tools.js.map +1 -1
  163. package/package.json +15 -11
  164. package/scripts/check-host-rpc.mjs +11 -4
  165. package/scripts/dump-prompts.mjs +29 -0
  166. package/scripts/measure-tool-surface.mjs +190 -0
  167. package/lib/settings-scope.d.ts +0 -56
  168. package/lib/settings-scope.js +0 -51
  169. package/lib/settings-scope.js.map +0 -1
package/lib/client.js CHANGED
@@ -7,6 +7,266 @@ window.__ModuleLoader__.load({
7
7
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
8
8
  let react = require("react");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
+ //#region src/policy-keys.ts
11
+ /**
12
+ * The keys of the two JSON policy objects, in one place.
13
+ *
14
+ * The settings card validates what a person types into the "automation asset policy" and "usage buffer" boxes against
15
+ * these lists and prints them under the boxes, so a key added to a policy is added here once. This module has no
16
+ * imports on purpose: the card bundles it for the browser. test/policy-keys.test.ts holds the lists equal to what the
17
+ * resolvers return and to the Host Config schema.
18
+ * @module dsh-browser/policy-keys
19
+ */
20
+ /** Every key of `automationAssets`, in documentation order. */
21
+ const ASSET_POLICY_KEYS = [
22
+ "enabled",
23
+ "directory",
24
+ "persistenceMode",
25
+ "activationMode",
26
+ "minSuccessfulRuns",
27
+ "minDistinctSessions",
28
+ "successWindowDays",
29
+ "minSuccessRate",
30
+ "maxCandidates",
31
+ "candidateTtlDays",
32
+ "maxSuggestionsPerDay",
33
+ "maxDrafts",
34
+ "maxActiveAssets",
35
+ "retrievalTopK",
36
+ "catalogTokenBudget",
37
+ "modelDevelopmentEnabled",
38
+ "maxModelDraftWritesPerSession",
39
+ "maxTestCredentials",
40
+ "minInputSetsForActivation"
41
+ ];
42
+ /** The keys of `automationAssets` that are not plain non-negative numbers. */
43
+ const ASSET_POLICY_BOOLEAN_KEYS = ["enabled", "modelDevelopmentEnabled"];
44
+ const ASSET_POLICY_STRING_KEYS = ["directory"];
45
+ const ASSET_POLICY_ENUMS = {
46
+ persistenceMode: [
47
+ "off",
48
+ "manual",
49
+ "suggest",
50
+ "auto-draft"
51
+ ],
52
+ activationMode: ["manual", "auto-tested"]
53
+ };
54
+ /** Numeric keys with a range the card enforces itself; the resolver would otherwise clamp silently. */
55
+ const ASSET_POLICY_INTEGER_RANGES = { minInputSetsForActivation: [1, 5] };
56
+ /** `usagePolicy`: every key with the inclusive integer range the resolver enforces. */
57
+ const USAGE_POLICY_BOUNDS = {
58
+ minDelayMs: [0, 6e4],
59
+ maxConcurrency: [1, 8],
60
+ burst: [1, 20],
61
+ maxPagesPerRun: [1, 100],
62
+ maxDepth: [0, 5],
63
+ retryLimit: [0, 5],
64
+ backoffBaseMs: [1, 6e4],
65
+ cooldownMs: [100, 3e5]
66
+ };
67
+ const USAGE_POLICY_KEYS = Object.keys(USAGE_POLICY_BOUNDS);
68
+ /** Length caps (in characters) of each kind of override. A longer value is ignored, not truncated. */
69
+ const PROMPT_LIMITS = {
70
+ /** A group or action summary: one line of the catalog. */
71
+ summary: 300,
72
+ /** The extra guidance of an action or sub-action. */
73
+ notes: 1e3,
74
+ /** The text of a detail topic (`observe.read.controls`), which is a page of its own. */
75
+ topicText: 3500,
76
+ /** A tool description (`prompts.tools.<tool>.description`). */
77
+ description: 1500,
78
+ /** The compact guide that replaces the skill pointer at the root. */
79
+ rootGuide: 1500,
80
+ /** The deployer's note at the end of the root. */
81
+ rootNote: 800,
82
+ /** The hint of one error code. */
83
+ errorHint: 600,
84
+ /** The skill's description in the skill list. */
85
+ skillDescription: 1e3,
86
+ /** Text appended to the skill body. */
87
+ skillAppend: 4e3,
88
+ /** A `skill.bodyFile` larger than this falls back to the packaged body. */
89
+ skillBodyFile: 2e4
90
+ };
91
+ //#endregion
92
+ //#region src/client/prompts-form.ts
93
+ /**
94
+ * Pure helpers for the "prompt text" section of the settings card.
95
+ *
96
+ * The whole `prompts` configuration is one staged JSON field (the host writes top-level fields); the card's separate
97
+ * controls (tool descriptions, root guide and note, the skill) and the JSON box for groups, actions and error hints
98
+ * are two views of that one draft. Nothing here touches the host or the DOM.
99
+ * @module dsh-browser/client/prompts-form
100
+ */
101
+ /** The single-line and multi-line text controls of the section, by id, with the path each edits and its length cap. */
102
+ const PROMPT_TEXT_FIELDS = [
103
+ {
104
+ id: "indexDescription",
105
+ path: [
106
+ "tools",
107
+ "browser_index",
108
+ "description"
109
+ ],
110
+ limit: PROMPT_LIMITS.description,
111
+ multiline: true
112
+ },
113
+ {
114
+ id: "callDescription",
115
+ path: [
116
+ "tools",
117
+ "browser_call",
118
+ "description"
119
+ ],
120
+ limit: PROMPT_LIMITS.description,
121
+ multiline: true
122
+ },
123
+ {
124
+ id: "rootGuide",
125
+ path: ["rootGuide"],
126
+ limit: PROMPT_LIMITS.rootGuide,
127
+ multiline: true
128
+ },
129
+ {
130
+ id: "rootNote",
131
+ path: ["rootNote"],
132
+ limit: PROMPT_LIMITS.rootNote,
133
+ multiline: true
134
+ },
135
+ {
136
+ id: "skillDescription",
137
+ path: ["skill", "description"],
138
+ limit: PROMPT_LIMITS.skillDescription,
139
+ multiline: false
140
+ },
141
+ {
142
+ id: "skillBodyFile",
143
+ path: ["skill", "bodyFile"],
144
+ limit: 1024,
145
+ multiline: false
146
+ },
147
+ {
148
+ id: "skillAppend",
149
+ path: ["skill", "append"],
150
+ limit: PROMPT_LIMITS.skillAppend,
151
+ multiline: true
152
+ }
153
+ ];
154
+ /** The members the JSON box edits; the other controls own the rest. */
155
+ const PROMPT_EXTRA_KEYS = [
156
+ "groups",
157
+ "actions",
158
+ "errorHints"
159
+ ];
160
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
161
+ function getAt(root, path) {
162
+ let node = root;
163
+ for (const key of path) {
164
+ if (!isRecord(node)) return void 0;
165
+ node = node[key];
166
+ }
167
+ return node;
168
+ }
169
+ /** A copy of `root` with `path` set to `value`; an empty or absent value removes it, and parents left empty go with it. */
170
+ function setAt(root, path, value) {
171
+ const copy = structuredClone(root);
172
+ const trail = [copy];
173
+ let node = copy;
174
+ for (const key of path.slice(0, -1)) {
175
+ const next = node[key];
176
+ node = isRecord(next) ? next : node[key] = {};
177
+ trail.push(node);
178
+ }
179
+ const last = path[path.length - 1];
180
+ if (value === void 0 || value === "") delete node[last];
181
+ else node[last] = value;
182
+ for (let depth = path.length - 1; depth > 0; depth--) {
183
+ const parent = trail[depth - 1];
184
+ if (Object.keys(trail[depth]).length === 0) delete parent[path[depth - 1]];
185
+ }
186
+ return copy;
187
+ }
188
+ const stringMap = (value, limit, inner) => isRecord(value) && Object.values(value).every((entry) => {
189
+ if (inner) return isRecord(entry) && Object.entries(entry).every(([key, text]) => inner.includes(key) && typeof text === "string" && text.length <= limit);
190
+ return typeof entry === "string" && entry.length <= limit;
191
+ });
192
+ /** The groups/actions/errorHints box: groups and actions are objects of `{summary, notes}`, errorHints a code-to-text map. */
193
+ function validExtras(value) {
194
+ if (Object.keys(value).some((key) => !PROMPT_EXTRA_KEYS.includes(key))) return false;
195
+ const { groups, actions, errorHints } = value;
196
+ if (groups !== void 0 && !stringMap(groups, PROMPT_LIMITS.summary, ["summary"])) return false;
197
+ if (actions !== void 0 && !(isRecord(actions) && Object.values(actions).every((entry) => isRecord(entry) && Object.entries(entry).every(([key, text]) => (key === "summary" || key === "notes") && typeof text === "string" && text.length <= (key === "summary" ? PROMPT_LIMITS.summary : PROMPT_LIMITS.topicText))))) return false;
198
+ if (errorHints !== void 0 && !stringMap(errorHints, PROMPT_LIMITS.errorHint)) return false;
199
+ return true;
200
+ }
201
+ /**
202
+ * Whether a whole `prompts` value is acceptable to save: known top-level keys, strings within their caps, a boolean
203
+ * `skill.enabled`. Group names, action names and error codes are the plugin's to judge (it reports unknown ones as
204
+ * diagnostics), so they are not checked here.
205
+ */
206
+ function validPrompts(value) {
207
+ const known = [
208
+ "tools",
209
+ "rootGuide",
210
+ "rootNote",
211
+ "groups",
212
+ "actions",
213
+ "errorHints",
214
+ "skill"
215
+ ];
216
+ if (Object.keys(value).some((key) => !known.includes(key))) return false;
217
+ for (const field of PROMPT_TEXT_FIELDS) {
218
+ const entry = getAt(value, field.path);
219
+ if (entry !== void 0 && (typeof entry !== "string" || entry.length > field.limit)) return false;
220
+ }
221
+ const tools = value.tools;
222
+ if (tools !== void 0 && !(isRecord(tools) && Object.keys(tools).every((key) => key === "browser_index" || key === "browser_call"))) return false;
223
+ const skill = value.skill;
224
+ if (skill !== void 0) {
225
+ if (!isRecord(skill) || Object.keys(skill).some((key) => ![
226
+ "enabled",
227
+ "description",
228
+ "bodyFile",
229
+ "append"
230
+ ].includes(key))) return false;
231
+ if (skill.enabled !== void 0 && typeof skill.enabled !== "boolean") return false;
232
+ }
233
+ return validExtras(Object.fromEntries(PROMPT_EXTRA_KEYS.filter((key) => value[key] !== void 0).map((key) => [key, value[key]])));
234
+ }
235
+ /** The JSON box's content for a `prompts` value: its groups, actions and errorHints, or empty when it has none. */
236
+ function extrasText(value) {
237
+ value = canonicalPrompts(value);
238
+ const extras = Object.fromEntries(PROMPT_EXTRA_KEYS.filter((key) => value[key] !== void 0).map((key) => [key, value[key]]));
239
+ return Object.keys(extras).length ? JSON.stringify(extras, null, 2) : "";
240
+ }
241
+ /** A copy of a `prompts` value with groups, actions and errorHints replaced by `extras` (absent members removed). */
242
+ function withExtras(value, extras) {
243
+ const copy = Object.fromEntries(Object.entries(value).filter(([key]) => !PROMPT_EXTRA_KEYS.includes(key)));
244
+ for (const key of PROMPT_EXTRA_KEYS) if (extras[key] !== void 0 && isRecord(extras[key]) && Object.keys(extras[key]).length) copy[key] = extras[key];
245
+ return copy;
246
+ }
247
+ /** Drop empty containers and the `skill.enabled=true` default, so equal settings compare equal whatever defaults the host filled in. */
248
+ function canonicalPrompts(value) {
249
+ const clean = (node) => {
250
+ if (!isRecord(node)) return node;
251
+ const out = {};
252
+ for (const [key, entry] of Object.entries(node)) {
253
+ const next = clean(entry);
254
+ if (isRecord(next) && Object.keys(next).length === 0) continue;
255
+ if (next === void 0 || next === "") continue;
256
+ out[key] = next;
257
+ }
258
+ return out;
259
+ };
260
+ const out = clean(value);
261
+ const skill = out.skill;
262
+ if (isRecord(skill) && skill.enabled === true) {
263
+ const { enabled: _enabled, ...rest } = skill;
264
+ if (Object.keys(rest).length) out.skill = rest;
265
+ else delete out.skill;
266
+ }
267
+ return out;
268
+ }
269
+ //#endregion
10
270
  //#region src/client/form.ts
11
271
  /** A free-text field that clears when emptied, so blanking the control resets it. */
12
272
  const textField = (field) => (0, _deepseek_ai_dsh_client_ui_primitives.settingsTextField)(field);
@@ -68,69 +328,45 @@ window.__ModuleLoader__.load({
68
328
  }
69
329
  }
70
330
  });
71
- const POLICY_BOUNDS = {
72
- minDelayMs: [0, 6e4],
73
- maxConcurrency: [1, 8],
74
- burst: [1, 20],
75
- maxPagesPerRun: [1, 100],
76
- maxDepth: [0, 5],
77
- retryLimit: [0, 5],
78
- backoffBaseMs: [1, 6e4],
79
- cooldownMs: [100, 3e5]
80
- };
81
- const ASSET_POLICY_KEYS = /* @__PURE__ */ new Set([
82
- "enabled",
83
- "directory",
84
- "persistenceMode",
85
- "activationMode",
86
- "minSuccessfulRuns",
87
- "minDistinctSessions",
88
- "successWindowDays",
89
- "minSuccessRate",
90
- "maxCandidates",
91
- "candidateTtlDays",
92
- "maxSuggestionsPerDay",
93
- "maxDrafts",
94
- "maxActiveAssets",
95
- "retrievalTopK",
96
- "catalogTokenBudget",
97
- "modelDevelopmentEnabled",
98
- "maxModelDraftWritesPerSession"
331
+ /** A JSON array of strings (Chromium launch arguments): written compactly on one line, an empty draft clears it. */
332
+ const stringArrayField = (field) => ({
333
+ field,
334
+ format: (value) => Array.isArray(value) && value.length ? JSON.stringify(value) : "",
335
+ parse(text) {
336
+ if (text.trim() === "") return { kind: "clear" };
337
+ try {
338
+ const value = JSON.parse(text);
339
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.length > 0) ? {
340
+ kind: "set",
341
+ value
342
+ } : void 0;
343
+ } catch {
344
+ return;
345
+ }
346
+ }
347
+ });
348
+ const ASSET_KEYS = new Set(ASSET_POLICY_KEYS);
349
+ const NON_NUMERIC_ASSET_KEYS = /* @__PURE__ */ new Set([
350
+ ...ASSET_POLICY_BOOLEAN_KEYS,
351
+ ...ASSET_POLICY_STRING_KEYS,
352
+ ...Object.keys(ASSET_POLICY_ENUMS)
99
353
  ]);
100
354
  function validAssetPolicy(value) {
101
- if (Object.keys(value).some((key) => !ASSET_POLICY_KEYS.has(key))) return false;
102
- if (value.enabled !== void 0 && typeof value.enabled !== "boolean") return false;
103
- if (value.directory !== void 0 && typeof value.directory !== "string") return false;
104
- if (value.modelDevelopmentEnabled !== void 0 && typeof value.modelDevelopmentEnabled !== "boolean") return false;
105
- if (value.persistenceMode !== void 0 && ![
106
- "off",
107
- "manual",
108
- "suggest",
109
- "auto-draft"
110
- ].includes(String(value.persistenceMode))) return false;
111
- if (value.activationMode !== void 0 && !["manual", "auto-tested"].includes(String(value.activationMode))) return false;
112
- const numeric = [
113
- "minSuccessfulRuns",
114
- "minDistinctSessions",
115
- "successWindowDays",
116
- "minSuccessRate",
117
- "maxCandidates",
118
- "candidateTtlDays",
119
- "maxSuggestionsPerDay",
120
- "maxDrafts",
121
- "maxActiveAssets",
122
- "retrievalTopK",
123
- "catalogTokenBudget",
124
- "maxModelDraftWritesPerSession"
125
- ];
355
+ if (Object.keys(value).some((key) => !ASSET_KEYS.has(key))) return false;
126
356
  return Object.entries(value).every(([key, entry]) => {
127
- if (!numeric.includes(key)) return true;
128
- return typeof entry === "number" && Number.isFinite(entry) && entry >= 0;
357
+ if (ASSET_POLICY_BOOLEAN_KEYS.includes(key)) return entry === void 0 || typeof entry === "boolean";
358
+ if (ASSET_POLICY_STRING_KEYS.includes(key)) return entry === void 0 || typeof entry === "string";
359
+ const choices = ASSET_POLICY_ENUMS[key];
360
+ if (choices) return entry === void 0 || choices.includes(String(entry));
361
+ if (NON_NUMERIC_ASSET_KEYS.has(key)) return true;
362
+ if (typeof entry !== "number" || !Number.isFinite(entry) || entry < 0) return false;
363
+ const range = ASSET_POLICY_INTEGER_RANGES[key];
364
+ return !range || Number.isInteger(entry) && entry >= range[0] && entry <= range[1];
129
365
  });
130
366
  }
131
367
  function validUsagePolicy(value) {
132
- if (Object.keys(value).some((key) => !(key in POLICY_BOUNDS))) return false;
133
- return Object.entries(POLICY_BOUNDS).every(([key, [min, max]]) => {
368
+ if (Object.keys(value).some((key) => !(key in USAGE_POLICY_BOUNDS))) return false;
369
+ return Object.entries(USAGE_POLICY_BOUNDS).every(([key, [min, max]]) => {
134
370
  const entry = value[key];
135
371
  return entry === void 0 || typeof entry === "number" && Number.isInteger(entry) && entry >= min && entry <= max;
136
372
  });
@@ -148,9 +384,11 @@ window.__ModuleLoader__.load({
148
384
  "autonomous",
149
385
  "unrestricted"
150
386
  ]),
387
+ enumField("toolSurface", ["indexed", "flat"]),
151
388
  enumField("browserRuntime", ["playwright", "patchright"]),
152
389
  textField("channel"),
153
390
  rangedNumberField("cdpPort", 1, 65535),
391
+ rangedNumberField("maxSessions", 1, 64),
154
392
  booleanField("headless"),
155
393
  booleanField("opencliEnabled"),
156
394
  booleanField("autoInstall"),
@@ -160,25 +398,110 @@ window.__ModuleLoader__.load({
160
398
  textField("snapshotDir"),
161
399
  booleanField("verbose")
162
400
  ];
163
- /** The JSON-shaped section fields the card renders as code editors. */
164
- const JSON_FIELD_SPECS = [jsonField("usagePolicy", validUsagePolicy), jsonField("automationAssets", validAssetPolicy)];
401
+ /** The JSON-shaped section fields: objects (policies, prompts) and the launch-argument array. */
402
+ const JSON_FIELD_SPECS = [
403
+ jsonField("usagePolicy", validUsagePolicy),
404
+ jsonField("automationAssets", validAssetPolicy),
405
+ jsonField("prompts", validPrompts),
406
+ stringArrayField("args")
407
+ ];
408
+ /**
409
+ * Public config fields the card has no form for, on purpose: nested records (named login states, rule packs with
410
+ * hash-pinned scripts) that are safer to edit in the profile config file. The card names each one and says so, instead
411
+ * of leaving it out without a word. `rulePacks` is not `.volatile()` in the Host schema, so no form could write it.
412
+ */
413
+ const CONFIG_FILE_ONLY_FIELDS = ["authProfiles", "rulePacks"];
165
414
  /** Section fields rendered as JSON code editors rather than single inputs. */
166
415
  const JSON_FIELDS = new Set(JSON_FIELD_SPECS.map((spec) => spec.field));
167
416
  /** Every field the card renders, in display order. */
168
417
  const ALL_FIELD_SPECS = [...FIELD_SPECS, ...JSON_FIELD_SPECS];
169
418
  var BrowserSettingsController = class {
419
+ scope;
170
420
  form;
171
421
  store;
422
+ /** What the person typed in the groups/actions/errorHints box, kept verbatim while it is not valid JSON yet. */
423
+ extrasRaw;
424
+ extrasInvalid = false;
172
425
  constructor(scope) {
426
+ this.scope = scope;
173
427
  this.form = new _deepseek_ai_dsh_client_ui_primitives.SettingsFormModel(scope, [...ALL_FIELD_SPECS]);
174
428
  this.store = this.form.bind(() => this.project());
175
429
  }
176
430
  inject() {
431
+ const actions = this.form.actions();
177
432
  return {
178
433
  hooks: { browserSettings: this.store },
179
- ...this.form.actions()
434
+ ...actions,
435
+ save: () => this.save(),
436
+ discard: () => {
437
+ this.extrasRaw = void 0;
438
+ this.extrasInvalid = false;
439
+ actions.discard();
440
+ },
441
+ editPromptText: (id, text) => this.editPromptText(id, text),
442
+ setPromptSkillEnabled: (enabled) => this.setPromptSkillEnabled(enabled),
443
+ editPromptExtras: (text) => this.editPromptExtras(text),
444
+ resetPromptExtras: () => this.resetPromptExtras()
180
445
  };
181
446
  }
447
+ async save() {
448
+ if (this.extrasInvalid) return;
449
+ await this.form.save();
450
+ if (!this.form.shell().failed && this.extrasRaw !== void 0) {
451
+ this.extrasRaw = void 0;
452
+ this.refresh();
453
+ }
454
+ }
455
+ /** The staged `prompts` value as an object (empty when none, or when its text is not JSON). */
456
+ promptsDraft() {
457
+ const text = this.form.field("prompts").text;
458
+ if (text.trim() === "") return {};
459
+ try {
460
+ const value = JSON.parse(text);
461
+ return value && typeof value === "object" && !Array.isArray(value) ? canonicalPrompts(value) : {};
462
+ } catch {
463
+ return {};
464
+ }
465
+ }
466
+ stagePrompts(value) {
467
+ const next = canonicalPrompts(value);
468
+ const stored = this.scope.getSnapshot().value?.prompts;
469
+ const text = JSON.stringify(next) === JSON.stringify(stored && typeof stored === "object" ? canonicalPrompts(stored) : {}) ? ALL_FIELD_SPECS.find((spec) => spec.field === "prompts").format(stored) : Object.keys(next).length ? JSON.stringify(next, null, 2) : "";
470
+ this.form.actions().edit("prompts", text);
471
+ }
472
+ refresh() {
473
+ this.form.actions().edit("prompts", this.form.field("prompts").text);
474
+ }
475
+ editPromptText(id, text) {
476
+ const field = PROMPT_TEXT_FIELDS.find((entry) => entry.id === id);
477
+ this.stagePrompts(setAt(this.promptsDraft(), field.path, text.trim() === "" ? void 0 : text));
478
+ }
479
+ setPromptSkillEnabled(enabled) {
480
+ this.stagePrompts(setAt(this.promptsDraft(), ["skill", "enabled"], enabled ? void 0 : false));
481
+ }
482
+ editPromptExtras(text) {
483
+ this.extrasRaw = text;
484
+ this.extrasInvalid = false;
485
+ if (text.trim() === "") {
486
+ this.stagePrompts(withExtras(this.promptsDraft(), {}));
487
+ return;
488
+ }
489
+ try {
490
+ const value = JSON.parse(text);
491
+ if (value && typeof value === "object" && !Array.isArray(value) && validExtras(value)) {
492
+ this.stagePrompts(withExtras(this.promptsDraft(), value));
493
+ return;
494
+ }
495
+ } catch {}
496
+ this.extrasInvalid = true;
497
+ this.refresh();
498
+ }
499
+ /** Back to the plugin's own text for groups, actions and error hints. */
500
+ resetPromptExtras() {
501
+ this.extrasRaw = void 0;
502
+ this.extrasInvalid = false;
503
+ this.stagePrompts(withExtras(this.promptsDraft(), {}));
504
+ }
182
505
  snapshot() {
183
506
  return this.store.getSnapshot();
184
507
  }
@@ -190,14 +513,838 @@ window.__ModuleLoader__.load({
190
513
  for (const spec of FIELD_SPECS) fields[spec.field] = this.form.field(spec.field);
191
514
  const jsonFields = {};
192
515
  for (const spec of JSON_FIELD_SPECS) jsonFields[spec.field] = this.form.field(spec.field);
516
+ const shell = this.form.shell();
193
517
  return {
194
- ...this.form.shell(),
518
+ ...shell,
519
+ invalid: shell.invalid || this.extrasInvalid,
195
520
  fields,
196
- jsonFields
521
+ jsonFields,
522
+ prompts: this.projectPrompts(jsonFields.prompts)
523
+ };
524
+ }
525
+ projectPrompts(field) {
526
+ const draft = this.promptsDraft();
527
+ const texts = {};
528
+ for (const entry of PROMPT_TEXT_FIELDS) {
529
+ const value = getAt(draft, entry.path);
530
+ const text = typeof value === "string" ? value : "";
531
+ texts[entry.id] = {
532
+ text,
533
+ invalid: text.length > entry.limit
534
+ };
535
+ }
536
+ return {
537
+ texts,
538
+ skillEnabled: getAt(draft, ["skill", "enabled"]) !== false,
539
+ extras: {
540
+ text: this.extrasRaw ?? extrasText(draft),
541
+ invalid: this.extrasInvalid
542
+ },
543
+ overridden: field.overridden,
544
+ invalid: field.invalid
197
545
  };
198
546
  }
199
547
  };
200
548
  //#endregion
549
+ //#region src/activation-rules.ts
550
+ /**
551
+ * How many different input sets the credential's passing test covered: its passed sets, one for a plain single run,
552
+ * and enough for any requirement when it is a `legacy` credential synthesized from data written before input sets
553
+ * existed (so upgrading never strands an already-tested asset).
554
+ */
555
+ function inputSetsCovered(credential) {
556
+ if (credential.legacy) return Number.POSITIVE_INFINITY;
557
+ return credential.inputSets ? credential.inputSets.filter((entry) => entry.passed).length : 1;
558
+ }
559
+ /** How many inputs the asset declares (typed `inputSchema` for v2, `inputNames` otherwise). Zero means the rule does not apply. */
560
+ function declaredInputs(asset) {
561
+ return Math.max(asset.inputSchema?.length ?? 0, asset.inputNames?.length ?? 0);
562
+ }
563
+ /** What stands between this asset and activation on input sets, or undefined when nothing does. `credential` is the latest of the current revision. */
564
+ function inputSetShortfall(asset, credential, required) {
565
+ const declared = declaredInputs(asset);
566
+ if (declared === 0) return void 0;
567
+ const covered = credential ? inputSetsCovered(credential) : 0;
568
+ return covered < required ? {
569
+ declared,
570
+ required,
571
+ covered
572
+ } : void 0;
573
+ }
574
+ //#endregion
575
+ //#region src/client/asset-editor.ts
576
+ /** The fields a person edits. Everything else (id, revision, hash, status, credentials, counters) is shown, never edited. */
577
+ const EDITABLE_KEYS = [
578
+ "kind",
579
+ "name",
580
+ "description",
581
+ "domains",
582
+ "tags",
583
+ "inputNames",
584
+ "schemaVersion",
585
+ "recipe",
586
+ "source",
587
+ "inputSchema",
588
+ "outputSchema",
589
+ "postconditions",
590
+ "requiredCapabilities"
591
+ ];
592
+ /** The saved asset as the editor text starts out: only the editable fields, so a test or a status change never makes it look edited. */
593
+ function editableView(asset) {
594
+ const record = asset;
595
+ return Object.fromEntries(EDITABLE_KEYS.filter((key) => record[key] !== void 0).map((key) => [key, structuredClone(record[key])]));
596
+ }
597
+ function serializeEditable(asset) {
598
+ return JSON.stringify(editableView(asset), null, 2);
599
+ }
600
+ /** JSON with sorted keys, so reformatting or reordering the text is not an edit. */
601
+ function stableJson(value) {
602
+ if (Array.isArray(value)) return "[" + value.map((entry) => stableJson(entry)).join(",") + "]";
603
+ if (value && typeof value === "object") {
604
+ const record = value;
605
+ return "{" + Object.keys(record).sort().map((key) => JSON.stringify(key) + ":" + stableJson(record[key])).join(",") + "}";
606
+ }
607
+ return JSON.stringify(value) ?? "null";
608
+ }
609
+ /** Whether the editor text differs from the baseline it was loaded from. Text that is not JSON is compared as text. */
610
+ function isDirty(text, baseline) {
611
+ if (text === baseline) return false;
612
+ try {
613
+ return stableJson(JSON.parse(text)) !== stableJson(JSON.parse(baseline));
614
+ } catch {
615
+ return true;
616
+ }
617
+ }
618
+ /** Parse the editor text into something saveable, or say why not. */
619
+ function parseEditor(text) {
620
+ let value;
621
+ try {
622
+ value = JSON.parse(text);
623
+ } catch (error) {
624
+ return {
625
+ ok: false,
626
+ message: error instanceof Error ? error.message : String(error)
627
+ };
628
+ }
629
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {
630
+ ok: false,
631
+ message: "the editor must hold one JSON object"
632
+ };
633
+ const record = value;
634
+ if (record.kind !== "recipe" && record.kind !== "userscript") return {
635
+ ok: false,
636
+ message: "kind must be \"recipe\" or \"userscript\""
637
+ };
638
+ if (typeof record.name !== "string" || !record.name.trim()) return {
639
+ ok: false,
640
+ message: "name must be a non-empty string"
641
+ };
642
+ return {
643
+ ok: true,
644
+ value: record
645
+ };
646
+ }
647
+ function shortHash(hash) {
648
+ return hash ? hash.slice(0, 8) : "--------";
649
+ }
650
+ function credentialView(asset) {
651
+ const credentials = asset?.testCredentials ?? [];
652
+ const current = credentials.filter((entry) => entry.revision === asset?.revision).at(-1);
653
+ return {
654
+ ...current ? { current } : {},
655
+ ...credentials.at(-1) ? { latest: credentials.at(-1) } : {},
656
+ vouches: !!asset && !!current && current.passed && asset.testStatus === "passed" && (asset.contentHash === void 0 || current.contentHash === asset.contentHash)
657
+ };
658
+ }
659
+ const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
660
+ function parseTestInputs(text) {
661
+ let value;
662
+ try {
663
+ value = JSON.parse(text.trim() || "{}");
664
+ } catch (error) {
665
+ return {
666
+ kind: "error",
667
+ message: "test inputs: " + (error instanceof Error ? error.message : String(error))
668
+ };
669
+ }
670
+ if (isObject(value)) return {
671
+ kind: "single",
672
+ inputs: value
673
+ };
674
+ if (!Array.isArray(value)) return {
675
+ kind: "error",
676
+ message: `test inputs must be a JSON object, or an array of 2 to 5 objects`
677
+ };
678
+ if (value.length < 2 || value.length > 5) return {
679
+ kind: "error",
680
+ message: `test inputs: an array holds 2 to 5 input objects, found ${value.length}`
681
+ };
682
+ const bad = value.findIndex((entry) => !isObject(entry));
683
+ if (bad >= 0) return {
684
+ kind: "error",
685
+ message: `test inputs: set ${bad + 1} is not an object`
686
+ };
687
+ return {
688
+ kind: "sets",
689
+ sets: value
690
+ };
691
+ }
692
+ function inputSetLines(credential) {
693
+ return (credential?.inputSets ?? []).map((entry) => ({
694
+ set: entry.index + 1,
695
+ passed: entry.passed,
696
+ status: `${entry.executionStatus}/${entry.validationStatus}`,
697
+ inputsDigest: entry.inputsDigest,
698
+ outputsDigest: entry.outputsDigest
699
+ }));
700
+ }
701
+ /** Pairs of sets (1-based) that got the same output from different inputs: what `PARAMETERIZATION_SUSPECT` is about. */
702
+ function identicalOutputPairs(credential) {
703
+ const lines = inputSetLines(credential);
704
+ const pairs = [];
705
+ for (let a = 0; a < lines.length; a += 1) for (let b = a + 1; b < lines.length; b += 1) if (lines[a].outputsDigest === lines[b].outputsDigest) pairs.push([lines[a].set, lines[b].set]);
706
+ return pairs;
707
+ }
708
+ //#endregion
709
+ //#region src/client/automation-assets-client.ts
710
+ const EMPTY_EDITOR = {
711
+ text: "",
712
+ baseline: "",
713
+ testUrl: "",
714
+ testInputs: "{}"
715
+ };
716
+ function localStore(initial) {
717
+ let snapshot = initial;
718
+ const listeners = /* @__PURE__ */ new Set();
719
+ return {
720
+ getSnapshot: () => snapshot,
721
+ subscribe(listener) {
722
+ listeners.add(listener);
723
+ return () => {
724
+ listeners.delete(listener);
725
+ };
726
+ },
727
+ set(next) {
728
+ snapshot = next;
729
+ for (const listener of listeners) listener();
730
+ },
731
+ update(updater) {
732
+ const draft = structuredClone(snapshot);
733
+ updater(draft);
734
+ snapshot = draft;
735
+ for (const listener of listeners) listener();
736
+ }
737
+ };
738
+ }
739
+ /** This plugin's private RPC channel, matching the Host half. */
740
+ const CHANNEL$1 = "/dsh-browser-assets";
741
+ /** A failed RPC call, with the structured details the Host attached (`errorCode`, `reason`, ...). */
742
+ var AssetCallError = class extends Error {
743
+ details;
744
+ constructor(message, details = {}) {
745
+ super(message);
746
+ this.details = details;
747
+ this.name = "AssetCallError";
748
+ }
749
+ get code() {
750
+ return typeof this.details.errorCode === "string" ? this.details.errorCode : void 0;
751
+ }
752
+ };
753
+ const VALIDATION_CODES = /* @__PURE__ */ new Set(["VALIDATION_FAILED", "VALIDATION_MISSING"]);
754
+ function noticeFor(error) {
755
+ const message = String(error instanceof Error ? error.message : error).slice(0, 400);
756
+ const code = error instanceof AssetCallError ? error.code : void 0;
757
+ return {
758
+ kind: code && VALIDATION_CODES.has(code) ? "validation" : "backend",
759
+ message,
760
+ ...code ? { code } : {}
761
+ };
762
+ }
763
+ /**
764
+ * The "new recipe" template: schema v2 (typed inputs, locators, a result to extract and a postcondition that checks it),
765
+ * so what a person starts from is what the plugin writes today. Every value is a placeholder to replace; the template
766
+ * itself passes the save validation, which test/automation-assets-client.test.ts holds it to.
767
+ */
768
+ const NEW_RECIPE = {
769
+ kind: "recipe",
770
+ schemaVersion: 2,
771
+ name: "New recipe",
772
+ description: "",
773
+ domains: ["example.com"],
774
+ tags: [],
775
+ inputSchema: [{
776
+ name: "keyword",
777
+ type: "string",
778
+ required: true,
779
+ example: "example"
780
+ }],
781
+ recipe: [
782
+ {
783
+ type: "fill",
784
+ locator: { label: "Search" },
785
+ value: "{{keyword}}"
786
+ },
787
+ {
788
+ type: "click",
789
+ locator: {
790
+ role: "button",
791
+ name: "Search"
792
+ }
793
+ },
794
+ {
795
+ type: "extract",
796
+ locator: { css: "#results" },
797
+ as: "results"
798
+ }
799
+ ],
800
+ postconditions: [{
801
+ output: "results",
802
+ nonEmpty: true
803
+ }]
804
+ };
805
+ const NEW_SCRIPT = {
806
+ kind: "userscript",
807
+ name: "New userscript",
808
+ description: "",
809
+ domains: [],
810
+ tags: [],
811
+ inputNames: [],
812
+ source: "// ==UserScript==\n// @name New userscript\n// @match https://example.com/*\n// @grant none\n// ==/UserScript==\nreturn { title: document.title }"
813
+ };
814
+ var AutomationAssetsController = class {
815
+ rpc;
816
+ store = localStore({
817
+ loading: true,
818
+ busy: false,
819
+ failed: false,
820
+ editor: EMPTY_EDITOR
821
+ });
822
+ disposed = false;
823
+ constructor(rpc) {
824
+ this.rpc = rpc;
825
+ this.refresh();
826
+ }
827
+ inject() {
828
+ return {
829
+ hooks: { automationAssets: this.store },
830
+ refreshAutomationAssets: () => {
831
+ this.refresh();
832
+ },
833
+ selectAutomationAsset: (id) => {
834
+ this.select(id);
835
+ },
836
+ saveAutomationAsset: (asset) => this.mutate("save", { asset }),
837
+ summarizeAutomationCandidate: (id) => this.mutate("summarize", { id }),
838
+ dismissAutomationCandidate: (id) => this.mutate("dismiss", { id }),
839
+ validateAutomationAsset: (id) => this.mutate("validate", { id }),
840
+ testAutomationAsset: (id, url, inputs, expectedRevision) => this.mutate("test", {
841
+ id,
842
+ url,
843
+ inputs,
844
+ ...expectedRevision !== void 0 ? { expectedRevision } : {}
845
+ }),
846
+ setAutomationAssetStatus: (id, status, expectedRevision) => this.mutate("status", {
847
+ id,
848
+ status,
849
+ ...expectedRevision !== void 0 ? { expectedRevision } : {}
850
+ }),
851
+ editAutomationAsset: (text) => this.edit(text),
852
+ setAssetTestUrl: (value) => this.patchEditor({
853
+ testUrl: value,
854
+ notice: void 0
855
+ }),
856
+ setAssetTestInputs: (value) => this.patchEditor({
857
+ testInputs: value,
858
+ notice: void 0
859
+ }),
860
+ requestSelectAutomationAsset: (id) => this.requestLeave({
861
+ kind: "select",
862
+ id
863
+ }),
864
+ requestNewAutomationAsset: (assetKind) => this.requestLeave({
865
+ kind: "new",
866
+ assetKind
867
+ }),
868
+ requestRefreshAutomationAssets: () => this.requestLeave({ kind: "refresh" }),
869
+ requestForkAutomationAsset: (id) => this.requestLeave({
870
+ kind: "fork",
871
+ id
872
+ }),
873
+ requestConvertAutomationAsset: (id) => this.requestLeave({
874
+ kind: "convert",
875
+ id
876
+ }),
877
+ confirmLeaveAutomationAsset: () => this.confirmLeave(),
878
+ cancelLeaveAutomationAsset: () => this.patchEditor({ confirm: void 0 }),
879
+ saveEditedAutomationAsset: () => this.saveEdited(),
880
+ saveAndTestAutomationAsset: () => this.saveAndTest(),
881
+ activateAutomationAsset: () => this.activate()
882
+ };
883
+ }
884
+ snapshot() {
885
+ return this.store.getSnapshot();
886
+ }
887
+ dispose() {
888
+ this.disposed = true;
889
+ }
890
+ /** Whether the editor holds edits that are not saved. */
891
+ dirty() {
892
+ const { editor } = this.store.getSnapshot();
893
+ return isDirty(editor.text, editor.baseline);
894
+ }
895
+ async refresh() {
896
+ this.publish({
897
+ ...this.store.getSnapshot(),
898
+ loading: true,
899
+ failed: false,
900
+ error: void 0
901
+ });
902
+ try {
903
+ const snapshot = await this.call("snapshot", {});
904
+ this.publish({
905
+ ...this.store.getSnapshot(),
906
+ loading: false,
907
+ snapshot
908
+ });
909
+ } catch (error) {
910
+ this.fail(error);
911
+ }
912
+ }
913
+ /** Load one asset into the selection and the editor, replacing whatever the editor held. */
914
+ async select(id) {
915
+ if (!id) {
916
+ this.publish({
917
+ ...this.store.getSnapshot(),
918
+ selected: void 0,
919
+ editor: {
920
+ ...this.store.getSnapshot().editor,
921
+ text: "",
922
+ baseline: "",
923
+ notice: void 0,
924
+ confirm: void 0
925
+ }
926
+ });
927
+ return;
928
+ }
929
+ try {
930
+ const selected = await this.call("get", { id });
931
+ this.publish({
932
+ ...this.store.getSnapshot(),
933
+ selected: selected ?? void 0,
934
+ failed: false,
935
+ error: void 0,
936
+ editor: this.editorFor(selected ?? void 0)
937
+ });
938
+ } catch (error) {
939
+ this.fail(error);
940
+ }
941
+ }
942
+ editorFor(asset) {
943
+ const previous = this.store.getSnapshot().editor;
944
+ const text = asset ? serializeEditable(asset) : "";
945
+ return {
946
+ text,
947
+ baseline: text,
948
+ testUrl: previous.testUrl,
949
+ testInputs: previous.testInputs
950
+ };
951
+ }
952
+ requestLeave(leave) {
953
+ if (this.dirty()) {
954
+ this.patchEditor({ confirm: leave });
955
+ return;
956
+ }
957
+ this.perform(leave);
958
+ }
959
+ confirmLeave() {
960
+ const leave = this.store.getSnapshot().editor.confirm;
961
+ this.patchEditor({ confirm: void 0 });
962
+ if (leave) this.perform(leave);
963
+ }
964
+ async perform(leave) {
965
+ switch (leave.kind) {
966
+ case "select":
967
+ await this.select(leave.id);
968
+ return;
969
+ case "new":
970
+ this.startNew(leave.assetKind);
971
+ return;
972
+ case "refresh": {
973
+ await this.refresh();
974
+ const id = this.store.getSnapshot().selected?.id;
975
+ if (id) await this.select(id);
976
+ return;
977
+ }
978
+ case "fork": {
979
+ const draft = await this.run(() => this.call("fork", { id: leave.id }));
980
+ if (draft) {
981
+ await this.reloadSnapshot();
982
+ this.publish({
983
+ ...this.store.getSnapshot(),
984
+ selected: draft,
985
+ editor: this.editorFor(draft)
986
+ });
987
+ }
988
+ return;
989
+ }
990
+ case "convert": {
991
+ const sourceName = this.store.getSnapshot().snapshot?.assets.find((entry) => entry.id === leave.id)?.name ?? this.store.getSnapshot().selected?.name ?? "";
992
+ const result = await this.run(() => this.call("convert", { id: leave.id }));
993
+ if (result) {
994
+ await this.reloadSnapshot();
995
+ this.publish({
996
+ ...this.store.getSnapshot(),
997
+ selected: result.draft,
998
+ editor: {
999
+ ...this.editorFor(result.draft),
1000
+ converted: {
1001
+ sourceName,
1002
+ notes: result.notes
1003
+ }
1004
+ }
1005
+ });
1006
+ }
1007
+ }
1008
+ }
1009
+ }
1010
+ startNew(assetKind) {
1011
+ const text = JSON.stringify(assetKind === "recipe" ? NEW_RECIPE : NEW_SCRIPT, null, 2);
1012
+ const current = this.store.getSnapshot();
1013
+ this.publish({
1014
+ ...current,
1015
+ selected: void 0,
1016
+ editor: {
1017
+ text,
1018
+ baseline: text,
1019
+ testUrl: current.editor.testUrl,
1020
+ testInputs: current.editor.testInputs
1021
+ }
1022
+ });
1023
+ }
1024
+ edit(text) {
1025
+ this.patchEditor({
1026
+ text,
1027
+ notice: void 0
1028
+ });
1029
+ }
1030
+ /** Save the editor text as a new revision. Resolves to the saved asset, or undefined when nothing was saved (the notice says why). */
1031
+ async saveEdited() {
1032
+ const { editor, selected } = this.store.getSnapshot();
1033
+ const parsed = parseEditor(editor.text);
1034
+ if (!parsed.ok) {
1035
+ this.patchEditor({ notice: {
1036
+ kind: "json",
1037
+ message: parsed.message
1038
+ } });
1039
+ return;
1040
+ }
1041
+ if (selected?.status === "active") {
1042
+ this.patchEditor({ notice: {
1043
+ kind: "refused",
1044
+ message: "active assets cannot be edited: create a repair draft first"
1045
+ } });
1046
+ return;
1047
+ }
1048
+ const value = parsed.value;
1049
+ if (selected?.id) value.id = selected.id;
1050
+ const saved = await this.run(() => this.call("save", { asset: value }));
1051
+ if (!saved) return void 0;
1052
+ await this.reloadSnapshot();
1053
+ this.publish({
1054
+ ...this.store.getSnapshot(),
1055
+ selected: saved,
1056
+ editor: {
1057
+ ...this.editorFor(saved),
1058
+ notice: void 0
1059
+ }
1060
+ });
1061
+ return saved;
1062
+ }
1063
+ /**
1064
+ * One button, one meaning: whatever is in the editor is what gets tested. Unsaved edits are saved first
1065
+ * (a new revision), and the test is pinned to that revision, so the test, its credential and a later
1066
+ * activation all refer to the content on screen.
1067
+ */
1068
+ async saveAndTest() {
1069
+ const state = this.store.getSnapshot();
1070
+ const url = state.editor.testUrl.trim();
1071
+ const parsedInputs = parseTestInputs(state.editor.testInputs);
1072
+ if (parsedInputs.kind === "error") {
1073
+ this.patchEditor({ notice: {
1074
+ kind: "json",
1075
+ message: parsedInputs.message
1076
+ } });
1077
+ return;
1078
+ }
1079
+ if (!url) {
1080
+ this.patchEditor({ notice: {
1081
+ kind: "json",
1082
+ message: "enter the test URL first"
1083
+ } });
1084
+ return;
1085
+ }
1086
+ let target = state.selected;
1087
+ if (this.dirty() || !target) {
1088
+ target = await this.saveEdited();
1089
+ if (!target) return;
1090
+ }
1091
+ if (target.status !== "draft") {
1092
+ this.patchEditor({ notice: {
1093
+ kind: "refused",
1094
+ message: "only drafts can be tested"
1095
+ } });
1096
+ return;
1097
+ }
1098
+ const id = target.id;
1099
+ const revision = target.revision;
1100
+ this.publish({
1101
+ ...this.store.getSnapshot(),
1102
+ busy: true
1103
+ });
1104
+ try {
1105
+ const body = parsedInputs.kind === "sets" ? { inputSets: parsedInputs.sets } : { inputs: parsedInputs.inputs };
1106
+ const tested = await this.call("test", {
1107
+ id,
1108
+ url,
1109
+ ...body,
1110
+ expectedRevision: revision
1111
+ });
1112
+ await this.reloadSnapshot();
1113
+ this.publish({
1114
+ ...this.store.getSnapshot(),
1115
+ busy: false,
1116
+ selected: tested,
1117
+ editor: {
1118
+ ...this.store.getSnapshot().editor,
1119
+ notice: void 0
1120
+ }
1121
+ });
1122
+ } catch (error) {
1123
+ const notice = noticeFor(error);
1124
+ await this.reloadSnapshot();
1125
+ let selected = this.store.getSnapshot().selected;
1126
+ try {
1127
+ selected = await this.call("get", { id }) ?? selected;
1128
+ } catch {}
1129
+ this.publish({
1130
+ ...this.store.getSnapshot(),
1131
+ busy: false,
1132
+ selected,
1133
+ editor: {
1134
+ ...this.store.getSnapshot().editor,
1135
+ notice
1136
+ }
1137
+ });
1138
+ }
1139
+ }
1140
+ /** Activate the revision on screen. Refused while the editor has edits, and the request names the revision. */
1141
+ async activate() {
1142
+ const { selected } = this.store.getSnapshot();
1143
+ if (!selected) return;
1144
+ if (this.dirty()) {
1145
+ this.patchEditor({ notice: {
1146
+ kind: "refused",
1147
+ message: "save and test your edits before activating"
1148
+ } });
1149
+ return;
1150
+ }
1151
+ const activated = await this.run(() => this.call("status", {
1152
+ id: selected.id,
1153
+ status: "active",
1154
+ expectedRevision: selected.revision
1155
+ }));
1156
+ if (!activated) return;
1157
+ await this.reloadSnapshot();
1158
+ this.publish({
1159
+ ...this.store.getSnapshot(),
1160
+ selected: activated,
1161
+ editor: {
1162
+ ...this.editorFor(activated),
1163
+ notice: void 0
1164
+ }
1165
+ });
1166
+ }
1167
+ /** Run a call with the busy flag; a failure becomes the editor's notice. */
1168
+ async run(fn) {
1169
+ this.publish({
1170
+ ...this.store.getSnapshot(),
1171
+ busy: true
1172
+ });
1173
+ try {
1174
+ const value = await fn();
1175
+ this.publish({
1176
+ ...this.store.getSnapshot(),
1177
+ busy: false
1178
+ });
1179
+ return value;
1180
+ } catch (error) {
1181
+ this.publish({
1182
+ ...this.store.getSnapshot(),
1183
+ busy: false,
1184
+ editor: {
1185
+ ...this.store.getSnapshot().editor,
1186
+ notice: noticeFor(error)
1187
+ }
1188
+ });
1189
+ return;
1190
+ }
1191
+ }
1192
+ async reloadSnapshot() {
1193
+ try {
1194
+ const snapshot = await this.call("snapshot", {});
1195
+ this.publish({
1196
+ ...this.store.getSnapshot(),
1197
+ snapshot
1198
+ });
1199
+ } catch {}
1200
+ }
1201
+ async mutate(endpoint, payload) {
1202
+ this.publish({
1203
+ ...this.store.getSnapshot(),
1204
+ busy: true,
1205
+ failed: false,
1206
+ error: void 0
1207
+ });
1208
+ const before = this.store.getSnapshot().selected;
1209
+ try {
1210
+ const value = await this.call(endpoint, payload);
1211
+ const selected = value && typeof value === "object" && "id" in value ? value : before;
1212
+ const snapshot = await this.call("snapshot", {});
1213
+ const replaced = !!selected && selected.id !== before?.id;
1214
+ const now = this.store.getSnapshot();
1215
+ this.publish({
1216
+ ...now,
1217
+ loading: false,
1218
+ busy: false,
1219
+ failed: false,
1220
+ error: void 0,
1221
+ snapshot,
1222
+ ...selected ? { selected } : {},
1223
+ ...replaced ? { editor: this.editorFor(selected) } : {}
1224
+ });
1225
+ } catch (error) {
1226
+ const now = this.store.getSnapshot();
1227
+ this.publish({
1228
+ ...now,
1229
+ loading: false,
1230
+ busy: false,
1231
+ failed: false,
1232
+ error: void 0,
1233
+ editor: {
1234
+ ...now.editor,
1235
+ notice: noticeFor(error)
1236
+ }
1237
+ });
1238
+ }
1239
+ }
1240
+ patchEditor(patch) {
1241
+ const current = this.store.getSnapshot();
1242
+ this.publish({
1243
+ ...current,
1244
+ editor: {
1245
+ ...current.editor,
1246
+ ...patch
1247
+ }
1248
+ });
1249
+ }
1250
+ async call(endpoint, payload) {
1251
+ const result = await this.rpc.call(CHANNEL$1, endpoint, payload);
1252
+ if (!result.ok) throw new AssetCallError(result.error.message, result.error.details ?? {});
1253
+ return result.value;
1254
+ }
1255
+ fail(error) {
1256
+ this.publish({
1257
+ ...this.store.getSnapshot(),
1258
+ loading: false,
1259
+ busy: false,
1260
+ failed: true,
1261
+ error: String(error instanceof Error ? error.message : error).slice(0, 300)
1262
+ });
1263
+ }
1264
+ publish(state) {
1265
+ if (!this.disposed) this.store.set(state);
1266
+ }
1267
+ };
1268
+ //#endregion
1269
+ //#region src/client/prompts-status-client.ts
1270
+ /** Same private channel as the automation assets (see automation-assets-rpc.ts). */
1271
+ const CHANNEL = "/dsh-browser-assets";
1272
+ /** Reads the plugin's own account of its prompt overrides, for the "prompt text" section. */
1273
+ var PromptsStatusController = class {
1274
+ rpc;
1275
+ store = localStore({
1276
+ loading: true,
1277
+ failed: false
1278
+ });
1279
+ disposed = false;
1280
+ constructor(rpc) {
1281
+ this.rpc = rpc;
1282
+ this.refresh();
1283
+ }
1284
+ inject() {
1285
+ return {
1286
+ hooks: { promptsStatus: this.store },
1287
+ refreshPromptsStatus: () => {
1288
+ this.refresh();
1289
+ },
1290
+ exportPromptDefaults: () => this.exportDefaults(),
1291
+ hidePromptDefaults: () => {
1292
+ const { defaults: _defaults, defaultsFailed: _failed, ...rest } = this.store.getSnapshot();
1293
+ this.publish(rest);
1294
+ }
1295
+ };
1296
+ }
1297
+ snapshot() {
1298
+ return this.store.getSnapshot();
1299
+ }
1300
+ dispose() {
1301
+ this.disposed = true;
1302
+ }
1303
+ /** Load the default prompt texts as JSON, for the read-only box under the section. */
1304
+ async exportDefaults() {
1305
+ try {
1306
+ const result = await this.rpc.call(CHANNEL, "promptsDefaults", {});
1307
+ if (!result.ok) throw new Error(result.error.message);
1308
+ const { defaultsFailed: _failed, ...rest } = this.store.getSnapshot();
1309
+ this.publish({
1310
+ ...rest,
1311
+ defaults: result.value.json
1312
+ });
1313
+ } catch {
1314
+ this.publish({
1315
+ ...this.store.getSnapshot(),
1316
+ defaultsFailed: true
1317
+ });
1318
+ }
1319
+ }
1320
+ async refresh() {
1321
+ const current = this.store.getSnapshot();
1322
+ this.publish({
1323
+ ...current,
1324
+ loading: true
1325
+ });
1326
+ try {
1327
+ const result = await this.rpc.call(CHANNEL, "prompts", {});
1328
+ if (!result.ok) throw new Error(result.error.message);
1329
+ this.publish({
1330
+ ...this.store.getSnapshot(),
1331
+ loading: false,
1332
+ failed: false,
1333
+ status: result.value
1334
+ });
1335
+ } catch {
1336
+ this.publish({
1337
+ ...this.store.getSnapshot(),
1338
+ loading: false,
1339
+ failed: true
1340
+ });
1341
+ }
1342
+ }
1343
+ publish(state) {
1344
+ if (!this.disposed) this.store.set(state);
1345
+ }
1346
+ };
1347
+ //#endregion
201
1348
  //#region src/client/styles.ts
202
1349
  const styles = {
203
1350
  card: "dsb-card",
@@ -281,6 +1428,8 @@ window.__ModuleLoader__.load({
281
1428
  * settings page uses, so the card matches them without restating their markup.
282
1429
  * @module dsh-browser/client/SettingsCard
283
1430
  */
1431
+ /** Fill `{name}` placeholders of a dictionary entry. */
1432
+ const fill = (text, values) => text.replace(/\{(\w+)\}/g, (_match, key) => String(values[key] ?? ""));
284
1433
  /** The copy the shared form frame renders, from this page's dictionary. */
285
1434
  function formLabels(t) {
286
1435
  return {
@@ -291,6 +1440,11 @@ window.__ModuleLoader__.load({
291
1440
  saving: t("saving")
292
1441
  };
293
1442
  }
1443
+ /**
1444
+ * Which fields each section shows, by field name, and which public fields it only names ("edit in the config file").
1445
+ * A public config field missing from here and from the prompt/usage sections would be silently absent from the card;
1446
+ * test/settings-card-render.test.ts renders the card and checks every Config field is a control or a note.
1447
+ */
294
1448
  const SECTION_LAYOUT = [
295
1449
  {
296
1450
  title: "freedom",
@@ -298,6 +1452,7 @@ window.__ModuleLoader__.load({
298
1452
  fields: [
299
1453
  "enabled",
300
1454
  "automationMode",
1455
+ "toolSurface",
301
1456
  "opencliEnabled"
302
1457
  ]
303
1458
  },
@@ -310,7 +1465,9 @@ window.__ModuleLoader__.load({
310
1465
  "headless",
311
1466
  "autoInstall",
312
1467
  "executablePath",
313
- "cdpPort"
1468
+ "cdpPort",
1469
+ "maxSessions",
1470
+ "args"
314
1471
  ]
315
1472
  },
316
1473
  {
@@ -321,36 +1478,61 @@ window.__ModuleLoader__.load({
321
1478
  "defaultAuthProfile",
322
1479
  "snapshotDir",
323
1480
  "verbose"
324
- ]
1481
+ ],
1482
+ notes: CONFIG_FILE_ONLY_FIELDS
325
1483
  }
326
1484
  ];
327
- /** The section field a spec names, when it is one of the single-input fields. */
328
- function sectionField(field) {
329
- return FIELD_SPECS.some((spec) => spec.field === field) ? field : void 0;
330
- }
1485
+ /** The usage section's JSON boxes. */
1486
+ const USAGE_FIELDS = ["usagePolicy", "automationAssets"];
1487
+ /** The keys each JSON policy box accepts, generated from the code's own lists so the hint cannot leave one out. */
1488
+ const POLICY_KEY_LISTS = {
1489
+ usagePolicy: USAGE_POLICY_KEYS,
1490
+ automationAssets: ASSET_POLICY_KEYS
1491
+ };
1492
+ /** The single-input fields, where `state.fields` holds their state. */
1493
+ const SINGLE_INPUT_FIELDS = new Set(FIELD_SPECS.map((spec) => spec.field));
331
1494
  function SettingsCard(props) {
332
1495
  const { t } = props;
333
1496
  const state = props.useBrowserSettings((snapshot) => snapshot);
334
1497
  if (props.view === "summary") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: t("description") });
335
1498
  const disabled = !state.writable;
336
- const numeric = /* @__PURE__ */ new Set(["cdpPort"]);
337
- const field = (spec) => {
338
- const known = sectionField(spec.field);
339
- const fieldState = known ? state.fields[known] : state.jsonFields[spec.field];
1499
+ const numeric = /* @__PURE__ */ new Set(["cdpPort", "maxSessions"]);
1500
+ const field = (name) => {
1501
+ const fieldState = SINGLE_INPUT_FIELDS.has(name) ? state.fields[name] : state.jsonFields[name];
1502
+ const policyKeys = POLICY_KEY_LISTS[name];
340
1503
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SettingsValueField, {
341
- id: `plugin-config-dsh-browser-${spec.field}`,
342
- label: t(spec.field),
343
- hint: t(`${spec.field}Hint`),
1504
+ id: `plugin-config-dsh-browser-${name}`,
1505
+ label: t(name),
1506
+ hint: policyKeys ? `${t(`${name}Hint`)} ${t("policyKeys")} ${policyKeys.join(", ")}` : t(`${name}Hint`),
344
1507
  overriddenLabel: t("overridden"),
345
1508
  resetLabel: t("reset"),
346
- invalidLabel: t(JSON_FIELDS.has(spec.field) ? "invalidJson" : "invalidNumber"),
347
- numeric: numeric.has(spec.field),
1509
+ invalidLabel: t(JSON_FIELDS.has(name) ? "invalidJson" : "invalidNumber"),
1510
+ numeric: numeric.has(name),
348
1511
  disabled,
349
1512
  ...fieldState,
350
- onEdit: (text) => props.edit(spec.field, text),
351
- onReset: () => props.resetField(spec.field)
352
- }, spec.field);
1513
+ onEdit: (text) => props.edit(name, text),
1514
+ onReset: () => props.resetField(name)
1515
+ }, name);
353
1516
  };
1517
+ /** A public field with no form: its name, what it is for, and where to edit it. */
1518
+ const configFileNote = (name) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1519
+ className: styles.field,
1520
+ "data-dsh-browser-config-note": name,
1521
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1522
+ className: styles.fieldHead,
1523
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1524
+ className: styles.label,
1525
+ children: t(name)
1526
+ })
1527
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1528
+ className: styles.hint,
1529
+ children: [
1530
+ t(`${name}Hint`),
1531
+ " ",
1532
+ t("configFileOnly")
1533
+ ]
1534
+ })]
1535
+ }, name);
354
1536
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.SettingsForm, {
355
1537
  labels: formLabels(t),
356
1538
  state,
@@ -362,9 +1544,9 @@ window.__ModuleLoader__.load({
362
1544
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
363
1545
  className: styles.sectionHead,
364
1546
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t(section.title) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t(section.hint) })]
365
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1547
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
366
1548
  className: styles.grid,
367
- children: section.fields.map((name) => field(FIELD_SPECS.find((spec) => spec.field === name)))
1549
+ children: [section.fields.map((name) => field(name)), section.notes?.map((name) => configFileNote(name))]
368
1550
  })]
369
1551
  }, section.title)),
370
1552
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
@@ -376,7 +1558,7 @@ window.__ModuleLoader__.load({
376
1558
  }),
377
1559
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
378
1560
  className: styles.grid,
379
- children: JSON_FIELD_SPECS.map((spec) => field(spec))
1561
+ children: USAGE_FIELDS.map((name) => field(name))
380
1562
  }),
381
1563
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
382
1564
  className: styles.notice,
@@ -385,72 +1567,313 @@ window.__ModuleLoader__.load({
385
1567
  })
386
1568
  ]
387
1569
  }),
1570
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PromptsPanel, {
1571
+ ...props,
1572
+ disabled
1573
+ }),
388
1574
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutomationAssetsPanel, { ...props })
389
1575
  ]
390
1576
  });
391
1577
  }
1578
+ /** Label and hint keys of each text control, by field id. */
1579
+ const PROMPT_LABELS = {
1580
+ indexDescription: {
1581
+ label: "promptsIndexDescription",
1582
+ hint: "promptsIndexDescriptionHint"
1583
+ },
1584
+ callDescription: {
1585
+ label: "promptsCallDescription",
1586
+ hint: "promptsCallDescriptionHint"
1587
+ },
1588
+ rootGuide: {
1589
+ label: "promptsRootGuide",
1590
+ hint: "promptsRootGuideHint"
1591
+ },
1592
+ rootNote: {
1593
+ label: "promptsRootNote",
1594
+ hint: "promptsRootNoteHint"
1595
+ },
1596
+ skillDescription: {
1597
+ label: "promptsSkillDescription",
1598
+ hint: "promptsSkillDescriptionHint"
1599
+ },
1600
+ skillBodyFile: {
1601
+ label: "promptsSkillBodyFile",
1602
+ hint: "promptsSkillBodyFileHint"
1603
+ },
1604
+ skillAppend: {
1605
+ label: "promptsSkillAppend",
1606
+ hint: "promptsSkillAppendHint"
1607
+ }
1608
+ };
1609
+ /**
1610
+ * The "prompt text" section: the text the model reads, overridable. The controls are views of one staged `prompts`
1611
+ * draft that saves with the rest of the form; the status block below shows what the running plugin reports about it.
1612
+ */
1613
+ function PromptsPanel(props) {
1614
+ const { t, disabled } = props;
1615
+ const settings = props.useBrowserSettings((snapshot) => snapshot);
1616
+ const status = props.usePromptsStatus((snapshot) => snapshot);
1617
+ const { refreshPromptsStatus } = props;
1618
+ (0, react.useEffect)(() => {
1619
+ if (!settings.saving) refreshPromptsStatus();
1620
+ }, [settings.saving, refreshPromptsStatus]);
1621
+ const prompts = settings.prompts;
1622
+ const report = status.status;
1623
+ const text = (id) => {
1624
+ const spec = PROMPT_TEXT_FIELDS.find((entry) => entry.id === id);
1625
+ const state = prompts.texts[id];
1626
+ const labels = PROMPT_LABELS[id];
1627
+ const inputId = `plugin-config-dsh-browser-prompts-${id}`;
1628
+ const common = {
1629
+ id: inputId,
1630
+ className: `${styles.input} ${state.invalid ? styles.invalidInput : ""}`,
1631
+ value: state.text,
1632
+ disabled,
1633
+ "aria-invalid": state.invalid || void 0,
1634
+ onChange: (value) => props.editPromptText(id, value)
1635
+ };
1636
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1637
+ className: styles.field,
1638
+ children: [
1639
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1640
+ className: styles.fieldHead,
1641
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1642
+ className: styles.label,
1643
+ htmlFor: inputId,
1644
+ children: t(labels.label)
1645
+ }), state.text ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1646
+ type: "button",
1647
+ className: styles.reset,
1648
+ disabled,
1649
+ onClick: () => props.editPromptText(id, ""),
1650
+ children: t("promptsReset")
1651
+ }) : null]
1652
+ }),
1653
+ spec.multiline ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1654
+ ...common,
1655
+ className: `${common.className} ${styles.textarea}`,
1656
+ rows: id === "rootNote" ? 3 : 5,
1657
+ spellCheck: false,
1658
+ onChange: (event) => common.onChange(event.currentTarget.value)
1659
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1660
+ ...common,
1661
+ type: "text",
1662
+ onChange: (event) => common.onChange(event.currentTarget.value)
1663
+ }),
1664
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1665
+ className: state.invalid ? styles.failed : styles.hint,
1666
+ children: [state.invalid ? t("promptsTooLong") : t(labels.hint), spec.limit > 1024 || state.text ? ` (${state.text.length}/${spec.limit})` : ""]
1667
+ })
1668
+ ]
1669
+ }, id);
1670
+ };
1671
+ const budgetLine = report ? `${t("promptsL0")}: ~${report.budget.l0Tokens} ${t("promptsTokens")} (${t("promptsBudget")} ${report.budget.l0Budget}) · ${t("promptsLargest")}: ${report.budget.largestLayer.name} ~${report.budget.largestLayer.tokens} (${t("promptsBudget")} ${report.budget.layerBudget})` : "";
1672
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1673
+ className: styles.section,
1674
+ "data-dsh-browser-prompts": true,
1675
+ children: [
1676
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1677
+ className: styles.sectionHead,
1678
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("prompts") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("promptsHint") })]
1679
+ }),
1680
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1681
+ className: styles.grid,
1682
+ children: [
1683
+ text("indexDescription"),
1684
+ text("callDescription"),
1685
+ text("rootGuide"),
1686
+ text("rootNote")
1687
+ ]
1688
+ }),
1689
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1690
+ className: styles.grid,
1691
+ children: [
1692
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1693
+ className: styles.field,
1694
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1695
+ className: styles.toggleLabel,
1696
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1697
+ type: "checkbox",
1698
+ className: styles.check,
1699
+ checked: prompts.skillEnabled,
1700
+ disabled,
1701
+ onChange: (event) => props.setPromptSkillEnabled(event.currentTarget.checked)
1702
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1703
+ className: styles.label,
1704
+ children: t("promptsSkillEnabled")
1705
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1706
+ className: styles.hint,
1707
+ children: t("promptsSkillEnabledHint")
1708
+ })] })]
1709
+ })
1710
+ }),
1711
+ text("skillDescription"),
1712
+ text("skillBodyFile"),
1713
+ text("skillAppend")
1714
+ ]
1715
+ }),
1716
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1717
+ className: styles.field,
1718
+ children: [
1719
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1720
+ className: styles.fieldHead,
1721
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1722
+ className: styles.label,
1723
+ htmlFor: "plugin-config-dsh-browser-prompts-extras",
1724
+ children: t("promptsExtras")
1725
+ }), prompts.extras.text ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1726
+ type: "button",
1727
+ className: styles.reset,
1728
+ disabled,
1729
+ onClick: props.resetPromptExtras,
1730
+ children: t("promptsExtrasReset")
1731
+ }) : null]
1732
+ }),
1733
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1734
+ id: "plugin-config-dsh-browser-prompts-extras",
1735
+ className: `${styles.input} ${styles.textarea} ${styles.code} ${prompts.extras.invalid ? styles.invalidInput : ""}`,
1736
+ rows: 10,
1737
+ spellCheck: false,
1738
+ disabled,
1739
+ "aria-invalid": prompts.extras.invalid || void 0,
1740
+ value: prompts.extras.text,
1741
+ onChange: (event) => props.editPromptExtras(event.currentTarget.value)
1742
+ }),
1743
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1744
+ className: prompts.extras.invalid ? styles.failed : styles.hint,
1745
+ role: prompts.extras.invalid ? "alert" : void 0,
1746
+ children: prompts.extras.invalid ? t("promptsExtrasInvalid") : t("promptsExtrasHint")
1747
+ })
1748
+ ]
1749
+ }),
1750
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1751
+ className: styles.notice,
1752
+ role: "status",
1753
+ "data-dsh-browser-prompts-status": true,
1754
+ children: [
1755
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("promptsStatus") }),
1756
+ " ",
1757
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1758
+ type: "button",
1759
+ className: styles.reset,
1760
+ onClick: refreshPromptsStatus,
1761
+ children: t("promptsRefresh")
1762
+ }),
1763
+ status.loading && !report ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("promptsLoading") }) : null,
1764
+ status.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("promptsStatusUnavailable") }) : null,
1765
+ report ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1766
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1767
+ "data-dsh-browser-prompts-budget": true,
1768
+ children: budgetLine
1769
+ }),
1770
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: report.overrides.length ? `${report.overrides.length} ${t("promptsOverrides")}: ${report.overrides.map((entry) => entry.length === void 0 ? entry.key : `${entry.key} (${entry.length})`).join(", ")}` : t("promptsNoOverrides") }),
1771
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
1772
+ t("promptsDiagnostics"),
1773
+ ": ",
1774
+ report.diagnostics.length ? "" : t("promptsNoDiagnostics")
1775
+ ] }),
1776
+ report.diagnostics.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1777
+ "data-dsh-browser-prompts-diagnostics": true,
1778
+ children: report.diagnostics.map((entry, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: entry.message }, index))
1779
+ }) : null
1780
+ ] }) : null
1781
+ ]
1782
+ }),
1783
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1784
+ className: styles.field,
1785
+ "data-dsh-browser-prompts-export": true,
1786
+ children: [
1787
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1788
+ className: styles.fieldHead,
1789
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1790
+ className: styles.label,
1791
+ children: t("promptsDefaults")
1792
+ }), status.defaults === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1793
+ type: "button",
1794
+ className: styles.secondary,
1795
+ onClick: () => void props.exportPromptDefaults(),
1796
+ children: t("promptsExport")
1797
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1798
+ className: styles.actions,
1799
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1800
+ type: "button",
1801
+ className: styles.secondary,
1802
+ onClick: () => {
1803
+ navigator.clipboard?.writeText(status.defaults ?? "").catch(() => void 0);
1804
+ },
1805
+ children: t("promptsCopy")
1806
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1807
+ type: "button",
1808
+ className: styles.secondary,
1809
+ onClick: props.hidePromptDefaults,
1810
+ children: t("promptsHideDefaults")
1811
+ })]
1812
+ })]
1813
+ }),
1814
+ status.defaults !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1815
+ className: `${styles.input} ${styles.textarea} ${styles.code}`,
1816
+ rows: 12,
1817
+ readOnly: true,
1818
+ spellCheck: false,
1819
+ "aria-label": t("promptsDefaults"),
1820
+ value: status.defaults,
1821
+ onFocus: (event) => event.currentTarget.select(),
1822
+ "data-dsh-browser-prompts-defaults": true
1823
+ }) : null,
1824
+ status.defaultsFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1825
+ className: styles.failed,
1826
+ role: "alert",
1827
+ children: t("promptsDefaultsFailed")
1828
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1829
+ className: styles.hint,
1830
+ children: t("promptsDefaultsHint")
1831
+ })
1832
+ ]
1833
+ }),
1834
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1835
+ className: styles.notice,
1836
+ role: "note",
1837
+ children: t("promptsRestart")
1838
+ })
1839
+ ]
1840
+ });
1841
+ }
392
1842
  function AutomationAssetsPanel(props) {
393
1843
  const { t } = props;
394
1844
  const state = props.useAutomationAssets((snapshot) => snapshot);
395
- const [draft, setDraft] = (0, react.useState)("");
396
- const [draftError, setDraftError] = (0, react.useState)(false);
397
- const [testUrl, setTestUrl] = (0, react.useState)("");
398
- const [testInputs, setTestInputs] = (0, react.useState)("{}");
399
- const selected = state.selected;
1845
+ const { editor, selected } = state;
1846
+ const dirty = isDirty(editor.text, editor.baseline);
1847
+ const credentials = credentialView(selected);
1848
+ const draft = selected?.status === "draft";
400
1849
  (0, react.useEffect)(() => {
401
- if (selected) setDraft(JSON.stringify(selected, null, 2));
402
- }, [selected?.id, selected?.revision]);
403
- const newAsset = (kind) => {
404
- props.selectAutomationAsset(void 0);
405
- setDraft(JSON.stringify(kind === "recipe" ? {
406
- kind,
407
- name: "New recipe",
408
- description: "",
409
- domains: [],
410
- tags: [],
411
- inputNames: [],
412
- recipe: [{
413
- type: "extract",
414
- selector: "main",
415
- mode: "text",
416
- limit: 20
417
- }]
418
- } : {
419
- kind,
420
- name: "New userscript",
421
- description: "",
422
- domains: [],
423
- tags: [],
424
- inputNames: [],
425
- source: "// ==UserScript==\n// @name New userscript\n// @match https://example.com/*\n// @grant none\n// ==/UserScript==\nreturn { title: document.title }"
426
- }, null, 2));
427
- setDraftError(false);
428
- };
429
- const save = async () => {
430
- try {
431
- const value = JSON.parse(draft);
432
- if (selected?.id) value.id = selected.id;
433
- await props.saveAutomationAsset(value);
434
- setDraftError(false);
435
- } catch {
436
- setDraftError(true);
437
- }
438
- };
439
- const runTest = async () => {
440
- if (!selected || !testUrl.trim()) {
441
- setDraftError(true);
442
- return;
443
- }
444
- try {
445
- const inputs = JSON.parse(testInputs);
446
- await props.testAutomationAsset(selected.id, testUrl.trim(), inputs);
447
- setDraftError(false);
448
- } catch {
449
- setDraftError(true);
450
- }
451
- };
1850
+ if (!dirty) return void 0;
1851
+ const guard = (event) => {
1852
+ event.preventDefault();
1853
+ event.returnValue = "";
1854
+ };
1855
+ window.addEventListener("beforeunload", guard);
1856
+ return () => window.removeEventListener("beforeunload", guard);
1857
+ }, [dirty]);
452
1858
  const candidates = state.snapshot?.candidates.filter((candidate) => candidate.suggestedAt && !candidate.dismissedAt) ?? [];
453
1859
  const assets = state.snapshot?.assets ?? [];
1860
+ const noticeLabel = (kind) => t({
1861
+ json: "assetNoticeJson",
1862
+ backend: "assetNoticeBackend",
1863
+ validation: "assetNoticeValidation",
1864
+ refused: "assetNoticeRefused"
1865
+ }[kind]);
1866
+ const saveFirst = dirty || !selected;
1867
+ const latest = credentials.latest;
1868
+ const sets = latest?.revision === selected?.revision ? inputSetLines(latest) : [];
1869
+ const suspect = latest && sets.length ? identicalOutputPairs(latest) : [];
1870
+ const required = state.snapshot?.policy.minInputSetsForActivation ?? 1;
1871
+ const shortfall = selected && draft && credentials.vouches ? inputSetShortfall(selected, credentials.current, required) : void 0;
1872
+ const shortfallText = shortfall ? fill(t("assetNeedsInputSets"), {
1873
+ required: shortfall.required,
1874
+ covered: shortfall.covered
1875
+ }) : "";
1876
+ const convertible = !!selected && selected.kind === "recipe" && (selected.schemaVersion ?? 1) === 1;
454
1877
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
455
1878
  className: styles.section,
456
1879
  "data-dsh-browser-assets": true,
@@ -511,24 +1934,44 @@ window.__ModuleLoader__.load({
511
1934
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
512
1935
  type: "button",
513
1936
  className: styles.secondary,
514
- onClick: () => newAsset("recipe"),
1937
+ onClick: () => props.requestNewAutomationAsset("recipe"),
515
1938
  children: t("assetNewRecipe")
516
1939
  }),
517
1940
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
518
1941
  type: "button",
519
1942
  className: styles.secondary,
520
- onClick: () => newAsset("userscript"),
1943
+ onClick: () => props.requestNewAutomationAsset("userscript"),
521
1944
  children: t("assetNewScript")
522
1945
  }),
523
1946
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
524
1947
  type: "button",
525
1948
  className: styles.secondary,
526
- onClick: props.refreshAutomationAssets,
1949
+ onClick: props.requestRefreshAutomationAssets,
527
1950
  children: t("assetRefresh")
528
1951
  })
529
1952
  ]
530
1953
  })]
531
1954
  }),
1955
+ editor.confirm ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1956
+ className: styles.failed,
1957
+ role: "alertdialog",
1958
+ "aria-label": t("assetLeaveTitle"),
1959
+ "data-dsh-browser-leave": true,
1960
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("assetLeaveTitle") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1961
+ className: styles.actions,
1962
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1963
+ type: "button",
1964
+ className: styles.secondary,
1965
+ onClick: props.cancelLeaveAutomationAsset,
1966
+ children: t("assetLeaveKeep")
1967
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1968
+ type: "button",
1969
+ className: styles.primary,
1970
+ onClick: props.confirmLeaveAutomationAsset,
1971
+ children: t("assetLeaveConfirm")
1972
+ })]
1973
+ })]
1974
+ }) : null,
532
1975
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
533
1976
  className: styles.assetLayout,
534
1977
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -536,7 +1979,7 @@ window.__ModuleLoader__.load({
536
1979
  children: assets.length ? assets.map((asset) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
537
1980
  type: "button",
538
1981
  className: `${styles.assetItem} ${selected?.id === asset.id ? styles.assetSelected : ""}`,
539
- onClick: () => props.selectAutomationAsset(asset.id),
1982
+ onClick: () => props.requestSelectAutomationAsset(asset.id),
540
1983
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: asset.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("small", { children: [
541
1984
  asset.kind,
542
1985
  " · ",
@@ -554,66 +1997,192 @@ window.__ModuleLoader__.load({
554
1997
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
555
1998
  className: styles.assetEditor,
556
1999
  children: [
557
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2000
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
558
2001
  className: styles.label,
559
2002
  htmlFor: "dsh-browser-asset-editor",
560
- children: t("assetEditor")
2003
+ children: [t("assetEditor"), dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2004
+ "data-dsh-browser-dirty": true,
2005
+ children: t("assetEditorDirtyBadge")
2006
+ })] }) : null]
561
2007
  }),
2008
+ selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2009
+ className: styles.hint,
2010
+ "data-dsh-browser-version": true,
2011
+ children: [
2012
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
2013
+ t("assetRevision"),
2014
+ " r",
2015
+ selected.revision,
2016
+ " · ",
2017
+ t("assetHash"),
2018
+ " ",
2019
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: shortHash(selected.contentHash) }),
2020
+ " · ",
2021
+ selected.status,
2022
+ selected.sourceAssetId ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2023
+ " · ",
2024
+ t("assetDerivedFrom"),
2025
+ " ",
2026
+ selected.sourceAssetId.slice(0, 8),
2027
+ " r",
2028
+ selected.sourceRevision
2029
+ ] }) : null
2030
+ ] }),
2031
+ credentials.latest ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2032
+ "data-dsh-browser-credential": true,
2033
+ children: [
2034
+ t("assetLastTest"),
2035
+ ": r",
2036
+ credentials.latest.revision,
2037
+ " · ",
2038
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: shortHash(credentials.latest.contentHash) }),
2039
+ " · ",
2040
+ credentials.latest.passed ? t("assetPassed") : t("assetNotPassed"),
2041
+ " (",
2042
+ credentials.latest.executionStatus,
2043
+ "/",
2044
+ credentials.latest.validationStatus,
2045
+ ", ",
2046
+ credentials.latest.evidenceLevel,
2047
+ credentials.latest.legacy ? ", legacy" : "",
2048
+ ") · ",
2049
+ new Date(credentials.latest.testedAt).toLocaleString(),
2050
+ " · ",
2051
+ t("assetInputsDigest"),
2052
+ " ",
2053
+ credentials.latest.inputsDigest,
2054
+ credentials.latest.revision !== selected.revision ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", t("assetStaleTest")] }) : null
2055
+ ]
2056
+ }) : null,
2057
+ selected.status === "draft" && !credentials.vouches ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("assetNoTest") }) : null,
2058
+ sets.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2059
+ "data-dsh-browser-sets": true,
2060
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [t("assetInputSets"), latest?.plannedSets && latest.plannedSets > sets.length ? ` (${t("assetSetsRan")} ${sets.length}/${latest.plannedSets})` : ""] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: sets.map((line) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2061
+ "data-dsh-browser-set": line.set,
2062
+ children: [
2063
+ t("assetInputSet"),
2064
+ " ",
2065
+ line.set,
2066
+ " ",
2067
+ t("assetInputSetUnit"),
2068
+ " · ",
2069
+ line.passed ? t("assetPassed") : t("assetNotPassed"),
2070
+ " (",
2071
+ line.status,
2072
+ ") · ",
2073
+ t("assetInputsDigest"),
2074
+ " ",
2075
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: line.inputsDigest })
2076
+ ]
2077
+ }, line.set)) })]
2078
+ }) : null,
2079
+ suspect.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2080
+ className: styles.failed,
2081
+ role: "note",
2082
+ "data-dsh-browser-suspect": true,
2083
+ children: fill(t("assetSuspect"), { pairs: suspect.map((pair) => pair.join("/")).join("; ") })
2084
+ }) : null,
2085
+ selected.pendingDisambiguation?.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2086
+ "data-dsh-browser-pending": true,
2087
+ children: fill(t("assetPending"), { n: selected.pendingDisambiguation.length })
2088
+ }) : null
2089
+ ]
2090
+ }) : null,
2091
+ editor.converted ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2092
+ className: styles.notice,
2093
+ role: "status",
2094
+ "data-dsh-browser-converted": true,
2095
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: fill(t("assetConverted"), { name: editor.converted.sourceName }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: editor.converted.notes.map((note, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: note }, index)) })]
2096
+ }) : null,
562
2097
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
563
2098
  id: "dsh-browser-asset-editor",
564
- className: `${styles.input} ${styles.textarea} ${styles.code} ${draftError ? styles.invalidInput : ""}`,
2099
+ "aria-label": t("assetEditor"),
2100
+ className: `${styles.input} ${styles.textarea} ${styles.code} ${editor.notice?.kind === "json" ? styles.invalidInput : ""}`,
565
2101
  rows: 18,
566
- value: draft,
2102
+ value: editor.text,
567
2103
  spellCheck: false,
2104
+ disabled: state.busy,
568
2105
  placeholder: t("assetEditorHint"),
569
- onChange: (event) => {
570
- setDraft(event.currentTarget.value);
571
- setDraftError(false);
572
- }
2106
+ onChange: (event) => props.editAutomationAsset(event.currentTarget.value)
573
2107
  }),
574
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2108
+ editor.notice ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2109
+ className: styles.failed,
2110
+ role: "alert",
2111
+ "data-dsh-browser-notice": editor.notice.kind,
2112
+ children: [
2113
+ noticeLabel(editor.notice.kind),
2114
+ editor.notice.message,
2115
+ editor.notice.code ? ` [${editor.notice.code}]` : ""
2116
+ ]
2117
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
575
2118
  className: styles.hint,
576
- children: draftError ? t("assetInvalid") : t("assetSourceBoundary")
2119
+ children: dirty ? t("assetUnsaved") : t("assetSourceBoundary")
577
2120
  }),
578
- selected?.status === "draft" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2121
+ draft || !selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
579
2122
  className: styles.assetTestForm,
580
2123
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
581
2124
  className: styles.input,
582
- value: testUrl,
2125
+ value: editor.testUrl,
2126
+ "aria-label": t("assetTestUrl"),
583
2127
  placeholder: t("assetTestUrl"),
584
- onChange: (event) => setTestUrl(event.currentTarget.value)
2128
+ onChange: (event) => props.setAssetTestUrl(event.currentTarget.value)
585
2129
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
586
2130
  className: `${styles.input} ${styles.textarea} ${styles.code}`,
587
2131
  rows: 3,
588
- value: testInputs,
2132
+ value: editor.testInputs,
589
2133
  spellCheck: false,
590
2134
  "aria-label": t("assetTestInputs"),
591
- onChange: (event) => setTestInputs(event.currentTarget.value)
2135
+ onChange: (event) => props.setAssetTestInputs(event.currentTarget.value)
592
2136
  })]
593
2137
  }) : null,
2138
+ shortfall ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2139
+ className: styles.hint,
2140
+ role: "note",
2141
+ "data-dsh-browser-activation-hint": true,
2142
+ children: shortfallText
2143
+ }) : null,
594
2144
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
595
2145
  className: styles.actions,
596
2146
  children: [
597
2147
  selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
598
2148
  type: "button",
599
2149
  className: styles.secondary,
600
- disabled: state.busy,
2150
+ disabled: state.busy || dirty,
601
2151
  onClick: () => props.validateAutomationAsset(selected.id),
602
2152
  children: t("assetValidate")
603
2153
  }) : null,
604
- selected?.status === "draft" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2154
+ draft || !selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2155
+ type: "button",
2156
+ className: styles.secondary,
2157
+ disabled: state.busy || !editor.testUrl.trim() || !editor.text,
2158
+ onClick: () => void props.saveAndTestAutomationAsset(),
2159
+ children: saveFirst ? t("assetSaveAndTest") : t("assetTest")
2160
+ }) : null,
2161
+ draft ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2162
+ type: "button",
2163
+ className: styles.secondary,
2164
+ disabled: state.busy || dirty || !credentials.vouches || !!shortfall,
2165
+ title: dirty ? t("assetActivateNeedsSave") : shortfallText || void 0,
2166
+ onClick: () => void props.activateAutomationAsset(),
2167
+ children: [
2168
+ t("assetActivate"),
2169
+ " r",
2170
+ selected.revision
2171
+ ]
2172
+ }) : null,
2173
+ convertible ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
605
2174
  type: "button",
606
2175
  className: styles.secondary,
607
- disabled: state.busy || !testUrl.trim(),
608
- onClick: () => void runTest(),
609
- children: t("assetTest")
2176
+ disabled: state.busy,
2177
+ onClick: () => props.requestConvertAutomationAsset(selected.id),
2178
+ children: t("assetConvert")
610
2179
  }) : null,
611
- selected?.status === "draft" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2180
+ selected && selected.status !== "draft" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
612
2181
  type: "button",
613
2182
  className: styles.secondary,
614
- disabled: state.busy || selected.testStatus !== "passed",
615
- onClick: () => props.setAutomationAssetStatus(selected.id, "active"),
616
- children: t("assetActivate")
2183
+ disabled: state.busy,
2184
+ onClick: () => props.requestForkAutomationAsset(selected.id),
2185
+ children: t("assetFork")
617
2186
  }) : null,
618
2187
  selected && selected.status !== "archived" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
619
2188
  type: "button",
@@ -625,8 +2194,8 @@ window.__ModuleLoader__.load({
625
2194
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
626
2195
  type: "button",
627
2196
  className: styles.primary,
628
- disabled: state.busy || !draft || selected?.status === "active",
629
- onClick: () => void save(),
2197
+ disabled: state.busy || !editor.text || selected?.status === "active",
2198
+ onClick: () => void props.saveEditedAutomationAsset(),
630
2199
  children: t("assetSaveDraft")
631
2200
  })
632
2201
  ]
@@ -658,22 +2227,28 @@ window.__ModuleLoader__.load({
658
2227
  enabledHint: "关闭后浏览器服务不可用。",
659
2228
  automationMode: "自动化模式",
660
2229
  automationModeHint: "read-only / standard / autonomous / unrestricted。",
2230
+ toolSurface: "工具面形态",
2231
+ toolSurfaceHint: "indexed(默认)只常驻 browser_index / browser_call 两个工具;flat 为每个动作注册独立工具,常驻上下文明显更大。保存后需重启 profile。",
661
2232
  browserRuntime: "运行时提供器",
662
2233
  browserRuntimeHint: "Patchright 仅支持 Chromium。",
663
2234
  channel: "浏览器通道",
664
2235
  channelHint: "chromium、chrome 或 msedge。Patchright 推荐 chrome。",
665
2236
  cdpPort: "CDP 调试端口",
666
2237
  cdpPortHint: "可选的本机远程调试端口(1–65535);留空即关闭。",
2238
+ maxSessions: "浏览器会话上限",
2239
+ maxSessionsHint: "同时持有页面的 session 数(1–64,默认 8);超出时关闭最久未使用的。所有 session 共用一个浏览器进程。",
667
2240
  args: "Chromium 启动参数 JSON",
668
- argsHint: "可选字符串数组;仅添加你信任并理解其影响的参数。",
2241
+ argsHint: "可选的字符串数组,例如 [\"--disable-gpu\"];仅添加你信任并理解其影响的参数。",
2242
+ configFileOnly: "结构较复杂,这里不提供表单:请在 profile 配置文件中本插件的配置段里编辑。",
669
2243
  headless: "无头模式",
670
2244
  headlessHint: "Patchright 兼容性最佳配置通常是关闭无头模式。",
671
2245
  opencliEnabled: "启用 OpenCLI",
672
2246
  opencliEnabledHint: "站点 adapter 和 Chrome Browser Bridge 总开关。",
673
2247
  usagePolicy: "调用缓冲 JSON",
674
- usagePolicyHint: "minDelayMs、maxConcurrency、burst、maxPagesPerRun、maxDepth、retryLimit、backoffBaseMs、cooldownMs。",
2248
+ usagePolicyHint: "并发、短时突发、爬取预算与退避;只写要改的键。",
2249
+ policyKeys: "可用键名:",
675
2250
  automationAssets: "自动化资产策略 JSON",
676
- automationAssetsHint: "候选阈值、持久化模式、激活模式、数量与上下文预算。",
2251
+ automationAssetsHint: "候选阈值、持久化模式、激活模式、数量与上下文预算、激活所需的输入组数;只写要改的键。",
677
2252
  assetLibrary: "可复用自动化资产(实验性)",
678
2253
  assetLibraryHint: "实验功能:候选先提示是否总结;草稿真实回放后再手动激活。源码仅在点选编辑时读取。",
679
2254
  assetLoading: "正在读取本地自动化资产…",
@@ -696,18 +2271,91 @@ window.__ModuleLoader__.load({
696
2271
  assetValidate: "静态校验",
697
2272
  assetTest: "真实回放",
698
2273
  assetTestUrl: "测试 URL(必须命中允许域名)",
699
- assetTestInputs: "测试输入 JSON",
2274
+ assetTestInputs: "测试输入 JSON(一个对象,或 2–5 个对象组成的数组,每组在全新上下文中回放)",
700
2275
  assetActivate: "激活",
701
2276
  assetArchive: "归档",
702
2277
  assetSaveDraft: "保存草稿",
2278
+ assetSaveAndTest: "保存并测试",
2279
+ assetUnsaved: "有未保存的修改:测试与激活不会使用它们,直到保存。",
2280
+ assetLeaveTitle: "有未保存的修改,离开会丢失它们。",
2281
+ assetLeaveConfirm: "放弃修改并继续",
2282
+ assetLeaveKeep: "继续编辑",
2283
+ assetNoticeJson: "JSON 无效,请求未发出:",
2284
+ assetNoticeBackend: "后端错误:",
2285
+ assetNoticeValidation: "回放已执行,但业务断言未通过:",
2286
+ assetNoticeRefused: "已拦截:",
2287
+ assetRevision: "版本",
2288
+ assetHash: "内容哈希",
2289
+ assetLastTest: "最近测试",
2290
+ assetNoTest: "当前版本尚未通过测试,不能激活。",
2291
+ assetStaleTest: "最近一次测试针对旧版本",
2292
+ assetPassed: "通过",
2293
+ assetNotPassed: "未通过",
2294
+ assetFork: "创建修复草稿",
2295
+ assetDerivedFrom: "派生自",
2296
+ assetInputsDigest: "输入摘要",
2297
+ assetActivateNeedsSave: "有未保存的修改:先「保存并测试」。",
2298
+ assetEditorDirtyBadge: "未保存",
2299
+ assetConvert: "转换为 v2",
2300
+ assetConverted: "已由「{name}」转换为新的 v2 草稿,原资产未改动。",
2301
+ assetPending: "{n} 个步骤仍取第一个匹配(pendingDisambiguation):先让定位唯一再测试。",
2302
+ assetInputSets: "最近一次测试的输入组",
2303
+ assetInputSet: "第",
2304
+ assetInputSetUnit: "组",
2305
+ assetSetsRan: "已运行",
2306
+ assetSuspect: "PARAMETERIZATION_SUSPECT:第 {pairs} 组输入不同,输出却完全相同,输入可能没有真正影响结果;测试仍算通过,请检查占位符是否传到了页面。",
2307
+ assetNeedsInputSets: "该资产声明了输入:激活要求当前版本的通过测试至少覆盖 {required} 组不同输入(当前 {covered} 组)。在“测试输入 JSON”里填 {required} 到 5 个对象的数组,再测试。",
2308
+ prompts: "提示文本",
2309
+ promptsHint: "覆盖模型看到的提示和建议文本;留空即使用内置默认值;“导出默认文本”可看到全部默认值。",
2310
+ promptsRestart: "两个工具的描述在注册时固定,保存后需重启 profile 才生效;其余文本在下一次调用时即生效。",
2311
+ promptsIndexDescription: "browser_index 工具描述",
2312
+ promptsIndexDescriptionHint: "常驻上下文,越短越好。上限 1500 字符;需重启生效。",
2313
+ promptsCallDescription: "browser_call 工具描述",
2314
+ promptsCallDescriptionHint: "合规声明会自动追加在末尾,无法删除。上限 1500 字符;需重启生效。",
2315
+ promptsRootGuide: "精简指南(无 skill 时)",
2316
+ promptsRootGuideHint: "无 skill 时替换 browser_index 根目录里的精简指南。上限 1500 字符。",
2317
+ promptsRootNote: "根目录附言",
2318
+ promptsRootNoteHint: "追加在 browser_index 根目录末尾,有 skill 时也显示;写部署方自己的建议。上限 800 字符。",
2319
+ promptsSkillEnabled: "注册 dsh-browser skill",
2320
+ promptsSkillEnabledHint: "关闭后不注册 skill,根目录改用精简指南。",
2321
+ promptsSkillDescription: "skill 描述",
2322
+ promptsSkillDescriptionHint: "替换技能列表里的描述。上限 1000 字符。",
2323
+ promptsSkillBodyFile: "skill 正文文件",
2324
+ promptsSkillBodyFileHint: "用该 Markdown 文件(绝对路径,≤ 20000 字符)替换 SKILL.md 正文;文件缺失或不可读时回退到内置正文。",
2325
+ promptsSkillAppend: "skill 正文追加",
2326
+ promptsSkillAppendHint: "追加到 skill 正文末尾。上限 4000 字符。",
2327
+ promptsExtras: "分组 / 动作 / 错误提示覆盖 JSON",
2328
+ promptsExtrasHint: "{\"groups\":{\"act\":{\"summary\":\"…\"}},\"actions\":{\"act.click\":{\"summary\":\"…\",\"notes\":\"…\"}},\"errorHints\":{\"DEADLINE\":\"…\"}}。动作名可含子动作(automation.develop.save)和详情页(observe.read.controls)。",
2329
+ promptsDefaults: "默认文本(只读)",
2330
+ promptsExport: "导出默认文本",
2331
+ promptsCopy: "复制",
2332
+ promptsHideDefaults: "收起",
2333
+ promptsDefaultsHint: "一份 JSON,结构与上面的覆盖项完全一致。复制后删掉保持默认的部分,改写其余部分,再放进配置;原样放回不会改变任何输出。",
2334
+ promptsDefaultsFailed: "暂时读不到默认文本,请稍后再试。",
2335
+ promptsExtrasReset: "恢复默认",
2336
+ promptsReset: "恢复默认",
2337
+ promptsExtrasInvalid: "JSON 无效,或键名、类型、长度超出限制。",
2338
+ promptsTooLong: "超出长度上限。",
2339
+ promptsStatus: "当前生效情况",
2340
+ promptsL0: "L0 常驻工具估算",
2341
+ promptsTokens: "tokens",
2342
+ promptsBudget: "预算",
2343
+ promptsOverrides: "项覆盖生效",
2344
+ promptsNoOverrides: "没有覆盖,全部使用内置默认值。",
2345
+ promptsDiagnostics: "诊断",
2346
+ promptsNoDiagnostics: "无诊断。",
2347
+ promptsStatusUnavailable: "暂时读不到插件状态,请稍后刷新。",
2348
+ promptsRefresh: "刷新",
2349
+ promptsLoading: "读取中…",
2350
+ promptsLargest: "最大的目录层",
703
2351
  autoInstall: "缺失时自动安装 Chromium",
704
- autoInstallHint: "可能触发较大下载,日常建议关闭并显式调用 browser_install。",
2352
+ autoInstallHint: "可能触发较大下载,日常建议关闭并显式执行 runtime.install 动作。",
705
2353
  storageStatePath: "全局 storageState 路径",
706
2354
  storageStatePathHint: "旧版兼容入口;新配置优先使用限域 AuthProfile。",
707
2355
  authProfiles: "AuthProfiles JSON",
708
2356
  authProfilesHint: "命名登录态、allowedDomains 与 persistState。",
709
2357
  defaultAuthProfile: "默认 AuthProfile",
710
- defaultAuthProfileHint: "未显式选择登录态时使用;browser_crawl 始终匿名。",
2358
+ defaultAuthProfileHint: "未显式选择登录态时使用;crawl.crawl 始终匿名。",
711
2359
  rulePacks: "RulePacks JSON",
712
2360
  rulePacksHint: "域名匹配、哈希固定 init script 和有界步骤。",
713
2361
  executablePath: "浏览器可执行文件",
@@ -750,22 +2398,28 @@ window.__ModuleLoader__.load({
750
2398
  enabledHint: "Disabling makes the browser service unavailable.",
751
2399
  automationMode: "Automation mode",
752
2400
  automationModeHint: "read-only / standard / autonomous / unrestricted.",
2401
+ toolSurface: "Tool surface",
2402
+ toolSurfaceHint: "indexed (default) keeps only browser_index / browser_call in context; flat registers one tool per action, which costs far more context. Restart the profile after saving.",
753
2403
  browserRuntime: "Runtime provider",
754
2404
  browserRuntimeHint: "Patchright is Chromium-only.",
755
2405
  channel: "Browser channel",
756
2406
  channelHint: "chromium, chrome, or msedge. Patchright recommends chrome.",
757
2407
  cdpPort: "CDP debugging port",
758
2408
  cdpPortHint: "Optional local remote-debugging port (1–65535); leave empty to disable.",
2409
+ maxSessions: "Browser session limit",
2410
+ maxSessionsHint: "Sessions that may hold a page at once (1–64, default 8); past it the least recently used one is closed. One browser process serves them all.",
759
2411
  args: "Chromium launch arguments JSON",
760
- argsHint: "Optional string array; add only arguments whose effects you understand and trust.",
2412
+ argsHint: "Optional array of strings, for example [\"--disable-gpu\"]; add only arguments whose effects you understand and trust.",
2413
+ configFileOnly: "Nested structure, so there is no form here: edit it in the plugin's section of the profile config file.",
761
2414
  headless: "Headless mode",
762
2415
  headlessHint: "Patchright compatibility is usually strongest in headed mode.",
763
2416
  opencliEnabled: "Enable OpenCLI",
764
2417
  opencliEnabledHint: "Master switch for site adapters and the Chrome Browser Bridge.",
765
2418
  usagePolicy: "Usage buffer JSON",
766
- usagePolicyHint: "minDelayMs, maxConcurrency, burst, maxPagesPerRun, maxDepth, retryLimit, backoffBaseMs, cooldownMs.",
2419
+ usagePolicyHint: "Concurrency, bursts, crawl budgets and backoff; write only the keys you change.",
2420
+ policyKeys: "Keys:",
767
2421
  automationAssets: "Automation asset policy JSON",
768
- automationAssetsHint: "Candidate thresholds, persistence, activation, count, and context budgets.",
2422
+ automationAssetsHint: "Candidate thresholds, persistence, activation, counts and context budgets, input sets needed to activate; write only the keys you change.",
769
2423
  assetLibrary: "Reusable automation assets (Experimental)",
770
2424
  assetLibraryHint: "Experimental: candidates ask before summarization; drafts require runtime replay before manual activation. Source loads only when selected.",
771
2425
  assetLoading: "Loading local automation assets…",
@@ -788,18 +2442,91 @@ window.__ModuleLoader__.load({
788
2442
  assetValidate: "Static validate",
789
2443
  assetTest: "Runtime replay",
790
2444
  assetTestUrl: "Test URL (must match an allowed domain)",
791
- assetTestInputs: "Test inputs JSON",
2445
+ assetTestInputs: "Test inputs JSON (one object, or an array of 2–5 objects, each replayed in a fresh context)",
792
2446
  assetActivate: "Activate",
793
2447
  assetArchive: "Archive",
794
2448
  assetSaveDraft: "Save draft",
2449
+ assetSaveAndTest: "Save and test",
2450
+ assetUnsaved: "Unsaved edits: testing and activation do not use them until they are saved.",
2451
+ assetLeaveTitle: "You have unsaved edits; leaving discards them.",
2452
+ assetLeaveConfirm: "Discard edits and continue",
2453
+ assetLeaveKeep: "Keep editing",
2454
+ assetNoticeJson: "Invalid JSON, nothing was sent: ",
2455
+ assetNoticeBackend: "Backend error: ",
2456
+ assetNoticeValidation: "The replay ran, but the business check did not hold: ",
2457
+ assetNoticeRefused: "Blocked: ",
2458
+ assetRevision: "Revision",
2459
+ assetHash: "Content hash",
2460
+ assetLastTest: "Latest test",
2461
+ assetNoTest: "This revision has no passed test, so it cannot be activated.",
2462
+ assetStaleTest: "The latest test was of an older revision",
2463
+ assetPassed: "passed",
2464
+ assetNotPassed: "not passed",
2465
+ assetFork: "Create repair draft",
2466
+ assetDerivedFrom: "Derived from",
2467
+ assetInputsDigest: "inputs digest",
2468
+ assetActivateNeedsSave: "Unsaved edits: use \"Save and test\" first.",
2469
+ assetEditorDirtyBadge: "unsaved",
2470
+ assetConvert: "Convert to v2",
2471
+ assetConverted: "Converted \"{name}\" into a new v2 draft; the original is untouched.",
2472
+ assetPending: "{n} step(s) still take the first match (pendingDisambiguation): make their locators unique before testing.",
2473
+ assetInputSets: "Input sets of the latest test",
2474
+ assetInputSet: "Set",
2475
+ assetInputSetUnit: "",
2476
+ assetSetsRan: "ran",
2477
+ assetSuspect: "PARAMETERIZATION_SUSPECT: sets {pairs} had different inputs but identical outputs, so the input may not drive the result. The test still passed; check that the placeholder reaches the page.",
2478
+ assetNeedsInputSets: "This asset declares inputs: activation needs a passing test of this revision that covered at least {required} different input sets (it covered {covered}). Put an array of {required} to 5 objects in the test inputs, then test.",
2479
+ prompts: "Prompt text",
2480
+ promptsHint: "Override the guidance and advice text the model reads; leave a field empty for the built-in default. \"Export default text\" shows every default.",
2481
+ promptsRestart: "The two tool descriptions are fixed at registration, so they apply after the profile restarts; everything else applies on the next call.",
2482
+ promptsIndexDescription: "browser_index tool description",
2483
+ promptsIndexDescriptionHint: "Always in context, so keep it short. At most 1500 characters; needs a restart.",
2484
+ promptsCallDescription: "browser_call tool description",
2485
+ promptsCallDescriptionHint: "The compliance notice is always appended and cannot be removed. At most 1500 characters; needs a restart.",
2486
+ promptsRootGuide: "Compact guide (no skill)",
2487
+ promptsRootGuideHint: "Replaces the compact guide at the browser_index root when no skill is available. At most 1500 characters.",
2488
+ promptsRootNote: "Root note",
2489
+ promptsRootNoteHint: "Appended to the end of the browser_index root, also when the skill is available: your own advice. At most 800 characters.",
2490
+ promptsSkillEnabled: "Register the dsh-browser skill",
2491
+ promptsSkillEnabledHint: "Off: the skill is not registered and the root shows the compact guide instead.",
2492
+ promptsSkillDescription: "Skill description",
2493
+ promptsSkillDescriptionHint: "Replaces the description in the skill list. At most 1000 characters.",
2494
+ promptsSkillBodyFile: "Skill body file",
2495
+ promptsSkillBodyFileHint: "A Markdown file (absolute path, at most 20000 characters) that replaces the SKILL.md body; a missing or unreadable file falls back to the packaged body.",
2496
+ promptsSkillAppend: "Skill body addition",
2497
+ promptsSkillAppendHint: "Appended to the end of the skill body. At most 4000 characters.",
2498
+ promptsExtras: "Group / action / error hint overrides (JSON)",
2499
+ promptsExtrasHint: "{\"groups\":{\"act\":{\"summary\":\"…\"}},\"actions\":{\"act.click\":{\"summary\":\"…\",\"notes\":\"…\"}},\"errorHints\":{\"DEADLINE\":\"…\"}}. Action keys may name a sub-action (automation.develop.save) or a detail page (observe.read.controls).",
2500
+ promptsDefaults: "Default text (read-only)",
2501
+ promptsExport: "Export default text",
2502
+ promptsCopy: "Copy",
2503
+ promptsHideDefaults: "Hide",
2504
+ promptsDefaultsHint: "One JSON in the exact structure of the overrides above. Copy it, delete what stays default, reword the rest, and put it in the configuration; pasted back unchanged it changes no output.",
2505
+ promptsDefaultsFailed: "The default text is not available right now; try again in a moment.",
2506
+ promptsExtrasReset: "Restore defaults",
2507
+ promptsReset: "Restore default",
2508
+ promptsExtrasInvalid: "Invalid JSON, or a key, type or length outside the limits.",
2509
+ promptsTooLong: "Over the length limit.",
2510
+ promptsStatus: "Currently in effect",
2511
+ promptsL0: "Always-on L0 tools (estimate)",
2512
+ promptsTokens: "tokens",
2513
+ promptsBudget: "budget",
2514
+ promptsOverrides: "overrides in effect",
2515
+ promptsNoOverrides: "No overrides: everything uses the built-in defaults.",
2516
+ promptsDiagnostics: "Diagnostics",
2517
+ promptsNoDiagnostics: "No diagnostics.",
2518
+ promptsStatusUnavailable: "The plugin status is not available right now; refresh in a moment.",
2519
+ promptsRefresh: "Refresh",
2520
+ promptsLoading: "Loading…",
2521
+ promptsLargest: "Largest catalog layer",
795
2522
  autoInstall: "Auto-install Chromium when missing",
796
- autoInstallHint: "May download a large binary; explicit browser_install is safer for daily use.",
2523
+ autoInstallHint: "May download a large binary; running the runtime.install action explicitly is safer for daily use.",
797
2524
  storageStatePath: "Global storageState path",
798
2525
  storageStatePathHint: "Legacy fallback; prefer domain-scoped AuthProfiles.",
799
2526
  authProfiles: "AuthProfiles JSON",
800
2527
  authProfilesHint: "Named states with allowedDomains and persistState.",
801
2528
  defaultAuthProfile: "Default AuthProfile",
802
- defaultAuthProfileHint: "Used when no profile is selected; browser_crawl is always anonymous.",
2529
+ defaultAuthProfileHint: "Used when no profile is selected; crawl.crawl is always anonymous.",
803
2530
  rulePacks: "RulePacks JSON",
804
2531
  rulePacksHint: "Domain match, hash-pinned init scripts, and bounded steps.",
805
2532
  executablePath: "Browser executable",
@@ -825,156 +2552,9 @@ window.__ModuleLoader__.load({
825
2552
  };
826
2553
  //#endregion
827
2554
  //#region src/client/settings-namespace.ts
828
- /** Settings namespace used by both the Host registry and the card slot key. */
2555
+ /** The settings namespace: the loader entry id of the bundle patch row (`cordis.patch.yml`), which `ctx.configForms` addresses. */
829
2556
  const SETTINGS_NAMESPACE = "browser";
830
2557
  //#endregion
831
- //#region src/client/automation-assets-client.ts
832
- function localStore(initial) {
833
- let snapshot = initial;
834
- const listeners = /* @__PURE__ */ new Set();
835
- return {
836
- getSnapshot: () => snapshot,
837
- subscribe(listener) {
838
- listeners.add(listener);
839
- return () => {
840
- listeners.delete(listener);
841
- };
842
- },
843
- set(next) {
844
- snapshot = next;
845
- for (const listener of listeners) listener();
846
- },
847
- update(updater) {
848
- const draft = structuredClone(snapshot);
849
- updater(draft);
850
- snapshot = draft;
851
- for (const listener of listeners) listener();
852
- }
853
- };
854
- }
855
- /** This plugin's private RPC channel, matching the Host half. */
856
- const CHANNEL = "/dsh-browser-assets";
857
- var AutomationAssetsController = class {
858
- rpc;
859
- store = localStore({
860
- loading: true,
861
- busy: false,
862
- failed: false
863
- });
864
- disposed = false;
865
- constructor(rpc) {
866
- this.rpc = rpc;
867
- this.refresh();
868
- }
869
- inject() {
870
- return {
871
- hooks: { automationAssets: this.store },
872
- refreshAutomationAssets: () => {
873
- this.refresh();
874
- },
875
- selectAutomationAsset: (id) => {
876
- this.select(id);
877
- },
878
- saveAutomationAsset: (asset) => this.mutate("save", { asset }),
879
- summarizeAutomationCandidate: (id) => this.mutate("summarize", { id }),
880
- dismissAutomationCandidate: (id) => this.mutate("dismiss", { id }),
881
- validateAutomationAsset: (id) => this.mutate("validate", { id }),
882
- testAutomationAsset: (id, url, inputs) => this.mutate("test", {
883
- id,
884
- url,
885
- inputs
886
- }),
887
- setAutomationAssetStatus: (id, status) => this.mutate("status", {
888
- id,
889
- status
890
- })
891
- };
892
- }
893
- snapshot() {
894
- return this.store.getSnapshot();
895
- }
896
- dispose() {
897
- this.disposed = true;
898
- }
899
- async refresh() {
900
- this.publish({
901
- ...this.store.getSnapshot(),
902
- loading: true,
903
- failed: false,
904
- error: void 0
905
- });
906
- try {
907
- const snapshot = await this.call("snapshot", {});
908
- this.publish({
909
- ...this.store.getSnapshot(),
910
- loading: false,
911
- snapshot
912
- });
913
- } catch (error) {
914
- this.fail(error);
915
- }
916
- }
917
- async select(id) {
918
- if (!id) {
919
- this.publish({
920
- ...this.store.getSnapshot(),
921
- selected: void 0
922
- });
923
- return;
924
- }
925
- try {
926
- const selected = await this.call("get", { id });
927
- this.publish({
928
- ...this.store.getSnapshot(),
929
- selected: selected ?? void 0,
930
- failed: false,
931
- error: void 0
932
- });
933
- } catch (error) {
934
- this.fail(error);
935
- }
936
- }
937
- async mutate(endpoint, payload) {
938
- this.publish({
939
- ...this.store.getSnapshot(),
940
- busy: true,
941
- failed: false,
942
- error: void 0
943
- });
944
- try {
945
- const value = await this.call(endpoint, payload);
946
- const selected = value && typeof value === "object" && "id" in value ? value : this.store.getSnapshot().selected;
947
- const snapshot = await this.call("snapshot", {});
948
- this.publish({
949
- loading: false,
950
- busy: false,
951
- failed: false,
952
- snapshot,
953
- ...selected ? { selected } : {}
954
- });
955
- } catch (error) {
956
- this.fail(error);
957
- }
958
- }
959
- async call(endpoint, payload) {
960
- const result = await this.rpc.call(CHANNEL, endpoint, payload);
961
- if (!result.ok) throw new Error(result.error.message);
962
- return result.value;
963
- }
964
- fail(error) {
965
- this.publish({
966
- ...this.store.getSnapshot(),
967
- loading: false,
968
- busy: false,
969
- failed: true,
970
- error: String(error instanceof Error ? error.message : error).slice(0, 300)
971
- });
972
- }
973
- publish(state) {
974
- if (!this.disposed) this.store.set(state);
975
- }
976
- };
977
- //#endregion
978
2558
  //#region src/client/index.ts
979
2559
  const name = "dsh-browser-client";
980
2560
  const inject = [
@@ -992,7 +2572,9 @@ window.__ModuleLoader__.load({
992
2572
  }), "dsh-browser: settings dictionaries");
993
2573
  const controller = new BrowserSettingsController(ctx.configForms.get(SETTINGS_NAMESPACE));
994
2574
  const assets = new AutomationAssetsController(ctx.connection.rpc);
2575
+ const prompts = new PromptsStatusController(ctx.connection.rpc);
995
2576
  ctx.effect(() => () => controller.dispose(), "dsh-browser: settings controller");
2577
+ ctx.effect(() => () => prompts.dispose(), "dsh-browser: prompts status controller");
996
2578
  ctx.effect(() => () => assets.dispose(), "dsh-browser: automation assets controller");
997
2579
  ctx.effect(() => ctx.configForms.whileServed([SETTINGS_NAMESPACE], () => ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
998
2580
  name: "plugins.bundle.config",
@@ -1001,12 +2583,15 @@ window.__ModuleLoader__.load({
1001
2583
  inject: () => {
1002
2584
  const settingsProps = controller.inject();
1003
2585
  const assetProps = assets.inject();
2586
+ const promptsProps = prompts.inject();
1004
2587
  return {
1005
2588
  ...settingsProps,
1006
2589
  ...assetProps,
2590
+ ...promptsProps,
1007
2591
  hooks: {
1008
2592
  ...settingsProps.hooks,
1009
- ...assetProps.hooks
2593
+ ...assetProps.hooks,
2594
+ ...promptsProps.hooks
1010
2595
  }
1011
2596
  };
1012
2597
  }