@mind-harness/mh-browser 0.2.3 → 0.2.5
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/THIRD-PARTY-NOTICES +23 -0
- package/extension/manifest.json +1 -1
- package/lib/client.cjs +446 -0
- package/lib/index.js +524 -274
- package/lib/typert.host.js +1 -1
- package/lib/typert.remote-client.js +1 -1
- package/lib/types/card.d.ts +24 -0
- package/lib/types/client.d.ts +53 -0
- package/lib/types/locales.d.ts +15 -0
- package/lib/types/row.d.ts +69 -0
- package/lib/types/service/abortable.d.ts +24 -0
- package/lib/types/service/extension.d.ts +7 -6
- package/lib/types/service/windows.d.ts +11 -3
- package/lib/types/tools/given.d.ts +5 -0
- package/package.json +24 -4
package/lib/index.js
CHANGED
|
@@ -33,6 +33,146 @@ function defaultProfileDir(profile, home = resolveDshHome()) {
|
|
|
33
33
|
return profile === void 0 ? join(home, "mh", "browser") : join(home, "mh", "profiles", profile.name, "browser");
|
|
34
34
|
}
|
|
35
35
|
//#endregion
|
|
36
|
+
//#region lib/types/types.js
|
|
37
|
+
var MhBrowserError = class extends Error {
|
|
38
|
+
code;
|
|
39
|
+
page;
|
|
40
|
+
constructor(code, message, page) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "MhBrowserError";
|
|
43
|
+
this.code = code;
|
|
44
|
+
if (page !== void 0) this.page = page;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
/** Owner of the person's own window. It belongs to no Session. */
|
|
48
|
+
const PERSON_OWNER = "person";
|
|
49
|
+
const UNKNOWN_TAB = "UNKNOWN_TAB";
|
|
50
|
+
const UNKNOWN_TAB_REASON = "no such tab in this session";
|
|
51
|
+
/**
|
|
52
|
+
* Internal: a wait ended because its signal aborted. A tool never reports it:
|
|
53
|
+
* a stopped call ends as the tool runtime's abort, a takeover as `TAKEN_OVER`.
|
|
54
|
+
*/
|
|
55
|
+
const INTERRUPTED = "INTERRUPTED";
|
|
56
|
+
/** The browser did not start or could not be restarted. */
|
|
57
|
+
const BROWSER_UNAVAILABLE = "BROWSER_UNAVAILABLE";
|
|
58
|
+
function unknownTab() {
|
|
59
|
+
return {
|
|
60
|
+
url: "",
|
|
61
|
+
reason: UNKNOWN_TAB_REASON
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region lib/types/service/abortable.js
|
|
66
|
+
const WAITING_STOPPED$1 = "stopped while waiting";
|
|
67
|
+
/** A wait ended because its signal aborted, including a deadline synthesized here. */
|
|
68
|
+
function stopped$1() {
|
|
69
|
+
return new MhBrowserError(INTERRUPTED, WAITING_STOPPED$1);
|
|
70
|
+
}
|
|
71
|
+
function isInterrupted(error) {
|
|
72
|
+
return error instanceof MhBrowserError && error.code === "INTERRUPTED";
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* `promise`, or a rejection when `signal` aborts. The original promise is
|
|
76
|
+
* always observed, so abandoning it leaves no unhandled rejection.
|
|
77
|
+
*/
|
|
78
|
+
function untilAborted(promise, signal) {
|
|
79
|
+
const settled = promise.then((value) => ({
|
|
80
|
+
ok: true,
|
|
81
|
+
value
|
|
82
|
+
}), (error) => ({
|
|
83
|
+
ok: false,
|
|
84
|
+
error
|
|
85
|
+
}));
|
|
86
|
+
if (signal === void 0) return unwrap(settled);
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
let done = false;
|
|
89
|
+
const finish = (run) => {
|
|
90
|
+
if (done) return;
|
|
91
|
+
done = true;
|
|
92
|
+
signal.removeEventListener("abort", onAbort);
|
|
93
|
+
run();
|
|
94
|
+
};
|
|
95
|
+
const onAbort = () => {
|
|
96
|
+
finish(() => reject(stopped$1()));
|
|
97
|
+
};
|
|
98
|
+
if (signal.aborted) {
|
|
99
|
+
finish(() => reject(stopped$1()));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
103
|
+
settled.then((result) => {
|
|
104
|
+
finish(() => {
|
|
105
|
+
if (result.ok) resolve(result.value);
|
|
106
|
+
else reject(result.error);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/** `signal`, a deadline of `ms`, or whichever of the two aborts first. */
|
|
112
|
+
function capped(signal, ms) {
|
|
113
|
+
const limit = AbortSignal.timeout(ms);
|
|
114
|
+
if (signal === void 0) return limit;
|
|
115
|
+
return AbortSignal.any([signal, limit]);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Run `op` once `previous` settles. Aborting `signal` while waiting rejects
|
|
119
|
+
* `run` and does not call `op`. `tail` still follows `previous`, and `op`
|
|
120
|
+
* when `op` has started, so a later caller cannot overlap the work.
|
|
121
|
+
*/
|
|
122
|
+
function enqueue(previous, op, signal) {
|
|
123
|
+
let started = Promise.resolve();
|
|
124
|
+
let began = false;
|
|
125
|
+
return {
|
|
126
|
+
run: new Promise((resolve, reject) => {
|
|
127
|
+
let settled = false;
|
|
128
|
+
const fail = () => {
|
|
129
|
+
if (settled) return;
|
|
130
|
+
settled = true;
|
|
131
|
+
reject(stopped$1());
|
|
132
|
+
};
|
|
133
|
+
const onAbort = () => {
|
|
134
|
+
if (began) return;
|
|
135
|
+
fail();
|
|
136
|
+
};
|
|
137
|
+
const start = () => {
|
|
138
|
+
signal?.removeEventListener("abort", onAbort);
|
|
139
|
+
if (settled || signal?.aborted === true) {
|
|
140
|
+
fail();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
let work;
|
|
144
|
+
try {
|
|
145
|
+
began = true;
|
|
146
|
+
work = op();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
started = Promise.reject(error);
|
|
149
|
+
started.catch(() => void 0);
|
|
150
|
+
if (!settled) {
|
|
151
|
+
settled = true;
|
|
152
|
+
reject(error);
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
started = work;
|
|
157
|
+
resolve(work);
|
|
158
|
+
};
|
|
159
|
+
if (signal?.aborted === true) {
|
|
160
|
+
fail();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (signal !== void 0) signal.addEventListener("abort", onAbort, { once: true });
|
|
164
|
+
previous.then(start, start);
|
|
165
|
+
}),
|
|
166
|
+
tail: previous.then(() => started, () => started).then(() => void 0, () => void 0)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function unwrap(settled) {
|
|
170
|
+
return settled.then((result) => {
|
|
171
|
+
if (result.ok) return result.value;
|
|
172
|
+
throw result.error;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
36
176
|
//#region lib/types/representation/agent-view.js
|
|
37
177
|
const AGENT_VIEW_VERSION = "0.1";
|
|
38
178
|
const AGENT_VIEW_ACCEPT = "text/markdown; variant=agent-view";
|
|
@@ -446,35 +586,6 @@ function formatSeconds(seconds) {
|
|
|
446
586
|
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
447
587
|
}
|
|
448
588
|
//#endregion
|
|
449
|
-
//#region lib/types/types.js
|
|
450
|
-
var MhBrowserError = class extends Error {
|
|
451
|
-
code;
|
|
452
|
-
page;
|
|
453
|
-
constructor(code, message, page) {
|
|
454
|
-
super(message);
|
|
455
|
-
this.name = "MhBrowserError";
|
|
456
|
-
this.code = code;
|
|
457
|
-
if (page !== void 0) this.page = page;
|
|
458
|
-
}
|
|
459
|
-
};
|
|
460
|
-
/** Owner of the person's own window. It belongs to no Session. */
|
|
461
|
-
const PERSON_OWNER = "person";
|
|
462
|
-
const UNKNOWN_TAB = "UNKNOWN_TAB";
|
|
463
|
-
const UNKNOWN_TAB_REASON = "no such tab in this session";
|
|
464
|
-
/**
|
|
465
|
-
* Internal: a wait ended because its signal aborted. A tool never reports it:
|
|
466
|
-
* a stopped call ends as the tool runtime's abort, a takeover as `TAKEN_OVER`.
|
|
467
|
-
*/
|
|
468
|
-
const INTERRUPTED = "INTERRUPTED";
|
|
469
|
-
/** The browser did not start or could not be restarted. */
|
|
470
|
-
const BROWSER_UNAVAILABLE = "BROWSER_UNAVAILABLE";
|
|
471
|
-
function unknownTab() {
|
|
472
|
-
return {
|
|
473
|
-
url: "",
|
|
474
|
-
reason: UNKNOWN_TAB_REASON
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
//#endregion
|
|
478
589
|
//#region lib/types/tools/session.js
|
|
479
590
|
/** dsh shows the model only the message, so the message starts with the code. */
|
|
480
591
|
var BrowserToolError = class extends HarnessError {
|
|
@@ -694,6 +805,18 @@ function visit(value, depth, found) {
|
|
|
694
805
|
for (const item of Object.values(row)) visit(item, depth + 1, found);
|
|
695
806
|
}
|
|
696
807
|
//#endregion
|
|
808
|
+
//#region lib/types/tools/given.js
|
|
809
|
+
/** A string the caller gave. Blank is the same as omitting it; the text itself is kept. */
|
|
810
|
+
function givenString(value) {
|
|
811
|
+
if (typeof value !== "string" || value.trim() === "") return void 0;
|
|
812
|
+
return value;
|
|
813
|
+
}
|
|
814
|
+
/** Seconds to wait. Zero is the same as omitting it; any other number is kept. */
|
|
815
|
+
function givenTime(value) {
|
|
816
|
+
if (typeof value !== "number" || value === 0) return void 0;
|
|
817
|
+
return value;
|
|
818
|
+
}
|
|
819
|
+
//#endregion
|
|
697
820
|
//#region lib/types/tools/delivery.js
|
|
698
821
|
/** Every mh-browser tool result's `meta.by`; a result without it is not a delivery. */
|
|
699
822
|
const META_BY = "mh-browser";
|
|
@@ -1370,9 +1493,9 @@ const OPS = [
|
|
|
1370
1493
|
},
|
|
1371
1494
|
build: async (args) => ({
|
|
1372
1495
|
kind: "wait_for",
|
|
1373
|
-
...
|
|
1374
|
-
...
|
|
1375
|
-
...
|
|
1496
|
+
...stringField("text", args.text),
|
|
1497
|
+
...stringField("textGone", args.textGone),
|
|
1498
|
+
...timeField(args.time)
|
|
1376
1499
|
})
|
|
1377
1500
|
},
|
|
1378
1501
|
{
|
|
@@ -1399,7 +1522,7 @@ const OPS = [
|
|
|
1399
1522
|
filename: true,
|
|
1400
1523
|
build: async (args) => ({
|
|
1401
1524
|
kind: "snapshot",
|
|
1402
|
-
...
|
|
1525
|
+
...stringField("target", args.target),
|
|
1403
1526
|
...typeof args.depth === "number" ? { depth: args.depth } : {},
|
|
1404
1527
|
...args.boxes === true ? { boxes: true } : {}
|
|
1405
1528
|
})
|
|
@@ -1428,8 +1551,8 @@ const OPS = [
|
|
|
1428
1551
|
filename: true,
|
|
1429
1552
|
build: async (args) => ({
|
|
1430
1553
|
kind: "find",
|
|
1431
|
-
...
|
|
1432
|
-
...
|
|
1554
|
+
...stringField("text", args.text),
|
|
1555
|
+
...stringField("regex", args.regex),
|
|
1433
1556
|
...typeof args.maxResults === "number" ? { maxResults: args.maxResults } : {}
|
|
1434
1557
|
})
|
|
1435
1558
|
},
|
|
@@ -1473,7 +1596,7 @@ const OPS = [
|
|
|
1473
1596
|
build: async (args) => ({
|
|
1474
1597
|
kind: "take_screenshot",
|
|
1475
1598
|
...typeof args.element === "string" ? { element: args.element } : {},
|
|
1476
|
-
...
|
|
1599
|
+
...stringField("target", args.target),
|
|
1477
1600
|
...args.type === "png" || args.type === "jpeg" || args.type === "webp" ? { type: args.type } : {},
|
|
1478
1601
|
...args.fullPage === true ? { fullPage: true } : {},
|
|
1479
1602
|
...args.scale === "css" || args.scale === "device" ? { scale: args.scale } : {}
|
|
@@ -1507,7 +1630,7 @@ const OPS = [
|
|
|
1507
1630
|
function: string(args.function),
|
|
1508
1631
|
callId: callIdOf(exec),
|
|
1509
1632
|
...typeof args.element === "string" ? { element: args.element } : {},
|
|
1510
|
-
...
|
|
1633
|
+
...stringField("target", args.target)
|
|
1511
1634
|
})
|
|
1512
1635
|
}
|
|
1513
1636
|
];
|
|
@@ -1542,7 +1665,7 @@ function defineOperation(ctx, reader, spec) {
|
|
|
1542
1665
|
const target = writeTarget(ctx, session);
|
|
1543
1666
|
ctx.mhBrowser.noteWorkspace(session.id, target);
|
|
1544
1667
|
const input = args;
|
|
1545
|
-
const filename = spec.filename === true
|
|
1668
|
+
const filename = spec.filename === true ? givenString(input.filename) : void 0;
|
|
1546
1669
|
if (filename !== void 0) await assertInside(target, filename);
|
|
1547
1670
|
try {
|
|
1548
1671
|
const tab = tabOf(reader, session, args.tab);
|
|
@@ -1600,6 +1723,18 @@ function string(value) {
|
|
|
1600
1723
|
function stringList$1(value) {
|
|
1601
1724
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
1602
1725
|
}
|
|
1726
|
+
function stringField(key, value) {
|
|
1727
|
+
const given = givenString(value);
|
|
1728
|
+
if (given === void 0) return {};
|
|
1729
|
+
if (key === "target") return { target: given };
|
|
1730
|
+
if (key === "text") return { text: given };
|
|
1731
|
+
if (key === "textGone") return { textGone: given };
|
|
1732
|
+
return { regex: given };
|
|
1733
|
+
}
|
|
1734
|
+
function timeField(value) {
|
|
1735
|
+
const given = givenTime(value);
|
|
1736
|
+
return given === void 0 ? {} : { time: given };
|
|
1737
|
+
}
|
|
1603
1738
|
function optionalString(args, key) {
|
|
1604
1739
|
return typeof args[key] === "string" ? { element: args[key] } : {};
|
|
1605
1740
|
}
|
|
@@ -1911,6 +2046,8 @@ function takeoverText(taken) {
|
|
|
1911
2046
|
}
|
|
1912
2047
|
//#endregion
|
|
1913
2048
|
//#region lib/types/presence/register.js
|
|
2049
|
+
/** A presence read that stops answering. One stuck page must not hold the next step. */
|
|
2050
|
+
const CHECK_MS = 6e4;
|
|
1914
2051
|
const BROWSER_TOOLS = /* @__PURE__ */ new Set([
|
|
1915
2052
|
"browser_open",
|
|
1916
2053
|
"browser_invoke",
|
|
@@ -1931,14 +2068,17 @@ function registerPresence(ctx, reader) {
|
|
|
1931
2068
|
ctx.effect(() => () => {
|
|
1932
2069
|
lifetime.abort();
|
|
1933
2070
|
});
|
|
1934
|
-
const waitForChecks = async (agent) => {
|
|
2071
|
+
const waitForChecks = async (agent, signal) => {
|
|
1935
2072
|
let pending;
|
|
1936
|
-
while ((pending = tails.get(agent)) !== void 0)
|
|
2073
|
+
while ((pending = tails.get(agent)) !== void 0) {
|
|
2074
|
+
await untilAborted(pending, signal).catch(() => void 0);
|
|
2075
|
+
if (signal.aborted) return;
|
|
2076
|
+
}
|
|
1937
2077
|
};
|
|
1938
2078
|
const queue = (agent, urls) => {
|
|
1939
2079
|
const current = (tails.get(agent) ?? Promise.resolve()).then(async () => {
|
|
1940
2080
|
if (lifetime.signal.aborted) return;
|
|
1941
|
-
const reminder = await ctx.mhBrowser.presenceCheck(agent.session.id, urls, { signal: lifetime.signal });
|
|
2081
|
+
const reminder = await ctx.mhBrowser.presenceCheck(agent.session.id, urls, { signal: AbortSignal.any([lifetime.signal, AbortSignal.timeout(CHECK_MS)]) });
|
|
1942
2082
|
if (reminder === void 0 || lifetime.signal.aborted) return;
|
|
1943
2083
|
syncInbox(agent, reader.presence(agent.session), [], reminderMessage(reminder));
|
|
1944
2084
|
}).catch((error) => {
|
|
@@ -2015,7 +2155,7 @@ function registerPresence(ctx, reader) {
|
|
|
2015
2155
|
agents.set(agent.session.id, agent);
|
|
2016
2156
|
const handed = messages.some((message) => message.source.kind === "user") ? ctx.mhBrowser.takePersonHandback(agent.session.id) : void 0;
|
|
2017
2157
|
if (handed !== void 0 && handed.urls.length > 0) queue(agent, handed.urls);
|
|
2018
|
-
await waitForChecks(agent);
|
|
2158
|
+
await waitForChecks(agent, signal);
|
|
2019
2159
|
signal.throwIfAborted();
|
|
2020
2160
|
const desired = await compose(ctx.mhBrowser, agent, reader.presence(agent.session), messages, signal);
|
|
2021
2161
|
signal.throwIfAborted();
|
|
@@ -2159,11 +2299,10 @@ function record(value) {
|
|
|
2159
2299
|
return {};
|
|
2160
2300
|
}
|
|
2161
2301
|
function checkOptions(signal, extra) {
|
|
2162
|
-
const options = {};
|
|
2302
|
+
const options = { signal: capped(signal, CHECK_MS) };
|
|
2163
2303
|
if (extra?.baseline === true) options.baseline = true;
|
|
2164
2304
|
if (extra?.replaces === true) options.replaces = true;
|
|
2165
2305
|
if (extra?.force === true) options.force = true;
|
|
2166
|
-
if (signal !== void 0) options.signal = signal;
|
|
2167
2306
|
return options;
|
|
2168
2307
|
}
|
|
2169
2308
|
function canonical$1(url) {
|
|
@@ -3684,6 +3823,8 @@ async function dispatch(page, op, snapshot, signal) {
|
|
|
3684
3823
|
//#region lib/types/service/evaluate.js
|
|
3685
3824
|
/** How long requests the function started are still collected after it returns. */
|
|
3686
3825
|
const SETTLE_MS$1 = 50;
|
|
3826
|
+
/** A debugger call or a detach the page no longer answers. */
|
|
3827
|
+
const CALL_MS$2 = 15e3;
|
|
3687
3828
|
/**
|
|
3688
3829
|
* Run the expression in the page and collect write requests.
|
|
3689
3830
|
* A request is from this call when its initiator stack, including parents,
|
|
@@ -3693,7 +3834,8 @@ const SETTLE_MS$1 = 50;
|
|
|
3693
3834
|
*/
|
|
3694
3835
|
async function evaluateMarked(page, source, target, callId, signal, dialog) {
|
|
3695
3836
|
const marker = `mh-evaluate-${callId}-${randomUUID().slice(0, 8)}`;
|
|
3696
|
-
const
|
|
3837
|
+
const limit = capped(signal, CALL_MS$2);
|
|
3838
|
+
const client = await untilAborted(page.context().newCDPSession(page), limit);
|
|
3697
3839
|
const notes = /* @__PURE__ */ new Map();
|
|
3698
3840
|
const onRequest = (event) => {
|
|
3699
3841
|
const method = event.request?.method?.toUpperCase();
|
|
@@ -3713,10 +3855,10 @@ async function evaluateMarked(page, source, target, callId, signal, dialog) {
|
|
|
3713
3855
|
const stop = whenAborted(signal);
|
|
3714
3856
|
let dialogOpen = false;
|
|
3715
3857
|
try {
|
|
3716
|
-
await client.send("Network.enable");
|
|
3717
|
-
await client.send("Debugger.enable");
|
|
3718
|
-
await client.send("Debugger.setSkipAllPauses", { skip: true });
|
|
3719
|
-
await client.send("Debugger.setAsyncCallStackDepth", { maxDepth: 32 });
|
|
3858
|
+
await untilAborted(client.send("Network.enable"), limit);
|
|
3859
|
+
await untilAborted(client.send("Debugger.enable"), limit);
|
|
3860
|
+
await untilAborted(client.send("Debugger.setSkipAllPauses", { skip: true }), limit);
|
|
3861
|
+
await untilAborted(client.send("Debugger.setAsyncCallStackDepth", { maxDepth: 32 }), limit);
|
|
3720
3862
|
const evaluation = (target === void 0 ? page.evaluate(runSource, {
|
|
3721
3863
|
expr: source,
|
|
3722
3864
|
marker
|
|
@@ -3768,7 +3910,7 @@ async function evaluateMarked(page, source, target, callId, signal, dialog) {
|
|
|
3768
3910
|
stop.dispose();
|
|
3769
3911
|
client.off("Network.requestWillBeSent", onRequest);
|
|
3770
3912
|
const detached = client.detach().catch(() => void 0);
|
|
3771
|
-
if (!dialogOpen) await detached;
|
|
3913
|
+
if (!dialogOpen) await untilAborted(detached, capped(signal, CALL_MS$2)).catch(() => void 0);
|
|
3772
3914
|
}
|
|
3773
3915
|
}
|
|
3774
3916
|
/** One line per group: `POST https://site × 2 (this call)`. */
|
|
@@ -4166,6 +4308,8 @@ const REPLIED = "继续";
|
|
|
4166
4308
|
//#region lib/types/service/extension.js
|
|
4167
4309
|
const PACKAGE = "@mind-harness/mh-browser";
|
|
4168
4310
|
const WORKER_MS = 15e3;
|
|
4311
|
+
/** A service-worker call that does not answer. The caller's signal can end it sooner. */
|
|
4312
|
+
const CALL_MS$1 = 15e3;
|
|
4169
4313
|
/**
|
|
4170
4314
|
* Directory of the unpacked extension shipped with this package.
|
|
4171
4315
|
* Walks up from this module: sources live in `src/`, the bundle in `lib/`.
|
|
@@ -4205,8 +4349,9 @@ var Extension = class Extension {
|
|
|
4205
4349
|
return `chrome-extension://${this.id}/${name}`;
|
|
4206
4350
|
}
|
|
4207
4351
|
/** `chrome.tabs.create` in the named window. Returns the extension's tab id. */
|
|
4208
|
-
async createTab(windowId, url) {
|
|
4209
|
-
|
|
4352
|
+
async createTab(windowId, url, signal) {
|
|
4353
|
+
const worker = await this.worker(signal);
|
|
4354
|
+
return this.answer(worker.evaluate(async (arg) => {
|
|
4210
4355
|
const tab = await globalThis.chrome.tabs.create({
|
|
4211
4356
|
windowId: arg.windowId,
|
|
4212
4357
|
url: arg.url,
|
|
@@ -4217,23 +4362,25 @@ var Extension = class Extension {
|
|
|
4217
4362
|
}, {
|
|
4218
4363
|
windowId,
|
|
4219
4364
|
url
|
|
4220
|
-
});
|
|
4365
|
+
}), signal);
|
|
4221
4366
|
}
|
|
4222
|
-
async tabIdsIn(windowId) {
|
|
4223
|
-
|
|
4367
|
+
async tabIdsIn(windowId, signal) {
|
|
4368
|
+
const worker = await this.worker(signal);
|
|
4369
|
+
return this.answer(worker.evaluate(async (target) => {
|
|
4224
4370
|
return (await globalThis.chrome.tabs.query({ windowId: target })).flatMap((tab) => tab.id === void 0 ? [] : [tab.id]);
|
|
4225
|
-
}, windowId);
|
|
4371
|
+
}, windowId), signal);
|
|
4226
4372
|
}
|
|
4227
|
-
async activate(tabId) {
|
|
4228
|
-
|
|
4373
|
+
async activate(tabId, signal) {
|
|
4374
|
+
const worker = await this.worker(signal);
|
|
4375
|
+
await this.answer(worker.evaluate(async (id) => {
|
|
4229
4376
|
await globalThis.chrome.tabs.update(id, { active: true });
|
|
4230
|
-
}, tabId);
|
|
4377
|
+
}, tabId), signal);
|
|
4231
4378
|
}
|
|
4232
|
-
async group(tabId, target) {
|
|
4233
|
-
const worker = await this.worker();
|
|
4379
|
+
async group(tabId, target, signal) {
|
|
4380
|
+
const worker = await this.worker(signal);
|
|
4234
4381
|
if ("groupId" in target) {
|
|
4235
4382
|
const groupId = target.groupId;
|
|
4236
|
-
return worker.evaluate(async (arg) => {
|
|
4383
|
+
return this.answer(worker.evaluate(async (arg) => {
|
|
4237
4384
|
return globalThis.chrome.tabs.group({
|
|
4238
4385
|
tabIds: [arg.tabId],
|
|
4239
4386
|
groupId: arg.groupId
|
|
@@ -4241,10 +4388,10 @@ var Extension = class Extension {
|
|
|
4241
4388
|
}, {
|
|
4242
4389
|
tabId,
|
|
4243
4390
|
groupId
|
|
4244
|
-
});
|
|
4391
|
+
}), signal);
|
|
4245
4392
|
}
|
|
4246
4393
|
const windowId = target.windowId;
|
|
4247
|
-
return worker.evaluate(async (arg) => {
|
|
4394
|
+
return this.answer(worker.evaluate(async (arg) => {
|
|
4248
4395
|
return globalThis.chrome.tabs.group({
|
|
4249
4396
|
tabIds: [arg.tabId],
|
|
4250
4397
|
createProperties: { windowId: arg.windowId }
|
|
@@ -4252,10 +4399,11 @@ var Extension = class Extension {
|
|
|
4252
4399
|
}, {
|
|
4253
4400
|
tabId,
|
|
4254
4401
|
windowId
|
|
4255
|
-
});
|
|
4402
|
+
}), signal);
|
|
4256
4403
|
}
|
|
4257
|
-
async styleGroup(groupId, title, color) {
|
|
4258
|
-
|
|
4404
|
+
async styleGroup(groupId, title, color, signal) {
|
|
4405
|
+
const worker = await this.worker(signal);
|
|
4406
|
+
await this.answer(worker.evaluate(async (arg) => {
|
|
4259
4407
|
await globalThis.chrome.tabGroups.update(arg.groupId, {
|
|
4260
4408
|
title: arg.title,
|
|
4261
4409
|
color: arg.color
|
|
@@ -4264,17 +4412,21 @@ var Extension = class Extension {
|
|
|
4264
4412
|
groupId,
|
|
4265
4413
|
title,
|
|
4266
4414
|
color
|
|
4267
|
-
});
|
|
4415
|
+
}), signal);
|
|
4268
4416
|
}
|
|
4269
|
-
async ungroup(tabId) {
|
|
4270
|
-
|
|
4417
|
+
async ungroup(tabId, signal) {
|
|
4418
|
+
const worker = await this.worker(signal);
|
|
4419
|
+
await this.answer(worker.evaluate(async (id) => {
|
|
4271
4420
|
await globalThis.chrome.tabs.ungroup(id);
|
|
4272
|
-
}, tabId);
|
|
4421
|
+
}, tabId), signal);
|
|
4422
|
+
}
|
|
4423
|
+
answer(pending, signal) {
|
|
4424
|
+
return untilAborted(pending, capped(signal, CALL_MS$1));
|
|
4273
4425
|
}
|
|
4274
|
-
async worker() {
|
|
4426
|
+
async worker(signal) {
|
|
4275
4427
|
const current = this.workerPage;
|
|
4276
4428
|
if (current !== void 0 && this.context.serviceWorkers().includes(current)) return current;
|
|
4277
|
-
const worker = await findWorker(this.context, this.id);
|
|
4429
|
+
const worker = await untilAborted(findWorker(this.context, this.id), capped(signal, CALL_MS$1));
|
|
4278
4430
|
this.workerPage = worker;
|
|
4279
4431
|
return worker;
|
|
4280
4432
|
}
|
|
@@ -4300,6 +4452,10 @@ async function findWorker(context, id, timeoutMs = WORKER_MS) {
|
|
|
4300
4452
|
const TAB_CAP = 8;
|
|
4301
4453
|
/** How long a new window or tab may take to appear. */
|
|
4302
4454
|
const TARGET_MS = 5e3;
|
|
4455
|
+
/** A CDP or extension call that does not answer. The caller's signal can end it sooner. */
|
|
4456
|
+
const CALL_MS = 15e3;
|
|
4457
|
+
/** Closing a page the renderer no longer answers. */
|
|
4458
|
+
const CLOSE_MS = 5e3;
|
|
4303
4459
|
const WORLD = "mh-browser";
|
|
4304
4460
|
const BINDING = "mhBrowserInput";
|
|
4305
4461
|
const LISTENER = `(() => {
|
|
@@ -4379,7 +4535,10 @@ var Windows = class {
|
|
|
4379
4535
|
if (extension !== void 0) for (const page of launched) await page.goto(extension.pageUrl("welcome.html")).catch(() => void 0);
|
|
4380
4536
|
context.on("page", (page) => {
|
|
4381
4537
|
for (const blank of launched.splice(0)) blank.close().catch(() => void 0);
|
|
4382
|
-
this.ingest(page)
|
|
4538
|
+
this.ingest(page).catch((error) => {
|
|
4539
|
+
if (closedTarget(error)) return;
|
|
4540
|
+
throw error;
|
|
4541
|
+
});
|
|
4383
4542
|
});
|
|
4384
4543
|
}
|
|
4385
4544
|
setPersonInput(fn) {
|
|
@@ -4518,10 +4677,10 @@ var Windows = class {
|
|
|
4518
4677
|
return open.reduce((best, tab) => tab.touchedAt >= best.touchedAt ? tab : best);
|
|
4519
4678
|
}
|
|
4520
4679
|
/** Make this tab the current one in its window. Does not raise the browser. */
|
|
4521
|
-
async activate(tab) {
|
|
4680
|
+
async activate(tab, signal) {
|
|
4522
4681
|
const extension = this.extension;
|
|
4523
4682
|
if (extension === void 0 || tab.chromeTabId === void 0) return;
|
|
4524
|
-
await extension.activate(tab.chromeTabId);
|
|
4683
|
+
await extension.activate(tab.chromeTabId, signal);
|
|
4525
4684
|
}
|
|
4526
4685
|
/** Group title for the person. Empty until a title is known. */
|
|
4527
4686
|
setTitle(owner, title) {
|
|
@@ -4548,54 +4707,70 @@ var Windows = class {
|
|
|
4548
4707
|
* use the extension. Without a window, later pages use `window.open` from
|
|
4549
4708
|
* an isolated world so they stay in that window.
|
|
4550
4709
|
*/
|
|
4551
|
-
openBlank(owner) {
|
|
4710
|
+
openBlank(owner, signal) {
|
|
4552
4711
|
this.pendingCreates.set(owner, (this.pendingCreates.get(owner) ?? 0) + 1);
|
|
4553
|
-
const
|
|
4554
|
-
const run =
|
|
4712
|
+
const queued = enqueue(this.creating, () => this.createTab(owner, signal), signal);
|
|
4713
|
+
const run = queued.run.finally(() => {
|
|
4555
4714
|
const left = (this.pendingCreates.get(owner) ?? 1) - 1;
|
|
4556
4715
|
if (left <= 0) this.pendingCreates.delete(owner);
|
|
4557
4716
|
else this.pendingCreates.set(owner, left);
|
|
4558
4717
|
});
|
|
4559
|
-
this.creating =
|
|
4718
|
+
this.creating = queued.tail;
|
|
4560
4719
|
return run;
|
|
4561
4720
|
}
|
|
4562
|
-
async createTab(owner) {
|
|
4721
|
+
async createTab(owner, signal) {
|
|
4563
4722
|
const extension = this.extension;
|
|
4564
4723
|
if (extension === void 0) {
|
|
4565
|
-
const opener = await this.openerFor(owner);
|
|
4566
|
-
const page = await this.openTabIn(opener);
|
|
4567
|
-
return this.keep(owner, page, void 0);
|
|
4724
|
+
const opener = await this.openerFor(owner, signal);
|
|
4725
|
+
const page = await this.openTabIn(opener, signal);
|
|
4726
|
+
return this.keep(owner, page, void 0, void 0, signal);
|
|
4568
4727
|
}
|
|
4569
4728
|
const open = this.owned(owner);
|
|
4570
4729
|
if (open.length === 0) {
|
|
4571
|
-
const page = await this.openWindow();
|
|
4572
|
-
const windowId = await this.windowIdOf(page);
|
|
4573
|
-
const chromeTabId = await this.extensionTabIn(extension, windowId);
|
|
4574
|
-
const tab = await this.keep(owner, page, chromeTabId);
|
|
4575
|
-
this.groupTab(tab);
|
|
4730
|
+
const page = await this.openWindow(signal);
|
|
4731
|
+
const windowId = await this.windowIdOf(page, signal);
|
|
4732
|
+
const chromeTabId = await this.extensionTabIn(extension, windowId, signal);
|
|
4733
|
+
const tab = await this.keep(owner, page, chromeTabId, void 0, signal);
|
|
4734
|
+
this.groupTab(tab, signal);
|
|
4576
4735
|
return tab;
|
|
4577
4736
|
}
|
|
4578
4737
|
const anchor = open.reduce((best, tab) => tab.touchedAt >= best.touchedAt ? tab : best);
|
|
4579
|
-
const opened = await this.openTabViaExtension(anchor.windowId);
|
|
4580
|
-
const tab = await this.keep(owner, opened.page, opened.chromeTabId, anchor.windowId);
|
|
4581
|
-
this.groupTab(tab);
|
|
4738
|
+
const opened = await this.openTabViaExtension(anchor.windowId, signal);
|
|
4739
|
+
const tab = await this.keep(owner, opened.page, opened.chromeTabId, anchor.windowId, signal);
|
|
4740
|
+
this.groupTab(tab, signal);
|
|
4582
4741
|
return tab;
|
|
4583
4742
|
}
|
|
4584
|
-
async keep(owner, page, chromeTabId, windowId) {
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4743
|
+
async keep(owner, page, chromeTabId, windowId, signal) {
|
|
4744
|
+
let tab;
|
|
4745
|
+
try {
|
|
4746
|
+
tab = {
|
|
4747
|
+
id: await this.allocate(),
|
|
4748
|
+
owner,
|
|
4749
|
+
page,
|
|
4750
|
+
targetId: await this.targetIdOf(page, signal),
|
|
4751
|
+
windowId: windowId ?? await this.windowIdOf(page, signal),
|
|
4752
|
+
chromeTabId,
|
|
4753
|
+
url: page.url(),
|
|
4754
|
+
touchedAt: Date.now(),
|
|
4755
|
+
...idleTab()
|
|
4756
|
+
};
|
|
4757
|
+
this.remember(tab);
|
|
4758
|
+
await this.arm(page, owner, tab.id, signal);
|
|
4759
|
+
return tab;
|
|
4760
|
+
} catch (error) {
|
|
4761
|
+
if (tab !== void 0 && this.byPage.get(page) === tab) await this.closePage(tab);
|
|
4762
|
+
else await this.shutdown(page);
|
|
4763
|
+
throw error;
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
/** The page from a create, or close it when this wait is abandoned. */
|
|
4767
|
+
async claimedPage(pending, signal) {
|
|
4768
|
+
try {
|
|
4769
|
+
return await untilAborted(pending, capped(signal, TARGET_MS));
|
|
4770
|
+
} catch (error) {
|
|
4771
|
+
pending.then((page) => this.shutdown(page)).catch(() => void 0);
|
|
4772
|
+
throw error;
|
|
4773
|
+
}
|
|
4599
4774
|
}
|
|
4600
4775
|
/**
|
|
4601
4776
|
* Close one tab. `note` is listed in the next `<tabs>` block. A tab the
|
|
@@ -4609,9 +4784,12 @@ var Windows = class {
|
|
|
4609
4784
|
}
|
|
4610
4785
|
await this.closePage(tab);
|
|
4611
4786
|
}
|
|
4612
|
-
async closeOwner(owner) {
|
|
4613
|
-
for (const tab of this.owned(owner))
|
|
4614
|
-
|
|
4787
|
+
async closeOwner(owner, signal) {
|
|
4788
|
+
for (const tab of this.owned(owner)) {
|
|
4789
|
+
if (signal?.aborted === true) break;
|
|
4790
|
+
await this.closePage(tab);
|
|
4791
|
+
}
|
|
4792
|
+
if (signal?.aborted !== true) await this.closeOpener(owner);
|
|
4615
4793
|
this.closedNotes.delete(owner);
|
|
4616
4794
|
for (const [id, last] of this.lastUrls) if (last.owner === owner) this.lastUrls.delete(id);
|
|
4617
4795
|
this.groups.delete(owner);
|
|
@@ -4629,7 +4807,7 @@ var Windows = class {
|
|
|
4629
4807
|
}
|
|
4630
4808
|
this.serviceClose.add(tab.page);
|
|
4631
4809
|
this.forget(tab);
|
|
4632
|
-
if (!tab.page.isClosed()) await tab.page
|
|
4810
|
+
if (!tab.page.isClosed()) await this.shutdown(tab.page);
|
|
4633
4811
|
}
|
|
4634
4812
|
/** A tab leaves the registry. The window closes with its last tab. */
|
|
4635
4813
|
forget(tab) {
|
|
@@ -4657,7 +4835,7 @@ var Windows = class {
|
|
|
4657
4835
|
session?.detach().catch(() => void 0);
|
|
4658
4836
|
if (opener.isClosed()) return;
|
|
4659
4837
|
this.serviceClose.add(opener);
|
|
4660
|
-
await
|
|
4838
|
+
await this.shutdown(opener);
|
|
4661
4839
|
}
|
|
4662
4840
|
/** Close least-recently touched idle tabs until the owner is within the cap. */
|
|
4663
4841
|
async enforceCap(owner) {
|
|
@@ -4704,57 +4882,58 @@ var Windows = class {
|
|
|
4704
4882
|
await writeCounter(this.profileDir, this.nextId);
|
|
4705
4883
|
return id;
|
|
4706
4884
|
}
|
|
4707
|
-
async openerFor(owner) {
|
|
4885
|
+
async openerFor(owner, signal) {
|
|
4708
4886
|
const existing = this.openers.get(owner);
|
|
4709
4887
|
if (existing !== void 0 && !existing.isClosed()) return existing;
|
|
4710
|
-
const opener = await this.openWindow();
|
|
4888
|
+
const opener = await this.openWindow(signal);
|
|
4711
4889
|
this.openers.set(owner, opener);
|
|
4712
|
-
await this.arm(opener, owner, void 0);
|
|
4890
|
+
await this.arm(opener, owner, void 0, signal);
|
|
4713
4891
|
return opener;
|
|
4714
4892
|
}
|
|
4715
|
-
async openWindow() {
|
|
4893
|
+
async openWindow(signal) {
|
|
4716
4894
|
const session = this.browserSession;
|
|
4717
4895
|
if (session === void 0) throw new Error("browser is not running");
|
|
4718
|
-
const created = await this.createWindowTarget(session);
|
|
4719
|
-
|
|
4896
|
+
const created = await this.createWindowTarget(session, signal);
|
|
4897
|
+
const pending = this.waitForTarget(created.targetId);
|
|
4898
|
+
return this.claimedPage(pending, signal);
|
|
4720
4899
|
}
|
|
4721
4900
|
/** Headed windows pass `focus: false`. A browser that rejects it is opened without the flag. */
|
|
4722
|
-
async createWindowTarget(session) {
|
|
4901
|
+
async createWindowTarget(session, signal) {
|
|
4723
4902
|
const base = {
|
|
4724
4903
|
url: "about:blank",
|
|
4725
4904
|
newWindow: true
|
|
4726
4905
|
};
|
|
4727
|
-
if (!this.headed) return
|
|
4906
|
+
if (!this.headed) return this.cdp(session, "Target.createTarget", base, signal);
|
|
4728
4907
|
try {
|
|
4729
|
-
return await
|
|
4908
|
+
return await this.cdp(session, "Target.createTarget", {
|
|
4730
4909
|
...base,
|
|
4731
4910
|
focus: false
|
|
4732
|
-
});
|
|
4733
|
-
} catch {
|
|
4734
|
-
|
|
4911
|
+
}, signal);
|
|
4912
|
+
} catch (error) {
|
|
4913
|
+
if (signal?.aborted === true || isInterrupted(error)) throw error;
|
|
4914
|
+
return this.cdp(session, "Target.createTarget", base, signal);
|
|
4735
4915
|
}
|
|
4736
4916
|
}
|
|
4737
|
-
async openTabIn(anchor) {
|
|
4738
|
-
|
|
4739
|
-
if (context === void 0) throw new Error("browser is not running");
|
|
4917
|
+
async openTabIn(anchor, signal) {
|
|
4918
|
+
if (this.context === void 0) throw new Error("browser is not running");
|
|
4740
4919
|
const claim = this.claimBlank();
|
|
4741
4920
|
try {
|
|
4742
|
-
const session = await
|
|
4921
|
+
const session = await this.pageSession(anchor, signal);
|
|
4743
4922
|
try {
|
|
4744
|
-
const tree = await
|
|
4745
|
-
const world = await
|
|
4923
|
+
const tree = await this.cdp(session, "Page.getFrameTree", void 0, signal);
|
|
4924
|
+
const world = await this.cdp(session, "Page.createIsolatedWorld", {
|
|
4746
4925
|
frameId: tree.frameTree.frame.id,
|
|
4747
4926
|
worldName: WORLD,
|
|
4748
4927
|
grantUniveralAccess: true
|
|
4749
|
-
});
|
|
4750
|
-
await
|
|
4928
|
+
}, signal);
|
|
4929
|
+
await this.cdp(session, "Runtime.evaluate", {
|
|
4751
4930
|
expression: `window.open(${JSON.stringify(claim.url)},'_blank','noopener,noreferrer')`,
|
|
4752
4931
|
contextId: world.executionContextId
|
|
4753
|
-
});
|
|
4932
|
+
}, signal);
|
|
4754
4933
|
} finally {
|
|
4755
|
-
await
|
|
4934
|
+
await this.shutdownSession(session);
|
|
4756
4935
|
}
|
|
4757
|
-
return await claim.page;
|
|
4936
|
+
return await this.claimedPage(claim.page, signal);
|
|
4758
4937
|
} finally {
|
|
4759
4938
|
claim.drop();
|
|
4760
4939
|
}
|
|
@@ -4797,22 +4976,23 @@ var Windows = class {
|
|
|
4797
4976
|
};
|
|
4798
4977
|
}
|
|
4799
4978
|
/** The extension's id for the only tab in a window just opened. */
|
|
4800
|
-
async extensionTabIn(extension, windowId) {
|
|
4979
|
+
async extensionTabIn(extension, windowId, signal) {
|
|
4801
4980
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
4802
|
-
|
|
4981
|
+
if (signal?.aborted === true) return void 0;
|
|
4982
|
+
const id = (await extension.tabIdsIn(windowId, signal))[0];
|
|
4803
4983
|
if (id !== void 0) return id;
|
|
4804
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4984
|
+
await untilAborted(new Promise((resolve) => setTimeout(resolve, 100)), signal).catch(() => void 0);
|
|
4805
4985
|
}
|
|
4806
4986
|
}
|
|
4807
4987
|
/** Ask the extension to open a blank tab in `windowId`, and claim it. */
|
|
4808
|
-
async openTabViaExtension(windowId) {
|
|
4988
|
+
async openTabViaExtension(windowId, signal) {
|
|
4809
4989
|
const extension = this.extension;
|
|
4810
4990
|
if (extension === void 0) throw new Error("browser is not running");
|
|
4811
4991
|
const claim = this.claimBlank();
|
|
4812
4992
|
try {
|
|
4813
|
-
const chromeTabId = await extension.createTab(windowId, claim.url);
|
|
4993
|
+
const chromeTabId = await extension.createTab(windowId, claim.url, signal);
|
|
4814
4994
|
return {
|
|
4815
|
-
page: await claim.page,
|
|
4995
|
+
page: await this.claimedPage(claim.page, signal),
|
|
4816
4996
|
chromeTabId
|
|
4817
4997
|
};
|
|
4818
4998
|
} finally {
|
|
@@ -4820,28 +5000,29 @@ var Windows = class {
|
|
|
4820
5000
|
}
|
|
4821
5001
|
}
|
|
4822
5002
|
/** Put a Session's tab in its group. The person's window is not a Session and stays ungrouped. */
|
|
4823
|
-
async groupTab(tab) {
|
|
5003
|
+
async groupTab(tab, signal) {
|
|
4824
5004
|
const extension = this.extension;
|
|
4825
5005
|
const chromeTabId = tab.chromeTabId;
|
|
4826
5006
|
if (extension === void 0 || chromeTabId === void 0 || tab.owner === "person") return;
|
|
4827
5007
|
try {
|
|
4828
5008
|
const known = this.groups.get(tab.owner);
|
|
4829
|
-
if (known === void 0) this.groups.set(tab.owner, await extension.group(chromeTabId, { windowId: tab.windowId }));
|
|
5009
|
+
if (known === void 0) this.groups.set(tab.owner, await extension.group(chromeTabId, { windowId: tab.windowId }, signal));
|
|
4830
5010
|
else try {
|
|
4831
|
-
await extension.group(chromeTabId, { groupId: known });
|
|
4832
|
-
} catch {
|
|
4833
|
-
|
|
5011
|
+
await extension.group(chromeTabId, { groupId: known }, signal);
|
|
5012
|
+
} catch (error) {
|
|
5013
|
+
if (isInterrupted(error)) return;
|
|
5014
|
+
this.groups.set(tab.owner, await extension.group(chromeTabId, { windowId: tab.windowId }, signal));
|
|
4834
5015
|
}
|
|
4835
|
-
await this.applyGroupStyle(tab.owner);
|
|
5016
|
+
await this.applyGroupStyle(tab.owner, signal);
|
|
4836
5017
|
} catch {}
|
|
4837
5018
|
}
|
|
4838
|
-
async applyGroupStyle(owner) {
|
|
5019
|
+
async applyGroupStyle(owner, signal) {
|
|
4839
5020
|
const extension = this.extension;
|
|
4840
5021
|
const groupId = this.groups.get(owner);
|
|
4841
5022
|
if (extension === void 0 || groupId === void 0) return;
|
|
4842
5023
|
const title = this.titles.get(owner) ?? "";
|
|
4843
5024
|
const waiting = this.waiting.has(owner);
|
|
4844
|
-
await extension.styleGroup(groupId, waiting ? `${WAITING_TITLE}${title}` : title, waiting ? "red" : "blue");
|
|
5025
|
+
await extension.styleGroup(groupId, waiting ? `${WAITING_TITLE}${title}` : title, waiting ? "red" : "blue", signal);
|
|
4845
5026
|
}
|
|
4846
5027
|
async ingest(page) {
|
|
4847
5028
|
if (this.byPage.has(page)) return;
|
|
@@ -4863,6 +5044,10 @@ var Windows = class {
|
|
|
4863
5044
|
slot.accept(page);
|
|
4864
5045
|
return;
|
|
4865
5046
|
}
|
|
5047
|
+
if (page.url().startsWith("about:blank#mh-")) {
|
|
5048
|
+
await this.shutdown(page);
|
|
5049
|
+
return;
|
|
5050
|
+
}
|
|
4866
5051
|
const opener = await page.opener().catch(() => null);
|
|
4867
5052
|
const fromOpener = opener === null ? void 0 : this.byPage.get(opener);
|
|
4868
5053
|
if (fromOpener !== void 0) {
|
|
@@ -4904,10 +5089,9 @@ var Windows = class {
|
|
|
4904
5089
|
* or a close the service did not start counts as well. A page the content
|
|
4905
5090
|
* tab opened itself is not counted for its first navigation or its close.
|
|
4906
5091
|
*/
|
|
4907
|
-
async arm(page, owner, tab) {
|
|
5092
|
+
async arm(page, owner, tab, signal) {
|
|
4908
5093
|
if (this.armed.has(page)) return;
|
|
4909
|
-
|
|
4910
|
-
if (context === void 0) return;
|
|
5094
|
+
if (this.context === void 0) return;
|
|
4911
5095
|
this.armed.add(page);
|
|
4912
5096
|
page.on("close", () => {
|
|
4913
5097
|
const live = this.context;
|
|
@@ -4923,19 +5107,29 @@ var Windows = class {
|
|
|
4923
5107
|
const known = this.byPage.get(page);
|
|
4924
5108
|
if (known !== void 0) this.forget(known);
|
|
4925
5109
|
});
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
5110
|
+
let session;
|
|
5111
|
+
try {
|
|
5112
|
+
session = await this.pageSession(page, signal);
|
|
5113
|
+
this.sessions.set(page, session);
|
|
5114
|
+
await this.cdp(session, "Runtime.enable", void 0, signal);
|
|
5115
|
+
await this.cdp(session, "Page.enable", void 0, signal);
|
|
5116
|
+
await this.cdp(session, "Runtime.addBinding", {
|
|
5117
|
+
name: BINDING,
|
|
5118
|
+
executionContextName: WORLD
|
|
5119
|
+
}, signal);
|
|
5120
|
+
await this.cdp(session, "Page.addScriptToEvaluateOnNewDocument", {
|
|
5121
|
+
source: LISTENER,
|
|
5122
|
+
worldName: WORLD,
|
|
5123
|
+
runImmediately: true
|
|
5124
|
+
}, signal);
|
|
5125
|
+
} catch (error) {
|
|
5126
|
+
if (closedTarget(error)) {
|
|
5127
|
+
const known = this.byPage.get(page);
|
|
5128
|
+
if (known !== void 0) this.forget(known);
|
|
5129
|
+
else this.dropSession(page);
|
|
5130
|
+
}
|
|
5131
|
+
throw error;
|
|
5132
|
+
}
|
|
4939
5133
|
session.on("Runtime.bindingCalled", () => {
|
|
4940
5134
|
this.report(owner, tab, page);
|
|
4941
5135
|
});
|
|
@@ -4979,23 +5173,46 @@ var Windows = class {
|
|
|
4979
5173
|
});
|
|
4980
5174
|
});
|
|
4981
5175
|
}
|
|
4982
|
-
|
|
4983
|
-
const
|
|
4984
|
-
|
|
4985
|
-
|
|
5176
|
+
dropSession(page) {
|
|
5177
|
+
const session = this.sessions.get(page);
|
|
5178
|
+
this.sessions.delete(page);
|
|
5179
|
+
session?.detach().catch(() => void 0);
|
|
5180
|
+
}
|
|
5181
|
+
async targetIdOf(page, signal) {
|
|
5182
|
+
const session = await this.pageSession(page, signal);
|
|
4986
5183
|
try {
|
|
4987
|
-
return (await
|
|
5184
|
+
return (await this.cdp(session, "Target.getTargetInfo", void 0, signal)).targetInfo.targetId;
|
|
4988
5185
|
} finally {
|
|
4989
|
-
await
|
|
5186
|
+
await this.shutdownSession(session);
|
|
4990
5187
|
}
|
|
4991
5188
|
}
|
|
4992
|
-
async windowIdOf(page) {
|
|
5189
|
+
async windowIdOf(page, signal) {
|
|
4993
5190
|
const session = this.browserSession;
|
|
4994
5191
|
if (session === void 0) throw new Error("browser is not running");
|
|
4995
|
-
const targetId = await this.targetIdOf(page);
|
|
4996
|
-
return (await
|
|
5192
|
+
const targetId = await this.targetIdOf(page, signal);
|
|
5193
|
+
return (await this.cdp(session, "Browser.getWindowForTarget", { targetId }, signal)).windowId;
|
|
5194
|
+
}
|
|
5195
|
+
pageSession(page, signal) {
|
|
5196
|
+
const context = this.context;
|
|
5197
|
+
if (context === void 0) return Promise.reject(/* @__PURE__ */ new Error("browser is not running"));
|
|
5198
|
+
return untilAborted(context.newCDPSession(page), capped(signal, CALL_MS));
|
|
5199
|
+
}
|
|
5200
|
+
cdp(session, method, params, signal) {
|
|
5201
|
+
const send = session.send.bind(session);
|
|
5202
|
+
return untilAborted(params === void 0 ? send(method) : send(method, params), capped(signal, CALL_MS));
|
|
5203
|
+
}
|
|
5204
|
+
/** Close a page that may no longer answer. A stall ends at `CLOSE_MS`. */
|
|
5205
|
+
shutdown(page) {
|
|
5206
|
+
return untilAborted(page.close(), AbortSignal.timeout(CLOSE_MS)).catch(() => void 0);
|
|
5207
|
+
}
|
|
5208
|
+
shutdownSession(session) {
|
|
5209
|
+
return untilAborted(session.detach(), AbortSignal.timeout(CLOSE_MS)).catch(() => void 0);
|
|
4997
5210
|
}
|
|
4998
5211
|
};
|
|
5212
|
+
/** Playwright's error once the page, its context, or the browser has gone. */
|
|
5213
|
+
function closedTarget(error) {
|
|
5214
|
+
return error instanceof Error && (error.message.includes("has been closed") || error.message.includes("Target closed"));
|
|
5215
|
+
}
|
|
4999
5216
|
function idleTab() {
|
|
5000
5217
|
return {
|
|
5001
5218
|
title: "",
|
|
@@ -5082,6 +5299,10 @@ const RELAUNCH_WINDOW_MS = 6e4;
|
|
|
5082
5299
|
const DOCUMENT_CAP = 64;
|
|
5083
5300
|
const OPEN_ADDRESS_REASON = "only http and https addresses are opened";
|
|
5084
5301
|
const STOP_WAIT_MS = 5e3;
|
|
5302
|
+
/** A page read the renderer no longer answers. The caller's signal can end it sooner. */
|
|
5303
|
+
const READ_MS = 15e3;
|
|
5304
|
+
/** Closing one Session's windows must not wait on a renderer that stopped answering. */
|
|
5305
|
+
const DISPOSE_MS = 3e4;
|
|
5085
5306
|
const WAITING_STOPPED = "stopped while waiting";
|
|
5086
5307
|
const STOPPED = Symbol("stopped");
|
|
5087
5308
|
const NAVIGATED_DURING_EVALUATE = "The page navigated before the function returned.";
|
|
@@ -5162,7 +5383,7 @@ let MhBrowserService = (() => {
|
|
|
5162
5383
|
ctx.on("session/disposed", (session) => {
|
|
5163
5384
|
const owner = session.id;
|
|
5164
5385
|
if (owner === void 0) return;
|
|
5165
|
-
this.exclusive(owner, () => this.windows.closeOwner(owner)).catch((error) => {
|
|
5386
|
+
this.exclusive(owner, () => this.windows.closeOwner(owner, AbortSignal.timeout(DISPOSE_MS)), AbortSignal.timeout(DISPOSE_MS)).catch((error) => {
|
|
5166
5387
|
ctx.logger.warn("closing the windows of %s failed: %o", owner, error);
|
|
5167
5388
|
});
|
|
5168
5389
|
this.book.forget(owner);
|
|
@@ -5184,22 +5405,30 @@ let MhBrowserService = (() => {
|
|
|
5184
5405
|
}
|
|
5185
5406
|
async open(owner, items, signal, options) {
|
|
5186
5407
|
const reveal = options?.reveal === true;
|
|
5187
|
-
|
|
5188
|
-
await this.
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5408
|
+
try {
|
|
5409
|
+
return await this.exclusive(owner, async () => {
|
|
5410
|
+
await this.up(owner === PERSON_OWNER);
|
|
5411
|
+
const views = [];
|
|
5412
|
+
for (const item of items) {
|
|
5413
|
+
if (signal?.aborted) {
|
|
5414
|
+
views.push({
|
|
5415
|
+
url: item.url,
|
|
5416
|
+
reason: WAITING_STOPPED
|
|
5417
|
+
});
|
|
5418
|
+
continue;
|
|
5419
|
+
}
|
|
5420
|
+
views.push(await this.openAddress(owner, item, signal, reveal));
|
|
5197
5421
|
}
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5422
|
+
await this.enforceCap(owner);
|
|
5423
|
+
return views;
|
|
5424
|
+
}, signal);
|
|
5425
|
+
} catch (error) {
|
|
5426
|
+
if (!isInterrupted(error)) throw error;
|
|
5427
|
+
return items.map((item) => ({
|
|
5428
|
+
url: item.url,
|
|
5429
|
+
reason: WAITING_STOPPED
|
|
5430
|
+
}));
|
|
5431
|
+
}
|
|
5203
5432
|
}
|
|
5204
5433
|
async view(owner, tab, signal) {
|
|
5205
5434
|
return this.exclusive(owner, async () => {
|
|
@@ -5211,8 +5440,8 @@ let MhBrowserService = (() => {
|
|
|
5211
5440
|
url: live.url,
|
|
5212
5441
|
reason: WAITING_STOPPED
|
|
5213
5442
|
};
|
|
5214
|
-
return this.refreshView(live);
|
|
5215
|
-
});
|
|
5443
|
+
return this.refreshView(live, signal);
|
|
5444
|
+
}, signal);
|
|
5216
5445
|
}
|
|
5217
5446
|
/** Pages whose current representation is already in the model's presence. */
|
|
5218
5447
|
rememberPresence(owner, urls) {
|
|
@@ -5278,7 +5507,7 @@ let MhBrowserService = (() => {
|
|
|
5278
5507
|
text: planned.text,
|
|
5279
5508
|
record: planned.record
|
|
5280
5509
|
};
|
|
5281
|
-
});
|
|
5510
|
+
}, opts?.signal);
|
|
5282
5511
|
}
|
|
5283
5512
|
/**
|
|
5284
5513
|
* Where a download the page starts may be written: the session's writable
|
|
@@ -5320,7 +5549,7 @@ let MhBrowserService = (() => {
|
|
|
5320
5549
|
kind: "response",
|
|
5321
5550
|
finalUrl: response.url(),
|
|
5322
5551
|
status: response.status(),
|
|
5323
|
-
body: new Uint8Array(await response.body())
|
|
5552
|
+
body: new Uint8Array(await untilAborted(response.body(), capped(opts?.signal, READ_MS)))
|
|
5324
5553
|
};
|
|
5325
5554
|
const mediaType = headerMap["content-type"];
|
|
5326
5555
|
const etag = headerMap.etag;
|
|
@@ -5374,7 +5603,7 @@ let MhBrowserService = (() => {
|
|
|
5374
5603
|
try {
|
|
5375
5604
|
if (live.dialogHeld && op.kind !== "handle_dialog") throw new MhBrowserError("MODAL_STATE", "a dialog is open; use browser_handle_dialog", await this.blockedView(live));
|
|
5376
5605
|
if (live.fileChooser !== void 0 && op.kind !== "file_upload") {
|
|
5377
|
-
const blocked = await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" });
|
|
5606
|
+
const blocked = await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" }, signal);
|
|
5378
5607
|
blocked.dialog = {
|
|
5379
5608
|
type: "filechooser",
|
|
5380
5609
|
message: live.fileChooser.isMultiple() ? "choose files (several allowed) with browser_file_upload" : "choose one file with browser_file_upload"
|
|
@@ -5387,8 +5616,8 @@ let MhBrowserService = (() => {
|
|
|
5387
5616
|
else this.windows.holdInput(live);
|
|
5388
5617
|
} else if (outcome.navigated) this.windows.onNavigated(live);
|
|
5389
5618
|
this.windows.touch(live);
|
|
5390
|
-
const shown = outcome.navigated ? await this.discover(live, live.page.url(), null) : await this.refreshSnapshot(live);
|
|
5391
|
-
const scoped = op.kind === "snapshot" && (op.target !== void 0 || op.depth !== void 0 || op.boxes === true) ? await this.scopedSnapshot(live, op) : void 0;
|
|
5619
|
+
const shown = outcome.navigated ? await this.discover(live, live.page.url(), null, signal) : await this.refreshSnapshot(live, signal);
|
|
5620
|
+
const scoped = op.kind === "snapshot" && (op.target !== void 0 || op.depth !== void 0 || op.boxes === true) ? await this.scopedSnapshot(live, op, signal) : void 0;
|
|
5392
5621
|
this.shown.set(live.id, shown);
|
|
5393
5622
|
this.windows.noteUrl(live, live.page.url());
|
|
5394
5623
|
const view = await this.pageFromShown(live, shown, {
|
|
@@ -5396,7 +5625,7 @@ let MhBrowserService = (() => {
|
|
|
5396
5625
|
...outcome.dialog !== void 0 ? { dialog: outcome.dialog } : {},
|
|
5397
5626
|
...outcome.incomplete !== void 0 ? { incomplete: outcome.incomplete } : {},
|
|
5398
5627
|
...outcome.waited !== void 0 ? { waited: outcome.waited } : {}
|
|
5399
|
-
});
|
|
5628
|
+
}, signal);
|
|
5400
5629
|
if (scoped !== void 0 && view.representation.kind === "snapshot") view.representation = {
|
|
5401
5630
|
...view.representation,
|
|
5402
5631
|
text: scoped
|
|
@@ -5413,12 +5642,12 @@ let MhBrowserService = (() => {
|
|
|
5413
5642
|
} catch (error) {
|
|
5414
5643
|
if (error instanceof MhBrowserError && error.code === "TAKEN_OVER") throw error;
|
|
5415
5644
|
if (this.control.ofTab(owner, tab) === "person") throw await this.takenOver(live);
|
|
5416
|
-
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" }));
|
|
5645
|
+
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" }, signal));
|
|
5417
5646
|
throw error;
|
|
5418
5647
|
} finally {
|
|
5419
5648
|
linked.release();
|
|
5420
5649
|
}
|
|
5421
|
-
});
|
|
5650
|
+
}, signal);
|
|
5422
5651
|
}
|
|
5423
5652
|
holdHandover(owner, tab) {
|
|
5424
5653
|
const live = this.windows.get(owner, tab);
|
|
@@ -5436,15 +5665,16 @@ let MhBrowserService = (() => {
|
|
|
5436
5665
|
const live = this.windows.get(owner, tab);
|
|
5437
5666
|
if (live === void 0 || live.dialogHeld) return void 0;
|
|
5438
5667
|
try {
|
|
5439
|
-
const
|
|
5668
|
+
const limit = capped(signal, READ_MS);
|
|
5669
|
+
const bytes = await untilAborted(live.page.screenshot({
|
|
5440
5670
|
type: "png",
|
|
5441
|
-
|
|
5442
|
-
});
|
|
5671
|
+
signal: limit
|
|
5672
|
+
}), limit);
|
|
5443
5673
|
return new Uint8Array(bytes);
|
|
5444
5674
|
} catch {
|
|
5445
5675
|
return;
|
|
5446
5676
|
}
|
|
5447
|
-
});
|
|
5677
|
+
}, signal);
|
|
5448
5678
|
}
|
|
5449
5679
|
invoke(owner, _page, call, opts) {
|
|
5450
5680
|
return this.exclusive(owner, async () => {
|
|
@@ -5461,7 +5691,7 @@ let MhBrowserService = (() => {
|
|
|
5461
5691
|
}, call);
|
|
5462
5692
|
await this.enforceCap(owner);
|
|
5463
5693
|
return sent;
|
|
5464
|
-
});
|
|
5694
|
+
}, opts?.signal);
|
|
5465
5695
|
}
|
|
5466
5696
|
/**
|
|
5467
5697
|
* Not queued: it opens no tab, and a long body must not hold up the
|
|
@@ -5493,11 +5723,11 @@ let MhBrowserService = (() => {
|
|
|
5493
5723
|
const live = this.windows.get(owner, req.tab);
|
|
5494
5724
|
if (live === void 0) throw new MhBrowserError(UNKNOWN_TAB, UNKNOWN_TAB_REASON);
|
|
5495
5725
|
this.windows.holdHandover(live);
|
|
5496
|
-
await this.windows.activate(live).catch(() => void 0);
|
|
5726
|
+
await this.windows.activate(live, signal).catch(() => void 0);
|
|
5497
5727
|
}
|
|
5498
5728
|
this.control.handover(owner, req.tab);
|
|
5499
5729
|
this.windows.markWaiting(owner, true);
|
|
5500
|
-
});
|
|
5730
|
+
}, signal);
|
|
5501
5731
|
}
|
|
5502
5732
|
/** Remember the Session title for its tab group. Empty when the host has none. */
|
|
5503
5733
|
noteSessionTitle(owner, title) {
|
|
@@ -5508,7 +5738,7 @@ let MhBrowserService = (() => {
|
|
|
5508
5738
|
await this.ready;
|
|
5509
5739
|
const live = this.windows.latest(owner);
|
|
5510
5740
|
if (live === void 0) return false;
|
|
5511
|
-
await live.page.bringToFront();
|
|
5741
|
+
await untilAborted(live.page.bringToFront(), AbortSignal.timeout(READ_MS));
|
|
5512
5742
|
return true;
|
|
5513
5743
|
}
|
|
5514
5744
|
/** The person asked to see the page this Session handed over. */
|
|
@@ -5519,11 +5749,12 @@ let MhBrowserService = (() => {
|
|
|
5519
5749
|
const named = this.control.namedTab(sessionId);
|
|
5520
5750
|
const live = (named === void 0 ? void 0 : this.windows.get(sessionId, named)) ?? this.windows.latest(sessionId);
|
|
5521
5751
|
if (live === void 0) return { status: "no-tab" };
|
|
5522
|
-
await live.page.bringToFront();
|
|
5752
|
+
await untilAborted(live.page.bringToFront(), capped(signal, READ_MS));
|
|
5523
5753
|
return { status: "revealed" };
|
|
5524
5754
|
}
|
|
5525
5755
|
/** The person answered. Returns the note, the operation count, and the named tab. */
|
|
5526
5756
|
async handBack(owner, note) {
|
|
5757
|
+
const signal = capped(void 0, READ_MS);
|
|
5527
5758
|
return this.exclusive(owner, async () => {
|
|
5528
5759
|
await this.ready;
|
|
5529
5760
|
const taken = this.control.finish(owner);
|
|
@@ -5532,14 +5763,14 @@ let MhBrowserService = (() => {
|
|
|
5532
5763
|
this.windows.markWaiting(owner, false);
|
|
5533
5764
|
const urls = this.presenceUrls(owner, taken.tabs);
|
|
5534
5765
|
for (const touch of this.personTouch) touch(owner, urls);
|
|
5535
|
-
const page = await this.snapshotNamed(owner, taken.tab);
|
|
5766
|
+
const page = await this.snapshotNamed(owner, taken.tab, signal);
|
|
5536
5767
|
return {
|
|
5537
5768
|
...taken,
|
|
5538
5769
|
urls,
|
|
5539
5770
|
...note !== void 0 && note !== "" ? { note } : {},
|
|
5540
5771
|
...page !== void 0 ? { page } : {}
|
|
5541
5772
|
};
|
|
5542
|
-
});
|
|
5773
|
+
}, signal);
|
|
5543
5774
|
}
|
|
5544
5775
|
/** A stopped handover gives control back. A finished hand-back is left alone. */
|
|
5545
5776
|
releaseIfHeld(owner) {
|
|
@@ -5581,7 +5812,7 @@ let MhBrowserService = (() => {
|
|
|
5581
5812
|
}
|
|
5582
5813
|
const opened = await this.openOne(owner, item.url, item.waitFor, signal, item.snapshot === true, reveal);
|
|
5583
5814
|
this.keepAgentView(opened);
|
|
5584
|
-
if (!wantsTab && opened.fresh && this.closable(owner, opened.live) && "representation" in opened.view && opened.view.representation.kind === "agent-view") {
|
|
5815
|
+
if (opened.live !== void 0 && !wantsTab && opened.fresh && this.closable(owner, opened.live) && "representation" in opened.view && opened.view.representation.kind === "agent-view") {
|
|
5585
5816
|
await this.windows.closeTab(opened.live);
|
|
5586
5817
|
const view = { ...opened.view };
|
|
5587
5818
|
delete view.tab;
|
|
@@ -5607,6 +5838,7 @@ let MhBrowserService = (() => {
|
|
|
5607
5838
|
}
|
|
5608
5839
|
/** A readable Agent View puts its origin on the list and keeps the document for the next read. */
|
|
5609
5840
|
keepAgentView(opened) {
|
|
5841
|
+
if (opened.live === void 0) return;
|
|
5610
5842
|
if (!("representation" in opened.view)) return;
|
|
5611
5843
|
const representation = opened.view.representation;
|
|
5612
5844
|
if (!(representation.kind === "agent-view" || representation.kind === "snapshot" && representation.agentView !== void 0)) return;
|
|
@@ -5738,7 +5970,7 @@ let MhBrowserService = (() => {
|
|
|
5738
5970
|
kind: "response",
|
|
5739
5971
|
finalUrl: current,
|
|
5740
5972
|
status,
|
|
5741
|
-
body: readsAsText(mediaType) ? new Uint8Array(await response.body()) : /* @__PURE__ */ new Uint8Array()
|
|
5973
|
+
body: readsAsText(mediaType) ? new Uint8Array(await untilAborted(response.body(), capped(signal, READ_MS))) : /* @__PURE__ */ new Uint8Array()
|
|
5742
5974
|
};
|
|
5743
5975
|
const etag = headerMap.etag;
|
|
5744
5976
|
const link = headerMap.link;
|
|
@@ -5751,8 +5983,20 @@ let MhBrowserService = (() => {
|
|
|
5751
5983
|
async openOne(owner, url, waitFor, signal, snapshot = false, reveal = false) {
|
|
5752
5984
|
assertWaitFor(waitFor);
|
|
5753
5985
|
const reused = this.idleTabOn(owner, url);
|
|
5754
|
-
const live = reused ?? await this.windows.openBlank(owner);
|
|
5755
5986
|
const fresh = reused === void 0;
|
|
5987
|
+
let live;
|
|
5988
|
+
try {
|
|
5989
|
+
live = reused ?? await this.windows.openBlank(owner, signal);
|
|
5990
|
+
} catch (error) {
|
|
5991
|
+
if (signal?.aborted === true || isInterrupted(error)) return {
|
|
5992
|
+
view: {
|
|
5993
|
+
url,
|
|
5994
|
+
reason: WAITING_STOPPED
|
|
5995
|
+
},
|
|
5996
|
+
fresh
|
|
5997
|
+
};
|
|
5998
|
+
throw error;
|
|
5999
|
+
}
|
|
5756
6000
|
const linked = this.control.link(owner, signal, live.id);
|
|
5757
6001
|
if (reveal && !linked.signal.aborted) await live.page.bringToFront().catch(() => void 0);
|
|
5758
6002
|
const nextDialog = this.windows.nextDialog(live);
|
|
@@ -5775,7 +6019,7 @@ let MhBrowserService = (() => {
|
|
|
5775
6019
|
nextDialog.cancel();
|
|
5776
6020
|
if (winner === "failed" && seenDialog === void 0) {
|
|
5777
6021
|
if (isDownloadStart(navigationError) || live.pendingDownload !== void 0) {
|
|
5778
|
-
const view = await this.pageFromShown(live, this.shown.get(live.id));
|
|
6022
|
+
const view = await this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
|
|
5779
6023
|
if (view.url === "" || view.url === "about:blank") view.url = canonical(url);
|
|
5780
6024
|
this.applyDocumentType(live, view);
|
|
5781
6025
|
return {
|
|
@@ -5826,8 +6070,8 @@ let MhBrowserService = (() => {
|
|
|
5826
6070
|
...optionalFragment(address),
|
|
5827
6071
|
snapshot: "",
|
|
5828
6072
|
relocate: false,
|
|
5829
|
-
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch)
|
|
5830
|
-
}) : await this.discover(live, address, response);
|
|
6073
|
+
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch, signal)
|
|
6074
|
+
}) : await this.discover(live, address, response, signal);
|
|
5831
6075
|
this.shown.set(live.id, shown);
|
|
5832
6076
|
const view = await this.pageFromShown(live, shown, {
|
|
5833
6077
|
...snapshot ? { surface: "snapshot" } : {},
|
|
@@ -5837,7 +6081,7 @@ let MhBrowserService = (() => {
|
|
|
5837
6081
|
} },
|
|
5838
6082
|
...load.dialog !== void 0 ? { dialog: load.dialog } : {},
|
|
5839
6083
|
...waited !== void 0 ? { waited } : {}
|
|
5840
|
-
});
|
|
6084
|
+
}, signal);
|
|
5841
6085
|
return {
|
|
5842
6086
|
view: await this.attachDownload(live, view, false),
|
|
5843
6087
|
fresh,
|
|
@@ -5872,7 +6116,7 @@ let MhBrowserService = (() => {
|
|
|
5872
6116
|
if (op.kind === "snapshot") return { navigated: false };
|
|
5873
6117
|
if (op.kind === "find") return {
|
|
5874
6118
|
navigated: false,
|
|
5875
|
-
summary: findInSnapshot(await this.aria(live), op)
|
|
6119
|
+
summary: findInSnapshot(await this.aria(live, signal), op)
|
|
5876
6120
|
};
|
|
5877
6121
|
if (op.kind === "evaluate") {
|
|
5878
6122
|
const snapshot = this.shown.get(live.id)?.snapshot ?? "";
|
|
@@ -6064,15 +6308,16 @@ let MhBrowserService = (() => {
|
|
|
6064
6308
|
};
|
|
6065
6309
|
}
|
|
6066
6310
|
/** A snapshot limited to a target, a depth, or boxes. Refs still resolve against the full one. */
|
|
6067
|
-
async scopedSnapshot(tab, op) {
|
|
6311
|
+
async scopedSnapshot(tab, op, signal) {
|
|
6068
6312
|
const options = {
|
|
6069
6313
|
mode: "ai",
|
|
6070
6314
|
...op.depth !== void 0 ? { depth: op.depth } : {},
|
|
6071
6315
|
...op.boxes === true ? { boxes: true } : {}
|
|
6072
6316
|
};
|
|
6073
6317
|
const snapshot = this.shown.get(tab.id)?.snapshot ?? "";
|
|
6074
|
-
|
|
6075
|
-
return tab.page.ariaSnapshot(options);
|
|
6318
|
+
const limit = capped(signal, READ_MS);
|
|
6319
|
+
if (op.target !== void 0) return untilAborted((await locate(tab.page, op.target, snapshot)).ariaSnapshot(options), limit);
|
|
6320
|
+
return untilAborted(tab.page.ariaSnapshot(options), limit);
|
|
6076
6321
|
}
|
|
6077
6322
|
/** A file chooser the action opened. The tab stays in use until it navigates. */
|
|
6078
6323
|
chooserHeld(live, chooser) {
|
|
@@ -6083,9 +6328,9 @@ let MhBrowserService = (() => {
|
|
|
6083
6328
|
message: chooser.isMultiple() ? "choose files (several allowed) with browser_file_upload" : "choose one file with browser_file_upload"
|
|
6084
6329
|
};
|
|
6085
6330
|
}
|
|
6086
|
-
async refreshSnapshot(tab) {
|
|
6331
|
+
async refreshSnapshot(tab, signal) {
|
|
6087
6332
|
const previous = this.shown.get(tab.id);
|
|
6088
|
-
const snapshot = tab.dialogHeld ? "" : await this.aria(tab);
|
|
6333
|
+
const snapshot = tab.dialogHeld ? "" : await this.aria(tab, signal);
|
|
6089
6334
|
if (previous === void 0) return {
|
|
6090
6335
|
pageUrl: canonical(tab.page.url()),
|
|
6091
6336
|
digest: "",
|
|
@@ -6120,20 +6365,20 @@ let MhBrowserService = (() => {
|
|
|
6120
6365
|
this.windows.endDispatch(page);
|
|
6121
6366
|
}
|
|
6122
6367
|
}
|
|
6123
|
-
async discover(tab, address, response) {
|
|
6124
|
-
const main = response === null ? void 0 : await mainDocument(response);
|
|
6368
|
+
async discover(tab, address, response, signal) {
|
|
6369
|
+
const main = response === null ? void 0 : await mainDocument(response, signal);
|
|
6125
6370
|
const relocated = main !== void 0 && !samePage(main.finalUrl, address);
|
|
6126
|
-
const domLinks = relocated ? [] : await readDomLinks(tab.page);
|
|
6371
|
+
const domLinks = relocated ? [] : await readDomLinks(tab.page, signal);
|
|
6127
6372
|
const previous = this.shown.get(tab.id);
|
|
6128
6373
|
return loadShown({
|
|
6129
6374
|
pageUrl: canonical(address),
|
|
6130
6375
|
...optionalFragment(address),
|
|
6131
|
-
snapshot: await this.aria(tab),
|
|
6376
|
+
snapshot: await this.aria(tab, signal),
|
|
6132
6377
|
...main !== void 0 && !relocated ? { main } : {},
|
|
6133
6378
|
...domLinks.length > 0 ? { domLinks } : {},
|
|
6134
6379
|
...previous !== void 0 ? { previous } : {},
|
|
6135
6380
|
relocate: relocated,
|
|
6136
|
-
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch)
|
|
6381
|
+
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch, signal)
|
|
6137
6382
|
});
|
|
6138
6383
|
}
|
|
6139
6384
|
fetchForRead(url, accept, ifNoneMatch, signal) {
|
|
@@ -6143,13 +6388,13 @@ let MhBrowserService = (() => {
|
|
|
6143
6388
|
...signal !== void 0 ? { signal } : {}
|
|
6144
6389
|
});
|
|
6145
6390
|
}
|
|
6146
|
-
async aria(tab) {
|
|
6147
|
-
return tab.page.ariaSnapshot({ mode: "ai" }).catch((error) => {
|
|
6391
|
+
async aria(tab, signal) {
|
|
6392
|
+
return untilAborted(tab.page.ariaSnapshot({ mode: "ai" }).catch((error) => {
|
|
6148
6393
|
this.ctx.logger.debug("aria snapshot failed: %s", errorText(error));
|
|
6149
6394
|
return "";
|
|
6150
|
-
});
|
|
6395
|
+
}), capped(signal, READ_MS));
|
|
6151
6396
|
}
|
|
6152
|
-
async pageFromShown(tab, shown, extra) {
|
|
6397
|
+
async pageFromShown(tab, shown, extra, signal) {
|
|
6153
6398
|
const current = shown ?? {
|
|
6154
6399
|
pageUrl: canonical(tab.url),
|
|
6155
6400
|
digest: "",
|
|
@@ -6157,10 +6402,10 @@ let MhBrowserService = (() => {
|
|
|
6157
6402
|
text: "",
|
|
6158
6403
|
snapshot: ""
|
|
6159
6404
|
};
|
|
6160
|
-
const title = tab.dialogHeld ? "" : await tab.page.title().catch((error) => {
|
|
6405
|
+
const title = tab.dialogHeld ? "" : await untilAborted(tab.page.title().catch((error) => {
|
|
6161
6406
|
this.ctx.logger.debug("page title failed: %s", errorText(error));
|
|
6162
6407
|
return "";
|
|
6163
|
-
});
|
|
6408
|
+
}), capped(signal, READ_MS));
|
|
6164
6409
|
this.windows.noteTitle(tab, title);
|
|
6165
6410
|
const view = {
|
|
6166
6411
|
tab: tab.id,
|
|
@@ -6205,7 +6450,7 @@ let MhBrowserService = (() => {
|
|
|
6205
6450
|
if (via !== void 0) return observationOf(url, via);
|
|
6206
6451
|
}
|
|
6207
6452
|
const existing = this.tabOn(owner, url);
|
|
6208
|
-
if (existing !== void 0) return observationOf(url, await this.refreshView(existing));
|
|
6453
|
+
if (existing !== void 0) return observationOf(url, await this.refreshView(existing, signal));
|
|
6209
6454
|
const opened = await this.openOne(owner, url, void 0, signal, false);
|
|
6210
6455
|
this.keepAgentView(opened);
|
|
6211
6456
|
const view = opened.view;
|
|
@@ -6229,22 +6474,22 @@ let MhBrowserService = (() => {
|
|
|
6229
6474
|
tabs.sort((left, right) => right.touchedAt - left.touchedAt);
|
|
6230
6475
|
return tabs[0];
|
|
6231
6476
|
}
|
|
6232
|
-
async refreshView(live) {
|
|
6477
|
+
async refreshView(live, signal) {
|
|
6233
6478
|
this.windows.touch(live);
|
|
6234
|
-
if (live.dialogHeld) return this.pageFromShown(live, this.shown.get(live.id));
|
|
6479
|
+
if (live.dialogHeld) return this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
|
|
6235
6480
|
const address = live.page.url();
|
|
6236
6481
|
const previous = this.shown.get(live.id);
|
|
6237
6482
|
const shown = await loadShown({
|
|
6238
6483
|
pageUrl: canonical(address),
|
|
6239
6484
|
...optionalFragment(address),
|
|
6240
|
-
snapshot: await this.aria(live),
|
|
6485
|
+
snapshot: await this.aria(live, signal),
|
|
6241
6486
|
...previous !== void 0 ? { previous } : {},
|
|
6242
6487
|
relocate: previous !== void 0 && !samePage(previous.pageUrl, address),
|
|
6243
|
-
fetch: (url, accept, ifNoneMatch) => this.fetchForRead(url, accept, ifNoneMatch)
|
|
6488
|
+
fetch: (url, accept, ifNoneMatch) => this.fetchForRead(url, accept, ifNoneMatch, signal)
|
|
6244
6489
|
});
|
|
6245
6490
|
this.shown.set(live.id, shown);
|
|
6246
6491
|
this.windows.noteUrl(live, address);
|
|
6247
|
-
return this.pageFromShown(live, shown);
|
|
6492
|
+
return this.pageFromShown(live, shown, void 0, signal);
|
|
6248
6493
|
}
|
|
6249
6494
|
/** The most recently touched idle tab on this address. A tab the person holds is left alone. */
|
|
6250
6495
|
idleTabOn(owner, url) {
|
|
@@ -6277,7 +6522,7 @@ let MhBrowserService = (() => {
|
|
|
6277
6522
|
const load = await waitForLoad(live.page, signal);
|
|
6278
6523
|
if (load.dialog !== void 0) this.windows.holdDialog(live);
|
|
6279
6524
|
this.windows.touch(live);
|
|
6280
|
-
const shown = await this.discover(live, live.page.url(), response);
|
|
6525
|
+
const shown = await this.discover(live, live.page.url(), response, signal);
|
|
6281
6526
|
this.shown.set(live.id, shown);
|
|
6282
6527
|
const view = await this.pageFromShown(live, shown, {
|
|
6283
6528
|
...load.load ? {} : { incomplete: {
|
|
@@ -6285,11 +6530,11 @@ let MhBrowserService = (() => {
|
|
|
6285
6530
|
pendingRequests: load.pendingRequests
|
|
6286
6531
|
} },
|
|
6287
6532
|
...load.dialog !== void 0 ? { dialog: load.dialog } : {}
|
|
6288
|
-
});
|
|
6533
|
+
}, signal);
|
|
6289
6534
|
return this.attachDownload(live, view, false);
|
|
6290
6535
|
} catch (error) {
|
|
6291
6536
|
if (isDownloadStart(error) || live.pendingDownload !== void 0) {
|
|
6292
|
-
const view = await this.pageFromShown(live, this.shown.get(live.id));
|
|
6537
|
+
const view = await this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
|
|
6293
6538
|
if (view.url === "" || view.url === "about:blank") view.url = canonical(url);
|
|
6294
6539
|
this.applyDocumentType(live, view);
|
|
6295
6540
|
return this.attachDownload(live, view, true);
|
|
@@ -6321,7 +6566,7 @@ let MhBrowserService = (() => {
|
|
|
6321
6566
|
return {
|
|
6322
6567
|
kind: "response",
|
|
6323
6568
|
status: response.status(),
|
|
6324
|
-
body: new Uint8Array(await response.body()),
|
|
6569
|
+
body: new Uint8Array(await untilAborted(response.body(), capped(signal, READ_MS))),
|
|
6325
6570
|
...location === void 0 || location === "" ? {} : { location },
|
|
6326
6571
|
...mediaType === void 0 ? {} : { mediaType }
|
|
6327
6572
|
};
|
|
@@ -6515,22 +6760,21 @@ let MhBrowserService = (() => {
|
|
|
6515
6760
|
}
|
|
6516
6761
|
return urls;
|
|
6517
6762
|
}
|
|
6518
|
-
async snapshotNamed(owner, tab) {
|
|
6763
|
+
async snapshotNamed(owner, tab, signal) {
|
|
6519
6764
|
if (tab === void 0) return void 0;
|
|
6520
6765
|
const live = this.windows.get(owner, tab);
|
|
6521
6766
|
if (live === void 0) return void 0;
|
|
6522
|
-
const shown = await this.refreshSnapshot(live);
|
|
6767
|
+
const shown = await this.refreshSnapshot(live, signal);
|
|
6523
6768
|
this.shown.set(live.id, shown);
|
|
6524
|
-
return this.pageFromShown(live, shown, { surface: "snapshot" });
|
|
6525
|
-
}
|
|
6526
|
-
exclusive(owner, op) {
|
|
6527
|
-
const
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
if (this.queues.get(owner) === tail) this.queues.delete(owner);
|
|
6769
|
+
return this.pageFromShown(live, shown, { surface: "snapshot" }, signal);
|
|
6770
|
+
}
|
|
6771
|
+
exclusive(owner, op, signal) {
|
|
6772
|
+
const queued = enqueue(this.queues.get(owner) ?? Promise.resolve(), op, signal);
|
|
6773
|
+
this.queues.set(owner, queued.tail);
|
|
6774
|
+
queued.tail.then(() => {
|
|
6775
|
+
if (this.queues.get(owner) === queued.tail) this.queues.delete(owner);
|
|
6532
6776
|
});
|
|
6533
|
-
return run;
|
|
6777
|
+
return queued.run;
|
|
6534
6778
|
}
|
|
6535
6779
|
};
|
|
6536
6780
|
})();
|
|
@@ -6578,24 +6822,24 @@ function fragmentOf(url) {
|
|
|
6578
6822
|
return;
|
|
6579
6823
|
}
|
|
6580
6824
|
}
|
|
6581
|
-
async function mainDocument(response) {
|
|
6825
|
+
async function mainDocument(response, signal) {
|
|
6582
6826
|
const headers = response.headers();
|
|
6583
6827
|
const mediaType = headers["content-type"];
|
|
6584
6828
|
const link = headers.link;
|
|
6585
6829
|
return {
|
|
6586
6830
|
status: response.status(),
|
|
6587
6831
|
finalUrl: response.url(),
|
|
6588
|
-
body: await response.text(),
|
|
6832
|
+
body: await untilAborted(response.text(), capped(signal, READ_MS)),
|
|
6589
6833
|
...mediaType !== void 0 ? { mediaType } : {},
|
|
6590
6834
|
...link !== void 0 ? { link } : {}
|
|
6591
6835
|
};
|
|
6592
6836
|
}
|
|
6593
|
-
async function readDomLinks(page) {
|
|
6594
|
-
return (await page.locator("link").evaluateAll((elements) => elements.map((element) => ({
|
|
6837
|
+
async function readDomLinks(page, signal) {
|
|
6838
|
+
return (await untilAborted(page.locator("link").evaluateAll((elements) => elements.map((element) => ({
|
|
6595
6839
|
rel: element.getAttribute("rel") ?? "",
|
|
6596
6840
|
href: element.getAttribute("href") ?? "",
|
|
6597
6841
|
title: element.getAttribute("title") ?? ""
|
|
6598
|
-
}))).catch(() => [])).filter((row) => row.rel !== "" && row.href !== "").map((row) => ({
|
|
6842
|
+
}))).catch(() => []), capped(signal, READ_MS))).filter((row) => row.rel !== "" && row.href !== "").map((row) => ({
|
|
6599
6843
|
rel: row.rel,
|
|
6600
6844
|
href: row.href,
|
|
6601
6845
|
...row.title !== "" ? { title: row.title } : {}
|
|
@@ -6868,9 +7112,10 @@ function browserHandoverTool(ctx, reader, headed = true) {
|
|
|
6868
7112
|
if (exec.signal.aborted) return quiet();
|
|
6869
7113
|
const session = sessionFor(ctx, exec);
|
|
6870
7114
|
try {
|
|
7115
|
+
const tab = givenString(args.tab);
|
|
6871
7116
|
await ctx.mhBrowser.handover(session.id, {
|
|
6872
7117
|
reason: args.reason,
|
|
6873
|
-
...
|
|
7118
|
+
...tab === void 0 ? {} : { tab }
|
|
6874
7119
|
}, exec.signal);
|
|
6875
7120
|
const answer = await askUntilAnswered(ctx, args.reason, exec, headed);
|
|
6876
7121
|
if (answer === ABORT || exec.signal.aborted) return quiet();
|
|
@@ -7014,6 +7259,7 @@ function browserInvokeTool(ctx, reader) {
|
|
|
7014
7259
|
if (resolved === void 0) throw new BrowserToolError("NOT_DELIVERED", `${NOT_DELIVERED_REASON}: ${args.page}`);
|
|
7015
7260
|
if ("ambiguous" in resolved) throw new BrowserToolError("INVALID_ARGS", `${args.page} is the id of more than one page shown to you; give the url of one: ${resolved.ambiguous.join(", ")}`);
|
|
7016
7261
|
const input = args.input === void 0 ? void 0 : asObject(args.input);
|
|
7262
|
+
const item = givenString(args.item);
|
|
7017
7263
|
try {
|
|
7018
7264
|
const sent = await ctx.mhBrowser.invoke(session.id, resolved.url, {
|
|
7019
7265
|
name: args.name,
|
|
@@ -7021,7 +7267,7 @@ function browserInvokeTool(ctx, reader) {
|
|
|
7021
7267
|
url: resolved.url,
|
|
7022
7268
|
text: resolved.text
|
|
7023
7269
|
},
|
|
7024
|
-
...
|
|
7270
|
+
...item === void 0 ? {} : { item },
|
|
7025
7271
|
...input === void 0 ? {} : { input }
|
|
7026
7272
|
}, {
|
|
7027
7273
|
signal: exec.signal,
|
|
@@ -7146,18 +7392,19 @@ function browserOpenTool(ctx, reader) {
|
|
|
7146
7392
|
}),
|
|
7147
7393
|
async execute(args, exec) {
|
|
7148
7394
|
if (args.pages.length < 1 || args.pages.length > 8) throw new BrowserToolError("INVALID_ARGS", "pages takes 1 to 8 entries");
|
|
7149
|
-
|
|
7150
|
-
const waitFor = page.waitFor;
|
|
7395
|
+
const items = args.pages.map((page) => {
|
|
7396
|
+
const waitFor = page.waitFor === void 0 ? void 0 : compactWait(page.waitFor);
|
|
7151
7397
|
if (waitFor !== void 0 && waitFor.text === void 0 && waitFor.textGone === void 0 && waitFor.time === void 0) throw new BrowserToolError("INVALID_ARGS", "waitFor needs text, textGone, or time");
|
|
7152
|
-
|
|
7398
|
+
return {
|
|
7399
|
+
url: page.url,
|
|
7400
|
+
...page.snapshot === true ? { snapshot: true } : {},
|
|
7401
|
+
...waitFor === void 0 ? {} : { waitFor }
|
|
7402
|
+
};
|
|
7403
|
+
});
|
|
7153
7404
|
const session = sessionFor(ctx, exec);
|
|
7154
7405
|
ctx.mhBrowser.noteWorkspace(session.id, writeTarget(ctx, session));
|
|
7155
7406
|
try {
|
|
7156
|
-
const pages = await ctx.mhBrowser.open(session.id,
|
|
7157
|
-
url: page.url,
|
|
7158
|
-
...page.snapshot === true ? { snapshot: true } : {},
|
|
7159
|
-
...page.waitFor === void 0 ? {} : { waitFor: compactWait(page.waitFor) }
|
|
7160
|
-
})), exec.signal);
|
|
7407
|
+
const pages = await ctx.mhBrowser.open(session.id, items, exec.signal);
|
|
7161
7408
|
const block = tabBlock(ctx, reader, session);
|
|
7162
7409
|
return {
|
|
7163
7410
|
text: `${renderOutcome(pages, void 0, true)}${block.suffix}`,
|
|
@@ -7174,10 +7421,13 @@ function browserOpenTool(ctx, reader) {
|
|
|
7174
7421
|
});
|
|
7175
7422
|
}
|
|
7176
7423
|
function compactWait(waitFor) {
|
|
7424
|
+
const text = givenString(waitFor.text);
|
|
7425
|
+
const textGone = givenString(waitFor.textGone);
|
|
7426
|
+
const time = givenTime(waitFor.time);
|
|
7177
7427
|
return {
|
|
7178
|
-
...
|
|
7179
|
-
...
|
|
7180
|
-
...
|
|
7428
|
+
...text === void 0 ? {} : { text },
|
|
7429
|
+
...textGone === void 0 ? {} : { textGone },
|
|
7430
|
+
...time === void 0 ? {} : { time }
|
|
7181
7431
|
};
|
|
7182
7432
|
}
|
|
7183
7433
|
function titleOf(name, url) {
|