@anweat/dsh-browser 0.1.17 → 0.2.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +238 -71
- package/assets/skills/dsh-browser/SKILL.md +120 -0
- package/assets/skills/dsh-browser/references/develop.md +48 -0
- package/assets/skills/dsh-browser/references/observe.md +91 -0
- package/assets/skills/dsh-browser/references/opencli.md +25 -0
- package/assets/skills/dsh-browser/references/recipes.md +149 -0
- package/assets/skills/dsh-browser/references/userscripts.md +44 -0
- package/cordis.patch.yml +4 -0
- package/lib/actions/act.d.ts +3 -0
- package/lib/actions/act.js +147 -0
- package/lib/actions/act.js.map +1 -0
- package/lib/actions/automation.d.ts +3 -0
- package/lib/actions/automation.js +356 -0
- package/lib/actions/automation.js.map +1 -0
- package/lib/actions/crawl.d.ts +3 -0
- package/lib/actions/crawl.js +28 -0
- package/lib/actions/crawl.js.map +1 -0
- package/lib/actions/errors.d.ts +53 -0
- package/lib/actions/errors.js +178 -0
- package/lib/actions/errors.js.map +1 -0
- package/lib/actions/index-view.d.ts +42 -0
- package/lib/actions/index-view.js +268 -0
- package/lib/actions/index-view.js.map +1 -0
- package/lib/actions/inspect.d.ts +3 -0
- package/lib/actions/inspect.js +35 -0
- package/lib/actions/inspect.js.map +1 -0
- package/lib/actions/observe.d.ts +3 -0
- package/lib/actions/observe.js +121 -0
- package/lib/actions/observe.js.map +1 -0
- package/lib/actions/opencli.d.ts +3 -0
- package/lib/actions/opencli.js +51 -0
- package/lib/actions/opencli.js.map +1 -0
- package/lib/actions/registry.d.ts +35 -0
- package/lib/actions/registry.js +89 -0
- package/lib/actions/registry.js.map +1 -0
- package/lib/actions/run.d.ts +34 -0
- package/lib/actions/run.js +226 -0
- package/lib/actions/run.js.map +1 -0
- package/lib/actions/runtime.d.ts +3 -0
- package/lib/actions/runtime.js +33 -0
- package/lib/actions/runtime.js.map +1 -0
- package/lib/actions/schema.d.ts +31 -0
- package/lib/actions/schema.js +309 -0
- package/lib/actions/schema.js.map +1 -0
- package/lib/actions/script.d.ts +3 -0
- package/lib/actions/script.js +97 -0
- package/lib/actions/script.js.map +1 -0
- package/lib/actions/shared.d.ts +31 -0
- package/lib/actions/shared.js +45 -0
- package/lib/actions/shared.js.map +1 -0
- package/lib/actions/surface.d.ts +22 -0
- package/lib/actions/surface.js +18 -0
- package/lib/actions/surface.js.map +1 -0
- package/lib/actions/target.d.ts +3 -0
- package/lib/actions/target.js +66 -0
- package/lib/actions/target.js.map +1 -0
- package/lib/actions/types.d.ts +181 -0
- package/lib/actions/types.js +48 -0
- package/lib/actions/types.js.map +1 -0
- package/lib/activation-rules.d.ts +29 -0
- package/lib/activation-rules.js +33 -0
- package/lib/activation-rules.js.map +1 -0
- package/lib/approval-policy.d.ts +40 -2
- package/lib/approval-policy.js +184 -97
- package/lib/approval-policy.js.map +1 -1
- package/lib/automation-assets-rpc.d.ts +8 -2
- package/lib/automation-assets-rpc.js +67 -6
- package/lib/automation-assets-rpc.js.map +1 -1
- package/lib/automation-assets.d.ts +152 -4
- package/lib/automation-assets.js +255 -15
- package/lib/automation-assets.js.map +1 -1
- package/lib/automation-convert.d.ts +23 -0
- package/lib/automation-convert.js +110 -0
- package/lib/automation-convert.js.map +1 -0
- package/lib/automation-development.d.ts +26 -0
- package/lib/automation-development.js +47 -0
- package/lib/automation-development.js.map +1 -1
- package/lib/automation-execution.d.ts +57 -1
- package/lib/automation-execution.js +167 -11
- package/lib/automation-execution.js.map +1 -1
- package/lib/automation-journal.d.ts +92 -0
- package/lib/automation-journal.js +449 -0
- package/lib/automation-journal.js.map +1 -0
- package/lib/automation-v2.d.ts +138 -0
- package/lib/automation-v2.js +631 -0
- package/lib/automation-v2.js.map +1 -0
- package/lib/automation.d.ts +82 -5
- package/lib/automation.js +239 -82
- package/lib/automation.js.map +1 -1
- package/lib/browser-service.d.ts +304 -32
- package/lib/browser-service.js +631 -182
- package/lib/browser-service.js.map +1 -1
- package/lib/client/SettingsCard.js +95 -57
- package/lib/client/SettingsCard.js.map +1 -1
- package/lib/client/asset-editor.d.ts +55 -0
- package/lib/client/asset-editor.js +101 -0
- package/lib/client/asset-editor.js.map +1 -0
- package/lib/client/automation-assets-client.d.ts +99 -2
- package/lib/client/automation-assets-client.js +243 -10
- package/lib/client/automation-assets-client.js.map +1 -1
- package/lib/client/form.d.ts +48 -4
- package/lib/client/form.js +142 -29
- package/lib/client/form.js.map +1 -1
- package/lib/client/index.d.ts +25 -2
- package/lib/client/index.js +5 -1
- package/lib/client/index.js.map +1 -1
- package/lib/client/locales.d.ts +85 -6
- package/lib/client/locales.js +64 -12
- package/lib/client/locales.js.map +1 -1
- package/lib/client/prompts-form.d.ts +67 -0
- package/lib/client/prompts-form.js +140 -0
- package/lib/client/prompts-form.js.map +1 -0
- package/lib/client/prompts-status-client.d.ts +34 -0
- package/lib/client/prompts-status-client.js +53 -0
- package/lib/client/prompts-status-client.js.map +1 -0
- package/lib/client.js +1914 -329
- package/lib/client.js.map +1 -1
- package/lib/config.d.ts +50 -1
- package/lib/config.js +31 -1
- package/lib/config.js.map +1 -1
- package/lib/dialogs.d.ts +37 -0
- package/lib/dialogs.js +73 -0
- package/lib/dialogs.js.map +1 -0
- package/lib/freedom.d.ts +23 -7
- package/lib/freedom.js +48 -59
- package/lib/freedom.js.map +1 -1
- package/lib/index.d.ts +4 -4
- package/lib/index.js +36 -10
- package/lib/index.js.map +1 -1
- package/lib/journal.d.ts +109 -0
- package/lib/journal.js +238 -0
- package/lib/journal.js.map +1 -0
- package/lib/locator.d.ts +82 -0
- package/lib/locator.js +190 -0
- package/lib/locator.js.map +1 -0
- package/lib/observe-script.d.ts +27 -0
- package/lib/observe-script.js +565 -0
- package/lib/observe-script.js.map +1 -0
- package/lib/observe.d.ts +179 -0
- package/lib/observe.js +368 -0
- package/lib/observe.js.map +1 -0
- package/lib/policy-keys.d.ts +30 -0
- package/lib/policy-keys.js +34 -0
- package/lib/policy-keys.js.map +1 -0
- package/lib/prompt-limits.d.ts +30 -0
- package/lib/prompt-limits.js +31 -0
- package/lib/prompt-limits.js.map +1 -0
- package/lib/prompts.d.ts +131 -0
- package/lib/prompts.js +295 -0
- package/lib/prompts.js.map +1 -0
- package/lib/skill.d.ts +100 -0
- package/lib/skill.js +181 -0
- package/lib/skill.js.map +1 -0
- package/lib/tool-defs.d.ts +72 -0
- package/lib/tool-defs.js +67 -0
- package/lib/tool-defs.js.map +1 -0
- package/lib/tools.d.ts +19 -4
- package/lib/tools.js +124 -922
- package/lib/tools.js.map +1 -1
- package/package.json +5 -1
- package/scripts/dump-prompts.mjs +29 -0
- package/scripts/measure-tool-surface.mjs +190 -0
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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) => !
|
|
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 (
|
|
128
|
-
return
|
|
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
|
|
133
|
-
return Object.entries(
|
|
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
|
|
164
|
-
const JSON_FIELD_SPECS = [
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
|
328
|
-
|
|
329
|
-
|
|
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 = (
|
|
338
|
-
const
|
|
339
|
-
const
|
|
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-${
|
|
342
|
-
label: t(
|
|
343
|
-
hint: t(`${
|
|
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(
|
|
347
|
-
numeric: numeric.has(
|
|
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(
|
|
351
|
-
onReset: () => props.resetField(
|
|
352
|
-
},
|
|
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.
|
|
1547
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
366
1548
|
className: styles.grid,
|
|
367
|
-
children: section.fields.map((name) => field(
|
|
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:
|
|
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
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
const
|
|
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 (
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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: () =>
|
|
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: () =>
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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:
|
|
2119
|
+
children: dirty ? t("assetUnsaved") : t("assetSourceBoundary")
|
|
577
2120
|
}),
|
|
578
|
-
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
608
|
-
onClick: () =>
|
|
609
|
-
children: t("
|
|
2176
|
+
disabled: state.busy,
|
|
2177
|
+
onClick: () => props.requestConvertAutomationAsset(selected.id),
|
|
2178
|
+
children: t("assetConvert")
|
|
610
2179
|
}) : null,
|
|
611
|
-
selected
|
|
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
|
|
615
|
-
onClick: () => props.
|
|
616
|
-
children: t("
|
|
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 || !
|
|
629
|
-
onClick: () => void
|
|
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: "
|
|
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: "
|
|
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: "未显式选择登录态时使用;
|
|
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
|
|
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: "
|
|
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,
|
|
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 (the plugin restarts after a save; 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 (the plugin restarts after a save; 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;
|
|
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;
|
|
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",
|
|
@@ -828,153 +2555,6 @@ window.__ModuleLoader__.load({
|
|
|
828
2555
|
/** Settings namespace used by both the Host registry and the card slot key. */
|
|
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
|
}
|