hypha-debugger 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.
@@ -2060,12 +2060,16 @@ async function takeScreenshot(selector, format, quality, max_width, max_height,
2060
2060
  // to the viewport dimensions.
2061
2061
  const viewportW = window.innerWidth;
2062
2062
  const viewportH = window.innerHeight;
2063
+ // 1x1 transparent PNG — used as placeholder for images that fail
2064
+ // to load (CORS-blocked, 404, etc.) so html-to-image doesn't reject.
2065
+ const TRANSPARENT_PIXEL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=";
2063
2066
  const captureOptions = {
2064
2067
  quality: qual,
2065
2068
  pixelRatio: 1, // always capture at 1x — we'll resize after
2066
2069
  cacheBust: true,
2067
2070
  skipAutoScale: true,
2068
2071
  skipFonts: true, // CORS-blocked stylesheets can hang font inlining
2072
+ imagePlaceholder: TRANSPARENT_PIXEL, // fallback for broken images
2069
2073
  filter: (el) => {
2070
2074
  // Exclude the debugger overlay and cursor from screenshots
2071
2075
  return (el.id !== "hypha-debugger-host" &&
@@ -2078,21 +2082,37 @@ async function takeScreenshot(selector, format, quality, max_width, max_height,
2078
2082
  captureOptions.width = viewportW;
2079
2083
  captureOptions.height = viewportH;
2080
2084
  }
2081
- let dataUrl;
2082
- try {
2083
- const capturePromise = fmt === "jpeg" ? toJpeg(node, captureOptions) : toPng(node, captureOptions);
2084
- // Hard timeout: pages with cross-origin resources can make
2085
- // html-to-image wait indefinitely on blocked fetches.
2086
- const timeoutMs = 15000;
2087
- dataUrl = await Promise.race([
2085
+ const runCapture = async (opts, timeoutMs = 15000) => {
2086
+ const capturePromise = fmt === "jpeg" ? toJpeg(node, opts) : toPng(node, opts);
2087
+ return Promise.race([
2088
2088
  capturePromise,
2089
- new Promise((_, reject) => setTimeout(() => reject(new Error(`Screenshot capture timed out after ${timeoutMs}ms (likely CORS-blocked resources)`)), timeoutMs)),
2089
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Screenshot capture timed out after ${timeoutMs}ms`)), timeoutMs)),
2090
2090
  ]);
2091
+ };
2092
+ let dataUrl;
2093
+ try {
2094
+ dataUrl = await runCapture(captureOptions);
2091
2095
  }
2092
2096
  catch (captureErr) {
2093
- return {
2094
- error: `Capture failed (html-to-image): ${errorMessage(captureErr)}`,
2095
- };
2097
+ // Fallback: retry without images (filter them out). Some pages have
2098
+ // images that html-to-image can't inline even with imagePlaceholder.
2099
+ try {
2100
+ const noImagesOpts = {
2101
+ ...captureOptions,
2102
+ filter: (el) => {
2103
+ if (!captureOptions.filter(el))
2104
+ return false;
2105
+ const tag = el.tagName?.toLowerCase();
2106
+ return tag !== "img" && tag !== "picture" && tag !== "video";
2107
+ },
2108
+ };
2109
+ dataUrl = await runCapture(noImagesOpts, 10000);
2110
+ }
2111
+ catch (retryErr) {
2112
+ return {
2113
+ error: `Capture failed: ${errorMessage(captureErr)} (retry without images also failed: ${errorMessage(retryErr)})`,
2114
+ };
2115
+ }
2096
2116
  }
2097
2117
  // Resize down to fit within (maxW × maxH) and re-encode. If resize
2098
2118
  // fails (e.g. data URL too large to load back into an Image), fall
@@ -2617,6 +2637,29 @@ function getFiberFromDOM(node) {
2617
2637
  k.startsWith("__reactInternalInstance$"));
2618
2638
  return fiberKey ? node[fiberKey] : null;
2619
2639
  }
2640
+ /**
2641
+ * Find the topmost DOM element that has a React fiber attached.
2642
+ * React mounts on various nodes — #root is common but not universal
2643
+ * (#app, #__next, body, etc.). We scan until we find one.
2644
+ */
2645
+ function findReactRoot() {
2646
+ // Try common selectors first (fast path)
2647
+ const common = ["#root", "#app", "#__next", "[data-reactroot]", "main", "body"];
2648
+ for (const sel of common) {
2649
+ const el = document.querySelector(sel);
2650
+ if (el && getFiberFromDOM(el))
2651
+ return el;
2652
+ }
2653
+ // Slow path: walk the tree, find first element with a fiber
2654
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null);
2655
+ let node = walker.currentNode;
2656
+ while (node) {
2657
+ if (node instanceof Element && getFiberFromDOM(node))
2658
+ return node;
2659
+ node = walker.nextNode();
2660
+ }
2661
+ return null;
2662
+ }
2620
2663
  function getComponentName(fiber) {
2621
2664
  const { type } = fiber;
2622
2665
  if (!type)
@@ -2727,16 +2770,30 @@ function fiberToInfo(fiber, depth, maxDepth) {
2727
2770
  return info;
2728
2771
  }
2729
2772
  function getReactTree(selector, max_depth) {
2730
- selector = selector ?? "#root";
2731
2773
  const maxDepth = max_depth ?? 5;
2732
- const rootEl = document.querySelector(selector);
2733
- if (!rootEl) {
2734
- return { error: `No element found for selector: ${selector}` };
2774
+ let rootEl;
2775
+ let usedSelector;
2776
+ if (selector) {
2777
+ rootEl = document.querySelector(selector);
2778
+ usedSelector = selector;
2779
+ if (!rootEl) {
2780
+ return { error: `No element found for selector: ${selector}` };
2781
+ }
2782
+ }
2783
+ else {
2784
+ // Auto-detect: scan for any element with a React fiber attached
2785
+ rootEl = findReactRoot();
2786
+ usedSelector = rootEl?.tagName.toLowerCase() + (rootEl?.id ? "#" + rootEl.id : "") || "(auto)";
2787
+ if (!rootEl) {
2788
+ return {
2789
+ 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.",
2790
+ };
2791
+ }
2735
2792
  }
2736
2793
  const fiber = getFiberFromDOM(rootEl);
2737
2794
  if (!fiber) {
2738
2795
  return {
2739
- error: `No React fiber found on element "${selector}". Is this a React app?`,
2796
+ error: `No React fiber found on element "${usedSelector}". Is this a React app?`,
2740
2797
  };
2741
2798
  }
2742
2799
  // Walk up to find the root component fiber (skip HostRoot)
@@ -2756,13 +2813,14 @@ function getReactTree(selector, max_depth) {
2756
2813
  }
2757
2814
  getReactTree.__schema__ = {
2758
2815
  name: "getReactTree",
2759
- description: "Inspect the React component tree starting from a DOM element. Returns component names, props, state (including hooks), and children hierarchy.",
2816
+ description: "Inspect the React component tree. Returns component names, props, state (including hooks), and children hierarchy. " +
2817
+ "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.",
2760
2818
  parameters: {
2761
2819
  type: "object",
2762
2820
  properties: {
2763
2821
  selector: {
2764
2822
  type: "string",
2765
- description: 'CSS selector of the React root element. Default: "#root".',
2823
+ description: "CSS selector of the React root element. Omit for auto-detection.",
2766
2824
  },
2767
2825
  max_depth: {
2768
2826
  type: "number",