@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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Mind Harness",
4
- "version": "0.2.1",
4
+ "version": "0.2.3",
5
5
  "permissions": ["tabGroups"],
6
6
  "background": { "service_worker": "sw.js" }
7
7
  }
@@ -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&nbsp;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: ["--enable-unsafe-extension-debugging", "--keep-alive-for-test"]
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 = 5e3;
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
- async function findWorker(context, id) {
4224
- const open = context.serviceWorkers().find((worker) => worker.url().includes(id));
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
- return context.waitForEvent("serviceworker", {
4227
- predicate: (worker) => worker.url().includes(id),
4228
- timeout: WORKER_MS
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
- const browser = context.browser();
6416
- await Promise.race([context.close().catch(() => void 0), new Promise((resolve) => setTimeout(resolve, 1e3))]);
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 HANDOVER = [
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
- "Call it instead of filling or submitting a login form, a Mind Harness login or consent form, or any form that issues credentials, and whenever only the person can continue."
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 = "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.";
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: HANDOVER,
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
  });
@@ -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":619,"column":9},
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":619,"column":9},
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): Promise<(PageView | PageError)[]>;
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
@@ -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.1",
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": [