@mind-harness/mh-browser 0.2.1 → 0.2.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/extension/welcome.css +94 -0
- package/extension/welcome.html +40 -0
- package/extension/welcome.js +15 -0
- package/lib/index.js +119 -36
- package/lib/typert.host.js +3 -3
- package/lib/typert.remote-client.js +1 -1
- package/lib/types/service/browser.d.ts +11 -0
- package/lib/types/service/extension.d.ts +5 -1
- package/lib/types/service/service.d.ts +4 -1
- package/lib/types/service/test-hooks.d.ts +4 -0
- package/lib/types/tools/handover.d.ts +1 -1
- package/lib/types/tools/register.d.ts +1 -1
- package/lib/types/words.d.ts +4 -0
- package/package.json +1 -1
package/extension/manifest.json
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/* Keep in step with the sign-in callback page in packages/mh-cloud/src/page.ts. */
|
|
2
|
+
:root {
|
|
3
|
+
--bg: #f4f4f6;
|
|
4
|
+
--card: #fff;
|
|
5
|
+
--stroke: rgba(0, 0, 0, 0.08);
|
|
6
|
+
--text: #18181b;
|
|
7
|
+
--muted: #6b6b73;
|
|
8
|
+
--accent: #4d6bfe;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@media (prefers-color-scheme: dark) {
|
|
12
|
+
:root {
|
|
13
|
+
--bg: #161618;
|
|
14
|
+
--card: #202023;
|
|
15
|
+
--stroke: rgba(255, 255, 255, 0.08);
|
|
16
|
+
--text: #ededf0;
|
|
17
|
+
--muted: #9a9aa3;
|
|
18
|
+
--accent: #7f95ff;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
* {
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
html,
|
|
27
|
+
body {
|
|
28
|
+
height: 100%;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
body {
|
|
32
|
+
margin: 0;
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
justify-content: center;
|
|
36
|
+
padding: 24px;
|
|
37
|
+
background: var(--bg);
|
|
38
|
+
color: var(--text);
|
|
39
|
+
font:
|
|
40
|
+
15px/1.6 -apple-system,
|
|
41
|
+
BlinkMacSystemFont,
|
|
42
|
+
'Segoe UI',
|
|
43
|
+
'PingFang SC',
|
|
44
|
+
'Hiragino Sans GB',
|
|
45
|
+
'Microsoft YaHei',
|
|
46
|
+
sans-serif;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.card {
|
|
50
|
+
width: 100%;
|
|
51
|
+
max-width: 420px;
|
|
52
|
+
padding: 36px 32px 28px;
|
|
53
|
+
border: 1px solid var(--stroke);
|
|
54
|
+
border-radius: 20px;
|
|
55
|
+
background: var(--card);
|
|
56
|
+
text-align: center;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.product {
|
|
60
|
+
margin: 0 0 24px;
|
|
61
|
+
font-size: 12px;
|
|
62
|
+
letter-spacing: 0.06em;
|
|
63
|
+
text-transform: uppercase;
|
|
64
|
+
color: var(--muted);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.mark {
|
|
68
|
+
width: 56px;
|
|
69
|
+
height: 56px;
|
|
70
|
+
margin: 0 auto 16px;
|
|
71
|
+
border-radius: 50%;
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
color: var(--accent);
|
|
76
|
+
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
h1 {
|
|
80
|
+
margin: 0 0 8px;
|
|
81
|
+
font-size: 20px;
|
|
82
|
+
font-weight: 600;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.line {
|
|
86
|
+
margin: 0 0 4px;
|
|
87
|
+
color: var(--muted);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.hint {
|
|
91
|
+
margin: 12px 0 0;
|
|
92
|
+
font-size: 13px;
|
|
93
|
+
color: var(--muted);
|
|
94
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="color-scheme" content="light dark" />
|
|
7
|
+
<title data-text="tab">Mind Harness browser</title>
|
|
8
|
+
<link rel="stylesheet" href="welcome.css" />
|
|
9
|
+
<script src="welcome.js" defer></script>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<main class="card">
|
|
13
|
+
<p class="product">Mind Harness</p>
|
|
14
|
+
<div class="mark" aria-hidden="true">
|
|
15
|
+
<svg
|
|
16
|
+
viewBox="0 0 24 24"
|
|
17
|
+
width="28"
|
|
18
|
+
height="28"
|
|
19
|
+
fill="none"
|
|
20
|
+
stroke="currentColor"
|
|
21
|
+
stroke-width="2.2"
|
|
22
|
+
stroke-linecap="round"
|
|
23
|
+
stroke-linejoin="round"
|
|
24
|
+
>
|
|
25
|
+
<rect x="3.5" y="5" width="17" height="14" rx="2.5" />
|
|
26
|
+
<path d="M3.5 9.5h17" />
|
|
27
|
+
</svg>
|
|
28
|
+
</div>
|
|
29
|
+
<h1 data-text="title">This is the Mind Harness browser</h1>
|
|
30
|
+
<p class="line" data-text="what">
|
|
31
|
+
The agent opens pages, reads them, and acts on them here. Sites you sign in to here stay
|
|
32
|
+
signed in for the agent.
|
|
33
|
+
</p>
|
|
34
|
+
<p class="line" data-text="keep">
|
|
35
|
+
Please keep this browser running. When the agent needs you, the conversation will tell you.
|
|
36
|
+
</p>
|
|
37
|
+
<p class="hint" data-text="closes">This window closes when the agent opens its first page.</p>
|
|
38
|
+
</main>
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const ZH = {
|
|
2
|
+
tab: 'Mind Harness 浏览器',
|
|
3
|
+
title: '这是 Mind\u00a0Harness 的浏览器',
|
|
4
|
+
what: 'Agent 在这里替你打开网页、读取内容、完成操作。你在这里登录过的网站,Agent 之后也能用上。',
|
|
5
|
+
keep: '请不要退出这个浏览器。需要你来接手时,对话里会提示你。',
|
|
6
|
+
closes: 'Agent 打开第一个网页后,这个窗口会自动关闭。',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (navigator.language.toLowerCase().startsWith('zh')) {
|
|
10
|
+
document.documentElement.lang = 'zh-CN'
|
|
11
|
+
for (const node of document.querySelectorAll('[data-text]')) {
|
|
12
|
+
const text = ZH[node.getAttribute('data-text')]
|
|
13
|
+
if (text !== undefined) node.textContent = text
|
|
14
|
+
}
|
|
15
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { assertSupportedJsonSchema, defineTool, validateJsonSchemaValue } from "
|
|
|
7
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
8
8
|
import { parse } from "yaml";
|
|
9
9
|
import { constants, readFileSync } from "node:fs";
|
|
10
|
-
import { lstat, mkdir, open, readFile, realpath, unlink, writeFile } from "node:fs/promises";
|
|
10
|
+
import { lstat, mkdir, open, readFile, readlink, realpath, unlink, writeFile } from "node:fs/promises";
|
|
11
11
|
import { z as z$1 } from "zod";
|
|
12
12
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
13
13
|
import { chromium } from "playwright";
|
|
@@ -2964,7 +2964,12 @@ async function launchBrowser(options) {
|
|
|
2964
2964
|
...options.headless ? {} : {
|
|
2965
2965
|
viewport: null,
|
|
2966
2966
|
ignoreDefaultArgs: ["--disable-extensions"],
|
|
2967
|
-
args: [
|
|
2967
|
+
args: [
|
|
2968
|
+
"--enable-unsafe-extension-debugging",
|
|
2969
|
+
"--keep-alive-for-test",
|
|
2970
|
+
"--disable-blink-features=AutomationControlled",
|
|
2971
|
+
"--test-type"
|
|
2972
|
+
]
|
|
2968
2973
|
},
|
|
2969
2974
|
...options.browser === "chromium" ? {} : { channel: options.browser },
|
|
2970
2975
|
...options.executablePath !== void 0 ? { executablePath: options.executablePath } : {}
|
|
@@ -2974,6 +2979,50 @@ async function launchBrowser(options) {
|
|
|
2974
2979
|
throw error;
|
|
2975
2980
|
}
|
|
2976
2981
|
}
|
|
2982
|
+
/**
|
|
2983
|
+
* Process id of the browser's main process. Playwright gives none for a
|
|
2984
|
+
* persistent context. Asks the browser first; on POSIX the profile's
|
|
2985
|
+
* `SingletonLock` link names it as `<host>-<pid>`.
|
|
2986
|
+
*/
|
|
2987
|
+
async function browserPid(context, profileDir) {
|
|
2988
|
+
const browser = context.browser();
|
|
2989
|
+
if (browser !== null) try {
|
|
2990
|
+
const session = await browser.newBrowserCDPSession();
|
|
2991
|
+
try {
|
|
2992
|
+
const main = (await session.send("SystemInfo.getProcessInfo")).processInfo.find((item) => item.type === "browser");
|
|
2993
|
+
if (main !== void 0 && main.id > 0) return main.id;
|
|
2994
|
+
} finally {
|
|
2995
|
+
await session.detach().catch(() => void 0);
|
|
2996
|
+
}
|
|
2997
|
+
} catch {}
|
|
2998
|
+
if (process.platform === "win32") return void 0;
|
|
2999
|
+
try {
|
|
3000
|
+
const target = await readlink(join(profileDir, "SingletonLock"));
|
|
3001
|
+
const pid = Number(target.slice(target.lastIndexOf("-") + 1));
|
|
3002
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
3003
|
+
} catch {
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
/**
|
|
3008
|
+
* Ask the browser to close; kill `pid` when that fails or takes longer than
|
|
3009
|
+
* `waitMs`, so a stuck browser does not keep the profile locked.
|
|
3010
|
+
*/
|
|
3011
|
+
async function closeOrKill(close, pid, waitMs = 1e3, kill = (target) => {
|
|
3012
|
+
process.kill(target, "SIGKILL");
|
|
3013
|
+
}) {
|
|
3014
|
+
let timer;
|
|
3015
|
+
const outcome = await Promise.race([close().then(() => "closed", () => "failed"), new Promise((resolve) => {
|
|
3016
|
+
timer = setTimeout(() => resolve("timeout"), waitMs);
|
|
3017
|
+
})]);
|
|
3018
|
+
clearTimeout(timer);
|
|
3019
|
+
if (outcome === "closed") return "closed";
|
|
3020
|
+
if (pid === void 0) return "stuck";
|
|
3021
|
+
try {
|
|
3022
|
+
kill(pid);
|
|
3023
|
+
} catch {}
|
|
3024
|
+
return "killed";
|
|
3025
|
+
}
|
|
2977
3026
|
//#endregion
|
|
2978
3027
|
//#region lib/types/service/control.js
|
|
2979
3028
|
/**
|
|
@@ -4107,10 +4156,16 @@ function handoverQuestion(reason) {
|
|
|
4107
4156
|
}
|
|
4108
4157
|
/** The option that hands the browser back. */
|
|
4109
4158
|
const HAND_BACK = "交回";
|
|
4159
|
+
/** The handover card when the browser has no window: the person cannot act in it. */
|
|
4160
|
+
function stepQuestion(reason) {
|
|
4161
|
+
return `需要你来:${reason}(浏览器没有窗口,这里做不了;写下怎么继续)`;
|
|
4162
|
+
}
|
|
4163
|
+
/** The option on that card when the person has nothing to add. */
|
|
4164
|
+
const REPLIED = "继续";
|
|
4110
4165
|
//#endregion
|
|
4111
4166
|
//#region lib/types/service/extension.js
|
|
4112
4167
|
const PACKAGE = "@mind-harness/mh-browser";
|
|
4113
|
-
const WORKER_MS =
|
|
4168
|
+
const WORKER_MS = 15e3;
|
|
4114
4169
|
/**
|
|
4115
4170
|
* Directory of the unpacked extension shipped with this package.
|
|
4116
4171
|
* Walks up from this module: sources live in `src/`, the bundle in `lib/`.
|
|
@@ -4145,6 +4200,10 @@ var Extension = class Extension {
|
|
|
4145
4200
|
const worker = await findWorker(context, loaded.id);
|
|
4146
4201
|
return new Extension(context, loaded.id, worker);
|
|
4147
4202
|
}
|
|
4203
|
+
/** Address of a page shipped in the extension directory. */
|
|
4204
|
+
pageUrl(name) {
|
|
4205
|
+
return `chrome-extension://${this.id}/${name}`;
|
|
4206
|
+
}
|
|
4148
4207
|
/** `chrome.tabs.create` in the named window. Returns the extension's tab id. */
|
|
4149
4208
|
async createTab(windowId, url) {
|
|
4150
4209
|
return (await this.worker()).evaluate(async (arg) => {
|
|
@@ -4220,13 +4279,21 @@ var Extension = class Extension {
|
|
|
4220
4279
|
return worker;
|
|
4221
4280
|
}
|
|
4222
4281
|
};
|
|
4223
|
-
|
|
4224
|
-
|
|
4282
|
+
/** The extension's service worker. Looks once more after the wait times out. */
|
|
4283
|
+
async function findWorker(context, id, timeoutMs = WORKER_MS) {
|
|
4284
|
+
const find = () => context.serviceWorkers().find((worker) => worker.url().includes(id));
|
|
4285
|
+
const open = find();
|
|
4225
4286
|
if (open !== void 0) return open;
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4287
|
+
try {
|
|
4288
|
+
return await context.waitForEvent("serviceworker", {
|
|
4289
|
+
predicate: (worker) => worker.url().includes(id),
|
|
4290
|
+
timeout: timeoutMs
|
|
4291
|
+
});
|
|
4292
|
+
} catch (error) {
|
|
4293
|
+
const late = find();
|
|
4294
|
+
if (late !== void 0) return late;
|
|
4295
|
+
throw new Error(`the browser extension did not start within ${String(timeoutMs / 1e3)} s`, { cause: error });
|
|
4296
|
+
}
|
|
4230
4297
|
}
|
|
4231
4298
|
//#endregion
|
|
4232
4299
|
//#region lib/types/service/windows.js
|
|
@@ -4308,6 +4375,8 @@ var Windows = class {
|
|
|
4308
4375
|
this.browserSession = await browser.newBrowserCDPSession();
|
|
4309
4376
|
if (options.headed) this.extension = await Extension.load(context, this.browserSession);
|
|
4310
4377
|
const launched = context.pages().filter((page) => page.url() === "about:blank");
|
|
4378
|
+
const extension = this.extension;
|
|
4379
|
+
if (extension !== void 0) for (const page of launched) await page.goto(extension.pageUrl("welcome.html")).catch(() => void 0);
|
|
4311
4380
|
context.on("page", (page) => {
|
|
4312
4381
|
for (const blank of launched.splice(0)) blank.close().catch(() => void 0);
|
|
4313
4382
|
this.ingest(page);
|
|
@@ -5055,6 +5124,7 @@ let MhBrowserService = (() => {
|
|
|
5055
5124
|
windows = (__runInitializers(this, _instanceExtraInitializers), new Windows());
|
|
5056
5125
|
launchOptions;
|
|
5057
5126
|
context;
|
|
5127
|
+
pids = /* @__PURE__ */ new WeakMap();
|
|
5058
5128
|
ready = Promise.resolve();
|
|
5059
5129
|
closing = false;
|
|
5060
5130
|
/** The browser is not running. The next call that needs a page starts it. */
|
|
@@ -5112,7 +5182,8 @@ let MhBrowserService = (() => {
|
|
|
5112
5182
|
this.agentViewOrigins.add(parsed.origin);
|
|
5113
5183
|
} catch {}
|
|
5114
5184
|
}
|
|
5115
|
-
async open(owner, items, signal) {
|
|
5185
|
+
async open(owner, items, signal, options) {
|
|
5186
|
+
const reveal = options?.reveal === true;
|
|
5116
5187
|
return this.exclusive(owner, async () => {
|
|
5117
5188
|
await this.up(owner === PERSON_OWNER);
|
|
5118
5189
|
const views = [];
|
|
@@ -5124,7 +5195,7 @@ let MhBrowserService = (() => {
|
|
|
5124
5195
|
});
|
|
5125
5196
|
continue;
|
|
5126
5197
|
}
|
|
5127
|
-
views.push(await this.openAddress(owner, item, signal));
|
|
5198
|
+
views.push(await this.openAddress(owner, item, signal, reveal));
|
|
5128
5199
|
}
|
|
5129
5200
|
await this.enforceCap(owner);
|
|
5130
5201
|
return views;
|
|
@@ -5287,7 +5358,9 @@ let MhBrowserService = (() => {
|
|
|
5287
5358
|
await (this.context?.browser())?.close();
|
|
5288
5359
|
},
|
|
5289
5360
|
page: (owner, tab) => this.windows.get(owner, tab)?.page,
|
|
5290
|
-
running: () => this.context !== void 0
|
|
5361
|
+
running: () => this.context !== void 0,
|
|
5362
|
+
pages: () => this.context?.pages() ?? [],
|
|
5363
|
+
pid: () => this.context === void 0 ? void 0 : this.pids.get(this.context)
|
|
5291
5364
|
};
|
|
5292
5365
|
}
|
|
5293
5366
|
async act(owner, tab, op, signal) {
|
|
@@ -5492,7 +5565,7 @@ let MhBrowserService = (() => {
|
|
|
5492
5565
|
* A readable Agent View found on a tab this call opened is returned without
|
|
5493
5566
|
* the tab, and that tab is not listed as closed.
|
|
5494
5567
|
*/
|
|
5495
|
-
async openAddress(owner, item, signal) {
|
|
5568
|
+
async openAddress(owner, item, signal, reveal = false) {
|
|
5496
5569
|
if (!openableAddress(item.url)) return {
|
|
5497
5570
|
url: item.url,
|
|
5498
5571
|
reason: OPEN_ADDRESS_REASON
|
|
@@ -5506,7 +5579,7 @@ let MhBrowserService = (() => {
|
|
|
5506
5579
|
};
|
|
5507
5580
|
if (via !== void 0) return via;
|
|
5508
5581
|
}
|
|
5509
|
-
const opened = await this.openOne(owner, item.url, item.waitFor, signal, item.snapshot === true);
|
|
5582
|
+
const opened = await this.openOne(owner, item.url, item.waitFor, signal, item.snapshot === true, reveal);
|
|
5510
5583
|
this.keepAgentView(opened);
|
|
5511
5584
|
if (!wantsTab && opened.fresh && this.closable(owner, opened.live) && "representation" in opened.view && opened.view.representation.kind === "agent-view") {
|
|
5512
5585
|
await this.windows.closeTab(opened.live);
|
|
@@ -5675,12 +5748,13 @@ let MhBrowserService = (() => {
|
|
|
5675
5748
|
return body;
|
|
5676
5749
|
}
|
|
5677
5750
|
}
|
|
5678
|
-
async openOne(owner, url, waitFor, signal, snapshot = false) {
|
|
5751
|
+
async openOne(owner, url, waitFor, signal, snapshot = false, reveal = false) {
|
|
5679
5752
|
assertWaitFor(waitFor);
|
|
5680
5753
|
const reused = this.idleTabOn(owner, url);
|
|
5681
5754
|
const live = reused ?? await this.windows.openBlank(owner);
|
|
5682
5755
|
const fresh = reused === void 0;
|
|
5683
5756
|
const linked = this.control.link(owner, signal, live.id);
|
|
5757
|
+
if (reveal && !linked.signal.aborted) await live.page.bringToFront().catch(() => void 0);
|
|
5684
5758
|
const nextDialog = this.windows.nextDialog(live);
|
|
5685
5759
|
try {
|
|
5686
5760
|
let seenDialog;
|
|
@@ -6354,6 +6428,8 @@ let MhBrowserService = (() => {
|
|
|
6354
6428
|
}
|
|
6355
6429
|
async start() {
|
|
6356
6430
|
const context = await launchBrowser(this.launchOptions);
|
|
6431
|
+
const pid = await browserPid(context, this.launchOptions.profileDir);
|
|
6432
|
+
if (pid !== void 0) this.pids.set(context, pid);
|
|
6357
6433
|
if (this.closing) {
|
|
6358
6434
|
await this.closeContext(context);
|
|
6359
6435
|
return;
|
|
@@ -6412,9 +6488,8 @@ let MhBrowserService = (() => {
|
|
|
6412
6488
|
}
|
|
6413
6489
|
/** Close a context the service is done with, and kill it if close stalls. */
|
|
6414
6490
|
async closeContext(context) {
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
(browser?.process?.())?.kill("SIGKILL");
|
|
6491
|
+
await closeOrKill(() => context.close(), this.pids.get(context));
|
|
6492
|
+
this.pids.delete(context);
|
|
6418
6493
|
}
|
|
6419
6494
|
refuseIfPerson(owner, tab) {
|
|
6420
6495
|
if (this.control.ofTab(owner, tab) !== "person") return;
|
|
@@ -6723,18 +6798,30 @@ function browserDownloadTool(ctx, reader) {
|
|
|
6723
6798
|
}
|
|
6724
6799
|
//#endregion
|
|
6725
6800
|
//#region lib/types/tools/handover.js
|
|
6726
|
-
const
|
|
6801
|
+
const THEIRS = "a check that they are human, a login form, a Mind Harness login or consent form, or any form that issues credentials. Those steps are theirs: do not click through them, work around them, or fetch the page another way. A human check can clear by itself; if it is still there after one wait, call this.";
|
|
6802
|
+
const HEADED = [
|
|
6727
6803
|
"Hand the browser to the person and wait until they hand it back.",
|
|
6804
|
+
"The browser is a window on the person's screen; they can see it and act in it.",
|
|
6728
6805
|
"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.",
|
|
6729
|
-
|
|
6806
|
+
`Call it when a site asks for the person themselves: ${THEIRS} A handover usually takes the person seconds.`,
|
|
6807
|
+
"Call it too whenever only the person can continue."
|
|
6808
|
+
].join(" ");
|
|
6809
|
+
const HEADLESS = [
|
|
6810
|
+
"Tell the person about a step only they can take, and wait for their reply.",
|
|
6811
|
+
"The browser has no window: the person cannot see it or act in it, so the step cannot be finished in this browser. Their reply says how to go on.",
|
|
6812
|
+
`Call it when a site asks for the person themselves: ${THEIRS}`,
|
|
6813
|
+
"Call it too whenever only the person can continue."
|
|
6730
6814
|
].join(" ");
|
|
6731
6815
|
const REASON = "The sentence the person sees on the handover card, unchanged.";
|
|
6732
|
-
const TAB =
|
|
6816
|
+
const TAB = {
|
|
6817
|
+
headed: "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.",
|
|
6818
|
+
headless: "Tab id of the page the step is on, copied from a page element's tab attribute; the result carries that page as it is after the reply. Omit it to name no tab; that does not pick the latest result. A page with an Agent View has no tab until you open it with snapshot."
|
|
6819
|
+
};
|
|
6733
6820
|
const ABORT = Symbol("abort");
|
|
6734
|
-
function browserHandoverTool(ctx, reader) {
|
|
6821
|
+
function browserHandoverTool(ctx, reader, headed = true) {
|
|
6735
6822
|
return defineTool({
|
|
6736
6823
|
name: "browser_handover",
|
|
6737
|
-
description:
|
|
6824
|
+
description: headed ? HEADED : HEADLESS,
|
|
6738
6825
|
parameters: {
|
|
6739
6826
|
reason: {
|
|
6740
6827
|
type: "string",
|
|
@@ -6743,7 +6830,7 @@ function browserHandoverTool(ctx, reader) {
|
|
|
6743
6830
|
},
|
|
6744
6831
|
tab: {
|
|
6745
6832
|
type: "string",
|
|
6746
|
-
description: TAB
|
|
6833
|
+
description: TAB[headed ? "headed" : "headless"]
|
|
6747
6834
|
}
|
|
6748
6835
|
},
|
|
6749
6836
|
output: {
|
|
@@ -6785,7 +6872,7 @@ function browserHandoverTool(ctx, reader) {
|
|
|
6785
6872
|
reason: args.reason,
|
|
6786
6873
|
...args.tab === void 0 ? {} : { tab: args.tab }
|
|
6787
6874
|
}, exec.signal);
|
|
6788
|
-
const answer = await askUntilAnswered(ctx, args.reason, exec);
|
|
6875
|
+
const answer = await askUntilAnswered(ctx, args.reason, exec, headed);
|
|
6789
6876
|
if (answer === ABORT || exec.signal.aborted) return quiet();
|
|
6790
6877
|
const note = answer.answers[0]?.custom;
|
|
6791
6878
|
const back = await ctx.mhBrowser.handBack(session.id, note);
|
|
@@ -6793,11 +6880,7 @@ function browserHandoverTool(ctx, reader) {
|
|
|
6793
6880
|
const pages = page === void 0 ? [] : [page];
|
|
6794
6881
|
const block = tabBlock(ctx, reader, session);
|
|
6795
6882
|
return {
|
|
6796
|
-
text: `${[
|
|
6797
|
-
...note === void 0 || note === "" ? [] : [note],
|
|
6798
|
-
`${String(back.operations)} operations`,
|
|
6799
|
-
takeoverText(back)
|
|
6800
|
-
].join("\n")}\n${renderOutcome(pages)}${block.suffix}`,
|
|
6883
|
+
text: `${[...note === void 0 || note === "" ? [] : [note], ...headed ? [`${String(back.operations)} operations`, takeoverText(back)] : []].join("\n")}\n${renderOutcome(pages)}${block.suffix}`,
|
|
6801
6884
|
meta: JSON.parse(JSON.stringify({
|
|
6802
6885
|
...readingsOf(pages, "handover"),
|
|
6803
6886
|
closed: block.closed,
|
|
@@ -6816,14 +6899,14 @@ function browserHandoverTool(ctx, reader) {
|
|
|
6816
6899
|
});
|
|
6817
6900
|
}
|
|
6818
6901
|
/** Closing the card is not an answer. Ask the same question again. */
|
|
6819
|
-
async function askUntilAnswered(ctx, reason, exec) {
|
|
6902
|
+
async function askUntilAnswered(ctx, reason, exec, headed) {
|
|
6820
6903
|
const stopped = aborted(exec.signal);
|
|
6821
6904
|
for (;;) try {
|
|
6822
6905
|
return await Promise.race([ctx.userQuestions.ask({
|
|
6823
6906
|
questions: [{
|
|
6824
6907
|
id: "handback",
|
|
6825
|
-
question: handoverQuestion(reason),
|
|
6826
|
-
options: [{ label: HAND_BACK }]
|
|
6908
|
+
question: headed ? handoverQuestion(reason) : stepQuestion(reason),
|
|
6909
|
+
options: [{ label: headed ? HAND_BACK : REPLIED }]
|
|
6827
6910
|
}],
|
|
6828
6911
|
...exec.agent !== void 0 ? { agent: exec.agent } : {},
|
|
6829
6912
|
signal: exec.signal
|
|
@@ -7187,14 +7270,14 @@ function line(receipt) {
|
|
|
7187
7270
|
}
|
|
7188
7271
|
//#endregion
|
|
7189
7272
|
//#region lib/types/tools/register.js
|
|
7190
|
-
function registerBrowserTools(ctx, reader) {
|
|
7273
|
+
function registerBrowserTools(ctx, reader, headed) {
|
|
7191
7274
|
followSessionTitles(ctx);
|
|
7192
7275
|
ctx.tools.register(browserOpenTool(ctx, reader));
|
|
7193
7276
|
for (const tool of pageOperationTools(ctx, reader)) ctx.tools.register(tool);
|
|
7194
7277
|
ctx.tools.register(browserInvokeTool(ctx, reader));
|
|
7195
7278
|
ctx.tools.register(browserDownloadTool(ctx, reader));
|
|
7196
7279
|
ctx.tools.register(browserPresenceTool(ctx, reader));
|
|
7197
|
-
ctx.tools.register(browserHandoverTool(ctx, reader));
|
|
7280
|
+
ctx.tools.register(browserHandoverTool(ctx, reader, headed));
|
|
7198
7281
|
}
|
|
7199
7282
|
//#endregion
|
|
7200
7283
|
//#region lib/types/index.js
|
|
@@ -7221,7 +7304,7 @@ async function apply(ctx, config) {
|
|
|
7221
7304
|
inject: ["mhBrowser", ...inject],
|
|
7222
7305
|
async apply(inner) {
|
|
7223
7306
|
const reader = await registerProjections(inner);
|
|
7224
|
-
registerBrowserTools(inner, reader);
|
|
7307
|
+
registerBrowserTools(inner, reader, !config.headless);
|
|
7225
7308
|
registerPresence(inner, reader);
|
|
7226
7309
|
}
|
|
7227
7310
|
});
|
package/lib/typert.host.js
CHANGED
|
@@ -44,7 +44,7 @@ const $remote = createTypertRemoteInfo({
|
|
|
44
44
|
create: _mind_harness_mh_browser_mhBrowser_revealHandover_result$schema,
|
|
45
45
|
jsonSchema: {"type":"object","properties":{"status":{"type":"string","enum":["revealed","not-handing-over","no-tab"]}},"required":["status"],"additionalProperties":true},
|
|
46
46
|
},
|
|
47
|
-
sourceLocation: {"file":"packages/mh-browser/src/service/service.ts","line":
|
|
47
|
+
sourceLocation: {"file":"packages/mh-browser/src/service/service.ts","line":624,"column":9},
|
|
48
48
|
},
|
|
49
49
|
},
|
|
50
50
|
}, z)
|
|
@@ -75,7 +75,7 @@ export const TYPERT = {
|
|
|
75
75
|
{
|
|
76
76
|
"kind": "method",
|
|
77
77
|
"name": "open",
|
|
78
|
-
"signature": "async open( owner: string, items: { url: string; waitFor?: WaitFor; snapshot?: boolean }[], signal?: AbortSignal, ): Promise<(PageView | PageError)[]>"
|
|
78
|
+
"signature": "async open( owner: string, items: { url: string; waitFor?: WaitFor; snapshot?: boolean }[], signal?: AbortSignal, options?: { reveal?: boolean }, ): Promise<(PageView | PageError)[]>"
|
|
79
79
|
},
|
|
80
80
|
{
|
|
81
81
|
"kind": "method",
|
|
@@ -359,7 +359,7 @@ export const TYPERT = {
|
|
|
359
359
|
},
|
|
360
360
|
{
|
|
361
361
|
"name": "TestHooks",
|
|
362
|
-
"declaration": "export interface TestHooks {\n crash(): Promise<void>;\n page(owner: string, tab: string): unknown;\n running(): boolean;\n}"
|
|
362
|
+
"declaration": "export interface TestHooks {\n crash(): Promise<void>;\n page(owner: string, tab: string): unknown;\n running(): boolean;\n pages(): unknown[];\n pid(): number | undefined;\n}"
|
|
363
363
|
},
|
|
364
364
|
{
|
|
365
365
|
"name": "WaitFor",
|
|
@@ -44,7 +44,7 @@ export const TYPERT_REMOTE = createTypertRemoteInfo({
|
|
|
44
44
|
create: _mind_harness_mh_browser_mhBrowser_revealHandover_result$schema,
|
|
45
45
|
jsonSchema: {"type":"object","properties":{"status":{"type":"string","enum":["revealed","not-handing-over","no-tab"]}},"required":["status"],"additionalProperties":true},
|
|
46
46
|
},
|
|
47
|
-
sourceLocation: {"file":"packages/mh-browser/src/service/service.ts","line":
|
|
47
|
+
sourceLocation: {"file":"packages/mh-browser/src/service/service.ts","line":624,"column":9},
|
|
48
48
|
},
|
|
49
49
|
},
|
|
50
50
|
}, z)
|
|
@@ -20,4 +20,15 @@ export declare function chromiumSandboxEnabled(browser: BrowserName, platform?:
|
|
|
20
20
|
* A missing browser is reported and not downloaded.
|
|
21
21
|
*/
|
|
22
22
|
export declare function launchBrowser(options: LaunchOptions): Promise<BrowserContext>;
|
|
23
|
+
/**
|
|
24
|
+
* Process id of the browser's main process. Playwright gives none for a
|
|
25
|
+
* persistent context. Asks the browser first; on POSIX the profile's
|
|
26
|
+
* `SingletonLock` link names it as `<host>-<pid>`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function browserPid(context: BrowserContext, profileDir: string): Promise<number | undefined>;
|
|
29
|
+
/**
|
|
30
|
+
* Ask the browser to close; kill `pid` when that fails or takes longer than
|
|
31
|
+
* `waitMs`, so a stuck browser does not keep the profile locked.
|
|
32
|
+
*/
|
|
33
|
+
export declare function closeOrKill(close: () => Promise<void>, pid: number | undefined, waitMs?: number, kill?: (target: number) => void): Promise<'closed' | 'killed' | 'stuck'>;
|
|
23
34
|
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BrowserContext, CDPSession } from 'playwright';
|
|
1
|
+
import type { BrowserContext, CDPSession, Worker } from 'playwright';
|
|
2
2
|
/**
|
|
3
3
|
* Directory of the unpacked extension shipped with this package.
|
|
4
4
|
* Walks up from this module: sources live in `src/`, the bundle in `lib/`.
|
|
@@ -14,6 +14,8 @@ export declare class Extension {
|
|
|
14
14
|
private workerPage;
|
|
15
15
|
private constructor();
|
|
16
16
|
static load(context: BrowserContext, browserSession: CDPSession): Promise<Extension>;
|
|
17
|
+
/** Address of a page shipped in the extension directory. */
|
|
18
|
+
pageUrl(name: string): string;
|
|
17
19
|
/** `chrome.tabs.create` in the named window. Returns the extension's tab id. */
|
|
18
20
|
createTab(windowId: number, url: string): Promise<number>;
|
|
19
21
|
tabIdsIn(windowId: number): Promise<number[]>;
|
|
@@ -27,4 +29,6 @@ export declare class Extension {
|
|
|
27
29
|
ungroup(tabId: number): Promise<void>;
|
|
28
30
|
private worker;
|
|
29
31
|
}
|
|
32
|
+
/** The extension's service worker. Looks once more after the wait times out. */
|
|
33
|
+
export declare function findWorker(context: BrowserContext, id: string, timeoutMs?: number): Promise<Worker>;
|
|
30
34
|
//# sourceMappingURL=extension.d.ts.map
|
|
@@ -34,6 +34,7 @@ export declare class MhBrowserService extends TypertRemoteService {
|
|
|
34
34
|
private readonly windows;
|
|
35
35
|
private readonly launchOptions;
|
|
36
36
|
private context;
|
|
37
|
+
private readonly pids;
|
|
37
38
|
private ready;
|
|
38
39
|
private closing;
|
|
39
40
|
/** The browser is not running. The next call that needs a page starts it. */
|
|
@@ -58,7 +59,9 @@ export declare class MhBrowserService extends TypertRemoteService {
|
|
|
58
59
|
url: string;
|
|
59
60
|
waitFor?: WaitFor;
|
|
60
61
|
snapshot?: boolean;
|
|
61
|
-
}[], signal?: AbortSignal
|
|
62
|
+
}[], signal?: AbortSignal, options?: {
|
|
63
|
+
reveal?: boolean;
|
|
64
|
+
}): Promise<(PageView | PageError)[]>;
|
|
62
65
|
view(owner: string, tab: string, signal?: AbortSignal): Promise<PageView | PageError>;
|
|
63
66
|
/** Pages whose current representation is already in the model's presence. */
|
|
64
67
|
rememberPresence(owner: string, urls: string[]): void;
|
|
@@ -5,5 +5,9 @@ export interface TestHooks {
|
|
|
5
5
|
page(owner: string, tab: string): unknown;
|
|
6
6
|
/** A browser process is up. */
|
|
7
7
|
running(): boolean;
|
|
8
|
+
/** Every page of the running browser, launch page included. */
|
|
9
|
+
pages(): unknown[];
|
|
10
|
+
/** The main browser process id recorded at start. */
|
|
11
|
+
pid(): number | undefined;
|
|
8
12
|
}
|
|
9
13
|
//# sourceMappingURL=test-hooks.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import { type ToolDefinition } from '@deepseek-ai/dsh-tools';
|
|
3
3
|
import type { LogReader } from '../projections.ts';
|
|
4
|
-
export declare function browserHandoverTool(ctx: Context, reader: LogReader): ToolDefinition;
|
|
4
|
+
export declare function browserHandoverTool(ctx: Context, reader: LogReader, headed?: boolean): ToolDefinition;
|
|
5
5
|
//# sourceMappingURL=handover.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import type { LogReader } from '../projections.ts';
|
|
3
|
-
export declare function registerBrowserTools(ctx: Context, reader: LogReader): void;
|
|
3
|
+
export declare function registerBrowserTools(ctx: Context, reader: LogReader, headed: boolean): void;
|
|
4
4
|
//# sourceMappingURL=register.d.ts.map
|
package/lib/types/words.d.ts
CHANGED
|
@@ -4,4 +4,8 @@ export declare const WAITING_TITLE = "\u7B49\u4F60\uFF1A";
|
|
|
4
4
|
export declare function handoverQuestion(reason: string): string;
|
|
5
5
|
/** The option that hands the browser back. */
|
|
6
6
|
export declare const HAND_BACK = "\u4EA4\u56DE";
|
|
7
|
+
/** The handover card when the browser has no window: the person cannot act in it. */
|
|
8
|
+
export declare function stepQuestion(reason: string): string;
|
|
9
|
+
/** The option on that card when the person has nothing to add. */
|
|
10
|
+
export declare const REPLIED = "\u7EE7\u7EED";
|
|
7
11
|
//# sourceMappingURL=words.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mind-harness/mh-browser",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Opens pages in a real browser for the agent, keeps chosen Agent Views in context, and hands the browser to you when a person is needed.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"files": [
|