@wenathlan/extension 1.1.32 → 1.1.34
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 +8 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +388 -12
- package/dist/index.js.map +3 -3
- package/dist/memory.d.ts +81 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +13 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +67 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +288 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +892 -25
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +1826 -88
- package/extension/dist/pagebridge.js.map +4 -4
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +13 -3
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +322 -25
- package/extension/dist/sidepanel.js.map +2 -2
- package/extension/dist/style.css +2 -1
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -66,18 +66,191 @@ var sessionmemory = class {
|
|
|
66
66
|
const retention = (await this.getsettings())?.outcomeretention;
|
|
67
67
|
await this.adapter.set("outcomes", retention === void 0 ? combined : combined.slice(0, retention));
|
|
68
68
|
}
|
|
69
|
+
/** Stores one clickable map under its observation version so every captured map stays available. */
|
|
70
|
+
async setmap(map) {
|
|
71
|
+
return this.adapter.set(`map${map.version}`, map);
|
|
72
|
+
}
|
|
73
|
+
/** Returns one stored clickable map by its observation version. */
|
|
74
|
+
async getmap(version) {
|
|
75
|
+
return this.adapter.get(`map${version}`);
|
|
76
|
+
}
|
|
77
|
+
/** Advances and persists the observation version counter used to stamp clickable maps. */
|
|
78
|
+
async nextobservationversion() {
|
|
79
|
+
const current = await this.adapter.get("observationversion") ?? 0;
|
|
80
|
+
const next = current + 1;
|
|
81
|
+
await this.adapter.set("observationversion", next);
|
|
82
|
+
return next;
|
|
83
|
+
}
|
|
84
|
+
/** Returns the latest observation version used to stamp a clickable map. */
|
|
85
|
+
async getobservationversion() {
|
|
86
|
+
return this.adapter.get("observationversion");
|
|
87
|
+
}
|
|
88
|
+
/** Returns the key hold registry, persisted so holds survive service worker restarts. */
|
|
89
|
+
async getholds() {
|
|
90
|
+
return await this.adapter.get("holds") ?? [];
|
|
91
|
+
}
|
|
92
|
+
/** Replaces the key hold registry after one press or release transition. */
|
|
93
|
+
async setholds(holds) {
|
|
94
|
+
return this.adapter.set("holds", holds);
|
|
95
|
+
}
|
|
96
|
+
/** Returns every dialog decision recorded for the audit trail. */
|
|
97
|
+
async getdialogs() {
|
|
98
|
+
return await this.adapter.get("dialogs") ?? [];
|
|
99
|
+
}
|
|
100
|
+
/** Records one dialog decision with the reviewed answer and the observed dialog text. */
|
|
101
|
+
async adddialog(decision) {
|
|
102
|
+
const records = await this.getdialogs();
|
|
103
|
+
await this.adapter.set("dialogs", [decision, ...records]);
|
|
104
|
+
}
|
|
105
|
+
/** Returns every retry outcome recorded with attempts and movement deltas. */
|
|
106
|
+
async getretries() {
|
|
107
|
+
return await this.adapter.get("retries") ?? [];
|
|
108
|
+
}
|
|
109
|
+
/** Records one retry outcome with the attempts made and the movement delta observed. */
|
|
110
|
+
async addretry(outcome) {
|
|
111
|
+
const records = await this.getretries();
|
|
112
|
+
await this.adapter.set("retries", [outcome, ...records]);
|
|
113
|
+
}
|
|
114
|
+
/** Returns every resolution summary stored per target mode. */
|
|
115
|
+
async getresolutions() {
|
|
116
|
+
return await this.adapter.get("resolutions") ?? [];
|
|
117
|
+
}
|
|
118
|
+
/** Records one resolution summary for later selector derivation. */
|
|
119
|
+
async addresolution(summary) {
|
|
120
|
+
const records = await this.getresolutions();
|
|
121
|
+
await this.adapter.set("resolutions", [summary, ...records]);
|
|
122
|
+
}
|
|
123
|
+
/** Returns the reviewed default dialog policy kept for the session auto handler. */
|
|
124
|
+
async getdialogpolicy() {
|
|
125
|
+
return this.adapter.get("dialogpolicy");
|
|
126
|
+
}
|
|
127
|
+
/** Stores the reviewed default dialog policy of the latest approved plan. */
|
|
128
|
+
async setdialogpolicy(policy) {
|
|
129
|
+
return this.adapter.set("dialogpolicy", policy);
|
|
130
|
+
}
|
|
131
|
+
/** Stores one observation capture under its version so every observation version stays available. */
|
|
132
|
+
async setobservation(record2) {
|
|
133
|
+
return this.adapter.set(`observation${record2.version}`, record2);
|
|
134
|
+
}
|
|
135
|
+
/** Returns one stored observation version. */
|
|
136
|
+
async getobservation(version) {
|
|
137
|
+
return this.adapter.get(`observation${version}`);
|
|
138
|
+
}
|
|
139
|
+
/** Returns the observation retention window; an absent setting keeps every capture. */
|
|
140
|
+
async observationretention() {
|
|
141
|
+
return (await this.getsettings())?.observationretention;
|
|
142
|
+
}
|
|
143
|
+
/** Stores one accessibility tree capture; retention is a user setting and an absent setting keeps every tree. */
|
|
144
|
+
async adda11ytree(capture) {
|
|
145
|
+
const records = await this.geta11ytrees();
|
|
146
|
+
const combined = [capture, ...records];
|
|
147
|
+
const retention = await this.observationretention();
|
|
148
|
+
await this.adapter.set("a11ytrees", retention === void 0 ? combined : combined.slice(0, retention));
|
|
149
|
+
}
|
|
150
|
+
/** Returns every stored accessibility tree capture, newest first. */
|
|
151
|
+
async geta11ytrees() {
|
|
152
|
+
return await this.adapter.get("a11ytrees") ?? [];
|
|
153
|
+
}
|
|
154
|
+
/** Stores one reader article capture; retention is a user setting and an absent setting keeps every article. */
|
|
155
|
+
async addreaderarticle(capture) {
|
|
156
|
+
const records = await this.getreaderarticles();
|
|
157
|
+
const combined = [capture, ...records];
|
|
158
|
+
const retention = await this.observationretention();
|
|
159
|
+
await this.adapter.set("readerarticles", retention === void 0 ? combined : combined.slice(0, retention));
|
|
160
|
+
}
|
|
161
|
+
/** Returns every stored reader article capture, newest first. */
|
|
162
|
+
async getreaderarticles() {
|
|
163
|
+
return await this.adapter.get("readerarticles") ?? [];
|
|
164
|
+
}
|
|
165
|
+
/** Records one dom mutation observed inside a reviewed watch. */
|
|
166
|
+
async addmutationevent(event) {
|
|
167
|
+
const records = await this.getmutationevents();
|
|
168
|
+
await this.adapter.set("mutationevents", [event, ...records]);
|
|
169
|
+
}
|
|
170
|
+
/** Returns the mutation event stream of every reviewed watch. */
|
|
171
|
+
async getmutationevents() {
|
|
172
|
+
return await this.adapter.get("mutationevents") ?? [];
|
|
173
|
+
}
|
|
174
|
+
/** Records one focus change observed inside a reviewed watch. */
|
|
175
|
+
async addfocusevent(event) {
|
|
176
|
+
const records = await this.getfocusevents();
|
|
177
|
+
await this.adapter.set("focusevents", [event, ...records]);
|
|
178
|
+
}
|
|
179
|
+
/** Returns the focus event stream of every reviewed watch. */
|
|
180
|
+
async getfocusevents() {
|
|
181
|
+
return await this.adapter.get("focusevents") ?? [];
|
|
182
|
+
}
|
|
183
|
+
/** Records one consent banner observed by a reviewed banner watch. */
|
|
184
|
+
async addbanner(event) {
|
|
185
|
+
const records = await this.getbanners();
|
|
186
|
+
await this.adapter.set("banners", [event, ...records]);
|
|
187
|
+
}
|
|
188
|
+
/** Returns every consent banner report observed so far. */
|
|
189
|
+
async getbanners() {
|
|
190
|
+
return await this.adapter.get("banners") ?? [];
|
|
191
|
+
}
|
|
192
|
+
/** Records one snapshot diff between two observation versions. */
|
|
193
|
+
async adddiff(diff) {
|
|
194
|
+
const records = await this.getdiffs();
|
|
195
|
+
await this.adapter.set("diffs", [diff, ...records]);
|
|
196
|
+
}
|
|
197
|
+
/** Returns every stored snapshot diff, newest first. */
|
|
198
|
+
async getdiffs() {
|
|
199
|
+
return await this.adapter.get("diffs") ?? [];
|
|
200
|
+
}
|
|
201
|
+
/** Records one derived selector with its stability score for reuse. */
|
|
202
|
+
async addselector(selector) {
|
|
203
|
+
const records = await this.getselectors();
|
|
204
|
+
await this.adapter.set("selectors", [selector, ...records]);
|
|
205
|
+
}
|
|
206
|
+
/** Returns every stored derived selector with its stability score, newest first. */
|
|
207
|
+
async getselectors() {
|
|
208
|
+
return await this.adapter.get("selectors") ?? [];
|
|
209
|
+
}
|
|
210
|
+
/** Records one detected template class or section fingerprint for its origin. */
|
|
211
|
+
async addtemplate(profile) {
|
|
212
|
+
const records = await this.gettemplates();
|
|
213
|
+
await this.adapter.set("templates", [profile, ...records]);
|
|
214
|
+
}
|
|
215
|
+
/** Returns every stored template class and section fingerprint, newest first. */
|
|
216
|
+
async gettemplates() {
|
|
217
|
+
return await this.adapter.get("templates") ?? [];
|
|
218
|
+
}
|
|
219
|
+
/** Records one watch registration so it survives service worker restarts. */
|
|
220
|
+
async addwatch(watch) {
|
|
221
|
+
const records = await this.getwatches();
|
|
222
|
+
await this.adapter.set("watches", [watch, ...records]);
|
|
223
|
+
}
|
|
224
|
+
/** Returns every watch registration, newest first, including closed windows. */
|
|
225
|
+
async getwatches() {
|
|
226
|
+
return await this.adapter.get("watches") ?? [];
|
|
227
|
+
}
|
|
228
|
+
/** Closes one watch registration by watch id once its reviewed lifetime window ends. */
|
|
229
|
+
async closewatch(watchid, closedat) {
|
|
230
|
+
const records = await this.getwatches();
|
|
231
|
+
await this.adapter.set("watches", records.map((watch) => watch.watchid === watchid && watch.closedat === void 0 ? { ...watch, closedat } : watch));
|
|
232
|
+
}
|
|
233
|
+
/** Returns the live page signals of language, template, scroll lock and banner state. */
|
|
234
|
+
async getsignals() {
|
|
235
|
+
return this.adapter.get("signals");
|
|
236
|
+
}
|
|
237
|
+
/** Replaces the live page signals after an observation step refreshes them. */
|
|
238
|
+
async setsignals(signals) {
|
|
239
|
+
return this.adapter.set("signals", signals);
|
|
240
|
+
}
|
|
69
241
|
};
|
|
70
242
|
function randomid() {
|
|
71
243
|
return crypto.randomUUID();
|
|
72
244
|
}
|
|
73
245
|
|
|
74
246
|
// policy.ts
|
|
75
|
-
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile"]);
|
|
76
|
-
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset"]);
|
|
77
|
-
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot"]);
|
|
247
|
+
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor"]);
|
|
248
|
+
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
|
|
249
|
+
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector"]);
|
|
78
250
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
79
|
-
var
|
|
80
|
-
var
|
|
251
|
+
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus"]);
|
|
252
|
+
var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection"]);
|
|
253
|
+
var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor"]);
|
|
81
254
|
function normalizeendpoint(value) {
|
|
82
255
|
const endpoint = new URL(value.trim());
|
|
83
256
|
if (endpoint.protocol !== "https:") throw new Error("Devthink accepts HTTPS endpoints only.");
|
|
@@ -121,19 +294,84 @@ function isnumericid(value) {
|
|
|
121
294
|
function numericoption(options, key) {
|
|
122
295
|
return options[key] === void 0 || typeof options[key] === "number" && Number.isFinite(options[key]);
|
|
123
296
|
}
|
|
297
|
+
function nonnegativeoption(options, key) {
|
|
298
|
+
return numericoption(options, key) && !(typeof options[key] === "number" && options[key] < 0);
|
|
299
|
+
}
|
|
300
|
+
function isnonempty(value) {
|
|
301
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
302
|
+
}
|
|
303
|
+
function ispoint(value) {
|
|
304
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
305
|
+
const point = value;
|
|
306
|
+
return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
|
|
307
|
+
}
|
|
308
|
+
function validatetargetref(reference) {
|
|
309
|
+
if (!reference || typeof reference !== "object" || Array.isArray(reference)) return { allowed: false, reason: "The reviewed target reference must be an object." };
|
|
310
|
+
const ref = reference;
|
|
311
|
+
if (ref.mode === "selector") return isnonempty(ref.selector) ? { allowed: true } : { allowed: false, reason: "The selector target reference needs a non-empty selector." };
|
|
312
|
+
if (ref.mode === "text") return isnonempty(ref.text) ? { allowed: true } : { allowed: false, reason: "The text target reference needs non-empty text." };
|
|
313
|
+
if (ref.mode === "aria") {
|
|
314
|
+
if (!isnonempty(ref.role)) return { allowed: false, reason: "The aria target reference needs a non-empty role." };
|
|
315
|
+
return isnonempty(ref.name) ? { allowed: true } : { allowed: false, reason: "The aria target reference needs a non-empty name." };
|
|
316
|
+
}
|
|
317
|
+
if (ref.mode === "name") return isnonempty(ref.name) ? { allowed: true } : { allowed: false, reason: "The name target reference needs a non-empty name." };
|
|
318
|
+
if (ref.mode === "xpath") return isnonempty(ref.xpath) ? { allowed: true } : { allowed: false, reason: "The xpath target reference needs a non-empty expression." };
|
|
319
|
+
if (ref.mode === "index") {
|
|
320
|
+
const index = ref.index;
|
|
321
|
+
return typeof index === "number" && Number.isInteger(index) && index >= 1 ? { allowed: true } : { allowed: false, reason: "The index target reference needs a positive integer map number." };
|
|
322
|
+
}
|
|
323
|
+
if (ref.mode === "point") {
|
|
324
|
+
const pointok = typeof ref.x === "number" && Number.isFinite(ref.x) && typeof ref.y === "number" && Number.isFinite(ref.y);
|
|
325
|
+
return pointok ? { allowed: true } : { allowed: false, reason: "The point target reference needs numeric x and y coordinates." };
|
|
326
|
+
}
|
|
327
|
+
return { allowed: false, reason: "The target reference mode must be selector, text, aria, name, xpath, index or point." };
|
|
328
|
+
}
|
|
329
|
+
function origingranted(session, origin) {
|
|
330
|
+
if (!session) return false;
|
|
331
|
+
const grants = session.grants ?? [session.origin];
|
|
332
|
+
return grants.includes(origin);
|
|
333
|
+
}
|
|
334
|
+
function validateinnerstep(options, origin) {
|
|
335
|
+
const stepid = options.stepid;
|
|
336
|
+
const kind = options.kind;
|
|
337
|
+
if (isnonempty(stepid)) {
|
|
338
|
+
if (kind !== void 0) return { allowed: false, reason: "The reviewed wrapper must reference a step id or an inline step, not both." };
|
|
339
|
+
return { allowed: true };
|
|
340
|
+
}
|
|
341
|
+
if (typeof kind !== "string" || !kind.trim()) return { allowed: false, reason: "A reviewed step id or inline step kind is required in options." };
|
|
342
|
+
if (kind === "retryaction" || kind === "enterframe") return { allowed: false, reason: "The reviewed inner step cannot be another wrapper kind." };
|
|
343
|
+
if (!allowedactions.has(kind)) return { allowed: false, reason: "The reviewed inner step kind is unsupported." };
|
|
344
|
+
const inneroptions = options.options;
|
|
345
|
+
if (inneroptions !== void 0 && (!inneroptions || typeof inneroptions !== "object" || Array.isArray(inneroptions))) return { allowed: false, reason: "The reviewed inner step options must be an object." };
|
|
346
|
+
const inner = {
|
|
347
|
+
id: "inner",
|
|
348
|
+
kind,
|
|
349
|
+
summary: "Reviewed inner step.",
|
|
350
|
+
risk: actionrisk(kind),
|
|
351
|
+
...isnonempty(options.target) ? { target: options.target } : {},
|
|
352
|
+
...isnonempty(options.value) ? { value: options.value } : {},
|
|
353
|
+
...inneroptions !== void 0 ? { options: JSON.stringify(inneroptions) } : {}
|
|
354
|
+
};
|
|
355
|
+
return validatestep(inner, origin);
|
|
356
|
+
}
|
|
124
357
|
function validatestep(step, origin) {
|
|
125
358
|
if (!allowedactions.has(step.kind)) return { allowed: false, reason: "Unsupported action kind." };
|
|
126
359
|
if (!step.summary.trim()) return { allowed: false, reason: "A human-readable action summary is required." };
|
|
127
|
-
if (targetactions.has(step.kind) && !step.target?.trim()) return { allowed: false, reason: "A page target is required." };
|
|
128
|
-
if (valueactions.has(step.kind) && !step.value?.trim()) return { allowed: false, reason: "A reviewed value is required." };
|
|
129
|
-
if (step.kind === "select" && !step.value?.trim()) return { allowed: false, reason: "A reviewed option value is required." };
|
|
130
|
-
if (step.kind === "navigate" && !step.value) return { allowed: false, reason: "A navigation URL is required." };
|
|
131
360
|
let options;
|
|
132
361
|
try {
|
|
133
362
|
options = parseoptions(step);
|
|
134
363
|
} catch {
|
|
135
364
|
return { allowed: false, reason: "Step options must be a JSON object." };
|
|
136
365
|
}
|
|
366
|
+
const hastargetref = options.targetref !== void 0;
|
|
367
|
+
if (targetactions.has(step.kind) && !step.target?.trim() && !hastargetref) return { allowed: false, reason: "A page target is required." };
|
|
368
|
+
if (valueactions.has(step.kind) && !step.value?.trim()) return { allowed: false, reason: "A reviewed value is required." };
|
|
369
|
+
if (step.kind === "select" && !step.value?.trim()) return { allowed: false, reason: "A reviewed option value is required." };
|
|
370
|
+
if (step.kind === "navigate" && !step.value) return { allowed: false, reason: "A navigation URL is required." };
|
|
371
|
+
if (hastargetref) {
|
|
372
|
+
const reference = validatetargetref(options.targetref);
|
|
373
|
+
if (!reference.allowed) return reference;
|
|
374
|
+
}
|
|
137
375
|
if (step.kind === "wait") {
|
|
138
376
|
try {
|
|
139
377
|
waitduration(step);
|
|
@@ -173,6 +411,86 @@ function validatestep(step, origin) {
|
|
|
173
411
|
}
|
|
174
412
|
if ((step.kind === "scrollpage" || step.kind === "scrollby") && (!numericoption(options, "x") || !numericoption(options, "y"))) return { allowed: false, reason: "Scroll amounts must be numbers in options." };
|
|
175
413
|
if (step.kind === "waitfor" && options.timeout !== void 0 && (typeof options.timeout !== "number" || options.timeout < 0)) return { allowed: false, reason: "The waitfor timeout must be zero or a positive number of milliseconds." };
|
|
414
|
+
if (step.kind === "movepointer") {
|
|
415
|
+
const path = options.pointpath;
|
|
416
|
+
if (!path || typeof path !== "object" || Array.isArray(path)) return { allowed: false, reason: "A reviewed pointpath with start and end points is required in options." };
|
|
417
|
+
const points = path;
|
|
418
|
+
if (!ispoint(points.start) || !ispoint(points.end)) return { allowed: false, reason: "The reviewed pointpath needs numeric start and end points." };
|
|
419
|
+
if (points.waypoints !== void 0 && (!Array.isArray(points.waypoints) || !points.waypoints.every((waypoint) => ispoint(waypoint)))) return { allowed: false, reason: "The reviewed pointpath waypoints must be numeric points." };
|
|
420
|
+
if (!nonnegativeoption(points, "duration")) return { allowed: false, reason: "The reviewed pointpath duration must be zero or a positive number of milliseconds." };
|
|
421
|
+
const speed = options.speedprofile;
|
|
422
|
+
if (speed !== void 0) {
|
|
423
|
+
if (!speed || typeof speed !== "object" || Array.isArray(speed)) return { allowed: false, reason: "The reviewed speed profile must be an object." };
|
|
424
|
+
const profile = speed;
|
|
425
|
+
if (profile.easing !== void 0 && profile.easing !== "linear" && profile.easing !== "easeinout") return { allowed: false, reason: "The reviewed easing must be linear or easeinout." };
|
|
426
|
+
if (!nonnegativeoption(profile, "peak")) return { allowed: false, reason: "The reviewed peak velocity must be zero or a positive number." };
|
|
427
|
+
if (!nonnegativeoption(profile, "jitter")) return { allowed: false, reason: "The reviewed jitter window must be zero or a positive number of milliseconds." };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (step.kind === "clickpoint" && (!hastargetref || options.targetref.mode !== "point")) return { allowed: false, reason: "A reviewed point target reference is required in options." };
|
|
431
|
+
if (step.kind === "clicktext" && (!hastargetref || options.targetref.mode !== "text")) return { allowed: false, reason: "A reviewed text target reference is required in options." };
|
|
432
|
+
if (step.kind === "clickaria" && (!hastargetref || options.targetref.mode !== "aria")) return { allowed: false, reason: "A reviewed aria target reference is required in options." };
|
|
433
|
+
if (step.kind === "clickname" && (!hastargetref || options.targetref.mode !== "name")) return { allowed: false, reason: "A reviewed name target reference is required in options." };
|
|
434
|
+
if (step.kind === "resolvexpath" && (!hastargetref || options.targetref.mode !== "xpath")) return { allowed: false, reason: "A reviewed xpath target reference is required in options." };
|
|
435
|
+
if (step.kind === "typetime" && options.delay !== void 0 && (typeof options.delay !== "number" || !Number.isFinite(options.delay) || options.delay < 0)) return { allowed: false, reason: "The reviewed per keystroke delay must be zero or a positive number of milliseconds." };
|
|
436
|
+
if (step.kind === "submitsearch") {
|
|
437
|
+
if (!isnonempty(options.results)) return { allowed: false, reason: "A reviewed results region selector is required in options." };
|
|
438
|
+
if (options.timeout !== void 0 && (typeof options.timeout !== "number" || !Number.isFinite(options.timeout) || options.timeout < 0)) return { allowed: false, reason: "The submitsearch timeout must be zero or a positive number of milliseconds." };
|
|
439
|
+
}
|
|
440
|
+
if (step.kind === "selectmulti") {
|
|
441
|
+
const values = options.values;
|
|
442
|
+
if (!Array.isArray(values) || values.length === 0 || !values.every((value) => isnonempty(value))) return { allowed: false, reason: "A reviewed list of option values is required in options." };
|
|
443
|
+
}
|
|
444
|
+
if (step.kind === "setslider") {
|
|
445
|
+
const slider = Number(step.value);
|
|
446
|
+
if (!Number.isFinite(slider)) return { allowed: false, reason: "The reviewed slider value must be a number." };
|
|
447
|
+
}
|
|
448
|
+
if (step.kind === "setdate" && !/^\d{4}-\d{2}-\d{2}$/.test(step.value ?? "")) return { allowed: false, reason: "The reviewed date must use the yyyy-mm-dd form." };
|
|
449
|
+
if (step.kind === "setcolor" && !/^#[0-9a-fA-F]{6}$/.test(step.value ?? "")) return { allowed: false, reason: "The reviewed color must use the #rrggbb form." };
|
|
450
|
+
if (step.kind === "keyhold" && options.holdid !== void 0 && !isnonempty(options.holdid)) return { allowed: false, reason: "The reviewed hold id must be a non-empty string." };
|
|
451
|
+
if (step.kind === "dismissdialog") {
|
|
452
|
+
const accept = options.accept;
|
|
453
|
+
const answer = options.answer;
|
|
454
|
+
if (accept === void 0 && !isnonempty(answer)) return { allowed: false, reason: "A reviewed accept flag or prompt answer is required in options." };
|
|
455
|
+
if (accept !== void 0 && typeof accept !== "boolean") return { allowed: false, reason: "The reviewed dialog accept flag must be a boolean." };
|
|
456
|
+
if (answer !== void 0 && !isnonempty(answer)) return { allowed: false, reason: "The reviewed prompt answer must be a non-empty string." };
|
|
457
|
+
}
|
|
458
|
+
if (step.kind === "pierceshadow" && options.shadow !== void 0) {
|
|
459
|
+
if (!Array.isArray(options.shadow) || !options.shadow.every((item) => isnonempty(item))) return { allowed: false, reason: "The reviewed shadow path must be a list of non-empty selectors." };
|
|
460
|
+
}
|
|
461
|
+
if (step.kind === "enterframe") {
|
|
462
|
+
const path = options.framepath;
|
|
463
|
+
if (!Array.isArray(path) || path.length === 0 || !path.every((item) => typeof item === "number" && Number.isInteger(item) && item >= 0)) return { allowed: false, reason: "A reviewed frame path of frame indexes is required in options." };
|
|
464
|
+
return validateinnerstep(options, origin);
|
|
465
|
+
}
|
|
466
|
+
if (step.kind === "retryaction") {
|
|
467
|
+
const inner = validateinnerstep(options, origin);
|
|
468
|
+
if (!inner.allowed) return inner;
|
|
469
|
+
const rule = options.retryrule;
|
|
470
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) return { allowed: false, reason: "A reviewed retry rule with attempts is required in options." };
|
|
471
|
+
const retry = rule;
|
|
472
|
+
if (typeof retry.attempts !== "number" || !Number.isInteger(retry.attempts) || retry.attempts < 1) return { allowed: false, reason: "The reviewed retry attempts must be a positive integer with no code ceiling." };
|
|
473
|
+
if (!nonnegativeoption(retry, "settle")) return { allowed: false, reason: "The reviewed retry settle window must be zero or a positive number of milliseconds." };
|
|
474
|
+
if (!nonnegativeoption(retry, "tolerance")) return { allowed: false, reason: "The reviewed retry movement tolerance must be zero or a positive number of pixels." };
|
|
475
|
+
}
|
|
476
|
+
if (watchactions.has(step.kind)) {
|
|
477
|
+
if (typeof options.lifetime !== "number" || !Number.isFinite(options.lifetime) || options.lifetime <= 0) return { allowed: false, reason: "A reviewed watch lifetime window in milliseconds is required in options." };
|
|
478
|
+
if (options.scopes !== void 0 && (!Array.isArray(options.scopes) || !options.scopes.every((scope) => isnonempty(scope)))) return { allowed: false, reason: "The reviewed watch scopes must be a list of non-empty selectors." };
|
|
479
|
+
if (options.events !== void 0 && (!Array.isArray(options.events) || !options.events.every((event) => isnonempty(event)))) return { allowed: false, reason: "The reviewed watch event kinds must be a list of non-empty strings." };
|
|
480
|
+
if (!nonnegativeoption(options, "poll")) return { allowed: false, reason: "The reviewed watch poll interval must be zero or a positive number of milliseconds." };
|
|
481
|
+
}
|
|
482
|
+
if (step.kind === "waitquiet") {
|
|
483
|
+
const rule = options.quietrule;
|
|
484
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) return { allowed: false, reason: "A reviewed quietrule with an idle threshold is required in options." };
|
|
485
|
+
const quiet = rule;
|
|
486
|
+
if (typeof quiet.idle !== "number" || !Number.isFinite(quiet.idle) || quiet.idle <= 0) return { allowed: false, reason: "The reviewed quiet idle threshold must be a positive number of milliseconds with no code ceiling." };
|
|
487
|
+
if (!nonnegativeoption(quiet, "poll")) return { allowed: false, reason: "The reviewed quiet poll interval must be zero or a positive number of milliseconds." };
|
|
488
|
+
if (!nonnegativeoption(quiet, "timeout")) return { allowed: false, reason: "The reviewed quiet timeout must be zero or a positive number of milliseconds." };
|
|
489
|
+
}
|
|
490
|
+
if (step.kind === "diffsnapshots") {
|
|
491
|
+
const versions = options.versions;
|
|
492
|
+
if (!Array.isArray(versions) || versions.length !== 2 || !versions.every((version) => typeof version === "number" && Number.isInteger(version) && version >= 1)) return { allowed: false, reason: "Two reviewed observation version numbers are required in options." };
|
|
493
|
+
}
|
|
176
494
|
return { allowed: true };
|
|
177
495
|
}
|
|
178
496
|
function sessiongate(input) {
|
|
@@ -188,6 +506,8 @@ function canexecute(input) {
|
|
|
188
506
|
if (!gate.allowed) return gate;
|
|
189
507
|
if (!input.plan || input.plan.state !== "approved") return { allowed: false, reason: "The plan has not received explicit approval." };
|
|
190
508
|
if (input.plan.expiresat <= now) return { allowed: false, reason: "The approved plan has expired." };
|
|
509
|
+
if ((input.step.kind === "pierceshadow" || input.step.kind === "enterframe") && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The shadow or frame step is outside the session origin grants." };
|
|
510
|
+
if (input.step.kind === "readjson" && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The json state read is outside the session origin grants." };
|
|
191
511
|
return validatestep(input.step, input.origin);
|
|
192
512
|
}
|
|
193
513
|
function canpreview(input) {
|
|
@@ -196,7 +516,13 @@ function canpreview(input) {
|
|
|
196
516
|
if (!gate.allowed) return gate;
|
|
197
517
|
if (!input.plan || !["pending", "approved"].includes(input.plan.state)) return { allowed: false, reason: "Only a reviewed pending or approved plan can be previewed." };
|
|
198
518
|
if (input.plan.expiresat <= now) return { allowed: false, reason: "The reviewed plan has expired." };
|
|
199
|
-
|
|
519
|
+
let options = {};
|
|
520
|
+
try {
|
|
521
|
+
options = parseoptions(input.step);
|
|
522
|
+
} catch {
|
|
523
|
+
options = {};
|
|
524
|
+
}
|
|
525
|
+
if (!targetactions.has(input.step.kind) && options.targetref === void 0) return { allowed: false, reason: "Only a target-based action can be previewed." };
|
|
200
526
|
return validatestep(input.step, input.origin);
|
|
201
527
|
}
|
|
202
528
|
|
|
@@ -224,9 +550,17 @@ function resetforplan(progress, plan, now) {
|
|
|
224
550
|
const snapshot2 = { planid: progress.planid, completedsteps: progress.completedsteps, ...progress.outcomes ? { outcomes: progress.outcomes } : {}, updatedat: progress.updatedat };
|
|
225
551
|
return { planid: plan.id, completedsteps: [], outcomes: [], prior: [...progress.prior ?? [], snapshot2], updatedat: now };
|
|
226
552
|
}
|
|
553
|
+
function watchclosed(startedat, lifetime, now) {
|
|
554
|
+
return now >= startedat + lifetime;
|
|
555
|
+
}
|
|
556
|
+
function recordwatchcompletion(progress, planid, stepid, startedat, lifetime, now) {
|
|
557
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
558
|
+
if (!watchclosed(startedat, lifetime, now)) return base;
|
|
559
|
+
return recordstep(base, planid, stepid, now);
|
|
560
|
+
}
|
|
227
561
|
|
|
228
562
|
// version.ts
|
|
229
|
-
var packageversion = "1.1.
|
|
563
|
+
var packageversion = "1.1.34";
|
|
230
564
|
|
|
231
565
|
// types.ts
|
|
232
566
|
var protocolversion = packageversion;
|
|
@@ -262,6 +596,11 @@ function parseproposal(value, origin) {
|
|
|
262
596
|
if (!evaluation.allowed) throw new Error(evaluation.reason);
|
|
263
597
|
return step;
|
|
264
598
|
});
|
|
599
|
+
for (const step of steps) {
|
|
600
|
+
if (step.kind !== "retryaction" && step.kind !== "enterframe") continue;
|
|
601
|
+
const options = parseoptions(step);
|
|
602
|
+
if (typeof options.stepid === "string" && !steps.some((candidate) => candidate.id === options.stepid)) throw new Error("A retry or frame wrapper references an unknown step id.");
|
|
603
|
+
}
|
|
265
604
|
const createdat = Date.now();
|
|
266
605
|
const expiresat = typeof planinput.expiresat === "number" ? planinput.expiresat : createdat + 10 * 60 * 1e3;
|
|
267
606
|
const plan = {
|
|
@@ -279,6 +618,28 @@ function parseproposal(value, origin) {
|
|
|
279
618
|
function requestbody(input) {
|
|
280
619
|
return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
|
|
281
620
|
}
|
|
621
|
+
function outcomeresponse(input) {
|
|
622
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {} });
|
|
623
|
+
}
|
|
624
|
+
function mapresponse(input) {
|
|
625
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
|
|
626
|
+
}
|
|
627
|
+
function heldkeysreport(input) {
|
|
628
|
+
return { version: protocolversion, tabid: input.tabid, heldkeys: input.holds };
|
|
629
|
+
}
|
|
630
|
+
function observationresponse(input) {
|
|
631
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, observation: input.observation });
|
|
632
|
+
}
|
|
633
|
+
function signalsreport(input) {
|
|
634
|
+
const signals = input.signals;
|
|
635
|
+
return {
|
|
636
|
+
version: protocolversion,
|
|
637
|
+
...signals && signals.language !== void 0 ? { language: signals.language } : {},
|
|
638
|
+
...signals && signals.template !== void 0 ? { template: signals.template } : {},
|
|
639
|
+
...signals && signals.scrolllocked !== void 0 ? { scrolllocked: signals.scrolllocked } : {},
|
|
640
|
+
...signals && signals.banner !== void 0 ? { banner: signals.banner } : {}
|
|
641
|
+
};
|
|
642
|
+
}
|
|
282
643
|
|
|
283
644
|
// extension/browsertabs.ts
|
|
284
645
|
var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
|
|
@@ -363,9 +724,145 @@ async function runbrowseraction(step, sessiontabid, windowid) {
|
|
|
363
724
|
}
|
|
364
725
|
}
|
|
365
726
|
|
|
727
|
+
// extension/pagedialogs.ts
|
|
728
|
+
function parsedialogpolicy(step) {
|
|
729
|
+
let options = {};
|
|
730
|
+
try {
|
|
731
|
+
options = parseoptions(step);
|
|
732
|
+
} catch {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
const accept = options.accept;
|
|
736
|
+
const answer = options.answer;
|
|
737
|
+
if (typeof accept !== "boolean" && typeof answer !== "string") return null;
|
|
738
|
+
return { accept: accept === true, ...typeof answer === "string" && answer.trim() ? { answer } : {} };
|
|
739
|
+
}
|
|
740
|
+
function dialoganswer(policy, dialog) {
|
|
741
|
+
if (dialog === "prompt") {
|
|
742
|
+
if (policy.answer === void 0 || policy.answer === "") return { accept: false };
|
|
743
|
+
return { accept: policy.accept !== false, ...policy.answer !== void 0 ? { answer: policy.answer } : {} };
|
|
744
|
+
}
|
|
745
|
+
return { accept: policy.accept };
|
|
746
|
+
}
|
|
747
|
+
function installdialoghandler(accept, answer, persistent) {
|
|
748
|
+
const world = globalThis;
|
|
749
|
+
const originals = world.devthinkoriginaldialogs ?? { confirm: window.confirm.bind(window), alert: window.alert.bind(window), prompt: window.prompt.bind(window) };
|
|
750
|
+
world.devthinkoriginaldialogs = originals;
|
|
751
|
+
const decide = (dialog) => {
|
|
752
|
+
if (dialog === "prompt") return answer ? { accept: true, answer } : { accept: false, answer: null };
|
|
753
|
+
return { accept, answer: null };
|
|
754
|
+
};
|
|
755
|
+
const record2 = (dialog, text2, result) => {
|
|
756
|
+
try {
|
|
757
|
+
const root = document.documentElement;
|
|
758
|
+
const log = JSON.parse(root.dataset.devthinkdialoglog ?? "[]");
|
|
759
|
+
log.push({ dialog, text: text2, result, at: Date.now() });
|
|
760
|
+
root.dataset.devthinkdialoglog = JSON.stringify(log);
|
|
761
|
+
} catch {
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
window.confirm = (text2) => {
|
|
765
|
+
const decision = decide("confirm");
|
|
766
|
+
record2("confirm", text2 ?? "", decision.accept);
|
|
767
|
+
if (!persistent) window.confirm = originals.confirm;
|
|
768
|
+
return decision.accept;
|
|
769
|
+
};
|
|
770
|
+
window.alert = (text2) => {
|
|
771
|
+
record2("alert", text2 ?? "", true);
|
|
772
|
+
if (!persistent) window.alert = originals.alert;
|
|
773
|
+
};
|
|
774
|
+
window.prompt = (text2, defaultvalue) => {
|
|
775
|
+
const decision = decide("prompt");
|
|
776
|
+
const outcome = decision.accept ? decision.answer ?? defaultvalue ?? "" : null;
|
|
777
|
+
record2("prompt", text2 ?? "", outcome);
|
|
778
|
+
if (!persistent) window.prompt = originals.prompt;
|
|
779
|
+
return outcome;
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// extension/pageinteract.ts
|
|
784
|
+
function optionsof(step) {
|
|
785
|
+
try {
|
|
786
|
+
return parseoptions(step);
|
|
787
|
+
} catch {
|
|
788
|
+
return {};
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function innerstep(step) {
|
|
792
|
+
const options = optionsof(step);
|
|
793
|
+
const kind = options.kind;
|
|
794
|
+
if (typeof kind !== "string" || !kind.trim()) return null;
|
|
795
|
+
const inneroptions = options.options;
|
|
796
|
+
return {
|
|
797
|
+
id: `${step.id}inner`,
|
|
798
|
+
kind,
|
|
799
|
+
summary: step.summary,
|
|
800
|
+
risk: step.risk,
|
|
801
|
+
...typeof options.target === "string" ? { target: options.target } : {},
|
|
802
|
+
...typeof options.value === "string" ? { value: options.value } : {},
|
|
803
|
+
...inneroptions && typeof inneroptions === "object" && !Array.isArray(inneroptions) ? { options: JSON.stringify(inneroptions) } : {}
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function pointdistance(a, b) {
|
|
807
|
+
return Math.hypot(b.x - a.x, b.y - a.y);
|
|
808
|
+
}
|
|
809
|
+
function settle(delay) {
|
|
810
|
+
return new Promise((resolve) => setTimeout(resolve, delay));
|
|
811
|
+
}
|
|
812
|
+
async function runretries(rule, probe, execute) {
|
|
813
|
+
const attempts = Math.max(1, Number.isFinite(rule.attempts) ? Math.floor(rule.attempts) : 1);
|
|
814
|
+
const tolerance = typeof rule.tolerance === "number" && Number.isFinite(rule.tolerance) ? rule.tolerance : 0;
|
|
815
|
+
const settles = typeof rule.settle === "number" && Number.isFinite(rule.settle) ? rule.settle : 0;
|
|
816
|
+
let previous = await probe();
|
|
817
|
+
let movement = 0;
|
|
818
|
+
let made = 0;
|
|
819
|
+
let last = "";
|
|
820
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
821
|
+
made = attempt;
|
|
822
|
+
const result = await execute(attempt);
|
|
823
|
+
last = result.summary;
|
|
824
|
+
if (result.ok) return { ok: true, attempts: made, movement, summary: `Retry interaction succeeded on attempt ${made} after ${movement.toFixed(1)} pixels of observed movement.` };
|
|
825
|
+
if (attempt >= attempts) break;
|
|
826
|
+
if (settles > 0) await settle(settles);
|
|
827
|
+
const current = await probe();
|
|
828
|
+
if (!current) {
|
|
829
|
+
previous = null;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (previous) {
|
|
833
|
+
const delta = pointdistance(previous, current);
|
|
834
|
+
movement = Math.max(movement, delta);
|
|
835
|
+
if (delta <= tolerance) return { ok: false, attempts: made, movement, summary: `The target stayed within the reviewed tolerance of ${tolerance} pixels; retry stopped after attempt ${made}. ${last}` };
|
|
836
|
+
}
|
|
837
|
+
previous = current;
|
|
838
|
+
}
|
|
839
|
+
return { ok: false, attempts: made, movement, summary: `Retry interaction failed after ${made} attempt${made === 1 ? "" : "s"} with ${movement.toFixed(1)} pixels of observed movement. ${last}` };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// extension/pagecontrols.ts
|
|
843
|
+
function presshold(holds, hold) {
|
|
844
|
+
if (holds.some((existing) => existing.holdid === hold.holdid && existing.releasedat === void 0)) return { holds, ok: false };
|
|
845
|
+
return { holds: [...holds, hold], ok: true };
|
|
846
|
+
}
|
|
847
|
+
function releasehold(holds, holdid, releasedat) {
|
|
848
|
+
let released;
|
|
849
|
+
const next = holds.map((hold) => {
|
|
850
|
+
if (hold.holdid !== holdid || hold.releasedat !== void 0) return hold;
|
|
851
|
+
released = { ...hold, releasedat };
|
|
852
|
+
return released;
|
|
853
|
+
});
|
|
854
|
+
return { holds: next, ...released ? { released } : {} };
|
|
855
|
+
}
|
|
856
|
+
function heldkeys(holds, tabid2) {
|
|
857
|
+
return holds.filter((hold) => hold.releasedat === void 0 && (tabid2 === void 0 || hold.tabid === void 0 || hold.tabid === tabid2));
|
|
858
|
+
}
|
|
859
|
+
|
|
366
860
|
// extension/background.ts
|
|
367
861
|
var sessionduration = 15 * 60 * 1e3;
|
|
368
862
|
var freshcheckkinds = /* @__PURE__ */ new Set(["focus", "inspect", "click", "type", "scroll", "select", "hover"]);
|
|
863
|
+
var pointerkinds = /* @__PURE__ */ new Set(["movepointer", "clickpoint", "shiftclick", "clicktext", "clickaria", "clickname", "pierceshadow"]);
|
|
864
|
+
var watchstepkinds = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus"]);
|
|
865
|
+
var observationstepkinds = /* @__PURE__ */ new Set(["a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "readselection", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector"]);
|
|
369
866
|
var chromestorage = {
|
|
370
867
|
async get(key) {
|
|
371
868
|
return (await chrome.storage.local.get(key))[key];
|
|
@@ -381,6 +878,13 @@ function extensionpage(sender) {
|
|
|
381
878
|
async function audit(kind, summary, extra = {}) {
|
|
382
879
|
await memory.addaudi({ id: randomid(), kind, at: Date.now(), summary, ...extra });
|
|
383
880
|
}
|
|
881
|
+
function stepoptions2(step) {
|
|
882
|
+
try {
|
|
883
|
+
return parseoptions(step);
|
|
884
|
+
} catch {
|
|
885
|
+
return {};
|
|
886
|
+
}
|
|
887
|
+
}
|
|
384
888
|
async function refreshcapabilities() {
|
|
385
889
|
const report = await readcapabilities();
|
|
386
890
|
await memory.setcapabilities(report);
|
|
@@ -402,13 +906,82 @@ async function snapshot(tabid2) {
|
|
|
402
906
|
} });
|
|
403
907
|
const value = result[0]?.result;
|
|
404
908
|
if (!value) throw new Error("The page did not return an observation.");
|
|
405
|
-
|
|
909
|
+
const observationcapture = value;
|
|
910
|
+
const version = await memory.nextobservationversion();
|
|
911
|
+
await memory.setobservation({ version, observation: observationcapture });
|
|
912
|
+
return observationcapture;
|
|
913
|
+
}
|
|
914
|
+
function plandialogpolicy(plan) {
|
|
915
|
+
const step = plan?.steps.find((candidate) => candidate.kind === "dismissdialog");
|
|
916
|
+
return step ? parsedialogpolicy(step) : null;
|
|
917
|
+
}
|
|
918
|
+
async function installdialogpolicy(tabid2, policy, persistent) {
|
|
919
|
+
await chrome.scripting.executeScript({
|
|
920
|
+
target: { tabId: tabid2 },
|
|
921
|
+
world: "MAIN",
|
|
922
|
+
func: installdialoghandler,
|
|
923
|
+
args: [policy.accept, policy.answer ?? "", persistent]
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
async function ensuredialoghandler(plan, tabid2) {
|
|
927
|
+
const policy = plandialogpolicy(plan) ?? await memory.getdialogpolicy();
|
|
928
|
+
if (!policy) return;
|
|
929
|
+
try {
|
|
930
|
+
await installdialogpolicy(tabid2, policy, true);
|
|
931
|
+
} catch {
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
async function harvestdialogs(session, plan, stepid, tabid2) {
|
|
935
|
+
const policy = plandialogpolicy(plan) ?? await memory.getdialogpolicy();
|
|
936
|
+
let observed = [];
|
|
937
|
+
try {
|
|
938
|
+
const result = await chrome.scripting.executeScript({ target: { tabId: tabid2 }, func: () => {
|
|
939
|
+
const bridge = globalThis.devthinkbridge;
|
|
940
|
+
return bridge?.readdialogs ? bridge.readdialogs() : [];
|
|
941
|
+
} });
|
|
942
|
+
const value = result[0]?.result;
|
|
943
|
+
if (Array.isArray(value)) observed = value;
|
|
944
|
+
} catch {
|
|
945
|
+
}
|
|
946
|
+
for (const entry of observed) {
|
|
947
|
+
const reviewed = policy ? dialoganswer(policy, ["confirm", "alert", "prompt"].includes(entry.dialog) ? entry.dialog : "confirm") : void 0;
|
|
948
|
+
const decision = {
|
|
949
|
+
id: randomid(),
|
|
950
|
+
dialog: entry.dialog,
|
|
951
|
+
text: entry.text,
|
|
952
|
+
accept: reviewed ? reviewed.accept : entry.result !== null && entry.result !== false,
|
|
953
|
+
...reviewed?.answer !== void 0 ? { answer: reviewed.answer } : {},
|
|
954
|
+
...session ? { sessionid: session.id } : {},
|
|
955
|
+
at: Number.isFinite(entry.at) ? entry.at : Date.now()
|
|
956
|
+
};
|
|
957
|
+
await memory.adddialog(decision);
|
|
958
|
+
await audit("dialog", `Dialog ${decision.dialog} answered ${decision.accept ? "accept" : "dismiss"}${decision.answer ? ` with the reviewed answer "${decision.answer}"` : ""} for dialog text "${decision.text.slice(0, 120)}".`, { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid });
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async function dispatchpagestep(step, tabid2, origin, plan) {
|
|
962
|
+
const session = await memory.getsession();
|
|
963
|
+
const activeplan = plan ?? await memory.getplan();
|
|
964
|
+
await ensuredialoghandler(activeplan, tabid2);
|
|
965
|
+
const result = await chrome.scripting.executeScript({ target: { tabId: tabid2 }, func: (action, expectedorigin) => {
|
|
966
|
+
const bridge = globalThis.devthinkbridge;
|
|
967
|
+
if (!bridge) throw new Error("Devthink page bridge is unavailable.");
|
|
968
|
+
return bridge.performstep(action, expectedorigin);
|
|
969
|
+
}, args: [step, origin] });
|
|
970
|
+
await harvestdialogs(session, activeplan, step.id, tabid2);
|
|
971
|
+
return result[0]?.result;
|
|
406
972
|
}
|
|
407
973
|
async function startsession() {
|
|
408
974
|
const { tab, origin } = await activecontext();
|
|
409
975
|
const session = { id: randomid(), tabid: tab.id, origin, startedat: Date.now(), expiresat: Date.now() + sessionduration, grants: [origin] };
|
|
410
976
|
await memory.setsession(session);
|
|
411
977
|
await audit("session", `Session started for ${origin}.`, { sessionid: session.id });
|
|
978
|
+
const policy = await memory.getdialogpolicy();
|
|
979
|
+
if (policy) {
|
|
980
|
+
try {
|
|
981
|
+
await installdialogpolicy(session.tabid, policy, true);
|
|
982
|
+
} catch {
|
|
983
|
+
}
|
|
984
|
+
}
|
|
412
985
|
return session;
|
|
413
986
|
}
|
|
414
987
|
async function diagnostic() {
|
|
@@ -451,6 +1024,236 @@ function browserauditkind(step) {
|
|
|
451
1024
|
if (step.kind.startsWith("window")) return "window";
|
|
452
1025
|
return "tab";
|
|
453
1026
|
}
|
|
1027
|
+
function stepauditkind(step, ok) {
|
|
1028
|
+
if (isbrowserkind(step.kind)) return browserauditkind(step);
|
|
1029
|
+
if (step.kind === "dismissdialog") return "dialog";
|
|
1030
|
+
if (step.kind === "keyhold" || step.kind === "keyrelease") return "hold";
|
|
1031
|
+
if (step.kind === "retryaction") return "retry";
|
|
1032
|
+
if (pointerkinds.has(step.kind)) return "pointer";
|
|
1033
|
+
if (watchstepkinds.has(step.kind)) return "watch";
|
|
1034
|
+
if (step.kind === "diffsnapshots") return "diff";
|
|
1035
|
+
if (observationstepkinds.has(step.kind)) return "observation";
|
|
1036
|
+
return ok ? "action" : "error";
|
|
1037
|
+
}
|
|
1038
|
+
function resolvedinnerstep(step, plan) {
|
|
1039
|
+
const options = stepoptions2(step);
|
|
1040
|
+
if (typeof options.stepid === "string" && options.stepid.trim()) {
|
|
1041
|
+
return plan.steps.find((candidate) => candidate.id === options.stepid) ?? null;
|
|
1042
|
+
}
|
|
1043
|
+
return innerstep(step);
|
|
1044
|
+
}
|
|
1045
|
+
async function executekeyhold(step, session, plan, tabid2, origin) {
|
|
1046
|
+
const output = await dispatchpagestep(step, tabid2, origin, plan);
|
|
1047
|
+
if (!output?.ok) return output ?? { ok: false, summary: "The key hold was not delivered." };
|
|
1048
|
+
const options = stepoptions2(step);
|
|
1049
|
+
const holdid = typeof options.holdid === "string" && options.holdid.trim() ? options.holdid : randomid();
|
|
1050
|
+
const modifiers = Array.isArray(options.modifiers) ? options.modifiers.filter((item) => typeof item === "string") : [];
|
|
1051
|
+
const hold = { holdid, key: step.value ?? "", ...modifiers.length > 0 ? { modifiers } : {}, tabid: tabid2, stepid: step.id, pressedat: Date.now() };
|
|
1052
|
+
const next = presshold(await memory.getholds(), hold);
|
|
1053
|
+
if (!next.ok) return { ok: false, summary: `Hold id ${holdid} is already held.` };
|
|
1054
|
+
await memory.setholds(next.holds);
|
|
1055
|
+
await audit("hold", `Key ${hold.key} held under hold id ${holdid} pressed at ${new Date(hold.pressedat).toISOString()}.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
|
|
1056
|
+
return { ok: true, summary: `Key ${hold.key} held under hold id ${holdid}.`, details: { holdid } };
|
|
1057
|
+
}
|
|
1058
|
+
async function executekeyrelease(step, session, plan, tabid2, origin) {
|
|
1059
|
+
const holdid = step.value ?? "";
|
|
1060
|
+
const holds = await memory.getholds();
|
|
1061
|
+
const held = holds.find((candidate) => candidate.holdid === holdid && candidate.releasedat === void 0);
|
|
1062
|
+
if (!held) throw new Error(`No held key matches hold id ${holdid}.`);
|
|
1063
|
+
const modifiers = held.modifiers ?? [];
|
|
1064
|
+
const dispatchstep = { ...step, value: held.key, options: JSON.stringify({ ...modifiers.length > 0 ? { modifiers } : {} }) };
|
|
1065
|
+
const output = await dispatchpagestep(dispatchstep, tabid2, origin, plan);
|
|
1066
|
+
const releasedat = Date.now();
|
|
1067
|
+
const transition = releasehold(holds, holdid, releasedat);
|
|
1068
|
+
await memory.setholds(transition.holds);
|
|
1069
|
+
await audit("hold", `Key ${held.key} released from hold id ${holdid} at ${new Date(releasedat).toISOString()} after ${Math.max(0, releasedat - held.pressedat)} milliseconds.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
|
|
1070
|
+
return { ok: output?.ok ?? false, summary: output?.summary ?? "Key release delivered.", details: { holdid } };
|
|
1071
|
+
}
|
|
1072
|
+
async function executedismissdialog(step, session, plan, tabid2, origin) {
|
|
1073
|
+
const policy = parsedialogpolicy(step);
|
|
1074
|
+
if (!policy) throw new Error("A reviewed dialog policy is required.");
|
|
1075
|
+
await installdialogpolicy(tabid2, policy, false);
|
|
1076
|
+
await harvestdialogs(session, plan, step.id, tabid2);
|
|
1077
|
+
const answer = policy.answer !== void 0 ? ` with the reviewed answer "${policy.answer}"` : "";
|
|
1078
|
+
return { ok: true, summary: `Dialog handler armed${answer} for the next confirm, alert or prompt.` };
|
|
1079
|
+
}
|
|
1080
|
+
async function executeretryaction(step, session, plan, tabid2, origin) {
|
|
1081
|
+
const rule = stepoptions2(step).retryrule;
|
|
1082
|
+
const inner = resolvedinnerstep(step, plan);
|
|
1083
|
+
if (!inner) throw new Error("The reviewed wrapper step could not be resolved.");
|
|
1084
|
+
const innergate = validatestep(inner, origin);
|
|
1085
|
+
if (!innergate.allowed) throw new Error(`The wrapped step is not allowed: ${innergate.reason}`);
|
|
1086
|
+
const probe = async () => {
|
|
1087
|
+
const probestep = { id: `${step.id}probe`, kind: "readgeometry", summary: step.summary, risk: "read", ...inner.target ? { target: inner.target } : {}, ...inner.options ? { options: inner.options } : {} };
|
|
1088
|
+
const output = await dispatchpagestep(probestep, tabid2, origin, plan);
|
|
1089
|
+
const geometry = output?.details?.geometry;
|
|
1090
|
+
if (!geometry || typeof geometry.x !== "number" || typeof geometry.y !== "number" || !Number.isFinite(geometry.x) || !Number.isFinite(geometry.y)) return null;
|
|
1091
|
+
return { x: geometry.x, y: geometry.y };
|
|
1092
|
+
};
|
|
1093
|
+
const execute = async () => {
|
|
1094
|
+
const output = await dispatchpagestep(inner, tabid2, origin, plan);
|
|
1095
|
+
return { ok: Boolean(output?.ok), summary: output?.summary ?? "The wrapped step returned no result." };
|
|
1096
|
+
};
|
|
1097
|
+
const outcome = await runretries(rule ?? { attempts: 1 }, probe, execute);
|
|
1098
|
+
const record2 = { stepid: step.id, attempts: outcome.attempts, movement: outcome.movement, ok: outcome.ok, at: Date.now() };
|
|
1099
|
+
await memory.addretry(record2);
|
|
1100
|
+
await audit("retry", outcome.summary, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
|
|
1101
|
+
return { ok: outcome.ok, summary: outcome.summary, details: { attempts: outcome.attempts, movement: outcome.movement } };
|
|
1102
|
+
}
|
|
1103
|
+
async function executemapclicks(step, plan, tabid2, origin) {
|
|
1104
|
+
const output = await dispatchpagestep(step, tabid2, origin, plan);
|
|
1105
|
+
if (!output?.ok) return output ?? { ok: false, summary: "The clickable map was not captured." };
|
|
1106
|
+
const entries = Array.isArray(output.details?.entries) ? output.details?.entries : [];
|
|
1107
|
+
const version = await memory.nextobservationversion();
|
|
1108
|
+
const map = { version, entries, builtat: Date.now() };
|
|
1109
|
+
await memory.setmap(map);
|
|
1110
|
+
return { ok: true, summary: output.summary, details: { entries, mapversion: version } };
|
|
1111
|
+
}
|
|
1112
|
+
async function executeenterframe(step, plan, tabid2, origin) {
|
|
1113
|
+
const inner = resolvedinnerstep(step, plan);
|
|
1114
|
+
if (!inner) throw new Error("The reviewed frame wrapper step could not be resolved.");
|
|
1115
|
+
const innergate = validatestep(inner, origin);
|
|
1116
|
+
if (!innergate.allowed) throw new Error(`The wrapped step is not allowed: ${innergate.reason}`);
|
|
1117
|
+
const options = stepoptions2(step);
|
|
1118
|
+
const inneroptions = inner.options ? stepoptions2(inner) : void 0;
|
|
1119
|
+
const derived = { ...step, options: JSON.stringify({ ...options, kind: inner.kind, ...inner.target ? { target: inner.target } : {}, ...inner.value ? { value: inner.value } : {}, ...inneroptions ? { options: inneroptions } : {} }) };
|
|
1120
|
+
return dispatchpagestep(derived, tabid2, origin, plan);
|
|
1121
|
+
}
|
|
1122
|
+
function observationnodes(observationcapture) {
|
|
1123
|
+
return observationcapture.interactive.map((entry) => ({ selector: entry.selector, tag: entry.role, text: entry.label, attributes: {} }));
|
|
1124
|
+
}
|
|
1125
|
+
function detailarray(details, key) {
|
|
1126
|
+
const value = details?.[key];
|
|
1127
|
+
return Array.isArray(value) ? value : [];
|
|
1128
|
+
}
|
|
1129
|
+
async function executediffsnapshots(step, session, plan, tabid2, origin) {
|
|
1130
|
+
const options = stepoptions2(step);
|
|
1131
|
+
const versions = Array.isArray(options.versions) ? options.versions.filter((item) => typeof item === "number") : [];
|
|
1132
|
+
const baseversion = versions[0];
|
|
1133
|
+
const targetversion = versions[1];
|
|
1134
|
+
if (baseversion === void 0 || targetversion === void 0) throw new Error("Two reviewed observation versions are required.");
|
|
1135
|
+
const base = await memory.getobservation(baseversion);
|
|
1136
|
+
const target = await memory.getobservation(targetversion);
|
|
1137
|
+
if (!base || !target) throw new Error("A reviewed observation version has not been captured yet.");
|
|
1138
|
+
const derived = { ...step, options: JSON.stringify({ ...options, base: observationnodes(base.observation), target: observationnodes(target.observation) }) };
|
|
1139
|
+
const output = await dispatchpagestep(derived, tabid2, origin, plan) ?? { ok: false, summary: "The snapshot diff returned no result." };
|
|
1140
|
+
const diff = {
|
|
1141
|
+
baseversion,
|
|
1142
|
+
targetversion,
|
|
1143
|
+
added: detailarray(output.details, "added"),
|
|
1144
|
+
removed: detailarray(output.details, "removed"),
|
|
1145
|
+
changed: detailarray(output.details, "changed"),
|
|
1146
|
+
at: Date.now()
|
|
1147
|
+
};
|
|
1148
|
+
await memory.adddiff(diff);
|
|
1149
|
+
await audit("diff", `Diffed observation versions ${baseversion} and ${targetversion}: ${diff.added.length} added, ${diff.removed.length} removed and ${diff.changed.length} changed nodes.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
|
|
1150
|
+
return { ok: Boolean(output.ok), summary: output.summary, details: { ...output.details ?? {}, versions: [baseversion, targetversion] } };
|
|
1151
|
+
}
|
|
1152
|
+
async function executewatchstep(step, session, plan, tabid2, origin) {
|
|
1153
|
+
const options = stepoptions2(step);
|
|
1154
|
+
const watchid = typeof options.watchid === "string" && options.watchid.trim() ? options.watchid : randomid();
|
|
1155
|
+
const scopes = Array.isArray(options.scopes) ? options.scopes.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
1156
|
+
const events = Array.isArray(options.events) ? options.events.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
1157
|
+
const lifetime = typeof options.lifetime === "number" && Number.isFinite(options.lifetime) && options.lifetime > 0 ? options.lifetime : 0;
|
|
1158
|
+
const watch = { watchid, kind: step.kind, stepid: step.id, sessionid: session.id, origin, scopes, events, startedat: Date.now(), lifetime };
|
|
1159
|
+
await memory.addwatch(watch);
|
|
1160
|
+
await audit("watch", `Watch ${step.kind} registered under id ${watchid} for the reviewed lifetime of ${lifetime} milliseconds inside ${scopes.length > 0 ? `scopes ${scopes.join(", ")}` : "the whole document"}.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
1161
|
+
const output = await dispatchpagestep(step, tabid2, origin, plan) ?? { ok: false, summary: "The watch returned no result." };
|
|
1162
|
+
await memory.closewatch(watchid, Date.now());
|
|
1163
|
+
if (step.kind === "watchmutate") {
|
|
1164
|
+
for (const entry of detailarray(output.details, "events")) {
|
|
1165
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1166
|
+
const record2 = entry;
|
|
1167
|
+
await memory.addmutationevent({ ...record2, sessionid: session.id });
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
if (step.kind === "watchfocus") {
|
|
1171
|
+
for (const entry of detailarray(output.details, "events")) {
|
|
1172
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1173
|
+
const record2 = entry;
|
|
1174
|
+
await memory.addfocusevent({ ...record2, sessionid: session.id });
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (step.kind === "watchbanner") {
|
|
1178
|
+
for (const entry of detailarray(output.details, "banners")) {
|
|
1179
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1180
|
+
const record2 = entry;
|
|
1181
|
+
await memory.addbanner({ ...record2, sessionid: session.id });
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
await audit("watch", `Watch ${watchid} closed after its reviewed lifetime of ${lifetime} milliseconds.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
1185
|
+
return { output, watch };
|
|
1186
|
+
}
|
|
1187
|
+
async function refreshsignals(step, output) {
|
|
1188
|
+
const current = await memory.getsignals();
|
|
1189
|
+
const next = current ? { ...current, refreshedat: Date.now() } : { refreshedat: Date.now() };
|
|
1190
|
+
let changed = false;
|
|
1191
|
+
if (step.kind === "readlang" || step.kind === "detectlanguage") {
|
|
1192
|
+
const language = output?.details?.language;
|
|
1193
|
+
if (typeof language === "string" && language) {
|
|
1194
|
+
next.language = language;
|
|
1195
|
+
changed = true;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (step.kind === "classifypage") {
|
|
1199
|
+
const template = output?.details?.template;
|
|
1200
|
+
if (typeof template === "string" && template) {
|
|
1201
|
+
next.template = template;
|
|
1202
|
+
changed = true;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
if (step.kind === "detectscrolllock") {
|
|
1206
|
+
const locked = output?.details?.locked;
|
|
1207
|
+
if (typeof locked === "boolean") {
|
|
1208
|
+
next.scrolllocked = locked;
|
|
1209
|
+
changed = true;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
if (step.kind === "watchbanner") {
|
|
1213
|
+
const banners = detailarray(output?.details, "banners");
|
|
1214
|
+
const first = banners[0];
|
|
1215
|
+
if (first && typeof first.kind === "string") {
|
|
1216
|
+
next.banner = first.kind;
|
|
1217
|
+
changed = true;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (changed) await memory.setsignals(next);
|
|
1221
|
+
}
|
|
1222
|
+
async function recordevidence(step, output, session, plan, origin) {
|
|
1223
|
+
const extra = { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid: step.id };
|
|
1224
|
+
if (step.kind === "a11ytree" && output?.details?.tree && typeof output.details.tree === "object") {
|
|
1225
|
+
const version = await memory.nextobservationversion();
|
|
1226
|
+
await memory.adda11ytree({ version, tree: output.details.tree, capturedat: Date.now() });
|
|
1227
|
+
await audit("observation", `Accessibility tree captured under observation version ${version} with ${output.details.nodecount ?? "an unknown count of"} nodes.`, extra);
|
|
1228
|
+
}
|
|
1229
|
+
if (step.kind === "readertree" && output?.details?.article && typeof output.details.article === "object") {
|
|
1230
|
+
const version = await memory.nextobservationversion();
|
|
1231
|
+
const article = output.details.article;
|
|
1232
|
+
await memory.addreaderarticle({ version, article, capturedat: Date.now() });
|
|
1233
|
+
await audit("observation", `Reader article captured under observation version ${version} with ${article.blocks.length} blocks and ${article.words} words.`, extra);
|
|
1234
|
+
}
|
|
1235
|
+
if (step.kind === "classifypage" && typeof output?.details?.template === "string") {
|
|
1236
|
+
const fingerprint = typeof output.details.fingerprint === "string" ? output.details.fingerprint : "";
|
|
1237
|
+
const profile = { origin, template: output.details.template, fingerprint, at: Date.now() };
|
|
1238
|
+
await memory.addtemplate(profile);
|
|
1239
|
+
await audit("observation", `Page template ${profile.template} classified for ${origin}${fingerprint ? ` with fingerprint ${fingerprint}` : ""}.`, extra);
|
|
1240
|
+
}
|
|
1241
|
+
if (step.kind === "fingerprintsection" && typeof output?.details?.fingerprint === "string") {
|
|
1242
|
+
const profile = { origin, template: "", fingerprint: output.details.fingerprint, ...typeof output.details.section === "string" ? { section: output.details.section } : {}, at: Date.now() };
|
|
1243
|
+
await memory.addtemplate(profile);
|
|
1244
|
+
await audit("observation", `Section fingerprint ${profile.fingerprint} computed for ${origin}.`, extra);
|
|
1245
|
+
}
|
|
1246
|
+
if (step.kind === "deriveselector") {
|
|
1247
|
+
const candidates = detailarray(output?.details, "candidates");
|
|
1248
|
+
const best = candidates[0];
|
|
1249
|
+
if (best && typeof best.selector === "string" && best.selector) {
|
|
1250
|
+
const record2 = { stepid: step.id, selector: best.selector, strategy: best.strategy, score: best.score, at: Date.now() };
|
|
1251
|
+
await memory.addselector(record2);
|
|
1252
|
+
await audit("observation", `Derived selector ${record2.selector} through the ${record2.strategy} strategy with stability ${record2.score}.`, extra);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
await refreshsignals(step, output);
|
|
1256
|
+
}
|
|
454
1257
|
async function executestep(stepid) {
|
|
455
1258
|
const session = await memory.getsession();
|
|
456
1259
|
const plan = await memory.getplan();
|
|
@@ -460,6 +1263,7 @@ async function executestep(stepid) {
|
|
|
460
1263
|
const gate = canexecute({ session, plan, step, tabid: tab.id, origin });
|
|
461
1264
|
if (!gate.allowed) throw new Error(gate.reason);
|
|
462
1265
|
let output;
|
|
1266
|
+
let watchwindow;
|
|
463
1267
|
if (isbrowserkind(step.kind)) {
|
|
464
1268
|
const capability = requiredcapability(step.kind);
|
|
465
1269
|
if (capability) {
|
|
@@ -467,25 +1271,45 @@ async function executestep(stepid) {
|
|
|
467
1271
|
if (!granted) throw new Error(`The ${capability} capability has not been granted; request it from the review panel.`);
|
|
468
1272
|
}
|
|
469
1273
|
output = await runbrowseraction(step, tab.id, tab.windowId ?? chrome.windows.WINDOW_ID_CURRENT);
|
|
1274
|
+
} else if (step.kind === "keyhold") {
|
|
1275
|
+
output = await executekeyhold(step, session, plan, tab.id, origin);
|
|
1276
|
+
} else if (step.kind === "keyrelease") {
|
|
1277
|
+
output = await executekeyrelease(step, session, plan, tab.id, origin);
|
|
1278
|
+
} else if (step.kind === "dismissdialog") {
|
|
1279
|
+
output = await executedismissdialog(step, session, plan, tab.id, origin);
|
|
1280
|
+
} else if (step.kind === "retryaction") {
|
|
1281
|
+
output = await executeretryaction(step, session, plan, tab.id, origin);
|
|
1282
|
+
} else if (step.kind === "mapclicks") {
|
|
1283
|
+
output = await executemapclicks(step, plan, tab.id, origin);
|
|
1284
|
+
} else if (step.kind === "enterframe") {
|
|
1285
|
+
output = await executeenterframe(step, plan, tab.id, origin);
|
|
1286
|
+
} else if (watchstepkinds.has(step.kind)) {
|
|
1287
|
+
if (!session || !plan || plan.state !== "approved") throw new Error("Watch kinds refuse to run outside an approved session plan.");
|
|
1288
|
+
const watched = await executewatchstep(step, session, plan, tab.id, origin);
|
|
1289
|
+
output = watched.output;
|
|
1290
|
+
watchwindow = { startedat: watched.watch.startedat, lifetime: watched.watch.lifetime };
|
|
1291
|
+
} else if (step.kind === "diffsnapshots") {
|
|
1292
|
+
output = await executediffsnapshots(step, session, plan, tab.id, origin);
|
|
470
1293
|
} else {
|
|
471
1294
|
if (step.target && freshcheckkinds.has(step.kind)) {
|
|
472
1295
|
const fresh = await snapshot(tab.id);
|
|
473
1296
|
if (!fresh.interactive.some((item) => item.selector === step.target)) throw new Error("The page changed and the target must be reviewed again.");
|
|
474
1297
|
}
|
|
475
|
-
|
|
476
|
-
const bridge = globalThis.devthinkbridge;
|
|
477
|
-
if (!bridge) throw new Error("Devthink page bridge is unavailable.");
|
|
478
|
-
return bridge.performstep(action, expectedorigin);
|
|
479
|
-
}, args: [step, origin] });
|
|
480
|
-
output = result[0]?.result;
|
|
1298
|
+
output = await dispatchpagestep(step, tab.id, origin, plan);
|
|
481
1299
|
}
|
|
1300
|
+
await recordevidence(step, output, session, plan, origin);
|
|
482
1301
|
const summary = output?.summary ?? "The page action returned no result.";
|
|
1302
|
+
const resolved = output?.details?.resolvedtarget;
|
|
1303
|
+
if (resolved) {
|
|
1304
|
+
await memory.addresolution({ stepid, mode: resolved.mode, selector: resolved.selector, label: resolved.label, at: Date.now() });
|
|
1305
|
+
}
|
|
483
1306
|
const outcome = { stepid, ok: Boolean(output?.ok), summary, ...output?.details ? { details: output.details } : {}, at: Date.now() };
|
|
484
|
-
const auditkind =
|
|
1307
|
+
const auditkind = stepauditkind(step, Boolean(output?.ok));
|
|
485
1308
|
await audit(auditkind, summary, { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid });
|
|
486
1309
|
await memory.addoutcome(outcome);
|
|
487
1310
|
if (output?.ok && plan) {
|
|
488
|
-
const
|
|
1311
|
+
const base = await memory.getprogress();
|
|
1312
|
+
const completed = watchwindow ? recordwatchcompletion(base, plan.id, stepid, watchwindow.startedat, watchwindow.lifetime, Date.now()) : recordstep(base, plan.id, stepid, Date.now());
|
|
489
1313
|
const tracked = recordoutcome(completed, plan.id, outcome, Date.now());
|
|
490
1314
|
await memory.setprogress(tracked);
|
|
491
1315
|
if (iscomplete(tracked, plan) && plan.state === "approved") {
|
|
@@ -504,12 +1328,12 @@ async function previewstep(stepid) {
|
|
|
504
1328
|
if (!step) throw new Error("Reviewed step was not found.");
|
|
505
1329
|
const gate = canpreview({ session, plan, step, tabid: tab.id, origin });
|
|
506
1330
|
if (!gate.allowed) throw new Error(gate.reason);
|
|
507
|
-
if (!step.target) throw new Error("Only a target-based step can be previewed.");
|
|
508
|
-
const result = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: (
|
|
1331
|
+
if (!step.target && !stepoptions2(step).targetref) throw new Error("Only a target-based step can be previewed.");
|
|
1332
|
+
const result = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: (action, expectedorigin) => {
|
|
509
1333
|
const bridge = globalThis.devthinkbridge;
|
|
510
1334
|
if (!bridge) throw new Error("Devthink page bridge is unavailable.");
|
|
511
|
-
return bridge.previewtarget(
|
|
512
|
-
}, args: [step
|
|
1335
|
+
return bridge.previewtarget(action, expectedorigin);
|
|
1336
|
+
}, args: [step, origin] });
|
|
513
1337
|
const output = result[0]?.result;
|
|
514
1338
|
const summary = output?.summary ?? "The target preview returned no result.";
|
|
515
1339
|
await audit(output?.ok ? "observe" : "error", summary, { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid });
|
|
@@ -559,7 +1383,14 @@ async function handlerequest(message, sender) {
|
|
|
559
1383
|
case "context": {
|
|
560
1384
|
const plan = await memory.getplan();
|
|
561
1385
|
const progress = await memory.getprogress();
|
|
562
|
-
|
|
1386
|
+
const session = await memory.getsession();
|
|
1387
|
+
const holds = heldkeys(await memory.getholds());
|
|
1388
|
+
const observationversion = await memory.getobservationversion();
|
|
1389
|
+
const map = observationversion !== void 0 && observationversion > 0 ? await memory.getmap(observationversion) : void 0;
|
|
1390
|
+
const a11y = (await memory.geta11ytrees())[0];
|
|
1391
|
+
const reader = (await memory.getreaderarticles())[0];
|
|
1392
|
+
const signals = await memory.getsignals();
|
|
1393
|
+
return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {} };
|
|
563
1394
|
}
|
|
564
1395
|
case "capabilities":
|
|
565
1396
|
return refreshcapabilities();
|
|
@@ -576,6 +1407,8 @@ async function handlerequest(message, sender) {
|
|
|
576
1407
|
if (!plan || plan.state !== "pending") throw new Error("Only a pending plan can be approved.");
|
|
577
1408
|
const approved = { ...plan, state: "approved", approvedat: Date.now() };
|
|
578
1409
|
await memory.setplan(approved);
|
|
1410
|
+
const policy = plandialogpolicy(approved);
|
|
1411
|
+
if (policy) await memory.setdialogpolicy(policy);
|
|
579
1412
|
const current = await memory.getsession();
|
|
580
1413
|
await audit("approval", "The user approved the reviewed plan.", { ...current ? { sessionid: current.id } : {}, planid: approved.id });
|
|
581
1414
|
return approved;
|
|
@@ -593,6 +1426,30 @@ async function handlerequest(message, sender) {
|
|
|
593
1426
|
return previewstep(input.stepid ?? "");
|
|
594
1427
|
case "execute":
|
|
595
1428
|
return executestep(input.stepid ?? "");
|
|
1429
|
+
case "outcome": {
|
|
1430
|
+
const plan = await memory.getplan();
|
|
1431
|
+
if (!plan) throw new Error("No plan is available for an outcome envelope.");
|
|
1432
|
+
const outcome = (await memory.getoutcomes()).find((candidate) => candidate.stepid === (input.stepid ?? ""));
|
|
1433
|
+
if (!outcome) throw new Error("No outcome exists for the reviewed step.");
|
|
1434
|
+
const resolved = outcome.details?.resolvedtarget;
|
|
1435
|
+
return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {} }));
|
|
1436
|
+
}
|
|
1437
|
+
case "map": {
|
|
1438
|
+
const plan = await memory.getplan();
|
|
1439
|
+
if (!plan) throw new Error("No plan is available for a map envelope.");
|
|
1440
|
+
const version = await memory.getobservationversion();
|
|
1441
|
+
const map = version !== void 0 ? await memory.getmap(version) : void 0;
|
|
1442
|
+
if (!map) throw new Error("No clickable map has been captured yet.");
|
|
1443
|
+
return JSON.parse(mapresponse({ map, plan }));
|
|
1444
|
+
}
|
|
1445
|
+
case "observation": {
|
|
1446
|
+
const plan = await memory.getplan();
|
|
1447
|
+
if (!plan) throw new Error("No plan is available for an observation envelope.");
|
|
1448
|
+
const version = await memory.getobservationversion();
|
|
1449
|
+
const record2 = version !== void 0 ? await memory.getobservation(version) : void 0;
|
|
1450
|
+
if (!record2) throw new Error("No observation has been captured yet.");
|
|
1451
|
+
return JSON.parse(observationresponse({ observation: record2.observation, plan }));
|
|
1452
|
+
}
|
|
596
1453
|
case "pausesession":
|
|
597
1454
|
return pausesession();
|
|
598
1455
|
case "resumesession":
|
|
@@ -613,6 +1470,16 @@ chrome.runtime.onMessage.addListener((message, sender, sendresponse) => {
|
|
|
613
1470
|
handlerequest(message, sender).then((value) => sendresponse({ ok: true, value })).catch((error) => sendresponse({ ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
614
1471
|
return true;
|
|
615
1472
|
});
|
|
1473
|
+
async function reconcilewatches() {
|
|
1474
|
+
for (const watch of await memory.getwatches()) {
|
|
1475
|
+
if (watch.closedat !== void 0) continue;
|
|
1476
|
+
if (!watchclosed(watch.startedat, watch.lifetime, Date.now())) continue;
|
|
1477
|
+
await memory.closewatch(watch.watchid, Date.now());
|
|
1478
|
+
await audit("watch", `Watch ${watch.watchid} of ${watch.kind} closed on service worker restart after its reviewed lifetime of ${watch.lifetime} milliseconds.`, { sessionid: watch.sessionid, stepid: watch.stepid });
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
reconcilewatches().catch(() => {
|
|
1482
|
+
});
|
|
616
1483
|
chrome.runtime.onConnect.addListener((port) => {
|
|
617
1484
|
if (port.name !== "devthinksidepanel" || port.sender?.id !== chrome.runtime.id || !port.sender.url?.startsWith(chrome.runtime.getURL(""))) return port.disconnect();
|
|
618
1485
|
port.onMessage.addListener((message) => {
|