@canaryai/cli 0.2.6 → 0.2.8

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
  import { createRequire as __cr } from "module"; const require = __cr(import.meta.url);
2
2
  import {
3
3
  getCanaryTmpDir
4
- } from "./chunk-AHYNXUHF.js";
4
+ } from "./chunk-XAA5VQ5N.js";
5
5
  import {
6
6
  consoleLogger
7
7
  } from "./chunk-P5Z2Y5VV.js";
@@ -119,7 +119,7 @@ var require_omggif = __commonJS({
119
119
  var min_code_size = 0;
120
120
  while (num_colors >>= 1) ++min_code_size;
121
121
  num_colors = 1 << min_code_size;
122
- var delay = opts.delay === void 0 ? 0 : opts.delay;
122
+ var delay2 = opts.delay === void 0 ? 0 : opts.delay;
123
123
  var disposal = opts.disposal === void 0 ? 0 : opts.disposal;
124
124
  if (disposal < 0 || disposal > 3)
125
125
  throw new Error("Disposal out of range.");
@@ -131,13 +131,13 @@ var require_omggif = __commonJS({
131
131
  if (transparent_index < 0 || transparent_index >= num_colors)
132
132
  throw new Error("Transparent color index.");
133
133
  }
134
- if (disposal !== 0 || use_transparency || delay !== 0) {
134
+ if (disposal !== 0 || use_transparency || delay2 !== 0) {
135
135
  buf[p++] = 33;
136
136
  buf[p++] = 249;
137
137
  buf[p++] = 4;
138
138
  buf[p++] = disposal << 2 | (use_transparency === true ? 1 : 0);
139
- buf[p++] = delay & 255;
140
- buf[p++] = delay >> 8 & 255;
139
+ buf[p++] = delay2 & 255;
140
+ buf[p++] = delay2 >> 8 & 255;
141
141
  buf[p++] = transparent_index;
142
142
  buf[p++] = 0;
143
143
  }
@@ -279,7 +279,7 @@ var require_omggif = __commonJS({
279
279
  }
280
280
  var no_eof = true;
281
281
  var frames = [];
282
- var delay = 0;
282
+ var delay2 = 0;
283
283
  var transparent_index = null;
284
284
  var disposal = 0;
285
285
  var loop_count = null;
@@ -311,7 +311,7 @@ var require_omggif = __commonJS({
311
311
  if (buf[p++] !== 4 || buf[p + 4] !== 0)
312
312
  throw new Error("Invalid graphics extension block.");
313
313
  var pf1 = buf[p++];
314
- delay = buf[p++] | buf[p++] << 8;
314
+ delay2 = buf[p++] | buf[p++] << 8;
315
315
  transparent_index = buf[p++];
316
316
  if ((pf1 & 1) === 0) transparent_index = null;
317
317
  disposal = pf1 >> 2 & 7;
@@ -370,7 +370,7 @@ var require_omggif = __commonJS({
370
370
  data_length: p - data_offset,
371
371
  transparent_index,
372
372
  interlaced: !!interlace_flag,
373
- delay,
373
+ delay: delay2,
374
374
  disposal
375
375
  });
376
376
  break;
@@ -3777,7 +3777,7 @@ var require_gifframe = __commonJS({
3777
3777
  var require_gifutil = __commonJS({
3778
3778
  "../../node_modules/.bun/gifwrap@0.10.1/node_modules/gifwrap/src/gifutil.js"(exports2) {
3779
3779
  "use strict";
3780
- var fs4 = __require("fs");
3780
+ var fs5 = __require("fs");
3781
3781
  var ImageQ = require_image_q();
3782
3782
  var BitmapImage2 = require_bitmapimage();
3783
3783
  var { GifFrame: GifFrame2 } = require_gifframe();
@@ -3892,14 +3892,14 @@ var require_gifutil = __commonJS({
3892
3892
  jimpImage.bitmap.data = bitmapImageToShare.bitmap.data;
3893
3893
  return jimpImage;
3894
3894
  };
3895
- exports2.write = function(path4, frames, spec, encoder) {
3895
+ exports2.write = function(path5, frames, spec, encoder) {
3896
3896
  encoder = encoder || defaultCodec;
3897
- const matches = path4.match(/\.[a-zA-Z]+$/);
3897
+ const matches = path5.match(/\.[a-zA-Z]+$/);
3898
3898
  if (matches !== null && INVALID_SUFFIXES.includes(matches[0].toLowerCase())) {
3899
- throw new Error(`GIF '${path4}' has an unexpected suffix`);
3899
+ throw new Error(`GIF '${path5}' has an unexpected suffix`);
3900
3900
  }
3901
3901
  return encoder.encodeGif(frames, spec).then((gif2) => {
3902
- return _writeBinary(path4, gif2.buffer).then(() => {
3902
+ return _writeBinary(path5, gif2.buffer).then(() => {
3903
3903
  return gif2;
3904
3904
  });
3905
3905
  });
@@ -3971,9 +3971,9 @@ var require_gifutil = __commonJS({
3971
3971
  }
3972
3972
  }
3973
3973
  }
3974
- function _readBinary(path4) {
3974
+ function _readBinary(path5) {
3975
3975
  return new Promise((resolve2, reject2) => {
3976
- fs4.readFile(path4, (err, buffer) => {
3976
+ fs5.readFile(path5, (err, buffer) => {
3977
3977
  if (err) {
3978
3978
  return reject2(err);
3979
3979
  }
@@ -3981,9 +3981,9 @@ var require_gifutil = __commonJS({
3981
3981
  });
3982
3982
  });
3983
3983
  }
3984
- function _writeBinary(path4, buffer) {
3984
+ function _writeBinary(path5, buffer) {
3985
3985
  return new Promise((resolve2, reject2) => {
3986
- fs4.writeFile(path4, buffer, (err) => {
3986
+ fs5.writeFile(path5, buffer, (err) => {
3987
3987
  if (err) {
3988
3988
  return reject2(err);
3989
3989
  }
@@ -5964,9 +5964,9 @@ var require_decoder = __commonJS({
5964
5964
  return a < 0 ? 0 : a > 255 ? 255 : a;
5965
5965
  }
5966
5966
  constructor.prototype = {
5967
- load: function load(path4) {
5967
+ load: function load(path5) {
5968
5968
  var xhr = new XMLHttpRequest();
5969
- xhr.open("GET", path4, true);
5969
+ xhr.open("GET", path5, true);
5970
5970
  xhr.responseType = "arraybuffer";
5971
5971
  xhr.onload = (function() {
5972
5972
  var data = new Uint8Array(xhr.response || xhr.mozResponseArrayBuffer);
@@ -18862,11 +18862,11 @@ var require_Mime = __commonJS({
18862
18862
  }
18863
18863
  }
18864
18864
  };
18865
- Mime.prototype.getType = function(path4) {
18866
- path4 = String(path4);
18867
- let last = path4.replace(/^.*[/\\]/, "").toLowerCase();
18865
+ Mime.prototype.getType = function(path5) {
18866
+ path5 = String(path5);
18867
+ let last = path5.replace(/^.*[/\\]/, "").toLowerCase();
18868
18868
  let ext = last.replace(/^.*\./, "").toLowerCase();
18869
- let hasPath = last.length < path4.length;
18869
+ let hasPath = last.length < path5.length;
18870
18870
  let hasDot = ext.length < last.length - 1;
18871
18871
  return (hasDot || !hasPath) && this._types[ext] || null;
18872
18872
  };
@@ -32623,9 +32623,9 @@ function createJimp({ plugins: pluginsArg, formats: formatsArg } = {}) {
32623
32623
  * await image.write("test/output.png");
32624
32624
  * ```
32625
32625
  */
32626
- async write(path4, options) {
32627
- const mimeType = import_lite.default.getType(path4);
32628
- await writeFile(path4, await this.getBuffer(mimeType, options));
32626
+ async write(path5, options) {
32627
+ const mimeType = import_lite.default.getType(path5);
32628
+ await writeFile(path5, await this.getBuffer(mimeType, options));
32629
32629
  }
32630
32630
  /**
32631
32631
  * Clone the image into a new Jimp instance.
@@ -38485,6 +38485,27 @@ function resolveContainerFieldLabel(container) {
38485
38485
  }
38486
38486
  return void 0;
38487
38487
  }
38488
+ function shouldDropInheritedFieldLabel(element, inheritedFieldLabel) {
38489
+ if (!inheritedFieldLabel) return false;
38490
+ if (INTERACTIVE_LEAF_ROLES.has(element.role)) return false;
38491
+ if (!isClickableByAttribute(element)) return false;
38492
+ const directText = getElementText(element);
38493
+ if (directText) {
38494
+ return directText.toLowerCase() !== inheritedFieldLabel.toLowerCase();
38495
+ }
38496
+ const childTexts = [];
38497
+ for (const child of element.children) {
38498
+ if (!INTERACTIVE_LEAF_ROLES.has(child.role)) {
38499
+ const text = getElementText(child);
38500
+ if (text && TEXT_CARRYING_ROLES.has(child.role)) {
38501
+ childTexts.push(text);
38502
+ }
38503
+ }
38504
+ }
38505
+ const composedText = childTexts.join(" ");
38506
+ if (!composedText) return false;
38507
+ return composedText.toLowerCase() !== inheritedFieldLabel.toLowerCase();
38508
+ }
38488
38509
  var TEXT_CARRYING_ROLES = /* @__PURE__ */ new Set([
38489
38510
  "generic",
38490
38511
  "paragraph",
@@ -38571,10 +38592,10 @@ function extractDataValue(text) {
38571
38592
  }
38572
38593
  function findRelevanceScope(elements, activeElementRef) {
38573
38594
  if (!activeElementRef) return null;
38574
- const path4 = [];
38595
+ const path5 = [];
38575
38596
  function findPath(el) {
38576
38597
  if (el.ref) {
38577
- path4.push({ ref: el.ref, role: el.role });
38598
+ path5.push({ ref: el.ref, role: el.role });
38578
38599
  }
38579
38600
  if (el.ref === activeElementRef) {
38580
38601
  return true;
@@ -38582,15 +38603,15 @@ function findRelevanceScope(elements, activeElementRef) {
38582
38603
  for (const child of el.children) {
38583
38604
  if (findPath(child)) return true;
38584
38605
  }
38585
- if (el.ref) path4.pop();
38606
+ if (el.ref) path5.pop();
38586
38607
  return false;
38587
38608
  }
38588
38609
  for (const el of elements) {
38589
38610
  if (findPath(el)) break;
38590
38611
  }
38591
- for (let i = path4.length - 1; i >= 0; i--) {
38592
- if (SECTION_BOUNDARY_ROLES.has(path4[i].role)) {
38593
- return path4[i].ref;
38612
+ for (let i = path5.length - 1; i >= 0; i--) {
38613
+ if (SECTION_BOUNDARY_ROLES.has(path5[i].role)) {
38614
+ return path5[i].ref;
38594
38615
  }
38595
38616
  }
38596
38617
  return null;
@@ -38639,9 +38660,9 @@ function searchElements(elements, searchTerms, scopeRef = null) {
38639
38660
  function findElementInSections(sections, targetRef, ancestors = []) {
38640
38661
  for (const section of sections) {
38641
38662
  const newAncestors = [...ancestors, section];
38642
- const path4 = newAncestors.map((s) => s.heading);
38663
+ const path5 = newAncestors.map((s) => s.heading);
38643
38664
  if (section.elements.some((el) => el.ref === targetRef)) {
38644
- return { section, ancestors: newAncestors, path: path4 };
38665
+ return { section, ancestors: newAncestors, path: path5 };
38645
38666
  }
38646
38667
  const found = findElementInSections(section.subsections, targetRef, newAncestors);
38647
38668
  if (found) return found;
@@ -38652,7 +38673,7 @@ function populateSearchContext(matches, sections) {
38652
38673
  for (const match of matches) {
38653
38674
  const result = findElementInSections(sections, match.ref);
38654
38675
  if (result) {
38655
- const { section, ancestors, path: path4 } = result;
38676
+ const { section, ancestors, path: path5 } = result;
38656
38677
  const inFocusedDialog = ancestors.some((s) => s.isFocused);
38657
38678
  const inBackground = section.isBackground || ancestors.some((s) => s.isBackground);
38658
38679
  if (inFocusedDialog) {
@@ -38663,7 +38684,7 @@ function populateSearchContext(matches, sections) {
38663
38684
  match.context = "foreground";
38664
38685
  }
38665
38686
  const SKIP_HEADINGS = ["generic", "main", "iframe"];
38666
- match.sectionPath = path4.filter((p) => !SKIP_HEADINGS.includes(p.toLowerCase())).slice(-3).join(" > ");
38687
+ match.sectionPath = path5.filter((p) => !SKIP_HEADINGS.includes(p.toLowerCase())).slice(-3).join(" > ");
38667
38688
  }
38668
38689
  }
38669
38690
  }
@@ -38825,6 +38846,24 @@ function findPrimaryColumnLabel(columnHeader) {
38825
38846
  };
38826
38847
  return search(columnHeader);
38827
38848
  }
38849
+ function isWidgetTable(element) {
38850
+ if (element.role !== "table") return false;
38851
+ const rows = element.children.filter((c2) => c2.role === "row");
38852
+ if (rows.length === 0 || rows.length > 2) return false;
38853
+ let totalCells = 0;
38854
+ let interactiveCells = 0;
38855
+ for (const row of rows) {
38856
+ for (const cell of row.children) {
38857
+ if (cell.role === "cell" || cell.role === "gridcell" || cell.role === "columnheader") {
38858
+ totalCells++;
38859
+ if (hasInteractiveDescendants(cell)) {
38860
+ interactiveCells++;
38861
+ }
38862
+ }
38863
+ }
38864
+ }
38865
+ return totalCells > 0 && interactiveCells >= totalCells * 0.5;
38866
+ }
38828
38867
  function detectGrid(element) {
38829
38868
  if (element.role !== "grid" && element.role !== "treegrid" && element.role !== "table") {
38830
38869
  return null;
@@ -39150,7 +39189,7 @@ function formatElement(element, options) {
39150
39189
  const textSource = normalizeIconLabelText(element.role, rawTextSource);
39151
39190
  const extracted = extractDataValue(textSource);
39152
39191
  const normalizedAttributes = Object.entries(element.attributes).filter(
39153
- ([key]) => key !== "ref" && key !== "cursor" && key !== "aria-controls" && key !== "aria-owns" && key !== "options"
39192
+ ([key]) => key !== "ref" && key !== "cursor" && key !== "aria-controls" && key !== "aria-owns" && key !== "options" && key !== "field"
39154
39193
  ).map(([key, val]) => {
39155
39194
  if (key === "aria-haspopup") return "haspopup";
39156
39195
  if (key === "aria-expanded") return `expanded=${val}`;
@@ -39169,7 +39208,7 @@ function formatElement(element, options) {
39169
39208
  ref: element.ref,
39170
39209
  role: element.role,
39171
39210
  name: textSource,
39172
- fieldLabel: options?.fieldLabel,
39211
+ fieldLabel: options?.fieldLabel || element.attributes["field"] || void 0,
39173
39212
  label: extracted.label || void 0,
39174
39213
  value: extracted.value,
39175
39214
  status: extracted.status,
@@ -39396,22 +39435,23 @@ function buildGridSection(element, gridInfo) {
39396
39435
  }
39397
39436
  function collectInteractiveElements(element, elements, subsections, depth, inheritedFieldLabel) {
39398
39437
  if ((INTERACTIVE_LEAF_ROLES.has(element.role) || isClickableByAttribute(element)) && element.ref) {
39438
+ const effectiveFieldLabel = shouldDropInheritedFieldLabel(element, inheritedFieldLabel) ? void 0 : inheritedFieldLabel;
39399
39439
  if (INTERACTIVE_LEAF_ROLES.has(element.role) && isExpandedWithActionableChildren(element)) {
39400
- elements.push(formatElement(element, { overrideLabel: composeTriggerLabel(element), fieldLabel: inheritedFieldLabel }));
39440
+ elements.push(formatElement(element, { overrideLabel: composeTriggerLabel(element), fieldLabel: effectiveFieldLabel }));
39401
39441
  collectExpandedChildren(element, elements);
39402
39442
  return;
39403
39443
  }
39404
39444
  if (!isMenuTriggerNoise(element)) {
39405
- elements.push(formatElement(element, { fieldLabel: inheritedFieldLabel }));
39445
+ elements.push(formatElement(element, { fieldLabel: effectiveFieldLabel }));
39406
39446
  }
39407
39447
  if (!INTERACTIVE_LEAF_ROLES.has(element.role) && isClickableByAttribute(element)) {
39408
39448
  for (const child of element.children) {
39409
- collectInteractiveElements(child, elements, subsections, depth + 1, inheritedFieldLabel);
39449
+ collectInteractiveElements(child, elements, subsections, depth + 1, effectiveFieldLabel);
39410
39450
  }
39411
39451
  }
39412
39452
  return;
39413
39453
  }
39414
- if (element.role === "heading" || STRUCTURAL_ROLES.has(element.role) || element.role === "grid" || element.role === "treegrid" || element.role === "table") {
39454
+ if (element.role === "heading" || STRUCTURAL_ROLES.has(element.role) || element.role === "grid" || element.role === "treegrid" || element.role === "table" && !isWidgetTable(element)) {
39415
39455
  const section = buildSection(element, depth);
39416
39456
  if (section) {
39417
39457
  subsections.push(section);
@@ -39449,7 +39489,8 @@ function collectInteractiveElements(element, elements, subsections, depth, inher
39449
39489
  collectInteractiveElements(child, elements, subsections, depth + 1);
39450
39490
  }
39451
39491
  } else if ((INTERACTIVE_LEAF_ROLES.has(child.role) || isClickableByAttribute(child)) && child.ref) {
39452
- const fieldLabel = findFieldLabelInContext(child, kids, ki) ?? inheritedFieldLabel;
39492
+ const rawFieldLabel = findFieldLabelInContext(child, kids, ki) ?? inheritedFieldLabel;
39493
+ const fieldLabel = shouldDropInheritedFieldLabel(child, rawFieldLabel) ? void 0 : rawFieldLabel;
39453
39494
  if (!isMenuTriggerNoise(child)) {
39454
39495
  if (INTERACTIVE_LEAF_ROLES.has(child.role) && isExpandedWithActionableChildren(child)) {
39455
39496
  elements.push(formatElement(child, { overrideLabel: composeTriggerLabel(child), fieldLabel }));
@@ -39464,7 +39505,7 @@ function collectInteractiveElements(element, elements, subsections, depth, inher
39464
39505
  }
39465
39506
  }
39466
39507
  } else {
39467
- const containerLabel = resolveContainerFieldLabel(child) ?? inheritedFieldLabel;
39508
+ const containerLabel = resolveContainerFieldLabel(child) ?? (child.role === "table" ? findFieldLabelFromSiblings(kids, ki) : void 0) ?? inheritedFieldLabel;
39468
39509
  collectInteractiveElements(child, elements, subsections, depth + 1, containerLabel);
39469
39510
  }
39470
39511
  }
@@ -39608,7 +39649,7 @@ var COLOR_NAMES = [
39608
39649
  "teal",
39609
39650
  "pink"
39610
39651
  ];
39611
- function generateFrontmatter(elementCount, sectionCount, activeElement, dialogStack, searchMatches, totalMatchCount, snapshotFilePath, probeResult) {
39652
+ function generateFrontmatter(elementCount, sectionCount, activeElement, dialogStack, searchMatches, totalMatchCount, snapshotFilePath, probeResult, currentTime, capturedToasts) {
39612
39653
  const activeLabel = activeElement ? normalizeIconLabelText(
39613
39654
  activeElement.role,
39614
39655
  INTERACTIVE_LEAF_ROLES.has(activeElement.role) && isExpandedWithActionableChildren(activeElement) ? composeTriggerLabel(activeElement) : activeElement.text || ""
@@ -39616,6 +39657,16 @@ function generateFrontmatter(elementCount, sectionCount, activeElement, dialogSt
39616
39657
  const activeDesc = activeElement ? `${activeElement.ref} ${activeElement.role} "${activeLabel}"` : "None";
39617
39658
  const dialogStackLine = dialogStack.length > 0 ? `
39618
39659
  DIALOG STACK: ${dialogStack.join(" \u2192 ")}` : "";
39660
+ let toastSection = "";
39661
+ if (capturedToasts && capturedToasts.length > 0) {
39662
+ const toastLines = capturedToasts.map((t) => {
39663
+ const ttl = t.snapshotsRemaining !== void 0 ? ` (expires in ${t.snapshotsRemaining} snapshots)` : "";
39664
+ return ` - [${t.role}] "${t.text}"${ttl}`;
39665
+ });
39666
+ toastSection = `
39667
+ TOAST NOTIFICATIONS:
39668
+ ${toastLines.join("\n")}`;
39669
+ }
39619
39670
  let searchSection = "";
39620
39671
  if (searchMatches && searchMatches.length > 0) {
39621
39672
  const hasContextInfo = searchMatches.some((m) => m.context);
@@ -39665,10 +39716,12 @@ PROBE RESULT:
39665
39716
  FULL TREE FILE: ${snapshotFilePath}
39666
39717
  Use bash or python_execute to grep/parse this file for detailed element analysis.`;
39667
39718
  }
39719
+ const browserTime = currentTime ?? (/* @__PURE__ */ new Date()).toISOString();
39668
39720
  return `---
39669
39721
  SEMANTIC SNAPSHOT - ${elementCount} elements in ${sectionCount} sections
39722
+ BROWSER TIME: ${browserTime} (UTC)
39670
39723
  FORMAT: Sections grouped by headings. [ref] tags identify clickable elements.
39671
- ACTIVE ELEMENT: ${activeDesc}${dialogStackLine}${searchSection}${probeSection}${fullTreeFileSection}
39724
+ ACTIVE ELEMENT: ${activeDesc}${dialogStackLine}${toastSection}${searchSection}${probeSection}${fullTreeFileSection}
39672
39725
  ACTIONS:
39673
39726
  - Click/type using [ref] values (e.g., browser_click ref=e123)${typeActionHint}
39674
39727
  - Click using coordinates from search results (e.g., browser_click x=450 y=320)
@@ -39738,7 +39791,9 @@ function formatSemanticSnapshot(yaml, options) {
39738
39791
  // Use re-sorted matches with context
39739
39792
  options?.totalMatchCount,
39740
39793
  options?.snapshotFilePath,
39741
- options?.probeResult
39794
+ options?.probeResult,
39795
+ options?.currentTime,
39796
+ options?.capturedToasts
39742
39797
  )
39743
39798
  );
39744
39799
  lines.push("");
@@ -40137,10 +40192,11 @@ function generateHint(diff2, networkInfo) {
40137
40192
  }
40138
40193
  return hints.join(" ");
40139
40194
  }
40140
- function formatSemanticDiff(diff2, actionDescription, networkInfo) {
40195
+ function formatSemanticDiff(diff2, actionDescription, networkInfo, currentTime) {
40141
40196
  const lines = [];
40142
40197
  lines.push("---");
40143
40198
  lines.push("SEMANTIC DIFF: Page state changes after browser action.");
40199
+ lines.push(`BROWSER TIME: ${currentTime ?? (/* @__PURE__ */ new Date()).toISOString()} (UTC)`);
40144
40200
  lines.push("Format: [PREVIOUS] value [CURRENT] value");
40145
40201
  lines.push("");
40146
40202
  lines.push(`ACTION: ${actionDescription}`);
@@ -40219,6 +40275,38 @@ function formatValue(value) {
40219
40275
  return `"${truncate(value, 20)}"`;
40220
40276
  }
40221
40277
 
40278
+ // ../browser-core/src/playwright-client/download-utils.ts
40279
+ import * as path from "path";
40280
+ function formatFileSize(bytes) {
40281
+ if (bytes < 1024) return `${bytes}B`;
40282
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
40283
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
40284
+ }
40285
+ function guessMimeType(filename) {
40286
+ const ext = path.extname(filename).toLowerCase();
40287
+ const mimeMap = {
40288
+ ".pdf": "application/pdf",
40289
+ ".txt": "text/plain",
40290
+ ".csv": "text/csv",
40291
+ ".json": "application/json",
40292
+ ".xml": "application/xml",
40293
+ ".html": "text/html",
40294
+ ".htm": "text/html",
40295
+ ".md": "text/markdown",
40296
+ ".png": "image/png",
40297
+ ".jpg": "image/jpeg",
40298
+ ".jpeg": "image/jpeg",
40299
+ ".gif": "image/gif",
40300
+ ".svg": "image/svg+xml",
40301
+ ".zip": "application/zip",
40302
+ ".doc": "application/msword",
40303
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
40304
+ ".xls": "application/vnd.ms-excel",
40305
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
40306
+ };
40307
+ return mimeMap[ext] ?? "application/octet-stream";
40308
+ }
40309
+
40222
40310
  // ../browser-core/src/playwright-client/element-inspection.ts
40223
40311
  function errorMessage(error) {
40224
40312
  return error instanceof Error ? error.message : String(error);
@@ -40618,7 +40706,7 @@ async function extractElementMetadata(page, locator, ref, logger2) {
40618
40706
  }
40619
40707
 
40620
40708
  // ../browser-core/src/tools/executor.ts
40621
- import * as path from "path";
40709
+ import * as path2 from "path";
40622
40710
  import * as fs2 from "fs/promises";
40623
40711
  import * as crypto2 from "crypto";
40624
40712
 
@@ -40747,7 +40835,10 @@ var BrowserToolExecutor = class {
40747
40835
  this.allowedHosts = options?.allowedHosts;
40748
40836
  this.autoSnapshotAfterAction = options?.autoSnapshotAfterAction !== false;
40749
40837
  this.includeScreenshotWithSnapshot = options?.includeScreenshotWithSnapshot !== false;
40750
- this.workspaceDir = options?.workspaceDir ?? path.join(getCanaryTmpDir(), `browser-workspace-${Date.now()}-${crypto2.randomUUID().slice(0, 8)}`);
40838
+ if (!options?.workspaceDir && !options?.tmpBaseDir) {
40839
+ options?.logger?.debug?.("[BrowserToolExecutor] No tmpBaseDir provided, using flat canary dir (not org-scoped)");
40840
+ }
40841
+ this.workspaceDir = options?.workspaceDir ?? path2.join(options?.tmpBaseDir ?? getCanaryTmpDir(), `browser-workspace-${Date.now()}-${crypto2.randomUUID().slice(0, 8)}`);
40751
40842
  this.logger = options?.logger;
40752
40843
  fs2.mkdir(this.workspaceDir, { recursive: true }).catch(() => {
40753
40844
  });
@@ -40912,15 +41003,18 @@ ${effectiveShowCoordinateGrid ? "Coordinate grid enabled - read coordinates from
40912
41003
  try {
40913
41004
  await fs2.mkdir(this.workspaceDir, { recursive: true });
40914
41005
  const filename = `snapshot-${Date.now()}-${crypto2.randomUUID().slice(0, 8)}.yaml`;
40915
- snapshotFilePath = path.join(this.workspaceDir, filename);
41006
+ snapshotFilePath = path2.join(this.workspaceDir, filename);
40916
41007
  await fs2.writeFile(snapshotFilePath, yaml, "utf-8");
40917
41008
  } catch {
40918
41009
  }
41010
+ const now = (/* @__PURE__ */ new Date()).toISOString();
40919
41011
  const semanticText = formatSemanticSnapshot(yaml, {
40920
41012
  searchMatches: searchMatches.length > 0 ? searchMatches : void 0,
40921
41013
  totalMatchCount: totalMatchCount > 0 ? totalMatchCount : void 0,
40922
41014
  snapshotFilePath,
40923
- probeResult
41015
+ probeResult,
41016
+ currentTime: now,
41017
+ capturedToasts: this.client.getCapturedToasts?.() ?? []
40924
41018
  });
40925
41019
  if (hasImages) {
40926
41020
  const resolutionNote = buildResolutionNote(
@@ -41163,7 +41257,7 @@ ${effectiveShowCoordinateGrid ? "Coordinate grid enabled - read coordinates from
41163
41257
  if (!afterModal) {
41164
41258
  const afterState2 = captureSnapshotState(afterUrl2, afterYaml2);
41165
41259
  const diff3 = compareSnapshots(beforeState, afterState2);
41166
- return formatSemanticDiff(diff3, `Dismissed overlay using ${strategy}`);
41260
+ return formatSemanticDiff(diff3, `Dismissed overlay using ${strategy}`, void 0, (/* @__PURE__ */ new Date()).toISOString());
41167
41261
  }
41168
41262
  } catch (err) {
41169
41263
  this.logger?.debug?.(`[BrowserTools] Dismiss strategy ${strategy} failed`, {
@@ -41179,7 +41273,7 @@ ${effectiveShowCoordinateGrid ? "Coordinate grid enabled - read coordinates from
41179
41273
  const diff2 = compareSnapshots(beforeState, afterState);
41180
41274
  return `Could not dismiss overlay with strategies: ${strategies.join(", ")}. Modal may require specific interaction.
41181
41275
 
41182
- ` + formatSemanticDiff(diff2, "Dismiss overlay attempted (failed)");
41276
+ ` + formatSemanticDiff(diff2, "Dismiss overlay attempted (failed)", void 0, (/* @__PURE__ */ new Date()).toISOString());
41183
41277
  }
41184
41278
  // ==================== Waiting ====================
41185
41279
  async waitFor(opts) {
@@ -41293,7 +41387,8 @@ ${effectiveShowCoordinateGrid ? "Coordinate grid enabled - read coordinates from
41293
41387
  }
41294
41388
  const diff2 = compareSnapshots(beforeState, afterState);
41295
41389
  const networkInfo = this.client.getPendingNetworkInfo?.() ?? void 0;
41296
- const diffText = formatSemanticDiff(diff2, actionDescription, networkInfo);
41390
+ const now = (/* @__PURE__ */ new Date()).toISOString();
41391
+ const diffText = formatSemanticDiff(diff2, actionDescription, networkInfo, now);
41297
41392
  const hasChanges = hasDiffChanges(diff2);
41298
41393
  const isStable = !networkInfo || networkInfo.pendingCount === 0;
41299
41394
  let autoSnapshot;
@@ -41303,11 +41398,11 @@ ${effectiveShowCoordinateGrid ? "Coordinate grid enabled - read coordinates from
41303
41398
  try {
41304
41399
  await fs2.mkdir(this.workspaceDir, { recursive: true });
41305
41400
  const filename = `snapshot-${Date.now()}-${crypto2.randomUUID().slice(0, 8)}.yaml`;
41306
- snapshotFilePath = path.join(this.workspaceDir, filename);
41401
+ snapshotFilePath = path2.join(this.workspaceDir, filename);
41307
41402
  await fs2.writeFile(snapshotFilePath, afterYaml, "utf-8");
41308
41403
  } catch {
41309
41404
  }
41310
- const semanticText = formatSemanticSnapshot(afterYaml, { snapshotFilePath });
41405
+ const semanticText = formatSemanticSnapshot(afterYaml, { snapshotFilePath, currentTime: now, capturedToasts: this.client.getCapturedToasts?.() ?? [] });
41311
41406
  const afterImages = isMCPContentWithImages(afterResult) ? afterResult.images : void 0;
41312
41407
  if (afterImages?.length && this.onScreenshot) {
41313
41408
  const img = afterImages[0];
@@ -41344,11 +41439,11 @@ ${effectiveShowCoordinateGrid ? "Coordinate grid enabled - read coordinates from
41344
41439
  try {
41345
41440
  await fs2.mkdir(this.workspaceDir, { recursive: true });
41346
41441
  const filename = `snapshot-${Date.now()}-${crypto2.randomUUID().slice(0, 8)}.yaml`;
41347
- snapshotFilePath = path.join(this.workspaceDir, filename);
41442
+ snapshotFilePath = path2.join(this.workspaceDir, filename);
41348
41443
  await fs2.writeFile(snapshotFilePath, yaml, "utf-8");
41349
41444
  } catch {
41350
41445
  }
41351
- const semanticText = formatSemanticSnapshot(yaml, { snapshotFilePath });
41446
+ const semanticText = formatSemanticSnapshot(yaml, { snapshotFilePath, currentTime: (/* @__PURE__ */ new Date()).toISOString(), capturedToasts: this.client.getCapturedToasts?.() ?? [] });
41352
41447
  const yamlBlockPattern = /- Page Snapshot:\n```yaml\n[\s\S]*?```/;
41353
41448
  if (yamlBlockPattern.test(result)) {
41354
41449
  return result.replace(yamlBlockPattern, `- Page Snapshot (semantic):
@@ -42583,8 +42678,8 @@ var errors = playwrightLoader.lazyloadExportOrDie("errors");
42583
42678
 
42584
42679
  // ../browser-core/src/playwright-client.ts
42585
42680
  var import_puppeteer_extra_plugin_stealth = __toESM(require_puppeteer_extra_plugin_stealth(), 1);
42586
- import * as fs3 from "fs/promises";
42587
- import * as path3 from "path";
42681
+ import * as fs4 from "fs/promises";
42682
+ import * as path4 from "path";
42588
42683
 
42589
42684
  // ../browser-core/src/playwright-client/toast-capture-script.ts
42590
42685
  var TOAST_CAPTURE_SCRIPT = `
@@ -42687,7 +42782,7 @@ var TOAST_CAPTURE_SCRIPT = `
42687
42782
  return (clone.textContent || '').trim();
42688
42783
  }
42689
42784
 
42690
- function pushToast(text, role) {
42785
+ function pushToast(text, role, el) {
42691
42786
  if (!text || text.length === 0 || text.length >= 1000) return;
42692
42787
  // Deduplicate: skip if we already have the same text from the last 2 seconds
42693
42788
  const now = Date.now();
@@ -42695,7 +42790,21 @@ var TOAST_CAPTURE_SCRIPT = `
42695
42790
  (t) => t.text === text && now - t.time < 2000
42696
42791
  );
42697
42792
  if (isDupe) return;
42698
- window.__capturedToasts.push({ text: text, role: role, time: now });
42793
+
42794
+ let placement = null;
42795
+ if (el && el.getBoundingClientRect) {
42796
+ const rect = el.getBoundingClientRect();
42797
+ if (rect.width > 0 && rect.height > 0) {
42798
+ placement = {
42799
+ x: Math.round(rect.x),
42800
+ y: Math.round(rect.y),
42801
+ width: Math.round(rect.width),
42802
+ height: Math.round(rect.height),
42803
+ };
42804
+ }
42805
+ }
42806
+
42807
+ window.__capturedToasts.push({ text: text, role: role, time: now, placement: placement });
42699
42808
  if (window.__capturedToasts.length > MAX_TOASTS) {
42700
42809
  window.__capturedToasts.shift();
42701
42810
  }
@@ -42714,7 +42823,7 @@ var TOAST_CAPTURE_SCRIPT = `
42714
42823
  if (el.matches && el.matches(TOAST_SELECTORS) && isElementVisible(el)) {
42715
42824
  const text = extractToastText(el);
42716
42825
  if (text) {
42717
- pushToast(text, el.getAttribute('role') || el.getAttribute('aria-live') || 'toast');
42826
+ pushToast(text, el.getAttribute('role') || el.getAttribute('aria-live') || 'toast', el);
42718
42827
  return;
42719
42828
  }
42720
42829
  }
@@ -42725,7 +42834,7 @@ var TOAST_CAPTURE_SCRIPT = `
42725
42834
  if (isElementVisible(desc)) {
42726
42835
  const text = extractToastText(desc);
42727
42836
  if (text) {
42728
- pushToast(text, desc.getAttribute('role') || desc.getAttribute('aria-live') || 'toast');
42837
+ pushToast(text, desc.getAttribute('role') || desc.getAttribute('aria-live') || 'toast', desc);
42729
42838
  }
42730
42839
  }
42731
42840
  }
@@ -42772,7 +42881,8 @@ var TOAST_CAPTURE_SCRIPT = `
42772
42881
  if (el.matches(TOAST_SELECTORS)) {
42773
42882
  pushToast(
42774
42883
  extractToastText(el),
42775
- el.getAttribute('role') || el.getAttribute('aria-live') || 'toast'
42884
+ el.getAttribute('role') || el.getAttribute('aria-live') || 'toast',
42885
+ el
42776
42886
  );
42777
42887
  // Install secondary observer if this is also a live-region container
42778
42888
  if (el.matches(LIVE_REGION_CONTAINER)) {
@@ -42787,7 +42897,8 @@ var TOAST_CAPTURE_SCRIPT = `
42787
42897
  for (const child of children) {
42788
42898
  pushToast(
42789
42899
  extractToastText(child),
42790
- child.getAttribute('role') || child.getAttribute('aria-live') || 'toast'
42900
+ child.getAttribute('role') || child.getAttribute('aria-live') || 'toast',
42901
+ child
42791
42902
  );
42792
42903
  }
42793
42904
  if (children.length > 0) return;
@@ -42803,7 +42914,7 @@ var TOAST_CAPTURE_SCRIPT = `
42803
42914
  const role = container
42804
42915
  ? container.getAttribute('role') || container.getAttribute('aria-live') || 'live-region'
42805
42916
  : 'live-region';
42806
- pushToast(text, role);
42917
+ pushToast(text, role, el);
42807
42918
  }
42808
42919
  // Install secondary observer on the container for future CSS toggles
42809
42920
  installContainerObserver(container);
@@ -43291,11 +43402,44 @@ async function enrichHiddenClickableElements(page, logger2) {
43291
43402
  try {
43292
43403
  const enrichedCount = await page.evaluate(() => {
43293
43404
  let count = 0;
43294
- const patterns = [
43295
- ["abbr.search-choice-close", "search-choice-close"],
43405
+ const chosenCloseButtons = document.querySelectorAll("abbr.search-choice-close");
43406
+ for (const el of chosenCloseButtons) {
43407
+ if (el.getAttribute("data-enriched-clickable") === "true") continue;
43408
+ if (el.getAttribute("role") === "button") continue;
43409
+ let fieldName;
43410
+ const container = el.closest(".chosen-container");
43411
+ if (container) {
43412
+ const selectEl = container.previousElementSibling;
43413
+ if (selectEl && selectEl.tagName === "SELECT") {
43414
+ const selectId = selectEl.id;
43415
+ if (selectId) {
43416
+ const labelEl = document.querySelector(`label[for="${selectId}"]`);
43417
+ if (labelEl?.textContent?.trim()) {
43418
+ fieldName = labelEl.textContent.trim();
43419
+ }
43420
+ }
43421
+ if (!fieldName) {
43422
+ fieldName = selectEl.getAttribute("aria-label") || void 0;
43423
+ }
43424
+ }
43425
+ if (!fieldName && container.parentElement) {
43426
+ const prev = container.parentElement.previousElementSibling;
43427
+ if (prev && (prev.tagName === "LABEL" || prev.querySelector("label"))) {
43428
+ const labelText = prev.textContent?.trim();
43429
+ if (labelText && labelText.length < 80) fieldName = labelText;
43430
+ }
43431
+ }
43432
+ }
43433
+ const label = fieldName ? `Clear ${fieldName}` : "Clear selection";
43434
+ el.setAttribute("role", "button");
43435
+ el.setAttribute("aria-label", label);
43436
+ el.setAttribute("data-enriched-clickable", "true");
43437
+ count++;
43438
+ }
43439
+ const genericPatterns = [
43296
43440
  [".select2-selection__clear", "select2-selection__clear"]
43297
43441
  ];
43298
- for (const [selector, fallbackLabel] of patterns) {
43442
+ for (const [selector, fallbackLabel] of genericPatterns) {
43299
43443
  const elements = document.querySelectorAll(selector);
43300
43444
  for (const el of elements) {
43301
43445
  if (el.getAttribute("data-enriched-clickable") === "true") continue;
@@ -43307,6 +43451,12 @@ async function enrichHiddenClickableElements(page, logger2) {
43307
43451
  count++;
43308
43452
  }
43309
43453
  }
43454
+ const chosenSearchInputs = document.querySelectorAll(
43455
+ ".chosen-container:not(.chosen-with-drop) .chosen-search-input"
43456
+ );
43457
+ for (const input of chosenSearchInputs) {
43458
+ input.setAttribute("aria-hidden", "true");
43459
+ }
43310
43460
  return count;
43311
43461
  });
43312
43462
  if (enrichedCount > 0) {
@@ -43345,10 +43495,29 @@ async function augmentHiddenSelectOptions(page, yaml, logger2) {
43345
43495
  if (!trigger) continue;
43346
43496
  const rect = trigger.getBoundingClientRect();
43347
43497
  const triggerText = trigger.textContent?.trim()?.slice(0, 80) || null;
43498
+ let fieldLabel = null;
43499
+ const selectId = sel.id;
43500
+ if (selectId) {
43501
+ const labelEl = document.querySelector(`label[for="${selectId}"]`);
43502
+ if (labelEl?.textContent?.trim()) {
43503
+ fieldLabel = labelEl.textContent.trim();
43504
+ }
43505
+ }
43506
+ if (!fieldLabel) {
43507
+ fieldLabel = sel.getAttribute("aria-label") || null;
43508
+ }
43509
+ if (!fieldLabel && parent.previousElementSibling) {
43510
+ const prev = parent.previousElementSibling;
43511
+ if (prev.tagName === "LABEL" || prev.querySelector("label")) {
43512
+ const labelText = prev.textContent?.trim();
43513
+ if (labelText && labelText.length < 80) fieldLabel = labelText;
43514
+ }
43515
+ }
43348
43516
  results.push({
43349
43517
  options: opts.slice(0, 15),
43350
43518
  triggerText,
43351
- triggerRect: rect.width > 0 ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } : null
43519
+ triggerRect: rect.width > 0 ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } : null,
43520
+ fieldLabel
43352
43521
  });
43353
43522
  }
43354
43523
  return results;
@@ -43360,7 +43529,7 @@ async function augmentHiddenSelectOptions(page, yaml, logger2) {
43360
43529
  const elements = parseSnapshot(yaml);
43361
43530
  let augmented = yaml;
43362
43531
  for (const selectInfo of hiddenSelects) {
43363
- const { options: opts, triggerText, triggerRect } = selectInfo;
43532
+ const { options: opts, triggerText, triggerRect, fieldLabel } = selectInfo;
43364
43533
  if (!opts.length) continue;
43365
43534
  let matchedRef = null;
43366
43535
  if (triggerText) {
@@ -43409,11 +43578,13 @@ async function augmentHiddenSelectOptions(page, yaml, logger2) {
43409
43578
  }
43410
43579
  if (!matchedRef) continue;
43411
43580
  const optionsValue = opts.join("|");
43412
- augmented = updateElementLineByRef(
43413
- augmented,
43414
- matchedRef,
43415
- (line) => appendAttributeIfMissing(line, "options", optionsValue)
43416
- );
43581
+ augmented = updateElementLineByRef(augmented, matchedRef, (line) => {
43582
+ let next = appendAttributeIfMissing(line, "options", optionsValue);
43583
+ if (fieldLabel) {
43584
+ next = appendAttributeIfMissing(next, "field", fieldLabel);
43585
+ }
43586
+ return next;
43587
+ });
43417
43588
  }
43418
43589
  return augmented;
43419
43590
  } catch (err) {
@@ -43469,38 +43640,6 @@ async function enrichInteractiveSVGElements(page, logger2) {
43469
43640
  }
43470
43641
  }
43471
43642
 
43472
- // ../browser-core/src/playwright-client/download-utils.ts
43473
- import * as path2 from "path";
43474
- function formatFileSize(bytes) {
43475
- if (bytes < 1024) return `${bytes}B`;
43476
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
43477
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
43478
- }
43479
- function guessMimeType(filename) {
43480
- const ext = path2.extname(filename).toLowerCase();
43481
- const mimeMap = {
43482
- ".pdf": "application/pdf",
43483
- ".txt": "text/plain",
43484
- ".csv": "text/csv",
43485
- ".json": "application/json",
43486
- ".xml": "application/xml",
43487
- ".html": "text/html",
43488
- ".htm": "text/html",
43489
- ".md": "text/markdown",
43490
- ".png": "image/png",
43491
- ".jpg": "image/jpeg",
43492
- ".jpeg": "image/jpeg",
43493
- ".gif": "image/gif",
43494
- ".svg": "image/svg+xml",
43495
- ".zip": "application/zip",
43496
- ".doc": "application/msword",
43497
- ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
43498
- ".xls": "application/vnd.ms-excel",
43499
- ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
43500
- };
43501
- return mimeMap[ext] ?? "application/octet-stream";
43502
- }
43503
-
43504
43643
  // ../browser-core/src/playwright-client/response-format.ts
43505
43644
  var TOAST_SNAPSHOT_TTL = 5;
43506
43645
  function formatMcpResponse(input) {
@@ -43509,7 +43648,8 @@ function formatMcpResponse(input) {
43509
43648
  lines.push("### Toast Notifications");
43510
43649
  for (const toast of input.capturedToasts) {
43511
43650
  const remaining = toast.snapshotsRemaining ?? 1;
43512
- lines.push(`- [${toast.role}] ${toast.text} (expires in ${remaining} snapshot${remaining !== 1 ? "s" : ""})`);
43651
+ const placementHint = toast.placement ? ` [visible at (${toast.placement.x}, ${toast.placement.y}), ${toast.placement.width}\xD7${toast.placement.height}px]` : "";
43652
+ lines.push(`- [${toast.role}] ${toast.text}${placementHint} (expires in ${remaining} snapshot${remaining !== 1 ? "s" : ""})`);
43513
43653
  }
43514
43654
  lines.push("");
43515
43655
  }
@@ -43558,17 +43698,184 @@ function formatMcpResponse(input) {
43558
43698
  };
43559
43699
  }
43560
43700
 
43701
+ // ../browser-core/src/playwright-client/launch-retry.ts
43702
+ var TRANSIENT_BROWSER_LAUNCH_PATTERNS = [
43703
+ /failed to connect/i,
43704
+ /syscall["':\s]+connect/i,
43705
+ /\bENOENT\b/i,
43706
+ /browser has been closed/i,
43707
+ /target page, context or browser has been closed/i,
43708
+ /failed to launch/i,
43709
+ /\bSIGKILL\b/i,
43710
+ /process was terminated/i
43711
+ ];
43712
+ function formatBrowserLaunchError(error) {
43713
+ if (error instanceof Error) {
43714
+ const extra = error;
43715
+ const parts = [error.message];
43716
+ if (extra.code) parts.push(`code=${extra.code}`);
43717
+ if (typeof extra.errno === "number") parts.push(`errno=${extra.errno}`);
43718
+ if (extra.syscall) parts.push(`syscall=${extra.syscall}`);
43719
+ if (extra.cause) parts.push(`cause=${formatBrowserLaunchError(extra.cause)}`);
43720
+ return parts.join(" ");
43721
+ }
43722
+ return String(error);
43723
+ }
43724
+ function isTransientBrowserLaunchError(error) {
43725
+ const text = formatBrowserLaunchError(error);
43726
+ return TRANSIENT_BROWSER_LAUNCH_PATTERNS.some((pattern) => pattern.test(text));
43727
+ }
43728
+ async function withBrowserLaunchRetry(operation, options) {
43729
+ const maxAttempts = options.maxAttempts ?? 3;
43730
+ const initialDelayMs = options.initialDelayMs ?? 150;
43731
+ let lastError;
43732
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
43733
+ try {
43734
+ return await operation(attempt);
43735
+ } catch (error) {
43736
+ lastError = error;
43737
+ if (attempt >= maxAttempts || !isTransientBrowserLaunchError(error)) {
43738
+ throw error;
43739
+ }
43740
+ const delayMs = initialDelayMs * 2 ** (attempt - 1);
43741
+ options.logger.warn("[DirectPlaywright] Browser launch failed transiently, retrying", {
43742
+ label: options.label,
43743
+ attempt,
43744
+ nextAttempt: attempt + 1,
43745
+ delayMs,
43746
+ error: formatBrowserLaunchError(error)
43747
+ });
43748
+ await new Promise((resolve2) => setTimeout(resolve2, delayMs));
43749
+ }
43750
+ }
43751
+ throw lastError;
43752
+ }
43753
+
43754
+ // ../browser-core/src/playwright-client/test-browser-lock.ts
43755
+ import * as fs3 from "fs/promises";
43756
+ import * as path3 from "path";
43757
+ var TEST_BROWSER_LOCK_DIR = path3.join(getCanaryTmpDir(), "playwright-test-browser-lock");
43758
+ var TEST_BROWSER_LOCK_METADATA_PATH = path3.join(TEST_BROWSER_LOCK_DIR, "owner.json");
43759
+ var TEST_BROWSER_LOCK_POLL_MS = 100;
43760
+ var TEST_BROWSER_LOCK_TIMEOUT_MS = 12e4;
43761
+ var TEST_BROWSER_LOCK_STALE_MS = 15 * 6e4;
43762
+ function shouldUseTestBrowserLock(options) {
43763
+ return process.env.NODE_ENV === "test" && !options.browserLease && !options.cdpUrl && options.browserMode !== "headed";
43764
+ }
43765
+ async function acquireTestBrowserLock(logger2) {
43766
+ const startedAt = Date.now();
43767
+ const owner = {
43768
+ pid: process.pid,
43769
+ acquiredAt: (/* @__PURE__ */ new Date()).toISOString()
43770
+ };
43771
+ while (true) {
43772
+ try {
43773
+ await fs3.mkdir(TEST_BROWSER_LOCK_DIR);
43774
+ await fs3.writeFile(TEST_BROWSER_LOCK_METADATA_PATH, JSON.stringify(owner), "utf8");
43775
+ logger2.debug("[DirectPlaywright] Acquired test browser lock", {
43776
+ lockDir: TEST_BROWSER_LOCK_DIR,
43777
+ pid: process.pid
43778
+ });
43779
+ let released = false;
43780
+ return async () => {
43781
+ if (released) return;
43782
+ released = true;
43783
+ try {
43784
+ await fs3.rm(TEST_BROWSER_LOCK_DIR, { recursive: true, force: true });
43785
+ logger2.debug("[DirectPlaywright] Released test browser lock", {
43786
+ lockDir: TEST_BROWSER_LOCK_DIR,
43787
+ pid: process.pid
43788
+ });
43789
+ } catch (error) {
43790
+ logger2.warn("[DirectPlaywright] Failed to release test browser lock", {
43791
+ lockDir: TEST_BROWSER_LOCK_DIR,
43792
+ pid: process.pid,
43793
+ error: error instanceof Error ? error.message : String(error)
43794
+ });
43795
+ }
43796
+ };
43797
+ } catch (error) {
43798
+ const code = error.code;
43799
+ if (code !== "EEXIST") {
43800
+ throw error;
43801
+ }
43802
+ await cleanupStaleTestBrowserLock(logger2);
43803
+ if (Date.now() - startedAt >= TEST_BROWSER_LOCK_TIMEOUT_MS) {
43804
+ throw new Error(
43805
+ `Timed out acquiring test browser lock after ${TEST_BROWSER_LOCK_TIMEOUT_MS}ms`
43806
+ );
43807
+ }
43808
+ await delay(TEST_BROWSER_LOCK_POLL_MS);
43809
+ }
43810
+ }
43811
+ }
43812
+ async function cleanupStaleTestBrowserLock(logger2) {
43813
+ const metadata = await readLockMetadata();
43814
+ if (!metadata) {
43815
+ const orphanedLockStats = await fs3.stat(TEST_BROWSER_LOCK_DIR).catch(() => null);
43816
+ if (!orphanedLockStats) {
43817
+ return;
43818
+ }
43819
+ const orphanedAgeMs = Date.now() - orphanedLockStats.mtimeMs;
43820
+ if (orphanedAgeMs < 1e3) {
43821
+ return;
43822
+ }
43823
+ logger2.warn("[DirectPlaywright] Removing orphaned test browser lock", {
43824
+ lockDir: TEST_BROWSER_LOCK_DIR,
43825
+ ageMs: orphanedAgeMs
43826
+ });
43827
+ await fs3.rm(TEST_BROWSER_LOCK_DIR, { recursive: true, force: true });
43828
+ return;
43829
+ }
43830
+ const ageMs = Date.now() - Date.parse(metadata.acquiredAt);
43831
+ if (ageMs < TEST_BROWSER_LOCK_STALE_MS && isProcessAlive(metadata.pid)) {
43832
+ return;
43833
+ }
43834
+ logger2.warn("[DirectPlaywright] Removing stale test browser lock", {
43835
+ lockDir: TEST_BROWSER_LOCK_DIR,
43836
+ ownerPid: metadata.pid,
43837
+ ageMs
43838
+ });
43839
+ await fs3.rm(TEST_BROWSER_LOCK_DIR, { recursive: true, force: true });
43840
+ }
43841
+ async function readLockMetadata() {
43842
+ try {
43843
+ const raw = await fs3.readFile(TEST_BROWSER_LOCK_METADATA_PATH, "utf8");
43844
+ return JSON.parse(raw);
43845
+ } catch {
43846
+ return null;
43847
+ }
43848
+ }
43849
+ function isProcessAlive(pid) {
43850
+ try {
43851
+ process.kill(pid, 0);
43852
+ return true;
43853
+ } catch (error) {
43854
+ const code = error.code;
43855
+ return code !== "ESRCH";
43856
+ }
43857
+ }
43858
+ function delay(ms) {
43859
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
43860
+ }
43861
+
43561
43862
  // ../browser-core/src/playwright-client.ts
43562
43863
  var PlaywrightClient = class _PlaywrightClient {
43563
43864
  disconnectInProgress = false;
43564
43865
  browserLease;
43866
+ releaseTestBrowserLock = null;
43565
43867
  logger;
43566
43868
  screencastEncoderFactory;
43869
+ tmpBaseDir;
43567
43870
  constructor(options) {
43568
43871
  this.browserLease = options?.browserLease ?? null;
43569
43872
  this.logger = options?.logger ?? consoleLogger;
43570
43873
  this.screencastEncoderFactory = options?.screencastEncoderFactory ?? null;
43571
43874
  this.cursorOverlay = new CursorOverlay({ enabled: false });
43875
+ if (!options?.tmpBaseDir) {
43876
+ this.logger.debug("[PlaywrightClient] No tmpBaseDir provided, using flat canary dir (not org-scoped)");
43877
+ }
43878
+ this.tmpBaseDir = options?.tmpBaseDir ?? getCanaryTmpDir();
43572
43879
  }
43573
43880
  /** Assign a stable monotonic ID to a page and return it. */
43574
43881
  _assignPageId(page) {
@@ -43718,8 +44025,8 @@ var PlaywrightClient = class _PlaywrightClient {
43718
44025
  const cursorOpts = typeof cursorOverlay === "object" ? cursorOverlay : {};
43719
44026
  this.cursorOverlay = new CursorOverlay({ ...cursorOpts, enabled: cursorEnabled });
43720
44027
  }
43721
- this._downloadDir = path3.join(getCanaryTmpDir(), `playwright-downloads-${Date.now()}`);
43722
- await fs3.mkdir(this._downloadDir, { recursive: true });
44028
+ this._downloadDir = path4.join(this.tmpBaseDir, `playwright-downloads-${Date.now()}`);
44029
+ await fs4.mkdir(this._downloadDir, { recursive: true });
43723
44030
  this.logger.info("[DirectPlaywright] Launching browser", {
43724
44031
  browserMode: resolvedBrowserMode,
43725
44032
  hasStorageState: !!storageStatePath,
@@ -43730,82 +44037,100 @@ var PlaywrightClient = class _PlaywrightClient {
43730
44037
  highlightEnabled: this.highlightEnabled,
43731
44038
  downloadDir: this._downloadDir
43732
44039
  });
43733
- if (cdpUrl) {
43734
- this.logger.info("[DirectPlaywright] Connecting via CDP", { cdpUrl });
43735
- this.browser = await this.withOperationTimeout(
43736
- playwrightChromium.connectOverCDP(cdpUrl),
43737
- 3e4,
43738
- "[DirectPlaywright] connectOverCDP"
43739
- );
43740
- const existingContexts = this.browser.contexts();
43741
- this.context = existingContexts[0] ?? await this.browser.newContext({ viewport: resolvedViewport });
43742
- } else if (this.browserLease && !this.stealthEnabled && !this.extensionsEnabled && resolvedBrowserMode !== "headed") {
43743
- const videoConfig2 = this.resolveVideoConfig(recordVideo);
43744
- const contextOptions = {
43745
- viewport: resolvedViewport,
43746
- acceptDownloads: true
43747
- };
43748
- if (storageStatePath) {
43749
- try {
43750
- await fs3.access(storageStatePath);
43751
- contextOptions.storageState = storageStatePath;
43752
- } catch {
44040
+ try {
44041
+ if (shouldUseTestBrowserLock({
44042
+ browserLease: !!this.browserLease,
44043
+ cdpUrl,
44044
+ browserMode: resolvedBrowserMode
44045
+ }) && !this.releaseTestBrowserLock) {
44046
+ this.releaseTestBrowserLock = await acquireTestBrowserLock(this.logger);
44047
+ }
44048
+ if (cdpUrl) {
44049
+ this.logger.info("[DirectPlaywright] Connecting via CDP", { cdpUrl });
44050
+ this.browser = await this.withOperationTimeout(
44051
+ playwrightChromium.connectOverCDP(cdpUrl),
44052
+ 3e4,
44053
+ "[DirectPlaywright] connectOverCDP"
44054
+ );
44055
+ const existingContexts = this.browser.contexts();
44056
+ this.context = existingContexts[0] ?? await this.browser.newContext({ viewport: resolvedViewport });
44057
+ } else if (this.browserLease && !this.stealthEnabled && !this.extensionsEnabled && resolvedBrowserMode !== "headed") {
44058
+ const videoConfig2 = this.resolveVideoConfig(recordVideo);
44059
+ const contextOptions = {
44060
+ viewport: resolvedViewport,
44061
+ acceptDownloads: true
44062
+ };
44063
+ if (storageStatePath) {
44064
+ try {
44065
+ await fs4.access(storageStatePath);
44066
+ contextOptions.storageState = storageStatePath;
44067
+ } catch {
44068
+ }
43753
44069
  }
43754
- }
43755
- if (videoConfig2) {
43756
- await fs3.mkdir(videoConfig2.dir, { recursive: true });
43757
- contextOptions.recordVideo = videoConfig2;
43758
- this.videoDir = videoConfig2.dir;
43759
- }
43760
- if (extraHTTPHeaders && Object.keys(extraHTTPHeaders).length > 0) {
43761
- contextOptions.extraHTTPHeaders = extraHTTPHeaders;
43762
- }
43763
- this.logger.info("[DirectPlaywright] Using pooled browser via lease", {
43764
- leaseId: this.browserLease.leaseId,
43765
- browserId: this.browserLease.browserId
43766
- });
43767
- this.context = await this.withOperationTimeout(
43768
- this.browserLease.createContext(contextOptions),
43769
- 3e4,
43770
- "[DirectPlaywright] browserLease.createContext"
43771
- );
43772
- this.browser = null;
43773
- } else {
43774
- if (this.browserLease) {
43775
- this.logger.info("[DirectPlaywright] Releasing pool lease for direct launch", {
44070
+ if (videoConfig2) {
44071
+ await fs4.mkdir(videoConfig2.dir, { recursive: true });
44072
+ contextOptions.recordVideo = videoConfig2;
44073
+ this.videoDir = videoConfig2.dir;
44074
+ }
44075
+ if (extraHTTPHeaders && Object.keys(extraHTTPHeaders).length > 0) {
44076
+ contextOptions.extraHTTPHeaders = extraHTTPHeaders;
44077
+ }
44078
+ this.logger.info("[DirectPlaywright] Using pooled browser via lease", {
43776
44079
  leaseId: this.browserLease.leaseId,
43777
- reason: resolvedBrowserMode === "headed" ? "headed-mode" : "requires-dedicated-browser"
44080
+ browserId: this.browserLease.browserId
43778
44081
  });
43779
- await this.browserLease.release();
43780
- this.browserLease = null;
44082
+ this.context = await this.withOperationTimeout(
44083
+ this.browserLease.createContext(contextOptions),
44084
+ 3e4,
44085
+ "[DirectPlaywright] browserLease.createContext"
44086
+ );
44087
+ this.browser = null;
44088
+ } else {
44089
+ if (this.browserLease) {
44090
+ this.logger.info("[DirectPlaywright] Releasing pool lease for direct launch", {
44091
+ leaseId: this.browserLease.leaseId,
44092
+ reason: resolvedBrowserMode === "headed" ? "headed-mode" : "requires-dedicated-browser"
44093
+ });
44094
+ await this.browserLease.release();
44095
+ this.browserLease = null;
44096
+ }
44097
+ const { browser, context } = await withBrowserLaunchRetry(
44098
+ () => this.launchBrowser({
44099
+ browserMode: resolvedBrowserMode,
44100
+ storageStatePath,
44101
+ viewport,
44102
+ recordVideo,
44103
+ stealth,
44104
+ extensions,
44105
+ extraHTTPHeaders
44106
+ }),
44107
+ {
44108
+ logger: this.logger,
44109
+ label: "direct-launch"
44110
+ }
44111
+ );
44112
+ this.browser = browser;
44113
+ this.context = context;
43781
44114
  }
43782
- const { browser, context } = await this.launchBrowser({
44115
+ this.attachContextListeners(this.context);
44116
+ if (this.browser) {
44117
+ this.attachBrowserListeners(this.browser);
44118
+ }
44119
+ await this.createPageWithListeners(this.context, "initial_connect");
44120
+ const now = Date.now();
44121
+ const initialPageId = this._assignPageId(this.page);
44122
+ this._pageCreationTimes.set(initialPageId, now);
44123
+ this._tabFocusLog.push({ pageIndex: initialPageId, timestamp: now });
44124
+ this.logger.info("[DirectPlaywright] Browser connected successfully", {
43783
44125
  browserMode: resolvedBrowserMode,
43784
- storageStatePath,
43785
- viewport,
43786
- recordVideo,
43787
- stealth,
43788
- extensions,
43789
- extraHTTPHeaders
44126
+ stealthEnabled: this.stealthEnabled,
44127
+ extensionsEnabled: this.extensionsEnabled,
44128
+ pooled: !!this.browserLease
43790
44129
  });
43791
- this.browser = browser;
43792
- this.context = context;
43793
- }
43794
- this.attachContextListeners(this.context);
43795
- if (this.browser) {
43796
- this.attachBrowserListeners(this.browser);
44130
+ } catch (error) {
44131
+ await this.cleanupFailedConnect(error);
44132
+ throw error;
43797
44133
  }
43798
- await this.createPageWithListeners(this.context, "initial_connect");
43799
- const now = Date.now();
43800
- const initialPageId = this._assignPageId(this.page);
43801
- this._pageCreationTimes.set(initialPageId, now);
43802
- this._tabFocusLog.push({ pageIndex: initialPageId, timestamp: now });
43803
- this.logger.info("[DirectPlaywright] Browser connected successfully", {
43804
- browserMode: resolvedBrowserMode,
43805
- stealthEnabled: this.stealthEnabled,
43806
- extensionsEnabled: this.extensionsEnabled,
43807
- pooled: !!this.browserLease
43808
- });
43809
44134
  }
43810
44135
  supportsContextSwap() {
43811
44136
  return !this.extensionsEnabled && (!!this.browser || !!this.browserLease);
@@ -43877,7 +44202,7 @@ var PlaywrightClient = class _PlaywrightClient {
43877
44202
  }
43878
44203
  if (nextStorageStatePath) {
43879
44204
  try {
43880
- await fs3.access(nextStorageStatePath);
44205
+ await fs4.access(nextStorageStatePath);
43881
44206
  contextOptions.storageState = nextStorageStatePath;
43882
44207
  this.logger.debug("[DirectPlaywright] Loading storage state for swapped context", {
43883
44208
  storageStatePath: nextStorageStatePath
@@ -43898,7 +44223,7 @@ var PlaywrightClient = class _PlaywrightClient {
43898
44223
  this._pageCreationTimes.clear();
43899
44224
  this._closedPageVideos = [];
43900
44225
  if (videoConfig) {
43901
- await fs3.mkdir(videoConfig.dir, { recursive: true });
44226
+ await fs4.mkdir(videoConfig.dir, { recursive: true });
43902
44227
  contextOptions.recordVideo = videoConfig;
43903
44228
  this.videoDir = videoConfig.dir;
43904
44229
  this.highlightEnabled = true;
@@ -43955,6 +44280,7 @@ var PlaywrightClient = class _PlaywrightClient {
43955
44280
  this.logger.debug("[DirectPlaywright] Stealth plugin applied");
43956
44281
  }
43957
44282
  const chromiumLauncher = useStealthMode ? chromium : playwrightChromium;
44283
+ const executablePath = await _PlaywrightClient.resolveChromiumExecutable(this.logger);
43958
44284
  const args = ["--no-sandbox", "--disable-dev-shm-usage"];
43959
44285
  if (browserMode === "headed" && process.platform === "darwin") {
43960
44286
  args.push(
@@ -43976,7 +44302,7 @@ var PlaywrightClient = class _PlaywrightClient {
43976
44302
  }
43977
44303
  if (storageStatePath) {
43978
44304
  try {
43979
- await fs3.access(storageStatePath);
44305
+ await fs4.access(storageStatePath);
43980
44306
  contextOptions.storageState = storageStatePath;
43981
44307
  this.logger.debug("[DirectPlaywright] Loading storage state from file", {
43982
44308
  storageStatePath
@@ -43988,7 +44314,7 @@ var PlaywrightClient = class _PlaywrightClient {
43988
44314
  }
43989
44315
  }
43990
44316
  if (videoConfig) {
43991
- await fs3.mkdir(videoConfig.dir, { recursive: true });
44317
+ await fs4.mkdir(videoConfig.dir, { recursive: true });
43992
44318
  contextOptions.recordVideo = videoConfig;
43993
44319
  this.videoDir = videoConfig.dir;
43994
44320
  this.logger.info("[DirectPlaywright] Video recording enabled", {
@@ -43997,9 +44323,9 @@ var PlaywrightClient = class _PlaywrightClient {
43997
44323
  });
43998
44324
  }
43999
44325
  if (hasExtensions) {
44000
- this.userDataDir = extensions.userDataDir || path3.join(getCanaryTmpDir(), `playwright-userdata-${Date.now()}`);
44326
+ this.userDataDir = extensions.userDataDir || path4.join(this.tmpBaseDir, `playwright-userdata-${Date.now()}`);
44001
44327
  this.shouldCleanupUserDataDir = !extensions.persistUserData;
44002
- await fs3.mkdir(this.userDataDir, { recursive: true });
44328
+ await fs4.mkdir(this.userDataDir, { recursive: true });
44003
44329
  if (browserMode === "headless") {
44004
44330
  args.push("--headless=new");
44005
44331
  }
@@ -44015,6 +44341,7 @@ var PlaywrightClient = class _PlaywrightClient {
44015
44341
  headless: false,
44016
44342
  // We use --headless=new in args for extension support
44017
44343
  args,
44344
+ ...executablePath ? { executablePath } : {},
44018
44345
  ...contextOptions
44019
44346
  }),
44020
44347
  3e4,
@@ -44025,7 +44352,8 @@ var PlaywrightClient = class _PlaywrightClient {
44025
44352
  const browser = await this.withOperationTimeout(
44026
44353
  chromiumLauncher.launch({
44027
44354
  headless: browserMode === "headless",
44028
- args
44355
+ args,
44356
+ ...executablePath ? { executablePath } : {}
44029
44357
  }),
44030
44358
  3e4,
44031
44359
  "[DirectPlaywright] chromium.launch"
@@ -44037,6 +44365,68 @@ var PlaywrightClient = class _PlaywrightClient {
44037
44365
  );
44038
44366
  return { browser, context };
44039
44367
  }
44368
+ /**
44369
+ * Resolve a working Chromium executable path, handling version mismatches
44370
+ * between the Playwright version bundled in this package and the browser
44371
+ * binaries installed on the system.
44372
+ *
44373
+ * When the exact expected binary doesn't exist (e.g. Playwright expects
44374
+ * chromium-1191 but only chromium-1194 is installed), this finds the
44375
+ * newest available chromium binary as a fallback.
44376
+ */
44377
+ static async resolveChromiumExecutable(logger2) {
44378
+ const defaultPath = playwrightChromium.executablePath();
44379
+ try {
44380
+ await fs4.access(defaultPath);
44381
+ return void 0;
44382
+ } catch {
44383
+ }
44384
+ const parts = defaultPath.split(path4.sep);
44385
+ const msPlaywrightIdx = parts.findIndex((p) => p === "ms-playwright");
44386
+ if (msPlaywrightIdx === -1) {
44387
+ logger2.warn("[DirectPlaywright] Cannot locate ms-playwright cache directory", {
44388
+ defaultPath
44389
+ });
44390
+ return void 0;
44391
+ }
44392
+ const cacheDir = parts.slice(0, msPlaywrightIdx + 1).join(path4.sep);
44393
+ const relativeBinaryPath = parts.slice(msPlaywrightIdx + 2).join(path4.sep);
44394
+ try {
44395
+ const entries = await fs4.readdir(cacheDir);
44396
+ const chromiumDirs = entries.filter((e) => e.startsWith("chromium-") && !e.includes("headless")).sort((a, b) => {
44397
+ const revA = parseInt(a.split("-")[1] ?? "0", 10);
44398
+ const revB = parseInt(b.split("-")[1] ?? "0", 10);
44399
+ return revB - revA;
44400
+ });
44401
+ for (const dir of chromiumDirs) {
44402
+ const candidate = path4.join(cacheDir, dir, relativeBinaryPath);
44403
+ try {
44404
+ await fs4.access(candidate);
44405
+ logger2.warn(
44406
+ "[DirectPlaywright] Using fallback Chromium binary (version mismatch)",
44407
+ {
44408
+ expected: defaultPath,
44409
+ using: candidate
44410
+ }
44411
+ );
44412
+ return candidate;
44413
+ } catch {
44414
+ continue;
44415
+ }
44416
+ }
44417
+ logger2.warn("[DirectPlaywright] No compatible Chromium binary found", {
44418
+ cacheDir,
44419
+ candidates: chromiumDirs,
44420
+ expectedRelativePath: relativeBinaryPath
44421
+ });
44422
+ } catch (err) {
44423
+ logger2.warn("[DirectPlaywright] Failed to scan browser cache directory", {
44424
+ cacheDir,
44425
+ error: String(err)
44426
+ });
44427
+ }
44428
+ return void 0;
44429
+ }
44040
44430
  async disconnect() {
44041
44431
  this.logger.info("[DirectPlaywright] Disconnecting");
44042
44432
  this.disconnectInProgress = true;
@@ -44096,7 +44486,7 @@ var PlaywrightClient = class _PlaywrightClient {
44096
44486
  this.lastSnapshotYaml = "";
44097
44487
  if (this._downloadDir) {
44098
44488
  try {
44099
- await fs3.rm(this._downloadDir, { recursive: true, force: true });
44489
+ await fs4.rm(this._downloadDir, { recursive: true, force: true });
44100
44490
  this.logger.debug("[DirectPlaywright] Cleaned up download directory", {
44101
44491
  downloadDir: this._downloadDir
44102
44492
  });
@@ -44108,12 +44498,26 @@ var PlaywrightClient = class _PlaywrightClient {
44108
44498
  }
44109
44499
  this._downloadDir = null;
44110
44500
  }
44501
+ if (this.traceDir) {
44502
+ try {
44503
+ await fs4.rm(this.traceDir, { recursive: true, force: true });
44504
+ this.logger.debug("[DirectPlaywright] Cleaned up trace directory", {
44505
+ traceDir: this.traceDir
44506
+ });
44507
+ } catch (err) {
44508
+ this.logger.warn("[DirectPlaywright] Failed to clean up trace directory", {
44509
+ traceDir: this.traceDir,
44510
+ error: err instanceof Error ? err.message : String(err)
44511
+ });
44512
+ }
44513
+ this.traceDir = null;
44514
+ }
44111
44515
  this._capturedDownloads = [];
44112
44516
  this._downloadCounter = 0;
44113
44517
  this._lastReportedDownloadIndex = 0;
44114
44518
  if (this.shouldCleanupUserDataDir && this.userDataDir) {
44115
44519
  try {
44116
- await fs3.rm(this.userDataDir, { recursive: true, force: true });
44520
+ await fs4.rm(this.userDataDir, { recursive: true, force: true });
44117
44521
  this.logger.debug("[DirectPlaywright] Cleaned up user data directory", {
44118
44522
  userDataDir: this.userDataDir
44119
44523
  });
@@ -44127,7 +44531,7 @@ var PlaywrightClient = class _PlaywrightClient {
44127
44531
  if (this.snapshotFilePaths.size > 0) {
44128
44532
  for (const filePath of this.snapshotFilePaths) {
44129
44533
  try {
44130
- await fs3.unlink(filePath);
44534
+ await fs4.unlink(filePath);
44131
44535
  } catch (err) {
44132
44536
  this.logger.warn("[DirectPlaywright] Failed to clean up snapshot file", {
44133
44537
  filePath,
@@ -44146,11 +44550,33 @@ var PlaywrightClient = class _PlaywrightClient {
44146
44550
  this.stealthEnabled = false;
44147
44551
  this.logger.info("[DirectPlaywright] Disconnected");
44148
44552
  } finally {
44553
+ await this.releaseTestBrowserLockIfHeld();
44149
44554
  this.contextClosedIntentionally = false;
44150
44555
  this.pageClosedIntentionally = false;
44151
44556
  this.disconnectInProgress = false;
44152
44557
  }
44153
44558
  }
44559
+ async cleanupFailedConnect(error) {
44560
+ this.logger.warn("[DirectPlaywright] Cleaning up after failed connect", {
44561
+ error: error instanceof Error ? error.message : String(error)
44562
+ });
44563
+ try {
44564
+ await this.disconnect();
44565
+ } catch (disconnectError) {
44566
+ this.logger.warn("[DirectPlaywright] Failed cleanup after connect error", {
44567
+ error: disconnectError instanceof Error ? disconnectError.message : String(disconnectError)
44568
+ });
44569
+ await this.releaseTestBrowserLockIfHeld();
44570
+ }
44571
+ }
44572
+ async releaseTestBrowserLockIfHeld() {
44573
+ if (!this.releaseTestBrowserLock) {
44574
+ return;
44575
+ }
44576
+ const release = this.releaseTestBrowserLock;
44577
+ this.releaseTestBrowserLock = null;
44578
+ await release();
44579
+ }
44154
44580
  // ================ NAVIGATION ================
44155
44581
  async navigate(url, opts) {
44156
44582
  const dialogMsg = this.getPendingDialogMessage();
@@ -44423,8 +44849,8 @@ Use coordinate-based clicking (x, y) to interact with elements visible in the sc
44423
44849
  }
44424
44850
  const sampleUrls = pendingRequests.slice(0, 3).map((url) => {
44425
44851
  try {
44426
- const path4 = new URL(url).pathname + new URL(url).search;
44427
- return path4.length > 60 ? path4.slice(0, 57) + "..." : path4;
44852
+ const path5 = new URL(url).pathname + new URL(url).search;
44853
+ return path5.length > 60 ? path5.slice(0, 57) + "..." : path5;
44428
44854
  } catch {
44429
44855
  return url.length > 60 ? url.slice(0, 57) + "..." : url;
44430
44856
  }
@@ -45099,7 +45525,7 @@ Use coordinate-based clicking (x, y) to interact with elements visible in the sc
45099
45525
  const video = closingPage.video();
45100
45526
  if (video) {
45101
45527
  const videoFileName = `video-tab${closePageId}-${Date.now()}.webm`;
45102
- const savePath = path3.join(this.videoDir, videoFileName);
45528
+ const savePath = path4.join(this.videoDir, videoFileName);
45103
45529
  await closingPage.close();
45104
45530
  await video.saveAs(savePath);
45105
45531
  const createdAt = this._pageCreationTimes.get(closePageId) ?? Date.now();
@@ -45196,8 +45622,8 @@ Use coordinate-based clicking (x, y) to interact with elements visible in the sc
45196
45622
  const timestamp = Date.now();
45197
45623
  const shortId = crypto.randomUUID().slice(0, 8);
45198
45624
  const filename = `snapshot-${timestamp}-${shortId}.yaml`;
45199
- const filePath = path3.join(getCanaryTmpDir(), filename);
45200
- await fs3.writeFile(filePath, yaml, "utf-8");
45625
+ const filePath = path4.join(this.tmpBaseDir, filename);
45626
+ await fs4.writeFile(filePath, yaml, "utf-8");
45201
45627
  this.snapshotFilePaths.add(filePath);
45202
45628
  this.logger.debug("[DirectPlaywright] Wrote snapshot to disk", {
45203
45629
  filePath,
@@ -45341,8 +45767,8 @@ Use coordinate-based clicking (x, y) to interact with elements visible in the sc
45341
45767
  async startTracing(_opts) {
45342
45768
  const context = await this.getContext();
45343
45769
  this.logger.info("[DirectPlaywright] Starting trace recording");
45344
- this.traceDir = path3.join(getCanaryTmpDir(), `playwright-trace-${Date.now()}`);
45345
- await fs3.mkdir(this.traceDir, { recursive: true });
45770
+ this.traceDir = path4.join(this.tmpBaseDir, `playwright-trace-${Date.now()}`);
45771
+ await fs4.mkdir(this.traceDir, { recursive: true });
45346
45772
  await context.tracing.start({ screenshots: true, snapshots: true });
45347
45773
  this.tracingActive = true;
45348
45774
  }
@@ -45358,7 +45784,7 @@ Use coordinate-based clicking (x, y) to interact with elements visible in the sc
45358
45784
  const context = await this.getContext();
45359
45785
  if (!this.traceDir) throw new Error("Tracing not started");
45360
45786
  this.logger.info("[DirectPlaywright] Stopping trace recording");
45361
- const tracePath = path3.join(this.traceDir, "trace.zip");
45787
+ const tracePath = path4.join(this.traceDir, "trace.zip");
45362
45788
  await context.tracing.stop({ path: tracePath });
45363
45789
  this.tracingActive = false;
45364
45790
  return {
@@ -46022,18 +46448,18 @@ Use coordinate-based clicking (x, y) to interact with elements visible in the sc
46022
46448
  this.logger.error("[DirectPlaywright] Download failed: no download directory", { id });
46023
46449
  return;
46024
46450
  }
46025
- const savePath = path3.join(this._downloadDir, `${id}-${suggestedFilename}`);
46451
+ const savePath = path4.join(this._downloadDir, `${id}-${suggestedFilename}`);
46026
46452
  download.saveAs(savePath).then(
46027
46453
  async () => {
46028
46454
  try {
46029
- const stat2 = await fs3.stat(savePath);
46455
+ const stat3 = await fs4.stat(savePath);
46030
46456
  entry.savedPath = savePath;
46031
- entry.sizeBytes = stat2.size;
46457
+ entry.sizeBytes = stat3.size;
46032
46458
  entry.completed = true;
46033
46459
  this.logger.info("[DirectPlaywright] Download completed", {
46034
46460
  id,
46035
46461
  suggestedFilename,
46036
- sizeBytes: stat2.size,
46462
+ sizeBytes: stat3.size,
46037
46463
  savedPath: savePath
46038
46464
  });
46039
46465
  } catch {
@@ -46175,13 +46601,13 @@ Use coordinate-based clicking (x, y) to interact with elements visible in the sc
46175
46601
  };
46176
46602
  if (ssp) {
46177
46603
  try {
46178
- await fs3.access(ssp);
46604
+ await fs4.access(ssp);
46179
46605
  ctxOpts.storageState = ssp;
46180
46606
  } catch {
46181
46607
  }
46182
46608
  }
46183
46609
  if (videoConfig2) {
46184
- await fs3.mkdir(videoConfig2.dir, { recursive: true });
46610
+ await fs4.mkdir(videoConfig2.dir, { recursive: true });
46185
46611
  ctxOpts.recordVideo = videoConfig2;
46186
46612
  }
46187
46613
  if (headers && Object.keys(headers).length > 0) {
@@ -46207,7 +46633,7 @@ Use coordinate-based clicking (x, y) to interact with elements visible in the sc
46207
46633
  }
46208
46634
  if (this.shouldCleanupUserDataDir && this.userDataDir) {
46209
46635
  try {
46210
- await fs3.rm(this.userDataDir, { recursive: true, force: true });
46636
+ await fs4.rm(this.userDataDir, { recursive: true, force: true });
46211
46637
  } catch {
46212
46638
  }
46213
46639
  this.userDataDir = null;
@@ -46296,7 +46722,7 @@ Use coordinate-based clicking (x, y) to interact with elements visible in the sc
46296
46722
  */
46297
46723
  resolveVideoConfig(recordVideo) {
46298
46724
  if (!recordVideo) return null;
46299
- const dir = recordVideo === true || !recordVideo.dir ? path3.join(getCanaryTmpDir(), `playwright-video-${Date.now()}`) : recordVideo.dir;
46725
+ const dir = recordVideo === true || !recordVideo.dir ? path4.join(this.tmpBaseDir, `playwright-video-${Date.now()}`) : recordVideo.dir;
46300
46726
  const size = recordVideo !== true && recordVideo.size ? recordVideo.size : DEFAULT_VIEWPORT;
46301
46727
  return { dir, size };
46302
46728
  }
@@ -46355,7 +46781,7 @@ Use coordinate-based clicking (x, y) to interact with elements visible in the sc
46355
46781
  for (const handle of videoHandles) {
46356
46782
  try {
46357
46783
  const videoFileName = `video-tab${handle.pageIndex}-${timestamp}.webm`;
46358
- const savePath = path3.join(this.videoDir, videoFileName);
46784
+ const savePath = path4.join(this.videoDir, videoFileName);
46359
46785
  await handle.video.saveAs(savePath);
46360
46786
  const createdAt = this._pageCreationTimes.get(handle.pageIndex) ?? timestamp;
46361
46787
  allPageVideos.push({
@@ -46421,7 +46847,14 @@ Use coordinate-based clicking (x, y) to interact with elements visible in the sc
46421
46847
  );
46422
46848
  return null;
46423
46849
  }
46850
+ // ================ TOASTS ================
46851
+ getCapturedToasts() {
46852
+ return this._lastCapturedToasts;
46853
+ }
46424
46854
  // ================ DOWNLOADS ================
46855
+ getCapturedDownloads() {
46856
+ return this._capturedDownloads;
46857
+ }
46425
46858
  async listDownloads() {
46426
46859
  if (this._capturedDownloads.length === 0) {
46427
46860
  return "No downloads captured during this session.";
@@ -46462,7 +46895,7 @@ Use coordinate-based clicking (x, y) to interact with elements visible in the sc
46462
46895
  const filename = target.suggestedFilename;
46463
46896
  const sizeBytes = target.sizeBytes ?? 0;
46464
46897
  const mimeType = this.guessMimeType(filename);
46465
- const ext = path3.extname(filename).toLowerCase();
46898
+ const ext = path4.extname(filename).toLowerCase();
46466
46899
  if (ext === ".pdf") {
46467
46900
  const { extractPdfText: extractPdfText2 } = await import("./pdf-extract-XYDS42VL.js");
46468
46901
  const { text } = await extractPdfText2(target.savedPath);
@@ -46483,7 +46916,7 @@ Use coordinate-based clicking (x, y) to interact with elements visible in the sc
46483
46916
  ".svg"
46484
46917
  ]);
46485
46918
  if (textExtensions.has(ext)) {
46486
- const content = await fs3.readFile(target.savedPath, "utf-8");
46919
+ const content = await fs4.readFile(target.savedPath, "utf-8");
46487
46920
  return { filename, sizeBytes, mimeType, textContent: content };
46488
46921
  }
46489
46922
  return { filename, sizeBytes, mimeType, textContent: null };
@@ -46594,6 +47027,7 @@ export {
46594
47027
  compareSnapshots,
46595
47028
  hasDiffChanges,
46596
47029
  formatSemanticDiff,
47030
+ guessMimeType,
46597
47031
  captureElementAtPoint,
46598
47032
  AUTO_SNAPSHOT_MARKER,
46599
47033
  isMCPContentWithImages,
@@ -46890,4 +47324,4 @@ playwright-extra/dist/index.esm.js:
46890
47324
  * @license MIT
46891
47325
  *)
46892
47326
  */
46893
- //# sourceMappingURL=chunk-PDC425CK.js.map
47327
+ //# sourceMappingURL=chunk-FK3EZADZ.js.map