@agent-native/core 0.85.5 → 0.85.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 (86) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/AssistantChat.tsx +1 -1
  5. package/corpus/core/src/client/use-pinch-zoom.ts +76 -11
  6. package/corpus/core/src/collab/presence.ts +63 -3
  7. package/corpus/templates/clips/desktop/src/lib/recorder.ts +245 -77
  8. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +190 -13
  9. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +16 -4
  10. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +46 -3
  11. package/corpus/templates/design/AGENTS.md +40 -4
  12. package/corpus/templates/design/actions/apply-component-prop-edit.ts +20 -73
  13. package/corpus/templates/design/actions/apply-motion-edit.ts +29 -2
  14. package/corpus/templates/design/actions/apply-visual-edit.ts +4 -2
  15. package/corpus/templates/design/actions/connect-localhost.ts +25 -18
  16. package/corpus/templates/design/actions/edit-design.ts +5 -3
  17. package/corpus/templates/design/actions/export-pdf.ts +8 -1
  18. package/corpus/templates/design/actions/export-zip.ts +10 -1
  19. package/corpus/templates/design/actions/get-motion-timeline.ts +26 -20
  20. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +10 -0
  21. package/corpus/templates/design/actions/insert-asset.ts +190 -7
  22. package/corpus/templates/design/actions/open-visual-edit.ts +27 -5
  23. package/corpus/templates/design/actions/present-design-variants.ts +12 -8
  24. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +18 -14
  25. package/corpus/templates/design/actions/run-design-audit.ts +7 -4
  26. package/corpus/templates/design/actions/write-local-file.ts +47 -22
  27. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +221 -66
  28. package/corpus/templates/design/app/components/design/EditPanel.tsx +650 -237
  29. package/corpus/templates/design/app/components/design/LayersPanel.tsx +526 -127
  30. package/corpus/templates/design/app/components/design/MotionDock.tsx +234 -46
  31. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1520 -410
  32. package/corpus/templates/design/app/components/design/StatesPanel.tsx +25 -2
  33. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +640 -72
  34. package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +605 -0
  35. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +50 -26
  36. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +16 -4
  37. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +90 -103
  38. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +111 -5
  39. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +13 -2
  40. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +42 -2
  41. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +11 -2
  42. package/corpus/templates/design/app/components/design/types.ts +16 -0
  43. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +24 -6
  44. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +15 -1
  45. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  46. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +77 -0
  47. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  48. package/corpus/templates/design/app/i18n-data.ts +18 -0
  49. package/corpus/templates/design/app/lib/design-import.ts +95 -0
  50. package/corpus/templates/design/app/pages/DesignEditor.tsx +4015 -840
  51. package/corpus/templates/design/changelog/2026-07-02-creating-two-frames-in-a-row-no-longer-freezes-and-reloads-t.md +6 -0
  52. package/corpus/templates/design/changelog/2026-07-03-canvas-interactions-now-match-figma-rotation-aware-resizing-.md +6 -0
  53. package/corpus/templates/design/changelog/2026-07-03-design-retries-stalled-screen-edits-with-a-smaller-safer-fol.md +6 -0
  54. package/corpus/templates/design/changelog/2026-07-03-inspector-fixes-mixed-selections-show-mixed-instead-of-wrong.md +6 -0
  55. package/corpus/templates/design/changelog/2026-07-03-keyframe-timeline-works-reliably-drag-keyframes-smoothly-pic.md +6 -0
  56. package/corpus/templates/design/changelog/2026-07-03-layers-panel-matches-figma-top-layer-on-top-drag-with-auto-s.md +6 -0
  57. package/corpus/templates/design/changelog/2026-07-03-much-faster-editor-smooth-dragging-zooming-and-panel-updates.md +6 -0
  58. package/corpus/templates/design/changelog/2026-07-03-pen-tool-refinements-click-the-first-point-to-close-with-a-d.md +6 -0
  59. package/corpus/templates/design/changelog/2026-07-03-text-editing-enter-adds-a-line-break-international-ime-typin.md +6 -0
  60. package/corpus/templates/design/changelog/2026-07-03-undo-and-redo-are-dependable-across-agent-edits-screen-switc.md +6 -0
  61. package/corpus/templates/design/server/lib/design-export.ts +43 -1
  62. package/corpus/templates/design/server/plugins/agent-chat.ts +1 -1
  63. package/corpus/templates/design/shared/board-file.ts +25 -5
  64. package/corpus/templates/design/shared/canvas-math.ts +754 -9
  65. package/corpus/templates/design/shared/code-layer.ts +304 -26
  66. package/corpus/templates/design/shared/color-utils.ts +84 -0
  67. package/corpus/templates/design/shared/design-source-capabilities.ts +16 -7
  68. package/corpus/templates/design/shared/motion-compiler.ts +75 -31
  69. package/corpus/templates/design/shared/motion-timeline.ts +335 -0
  70. package/corpus/templates/design/shared/pen-path.ts +200 -23
  71. package/dist/client/AssistantChat.js +1 -1
  72. package/dist/client/AssistantChat.js.map +1 -1
  73. package/dist/client/use-pinch-zoom.d.ts.map +1 -1
  74. package/dist/client/use-pinch-zoom.js +76 -11
  75. package/dist/client/use-pinch-zoom.js.map +1 -1
  76. package/dist/collab/awareness.d.ts +2 -2
  77. package/dist/collab/awareness.d.ts.map +1 -1
  78. package/dist/collab/presence.d.ts.map +1 -1
  79. package/dist/collab/presence.js +54 -3
  80. package/dist/collab/presence.js.map +1 -1
  81. package/dist/collab/routes.d.ts +1 -1
  82. package/dist/collab/struct-routes.d.ts +1 -1
  83. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  84. package/dist/observability/routes.d.ts +2 -2
  85. package/dist/secrets/routes.d.ts +3 -3
  86. package/package.json +1 -1
@@ -27,10 +27,27 @@ export function createCornerNode(point: PenPoint): PenNode {
27
27
  return { point: { ...point } };
28
28
  }
29
29
 
30
- export function createSmoothNode(anchor: PenPoint, handleOut: PenPoint) {
30
+ /**
31
+ * Creates a smooth (symmetric-handle) anchor by default: `handleIn` mirrors
32
+ * `handleOut` across `anchor`, giving the anchor a continuous tangent on
33
+ * both sides — this is what dragging a fresh pen anchor normally produces.
34
+ *
35
+ * Pass `{ breakSymmetry: true }` (Figma: hold Alt/Option while dragging a
36
+ * newly placed anchor) to break that symmetry into a cusp: `handleOut`
37
+ * still follows the drag, but no mirrored `handleIn` is created, so the
38
+ * incoming segment stays a plain corner while the outgoing segment curves
39
+ * independently.
40
+ */
41
+ export function createSmoothNode(
42
+ anchor: PenPoint,
43
+ handleOut: PenPoint,
44
+ options?: { breakSymmetry?: boolean },
45
+ ): PenNode {
31
46
  return {
32
47
  point: { ...anchor },
33
- handleIn: mirrorPoint(anchor, handleOut),
48
+ handleIn: options?.breakSymmetry
49
+ ? undefined
50
+ : mirrorPoint(anchor, handleOut),
34
51
  handleOut: { ...handleOut },
35
52
  };
36
53
  }
@@ -62,17 +79,58 @@ export function constrainPointTo45Degrees(
62
79
  ): PenPoint {
63
80
  const dx = point.x - origin.x;
64
81
  const dy = point.y - origin.y;
65
- const distance = Math.hypot(dx, dy);
66
- if (distance === 0) return { ...point };
82
+ if (dx === 0 && dy === 0) return { ...point };
67
83
 
68
84
  const angle = Math.atan2(dy, dx);
69
85
  const snappedAngle = Math.round(angle / (Math.PI / 4)) * (Math.PI / 4);
86
+ const axisX = Math.cos(snappedAngle);
87
+ const axisY = Math.sin(snappedAngle);
88
+
89
+ // Project the drag vector onto the snapped axis component-wise (like
90
+ // Figma), rather than preserving the raw radial distance. For an
91
+ // axis-aligned snap (0/90/180/270) this reduces to keeping the dominant
92
+ // component and zeroing the other; for diagonal snaps it reduces to the
93
+ // usual equal-magnitude diagonal.
94
+ const projection = dx * axisX + dy * axisY;
70
95
  return {
71
- x: origin.x + Math.cos(snappedAngle) * distance,
72
- y: origin.y + Math.sin(snappedAngle) * distance,
96
+ x: origin.x + axisX * projection,
97
+ y: origin.y + axisY * projection,
73
98
  };
74
99
  }
75
100
 
101
+ /**
102
+ * Light pen-anchor snapping (P15): snap a candidate new-anchor point to any
103
+ * existing anchor point of the path currently being drawn (Figma snaps new
104
+ * anchors onto other anchors of the same path so you can precisely re-hit a
105
+ * prior point), and otherwise round to the nearest integer canvas px once
106
+ * the user is zoomed in to 100% or more (where sub-pixel placement is
107
+ * rarely intentional and hairline anti-aliasing becomes visible).
108
+ *
109
+ * This intentionally does not snap to *other* shapes/frames on the canvas —
110
+ * that's the existing computeMoveSnap/grid machinery's job for whole-object
111
+ * moves, not a per-anchor pen concern.
112
+ */
113
+ export function snapPenAnchorPoint(
114
+ point: PenPoint,
115
+ path: PenPath | null,
116
+ options: { hitRadius: number; zoom: number },
117
+ ): PenPoint {
118
+ const existingAnchor = path?.nodes.find(
119
+ (node) =>
120
+ Math.hypot(node.point.x - point.x, node.point.y - point.y) <=
121
+ options.hitRadius,
122
+ );
123
+ if (existingAnchor) {
124
+ return { ...existingAnchor.point };
125
+ }
126
+
127
+ if (options.zoom >= 100) {
128
+ return { x: Math.round(point.x), y: Math.round(point.y) };
129
+ }
130
+
131
+ return point;
132
+ }
133
+
76
134
  export function isPenCloseTarget(
77
135
  path: PenPath | null,
78
136
  point: PenPoint,
@@ -84,25 +142,151 @@ export function isPenCloseTarget(
84
142
  }
85
143
 
86
144
  export function getPenPathGeometry(path: PenPath): PenGeometry {
87
- const points = path.nodes.flatMap((node) =>
88
- [node.point, node.handleIn, node.handleOut].filter(isPenPoint),
89
- );
90
- if (points.length === 0) {
145
+ if (path.nodes.length === 0) {
91
146
  return { x: 0, y: 0, width: MIN_PATH_SIZE, height: MIN_PATH_SIZE };
92
147
  }
93
148
 
94
- const left = Math.min(...points.map((point) => point.x));
95
- const top = Math.min(...points.map((point) => point.y));
96
- const right = Math.max(...points.map((point) => point.x));
97
- const bottom = Math.max(...points.map((point) => point.y));
149
+ // Tight bounds: rather than bounding all anchors *and* control handles
150
+ // (which over-counts a handle that pulls a curve's tangent can sit well
151
+ // outside the curve's actual extent), walk each rendered segment and
152
+ // bound the real curve geometry: anchor endpoints plus any local extrema
153
+ // found by solving the cubic Bezier derivative per axis. This matches
154
+ // what `serializePenPath` actually draws (an `L` segment when handles
155
+ // coincide with their anchors, otherwise a `C` segment).
156
+ let left = Infinity;
157
+ let top = Infinity;
158
+ let right = -Infinity;
159
+ let bottom = -Infinity;
160
+
161
+ const include = (point: PenPoint) => {
162
+ if (point.x < left) left = point.x;
163
+ if (point.x > right) right = point.x;
164
+ if (point.y < top) top = point.y;
165
+ if (point.y > bottom) bottom = point.y;
166
+ };
167
+
168
+ const { nodes, closed } = path;
169
+ include(nodes[0].point);
170
+
171
+ for (let i = 1; i < nodes.length; i++) {
172
+ includeSegmentBounds(nodes[i - 1], nodes[i], include);
173
+ }
174
+ if (closed && nodes.length > 1) {
175
+ includeSegmentBounds(nodes[nodes.length - 1], nodes[0], include);
176
+ }
177
+
178
+ if (!Number.isFinite(left)) {
179
+ return { x: 0, y: 0, width: MIN_PATH_SIZE, height: MIN_PATH_SIZE };
180
+ }
181
+
182
+ const width = right - left;
183
+ const height = bottom - top;
184
+ // Degenerate (zero-area) paths — e.g. a single anchor, or a perfectly
185
+ // straight horizontal/vertical two-point path — still need a visible
186
+ // selection box, so floor to a minimum size in that case only.
187
+ if (width <= 0 && height <= 0) {
188
+ return { x: left, y: top, width: MIN_PATH_SIZE, height: MIN_PATH_SIZE };
189
+ }
98
190
  return {
99
191
  x: left,
100
192
  y: top,
101
- width: Math.max(MIN_PATH_SIZE, right - left),
102
- height: Math.max(MIN_PATH_SIZE, bottom - top),
193
+ width: width > 0 ? width : MIN_PATH_SIZE,
194
+ height: height > 0 ? height : MIN_PATH_SIZE,
103
195
  };
104
196
  }
105
197
 
198
+ function includeSegmentBounds(
199
+ from: PenNode,
200
+ to: PenNode,
201
+ include: (point: PenPoint) => void,
202
+ ) {
203
+ const c1 = from.handleOut ?? from.point;
204
+ const c2 = to.handleIn ?? to.point;
205
+ include(to.point);
206
+
207
+ // Straight segment (serializePenPath emits `L` in this case) — the two
208
+ // anchors already bound it, no interior extrema to solve for.
209
+ if (samePoint(c1, from.point) && samePoint(c2, to.point)) {
210
+ return;
211
+ }
212
+
213
+ include(c1);
214
+ include(c2);
215
+ for (const t of cubicBezierExtremaTs(from.point.x, c1.x, c2.x, to.point.x)) {
216
+ include({
217
+ x: cubicBezierValue(from.point.x, c1.x, c2.x, to.point.x, t),
218
+ y: cubicBezierValue(from.point.y, c1.y, c2.y, to.point.y, t),
219
+ });
220
+ }
221
+ for (const t of cubicBezierExtremaTs(from.point.y, c1.y, c2.y, to.point.y)) {
222
+ include({
223
+ x: cubicBezierValue(from.point.x, c1.x, c2.x, to.point.x, t),
224
+ y: cubicBezierValue(from.point.y, c1.y, c2.y, to.point.y, t),
225
+ });
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Roots of B'(t) = 0 for a single-axis cubic Bezier with control points
231
+ * p0..p3, restricted to t in (0, 1) (endpoints are already included by the
232
+ * caller via the anchor points).
233
+ *
234
+ * B(t) = (1-t)^3 p0 + 3(1-t)^2 t p1 + 3(1-t) t^2 p2 + t^3 p3
235
+ * B'(t) = 3(1-t)^2 (p1-p0) + 6(1-t)t (p2-p1) + 3t^2 (p3-p2)
236
+ * = a t^2 + b t + c, with:
237
+ * a = 3 * (-p0 + 3p1 - 3p2 + p3)
238
+ * b = 6 * (p0 - 2p1 + p2)
239
+ * c = 3 * (p1 - p0)
240
+ */
241
+ function cubicBezierExtremaTs(
242
+ p0: number,
243
+ p1: number,
244
+ p2: number,
245
+ p3: number,
246
+ ): number[] {
247
+ const a = 3 * (-p0 + 3 * p1 - 3 * p2 + p3);
248
+ const b = 6 * (p0 - 2 * p1 + p2);
249
+ const c = 3 * (p1 - p0);
250
+
251
+ const roots: number[] = [];
252
+ const EPS = 1e-9;
253
+
254
+ if (Math.abs(a) < EPS) {
255
+ // Linear derivative: at most one root.
256
+ if (Math.abs(b) >= EPS) {
257
+ const t = -c / b;
258
+ if (t > 0 && t < 1) roots.push(t);
259
+ }
260
+ return roots;
261
+ }
262
+
263
+ const discriminant = b * b - 4 * a * c;
264
+ if (discriminant < 0) return roots;
265
+
266
+ const sqrtDisc = Math.sqrt(discriminant);
267
+ const t1 = (-b + sqrtDisc) / (2 * a);
268
+ const t2 = (-b - sqrtDisc) / (2 * a);
269
+ if (t1 > 0 && t1 < 1) roots.push(t1);
270
+ if (t2 > 0 && t2 < 1) roots.push(t2);
271
+ return roots;
272
+ }
273
+
274
+ function cubicBezierValue(
275
+ p0: number,
276
+ p1: number,
277
+ p2: number,
278
+ p3: number,
279
+ t: number,
280
+ ): number {
281
+ const mt = 1 - t;
282
+ return (
283
+ mt * mt * mt * p0 +
284
+ 3 * mt * mt * t * p1 +
285
+ 3 * mt * t * t * p2 +
286
+ t * t * t * p3
287
+ );
288
+ }
289
+
106
290
  export function serializePenPath(path: PenPath): string {
107
291
  const [first, ...rest] = path.nodes;
108
292
  if (!first) return "";
@@ -121,13 +305,6 @@ export function serializePenPath(path: PenPath): string {
121
305
  return commands.join(" ");
122
306
  }
123
307
 
124
- export function offsetPenPath(path: PenPath, offset: PenPoint): PenPath {
125
- return transformPenPath(path, (point) => ({
126
- x: point.x - offset.x,
127
- y: point.y - offset.y,
128
- }));
129
- }
130
-
131
308
  export function translatePenPath(
132
309
  path: PenPath,
133
310
  dx: number,
@@ -71,7 +71,7 @@ const BACKGROUND_ACTIVE_RUN_STUCK_THRESHOLD_MS = 13 * 60_000;
71
71
  const ACTIVE_RUN_POLL_INTERVAL_MS = 150;
72
72
  const AUTO_RESUME_STATUS_TIMEOUT_MS = 30_000;
73
73
  const MAX_RECONNECT_AUTO_RECOVERIES = 3;
74
- const RECONNECT_NO_PROGRESS_CONTINUE_MESSAGE = "Continue from where you stopped. Use the partial work above, verify what succeeded, and finish the original request. Do not rerun the exact same failed tool input unless the failure was transient or the user explicitly asked for an exact rerun. Prefer dedicated app actions over raw database edits when they exist.";
74
+ const RECONNECT_NO_PROGRESS_CONTINUE_MESSAGE = "Continue from where you stopped. Use the partial work above, verify what succeeded, and finish the original request. If the last visible step was preparing an app action and no tool result was returned, treat that action input as stalled or too large: change strategy, use a smaller bounded input, and preserve optional details as visible affordances instead of repeating the same giant action. Do not rerun the exact same failed tool input unless the failure was transient or the user explicitly asked for an exact rerun. Prefer dedicated app actions over raw database edits when they exist.";
75
75
  // How long a single activity (model call, tool prep, long tool) must stay
76
76
  // in-flight before its label is surfaced in the running indicator. Below this
77
77
  // the indicator stays a steady "Thinking" so normal fast turns don't flicker