hypha-debugger 0.2.6 → 0.2.7

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.
@@ -10405,12 +10405,16 @@
10405
10405
  // to the viewport dimensions.
10406
10406
  const viewportW = window.innerWidth;
10407
10407
  const viewportH = window.innerHeight;
10408
+ // 1x1 transparent PNG — used as placeholder for images that fail
10409
+ // to load (CORS-blocked, 404, etc.) so html-to-image doesn't reject.
10410
+ const TRANSPARENT_PIXEL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=";
10408
10411
  const captureOptions = {
10409
10412
  quality: qual,
10410
10413
  pixelRatio: 1, // always capture at 1x — we'll resize after
10411
10414
  cacheBust: true,
10412
10415
  skipAutoScale: true,
10413
10416
  skipFonts: true, // CORS-blocked stylesheets can hang font inlining
10417
+ imagePlaceholder: TRANSPARENT_PIXEL, // fallback for broken images
10414
10418
  filter: (el) => {
10415
10419
  // Exclude the debugger overlay and cursor from screenshots
10416
10420
  return (el.id !== "hypha-debugger-host" &&
@@ -10423,21 +10427,37 @@
10423
10427
  captureOptions.width = viewportW;
10424
10428
  captureOptions.height = viewportH;
10425
10429
  }
10426
- let dataUrl;
10427
- try {
10428
- const capturePromise = fmt === "jpeg" ? toJpeg(node, captureOptions) : toPng(node, captureOptions);
10429
- // Hard timeout: pages with cross-origin resources can make
10430
- // html-to-image wait indefinitely on blocked fetches.
10431
- const timeoutMs = 15000;
10432
- dataUrl = await Promise.race([
10430
+ const runCapture = async (opts, timeoutMs = 15000) => {
10431
+ const capturePromise = fmt === "jpeg" ? toJpeg(node, opts) : toPng(node, opts);
10432
+ return Promise.race([
10433
10433
  capturePromise,
10434
- new Promise((_, reject) => setTimeout(() => reject(new Error(`Screenshot capture timed out after ${timeoutMs}ms (likely CORS-blocked resources)`)), timeoutMs)),
10434
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Screenshot capture timed out after ${timeoutMs}ms`)), timeoutMs)),
10435
10435
  ]);
10436
+ };
10437
+ let dataUrl;
10438
+ try {
10439
+ dataUrl = await runCapture(captureOptions);
10436
10440
  }
10437
10441
  catch (captureErr) {
10438
- return {
10439
- error: `Capture failed (html-to-image): ${errorMessage(captureErr)}`,
10440
- };
10442
+ // Fallback: retry without images (filter them out). Some pages have
10443
+ // images that html-to-image can't inline even with imagePlaceholder.
10444
+ try {
10445
+ const noImagesOpts = {
10446
+ ...captureOptions,
10447
+ filter: (el) => {
10448
+ if (!captureOptions.filter(el))
10449
+ return false;
10450
+ const tag = el.tagName?.toLowerCase();
10451
+ return tag !== "img" && tag !== "picture" && tag !== "video";
10452
+ },
10453
+ };
10454
+ dataUrl = await runCapture(noImagesOpts, 10000);
10455
+ }
10456
+ catch (retryErr) {
10457
+ return {
10458
+ error: `Capture failed: ${errorMessage(captureErr)} (retry without images also failed: ${errorMessage(retryErr)})`,
10459
+ };
10460
+ }
10441
10461
  }
10442
10462
  // Resize down to fit within (maxW × maxH) and re-encode. If resize
10443
10463
  // fails (e.g. data URL too large to load back into an Image), fall
@@ -10962,6 +10982,29 @@
10962
10982
  k.startsWith("__reactInternalInstance$"));
10963
10983
  return fiberKey ? node[fiberKey] : null;
10964
10984
  }
10985
+ /**
10986
+ * Find the topmost DOM element that has a React fiber attached.
10987
+ * React mounts on various nodes — #root is common but not universal
10988
+ * (#app, #__next, body, etc.). We scan until we find one.
10989
+ */
10990
+ function findReactRoot() {
10991
+ // Try common selectors first (fast path)
10992
+ const common = ["#root", "#app", "#__next", "[data-reactroot]", "main", "body"];
10993
+ for (const sel of common) {
10994
+ const el = document.querySelector(sel);
10995
+ if (el && getFiberFromDOM(el))
10996
+ return el;
10997
+ }
10998
+ // Slow path: walk the tree, find first element with a fiber
10999
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null);
11000
+ let node = walker.currentNode;
11001
+ while (node) {
11002
+ if (node instanceof Element && getFiberFromDOM(node))
11003
+ return node;
11004
+ node = walker.nextNode();
11005
+ }
11006
+ return null;
11007
+ }
10965
11008
  function getComponentName(fiber) {
10966
11009
  const { type } = fiber;
10967
11010
  if (!type)
@@ -11072,16 +11115,30 @@
11072
11115
  return info;
11073
11116
  }
11074
11117
  function getReactTree(selector, max_depth) {
11075
- selector = selector ?? "#root";
11076
11118
  const maxDepth = max_depth ?? 5;
11077
- const rootEl = document.querySelector(selector);
11078
- if (!rootEl) {
11079
- return { error: `No element found for selector: ${selector}` };
11119
+ let rootEl;
11120
+ let usedSelector;
11121
+ if (selector) {
11122
+ rootEl = document.querySelector(selector);
11123
+ usedSelector = selector;
11124
+ if (!rootEl) {
11125
+ return { error: `No element found for selector: ${selector}` };
11126
+ }
11127
+ }
11128
+ else {
11129
+ // Auto-detect: scan for any element with a React fiber attached
11130
+ rootEl = findReactRoot();
11131
+ usedSelector = rootEl?.tagName.toLowerCase() + (rootEl?.id ? "#" + rootEl.id : "") || "(auto)";
11132
+ if (!rootEl) {
11133
+ return {
11134
+ error: "No React root found on this page. Tried #root, #app, #__next, [data-reactroot], main, body — none had React fibers. This page may not be a React app, or React may not have rendered yet. Pass an explicit `selector` if you know the mount point.",
11135
+ };
11136
+ }
11080
11137
  }
11081
11138
  const fiber = getFiberFromDOM(rootEl);
11082
11139
  if (!fiber) {
11083
11140
  return {
11084
- error: `No React fiber found on element "${selector}". Is this a React app?`,
11141
+ error: `No React fiber found on element "${usedSelector}". Is this a React app?`,
11085
11142
  };
11086
11143
  }
11087
11144
  // Walk up to find the root component fiber (skip HostRoot)
@@ -11101,13 +11158,14 @@
11101
11158
  }
11102
11159
  getReactTree.__schema__ = {
11103
11160
  name: "getReactTree",
11104
- description: "Inspect the React component tree starting from a DOM element. Returns component names, props, state (including hooks), and children hierarchy.",
11161
+ description: "Inspect the React component tree. Returns component names, props, state (including hooks), and children hierarchy. " +
11162
+ "When no selector is provided, auto-detects the React root by scanning common mount points (#root, #app, #__next, [data-reactroot], main, body) and then walking the DOM for any element with a React fiber attached.",
11105
11163
  parameters: {
11106
11164
  type: "object",
11107
11165
  properties: {
11108
11166
  selector: {
11109
11167
  type: "string",
11110
- description: 'CSS selector of the React root element. Default: "#root".',
11168
+ description: "CSS selector of the React root element. Omit for auto-detection.",
11111
11169
  },
11112
11170
  max_depth: {
11113
11171
  type: "number",