@replohq/sandbox-runtime 1.6.5 → 1.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -261,6 +261,7 @@ export declare const errorUserFacingDetailsMessageSchema: z.ZodObject<{
261
261
  isRecoverable: z.ZodOptional<z.ZodBoolean>;
262
262
  detail: z.ZodOptional<z.ZodUnion<[z.ZodArray<z.ZodString>, z.ZodString]>>;
263
263
  key: z.ZodString;
264
+ details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
264
265
  usage: z.ZodOptional<z.ZodObject<{
265
266
  current: z.ZodNumber;
266
267
  maximum: z.ZodNumber;
@@ -327,6 +328,7 @@ export declare const errorUserFacingDetailsSchema: z.ZodDiscriminatedUnion<[z.Zo
327
328
  isRecoverable: z.ZodOptional<z.ZodBoolean>;
328
329
  detail: z.ZodOptional<z.ZodUnion<[z.ZodArray<z.ZodString>, z.ZodString]>>;
329
330
  key: z.ZodString;
331
+ details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
330
332
  usage: z.ZodOptional<z.ZodObject<{
331
333
  current: z.ZodNumber;
332
334
  maximum: z.ZodNumber;
@@ -4,6 +4,9 @@ export type ErrorUserFacingDetailsWithMessage = {
4
4
  isRecoverable?: boolean;
5
5
  detail?: string[] | string;
6
6
  key: string;
7
+ details?: {
8
+ [x: string]: unknown;
9
+ };
7
10
  usage?: {
8
11
  current: number;
9
12
  maximum: number;
@@ -52,6 +52,11 @@ var errorUserFacingDetailsMessageSchema = z.object({
52
52
  // in FullPageModal only.
53
53
  detail: z.array(z.string()).or(z.string()).optional(),
54
54
  key: z.string(),
55
+ // Structured data for API callers next to the human message, e.g. the
56
+ // current draft version on a project-context 409 or the offending paths on
57
+ // a 422. The worker error handler serializes userFacingDetails as-is, so
58
+ // this is the one place such data can travel without a second envelope.
59
+ details: z.record(z.string(), z.unknown()).optional(),
55
60
  usage: z.object({
56
61
  current: z.number(),
57
62
  maximum: z.number()
package/buildMetadata.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sandbox-runtime",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "branch": "HEAD",
5
- "commit": "5d112aef8f978ceb8157400597f494b241e6ac1c",
6
- "builtAt": "2026-09-12T01:09:48.099Z"
5
+ "commit": "6f35acf4a9b5e632c79b8914aa2b66350bc80485",
6
+ "builtAt": "2026-09-14T19:23:28.579Z"
7
7
  };
@@ -0,0 +1,134 @@
1
+ // ../replo-utils/lib/viewportSnapshot.ts
2
+ var VIEWPORT_SNAPSHOT_REQUEST = "replo:viewport-snapshot:request";
3
+ var VIEWPORT_SNAPSHOT_RESPONSE = "replo:viewport-snapshot:response";
4
+ var VIEWPORT_SNAPSHOT_MAX_DEPTH = 2;
5
+ function isTrustedViewportSnapshotRequester(origin) {
6
+ let hostname;
7
+ try {
8
+ hostname = new URL(origin).hostname;
9
+ } catch {
10
+ return false;
11
+ }
12
+ if (hostname === "replo.app" || hostname.endsWith(".replo.app")) {
13
+ return true;
14
+ }
15
+ if (process.env.NODE_ENV === "production") {
16
+ return false;
17
+ }
18
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname.endsWith(".ngrok.app");
19
+ }
20
+ function canReceiveMessages(value) {
21
+ return typeof value === "object" && value !== null && "postMessage" in value && typeof value.postMessage === "function";
22
+ }
23
+ function isResponseMessage(data) {
24
+ return typeof data === "object" && data !== null && "type" in data && data.type === VIEWPORT_SNAPSHOT_RESPONSE && "requestId" in data && typeof data.requestId === "string" && "frameIndex" in data && typeof data.frameIndex === "number";
25
+ }
26
+ function isRequestMessage(data) {
27
+ return typeof data === "object" && data !== null && "type" in data && data.type === VIEWPORT_SNAPSHOT_REQUEST && "requestId" in data && typeof data.requestId === "string" && "frameIndex" in data && typeof data.frameIndex === "number" && "remainingDepth" in data && typeof data.remainingDepth === "number";
28
+ }
29
+ function requestChildViewportSnapshots({
30
+ targetWindow,
31
+ requestId,
32
+ timeoutMs,
33
+ remainingDepth = VIEWPORT_SNAPSHOT_MAX_DEPTH
34
+ }) {
35
+ const frameCount = targetWindow.frames.length;
36
+ if (frameCount === 0 || remainingDepth <= 0) {
37
+ return Promise.resolve([]);
38
+ }
39
+ return new Promise((resolve) => {
40
+ const collected = /* @__PURE__ */ new Map();
41
+ const settle = () => {
42
+ targetWindow.removeEventListener("message", onMessage);
43
+ targetWindow.clearTimeout(timeoutId);
44
+ resolve(
45
+ [...collected.entries()].map(([frameIndex, capture]) => ({ frameIndex, capture })).sort((first, second) => {
46
+ return first.frameIndex - second.frameIndex;
47
+ })
48
+ );
49
+ };
50
+ const onMessage = (event) => {
51
+ if (!isResponseMessage(event.data) || event.data.requestId !== requestId || // A frame may only answer for its own slot — anything else could
52
+ // overwrite a sibling's capture or force an early settle.
53
+ event.source !== targetWindow.frames[event.data.frameIndex]) {
54
+ return;
55
+ }
56
+ if (event.data.capture !== null) {
57
+ collected.set(event.data.frameIndex, event.data.capture);
58
+ }
59
+ if (collected.size === frameCount) {
60
+ settle();
61
+ }
62
+ };
63
+ targetWindow.addEventListener("message", onMessage);
64
+ const timeoutId = targetWindow.setTimeout(settle, timeoutMs);
65
+ for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) {
66
+ const message = {
67
+ type: VIEWPORT_SNAPSHOT_REQUEST,
68
+ requestId,
69
+ frameIndex,
70
+ remainingDepth
71
+ };
72
+ targetWindow.frames[frameIndex]?.postMessage(message, "*");
73
+ }
74
+ });
75
+ }
76
+ function startViewportSnapshotResponder({
77
+ targetWindow,
78
+ takeSnapshot,
79
+ measureFrameRect,
80
+ isTrustedRequester = isTrustedViewportSnapshotRequester,
81
+ cascadeTimeoutMs = 1500
82
+ }) {
83
+ if (targetWindow.parent === targetWindow) {
84
+ return () => {
85
+ };
86
+ }
87
+ const onMessage = (event) => {
88
+ if (!isRequestMessage(event.data) || event.source !== targetWindow.parent || !canReceiveMessages(event.source) || !isTrustedRequester(event.origin ?? "")) {
89
+ return;
90
+ }
91
+ const { requestId, frameIndex, remainingDepth } = event.data;
92
+ const requester = event.source;
93
+ const requesterOrigin = event.origin ?? "*";
94
+ const respond = (capture) => {
95
+ const response = {
96
+ type: VIEWPORT_SNAPSHOT_RESPONSE,
97
+ requestId,
98
+ frameIndex,
99
+ capture
100
+ };
101
+ requester.postMessage(response, requesterOrigin);
102
+ };
103
+ let snapshot = null;
104
+ try {
105
+ snapshot = takeSnapshot();
106
+ } catch {
107
+ }
108
+ if (snapshot === null) {
109
+ respond(null);
110
+ return;
111
+ }
112
+ void requestChildViewportSnapshots({
113
+ targetWindow,
114
+ // Derived per level so a child's own fan-out can't collide with ours.
115
+ requestId: `${requestId}:${frameIndex}`,
116
+ timeoutMs: cascadeTimeoutMs,
117
+ remainingDepth: remainingDepth - 1
118
+ }).then((childCaptures) => {
119
+ respond({
120
+ snapshot,
121
+ childFrames: childCaptures.flatMap((child) => {
122
+ const rect = measureFrameRect(child.frameIndex);
123
+ return rect ? [{ frameIndex: child.frameIndex, rect, capture: child.capture }] : [];
124
+ })
125
+ });
126
+ });
127
+ };
128
+ targetWindow.addEventListener("message", onMessage);
129
+ return () => {
130
+ return targetWindow.removeEventListener("message", onMessage);
131
+ };
132
+ }
133
+
134
+ export { VIEWPORT_SNAPSHOT_MAX_DEPTH, VIEWPORT_SNAPSHOT_REQUEST, VIEWPORT_SNAPSHOT_RESPONSE, isTrustedViewportSnapshotRequester, requestChildViewportSnapshots, startViewportSnapshotResponder };
@@ -2,6 +2,7 @@
2
2
  import { useEffect, useRef } from "react";
3
3
  import { buildGoogleFontsCssUrl } from "../_vendor/replo-utils/lib/fonts.mjs";
4
4
  import { designLibraryPreviewMessageSchema } from "../_vendor/schemas/designLibrary.mjs";
5
+ import { isAllowedEditorMessage } from "../annotations/bridgeGate";
5
6
  const FONT_LINK_DATA_KEY = "reploDesignLibraryFont";
6
7
  function escapeCssString(value) {
7
8
  return value.replace(/\\/gu, "\\\\").replace(/'/gu, "\\'");
@@ -13,6 +14,7 @@ function DesignLibraryPreview() {
13
14
  const ownsDefaultFont = useRef(false);
14
15
  useEffect(() => {
15
16
  const handleMessage = (event) => {
17
+ if (!isAllowedEditorMessage(event)) return;
16
18
  const parsed = designLibraryPreviewMessageSchema.safeParse(event.data);
17
19
  if (!parsed.success) return;
18
20
  const { cssVars, fonts, defaultFontCssVar } = parsed.data;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../design-library-preview/DesignLibraryPreview.tsx"],
4
- "sourcesContent": ["\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nimport { buildGoogleFontsCssUrl } from \"replo-utils/lib/fonts\";\nimport { designLibraryPreviewMessageSchema } from \"schemas/designLibrary\";\n\nconst FONT_LINK_DATA_KEY = \"reploDesignLibraryFont\";\n\nfunction escapeCssString(value: string): string {\n return value.replace(/\\\\/gu, \"\\\\\\\\\").replace(/'/gu, \"\\\\'\");\n}\n\nexport function DesignLibraryPreview() {\n // NOTE (Ben, 2026-06-03): The custom-property keys we set last time, so on the\n // next message we remove only the ones we own that the new override set drops.\n const ownedKeys = useRef<Set<string> | null>(null);\n // NOTE (Ben, 2026-06-05): The Google Font <link> hrefs we injected, keyed by\n // href so we de-dupe and remove only our own when the override set changes.\n const ownedFontLinks = useRef<Map<string, HTMLLinkElement> | null>(null);\n // NOTE (Ben, 2026-06-05): The @font-face <style> nodes for uploaded custom\n // fonts, keyed by family with the URL we injected, so we de-dupe, re-inject\n // when a family's asset URL changes (re-upload), and remove only our own when\n // the override set changes.\n const ownedFontFaces = useRef<Map<\n string,\n { url: string; style: HTMLStyleElement }\n > | null>(null);\n // NOTE (Ben, 2026-06-05): Whether we set the document default font-family, so\n // we can clear just that.\n const ownsDefaultFont = useRef(false);\n\n // eslint-disable-next-line replo/no-use-effect -- Subscribes to window message events from the parent editor.\n useEffect(() => {\n const handleMessage = (event: MessageEvent) => {\n const parsed = designLibraryPreviewMessageSchema.safeParse(event.data);\n if (!parsed.success) return;\n const { cssVars, fonts, defaultFontCssVar } = parsed.data;\n const root = document.documentElement;\n\n // --- CSS variable overrides (colors + font role vars) ---\n const nextVars: Record<string, string> = { ...cssVars };\n for (const font of fonts) {\n nextVars[font.cssVar] = `'${escapeCssString(font.family)}'`;\n }\n const owned = ownedKeys.current ?? new Set();\n for (const key of owned) {\n if (!(key in nextVars)) root.style.removeProperty(key);\n }\n ownedKeys.current = new Set(Object.keys(nextVars));\n for (const [key, value] of Object.entries(nextVars)) {\n root.style.setProperty(key, value);\n }\n\n // Custom uploaded fonts carry a faceUrl; Google fonts don't.\n const customFonts = fonts.filter((font) => Boolean(font.faceUrl));\n const googleFonts = fonts.filter((font) => !font.faceUrl);\n\n // --- Google Font stylesheets (<link> to the CDN) ---\n const links = ownedFontLinks.current ?? new Map();\n const nextHrefs = new Set(\n googleFonts.map((font) => buildGoogleFontsCssUrl(font.family)),\n );\n for (const [href, link] of links) {\n if (!nextHrefs.has(href)) {\n link.remove();\n links.delete(href);\n }\n }\n for (const href of nextHrefs) {\n if (links.has(href)) continue;\n const link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = href;\n link.dataset[FONT_LINK_DATA_KEY] = \"\";\n document.head.append(link);\n links.set(href, link);\n }\n ownedFontLinks.current = links;\n\n // --- Custom uploaded fonts (@font-face from the asset URL) ---\n const faces = ownedFontFaces.current ?? new Map();\n const nextFamilies = new Set(customFonts.map((font) => font.family));\n for (const [family, entry] of faces) {\n if (!nextFamilies.has(family)) {\n entry.style.remove();\n faces.delete(family);\n }\n }\n for (const font of customFonts) {\n if (!font.faceUrl) continue;\n const existing = faces.get(font.family);\n // Re-upload reuses the family but changes the asset URL; replace the\n // stale node rather than skipping it.\n if (existing?.url === font.faceUrl) continue;\n existing?.style.remove();\n const style = document.createElement(\"style\");\n style.dataset[FONT_LINK_DATA_KEY] = \"\";\n const format = font.format\n ? ` format('${escapeCssString(font.format)}')`\n : \"\";\n style.textContent = `@font-face{font-family:'${escapeCssString(font.family)}';font-weight:400;font-style:normal;src:url('${escapeCssString(font.faceUrl)}')${format};font-display:swap;}`;\n document.head.append(style);\n faces.set(font.family, { url: font.faceUrl, style });\n }\n ownedFontFaces.current = faces;\n\n // --- Document default font ---\n if (defaultFontCssVar) {\n root.style.setProperty(\"font-family\", `var(${defaultFontCssVar})`);\n ownsDefaultFont.current = true;\n } else if (ownsDefaultFont.current) {\n root.style.removeProperty(\"font-family\");\n ownsDefaultFont.current = false;\n }\n };\n window.addEventListener(\"message\", handleMessage);\n return () => {\n window.removeEventListener(\"message\", handleMessage);\n };\n }, []);\n\n return null;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nimport { buildGoogleFontsCssUrl } from \"replo-utils/lib/fonts\";\nimport { designLibraryPreviewMessageSchema } from \"schemas/designLibrary\";\n\nimport { isAllowedEditorMessage } from \"../annotations/bridgeGate\";\n\nconst FONT_LINK_DATA_KEY = \"reploDesignLibraryFont\";\n\nfunction escapeCssString(value: string): string {\n return value.replace(/\\\\/gu, \"\\\\\\\\\").replace(/'/gu, \"\\\\'\");\n}\n\nexport function DesignLibraryPreview() {\n // NOTE (Ben, 2026-06-03): The custom-property keys we set last time, so on the\n // next message we remove only the ones we own that the new override set drops.\n const ownedKeys = useRef<Set<string> | null>(null);\n // NOTE (Ben, 2026-06-05): The Google Font <link> hrefs we injected, keyed by\n // href so we de-dupe and remove only our own when the override set changes.\n const ownedFontLinks = useRef<Map<string, HTMLLinkElement> | null>(null);\n // NOTE (Ben, 2026-06-05): The @font-face <style> nodes for uploaded custom\n // fonts, keyed by family with the URL we injected, so we de-dupe, re-inject\n // when a family's asset URL changes (re-upload), and remove only our own when\n // the override set changes.\n const ownedFontFaces = useRef<Map<\n string,\n { url: string; style: HTMLStyleElement }\n > | null>(null);\n // NOTE (Ben, 2026-06-05): Whether we set the document default font-family, so\n // we can clear just that.\n const ownsDefaultFont = useRef(false);\n\n // eslint-disable-next-line replo/no-use-effect -- Subscribes to window message events from the parent editor.\n useEffect(() => {\n const handleMessage = (event: MessageEvent) => {\n if (!isAllowedEditorMessage(event)) return;\n const parsed = designLibraryPreviewMessageSchema.safeParse(event.data);\n if (!parsed.success) return;\n const { cssVars, fonts, defaultFontCssVar } = parsed.data;\n const root = document.documentElement;\n\n // --- CSS variable overrides (colors + font role vars) ---\n const nextVars: Record<string, string> = { ...cssVars };\n for (const font of fonts) {\n nextVars[font.cssVar] = `'${escapeCssString(font.family)}'`;\n }\n const owned = ownedKeys.current ?? new Set();\n for (const key of owned) {\n if (!(key in nextVars)) root.style.removeProperty(key);\n }\n ownedKeys.current = new Set(Object.keys(nextVars));\n for (const [key, value] of Object.entries(nextVars)) {\n root.style.setProperty(key, value);\n }\n\n // Custom uploaded fonts carry a faceUrl; Google fonts don't.\n const customFonts = fonts.filter((font) => Boolean(font.faceUrl));\n const googleFonts = fonts.filter((font) => !font.faceUrl);\n\n // --- Google Font stylesheets (<link> to the CDN) ---\n const links = ownedFontLinks.current ?? new Map();\n const nextHrefs = new Set(\n googleFonts.map((font) => buildGoogleFontsCssUrl(font.family)),\n );\n for (const [href, link] of links) {\n if (!nextHrefs.has(href)) {\n link.remove();\n links.delete(href);\n }\n }\n for (const href of nextHrefs) {\n if (links.has(href)) continue;\n const link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = href;\n link.dataset[FONT_LINK_DATA_KEY] = \"\";\n document.head.append(link);\n links.set(href, link);\n }\n ownedFontLinks.current = links;\n\n // --- Custom uploaded fonts (@font-face from the asset URL) ---\n const faces = ownedFontFaces.current ?? new Map();\n const nextFamilies = new Set(customFonts.map((font) => font.family));\n for (const [family, entry] of faces) {\n if (!nextFamilies.has(family)) {\n entry.style.remove();\n faces.delete(family);\n }\n }\n for (const font of customFonts) {\n if (!font.faceUrl) continue;\n const existing = faces.get(font.family);\n // Re-upload reuses the family but changes the asset URL; replace the\n // stale node rather than skipping it.\n if (existing?.url === font.faceUrl) continue;\n existing?.style.remove();\n const style = document.createElement(\"style\");\n style.dataset[FONT_LINK_DATA_KEY] = \"\";\n const format = font.format\n ? ` format('${escapeCssString(font.format)}')`\n : \"\";\n style.textContent = `@font-face{font-family:'${escapeCssString(font.family)}';font-weight:400;font-style:normal;src:url('${escapeCssString(font.faceUrl)}')${format};font-display:swap;}`;\n document.head.append(style);\n faces.set(font.family, { url: font.faceUrl, style });\n }\n ownedFontFaces.current = faces;\n\n // --- Document default font ---\n if (defaultFontCssVar) {\n root.style.setProperty(\"font-family\", `var(${defaultFontCssVar})`);\n ownsDefaultFont.current = true;\n } else if (ownsDefaultFont.current) {\n root.style.removeProperty(\"font-family\");\n ownsDefaultFont.current = false;\n }\n };\n window.addEventListener(\"message\", handleMessage);\n return () => {\n window.removeEventListener(\"message\", handleMessage);\n };\n }, []);\n\n return null;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@replohq/sandbox-runtime",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sandbox-runtime",
6
- "version": "1.6.5",
6
+ "version": "1.6.7",
7
7
  "branch": "HEAD",
8
- "commit": "5d112aef8f978ceb8157400597f494b241e6ac1c",
9
- "builtAt": "2026-09-12T01:09:48.099Z"
8
+ "commit": "6f35acf4a9b5e632c79b8914aa2b66350bc80485",
9
+ "builtAt": "2026-09-14T19:23:28.579Z"
10
10
  },
11
11
  "description": "Replo sandbox runtime — editor integration for agent-built Next.js sites.",
12
12
  "license": "SEE LICENSE IN LICENSE",
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useViewportSnapshotResponder } from "./_vendor/replo-utils/react/use-viewport-snapshot-responder.mjs";
3
4
  import { AnnotationLayerMount } from "./annotations/AnnotationLayerMount";
4
5
  import { ClientErrorBoundary } from "./ClientErrorBoundary";
5
6
  import { DesignLibraryPreview } from "./design-library-preview/DesignLibraryPreview";
@@ -9,6 +10,10 @@ import { NavigationReceiver } from "./NavigationReceiver";
9
10
  import { NextErrorReporter } from "./NextErrorReporter";
10
11
  import PostHogRelay from "./posthog-relay";
11
12
  import { EditorSelection } from "./select-mode/EditorSelection";
13
+ function ViewportSnapshotResponder() {
14
+ useViewportSnapshotResponder();
15
+ return null;
16
+ }
12
17
  function SandboxRuntime({ children }) {
13
18
  const isDevServer = process.env.NODE_ENV === "development";
14
19
  return isDevServer ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -18,6 +23,7 @@ function SandboxRuntime({ children }) {
18
23
  /* @__PURE__ */ jsx(LinkInterceptor, {}),
19
24
  /* @__PURE__ */ jsx(NavigationReceiver, {}),
20
25
  /* @__PURE__ */ jsx(PostHogRelay, {}),
26
+ /* @__PURE__ */ jsx(ViewportSnapshotResponder, {}),
21
27
  /* @__PURE__ */ jsx(EditorSelection, {}),
22
28
  /* @__PURE__ */ jsx(AnnotationLayerMount, {}),
23
29
  /* @__PURE__ */ jsx(DesignLibraryPreview, {}),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../sandbox-runtime.tsx"],
4
- "sourcesContent": ["\"use client\";\n\nimport { AnnotationLayerMount } from \"./annotations/AnnotationLayerMount\";\nimport { ClientErrorBoundary } from \"./ClientErrorBoundary\";\nimport { DesignLibraryPreview } from \"./design-library-preview/DesignLibraryPreview\";\nimport { DocumentReadyBeacon } from \"./DocumentReadyBeacon\";\nimport LinkInterceptor from \"./LinkInterceptor\";\nimport { NavigationReceiver } from \"./NavigationReceiver\";\nimport { NextErrorReporter } from \"./NextErrorReporter\";\nimport PostHogRelay from \"./posthog-relay\";\nimport { EditorSelection } from \"./select-mode/EditorSelection\";\n\nexport function SandboxRuntime({ children }: React.PropsWithChildren) {\n // The editor only embeds dev servers (`next dev`), so editor-integration components render there only \u2014 production builds ship none of them and behave like plain Next apps.\n const isDevServer = process.env.NODE_ENV === \"development\";\n\n return isDevServer ? (\n <>\n <DocumentReadyBeacon />\n {/* NOTE (Max, 2026-06-08): Outside ClientErrorBoundary on purpose \u2014 when\n the boundary catches a runtime error it swaps its children for a\n neutral cover, which would unmount the reporter and freeze it in the\n error state. It must stay mounted to observe Next's overlay across\n error <-> recover. */}\n <NextErrorReporter />\n <ClientErrorBoundary>\n <LinkInterceptor />\n <NavigationReceiver />\n <PostHogRelay />\n <EditorSelection />\n <AnnotationLayerMount />\n <DesignLibraryPreview />\n {children}\n </ClientErrorBoundary>\n </>\n ) : (\n children\n );\n}\n"],
5
- "mappings": ";AAiBI,mBACE,KAOA,YARF;AAfJ,SAAS,4BAA4B;AACrC,SAAS,2BAA2B;AACpC,SAAS,4BAA4B;AACrC,SAAS,2BAA2B;AACpC,OAAO,qBAAqB;AAC5B,SAAS,0BAA0B;AACnC,SAAS,yBAAyB;AAClC,OAAO,kBAAkB;AACzB,SAAS,uBAAuB;AAEzB,SAAS,eAAe,EAAE,SAAS,GAA4B;AAEpE,QAAM,cAAc,QAAQ,IAAI,aAAa;AAE7C,SAAO,cACL,iCACE;AAAA,wBAAC,uBAAoB;AAAA,IAMrB,oBAAC,qBAAkB;AAAA,IACnB,qBAAC,uBACC;AAAA,0BAAC,mBAAgB;AAAA,MACjB,oBAAC,sBAAmB;AAAA,MACpB,oBAAC,gBAAa;AAAA,MACd,oBAAC,mBAAgB;AAAA,MACjB,oBAAC,wBAAqB;AAAA,MACtB,oBAAC,wBAAqB;AAAA,MACrB;AAAA,OACH;AAAA,KACF,IAEA;AAEJ;",
4
+ "sourcesContent": ["\"use client\";\n\nimport { useViewportSnapshotResponder } from \"replo-utils/react/use-viewport-snapshot-responder\";\n\nimport { AnnotationLayerMount } from \"./annotations/AnnotationLayerMount\";\nimport { ClientErrorBoundary } from \"./ClientErrorBoundary\";\nimport { DesignLibraryPreview } from \"./design-library-preview/DesignLibraryPreview\";\nimport { DocumentReadyBeacon } from \"./DocumentReadyBeacon\";\nimport LinkInterceptor from \"./LinkInterceptor\";\nimport { NavigationReceiver } from \"./NavigationReceiver\";\nimport { NextErrorReporter } from \"./NextErrorReporter\";\nimport PostHogRelay from \"./posthog-relay\";\nimport { EditorSelection } from \"./select-mode/EditorSelection\";\n\nfunction ViewportSnapshotResponder() {\n useViewportSnapshotResponder();\n return null;\n}\n\nexport function SandboxRuntime({ children }: React.PropsWithChildren) {\n // The editor only embeds dev servers (`next dev`), so editor-integration components render there only \u2014 production builds ship none of them and behave like plain Next apps.\n const isDevServer = process.env.NODE_ENV === \"development\";\n\n return isDevServer ? (\n <>\n <DocumentReadyBeacon />\n {/* NOTE (Max, 2026-06-08): Outside ClientErrorBoundary on purpose \u2014 when\n the boundary catches a runtime error it swaps its children for a\n neutral cover, which would unmount the reporter and freeze it in the\n error state. It must stay mounted to observe Next's overlay across\n error <-> recover. */}\n <NextErrorReporter />\n <ClientErrorBoundary>\n <LinkInterceptor />\n <NavigationReceiver />\n <PostHogRelay />\n <ViewportSnapshotResponder />\n <EditorSelection />\n <AnnotationLayerMount />\n <DesignLibraryPreview />\n {children}\n </ClientErrorBoundary>\n </>\n ) : (\n children\n );\n}\n"],
5
+ "mappings": ";AAwBI,mBACE,KAOA,YARF;AAtBJ,SAAS,oCAAoC;AAE7C,SAAS,4BAA4B;AACrC,SAAS,2BAA2B;AACpC,SAAS,4BAA4B;AACrC,SAAS,2BAA2B;AACpC,OAAO,qBAAqB;AAC5B,SAAS,0BAA0B;AACnC,SAAS,yBAAyB;AAClC,OAAO,kBAAkB;AACzB,SAAS,uBAAuB;AAEhC,SAAS,4BAA4B;AACnC,+BAA6B;AAC7B,SAAO;AACT;AAEO,SAAS,eAAe,EAAE,SAAS,GAA4B;AAEpE,QAAM,cAAc,QAAQ,IAAI,aAAa;AAE7C,SAAO,cACL,iCACE;AAAA,wBAAC,uBAAoB;AAAA,IAMrB,oBAAC,qBAAkB;AAAA,IACnB,qBAAC,uBACC;AAAA,0BAAC,mBAAgB;AAAA,MACjB,oBAAC,sBAAmB;AAAA,MACpB,oBAAC,gBAAa;AAAA,MACd,oBAAC,6BAA0B;AAAA,MAC3B,oBAAC,mBAAgB;AAAA,MACjB,oBAAC,wBAAqB;AAAA,MACtB,oBAAC,wBAAqB;AAAA,MACrB;AAAA,OACH;AAAA,KACF,IAEA;AAEJ;",
6
6
  "names": []
7
7
  }