@agent-native/core 0.124.1 → 0.124.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 (43) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +15 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/assets/branding/favicon-base64.ts +5 -0
  5. package/corpus/core/src/cli/templates-meta.ts +1 -0
  6. package/corpus/core/src/server/email.ts +2 -5
  7. package/corpus/core/src/shared/streaming-text-smoothing.ts +15 -14
  8. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +34 -11
  9. package/corpus/templates/design/.agents/skills/responsive-breakpoints/SKILL.md +8 -0
  10. package/corpus/templates/design/actions/generate-design.ts +259 -36
  11. package/corpus/templates/design/actions/generate-screens.ts +51 -14
  12. package/corpus/templates/design/actions/present-design-variants.ts +6 -3
  13. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +8 -0
  14. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +158 -9
  15. package/corpus/templates/design/app/components/design/design-canvas/content-size-report.ts +124 -0
  16. package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +61 -9
  17. package/corpus/templates/design/changelog/2026-07-24-ai-designs-now-default-to-a-full-size-desktop-mobile-pair-fr.md +6 -0
  18. package/corpus/templates/design/changelog/2026-07-24-design-frames-now-fill-the-viewport-height-and-grow-to-fit-t.md +6 -0
  19. package/corpus/templates/design/changelog/2026-07-24-generating-an-additional-screen-now-places-it-beside-the-exi.md +6 -0
  20. package/dist/assets/branding/favicon-base64.d.ts +2 -0
  21. package/dist/assets/branding/favicon-base64.d.ts.map +1 -0
  22. package/dist/assets/branding/favicon-base64.js +5 -0
  23. package/dist/assets/branding/favicon-base64.js.map +1 -0
  24. package/dist/cli/templates-meta.d.ts.map +1 -1
  25. package/dist/cli/templates-meta.js +1 -0
  26. package/dist/cli/templates-meta.js.map +1 -1
  27. package/dist/collab/awareness.d.ts +2 -2
  28. package/dist/collab/awareness.d.ts.map +1 -1
  29. package/dist/collab/routes.d.ts +1 -1
  30. package/dist/notifications/routes.d.ts +3 -3
  31. package/dist/observability/routes.d.ts +1 -1
  32. package/dist/resources/handlers.d.ts +1 -1
  33. package/dist/server/email.d.ts.map +1 -1
  34. package/dist/server/email.js +2 -2
  35. package/dist/server/email.js.map +1 -1
  36. package/dist/shared/streaming-text-smoothing.d.ts.map +1 -1
  37. package/dist/shared/streaming-text-smoothing.js +15 -8
  38. package/dist/shared/streaming-text-smoothing.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/assets/branding/favicon-base64.ts +5 -0
  41. package/src/cli/templates-meta.ts +1 -0
  42. package/src/server/email.ts +2 -5
  43. package/src/shared/streaming-text-smoothing.ts +15 -14
@@ -77,15 +77,66 @@ const GENERATION_VIEWPORT_SIZES: Record<
77
77
  > = {
78
78
  mobile: { width: 390, height: 844 },
79
79
  tablet: { width: 768, height: 1024 },
80
- desktop: { width: 1440, height: 1024 },
80
+ desktop: { width: 1440, height: 900 },
81
81
  };
82
+ const GENERATION_VIEWPORT_LABELS: Record<GenerationViewport, string> = {
83
+ mobile: "Mobile",
84
+ tablet: "Tablet",
85
+ desktop: "Desktop",
86
+ };
87
+ // Widest → narrowest. The widest requested device seeds the primary/base
88
+ // frame; only the narrower devices become breakpoint frames.
89
+ const DEVICE_WIDTH_ORDER: readonly GenerationViewport[] = [
90
+ "desktop",
91
+ "tablet",
92
+ "mobile",
93
+ ];
82
94
  const GENERATED_FRAME_GAP = 96;
83
- const DEFAULT_RESPONSIVE_BREAKPOINTS = [390, 768, 1440].map((widthPx) => ({
84
- id: `generated-${widthPx}`,
85
- label: widthPx === 390 ? "Mobile" : widthPx === 768 ? "Tablet" : "Desktop",
86
- widthPx,
87
- prefix: widthToPrefix(widthPx),
88
- }));
95
+
96
+ function widestGenerationDevice(
97
+ devices: readonly GenerationViewport[],
98
+ ): GenerationViewport {
99
+ return (
100
+ DEVICE_WIDTH_ORDER.find((device) => devices.includes(device)) ??
101
+ DEFAULT_GENERATION_VIEWPORT
102
+ );
103
+ }
104
+
105
+ // Devices used when the caller omits `devices`: the requested primary form
106
+ // factor as the base plus Mobile, unless the primary already is Mobile. The
107
+ // default (desktop primary) therefore yields a Desktop base + Mobile
108
+ // breakpoint, matching the two-screen default.
109
+ function devicesForPrimaryViewport(
110
+ primaryViewport: GenerationViewport,
111
+ ): GenerationViewport[] {
112
+ return primaryViewport === "mobile"
113
+ ? ["mobile"]
114
+ : [primaryViewport, "mobile"];
115
+ }
116
+
117
+ // Breakpoint frames = every requested device NARROWER than the primary
118
+ // (widest) one, ascending by width. The primary/widest width is never emitted,
119
+ // so a single-device request produces an empty set (one frame, no sub-frames)
120
+ // and no redundant frame ever lands at the base canvas width.
121
+ function breakpointSetForDevices(devices: readonly GenerationViewport[]) {
122
+ const primaryWidth =
123
+ GENERATION_VIEWPORT_SIZES[widestGenerationDevice(devices)].width;
124
+ return Array.from(new Set(devices))
125
+ .filter((device) => GENERATION_VIEWPORT_SIZES[device].width < primaryWidth)
126
+ .sort(
127
+ (a, b) =>
128
+ GENERATION_VIEWPORT_SIZES[a].width - GENERATION_VIEWPORT_SIZES[b].width,
129
+ )
130
+ .map((device) => {
131
+ const widthPx = GENERATION_VIEWPORT_SIZES[device].width;
132
+ return {
133
+ id: `generated-${widthPx}`,
134
+ label: GENERATION_VIEWPORT_LABELS[device],
135
+ widthPx,
136
+ prefix: widthToPrefix(widthPx),
137
+ };
138
+ });
139
+ }
89
140
 
90
141
  const reuseLabelSchema = z
91
142
  .object({
@@ -123,16 +174,6 @@ function hasBreakpointSet(value: unknown): boolean {
123
174
  return Array.isArray(breakpoints) && breakpoints.length > 0;
124
175
  }
125
176
 
126
- function nextGeneratedFrameX(
127
- frames: ReturnType<typeof parseCanvasFrameGeometryById>,
128
- ): number {
129
- return Object.values(frames).reduce((furthestRight, frame) => {
130
- const x = frame.x ?? 0;
131
- const width = frame.width ?? 0;
132
- return Math.max(furthestRight, x + width + GENERATED_FRAME_GAP);
133
- }, 0);
134
- }
135
-
136
177
  function jsonValuesEqual(left: unknown, right: unknown): boolean {
137
178
  return JSON.stringify(left) === JSON.stringify(right);
138
179
  }
@@ -393,7 +434,7 @@ const generateDesignAgentParameters = {
393
434
  type: "string",
394
435
  description:
395
436
  "Optional JSON array of overview-canvas placements keyed by filename or fileId. " +
396
- "Pass explicit x/y/width/height for every generated screen; desktop is 1440x1024.",
437
+ "Pass explicit x/y/width/height for every generated screen; desktop is 1440x900.",
397
438
  },
398
439
  contextPackId: {
399
440
  type: "string",
@@ -415,8 +456,20 @@ const generateDesignAgentParameters = {
415
456
  type: "string",
416
457
  enum: ["mobile", "tablet", "desktop"],
417
458
  description:
418
- "The requested primary form factor. Defaults to desktop (1440x1024). " +
419
- "Set this from the intake answer when no explicit canvasFrames placement is supplied.",
459
+ "The requested primary form factor. Defaults to desktop (1440x900). " +
460
+ "Set this from the intake answer when no explicit canvasFrames placement is supplied. " +
461
+ "Ignored when `devices` is provided (the widest device becomes primary).",
462
+ },
463
+ devices: {
464
+ type: "array",
465
+ items: { type: "string", enum: ["mobile", "tablet", "desktop"] },
466
+ description:
467
+ "Device set for responsive frames. Honor the devices the prompt " +
468
+ 'explicitly names; omit to default to ["desktop","mobile"]. The widest ' +
469
+ "device becomes the primary/base frame and the narrower devices become " +
470
+ "breakpoint frames — never a duplicate of the base width and never an " +
471
+ "auto-added tablet. A single device yields one frame with no breakpoints. " +
472
+ "When provided, this overrides primaryViewport.",
420
473
  },
421
474
  },
422
475
  required: ["designId", "prompt", "files"],
@@ -438,9 +491,12 @@ const generateDesignAction = defineAction({
438
491
  "When `designSystemId` is provided, first use `get-design-system` and apply " +
439
492
  "its `agentContext` tokens/docs before writing the file content; do not " +
440
493
  "treat the id alone as enough design-system context. " +
441
- "Every web design must be responsive. Use a desktop 1440x1024 primary " +
442
- "canvas by default, or set `primaryViewport` for an explicitly mobile- or " +
443
- "tablet-primary design; this action adds responsive editor breakpoints. " +
494
+ "Every web design must be responsive. This action adds responsive editor " +
495
+ "breakpoints: by default a Desktop 1440x900 base frame plus a Mobile " +
496
+ "breakpoint (no auto tablet, no duplicate desktop). Pass `devices` to honor " +
497
+ "the form factors the prompt explicitly names — the widest becomes the base " +
498
+ "frame and the narrower ones become breakpoint frames. Set `primaryViewport` " +
499
+ "for a mobile- or tablet-primary design when not passing `devices`. " +
444
500
  "Do not report a design as ready until this action succeeds. " +
445
501
  "When adding multiple screens or states, pass canvasFrames with filenames " +
446
502
  "and x/y/width/height so the new screens appear placed on the overview canvas.",
@@ -599,8 +655,20 @@ const generateDesignAction = defineAction({
599
655
  .optional()
600
656
  .default(DEFAULT_GENERATION_VIEWPORT)
601
657
  .describe(
602
- "Primary generated viewport. Defaults to desktop (1440x1024); set " +
603
- "mobile or tablet only when that is the requested form factor.",
658
+ "Primary generated viewport. Defaults to desktop (1440x900); set " +
659
+ "mobile or tablet only when that is the requested form factor. " +
660
+ "Ignored when `devices` is provided (the widest device wins).",
661
+ ),
662
+ devices: z
663
+ .array(z.enum(["mobile", "tablet", "desktop"]))
664
+ .optional()
665
+ .describe(
666
+ "Explicit device set for responsive frames. Honor the devices the " +
667
+ 'prompt names; omit to default to ["desktop","mobile"]. Widest ' +
668
+ "device = primary/base frame; narrower devices = breakpoint frames " +
669
+ "(never the base width, never an auto tablet). One device = a single " +
670
+ "frame with no breakpoints. When provided, this overrides " +
671
+ "primaryViewport.",
604
672
  ),
605
673
  }),
606
674
  mcpApp: {
@@ -622,6 +690,7 @@ const generateDesignAction = defineAction({
622
690
  tweaks,
623
691
  canvasFrames,
624
692
  primaryViewport,
693
+ devices,
625
694
  contextPackId,
626
695
  contextModeOverride,
627
696
  reuseLabels,
@@ -827,6 +896,15 @@ const generateDesignAction = defineAction({
827
896
  ...tweak,
828
897
  type: tweak.type === "color-swatches" ? "color-swatch" : tweak.type,
829
898
  }));
899
+ // An explicit `devices` list wins over primaryViewport; otherwise derive
900
+ // the device set from primaryViewport so the default stays Desktop base +
901
+ // Mobile breakpoint. The widest resolved device seeds the primary frame.
902
+ const resolvedDevices =
903
+ devices && devices.length > 0
904
+ ? devices
905
+ : devicesForPrimaryViewport(primaryViewport);
906
+ const resolvedPrimaryViewport = widestGenerationDevice(resolvedDevices);
907
+ const generatedBreakpointSet = breakpointSetForDevices(resolvedDevices);
830
908
  await mutateDesignData({
831
909
  designId,
832
910
  mutate: (prevData, { updatedAt }) => {
@@ -865,9 +943,77 @@ const generateDesignAction = defineAction({
865
943
  : undefined;
866
944
  },
867
945
  });
868
- const viewport = GENERATION_VIEWPORT_SIZES[primaryViewport];
869
- let nextX = nextGeneratedFrameX(merged.canvasFrames);
870
- const generationFrames = [...merged.placedFrames];
946
+ const viewport = GENERATION_VIEWPORT_SIZES[resolvedPrimaryViewport];
947
+ // Frames placed by an earlier call: never moved, and counted as
948
+ // occupied so a new screen is never dropped on top of one.
949
+ const preExistingFrameIds = new Set(
950
+ prevData.canvasFrames && typeof prevData.canvasFrames === "object"
951
+ ? Object.keys(prevData.canvasFrames as Record<string, unknown>)
952
+ : [],
953
+ );
954
+ const rectOf = (frame: {
955
+ x?: number;
956
+ y?: number;
957
+ width?: number;
958
+ height?: number;
959
+ rotation?: number;
960
+ }) => {
961
+ const x = frame.x ?? 0;
962
+ const y = frame.y ?? 0;
963
+ const width = frame.width ?? 0;
964
+ const height = frame.height ?? 0;
965
+ const rotation = frame.rotation ?? 0;
966
+ if (!rotation || width <= 0 || height <= 0) {
967
+ return { x, y, width, height };
968
+ }
969
+ // Existing frames render rotated about their center; use the rotated
970
+ // rect's axis-aligned bounding box so a new screen isn't dropped over
971
+ // a rotated frame's real footprint.
972
+ const radians = (rotation * Math.PI) / 180;
973
+ const cos = Math.abs(Math.cos(radians));
974
+ const sin = Math.abs(Math.sin(radians));
975
+ const aabbWidth = width * cos + height * sin;
976
+ const aabbHeight = width * sin + height * cos;
977
+ return {
978
+ x: x + width / 2 - aabbWidth / 2,
979
+ y: y + height / 2 - aabbHeight / 2,
980
+ width: aabbWidth,
981
+ height: aabbHeight,
982
+ };
983
+ };
984
+ const framesOverlap = (
985
+ a: ReturnType<typeof rectOf>,
986
+ b: ReturnType<typeof rectOf>,
987
+ ) =>
988
+ a.width > 0 &&
989
+ a.height > 0 &&
990
+ b.width > 0 &&
991
+ b.height > 0 &&
992
+ a.x < b.x + b.width &&
993
+ a.x + a.width > b.x &&
994
+ a.y < b.y + b.height &&
995
+ a.y + a.height > b.y;
996
+ const occupiedRects: Array<ReturnType<typeof rectOf>> = [];
997
+ for (const id of preExistingFrameIds) {
998
+ const frame = merged.canvasFrames[id];
999
+ if (frame) occupiedRects.push(rectOf(frame));
1000
+ }
1001
+ // Keep arg placements for files we're not regenerating; the regenerated
1002
+ // ones are (re)placed below, so rebuild their entries here rather than
1003
+ // carry a pre-relocation position that would fail the isApplied check.
1004
+ const regeneratedFileIds = new Set(savedFiles.map((file) => file.id));
1005
+ const generationFrames = merged.placedFrames.filter(
1006
+ (placed) => !regeneratedFileIds.has(placed.fileId),
1007
+ );
1008
+ for (const placed of generationFrames) {
1009
+ occupiedRects.push(rectOf(placed.frame));
1010
+ }
1011
+ // Relocate target: right of every occupied (rotation-aware) rect.
1012
+ let nextX = occupiedRects.reduce(
1013
+ (right, rect) =>
1014
+ Math.max(right, rect.x + rect.width + GENERATED_FRAME_GAP),
1015
+ 0,
1016
+ );
871
1017
  for (const file of savedFiles) {
872
1018
  const source = files.find(
873
1019
  (candidate) => candidate.filename === file.filename,
@@ -875,18 +1021,50 @@ const generateDesignAction = defineAction({
875
1021
  if (!source || !isRenderableDesignFile(source)) continue;
876
1022
  const current = merged.canvasFrames[file.id] ?? {};
877
1023
  if (
1024
+ preExistingFrameIds.has(file.id) &&
878
1025
  current.x !== undefined &&
879
1026
  current.y !== undefined &&
880
1027
  current.width !== undefined &&
881
1028
  current.height !== undefined
882
1029
  ) {
1030
+ // An explicit device request resizes the primary frame to the
1031
+ // requested viewport (preserving position/rotation); otherwise a
1032
+ // regenerated screen keeps its exact stored geometry.
1033
+ if (devices && devices.length > 0) {
1034
+ merged.canvasFrames[file.id] = {
1035
+ ...current,
1036
+ width: viewport.width,
1037
+ height: viewport.height,
1038
+ };
1039
+ }
883
1040
  continue;
884
1041
  }
1042
+ const width = current.width ?? viewport.width;
1043
+ const height = current.height ?? viewport.height;
1044
+ let x = current.x ?? nextX;
1045
+ let y = current.y ?? 0;
1046
+ // Bump a new screen clear of everything if its requested/default spot
1047
+ // overlaps (e.g. a second screen defaulting to the same origin). The
1048
+ // candidate carries its own rotation so a rotated placement is tested
1049
+ // by its real footprint, not its unrotated rectangle.
1050
+ const candidateRect = rectOf({
1051
+ x,
1052
+ y,
1053
+ width,
1054
+ height,
1055
+ rotation: current.rotation,
1056
+ });
1057
+ if (
1058
+ occupiedRects.some((rect) => framesOverlap(candidateRect, rect))
1059
+ ) {
1060
+ x = nextX;
1061
+ y = 0;
1062
+ }
885
1063
  const frame = {
886
- x: current.x ?? nextX,
887
- y: current.y ?? 0,
888
- width: current.width ?? viewport.width,
889
- height: current.height ?? viewport.height,
1064
+ x,
1065
+ y,
1066
+ width,
1067
+ height,
890
1068
  z: current.z ?? generationFrames.length,
891
1069
  ...(current.rotation === undefined
892
1070
  ? {}
@@ -898,14 +1076,34 @@ const generateDesignAction = defineAction({
898
1076
  filename: file.filename,
899
1077
  frame,
900
1078
  });
901
- nextX = frame.x + frame.width + GENERATED_FRAME_GAP;
1079
+ occupiedRects.push(rectOf(frame));
1080
+ nextX = Math.max(nextX, frame.x + frame.width + GENERATED_FRAME_GAP);
902
1081
  }
903
1082
  mergedData.canvasFrames = merged.canvasFrames;
904
1083
  placedFrames = generationFrames;
905
- if (!hasBreakpointSet(mergedData.breakpointSet)) {
1084
+ // An explicit `devices` request is authoritative: replace the design's
1085
+ // breakpoint set with the derived one (or drop it for a single device),
1086
+ // so regenerating e.g. as [mobile,tablet,desktop] can't silently retain
1087
+ // a stale narrower set. Without explicit devices, only seed a default
1088
+ // set when none exists — a plain content regen never clobbers the
1089
+ // user's own breakpoints.
1090
+ if (devices && devices.length > 0) {
1091
+ if (generatedBreakpointSet.length > 0) {
1092
+ mergedData.breakpointSet = {
1093
+ id: "generated-responsive",
1094
+ breakpoints: generatedBreakpointSet,
1095
+ };
1096
+ } else {
1097
+ delete mergedData.breakpointSet;
1098
+ }
1099
+ mergedData.breakpointSetUpdatedAt = updatedAt;
1100
+ } else if (
1101
+ generatedBreakpointSet.length > 0 &&
1102
+ !hasBreakpointSet(mergedData.breakpointSet)
1103
+ ) {
906
1104
  mergedData.breakpointSet = {
907
1105
  id: "generated-responsive",
908
- breakpoints: DEFAULT_RESPONSIVE_BREAKPOINTS,
1106
+ breakpoints: generatedBreakpointSet,
909
1107
  };
910
1108
  mergedData.breakpointSetUpdatedAt = updatedAt;
911
1109
  }
@@ -940,7 +1138,32 @@ const generateDesignAction = defineAction({
940
1138
  );
941
1139
  }),
942
1140
  );
943
- return framesApplied && hasBreakpointSet(current.breakpointSet);
1141
+ // For an explicit `devices` request, verify the persisted breakpoint
1142
+ // widths actually match the requested set (not merely that some set
1143
+ // exists), so a partial/stale write is retried rather than accepted.
1144
+ const currentBreakpointWidths = (
1145
+ Array.isArray(
1146
+ (current.breakpointSet as { breakpoints?: unknown })?.breakpoints,
1147
+ )
1148
+ ? (
1149
+ current.breakpointSet as {
1150
+ breakpoints: Array<{ widthPx?: number }>;
1151
+ }
1152
+ ).breakpoints
1153
+ : []
1154
+ )
1155
+ .map((bp) => bp.widthPx)
1156
+ .filter((w): w is number => typeof w === "number")
1157
+ .sort((a, b) => a - b);
1158
+ const expectedBreakpointWidths = generatedBreakpointSet
1159
+ .map((bp) => bp.widthPx)
1160
+ .sort((a, b) => a - b);
1161
+ const breakpointSetApplied =
1162
+ devices && devices.length > 0
1163
+ ? jsonValuesEqual(currentBreakpointWidths, expectedBreakpointWidths)
1164
+ : generatedBreakpointSet.length === 0 ||
1165
+ hasBreakpointSet(current.breakpointSet);
1166
+ return framesApplied && breakpointSetApplied;
944
1167
  },
945
1168
  });
946
1169
 
@@ -34,7 +34,7 @@ const DEVICE_REGION_SIZE: Record<
34
34
  > = {
35
35
  mobile: { width: 390, height: 844 },
36
36
  tablet: { width: 768, height: 1024 },
37
- desktop: { width: 1440, height: 1024 },
37
+ desktop: { width: 1440, height: 900 },
38
38
  };
39
39
 
40
40
  function regionSizeForScreen(screen: {
@@ -135,10 +135,11 @@ const requestedScreenSchema = z
135
135
  .enum(["mobile", "tablet", "desktop"])
136
136
  .optional()
137
137
  .describe(
138
- "Intended viewport for this screen. Defaults to desktop-sized when " +
139
- "omitted pass 'mobile' for phone screens and 'tablet' for iPad-" +
140
- "width screens so the canvas region matches the content instead of " +
141
- "always sizing every screen as desktop.",
138
+ "Intended viewport for this DISTINCT screen. Defaults to desktop-sized " +
139
+ "when omitted. Use 'mobile' or 'tablet' only for a genuinely " +
140
+ "device-specific screen (e.g. a mobile-only page), NOT to create " +
141
+ "per-device copies of the same page — those are breakpoint frames of " +
142
+ "one document, chosen with generate-design's `devices` param.",
142
143
  ),
143
144
  width: z
144
145
  .number()
@@ -168,15 +169,20 @@ const requestedScreenSchema = z
168
169
 
169
170
  export default defineAction({
170
171
  description:
171
- "Start a multi-screen generation session on the Design canvas. " +
172
- "Use this before generating multiple screens or variations in parallel: it " +
173
- "assigns non-overlapping canvas regions and returns per-frame generation " +
174
- "instructions including canvasFrame placements. The session state is " +
175
- "agent-facing planning state consumed by generate-design and view-screen. " +
176
- "Pass each screen's deviceType ('mobile', 'tablet', or 'desktop') so its " +
177
- "canvas region matches the content a desktop dashboard should not be " +
178
- "boxed into a 390px mobile-width frame. Defaults to desktop when omitted; " +
179
- "pass explicit width/height instead for a non-standard viewport. " +
172
+ "Start a multi-screen generation session on the Design canvas for " +
173
+ "genuinely DISTINCT screens different pages or states such as Home, " +
174
+ "Dashboard, and Checkout each saved as its own screen file. " +
175
+ "Do NOT use this to model device responsiveness: mobile/tablet/desktop " +
176
+ "variants of the SAME page are breakpoint frames of one document, not " +
177
+ "separate screen files. Choose those device frames with generate-design's " +
178
+ "`devices` param instead of adding one screen per device here. " +
179
+ "It assigns non-overlapping canvas regions and returns per-frame " +
180
+ "generation instructions including canvasFrame placements. The session " +
181
+ "state is agent-facing planning state consumed by generate-design and " +
182
+ "view-screen. Set a screen's deviceType ('mobile', 'tablet', or 'desktop') " +
183
+ "only for a genuinely device-specific distinct screen (e.g. a mobile-only " +
184
+ "page) so its canvas region matches the content; it defaults to desktop, " +
185
+ "or pass explicit width/height for a non-standard viewport. " +
180
186
  "After this action, fan out calls to generate-design for each returned " +
181
187
  "frame, passing the returned canvasFrame values to generate-design so " +
182
188
  "screens appear in the infinite overview canvas.",
@@ -326,6 +332,37 @@ export default defineAction({
326
332
  .map((file) => file.filename)
327
333
  .filter((filename): filename is string => Boolean(filename)),
328
334
  );
335
+
336
+ // Region packing starts at x:0, so offset the whole batch past screens
337
+ // already on the board — otherwise a follow-up session stacks its screens
338
+ // on top of existing ones (generate-design's per-frame collision check is
339
+ // only a fallback; offsetting here keeps the batch's own layout intact).
340
+ const existingDesignRows = await db
341
+ .select({ data: schema.designs.data })
342
+ .from(schema.designs)
343
+ .where(eq(schema.designs.id, designId));
344
+ let boardRightEdge = 0;
345
+ try {
346
+ const raw = existingDesignRows[0]?.data;
347
+ const parsed = raw ? (JSON.parse(raw) as Record<string, unknown>) : {};
348
+ const frames = (parsed.canvasFrames ?? {}) as Record<
349
+ string,
350
+ { x?: number; width?: number }
351
+ >;
352
+ for (const frame of Object.values(frames)) {
353
+ const x = typeof frame?.x === "number" ? frame.x : 0;
354
+ const width = typeof frame?.width === "number" ? frame.width : 0;
355
+ boardRightEdge = Math.max(
356
+ boardRightEdge,
357
+ x + width + DEFAULT_ASSIGNED_REGION_GAP,
358
+ );
359
+ }
360
+ } catch {
361
+ // Malformed data — fall back to the origin.
362
+ }
363
+ if (boardRightEdge > 0) {
364
+ for (const region of regions) region.x += boardRightEdge;
365
+ }
329
366
  // Dedupe any filename (explicit or auto-generated) so two screens can
330
367
  // never resolve to the same target file, and so no target collides with
331
368
  // an existing screen — otherwise generate-design silently overwrites the
@@ -29,10 +29,13 @@ const MOBILE_HEIGHT = 844;
29
29
  const TABLET_WIDTH = 768;
30
30
  const TABLET_HEIGHT = 1024;
31
31
  const DESKTOP_WIDTH = 1440;
32
- const DESKTOP_HEIGHT = 1024;
33
- const DEFAULT_RESPONSIVE_BREAKPOINTS = [390, 768, 1440].map((widthPx) => ({
32
+ const DESKTOP_HEIGHT = 900;
33
+ // Desktop-base default: the primary/base frame is Desktop (1440), so the
34
+ // breakpoint set is Mobile only. The primary width is never included and no
35
+ // tablet is auto-added, matching generate-design's device derivation.
36
+ const DEFAULT_RESPONSIVE_BREAKPOINTS = [MOBILE_WIDTH].map((widthPx) => ({
34
37
  id: `generated-${widthPx}`,
35
- label: widthPx === 390 ? "Mobile" : widthPx === 768 ? "Tablet" : "Desktop",
38
+ label: "Mobile",
36
39
  widthPx,
37
40
  prefix: widthToPrefix(widthPx),
38
41
  }));
@@ -66,6 +66,7 @@ import { zoomBridgeScript } from "../../../.generated/bridge/zoom.generated";
66
66
  import { isTrustedCanvasBridgeMessage } from "./bridge-security";
67
67
  import { captureAnnotatedScreenshot } from "./design-canvas/annotation-snapshot";
68
68
  import { submitDesignAnnotations } from "./design-canvas/annotation-submit";
69
+ import { appendContentSizeReporter } from "./design-canvas/content-size-report";
69
70
  import {
70
71
  getScreenContentPointFromClient,
71
72
  getZoomToCursorScrollDelta,
@@ -2181,6 +2182,13 @@ export function DesignCanvas({
2181
2182
  ].join("");
2182
2183
  frameDocument = `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${frameStyle}</head><body>${iframeRenderContent}${bridgeToInject}</body></html>`;
2183
2184
  }
2185
+ // Overview frames report their own content height so the canvas can
2186
+ // content-fit them (Framer-style). Embedded (overview) frames only — a
2187
+ // focused single-screen view fills the viewport and must keep native
2188
+ // 100vh/min-h-screen, which the reporter's guard would otherwise pin.
2189
+ if (isEmbeddedFrame) {
2190
+ frameDocument = appendContentSizeReporter(frameDocument);
2191
+ }
2184
2192
  return injectSessionReplayIframeBootstrap(frameDocument);
2185
2193
  // editorChromeScaleX/Y are intentionally NOT deps: they only seed the initial
2186
2194
  // baked chrome scale. Live zoom updates flow through the set-editor-chrome-scale