@mind-harness/mh-browser 0.1.1 → 0.1.3
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/extension/manifest.json +1 -1
- package/lib/index.js +2055 -1093
- package/lib/typert.host.js +27 -16
- package/lib/typert.remote-client.js +1 -1
- package/lib/types/render/envelope.d.ts +24 -5
- package/lib/types/representation/agent-view.d.ts +2 -0
- package/lib/types/representation/agent-view.js +2 -0
- package/lib/types/representation/read.d.ts +1 -0
- package/lib/types/service/actions.d.ts +9 -6
- package/lib/types/service/control.d.ts +15 -5
- package/lib/types/service/evaluate.d.ts +36 -0
- package/lib/types/service/find.d.ts +7 -0
- package/lib/types/service/invoke.d.ts +3 -4
- package/lib/types/service/service.d.ts +56 -9
- package/lib/types/service/windows.d.ts +8 -3
- package/lib/types/tools/delivery.d.ts +9 -11
- package/lib/types/tools/page-ops.d.ts +6 -0
- package/lib/types/tools/paths.d.ts +13 -0
- package/lib/types/tools/tab-log.d.ts +15 -1
- package/lib/types/types.d.ts +102 -36
- package/package.json +22 -20
package/lib/index.js
CHANGED
|
@@ -5,12 +5,12 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
5
5
|
import { HarnessError, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
6
6
|
import { z as z$1 } from "zod";
|
|
7
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
+
import { constants, readFileSync } from "node:fs";
|
|
8
9
|
import { lstat, mkdir, open, readFile, realpath, unlink, writeFile } from "node:fs/promises";
|
|
9
10
|
import { parse } from "yaml";
|
|
11
|
+
import { assertSupportedJsonSchema, defineTool, validateJsonSchemaValue } from "@deepseek-ai/dsh-tools";
|
|
10
12
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
11
13
|
import { chromium } from "playwright";
|
|
12
|
-
import { constants, readFileSync } from "node:fs";
|
|
13
|
-
import { assertSupportedJsonSchema, defineTool, validateJsonSchemaValue } from "@deepseek-ai/dsh-tools";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { UserQuestionError } from "@deepseek-ai/dsh-user-questions";
|
|
16
16
|
//#region lib/types/config.js
|
|
@@ -127,7 +127,7 @@ function presenceSet(state) {
|
|
|
127
127
|
}
|
|
128
128
|
/** The text of a reminder, as `shown` records it. */
|
|
129
129
|
function textDigest(message) {
|
|
130
|
-
return createHash("sha256").update(textOf$
|
|
130
|
+
return createHash("sha256").update(textOf$3(message)).digest("hex");
|
|
131
131
|
}
|
|
132
132
|
function applySplice(state, event) {
|
|
133
133
|
const data = record$3(event.data);
|
|
@@ -254,7 +254,7 @@ function entryOf$1(value) {
|
|
|
254
254
|
...reason === void 0 ? {} : { reason }
|
|
255
255
|
}];
|
|
256
256
|
}
|
|
257
|
-
function textOf$
|
|
257
|
+
function textOf$3(value) {
|
|
258
258
|
const content = record$3(value).content;
|
|
259
259
|
if (!Array.isArray(content)) return "";
|
|
260
260
|
return content.map((block) => record$3(block).text).filter((text) => typeof text === "string").join("\n");
|
|
@@ -279,6 +279,8 @@ var MhBrowserError = class extends Error {
|
|
|
279
279
|
if (page !== void 0) this.page = page;
|
|
280
280
|
}
|
|
281
281
|
};
|
|
282
|
+
/** Owner of the person's own window. It belongs to no Session. */
|
|
283
|
+
const PERSON_OWNER = "person";
|
|
282
284
|
const UNKNOWN_TAB = "UNKNOWN_TAB";
|
|
283
285
|
const UNKNOWN_TAB_REASON = "no such tab in this session";
|
|
284
286
|
/**
|
|
@@ -384,6 +386,65 @@ async function filesInWorkspace(cwd, files) {
|
|
|
384
386
|
}
|
|
385
387
|
return resolved;
|
|
386
388
|
}
|
|
389
|
+
/** Refuse a filename that leaves the session's write root, without creating it. */
|
|
390
|
+
async function assertInside(target, filename) {
|
|
391
|
+
await place(target, filename);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Write `filename` under the session's write root. A path that leaves the
|
|
395
|
+
* root is refused, through a symbolic link as well: the deepest existing
|
|
396
|
+
* directory is checked before any is created, and the file itself is not
|
|
397
|
+
* followed if it is a link.
|
|
398
|
+
*/
|
|
399
|
+
async function writeInside(target, filename, data) {
|
|
400
|
+
const file = await place(target, filename);
|
|
401
|
+
assertUnder(file.root, await realpath(await deepestExisting(dirname(file.file))), filename);
|
|
402
|
+
await mkdir(dirname(file.file), { recursive: true });
|
|
403
|
+
const parent = await realpath(dirname(file.file));
|
|
404
|
+
assertUnder(file.root, parent, filename);
|
|
405
|
+
const placed = resolve(parent, basename(file.file));
|
|
406
|
+
let handle;
|
|
407
|
+
try {
|
|
408
|
+
handle = await open(placed, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (error.code === "ELOOP") throw new BrowserToolError("OUTSIDE_WORKSPACE", `${filename} is outside the session workspace`);
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
await handle.writeFile(data);
|
|
415
|
+
} finally {
|
|
416
|
+
await handle.close();
|
|
417
|
+
}
|
|
418
|
+
const bytes = typeof data === "string" ? Buffer.byteLength(data) : data.byteLength;
|
|
419
|
+
return {
|
|
420
|
+
path: relative(file.root, placed),
|
|
421
|
+
bytes
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function assertUnder(root, path, filename) {
|
|
425
|
+
const fromRoot = relative(root, path);
|
|
426
|
+
if (fromRoot.startsWith("..") || isAbsolute(fromRoot)) throw new BrowserToolError("OUTSIDE_WORKSPACE", `${filename} is outside the session workspace`);
|
|
427
|
+
}
|
|
428
|
+
async function deepestExisting(path) {
|
|
429
|
+
for (let current = path;; current = dirname(current)) try {
|
|
430
|
+
await lstat(current);
|
|
431
|
+
return current;
|
|
432
|
+
} catch {
|
|
433
|
+
if (dirname(current) === current) return current;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function place(target, filename) {
|
|
437
|
+
if ("denied" in target) throw new BrowserToolError(target.code, target.denied);
|
|
438
|
+
if (filename.trim() === "") throw new BrowserToolError("INVALID_ARGS", "filename: give a path inside the workspace");
|
|
439
|
+
const root = await realpath(target.root).catch(() => target.root);
|
|
440
|
+
const abs = isAbsolute(filename) ? filename : resolve(root, filename);
|
|
441
|
+
const direct = relative(root, abs);
|
|
442
|
+
if (direct.startsWith("..") || isAbsolute(direct)) throw new BrowserToolError("OUTSIDE_WORKSPACE", `${filename} is outside the session workspace`);
|
|
443
|
+
return {
|
|
444
|
+
root,
|
|
445
|
+
file: abs
|
|
446
|
+
};
|
|
447
|
+
}
|
|
387
448
|
//#endregion
|
|
388
449
|
//#region lib/types/tools/attachments.js
|
|
389
450
|
function initAttachments() {
|
|
@@ -452,7 +513,12 @@ function visit(value, depth, found) {
|
|
|
452
513
|
}
|
|
453
514
|
for (const item of Object.values(row)) visit(item, depth + 1, found);
|
|
454
515
|
}
|
|
516
|
+
//#endregion
|
|
517
|
+
//#region lib/types/representation/agent-view.js
|
|
518
|
+
const AGENT_VIEW_VERSION = "0.1";
|
|
455
519
|
const AGENT_VIEW_ACCEPT = "text/markdown; variant=agent-view";
|
|
520
|
+
/** Accept on the page address when its origin is known to offer Agent Views. */
|
|
521
|
+
const PAGE_ACCEPT = "text/markdown; variant=agent-view, text/html;q=0.9, */*;q=0.8";
|
|
456
522
|
const FENCE_OPEN = /```agent-view\b/g;
|
|
457
523
|
const HTTP_METHODS = /* @__PURE__ */ new Set([
|
|
458
524
|
"GET",
|
|
@@ -659,6 +725,208 @@ function isAbsoluteHttp(uri) {
|
|
|
659
725
|
}
|
|
660
726
|
}
|
|
661
727
|
//#endregion
|
|
728
|
+
//#region lib/types/render/envelope.js
|
|
729
|
+
/** Every element the service writes around site text. */
|
|
730
|
+
const FRAMES = [
|
|
731
|
+
"system-reminder",
|
|
732
|
+
"page",
|
|
733
|
+
"page-error",
|
|
734
|
+
"added",
|
|
735
|
+
"changed",
|
|
736
|
+
"removed",
|
|
737
|
+
"absent",
|
|
738
|
+
"action-response",
|
|
739
|
+
"tabs",
|
|
740
|
+
"dialog",
|
|
741
|
+
"download",
|
|
742
|
+
"download-error"
|
|
743
|
+
];
|
|
744
|
+
const CLOSERS = new RegExp(`<\\s*/\\s*(${FRAMES.join("|")})(?![\\w-])`, "gi");
|
|
745
|
+
const OPENERS = new RegExp(`<\\s*(${FRAMES.join("|")})(?![\\w-])`, "gi");
|
|
746
|
+
/**
|
|
747
|
+
* Escape a closer a site could use to end a frame the service opened, and an
|
|
748
|
+
* opener it could use to pass its text off as one. Only `</name` and `<name`
|
|
749
|
+
* are rewritten, so a tag cut off before `>` is caught and no site text is dropped.
|
|
750
|
+
*/
|
|
751
|
+
function escapeBody(text) {
|
|
752
|
+
return text.replace(CLOSERS, (_match, tag) => `<\\/${tag.toLowerCase()}`).replace(OPENERS, (_match, tag) => `<\\${tag.toLowerCase()}`);
|
|
753
|
+
}
|
|
754
|
+
/** Escape text the service writes into an attribute. */
|
|
755
|
+
function escapeAttr(text) {
|
|
756
|
+
return text.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, " ").replace(/\n/g, " ").replace(/\t/g, "	");
|
|
757
|
+
}
|
|
758
|
+
/** Site text that must stay on one line, such as a title in `<tabs>`. */
|
|
759
|
+
function oneLine(text) {
|
|
760
|
+
return text.replace(/[\u0000-\u001f\u007f\u2028\u2029]+/g, " ");
|
|
761
|
+
}
|
|
762
|
+
function incompleteText(incomplete) {
|
|
763
|
+
const pending = `${String(incomplete.pendingRequests)} requests pending`;
|
|
764
|
+
return incomplete.load ? pending : `load not fired; ${pending}`;
|
|
765
|
+
}
|
|
766
|
+
function waitedText(waited) {
|
|
767
|
+
const seconds = formatSeconds(waited.seconds);
|
|
768
|
+
if (waited.met === "text") return `text appeared after ${seconds} s`;
|
|
769
|
+
if (waited.met === "textGone") return `text gone after ${seconds} s`;
|
|
770
|
+
if (waited.met === "time") return `waited ${seconds} s`;
|
|
771
|
+
return `not met after ${seconds} s`;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* `withDigest` is for a page attached to a tool error: an error result has no
|
|
775
|
+
* meta, so the digest in the header is what records the delivery.
|
|
776
|
+
*/
|
|
777
|
+
function renderPage(view, screenshot, omitBody = false, withDigest = false) {
|
|
778
|
+
const body = view.summary !== void 0 ? view.detail === void 0 || view.detail === "" ? view.summary : `${view.summary}\n${view.detail}` : view.headerOnly ? view.inPresence ? "" : "Open this page to read it or act on it." : omitBody ? "Content follows in the next presence reminder." : pageBody(view);
|
|
779
|
+
const dialog = view.dialog === void 0 ? "" : `\n${renderDialog(view.dialog.type, view.dialog.message)}`;
|
|
780
|
+
const digest = withDigest && !omitBody ? digestOf$1(view) : void 0;
|
|
781
|
+
const digestAttr = digest === void 0 ? "" : attr$1("digest", digest);
|
|
782
|
+
return `<page${pageAttrs(view, screenshot)}${digestAttr}>${escapeBody(body)}${dialog}</page>`;
|
|
783
|
+
}
|
|
784
|
+
function renderActionResponse(response) {
|
|
785
|
+
const body = response.lost ? "No response arrived after sending; the action may have taken effect." : response.body !== void 0 ? response.body : response.bytes !== void 0 ? `${response.mediaType ?? "application/octet-stream"} (${String(response.bytes)} bytes)` : "";
|
|
786
|
+
return `<action-response${attr$1("name", response.name)}${response.lost === true ? attr$1("lost", "true") : ""}${response.status === void 0 ? "" : attr$1("status", String(response.status))}${response.location === void 0 ? "" : attr$1("location", response.location)}>${escapeBody(body)}</action-response>`;
|
|
787
|
+
}
|
|
788
|
+
function renderPageError(error) {
|
|
789
|
+
const tab = error.tab === void 0 ? "" : attr$1("tab", error.tab);
|
|
790
|
+
return `<page-error${attr$1("url", error.url)}${tab}${attr$1("reason", error.reason)}></page-error>`;
|
|
791
|
+
}
|
|
792
|
+
function renderTabs(tabs, closed) {
|
|
793
|
+
const lines = tabs.map((tab) => {
|
|
794
|
+
const title = tab.title === "" ? "" : ` — ${oneLine(tab.title)}`;
|
|
795
|
+
const flags = [
|
|
796
|
+
tab.inPresence ? "in presence" : "",
|
|
797
|
+
tab.mayHaveChanged ? "may have changed" : "",
|
|
798
|
+
tab.occupied ? "in use" : ""
|
|
799
|
+
].filter((flag) => flag !== "");
|
|
800
|
+
const suffix = flags.length === 0 ? "" : ` ${flags.join(", ")}`;
|
|
801
|
+
return `${tab.tab} ${oneLine(tab.url)}${title}${suffix}`;
|
|
802
|
+
});
|
|
803
|
+
const closedLines = closed.map((note) => `closed: ${note}`);
|
|
804
|
+
return `<tabs>\n${escapeBody([...lines, ...closedLines].join("\n"))}\n</tabs>`;
|
|
805
|
+
}
|
|
806
|
+
function renderDialog(type, message) {
|
|
807
|
+
return `<dialog${attr$1("type", type)}>${escapeBody(message)}</dialog>`;
|
|
808
|
+
}
|
|
809
|
+
function renderDownload(download) {
|
|
810
|
+
if (download.kind === "error") {
|
|
811
|
+
const status = download.status === void 0 ? "" : attr$1("status", String(download.status));
|
|
812
|
+
return `<download-error${attr$1("url", download.url)}${status}${attr$1("reason", download.reason)}></download-error>`;
|
|
813
|
+
}
|
|
814
|
+
const media = download.mediaType === void 0 ? "" : attr$1("media-type", download.mediaType);
|
|
815
|
+
return `<download${attr$1("path", download.path)}${attr$1("url", download.url)}${media}${attr$1("bytes", String(download.bytes))}${attr$1("sha256", download.sha256)}${attr$1("observed", download.observed)}></download>`;
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* `omitInPresence`: the page's body is the same one presence carries, so a
|
|
819
|
+
* page in presence gets only its attributes.
|
|
820
|
+
*/
|
|
821
|
+
function renderOutcome(pages, screenshot, omitInPresence = false) {
|
|
822
|
+
return pages.map((page) => {
|
|
823
|
+
const main = "representation" in page ? renderPage(page, screenshot, omitInPresence && page.inPresence) : renderPageError(page);
|
|
824
|
+
if (!("representation" in page) || page.download === void 0) return main;
|
|
825
|
+
return `${main}\n${renderDownload(page.download)}`;
|
|
826
|
+
}).join("\n");
|
|
827
|
+
}
|
|
828
|
+
function tabSnap(tabs, closed) {
|
|
829
|
+
return {
|
|
830
|
+
tabs: tabs.map((tab) => ({
|
|
831
|
+
id: tab.tab,
|
|
832
|
+
url: tab.url,
|
|
833
|
+
presence: tab.inPresence,
|
|
834
|
+
changed: tab.mayHaveChanged,
|
|
835
|
+
inUse: tab.occupied
|
|
836
|
+
})),
|
|
837
|
+
closed: [...closed]
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Attach the full list when this session has not attached one yet, when the
|
|
842
|
+
* tab key changed, or when this result closed something. Title is not a change.
|
|
843
|
+
*/
|
|
844
|
+
function tabsChanged(previous, next) {
|
|
845
|
+
if (next.closed.length > 0) return true;
|
|
846
|
+
if (previous === void 0) return next.tabs.length > 0;
|
|
847
|
+
return JSON.stringify(previous.tabs) !== JSON.stringify(next.tabs);
|
|
848
|
+
}
|
|
849
|
+
/** Reading fields persisted in tool meta. `by` marks the result as this plugin's. */
|
|
850
|
+
function readingsOf(pages, action) {
|
|
851
|
+
return {
|
|
852
|
+
by: "mh-browser",
|
|
853
|
+
action,
|
|
854
|
+
pages: pages.map((page) => {
|
|
855
|
+
if (!("representation" in page)) return {
|
|
856
|
+
url: page.url,
|
|
857
|
+
...page.tab === void 0 ? {} : { tab: page.tab },
|
|
858
|
+
representation: "snapshot",
|
|
859
|
+
reason: page.reason
|
|
860
|
+
};
|
|
861
|
+
const version = versionOf$1(page);
|
|
862
|
+
const digest = page.headerOnly ? void 0 : digestOf$1(page);
|
|
863
|
+
const id = page.headerOnly ? void 0 : idOf(page);
|
|
864
|
+
return {
|
|
865
|
+
url: page.url,
|
|
866
|
+
...page.tab === void 0 ? {} : { tab: page.tab },
|
|
867
|
+
representation: page.headerOnly ? "header" : page.representation.kind,
|
|
868
|
+
...version === void 0 ? {} : { version },
|
|
869
|
+
...digest === void 0 ? {} : { digest },
|
|
870
|
+
...id === void 0 ? {} : { id },
|
|
871
|
+
...page.incomplete === void 0 ? {} : { incomplete: incompleteText(page.incomplete) }
|
|
872
|
+
};
|
|
873
|
+
})
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function pageAttrs(view, screenshot) {
|
|
877
|
+
const representation = view.representation;
|
|
878
|
+
const version = versionOf$1(view);
|
|
879
|
+
const agentView = representation.kind === "snapshot" && representation.agentView !== void 0 ? attr$1("agent-view", "available") : "";
|
|
880
|
+
return [
|
|
881
|
+
attr$1("url", view.url),
|
|
882
|
+
view.tab === void 0 ? "" : attr$1("tab", view.tab),
|
|
883
|
+
view.headerOnly ? attr$1("title", view.title) : "",
|
|
884
|
+
attr$1("representation", representation.kind),
|
|
885
|
+
version === void 0 ? "" : attr$1("version", version),
|
|
886
|
+
attr$1("observed", view.observedAt),
|
|
887
|
+
view.fragment === void 0 ? "" : attr$1("fragment", view.fragment),
|
|
888
|
+
view.status === void 0 ? "" : attr$1("status", String(view.status)),
|
|
889
|
+
view.mediaType === void 0 ? "" : attr$1("media-type", view.mediaType),
|
|
890
|
+
view.incomplete === void 0 ? "" : attr$1("incomplete", incompleteText(view.incomplete)),
|
|
891
|
+
view.waited === void 0 ? "" : attr$1("waited", waitedText(view.waited)),
|
|
892
|
+
view.inPresence ? attr$1("in-presence", "true") : "",
|
|
893
|
+
agentView,
|
|
894
|
+
representation.kind === "snapshot" && representation.agentViewError !== void 0 ? attr$1("agent-view-error", representation.agentViewError) : "",
|
|
895
|
+
screenshot === void 0 ? "" : attr$1("screenshot", screenshot)
|
|
896
|
+
].join("");
|
|
897
|
+
}
|
|
898
|
+
const BODY_LIMIT = 2e5;
|
|
899
|
+
function pageBody(view) {
|
|
900
|
+
const representation = view.representation;
|
|
901
|
+
if (view.mediaType === void 0 || representation.kind !== "snapshot") return representation.text;
|
|
902
|
+
if (view.body !== void 0) {
|
|
903
|
+
if (view.body.length <= BODY_LIMIT) return view.body;
|
|
904
|
+
return `${view.body.slice(0, BODY_LIMIT)}\nCut at ${String(BODY_LIMIT)} characters; use browser_download to save the whole file into the workspace.`;
|
|
905
|
+
}
|
|
906
|
+
if (view.status !== void 0) return `The site answered HTTP ${String(view.status)}; the ${view.mediaType} body is not shown.`;
|
|
907
|
+
return "Not an HTML page; use browser_download to save it into the workspace.";
|
|
908
|
+
}
|
|
909
|
+
function versionOf$1(view) {
|
|
910
|
+
const representation = view.representation;
|
|
911
|
+
if (representation.kind === "agent-view") return representation.version;
|
|
912
|
+
return representation.agentView?.version;
|
|
913
|
+
}
|
|
914
|
+
function digestOf$1(view) {
|
|
915
|
+
if (view.representation.kind !== "agent-view") return void 0;
|
|
916
|
+
return representationDigest(view.representation.text);
|
|
917
|
+
}
|
|
918
|
+
function idOf(view) {
|
|
919
|
+
if (view.representation.kind !== "agent-view") return void 0;
|
|
920
|
+
return agentViewId(view.representation.text, view.url);
|
|
921
|
+
}
|
|
922
|
+
function attr$1(name, value) {
|
|
923
|
+
return ` ${name}="${escapeAttr(value)}"`;
|
|
924
|
+
}
|
|
925
|
+
function formatSeconds(seconds) {
|
|
926
|
+
const rounded = Math.round(seconds * 10) / 10;
|
|
927
|
+
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
928
|
+
}
|
|
929
|
+
//#endregion
|
|
662
930
|
//#region lib/types/tools/delivery.js
|
|
663
931
|
/** Every mh-browser tool result's `meta.by`; a result without it is not a delivery. */
|
|
664
932
|
const META_BY = "mh-browser";
|
|
@@ -670,81 +938,70 @@ function initDeliveries() {
|
|
|
670
938
|
};
|
|
671
939
|
}
|
|
672
940
|
/**
|
|
673
|
-
* One event folded into the delivery record.
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
* of context, presence reminders included: the replacement baseline that
|
|
677
|
-
* follows the summary is the delivery that counts.
|
|
941
|
+
* One event folded into the delivery record. Compaction does not revoke a
|
|
942
|
+
* delivery. A later result with the same call id is the replacement a prune
|
|
943
|
+
* appends, so it does not replace the text the model was shown.
|
|
678
944
|
*/
|
|
679
945
|
function applyDelivery(state, event) {
|
|
680
|
-
if (event.type === "compaction/summary"
|
|
681
|
-
if (event.type === "compaction/prune") {
|
|
682
|
-
const shadowed = new Set(numberList(record$2(event.data).shadowedSeqs));
|
|
683
|
-
return markOut(state, (item) => shadowed.has(item.seq));
|
|
684
|
-
}
|
|
685
|
-
const replaced = replacedRange(event.surfaceOp);
|
|
686
|
-
if (replaced !== void 0) return markOut(state, (item) => item.seq >= replaced.start && item.seq <= replaced.end);
|
|
946
|
+
if (event.type === "compaction/summary" || event.type === "compaction/prune") return state;
|
|
687
947
|
if (event.type === "tool/result") return withShown(state, fromTool(event));
|
|
688
948
|
if (event.type === "user/message") return withShown(state, fromPresence(event));
|
|
689
949
|
return state;
|
|
690
950
|
}
|
|
691
951
|
/**
|
|
692
|
-
* The Agent View of this address the model saw last, and whether it is still
|
|
693
|
-
* in context. A page shown only as a snapshot has no Agent View digest.
|
|
694
|
-
*/
|
|
695
|
-
function latestSeen(state, url) {
|
|
696
|
-
const matches = state.deliveries.filter((item) => sameUrl(item.url, url));
|
|
697
|
-
if (matches.length === 0) return void 0;
|
|
698
|
-
const latest = matches.filter((item) => item.digest !== void 0).at(-1);
|
|
699
|
-
if (latest === void 0) return { digest: "" };
|
|
700
|
-
if (!latest.inContext) return "not-in-context";
|
|
701
|
-
return seenOf(latest);
|
|
702
|
-
}
|
|
703
|
-
function seenOf(item) {
|
|
704
|
-
return {
|
|
705
|
-
digest: item.digest ?? "",
|
|
706
|
-
...item.version === void 0 ? {} : { version: item.version }
|
|
707
|
-
};
|
|
708
|
-
}
|
|
709
|
-
/**
|
|
710
952
|
* The document `page` names: the one shown at that address, else the one whose
|
|
711
953
|
* Agent View declares that `id`. One `id` can name several documents, such as
|
|
712
|
-
* the pages of a collection; when
|
|
713
|
-
*
|
|
954
|
+
* the pages of a collection; when they differ, the caller must pick an address.
|
|
955
|
+
* `text` is the body the model was shown for that document.
|
|
714
956
|
*/
|
|
715
957
|
function resolvePage(state, page) {
|
|
716
|
-
const atAddress =
|
|
717
|
-
if (atAddress !== void 0) return
|
|
718
|
-
url: page,
|
|
719
|
-
seen: atAddress
|
|
720
|
-
};
|
|
958
|
+
const atAddress = documentOf(state.deliveries.filter((item) => sameUrl(item.url, page)));
|
|
959
|
+
if (atAddress !== void 0) return shownPage(atAddress);
|
|
721
960
|
const named = state.deliveries.filter((item) => item.id !== void 0 && sameUrl(item.id, page));
|
|
722
961
|
if (named.length === 0) return void 0;
|
|
723
962
|
const byUrl = /* @__PURE__ */ new Map();
|
|
724
963
|
for (const item of named) {
|
|
725
|
-
byUrl.
|
|
726
|
-
|
|
964
|
+
const list = byUrl.get(item.url) ?? [];
|
|
965
|
+
list.push(item);
|
|
966
|
+
byUrl.set(item.url, list);
|
|
727
967
|
}
|
|
728
|
-
const latest = [...byUrl.values()]
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
return seen !== void 0 && seen !== "not-in-context" && seen.digest === item.digest;
|
|
968
|
+
const latest = [...byUrl.values()].flatMap((list) => {
|
|
969
|
+
const doc = documentOf(list);
|
|
970
|
+
return doc === void 0 ? [] : [doc];
|
|
732
971
|
});
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
url: last.url,
|
|
736
|
-
seen: "not-in-context"
|
|
737
|
-
};
|
|
738
|
-
if (new Set(current.map((item) => item.digest)).size > 1) return { ambiguous: current.map((item) => item.url) };
|
|
739
|
-
const chosen = current.at(-1);
|
|
972
|
+
if (new Set(latest.map((item) => item.digest)).size > 1) return { ambiguous: latest.map((item) => item.url) };
|
|
973
|
+
const chosen = latest.reduce((best, item) => best === void 0 || item.seq >= best.seq ? item : best, void 0);
|
|
740
974
|
if (chosen === void 0) return void 0;
|
|
975
|
+
return shownPage(chosen);
|
|
976
|
+
}
|
|
977
|
+
function shownPage(item) {
|
|
741
978
|
return {
|
|
742
|
-
url:
|
|
743
|
-
|
|
979
|
+
url: item.url,
|
|
980
|
+
text: item.text ?? ""
|
|
744
981
|
};
|
|
745
982
|
}
|
|
983
|
+
/**
|
|
984
|
+
* The last Agent View shown at these deliveries, else the last delivery. A
|
|
985
|
+
* result that left the body to presence carries the same digest without the
|
|
986
|
+
* body; the body comes from the latest delivery with that digest that has one.
|
|
987
|
+
*/
|
|
988
|
+
function documentOf(matches) {
|
|
989
|
+
const views = matches.filter((item) => item.digest !== void 0);
|
|
990
|
+
const last = views.at(-1);
|
|
991
|
+
if (last === void 0) return matches.at(-1);
|
|
992
|
+
if (last.text !== void 0) return last;
|
|
993
|
+
for (let index = views.length - 1; index >= 0; index -= 1) {
|
|
994
|
+
const item = views[index];
|
|
995
|
+
if (item !== void 0 && item.digest === last.digest && item.text !== void 0) return {
|
|
996
|
+
...last,
|
|
997
|
+
text: item.text
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
return last;
|
|
1001
|
+
}
|
|
746
1002
|
function withShown(state, shown) {
|
|
747
1003
|
if (shown === void 0 || shown.deliveries.length === 0) return state;
|
|
1004
|
+
if (shown.callId !== void 0 && state.deliveries.some((item) => item.callId === shown.callId)) return state;
|
|
748
1005
|
const known = new Set(state.tokens);
|
|
749
1006
|
const fresh = urlsIn(shown.text).filter((token) => {
|
|
750
1007
|
if (known.has(token)) return false;
|
|
@@ -756,16 +1013,6 @@ function withShown(state, shown) {
|
|
|
756
1013
|
tokens: fresh.length === 0 ? state.tokens : [...state.tokens, ...fresh]
|
|
757
1014
|
};
|
|
758
1015
|
}
|
|
759
|
-
function markOut(state, out) {
|
|
760
|
-
if (!state.deliveries.some((item) => item.inContext && out(item))) return state;
|
|
761
|
-
return {
|
|
762
|
-
...state,
|
|
763
|
-
deliveries: state.deliveries.map((item) => item.inContext && out(item) ? {
|
|
764
|
-
...item,
|
|
765
|
-
inContext: false
|
|
766
|
-
} : item)
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
1016
|
const URL_TOKEN = /https?:\/\/[^\s"'<>`]+/gi;
|
|
770
1017
|
function urlsIn(text) {
|
|
771
1018
|
const found = [];
|
|
@@ -783,8 +1030,8 @@ function fromTool(event) {
|
|
|
783
1030
|
const data = record$2(event.data);
|
|
784
1031
|
const message = record$2(data.message);
|
|
785
1032
|
const callId = readString(message.toolCallId);
|
|
786
|
-
const text = textOf$
|
|
787
|
-
if (message.isError === true) return
|
|
1033
|
+
const text = textOf$2(message.content);
|
|
1034
|
+
if (message.isError === true) return void 0;
|
|
788
1035
|
const meta = record$2(data.meta);
|
|
789
1036
|
if (meta.by !== "mh-browser") return void 0;
|
|
790
1037
|
const pages = meta.pages;
|
|
@@ -792,25 +1039,28 @@ function fromTool(event) {
|
|
|
792
1039
|
const deliveries = [];
|
|
793
1040
|
for (const page of pages) {
|
|
794
1041
|
const row = record$2(page);
|
|
1042
|
+
if (row.representation === "header" || readString(row.reason) !== void 0) continue;
|
|
795
1043
|
const url = readString(row.url);
|
|
796
1044
|
if (url === void 0) continue;
|
|
797
1045
|
const digest = readString(row.digest);
|
|
798
1046
|
const version = readString(row.version);
|
|
799
1047
|
const id = readString(row.id);
|
|
1048
|
+
const body = digest === void 0 ? void 0 : shownBody(text, url);
|
|
800
1049
|
deliveries.push({
|
|
801
1050
|
url,
|
|
802
1051
|
seq: event.seq,
|
|
803
1052
|
kind: "tool",
|
|
804
|
-
inContext: true,
|
|
805
1053
|
...callId === void 0 ? {} : { callId },
|
|
806
1054
|
...digest === void 0 ? {} : { digest },
|
|
807
1055
|
...version === void 0 ? {} : { version },
|
|
808
|
-
...id === void 0 ? {} : { id }
|
|
1056
|
+
...id === void 0 ? {} : { id },
|
|
1057
|
+
...body === void 0 ? {} : { text: body }
|
|
809
1058
|
});
|
|
810
1059
|
}
|
|
811
1060
|
return {
|
|
812
1061
|
deliveries,
|
|
813
|
-
text
|
|
1062
|
+
text,
|
|
1063
|
+
...callId === void 0 ? {} : { callId }
|
|
814
1064
|
};
|
|
815
1065
|
}
|
|
816
1066
|
function fromPresence(event) {
|
|
@@ -828,76 +1078,65 @@ function fromPresence(event) {
|
|
|
828
1078
|
const digest = readString(row.digest);
|
|
829
1079
|
const version = readString(row.version);
|
|
830
1080
|
const id = readString(row.id);
|
|
1081
|
+
const body = digest === void 0 ? void 0 : shownBody(textOf$2(message.content), url);
|
|
831
1082
|
deliveries.push({
|
|
832
1083
|
url,
|
|
833
1084
|
seq: event.seq,
|
|
834
1085
|
kind: "presence",
|
|
835
|
-
inContext: true,
|
|
836
1086
|
...digest === void 0 ? {} : { digest },
|
|
837
1087
|
...version === void 0 ? {} : { version },
|
|
838
|
-
...id === void 0 ? {} : { id }
|
|
1088
|
+
...id === void 0 ? {} : { id },
|
|
1089
|
+
...body === void 0 ? {} : { text: body }
|
|
839
1090
|
});
|
|
840
1091
|
}
|
|
841
1092
|
return {
|
|
842
1093
|
deliveries,
|
|
843
|
-
text: textOf$
|
|
1094
|
+
text: textOf$2(message.content)
|
|
844
1095
|
};
|
|
845
1096
|
}
|
|
1097
|
+
const OMITTED_BODY = /* @__PURE__ */ new Set([
|
|
1098
|
+
"",
|
|
1099
|
+
"Content follows in the next presence reminder.",
|
|
1100
|
+
"Open this page to read it or act on it."
|
|
1101
|
+
]);
|
|
846
1102
|
/**
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
1103
|
+
* The `<page>` body the model was shown for this address. Site text cannot
|
|
1104
|
+
* open or close a frame (`escapeBody`), so every frame tag here is the
|
|
1105
|
+
* Service's. A spill can still cut out the closer and whatever follows it up
|
|
1106
|
+
* to a later closer; the body ends at the spill gap or at the next frame the
|
|
1107
|
+
* Service opened, whichever comes first.
|
|
851
1108
|
*/
|
|
852
|
-
function
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
const
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
}],
|
|
877
|
-
text
|
|
878
|
-
};
|
|
1109
|
+
function shownBody(text, url) {
|
|
1110
|
+
for (const match of text.matchAll(/<(page|added|changed)(?=[\s>])([^>]*)>([\s\S]*?)<\/\1>/g)) {
|
|
1111
|
+
const found = attrOf(match[2] ?? "", "url");
|
|
1112
|
+
if (found === void 0 || !sameUrl(found, url)) continue;
|
|
1113
|
+
const body = unescapeBody(upToBoundary(withoutDialog(match[3] ?? "")));
|
|
1114
|
+
if (OMITTED_BODY.has(body.trim())) return void 0;
|
|
1115
|
+
return body;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
const SPILL_GAP = "\n\n[...]\n\n";
|
|
1119
|
+
const FRAME_OPENER = new RegExp(`<(${FRAMES.join("|")})(?=[\\s>/])`);
|
|
1120
|
+
function upToBoundary(body) {
|
|
1121
|
+
const gap = body.indexOf(SPILL_GAP);
|
|
1122
|
+
const kept = gap === -1 ? body : body.slice(0, gap);
|
|
1123
|
+
const opener = FRAME_OPENER.exec(kept);
|
|
1124
|
+
return opener === null ? kept : kept.slice(0, opener.index);
|
|
1125
|
+
}
|
|
1126
|
+
const DIALOG_SUFFIX = /\n<dialog(?=[\s>])[^>]*>[\s\S]*?<\/dialog>$/;
|
|
1127
|
+
function withoutDialog(body) {
|
|
1128
|
+
return body.replace(DIALOG_SUFFIX, "");
|
|
1129
|
+
}
|
|
1130
|
+
const ESCAPED_TAG = new RegExp(`<\\\\(/?)(${FRAMES.join("|")})(?![\\w-])`, "g");
|
|
1131
|
+
function unescapeBody(body) {
|
|
1132
|
+
return body.replace(ESCAPED_TAG, "<$1$2");
|
|
879
1133
|
}
|
|
880
1134
|
function attrOf(header, name) {
|
|
881
1135
|
const value = new RegExp(`\\s${name}="([^"]*)"`).exec(header)?.[1];
|
|
882
1136
|
if (value === void 0 || value === "") return void 0;
|
|
883
1137
|
return value.replace(/"/g, "\"").replace(/</g, "<").replace(/>/g, ">").replace(/ /g, "\n").replace(/ /g, "\r").replace(/	/g, " ").replace(/&/g, "&");
|
|
884
1138
|
}
|
|
885
|
-
function
|
|
886
|
-
const row = record$2(op);
|
|
887
|
-
if (row.op !== "replace") return void 0;
|
|
888
|
-
const start = row.startSeq;
|
|
889
|
-
const end = row.endSeq;
|
|
890
|
-
if (typeof start !== "number" || typeof end !== "number" || end < start) return void 0;
|
|
891
|
-
return {
|
|
892
|
-
start,
|
|
893
|
-
end
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
function numberList(value) {
|
|
897
|
-
if (!Array.isArray(value)) return [];
|
|
898
|
-
return value.filter((item) => typeof item === "number");
|
|
899
|
-
}
|
|
900
|
-
function textOf$1(content) {
|
|
1139
|
+
function textOf$2(content) {
|
|
901
1140
|
if (!Array.isArray(content)) return "";
|
|
902
1141
|
const parts = [];
|
|
903
1142
|
for (const block of content) {
|
|
@@ -931,16 +1170,38 @@ function initTabLog() {
|
|
|
931
1170
|
return { open: [] };
|
|
932
1171
|
}
|
|
933
1172
|
function applyTabLog(state, event) {
|
|
934
|
-
|
|
1173
|
+
const cleared = event.type === "compaction/summary" || event.type === "compaction/prune" || event.surfaceOp === "replace";
|
|
1174
|
+
if (event.type !== "tool/result") return cleared ? withoutSnap(state) : state;
|
|
935
1175
|
const meta = record$1(record$1(event.data).meta);
|
|
936
|
-
if (meta.by !== "mh-browser") return state;
|
|
1176
|
+
if (meta.by !== "mh-browser") return cleared ? withoutSnap(state) : state;
|
|
937
1177
|
const named = Array.isArray(meta.pages) ? meta.pages.map((page) => record$1(page).tab).filter(isTabId) : [];
|
|
938
1178
|
const closed = Array.isArray(meta.closed) ? meta.closed.filter((note) => typeof note === "string").map(tabOfNote) : [];
|
|
939
|
-
if (named.length === 0 && closed.length === 0) return state;
|
|
940
1179
|
const next = new Set(state.open);
|
|
941
1180
|
for (const tab of named) next.add(tab);
|
|
942
1181
|
for (const tab of closed) next.delete(tab);
|
|
943
|
-
|
|
1182
|
+
const keptSnap = (cleared ? void 0 : snapOf(meta.tabs)) ?? (cleared ? void 0 : state.snap);
|
|
1183
|
+
const lastTab = Array.isArray(meta.pages) ? named[named.length - 1] : state.lastTab;
|
|
1184
|
+
return {
|
|
1185
|
+
open: [...next],
|
|
1186
|
+
...lastTab === void 0 ? {} : { lastTab },
|
|
1187
|
+
...keptSnap === void 0 ? {} : { snap: keptSnap }
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
/** The `<tabs>` suffix for this result, and the snap to store when it is attached. */
|
|
1191
|
+
function tabBlock(ctx, reader, session) {
|
|
1192
|
+
const closed = closedNotes(ctx, reader, session);
|
|
1193
|
+
const live = ctx.mhBrowser.tabs(session.id);
|
|
1194
|
+
const snap = tabSnap(live, closed);
|
|
1195
|
+
const previous = reader.tabs(session.session).snap;
|
|
1196
|
+
if (!tabsChanged(previous, snap)) return {
|
|
1197
|
+
suffix: "",
|
|
1198
|
+
closed
|
|
1199
|
+
};
|
|
1200
|
+
return {
|
|
1201
|
+
suffix: `\n${renderTabs(live, closed)}`,
|
|
1202
|
+
closed,
|
|
1203
|
+
snap
|
|
1204
|
+
};
|
|
944
1205
|
}
|
|
945
1206
|
/**
|
|
946
1207
|
* The closed notes for this result: the service's own, then each tab an
|
|
@@ -959,6 +1220,28 @@ function tabOfNote(note) {
|
|
|
959
1220
|
function isTabId(value) {
|
|
960
1221
|
return typeof value === "string" && /^t\d+$/.test(value);
|
|
961
1222
|
}
|
|
1223
|
+
function withoutSnap(state) {
|
|
1224
|
+
const { snap: _snap, ...rest } = state;
|
|
1225
|
+
return rest;
|
|
1226
|
+
}
|
|
1227
|
+
function snapOf(value) {
|
|
1228
|
+
const snap = record$1(value);
|
|
1229
|
+
if (!Array.isArray(snap.tabs)) return void 0;
|
|
1230
|
+
return {
|
|
1231
|
+
tabs: snap.tabs.flatMap((item) => {
|
|
1232
|
+
const row = record$1(item);
|
|
1233
|
+
if (typeof row.id !== "string" || typeof row.url !== "string") return [];
|
|
1234
|
+
return [{
|
|
1235
|
+
id: row.id,
|
|
1236
|
+
url: row.url,
|
|
1237
|
+
presence: row.presence === true,
|
|
1238
|
+
changed: row.changed === true,
|
|
1239
|
+
inUse: row.inUse === true
|
|
1240
|
+
}];
|
|
1241
|
+
}),
|
|
1242
|
+
closed: Array.isArray(snap.closed) ? snap.closed.filter((note) => typeof note === "string") : []
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
962
1245
|
function record$1(value) {
|
|
963
1246
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
964
1247
|
}
|
|
@@ -968,7 +1251,7 @@ function record$1(value) {
|
|
|
968
1251
|
function registerProjections(ctx) {
|
|
969
1252
|
ctx.sessionProjections.register({
|
|
970
1253
|
key: "mhBrowserDeliveries",
|
|
971
|
-
stateVersion:
|
|
1254
|
+
stateVersion: 2,
|
|
972
1255
|
stateSchema: plainObject(["deliveries", "tokens"]),
|
|
973
1256
|
init: initDeliveries,
|
|
974
1257
|
apply: (state, event) => applyDelivery(state, {
|
|
@@ -1000,7 +1283,7 @@ function registerProjections(ctx) {
|
|
|
1000
1283
|
});
|
|
1001
1284
|
ctx.sessionProjections.register({
|
|
1002
1285
|
key: "mhBrowserTabs",
|
|
1003
|
-
stateVersion:
|
|
1286
|
+
stateVersion: 2,
|
|
1004
1287
|
stateSchema: plainObject(["open"]),
|
|
1005
1288
|
init: initTabLog,
|
|
1006
1289
|
apply: (state, event) => applyTabLog(state, event)
|
|
@@ -1024,23 +1307,676 @@ function plainObject(keys) {
|
|
|
1024
1307
|
return z$1.custom((value) => value !== null && typeof value === "object" && keys.every((key) => key in value));
|
|
1025
1308
|
}
|
|
1026
1309
|
//#endregion
|
|
1027
|
-
//#region lib/types/
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1310
|
+
//#region lib/types/tools/page-ops.js
|
|
1311
|
+
const IMAGE_VALUE = {
|
|
1312
|
+
type: "object",
|
|
1313
|
+
additionalProperties: false,
|
|
1314
|
+
properties: {
|
|
1315
|
+
attachmentId: {
|
|
1316
|
+
type: "string",
|
|
1317
|
+
required: true
|
|
1318
|
+
},
|
|
1319
|
+
mediaType: {
|
|
1320
|
+
type: "string",
|
|
1321
|
+
enum: [
|
|
1322
|
+
"image/png",
|
|
1323
|
+
"image/jpeg",
|
|
1324
|
+
"image/webp",
|
|
1325
|
+
"image/gif"
|
|
1326
|
+
],
|
|
1327
|
+
required: true
|
|
1328
|
+
},
|
|
1329
|
+
bytes: {
|
|
1330
|
+
type: "integer",
|
|
1331
|
+
required: true
|
|
1332
|
+
},
|
|
1333
|
+
width: {
|
|
1334
|
+
type: "integer",
|
|
1335
|
+
required: true
|
|
1336
|
+
},
|
|
1337
|
+
height: {
|
|
1338
|
+
type: "integer",
|
|
1339
|
+
required: true
|
|
1340
|
+
},
|
|
1341
|
+
name: { type: "string" },
|
|
1342
|
+
originalDimensions: {
|
|
1343
|
+
type: "object",
|
|
1344
|
+
additionalProperties: false,
|
|
1345
|
+
properties: {
|
|
1346
|
+
width: {
|
|
1347
|
+
type: "integer",
|
|
1348
|
+
required: true
|
|
1349
|
+
},
|
|
1350
|
+
height: {
|
|
1351
|
+
type: "integer",
|
|
1352
|
+
required: true
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
const TAB$1 = "Tab id copied from the latest result in this session: a tabs list, or a page element's tab attribute. Omit it to use the tab of that latest result; if it has no tab, the call fails with INVALID_ARGS. An idle tab may be closed, and a closed id fails with UNKNOWN_TAB: open the address again with browser_open. A tab with unsubmitted input is marked in use and is not closed, and opening or a declared action does not reload it. Tabs are gone after a restart. A page with an Agent View has no tab until you open it with snapshot.";
|
|
1359
|
+
const TARGET = "A ref copied from this tab's latest snapshot, such as e3, or a selector that matches one element. A ref that is not in the current snapshot fails with STALE_REF.";
|
|
1360
|
+
const ELEMENT = "What the person sees in the log. It does not locate the element.";
|
|
1361
|
+
const FILENAME = "A path inside the workspace. The result is saved there and the reply is the path and the byte count. Omit it and the result stays in the reply.";
|
|
1362
|
+
const OUTPUT = {
|
|
1363
|
+
schema: {
|
|
1364
|
+
type: "object",
|
|
1365
|
+
additionalProperties: false,
|
|
1366
|
+
properties: {
|
|
1367
|
+
text: {
|
|
1368
|
+
type: "string",
|
|
1369
|
+
required: true
|
|
1370
|
+
},
|
|
1371
|
+
meta: {
|
|
1372
|
+
type: "json",
|
|
1373
|
+
required: true
|
|
1374
|
+
},
|
|
1375
|
+
image: IMAGE_VALUE
|
|
1376
|
+
}
|
|
1377
|
+
},
|
|
1378
|
+
render: (_args, value) => {
|
|
1379
|
+
const text = {
|
|
1380
|
+
type: "text",
|
|
1381
|
+
text: value.text
|
|
1382
|
+
};
|
|
1383
|
+
if (value.image === void 0) return [text];
|
|
1384
|
+
return [text, {
|
|
1385
|
+
type: "image",
|
|
1386
|
+
attachment: imageRef(value.image)
|
|
1387
|
+
}];
|
|
1388
|
+
},
|
|
1389
|
+
presentationMeta: (_args, value) => value.meta
|
|
1390
|
+
};
|
|
1391
|
+
const TARGET_FIELDS = {
|
|
1392
|
+
target: {
|
|
1393
|
+
type: "string",
|
|
1394
|
+
required: true,
|
|
1395
|
+
description: TARGET
|
|
1396
|
+
},
|
|
1397
|
+
element: {
|
|
1398
|
+
type: "string",
|
|
1399
|
+
description: ELEMENT
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
const PAGE_OPERATION_NAMES = [
|
|
1403
|
+
"browser_click",
|
|
1404
|
+
"browser_hover",
|
|
1405
|
+
"browser_select_option",
|
|
1406
|
+
"browser_drag",
|
|
1407
|
+
"browser_type",
|
|
1408
|
+
"browser_fill_form",
|
|
1409
|
+
"browser_press_key",
|
|
1410
|
+
"browser_file_upload",
|
|
1411
|
+
"browser_handle_dialog",
|
|
1412
|
+
"browser_navigate_back",
|
|
1413
|
+
"browser_wait_for",
|
|
1414
|
+
"browser_snapshot",
|
|
1415
|
+
"browser_find",
|
|
1416
|
+
"browser_take_screenshot",
|
|
1417
|
+
"browser_evaluate"
|
|
1418
|
+
];
|
|
1419
|
+
const OPS = [
|
|
1420
|
+
{
|
|
1421
|
+
name: "browser_click",
|
|
1422
|
+
description: "Click an element.",
|
|
1423
|
+
properties: {
|
|
1424
|
+
...TARGET_FIELDS,
|
|
1425
|
+
doubleClick: {
|
|
1426
|
+
type: "boolean",
|
|
1427
|
+
description: "Double-click instead of click."
|
|
1428
|
+
},
|
|
1429
|
+
button: {
|
|
1430
|
+
type: "string",
|
|
1431
|
+
enum: [
|
|
1432
|
+
"left",
|
|
1433
|
+
"right",
|
|
1434
|
+
"middle"
|
|
1435
|
+
],
|
|
1436
|
+
description: "Mouse button. Defaults to left."
|
|
1437
|
+
},
|
|
1438
|
+
modifiers: {
|
|
1439
|
+
type: "array",
|
|
1440
|
+
description: "Modifier keys held during the click.",
|
|
1441
|
+
items: {
|
|
1442
|
+
type: "string",
|
|
1443
|
+
enum: [
|
|
1444
|
+
"Alt",
|
|
1445
|
+
"Control",
|
|
1446
|
+
"ControlOrMeta",
|
|
1447
|
+
"Meta",
|
|
1448
|
+
"Shift"
|
|
1449
|
+
]
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
},
|
|
1453
|
+
build: async (args) => ({
|
|
1454
|
+
kind: "click",
|
|
1455
|
+
target: string(args.target),
|
|
1456
|
+
...optionalString(args, "element"),
|
|
1457
|
+
...args.doubleClick === true ? { doubleClick: true } : {},
|
|
1458
|
+
...args.button === "left" || args.button === "right" || args.button === "middle" ? { button: args.button } : {},
|
|
1459
|
+
...Array.isArray(args.modifiers) ? { modifiers: args.modifiers.filter(isModifier) } : {}
|
|
1460
|
+
})
|
|
1461
|
+
},
|
|
1462
|
+
{
|
|
1463
|
+
name: "browser_hover",
|
|
1464
|
+
description: "Hover over an element.",
|
|
1465
|
+
properties: { ...TARGET_FIELDS },
|
|
1466
|
+
build: async (args) => ({
|
|
1467
|
+
kind: "hover",
|
|
1468
|
+
target: string(args.target),
|
|
1469
|
+
...optionalString(args, "element")
|
|
1470
|
+
})
|
|
1471
|
+
},
|
|
1472
|
+
{
|
|
1473
|
+
name: "browser_select_option",
|
|
1474
|
+
description: "Choose options on a list element. This does not submit.",
|
|
1475
|
+
properties: {
|
|
1476
|
+
...TARGET_FIELDS,
|
|
1477
|
+
values: {
|
|
1478
|
+
type: "array",
|
|
1479
|
+
required: true,
|
|
1480
|
+
description: "Option values copied from the control. At least one.",
|
|
1481
|
+
items: { type: "string" }
|
|
1482
|
+
}
|
|
1483
|
+
},
|
|
1484
|
+
build: async (args) => ({
|
|
1485
|
+
kind: "select_option",
|
|
1486
|
+
target: string(args.target),
|
|
1487
|
+
values: stringList$1(args.values),
|
|
1488
|
+
...optionalString(args, "element")
|
|
1489
|
+
})
|
|
1490
|
+
},
|
|
1491
|
+
{
|
|
1492
|
+
name: "browser_drag",
|
|
1493
|
+
description: "Drag from one element to another.",
|
|
1494
|
+
properties: {
|
|
1495
|
+
startTarget: {
|
|
1496
|
+
type: "string",
|
|
1497
|
+
required: true,
|
|
1498
|
+
description: TARGET
|
|
1499
|
+
},
|
|
1500
|
+
startElement: {
|
|
1501
|
+
type: "string",
|
|
1502
|
+
description: ELEMENT
|
|
1503
|
+
},
|
|
1504
|
+
endTarget: {
|
|
1505
|
+
type: "string",
|
|
1506
|
+
required: true,
|
|
1507
|
+
description: TARGET
|
|
1508
|
+
},
|
|
1509
|
+
endElement: {
|
|
1510
|
+
type: "string",
|
|
1511
|
+
description: ELEMENT
|
|
1512
|
+
}
|
|
1513
|
+
},
|
|
1514
|
+
build: async (args) => ({
|
|
1515
|
+
kind: "drag",
|
|
1516
|
+
startTarget: string(args.startTarget),
|
|
1517
|
+
endTarget: string(args.endTarget),
|
|
1518
|
+
...optional("startElement", args.startElement),
|
|
1519
|
+
...optional("endElement", args.endElement)
|
|
1520
|
+
})
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
name: "browser_type",
|
|
1524
|
+
description: "Type into an element. submit presses Enter afterwards; otherwise this does not submit. Do not type into a login form or a form that issues credentials; call browser_handover.",
|
|
1525
|
+
properties: {
|
|
1526
|
+
...TARGET_FIELDS,
|
|
1527
|
+
text: {
|
|
1528
|
+
type: "string",
|
|
1529
|
+
required: true,
|
|
1530
|
+
description: "Text to type into the element."
|
|
1531
|
+
},
|
|
1532
|
+
submit: {
|
|
1533
|
+
type: "boolean",
|
|
1534
|
+
description: "Press Enter after typing."
|
|
1535
|
+
},
|
|
1536
|
+
slowly: {
|
|
1537
|
+
type: "boolean",
|
|
1538
|
+
description: "Type one character at a time. Otherwise the whole text is filled at once."
|
|
1539
|
+
}
|
|
1540
|
+
},
|
|
1541
|
+
build: async (args) => ({
|
|
1542
|
+
kind: "type",
|
|
1543
|
+
target: string(args.target),
|
|
1544
|
+
text: string(args.text),
|
|
1545
|
+
...optionalString(args, "element"),
|
|
1546
|
+
...args.submit === true ? { submit: true } : {},
|
|
1547
|
+
...args.slowly === true ? { slowly: true } : {}
|
|
1548
|
+
})
|
|
1549
|
+
},
|
|
1550
|
+
{
|
|
1551
|
+
name: "browser_fill_form",
|
|
1552
|
+
description: "Fill fields. This does not submit. Do not fill a login form or a form that issues credentials; call browser_handover.",
|
|
1553
|
+
properties: { fields: {
|
|
1554
|
+
type: "array",
|
|
1555
|
+
required: true,
|
|
1556
|
+
description: "Fields to write in one step. At least one. This does not submit.",
|
|
1557
|
+
items: {
|
|
1558
|
+
type: "object",
|
|
1559
|
+
additionalProperties: false,
|
|
1560
|
+
properties: {
|
|
1561
|
+
...TARGET_FIELDS,
|
|
1562
|
+
name: {
|
|
1563
|
+
type: "string",
|
|
1564
|
+
required: true,
|
|
1565
|
+
description: "Field name, for the person reading the log."
|
|
1566
|
+
},
|
|
1567
|
+
type: {
|
|
1568
|
+
type: "string",
|
|
1569
|
+
required: true,
|
|
1570
|
+
enum: [
|
|
1571
|
+
"textbox",
|
|
1572
|
+
"checkbox",
|
|
1573
|
+
"radio",
|
|
1574
|
+
"combobox",
|
|
1575
|
+
"slider"
|
|
1576
|
+
],
|
|
1577
|
+
description: "textbox, checkbox, radio, combobox, or slider."
|
|
1578
|
+
},
|
|
1579
|
+
value: {
|
|
1580
|
+
type: "string",
|
|
1581
|
+
required: true,
|
|
1582
|
+
description: "Text to write. A checkbox or radio is 'true' or 'false'. A combobox is the option text."
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
} },
|
|
1587
|
+
build: async (args) => ({
|
|
1588
|
+
kind: "fill_form",
|
|
1589
|
+
fields: fieldsOf(args.fields)
|
|
1590
|
+
})
|
|
1591
|
+
},
|
|
1592
|
+
{
|
|
1593
|
+
name: "browser_press_key",
|
|
1594
|
+
description: "Press a key on this tab. Enter returns a snapshot; any other key returns a sentence.",
|
|
1595
|
+
properties: { key: {
|
|
1596
|
+
type: "string",
|
|
1597
|
+
required: true,
|
|
1598
|
+
description: "Key name, such as Enter or ArrowDown, or one character."
|
|
1599
|
+
} },
|
|
1600
|
+
build: async (args) => ({
|
|
1601
|
+
kind: "press_key",
|
|
1602
|
+
key: string(args.key)
|
|
1603
|
+
})
|
|
1604
|
+
},
|
|
1605
|
+
{
|
|
1606
|
+
name: "browser_file_upload",
|
|
1607
|
+
description: "Give files to the open file chooser. Omit paths to cancel it.",
|
|
1608
|
+
properties: { paths: {
|
|
1609
|
+
type: "array",
|
|
1610
|
+
description: "Attachment references from this session, or paths inside the workspace. Omit it to cancel the chooser.",
|
|
1611
|
+
items: { type: "string" }
|
|
1612
|
+
} },
|
|
1613
|
+
build: async (args, ctx, reader, session) => {
|
|
1614
|
+
const values = Array.isArray(args.paths) ? args.paths : void 0;
|
|
1615
|
+
const paths = values === void 0 ? void 0 : await uploadPaths(ctx, reader.attachments(session.session), session.cwd, values);
|
|
1616
|
+
return {
|
|
1617
|
+
kind: "file_upload",
|
|
1618
|
+
...paths === void 0 ? {} : { paths }
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
},
|
|
1622
|
+
{
|
|
1623
|
+
name: "browser_handle_dialog",
|
|
1624
|
+
description: "Accept or dismiss the dialog open on this tab.",
|
|
1625
|
+
properties: {
|
|
1626
|
+
accept: {
|
|
1627
|
+
type: "boolean",
|
|
1628
|
+
required: true,
|
|
1629
|
+
description: "True accepts. False dismisses."
|
|
1630
|
+
},
|
|
1631
|
+
promptText: {
|
|
1632
|
+
type: "string",
|
|
1633
|
+
description: "Text to enter when accepting a prompt."
|
|
1634
|
+
}
|
|
1635
|
+
},
|
|
1636
|
+
build: async (args) => ({
|
|
1637
|
+
kind: "handle_dialog",
|
|
1638
|
+
accept: args.accept === true,
|
|
1639
|
+
...typeof args.promptText === "string" ? { promptText: args.promptText } : {}
|
|
1640
|
+
})
|
|
1641
|
+
},
|
|
1642
|
+
{
|
|
1643
|
+
name: "browser_navigate_back",
|
|
1644
|
+
description: "Go back one step in this tab's history.",
|
|
1645
|
+
properties: {},
|
|
1646
|
+
build: async () => ({ kind: "navigate_back" })
|
|
1647
|
+
},
|
|
1648
|
+
{
|
|
1649
|
+
name: "browser_wait_for",
|
|
1650
|
+
description: "Wait for text to appear or disappear, or for a number of seconds, then return the snapshot.",
|
|
1651
|
+
properties: {
|
|
1652
|
+
text: {
|
|
1653
|
+
type: "string",
|
|
1654
|
+
description: "Text to wait for. The snapshot is returned even if it never appears."
|
|
1655
|
+
},
|
|
1656
|
+
textGone: {
|
|
1657
|
+
type: "string",
|
|
1658
|
+
description: "Text to wait until it disappears. With text set too, the first one wins."
|
|
1659
|
+
},
|
|
1660
|
+
time: {
|
|
1661
|
+
type: "number",
|
|
1662
|
+
description: "Seconds to wait, or the limit for text and textGone. Capped at 30. Default 30."
|
|
1663
|
+
}
|
|
1664
|
+
},
|
|
1665
|
+
build: async (args) => ({
|
|
1666
|
+
kind: "wait_for",
|
|
1667
|
+
...typeof args.text === "string" ? { text: args.text } : {},
|
|
1668
|
+
...typeof args.textGone === "string" ? { textGone: args.textGone } : {},
|
|
1669
|
+
...typeof args.time === "number" ? { time: args.time } : {}
|
|
1670
|
+
})
|
|
1671
|
+
},
|
|
1672
|
+
{
|
|
1673
|
+
name: "browser_snapshot",
|
|
1674
|
+
description: "Return this tab's aria snapshot. A page that also has an Agent View still returns the snapshot; the agent-view attribute says one is available, and its text is not included.",
|
|
1675
|
+
properties: {
|
|
1676
|
+
target: {
|
|
1677
|
+
type: "string",
|
|
1678
|
+
description: "Limit the snapshot to this ref or selector."
|
|
1679
|
+
},
|
|
1680
|
+
depth: {
|
|
1681
|
+
type: "number",
|
|
1682
|
+
description: "Limit how deep the snapshot tree goes."
|
|
1683
|
+
},
|
|
1684
|
+
boxes: {
|
|
1685
|
+
type: "boolean",
|
|
1686
|
+
description: "Include each element's box."
|
|
1687
|
+
},
|
|
1688
|
+
filename: {
|
|
1689
|
+
type: "string",
|
|
1690
|
+
description: FILENAME
|
|
1691
|
+
}
|
|
1692
|
+
},
|
|
1693
|
+
filename: true,
|
|
1694
|
+
build: async (args) => ({
|
|
1695
|
+
kind: "snapshot",
|
|
1696
|
+
...typeof args.target === "string" ? { target: args.target } : {},
|
|
1697
|
+
...typeof args.depth === "number" ? { depth: args.depth } : {},
|
|
1698
|
+
...args.boxes === true ? { boxes: true } : {}
|
|
1699
|
+
})
|
|
1700
|
+
},
|
|
1701
|
+
{
|
|
1702
|
+
name: "browser_find",
|
|
1703
|
+
description: "Search this tab's snapshot and return the matching lines with a little context.",
|
|
1704
|
+
properties: {
|
|
1705
|
+
text: {
|
|
1706
|
+
type: "string",
|
|
1707
|
+
description: "Plain text to find. Case-insensitive. Give text or regex, not both."
|
|
1708
|
+
},
|
|
1709
|
+
regex: {
|
|
1710
|
+
type: "string",
|
|
1711
|
+
description: "Regular expression to find. Wrap it in slashes to add flags, such as /error/i."
|
|
1712
|
+
},
|
|
1713
|
+
maxResults: {
|
|
1714
|
+
type: "number",
|
|
1715
|
+
description: "Maximum number of matches to return. A positive integer."
|
|
1716
|
+
},
|
|
1717
|
+
filename: {
|
|
1718
|
+
type: "string",
|
|
1719
|
+
description: FILENAME
|
|
1720
|
+
}
|
|
1721
|
+
},
|
|
1722
|
+
filename: true,
|
|
1723
|
+
build: async (args) => ({
|
|
1724
|
+
kind: "find",
|
|
1725
|
+
...typeof args.text === "string" ? { text: args.text } : {},
|
|
1726
|
+
...typeof args.regex === "string" ? { regex: args.regex } : {},
|
|
1727
|
+
...typeof args.maxResults === "number" ? { maxResults: args.maxResults } : {}
|
|
1728
|
+
})
|
|
1729
|
+
},
|
|
1730
|
+
{
|
|
1731
|
+
name: "browser_take_screenshot",
|
|
1732
|
+
description: "Take a screenshot of this tab. Use browser_snapshot to read the page and get refs.",
|
|
1733
|
+
properties: {
|
|
1734
|
+
element: {
|
|
1735
|
+
type: "string",
|
|
1736
|
+
description: ELEMENT
|
|
1737
|
+
},
|
|
1738
|
+
target: {
|
|
1739
|
+
type: "string",
|
|
1740
|
+
description: "Screenshot this ref or selector instead of the viewport."
|
|
1741
|
+
},
|
|
1742
|
+
type: {
|
|
1743
|
+
type: "string",
|
|
1744
|
+
enum: [
|
|
1745
|
+
"png",
|
|
1746
|
+
"jpeg",
|
|
1747
|
+
"webp"
|
|
1748
|
+
],
|
|
1749
|
+
description: "Image format. Defaults to png."
|
|
1750
|
+
},
|
|
1751
|
+
fullPage: {
|
|
1752
|
+
type: "boolean",
|
|
1753
|
+
description: "Capture the full scrollable page. Not with a target."
|
|
1754
|
+
},
|
|
1755
|
+
scale: {
|
|
1756
|
+
type: "string",
|
|
1757
|
+
enum: ["css", "device"],
|
|
1758
|
+
description: "css sizes the image in CSS pixels. device uses device pixels. Defaults to css."
|
|
1759
|
+
},
|
|
1760
|
+
filename: {
|
|
1761
|
+
type: "string",
|
|
1762
|
+
description: FILENAME
|
|
1763
|
+
}
|
|
1764
|
+
},
|
|
1765
|
+
filename: true,
|
|
1766
|
+
image: true,
|
|
1767
|
+
build: async (args) => ({
|
|
1768
|
+
kind: "take_screenshot",
|
|
1769
|
+
...typeof args.element === "string" ? { element: args.element } : {},
|
|
1770
|
+
...typeof args.target === "string" ? { target: args.target } : {},
|
|
1771
|
+
...args.type === "png" || args.type === "jpeg" || args.type === "webp" ? { type: args.type } : {},
|
|
1772
|
+
...args.fullPage === true ? { fullPage: true } : {},
|
|
1773
|
+
...args.scale === "css" || args.scale === "device" ? { scale: args.scale } : {}
|
|
1774
|
+
})
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
name: "browser_evaluate",
|
|
1778
|
+
description: "Run a function in this tab's page, with the page's login. Write requests made while it runs are listed after the return value.",
|
|
1779
|
+
properties: {
|
|
1780
|
+
element: {
|
|
1781
|
+
type: "string",
|
|
1782
|
+
description: ELEMENT
|
|
1783
|
+
},
|
|
1784
|
+
target: {
|
|
1785
|
+
type: "string",
|
|
1786
|
+
description: "Pass this ref or selector as the function's element argument."
|
|
1787
|
+
},
|
|
1788
|
+
function: {
|
|
1789
|
+
type: "string",
|
|
1790
|
+
required: true,
|
|
1791
|
+
description: "A function, () => { ... } or (element) => { ... } when target is set."
|
|
1792
|
+
},
|
|
1793
|
+
filename: {
|
|
1794
|
+
type: "string",
|
|
1795
|
+
description: FILENAME
|
|
1796
|
+
}
|
|
1797
|
+
},
|
|
1798
|
+
filename: true,
|
|
1799
|
+
build: async (args, _ctx, _reader, _session, exec) => ({
|
|
1800
|
+
kind: "evaluate",
|
|
1801
|
+
function: string(args.function),
|
|
1802
|
+
callId: callIdOf(exec),
|
|
1803
|
+
...typeof args.element === "string" ? { element: args.element } : {},
|
|
1804
|
+
...typeof args.target === "string" ? { target: args.target } : {}
|
|
1805
|
+
})
|
|
1806
|
+
}
|
|
1807
|
+
];
|
|
1808
|
+
function pageOperationTools(ctx) {
|
|
1809
|
+
const reader = logReader(ctx);
|
|
1810
|
+
return OPS.map((spec) => defineOperation(ctx, reader, spec));
|
|
1811
|
+
}
|
|
1812
|
+
function defineOperation(ctx, reader, spec) {
|
|
1813
|
+
return defineTool({
|
|
1814
|
+
name: spec.name,
|
|
1815
|
+
description: spec.description,
|
|
1816
|
+
parameters: {
|
|
1817
|
+
tab: {
|
|
1818
|
+
type: "string",
|
|
1819
|
+
description: TAB$1
|
|
1820
|
+
},
|
|
1821
|
+
...spec.properties
|
|
1822
|
+
},
|
|
1823
|
+
output: OUTPUT,
|
|
1824
|
+
isConcurrencySafe: () => false,
|
|
1825
|
+
timeoutMs: 6e4,
|
|
1826
|
+
presentCall: (args) => ({
|
|
1827
|
+
card: "generic",
|
|
1828
|
+
kind: "other",
|
|
1829
|
+
title: `${spec.name} ${typeof args.tab === "string" ? args.tab : ""}`.trim()
|
|
1830
|
+
}),
|
|
1831
|
+
presentResult: (args) => ({
|
|
1832
|
+
card: "generic",
|
|
1833
|
+
title: `${spec.name} ${typeof args.tab === "string" ? args.tab : ""}`.trim()
|
|
1834
|
+
}),
|
|
1835
|
+
async execute(args, exec) {
|
|
1836
|
+
const session = sessionFor(ctx, exec);
|
|
1837
|
+
const target = writeTarget(ctx, session);
|
|
1838
|
+
ctx.mhBrowser.noteWorkspace(session.id, target);
|
|
1839
|
+
const input = args;
|
|
1840
|
+
const filename = spec.filename === true && typeof input.filename === "string" ? input.filename : void 0;
|
|
1841
|
+
if (filename !== void 0) await assertInside(target, filename);
|
|
1842
|
+
try {
|
|
1843
|
+
const tab = tabOf(reader, session, args.tab);
|
|
1844
|
+
const op = await spec.build(input, ctx, reader, session, exec);
|
|
1845
|
+
const view = await ctx.mhBrowser.act(session.id, tab, op, exec.signal);
|
|
1846
|
+
const saved = filename === void 0 ? void 0 : await saveResult(target, filename, view);
|
|
1847
|
+
if (saved !== void 0) {
|
|
1848
|
+
view.summary = `${saved.path}\n${String(saved.bytes)} bytes`;
|
|
1849
|
+
if (view.writes === void 0) delete view.detail;
|
|
1850
|
+
delete view.shot;
|
|
1851
|
+
}
|
|
1852
|
+
const image = spec.image === true && filename === void 0 && view.shot !== void 0 ? await saveShot(ctx, view) : void 0;
|
|
1853
|
+
const block = tabBlock(ctx, reader, session);
|
|
1854
|
+
return {
|
|
1855
|
+
text: `${renderOutcome([view], image?.attachmentId)}${block.suffix}`,
|
|
1856
|
+
meta: JSON.parse(JSON.stringify({
|
|
1857
|
+
...readingsOf([view], spec.name.slice(8)),
|
|
1858
|
+
...view.writes === void 0 ? {} : { writes: view.writes },
|
|
1859
|
+
closed: block.closed,
|
|
1860
|
+
...block.snap === void 0 ? {} : { tabs: block.snap }
|
|
1861
|
+
})),
|
|
1862
|
+
...image === void 0 ? {} : { image: imageValue(image) }
|
|
1863
|
+
};
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
if (error instanceof MhBrowserError && error.code === "UNKNOWN_TAB") {
|
|
1866
|
+
const block = tabBlock(ctx, reader, session);
|
|
1867
|
+
rethrow(new MhBrowserError(UNKNOWN_TAB, `${UNKNOWN_TAB_REASON} ${typeof args.tab === "string" ? args.tab : ""}`.trim()), `${renderOutcome([])}${block.suffix}`);
|
|
1868
|
+
}
|
|
1869
|
+
if (error instanceof MhBrowserError && (error.code === "STALE_REF" || error.code === "TAKEN_OVER" || error.code === "MODAL_STATE" || error.code === "PERSON_IN_CONTROL") && error.page !== void 0) {
|
|
1870
|
+
const block = tabBlock(ctx, reader, session);
|
|
1871
|
+
rethrow(error, `${renderOutcome([error.page])}${block.suffix}`);
|
|
1872
|
+
}
|
|
1873
|
+
rethrow(error);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
/** Probe the path before the action, then write the result the model would have been shown. */
|
|
1879
|
+
async function saveResult(target, filename, view) {
|
|
1880
|
+
return writeInside(target, filename, view.shot?.bytes ?? textOf$1(view));
|
|
1881
|
+
}
|
|
1882
|
+
function textOf$1(view) {
|
|
1883
|
+
if (view.summary !== void 0 && view.summary !== "") return view.detail === void 0 || view.detail === "" ? view.summary : `${view.summary}\n${view.detail}`;
|
|
1884
|
+
return view.representation.text;
|
|
1885
|
+
}
|
|
1886
|
+
function tabOf(reader, session, tab) {
|
|
1887
|
+
if (typeof tab === "string" && tab !== "") return tab;
|
|
1888
|
+
const last = reader.tabs(session.session).lastTab;
|
|
1889
|
+
if (last === void 0) throw new BrowserToolError("INVALID_ARGS", "tab: give a tab, or open the page with snapshot first");
|
|
1890
|
+
return last;
|
|
1891
|
+
}
|
|
1892
|
+
function string(value) {
|
|
1893
|
+
return typeof value === "string" ? value : "";
|
|
1894
|
+
}
|
|
1895
|
+
function stringList$1(value) {
|
|
1896
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
1897
|
+
}
|
|
1898
|
+
function optionalString(args, key) {
|
|
1899
|
+
return typeof args[key] === "string" ? { element: args[key] } : {};
|
|
1900
|
+
}
|
|
1901
|
+
function optional(key, value) {
|
|
1902
|
+
return typeof value === "string" ? { [key]: value } : {};
|
|
1903
|
+
}
|
|
1904
|
+
function fieldsOf(value) {
|
|
1905
|
+
if (!Array.isArray(value)) return [];
|
|
1906
|
+
return value.flatMap((item) => {
|
|
1907
|
+
if (item === null || typeof item !== "object") return [];
|
|
1908
|
+
const row = item;
|
|
1909
|
+
const type = row.type;
|
|
1910
|
+
if (type !== "textbox" && type !== "checkbox" && type !== "radio" && type !== "combobox" && type !== "slider") return [];
|
|
1911
|
+
return [{
|
|
1912
|
+
target: string(row.target),
|
|
1913
|
+
...typeof row.element === "string" ? { element: row.element } : {},
|
|
1914
|
+
name: string(row.name),
|
|
1915
|
+
type,
|
|
1916
|
+
value: string(row.value)
|
|
1917
|
+
}];
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
function callIdOf(exec) {
|
|
1921
|
+
return String(exec.callId);
|
|
1922
|
+
}
|
|
1923
|
+
function imageValue(ref) {
|
|
1924
|
+
return {
|
|
1925
|
+
attachmentId: ref.attachmentId,
|
|
1926
|
+
mediaType: ref.mediaType,
|
|
1927
|
+
bytes: ref.bytes,
|
|
1928
|
+
width: ref.width,
|
|
1929
|
+
height: ref.height,
|
|
1930
|
+
...ref.name === void 0 ? {} : { name: ref.name },
|
|
1931
|
+
...ref.originalDimensions === void 0 ? {} : { originalDimensions: { ...ref.originalDimensions } }
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
function imageRef(value) {
|
|
1935
|
+
return {
|
|
1936
|
+
attachmentId: value.attachmentId,
|
|
1937
|
+
mediaType: value.mediaType,
|
|
1938
|
+
bytes: value.bytes,
|
|
1939
|
+
width: value.width,
|
|
1940
|
+
height: value.height,
|
|
1941
|
+
...value.name === void 0 ? {} : { name: value.name },
|
|
1942
|
+
...value.originalDimensions === void 0 ? {} : { originalDimensions: { ...value.originalDimensions } }
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
async function saveShot(ctx, view) {
|
|
1946
|
+
const shot = view.shot;
|
|
1947
|
+
if (shot === void 0) return void 0;
|
|
1948
|
+
try {
|
|
1949
|
+
return await ctx.attachments.saveImage({
|
|
1950
|
+
data: shot.bytes,
|
|
1951
|
+
mediaType: shot.mediaType,
|
|
1952
|
+
name: `${view.tab ?? "page"}.${shot.mediaType.slice(6)}`
|
|
1953
|
+
});
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
ctx.logger.warn("saving the screenshot failed: %o", error);
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
function isModifier(value) {
|
|
1960
|
+
return value === "Alt" || value === "Control" || value === "ControlOrMeta" || value === "Meta" || value === "Shift";
|
|
1961
|
+
}
|
|
1962
|
+
//#endregion
|
|
1963
|
+
//#region lib/types/presence/notice.js
|
|
1964
|
+
/** The sentence a session sees after the person hands the browser back. */
|
|
1965
|
+
function takeoverText(taken) {
|
|
1966
|
+
const tabs = taken.tabs.length === 0 ? "none" : taken.tabs.join(", ");
|
|
1967
|
+
return [
|
|
1968
|
+
"<system-reminder>",
|
|
1969
|
+
`The person took over the browser and handed it back (${String(taken.operations)} operations, ${taken.from}–${taken.to}). Tabs that may have changed: ${tabs}. Look at them again before acting on them.`,
|
|
1970
|
+
"</system-reminder>"
|
|
1971
|
+
].join("\n");
|
|
1972
|
+
}
|
|
1973
|
+
//#endregion
|
|
1974
|
+
//#region lib/types/presence/register.js
|
|
1975
|
+
const BROWSER_TOOLS = /* @__PURE__ */ new Set([
|
|
1976
|
+
"browser_open",
|
|
1042
1977
|
"browser_invoke",
|
|
1043
|
-
"browser_presence"
|
|
1978
|
+
"browser_presence",
|
|
1979
|
+
...PAGE_OPERATION_NAMES
|
|
1044
1980
|
]);
|
|
1045
1981
|
/**
|
|
1046
1982
|
* Track browser touches, check the pages they named after the step, and merge
|
|
@@ -1266,13 +2202,7 @@ function touchedUrls(service, exec, result) {
|
|
|
1266
2202
|
const args = record(exec.arguments);
|
|
1267
2203
|
return [...stringList(args.put), ...stringList(args.remove)].map(canonical$1);
|
|
1268
2204
|
}
|
|
1269
|
-
if (result.isError)
|
|
1270
|
-
if (exec.name !== "browser_invoke" || errorCode(result) !== "STALE_VIEW") return [];
|
|
1271
|
-
const page = record(exec.arguments).page;
|
|
1272
|
-
if (typeof page !== "string") return [];
|
|
1273
|
-
const key = canonical$1(page);
|
|
1274
|
-
return known.has(key) ? [key] : [];
|
|
1275
|
-
}
|
|
2205
|
+
if (result.isError) return [];
|
|
1276
2206
|
const value = result.value;
|
|
1277
2207
|
const pages = record(record(value).meta).pages;
|
|
1278
2208
|
if (!Array.isArray(pages)) return [];
|
|
@@ -1283,11 +2213,6 @@ function touchedUrls(service, exec, result) {
|
|
|
1283
2213
|
return known.has(key) ? [key] : [];
|
|
1284
2214
|
});
|
|
1285
2215
|
}
|
|
1286
|
-
function errorCode(result) {
|
|
1287
|
-
if (!result.isError) return void 0;
|
|
1288
|
-
const code = record(record(record(result).error).info).code;
|
|
1289
|
-
return typeof code === "string" ? code : void 0;
|
|
1290
|
-
}
|
|
1291
2216
|
function stringList(value) {
|
|
1292
2217
|
if (!Array.isArray(value)) return [];
|
|
1293
2218
|
return value.filter((item) => typeof item === "string" && item !== "");
|
|
@@ -1439,183 +2364,6 @@ var PresenceBook = class {
|
|
|
1439
2364
|
}
|
|
1440
2365
|
};
|
|
1441
2366
|
//#endregion
|
|
1442
|
-
//#region lib/types/render/envelope.js
|
|
1443
|
-
const CLOSERS = new RegExp(`<\\s*/\\s*(${[
|
|
1444
|
-
"system-reminder",
|
|
1445
|
-
"page",
|
|
1446
|
-
"page-error",
|
|
1447
|
-
"added",
|
|
1448
|
-
"changed",
|
|
1449
|
-
"removed",
|
|
1450
|
-
"absent",
|
|
1451
|
-
"action-response",
|
|
1452
|
-
"tabs",
|
|
1453
|
-
"dialog",
|
|
1454
|
-
"download",
|
|
1455
|
-
"download-error"
|
|
1456
|
-
].join("|")})(?![\\w-])`, "gi");
|
|
1457
|
-
/**
|
|
1458
|
-
* Escape a closer a site could use to end a frame the service opened. Only
|
|
1459
|
-
* its opening `</name` is rewritten, so a closer cut off before `>` is caught
|
|
1460
|
-
* and no site text is dropped.
|
|
1461
|
-
*/
|
|
1462
|
-
function escapeBody(text) {
|
|
1463
|
-
return text.replace(CLOSERS, (_match, tag) => `<\\/${tag.toLowerCase()}`);
|
|
1464
|
-
}
|
|
1465
|
-
/** Escape text the service writes into an attribute. */
|
|
1466
|
-
function escapeAttr(text) {
|
|
1467
|
-
return text.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, " ").replace(/\n/g, " ").replace(/\t/g, "	");
|
|
1468
|
-
}
|
|
1469
|
-
/** Site text that must stay on one line, such as a title in `<tabs>`. */
|
|
1470
|
-
function oneLine(text) {
|
|
1471
|
-
return text.replace(/[\u0000-\u001f\u007f\u2028\u2029]+/g, " ");
|
|
1472
|
-
}
|
|
1473
|
-
function incompleteText(incomplete) {
|
|
1474
|
-
const pending = `${String(incomplete.pendingRequests)} requests pending`;
|
|
1475
|
-
return incomplete.load ? pending : `load not fired; ${pending}`;
|
|
1476
|
-
}
|
|
1477
|
-
function waitedText(waited) {
|
|
1478
|
-
const seconds = formatSeconds(waited.seconds);
|
|
1479
|
-
if (waited.met === "text") return `text appeared after ${seconds} s`;
|
|
1480
|
-
if (waited.met === "textGone") return `text gone after ${seconds} s`;
|
|
1481
|
-
if (waited.met === "time") return `waited ${seconds} s`;
|
|
1482
|
-
return `not met after ${seconds} s`;
|
|
1483
|
-
}
|
|
1484
|
-
/**
|
|
1485
|
-
* `withDigest` is for a page attached to a tool error: an error result has no
|
|
1486
|
-
* meta, so the digest in the header is what records the delivery.
|
|
1487
|
-
*/
|
|
1488
|
-
function renderPage(view, screenshot, omitBody = false, withDigest = false) {
|
|
1489
|
-
const body = omitBody ? "Content follows in the next presence reminder." : pageBody(view);
|
|
1490
|
-
const dialog = view.dialog === void 0 ? "" : `\n${renderDialog(view.dialog.type, view.dialog.message)}`;
|
|
1491
|
-
const digest = withDigest && !omitBody ? digestOf$1(view) : void 0;
|
|
1492
|
-
const digestAttr = digest === void 0 ? "" : attr$1("digest", digest);
|
|
1493
|
-
return `<page${pageAttrs(view, screenshot)}${digestAttr}>${escapeBody(body)}${dialog}</page>`;
|
|
1494
|
-
}
|
|
1495
|
-
function renderActionResponse(response) {
|
|
1496
|
-
const body = response.lost ? "No response arrived after sending; the action may have taken effect." : response.body !== void 0 ? response.body : response.bytes !== void 0 ? `${response.mediaType ?? "application/octet-stream"} (${String(response.bytes)} bytes)` : "";
|
|
1497
|
-
return `<action-response${attr$1("name", response.name)}${response.lost === true ? attr$1("lost", "true") : ""}${response.status === void 0 ? "" : attr$1("status", String(response.status))}${response.location === void 0 ? "" : attr$1("location", response.location)}>${escapeBody(body)}</action-response>`;
|
|
1498
|
-
}
|
|
1499
|
-
function renderPageError(error) {
|
|
1500
|
-
const tab = error.tab === void 0 ? "" : attr$1("tab", error.tab);
|
|
1501
|
-
return `<page-error${attr$1("url", error.url)}${tab}${attr$1("reason", error.reason)}></page-error>`;
|
|
1502
|
-
}
|
|
1503
|
-
function renderTabs(tabs, closed) {
|
|
1504
|
-
const lines = tabs.map((tab) => {
|
|
1505
|
-
const title = tab.title === "" ? "" : ` — ${oneLine(tab.title)}`;
|
|
1506
|
-
const flags = [
|
|
1507
|
-
tab.inPresence ? "in presence" : "",
|
|
1508
|
-
tab.mayHaveChanged ? "may have changed" : "",
|
|
1509
|
-
tab.occupied ? "in use" : ""
|
|
1510
|
-
].filter((flag) => flag !== "");
|
|
1511
|
-
const suffix = flags.length === 0 ? "" : ` ${flags.join(", ")}`;
|
|
1512
|
-
return `${tab.tab} ${oneLine(tab.url)}${title}${suffix}`;
|
|
1513
|
-
});
|
|
1514
|
-
const closedLines = closed.map((note) => `closed: ${note}`);
|
|
1515
|
-
return `<tabs>\n${escapeBody([...lines, ...closedLines].join("\n"))}\n</tabs>`;
|
|
1516
|
-
}
|
|
1517
|
-
function renderDialog(type, message) {
|
|
1518
|
-
return `<dialog${attr$1("type", type)}>${escapeBody(message)}</dialog>`;
|
|
1519
|
-
}
|
|
1520
|
-
function renderDownload(download) {
|
|
1521
|
-
if (download.kind === "error") {
|
|
1522
|
-
const status = download.status === void 0 ? "" : attr$1("status", String(download.status));
|
|
1523
|
-
return `<download-error${attr$1("url", download.url)}${status}${attr$1("reason", download.reason)}></download-error>`;
|
|
1524
|
-
}
|
|
1525
|
-
const media = download.mediaType === void 0 ? "" : attr$1("media-type", download.mediaType);
|
|
1526
|
-
return `<download${attr$1("path", download.path)}${attr$1("url", download.url)}${media}${attr$1("bytes", String(download.bytes))}${attr$1("sha256", download.sha256)}${attr$1("observed", download.observed)}></download>`;
|
|
1527
|
-
}
|
|
1528
|
-
/**
|
|
1529
|
-
* `omitInPresence`: the page's body is the same one presence carries, so a
|
|
1530
|
-
* page in presence gets only its attributes.
|
|
1531
|
-
*/
|
|
1532
|
-
function renderOutcome(pages, tabs, closed, screenshot, omitInPresence = false) {
|
|
1533
|
-
return `${pages.map((page) => {
|
|
1534
|
-
const main = "representation" in page ? renderPage(page, screenshot, omitInPresence && page.inPresence) : renderPageError(page);
|
|
1535
|
-
if (!("representation" in page) || page.download === void 0) return main;
|
|
1536
|
-
return `${main}\n${renderDownload(page.download)}`;
|
|
1537
|
-
}).join("\n")}\n${renderTabs(tabs, closed)}`;
|
|
1538
|
-
}
|
|
1539
|
-
/** Reading fields persisted in tool meta. `by` marks the result as this plugin's. */
|
|
1540
|
-
function readingsOf(pages, action) {
|
|
1541
|
-
return {
|
|
1542
|
-
by: "mh-browser",
|
|
1543
|
-
action,
|
|
1544
|
-
pages: pages.map((page) => {
|
|
1545
|
-
if (!("representation" in page)) return {
|
|
1546
|
-
url: page.url,
|
|
1547
|
-
...page.tab === void 0 ? {} : { tab: page.tab },
|
|
1548
|
-
representation: "snapshot",
|
|
1549
|
-
reason: page.reason
|
|
1550
|
-
};
|
|
1551
|
-
const version = versionOf$1(page);
|
|
1552
|
-
const digest = digestOf$1(page);
|
|
1553
|
-
const id = idOf(page);
|
|
1554
|
-
return {
|
|
1555
|
-
url: page.url,
|
|
1556
|
-
tab: page.tab,
|
|
1557
|
-
representation: page.representation.kind,
|
|
1558
|
-
...version === void 0 ? {} : { version },
|
|
1559
|
-
...digest === void 0 ? {} : { digest },
|
|
1560
|
-
...id === void 0 ? {} : { id },
|
|
1561
|
-
...page.incomplete === void 0 ? {} : { incomplete: incompleteText(page.incomplete) }
|
|
1562
|
-
};
|
|
1563
|
-
})
|
|
1564
|
-
};
|
|
1565
|
-
}
|
|
1566
|
-
function pageAttrs(view, screenshot) {
|
|
1567
|
-
const representation = view.representation;
|
|
1568
|
-
const version = versionOf$1(view);
|
|
1569
|
-
const agentView = representation.kind === "snapshot" && representation.agentView !== void 0 ? attr$1("agent-view", "available") : "";
|
|
1570
|
-
return [
|
|
1571
|
-
attr$1("url", view.url),
|
|
1572
|
-
attr$1("tab", view.tab),
|
|
1573
|
-
attr$1("representation", representation.kind),
|
|
1574
|
-
version === void 0 ? "" : attr$1("version", version),
|
|
1575
|
-
attr$1("observed", view.observedAt),
|
|
1576
|
-
view.fragment === void 0 ? "" : attr$1("fragment", view.fragment),
|
|
1577
|
-
view.status === void 0 ? "" : attr$1("status", String(view.status)),
|
|
1578
|
-
view.mediaType === void 0 ? "" : attr$1("media-type", view.mediaType),
|
|
1579
|
-
view.incomplete === void 0 ? "" : attr$1("incomplete", incompleteText(view.incomplete)),
|
|
1580
|
-
view.waited === void 0 ? "" : attr$1("waited", waitedText(view.waited)),
|
|
1581
|
-
view.inPresence ? attr$1("in-presence", "true") : "",
|
|
1582
|
-
agentView,
|
|
1583
|
-
representation.kind === "snapshot" && representation.agentViewError !== void 0 ? attr$1("agent-view-error", representation.agentViewError) : "",
|
|
1584
|
-
screenshot === void 0 ? "" : attr$1("screenshot", screenshot)
|
|
1585
|
-
].join("");
|
|
1586
|
-
}
|
|
1587
|
-
const BODY_LIMIT = 2e5;
|
|
1588
|
-
function pageBody(view) {
|
|
1589
|
-
const representation = view.representation;
|
|
1590
|
-
if (view.mediaType === void 0 || representation.kind !== "snapshot") return representation.text;
|
|
1591
|
-
if (view.body !== void 0) {
|
|
1592
|
-
if (view.body.length <= BODY_LIMIT) return view.body;
|
|
1593
|
-
return `${view.body.slice(0, BODY_LIMIT)}\nCut at ${String(BODY_LIMIT)} characters; use browser_download to save the whole file into the workspace.`;
|
|
1594
|
-
}
|
|
1595
|
-
if (view.status !== void 0) return `The site answered HTTP ${String(view.status)}; the ${view.mediaType} body is not shown.`;
|
|
1596
|
-
return "Not an HTML page; use browser_download to save it into the workspace.";
|
|
1597
|
-
}
|
|
1598
|
-
function versionOf$1(view) {
|
|
1599
|
-
const representation = view.representation;
|
|
1600
|
-
if (representation.kind === "agent-view") return representation.version;
|
|
1601
|
-
return representation.agentView?.version;
|
|
1602
|
-
}
|
|
1603
|
-
function digestOf$1(view) {
|
|
1604
|
-
if (view.representation.kind !== "agent-view") return void 0;
|
|
1605
|
-
return representationDigest(view.representation.text);
|
|
1606
|
-
}
|
|
1607
|
-
function idOf(view) {
|
|
1608
|
-
if (view.representation.kind !== "agent-view") return void 0;
|
|
1609
|
-
return agentViewId(view.representation.text, view.url);
|
|
1610
|
-
}
|
|
1611
|
-
function attr$1(name, value) {
|
|
1612
|
-
return ` ${name}="${escapeAttr(value)}"`;
|
|
1613
|
-
}
|
|
1614
|
-
function formatSeconds(seconds) {
|
|
1615
|
-
const rounded = Math.round(seconds * 10) / 10;
|
|
1616
|
-
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
1617
|
-
}
|
|
1618
|
-
//#endregion
|
|
1619
2367
|
//#region lib/types/presence/plan.js
|
|
1620
2368
|
const SOURCE_ORDER = [
|
|
1621
2369
|
"person via sidebar",
|
|
@@ -2083,10 +2831,14 @@ async function loadShown(input) {
|
|
|
2083
2831
|
base.mediaType = input.main.mediaType;
|
|
2084
2832
|
if (isTextual(input.main.mediaType)) base.body = input.main.body;
|
|
2085
2833
|
}
|
|
2086
|
-
if (input.main !== void 0 && isAgentViewContentType(input.main.mediaType, input.main.body) && samePage(input.main.finalUrl, input.pageUrl))
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2834
|
+
if (input.main !== void 0 && isAgentViewContentType(input.main.mediaType, input.main.body) && samePage(input.main.finalUrl, input.pageUrl)) {
|
|
2835
|
+
const shown = fromDocument({
|
|
2836
|
+
...base,
|
|
2837
|
+
href: input.pageUrl
|
|
2838
|
+
}, input.main.body, input.pageUrl);
|
|
2839
|
+
if (input.main.etag !== void 0) shown.etag = input.main.etag;
|
|
2840
|
+
return shown;
|
|
2841
|
+
}
|
|
2090
2842
|
if (input.relocate) {
|
|
2091
2843
|
const html = await input.fetch(input.pageUrl, "text/html");
|
|
2092
2844
|
if (html.kind === "error") return failed(base, html.reason);
|
|
@@ -2225,27 +2977,43 @@ async function launchBrowser(options) {
|
|
|
2225
2977
|
//#endregion
|
|
2226
2978
|
//#region lib/types/service/control.js
|
|
2227
2979
|
/**
|
|
2228
|
-
* Who holds
|
|
2229
|
-
*
|
|
2230
|
-
* A
|
|
2980
|
+
* Who holds each tab of one session. Person input outside a service dispatch
|
|
2981
|
+
* moves that tab to the person and aborts the action still waiting on it.
|
|
2982
|
+
* A handover that names no tab holds every tab until hand-back.
|
|
2231
2983
|
*/
|
|
2232
2984
|
var Control = class {
|
|
2233
2985
|
who = /* @__PURE__ */ new Map();
|
|
2234
2986
|
holding = /* @__PURE__ */ new Set();
|
|
2987
|
+
/** A handover that named no tab: every tab of the session is the person's. */
|
|
2988
|
+
blanket = /* @__PURE__ */ new Set();
|
|
2235
2989
|
inflight = /* @__PURE__ */ new Map();
|
|
2236
2990
|
operations = /* @__PURE__ */ new Map();
|
|
2237
2991
|
tabs = /* @__PURE__ */ new Map();
|
|
2238
2992
|
since = /* @__PURE__ */ new Map();
|
|
2239
2993
|
named = /* @__PURE__ */ new Map();
|
|
2994
|
+
/** Whether the person holds any tab, or a handover is waiting. */
|
|
2240
2995
|
of(owner) {
|
|
2241
2996
|
return this.who.get(owner) ?? "model";
|
|
2242
2997
|
}
|
|
2243
|
-
/**
|
|
2244
|
-
|
|
2998
|
+
/** The person holds this tab until they hand it back. */
|
|
2999
|
+
ofTab(owner, tab) {
|
|
3000
|
+
if (this.blanket.has(owner)) return "person";
|
|
3001
|
+
if (this.tabs.get(owner)?.has(tab) === true) return "person";
|
|
3002
|
+
return "model";
|
|
3003
|
+
}
|
|
3004
|
+
/**
|
|
3005
|
+
* Abort this waiting action when the caller aborts, or when the person
|
|
3006
|
+
* takes the tab it is running on. A call with no tab (the request channel)
|
|
3007
|
+
* ends only when its caller aborts.
|
|
3008
|
+
*/
|
|
3009
|
+
link(owner, caller, tab) {
|
|
2245
3010
|
const ctrl = new AbortController();
|
|
2246
|
-
const
|
|
3011
|
+
const byTab = this.inflight.get(owner) ?? /* @__PURE__ */ new Map();
|
|
3012
|
+
const key = tab ?? "";
|
|
3013
|
+
const set = byTab.get(key) ?? /* @__PURE__ */ new Set();
|
|
2247
3014
|
set.add(ctrl);
|
|
2248
|
-
|
|
3015
|
+
byTab.set(key, set);
|
|
3016
|
+
this.inflight.set(owner, byTab);
|
|
2249
3017
|
const onCaller = () => ctrl.abort();
|
|
2250
3018
|
if (caller?.aborted === true) ctrl.abort();
|
|
2251
3019
|
else caller?.addEventListener("abort", onCaller, { once: true });
|
|
@@ -2272,7 +3040,7 @@ var Control = class {
|
|
|
2272
3040
|
}
|
|
2273
3041
|
this.operations.set(owner, (this.operations.get(owner) ?? 0) + 1);
|
|
2274
3042
|
if (tab !== void 0) this.tabs.get(owner)?.add(tab);
|
|
2275
|
-
|
|
3043
|
+
this.abort(owner, tab);
|
|
2276
3044
|
}
|
|
2277
3045
|
/** A handover is waiting for the person to hand the browser back. */
|
|
2278
3046
|
holds(owner) {
|
|
@@ -2285,8 +3053,12 @@ var Control = class {
|
|
|
2285
3053
|
/** The model hands the browser to the person and waits for the card. */
|
|
2286
3054
|
handover(owner, tab) {
|
|
2287
3055
|
this.holding.add(owner);
|
|
2288
|
-
if (tab
|
|
2289
|
-
|
|
3056
|
+
if (tab === void 0) this.blanket.add(owner);
|
|
3057
|
+
else this.named.set(owner, tab);
|
|
3058
|
+
if (this.who.get(owner) === "person") {
|
|
3059
|
+
if (tab !== void 0) this.tabs.get(owner)?.add(tab);
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
2290
3062
|
this.who.set(owner, "person");
|
|
2291
3063
|
this.since.set(owner, (/* @__PURE__ */ new Date()).toISOString());
|
|
2292
3064
|
this.operations.set(owner, 0);
|
|
@@ -2320,13 +3092,23 @@ var Control = class {
|
|
|
2320
3092
|
}
|
|
2321
3093
|
/** The Session is gone. Its in-flight calls are aborted. */
|
|
2322
3094
|
forget(owner) {
|
|
2323
|
-
|
|
3095
|
+
this.abort(owner, void 0);
|
|
2324
3096
|
this.inflight.delete(owner);
|
|
2325
3097
|
this.clear(owner);
|
|
2326
3098
|
this.who.delete(owner);
|
|
2327
3099
|
}
|
|
3100
|
+
abort(owner, tab) {
|
|
3101
|
+
const byTab = this.inflight.get(owner);
|
|
3102
|
+
if (byTab === void 0) return;
|
|
3103
|
+
if (tab === void 0) {
|
|
3104
|
+
for (const set of byTab.values()) for (const ctrl of set) ctrl.abort();
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
for (const ctrl of byTab.get(tab) ?? []) ctrl.abort();
|
|
3108
|
+
}
|
|
2328
3109
|
clear(owner) {
|
|
2329
3110
|
this.holding.delete(owner);
|
|
3111
|
+
this.blanket.delete(owner);
|
|
2330
3112
|
this.who.set(owner, "model");
|
|
2331
3113
|
this.operations.delete(owner);
|
|
2332
3114
|
this.tabs.delete(owner);
|
|
@@ -2441,75 +3223,65 @@ const KEPT_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
2441
3223
|
"default",
|
|
2442
3224
|
"examples"
|
|
2443
3225
|
]);
|
|
2444
|
-
async function runInvoke(host,
|
|
2445
|
-
const
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
if (
|
|
2450
|
-
const
|
|
2451
|
-
const origin = view.url === "" ? pageUrl : view.url;
|
|
2452
|
-
const plan = planAction(doc, action, origin, call);
|
|
3226
|
+
async function runInvoke(host, call) {
|
|
3227
|
+
const origin = call.shown.url;
|
|
3228
|
+
const read = readAgentView(call.shown.text, origin);
|
|
3229
|
+
if (!read.ok) throw new MhBrowserError("NOT_DECLARED", read.reason);
|
|
3230
|
+
const action = read.doc.actions[call.name];
|
|
3231
|
+
if (action === void 0) throw new MhBrowserError("NOT_DECLARED", `no declared action named ${call.name}`);
|
|
3232
|
+
const plan = planAction(read.doc, action, origin, call);
|
|
2453
3233
|
if (plan.method === "GET") {
|
|
2454
|
-
const landed = await host
|
|
3234
|
+
const landed = await land(host, withQuery(plan.url, plan.query));
|
|
2455
3235
|
return {
|
|
2456
3236
|
schemaRejected: plan.schemaRejected,
|
|
2457
3237
|
page: landed
|
|
2458
3238
|
};
|
|
2459
|
-
}
|
|
2460
|
-
const target = plan.method === "DELETE" ? withQuery(plan.url, plan.query) : plan.url;
|
|
2461
|
-
const sent = await host.send(await requestOf(host, plan, target));
|
|
2462
|
-
if (sent.kind === "lost")
|
|
2463
|
-
|
|
2464
|
-
response: {
|
|
2465
|
-
name: call.name,
|
|
2466
|
-
lost: true
|
|
2467
|
-
},
|
|
2468
|
-
page: view
|
|
2469
|
-
};
|
|
2470
|
-
const response = actionResponse(call.name, sent, plan.url);
|
|
2471
|
-
if (response.location !== void 0 && sameOrigin(response.location, origin)) {
|
|
2472
|
-
const landed = await host.navigate(view.tab, response.location);
|
|
3239
|
+
}
|
|
3240
|
+
const target = plan.method === "DELETE" ? withQuery(plan.url, plan.query) : plan.url;
|
|
3241
|
+
const sent = await host.send(await requestOf(host, plan, target));
|
|
3242
|
+
if (sent.kind === "lost") {
|
|
3243
|
+
const page = header(await landAfterSend(host, origin));
|
|
2473
3244
|
return {
|
|
2474
3245
|
schemaRejected: plan.schemaRejected,
|
|
2475
|
-
response
|
|
2476
|
-
|
|
3246
|
+
response: {
|
|
3247
|
+
name: call.name,
|
|
3248
|
+
lost: true
|
|
3249
|
+
},
|
|
3250
|
+
page
|
|
2477
3251
|
};
|
|
2478
3252
|
}
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
response,
|
|
2482
|
-
page: view
|
|
2483
|
-
};
|
|
2484
|
-
const reloaded = await host.reload(pageUrl, view.tab);
|
|
3253
|
+
const response = actionResponse(call.name, sent, plan.url);
|
|
3254
|
+
const follows = response.location !== void 0 && sameOrigin(response.location, origin) ? response.location : origin;
|
|
2485
3255
|
return {
|
|
2486
3256
|
schemaRejected: plan.schemaRejected,
|
|
2487
3257
|
response,
|
|
2488
|
-
page:
|
|
3258
|
+
page: header(await landAfterSend(host, follows))
|
|
2489
3259
|
};
|
|
2490
3260
|
}
|
|
2491
|
-
/**
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
const
|
|
2505
|
-
|
|
2506
|
-
return
|
|
2507
|
-
}
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
3261
|
+
/** The request already went out, so a failed landing is reported on the page and the response is kept. */
|
|
3262
|
+
async function landAfterSend(host, url) {
|
|
3263
|
+
try {
|
|
3264
|
+
return await land(host, url);
|
|
3265
|
+
} catch (error) {
|
|
3266
|
+
return {
|
|
3267
|
+
url,
|
|
3268
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
/** Open the address, then reload any other idle tab still showing it. */
|
|
3273
|
+
async function land(host, url) {
|
|
3274
|
+
const page = await host.open(url);
|
|
3275
|
+
await host.refreshIdle(url, "tab" in page ? page.tab : void 0);
|
|
3276
|
+
return page;
|
|
3277
|
+
}
|
|
3278
|
+
/** A non-GET action shows the landing page's header and does not deliver it. */
|
|
3279
|
+
function header(page) {
|
|
3280
|
+
if (!("representation" in page)) return page;
|
|
3281
|
+
return {
|
|
3282
|
+
...page,
|
|
3283
|
+
headerOnly: true
|
|
3284
|
+
};
|
|
2513
3285
|
}
|
|
2514
3286
|
function planAction(doc, action, pageUrl, call) {
|
|
2515
3287
|
const input = objectInput(call.input);
|
|
@@ -2731,84 +3503,386 @@ function isRecord(value) {
|
|
|
2731
3503
|
}
|
|
2732
3504
|
//#endregion
|
|
2733
3505
|
//#region lib/types/service/actions.js
|
|
2734
|
-
|
|
2735
|
-
|
|
3506
|
+
const REF = /^(?:f\d+)?e\d+$/;
|
|
3507
|
+
const TIMEOUT = 1e4;
|
|
3508
|
+
/** A page operation returns a snapshot after it. The rest return one sentence. */
|
|
3509
|
+
function showsSnapshot(op) {
|
|
3510
|
+
if (op.kind === "press_key") return op.key === "Enter";
|
|
3511
|
+
if (op.kind === "type") return op.slowly === true || op.submit === true;
|
|
3512
|
+
return op.kind === "click" || op.kind === "hover" || op.kind === "drag" || op.kind === "select_option" || op.kind === "file_upload" || op.kind === "navigate_back" || op.kind === "wait_for" || op.kind === "snapshot" || op.kind === "take_screenshot";
|
|
3513
|
+
}
|
|
3514
|
+
/** The sentence for an operation whose result has no snapshot. */
|
|
3515
|
+
function summaryOf(op) {
|
|
3516
|
+
if (op.kind === "fill_form") return `Filled ${String(op.fields.length)} fields.`;
|
|
3517
|
+
if (op.kind === "type") return `Typed into ${op.element ?? op.target}.`;
|
|
3518
|
+
if (op.kind === "press_key") return `Pressed ${op.key}.`;
|
|
3519
|
+
if (op.kind === "handle_dialog") return op.accept ? "Dialog accepted." : "Dialog dismissed.";
|
|
3520
|
+
}
|
|
3521
|
+
/** Run one snapshot action. Dialog, wait, upload, find, and evaluate are handled by the service. */
|
|
3522
|
+
async function runAction(page, op, snapshot, signal) {
|
|
2736
3523
|
let navigated = false;
|
|
2737
3524
|
const onNav = (frame) => {
|
|
2738
3525
|
if (frame.parentFrame() === null) navigated = true;
|
|
2739
3526
|
};
|
|
2740
3527
|
page.on("framenavigated", onNav);
|
|
2741
3528
|
try {
|
|
2742
|
-
await dispatch(page,
|
|
3529
|
+
await dispatch(page, op, snapshot, signal);
|
|
2743
3530
|
return navigated;
|
|
3531
|
+
} catch (error) {
|
|
3532
|
+
if (error instanceof MhBrowserError) throw error;
|
|
3533
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3534
|
+
const first = message.split("\n")[0] ?? message;
|
|
3535
|
+
if (first.includes("strict mode violation")) throw new MhBrowserError("INVALID_ARGS", first);
|
|
3536
|
+
throw error;
|
|
2744
3537
|
} finally {
|
|
2745
3538
|
page.off("framenavigated", onNav);
|
|
2746
3539
|
}
|
|
2747
3540
|
}
|
|
2748
|
-
async function
|
|
3541
|
+
async function locate(page, target, snapshot) {
|
|
3542
|
+
if (REF.test(target)) {
|
|
3543
|
+
if (!snapshot.includes(`[ref=${target}]`)) throw new MhBrowserError("STALE_REF", `Ref ${target} not found in the current page snapshot. Try capturing new snapshot.`);
|
|
3544
|
+
return page.getByRef(target);
|
|
3545
|
+
}
|
|
3546
|
+
const found = await page.$(target);
|
|
3547
|
+
if (found === null) throw new MhBrowserError("STALE_REF", `"${target}" does not match any elements.`);
|
|
3548
|
+
await found.dispose();
|
|
3549
|
+
return page.locator(target);
|
|
3550
|
+
}
|
|
3551
|
+
async function dispatch(page, op, snapshot, signal) {
|
|
2749
3552
|
const options = signal === void 0 ? {} : { signal };
|
|
2750
|
-
if (
|
|
2751
|
-
await page.
|
|
2752
|
-
button:
|
|
2753
|
-
clickCount:
|
|
2754
|
-
...
|
|
2755
|
-
timeout:
|
|
3553
|
+
if (op.kind === "click") {
|
|
3554
|
+
await (await locate(page, op.target, snapshot)).click({
|
|
3555
|
+
button: op.button ?? "left",
|
|
3556
|
+
clickCount: op.doubleClick === true ? 2 : 1,
|
|
3557
|
+
...op.modifiers !== void 0 ? { modifiers: op.modifiers } : {},
|
|
3558
|
+
timeout: TIMEOUT,
|
|
2756
3559
|
...options
|
|
2757
3560
|
});
|
|
2758
3561
|
return;
|
|
2759
3562
|
}
|
|
2760
|
-
if (
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
timeout: 1e4,
|
|
3563
|
+
if (op.kind === "hover") {
|
|
3564
|
+
await (await locate(page, op.target, snapshot)).hover({
|
|
3565
|
+
timeout: TIMEOUT,
|
|
2764
3566
|
...options
|
|
2765
3567
|
});
|
|
2766
3568
|
return;
|
|
2767
3569
|
}
|
|
2768
|
-
if (
|
|
2769
|
-
if (
|
|
2770
|
-
|
|
2771
|
-
timeout:
|
|
3570
|
+
if (op.kind === "select_option") {
|
|
3571
|
+
if (op.values.length === 0) throw new MhBrowserError("INVALID_ARGS", "select_option needs at least one value");
|
|
3572
|
+
await (await locate(page, op.target, snapshot)).selectOption(op.values, {
|
|
3573
|
+
timeout: TIMEOUT,
|
|
2772
3574
|
...options
|
|
2773
3575
|
});
|
|
2774
3576
|
return;
|
|
2775
3577
|
}
|
|
2776
|
-
if (
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
3578
|
+
if (op.kind === "drag") {
|
|
3579
|
+
const start = await locate(page, op.startTarget, snapshot);
|
|
3580
|
+
const end = await locate(page, op.endTarget, snapshot);
|
|
3581
|
+
await start.dragTo(end, {
|
|
3582
|
+
timeout: TIMEOUT,
|
|
3583
|
+
...options
|
|
3584
|
+
});
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
if (op.kind === "type") {
|
|
3588
|
+
const locator = await locate(page, op.target, snapshot);
|
|
3589
|
+
if (op.slowly === true) await locator.pressSequentially(op.text, {
|
|
3590
|
+
timeout: TIMEOUT,
|
|
3591
|
+
...options
|
|
3592
|
+
});
|
|
3593
|
+
else await locator.fill(op.text, {
|
|
3594
|
+
timeout: TIMEOUT,
|
|
3595
|
+
...options
|
|
3596
|
+
});
|
|
3597
|
+
if (op.submit === true) await locator.press("Enter", {
|
|
3598
|
+
timeout: TIMEOUT,
|
|
3599
|
+
...options
|
|
3600
|
+
});
|
|
3601
|
+
return;
|
|
3602
|
+
}
|
|
3603
|
+
if (op.kind === "fill_form") {
|
|
3604
|
+
if (op.fields.length === 0) throw new MhBrowserError("INVALID_ARGS", "fill_form needs at least one field");
|
|
3605
|
+
for (const field of op.fields) {
|
|
3606
|
+
const locator = await locate(page, field.target, snapshot);
|
|
3607
|
+
if (field.type === "checkbox" || field.type === "radio") {
|
|
3608
|
+
if (field.value !== "true" && field.value !== "false") throw new MhBrowserError("INVALID_ARGS", "a checkbox value is true or false");
|
|
3609
|
+
await locator.setChecked(field.value === "true", {
|
|
3610
|
+
timeout: TIMEOUT,
|
|
2787
3611
|
...options
|
|
2788
3612
|
});
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
3613
|
+
} else if (field.type === "combobox") await locator.selectOption({ label: field.value }, {
|
|
3614
|
+
timeout: TIMEOUT,
|
|
3615
|
+
...options
|
|
3616
|
+
});
|
|
3617
|
+
else await locator.fill(field.value, {
|
|
3618
|
+
timeout: TIMEOUT,
|
|
2792
3619
|
...options
|
|
2793
3620
|
});
|
|
2794
3621
|
}
|
|
2795
3622
|
return;
|
|
2796
3623
|
}
|
|
2797
|
-
if (
|
|
2798
|
-
|
|
2799
|
-
await page.getByRef(action.ref).setInputFiles(action.files, {
|
|
2800
|
-
timeout: 1e4,
|
|
2801
|
-
...options
|
|
2802
|
-
});
|
|
3624
|
+
if (op.kind === "press_key") {
|
|
3625
|
+
await page.keyboard.press(op.key);
|
|
2803
3626
|
return;
|
|
2804
3627
|
}
|
|
2805
|
-
await page.goBack({
|
|
3628
|
+
if (op.kind === "navigate_back") await page.goBack({
|
|
2806
3629
|
waitUntil: "commit",
|
|
2807
|
-
timeout:
|
|
3630
|
+
timeout: TIMEOUT,
|
|
2808
3631
|
...options
|
|
2809
3632
|
});
|
|
2810
3633
|
}
|
|
2811
3634
|
//#endregion
|
|
3635
|
+
//#region lib/types/service/evaluate.js
|
|
3636
|
+
/** How long requests the function started are still collected after it returns. */
|
|
3637
|
+
const SETTLE_MS$1 = 50;
|
|
3638
|
+
/**
|
|
3639
|
+
* Run the expression in the page and collect write requests.
|
|
3640
|
+
* A request is from this call when its initiator stack, including parents,
|
|
3641
|
+
* has a frame from the sourceURL marker. Anything else is contemporaneous.
|
|
3642
|
+
* GET, HEAD, and OPTIONS are not writes. A dialog, a stop, or a navigation
|
|
3643
|
+
* ends the wait; requests already sent are reported either way.
|
|
3644
|
+
*/
|
|
3645
|
+
async function evaluateMarked(page, source, target, callId, signal, dialog) {
|
|
3646
|
+
const marker = `mh-evaluate-${callId}-${randomUUID().slice(0, 8)}`;
|
|
3647
|
+
const client = await page.context().newCDPSession(page);
|
|
3648
|
+
const notes = /* @__PURE__ */ new Map();
|
|
3649
|
+
const onRequest = (event) => {
|
|
3650
|
+
const method = event.request?.method?.toUpperCase();
|
|
3651
|
+
const url = event.request?.url;
|
|
3652
|
+
if (method === void 0 || url === void 0) return;
|
|
3653
|
+
if (method === "GET" || method === "HEAD" || method === "OPTIONS") return;
|
|
3654
|
+
const id = event.requestId ?? `${String(notes.size)}`;
|
|
3655
|
+
if (notes.has(id)) return;
|
|
3656
|
+
notes.set(id, {
|
|
3657
|
+
method,
|
|
3658
|
+
origin: originOf(url),
|
|
3659
|
+
fromCall: stackHas(event.initiator?.stack, marker)
|
|
3660
|
+
});
|
|
3661
|
+
};
|
|
3662
|
+
const writes = () => countWrites([...notes.values()]);
|
|
3663
|
+
client.on("Network.requestWillBeSent", onRequest);
|
|
3664
|
+
const stop = whenAborted(signal);
|
|
3665
|
+
let dialogOpen = false;
|
|
3666
|
+
try {
|
|
3667
|
+
await client.send("Network.enable");
|
|
3668
|
+
await client.send("Debugger.enable");
|
|
3669
|
+
await client.send("Debugger.setSkipAllPauses", { skip: true });
|
|
3670
|
+
await client.send("Debugger.setAsyncCallStackDepth", { maxDepth: 32 });
|
|
3671
|
+
const evaluation = (target === void 0 ? page.evaluate(runSource, {
|
|
3672
|
+
expr: source,
|
|
3673
|
+
marker
|
|
3674
|
+
}) : target.evaluate(runSourceOn, {
|
|
3675
|
+
expr: source,
|
|
3676
|
+
marker
|
|
3677
|
+
})).then((raw) => ({
|
|
3678
|
+
kind: "value",
|
|
3679
|
+
raw
|
|
3680
|
+
}), (error) => ({
|
|
3681
|
+
kind: "error",
|
|
3682
|
+
error
|
|
3683
|
+
}));
|
|
3684
|
+
const winner = await Promise.race([
|
|
3685
|
+
evaluation,
|
|
3686
|
+
stop.promise.then(() => ({ kind: "stopped" })),
|
|
3687
|
+
dialog.then((shown) => ({
|
|
3688
|
+
kind: "dialog",
|
|
3689
|
+
dialog: shown
|
|
3690
|
+
}))
|
|
3691
|
+
]);
|
|
3692
|
+
if (winner.kind === "stopped") return {
|
|
3693
|
+
kind: "stopped",
|
|
3694
|
+
writes: writes()
|
|
3695
|
+
};
|
|
3696
|
+
if (winner.kind === "dialog") {
|
|
3697
|
+
dialogOpen = true;
|
|
3698
|
+
return {
|
|
3699
|
+
kind: "dialog",
|
|
3700
|
+
dialog: winner.dialog,
|
|
3701
|
+
writes: writes()
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
if (winner.kind === "error") {
|
|
3705
|
+
if (!contextGone(winner.error)) throw winner.error;
|
|
3706
|
+
await delay(SETTLE_MS$1);
|
|
3707
|
+
return {
|
|
3708
|
+
kind: "navigated",
|
|
3709
|
+
writes: writes()
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
await delay(SETTLE_MS$1);
|
|
3713
|
+
return {
|
|
3714
|
+
kind: "value",
|
|
3715
|
+
value: JSON.stringify(winner.raw, null, 2) ?? "undefined",
|
|
3716
|
+
writes: writes()
|
|
3717
|
+
};
|
|
3718
|
+
} finally {
|
|
3719
|
+
stop.dispose();
|
|
3720
|
+
client.off("Network.requestWillBeSent", onRequest);
|
|
3721
|
+
const detached = client.detach().catch(() => void 0);
|
|
3722
|
+
if (!dialogOpen) await detached;
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
/** One line per group: `POST https://site × 2 (this call)`. */
|
|
3726
|
+
function formatWrites(writes) {
|
|
3727
|
+
return writes.map((note) => `${note.method} ${note.origin} × ${String(note.count)} (${note.fromCall ? "this call" : "contemporaneous"})`).join("\n");
|
|
3728
|
+
}
|
|
3729
|
+
function countWrites(notes) {
|
|
3730
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3731
|
+
for (const note of notes) {
|
|
3732
|
+
const key = `${note.method} ${note.origin} ${note.fromCall ? "call" : "page"}`;
|
|
3733
|
+
const found = counts.get(key);
|
|
3734
|
+
if (found === void 0) counts.set(key, {
|
|
3735
|
+
...note,
|
|
3736
|
+
count: 1
|
|
3737
|
+
});
|
|
3738
|
+
else found.count += 1;
|
|
3739
|
+
}
|
|
3740
|
+
return [...counts.values()];
|
|
3741
|
+
}
|
|
3742
|
+
function whenAborted(signal) {
|
|
3743
|
+
if (signal === void 0) return {
|
|
3744
|
+
promise: new Promise(() => void 0),
|
|
3745
|
+
dispose: () => {}
|
|
3746
|
+
};
|
|
3747
|
+
if (signal.aborted) return {
|
|
3748
|
+
promise: Promise.resolve(),
|
|
3749
|
+
dispose: () => {}
|
|
3750
|
+
};
|
|
3751
|
+
let onAbort = () => {};
|
|
3752
|
+
return {
|
|
3753
|
+
promise: new Promise((resolve) => {
|
|
3754
|
+
onAbort = () => resolve();
|
|
3755
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3756
|
+
}),
|
|
3757
|
+
dispose: () => signal.removeEventListener("abort", onAbort)
|
|
3758
|
+
};
|
|
3759
|
+
}
|
|
3760
|
+
function contextGone(error) {
|
|
3761
|
+
return (error instanceof Error ? error.message : String(error)).includes("Execution context was destroyed");
|
|
3762
|
+
}
|
|
3763
|
+
function delay(ms) {
|
|
3764
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3765
|
+
}
|
|
3766
|
+
function runSource(input) {
|
|
3767
|
+
const value = eval(`(${input.expr})\n//# sourceURL=${input.marker}`);
|
|
3768
|
+
return typeof value === "function" ? value() : value;
|
|
3769
|
+
}
|
|
3770
|
+
function runSourceOn(element, input) {
|
|
3771
|
+
const value = eval(`(${input.expr})\n//# sourceURL=${input.marker}`);
|
|
3772
|
+
return typeof value === "function" ? value(element) : value;
|
|
3773
|
+
}
|
|
3774
|
+
function stackHas(stack, marker) {
|
|
3775
|
+
for (let cursor = stack; cursor !== void 0; cursor = cursor.parent) for (const frame of cursor.callFrames ?? []) if (frame.url === marker) return true;
|
|
3776
|
+
return false;
|
|
3777
|
+
}
|
|
3778
|
+
function originOf(url) {
|
|
3779
|
+
try {
|
|
3780
|
+
return new URL(url).origin;
|
|
3781
|
+
} catch {
|
|
3782
|
+
return url;
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
//#endregion
|
|
3786
|
+
//#region lib/types/service/find.js
|
|
3787
|
+
const CONTEXT_LINES = 3;
|
|
3788
|
+
/** Search one snapshot. The header and the miss sentence are what the model reads. */
|
|
3789
|
+
function findInSnapshot(snapshot, query) {
|
|
3790
|
+
if (query.text === void 0 && query.regex === void 0 || query.text !== void 0 && query.regex !== void 0) throw new MhBrowserError("INVALID_ARGS", query.text !== void 0 && query.regex !== void 0 ? "Provide only one of \"text\" or \"regex\", not both." : "Provide either \"text\" or \"regex\" to search for.");
|
|
3791
|
+
if (query.maxResults !== void 0 && (!Number.isInteger(query.maxResults) || query.maxResults <= 0)) throw new MhBrowserError("INVALID_ARGS", "\"maxResults\" must be a positive integer.");
|
|
3792
|
+
const matched = matcher(query.text, query.regex);
|
|
3793
|
+
const lines = snapshot.split("\n");
|
|
3794
|
+
const indents = lines.map(indentOf);
|
|
3795
|
+
const hits = [];
|
|
3796
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
3797
|
+
const line = lines[index];
|
|
3798
|
+
if (line !== void 0 && matched.test(line)) hits.push(index);
|
|
3799
|
+
}
|
|
3800
|
+
const label = matched.label;
|
|
3801
|
+
if (hits.length === 0) return `No matches found for ${label}.`;
|
|
3802
|
+
const shown = query.maxResults === void 0 ? hits : hits.slice(0, query.maxResults);
|
|
3803
|
+
const windows = mergeWindows(shown, lines.length);
|
|
3804
|
+
const onPath = /* @__PURE__ */ new Set();
|
|
3805
|
+
for (const hit of shown) {
|
|
3806
|
+
onPath.add(hit);
|
|
3807
|
+
for (const ancestor of ancestorIndices(lines, indents, hit)) onPath.add(ancestor);
|
|
3808
|
+
}
|
|
3809
|
+
const snippets = windows.map((window) => {
|
|
3810
|
+
const indices = ancestorIndices(lines, indents, window.start);
|
|
3811
|
+
for (let index = window.start; index <= window.end; index += 1) indices.push(index);
|
|
3812
|
+
const out = [];
|
|
3813
|
+
for (let index = 0; index < indices.length; index += 1) {
|
|
3814
|
+
const lineNo = indices[index];
|
|
3815
|
+
const previous = indices[index - 1];
|
|
3816
|
+
if (lineNo !== void 0 && previous !== void 0 && lineNo > previous + 1 && !onPath.has(lineNo) && !onPath.has(previous)) out.push(`${" ".repeat(indents[lineNo] ?? 0)}...`);
|
|
3817
|
+
const line = lineNo === void 0 ? void 0 : lines[lineNo];
|
|
3818
|
+
if (line !== void 0) out.push(line);
|
|
3819
|
+
}
|
|
3820
|
+
return out.join("\n");
|
|
3821
|
+
});
|
|
3822
|
+
return `${shown.length < hits.length ? `Found ${String(hits.length)} matches for ${label} (showing first ${String(shown.length)}):` : `Found ${String(hits.length)} matches for ${label}:`}\n\n${snippets.join("\n\n----\n\n")}`;
|
|
3823
|
+
}
|
|
3824
|
+
function matcher(text, regex) {
|
|
3825
|
+
if (regex !== void 0) {
|
|
3826
|
+
let pattern;
|
|
3827
|
+
try {
|
|
3828
|
+
pattern = compileRegex(regex);
|
|
3829
|
+
} catch {
|
|
3830
|
+
throw new MhBrowserError("INVALID_ARGS", "Invalid regular expression");
|
|
3831
|
+
}
|
|
3832
|
+
return {
|
|
3833
|
+
label: String(pattern),
|
|
3834
|
+
test(line) {
|
|
3835
|
+
pattern.lastIndex = 0;
|
|
3836
|
+
return pattern.test(line);
|
|
3837
|
+
}
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3840
|
+
const needle = (text ?? "").toLowerCase();
|
|
3841
|
+
return {
|
|
3842
|
+
label: `"${text ?? ""}"`,
|
|
3843
|
+
test(line) {
|
|
3844
|
+
return line.toLowerCase().includes(needle);
|
|
3845
|
+
}
|
|
3846
|
+
};
|
|
3847
|
+
}
|
|
3848
|
+
function compileRegex(source) {
|
|
3849
|
+
const literal = /^\/(.*)\/([a-z]*)$/.exec(source);
|
|
3850
|
+
const pattern = literal?.[1] ?? source;
|
|
3851
|
+
const flags = literal?.[2]?.replace(/g/g, "") ?? "";
|
|
3852
|
+
return new RegExp(pattern, flags);
|
|
3853
|
+
}
|
|
3854
|
+
function indentOf(line) {
|
|
3855
|
+
return line.length - line.trimStart().length;
|
|
3856
|
+
}
|
|
3857
|
+
function ancestorIndices(lines, indents, index) {
|
|
3858
|
+
const found = [];
|
|
3859
|
+
let indent = indents[index] ?? 0;
|
|
3860
|
+
for (let cursor = index - 1; cursor >= 0 && indent > 0; cursor -= 1) {
|
|
3861
|
+
const line = lines[cursor];
|
|
3862
|
+
if (line === void 0 || line.trim() === "") continue;
|
|
3863
|
+
const next = indents[cursor] ?? 0;
|
|
3864
|
+
if (next < indent) {
|
|
3865
|
+
found.push(cursor);
|
|
3866
|
+
indent = next;
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
return found.reverse();
|
|
3870
|
+
}
|
|
3871
|
+
function mergeWindows(hits, length) {
|
|
3872
|
+
const windows = [];
|
|
3873
|
+
for (const hit of hits) {
|
|
3874
|
+
const start = Math.max(0, hit - CONTEXT_LINES);
|
|
3875
|
+
const end = Math.min(length - 1, hit + CONTEXT_LINES);
|
|
3876
|
+
const last = windows[windows.length - 1];
|
|
3877
|
+
if (last !== void 0 && start <= last.end + 1) last.end = Math.max(last.end, end);
|
|
3878
|
+
else windows.push({
|
|
3879
|
+
start,
|
|
3880
|
+
end
|
|
3881
|
+
});
|
|
3882
|
+
}
|
|
3883
|
+
return windows;
|
|
3884
|
+
}
|
|
3885
|
+
//#endregion
|
|
2812
3886
|
//#region lib/types/service/waits.js
|
|
2813
3887
|
const LOAD_MS = 5e3;
|
|
2814
3888
|
const WAIT_CAP_MS = 3e4;
|
|
@@ -3309,6 +4383,7 @@ var Windows = class {
|
|
|
3309
4383
|
tab.inputHeld = false;
|
|
3310
4384
|
tab.mayHaveChanged = false;
|
|
3311
4385
|
tab.occupied = tab.dialogHeld || tab.handoverHeld;
|
|
4386
|
+
tab.fileChooser = void 0;
|
|
3312
4387
|
}
|
|
3313
4388
|
markMayHaveChanged(tab) {
|
|
3314
4389
|
tab.mayHaveChanged = true;
|
|
@@ -3442,11 +4517,16 @@ var Windows = class {
|
|
|
3442
4517
|
await this.arm(page, owner, tab.id);
|
|
3443
4518
|
return tab;
|
|
3444
4519
|
}
|
|
3445
|
-
/**
|
|
4520
|
+
/**
|
|
4521
|
+
* Close one tab. `note` is listed in the next `<tabs>` block. A tab the
|
|
4522
|
+
* model was never shown (an Agent View discovered on a new tab) omits it.
|
|
4523
|
+
*/
|
|
3446
4524
|
async closeTab(tab, note) {
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
4525
|
+
if (note !== void 0) {
|
|
4526
|
+
const notes = this.closedNotes.get(tab.owner) ?? [];
|
|
4527
|
+
notes.push(note);
|
|
4528
|
+
this.closedNotes.set(tab.owner, notes);
|
|
4529
|
+
}
|
|
3450
4530
|
await this.closePage(tab);
|
|
3451
4531
|
}
|
|
3452
4532
|
async closeOwner(owner) {
|
|
@@ -3845,6 +4925,7 @@ function idleTab() {
|
|
|
3845
4925
|
handoverHeld: false,
|
|
3846
4926
|
inPresence: false,
|
|
3847
4927
|
mayHaveChanged: false,
|
|
4928
|
+
fileChooser: void 0,
|
|
3848
4929
|
pendingDialog: void 0,
|
|
3849
4930
|
dialogWaiters: [],
|
|
3850
4931
|
pendingDownload: void 0,
|
|
@@ -3919,6 +5000,8 @@ const RELAUNCH_LIMIT = 3;
|
|
|
3919
5000
|
const RELAUNCH_WINDOW_MS = 6e4;
|
|
3920
5001
|
const STOP_WAIT_MS = 5e3;
|
|
3921
5002
|
const WAITING_STOPPED = "stopped while waiting";
|
|
5003
|
+
const STOPPED = Symbol("stopped");
|
|
5004
|
+
const NAVIGATED_DURING_EVALUATE = "The page navigated before the function returned.";
|
|
3922
5005
|
const MAY_HAVE_TAKEN_EFFECT = "taken over by the person; the action may have partly taken effect";
|
|
3923
5006
|
const NO_WORKSPACE_REASON = "this session has no working directory";
|
|
3924
5007
|
/**
|
|
@@ -3970,6 +5053,10 @@ let MhBrowserService = (() => {
|
|
|
3970
5053
|
workspaces = /* @__PURE__ */ new Map();
|
|
3971
5054
|
control = new Control();
|
|
3972
5055
|
personTouch = /* @__PURE__ */ new Set();
|
|
5056
|
+
/** Origins that have served a readable Agent View. Memory only; a restart clears it. */
|
|
5057
|
+
agentViewOrigins = /* @__PURE__ */ new Set();
|
|
5058
|
+
/** The last Agent View read for a page address, so a later read can send If-None-Match. */
|
|
5059
|
+
documents = /* @__PURE__ */ new Map();
|
|
3973
5060
|
constructor(ctx, config) {
|
|
3974
5061
|
super(ctx, "mhBrowser");
|
|
3975
5062
|
this.maxBytes = config.presence.maxBytes;
|
|
@@ -4002,28 +5089,30 @@ let MhBrowserService = (() => {
|
|
|
4002
5089
|
await this.stop();
|
|
4003
5090
|
});
|
|
4004
5091
|
}
|
|
5092
|
+
/** Remember an origin whose pages offer Agent Views. A missing page does not remove it. */
|
|
5093
|
+
declareAgentViewOrigin(origin) {
|
|
5094
|
+
try {
|
|
5095
|
+
const parsed = new URL(origin);
|
|
5096
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
|
|
5097
|
+
this.agentViewOrigins.add(parsed.origin);
|
|
5098
|
+
} catch {}
|
|
5099
|
+
}
|
|
4005
5100
|
async open(owner, items, signal) {
|
|
4006
5101
|
return this.exclusive(owner, async () => {
|
|
4007
5102
|
await this.ready;
|
|
4008
|
-
this.refuseIfPerson(owner);
|
|
4009
|
-
const linked = this.control.link(owner, signal);
|
|
4010
5103
|
const views = [];
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
continue;
|
|
4019
|
-
}
|
|
4020
|
-
views.push(await this.openOne(owner, item.url, item.waitFor, linked.signal));
|
|
5104
|
+
for (const item of items) {
|
|
5105
|
+
if (signal?.aborted) {
|
|
5106
|
+
views.push({
|
|
5107
|
+
url: item.url,
|
|
5108
|
+
reason: WAITING_STOPPED
|
|
5109
|
+
});
|
|
5110
|
+
continue;
|
|
4021
5111
|
}
|
|
4022
|
-
await this.
|
|
4023
|
-
return views;
|
|
4024
|
-
} finally {
|
|
4025
|
-
linked.release();
|
|
5112
|
+
views.push(await this.openAddress(owner, item, signal));
|
|
4026
5113
|
}
|
|
5114
|
+
await this.enforceCap(owner);
|
|
5115
|
+
return views;
|
|
4027
5116
|
});
|
|
4028
5117
|
}
|
|
4029
5118
|
async view(owner, tab, signal) {
|
|
@@ -4137,7 +5226,8 @@ let MhBrowserService = (() => {
|
|
|
4137
5226
|
if (opts?.ifNoneMatch !== void 0) headers["if-none-match"] = opts.ifNoneMatch;
|
|
4138
5227
|
const response = await context.request.get(url, {
|
|
4139
5228
|
headers,
|
|
4140
|
-
failOnStatusCode: false
|
|
5229
|
+
failOnStatusCode: false,
|
|
5230
|
+
...opts?.signal !== void 0 ? { signal: opts.signal } : {}
|
|
4141
5231
|
});
|
|
4142
5232
|
const headerMap = response.headers();
|
|
4143
5233
|
const body = {
|
|
@@ -4181,24 +5271,32 @@ let MhBrowserService = (() => {
|
|
|
4181
5271
|
page: (owner, tab) => this.windows.get(owner, tab)?.page
|
|
4182
5272
|
};
|
|
4183
5273
|
}
|
|
4184
|
-
async act(owner, tab,
|
|
5274
|
+
async act(owner, tab, op, signal) {
|
|
4185
5275
|
return this.exclusive(owner, async () => {
|
|
4186
5276
|
await this.ready;
|
|
4187
|
-
this.refuseIfPerson(owner);
|
|
5277
|
+
this.refuseIfPerson(owner, tab);
|
|
4188
5278
|
const live = this.windows.get(owner, tab);
|
|
4189
5279
|
if (live === void 0) throw new MhBrowserError(UNKNOWN_TAB, UNKNOWN_TAB_REASON);
|
|
4190
5280
|
if (signal?.aborted) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
|
|
4191
|
-
const linked = this.control.link(owner, signal);
|
|
5281
|
+
const linked = this.control.link(owner, signal, tab);
|
|
4192
5282
|
try {
|
|
4193
|
-
if (live.dialogHeld &&
|
|
4194
|
-
if (
|
|
4195
|
-
|
|
4196
|
-
|
|
5283
|
+
if (live.dialogHeld && op.kind !== "handle_dialog") throw new MhBrowserError("MODAL_STATE", "a dialog is open; use browser_handle_dialog", await this.blockedView(live));
|
|
5284
|
+
if (live.fileChooser !== void 0 && op.kind !== "file_upload") {
|
|
5285
|
+
const blocked = await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" });
|
|
5286
|
+
blocked.dialog = {
|
|
5287
|
+
type: "filechooser",
|
|
5288
|
+
message: live.fileChooser.isMultiple() ? "choose files (several allowed) with browser_file_upload" : "choose one file with browser_file_upload"
|
|
5289
|
+
};
|
|
5290
|
+
throw new MhBrowserError("MODAL_STATE", "a file chooser is open; use browser_file_upload", blocked);
|
|
5291
|
+
}
|
|
5292
|
+
const outcome = await this.perform(live, op, linked.signal);
|
|
5293
|
+
if (op.kind === "fill_form" || op.kind === "select_option" || op.kind === "type" || op.kind === "file_upload") {
|
|
4197
5294
|
if (outcome.navigated) this.windows.onNavigated(live);
|
|
4198
5295
|
else this.windows.holdInput(live);
|
|
4199
5296
|
} else if (outcome.navigated) this.windows.onNavigated(live);
|
|
4200
5297
|
this.windows.touch(live);
|
|
4201
5298
|
const shown = outcome.navigated ? await this.discover(live, live.page.url(), null) : await this.refreshSnapshot(live);
|
|
5299
|
+
const scoped = op.kind === "snapshot" && (op.target !== void 0 || op.depth !== void 0 || op.boxes === true) ? await this.scopedSnapshot(live, op) : void 0;
|
|
4202
5300
|
this.shown.set(live.id, shown);
|
|
4203
5301
|
this.windows.noteUrl(live, live.page.url());
|
|
4204
5302
|
const view = await this.pageFromShown(live, shown, {
|
|
@@ -4207,9 +5305,23 @@ let MhBrowserService = (() => {
|
|
|
4207
5305
|
...outcome.incomplete !== void 0 ? { incomplete: outcome.incomplete } : {},
|
|
4208
5306
|
...outcome.waited !== void 0 ? { waited: outcome.waited } : {}
|
|
4209
5307
|
});
|
|
5308
|
+
if (scoped !== void 0 && view.representation.kind === "snapshot") view.representation = {
|
|
5309
|
+
...view.representation,
|
|
5310
|
+
text: scoped
|
|
5311
|
+
};
|
|
5312
|
+
if (!showsSnapshot(op)) {
|
|
5313
|
+
const summary = outcome.summary ?? summaryOf(op);
|
|
5314
|
+
if (summary !== void 0) view.summary = summary;
|
|
5315
|
+
if (outcome.detail !== void 0 && outcome.detail !== "") view.detail = outcome.detail;
|
|
5316
|
+
}
|
|
5317
|
+
if (op.kind === "take_screenshot") view.summary = "";
|
|
5318
|
+
if (outcome.shot !== void 0) view.shot = outcome.shot;
|
|
5319
|
+
if (outcome.writes !== void 0) view.writes = outcome.writes;
|
|
4210
5320
|
return this.attachDownload(live, view, false);
|
|
4211
5321
|
} catch (error) {
|
|
4212
|
-
if (
|
|
5322
|
+
if (error instanceof MhBrowserError && error.code === "TAKEN_OVER") throw error;
|
|
5323
|
+
if (this.control.ofTab(owner, tab) === "person") throw await this.takenOver(live);
|
|
5324
|
+
if (error instanceof MhBrowserError && error.code === "STALE_REF" && error.page === void 0) throw new MhBrowserError(error.code, error.message, await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" }));
|
|
4213
5325
|
throw error;
|
|
4214
5326
|
} finally {
|
|
4215
5327
|
linked.release();
|
|
@@ -4242,50 +5354,30 @@ let MhBrowserService = (() => {
|
|
|
4242
5354
|
}
|
|
4243
5355
|
});
|
|
4244
5356
|
}
|
|
4245
|
-
invoke(owner,
|
|
5357
|
+
invoke(owner, _page, call, opts) {
|
|
4246
5358
|
return this.exclusive(owner, async () => {
|
|
4247
5359
|
await this.ready;
|
|
4248
|
-
this.refuseIfPerson(owner);
|
|
4249
5360
|
if (opts?.signal?.aborted) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
|
|
4250
|
-
const
|
|
4251
|
-
const
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
})
|
|
4262
|
-
}, page, call);
|
|
4263
|
-
await this.enforceCap(owner);
|
|
4264
|
-
return sent;
|
|
4265
|
-
} catch (error) {
|
|
4266
|
-
if (this.control.of(owner) === "person") throw new MhBrowserError("TAKEN_OVER", MAY_HAVE_TAKEN_EFFECT);
|
|
4267
|
-
throw error;
|
|
4268
|
-
} finally {
|
|
4269
|
-
linked.release();
|
|
4270
|
-
}
|
|
5361
|
+
const signal = opts?.signal;
|
|
5362
|
+
const sent = await runInvoke({
|
|
5363
|
+
open: (url) => this.openAddress(owner, { url }, signal),
|
|
5364
|
+
refreshIdle: (url, except) => this.refreshIdle(owner, url, except, signal),
|
|
5365
|
+
send: (request) => this.sendAction(request, signal),
|
|
5366
|
+
readFile: opts?.readFile ?? (async () => {
|
|
5367
|
+
throw new MhBrowserError("NO_WORKSPACE", NO_WORKSPACE_REASON);
|
|
5368
|
+
})
|
|
5369
|
+
}, call);
|
|
5370
|
+
await this.enforceCap(owner);
|
|
5371
|
+
return sent;
|
|
4271
5372
|
});
|
|
4272
5373
|
}
|
|
4273
5374
|
/**
|
|
4274
5375
|
* Not queued: it opens no tab, and a long body must not hold up the
|
|
4275
5376
|
* Session's other calls. `root` is the directory the session may write.
|
|
4276
5377
|
*/
|
|
4277
|
-
async download(
|
|
5378
|
+
async download(_owner, url, root, signal) {
|
|
4278
5379
|
await this.ready;
|
|
4279
|
-
this.
|
|
4280
|
-
const linked = this.control.link(owner, signal);
|
|
4281
|
-
try {
|
|
4282
|
-
return await this.saveResponse(url, root, linked.signal);
|
|
4283
|
-
} catch (error) {
|
|
4284
|
-
if (this.control.of(owner) === "person") throw new MhBrowserError("TAKEN_OVER", MAY_HAVE_TAKEN_EFFECT);
|
|
4285
|
-
throw error;
|
|
4286
|
-
} finally {
|
|
4287
|
-
linked.release();
|
|
4288
|
-
}
|
|
5380
|
+
return this.saveResponse(url, root, signal);
|
|
4289
5381
|
}
|
|
4290
5382
|
controlOf(owner) {
|
|
4291
5383
|
return this.control.of(owner);
|
|
@@ -4376,9 +5468,185 @@ let MhBrowserService = (() => {
|
|
|
4376
5468
|
urls: this.presenceUrls(owner, taken.tabs)
|
|
4377
5469
|
};
|
|
4378
5470
|
}
|
|
4379
|
-
|
|
5471
|
+
/**
|
|
5472
|
+
* Read one address. A known origin is tried on the request channel first.
|
|
5473
|
+
* A readable Agent View found on a tab this call opened is returned without
|
|
5474
|
+
* the tab, and that tab is not listed as closed.
|
|
5475
|
+
*/
|
|
5476
|
+
async openAddress(owner, item, signal) {
|
|
5477
|
+
const wantsTab = owner === "person" || item.snapshot === true || item.waitFor !== void 0;
|
|
5478
|
+
if (!wantsTab && this.knownOrigin(item.url)) {
|
|
5479
|
+
const via = await this.readKnown(owner, item.url, signal);
|
|
5480
|
+
if (via === STOPPED) return {
|
|
5481
|
+
url: item.url,
|
|
5482
|
+
reason: WAITING_STOPPED
|
|
5483
|
+
};
|
|
5484
|
+
if (via !== void 0) return via;
|
|
5485
|
+
}
|
|
5486
|
+
const opened = await this.openOne(owner, item.url, item.waitFor, signal, item.snapshot === true);
|
|
5487
|
+
this.keepAgentView(opened);
|
|
5488
|
+
if (!wantsTab && opened.fresh && this.closable(owner, opened.live) && "representation" in opened.view && opened.view.representation.kind === "agent-view") {
|
|
5489
|
+
await this.windows.closeTab(opened.live);
|
|
5490
|
+
const view = { ...opened.view };
|
|
5491
|
+
delete view.tab;
|
|
5492
|
+
return view;
|
|
5493
|
+
}
|
|
5494
|
+
return opened.view;
|
|
5495
|
+
}
|
|
5496
|
+
/** A tab this call opened can be closed again unless the person took it or it holds input. */
|
|
5497
|
+
closable(owner, live) {
|
|
5498
|
+
return this.control.ofTab(owner, live.id) !== "person" && !live.occupied;
|
|
5499
|
+
}
|
|
5500
|
+
knownOrigin(url) {
|
|
5501
|
+
try {
|
|
5502
|
+
return this.agentViewOrigins.has(new URL(url).origin);
|
|
5503
|
+
} catch {
|
|
5504
|
+
return false;
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
5507
|
+
rememberOrigin(url) {
|
|
5508
|
+
try {
|
|
5509
|
+
this.agentViewOrigins.add(new URL(url).origin);
|
|
5510
|
+
} catch {}
|
|
5511
|
+
}
|
|
5512
|
+
/** A readable Agent View puts its origin on the list and keeps the document for the next read. */
|
|
5513
|
+
keepAgentView(opened) {
|
|
5514
|
+
if (!("representation" in opened.view)) return;
|
|
5515
|
+
const representation = opened.view.representation;
|
|
5516
|
+
if (!(representation.kind === "agent-view" || representation.kind === "snapshot" && representation.agentView !== void 0)) return;
|
|
5517
|
+
this.rememberOrigin(opened.view.url);
|
|
5518
|
+
const shown = this.shown.get(opened.live.id);
|
|
5519
|
+
if (shown?.kind === "agent-view") this.documents.set(canonical(shown.pageUrl), shown);
|
|
5520
|
+
}
|
|
5521
|
+
/**
|
|
5522
|
+
* Read a known origin without a tab. `undefined` means this page has to be
|
|
5523
|
+
* opened in a tab: nothing declared, the declaration could not be read, or
|
|
5524
|
+
* the request failed. The origin stays on the list either way. A stop
|
|
5525
|
+
* ends the read without falling back to a tab.
|
|
5526
|
+
*/
|
|
5527
|
+
async readKnown(owner, url, signal) {
|
|
5528
|
+
const key = canonical(url);
|
|
5529
|
+
const previous = this.documents.get(key);
|
|
5530
|
+
const servedAtPage = previous?.kind === "agent-view" && previous.href !== void 0 && canonical(previous.href) === canonical(previous.pageUrl);
|
|
5531
|
+
let response;
|
|
5532
|
+
try {
|
|
5533
|
+
const got = await this.fetchSameOrigin(url, PAGE_ACCEPT, servedAtPage ? previous.etag : void 0, signal);
|
|
5534
|
+
if (signal?.aborted === true) return STOPPED;
|
|
5535
|
+
if (got === void 0) return void 0;
|
|
5536
|
+
response = got;
|
|
5537
|
+
} catch {
|
|
5538
|
+
return signal?.aborted === true ? STOPPED : void 0;
|
|
5539
|
+
}
|
|
5540
|
+
if (response.status === 304 && previous?.kind === "agent-view") return this.pageOfShown(owner, previous);
|
|
5541
|
+
const text = new TextDecoder().decode(response.body);
|
|
5542
|
+
const pageUrl = canonical(response.finalUrl === "" ? url : response.finalUrl);
|
|
5543
|
+
const cached = this.documents.get(pageUrl);
|
|
5544
|
+
const shown = await loadShown({
|
|
5545
|
+
pageUrl,
|
|
5546
|
+
...optionalFragment(response.finalUrl === "" ? url : response.finalUrl),
|
|
5547
|
+
snapshot: "",
|
|
5548
|
+
main: {
|
|
5549
|
+
status: response.status,
|
|
5550
|
+
...response.mediaType !== void 0 ? { mediaType: response.mediaType } : {},
|
|
5551
|
+
finalUrl: response.finalUrl === "" ? url : response.finalUrl,
|
|
5552
|
+
...response.link !== void 0 ? { link: response.link } : {},
|
|
5553
|
+
...response.etag !== void 0 ? { etag: response.etag } : {},
|
|
5554
|
+
body: text
|
|
5555
|
+
},
|
|
5556
|
+
domLinks: linksInHtml(text, response.finalUrl === "" ? url : response.finalUrl),
|
|
5557
|
+
...cached !== void 0 ? { previous: cached } : {},
|
|
5558
|
+
relocate: false,
|
|
5559
|
+
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch, signal)
|
|
5560
|
+
});
|
|
5561
|
+
if (stopped(signal)) return STOPPED;
|
|
5562
|
+
if (shown.kind !== "agent-view") return void 0;
|
|
5563
|
+
this.documents.set(pageUrl, shown);
|
|
5564
|
+
this.rememberOrigin(pageUrl);
|
|
5565
|
+
return this.pageOfShown(owner, shown);
|
|
5566
|
+
}
|
|
5567
|
+
/** One page from a document, with no tab. */
|
|
5568
|
+
pageOfShown(owner, shown) {
|
|
5569
|
+
const view = {
|
|
5570
|
+
url: shown.pageUrl,
|
|
5571
|
+
title: "",
|
|
5572
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5573
|
+
representation: shown.kind === "agent-view" ? {
|
|
5574
|
+
kind: "agent-view",
|
|
5575
|
+
text: shown.text,
|
|
5576
|
+
...shown.version !== void 0 ? { version: shown.version } : {},
|
|
5577
|
+
...shown.href !== void 0 ? { href: shown.href } : {}
|
|
5578
|
+
} : {
|
|
5579
|
+
kind: "snapshot",
|
|
5580
|
+
text: shown.snapshot,
|
|
5581
|
+
...shown.agentViewError !== void 0 ? { agentViewError: shown.agentViewError } : {}
|
|
5582
|
+
},
|
|
5583
|
+
inPresence: this.book.isShown(owner, canonical(shown.pageUrl))
|
|
5584
|
+
};
|
|
5585
|
+
if (shown.fragment !== void 0) view.fragment = shown.fragment;
|
|
5586
|
+
if (shown.status !== void 0) view.status = shown.status;
|
|
5587
|
+
if (shown.mediaType !== void 0) view.mediaType = shown.mediaType;
|
|
5588
|
+
if (shown.body !== void 0) view.body = shown.body;
|
|
5589
|
+
return view;
|
|
5590
|
+
}
|
|
5591
|
+
/**
|
|
5592
|
+
* GET that follows same-origin redirects only. `undefined` when it cannot.
|
|
5593
|
+
* A body that is neither text nor an Agent View is not read: the page then
|
|
5594
|
+
* goes to a tab, which loads it anyway.
|
|
5595
|
+
*/
|
|
5596
|
+
async fetchSameOrigin(url, accept, ifNoneMatch, signal) {
|
|
5597
|
+
const context = this.context;
|
|
5598
|
+
if (context === void 0) return void 0;
|
|
5599
|
+
let current = url;
|
|
5600
|
+
let origin;
|
|
5601
|
+
try {
|
|
5602
|
+
origin = new URL(url).origin;
|
|
5603
|
+
} catch {
|
|
5604
|
+
return;
|
|
5605
|
+
}
|
|
5606
|
+
for (let hop = 0; hop < 10; hop += 1) {
|
|
5607
|
+
const headers = { accept };
|
|
5608
|
+
if (ifNoneMatch !== void 0 && hop === 0) headers["if-none-match"] = ifNoneMatch;
|
|
5609
|
+
const response = await context.request.get(current, {
|
|
5610
|
+
headers,
|
|
5611
|
+
maxRedirects: 0,
|
|
5612
|
+
failOnStatusCode: false,
|
|
5613
|
+
...signal !== void 0 ? { signal } : {}
|
|
5614
|
+
});
|
|
5615
|
+
const status = response.status();
|
|
5616
|
+
const location = response.headers().location;
|
|
5617
|
+
if (status >= 300 && status < 400 && location !== void 0 && location !== "") {
|
|
5618
|
+
let next;
|
|
5619
|
+
try {
|
|
5620
|
+
next = new URL(location, current);
|
|
5621
|
+
} catch {
|
|
5622
|
+
return;
|
|
5623
|
+
}
|
|
5624
|
+
if (next.origin !== origin) return void 0;
|
|
5625
|
+
current = next.href;
|
|
5626
|
+
continue;
|
|
5627
|
+
}
|
|
5628
|
+
const headerMap = response.headers();
|
|
5629
|
+
const mediaType = headerMap["content-type"];
|
|
5630
|
+
const body = {
|
|
5631
|
+
kind: "response",
|
|
5632
|
+
finalUrl: current,
|
|
5633
|
+
status,
|
|
5634
|
+
body: readsAsText(mediaType) ? new Uint8Array(await response.body()) : /* @__PURE__ */ new Uint8Array()
|
|
5635
|
+
};
|
|
5636
|
+
const etag = headerMap.etag;
|
|
5637
|
+
const link = headerMap.link;
|
|
5638
|
+
if (mediaType !== void 0) body.mediaType = mediaType;
|
|
5639
|
+
if (etag !== void 0) body.etag = etag;
|
|
5640
|
+
if (link !== void 0) body.link = link;
|
|
5641
|
+
return body;
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
async openOne(owner, url, waitFor, signal, snapshot = false) {
|
|
4380
5645
|
assertWaitFor(waitFor);
|
|
4381
|
-
const
|
|
5646
|
+
const reused = this.idleTabOn(owner, url);
|
|
5647
|
+
const live = reused ?? await this.windows.openBlank(owner);
|
|
5648
|
+
const fresh = reused === void 0;
|
|
5649
|
+
const linked = this.control.link(owner, signal, live.id);
|
|
4382
5650
|
const nextDialog = this.windows.nextDialog(live);
|
|
4383
5651
|
try {
|
|
4384
5652
|
let seenDialog;
|
|
@@ -4388,7 +5656,7 @@ let MhBrowserService = (() => {
|
|
|
4388
5656
|
});
|
|
4389
5657
|
let navigationError;
|
|
4390
5658
|
let response = null;
|
|
4391
|
-
const raced = this.goto(live.page, url, signal).then((value) => {
|
|
5659
|
+
const raced = this.goto(live.page, url, linked.signal).then((value) => {
|
|
4392
5660
|
response = value;
|
|
4393
5661
|
return "navigated";
|
|
4394
5662
|
}, (error) => {
|
|
@@ -4402,14 +5670,22 @@ let MhBrowserService = (() => {
|
|
|
4402
5670
|
const view = await this.pageFromShown(live, this.shown.get(live.id));
|
|
4403
5671
|
if (view.url === "" || view.url === "about:blank") view.url = canonical(url);
|
|
4404
5672
|
this.applyDocumentType(live, view);
|
|
4405
|
-
return
|
|
5673
|
+
return {
|
|
5674
|
+
view: await this.attachDownload(live, view, true),
|
|
5675
|
+
fresh,
|
|
5676
|
+
live
|
|
5677
|
+
};
|
|
4406
5678
|
}
|
|
4407
5679
|
throw navigationError;
|
|
4408
5680
|
}
|
|
4409
|
-
if (signal
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
5681
|
+
if (linked.signal.aborted) return {
|
|
5682
|
+
view: {
|
|
5683
|
+
tab: live.id,
|
|
5684
|
+
url,
|
|
5685
|
+
reason: WAITING_STOPPED
|
|
5686
|
+
},
|
|
5687
|
+
fresh,
|
|
5688
|
+
live
|
|
4413
5689
|
};
|
|
4414
5690
|
const current = live.page.url();
|
|
4415
5691
|
const address = current === "about:blank" ? url : current;
|
|
@@ -4425,11 +5701,11 @@ let MhBrowserService = (() => {
|
|
|
4425
5701
|
load: false,
|
|
4426
5702
|
pendingRequests: 0,
|
|
4427
5703
|
dialog
|
|
4428
|
-
} : await waitForLoad(live.page, signal);
|
|
5704
|
+
} : await waitForLoad(live.page, linked.signal);
|
|
4429
5705
|
if (load.dialog !== void 0) this.windows.holdDialog(live);
|
|
4430
5706
|
let waited;
|
|
4431
5707
|
if (waitFor !== void 0 && load.dialog === void 0) {
|
|
4432
|
-
const text = await waitForText(live.page, waitFor, signal);
|
|
5708
|
+
const text = await waitForText(live.page, waitFor, linked.signal);
|
|
4433
5709
|
waited = {
|
|
4434
5710
|
met: text.met,
|
|
4435
5711
|
seconds: text.seconds
|
|
@@ -4446,6 +5722,7 @@ let MhBrowserService = (() => {
|
|
|
4446
5722
|
}) : await this.discover(live, address, response);
|
|
4447
5723
|
this.shown.set(live.id, shown);
|
|
4448
5724
|
const view = await this.pageFromShown(live, shown, {
|
|
5725
|
+
...snapshot ? { surface: "snapshot" } : {},
|
|
4449
5726
|
...load.load ? {} : { incomplete: {
|
|
4450
5727
|
load: load.load,
|
|
4451
5728
|
pendingRequests: load.pendingRequests
|
|
@@ -4453,26 +5730,51 @@ let MhBrowserService = (() => {
|
|
|
4453
5730
|
...load.dialog !== void 0 ? { dialog: load.dialog } : {},
|
|
4454
5731
|
...waited !== void 0 ? { waited } : {}
|
|
4455
5732
|
});
|
|
4456
|
-
return
|
|
5733
|
+
return {
|
|
5734
|
+
view: await this.attachDownload(live, view, false),
|
|
5735
|
+
fresh,
|
|
5736
|
+
live
|
|
5737
|
+
};
|
|
4457
5738
|
} catch (error) {
|
|
4458
5739
|
nextDialog.cancel();
|
|
4459
|
-
if (this.control.
|
|
4460
|
-
if (signal
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
5740
|
+
if (this.control.ofTab(owner, live.id) === "person") throw await this.takenOver(live);
|
|
5741
|
+
if (linked.signal.aborted || error instanceof MhBrowserError && error.code === "INTERRUPTED") return {
|
|
5742
|
+
view: {
|
|
5743
|
+
tab: live.id,
|
|
5744
|
+
url,
|
|
5745
|
+
reason: WAITING_STOPPED
|
|
5746
|
+
},
|
|
5747
|
+
fresh,
|
|
5748
|
+
live
|
|
4464
5749
|
};
|
|
4465
5750
|
return {
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
5751
|
+
view: {
|
|
5752
|
+
tab: live.id,
|
|
5753
|
+
url,
|
|
5754
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
5755
|
+
},
|
|
5756
|
+
fresh,
|
|
5757
|
+
live
|
|
4469
5758
|
};
|
|
5759
|
+
} finally {
|
|
5760
|
+
linked.release();
|
|
4470
5761
|
}
|
|
4471
5762
|
}
|
|
4472
|
-
async perform(live,
|
|
4473
|
-
if (
|
|
4474
|
-
if (
|
|
4475
|
-
|
|
5763
|
+
async perform(live, op, signal) {
|
|
5764
|
+
if (op.kind === "snapshot") return { navigated: false };
|
|
5765
|
+
if (op.kind === "find") return {
|
|
5766
|
+
navigated: false,
|
|
5767
|
+
summary: findInSnapshot(await this.aria(live), op)
|
|
5768
|
+
};
|
|
5769
|
+
if (op.kind === "evaluate") {
|
|
5770
|
+
const snapshot = this.shown.get(live.id)?.snapshot ?? "";
|
|
5771
|
+
const target = op.target === void 0 ? void 0 : await locate(live.page, op.target, snapshot);
|
|
5772
|
+
return this.evaluateOn(live, op, target, signal);
|
|
5773
|
+
}
|
|
5774
|
+
if (op.kind === "take_screenshot") return this.shoot(live, op, signal);
|
|
5775
|
+
if (op.kind === "file_upload") return this.uploadHeld(live, op, signal);
|
|
5776
|
+
if (op.kind === "wait_for") {
|
|
5777
|
+
const waitFor = waitFrom(op);
|
|
4476
5778
|
assertWaitFor(waitFor);
|
|
4477
5779
|
const text = await waitForText(live.page, waitFor, signal);
|
|
4478
5780
|
if (text.dialog !== void 0) this.windows.holdDialog(live);
|
|
@@ -4485,11 +5787,11 @@ let MhBrowserService = (() => {
|
|
|
4485
5787
|
...text.dialog !== void 0 ? { dialog: text.dialog } : {}
|
|
4486
5788
|
};
|
|
4487
5789
|
}
|
|
4488
|
-
if (
|
|
5790
|
+
if (op.kind === "handle_dialog") {
|
|
4489
5791
|
const dialog = live.pendingDialog;
|
|
4490
5792
|
if (dialog === void 0) throw new MhBrowserError("INVALID_ARGS", "this page has no dialog");
|
|
4491
5793
|
const before = live.page.url();
|
|
4492
|
-
if (
|
|
5794
|
+
if (op.accept) await (op.promptText !== void 0 ? dialog.accept(op.promptText) : dialog.accept());
|
|
4493
5795
|
else await dialog.dismiss();
|
|
4494
5796
|
this.windows.clearDialog(live);
|
|
4495
5797
|
const navigated = live.page.url() !== before;
|
|
@@ -4501,9 +5803,10 @@ let MhBrowserService = (() => {
|
|
|
4501
5803
|
const requests = trackActionRequests(live.page);
|
|
4502
5804
|
const nextDialog = this.windows.nextDialog(live);
|
|
4503
5805
|
const chooser = watchFileChooser(live.page);
|
|
5806
|
+
const snapshot = this.shown.get(live.id)?.snapshot ?? "";
|
|
4504
5807
|
this.windows.beginDispatch(live.page);
|
|
4505
5808
|
try {
|
|
4506
|
-
const finished = runAction(live.page,
|
|
5809
|
+
const finished = runAction(live.page, op, snapshot, signal).then((navigated) => ({
|
|
4507
5810
|
kind: "done",
|
|
4508
5811
|
navigated
|
|
4509
5812
|
}));
|
|
@@ -4511,9 +5814,9 @@ let MhBrowserService = (() => {
|
|
|
4511
5814
|
kind: "dialog",
|
|
4512
5815
|
dialog
|
|
4513
5816
|
}));
|
|
4514
|
-
const chose = chooser.opened.then((
|
|
5817
|
+
const chose = chooser.opened.then((opened) => ({
|
|
4515
5818
|
kind: "chooser",
|
|
4516
|
-
|
|
5819
|
+
opened
|
|
4517
5820
|
}));
|
|
4518
5821
|
const winner = await Promise.race([
|
|
4519
5822
|
finished,
|
|
@@ -4537,13 +5840,13 @@ let MhBrowserService = (() => {
|
|
|
4537
5840
|
finished.catch(() => void 0);
|
|
4538
5841
|
return {
|
|
4539
5842
|
navigated: false,
|
|
4540
|
-
dialog: this.chooserHeld(live, winner.
|
|
5843
|
+
dialog: this.chooserHeld(live, winner.opened)
|
|
4541
5844
|
};
|
|
4542
5845
|
}
|
|
4543
|
-
const settledLoad = await Promise.race([settleAction(live.page, winner.navigated, requests, signal), chooser.opened.then((
|
|
5846
|
+
const settledLoad = await Promise.race([settleAction(live.page, winner.navigated, requests, signal), chooser.opened.then((opened) => ({
|
|
4544
5847
|
load: true,
|
|
4545
5848
|
pendingRequests: 0,
|
|
4546
|
-
dialog: this.chooserHeld(live,
|
|
5849
|
+
dialog: this.chooserHeld(live, opened)
|
|
4547
5850
|
}))]);
|
|
4548
5851
|
return {
|
|
4549
5852
|
navigated: winner.navigated,
|
|
@@ -4555,12 +5858,121 @@ let MhBrowserService = (() => {
|
|
|
4555
5858
|
chooser.stop();
|
|
4556
5859
|
}
|
|
4557
5860
|
}
|
|
5861
|
+
/**
|
|
5862
|
+
* `browser_evaluate`. A dialog the function opens is held like any other; a
|
|
5863
|
+
* stop or a takeover ends the wait and still reports the writes already sent.
|
|
5864
|
+
*/
|
|
5865
|
+
async evaluateOn(live, op, target, signal) {
|
|
5866
|
+
const nextDialog = this.windows.nextDialog(live);
|
|
5867
|
+
let navigated = false;
|
|
5868
|
+
const onNav = (frame) => {
|
|
5869
|
+
if (frame.parentFrame() === null) navigated = true;
|
|
5870
|
+
};
|
|
5871
|
+
live.page.on("framenavigated", onNav);
|
|
5872
|
+
try {
|
|
5873
|
+
const evaluated = await evaluateMarked(live.page, op.function, target, op.callId, signal, nextDialog.dialog);
|
|
5874
|
+
const writes = formatWrites(evaluated.writes);
|
|
5875
|
+
const counted = evaluated.writes.length === 0 ? {} : { writes: evaluated.writes };
|
|
5876
|
+
if (evaluated.kind === "stopped") {
|
|
5877
|
+
const note = writes === "" ? "" : `\n${writes}`;
|
|
5878
|
+
if (this.control.ofTab(live.owner, live.id) === "person") throw await this.takenOver(live, note);
|
|
5879
|
+
throw new MhBrowserError(INTERRUPTED, `${WAITING_STOPPED}${note}`);
|
|
5880
|
+
}
|
|
5881
|
+
if (evaluated.kind === "dialog") {
|
|
5882
|
+
this.windows.holdDialog(live, evaluated.dialog);
|
|
5883
|
+
return {
|
|
5884
|
+
navigated: false,
|
|
5885
|
+
dialog: {
|
|
5886
|
+
type: evaluated.dialog.type(),
|
|
5887
|
+
message: evaluated.dialog.message()
|
|
5888
|
+
},
|
|
5889
|
+
summary: "",
|
|
5890
|
+
detail: writes,
|
|
5891
|
+
...counted
|
|
5892
|
+
};
|
|
5893
|
+
}
|
|
5894
|
+
if (evaluated.kind === "navigated") return {
|
|
5895
|
+
navigated: true,
|
|
5896
|
+
summary: NAVIGATED_DURING_EVALUATE,
|
|
5897
|
+
detail: writes,
|
|
5898
|
+
...counted
|
|
5899
|
+
};
|
|
5900
|
+
return {
|
|
5901
|
+
navigated,
|
|
5902
|
+
summary: evaluated.value,
|
|
5903
|
+
detail: writes,
|
|
5904
|
+
...counted
|
|
5905
|
+
};
|
|
5906
|
+
} finally {
|
|
5907
|
+
live.page.off("framenavigated", onNav);
|
|
5908
|
+
nextDialog.cancel();
|
|
5909
|
+
}
|
|
5910
|
+
}
|
|
5911
|
+
async uploadHeld(live, op, signal) {
|
|
5912
|
+
const chooser = live.fileChooser;
|
|
5913
|
+
if (chooser === void 0) throw new MhBrowserError("INVALID_ARGS", "No file chooser visible");
|
|
5914
|
+
const paths = op.paths;
|
|
5915
|
+
if (paths === void 0 || paths.length === 0) {
|
|
5916
|
+
live.fileChooser = void 0;
|
|
5917
|
+
return { navigated: false };
|
|
5918
|
+
}
|
|
5919
|
+
const requests = trackActionRequests(live.page);
|
|
5920
|
+
let navigated = false;
|
|
5921
|
+
const onNav = (frame) => {
|
|
5922
|
+
if (frame.parentFrame() === null) navigated = true;
|
|
5923
|
+
};
|
|
5924
|
+
live.page.on("framenavigated", onNav);
|
|
5925
|
+
this.windows.beginDispatch(live.page);
|
|
5926
|
+
try {
|
|
5927
|
+
await chooser.setFiles(paths, signal === void 0 ? {} : { signal });
|
|
5928
|
+
live.fileChooser = void 0;
|
|
5929
|
+
await settleAction(live.page, navigated, requests, signal);
|
|
5930
|
+
return { navigated };
|
|
5931
|
+
} finally {
|
|
5932
|
+
live.page.off("framenavigated", onNav);
|
|
5933
|
+
this.windows.endDispatch(live.page);
|
|
5934
|
+
requests.stop();
|
|
5935
|
+
}
|
|
5936
|
+
}
|
|
5937
|
+
async shoot(live, op, signal) {
|
|
5938
|
+
if (op.fullPage === true && op.target !== void 0) throw new MhBrowserError("INVALID_ARGS", "fullPage cannot be used with element screenshots.");
|
|
5939
|
+
const type = op.type ?? "png";
|
|
5940
|
+
const snapshot = this.shown.get(live.id)?.snapshot ?? "";
|
|
5941
|
+
const locator = op.target === void 0 ? void 0 : await locate(live.page, op.target, snapshot);
|
|
5942
|
+
const options = {
|
|
5943
|
+
type,
|
|
5944
|
+
...type === "jpeg" ? { quality: 90 } : {},
|
|
5945
|
+
...op.scale !== void 0 ? { scale: op.scale } : {},
|
|
5946
|
+
...op.fullPage === true ? { fullPage: true } : {},
|
|
5947
|
+
...signal !== void 0 ? { signal } : {}
|
|
5948
|
+
};
|
|
5949
|
+
const bytes = locator === void 0 ? await live.page.screenshot(options) : await locator.screenshot(options);
|
|
5950
|
+
return {
|
|
5951
|
+
navigated: false,
|
|
5952
|
+
shot: {
|
|
5953
|
+
mediaType: `image/${type}`,
|
|
5954
|
+
bytes: new Uint8Array(bytes)
|
|
5955
|
+
}
|
|
5956
|
+
};
|
|
5957
|
+
}
|
|
5958
|
+
/** A snapshot limited to a target, a depth, or boxes. Refs still resolve against the full one. */
|
|
5959
|
+
async scopedSnapshot(tab, op) {
|
|
5960
|
+
const options = {
|
|
5961
|
+
mode: "ai",
|
|
5962
|
+
...op.depth !== void 0 ? { depth: op.depth } : {},
|
|
5963
|
+
...op.boxes === true ? { boxes: true } : {}
|
|
5964
|
+
};
|
|
5965
|
+
const snapshot = this.shown.get(tab.id)?.snapshot ?? "";
|
|
5966
|
+
if (op.target !== void 0) return (await locate(tab.page, op.target, snapshot)).ariaSnapshot(options);
|
|
5967
|
+
return tab.page.ariaSnapshot(options);
|
|
5968
|
+
}
|
|
4558
5969
|
/** A file chooser the action opened. The tab stays in use until it navigates. */
|
|
4559
|
-
chooserHeld(live,
|
|
5970
|
+
chooserHeld(live, chooser) {
|
|
5971
|
+
live.fileChooser = chooser;
|
|
4560
5972
|
this.windows.holdInput(live);
|
|
4561
5973
|
return {
|
|
4562
5974
|
type: "filechooser",
|
|
4563
|
-
message:
|
|
5975
|
+
message: chooser.isMultiple() ? "choose files (several allowed) with browser_file_upload" : "choose one file with browser_file_upload"
|
|
4564
5976
|
};
|
|
4565
5977
|
}
|
|
4566
5978
|
async refreshSnapshot(tab) {
|
|
@@ -4616,10 +6028,11 @@ let MhBrowserService = (() => {
|
|
|
4616
6028
|
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch)
|
|
4617
6029
|
});
|
|
4618
6030
|
}
|
|
4619
|
-
fetchForRead(url, accept, ifNoneMatch) {
|
|
6031
|
+
fetchForRead(url, accept, ifNoneMatch, signal) {
|
|
4620
6032
|
return this.fetch(url, {
|
|
4621
6033
|
accept,
|
|
4622
|
-
...ifNoneMatch !== void 0 ? { ifNoneMatch } : {}
|
|
6034
|
+
...ifNoneMatch !== void 0 ? { ifNoneMatch } : {},
|
|
6035
|
+
...signal !== void 0 ? { signal } : {}
|
|
4623
6036
|
});
|
|
4624
6037
|
}
|
|
4625
6038
|
async aria(tab) {
|
|
@@ -4663,13 +6076,36 @@ let MhBrowserService = (() => {
|
|
|
4663
6076
|
if (extra?.waited !== void 0) view.waited = extra.waited;
|
|
4664
6077
|
return view;
|
|
4665
6078
|
}
|
|
4666
|
-
/**
|
|
6079
|
+
/**
|
|
6080
|
+
* Read one presence page. A known origin is read on the request channel,
|
|
6081
|
+
* with If-None-Match when the document was read before. Anything else uses
|
|
6082
|
+
* a tab already on the address, or opens one and closes it again.
|
|
6083
|
+
*/
|
|
4667
6084
|
async observePresence(owner, url, signal) {
|
|
6085
|
+
if (this.knownOrigin(url)) {
|
|
6086
|
+
const via = await this.readKnown(owner, url, signal);
|
|
6087
|
+
if (via === STOPPED) return observationOf(url, {
|
|
6088
|
+
url,
|
|
6089
|
+
reason: WAITING_STOPPED
|
|
6090
|
+
});
|
|
6091
|
+
if (via !== void 0) return observationOf(url, via);
|
|
6092
|
+
}
|
|
4668
6093
|
const existing = this.tabOn(owner, url);
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
6094
|
+
if (existing !== void 0) return observationOf(url, await this.refreshView(existing));
|
|
6095
|
+
const opened = await this.openOne(owner, url, void 0, signal, false);
|
|
6096
|
+
this.keepAgentView(opened);
|
|
6097
|
+
const view = opened.view;
|
|
6098
|
+
if (opened.fresh && "tab" in view && view.tab !== void 0) {
|
|
6099
|
+
const live = this.windows.get(owner, view.tab);
|
|
6100
|
+
if (live !== void 0 && this.closable(owner, live)) {
|
|
6101
|
+
const agent = "representation" in view && view.representation.kind === "agent-view";
|
|
6102
|
+
await this.windows.closeTab(live, agent ? void 0 : `${live.id} (idle)`);
|
|
6103
|
+
if (agent) {
|
|
6104
|
+
const shown = { ...view };
|
|
6105
|
+
delete shown.tab;
|
|
6106
|
+
return observationOf(url, shown);
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
4673
6109
|
}
|
|
4674
6110
|
return observationOf(url, view);
|
|
4675
6111
|
}
|
|
@@ -4694,17 +6130,12 @@ let MhBrowserService = (() => {
|
|
|
4694
6130
|
});
|
|
4695
6131
|
this.shown.set(live.id, shown);
|
|
4696
6132
|
this.windows.noteUrl(live, address);
|
|
4697
|
-
return this.pageFromShown(live, shown);
|
|
4698
|
-
}
|
|
4699
|
-
async idleView(owner, url) {
|
|
4700
|
-
const live = this.idleTabOn(owner, url);
|
|
4701
|
-
if (live === void 0) return void 0;
|
|
4702
|
-
return this.refreshView(live);
|
|
6133
|
+
return this.pageFromShown(live, shown);
|
|
4703
6134
|
}
|
|
4704
|
-
/** The most recently touched idle tab on this address. */
|
|
6135
|
+
/** The most recently touched idle tab on this address. A tab the person holds is left alone. */
|
|
4705
6136
|
idleTabOn(owner, url) {
|
|
4706
6137
|
const pageUrl = canonical(url);
|
|
4707
|
-
const tabs = this.windows.list(owner).map((info) => this.windows.get(owner, info.tab)).filter((tab) => tab !== void 0 && canonical(tab.url) === pageUrl && !tab.occupied);
|
|
6138
|
+
const tabs = this.windows.list(owner).map((info) => this.windows.get(owner, info.tab)).filter((tab) => tab !== void 0 && canonical(tab.url) === pageUrl && !tab.occupied && this.control.ofTab(owner, tab.id) !== "person");
|
|
4708
6139
|
tabs.sort((left, right) => right.touchedAt - left.touchedAt);
|
|
4709
6140
|
return tabs[0];
|
|
4710
6141
|
}
|
|
@@ -4713,10 +6144,15 @@ let MhBrowserService = (() => {
|
|
|
4713
6144
|
if (owner === "person") return;
|
|
4714
6145
|
await this.windows.enforceCap(owner);
|
|
4715
6146
|
}
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
6147
|
+
/** Other tabs on this address: idle ones reload, occupied or taken ones are marked. */
|
|
6148
|
+
async refreshIdle(owner, url, except, signal) {
|
|
6149
|
+
const pageUrl = canonical(url);
|
|
6150
|
+
const tabs = this.windows.list(owner).map((info) => this.windows.get(owner, info.tab)).filter((item) => item !== void 0 && canonical(item.url) === pageUrl);
|
|
6151
|
+
for (const item of tabs) {
|
|
6152
|
+
if (item.id === except) continue;
|
|
6153
|
+
if (item.occupied || this.control.ofTab(owner, item.id) === "person") this.windows.markMayHaveChanged(item);
|
|
6154
|
+
else await this.navigateTab(item, item.page.url(), signal);
|
|
6155
|
+
}
|
|
4720
6156
|
}
|
|
4721
6157
|
async navigateTab(live, url, signal) {
|
|
4722
6158
|
try {
|
|
@@ -4751,18 +6187,6 @@ let MhBrowserService = (() => {
|
|
|
4751
6187
|
};
|
|
4752
6188
|
}
|
|
4753
6189
|
}
|
|
4754
|
-
async reloadSame(owner, url, tab, signal) {
|
|
4755
|
-
const pageUrl = canonical(url);
|
|
4756
|
-
const current = this.windows.get(owner, tab);
|
|
4757
|
-
if (current === void 0) return unknownTab();
|
|
4758
|
-
const tabs = this.windows.list(owner).map((info) => this.windows.get(owner, info.tab)).filter((item) => item !== void 0 && canonical(item.url) === pageUrl);
|
|
4759
|
-
for (const item of tabs) {
|
|
4760
|
-
if (item.id === current.id) continue;
|
|
4761
|
-
if (item.occupied) this.windows.markMayHaveChanged(item);
|
|
4762
|
-
else await this.navigateTab(item, item.page.url(), signal);
|
|
4763
|
-
}
|
|
4764
|
-
return this.navigateTab(current, current.page.url(), signal);
|
|
4765
|
-
}
|
|
4766
6190
|
async sendAction(request, signal) {
|
|
4767
6191
|
const context = this.context;
|
|
4768
6192
|
if (context === void 0) return { kind: "lost" };
|
|
@@ -4926,16 +6350,16 @@ let MhBrowserService = (() => {
|
|
|
4926
6350
|
(browser?.process?.())?.kill("SIGKILL");
|
|
4927
6351
|
this.context = void 0;
|
|
4928
6352
|
}
|
|
4929
|
-
refuseIfPerson(owner) {
|
|
4930
|
-
if (this.control.
|
|
4931
|
-
throw new MhBrowserError("PERSON_IN_CONTROL", "the person is using
|
|
6353
|
+
refuseIfPerson(owner, tab) {
|
|
6354
|
+
if (this.control.ofTab(owner, tab) !== "person") return;
|
|
6355
|
+
throw new MhBrowserError("PERSON_IN_CONTROL", "the person is using this tab; call browser_handover with this tab to wait until they hand it back, or work in another tab");
|
|
4932
6356
|
}
|
|
4933
|
-
async takenOver(live) {
|
|
6357
|
+
async takenOver(live, note = "") {
|
|
4934
6358
|
try {
|
|
4935
6359
|
const page = await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" });
|
|
4936
|
-
return new MhBrowserError("TAKEN_OVER", MAY_HAVE_TAKEN_EFFECT
|
|
6360
|
+
return new MhBrowserError("TAKEN_OVER", `${MAY_HAVE_TAKEN_EFFECT}${note}`, page);
|
|
4937
6361
|
} catch {
|
|
4938
|
-
return new MhBrowserError("TAKEN_OVER", MAY_HAVE_TAKEN_EFFECT);
|
|
6362
|
+
return new MhBrowserError("TAKEN_OVER", `${MAY_HAVE_TAKEN_EFFECT}${note}`);
|
|
4939
6363
|
}
|
|
4940
6364
|
}
|
|
4941
6365
|
presenceUrls(owner, tabs) {
|
|
@@ -5116,7 +6540,7 @@ function watchFileChooser(page) {
|
|
|
5116
6540
|
let onChooser;
|
|
5117
6541
|
return {
|
|
5118
6542
|
opened: new Promise((resolve) => {
|
|
5119
|
-
onChooser = (chooser) => resolve(chooser
|
|
6543
|
+
onChooser = (chooser) => resolve(chooser);
|
|
5120
6544
|
page.on("filechooser", onChooser);
|
|
5121
6545
|
}),
|
|
5122
6546
|
stop: () => {
|
|
@@ -5124,15 +6548,6 @@ function watchFileChooser(page) {
|
|
|
5124
6548
|
}
|
|
5125
6549
|
};
|
|
5126
6550
|
}
|
|
5127
|
-
function missingRef(snapshot, action) {
|
|
5128
|
-
return refsOf(action).find((ref) => !snapshot.includes(`[ref=${ref}]`));
|
|
5129
|
-
}
|
|
5130
|
-
function refsOf(action) {
|
|
5131
|
-
if (action.kind === "click" || action.kind === "select" || action.kind === "upload") return [action.ref];
|
|
5132
|
-
if (action.kind === "press") return action.ref === void 0 ? [] : [action.ref];
|
|
5133
|
-
if (action.kind === "fill") return action.fields.map((field) => field.ref);
|
|
5134
|
-
return [];
|
|
5135
|
-
}
|
|
5136
6551
|
function snapshotSurface(current) {
|
|
5137
6552
|
const agentView = current.kind === "agent-view" ? current.version !== void 0 ? { version: current.version } : {} : void 0;
|
|
5138
6553
|
return {
|
|
@@ -5142,478 +6557,15 @@ function snapshotSurface(current) {
|
|
|
5142
6557
|
...agentView !== void 0 ? { agentView } : {}
|
|
5143
6558
|
};
|
|
5144
6559
|
}
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
const
|
|
5149
|
-
type
|
|
5150
|
-
additionalProperties: false,
|
|
5151
|
-
properties: {
|
|
5152
|
-
attachmentId: {
|
|
5153
|
-
type: "string",
|
|
5154
|
-
required: true
|
|
5155
|
-
},
|
|
5156
|
-
mediaType: {
|
|
5157
|
-
type: "string",
|
|
5158
|
-
enum: [
|
|
5159
|
-
"image/png",
|
|
5160
|
-
"image/jpeg",
|
|
5161
|
-
"image/webp",
|
|
5162
|
-
"image/gif"
|
|
5163
|
-
],
|
|
5164
|
-
required: true
|
|
5165
|
-
},
|
|
5166
|
-
bytes: {
|
|
5167
|
-
type: "integer",
|
|
5168
|
-
required: true
|
|
5169
|
-
},
|
|
5170
|
-
width: {
|
|
5171
|
-
type: "integer",
|
|
5172
|
-
required: true
|
|
5173
|
-
},
|
|
5174
|
-
height: {
|
|
5175
|
-
type: "integer",
|
|
5176
|
-
required: true
|
|
5177
|
-
},
|
|
5178
|
-
name: { type: "string" },
|
|
5179
|
-
originalDimensions: {
|
|
5180
|
-
type: "object",
|
|
5181
|
-
additionalProperties: false,
|
|
5182
|
-
properties: {
|
|
5183
|
-
width: {
|
|
5184
|
-
type: "integer",
|
|
5185
|
-
required: true
|
|
5186
|
-
},
|
|
5187
|
-
height: {
|
|
5188
|
-
type: "integer",
|
|
5189
|
-
required: true
|
|
5190
|
-
}
|
|
5191
|
-
}
|
|
5192
|
-
}
|
|
5193
|
-
}
|
|
5194
|
-
};
|
|
5195
|
-
function imageValue(ref) {
|
|
5196
|
-
return {
|
|
5197
|
-
attachmentId: ref.attachmentId,
|
|
5198
|
-
mediaType: ref.mediaType,
|
|
5199
|
-
bytes: ref.bytes,
|
|
5200
|
-
width: ref.width,
|
|
5201
|
-
height: ref.height,
|
|
5202
|
-
...ref.name === void 0 ? {} : { name: ref.name },
|
|
5203
|
-
...ref.originalDimensions === void 0 ? {} : { originalDimensions: { ...ref.originalDimensions } }
|
|
5204
|
-
};
|
|
5205
|
-
}
|
|
5206
|
-
/** `dsh-llm` types its block against the hoisted `dsh-attachment`, so the id is re-branded through that type. */
|
|
5207
|
-
function imageRef(value) {
|
|
5208
|
-
return {
|
|
5209
|
-
attachmentId: value.attachmentId,
|
|
5210
|
-
mediaType: value.mediaType,
|
|
5211
|
-
bytes: value.bytes,
|
|
5212
|
-
width: value.width,
|
|
5213
|
-
height: value.height,
|
|
5214
|
-
...value.name === void 0 ? {} : { name: value.name },
|
|
5215
|
-
...value.originalDimensions === void 0 ? {} : { originalDimensions: { ...value.originalDimensions } }
|
|
5216
|
-
};
|
|
5217
|
-
}
|
|
5218
|
-
const ACT = [
|
|
5219
|
-
"Do one thing on a tab's human page and return its aria snapshot.",
|
|
5220
|
-
"Use it for a page without an Agent View, or to work the human page of one; to read a page use browser_open, and to call a declared action use browser_invoke.",
|
|
5221
|
-
"A page with an Agent View still returns a snapshot (agent-view=\"available\"); use look first when you need refs for it.",
|
|
5222
|
-
"A tab is in use while it has unsubmitted input, an open dialog, or a handover; an idle tab may be closed (UNKNOWN_TAB), and browser_open of its address opens it again.",
|
|
5223
|
-
"Do not fill or submit a login form or a form that issues credentials; call browser_handover."
|
|
5224
|
-
].join(" ");
|
|
5225
|
-
const TAB$1 = "Tab id copied from the latest browser result. An unknown id fails with UNKNOWN_TAB and returns the current tabs list.";
|
|
5226
|
-
const ACTION = [
|
|
5227
|
-
"The one action to perform.",
|
|
5228
|
-
"ref is an element ref copied from this tab's latest snapshot, such as e3; one that is not in that snapshot fails with STALE_REF and returns the current snapshot.",
|
|
5229
|
-
"element is an optional short label for the person reading the log; it does not locate anything.",
|
|
5230
|
-
"press without a ref goes to the focused element."
|
|
5231
|
-
].join(" ");
|
|
5232
|
-
const REF = "Element ref, such as e3.";
|
|
5233
|
-
const ELEMENT = "Label for the person.";
|
|
5234
|
-
function browserActTool(ctx) {
|
|
5235
|
-
const reader = logReader(ctx);
|
|
5236
|
-
return defineTool({
|
|
5237
|
-
name: "browser_act",
|
|
5238
|
-
description: ACT,
|
|
5239
|
-
parameters: {
|
|
5240
|
-
tab: {
|
|
5241
|
-
type: "string",
|
|
5242
|
-
required: true,
|
|
5243
|
-
description: TAB$1
|
|
5244
|
-
},
|
|
5245
|
-
action: {
|
|
5246
|
-
required: true,
|
|
5247
|
-
description: ACTION,
|
|
5248
|
-
oneOf: [
|
|
5249
|
-
{
|
|
5250
|
-
type: "object",
|
|
5251
|
-
additionalProperties: false,
|
|
5252
|
-
description: "Click an element.",
|
|
5253
|
-
properties: {
|
|
5254
|
-
kind: {
|
|
5255
|
-
type: "string",
|
|
5256
|
-
const: "click",
|
|
5257
|
-
required: true,
|
|
5258
|
-
description: "click"
|
|
5259
|
-
},
|
|
5260
|
-
ref: {
|
|
5261
|
-
type: "string",
|
|
5262
|
-
required: true,
|
|
5263
|
-
description: REF
|
|
5264
|
-
},
|
|
5265
|
-
element: {
|
|
5266
|
-
type: "string",
|
|
5267
|
-
description: ELEMENT
|
|
5268
|
-
},
|
|
5269
|
-
double: {
|
|
5270
|
-
type: "boolean",
|
|
5271
|
-
description: "Double-click instead of click."
|
|
5272
|
-
},
|
|
5273
|
-
button: {
|
|
5274
|
-
type: "string",
|
|
5275
|
-
enum: [
|
|
5276
|
-
"left",
|
|
5277
|
-
"right",
|
|
5278
|
-
"middle"
|
|
5279
|
-
],
|
|
5280
|
-
description: "Defaults to left."
|
|
5281
|
-
},
|
|
5282
|
-
modifiers: {
|
|
5283
|
-
type: "array",
|
|
5284
|
-
description: "Modifier keys held during the click.",
|
|
5285
|
-
items: {
|
|
5286
|
-
type: "string",
|
|
5287
|
-
enum: [
|
|
5288
|
-
"Alt",
|
|
5289
|
-
"Control",
|
|
5290
|
-
"Meta",
|
|
5291
|
-
"Shift"
|
|
5292
|
-
]
|
|
5293
|
-
}
|
|
5294
|
-
}
|
|
5295
|
-
}
|
|
5296
|
-
},
|
|
5297
|
-
{
|
|
5298
|
-
type: "object",
|
|
5299
|
-
additionalProperties: false,
|
|
5300
|
-
description: "Choose options on a list element; this does not submit.",
|
|
5301
|
-
properties: {
|
|
5302
|
-
kind: {
|
|
5303
|
-
type: "string",
|
|
5304
|
-
const: "select",
|
|
5305
|
-
required: true,
|
|
5306
|
-
description: "select"
|
|
5307
|
-
},
|
|
5308
|
-
ref: {
|
|
5309
|
-
type: "string",
|
|
5310
|
-
required: true,
|
|
5311
|
-
description: REF
|
|
5312
|
-
},
|
|
5313
|
-
element: {
|
|
5314
|
-
type: "string",
|
|
5315
|
-
description: ELEMENT
|
|
5316
|
-
},
|
|
5317
|
-
values: {
|
|
5318
|
-
type: "array",
|
|
5319
|
-
required: true,
|
|
5320
|
-
description: "Option values copied from the control; at least one.",
|
|
5321
|
-
items: { type: "string" }
|
|
5322
|
-
}
|
|
5323
|
-
}
|
|
5324
|
-
},
|
|
5325
|
-
{
|
|
5326
|
-
type: "object",
|
|
5327
|
-
additionalProperties: false,
|
|
5328
|
-
description: "Press a key.",
|
|
5329
|
-
properties: {
|
|
5330
|
-
kind: {
|
|
5331
|
-
type: "string",
|
|
5332
|
-
const: "press",
|
|
5333
|
-
required: true,
|
|
5334
|
-
description: "press"
|
|
5335
|
-
},
|
|
5336
|
-
key: {
|
|
5337
|
-
type: "string",
|
|
5338
|
-
required: true,
|
|
5339
|
-
description: "Key name, such as Enter or ArrowDown."
|
|
5340
|
-
},
|
|
5341
|
-
ref: {
|
|
5342
|
-
type: "string",
|
|
5343
|
-
description: REF
|
|
5344
|
-
},
|
|
5345
|
-
element: {
|
|
5346
|
-
type: "string",
|
|
5347
|
-
description: ELEMENT
|
|
5348
|
-
}
|
|
5349
|
-
}
|
|
5350
|
-
},
|
|
5351
|
-
{
|
|
5352
|
-
type: "object",
|
|
5353
|
-
additionalProperties: false,
|
|
5354
|
-
description: "Fill fields; this does not submit.",
|
|
5355
|
-
properties: {
|
|
5356
|
-
kind: {
|
|
5357
|
-
type: "string",
|
|
5358
|
-
const: "fill",
|
|
5359
|
-
required: true,
|
|
5360
|
-
description: "fill"
|
|
5361
|
-
},
|
|
5362
|
-
fields: {
|
|
5363
|
-
type: "array",
|
|
5364
|
-
required: true,
|
|
5365
|
-
description: "Fields to write in one step; at least one.",
|
|
5366
|
-
items: {
|
|
5367
|
-
type: "object",
|
|
5368
|
-
additionalProperties: false,
|
|
5369
|
-
properties: {
|
|
5370
|
-
ref: {
|
|
5371
|
-
type: "string",
|
|
5372
|
-
required: true,
|
|
5373
|
-
description: REF
|
|
5374
|
-
},
|
|
5375
|
-
element: {
|
|
5376
|
-
type: "string",
|
|
5377
|
-
description: ELEMENT
|
|
5378
|
-
},
|
|
5379
|
-
value: {
|
|
5380
|
-
type: "string",
|
|
5381
|
-
required: true,
|
|
5382
|
-
description: "Text to write; for a checkbox, true or false."
|
|
5383
|
-
}
|
|
5384
|
-
}
|
|
5385
|
-
}
|
|
5386
|
-
}
|
|
5387
|
-
}
|
|
5388
|
-
},
|
|
5389
|
-
{
|
|
5390
|
-
type: "object",
|
|
5391
|
-
additionalProperties: false,
|
|
5392
|
-
description: "Set files on a file input.",
|
|
5393
|
-
properties: {
|
|
5394
|
-
kind: {
|
|
5395
|
-
type: "string",
|
|
5396
|
-
const: "upload",
|
|
5397
|
-
required: true,
|
|
5398
|
-
description: "upload"
|
|
5399
|
-
},
|
|
5400
|
-
ref: {
|
|
5401
|
-
type: "string",
|
|
5402
|
-
required: true,
|
|
5403
|
-
description: REF
|
|
5404
|
-
},
|
|
5405
|
-
element: {
|
|
5406
|
-
type: "string",
|
|
5407
|
-
description: ELEMENT
|
|
5408
|
-
},
|
|
5409
|
-
files: {
|
|
5410
|
-
type: "array",
|
|
5411
|
-
required: true,
|
|
5412
|
-
description: "Attachment ids shown in this session, or paths inside the session cwd; at least one.",
|
|
5413
|
-
items: { type: "string" }
|
|
5414
|
-
}
|
|
5415
|
-
}
|
|
5416
|
-
},
|
|
5417
|
-
{
|
|
5418
|
-
type: "object",
|
|
5419
|
-
additionalProperties: false,
|
|
5420
|
-
description: "Accept or dismiss the open dialog.",
|
|
5421
|
-
properties: {
|
|
5422
|
-
kind: {
|
|
5423
|
-
type: "string",
|
|
5424
|
-
const: "dialog",
|
|
5425
|
-
required: true,
|
|
5426
|
-
description: "dialog"
|
|
5427
|
-
},
|
|
5428
|
-
accept: {
|
|
5429
|
-
type: "boolean",
|
|
5430
|
-
required: true,
|
|
5431
|
-
description: "True accepts, false dismisses."
|
|
5432
|
-
},
|
|
5433
|
-
promptText: {
|
|
5434
|
-
type: "string",
|
|
5435
|
-
description: "Text to enter when accepting a prompt."
|
|
5436
|
-
}
|
|
5437
|
-
}
|
|
5438
|
-
},
|
|
5439
|
-
{
|
|
5440
|
-
type: "object",
|
|
5441
|
-
additionalProperties: false,
|
|
5442
|
-
description: "Go back one step in this tab's history.",
|
|
5443
|
-
properties: { kind: {
|
|
5444
|
-
type: "string",
|
|
5445
|
-
const: "back",
|
|
5446
|
-
required: true,
|
|
5447
|
-
description: "back"
|
|
5448
|
-
} }
|
|
5449
|
-
},
|
|
5450
|
-
{
|
|
5451
|
-
type: "object",
|
|
5452
|
-
additionalProperties: false,
|
|
5453
|
-
description: "Wait for text to appear or disappear, or for time seconds, then return the snapshot.",
|
|
5454
|
-
properties: {
|
|
5455
|
-
kind: {
|
|
5456
|
-
type: "string",
|
|
5457
|
-
const: "wait",
|
|
5458
|
-
required: true,
|
|
5459
|
-
description: "wait"
|
|
5460
|
-
},
|
|
5461
|
-
text: {
|
|
5462
|
-
type: "string",
|
|
5463
|
-
description: "Text to wait for; the snapshot is returned even if it never appears."
|
|
5464
|
-
},
|
|
5465
|
-
textGone: {
|
|
5466
|
-
type: "string",
|
|
5467
|
-
description: "Text to wait until it disappears; with text set too, the first one wins."
|
|
5468
|
-
},
|
|
5469
|
-
time: {
|
|
5470
|
-
type: "number",
|
|
5471
|
-
description: "Seconds to wait, or the limit for text and textGone; capped at 30, default 30."
|
|
5472
|
-
}
|
|
5473
|
-
}
|
|
5474
|
-
},
|
|
5475
|
-
{
|
|
5476
|
-
type: "object",
|
|
5477
|
-
additionalProperties: false,
|
|
5478
|
-
description: "Return the current snapshot without acting; use it to obtain refs.",
|
|
5479
|
-
properties: { kind: {
|
|
5480
|
-
type: "string",
|
|
5481
|
-
const: "look",
|
|
5482
|
-
required: true,
|
|
5483
|
-
description: "look"
|
|
5484
|
-
} }
|
|
5485
|
-
}
|
|
5486
|
-
]
|
|
5487
|
-
},
|
|
5488
|
-
screenshot: {
|
|
5489
|
-
type: "boolean",
|
|
5490
|
-
description: "True adds a PNG of this tab after the page; none is taken while a dialog is open."
|
|
5491
|
-
}
|
|
5492
|
-
},
|
|
5493
|
-
output: {
|
|
5494
|
-
schema: {
|
|
5495
|
-
type: "object",
|
|
5496
|
-
additionalProperties: false,
|
|
5497
|
-
properties: {
|
|
5498
|
-
text: {
|
|
5499
|
-
type: "string",
|
|
5500
|
-
required: true
|
|
5501
|
-
},
|
|
5502
|
-
meta: {
|
|
5503
|
-
type: "json",
|
|
5504
|
-
required: true
|
|
5505
|
-
},
|
|
5506
|
-
image: IMAGE_VALUE
|
|
5507
|
-
}
|
|
5508
|
-
},
|
|
5509
|
-
render: (_args, value) => {
|
|
5510
|
-
const text = {
|
|
5511
|
-
type: "text",
|
|
5512
|
-
text: value.text
|
|
5513
|
-
};
|
|
5514
|
-
if (value.image === void 0) return [text];
|
|
5515
|
-
return [text, {
|
|
5516
|
-
type: "image",
|
|
5517
|
-
attachment: imageRef(value.image)
|
|
5518
|
-
}];
|
|
5519
|
-
},
|
|
5520
|
-
presentationMeta: (_args, value) => value.meta
|
|
5521
|
-
},
|
|
5522
|
-
isConcurrencySafe: () => false,
|
|
5523
|
-
timeoutMs: 6e4,
|
|
5524
|
-
presentCall: (args) => ({
|
|
5525
|
-
card: "generic",
|
|
5526
|
-
kind: "other",
|
|
5527
|
-
title: `browser_act ${args.tab} ${args.action.kind}`
|
|
5528
|
-
}),
|
|
5529
|
-
presentResult: (args) => ({
|
|
5530
|
-
card: "generic",
|
|
5531
|
-
title: `browser_act ${args.tab} ${args.action.kind}`
|
|
5532
|
-
}),
|
|
5533
|
-
async execute(args, exec) {
|
|
5534
|
-
const session = sessionFor(ctx, exec);
|
|
5535
|
-
ctx.mhBrowser.noteWorkspace(session.id, writeTarget(ctx, session));
|
|
5536
|
-
try {
|
|
5537
|
-
const action = await toAction(args.action, (files) => uploadPaths(ctx, reader.attachments(session.session), session.cwd, files));
|
|
5538
|
-
const view = await ctx.mhBrowser.act(session.id, args.tab, action, exec.signal);
|
|
5539
|
-
const image = args.screenshot === true ? await screenshot(ctx, session.id, args.tab, exec.signal) : void 0;
|
|
5540
|
-
const closed = closedNotes(ctx, reader, session);
|
|
5541
|
-
const tabs = ctx.mhBrowser.tabs(session.id);
|
|
5542
|
-
return {
|
|
5543
|
-
text: renderOutcome([view], tabs, closed, image === void 0 ? void 0 : image.attachmentId),
|
|
5544
|
-
meta: JSON.parse(JSON.stringify({
|
|
5545
|
-
...readingsOf([view], action.kind),
|
|
5546
|
-
closed
|
|
5547
|
-
})),
|
|
5548
|
-
...image === void 0 ? {} : { image: imageValue(image) }
|
|
5549
|
-
};
|
|
5550
|
-
} catch (error) {
|
|
5551
|
-
if (error instanceof MhBrowserError && error.code === "UNKNOWN_TAB") rethrow(new MhBrowserError(UNKNOWN_TAB, `${UNKNOWN_TAB_REASON} ${args.tab}`), renderOutcome([], ctx.mhBrowser.tabs(session.id), closedNotes(ctx, reader, session)));
|
|
5552
|
-
if (error instanceof MhBrowserError && (error.code === "STALE_REF" || error.code === "TAKEN_OVER") && error.page !== void 0) rethrow(error, renderOutcome([error.page], ctx.mhBrowser.tabs(session.id), closedNotes(ctx, reader, session)));
|
|
5553
|
-
rethrow(error);
|
|
5554
|
-
}
|
|
5555
|
-
}
|
|
5556
|
-
});
|
|
5557
|
-
}
|
|
5558
|
-
/** The action already happened, so a screenshot that cannot be taken or saved is left out, not an error. */
|
|
5559
|
-
async function screenshot(ctx, owner, tab, signal) {
|
|
5560
|
-
const data = await ctx.mhBrowser.capture(owner, tab, signal);
|
|
5561
|
-
if (data === void 0) return void 0;
|
|
5562
|
-
try {
|
|
5563
|
-
return await ctx.attachments.saveImage({
|
|
5564
|
-
data,
|
|
5565
|
-
mediaType: "image/png",
|
|
5566
|
-
name: `${tab}.png`
|
|
5567
|
-
});
|
|
5568
|
-
} catch (error) {
|
|
5569
|
-
ctx.logger.warn("saving the screenshot of %s failed: %o", tab, error);
|
|
5570
|
-
return;
|
|
5571
|
-
}
|
|
6560
|
+
/** Text and Agent View bodies are read on the request channel; anything else is left to a tab. */
|
|
6561
|
+
function readsAsText(mediaType) {
|
|
6562
|
+
if (mediaType === void 0) return true;
|
|
6563
|
+
const type = mediaType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
6564
|
+
return type.startsWith("text/") || type === "application/xhtml+xml";
|
|
5572
6565
|
}
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
ref: action.ref,
|
|
5577
|
-
...action.double !== void 0 ? { double: action.double } : {},
|
|
5578
|
-
...action.button !== void 0 ? { button: action.button } : {},
|
|
5579
|
-
...action.modifiers !== void 0 ? { modifiers: [...action.modifiers] } : {}
|
|
5580
|
-
};
|
|
5581
|
-
if (action.kind === "select" && action.ref !== void 0 && action.values !== void 0) return {
|
|
5582
|
-
kind: "select",
|
|
5583
|
-
ref: action.ref,
|
|
5584
|
-
values: [...action.values]
|
|
5585
|
-
};
|
|
5586
|
-
if (action.kind === "press" && action.key !== void 0) return {
|
|
5587
|
-
kind: "press",
|
|
5588
|
-
key: action.key,
|
|
5589
|
-
...action.ref !== void 0 ? { ref: action.ref } : {}
|
|
5590
|
-
};
|
|
5591
|
-
if (action.kind === "fill" && action.fields !== void 0) return {
|
|
5592
|
-
kind: "fill",
|
|
5593
|
-
fields: action.fields.map((field) => ({
|
|
5594
|
-
ref: field.ref,
|
|
5595
|
-
value: field.value
|
|
5596
|
-
}))
|
|
5597
|
-
};
|
|
5598
|
-
if (action.kind === "upload" && action.ref !== void 0 && action.files !== void 0) return {
|
|
5599
|
-
kind: "upload",
|
|
5600
|
-
ref: action.ref,
|
|
5601
|
-
files: await resolveFiles(action.files)
|
|
5602
|
-
};
|
|
5603
|
-
if (action.kind === "dialog" && action.accept !== void 0) return {
|
|
5604
|
-
kind: "dialog",
|
|
5605
|
-
accept: action.accept,
|
|
5606
|
-
...action.promptText !== void 0 ? { promptText: action.promptText } : {}
|
|
5607
|
-
};
|
|
5608
|
-
if (action.kind === "back") return { kind: "back" };
|
|
5609
|
-
if (action.kind === "wait") return {
|
|
5610
|
-
kind: "wait",
|
|
5611
|
-
...action.text !== void 0 ? { text: action.text } : {},
|
|
5612
|
-
...action.textGone !== void 0 ? { textGone: action.textGone } : {},
|
|
5613
|
-
...action.time !== void 0 ? { time: action.time } : {}
|
|
5614
|
-
};
|
|
5615
|
-
if (action.kind === "look") return { kind: "look" };
|
|
5616
|
-
throw new Error(`unknown action ${action.kind}`);
|
|
6566
|
+
/** Read after an await: an earlier check does not narrow it. */
|
|
6567
|
+
function stopped(signal) {
|
|
6568
|
+
return signal?.aborted === true;
|
|
5617
6569
|
}
|
|
5618
6570
|
//#endregion
|
|
5619
6571
|
//#region lib/types/tools/download.js
|
|
@@ -5667,8 +6619,8 @@ function browserDownloadTool(ctx) {
|
|
|
5667
6619
|
ctx.mhBrowser.noteWorkspace(session.id, target);
|
|
5668
6620
|
if ("denied" in target) throw new BrowserToolError(target.code, target.denied);
|
|
5669
6621
|
const saved = await ctx.mhBrowser.download(session.id, args.url, target.root, exec.signal);
|
|
5670
|
-
const
|
|
5671
|
-
const text = `${renderDownload(saved)}
|
|
6622
|
+
const block = tabBlock(ctx, reader, session);
|
|
6623
|
+
const text = `${renderDownload(saved)}${block.suffix}`;
|
|
5672
6624
|
const download = saved.kind === "saved" ? {
|
|
5673
6625
|
path: saved.path,
|
|
5674
6626
|
bytes: saved.bytes,
|
|
@@ -5684,8 +6636,9 @@ function browserDownloadTool(ctx) {
|
|
|
5684
6636
|
by: META_BY,
|
|
5685
6637
|
action: "download",
|
|
5686
6638
|
pages: [],
|
|
5687
|
-
closed,
|
|
5688
|
-
download
|
|
6639
|
+
closed: block.closed,
|
|
6640
|
+
download,
|
|
6641
|
+
...block.snap === void 0 ? {} : { tabs: block.snap }
|
|
5689
6642
|
}))
|
|
5690
6643
|
};
|
|
5691
6644
|
}
|
|
@@ -5695,11 +6648,11 @@ function browserDownloadTool(ctx) {
|
|
|
5695
6648
|
//#region lib/types/tools/handover.js
|
|
5696
6649
|
const HANDOVER = [
|
|
5697
6650
|
"Hand the browser to the person and wait until they hand it back.",
|
|
5698
|
-
"
|
|
5699
|
-
"
|
|
6651
|
+
"The person can take over a tab at any time; that stops the action in progress on that tab, and a tab they have touched stays with them until they hand it back. Other tabs keep working.",
|
|
6652
|
+
"Call it instead of filling or submitting a login form, a Memory login or consent form, or any form that issues credentials, and whenever only the person can continue."
|
|
5700
6653
|
].join(" ");
|
|
5701
6654
|
const REASON = "The sentence the person sees on the handover card, unchanged.";
|
|
5702
|
-
const TAB = "Tab id
|
|
6655
|
+
const TAB = "Tab id of the human page to show the person, copied from a page element's tab attribute. Omit it to hand over without naming a tab; that does not pick the latest result. A page with an Agent View has no tab until you open it with snapshot.";
|
|
5703
6656
|
const ABORT = Symbol("abort");
|
|
5704
6657
|
function browserHandoverTool(ctx) {
|
|
5705
6658
|
const reader = logReader(ctx);
|
|
@@ -5762,23 +6715,23 @@ function browserHandoverTool(ctx) {
|
|
|
5762
6715
|
const back = await ctx.mhBrowser.handBack(session.id, note);
|
|
5763
6716
|
const page = back.page;
|
|
5764
6717
|
const pages = page === void 0 ? [] : [page];
|
|
5765
|
-
const
|
|
5766
|
-
const tabs = ctx.mhBrowser.tabs(session.id);
|
|
6718
|
+
const block = tabBlock(ctx, reader, session);
|
|
5767
6719
|
return {
|
|
5768
6720
|
text: `${[
|
|
5769
6721
|
...note === void 0 || note === "" ? [] : [note],
|
|
5770
6722
|
`${String(back.operations)} operations`,
|
|
5771
6723
|
takeoverText(back)
|
|
5772
|
-
].join("\n")}\n${renderOutcome(pages
|
|
6724
|
+
].join("\n")}\n${renderOutcome(pages)}${block.suffix}`,
|
|
5773
6725
|
meta: JSON.parse(JSON.stringify({
|
|
5774
6726
|
...readingsOf(pages, "handover"),
|
|
5775
|
-
closed,
|
|
5776
|
-
operations: back.operations
|
|
6727
|
+
closed: block.closed,
|
|
6728
|
+
operations: back.operations,
|
|
6729
|
+
...block.snap === void 0 ? {} : { tabs: block.snap }
|
|
5777
6730
|
}))
|
|
5778
6731
|
};
|
|
5779
6732
|
} catch (error) {
|
|
5780
6733
|
if (exec.signal.aborted || isAskAborted(error)) return quiet();
|
|
5781
|
-
if (error instanceof MhBrowserError && error.code === "UNKNOWN_TAB") rethrow(new MhBrowserError(UNKNOWN_TAB, `${UNKNOWN_TAB_REASON} ${args.tab ?? ""}`.trim()), renderOutcome([]
|
|
6734
|
+
if (error instanceof MhBrowserError && error.code === "UNKNOWN_TAB") rethrow(new MhBrowserError(UNKNOWN_TAB, `${UNKNOWN_TAB_REASON} ${args.tab ?? ""}`.trim()), `${renderOutcome([])}${tabBlock(ctx, reader, session).suffix}`);
|
|
5782
6735
|
rethrow(error);
|
|
5783
6736
|
} finally {
|
|
5784
6737
|
ctx.mhBrowser.releaseIfHeld(session.id);
|
|
@@ -5828,10 +6781,10 @@ function isAskAborted(error) {
|
|
|
5828
6781
|
//#endregion
|
|
5829
6782
|
//#region lib/types/tools/invoke.js
|
|
5830
6783
|
const INVOKE = [
|
|
5831
|
-
"Call one action an Agent View declares,
|
|
5832
|
-
"
|
|
5833
|
-
"
|
|
5834
|
-
"An action declared by a document in requires is called
|
|
6784
|
+
"Call one action an Agent View declares, in the principal's name on that site. The call uses the page address and does not need a tab.",
|
|
6785
|
+
"A declared GET returns the resulting page the way browser_open does; any other action returns only the header of the page it lands on, so open that page to read it or act on it.",
|
|
6786
|
+
"The call uses the Agent View you were shown; if the page has changed, the site refuses and the result says so.",
|
|
6787
|
+
"An action declared by a document in requires is called with that document's address as page.",
|
|
5835
6788
|
"Do not submit a login form or a form that issues credentials; call browser_handover."
|
|
5836
6789
|
].join(" ");
|
|
5837
6790
|
const PAGE = "The url attribute of a page element in a tool result or a presence reminder, or the id in that page's Agent View. When one id names several pages shown to you, use the url of the one you mean.";
|
|
@@ -5906,7 +6859,10 @@ function browserInvokeTool(ctx) {
|
|
|
5906
6859
|
try {
|
|
5907
6860
|
const sent = await ctx.mhBrowser.invoke(session.id, resolved.url, {
|
|
5908
6861
|
name: args.name,
|
|
5909
|
-
|
|
6862
|
+
shown: {
|
|
6863
|
+
url: resolved.url,
|
|
6864
|
+
text: resolved.text
|
|
6865
|
+
},
|
|
5910
6866
|
...args.item === void 0 ? {} : { item: args.item },
|
|
5911
6867
|
...input === void 0 ? {} : { input }
|
|
5912
6868
|
}, {
|
|
@@ -5914,20 +6870,20 @@ function browserInvokeTool(ctx) {
|
|
|
5914
6870
|
readFile: (value) => resolveBinary(ctx, reader.attachments(session.session), session.cwd, value)
|
|
5915
6871
|
});
|
|
5916
6872
|
const pages = [sent.page];
|
|
5917
|
-
const
|
|
6873
|
+
const block = tabBlock(ctx, reader, session);
|
|
5918
6874
|
return {
|
|
5919
|
-
text: `${sent.response === void 0 ? "" : `${renderActionResponse(sent.response)}\n`}${"representation" in sent.page ? renderPage(sent.page, void 0, sent.page.inPresence) : renderPageError(sent.page)}
|
|
6875
|
+
text: `${sent.response === void 0 ? "" : `${renderActionResponse(sent.response)}\n`}${"representation" in sent.page ? renderPage(sent.page, void 0, sent.page.inPresence && !sent.page.headerOnly) : renderPageError(sent.page)}${block.suffix}`,
|
|
5920
6876
|
meta: JSON.parse(JSON.stringify({
|
|
5921
6877
|
...readingsOf(pages, `invoke:${args.name}`),
|
|
5922
|
-
closed,
|
|
6878
|
+
closed: block.closed,
|
|
6879
|
+
...block.snap === void 0 ? {} : { tabs: block.snap },
|
|
5923
6880
|
...sent.schemaRejected ? { schemaRejected: true } : {}
|
|
5924
6881
|
}))
|
|
5925
6882
|
};
|
|
5926
6883
|
} catch (error) {
|
|
5927
6884
|
if (error instanceof MhBrowserError && error.page !== void 0) {
|
|
5928
|
-
const
|
|
5929
|
-
|
|
5930
|
-
rethrow(error, `${renderPage(error.page, void 0, omit, stale)}\n${renderTabs(ctx.mhBrowser.tabs(session.id), closedNotes(ctx, reader, session))}`);
|
|
6885
|
+
const block = tabBlock(ctx, reader, session);
|
|
6886
|
+
rethrow(error, `${renderPage(error.page, void 0, error.page.inPresence)}${block.suffix}`);
|
|
5931
6887
|
}
|
|
5932
6888
|
rethrow(error);
|
|
5933
6889
|
}
|
|
@@ -5947,14 +6903,14 @@ async function resolveBinary(ctx, seen, cwd, value) {
|
|
|
5947
6903
|
//#endregion
|
|
5948
6904
|
//#region lib/types/tools/open.js
|
|
5949
6905
|
const OPEN = [
|
|
5950
|
-
"Read 1 to 8 pages
|
|
5951
|
-
"
|
|
5952
|
-
"A page lists
|
|
5953
|
-
"An address with no version attribute is one observation; agent-view-error means the Agent View could not be read and the page has no declared actions."
|
|
6906
|
+
"Read 1 to 8 pages. A page with an Agent View returns that document; any other page returns an aria snapshot.",
|
|
6907
|
+
"A page with no version attribute is one observation. agent-view-error means the Agent View could not be read, so this page is a snapshot and has no declared actions.",
|
|
6908
|
+
"A page lists documents in requires; those are not fetched with it, so open their addresses to read them or to call their actions."
|
|
5954
6909
|
].join(" ");
|
|
5955
6910
|
const PAGES = "The pages to read, 1 to 8, each returned in order as its own element.";
|
|
5956
6911
|
const URL$1 = "The address to read: from a page shown to you, from the principal, or one you composed; opening a composed address is how it is checked before you use it elsewhere.";
|
|
5957
6912
|
const WAIT = "Optional wait after the document loads for text to appear or disappear, or for time seconds; at least one must be set. If not met, the page is still returned and waited says so.";
|
|
6913
|
+
const SNAPSHOT = "Ask for the human page: open a tab and return its snapshot, for page operations and handover. A page without an Agent View already does.";
|
|
5958
6914
|
function browserOpenTool(ctx) {
|
|
5959
6915
|
const reader = logReader(ctx);
|
|
5960
6916
|
return defineTool({
|
|
@@ -5973,6 +6929,10 @@ function browserOpenTool(ctx) {
|
|
|
5973
6929
|
required: true,
|
|
5974
6930
|
description: URL$1
|
|
5975
6931
|
},
|
|
6932
|
+
snapshot: {
|
|
6933
|
+
type: "boolean",
|
|
6934
|
+
description: SNAPSHOT
|
|
6935
|
+
},
|
|
5976
6936
|
waitFor: {
|
|
5977
6937
|
type: "object",
|
|
5978
6938
|
additionalProperties: false,
|
|
@@ -6038,14 +6998,16 @@ function browserOpenTool(ctx) {
|
|
|
6038
6998
|
try {
|
|
6039
6999
|
const pages = await ctx.mhBrowser.open(session.id, args.pages.map((page) => ({
|
|
6040
7000
|
url: page.url,
|
|
7001
|
+
...page.snapshot === true ? { snapshot: true } : {},
|
|
6041
7002
|
...page.waitFor === void 0 ? {} : { waitFor: compactWait(page.waitFor) }
|
|
6042
7003
|
})), exec.signal);
|
|
6043
|
-
const
|
|
7004
|
+
const block = tabBlock(ctx, reader, session);
|
|
6044
7005
|
return {
|
|
6045
|
-
text: renderOutcome(pages,
|
|
7006
|
+
text: `${renderOutcome(pages, void 0, true)}${block.suffix}`,
|
|
6046
7007
|
meta: JSON.parse(JSON.stringify({
|
|
6047
7008
|
...readingsOf(pages, "open"),
|
|
6048
|
-
closed
|
|
7009
|
+
closed: block.closed,
|
|
7010
|
+
...block.snap === void 0 ? {} : { tabs: block.snap }
|
|
6049
7011
|
}))
|
|
6050
7012
|
};
|
|
6051
7013
|
} catch (error) {
|
|
@@ -6124,7 +7086,6 @@ function browserPresenceTool(ctx) {
|
|
|
6124
7086
|
}),
|
|
6125
7087
|
async execute(args, exec) {
|
|
6126
7088
|
const session = sessionFor(ctx, exec);
|
|
6127
|
-
if (ctx.mhBrowser.controlOf(session.id) === "person") throw new BrowserToolError("PERSON_IN_CONTROL", "the person is using the browser; call browser_handover to wait until they hand it back");
|
|
6128
7089
|
const put = args.put ?? [];
|
|
6129
7090
|
const remove = args.remove ?? [];
|
|
6130
7091
|
if (put.length + remove.length === 0) throw new BrowserToolError("INVALID_ARGS", "give put, remove, or both");
|
|
@@ -6133,15 +7094,16 @@ function browserPresenceTool(ctx) {
|
|
|
6133
7094
|
const added = ctx.mhBrowser.presencePut(session.id, put, "model");
|
|
6134
7095
|
const removed = ctx.mhBrowser.presenceRemove(session.id, remove);
|
|
6135
7096
|
const receipts = [...added, ...removed];
|
|
6136
|
-
const
|
|
7097
|
+
const block = tabBlock(ctx, reader, session);
|
|
6137
7098
|
return {
|
|
6138
|
-
text: `${receipts.map(line).join("\n")}
|
|
7099
|
+
text: `${receipts.map(line).join("\n")}${block.suffix}`,
|
|
6139
7100
|
meta: JSON.parse(JSON.stringify({
|
|
6140
7101
|
by: META_BY,
|
|
6141
7102
|
action: "presence",
|
|
6142
7103
|
pages: [],
|
|
6143
|
-
closed,
|
|
6144
|
-
receipts
|
|
7104
|
+
closed: block.closed,
|
|
7105
|
+
receipts,
|
|
7106
|
+
...block.snap === void 0 ? {} : { tabs: block.snap }
|
|
6145
7107
|
}))
|
|
6146
7108
|
};
|
|
6147
7109
|
}
|
|
@@ -6155,7 +7117,7 @@ function line(receipt) {
|
|
|
6155
7117
|
function registerBrowserTools(ctx) {
|
|
6156
7118
|
followSessionTitles(ctx);
|
|
6157
7119
|
ctx.tools.register(browserOpenTool(ctx));
|
|
6158
|
-
ctx.tools.register(
|
|
7120
|
+
for (const tool of pageOperationTools(ctx)) ctx.tools.register(tool);
|
|
6159
7121
|
ctx.tools.register(browserInvokeTool(ctx));
|
|
6160
7122
|
ctx.tools.register(browserDownloadTool(ctx));
|
|
6161
7123
|
ctx.tools.register(browserPresenceTool(ctx));
|