@mind-harness/mh-browser 0.2.5 → 0.2.6

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/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
1
+ import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, win32 } from "node:path";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
4
4
  import { isDeepStrictEqual } from "node:util";
@@ -6,11 +6,13 @@ import { HarnessError, createUserMessage } from "@deepseek-ai/dsh-llm";
6
6
  import { assertSupportedJsonSchema, defineTool, validateJsonSchemaValue } from "@deepseek-ai/dsh-tools";
7
7
  import { createHash, randomUUID } from "node:crypto";
8
8
  import { parse } from "yaml";
9
- import { constants, readFileSync } from "node:fs";
9
+ import { accessSync, constants, readFileSync, realpathSync, statSync } from "node:fs";
10
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";
14
+ import { execFile } from "node:child_process";
15
+ import { homedir } from "node:os";
14
16
  import { fileURLToPath } from "node:url";
15
17
  import { UserQuestionError } from "@deepseek-ai/dsh-user-questions";
16
18
  //#region lib/types/config.js
@@ -20,7 +22,8 @@ const browserConfig = z.object({
20
22
  "chrome",
21
23
  "msedge",
22
24
  "chromium"
23
- ]).default("chrome").description("Which installed browser to drive. chromium is the one Playwright downloads."),
25
+ ]).default("chrome").volatile().description("Which browser to drive. chromium is the one Playwright downloads. A new value restarts the browser once it is idle."),
26
+ executablePath: z.string().volatile().description("Absolute path of the browser executable. `~` is expanded. Leave empty to locate one. A new value restarts the browser once it is idle."),
24
27
  profileDir: z.string().description("Browser profile directory, kept between runs. Defaults to mh/profiles/<dsh profile>/browser under the dsh home."),
25
28
  headless: z.boolean().default(false).description("Run the browser without a window.")
26
29
  });
@@ -3069,16 +3072,267 @@ function sameOrigin$1(left, right) {
3069
3072
  }
3070
3073
  }
3071
3074
  //#endregion
3072
- //#region lib/types/service/browser.js
3073
- const MISSING = {
3074
- chrome: "Google Chrome not found; install it, or change `browser` in the configuration",
3075
- msedge: "Microsoft Edge not found; install it, or change `browser` in the configuration",
3076
- chromium: "Playwright's Chromium not found; install it, or change `browser` in the configuration"
3075
+ //#region lib/types/service/locate.js
3076
+ const MAC_APP = {
3077
+ chrome: [
3078
+ "Google Chrome.app",
3079
+ "Contents",
3080
+ "MacOS",
3081
+ "Google Chrome"
3082
+ ],
3083
+ msedge: [
3084
+ "Microsoft Edge.app",
3085
+ "Contents",
3086
+ "MacOS",
3087
+ "Microsoft Edge"
3088
+ ]
3089
+ };
3090
+ const LINUX_PATH = {
3091
+ chrome: "/opt/google/chrome/chrome",
3092
+ msedge: "/opt/microsoft/msedge/msedge"
3093
+ };
3094
+ const WIN_SUFFIX = {
3095
+ chrome: "Google\\Chrome\\Application\\chrome.exe",
3096
+ msedge: "Microsoft\\Edge\\Application\\msedge.exe"
3097
+ };
3098
+ const MAC_BUNDLE = {
3099
+ chrome: {
3100
+ id: "com.google.Chrome",
3101
+ binary: "Google Chrome"
3102
+ },
3103
+ msedge: {
3104
+ id: "com.microsoft.edgemac",
3105
+ binary: "Microsoft Edge"
3106
+ }
3107
+ };
3108
+ const LINUX_NAMES = {
3109
+ chrome: ["google-chrome-stable", "google-chrome"],
3110
+ msedge: ["microsoft-edge-stable", "microsoft-edge"]
3111
+ };
3112
+ const WIN_EXE = {
3113
+ chrome: "chrome.exe",
3114
+ msedge: "msedge.exe"
3077
3115
  };
3078
- function missingExecutable(error) {
3079
- const message = error instanceof Error ? error.message : String(error);
3080
- return /ENOENT|doesn.t exist|Executable doesn't exist|not found/i.test(message);
3116
+ const TITLE = {
3117
+ chrome: "Google Chrome",
3118
+ msedge: "Microsoft Edge"
3119
+ };
3120
+ const DISCOVER_MS = 2e3;
3121
+ /** The fixed paths checked for `browser` on `env`, in the order they are tried. */
3122
+ function candidatePaths(browser, env) {
3123
+ if (env.platform === "darwin") return ["/Applications", posix.join(env.home, "Applications")].map((root) => posix.join(root, ...MAC_APP[browser]));
3124
+ if (env.platform === "linux") return [LINUX_PATH[browser]];
3125
+ if (env.platform !== "win32") return [];
3126
+ const drive = env.env.HOMEDRIVE;
3127
+ const roots = [
3128
+ env.env.LOCALAPPDATA,
3129
+ env.env.PROGRAMFILES,
3130
+ env.env["PROGRAMFILES(X86)"],
3131
+ typeof drive === "string" && drive !== "" ? `${drive}\\Program Files` : void 0,
3132
+ typeof drive === "string" && drive !== "" ? `${drive}\\Program Files (x86)` : void 0
3133
+ ];
3134
+ const suffix = WIN_SUFFIX[browser];
3135
+ const paths = [];
3136
+ for (const root of roots) {
3137
+ if (root === void 0 || root === "") continue;
3138
+ paths.push(win32.join(root, suffix));
3139
+ }
3140
+ return paths;
3141
+ }
3142
+ /**
3143
+ * `configured` is used as given: a path that cannot be run is a failure, and
3144
+ * nothing else is searched. `~` expands to `env.home`.
3145
+ */
3146
+ function checkConfigured(configured, env) {
3147
+ const path = expandHome(configured, env.home, env.platform);
3148
+ if (!(env.platform === "win32" ? win32 : posix).isAbsolute(path)) return {
3149
+ ok: false,
3150
+ path,
3151
+ code: "not-absolute"
3152
+ };
3153
+ const access = env.access(path);
3154
+ if (access !== "ok") return {
3155
+ ok: false,
3156
+ path,
3157
+ code: access.code
3158
+ };
3159
+ return {
3160
+ ok: true,
3161
+ path
3162
+ };
3163
+ }
3164
+ /** Failure for a path the configuration names. */
3165
+ function configuredPathError(path, code) {
3166
+ if (code === "not-absolute") return /* @__PURE__ */ new Error(`The configured browser path must be absolute: ${path}`);
3167
+ return /* @__PURE__ */ new Error(`The configured browser path cannot be run (${code}): ${path}`);
3168
+ }
3169
+ /** Failure listing every path that was tried. */
3170
+ function browserNotFoundError(browser, missing) {
3171
+ const tried = missing.map((item) => `${item.path} (${item.reason})`).join("; ");
3172
+ const detail = tried === "" ? "" : ` Tried: ${tried}.`;
3173
+ return /* @__PURE__ */ new Error(`${TITLE[browser]} not found.${detail} Install it, or set the browser's path in the configuration.`);
3174
+ }
3175
+ /**
3176
+ * The path to launch. A configured path is checked and never replaced.
3177
+ * `chromium` without one returns undefined, so Playwright uses its own build.
3178
+ * An installed browser that cannot be found throws.
3179
+ */
3180
+ async function resolveExecutable(browser, configured, env, signal) {
3181
+ if (configured !== void 0 && configured !== "") {
3182
+ const checked = checkConfigured(configured, env);
3183
+ if (!checked.ok) throw configuredPathError(checked.path, checked.code);
3184
+ return checked.path;
3185
+ }
3186
+ if (browser === "chromium") return void 0;
3187
+ const located = await locateInstalled(browser, env, signal);
3188
+ if ("found" in located) return located.found;
3189
+ throw browserNotFoundError(browser, located.missing);
3190
+ }
3191
+ /**
3192
+ * Where `browser` is installed when the configuration names no path.
3193
+ * Fixed candidates first, then `env.discover`.
3194
+ */
3195
+ async function locateInstalled(browser, env, signal) {
3196
+ const missing = [];
3197
+ const seen = /* @__PURE__ */ new Set();
3198
+ const visit = (path) => {
3199
+ if (seen.has(path)) return void 0;
3200
+ seen.add(path);
3201
+ const access = env.access(path);
3202
+ if (access === "ok") return path;
3203
+ missing.push({
3204
+ path,
3205
+ reason: reasonForCode(access.code)
3206
+ });
3207
+ };
3208
+ for (const path of candidatePaths(browser, env)) {
3209
+ const found = visit(path);
3210
+ if (found !== void 0) return { found };
3211
+ }
3212
+ if (signal?.aborted === true) return { missing };
3213
+ for (const path of await env.discover(browser, signal)) {
3214
+ const found = visit(path);
3215
+ if (found !== void 0) return { found };
3216
+ }
3217
+ return { missing };
3218
+ }
3219
+ /** `path` is Playwright's Chromium. Missing or not executable is `missing`. */
3220
+ function locateBundled(path, access) {
3221
+ const result = access(path);
3222
+ if (result === "ok") return { found: path };
3223
+ return { missing: [{
3224
+ path,
3225
+ reason: reasonForCode(result.code)
3226
+ }] };
3227
+ }
3228
+ /** This machine: fixed paths, then Spotlight, App Paths, or `PATH`. */
3229
+ function hostLocateEnv() {
3230
+ const env = {
3231
+ platform: process.platform,
3232
+ env: process.env,
3233
+ home: homedir(),
3234
+ access: accessFile,
3235
+ discover: (browser, signal) => discoverInstalled(browser, env, signal)
3236
+ };
3237
+ return env;
3238
+ }
3239
+ /** A `.app` bundle path reported by Spotlight, turned into its executable. */
3240
+ function executableInApp(reported, binary) {
3241
+ const at = reported.indexOf(".app");
3242
+ if (at < 0) return void 0;
3243
+ return posix.join(reported.slice(0, at + 4), "Contents", "MacOS", binary);
3244
+ }
3245
+ /** The default value from `reg query ... /ve` output. */
3246
+ function regValue(stdout) {
3247
+ for (const line of stdout.split(/\r?\n/)) {
3248
+ const value = /\bREG_(?:EXPAND_)?SZ\s+(\S.*?)\s*$/.exec(line)?.[1]?.trim();
3249
+ if (value !== void 0 && value !== "") return value;
3250
+ }
3251
+ }
3252
+ /** Text for an errno from {@link LocateEnv.access}. */
3253
+ function reasonForCode(code) {
3254
+ if (code === "ENOENT" || code === "ENOTDIR") return "does not exist";
3255
+ if (code === "EISDIR") return "is a directory";
3256
+ return `permission denied: ${code}`;
3257
+ }
3258
+ function expandHome(configured, home, platform) {
3259
+ const slash = platform === "win32" ? win32 : posix;
3260
+ if (configured === "~") return home;
3261
+ if (configured.startsWith("~/") || configured.startsWith("~\\")) return slash.join(home, configured.slice(2));
3262
+ return configured;
3263
+ }
3264
+ function accessFile(path) {
3265
+ try {
3266
+ if (statSync(path).isDirectory()) return { code: "EISDIR" };
3267
+ accessSync(path, constants.X_OK);
3268
+ return "ok";
3269
+ } catch (error) {
3270
+ return { code: error !== null && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : "EACCES" };
3271
+ }
3272
+ }
3273
+ async function discoverInstalled(browser, env, signal) {
3274
+ try {
3275
+ if (env.platform === "darwin") return await discoverMac(browser, signal);
3276
+ if (env.platform === "win32") return await discoverWindows(browser, signal);
3277
+ if (env.platform === "linux") return discoverLinux(browser, env);
3278
+ } catch {}
3279
+ return [];
3280
+ }
3281
+ async function discoverMac(browser, signal) {
3282
+ const bundle = MAC_BUNDLE[browser];
3283
+ const stdout = await command("mdfind", [`kMDItemCFBundleIdentifier == '${bundle.id}'`], signal);
3284
+ const found = [];
3285
+ for (const line of stdout.split(/\r?\n/)) {
3286
+ const trimmed = line.trim();
3287
+ if (trimmed === "") continue;
3288
+ const executable = executableInApp(trimmed, bundle.binary);
3289
+ if (executable !== void 0) found.push(executable);
3290
+ }
3291
+ return found;
3292
+ }
3293
+ async function discoverWindows(browser, signal) {
3294
+ const exe = WIN_EXE[browser];
3295
+ const found = [];
3296
+ for (const hive of ["HKCU", "HKLM"]) {
3297
+ const value = regValue(await command("reg", [
3298
+ "query",
3299
+ `${hive}\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${exe}`,
3300
+ "/ve"
3301
+ ], signal));
3302
+ if (value !== void 0) found.push(value);
3303
+ }
3304
+ return found;
3305
+ }
3306
+ function discoverLinux(browser, env) {
3307
+ const entries = (env.env.PATH ?? "").split(posix.delimiter);
3308
+ const found = [];
3309
+ for (const name of LINUX_NAMES[browser]) for (const dir of entries) {
3310
+ if (dir === "") continue;
3311
+ const candidate = posix.join(dir, name);
3312
+ if (env.access(candidate) !== "ok") continue;
3313
+ try {
3314
+ found.push(realpathSync(candidate));
3315
+ } catch {
3316
+ found.push(candidate);
3317
+ }
3318
+ }
3319
+ return found;
3320
+ }
3321
+ function command(file, args, signal) {
3322
+ if (signal?.aborted === true) return Promise.resolve("");
3323
+ const timer = AbortSignal.timeout(DISCOVER_MS);
3324
+ const stop = signal === void 0 ? timer : AbortSignal.any([signal, timer]);
3325
+ return new Promise((resolve) => {
3326
+ execFile(file, args, {
3327
+ windowsHide: true,
3328
+ signal: stop
3329
+ }, (error, stdout) => {
3330
+ resolve(error ? "" : String(stdout));
3331
+ });
3332
+ });
3081
3333
  }
3334
+ //#endregion
3335
+ //#region lib/types/service/browser.js
3082
3336
  /**
3083
3337
  * Playwright appends `--no-sandbox` unless this is true, and branded Chrome
3084
3338
  * then warns that stability and security will suffer. An installed browser
@@ -3089,34 +3343,45 @@ function chromiumSandboxEnabled(browser, platform = process.platform) {
3089
3343
  return platform !== "linux" || browser !== "chromium";
3090
3344
  }
3091
3345
  /**
3092
- * One persistent context for the whole process. `chromium` omits `channel`
3093
- * so Playwright uses its own browser; the others use a local install.
3094
- * A missing browser is reported and not downloaded.
3346
+ * One persistent context for the whole process. An installed browser is located
3347
+ * here and passed as `executablePath`; `channel` names which install that was.
3348
+ * `chromium` omits `channel` so Playwright uses its own browser unless a path
3349
+ * is configured. A missing browser is reported and not downloaded. A launch
3350
+ * that fails after the executable was found is reported as Playwright said.
3095
3351
  */
3096
- async function launchBrowser(options) {
3352
+ async function launchBrowser(options, env = hostLocateEnv()) {
3097
3353
  await mkdir(options.profileDir, { recursive: true });
3354
+ const executablePath = await resolveExecutable(options.browser, options.executablePath, env);
3355
+ return chromium.launchPersistentContext(options.profileDir, {
3356
+ headless: options.headless,
3357
+ acceptDownloads: true,
3358
+ chromiumSandbox: chromiumSandboxEnabled(options.browser),
3359
+ ...options.headless ? {} : {
3360
+ viewport: null,
3361
+ ignoreDefaultArgs: ["--disable-extensions"],
3362
+ args: [
3363
+ "--enable-unsafe-extension-debugging",
3364
+ "--keep-alive-for-test",
3365
+ "--disable-blink-features=AutomationControlled",
3366
+ "--test-type"
3367
+ ]
3368
+ },
3369
+ ...options.browser === "chromium" ? {} : { channel: options.browser },
3370
+ ...executablePath !== void 0 ? { executablePath } : {}
3371
+ });
3372
+ }
3373
+ /** Playwright's own Chromium, or why that file cannot be run. */
3374
+ function bundledChromiumLocation(access) {
3375
+ let path;
3098
3376
  try {
3099
- return await chromium.launchPersistentContext(options.profileDir, {
3100
- headless: options.headless,
3101
- acceptDownloads: true,
3102
- chromiumSandbox: chromiumSandboxEnabled(options.browser),
3103
- ...options.headless ? {} : {
3104
- viewport: null,
3105
- ignoreDefaultArgs: ["--disable-extensions"],
3106
- args: [
3107
- "--enable-unsafe-extension-debugging",
3108
- "--keep-alive-for-test",
3109
- "--disable-blink-features=AutomationControlled",
3110
- "--test-type"
3111
- ]
3112
- },
3113
- ...options.browser === "chromium" ? {} : { channel: options.browser },
3114
- ...options.executablePath !== void 0 ? { executablePath: options.executablePath } : {}
3115
- });
3377
+ path = chromium.executablePath();
3116
3378
  } catch (error) {
3117
- if (missingExecutable(error) || options.executablePath !== void 0) throw new Error(MISSING[options.browser]);
3118
- throw error;
3379
+ return { missing: [{
3380
+ path: "chromium",
3381
+ reason: error instanceof Error ? error.message : "does not exist"
3382
+ }] };
3119
3383
  }
3384
+ return locateBundled(path, access);
3120
3385
  }
3121
3386
  /**
3122
3387
  * Process id of the browser's main process. Playwright gives none for a
@@ -3179,6 +3444,15 @@ var Control = class {
3179
3444
  tabs = /* @__PURE__ */ new Map();
3180
3445
  since = /* @__PURE__ */ new Map();
3181
3446
  named = /* @__PURE__ */ new Map();
3447
+ /**
3448
+ * A handover is waiting, or the person holds a tab. The browser is not
3449
+ * restarted out from under either.
3450
+ */
3451
+ busy() {
3452
+ if (this.holding.size > 0) return true;
3453
+ for (const who of this.who.values()) if (who === "person") return true;
3454
+ return false;
3455
+ }
3182
3456
  /** Whether the person holds any tab, or a handover is waiting. */
3183
3457
  of(owner) {
3184
3458
  return this.who.get(owner) ?? "model";
@@ -5318,11 +5592,24 @@ const NO_WORKSPACE_REASON = "this session has no working directory";
5318
5592
  let MhBrowserService = (() => {
5319
5593
  let _classSuper = TypertRemoteService;
5320
5594
  let _instanceExtraInitializers = [];
5595
+ let _locateFor_decorators;
5321
5596
  let _revealHandover_decorators;
5322
5597
  return class MhBrowserService extends _classSuper {
5323
5598
  static {
5324
5599
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
5600
+ _locateFor_decorators = [Remote];
5325
5601
  _revealHandover_decorators = [Remote];
5602
+ __esDecorate(this, null, _locateFor_decorators, {
5603
+ kind: "method",
5604
+ name: "locateFor",
5605
+ static: false,
5606
+ private: false,
5607
+ access: {
5608
+ has: (obj) => "locateFor" in obj,
5609
+ get: (obj) => obj.locateFor
5610
+ },
5611
+ metadata: _metadata
5612
+ }, null, _instanceExtraInitializers);
5326
5613
  __esDecorate(this, null, _revealHandover_decorators, {
5327
5614
  kind: "method",
5328
5615
  name: "revealHandover",
@@ -5341,13 +5628,23 @@ let MhBrowserService = (() => {
5341
5628
  value: _metadata
5342
5629
  });
5343
5630
  }
5344
- static Config = browserConfig;
5345
5631
  windows = (__runInitializers(this, _instanceExtraInitializers), new Windows());
5346
- launchOptions;
5632
+ headless;
5633
+ profileDir;
5634
+ browserName;
5635
+ executablePath;
5636
+ /** Tests replace the machine lookup. Unset uses this machine. */
5637
+ locator;
5347
5638
  context;
5348
5639
  pids = /* @__PURE__ */ new WeakMap();
5349
5640
  ready = Promise.resolve();
5350
5641
  closing = false;
5642
+ /** A browser restart was requested and has not run yet. */
5643
+ relaunchWanted = false;
5644
+ /** Counts browser and path changes. A start that sampled the latest one clears the restart. */
5645
+ configEpoch = 0;
5646
+ /** The running browser is being closed so the new configuration can start. */
5647
+ replacing = false;
5351
5648
  /** The browser is not running. The next call that needs a page starts it. */
5352
5649
  exited = true;
5353
5650
  exits = [];
@@ -5366,11 +5663,16 @@ let MhBrowserService = (() => {
5366
5663
  constructor(ctx, config) {
5367
5664
  super(ctx, "mhBrowser");
5368
5665
  this.maxBytes = config.presence.maxBytes;
5369
- this.launchOptions = {
5370
- browser: config.browser,
5371
- headless: config.headless,
5372
- profileDir: config.profileDir ?? defaultProfileDir(ctx.get("profileContext"))
5373
- };
5666
+ this.headless = config.headless;
5667
+ this.profileDir = config.profileDir ?? defaultProfileDir(ctx.get("profileContext"));
5668
+ this.browserName = config.browser;
5669
+ this.executablePath = config.executablePath ?? { get: () => void 0 };
5670
+ Object.getPrototypeOf(ctx).on("loader/volatile-update", (paths) => {
5671
+ if (!paths.some((path) => path.includes("browser") || path.includes("executablePath"))) return;
5672
+ this.configEpoch += 1;
5673
+ this.relaunchWanted = true;
5674
+ this.relaunchIfIdle();
5675
+ });
5374
5676
  this.windows.setPersonInput((owner, tab) => {
5375
5677
  const live = tab === void 0 ? void 0 : this.windows.get(owner, tab);
5376
5678
  this.control.note(owner, tab);
@@ -5589,9 +5891,26 @@ let MhBrowserService = (() => {
5589
5891
  page: (owner, tab) => this.windows.get(owner, tab)?.page,
5590
5892
  running: () => this.context !== void 0,
5591
5893
  pages: () => this.context?.pages() ?? [],
5592
- pid: () => this.context === void 0 ? void 0 : this.pids.get(this.context)
5894
+ pid: () => this.context === void 0 ? void 0 : this.pids.get(this.context),
5895
+ useLocator: (env) => {
5896
+ this.locator = env;
5897
+ }
5593
5898
  };
5594
5899
  }
5900
+ /**
5901
+ * Where `browser` would be launched from when no path is configured.
5902
+ * The configured path is not consulted: the answer is what an empty path uses.
5903
+ */
5904
+ async locateFor(browser, signal) {
5905
+ const stopped = () => signal?.aborted === true;
5906
+ if (stopped()) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
5907
+ if (browser !== "chrome" && browser !== "msedge" && browser !== "chromium") throw new MhBrowserError("INVALID_ARGS", "browser must be chrome, msedge, or chromium");
5908
+ const env = this.locateEnv();
5909
+ if (browser === "chromium") return bundledChromiumLocation(env.access);
5910
+ const located = await locateInstalled(browser, env, signal);
5911
+ if (stopped()) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
5912
+ return located;
5913
+ }
5595
5914
  async act(owner, tab, op, signal) {
5596
5915
  return this.exclusive(owner, async () => {
5597
5916
  await this.ready;
@@ -5777,18 +6096,22 @@ let MhBrowserService = (() => {
5777
6096
  if (!this.control.releaseIfHeld(owner)) return;
5778
6097
  this.windows.releaseHandover(owner);
5779
6098
  this.windows.markWaiting(owner, false);
6099
+ this.relaunchIfIdle();
5780
6100
  }
5781
6101
  /**
5782
6102
  * The person took over between turns. The next message they send is the
5783
6103
  * hand-back. A handover that is still waiting on its card is not.
6104
+ * The addresses are read before a waiting restart closes the pages.
5784
6105
  */
5785
6106
  takePersonHandback(owner) {
5786
6107
  const taken = this.control.takePerson(owner);
5787
6108
  if (taken === void 0) return void 0;
6109
+ const urls = this.presenceUrls(owner, taken.tabs);
5788
6110
  this.windows.releaseHandover(owner);
6111
+ this.relaunchIfIdle();
5789
6112
  return {
5790
6113
  ...taken,
5791
- urls: this.presenceUrls(owner, taken.tabs)
6114
+ urls
5792
6115
  };
5793
6116
  }
5794
6117
  /**
@@ -6672,8 +6995,10 @@ let MhBrowserService = (() => {
6672
6995
  return started;
6673
6996
  }
6674
6997
  async start() {
6675
- const context = await launchBrowser(this.launchOptions);
6676
- const pid = await browserPid(context, this.launchOptions.profileDir);
6998
+ const choice = this.launchChoice();
6999
+ const epoch = this.configEpoch;
7000
+ const context = await launchBrowser(choice, this.locateEnv());
7001
+ const pid = await browserPid(context, choice.profileDir);
6677
7002
  if (pid !== void 0) this.pids.set(context, pid);
6678
7003
  if (this.closing) {
6679
7004
  await this.closeContext(context);
@@ -6681,15 +7006,16 @@ let MhBrowserService = (() => {
6681
7006
  }
6682
7007
  this.context = context;
6683
7008
  try {
6684
- await this.windows.attach(context, this.launchOptions.profileDir, { headed: !this.launchOptions.headless });
7009
+ await this.windows.attach(context, choice.profileDir, { headed: !choice.headless });
6685
7010
  } catch (error) {
6686
7011
  this.context = void 0;
6687
7012
  this.windows.dropPages();
6688
7013
  await this.closeContext(context);
6689
7014
  throw error;
6690
7015
  }
7016
+ if (this.configEpoch === epoch) this.relaunchWanted = false;
6691
7017
  context.browser()?.on("disconnected", () => {
6692
- if (this.closing || this.context !== context) return;
7018
+ if (this.closing || this.replacing || this.context !== context) return;
6693
7019
  this.onExit();
6694
7020
  });
6695
7021
  }
@@ -6773,9 +7099,69 @@ let MhBrowserService = (() => {
6773
7099
  this.queues.set(owner, queued.tail);
6774
7100
  queued.tail.then(() => {
6775
7101
  if (this.queues.get(owner) === queued.tail) this.queues.delete(owner);
7102
+ if (this.queues.size === 0) this.relaunchIfIdle();
6776
7103
  });
6777
7104
  return queued.run;
6778
7105
  }
7106
+ launchChoice() {
7107
+ const configured = this.executablePath.get();
7108
+ return {
7109
+ browser: this.browserName.get(),
7110
+ headless: this.headless,
7111
+ profileDir: this.profileDir,
7112
+ ...configured !== void 0 && configured !== "" ? { executablePath: configured } : {}
7113
+ };
7114
+ }
7115
+ locateEnv() {
7116
+ return this.locator ?? hostLocateEnv();
7117
+ }
7118
+ idle() {
7119
+ return this.queues.size === 0 && !this.control.busy();
7120
+ }
7121
+ /**
7122
+ * Close and start again when a configuration change is waiting and nothing
7123
+ * is using the browser. A browser that is not running keeps the new values
7124
+ * for the next start. The close is not counted as a crash.
7125
+ */
7126
+ relaunchIfIdle() {
7127
+ if (!this.relaunchWanted || this.closing || this.replacing) return;
7128
+ if (!this.idle()) return;
7129
+ if (this.context === void 0) {
7130
+ if (this.exited) {
7131
+ this.relaunchWanted = false;
7132
+ return;
7133
+ }
7134
+ this.ready.then(() => this.relaunchIfIdle(), () => {
7135
+ this.relaunchWanted = false;
7136
+ });
7137
+ return;
7138
+ }
7139
+ this.relaunchWanted = false;
7140
+ this.replacing = true;
7141
+ const run = this.replaceBrowser();
7142
+ this.ready = run;
7143
+ const finish = () => {
7144
+ this.replacing = false;
7145
+ if (this.relaunchWanted) this.relaunchIfIdle();
7146
+ };
7147
+ run.then(finish, finish);
7148
+ }
7149
+ async replaceBrowser() {
7150
+ this.windows.suppressPersonInput();
7151
+ const context = this.context;
7152
+ this.context = void 0;
7153
+ this.windows.dropPages();
7154
+ if (context !== void 0) await this.closeContext(context);
7155
+ if (this.closing) return;
7156
+ this.exited = false;
7157
+ try {
7158
+ await this.launch();
7159
+ this.windows.resumePersonInput();
7160
+ } catch (error) {
7161
+ this.exited = true;
7162
+ throw error;
7163
+ }
7164
+ }
6779
7165
  };
6780
7166
  })();
6781
7167
  const MAX_REDIRECTS = 10;
@@ -7541,6 +7927,7 @@ const inject = [
7541
7927
  ];
7542
7928
  const Config = browserConfig;
7543
7929
  async function apply(ctx, config) {
7930
+ const resolved = browserSettings(config);
7544
7931
  ctx.plugin({
7545
7932
  name: "mh-browser-slot",
7546
7933
  inject: ["browserUse", "mhBrowser"],
@@ -7548,16 +7935,29 @@ async function apply(ctx, config) {
7548
7935
  inner.browserUse.register("mh-browser");
7549
7936
  }
7550
7937
  });
7551
- await ctx.plugin(MhBrowserService, config);
7938
+ await ctx.plugin(MhBrowserService, resolved);
7552
7939
  await ctx.plugin({
7553
7940
  name: "mh-browser-runtime",
7554
7941
  inject: ["mhBrowser", ...inject],
7555
7942
  async apply(inner) {
7556
7943
  const reader = await registerProjections(inner);
7557
- registerBrowserTools(inner, reader, !config.headless);
7944
+ registerBrowserTools(inner, reader, !resolved.headless);
7558
7945
  registerPresence(inner, reader);
7559
7946
  }
7560
7947
  });
7561
7948
  }
7949
+ /**
7950
+ * The loader passes a config it has already resolved, including the volatile
7951
+ * fields. A direct caller passes the raw values, which are resolved here.
7952
+ * Resolving twice rejects a volatile value as if it were the wrong type.
7953
+ */
7954
+ function browserSettings(config) {
7955
+ if (isResolved(config)) return config;
7956
+ return browserConfig(config);
7957
+ }
7958
+ function isResolved(config) {
7959
+ const browser = config.browser;
7960
+ return typeof browser === "object" && browser !== null && "get" in browser;
7961
+ }
7562
7962
  //#endregion
7563
7963
  export { Config, MhBrowserError, MhBrowserService, UNKNOWN_TAB, UNKNOWN_TAB_REASON, apply, inject, name, readAgentView, unknownTab };