@agent-native/core 0.175.2-nightly-20260826150235 → 0.175.3

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 (64) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/app/components/player/comments-panel.tsx +1 -1
  3. package/corpus/templates/clips/app/components/player/video-player.tsx +5 -3
  4. package/corpus/templates/clips/desktop/src/app.tsx +171 -51
  5. package/corpus/templates/clips/desktop/src/lib/pill-completion.ts +111 -0
  6. package/corpus/templates/clips/desktop/src/lib/pill-session.ts +34 -0
  7. package/corpus/templates/clips/desktop/src/lib/recorder.ts +181 -79
  8. package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +28 -4
  9. package/corpus/templates/clips/desktop/src/main.tsx +4 -4
  10. package/corpus/templates/clips/desktop/src/overlays/record-pill.tsx +1320 -0
  11. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +1 -1
  12. package/corpus/templates/clips/desktop/src/styles.css +20 -308
  13. package/corpus/templates/clips/desktop/src/tailwind.css +85 -0
  14. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +153 -70
  15. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +15 -0
  16. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +25 -7
  17. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +3 -3
  18. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +375 -24
  19. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +22 -30
  20. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +5 -0
  21. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +134 -14
  22. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +233 -4
  23. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +8 -5
  24. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +1 -1
  25. package/corpus/templates/design/.generated/bridge/embedded-wheel.generated.ts +3 -2
  26. package/corpus/templates/design/.generated/bridge/zoom.generated.ts +8 -2
  27. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +46 -10
  28. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +89 -63
  29. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -1
  30. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +8 -2
  31. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +11 -2
  32. package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -4
  33. package/corpus/templates/design/app/components/design/multi-screen/zoom-gesture.ts +19 -63
  34. package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +2 -1
  35. package/corpus/templates/design/shared/canvas-math.ts +11 -1
  36. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +44 -18
  37. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +88 -23
  38. package/corpus/templates/slides/app/context/DeckContext.tsx +155 -86
  39. package/corpus/templates/slides/app/pages/DeckEditor.tsx +7 -5
  40. package/corpus/templates/slides/changelog/2026-08-26-thumbnail-arrow-keys-now-navigate-slides-after-selection.md +6 -0
  41. package/dist/client/hooks/index.d.ts +1 -0
  42. package/dist/client/hooks/index.js +1 -0
  43. package/dist/client/use-pinch-zoom.js +17 -4
  44. package/dist/client/zoom-gesture.d.ts +74 -0
  45. package/dist/client/zoom-gesture.js +100 -0
  46. package/dist/collab/routes.d.ts +1 -1
  47. package/dist/collab/struct-routes.d.ts +1 -1
  48. package/dist/observability/routes.d.ts +3 -3
  49. package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
  50. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  51. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  52. package/dist/server/builder-browser.d.ts +13 -13
  53. package/dist/server/builder-browser.js +46 -15
  54. package/dist/server/builder-oauth.d.ts +12 -0
  55. package/dist/server/builder-oauth.js +13 -2
  56. package/dist/server/core-routes-plugin.js +54 -19
  57. package/dist/server/realtime-token.d.ts +1 -1
  58. package/dist/server/transcribe-voice.d.ts +1 -1
  59. package/dist/vite/client.js +2 -0
  60. package/docs/content/template-chat-developers.mdx +100 -13
  61. package/docs/content/template-chat-first-edits.mdx +5 -5
  62. package/docs/content/template-chat.mdx +84 -36
  63. package/package.json +4 -3
  64. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +0 -458
@@ -5,6 +5,13 @@ import {
5
5
  } from "@agent-native/core/client/host";
6
6
  import { useT } from "@agent-native/core/client/i18n";
7
7
  import { type ReviewThread } from "@agent-native/core/client/review";
8
+ import {
9
+ clampZoomFactor,
10
+ normalizeWheelDeltaPx,
11
+ resolveZoomGestureDevice,
12
+ zoomFactorForWheelDelta,
13
+ type ZoomGestureDevice,
14
+ } from "@agent-native/core/client/zoom-gesture";
8
15
  import type { ReviewComment } from "@agent-native/core/review";
9
16
  import { injectDocumentMarkup } from "@agent-native/core/shared";
10
17
  import { isLoopbackPreviewAllowed } from "@shared/builder-preview-url";
@@ -1239,6 +1246,7 @@ export function DesignCanvas({
1239
1246
  // Queue them here until ready fires (or the iframe finishes loading, as a
1240
1247
  // fallback for older/interact-mode documents that never inject the chrome
1241
1248
  // bridge and thus never post ready) and flush in order.
1249
+ const pinchZoomDeviceRef = useRef<ZoomGestureDevice | null>(null);
1242
1250
  const bridgeReadyRef = useRef(false);
1243
1251
  const [readyIframeDocumentIdentity, setReadyIframeDocumentIdentity] =
1244
1252
  useState<string | null>(null);
@@ -1517,6 +1525,19 @@ export function DesignCanvas({
1517
1525
  .replace("__EDITING_SAFETY_ENABLED__", interactMode ? "false" : "true"),
1518
1526
  [interactMode],
1519
1527
  );
1528
+ // srcdoc is rebuilt per document, so unlike the keyed live-edit bundle above
1529
+ // it can carry the real first-paint value: forwarding that arrives only by
1530
+ // postMessage stays dead until the handshake, and is stranded by a swap.
1531
+ const embeddedGestureBridgeForSrcdoc = useMemo(
1532
+ () =>
1533
+ EMBEDDED_WHEEL_BRIDGE_SCRIPT.replace(
1534
+ "__EMBEDDED_WHEEL_FORWARDING_ENABLED__",
1535
+ isEmbeddedFrame && !interactMode ? "true" : "false",
1536
+ )
1537
+ .replace("__EMBEDDED_SPACE_KEY_FORWARDING_ENABLED__", "false")
1538
+ .replace("__EDITING_SAFETY_ENABLED__", interactMode ? "false" : "true"),
1539
+ [interactMode, isEmbeddedFrame],
1540
+ );
1520
1541
  const includeLiveEditEditorChrome = !interactMode && !readOnly;
1521
1542
  const liveEditBridgeScript = useMemo(
1522
1543
  () =>
@@ -2420,7 +2441,7 @@ export function DesignCanvas({
2420
2441
  ZOOM_BRIDGE_SCRIPT +
2421
2442
  NAV_BRIDGE_SCRIPT +
2422
2443
  LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT +
2423
- embeddedGestureBridgeForCurrentState +
2444
+ embeddedGestureBridgeForSrcdoc +
2424
2445
  editorChromeBridge +
2425
2446
  imageDiagBridge;
2426
2447
  const frameContent = getEmbeddedFrameDocumentContent({
@@ -2468,7 +2489,7 @@ export function DesignCanvas({
2468
2489
  interactMode,
2469
2490
  isEmbeddedFrame,
2470
2491
  embeddedFrameBackground,
2471
- embeddedGestureBridgeForCurrentState,
2492
+ embeddedGestureBridgeForSrcdoc,
2472
2493
  iframeRenderContent,
2473
2494
  transparentBackground,
2474
2495
  ]);
@@ -3147,6 +3168,10 @@ export function DesignCanvas({
3147
3168
  return;
3148
3169
  }
3149
3170
  if (e.data.type === "pinch-zoom-wheel") {
3171
+ // An embedded frame's zoom belongs to the parent canvas, which already
3172
+ // gets the same gesture as embedded-canvas-wheel. Both bridges are
3173
+ // installed in every document, so without this the two apply twice.
3174
+ if (isEmbeddedFrame) return;
3150
3175
  if (!onZoomChange) return;
3151
3176
  const iframe = iframeRef.current;
3152
3177
  const scroll = scrollContainerRef.current;
@@ -3158,15 +3183,26 @@ export function DesignCanvas({
3158
3183
  // handler above applies to its own wheel deltas/coordinates).
3159
3184
  const rawDeltaY = Number(e.data.deltaY);
3160
3185
  if (!Number.isFinite(rawDeltaY)) return;
3161
- // Mirror usePinchZoom's algorithm here. We can't reliably re-dispatch
3162
- // a synthetic WheelEvent to trigger the hook's listener — untrusted
3163
- // events are inconsistent across browsers — so just compute the
3164
- // next zoom directly using the same exponential factor + cursor-anchor
3165
- // math. Clamp range matches the usePinchZoom call above
3166
- // (DEFAULT_CANVAS_MIN_ZOOM–DEFAULT_CANVAS_MAX_ZOOM).
3186
+ // A synthetic WheelEvent cannot reliably reach usePinchZoom's listener,
3187
+ // so this path computes the factor itself — from the same module, never
3188
+ // re-derived by hand, or the two curves drift apart again.
3189
+ const forwardedMode = Number(e.data.deltaMode);
3190
+ const deltaMode = Number.isFinite(forwardedMode) ? forwardedMode : 0;
3191
+ pinchZoomDeviceRef.current = resolveZoomGestureDevice({
3192
+ deltaY: rawDeltaY,
3193
+ deltaMode,
3194
+ ctrlKey: Boolean(e.data.ctrlKey),
3195
+ metaKey: Boolean(e.data.metaKey),
3196
+ atMs: performance.now(),
3197
+ previous: pinchZoomDeviceRef.current,
3198
+ });
3167
3199
  const currentZoom = zoomRef.current;
3168
- const clampedDelta = Math.max(-50, Math.min(50, rawDeltaY));
3169
- const factor = Math.exp(-clampedDelta * 0.01);
3200
+ const factor = clampZoomFactor(
3201
+ zoomFactorForWheelDelta(
3202
+ normalizeWheelDeltaPx(rawDeltaY, deltaMode),
3203
+ pinchZoomDeviceRef.current.pinch,
3204
+ ),
3205
+ );
3170
3206
  const nextZoom = Math.max(
3171
3207
  DEFAULT_CANVAS_MIN_ZOOM,
3172
3208
  Math.min(DEFAULT_CANVAS_MAX_ZOOM, currentZoom * factor),
@@ -4,7 +4,9 @@ import {
4
4
  } from "@agent-native/core/client/host";
5
5
  import { useT } from "@agent-native/core/client/i18n";
6
6
  import {
7
+ CANVAS_FIT_PADDING_PX,
7
8
  DEFAULT_CANVAS_MAX_ZOOM,
9
+ DEFAULT_CANVAS_AUTOFIT_MIN_ZOOM,
8
10
  DEFAULT_CANVAS_MIN_ZOOM,
9
11
  DEFAULT_SNAP_THRESHOLD_SCREEN_PX,
10
12
  canvasToScreenPoint,
@@ -178,7 +180,10 @@ const EMPTY_SCREEN_IDS: readonly string[] = [];
178
180
  // redeclared locally and drifting from the shared constant.
179
181
  const MIN_ZOOM = DEFAULT_CANVAS_MIN_ZOOM;
180
182
  const MAX_ZOOM = DEFAULT_CANVAS_MAX_ZOOM;
181
- const MAX_WHEEL_PAN_DELTA = 140;
183
+ const AUTOFIT_MIN_ZOOM = DEFAULT_CANVAS_AUTOFIT_MIN_ZOOM;
184
+ // Must match embedded-wheel.bridge.ts's payload bound, or every fast swipe
185
+ // arriving over a screen loses its tail to the tighter of the two clamps.
186
+ const MAX_WHEEL_PAN_DELTA = 240;
182
187
  /** Live counter-scale for constant-size frame chrome, written on the world
183
188
  * element every gesture frame by applyViewToDom. React supplies the same
184
189
  * number as the var's fallback, so first paint and SSR are unaffected. */
@@ -379,9 +384,12 @@ import {
379
384
  import type { AlignmentGuide, CanvasFrameEntry } from "./multi-screen/types";
380
385
  import { vectorEditCanvasToLocalPoint } from "./multi-screen/vector-edit-geometry";
381
386
  import {
382
- isPinchZoomDelta,
387
+ accumulateZoomFactor,
388
+ clampZoomFactor,
389
+ normalizeWheelDeltaPx,
383
390
  resolveExternalZoomAnchor,
384
- resolveZoomFactor,
391
+ resolveZoomGestureDevice,
392
+ type ZoomGestureDevice,
385
393
  } from "./multi-screen/zoom-gesture";
386
394
 
387
395
  /**
@@ -1003,7 +1011,18 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
1003
1011
  * reply overwrites the selection the user actually made. */
1004
1012
  const drillInRequestRef = useRef(0);
1005
1013
  const feedbackTimerRef = useRef<number | null>(null);
1006
- const pendingWheelGestureRef = useRef<PendingWheelGesture | null>(null);
1014
+ // Two slots, not one: Cmd pressed or released mid-scroll turns a single
1015
+ // physical stream into interleaved zoom and pan ticks, and a shared slot
1016
+ // drops whichever kind the other overwrote.
1017
+ const pendingZoomGestureRef = useRef<Extract<
1018
+ PendingWheelGesture,
1019
+ { mode: "zoom" }
1020
+ > | null>(null);
1021
+ const pendingPanGestureRef = useRef<Extract<
1022
+ PendingWheelGesture,
1023
+ { mode: "pan" }
1024
+ > | null>(null);
1025
+ const zoomGestureDeviceRef = useRef<ZoomGestureDevice | null>(null);
1007
1026
  const wheelGestureFrameRef = useRef<number | null>(null);
1008
1027
  const worldRef = useRef<HTMLDivElement>(null);
1009
1028
  const pixelGridRef = useRef<HTMLDivElement>(null);
@@ -1649,12 +1668,15 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
1649
1668
  const boundsTop = bounds?.top ?? 0;
1650
1669
  // Leave a Figma-like board gutter beside the last frame for quick drops/draws,
1651
1670
  // and fit tall single frames so lower canvas interactions remain reachable.
1671
+ const minFitScale = AUTOFIT_MIN_ZOOM / 100;
1652
1672
  const widthFitScale =
1653
1673
  renderedScreens.length > 1 && totalWidth > 0
1654
- ? Math.max(0.1, (rect.width - 180) / totalWidth)
1674
+ ? Math.max(minFitScale, (rect.width - 180) / totalWidth)
1655
1675
  : scale;
1656
1676
  const heightFitScale =
1657
- totalHeight > 0 ? Math.max(0.1, (rect.height - 96) / totalHeight) : scale;
1677
+ totalHeight > 0
1678
+ ? Math.max(minFitScale, (rect.height - 96) / totalHeight)
1679
+ : scale;
1658
1680
  const nextScale = Math.min(scale, widthFitScale, heightFitScale);
1659
1681
  if (nextScale < scale) {
1660
1682
  const nextZoom = nextScale * 100;
@@ -1662,8 +1684,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
1662
1684
  setCanvasZoom(nextZoom);
1663
1685
  onZoomChange?.(nextZoom);
1664
1686
  }
1665
- const visualLeft = Math.max(24, (rect.width - totalWidth * nextScale) / 2);
1666
- const visualTop = Math.max(24, (rect.height - totalHeight * nextScale) / 2);
1687
+ // Genuinely centred, including when content overflows: flooring these
1688
+ // pins an oversized lineup against one edge and runs it off the other.
1689
+ const visualLeft = (rect.width - totalWidth * nextScale) / 2;
1690
+ const visualTop = (rect.height - totalHeight * nextScale) / 2;
1667
1691
  const nextPan = {
1668
1692
  x: visualLeft - (SURFACE_PADDING + boundsLeft) * nextScale,
1669
1693
  y: visualTop - (SURFACE_PADDING + boundsTop) * nextScale,
@@ -7050,7 +7074,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
7050
7074
  cameraCommand.fitBounds,
7051
7075
  { width: rect.width, height: rect.height },
7052
7076
  {
7053
- paddingScreenPx: cameraCommand.paddingScreenPx ?? 64,
7077
+ paddingScreenPx:
7078
+ cameraCommand.paddingScreenPx ?? CANVAS_FIT_PADDING_PX,
7054
7079
  // Frame geometry is canvas-space, while every frame DOM node lives
7055
7080
  // inside the padded world. Include that shared origin so fitting a
7056
7081
  // small drawn/preset frame actually centers its painted card.
@@ -7147,70 +7172,65 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
7147
7172
 
7148
7173
  const flushPendingWheelGesture = useCallback(() => {
7149
7174
  wheelGestureFrameRef.current = null;
7150
- const gesture = pendingWheelGestureRef.current;
7151
- pendingWheelGestureRef.current = null;
7152
- if (!gesture) return;
7175
+ const zoomGesture = pendingZoomGestureRef.current;
7176
+ const panGesture = pendingPanGestureRef.current;
7177
+ pendingZoomGestureRef.current = null;
7178
+ pendingPanGestureRef.current = null;
7179
+
7180
+ let nextPan = panRef.current;
7181
+ let moved = false;
7182
+ let settleChrome = false;
7153
7183
 
7154
- if (gesture.mode === "zoom") {
7184
+ if (zoomGesture) {
7155
7185
  const currentZoom = zoomRef.current;
7156
7186
  const nextZoom = clamp(
7157
- currentZoom * resolveZoomFactor(gesture.deltaY, gesture.pinch),
7187
+ currentZoom * clampZoomFactor(zoomGesture.factor),
7158
7188
  MIN_ZOOM,
7159
7189
  MAX_ZOOM,
7160
7190
  );
7161
- if (nextZoom === currentZoom) return;
7191
+ if (nextZoom !== currentZoom) {
7192
+ nextPan = getPanForZoomToCursor({
7193
+ pan: nextPan,
7194
+ cursor: zoomGesture.cursor,
7195
+ oldZoom: currentZoom,
7196
+ nextZoom,
7197
+ });
7198
+ zoomRef.current = nextZoom;
7199
+ moved = true;
7200
+ settleChrome = true;
7201
+ }
7202
+ }
7162
7203
 
7163
- const nextPan = getPanForZoomToCursor({
7164
- pan: panRef.current,
7165
- cursor: gesture.cursor,
7166
- oldZoom: currentZoom,
7167
- nextZoom,
7168
- });
7169
- zoomRef.current = nextZoom;
7170
- panRef.current = nextPan;
7171
- markWheelGestureActive();
7172
- applyViewToDom();
7173
- scheduleViewCommit({ settleChrome: true });
7174
- return;
7204
+ if (panGesture) {
7205
+ nextPan = {
7206
+ x: nextPan.x - panGesture.deltaX,
7207
+ y: nextPan.y - panGesture.deltaY,
7208
+ };
7209
+ moved = true;
7175
7210
  }
7176
7211
 
7177
- const nextPan = {
7178
- x: panRef.current.x - gesture.deltaX,
7179
- y: panRef.current.y - gesture.deltaY,
7180
- };
7212
+ if (!moved) return;
7181
7213
  panRef.current = nextPan;
7182
7214
  markWheelGestureActive();
7183
7215
  applyViewToDom();
7184
- scheduleViewCommit();
7216
+ scheduleViewCommit(settleChrome ? { settleChrome: true } : undefined);
7185
7217
  }, [applyViewToDom, markWheelGestureActive, scheduleViewCommit]);
7186
7218
 
7187
7219
  const enqueueWheelGesture = useCallback(
7188
7220
  (gesture: PendingWheelGesture) => {
7189
- const pending = pendingWheelGestureRef.current;
7190
- // Only accumulate like with like: a pinch delta and a wheel notch map to
7191
- // zoom through different curves, so summing them would apply one curve to
7192
- // the other's units.
7193
- if (
7194
- pending?.mode === "zoom" &&
7195
- gesture.mode === "zoom" &&
7196
- pending.pinch === gesture.pinch
7197
- ) {
7198
- pendingWheelGestureRef.current = {
7199
- mode: "zoom",
7200
- deltaY: pending.deltaY + gesture.deltaY,
7201
- pinch: gesture.pinch,
7202
- cursor: gesture.cursor,
7203
- clientX: gesture.clientX,
7204
- clientY: gesture.clientY,
7221
+ if (gesture.mode === "zoom") {
7222
+ const pending = pendingZoomGestureRef.current;
7223
+ pendingZoomGestureRef.current = {
7224
+ ...gesture,
7225
+ factor: (pending?.factor ?? 1) * gesture.factor,
7205
7226
  };
7206
- } else if (pending?.mode === "pan" && gesture.mode === "pan") {
7207
- pendingWheelGestureRef.current = {
7227
+ } else {
7228
+ const pending = pendingPanGestureRef.current;
7229
+ pendingPanGestureRef.current = {
7208
7230
  mode: "pan",
7209
- deltaX: pending.deltaX + gesture.deltaX,
7210
- deltaY: pending.deltaY + gesture.deltaY,
7231
+ deltaX: (pending?.deltaX ?? 0) + gesture.deltaX,
7232
+ deltaY: (pending?.deltaY ?? 0) + gesture.deltaY,
7211
7233
  };
7212
- } else {
7213
- pendingWheelGestureRef.current = gesture;
7214
7234
  }
7215
7235
 
7216
7236
  if (wheelGestureFrameRef.current !== null) return;
@@ -7244,13 +7264,18 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
7244
7264
  // no forced style/layout read on the hot pan path.
7245
7265
  const rect = surfaceRef.current?.getBoundingClientRect();
7246
7266
  if (!rect) return;
7247
- // Raw delta: the per-frame clamp lives on the zoom FACTOR now (see
7248
- // resolveZoomFactor), so accumulation stays frame-rate independent.
7249
- // Line/page delta modes are always discrete wheels, never a pinch.
7267
+ const device = resolveZoomGestureDevice({
7268
+ deltaY: args.deltaY,
7269
+ deltaMode: args.deltaMode,
7270
+ ctrlKey: args.ctrlKey,
7271
+ metaKey: args.metaKey,
7272
+ atMs: performance.now(),
7273
+ previous: zoomGestureDeviceRef.current,
7274
+ });
7275
+ zoomGestureDeviceRef.current = device;
7250
7276
  enqueueWheelGesture({
7251
7277
  mode: "zoom",
7252
- deltaY: delta.y,
7253
- pinch: args.deltaMode === 0 && isPinchZoomDelta(args.deltaY),
7278
+ factor: accumulateZoomFactor(1, delta.y, device.pinch),
7254
7279
  cursor: {
7255
7280
  x: args.clientX - rect.left,
7256
7281
  y: args.clientY - rect.top,
@@ -7289,7 +7314,9 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
7289
7314
  if (!event.isTrusted) return;
7290
7315
  // Pan/zoom mid-drag invalidates the gesture's cached snap viewport.
7291
7316
  invalidateSnapGesture();
7292
- event.preventDefault();
7317
+ // Chrome sends non-cancelable wheel events during a fling; cancelling one
7318
+ // is a no-op that logs an Intervention per event and still scrolls.
7319
+ if (event.cancelable) event.preventDefault();
7293
7320
  event.stopPropagation();
7294
7321
  enqueueWheelGestureFromClient({
7295
7322
  deltaX: event.deltaX,
@@ -11586,10 +11613,9 @@ function getWheelDeltaFromValues(
11586
11613
  deltaY: number,
11587
11614
  deltaMode: number,
11588
11615
  ) {
11589
- const multiplier = deltaMode === 1 ? 16 : deltaMode === 2 ? 800 : 1;
11590
11616
  return {
11591
- x: deltaX * multiplier,
11592
- y: deltaY * multiplier,
11617
+ x: normalizeWheelDeltaPx(deltaX, deltaMode),
11618
+ y: normalizeWheelDeltaPx(deltaY, deltaMode),
11593
11619
  };
11594
11620
  }
11595
11621
 
@@ -5717,7 +5717,9 @@ declare var __INITIAL_SOURCE_HEAD__: string;
5717
5717
  }
5718
5718
 
5719
5719
  function stopNativeInteraction(e: Event): void {
5720
- e.preventDefault();
5720
+ // A fling's wheel events are not cancelable; cancelling one logs a browser
5721
+ // Intervention per event and scrolls anyway.
5722
+ if (e.cancelable) e.preventDefault();
5721
5723
  e.stopPropagation();
5722
5724
  if (e.stopImmediatePropagation) e.stopImmediatePropagation();
5723
5725
  }
@@ -96,7 +96,9 @@ declare var __EDITING_SAFETY_ENABLED__: boolean;
96
96
  }
97
97
 
98
98
  function stopNativeInteraction(e: Event): void {
99
- e.preventDefault();
99
+ // A fling's wheel events are not cancelable; cancelling one logs a browser
100
+ // Intervention per event and scrolls anyway.
101
+ if (e.cancelable) e.preventDefault();
100
102
  e.stopPropagation();
101
103
  if (e.stopImmediatePropagation) e.stopImmediatePropagation();
102
104
  }
@@ -120,7 +122,11 @@ declare var __EDITING_SAFETY_ENABLED__: boolean;
120
122
  }
121
123
 
122
124
  function onWheel(e: WheelEvent): void {
123
- if (!wheelEnabled) return;
125
+ // Zoom forwards whatever the pan flag says: the flag only arrives by
126
+ // postMessage and a document swap strands it, while the zoom bridge cancels
127
+ // ctrl/meta wheel here regardless — so gating it makes the gesture vanish.
128
+ var zoomIntent = !!(e.ctrlKey || e.metaKey);
129
+ if (!wheelEnabled && !zoomIntent) return;
124
130
  stopNativeInteraction(e);
125
131
  postToParent({
126
132
  type: "embedded-canvas-wheel",
@@ -8,7 +8,8 @@
8
8
  *
9
9
  * Protocol (iframe → parent):
10
10
  *
11
- * { type: 'pinch-zoom-wheel', deltaY, clientX, clientY }
11
+ * { type: 'pinch-zoom-wheel', deltaY, deltaMode, clientX, clientY,
12
+ * ctrlKey, metaKey }
12
13
  *
13
14
  * Rules:
14
15
  * • No import/require of any module (DOM globals only).
@@ -23,7 +24,9 @@
23
24
  document.documentElement || document.body || document;
24
25
  function onWheel(e: WheelEvent): void {
25
26
  if (!(e.ctrlKey || e.metaKey)) return;
26
- e.preventDefault();
27
+ // A fling's wheel events are not cancelable; cancelling one logs a browser
28
+ // Intervention per event and scrolls anyway.
29
+ if (e.cancelable) e.preventDefault();
27
30
  try {
28
31
  (window.parent as Window).postMessage(
29
32
  {
@@ -31,6 +34,12 @@
31
34
  deltaY: e.deltaY,
32
35
  clientX: e.clientX,
33
36
  clientY: e.clientY,
37
+ // The parent classifies and scales from these three. Dropping the
38
+ // modifiers puts every pinch on the notch curve; dropping the mode
39
+ // makes a Firefox line tick read as 3px of travel.
40
+ deltaMode: e.deltaMode,
41
+ ctrlKey: !!e.ctrlKey,
42
+ metaKey: !!e.metaKey,
34
43
  },
35
44
  "*",
36
45
  );
@@ -857,10 +857,9 @@ export type DragState =
857
857
  export type PendingWheelGesture =
858
858
  | {
859
859
  mode: "zoom";
860
- deltaY: number;
861
- /** Trackpad pinch rather than a discrete mouse notch — the two use
862
- * different sensitivities, so accumulation must not mix them. */
863
- pinch: boolean;
860
+ /** Already-curved multiplier, not a raw delta: the two devices' curves
861
+ * differ, so only the factor is safe to accumulate across events. */
862
+ factor: number;
864
863
  cursor: Point;
865
864
  clientX: number;
866
865
  clientY: number;
@@ -1,69 +1,25 @@
1
1
  /**
2
- * Wheel/pinch → zoom-factor conversion.
2
+ * Wheel/pinch zoom for the overview canvas.
3
3
  *
4
- * The two input devices are not comparable and must not share a sensitivity.
5
- * A mouse wheel emits coarse, discrete notches (one detent ≈ 100px in Chrome's
6
- * pixel delta mode); a trackpad pinch emits fine, often fractional deltas at a
7
- * far higher rate. Feeding both through one `exp(-deltaY * 0.01)` meant a
8
- * single mouse notch multiplied zoom by e¹ ≈ 2.72× — which is what made canvas
9
- * zoom feel uncontrollable next to Figma's ~1.1× per notch.
10
- *
11
- * The clamp applies to the resulting FACTOR, not to the accumulated delta.
12
- * Clamping the delta made the step frame-rate dependent: deltas accumulate
13
- * between animation frames, so a dropped frame silently doubled the distance
14
- * travelled. Clamping the factor means a dropped frame costs smoothness, never
15
- * distance.
16
- */
17
-
18
- /** One mouse-wheel detent in `deltaMode === 0` pixels (Chrome/Safari). */
19
- export const MOUSE_WHEEL_NOTCH_PX = 100;
20
- /** Zoom multiplier for one full mouse-wheel notch, matching Figma's feel. */
21
- export const ZOOM_STEP_PER_NOTCH = 1.1;
22
- /** Multiplier exponent per pixel of trackpad pinch delta. */
23
- export const PINCH_ZOOM_SENSITIVITY = 0.0075;
24
- /** Ceiling on a single frame's zoom change, in either direction. */
25
- export const MAX_ZOOM_FACTOR_PER_FRAME = 1.6;
26
-
27
- /**
28
- * Whether a ctrl/meta-modified wheel delta came from a trackpad pinch rather
29
- * than a mouse wheel. Browsers report macOS pinch as a synthetic ctrl+wheel
30
- * with small, frequently fractional pixel deltas, while a real wheel detent is
31
- * a whole number near the notch size. Only meaningful for `deltaMode === 0`;
32
- * line/page modes are always discrete wheels and callers must not ask.
4
+ * The device split and both curves are shared, because a surface that
5
+ * re-implements either half reproduces the same mouse-reads-as-pinch bug.
6
+ * Only the anchor below belongs to this surface.
33
7
  */
34
- export function isPinchZoomDelta(deltaY: number): boolean {
35
- if (!Number.isFinite(deltaY)) return false;
36
- return (
37
- !Number.isInteger(deltaY) || Math.abs(deltaY) < MOUSE_WHEEL_NOTCH_PX / 2
38
- );
39
- }
40
-
41
- /** Unclamped zoom multiplier for a wheel/pinch delta. Negative delta (scroll
42
- * up / pinch open) zooms in, matching the browser's wheel convention. */
43
- export function zoomFactorForWheelDelta(
44
- deltaY: number,
45
- pinch: boolean,
46
- ): number {
47
- if (!Number.isFinite(deltaY) || deltaY === 0) return 1;
48
- return pinch
49
- ? Math.exp(-deltaY * PINCH_ZOOM_SENSITIVITY)
50
- : Math.pow(ZOOM_STEP_PER_NOTCH, -deltaY / MOUSE_WHEEL_NOTCH_PX);
51
- }
52
-
53
- /** Bounds one frame's zoom change symmetrically in multiplicative space, so
54
- * zooming in and back out by the same clamped amount is a round trip. */
55
- export function clampZoomFactor(factor: number): number {
56
- if (!Number.isFinite(factor) || factor <= 0) return 1;
57
- return Math.min(
58
- MAX_ZOOM_FACTOR_PER_FRAME,
59
- Math.max(1 / MAX_ZOOM_FACTOR_PER_FRAME, factor),
60
- );
61
- }
62
-
63
- /** The zoom multiplier to apply for one flushed gesture frame. */
64
- export function resolveZoomFactor(deltaY: number, pinch: boolean): number {
65
- return clampZoomFactor(zoomFactorForWheelDelta(deltaY, pinch));
66
- }
8
+ export {
9
+ accumulateZoomFactor,
10
+ clampZoomFactor,
11
+ isPinchZoomDelta,
12
+ MAX_PINCH_DELTA_PX,
13
+ MAX_ZOOM_FACTOR_PER_FRAME,
14
+ MOUSE_WHEEL_NOTCH_PX,
15
+ normalizeWheelDeltaPx,
16
+ PINCH_ZOOM_SENSITIVITY,
17
+ resolveZoomGestureDevice,
18
+ ZOOM_GESTURE_IDLE_RESET_MS,
19
+ ZOOM_STEP_PER_NOTCH,
20
+ zoomFactorForWheelDelta,
21
+ type ZoomGestureDevice,
22
+ } from "@agent-native/core/client/zoom-gesture";
67
23
 
68
24
  /** Anchor for an externally driven zoom change. Only hold the frame centre when
69
25
  * it is on screen: a frame taller than the viewport has its centre off-screen,
@@ -1,5 +1,6 @@
1
1
  import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
2
2
  import {
3
+ CANVAS_FIT_PADDING_PX,
3
4
  DEFAULT_CANVAS_MAX_ZOOM,
4
5
  DEFAULT_CANVAS_MIN_ZOOM,
5
6
  getCameraForBounds,
@@ -169,7 +170,7 @@ export function computeFitCameraForFrames(
169
170
  const bounds = getFrameGroupBounds(frames);
170
171
  if (!bounds) return null;
171
172
  return getCameraForBounds(bounds, viewport, {
172
- paddingScreenPx: options?.paddingScreenPx ?? 64,
173
+ paddingScreenPx: options?.paddingScreenPx ?? CANVAS_FIT_PADDING_PX,
173
174
  minZoom: DEFAULT_CANVAS_MIN_ZOOM,
174
175
  maxZoom: DEFAULT_CANVAS_MAX_ZOOM,
175
176
  fallbackZoom: 100,
@@ -282,6 +282,16 @@ export const DEFAULT_ASSIGNED_REGION_MAX_COLUMNS = 3;
282
282
  export const DEFAULT_CANVAS_MIN_ZOOM = 2;
283
283
  export const DEFAULT_CANVAS_MAX_ZOOM = 25600;
284
284
 
285
+ /**
286
+ * Floor for the AUTOMATIC shrink-to-fit that runs on a screen-count change —
287
+ * never for an explicit Zoom to fit, which must be free to reach MIN_ZOOM or a
288
+ * board of more than ~9 screens in a row stops fitting.
289
+ */
290
+ export const DEFAULT_CANVAS_AUTOFIT_MIN_ZOOM = 10;
291
+
292
+ /** Breathing room a fit leaves around the fitted bounds, in screen px. */
293
+ export const CANVAS_FIT_PADDING_PX = 64;
294
+
285
295
  export function screenToCanvasPoint(
286
296
  point: CanvasPoint,
287
297
  camera: CanvasCamera,
@@ -511,7 +521,7 @@ export function getCameraForBounds(
511
521
  bounds: FrameBounds | FrameGeometry | null,
512
522
  viewport: CanvasSize,
513
523
  {
514
- paddingScreenPx = 48,
524
+ paddingScreenPx = CANVAS_FIT_PADDING_PX,
515
525
  canvasPadding = 0,
516
526
  minZoom = 10,
517
527
  maxZoom = 400,