shadimage
nteract
componentisolated-framesource: ok · hit

Isolated Frame

Secure iframe component for rendering untrusted output content. Uses blob URLs with sandbox restrictions to isolate JavaScript execution. Includes message bridge for parent-child communication and widget comm protocol support.

npx shadcn add "https://nteract-elements.vercel.app/r/isolated-frame.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light

Registry dependencies

@nteract/widget-store

Files7 · source cached 10/8/2026, 9:02:00 PM

registry/outputs/isolated/index.ts
export {
  CommBridgeManager,
  createCommBridgeManager,
} from "./comm-bridge-manager";
// Message protocol types
export type {
  ClearMessage,
  EvalMessage,
  EvalResultMessage,
  IframeErrorMessage,
  // Utilities
  IframeMessage,
  // Iframe → Parent
  IframeToParentMessage,
  LinkClickMessage,
  MessageType,
  // Parent → Iframe
  ParentToIframeMessage,
  PingMessage,
  PongMessage,
  ReadyMessage,
  RenderCompleteMessage,
  RenderMessage,
  RenderPayload,
  ResizeMessage,
  ThemeMessage,
  WidgetStateMessage,
  WidgetUpdateMessage,
} from "./frame-bridge";
export { isIframeMessage, isMessageType } from "./frame-bridge";
export type { FrameHtmlOptions } from "./frame-html";
// HTML template generator
export { createFrameBlobUrl, generateFrameHtml } from "./frame-html";
// Security testing component
export { IsolationTest } from "./IsolationTest";
export type { IsolatedFrameHandle, IsolatedFrameProps } from "./isolated-frame";
export { IsolatedFrame } from "./isolated-frame";
// Provider and hook for renderer bundle
export {
  IsolatedRendererProvider,
  useIsolatedRenderer,
} from "./isolated-renderer-context";
registry/outputs/isolated/isolated-frame.tsx
"use client";

import {
  forwardRef,
  useCallback,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from "react";
import type {
  IframeToParentMessage,
  ParentToIframeMessage,
  RenderPayload,
} from "./frame-bridge";
import { isIframeMessage } from "./frame-bridge";
import { createFrameBlobUrl } from "./frame-html";
import { useIsolatedRenderer } from "./isolated-renderer-context";

export interface IsolatedFrameProps {
  /**
   * Unique ID for this frame (used for message routing).
   */
  id?: string;

  /**
   * Initial content to render when the frame is ready.
   */
  initialContent?: RenderPayload;

  /**
   * Whether to use dark mode styling.
   */
  darkMode?: boolean;

  /**
   * Minimum height of the iframe in pixels.
   * @default 24
   */
  minHeight?: number;

  /**
   * Maximum height of the iframe in pixels.
   * @default 2000
   */
  maxHeight?: number;

  /**
   * Additional CSS classes for the iframe container.
   */
  className?: string;

  /**
   * Callback when the iframe is ready to receive messages.
   */
  onReady?: () => void;

  /**
   * Callback when the iframe content resizes.
   */
  onResize?: (height: number) => void;

  /**
   * Callback when a link is clicked in the iframe.
   */
  onLinkClick?: (url: string, newTab: boolean) => void;

  /**
   * Callback when the user double-clicks in the iframe.
   */
  onDoubleClick?: () => void;

  /**
   * Callback when a widget state update is sent from the iframe.
   */
  onWidgetUpdate?: (commId: string, state: Record<string, unknown>) => void;

  /**
   * Callback when an error occurs in the iframe.
   */
  onError?: (error: { message: string; stack?: string }) => void;

  /**
   * Callback for all messages from the iframe (for debugging or custom handling).
   */
  onMessage?: (message: IframeToParentMessage) => void;
}

export interface IsolatedFrameHandle {
  /**
   * Send a message to the iframe.
   */
  send: (message: ParentToIframeMessage) => void;

  /**
   * Send content to render in the iframe.
   */
  render: (payload: RenderPayload) => void;

  /**
   * Evaluate code in the iframe (for bootstrap/injection).
   */
  eval: (code: string) => void;

  /**
   * Update theme settings in the iframe.
   */
  setTheme: (isDark: boolean) => void;

  /**
   * Clear all content in the iframe.
   */
  clear: () => void;

  /**
   * Search for text within the iframe's rendered content.
   * Pass empty string to clear search highlights.
   */
  search: (query: string, caseSensitive?: boolean) => void;

  /**
   * Navigate to a specific search match by index.
   */
  searchNavigate: (matchIndex: number) => void;

  /**
   * Whether the iframe is ready to receive messages.
   * True after the React renderer bundle is initialized.
   */
  isReady: boolean;

  /**
   * Whether the iframe bootstrap HTML is loaded.
   * True before the React renderer bundle is loaded.
   */
  isIframeReady: boolean;
}

/**
 * Sandbox attributes for the isolated iframe.
 *
 * CRITICAL: Do NOT include 'allow-same-origin' - this would give the iframe
 * access to the parent's origin and Tauri APIs.
 */
const SANDBOX_ATTRS = [
  "allow-scripts", // Required for rendering interactive content
  "allow-downloads", // Allow file downloads (e.g., from widgets)
  "allow-forms", // Allow form submissions
  "allow-pointer-lock", // For interactive visualizations
  "allow-popups", // Allow window.open (for links)
  "allow-popups-to-escape-sandbox", // Popups should be unrestricted
  "allow-modals", // Allow alert/confirm/prompt
].join(" ");

/**
 * IsolatedFrame component - Renders untrusted content in a secure iframe.
 *
 * Uses a blob: URL with sandbox restrictions to ensure the iframe content
 * cannot access Tauri APIs or the parent DOM. Communication happens via
 * postMessage.
 *
 * **Requires** `IsolatedRendererProvider` to be present in the component tree.
 *
 * @example
 * ```tsx
 * // In your app root or layout:
 * <IsolatedRendererProvider basePath="/isolated">
 *   <App />
 * </IsolatedRendererProvider>
 *
 * // Then use IsolatedFrame anywhere:
 * const frameRef = useRef<IsolatedFrameHandle>(null);
 *
 * <IsolatedFrame
 *   ref={frameRef}
 *   darkMode={true}
 *   onReady={() => {
 *     frameRef.current?.render({
 *       mimeType: "text/html",
 *       data: "<h1>Hello from isolated frame!</h1>"
 *     });
 *   }}
 *   onResize={(height) => console.log("New height:", height)}
 * />
 * ```
 */
export const IsolatedFrame = forwardRef<
  IsolatedFrameHandle,
  IsolatedFrameProps
>(function IsolatedFrame(
  {
    id,
    initialContent,
    darkMode = true,
    minHeight = 24,
    maxHeight = 2000,
    className = "",
    onReady,
    onResize,
    onLinkClick,
    onDoubleClick,
    onWidgetUpdate,
    onError,
    onMessage,
  },
  ref,
) {
  // Get renderer bundle from context (provided by IsolatedRendererProvider)
  const {
    rendererCode,
    rendererCss,
    isLoading: providerLoading,
    error: providerError,
  } = useIsolatedRenderer();
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const [blobUrl, setBlobUrl] = useState<string | null>(null);
  // Track iframe ready (bootstrap HTML loaded)
  const [isIframeReady, setIsIframeReady] = useState(false);
  // Track renderer ready (React bundle initialized)
  const [isReady, setIsReady] = useState(false);
  // Use ref to track ready state for send callback (avoids stale closure)
  const isReadyRef = useRef(false);
  const [height, setHeight] = useState(minHeight);

  // Queue messages until iframe is ready
  const pendingMessagesRef = useRef<ParentToIframeMessage[]>([]);
  // Track if we've started bootstrapping to avoid double-fetch
  const bootstrappingRef = useRef(false);

  // Track initial darkMode for blob URL (don't recreate blob on theme change)
  const initialDarkModeRef = useRef(darkMode);

  // Create blob URL on mount (only once, with initial darkMode)
  useEffect(() => {
    const url = createFrameBlobUrl({ darkMode: initialDarkModeRef.current });
    setBlobUrl(url);

    return () => {
      URL.revokeObjectURL(url);
    };
  }, []);

  // Forward theme changes to iframe (without recreating the blob)
  useEffect(() => {
    if (isReady && iframeRef.current?.contentWindow) {
      iframeRef.current.contentWindow.postMessage(
        { type: "theme", payload: { isDark: darkMode } },
        "*",
      );
    }
  }, [darkMode, isReady]);

  // Keep ref in sync with state (ref avoids stale closures in callbacks)
  useEffect(() => {
    isReadyRef.current = isReady;
  }, [isReady]);

  // Surface provider errors to consumers
  useEffect(() => {
    if (providerError && !providerLoading) {
      onError?.({ message: providerError.message, stack: providerError.stack });
    }
  }, [providerError, providerLoading, onError]);

  // Send a message to the iframe
  // Uses ref to check ready state to avoid stale closure issues
  const send = useCallback(
    (message: ParentToIframeMessage) => {
      if (!isReadyRef.current) {
        // Queue message until ready
        pendingMessagesRef.current.push(message);
        return;
      }

      if (iframeRef.current?.contentWindow) {
        iframeRef.current.contentWindow.postMessage(message, "*");
      }
    },
    [], // No deps - uses ref instead of state
  );

  // Flush pending messages when ready
  useEffect(() => {
    if (isReady && pendingMessagesRef.current.length > 0) {
      const pending = pendingMessagesRef.current;
      pendingMessagesRef.current = [];
      pending.forEach((msg) => {
        if (iframeRef.current?.contentWindow) {
          iframeRef.current.contentWindow.postMessage(msg, "*");
        }
      });
    }
  }, [isReady]);

  // Handle messages from iframe
  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      // Verify the message is from our iframe
      if (event.source !== iframeRef.current?.contentWindow) {
        return;
      }

      const data = event.data;
      if (!isIframeMessage(data)) {
        return;
      }

      // Call generic message handler
      onMessage?.(data);

      // Handle specific message types
      switch (data.type) {
        case "ready":
          // Iframe bootstrap HTML is loaded
          // Renderer injection is handled by a separate useEffect
          setIsIframeReady(true);
          break;

        case "renderer_ready":
          // React renderer bundle is initialized
          setIsReady(true);
          onReady?.();
          // Render initial content if provided
          if (initialContent) {
            iframeRef.current?.contentWindow?.postMessage(
              { type: "render", payload: initialContent },
              "*",
            );
          }
          break;

        case "resize":
          if (data.payload?.height != null) {
            const newHeight = Math.max(
              minHeight,
              Math.min(maxHeight, data.payload.height),
            );
            setHeight(newHeight);
            onResize?.(newHeight);
          }
          break;

        case "link_click":
          if (data.payload?.url) {
            onLinkClick?.(data.payload.url, data.payload.newTab ?? false);
          }
          break;

        case "dblclick":
          onDoubleClick?.();
          break;

        case "widget_update":
          if (data.payload?.commId && data.payload?.state) {
            onWidgetUpdate?.(data.payload.commId, data.payload.state);
          }
          break;

        case "error":
          if (data.payload) {
            onError?.(data.payload);
          }
          break;

        case "eval_result":
          // Surface bundle eval failures to help diagnose injection issues
          if (data.payload?.success === false) {
            console.error(
              "[IsolatedFrame] Bundle eval failed:",
              data.payload.error,
            );
            onError?.({ message: `Bundle eval failed: ${data.payload.error}` });
          }
          break;
      }
    };

    window.addEventListener("message", handleMessage);
    return () => window.removeEventListener("message", handleMessage);
  }, [
    initialContent,
    minHeight,
    maxHeight,
    onReady,
    onResize,
    onLinkClick,
    onDoubleClick,
    onWidgetUpdate,
    onError,
    onMessage,
  ]);

  // Inject renderer when iframe is ready AND bundle props are available
  useEffect(() => {
    if (
      isIframeReady &&
      !isReady &&
      !bootstrappingRef.current &&
      rendererCode &&
      rendererCss &&
      iframeRef.current?.contentWindow
    ) {
      bootstrappingRef.current = true;

      // Inject CSS first (idempotent - checks if already loaded)
      const cssCode = `
        (function() {
          if (window.__ISOLATED_CSS_LOADED__) return;
          window.__ISOLATED_CSS_LOADED__ = true;
          var style = document.createElement('style');
          style.textContent = ${JSON.stringify(rendererCss)};
          document.head.appendChild(style);
        })();
      `;
      iframeRef.current.contentWindow.postMessage(
        { type: "eval", payload: { code: cssCode } },
        "*",
      );
      // Then inject JS bundle (idempotent - checks if already loaded)
      // Use string concatenation instead of template literal to avoid issues
      // with backticks or ${} in the bundled code
      const jsWrapper =
        "(function() {" +
        "if (window.__ISOLATED_RENDERER_LOADED__) return;" +
        "window.__ISOLATED_RENDERER_LOADED__ = true;" +
        rendererCode +
        "})();";
      iframeRef.current.contentWindow.postMessage(
        { type: "eval", payload: { code: jsWrapper } },
        "*",
      );
    }
  }, [isIframeReady, isReady, rendererCode, rendererCss]);

  // Expose imperative API
  useImperativeHandle(
    ref,
    () => ({
      send,
      render: (payload: RenderPayload) => send({ type: "render", payload }),
      eval: (code: string) => send({ type: "eval", payload: { code } }),
      setTheme: (isDark: boolean) =>
        send({ type: "theme", payload: { isDark } }),
      clear: () => send({ type: "clear" }),
      search: (query: string, caseSensitive?: boolean) => {
        // Search handler is in bootstrap HTML, so send directly when iframe is loaded
        // (bypasses the isReady queue which waits for the React renderer)
        if (iframeRef.current?.contentWindow) {
          iframeRef.current.contentWindow.postMessage(
            { type: "search", payload: { query, caseSensitive } },
            "*",
          );
        }
      },
      searchNavigate: (matchIndex: number) => {
        if (iframeRef.current?.contentWindow) {
          iframeRef.current.contentWindow.postMessage(
            { type: "search_navigate", payload: { matchIndex } },
            "*",
          );
        }
      },
      isReady,
      isIframeReady,
    }),
    [send, isReady, isIframeReady],
  );

  if (!blobUrl) {
    return null;
  }

  return (
    <iframe
      ref={iframeRef}
      id={id}
      src={blobUrl}
      sandbox={SANDBOX_ATTRS}
      className={className}
      data-slot="isolated-frame"
      style={{
        width: "100%",
        height: `${height}px`,
        border: "none",
        display: "block",
      }}
      title="Isolated output frame"
    />
  );
});
registry/outputs/isolated/frame-bridge.ts
/**
 * Message protocol types for parent ↔ iframe communication.
 *
 * This module defines the contract between the parent window and isolated output frames.
 * All communication happens via postMessage with structured message types.
 */

// --- Message Types: Parent → Iframe ---

/**
 * Bootstrap the iframe with JavaScript code.
 * Used to inject the ESM renderer bundle into the iframe.
 */
export interface EvalMessage {
  type: "eval";
  payload: {
    /** JavaScript code to evaluate in the iframe context */
    code: string;
  };
}

/**
 * Render output content in the iframe.
 */
export interface RenderMessage {
  type: "render";
  payload: RenderPayload;
}

export interface RenderPayload {
  /** MIME type of the content (e.g., "text/html", "text/markdown") */
  mimeType: string;
  /** The content data (format depends on MIME type) */
  data: unknown;
  /** Optional metadata for the output */
  metadata?: Record<string, unknown>;
  /** Cell ID this output belongs to (for routing) */
  cellId?: string;
  /** Output index within the cell */
  outputIndex?: number;
  /** If true, append to existing outputs instead of replacing */
  append?: boolean;
  /** If true, replace all existing outputs with this single output */
  replace?: boolean;
}

/**
 * Update widget state in the iframe.
 */
export interface WidgetStateMessage {
  type: "widget_state";
  payload: {
    /** Comm ID of the widget */
    commId: string;
    /** Updated state to merge */
    state: Record<string, unknown>;
    /** Optional buffers (base64 encoded) */
    buffers?: string[];
  };
}

/**
 * Sync theme with the iframe.
 */
export interface ThemeMessage {
  type: "theme";
  payload: {
    /** Whether dark mode is active */
    isDark: boolean;
    /** Optional CSS variables to inject */
    cssVariables?: Record<string, string>;
  };
}

/**
 * Ping the iframe (for health checks and latency measurement).
 */
export interface PingMessage {
  type: "ping";
  payload?: {
    sentAt: number;
  };
}

/**
 * Clear all rendered content in the iframe.
 */
export interface ClearMessage {
  type: "clear";
}

// --- Widget Comm Protocol: Parent → Iframe ---

/**
 * Forward a comm_open message to the iframe.
 * Sent when a widget model is created by the kernel.
 */
export interface CommOpenMessage {
  type: "comm_open";
  payload: {
    /** Comm ID of the widget */
    commId: string;
    /** Target name (e.g., "jupyter.widget") */
    targetName: string;
    /** Initial widget state */
    state: Record<string, unknown>;
    /** Buffer paths for binary data reconstruction */
    bufferPaths?: string[][];
    /** Binary buffers (transferred via structured clone) */
    buffers?: ArrayBuffer[];
  };
}

/**
 * Forward a comm_msg to the iframe.
 * Sent for state updates and custom messages from kernel.
 */
export interface CommMsgMessage {
  type: "comm_msg";
  payload: {
    /** Comm ID of the widget */
    commId: string;
    /** Message method: "update" or "custom" */
    method: "update" | "custom";
    /** State patch (for update) or custom content (for custom) */
    data: Record<string, unknown>;
    /** Buffer paths for binary data reconstruction */
    bufferPaths?: string[][];
    /** Binary buffers (transferred via structured clone) */
    buffers?: ArrayBuffer[];
  };
}

/**
 * Forward a comm_close message to the iframe.
 * Sent when a widget is destroyed by the kernel.
 */
export interface CommCloseMessage {
  type: "comm_close";
  payload: {
    /** Comm ID of the widget to close */
    commId: string;
  };
}

/**
 * Sync all existing widget models to the iframe.
 * Sent on iframe ready to bootstrap existing widgets.
 */
export interface CommSyncMessage {
  type: "comm_sync";
  payload: {
    /** Array of existing models to sync */
    models: Array<{
      commId: string;
      targetName: string;
      state: Record<string, unknown>;
      buffers?: ArrayBuffer[];
    }>;
  };
}

/**
 * Signal that the parent's comm bridge is ready.
 * Iframe should respond with widget_ready to trigger comm_sync.
 */
export interface BridgeReadyMessage {
  type: "bridge_ready";
}

// --- Global Find: Parent → Iframe ---

/**
 * Search for text within the iframe's rendered content.
 * The iframe should highlight all matches and report the count.
 */
export interface SearchMessage {
  type: "search";
  payload: {
    /** The search query string (empty string clears search) */
    query: string;
    /** Whether the search should be case-sensitive */
    caseSensitive?: boolean;
  };
}

/**
 * Navigate to a specific match in the iframe's search results.
 */
export interface SearchNavigateMessage {
  type: "search_navigate";
  payload: {
    /** The index of the match to navigate to (0-based) */
    matchIndex: number;
  };
}

/**
 * All message types that can be sent from parent to iframe.
 */
export type ParentToIframeMessage =
  | EvalMessage
  | RenderMessage
  | WidgetStateMessage
  | ThemeMessage
  | PingMessage
  | ClearMessage
  | CommOpenMessage
  | CommMsgMessage
  | CommCloseMessage
  | CommSyncMessage
  | BridgeReadyMessage
  | SearchMessage
  | SearchNavigateMessage;

// --- Message Types: Iframe → Parent ---

/**
 * Iframe has finished loading and is ready to receive messages.
 */
export interface ReadyMessage {
  type: "ready";
}

/**
 * Response to a ping message.
 */
export interface PongMessage {
  type: "pong";
  payload: {
    receivedAt: number;
    /** Echo back the payload from the ping */
    echo?: unknown;
  };
}

/**
 * Result of evaluating code in the iframe.
 */
export interface EvalResultMessage {
  type: "eval_result";
  payload: {
    success: boolean;
    result?: string;
    error?: string;
  };
}

/**
 * Iframe content has finished rendering.
 */
export interface RenderCompleteMessage {
  type: "render_complete";
  payload?: {
    /** Height of the rendered content */
    height?: number;
  };
}

/**
 * Iframe content size has changed.
 */
export interface ResizeMessage {
  type: "resize";
  payload: {
    /** New height of the content */
    height: number;
    /** New width of the content (optional) */
    width?: number;
  };
}

/**
 * User clicked a link in the iframe.
 */
export interface LinkClickMessage {
  type: "link_click";
  payload: {
    /** The URL that was clicked */
    url: string;
    /** Whether it was a ctrl/cmd click */
    newTab: boolean;
  };
}

/**
 * User double-clicked in the iframe.
 */
export interface DoubleClickMessage {
  type: "dblclick";
}

/**
 * Widget state was updated in the iframe (needs to sync to kernel).
 */
export interface WidgetUpdateMessage {
  type: "widget_update";
  payload: {
    /** Comm ID of the widget */
    commId: string;
    /** Updated state */
    state: Record<string, unknown>;
    /** Optional buffers (base64 encoded) */
    buffers?: string[];
  };
}

/**
 * An error occurred in the iframe.
 */
export interface IframeErrorMessage {
  type: "error";
  payload: {
    message: string;
    stack?: string;
  };
}

/**
 * The React renderer bundle has been loaded and initialized.
 * This is sent after the bundle is eval'd and React is mounted.
 */
export interface RendererReadyMessage {
  type: "renderer_ready";
}

// --- Widget Comm Protocol: Iframe → Parent ---

/**
 * Iframe widget system is ready to receive comm messages.
 * Parent should send comm_sync with existing models after this.
 */
export interface WidgetReadyMessage {
  type: "widget_ready";
}

/**
 * Widget initiated a state update or custom message.
 * Parent should forward to kernel and update its store.
 */
export interface WidgetCommMsgMessage {
  type: "widget_comm_msg";
  payload: {
    /** Comm ID of the widget */
    commId: string;
    /** Message method: "update" or "custom" */
    method: "update" | "custom";
    /** State patch or custom content */
    data: Record<string, unknown>;
    /** Buffer paths */
    bufferPaths?: string[][];
    /** Binary buffers */
    buffers?: ArrayBuffer[];
  };
}

/**
 * Widget initiated comm close.
 * Parent should forward to kernel and clean up.
 */
export interface WidgetCommCloseMessage {
  type: "widget_comm_close";
  payload: {
    /** Comm ID of the widget to close */
    commId: string;
  };
}

// --- Global Find: Iframe → Parent ---

/**
 * Report search results from the iframe.
 * Sent after processing a search message.
 */
export interface SearchResultsMessage {
  type: "search_results";
  payload: {
    /** Number of matches found */
    count: number;
  };
}

/**
 * All message types that can be sent from iframe to parent.
 */
export type IframeToParentMessage =
  | ReadyMessage
  | PongMessage
  | EvalResultMessage
  | RenderCompleteMessage
  | ResizeMessage
  | LinkClickMessage
  | DoubleClickMessage
  | WidgetUpdateMessage
  | IframeErrorMessage
  | RendererReadyMessage
  | WidgetReadyMessage
  | WidgetCommMsgMessage
  | WidgetCommCloseMessage
  | SearchResultsMessage;

// --- Utility Types ---

/**
 * All message types (for generic handling).
 */
export type IframeMessage = ParentToIframeMessage | IframeToParentMessage;

/**
 * Extract the message type string.
 */
export type MessageType = IframeMessage["type"];

/**
 * Type guard to check if a message is from the iframe.
 */
export function isIframeMessage(data: unknown): data is IframeToParentMessage {
  if (typeof data !== "object" || data === null) return false;
  const msg = data as { type?: unknown };
  return (
    typeof msg.type === "string" &&
    [
      "ready",
      "pong",
      "eval_result",
      "render_complete",
      "resize",
      "link_click",
      "dblclick",
      "widget_update",
      "error",
      "renderer_ready",
      "widget_ready",
      "widget_comm_msg",
      "widget_comm_close",
      "search_results",
    ].includes(msg.type)
  );
}

/**
 * Type guard for specific message types.
 */
export function isMessageType<T extends IframeMessage["type"]>(
  data: unknown,
  type: T,
): data is Extract<IframeMessage, { type: T }> {
  if (typeof data !== "object" || data === null) return false;
  return (data as { type?: unknown }).type === type;
}
registry/outputs/isolated/frame-html.ts
/**
 * HTML template generator for isolated output frames.
 *
 * Creates the minimal HTML document that runs inside the blob URL iframe.
 * This document handles the message protocol and provides a render target
 * for output content.
 *
 * Security: This code runs in an isolated origin (blob:) with sandbox
 * restrictions, so it cannot access Tauri APIs or the parent DOM.
 */

export interface FrameHtmlOptions {
  /**
   * Whether to include dark mode styles by default.
   */
  darkMode?: boolean;
  /**
   * Additional CSS to inject into the frame.
   */
  additionalCss?: string;
  /**
   * Additional JavaScript to inject (runs after bootstrap).
   */
  additionalScript?: string;
}

/**
 * Generate the HTML template for an isolated output frame.
 *
 * The generated HTML includes:
 * - Basic styling for outputs (respects light/dark mode)
 * - Message handler for parent communication
 * - ResizeObserver for auto-sizing
 * - Ready notification on load
 *
 * @param options - Configuration options for the frame
 * @returns HTML string to be used with a blob URL
 */
export function generateFrameHtml(options: FrameHtmlOptions = {}): string {
  const {
    darkMode = true,
    additionalCss = "",
    additionalScript = "",
  } = options;

  return `<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="Content-Security-Policy" content="default-src 'self' blob: data:; script-src 'unsafe-inline' 'unsafe-eval' blob: https:; style-src 'unsafe-inline'; img-src * data: blob:; font-src * data:; connect-src *;">
  <style>
    :root {
      --bg-primary: ${darkMode ? "#0a0a0a" : "#ffffff"};
      --bg-secondary: ${darkMode ? "#1a1a1a" : "#f5f5f5"};
      --text-primary: ${darkMode ? "#e0e0e0" : "#1a1a1a"};
      --text-secondary: ${darkMode ? "#a0a0a0" : "#666666"};
      --border-color: ${darkMode ? "#333333" : "#e0e0e0"};
      --accent-color: #3b82f6;
      --error-color: #ef4444;
      --success-color: #22c55e;
    }

    * {
      box-sizing: border-box;
    }

    html, body {
      margin: 0;
      padding: 0;
      font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      line-height: 1.5;
      background: transparent;
      color: var(--text-primary);
    }

    /* Output container */
    #root {
      min-height: 1px;
    }

    /* Reset common elements - 0.875rem matches Tailwind's text-sm */
    pre, code {
      font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
      font-size: 0.875rem;
    }

    pre {
      margin: 0;
      padding: 8px;
      background: var(--bg-secondary);
      border-radius: 4px;
      overflow-x: auto;
      white-space: pre-wrap;
      word-wrap: break-word;
    }

    /* Table styling for pandas DataFrames - 0.875rem matches Tailwind's text-sm */
    table {
      border-collapse: collapse;
      margin: 8px 0;
      font-size: 0.875rem;
    }

    th, td {
      border: 1px solid var(--border-color);
      padding: 4px 8px;
      text-align: left;
    }

    th {
      background: var(--bg-secondary);
      font-weight: 600;
    }

    /* Image outputs */
    img {
      max-width: 100%;
      height: auto;
    }

    /* Links */
    a {
      color: var(--accent-color);
      text-decoration: none;
    }

    a:hover {
      text-decoration: underline;
    }

    /* Error styling */
    .error {
      color: var(--error-color);
    }

    .error pre {
      background: ${darkMode ? "#1a1010" : "#fef2f2"};
      color: var(--error-color);
    }

    ${additionalCss}
  </style>
</head>
<body>
  <div id="root"></div>
  <script>
    (function() {
      'use strict';

      // --- State ---
      let isReady = false;
      const root = document.getElementById('root');

      // --- Message Handler ---
      // Note: When the React renderer bundle is loaded, it sets window.__REACT_RENDERER_ACTIVE__
      // and the inline handlers should defer to React for render/theme/clear messages.
      window.addEventListener('message', function(event) {
        // Only accept messages from our parent window
        if (event.source !== window.parent) {
          return;
        }

        const { type, payload } = event.data || {};

        try {
          switch (type) {
            case 'ping':
              handlePing(payload);
              break;

            case 'eval':
              handleEval(payload);
              break;

            case 'render':
              // Skip inline rendering if React renderer is active
              if (window.__REACT_RENDERER_ACTIVE__) return;
              handleRender(payload);
              break;

            case 'theme':
              // Skip inline theme handling if React renderer is active
              if (window.__REACT_RENDERER_ACTIVE__) return;
              handleTheme(payload);
              break;

            case 'clear':
              // Skip inline clear if React renderer is active
              if (window.__REACT_RENDERER_ACTIVE__) return;
              handleClear();
              break;

            case 'widget_state':
              handleWidgetState(payload);
              break;

            case 'search':
              handleSearch(payload);
              break;

            case 'search_navigate':
              handleSearchNavigate(payload);
              break;

            // Comm bridge messages - handled by React widget system, ignore here
            case 'bridge_ready':
            case 'comm_open':
            case 'comm_msg':
            case 'comm_close':
            case 'comm_sync':
              // These are handled by widget-bridge-client.ts
              break;

            default:
              console.warn('[frame] Unknown message type:', type);
          }
        } catch (err) {
          sendError(err);
        }
      });

      // --- Message Handlers ---

      function handlePing(payload) {
        send('pong', {
          receivedAt: Date.now(),
          echo: payload
        });
      }

      function handleEval(payload) {
        const { code } = payload || {};
        if (!code) {
          send('eval_result', { success: false, error: 'No code provided' });
          return;
        }

        // Store the current message for access during eval
        window.currentMessage = event;
        try {
          const result = eval.call(null, code);
          send('eval_result', { success: true, result: String(result ?? 'undefined') });
        } catch (err) {
          send('eval_result', { success: false, error: err.message });
        } finally {
          delete window.currentMessage;
        }
      }

      function handleRender(payload) {
        const { mimeType, data, metadata, append } = payload || {};

        // Create output container
        const output = document.createElement('div');
        output.className = 'output-item';
        output.style.marginBottom = '8px';

        if (mimeType === 'text/html') {
          // Use createContextualFragment for proper script execution
          const range = document.createRange();
          const fragment = range.createContextualFragment(String(data));
          output.appendChild(fragment);
        } else if (mimeType === 'text/plain') {
          const pre = document.createElement('pre');
          // Handle ANSI escape codes for colored output
          pre.innerHTML = parseAnsi(String(data));
          output.appendChild(pre);
        } else if (mimeType === 'image/svg+xml') {
          // SVG: render inline
          const container = document.createElement('div');
          container.innerHTML = String(data);
          const svg = container.querySelector('svg');
          if (svg) {
            svg.style.maxWidth = '100%';
            svg.style.height = 'auto';
            output.appendChild(svg);
          } else {
            output.appendChild(container);
          }
        } else if (mimeType && mimeType.startsWith('image/')) {
          const img = document.createElement('img');
          const imgData = String(data);
          // Check if it's base64 or a URL
          if (imgData.startsWith('data:') || imgData.startsWith('http')) {
            img.src = imgData;
          } else {
            img.src = 'data:' + mimeType + ';base64,' + imgData;
          }
          if (metadata?.width) img.width = metadata.width;
          if (metadata?.height) img.height = metadata.height;
          output.appendChild(img);
        } else if (mimeType === 'application/json') {
          // JSON: render as formatted, collapsible tree
          const pre = document.createElement('pre');
          try {
            const parsed = typeof data === 'string' ? JSON.parse(data) : data;
            pre.textContent = JSON.stringify(parsed, null, 2);
          } catch (e) {
            pre.textContent = String(data);
          }
          output.appendChild(pre);
        } else {
          // Fallback: render as text
          const pre = document.createElement('pre');
          pre.textContent = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
          output.appendChild(pre);
        }

        // Append or replace
        if (append) {
          root.appendChild(output);
        } else {
          root.innerHTML = '';
          root.appendChild(output);
        }

        // Notify completion
        requestAnimationFrame(function() {
          send('render_complete', { height: document.body.scrollHeight });
        });
      }

      // Basic ANSI escape code parser
      function parseAnsi(text) {
        // Simple ANSI color mapping
        const colors = {
          '30': '#000', '31': '#e74c3c', '32': '#2ecc71', '33': '#f1c40f',
          '34': '#3498db', '35': '#9b59b6', '36': '#1abc9c', '37': '#ecf0f1',
          '90': '#7f8c8d', '91': '#e74c3c', '92': '#2ecc71', '93': '#f1c40f',
          '94': '#3498db', '95': '#9b59b6', '96': '#1abc9c', '97': '#fff'
        };

        // Escape HTML
        let result = text
          .replace(/&/g, '&amp;')
          .replace(/</g, '&lt;')
          .replace(/>/g, '&gt;');

        // Parse ANSI codes
        result = result.replace(/\\x1b\\[(\\d+(?:;\\d+)*)m/g, function(match, codes) {
          const codeList = codes.split(';');
          let style = '';
          for (const code of codeList) {
            if (code === '0') return '</span>';
            if (code === '1') style += 'font-weight:bold;';
            if (code === '3') style += 'font-style:italic;';
            if (code === '4') style += 'text-decoration:underline;';
            if (colors[code]) style += 'color:' + colors[code] + ';';
          }
          return style ? '<span style="' + style + '">' : '';
        });

        // Also handle \\e[ format
        result = result.replace(/\\e\\[(\\d+(?:;\\d+)*)m/g, function(match, codes) {
          const codeList = codes.split(';');
          let style = '';
          for (const code of codeList) {
            if (code === '0') return '</span>';
            if (code === '1') style += 'font-weight:bold;';
            if (colors[code]) style += 'color:' + colors[code] + ';';
          }
          return style ? '<span style="' + style + '">' : '';
        });

        return result;
      }

      function handleTheme(payload) {
        const { isDark, cssVariables } = payload || {};
        const rootEl = document.documentElement;

        if (isDark !== undefined) {
          rootEl.style.setProperty('--bg-primary', isDark ? '#0a0a0a' : '#ffffff');
          rootEl.style.setProperty('--bg-secondary', isDark ? '#1a1a1a' : '#f5f5f5');
          rootEl.style.setProperty('--text-primary', isDark ? '#e0e0e0' : '#1a1a1a');
          rootEl.style.setProperty('--text-secondary', isDark ? '#a0a0a0' : '#666666');
          rootEl.style.setProperty('--border-color', isDark ? '#333333' : '#e0e0e0');
        }

        if (cssVariables) {
          Object.entries(cssVariables).forEach(function([key, value]) {
            rootEl.style.setProperty(key, value);
          });
        }
      }

      function handleClear() {
        root.innerHTML = '';
        send('render_complete', { height: document.body.scrollHeight });
      }

      function handleWidgetState(payload) {
        // Widget state updates are handled by the injected renderer bundle
        // This is a placeholder that fires a custom event
        window.dispatchEvent(new CustomEvent('widget_state', { detail: payload }));
      }

      // --- Search ---
      var searchMarks = [];
      var currentSearchIndex = -1;

      function handleSearch(payload) {
        var query = (payload && payload.query) || '';
        var caseSensitive = payload && payload.caseSensitive;
        clearSearchMarks();
        if (!query) {
          send('search_results', { count: 0 });
          return;
        }
        var marks = [];
        var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
        var node;
        var compareQuery = caseSensitive ? query : query.toLowerCase();
        // Collect all text nodes and their match positions
        var matches = [];
        while ((node = walker.nextNode())) {
          var text = node.nodeValue || '';
          var compareText = caseSensitive ? text : text.toLowerCase();
          var pos = 0;
          while ((pos = compareText.indexOf(compareQuery, pos)) !== -1) {
            matches.push({ node: node, offset: pos, length: query.length });
            pos += query.length;
          }
        }
        // Highlight matches in reverse order to preserve offsets
        for (var i = matches.length - 1; i >= 0; i--) {
          var m = matches[i];
          try {
            var range = document.createRange();
            range.setStart(m.node, m.offset);
            range.setEnd(m.node, m.offset + m.length);
            var mark = document.createElement('mark');
            mark.className = 'global-find-match';
            mark.style.cssText = 'background: #fbbf24; color: #000; border-radius: 2px; padding: 0;';
            range.surroundContents(mark);
            marks.unshift(mark);
          } catch (e) {
            // surroundContents can fail if range crosses element boundaries
          }
        }
        searchMarks = marks;
        currentSearchIndex = -1;
        send('search_results', { count: marks.length });
      }

      function handleSearchNavigate(payload) {
        var matchIndex = (payload && payload.matchIndex) || 0;
        if (searchMarks.length === 0) return;
        // Clear previous active highlight
        if (currentSearchIndex >= 0 && currentSearchIndex < searchMarks.length) {
          searchMarks[currentSearchIndex].style.cssText = 'background: #fbbf24; color: #000; border-radius: 2px; padding: 0;';
        }
        currentSearchIndex = matchIndex;
        if (currentSearchIndex >= 0 && currentSearchIndex < searchMarks.length) {
          var active = searchMarks[currentSearchIndex];
          active.style.cssText = 'background: #f97316; color: #000; border-radius: 2px; padding: 0;';
          active.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
        }
      }

      function clearSearchMarks() {
        for (var i = 0; i < searchMarks.length; i++) {
          var mark = searchMarks[i];
          var parent = mark.parentNode;
          if (parent) {
            while (mark.firstChild) {
              parent.insertBefore(mark.firstChild, mark);
            }
            parent.removeChild(mark);
            parent.normalize();
          }
        }
        searchMarks = [];
        currentSearchIndex = -1;
      }

      // --- Utilities ---

      function send(type, payload) {
        window.parent.postMessage({ type: type, payload: payload }, '*');
      }

      function sendError(err) {
        send('error', {
          message: err.message || String(err),
          stack: err.stack
        });
      }

      // --- Resize Observer ---
      const resizeObserver = new ResizeObserver(function(entries) {
        const height = document.body.scrollHeight;
        send('resize', { height: height });
      });
      resizeObserver.observe(document.body);

      // --- Link Click Interception ---
      document.addEventListener('click', function(e) {
        const link = e.target.closest('a');
        if (link && link.href) {
          e.preventDefault();
          send('link_click', {
            url: link.href,
            newTab: e.metaKey || e.ctrlKey
          });
        }
      });

      // --- Double Click Forwarding ---
      document.addEventListener('dblclick', function(e) {
        // Don't forward double-clicks on links (user is selecting text)
        const link = e.target.closest('a');
        if (!link) {
          send('dblclick', null);
        }
      });

      // --- Error Handler ---
      window.addEventListener('error', function(e) {
        sendError(e.error || new Error(e.message));
      });

      window.addEventListener('unhandledrejection', function(e) {
        sendError(e.reason || new Error('Unhandled promise rejection'));
      });

      // --- Additional Script ---
      ${additionalScript}

      // --- Ready Signal ---
      isReady = true;
      send('ready', null);
    })();
  </script>
</body>
</html>`;
}

/**
 * Create a blob URL from the frame HTML.
 *
 * @param options - Configuration options for the frame
 * @returns A blob: URL that can be used as iframe src
 */
export function createFrameBlobUrl(options?: FrameHtmlOptions): string {
  const html = generateFrameHtml(options);
  const blob = new Blob([html], { type: "text/html" });
  return URL.createObjectURL(blob);
}
registry/outputs/isolated/comm-bridge-manager.ts
/**
 * Comm Bridge Manager - Parent Side
 *
 * This module manages the communication bridge between the parent window's
 * widget system and an isolated iframe. It:
 * - Buffers comm messages until iframe sends `widget_ready`
 * - Syncs all existing widget models to iframe on ready
 * - Forwards comm messages from kernel to iframe
 * - Handles widget messages from iframe and updates parent store + kernel
 *
 * Security: The iframe cannot access Tauri APIs directly. All widget
 * communication must go through this controlled postMessage bridge.
 */

import type { WidgetStore } from "@/registry/widgets/widget-store";
import type {
  CommCloseMessage,
  CommMsgMessage,
  CommOpenMessage,
  CommSyncMessage,
  IframeToParentMessage,
} from "./frame-bridge";
import type { IsolatedFrameHandle } from "./isolated-frame";

// Type for sending messages to kernel
type SendUpdate = (
  commId: string,
  state: Record<string, unknown>,
  buffers?: ArrayBuffer[],
) => void;

type SendCustom = (
  commId: string,
  content: Record<string, unknown>,
  buffers?: ArrayBuffer[],
) => void;

type CloseComm = (commId: string) => void;

interface CommBridgeManagerOptions {
  /** The isolated frame handle for sending messages */
  frame: IsolatedFrameHandle;
  /** The parent widget store */
  store: WidgetStore;
  /** Function to send state updates to kernel */
  sendUpdate: SendUpdate;
  /** Function to send custom messages to kernel */
  sendCustom: SendCustom;
  /** Function to close a comm with kernel */
  closeComm: CloseComm;
}

/**
 * Comm Bridge Manager for proxying widget communication to an isolated iframe.
 *
 * Usage:
 * 1. Create manager when IsolatedFrame is mounted
 * 2. Subscribe to widget store changes to forward to iframe
 * 3. Handle iframe messages via onMessage callback
 * 4. Dispose when iframe is unmounted
 */
export class CommBridgeManager {
  private frame: IsolatedFrameHandle;
  private store: WidgetStore;
  private sendUpdateToKernel: SendUpdate;
  private sendCustomToKernel: SendCustom;
  private closeCommWithKernel: CloseComm;

  private isWidgetReady = false;
  private messageBuffer: Array<
    CommOpenMessage | CommMsgMessage | CommCloseMessage
  > = [];
  private storeUnsubscribe: (() => void) | null = null;

  // Track which models have been sent to avoid duplicate sends
  private sentModels = new Set<string>();

  // Track previous state for each model to detect kernel updates
  private previousState = new Map<string, Record<string, unknown>>();

  // Flag to prevent echoing iframe updates back to iframe
  private isProcessingIframeUpdate = false;

  // Track custom message subscriptions for each model
  private customMessageUnsubscribers = new Map<string, () => void>();

  constructor(options: CommBridgeManagerOptions) {
    this.frame = options.frame;
    this.store = options.store;
    this.sendUpdateToKernel = options.sendUpdate;
    this.sendCustomToKernel = options.sendCustom;
    this.closeCommWithKernel = options.closeComm;

    // Subscribe to store changes to forward to iframe
    this.storeUnsubscribe = this.store.subscribe(() => {
      if (!this.isWidgetReady) return;
      // Skip if this change came from iframe (avoid echo)
      if (this.isProcessingIframeUpdate) return;
      this.syncModels();
    });

    // Signal to iframe that parent bridge is ready
    // Iframe will respond with widget_ready to trigger comm_sync
    this.frame.send({ type: "bridge_ready" });
  }

  /**
   * Handle a message from the iframe.
   * Call this from the IsolatedFrame's onMessage callback.
   */
  handleIframeMessage(message: IframeToParentMessage): void {
    switch (message.type) {
      case "widget_ready":
        this.handleWidgetReady();
        break;

      case "widget_comm_msg":
        this.handleWidgetCommMsg(message.payload);
        break;

      case "widget_comm_close":
        this.handleWidgetCommClose(message.payload);
        break;
    }
  }

  /**
   * Forward a comm_open to the iframe.
   * Called when a widget model is created by the kernel.
   */
  sendCommOpen(
    commId: string,
    targetName: string,
    state: Record<string, unknown>,
    buffers?: ArrayBuffer[],
  ): void {
    const msg: CommOpenMessage = {
      type: "comm_open",
      payload: {
        commId,
        targetName,
        state,
        buffers,
      },
    };

    if (this.isWidgetReady) {
      this.frame.send(msg);
      this.sentModels.add(commId);
    } else {
      this.messageBuffer.push(msg);
    }
  }

  /**
   * Forward a comm_msg (state update or custom message) to the iframe.
   * Called when the kernel sends a state update or custom message.
   */
  sendCommMsg(
    commId: string,
    method: "update" | "custom",
    data: Record<string, unknown>,
    buffers?: ArrayBuffer[],
  ): void {
    const msg: CommMsgMessage = {
      type: "comm_msg",
      payload: {
        commId,
        method,
        data,
        buffers,
      },
    };

    if (this.isWidgetReady) {
      this.frame.send(msg);
    } else {
      this.messageBuffer.push(msg);
    }
  }

  /**
   * Forward a comm_close to the iframe.
   * Called when the kernel closes a widget.
   */
  sendCommClose(commId: string): void {
    const msg: CommCloseMessage = {
      type: "comm_close",
      payload: { commId },
    };

    if (this.isWidgetReady) {
      this.frame.send(msg);
      this.sentModels.delete(commId);
    } else {
      this.messageBuffer.push(msg);
    }
  }

  /**
   * Clean up subscriptions and state.
   */
  dispose(): void {
    if (this.storeUnsubscribe) {
      this.storeUnsubscribe();
      this.storeUnsubscribe = null;
    }
    // Unsubscribe from all custom message subscriptions
    for (const unsubscribe of this.customMessageUnsubscribers.values()) {
      unsubscribe();
    }
    this.customMessageUnsubscribers.clear();
    this.messageBuffer = [];
    this.sentModels.clear();
    this.previousState.clear();
    this.isWidgetReady = false;
  }

  // --- Private Methods ---

  private handleWidgetReady(): void {
    this.isWidgetReady = true;

    // Send comm_sync with all existing models
    const models = this.store.getSnapshot();
    const modelArray: CommSyncMessage["payload"]["models"] = [];

    for (const [commId, model] of models) {
      modelArray.push({
        commId,
        targetName: model.modelModule || "jupyter.widget",
        state: model.state,
        buffers: model.buffers,
      });
      this.sentModels.add(commId);
      // Store initial state for change detection
      this.previousState.set(commId, this.cloneStateSnapshot(model.state));
      // Subscribe to custom messages for this model
      this.subscribeToModelCustomMessages(commId);
    }

    if (modelArray.length > 0) {
      const syncMsg: CommSyncMessage = {
        type: "comm_sync",
        payload: { models: modelArray },
      };
      try {
        this.frame.send(syncMsg);
      } catch (e) {
        console.error("[CommBridge] Error sending comm_sync:", e);
      }
    }

    // Flush buffered messages
    for (const msg of this.messageBuffer) {
      this.frame.send(msg);
      if (msg.type === "comm_open") {
        this.sentModels.add(msg.payload.commId);
      } else if (msg.type === "comm_close") {
        this.sentModels.delete(msg.payload.commId);
      }
    }
    this.messageBuffer = [];
  }

  private handleWidgetCommMsg(payload: {
    commId: string;
    method: "update" | "custom";
    data: Record<string, unknown>;
    bufferPaths?: string[][];
    buffers?: ArrayBuffer[];
  }): void {
    const { commId, method, data, buffers } = payload;

    if (method === "update") {
      // Set flag to prevent echoing this update back to iframe
      this.isProcessingIframeUpdate = true;
      try {
        // Update parent store first (so UI stays in sync)
        this.store.updateModel(commId, data, buffers);
        // Update our tracked state
        const current = this.previousState.get(commId) ?? {};
        this.previousState.set(
          commId,
          this.cloneStateSnapshot({ ...current, ...data }),
        );
        // Then forward to kernel
        this.sendUpdateToKernel(commId, data, buffers);
      } finally {
        this.isProcessingIframeUpdate = false;
      }
    } else if (method === "custom") {
      // Custom messages go directly to kernel (no store update)
      this.sendCustomToKernel(commId, data, buffers);
    }
  }

  private handleWidgetCommClose(payload: { commId: string }): void {
    const { commId } = payload;

    // Update parent store
    this.store.deleteModel(commId);
    // Forward to kernel
    this.closeCommWithKernel(commId);
    // Clean up tracking
    this.sentModels.delete(commId);
  }

  /**
   * Sync models with iframe: new models, deleted models, and state changes.
   * Called when store changes after widget_ready.
   */
  private syncModels(): void {
    const models = this.store.getSnapshot();

    for (const [commId, model] of models) {
      if (!this.sentModels.has(commId)) {
        // New model - send comm_open
        this.sendCommOpen(
          commId,
          model.modelModule || "jupyter.widget",
          model.state,
          model.buffers,
        );
        // Store initial state for change detection
        this.previousState.set(commId, this.cloneStateSnapshot(model.state));
        // Subscribe to custom messages for this model
        this.subscribeToModelCustomMessages(commId);
      } else {
        // Existing model - check for state changes
        const previous = this.previousState.get(commId);
        if (previous) {
          const changedKeys = this.getChangedKeys(previous, model.state);
          if (changedKeys.length > 0) {
            // Build delta with only changed keys
            const delta: Record<string, unknown> = {};
            for (const key of changedKeys) {
              delta[key] = model.state[key];
            }
            // Forward state update to iframe
            this.sendCommMsg(commId, "update", delta, model.buffers);
            // Update tracked state
            this.previousState.set(
              commId,
              this.cloneStateSnapshot(model.state),
            );
          }
        }
      }
    }

    // Check for deleted models
    for (const commId of this.sentModels) {
      if (!models.has(commId)) {
        this.sendCommClose(commId);
        this.previousState.delete(commId);
        // Unsubscribe from custom messages
        this.unsubscribeFromModelCustomMessages(commId);
      }
    }
  }

  /**
   * Subscribe to custom messages for a model and forward them to iframe.
   * This is critical for anywidgets like quak that use custom messages for data.
   */
  private subscribeToModelCustomMessages(commId: string): void {
    // Don't double-subscribe
    if (this.customMessageUnsubscribers.has(commId)) return;

    const unsubscribe = this.store.subscribeToCustomMessage(
      commId,
      (content: Record<string, unknown>, buffers?: DataView[]) => {
        // Convert DataView[] to ArrayBuffer[] for postMessage
        const arrayBuffers = buffers?.map(
          (dv: DataView) => dv.buffer as ArrayBuffer,
        );
        // Forward custom message to iframe
        this.sendCommMsg(commId, "custom", content, arrayBuffers);
      },
    );

    this.customMessageUnsubscribers.set(commId, unsubscribe);
  }

  /**
   * Unsubscribe from custom messages for a model.
   */
  private unsubscribeFromModelCustomMessages(commId: string): void {
    const unsubscribe = this.customMessageUnsubscribers.get(commId);
    if (unsubscribe) {
      unsubscribe();
      this.customMessageUnsubscribers.delete(commId);
    }
  }

  /**
   * Get keys that have changed between two state objects.
   * Uses shallow comparison for performance.
   */
  private getChangedKeys(
    previous: Record<string, unknown>,
    current: Record<string, unknown>,
  ): string[] {
    const changed: string[] = [];
    const allKeys = new Set([
      ...Object.keys(previous),
      ...Object.keys(current),
    ]);
    for (const key of allKeys) {
      if (this.valuesAreDifferent(previous[key], current[key])) {
        changed.push(key);
      }
    }
    return changed;
  }

  /**
   * Create a deep snapshot of model state so future in-place mutations are detectable.
   */
  private cloneStateSnapshot(
    state: Record<string, unknown>,
  ): Record<string, unknown> {
    try {
      return structuredClone(state);
    } catch {
      return { ...state };
    }
  }

  /**
   * Compare two state values.
   * For object/array values, use JSON content comparison so deep snapshots
   * can detect in-place mutations from live state objects.
   */
  private valuesAreDifferent(previous: unknown, current: unknown): boolean {
    if (
      typeof previous !== "object" ||
      previous === null ||
      typeof current !== "object" ||
      current === null
    ) {
      return previous !== current;
    }

    try {
      return JSON.stringify(previous) !== JSON.stringify(current);
    } catch {
      // If value can't be serialized consistently, err on sending an update.
      return true;
    }
  }
}

/**
 * Create a comm bridge manager for an isolated frame.
 */
export function createCommBridgeManager(
  options: CommBridgeManagerOptions,
): CommBridgeManager {
  return new CommBridgeManager(options);
}
registry/outputs/isolated/IsolationTest.tsx
"use client";

import { useEffect, useRef, useState } from "react";

import { IsolatedFrame, type IsolatedFrameHandle } from "./isolated-frame";
import { IsolatedRendererProvider } from "./isolated-renderer-context";

/**
 * Test results from the isolated iframe
 */
interface IsolationTestResult {
  hasTauri: boolean;
  hasInvoke: boolean;
  canAccessParentDocument: boolean;
  canAccessParentLocalStorage: boolean;
  canUseOwnLocalStorage: boolean;
  canUseOwnCookies: boolean;
  canUseIndexedDB: boolean;
  canFetchParentOrigin: boolean;
  windowOrigin: string;
  error?: string;
}

/**
 * HTML template for the isolation test iframe.
 * This runs inside the iframe and reports back via postMessage.
 * Also handles bidirectional communication (ping/pong, eval, render).
 */
const ISOLATION_TEST_HTML = `<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    body {
      font-family: system-ui, sans-serif;
      padding: 16px;
      margin: 0;
      background: #1a1a1a;
      color: #e0e0e0;
    }
    .test-item {
      padding: 8px;
      margin: 4px 0;
      border-radius: 4px;
    }
    .pass { background: #1e3a1e; color: #4ade80; }
    .fail { background: #3a1e1e; color: #f87171; }
    .info { background: #1e2a3a; color: #60a5fa; }
    pre { font-size: 12px; overflow: auto; }
    #render-target { margin-top: 16px; padding: 8px; border: 1px dashed #444; min-height: 20px; }
  </style>
</head>
<body>
  <h3>Iframe Isolation Test</h3>
  <div id="results"></div>
  <div id="messages"></div>
  <div id="render-target"></div>
  <script>
    const results = {
      hasTauri: false,
      hasInvoke: false,
      canAccessParentDocument: false,
      canAccessParentLocalStorage: false,
      canUseOwnLocalStorage: false,
      canUseOwnCookies: false,
      canUseIndexedDB: false,
      canFetchParentOrigin: false,
      windowOrigin: window.origin || 'null',
      error: null
    };

    // Test 1: Check for window.__TAURI__
    try {
      results.hasTauri = typeof window.__TAURI__ !== 'undefined';
    } catch (e) {
      results.hasTauri = false;
    }

    // Test 2: Check for invoke function
    try {
      results.hasInvoke = typeof window.__TAURI_INTERNALS__?.invoke === 'function' ||
                          typeof window.__TAURI__?.core?.invoke === 'function';
    } catch (e) {
      results.hasInvoke = false;
    }

    // Test 3: Try to access parent document
    try {
      const test = window.parent.document.body;
      results.canAccessParentDocument = true;
    } catch (e) {
      results.canAccessParentDocument = false;
    }

    // Test 4: Try to access parent localStorage
    try {
      const test = window.parent.localStorage.getItem('test');
      results.canAccessParentLocalStorage = true;
    } catch (e) {
      results.canAccessParentLocalStorage = false;
    }

    // Test 5: Try to use iframe's own localStorage (should fail with opaque origin)
    try {
      localStorage.setItem('isolation_test', 'test');
      localStorage.removeItem('isolation_test');
      results.canUseOwnLocalStorage = true;
    } catch (e) {
      results.canUseOwnLocalStorage = false;
    }

    // Test 6: Try to use cookies
    try {
      document.cookie = 'isolation_test=1';
      results.canUseOwnCookies = document.cookie.includes('isolation_test');
    } catch (e) {
      results.canUseOwnCookies = false;
    }

    // Test 7: Try to use IndexedDB (should fail with opaque origin)
    try {
      const request = indexedDB.open('isolation_test', 1);
      request.onsuccess = () => {
        results.canUseIndexedDB = true;
        request.result.close();
        indexedDB.deleteDatabase('isolation_test');
        updateResults();
      };
      request.onerror = () => {
        results.canUseIndexedDB = false;
        updateResults();
      };
    } catch (e) {
      results.canUseIndexedDB = false;
    }

    // Test 8: Try to fetch parent origin (will test after display)
    // This is async, so we'll update results later

    // Display results in iframe
    const container = document.getElementById('results');
    const tests = [
      { name: 'window.__TAURI__ exists', value: results.hasTauri, expectFalse: true },
      { name: 'invoke() accessible', value: results.hasInvoke, expectFalse: true },
      { name: 'Can access parent document', value: results.canAccessParentDocument, expectFalse: true },
      { name: 'Can access parent localStorage', value: results.canAccessParentLocalStorage, expectFalse: true },
      { name: 'Can use own localStorage', value: results.canUseOwnLocalStorage, expectFalse: true, info: 'Opaque origin blocks storage' },
      { name: 'Can use cookies', value: results.canUseOwnCookies, expectFalse: true, info: 'Opaque origin blocks cookies' },
      { name: 'Can use IndexedDB', value: results.canUseIndexedDB, expectFalse: true, info: 'Opaque origin blocks IndexedDB' },
    ];

    function updateResults() {
      container.innerHTML = '';
      const currentTests = [
        { name: 'window.__TAURI__ exists', value: results.hasTauri, expectFalse: true },
        { name: 'invoke() accessible', value: results.hasInvoke, expectFalse: true },
        { name: 'Can access parent document', value: results.canAccessParentDocument, expectFalse: true },
        { name: 'Can access parent localStorage', value: results.canAccessParentLocalStorage, expectFalse: true },
        { name: 'Can use own localStorage', value: results.canUseOwnLocalStorage, expectFalse: true },
        { name: 'Can use cookies', value: results.canUseOwnCookies, expectFalse: true },
        { name: 'Can use IndexedDB', value: results.canUseIndexedDB, expectFalse: true },
        { name: 'Can fetch parent origin', value: results.canFetchParentOrigin, expectFalse: true },
      ];
      currentTests.forEach(test => {
        const pass = test.expectFalse ? !test.value : test.value;
        const div = document.createElement('div');
        div.className = 'test-item ' + (pass ? 'pass' : 'fail');
        div.textContent = (pass ? '✓ ' : '✗ ') + test.name + ': ' + test.value;
        container.appendChild(div);
      });
      const originDiv = document.createElement('div');
      originDiv.className = 'test-item info';
      originDiv.innerHTML = '<pre>Window origin: ' + results.windowOrigin + '</pre>';
      container.appendChild(originDiv);
      window.parent.postMessage({ type: 'isolation_test_result', results }, '*');
    }

    tests.forEach(test => {
      const pass = test.expectFalse ? !test.value : test.value;
      const div = document.createElement('div');
      div.className = 'test-item ' + (pass ? 'pass' : 'fail');
      div.textContent = (pass ? '✓ ' : '✗ ') + test.name + ': ' + test.value;
      container.appendChild(div);
    });

    const originDiv = document.createElement('div');
    originDiv.className = 'test-item info';
    originDiv.innerHTML = '<pre>Window origin: ' + results.windowOrigin + '</pre>';
    container.appendChild(originDiv);

    // Test fetch to parent origin (async)
    if (window.parent !== window) {
      let parentOrigin = '/';
      try {
        parentOrigin = window.parent.location?.origin || '/';
      } catch {
        // Expected in sandboxed iframe - origin access blocked
      }
      fetch(parentOrigin, { mode: 'cors' })
        .then(() => {
          results.canFetchParentOrigin = true;
          updateResults();
        })
        .catch(() => {
          results.canFetchParentOrigin = false;
          updateResults();
        });
    }

    // Send initial results to parent
    window.parent.postMessage({ type: 'isolation_test_result', results }, '*');

    // --- Bidirectional Communication ---
    const messagesContainer = document.getElementById('messages');
    const renderTarget = document.getElementById('render-target');

    function logMessage(text, type = 'info') {
      const div = document.createElement('div');
      div.className = 'test-item ' + type;
      div.textContent = text;
      messagesContainer.appendChild(div);
    }

    // Listen for messages from parent
    window.addEventListener('message', function(event) {
      // Only accept messages from our parent window
      if (event.source !== window.parent) {
        return;
      }

      const { type, payload } = event.data || {};

      switch (type) {
        case 'ping':
          // Respond to ping with pong
          logMessage('Received ping, sending pong...', 'info');
          window.parent.postMessage({
            type: 'pong',
            payload: {
              receivedAt: Date.now(),
              echo: payload
            }
          }, '*');
          break;

        case 'eval':
          // Bootstrap/eval pattern (like Colab)
          logMessage('Received eval command', 'info');
          window.currentMessage = event;
          try {
            const result = eval.call(null, payload.code);
            window.parent.postMessage({
              type: 'eval_result',
              payload: { success: true, result: String(result) }
            }, '*');
          } catch (e) {
            window.parent.postMessage({
              type: 'eval_result',
              payload: { success: false, error: e.message }
            }, '*');
          } finally {
            delete window.currentMessage;
          }
          break;

        case 'render':
          // Render HTML content
          logMessage('Received render command: ' + payload.mimeType, 'info');
          if (payload.mimeType === 'text/html') {
            const range = document.createRange();
            const fragment = range.createContextualFragment(payload.data);
            renderTarget.innerHTML = '';
            renderTarget.appendChild(fragment);
            window.parent.postMessage({ type: 'render_complete' }, '*');
          }
          break;

        default:
          logMessage('Unknown message type: ' + type, 'fail');
      }
    });

    // Notify parent that iframe is ready for communication
    window.parent.postMessage({ type: 'ready' }, '*');
  </script>
</body>
</html>`;

/**
 * IsolationTest component - A proof-of-concept to verify that blob URL iframes
 * are properly isolated from Tauri's IPC injection.
 *
 * This component creates an iframe using a blob: URL with sandbox attributes
 * that should prevent access to Tauri APIs while still allowing script execution.
 *
 * Expected results for proper isolation:
 * - window.__TAURI__ should be undefined
 * - invoke() should not be accessible
 * - Parent document should not be accessible
 * - Parent localStorage should not be accessible
 */
/**
 * Communication test state
 */
interface CommTestState {
  iframeReady: boolean;
  pingCount: number;
  lastPongTime: number | null;
  evalResult: string | null;
  renderComplete: boolean;
}

export function IsolationTest() {
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const [blobUrl, setBlobUrl] = useState<string | null>(null);
  const [testResult, setTestResult] = useState<IsolationTestResult | null>(
    null,
  );
  const [parentHasTauri, setParentHasTauri] = useState<boolean>(false);
  const [commState, setCommState] = useState<CommTestState>({
    iframeReady: false,
    pingCount: 0,
    lastPongTime: null,
    evalResult: null,
    renderComplete: false,
  });

  // Check if parent has Tauri (for comparison)
  useEffect(() => {
    setParentHasTauri(
      typeof (window as unknown as { __TAURI__?: unknown }).__TAURI__ !==
        "undefined",
    );
  }, []);

  // Create blob URL on mount
  useEffect(() => {
    const blob = new Blob([ISOLATION_TEST_HTML], { type: "text/html" });
    const url = URL.createObjectURL(blob);
    setBlobUrl(url);

    return () => {
      URL.revokeObjectURL(url);
    };
  }, []);

  // Listen for messages from iframe
  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      const { type, payload } = event.data || {};

      switch (type) {
        case "isolation_test_result":
          setTestResult(payload ?? event.data.results);
          break;
        case "ready":
          setCommState((prev) => ({ ...prev, iframeReady: true }));
          break;
        case "pong":
          setCommState((prev) => ({
            ...prev,
            pingCount: prev.pingCount + 1,
            lastPongTime: payload?.receivedAt ?? Date.now(),
          }));
          break;
        case "eval_result":
          setCommState((prev) => ({
            ...prev,
            evalResult: payload?.success
              ? `Success: ${payload.result}`
              : `Error: ${payload?.error}`,
          }));
          break;
        case "render_complete":
          setCommState((prev) => ({ ...prev, renderComplete: true }));
          break;
      }
    };

    window.addEventListener("message", handleMessage);
    return () => window.removeEventListener("message", handleMessage);
  }, []);

  // Send message to iframe
  const sendToIframe = (type: string, payload?: unknown) => {
    if (iframeRef.current?.contentWindow) {
      iframeRef.current.contentWindow.postMessage({ type, payload }, "*");
    }
  };

  const handlePing = () => {
    sendToIframe("ping", { sentAt: Date.now() });
  };

  const handleEval = () => {
    sendToIframe("eval", { code: "1 + 2 + 3" });
  };

  const handleRender = () => {
    setCommState((prev) => ({ ...prev, renderComplete: false }));
    sendToIframe("render", {
      mimeType: "text/html",
      data: "<div style='color: #4ade80; padding: 8px;'>HTML rendered via postMessage!</div>",
    });
  };

  const isIsolated =
    testResult &&
    !testResult.hasTauri &&
    !testResult.hasInvoke &&
    !testResult.canAccessParentDocument &&
    !testResult.canAccessParentLocalStorage &&
    !testResult.canUseOwnLocalStorage &&
    !testResult.canUseOwnCookies &&
    !testResult.canUseIndexedDB &&
    !testResult.canFetchParentOrigin;

  return (
    <div
      data-testid="isolation-test"
      className="bg-background text-foreground space-y-4 p-4"
    >
      <h2 className="text-lg font-semibold">Blob URL Iframe Isolation Test</h2>

      {/* Parent context info */}
      <div className="bg-muted rounded p-3">
        <h3 className="mb-2 font-medium">Parent Window Context:</h3>
        <p className="text-sm">
          window.__TAURI__ exists:{" "}
          <span
            className={parentHasTauri ? "text-yellow-500" : "text-green-500"}
          >
            {parentHasTauri ? "Yes (expected in Tauri app)" : "No"}
          </span>
        </p>
        <p className="text-sm">
          Window origin: <code className="text-xs">{window.origin}</code>
        </p>
      </div>

      {/* Test results */}
      {testResult && (
        <div
          className={`rounded p-3 ${
            isIsolated
              ? "border border-green-700 bg-green-950"
              : "border border-red-700 bg-red-950"
          }`}
        >
          <h3 className="mb-2 font-medium">
            {isIsolated ? "Iframe is properly isolated!" : "Isolation FAILED"}
          </h3>
          <ul className="space-y-1 text-sm">
            <li>
              Tauri API blocked:{" "}
              <span
                className={
                  !testResult.hasTauri ? "text-green-500" : "text-red-500"
                }
              >
                {!testResult.hasTauri ? "Yes" : "No"}
              </span>
            </li>
            <li>
              invoke() blocked:{" "}
              <span
                className={
                  !testResult.hasInvoke ? "text-green-500" : "text-red-500"
                }
              >
                {!testResult.hasInvoke ? "Yes" : "No"}
              </span>
            </li>
            <li>
              Parent document blocked:{" "}
              <span
                className={
                  !testResult.canAccessParentDocument
                    ? "text-green-500"
                    : "text-red-500"
                }
              >
                {!testResult.canAccessParentDocument ? "Yes" : "No"}
              </span>
            </li>
            <li>
              Parent localStorage blocked:{" "}
              <span
                className={
                  !testResult.canAccessParentLocalStorage
                    ? "text-green-500"
                    : "text-red-500"
                }
              >
                {!testResult.canAccessParentLocalStorage ? "Yes" : "No"}
              </span>
            </li>
            <li>
              Own localStorage blocked (opaque origin):{" "}
              <span
                className={
                  !testResult.canUseOwnLocalStorage
                    ? "text-green-500"
                    : "text-red-500"
                }
              >
                {!testResult.canUseOwnLocalStorage ? "Yes" : "No"}
              </span>
            </li>
            <li>
              Cookies blocked (opaque origin):{" "}
              <span
                className={
                  !testResult.canUseOwnCookies
                    ? "text-green-500"
                    : "text-red-500"
                }
              >
                {!testResult.canUseOwnCookies ? "Yes" : "No"}
              </span>
            </li>
            <li>
              IndexedDB blocked (opaque origin):{" "}
              <span
                className={
                  !testResult.canUseIndexedDB
                    ? "text-green-500"
                    : "text-red-500"
                }
              >
                {!testResult.canUseIndexedDB ? "Yes" : "No"}
              </span>
            </li>
            <li>
              Parent origin fetch blocked:{" "}
              <span
                className={
                  !testResult.canFetchParentOrigin
                    ? "text-green-500"
                    : "text-red-500"
                }
              >
                {!testResult.canFetchParentOrigin ? "Yes" : "No"}
              </span>
            </li>
            <li>
              Iframe origin:{" "}
              <code className="text-xs">{testResult.windowOrigin}</code>
            </li>
          </ul>
        </div>
      )}

      {/* Communication Test Controls */}
      <div className="bg-muted space-y-3 rounded p-3">
        <h3 className="font-medium">Bidirectional Communication Test:</h3>
        <div className="flex items-center gap-2 text-sm">
          <span>
            Iframe ready:{" "}
            <span
              className={
                commState.iframeReady ? "text-green-500" : "text-yellow-500"
              }
            >
              {commState.iframeReady ? "Yes" : "Waiting..."}
            </span>
          </span>
        </div>
        <div className="flex flex-wrap gap-2">
          <button
            onClick={handlePing}
            disabled={!commState.iframeReady}
            className="rounded bg-blue-600 px-3 py-1.5 text-sm hover:bg-blue-700 disabled:bg-gray-600"
          >
            Send Ping
          </button>
          <button
            onClick={handleEval}
            disabled={!commState.iframeReady}
            className="rounded bg-purple-600 px-3 py-1.5 text-sm hover:bg-purple-700 disabled:bg-gray-600"
          >
            Test Eval (1+2+3)
          </button>
          <button
            onClick={handleRender}
            disabled={!commState.iframeReady}
            className="rounded bg-green-600 px-3 py-1.5 text-sm hover:bg-green-700 disabled:bg-gray-600"
          >
            Test Render HTML
          </button>
        </div>
        <div className="space-y-1 text-sm">
          <p>Pong responses received: {commState.pingCount}</p>
          {commState.evalResult && <p>Eval result: {commState.evalResult}</p>}
          {commState.renderComplete && (
            <p className="text-green-500">Render completed successfully</p>
          )}
        </div>
      </div>

      {/* The actual isolated iframe */}
      {blobUrl && (
        <div className="overflow-hidden rounded border">
          <iframe
            ref={iframeRef}
            src={blobUrl}
            sandbox="allow-scripts"
            className="h-80 w-full bg-neutral-900"
            title="Isolation Test Frame"
          />
        </div>
      )}

      {/* Sandbox attribute explanation */}
      <div className="text-muted-foreground space-y-2 text-xs">
        <p>
          <strong>Sandbox attributes:</strong> allow-scripts (no
          allow-same-origin)
        </p>
        <p>
          Without <code>allow-same-origin</code>, the iframe gets an{" "}
          <strong>opaque origin</strong> which:
        </p>
        <ul className="ml-4 list-disc space-y-1">
          <li>Cannot access parent document, localStorage, or cookies</li>
          <li>Cannot use its own localStorage, cookies, or IndexedDB</li>
          <li>Blocks Tauri IPC injection (Tauri only injects at app origin)</li>
        </ul>
        <p className="border-l-2 border-yellow-600 bg-yellow-950/30 p-2">
          <strong>⚠️ Web Security Note:</strong> On the web, blob URLs inherit
          the creator&apos;s origin. The sandbox attribute creates isolation,
          but for maximum security in production web apps, serve untrusted
          content from a <strong>separate domain</strong> (e.g.,{" "}
          <code>runtusercontent.com</code>) rather than a subdomain, as
          subdomains can share cookies in some configurations.
        </p>
      </div>

      {/* Production IsolatedFrame Demo */}
      <ProductionFrameDemo />
    </div>
  );
}

/**
 * Demo of the production IsolatedFrame component.
 */
function ProductionFrameDemo() {
  const frameRef = useRef<IsolatedFrameHandle>(null);
  const [isReady, setIsReady] = useState(false);
  const [height, setHeight] = useState(0);

  const handleRenderHtml = () => {
    frameRef.current?.render({
      mimeType: "text/html",
      data: `
        <h2 style="margin: 0 0 8px 0;">Production IsolatedFrame Test</h2>
        <p>This content was rendered via the <code>IsolatedFrame</code> component.</p>
        <table>
          <tr><th>Feature</th><th>Status</th></tr>
          <tr><td>Blob URL isolation</td><td style="color: #4ade80;">Works</td></tr>
          <tr><td>postMessage communication</td><td style="color: #4ade80;">Works</td></tr>
          <tr><td>Auto-resizing</td><td style="color: #4ade80;">Works</td></tr>
        </table>
        <script>console.log('Script executed in isolated frame!');</script>
      `,
    });
  };

  const handleRenderImage = () => {
    // A small test image (1x1 red pixel in base64)
    frameRef.current?.render({
      mimeType: "image/png",
      data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==",
    });
  };

  const handleClear = () => {
    frameRef.current?.clear();
  };

  return (
    <IsolatedRendererProvider basePath="/isolated">
      <div className="bg-muted border-border mt-4 space-y-3 rounded border-t p-3">
        <h3 className="font-medium">Production IsolatedFrame Component:</h3>
        <div className="flex items-center gap-2 text-sm">
          <span>
            Ready:{" "}
            <span className={isReady ? "text-green-500" : "text-yellow-500"}>
              {isReady ? "Yes" : "Waiting..."}
            </span>
          </span>
          <span className="text-muted-foreground">|</span>
          <span>Height: {height}px</span>
        </div>
        <div className="flex flex-wrap gap-2">
          <button
            onClick={handleRenderHtml}
            disabled={!isReady}
            className="rounded bg-blue-600 px-3 py-1.5 text-sm hover:bg-blue-700 disabled:bg-gray-600"
          >
            Render HTML
          </button>
          <button
            onClick={handleRenderImage}
            disabled={!isReady}
            className="rounded bg-purple-600 px-3 py-1.5 text-sm hover:bg-purple-700 disabled:bg-gray-600"
          >
            Render Image
          </button>
          <button
            onClick={handleClear}
            disabled={!isReady}
            className="rounded bg-red-600 px-3 py-1.5 text-sm hover:bg-red-700 disabled:bg-gray-600"
          >
            Clear
          </button>
        </div>
        <div className="overflow-hidden rounded border">
          <IsolatedFrame
            ref={frameRef}
            darkMode={true}
            minHeight={48}
            maxHeight={400}
            onReady={() => setIsReady(true)}
            onResize={setHeight}
            onLinkClick={(url, newTab) => {
              console.log("Link clicked:", url, newTab);
              window.open(url, newTab ? "_blank" : "_self");
            }}
            onError={(err) => console.error("Frame error:", err)}
          />
        </div>
      </div>
    </IsolatedRendererProvider>
  );
}
registry/outputs/isolated/isolated-renderer-context.tsx
"use client";

import {
  createContext,
  type ReactNode,
  useContext,
  useEffect,
  useState,
} from "react";

interface IsolatedRendererBundle {
  rendererCode: string;
  rendererCss: string;
}

interface IsolatedRendererContextValue {
  rendererCode: string | undefined;
  rendererCss: string | undefined;
  isLoading: boolean;
  error: Error | null;
}

const IsolatedRendererContext =
  createContext<IsolatedRendererContextValue | null>(null);

interface IsolatedRendererProviderProps {
  children: ReactNode;
  /** Base path to fetch isolated-renderer.js and isolated-renderer.css from */
  basePath?: string;
  /** Custom loader function (e.g., for Vite virtual modules) */
  loader?: () => Promise<IsolatedRendererBundle>;
}

// Module-level cache (shared across all provider instances)
let bundleCache: IsolatedRendererBundle | null = null;
let loadingPromise: Promise<IsolatedRendererBundle> | null = null;

/**
 * Provider for the isolated renderer bundle.
 *
 * Wrap your app (or the part that uses IsolatedFrame) with this provider.
 *
 * @example
 * // Option A: Fetch from a URL path
 * <IsolatedRendererProvider basePath="/isolated">
 *   <App />
 * </IsolatedRendererProvider>
 *
 * @example
 * // Option B: Use Vite virtual module (for Tauri/bundled apps)
 * <IsolatedRendererProvider loader={() => import("virtual:isolated-renderer")}>
 *   <App />
 * </IsolatedRendererProvider>
 */
export function IsolatedRendererProvider({
  children,
  basePath,
  loader,
}: IsolatedRendererProviderProps) {
  const [state, setState] = useState<IsolatedRendererContextValue>(() => ({
    rendererCode: bundleCache?.rendererCode,
    rendererCss: bundleCache?.rendererCss,
    isLoading: !bundleCache,
    error: null,
  }));

  useEffect(() => {
    if (bundleCache) {
      // Already loaded, update state if needed
      if (state.isLoading) {
        setState({
          rendererCode: bundleCache.rendererCode,
          rendererCss: bundleCache.rendererCss,
          isLoading: false,
          error: null,
        });
      }
      return;
    }

    let cancelled = false;

    if (!loadingPromise) {
      if (loader) {
        // Use custom loader (Vite plugin, etc.)
        loadingPromise = loader();
      } else if (basePath) {
        // Fetch from URL
        loadingPromise = Promise.all([
          fetch(`${basePath}/isolated-renderer.js`).then((r) => {
            if (!r.ok)
              throw new Error(`Failed to fetch renderer JS: ${r.status}`);
            return r.text();
          }),
          fetch(`${basePath}/isolated-renderer.css`).then((r) => {
            if (!r.ok)
              throw new Error(`Failed to fetch renderer CSS: ${r.status}`);
            return r.text();
          }),
        ]).then(([js, css]) => ({ rendererCode: js, rendererCss: css }));
      } else {
        const error = new Error(
          "IsolatedRendererProvider requires either 'basePath' or 'loader' prop. " +
            "See: https://elements.nteract.io/docs/outputs/isolated-frame#setup",
        );
        setState((s) => ({ ...s, isLoading: false, error }));
        return;
      }
    }

    loadingPromise
      .then((bundle) => {
        bundleCache = bundle;
        if (!cancelled) {
          setState({
            rendererCode: bundle.rendererCode,
            rendererCss: bundle.rendererCss,
            isLoading: false,
            error: null,
          });
        }
      })
      .catch((error) => {
        console.error("[IsolatedRendererProvider] Bundle load failed:", error);
        loadingPromise = null; // Allow retry on next mount
        if (!cancelled) {
          setState((s) => ({ ...s, isLoading: false, error }));
        }
      });

    return () => {
      cancelled = true;
    };
  }, [basePath, loader, state.isLoading]);

  return (
    <IsolatedRendererContext.Provider value={state}>
      {children}
    </IsolatedRendererContext.Provider>
  );
}

// Default state when no provider is present (e.g., during SSR)
const NO_PROVIDER_STATE: IsolatedRendererContextValue = {
  rendererCode: undefined,
  rendererCss: undefined,
  isLoading: true,
  error: null,
};

/**
 * Hook to access the isolated renderer bundle.
 *
 * Returns a "loading" state if used outside IsolatedRendererProvider,
 * which allows components to render safely during SSR.
 * In development, logs a warning when no provider is present.
 */
export function useIsolatedRenderer(): IsolatedRendererContextValue {
  const context = useContext(IsolatedRendererContext);
  if (!context) {
    // During SSR or when provider is missing, return a "not ready" state
    // This allows components to render without crashing
    if (
      process.env.NODE_ENV === "development" &&
      typeof window !== "undefined"
    ) {
      console.warn(
        "useIsolatedRenderer: No IsolatedRendererProvider found. " +
          "Wrap your app with <IsolatedRendererProvider>. " +
          "See: https://elements.nteract.io/docs/outputs/isolated-frame#setup",
      );
    }
    return NO_PROVIDER_STATE;
  }
  return context;
}

/**
 * Reset the bundle cache (useful for testing).
 * @internal
 */
export function _resetBundleCache() {
  bundleCache = null;
  loadingPromise = null;
}

More from nteract

Browse all components in this library →