@agent-native/core 0.137.5 → 0.137.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.
Files changed (32) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/app/components/library/library-layout.tsx +95 -17
  3. package/corpus/templates/clips/app/components/library/space-card.tsx +136 -66
  4. package/corpus/templates/clips/app/components/library/space-dialogs.tsx +144 -0
  5. package/corpus/templates/clips/app/hooks/use-library.ts +2 -2
  6. package/corpus/templates/clips/app/i18n/en-US.ts +13 -0
  7. package/corpus/templates/clips/app/routes/_app.spaces._index.tsx +6 -2
  8. package/corpus/templates/clips/desktop/src/components/MediaDeviceRow.tsx +106 -87
  9. package/corpus/templates/clips/desktop/src/styles.css +19 -1
  10. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +7 -0
  11. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +103 -49
  12. package/corpus/templates/design/app/components/design/multi-screen/overview-layout.ts +4 -0
  13. package/corpus/templates/design/app/components/design/multi-screen/paint-suppression.ts +85 -0
  14. package/corpus/templates/design/changelog/2026-08-03-zoom-screen-blink.md +6 -0
  15. package/dist/agent/run-store.js +18 -0
  16. package/dist/client/AssistantChat.js +1 -0
  17. package/dist/client/chat/message-components.d.ts +10 -1
  18. package/dist/client/chat/message-components.js +13 -4
  19. package/dist/client/chat/runtime.js +20 -9
  20. package/dist/client/chat/tool-call-display.d.ts +7 -0
  21. package/dist/client/chat/tool-call-display.js +9 -1
  22. package/dist/client/sse-event-processor.d.ts +2 -0
  23. package/dist/client/sse-event-processor.js +39 -1
  24. package/dist/collab/awareness.d.ts +2 -2
  25. package/dist/collab/routes.d.ts +1 -1
  26. package/dist/collab/struct-routes.d.ts +1 -1
  27. package/dist/notifications/routes.d.ts +1 -1
  28. package/dist/progress/routes.d.ts +1 -1
  29. package/dist/resources/handlers.d.ts +1 -1
  30. package/dist/secrets/routes.d.ts +3 -3
  31. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  32. package/package.json +1 -1
@@ -104,7 +104,7 @@ export function MediaDeviceRow({
104
104
  const { open, setOpen, rowRef } = useRowMenu();
105
105
 
106
106
  const disabled = !on;
107
- const canOpenMenu = !disabled || (kind === "mic" && !!onSystemAudioToggle);
107
+ const canOpenMenu = on || (kind === "mic" && !!onSystemAudioToggle);
108
108
  const defaultLabel = kind === "camera" ? "Default camera" : "Default mic";
109
109
  const accessLabel =
110
110
  kind === "camera" ? "Allow camera access" : "Allow microphone access";
@@ -112,106 +112,125 @@ export function MediaDeviceRow({
112
112
  kind === "camera" ? "Refresh cameras" : "Refresh microphones";
113
113
 
114
114
  return (
115
- <div className={`row ${on ? "row-on" : "row-off"}`} ref={rowRef}>
116
- <span className="row-icon">
117
- <Icon />
118
- </span>
119
- <button
120
- type="button"
121
- className="row-button"
122
- onClick={() => {
123
- if (canOpenMenu) setOpen((v) => !v);
124
- }}
125
- disabled={!canOpenMenu}
126
- title={label}
127
- >
128
- <span className="row-label">{label}</span>
129
- {kind === "mic" && on ? (
130
- <MicWave deviceId={selectedId} active={on && meterActive} />
131
- ) : (
132
- <span className="row-flex" aria-hidden />
133
- )}
134
- <span className="row-chev" aria-hidden>
135
- <ChevronDown />
115
+ <div className="media-device-row" ref={rowRef}>
116
+ <div className={`row ${on ? "row-on" : "row-off"}`}>
117
+ <span className="row-icon">
118
+ <Icon />
136
119
  </span>
137
- </button>
138
- <Toggle
139
- on={on}
140
- onChange={(v) => {
141
- if (!v) setOpen(false);
142
- onToggle(v);
143
- }}
144
- label={kind === "camera" ? "Camera" : "Microphone"}
145
- />
146
- {open ? (
147
- <div className="row-menu" role="menu">
120
+ <button
121
+ type="button"
122
+ className="row-button"
123
+ onClick={() => setOpen((v) => !v)}
124
+ disabled={disabled}
125
+ title={label}
126
+ >
127
+ <span className="row-label">{label}</span>
128
+ {kind === "mic" && on ? (
129
+ <MicWave deviceId={selectedId} active={on && meterActive} />
130
+ ) : (
131
+ <span className="row-flex" aria-hidden />
132
+ )}
133
+ </button>
134
+ {canOpenMenu ? (
148
135
  <button
149
136
  type="button"
150
- className={`row-menu-item ${!selectedId ? "selected" : ""}`}
151
- role="menuitemradio"
152
- aria-checked={!selectedId}
153
- onClick={() => {
154
- onSelect("", "");
155
- setOpen(false);
156
- }}
137
+ className="row-menu-trigger"
138
+ onClick={() => setOpen((value) => !value)}
139
+ aria-label={on ? `Choose ${kind}` : "System audio settings"}
140
+ aria-expanded={open}
141
+ aria-haspopup="menu"
157
142
  >
158
- <span className="row-menu-check" aria-hidden>
159
- {!selectedId ? <CheckIcon /> : null}
160
- </span>
161
- <span className="row-menu-label">{defaultLabel}</span>
143
+ <ChevronDown />
162
144
  </button>
163
- {devices.length === 0 ? (
164
- <button
165
- type="button"
166
- className="row-menu-item row-menu-action"
167
- role="menuitem"
168
- onClick={() => {
169
- onRefresh();
170
- setOpen(false);
171
- }}
172
- >
173
- <span className="row-menu-check" aria-hidden />
174
- <span className="row-menu-label">{accessLabel}</span>
175
- </button>
176
- ) : (
145
+ ) : (
146
+ <span className="row-chev" aria-hidden>
147
+ <ChevronDown />
148
+ </span>
149
+ )}
150
+ <Toggle
151
+ on={on}
152
+ onChange={(v) => {
153
+ if (!v) setOpen(false);
154
+ onToggle(v);
155
+ }}
156
+ label={kind === "camera" ? "Camera" : "Microphone"}
157
+ />
158
+ </div>
159
+ {open && canOpenMenu ? (
160
+ <div className="row-menu" role="menu">
161
+ {on ? (
177
162
  <>
178
- {devices.map((d) => {
179
- const isSelected = !!selectedId && d.deviceId === selectedId;
180
- return (
181
- <button
182
- key={d.deviceId}
183
- type="button"
184
- className={`row-menu-item ${isSelected ? "selected" : ""}`}
185
- role="menuitemradio"
186
- aria-checked={isSelected}
187
- onClick={() => {
188
- onSelect(d.deviceId, d.label);
189
- setOpen(false);
190
- }}
191
- >
192
- <span className="row-menu-check" aria-hidden>
193
- {isSelected ? <CheckIcon /> : null}
194
- </span>
195
- <span className="row-menu-label">
196
- {d.label || (kind === "camera" ? "Camera" : "Microphone")}
197
- </span>
198
- </button>
199
- );
200
- })}
201
163
  <button
202
164
  type="button"
203
- className="row-menu-item row-menu-action"
204
- role="menuitem"
165
+ className={`row-menu-item ${!selectedId ? "selected" : ""}`}
166
+ role="menuitemradio"
167
+ aria-checked={!selectedId}
205
168
  onClick={() => {
206
- onRefresh();
169
+ onSelect("", "");
207
170
  setOpen(false);
208
171
  }}
209
172
  >
210
- <span className="row-menu-check" aria-hidden />
211
- <span className="row-menu-label">{refreshLabel}</span>
173
+ <span className="row-menu-check" aria-hidden>
174
+ {!selectedId ? <CheckIcon /> : null}
175
+ </span>
176
+ <span className="row-menu-label">{defaultLabel}</span>
212
177
  </button>
178
+ {devices.length === 0 ? (
179
+ <button
180
+ type="button"
181
+ className="row-menu-item row-menu-action"
182
+ role="menuitem"
183
+ onClick={() => {
184
+ onRefresh();
185
+ setOpen(false);
186
+ }}
187
+ >
188
+ <span className="row-menu-check" aria-hidden />
189
+ <span className="row-menu-label">{accessLabel}</span>
190
+ </button>
191
+ ) : (
192
+ <>
193
+ {devices.map((d) => {
194
+ const isSelected =
195
+ !!selectedId && d.deviceId === selectedId;
196
+ return (
197
+ <button
198
+ key={d.deviceId}
199
+ type="button"
200
+ className={`row-menu-item ${isSelected ? "selected" : ""}`}
201
+ role="menuitemradio"
202
+ aria-checked={isSelected}
203
+ onClick={() => {
204
+ onSelect(d.deviceId, d.label);
205
+ setOpen(false);
206
+ }}
207
+ >
208
+ <span className="row-menu-check" aria-hidden>
209
+ {isSelected ? <CheckIcon /> : null}
210
+ </span>
211
+ <span className="row-menu-label">
212
+ {d.label ||
213
+ (kind === "camera" ? "Camera" : "Microphone")}
214
+ </span>
215
+ </button>
216
+ );
217
+ })}
218
+ <button
219
+ type="button"
220
+ className="row-menu-item row-menu-action"
221
+ role="menuitem"
222
+ onClick={() => {
223
+ onRefresh();
224
+ setOpen(false);
225
+ }}
226
+ >
227
+ <span className="row-menu-check" aria-hidden />
228
+ <span className="row-menu-label">{refreshLabel}</span>
229
+ </button>
230
+ </>
231
+ )}
213
232
  </>
214
- )}
233
+ ) : null}
215
234
  {kind === "mic" && onSystemAudioToggle ? (
216
235
  <div className="row-menu-toggle">
217
236
  <span className="row-menu-toggle-label">Record System audio</span>
@@ -605,6 +605,10 @@ body[data-clips-route="recording-pill"] #root {
605
605
  }
606
606
 
607
607
  /* Popover menu rendered below a device row. */
608
+ .media-device-row {
609
+ position: relative;
610
+ }
611
+
608
612
  .row-menu {
609
613
  position: absolute;
610
614
  top: calc(100% + 4px);
@@ -645,7 +649,21 @@ body[data-clips-route="recording-pill"] #root {
645
649
  background: var(--surface-hover);
646
650
  }
647
651
 
648
- /* System-audio toggle sits below the device list, separated by a divider. */
652
+ .row-menu-trigger {
653
+ display: inline-flex;
654
+ align-items: center;
655
+ padding: 0;
656
+ border: none;
657
+ background: transparent;
658
+ color: var(--fg-muted);
659
+ cursor: pointer;
660
+ }
661
+
662
+ .row-menu-trigger:focus-visible {
663
+ box-shadow: 0 0 0 2px var(--brand-ring);
664
+ border-radius: 4px;
665
+ }
666
+
649
667
  .row-menu-toggle {
650
668
  display: flex;
651
669
  align-items: center;
@@ -1293,6 +1293,10 @@ export function DesignCanvas({
1293
1293
  [probeBridgeReadinessUntilDrained],
1294
1294
  );
1295
1295
  const [renderedContent, setRenderedContent] = useState(content);
1296
+ // What a freshly loaded document already contains, since srcdoc is built from
1297
+ // it. The load handler below needs this to skip redundant pushes.
1298
+ const renderedContentRef = useRef(renderedContent);
1299
+ renderedContentRef.current = renderedContent;
1296
1300
  // True while a drawing send is capturing/compositing/uploading the
1297
1301
  // annotated screenshot (see design-canvas/annotation-snapshot.ts). Drives
1298
1302
  // SharedDrawOverlay's busy Send state so a slow capture can't be triggered
@@ -4043,6 +4047,9 @@ export function DesignCanvas({
4043
4047
  const replaceLatestRuntimeContent = () => {
4044
4048
  const nextContent = runtimeReplacementContentRef.current;
4045
4049
  if (nextContent === undefined) return;
4050
+ // The document that just loaded was built from these bytes; swapping it
4051
+ // for itself only costs a blank frame.
4052
+ if (renderedContentRef.current === nextContent) return;
4046
4053
  if (replaceRuntimeContentInPlace(nextContent)) {
4047
4054
  lastRuntimeReplacementKeyRef.current = runtimeReplacementKeyRef.current;
4048
4055
  lastRuntimeReplacementContentRef.current = nextContent;
@@ -244,7 +244,10 @@ import {
244
244
  sameResolvedMetadata,
245
245
  type ResolvedMetadataCacheEntry,
246
246
  } from "./multi-screen/screen-content-cache";
247
- import { setWheelCameraGestureActive } from "./multi-screen/wheel-gesture-state";
247
+ import {
248
+ isWheelCameraGestureActive,
249
+ setWheelCameraGestureActive,
250
+ } from "./multi-screen/wheel-gesture-state";
248
251
 
249
252
  // Figma parity: a plain click (no drag) with the rectangle or ellipse tool
250
253
  // places a 100x100 shape. Both tools share this default via the "else"
@@ -340,6 +343,13 @@ import {
340
343
  screenPxToCanvasPx,
341
344
  stopPercentFromDraggedPoint,
342
345
  } from "./multi-screen/gradient-overlay-geometry";
346
+ import {
347
+ applyScreenPaintSuppression,
348
+ collectScreenPaintTargets,
349
+ resolveSuppressedScreenIds,
350
+ type ScreenPaintCandidate,
351
+ type ScreenPaintTarget,
352
+ } from "./multi-screen/paint-suppression";
343
353
  import {
344
354
  getPrimitiveDropTargetForPoint,
345
355
  getPrimitiveLowZoomHitRect,
@@ -651,26 +661,33 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
651
661
  : null,
652
662
  [boardFrameGeometry],
653
663
  );
664
+ // Both board layers must ask this one question: an empty <body> is a truthy
665
+ // string, and the replica is opaque, so a string-only gate slabs the board.
666
+ const boardSurfaceHtml = hasBoardSurfaceContent(boardFileContent)
667
+ ? boardFileContent
668
+ : undefined;
669
+ const boardHasSurfaceContent = boardSurfaceHtml !== undefined;
654
670
  const boardStaticPreviewContent = useMemo(() => {
655
671
  if (
656
672
  !boardFrameGeometry ||
657
673
  !boardStaticPreviewViewport ||
658
- !boardFileContent
674
+ !boardSurfaceHtml
659
675
  ) {
660
676
  return null;
661
677
  }
662
678
  return getBoardSurfaceStaticPreviewContent({
663
- html: boardFileContent,
679
+ html: boardSurfaceHtml,
664
680
  logicalGeometry: boardFrameGeometry,
665
681
  viewport: boardStaticPreviewViewport,
666
682
  });
667
- }, [boardFileContent, boardFrameGeometry, boardStaticPreviewViewport]);
683
+ }, [boardSurfaceHtml, boardFrameGeometry, boardStaticPreviewViewport]);
668
684
  const showBoardStaticPreview = Boolean(
669
685
  boardFrameGeometry &&
670
686
  boardSurfaceRenderGeometry &&
671
687
  boardStaticPreviewContent &&
672
688
  shouldRenderBoardSurfaceStaticPreview({
673
689
  zoom: canvasZoom,
690
+ hasSurfaceContent: boardHasSurfaceContent,
674
691
  viewportGeometry: boardViewportGeometry,
675
692
  renderGeometry: boardSurfaceRenderGeometry,
676
693
  }),
@@ -960,6 +977,12 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
960
977
  const liveScreenIdsRef = useRef<Set<string>>(new Set());
961
978
  const lastVisibleEpochByScreenIdRef = useRef<Map<string, number>>(new Map());
962
979
  const cullAccessEpochRef = useRef(0);
980
+ const wheelGestureFilteredIframesRef = useRef<HTMLElement[]>([]);
981
+ // Paint suppression is resolved imperatively against the live camera, so its
982
+ // inputs live in refs readable from applyViewToDom's render-free tick.
983
+ const screenPaintCandidatesRef = useRef<ScreenPaintCandidate[]>([]);
984
+ const screenPaintTargetsRef = useRef<ScreenPaintTarget[]>([]);
985
+ const surfaceSizeRef = useRef({ width: 0, height: 0 });
963
986
  useEffect(() => {
964
987
  const liveScreenIds = new Set(screens.map((screen) => screen.id));
965
988
  for (const id of hasBeenVisibleScreenIdsRef.current) {
@@ -992,6 +1015,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
992
1015
  const surface = surfaceRef.current;
993
1016
  if (!surface) return;
994
1017
  const updateSize = (width: number, height: number) => {
1018
+ surfaceSizeRef.current = { width, height };
995
1019
  setSurfaceSize((current) =>
996
1020
  current.width === width && current.height === height
997
1021
  ? current
@@ -6643,6 +6667,24 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
6643
6667
  [activeTool, localActiveTool, updateAltHoverMeasurement, updatePenPointer],
6644
6668
  );
6645
6669
 
6670
+ // Overscan is deliberately excluded: this answers "is it on screen right
6671
+ // now", and anything off screen costs nothing to leave unpainted.
6672
+ const syncScreenPaintSuppression = useCallback(() => {
6673
+ applyScreenPaintSuppression(
6674
+ screenPaintTargetsRef.current,
6675
+ resolveSuppressedScreenIds(
6676
+ screenPaintCandidatesRef.current,
6677
+ getOverscannedViewportCanvasBounds(
6678
+ surfaceSizeRef.current,
6679
+ panRef.current,
6680
+ zoomRef.current,
6681
+ 0,
6682
+ ),
6683
+ ),
6684
+ { relaxOnly: isWheelCameraGestureActive() },
6685
+ );
6686
+ }, []);
6687
+
6646
6688
  // Push the current pan/zoom straight to the DOM. A wheel/pinch gesture must
6647
6689
  // NEVER re-render React's canvas tree during the gesture: each render re-runs
6648
6690
  // renderScreenContent (which re-creates the active screen's live DesignCanvas
@@ -6688,7 +6730,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
6688
6730
  marqueeOverlay.style.width = `${Math.max(1, activeMarquee.width * nextScale)}px`;
6689
6731
  marqueeOverlay.style.height = `${Math.max(1, activeMarquee.height * nextScale)}px`;
6690
6732
  }
6691
- }, []);
6733
+ // Same tick as the transform: a screen this move brings on screen must
6734
+ // paint in the frame it becomes visible, not at the debounced commit.
6735
+ syncScreenPaintSuppression();
6736
+ }, [syncScreenPaintSuppression]);
6692
6737
 
6693
6738
  const startChromeSettle = useCallback(() => {
6694
6739
  if (chromeSettleTimerRef.current !== null) {
@@ -6714,6 +6759,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
6714
6759
  if (wheelGestureActiveRef.current) {
6715
6760
  wheelGestureActiveRef.current = false;
6716
6761
  setWheelCameraGestureActive(false);
6762
+ wheelGestureFilteredIframesRef.current.forEach((iframe) => {
6763
+ if (iframe.isConnected) iframe.style.filter = "";
6764
+ });
6765
+ wheelGestureFilteredIframesRef.current = [];
6717
6766
  const muted = wheelGestureMutedElementsRef.current;
6718
6767
  wheelGestureMutedElementsRef.current = null;
6719
6768
  if (muted) {
@@ -6864,6 +6913,18 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
6864
6913
  element.style.pointerEvents = "none";
6865
6914
  });
6866
6915
  wheelGestureMutedElementsRef.current = muted;
6916
+ // A nested frame re-rasters on every scale change no matter what layer the
6917
+ // canvas gets, starving the renderer. A filter — a no-op at this radius —
6918
+ // gives each one a surface the compositor scales from cache. commitView must
6919
+ // clear it, or they stay soft at rest.
6920
+ const filtered: HTMLElement[] = [];
6921
+ surface
6922
+ .querySelectorAll<HTMLElement>("[data-screen-content] iframe")
6923
+ .forEach((iframe) => {
6924
+ iframe.style.filter = "blur(0.001px)";
6925
+ filtered.push(iframe);
6926
+ });
6927
+ wheelGestureFilteredIframesRef.current = filtered;
6867
6928
  }, [cancelPendingStaticBoardSelection]);
6868
6929
 
6869
6930
  const flushPendingWheelGesture = useCallback(() => {
@@ -7615,23 +7676,23 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
7615
7676
  pan,
7616
7677
  canvasZoom,
7617
7678
  );
7679
+ const candidates = canvasFrames.map(({ screen, metadata, geometry }) => ({
7680
+ id: screen.id,
7681
+ geometry: getResponsiveScreenCullGeometry(
7682
+ screen,
7683
+ geometry,
7684
+ (widthPx) =>
7685
+ measuredIframeHeights[getBreakpointIframeId(screen.id, widthPx)],
7686
+ ),
7687
+ // Count only the breakpoint frames actually mounted (the row filters
7688
+ // duplicates of the device width) so the iframe budget isn't
7689
+ // over-consumed, prematurely evicting visible frames.
7690
+ iframeCount:
7691
+ 1 +
7692
+ visibleBreakpointWidths(screen.breakpointWidths, metadata.width).length,
7693
+ }));
7618
7694
  const next = computeBoundedScreenCullState({
7619
- candidates: canvasFrames.map(({ screen, metadata, geometry }) => ({
7620
- id: screen.id,
7621
- geometry: getResponsiveScreenCullGeometry(
7622
- screen,
7623
- geometry,
7624
- (widthPx) =>
7625
- measuredIframeHeights[getBreakpointIframeId(screen.id, widthPx)],
7626
- ),
7627
- // Count only the breakpoint frames actually mounted (the row filters
7628
- // duplicates of the device width) so the iframe budget isn't
7629
- // over-consumed, prematurely evicting visible frames.
7630
- iframeCount:
7631
- 1 +
7632
- visibleBreakpointWidths(screen.breakpointWidths, metadata.width)
7633
- .length,
7634
- })),
7695
+ candidates,
7635
7696
  viewport,
7636
7697
  protectedScreenIds: protectedLiveScreenIds,
7637
7698
  previousLiveScreenIds: liveScreenIdsRef.current,
@@ -7642,6 +7703,11 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
7642
7703
  liveScreenIdsRef.current = next.liveScreenIds;
7643
7704
  hasBeenVisibleScreenIdsRef.current = next.everVisibleScreenIds;
7644
7705
  lastVisibleEpochByScreenIdRef.current = next.lastVisibleEpochByScreenId;
7706
+ screenPaintCandidatesRef.current = candidates.map(({ id, geometry }) => ({
7707
+ id,
7708
+ geometry,
7709
+ tier: next.tierByScreenId.get(id) ?? "visible",
7710
+ }));
7645
7711
  return next.tierByScreenId;
7646
7712
  }, [
7647
7713
  canvasFrames,
@@ -7651,6 +7717,16 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
7651
7717
  protectedLiveScreenIds,
7652
7718
  surfaceSize,
7653
7719
  ]);
7720
+ // No dependency array on purpose: any render can mount, unmount, or reorder a
7721
+ // screen's content wrapper, and a wrapper React just mounted carries none of
7722
+ // the suppression state this owns.
7723
+ useEffect(() => {
7724
+ screenPaintTargetsRef.current = collectScreenPaintTargets(
7725
+ surfaceRef.current,
7726
+ );
7727
+ syncScreenPaintSuppression();
7728
+ });
7729
+
7654
7730
  const topScreenId = useMemo(
7655
7731
  () =>
7656
7732
  selectedIds.find((id) =>
@@ -7897,7 +7973,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
7897
7973
 
7898
7974
  {boardFileId &&
7899
7975
  boardFileContent !== undefined &&
7900
- hasBoardSurfaceContent(boardFileContent) &&
7976
+ boardHasSurfaceContent &&
7901
7977
  (() => {
7902
7978
  const boardGeo = boardSurfaceRenderGeometry ?? {
7903
7979
  x: 0,
@@ -9448,18 +9524,10 @@ const Screen = memo(function Screen({
9448
9524
  const previewUrl = metadata.previewUrl ?? getPreviewUrl(screen.content);
9449
9525
  const previewViewport = getScreenPreviewViewport(metadata, geometry);
9450
9526
  const suppressNextClick = useRef(false);
9451
- // Overview viewport culling (PF22): a "culled" screen keeps its content
9452
- // (iframe/DesignCanvas) fully mounted unmounting would lose all internal
9453
- // iframe state (scroll position, form input, in-progress Alpine/JS state)
9454
- // but skips paint/layout cost for it via visibility:hidden +
9455
- // contentVisibility:"hidden" on the content wrapper. Deliberately NOT
9456
- // display:none (can drop layout/scroll state on some engines) and NOT
9457
- // will-change (a prior perf attempt using will-change on this same overview
9458
- // caused permanent blur by pinning a low-res compositor layer — see the
9459
- // world-transform comment near applyViewToDom). "placeholder" and
9460
- // "evicted" tiers have no mounted browsing context, so they render the
9461
- // chrome-only placeholder below regardless of this flag.
9462
- const { shouldMount: shouldMountContent, isHidden: isCulled } =
9527
+ // Overview viewport culling (PF22): mounting only. Unmounting a culled screen
9528
+ // would lose its iframe's scroll/form/Alpine state, so it stays mounted and
9529
+ // paint-suppression.ts decides paint against the live camera instead.
9530
+ const { shouldMount: shouldMountContent } =
9463
9531
  getScreenContentCullState(cullTier);
9464
9532
  const [directlyHovered, setDirectlyHovered] = useState(false);
9465
9533
  const frameDirectlyHovered =
@@ -9786,16 +9854,6 @@ const Screen = memo(function Screen({
9786
9854
  )}
9787
9855
  style={{
9788
9856
  pointerEvents: screenContentInteractive ? "auto" : "none",
9789
- // Tier B (PF22): keep the mounted iframe/DesignCanvas alive (see
9790
- // the isCulled comment above) but skip its paint/layout cost.
9791
- // visibility:hidden (not display:none) keeps the box in the
9792
- // layout/measurement tree; contentVisibility:"hidden" skips
9793
- // rendering its subtree entirely until it's shown again. Neither
9794
- // property touches will-change/compositor layers, so this can't
9795
- // reproduce the permanent-blur regression a prior will-change
9796
- // attempt on this same surface caused.
9797
- visibility: isCulled ? "hidden" : undefined,
9798
- contentVisibility: isCulled ? "hidden" : undefined,
9799
9857
  }}
9800
9858
  >
9801
9859
  {!shouldMountContent ? (
@@ -10167,7 +10225,7 @@ function BreakpointPreviewRow({
10167
10225
  // draft value of its width input.
10168
10226
  const [menuOpenForWidth, setMenuOpenForWidth] = useState<number | null>(null);
10169
10227
  const [widthDraft, setWidthDraft] = useState("");
10170
- const { shouldMount: shouldMountContent, isHidden: isCulled } =
10228
+ const { shouldMount: shouldMountContent } =
10171
10229
  getScreenContentCullState(cullTier);
10172
10230
 
10173
10231
  return (
@@ -10476,10 +10534,6 @@ function BreakpointPreviewRow({
10476
10534
  data-screen-content
10477
10535
  data-cull-tier={cullTier}
10478
10536
  className="relative block h-full w-full overflow-hidden rounded-[inherit] bg-white ring-1 ring-inset ring-border"
10479
- style={{
10480
- visibility: isCulled ? "hidden" : undefined,
10481
- contentVisibility: isCulled ? "hidden" : undefined,
10482
- }}
10483
10537
  >
10484
10538
  {!shouldMountContent ? (
10485
10539
  <div
@@ -96,9 +96,13 @@ export function getBoardSurfaceLayerStyle(args: {
96
96
  */
97
97
  export function shouldRenderBoardSurfaceStaticPreview(args: {
98
98
  zoom: number;
99
+ hasSurfaceContent: boolean;
99
100
  viewportGeometry?: FrameGeometry | null;
100
101
  renderGeometry: FrameGeometry;
101
102
  }) {
103
+ // The replica is opaque. Backing a layer that is not rendering just slabs the
104
+ // board in its own colour, which reads as a themed background gone wrong.
105
+ if (!args.hasSurfaceContent) return false;
102
106
  if (args.viewportGeometry) {
103
107
  return (
104
108
  args.viewportGeometry.width > args.renderGeometry.width ||
@@ -0,0 +1,85 @@
1
+ import {
2
+ getScreenContentCullState,
3
+ isFrameWithinOverscannedViewport,
4
+ type OverscannedViewportBounds,
5
+ type ScreenCullTier,
6
+ } from "./culling";
7
+ import type { FrameGeometry } from "./types";
8
+
9
+ // Zoom scales the world viewport rather than translating it, so no overscan
10
+ // covers a gesture's lag. React must never also write visibility here: its style
11
+ // diff would stop re-applying a value this owner has since cleared.
12
+
13
+ /** Lets a freshly mounted wrapper still be written while an unchanged one is
14
+ * skipped. */
15
+ const SUPPRESSED_MARKER_ATTRIBUTE = "screenPaintSuppressed";
16
+
17
+ export interface ScreenPaintCandidate {
18
+ id: string;
19
+ geometry: FrameGeometry;
20
+ tier: ScreenCullTier;
21
+ }
22
+
23
+ export interface ScreenPaintTarget {
24
+ element: HTMLElement;
25
+ screenId: string;
26
+ }
27
+
28
+ /** Every content wrapper belonging to a screen, including its breakpoint
29
+ * previews — paint is decided per screen, so the whole shell moves together. */
30
+ export function collectScreenPaintTargets(
31
+ surface: HTMLElement | null,
32
+ ): ScreenPaintTarget[] {
33
+ if (!surface) return [];
34
+ const targets: ScreenPaintTarget[] = [];
35
+ surface
36
+ .querySelectorAll<HTMLElement>("[data-screen-content]")
37
+ .forEach((element) => {
38
+ const screenId = element.closest<HTMLElement>("[data-screen-shell]")
39
+ ?.dataset.frameId;
40
+ if (screenId) targets.push({ element, screenId });
41
+ });
42
+ return targets;
43
+ }
44
+
45
+ /** Screens whose paint the browser may skip for the camera passed in. A
46
+ * `liveViewport` of `null` (surface not measured yet) suppresses nothing. */
47
+ export function resolveSuppressedScreenIds(
48
+ candidates: readonly ScreenPaintCandidate[],
49
+ liveViewport: OverscannedViewportBounds | null,
50
+ ): Set<string> {
51
+ const suppressed = new Set<string>();
52
+ if (!liveViewport) return suppressed;
53
+ for (const candidate of candidates) {
54
+ if (!getScreenContentCullState(candidate.tier).isHidden) continue;
55
+ if (isFrameWithinOverscannedViewport(candidate.geometry, liveViewport)) {
56
+ continue;
57
+ }
58
+ suppressed.add(candidate.id);
59
+ }
60
+ return suppressed;
61
+ }
62
+
63
+ export function applyScreenPaintSuppression(
64
+ targets: readonly ScreenPaintTarget[],
65
+ suppressedScreenIds: ReadonlySet<string>,
66
+ options?: { relaxOnly?: boolean },
67
+ ): void {
68
+ for (const { element, screenId } of targets) {
69
+ const suppress = suppressedScreenIds.has(screenId);
70
+ const applied = element.dataset[SUPPRESSED_MARKER_ATTRIBUTE] === "true";
71
+ if (applied === suppress) continue;
72
+ // Mid-gesture, only relax: hiding a screen the camera is still crossing
73
+ // guarantees an unpainted frame when it returns.
74
+ if (options?.relaxOnly && suppress) continue;
75
+ if (suppress) {
76
+ // Never content-visibility: it discards the subtree's rendering state, so
77
+ // un-hiding blanks the iframe for a frame.
78
+ element.style.setProperty("visibility", "hidden");
79
+ element.dataset[SUPPRESSED_MARKER_ATTRIBUTE] = "true";
80
+ } else {
81
+ element.style.removeProperty("visibility");
82
+ delete element.dataset[SUPPRESSED_MARKER_ATTRIBUTE];
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-03
4
+ ---
5
+
6
+ Screens no longer blink out and back in while zooming a multi-screen canvas.