@replohq/sandbox-runtime 1.7.4 → 1.7.6

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.
package/buildMetadata.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sandbox-runtime",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
4
4
  "branch": "HEAD",
5
- "commit": "f6125c1834fe26cb6f884cdff75da5f5788f3ca8",
6
- "builtAt": "2026-09-17T01:40:15.017Z"
5
+ "commit": "eebf20f7ddeeb977a292b14a7abe974b07255b14",
6
+ "builtAt": "2026-09-18T20:52:15.073Z"
7
7
  };
@@ -14,6 +14,7 @@ import {
14
14
  createIframeRpcChannel,
15
15
  createIframeRpcRemote
16
16
  } from "../_vendor/schemas/iframeRpcChannel.mjs";
17
+ import { PREVIEW_RENDERED_EVENT } from "../select-mode/previewRendered";
17
18
  import { getComponentPreviewHref } from "./component-preview-href";
18
19
  import {
19
20
  mergeClientProps,
@@ -38,6 +39,7 @@ function getCurrentHref() {
38
39
  function useComponentPreviewChannel(onSetProps) {
39
40
  const onSetPropsRef = useRef(onSetProps);
40
41
  onSetPropsRef.current = onSetProps;
42
+ const pendingUpdates = useRef(Promise.resolve());
41
43
  useIframeRpcChannel(
42
44
  useMemo(() => {
43
45
  if (typeof window === "undefined") {
@@ -61,7 +63,20 @@ function useComponentPreviewChannel(onSetProps) {
61
63
  outgoingMethods: componentPreviewToWebsiteBuilderMethodConfig,
62
64
  incomingMethods: websiteBuilderToComponentPreviewMethodConfig,
63
65
  handlers: {
64
- setProps: (input) => onSetPropsRef.current(input)
66
+ setProps: (input) => {
67
+ const result = pendingUpdates.current.then(async () => {
68
+ const result2 = await onSetPropsRef.current(input);
69
+ window.dispatchEvent(new Event(PREVIEW_RENDERED_EVENT));
70
+ return result2;
71
+ });
72
+ pendingUpdates.current = result.then(
73
+ () => {
74
+ },
75
+ () => {
76
+ }
77
+ );
78
+ return result;
79
+ }
65
80
  }
66
81
  });
67
82
  return {
@@ -109,30 +124,28 @@ function useComponentPreviewChannel(onSetProps) {
109
124
  }, [])
110
125
  );
111
126
  }
112
- function useNavigateAndSettle() {
113
- const router = useRouter();
114
- const [isNavigationPending, startNavigation] = useTransition();
127
+ function useCommitPreviewUpdate() {
128
+ const [isUpdatePending, startUpdate] = useTransition();
115
129
  const settleResolvers = useRef([]);
116
130
  useEffect(() => {
117
- if (!isNavigationPending) {
131
+ if (!isUpdatePending) {
118
132
  const resolvers = settleResolvers.current;
119
133
  settleResolvers.current = [];
120
134
  for (const resolve of resolvers) {
121
135
  resolve();
122
136
  }
123
137
  }
124
- }, [isNavigationPending]);
125
- return (href) => {
138
+ }, [isUpdatePending]);
139
+ return (update) => {
126
140
  return new Promise((resolve) => {
127
141
  settleResolvers.current.push(resolve);
128
- startNavigation(() => {
129
- router.replace(href, { scroll: false });
130
- });
142
+ startUpdate(update);
131
143
  });
132
144
  };
133
145
  }
134
146
  function ComponentPreviewNavigation() {
135
- const navigateAndSettle = useNavigateAndSettle();
147
+ const router = useRouter();
148
+ const commitPreviewUpdate = useCommitPreviewUpdate();
136
149
  useComponentPreviewChannel(
137
150
  async ({ props, sourceRevision }) => {
138
151
  const nextHref = getComponentPreviewHref({
@@ -143,7 +156,9 @@ function ComponentPreviewNavigation() {
143
156
  if (nextHref === getCurrentHref()) {
144
157
  return { mode: "unchanged" };
145
158
  }
146
- await navigateAndSettle(nextHref);
159
+ await commitPreviewUpdate(() => {
160
+ return router.replace(nextHref, { scroll: false });
161
+ });
147
162
  return { mode: "navigation" };
148
163
  }
149
164
  );
@@ -165,7 +180,8 @@ function ComponentPreviewStage({
165
180
  propsRef.current = props;
166
181
  const lastRawPropsRef = useRef(initialRawProps);
167
182
  const lastSourceRevisionRef = useRef(initialSourceRevision);
168
- const navigateAndSettle = useNavigateAndSettle();
183
+ const router = useRouter();
184
+ const commitPreviewUpdate = useCommitPreviewUpdate();
169
185
  useComponentPreviewChannel(
170
186
  async ({
171
187
  props: nextRawProps,
@@ -183,17 +199,21 @@ function ComponentPreviewStage({
183
199
  previous: lastRawPropsRef.current,
184
200
  next: nextRawProps
185
201
  })) {
186
- await navigateAndSettle(nextHref);
202
+ await commitPreviewUpdate(() => {
203
+ return router.replace(nextHref, { scroll: false });
204
+ });
187
205
  lastRawPropsRef.current = nextRawProps;
188
206
  lastSourceRevisionRef.current = sourceRevision;
189
207
  return { mode: "navigation" };
190
208
  }
191
- setProps(
192
- mergeClientProps({
193
- currentResolved: propsRef.current,
194
- nextRaw: nextRawProps
195
- })
196
- );
209
+ await commitPreviewUpdate(() => {
210
+ return setProps(
211
+ mergeClientProps({
212
+ currentResolved: propsRef.current,
213
+ nextRaw: nextRawProps
214
+ })
215
+ );
216
+ });
197
217
  lastRawPropsRef.current = nextRawProps;
198
218
  window.history.replaceState(null, "", nextHref);
199
219
  return { mode: "client" };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@replohq/sandbox-runtime",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sandbox-runtime",
6
- "version": "1.7.4",
6
+ "version": "1.7.6",
7
7
  "branch": "HEAD",
8
- "commit": "f6125c1834fe26cb6f884cdff75da5f5788f3ca8",
9
- "builtAt": "2026-09-17T01:40:15.017Z"
8
+ "commit": "eebf20f7ddeeb977a292b14a7abe974b07255b14",
9
+ "builtAt": "2026-09-18T20:52:15.073Z"
10
10
  },
11
11
  "description": "Replo sandbox runtime — editor integration for agent-built Next.js sites.",
12
12
  "license": "SEE LICENSE IN LICENSE",
@@ -15,7 +15,10 @@ import { useSelectionScrollSync } from "./hooks/useSelectionScrollSync";
15
15
  import { useSelectModeController } from "./hooks/useSelectModeController";
16
16
  import { getRepetitionSiblingElements } from "./hooks/useSiblingResolution";
17
17
  import { getWebsiteBuilderRpc } from "./iframeRpcChannel";
18
- import { isSelectionStillCurrent } from "./selectionPublish";
18
+ import { getDocumentScrollOffset } from "./interactions/selectionGeometry";
19
+ import { PREVIEW_RENDERED_EVENT } from "./previewRendered";
20
+ import { showElementSelectionFrames } from "./selectionFrame";
21
+ import { getConnectedSelectedElement } from "./selectModeSession";
19
22
  import { symbolicateSelectedElement } from "./symbolicate";
20
23
  function EditorSelection() {
21
24
  const {
@@ -40,9 +43,13 @@ function EditorSelection() {
40
43
  if (session.type !== "element") {
41
44
  return;
42
45
  }
43
- const selectedElement = session.data.kind === "react-element" ? document.querySelector(session.data.selector) : null;
44
- const element = selectedElement ?? session.element;
45
- if (!(element instanceof Element) || !element.isConnected) {
46
+ const element = getConnectedSelectedElement(session);
47
+ if (!element) {
48
+ setSelectionType({ selectionType: "none" });
49
+ getWebsiteBuilderRpc()?.areaSelected({
50
+ rect: session.data.rect,
51
+ items: []
52
+ });
46
53
  return;
47
54
  }
48
55
  const siblingElements = getRepetitionSiblingElements({ element });
@@ -54,22 +61,24 @@ function EditorSelection() {
54
61
  if (descriptor.kind !== "react-element") {
55
62
  return;
56
63
  }
57
- interactionSessionRef.current = {
64
+ const refreshedSession = {
58
65
  ...session,
59
66
  element,
60
67
  siblingElements,
61
68
  data: descriptor
62
69
  };
70
+ interactionSessionRef.current = refreshedSession;
71
+ showElementSelectionFrames({
72
+ elements: [element, ...siblingElements],
73
+ scrollOffset: getDocumentScrollOffset()
74
+ });
63
75
  void symbolicateSelectedElement(descriptor).then((symbolicated) => {
64
- if (!isSelectionStillCurrent({
65
- session: interactionSessionRef.current,
66
- selection: { type: "element", element }
67
- })) {
76
+ if (interactionSessionRef.current !== refreshedSession) {
68
77
  return;
69
78
  }
70
79
  getWebsiteBuilderRpc()?.elementSelected({ data: symbolicated });
71
80
  });
72
- }, [interactionSessionRef]);
81
+ }, [interactionSessionRef, setSelectionType]);
73
82
  useEffect(() => {
74
83
  installFastRefreshRelay({
75
84
  onComplete: () => {
@@ -77,6 +86,17 @@ function EditorSelection() {
77
86
  postSelectedElementSnapshot();
78
87
  }
79
88
  });
89
+ window.addEventListener(
90
+ PREVIEW_RENDERED_EVENT,
91
+ postSelectedElementSnapshot
92
+ );
93
+ return () => {
94
+ window.removeEventListener(
95
+ PREVIEW_RENDERED_EVENT,
96
+ postSelectedElementSnapshot
97
+ );
98
+ installFastRefreshRelay();
99
+ };
80
100
  }, [editingElementRef, postSelectedElementSnapshot, reapplyOverrides]);
81
101
  useParentMessaging({
82
102
  applyOptimisticUpdate,
@@ -1,4 +1,5 @@
1
1
  import { getWebsiteBuilderRpc } from "./iframeRpcChannel";
2
+ import { invalidateSymbolicationCache } from "./symbolicate";
2
3
  let hasInstalledFastRefreshRelay = false;
3
4
  let onFastRefreshComplete = null;
4
5
  function installFastRefreshRelay({
@@ -13,6 +14,9 @@ function installFastRefreshRelay({
13
14
  const originalInfo = console.info;
14
15
  const relayIfFastRefreshComplete = (...args) => {
15
16
  const message = args.join(" ");
17
+ if (message.includes("[Fast Refresh] rebuilding") || message.includes("[Fast Refresh] done")) {
18
+ invalidateSymbolicationCache();
19
+ }
16
20
  if (message.includes("[Fast Refresh] done")) {
17
21
  getWebsiteBuilderRpc()?.fastRefreshComplete(void 0);
18
22
  onFastRefreshComplete?.();
@@ -308,7 +308,9 @@ function useSelectionInteractions({
308
308
  data
309
309
  };
310
310
  void maybeSymbolicate(data).then((symbolicated) => {
311
- if (!isSelectionStillCurrent({
311
+ const session = interactionSessionRef.current;
312
+ const isPendingClickOnSelection = session.type === "drag" && !session.hasExceededThreshold && committedSelectionAtPointerDown.length === 1 && committedSelectionAtPointerDown[0] === selectionElement;
313
+ if (!isPendingClickOnSelection && !isSelectionStillCurrent({
312
314
  session: interactionSessionRef.current,
313
315
  selection: { type: "element", element: selectionElement }
314
316
  })) {
@@ -0,0 +1 @@
1
+ export declare const PREVIEW_RENDERED_EVENT = "replo:preview-rendered";
@@ -0,0 +1,4 @@
1
+ const PREVIEW_RENDERED_EVENT = "replo:preview-rendered";
2
+ export {
3
+ PREVIEW_RENDERED_EVENT
4
+ };
@@ -1,2 +1,3 @@
1
1
  import type { SelectedElement } from "../_vendor/schemas/generated/selectMode";
2
+ export declare function invalidateSymbolicationCache(): void;
2
3
  export declare function symbolicateSelectedElement(selectedElement: SelectedElement): Promise<SelectedElement>;
@@ -66,7 +66,46 @@ function extractResolvedFrame(entry) {
66
66
  }
67
67
  return entry.value?.originalStackFrame ?? null;
68
68
  }
69
- async function postFrames({
69
+ const MAX_SYMBOLICATION_CACHE_ENTRIES = 128;
70
+ const symbolications = /* @__PURE__ */ new Map();
71
+ let symbolicationRevision = 0;
72
+ function invalidateSymbolicationCache() {
73
+ symbolications.clear();
74
+ symbolicationRevision += 1;
75
+ }
76
+ async function postFrames(request) {
77
+ if (request.frames.length === 0) {
78
+ return [];
79
+ }
80
+ const key = JSON.stringify(request);
81
+ const cached = symbolications.get(key);
82
+ if (cached) {
83
+ return cached;
84
+ }
85
+ const revision = symbolicationRevision;
86
+ const promise = fetchFrames(request).then((results) => {
87
+ if (revision !== symbolicationRevision) {
88
+ return request.frames.map(() => null);
89
+ }
90
+ const hasUnresolvedFrames = results.length !== request.frames.length || results.some((result, index) => {
91
+ const frame = extractResolvedFrame(result);
92
+ return !frame?.file || frame.file === request.frames[index]?.file;
93
+ });
94
+ if (hasUnresolvedFrames && symbolications.get(key) === promise) {
95
+ symbolications.delete(key);
96
+ }
97
+ return results;
98
+ });
99
+ if (symbolications.size >= MAX_SYMBOLICATION_CACHE_ENTRIES) {
100
+ const oldestKey = symbolications.keys().next().value;
101
+ if (oldestKey !== void 0) {
102
+ symbolications.delete(oldestKey);
103
+ }
104
+ }
105
+ symbolications.set(key, promise);
106
+ return promise;
107
+ }
108
+ async function fetchFrames({
70
109
  frames,
71
110
  isServer
72
111
  }) {
@@ -195,5 +234,6 @@ async function symbolicateSelectedElement(selectedElement) {
195
234
  };
196
235
  }
197
236
  export {
237
+ invalidateSymbolicationCache,
198
238
  symbolicateSelectedElement
199
239
  };