@agent-native/core 0.187.0 → 0.187.1-nightly-20260925003003

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 (158) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +4 -3
  3. package/corpus/templates/analytics/changelog/2026-09-24-analytics-sidebar-footer-aligns-consistently.md +6 -0
  4. package/corpus/templates/analytics/changelog/2026-09-24-dashboard-emails-match-stacked-area-totals.md +6 -0
  5. package/corpus/templates/analytics/server/lib/report-chart-svg.ts +47 -20
  6. package/corpus/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx +1 -1
  7. package/corpus/templates/calendar/changelog/2026-09-24-guest-inputs-wrap-beneath-existing-guests-so-the-active-inpu.md +6 -0
  8. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +8 -3
  9. package/corpus/templates/clips/app/lib/recording-link.ts +21 -0
  10. package/corpus/templates/clips/app/routes/record.tsx +27 -8
  11. package/corpus/templates/clips/changelog/2026-09-24-the-camera-bubble-stays-visible-while-you-reposition-it-befo.md +6 -0
  12. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +7 -0
  13. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +63 -55
  14. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +251 -74
  15. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +66 -56
  16. package/corpus/templates/design/app/components/design/EditPanel.tsx +19 -0
  17. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +59 -9
  18. package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +4 -0
  19. package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +119 -0
  20. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +48 -51
  21. package/corpus/templates/design/app/components/design/edit-panel/style-change-types.ts +1 -0
  22. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +298 -187
  23. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +2 -2
  24. package/corpus/templates/design/app/components/design/inspector/index.ts +4 -0
  25. package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -2
  26. package/corpus/templates/design/app/i18n/en-US.ts +6 -0
  27. package/corpus/templates/design/app/pages/Present.tsx +101 -87
  28. package/corpus/templates/design/app/pages/design-editor/clone-and-pen-edit.ts +75 -14
  29. package/corpus/templates/design/app/pages/design-editor/commands/README.md +1 -0
  30. package/corpus/templates/design/app/pages/design-editor/commands/commit-relative-style-delta-to-selected-layers.ts +69 -56
  31. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +16 -0
  32. package/corpus/templates/design/app/pages/design-editor/commands/scale-selection.ts +17 -2
  33. package/corpus/templates/design/app/pages/design-editor/commands/styles-change.ts +23 -30
  34. package/corpus/templates/design/app/pages/design-editor/commands/swap-fill-stroke.ts +105 -0
  35. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +21 -0
  36. package/corpus/templates/design/app/pages/present-review-state.ts +7 -1
  37. package/corpus/templates/design/changelog/2026-09-24-a-vector-s-stroke-row-shows-position-beside-weight-instead-o.md +6 -0
  38. package/corpus/templates/design/changelog/2026-09-24-css-margins-can-be-adjusted-on-the-canvas-and-in-the-layout-.md +6 -0
  39. package/corpus/templates/design/changelog/2026-09-24-design-reviews-show-the-real-canvas-beside-its-conversation.md +6 -0
  40. package/corpus/templates/design/changelog/2026-09-24-double-clicking-text-or-a-vector-on-the-board-starts-editing.md +6 -0
  41. package/corpus/templates/design/changelog/2026-09-24-interact-opens-a-focused-responsive-preview-without-reloadin.md +6 -0
  42. package/corpus/templates/design/changelog/2026-09-24-keep-gradient-values-visible-and-limit-canvas-paints-to-supp.md +6 -0
  43. package/corpus/templates/design/changelog/2026-09-24-open-pen-paths-no-longer-paint-a-filled-triangle-after-shift.md +6 -0
  44. package/corpus/templates/design/changelog/2026-09-24-re-clicking-the-active-responsive-preview-returns-to-the-can.md +6 -0
  45. package/corpus/templates/design/changelog/2026-09-24-resized-vectors-can-be-point-edited-and-the-k-scale-tool-sho.md +6 -0
  46. package/corpus/templates/design/changelog/2026-09-24-while-editing-text-click-to-place-the-caret-and-double-click.md +6 -0
  47. package/corpus/templates/design/e2e/helpers.ts +19 -2
  48. package/corpus/templates/mail/app/components/email/ComposeModal.tsx +1 -1
  49. package/corpus/templates/mail/changelog/2026-09-24-gmail-push-ack.md +6 -0
  50. package/corpus/templates/mail/changelog/2026-09-24-the-composer-keeps-its-taller-layout-in-a-narrower-window.md +6 -0
  51. package/corpus/templates/mail/server/db/schema.ts +26 -1
  52. package/corpus/templates/mail/server/lib/inbox-store.ts +68 -23
  53. package/corpus/templates/mail/server/lib/inbox-sync.ts +43 -17
  54. package/corpus/templates/mail/server/plugins/db.ts +19 -0
  55. package/corpus/templates/mail/server/routes/api/gmail/push.post.ts +4 -15
  56. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +45 -30
  57. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +18 -2
  58. package/corpus/templates/slides/actions/export-html.ts +28 -17
  59. package/corpus/templates/slides/app/hooks/use-deck-design-system.ts +27 -15
  60. package/corpus/templates/slides/app/pages/DeckEditor.tsx +1 -1
  61. package/corpus/templates/slides/changelog/2026-09-23-decks-with-no-design-system-now-follow-a-theme-chosen-for-th.md +6 -0
  62. package/corpus/templates/slides/shared/slide-background.ts +3 -1
  63. package/dist/agent/engine/builder-engine.js +11 -0
  64. package/dist/agent/engine/types.d.ts +1 -0
  65. package/dist/agent/production-agent.d.ts +2 -0
  66. package/dist/agent/production-agent.js +32 -4
  67. package/dist/agent/run-loop-with-resume.js +6 -0
  68. package/dist/cli/index.js +4 -4
  69. package/dist/cli/process.d.ts +10 -1
  70. package/dist/cli/process.js +66 -0
  71. package/dist/cli/workspace-dev.js +2 -0
  72. package/dist/client/agent-chat.js +13 -14
  73. package/dist/client/error-format.js +4 -4
  74. package/dist/client/observability/ObservabilityDashboard.js +95 -65
  75. package/dist/client/observability/OutputPreview.d.ts +2 -1
  76. package/dist/client/observability/OutputPreview.js +64 -7
  77. package/dist/client/observability/useObservability.d.ts +2 -1
  78. package/dist/client/observability/useObservability.js +5 -1
  79. package/dist/client/settings/UsageSection.js +105 -17
  80. package/dist/collab/routes.d.ts +1 -1
  81. package/dist/collab/struct-routes.d.ts +1 -1
  82. package/dist/db/client.d.ts +44 -0
  83. package/dist/db/client.js +62 -3
  84. package/dist/db/create-get-db.js +23 -1
  85. package/dist/db/embedded-runtime.d.ts +21 -0
  86. package/dist/db/embedded-runtime.js +27 -0
  87. package/dist/db/migrations.js +2 -0
  88. package/dist/db/server-runtime.d.ts +22 -0
  89. package/dist/db/server-runtime.js +26 -0
  90. package/dist/deploy/build.d.ts +29 -0
  91. package/dist/deploy/build.js +204 -121
  92. package/dist/feature-flags/index.d.ts +1 -1
  93. package/dist/feature-flags/index.js +1 -1
  94. package/dist/feature-flags/registry.d.ts +1 -0
  95. package/dist/feature-flags/registry.js +5 -0
  96. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  97. package/dist/index.d.ts +1 -1
  98. package/dist/index.js +1 -1
  99. package/dist/integrations/webhook-handler.js +4 -1
  100. package/dist/jobs/background-automation-runner.js +6 -1
  101. package/dist/localization/core-messages/ar-SA.js +9 -1
  102. package/dist/localization/core-messages/de-DE.js +9 -1
  103. package/dist/localization/core-messages/en-US.d.ts +9 -1
  104. package/dist/localization/core-messages/en-US.js +9 -1
  105. package/dist/localization/core-messages/es-ES.js +9 -1
  106. package/dist/localization/core-messages/fr-FR.js +9 -1
  107. package/dist/localization/core-messages/hi-IN.js +9 -1
  108. package/dist/localization/core-messages/ja-JP.js +9 -1
  109. package/dist/localization/core-messages/ko-KR.js +9 -1
  110. package/dist/localization/core-messages/pt-BR.js +9 -1
  111. package/dist/localization/core-messages/supplemental/ar-SA.js +1 -0
  112. package/dist/localization/core-messages/supplemental/de-DE.js +1 -0
  113. package/dist/localization/core-messages/supplemental/en-US.js +1 -0
  114. package/dist/localization/core-messages/supplemental/es-ES.js +1 -0
  115. package/dist/localization/core-messages/supplemental/fr-FR.js +1 -0
  116. package/dist/localization/core-messages/supplemental/hi-IN.js +1 -0
  117. package/dist/localization/core-messages/supplemental/ja-JP.js +1 -0
  118. package/dist/localization/core-messages/supplemental/ko-KR.js +1 -0
  119. package/dist/localization/core-messages/supplemental/pt-BR.js +1 -0
  120. package/dist/localization/core-messages/supplemental/zh-CN.js +1 -0
  121. package/dist/localization/core-messages/supplemental/zh-TW.js +1 -0
  122. package/dist/localization/core-messages/zh-CN.js +9 -1
  123. package/dist/localization/core-messages/zh-TW.js +9 -1
  124. package/dist/localization/core-messages.d.ts +9 -1
  125. package/dist/localization/mcp-settings-messages.d.ts +1 -0
  126. package/dist/mcp/connect-route.js +45 -8
  127. package/dist/mcp/connect-store.d.ts +7 -5
  128. package/dist/mcp/connect-store.js +30 -39
  129. package/dist/notifications/routes.d.ts +3 -3
  130. package/dist/observability/actions/get-observability-review-detail.d.ts +7 -0
  131. package/dist/observability/actions/get-observability-review-detail.js +25 -0
  132. package/dist/observability/reviews.d.ts +9 -1
  133. package/dist/observability/reviews.js +20 -0
  134. package/dist/observability/routes.d.ts +3 -3
  135. package/dist/observability/types.d.ts +8 -0
  136. package/dist/org/handlers.d.ts +1 -1
  137. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  138. package/dist/provider-api/actions/provider-api.d.ts +9 -9
  139. package/dist/scripts/db/query.js +6 -1
  140. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  141. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  142. package/dist/server/agent-teams.js +4 -1
  143. package/dist/server/auth.js +60 -20
  144. package/dist/server/better-auth-instance.js +7 -1
  145. package/dist/server/core-routes-plugin.d.ts +1 -1
  146. package/dist/server/core-routes-plugin.js +2 -0
  147. package/dist/server/embedded.js +6 -0
  148. package/dist/server/framework-request-handler.js +6 -0
  149. package/dist/server/realtime-token.d.ts +1 -1
  150. package/dist/usage/actions/get-usage-metrics.js +4 -1
  151. package/dist/usage/metrics-store.d.ts +19 -0
  152. package/dist/usage/metrics-store.js +106 -14
  153. package/dist/usage/store.d.ts +7 -2
  154. package/dist/usage/store.js +25 -4
  155. package/dist/vite/action-types-plugin.js +4 -0
  156. package/dist/vite/client.d.ts +6 -1
  157. package/dist/vite/client.js +40 -1
  158. package/package.json +4 -3
@@ -16,7 +16,7 @@ export interface StylesChangeArgs {
16
16
  styles: Record<string, string>,
17
17
  ) => boolean;
18
18
  commitRelativeStyleDeltaToSelectedLayers: (
19
- property: string,
19
+ property: string | string[],
20
20
  operation: number | ScrubRelativeExpression,
21
21
  phase?: StyleChangeMeta["phase"],
22
22
  ) => boolean;
@@ -78,7 +78,7 @@ export function runStylesChange(
78
78
  if (meta?.capturedStyleTargets && meta.phase !== "preview") {
79
79
  commitCapturedStyleTargets(
80
80
  Object.fromEntries(
81
- Object.entries(styles).filter(([, value]) => Boolean(value)),
81
+ Object.entries(styles).filter(([, value]) => value !== undefined),
82
82
  ),
83
83
  meta.capturedStyleTargets,
84
84
  meta.interactionState,
@@ -115,16 +115,29 @@ export function runStylesChange(
115
115
  return;
116
116
  }
117
117
  const selector = selectedElement?.selector ?? "body";
118
- const entries = Object.entries(styles).filter(([, value]) => Boolean(value));
118
+ const entries = Object.entries(styles).filter(
119
+ ([, value]) => value !== undefined,
120
+ );
119
121
  if (entries.length === 0) return;
120
- if (meta?.relativeExpression && entries.length === 1) {
121
- const [property] = entries[0]!;
122
- commitRelativeStyleDeltaToSelectedLayers(
123
- property,
124
- meta.relativeExpression,
125
- meta.phase,
122
+ const relativeProperties =
123
+ meta?.relativeDeltaProperties ??
124
+ (entries.length === 1 ? [entries[0]![0]] : []);
125
+ const relativeOperation = meta?.relativeExpression ?? meta?.relativeDelta;
126
+ if (
127
+ relativeOperation !== undefined &&
128
+ relativeProperties.length > 0 &&
129
+ relativeProperties.every((property) =>
130
+ entries.some(([entryProperty]) => entryProperty === property),
131
+ )
132
+ ) {
133
+ const applied = commitRelativeStyleDeltaToSelectedLayers(
134
+ relativeProperties.length === 1
135
+ ? relativeProperties[0]!
136
+ : relativeProperties,
137
+ relativeOperation,
138
+ meta?.phase,
126
139
  );
127
- return;
140
+ if (meta?.relativeExpression || applied) return;
128
141
  }
129
142
  const target = styleWriteTarget({ selector, selectedElement });
130
143
  // T10: mirror handleStyleChange's text-range routing here. Without
@@ -169,26 +182,6 @@ export function runStylesChange(
169
182
  }
170
183
  return;
171
184
  }
172
- // Mixed-value arrow-step parity (item 7): see handleStyleChange's
173
- // matching comment for the full defensive-read rationale. A relative
174
- // delta is inherently single-valued (one scrub gesture on one field),
175
- // so this only applies when the batched patch has exactly one entry —
176
- // a multi-property patch (e.g. a shadow popover's X+Y+blur+spread all
177
- // at once) has no single delta to apply per-node and falls through to
178
- // the existing absolute-value paths unchanged.
179
- const relativeDelta = (meta as { relativeDelta?: number } | undefined)
180
- ?.relativeDelta;
181
- if (typeof relativeDelta === "number" && entries.length === 1) {
182
- const [singleProperty] = entries[0]!;
183
- if (
184
- commitRelativeStyleDeltaToSelectedLayers(
185
- singleProperty,
186
- relativeDelta,
187
- meta?.phase,
188
- )
189
- )
190
- return;
191
- }
192
185
  if (
193
186
  selectedElement &&
194
187
  commitStylesToSelectedLayers(Object.fromEntries(entries), meta?.phase)
@@ -0,0 +1,105 @@
1
+ import { parseCssColor, rgbaToCss } from "@shared/color-utils";
2
+
3
+ import { isVectorShapeElement } from "@/components/design/edit-panel/element-classification";
4
+ import type { ElementInfo } from "@/components/design/types";
5
+
6
+ export interface SwapFillStrokeArgs {
7
+ canEditDesign: boolean;
8
+ selectedElement: ElementInfo | null;
9
+ handleStylesChange: (styles: Record<string, string>) => void;
10
+ }
11
+
12
+ function hasPaint(value: string): boolean {
13
+ const normalized = value.trim().toLowerCase();
14
+ if (!normalized || normalized === "none" || normalized === "transparent") {
15
+ return false;
16
+ }
17
+ const color = parseCssColor(value);
18
+ return color ? color.a > 0 : true;
19
+ }
20
+
21
+ function swappedPaint(
22
+ paint: string,
23
+ opacity: string | undefined,
24
+ ): { paint: string; opacity: string } {
25
+ const alpha = Number.parseFloat(opacity ?? "1");
26
+ if (!Number.isFinite(alpha) || alpha >= 1) return { paint, opacity: "" };
27
+ const color = parseCssColor(paint);
28
+ if (color) {
29
+ return { paint: rgbaToCss({ ...color, a: color.a * alpha }), opacity: "" };
30
+ }
31
+ // A gradient value has nowhere to carry alpha, so it keeps its opacity.
32
+ return { paint, opacity: String(alpha) };
33
+ }
34
+
35
+ /**
36
+ * Figma's Shift+X. Matches Figma even when one side is empty: an element with
37
+ * a fill and no stroke ends up with a stroke and no fill (not a no-op). Both
38
+ * sides commit together so the swap is a single undo step.
39
+ */
40
+ export function runSwapFillStroke({
41
+ canEditDesign,
42
+ selectedElement,
43
+ handleStylesChange,
44
+ }: SwapFillStrokeArgs): void {
45
+ if (!canEditDesign || !selectedElement) return;
46
+ if (isVectorShapeElement(selectedElement)) {
47
+ const styles = selectedElement.computedStyles;
48
+ // SVG gradient styles render as url(#id), but the editor's authored
49
+ // custom properties hold the portable gradient values. Capture both
50
+ // before committing either side so Shift+X can rebuild each definition
51
+ // without leaving the other paint pointing at a removed id.
52
+ const fill =
53
+ selectedElement.inlineStyles?.["--an-vector-fill-gradient"] ||
54
+ styles["--an-vector-fill-gradient"] ||
55
+ styles.fill ||
56
+ "";
57
+ const stroke =
58
+ selectedElement.inlineStyles?.["--an-vector-stroke-gradient"] ||
59
+ styles["--an-vector-stroke-gradient"] ||
60
+ styles.stroke ||
61
+ "";
62
+ const fillHasPaint = hasPaint(fill);
63
+ const strokeWidth = Number.parseFloat(styles.strokeWidth ?? "0");
64
+ const strokeOpacity = Number.parseFloat(styles.strokeOpacity ?? "1");
65
+ const strokeHasPaint =
66
+ hasPaint(stroke) && strokeWidth > 0 && strokeOpacity > 0;
67
+ if (!fillHasPaint && !strokeHasPaint) return;
68
+ // Opacity rides in the colours where it can: an inline fill-opacity
69
+ // would override the fill-opacity="0" that keeps an open path's chord
70
+ // unpainted. An empty value removes the old declaration.
71
+ const nextFill = strokeHasPaint
72
+ ? swappedPaint(stroke, styles.strokeOpacity)
73
+ : { paint: "none", opacity: "" };
74
+ const nextStroke = fillHasPaint
75
+ ? swappedPaint(fill, styles.fillOpacity)
76
+ : { paint: "none", opacity: "" };
77
+ // Removing only the inline value would leave an imported SVG's
78
+ // presentation opacity multiplying the swapped colour.
79
+ const opacityFor = (
80
+ next: { paint: string; opacity: string },
81
+ current: string | undefined,
82
+ ) =>
83
+ next.opacity === "" &&
84
+ next.paint !== "none" &&
85
+ Number.parseFloat(current ?? "1") < 1
86
+ ? "1"
87
+ : next.opacity;
88
+ const fillOpacity = opacityFor(nextFill, styles.fillOpacity);
89
+ const strokeOpacityValue = opacityFor(nextStroke, styles.strokeOpacity);
90
+ handleStylesChange({
91
+ fill: nextFill.paint,
92
+ fillOpacity,
93
+ stroke: nextStroke.paint,
94
+ strokeOpacity: strokeOpacityValue,
95
+ ...(fillHasPaint && !strokeHasPaint ? { strokeWidth: "1px" } : {}),
96
+ });
97
+ return;
98
+ }
99
+ const currentFill = selectedElement.computedStyles.backgroundColor ?? "";
100
+ const currentStroke = selectedElement.computedStyles.borderColor ?? "";
101
+ handleStylesChange({
102
+ backgroundColor: currentStroke || "transparent",
103
+ borderColor: currentFill || "transparent",
104
+ });
105
+ }
@@ -1764,6 +1764,27 @@ export function applyScopedVisualStyleEdit(args: {
1764
1764
  source,
1765
1765
  } = args;
1766
1766
  const normalizedProperty = normalizeCssPropertyName(property);
1767
+ // An empty value clears the declaration at the edit's own scope.
1768
+ if (value === "") {
1769
+ return applyVisualEdit(
1770
+ content,
1771
+ upperBoundPx == null
1772
+ ? {
1773
+ kind: "style",
1774
+ operation: "remove",
1775
+ target,
1776
+ property: normalizedProperty,
1777
+ }
1778
+ : {
1779
+ kind: "breakpoint-style",
1780
+ operation: "remove",
1781
+ target,
1782
+ maxWidthPx: upperBoundPx,
1783
+ property: normalizedProperty,
1784
+ },
1785
+ { source },
1786
+ );
1787
+ }
1767
1788
  if (upperBoundPx != null && isVectorEndpointProperty(normalizedProperty)) {
1768
1789
  // Endpoint values require marker definitions and shape attributes, which
1769
1790
  // cannot be represented by a media-scoped custom property. Let the shared
@@ -5,9 +5,15 @@ export interface PresentReviewState {
5
5
 
6
6
  export function resolvePresentEscapeAction(
7
7
  state: PresentReviewState,
8
- ): "close-comments" | "defer-to-comment-mode" | "exit-presentation" {
8
+ reviewEmbed = false,
9
+ ):
10
+ | "close-comments"
11
+ | "defer-to-comment-mode"
12
+ | "stay-in-review-embed"
13
+ | "exit-presentation" {
9
14
  if (state.commentsOpen) return "close-comments";
10
15
  if (state.commentMode) return "defer-to-comment-mode";
16
+ if (reviewEmbed) return "stay-in-review-embed";
11
17
  return "exit-presentation";
12
18
  }
13
19
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-09-24
4
+ ---
5
+
6
+ A vector's stroke row shows Position beside Weight instead of a lone Weight field.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-09-24
4
+ ---
5
+
6
+ CSS margins can be adjusted on the canvas and in the layout inspector.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-09-24
4
+ ---
5
+
6
+ Design reviews show the real canvas beside its conversation.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-24
4
+ ---
5
+
6
+ Double-clicking text or a vector on the board starts editing it, and clicking empty canvas deselects it.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-24
4
+ ---
5
+
6
+ Interact opens a focused responsive preview without reloading the running design.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-24
4
+ ---
5
+
6
+ Keep gradient values visible and limit canvas paints to supported backgrounds
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-24
4
+ ---
5
+
6
+ Open pen paths no longer paint a filled triangle after Shift+X, matching Figma.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-24
4
+ ---
5
+
6
+ Re-clicking the active responsive preview returns to the canvas even with pending edits
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-09-24
4
+ ---
5
+
6
+ Resized vectors can be point-edited, and the K scale tool shows a Scale section with a live factor.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-24
4
+ ---
5
+
6
+ While editing text, click to place the caret and double-click a word to restyle just that word.
@@ -457,6 +457,12 @@ export async function enterInteractView(
457
457
  "data-screen-interact-mode",
458
458
  "false",
459
459
  );
460
+ const previewIframe = screenShell
461
+ .locator(DESIGN_PREVIEW_IFRAME_SELECTOR)
462
+ .first();
463
+ await expect(previewIframe).toBeVisible();
464
+ const previewIframeHandle = await previewIframe.elementHandle();
465
+ if (!previewIframeHandle) throw new Error("screen preview iframe is missing");
460
466
  // The screen card can extend beneath the fixed inspector at narrow canvas
461
467
  // widths; invoke the button without relying on the panel's overlapping
462
468
  // physical hit area.
@@ -467,8 +473,19 @@ export async function enterInteractView(
467
473
  "data-screen-interact-mode",
468
474
  "true",
469
475
  );
470
- // Interact is a responsive view of the same editor. Rails and the screen
471
- // shell stay mounted; assert the view's own device preview below instead.
476
+ await expect(
477
+ page.getByRole("button", { name: "Exit responsive preview" }),
478
+ ).toBeVisible();
479
+ expect(
480
+ await previewIframeHandle.evaluate((before) =>
481
+ Boolean(
482
+ before
483
+ .closest("[data-screen-shell]")
484
+ ?.querySelector("iframe[data-design-preview-iframe]") === before,
485
+ ),
486
+ ),
487
+ ).toBe(true);
488
+ // The focused responsive preview keeps its original iframe in the DOM.
472
489
  await expect
473
490
  .poll(
474
491
  async () =>
@@ -843,7 +843,7 @@ export function ComposeModal({
843
843
  ? "bottom-0 h-11 rounded-t-xl sm:w-[540px]"
844
844
  : isExpanded
845
845
  ? "top-0 bottom-0 h-auto rounded-none sm:top-4 sm:bottom-4 sm:w-[min(960px,calc(100vw-var(--compose-right)-1rem))] sm:rounded-xl"
846
- : "bottom-0 h-[100dvh] sm:h-[min(540px,_calc(100dvh_-_2rem))] sm:w-[min(800px,_calc(100vw_-_var(--compose-right)_-_1rem))] sm:rounded-xl",
846
+ : "bottom-0 h-[100dvh] sm:h-[min(540px,_calc(100dvh_-_2rem))] md:w-[min(490px,_calc(100vw_-_var(--compose-right)_-_1rem))] sm:rounded-xl",
847
847
  )}
848
848
  data-mail-compose
849
849
  style={composeStyle}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-24
4
+ ---
5
+
6
+ Gmail inboxes refresh reliably when push notifications are delayed.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-09-24
4
+ ---
5
+
6
+ The composer keeps its taller layout in a narrower window.
@@ -1,4 +1,10 @@
1
- import { table, text, integer, index } from "@agent-native/core/db/schema";
1
+ import {
2
+ bigint,
3
+ index,
4
+ integer,
5
+ table,
6
+ text,
7
+ } from "@agent-native/core/db/schema";
2
8
 
3
9
  /**
4
10
  * Short-lived, owner-scoped continuation state for the external Mail
@@ -111,6 +117,9 @@ export const mailSyncAccounts = table(
111
117
  .default("idle"),
112
118
  lastError: text("last_error"),
113
119
  lastSyncedAt: integer("last_synced_at"),
120
+ lastPushGeneration: bigint("last_push_generation", { mode: "number" })
121
+ .notNull()
122
+ .default(0),
114
123
  syncClaimId: text("sync_claim_id"),
115
124
  syncClaimedAt: integer("sync_claimed_at"),
116
125
  // Compact cached labels.list result: [{id,name,type,color?,messagesTotal?,
@@ -123,6 +132,22 @@ export const mailSyncAccounts = table(
123
132
  (t) => [index("mail_sync_accounts_owner_idx").on(t.ownerEmail)],
124
133
  );
125
134
 
135
+ export const mailInboxPushInvalidations = table(
136
+ "mail_inbox_push_invalidations",
137
+ {
138
+ id: text("id").primaryKey(),
139
+ ownerEmail: text("owner_email").notNull(),
140
+ accountEmail: text("account_email").notNull(),
141
+ generation: bigint("generation", { mode: "number" }).notNull().default(1),
142
+ },
143
+ (t) => [
144
+ index("mail_inbox_push_invalidations_owner_account_idx").on(
145
+ t.ownerEmail,
146
+ t.accountEmail,
147
+ ),
148
+ ],
149
+ );
150
+
126
151
  /**
127
152
  * SQL mirror of each connected account's INBOX threads, kept fresh by
128
153
  * `server/lib/inbox-sync.ts`. Metadata only — no bodies, no HTML.
@@ -69,6 +69,7 @@ export type SyncAccountRow = {
69
69
  status: "idle" | "syncing" | "error" | "needs_reauth";
70
70
  lastError: string | null;
71
71
  lastSyncedAt: number | null;
72
+ lastPushGeneration: number;
72
73
  syncClaimId: string | null;
73
74
  syncClaimedAt: number | null;
74
75
  labels: CachedGmailLabel[] | null;
@@ -144,6 +145,7 @@ function toSyncAccountRow(
144
145
  status: row.status as SyncAccountRow["status"],
145
146
  lastError: row.lastError,
146
147
  lastSyncedAt: row.lastSyncedAt,
148
+ lastPushGeneration: row.lastPushGeneration,
147
149
  syncClaimId: row.syncClaimId,
148
150
  syncClaimedAt: row.syncClaimedAt,
149
151
  labels: row.labelsJson
@@ -631,6 +633,7 @@ export type ThreadUpsertInput = {
631
633
 
632
634
  export async function upsertInboxThreadRows(
633
635
  rows: ThreadUpsertInput[],
636
+ db: any = getDb(),
634
637
  ): Promise<void> {
635
638
  if (rows.length === 0) return;
636
639
  const now = Date.now();
@@ -670,7 +673,7 @@ export async function upsertInboxThreadRows(
670
673
  localMutationFields: null,
671
674
  }));
672
675
 
673
- await getDb()
676
+ await db
674
677
  .insert(schema.mailInboxThreads)
675
678
  .values(values)
676
679
  .onConflictDoUpdate({
@@ -747,6 +750,7 @@ export async function deleteInboxThreadRow(
747
750
  accountEmail: string,
748
751
  threadId: string,
749
752
  readStartedAt?: number,
753
+ db: any = getDb(),
750
754
  ): Promise<void> {
751
755
  const conditions = [
752
756
  eq(
@@ -757,9 +761,7 @@ export async function deleteInboxThreadRow(
757
761
  if (readStartedAt !== undefined) {
758
762
  conditions.push(lt(schema.mailInboxThreads.updatedAt, readStartedAt));
759
763
  }
760
- await getDb()
761
- .delete(schema.mailInboxThreads)
762
- .where(and(...conditions));
764
+ await db.delete(schema.mailInboxThreads).where(and(...conditions));
763
765
  }
764
766
 
765
767
  /**
@@ -770,8 +772,9 @@ export async function markThreadsOutOfInboxBeforeSync(
770
772
  ownerEmail: string,
771
773
  accountEmail: string,
772
774
  cutoffSyncedAt: number,
775
+ db: any = getDb(),
773
776
  ): Promise<void> {
774
- await getDb()
777
+ await db
775
778
  .update(schema.mailInboxThreads)
776
779
  .set({ inInbox: 0, updatedAt: Date.now() })
777
780
  .where(
@@ -827,26 +830,25 @@ export class SyncClaimLostError extends Error {
827
830
  }
828
831
  }
829
832
 
830
- /**
831
- * Guards a batch of row writes (`upsertInboxThreadRows`,
832
- * `markThreadsOutOfInboxBeforeSync`, `deleteInboxThreadRow`) that have no
833
- * `sync_claim_id` column of their own to fence against, unlike
834
- * {@link patchSyncAccount}'s `opts.claimId`. One SELECT immediately before
835
- * the write; throws {@link SyncClaimLostError} when the claim no longer
836
- * matches instead of letting a lapsed worker overwrite a newer worker's rows.
837
- */
838
- export async function assertSyncClaimHeld(
833
+ export async function withSyncClaim<T>(
839
834
  ownerEmail: string,
840
835
  accountEmail: string,
841
836
  claimId: string,
842
- ): Promise<void> {
843
- const rows = await getDb()
844
- .select({ syncClaimId: schema.mailSyncAccounts.syncClaimId })
845
- .from(schema.mailSyncAccounts)
846
- .where(eq(schema.mailSyncAccounts.id, rowId(ownerEmail, accountEmail)))
847
- .limit(1);
848
- if (rows[0]?.syncClaimId !== claimId)
849
- throw new SyncClaimLostError(accountEmail);
837
+ write: (tx: any) => Promise<T>,
838
+ ): Promise<T> {
839
+ return getDb().transaction(async (tx: any) => {
840
+ const rows = await tx
841
+ .select({ syncClaimId: schema.mailSyncAccounts.syncClaimId })
842
+ .from(schema.mailSyncAccounts)
843
+ .where(eq(schema.mailSyncAccounts.id, rowId(ownerEmail, accountEmail)))
844
+ .for("update")
845
+ .limit(1);
846
+ if (rows[0]?.syncClaimId !== claimId)
847
+ throw new SyncClaimLostError(accountEmail);
848
+
849
+ // Keep row mutations inside the lock so replacement claims cannot interleave.
850
+ return write(tx);
851
+ });
850
852
  }
851
853
 
852
854
  /**
@@ -917,12 +919,55 @@ export type SyncAccountPatch = Partial<{
917
919
  status: SyncAccountRow["status"];
918
920
  lastError: string | null;
919
921
  lastSyncedAt: number | null;
922
+ lastPushGeneration: number;
920
923
  syncClaimId: string | null;
921
924
  syncClaimedAt: number | null;
922
925
  labels: CachedGmailLabel[];
923
926
  labelsUpdatedAt: number;
924
927
  }>;
925
928
 
929
+ export async function recordInboxPushInvalidation(
930
+ ownerEmail: string,
931
+ accountEmail: string,
932
+ ): Promise<void> {
933
+ await getDb()
934
+ .insert(schema.mailInboxPushInvalidations)
935
+ .values({
936
+ id: rowId(ownerEmail, accountEmail),
937
+ ownerEmail: ownerEmail.toLowerCase(),
938
+ accountEmail: accountEmail.toLowerCase(),
939
+ generation: 1,
940
+ })
941
+ .onConflictDoUpdate({
942
+ target: schema.mailInboxPushInvalidations.id,
943
+ set: {
944
+ generation: sql`${schema.mailInboxPushInvalidations.generation} + 1`,
945
+ },
946
+ });
947
+ }
948
+
949
+ export async function readInboxPushGeneration(
950
+ ownerEmail: string,
951
+ accountEmail: string,
952
+ ): Promise<number> {
953
+ const rows = await getDb()
954
+ .select({ generation: schema.mailInboxPushInvalidations.generation })
955
+ .from(schema.mailInboxPushInvalidations)
956
+ .where(
957
+ and(
958
+ eq(
959
+ schema.mailInboxPushInvalidations.ownerEmail,
960
+ ownerEmail.toLowerCase(),
961
+ ),
962
+ eq(
963
+ schema.mailInboxPushInvalidations.accountEmail,
964
+ accountEmail.toLowerCase(),
965
+ ),
966
+ ),
967
+ );
968
+ return rows.reduce((total, row) => total + row.generation, 0);
969
+ }
970
+
926
971
  /**
927
972
  * Updates one sync-account row. When `opts.claimId` is given, the write is
928
973
  * fenced with `AND sync_claim_id = ?` and the return value says whether a row
@@ -960,7 +1005,7 @@ export async function patchSyncAccount(
960
1005
  * running against the reset row keeps its claim, and a later fenced write
961
1006
  * from that same stale run would succeed and could restore the old
962
1007
  * watermark. Clearing the claim here means that worker's next fenced write
963
- * (via `patchSyncAccount`'s `opts.claimId` or `assertSyncClaimHeld`) raises
1008
+ * (via `patchSyncAccount`'s `opts.claimId` or `withSyncClaim`) raises
964
1009
  * {@link SyncClaimLostError} and stops it instead.
965
1010
  */
966
1011
  export async function resetSyncAccountProgress(