@agent-native/core 0.131.2 → 0.131.4

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 (100) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +34 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/types.ts +6 -0
  5. package/corpus/core/src/action.ts +13 -0
  6. package/corpus/core/src/client/AgentPanel.tsx +58 -34
  7. package/corpus/core/src/localization/default-messages.ts +6 -0
  8. package/corpus/core/src/server/agent-capabilities.ts +43 -8
  9. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +63 -1
  10. package/corpus/templates/analytics/AGENTS.md +5 -0
  11. package/corpus/templates/analytics/actions/hubspot-deals.ts +1 -0
  12. package/corpus/templates/analytics/actions/hubspot-records.ts +1 -0
  13. package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +2 -0
  14. package/corpus/templates/calendar/actions/get-event.ts +1 -0
  15. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +1 -0
  16. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +7 -3
  17. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +14 -19
  18. package/corpus/templates/calendar/app/hooks/use-events.ts +7 -0
  19. package/corpus/templates/calendar/app/lib/event-form-utils.ts +13 -0
  20. package/corpus/templates/calendar/app/pages/CalendarView.tsx +21 -14
  21. package/corpus/templates/calendar/changelog/2026-07-29-unnamed-events-now-use-a-title-placeholder.md +6 -0
  22. package/corpus/templates/calendar/server/lib/google-calendar.ts +2 -0
  23. package/corpus/templates/calendar/server/lib/ical-fetcher.ts +1 -0
  24. package/corpus/templates/calendar/shared/api.ts +2 -0
  25. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +3 -1
  26. package/corpus/templates/design/actions/get-design.ts +11 -3
  27. package/corpus/templates/design/actions/present-design-variants.ts +62 -5
  28. package/corpus/templates/design/app/components/design/BreakpointBar.tsx +64 -1
  29. package/corpus/templates/design/app/components/design/EditPanel.tsx +199 -25
  30. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +160 -46
  31. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +7 -1
  32. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +59 -12
  33. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +1 -1
  34. package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +34 -29
  35. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +2 -2
  36. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +28 -10
  37. package/corpus/templates/design/app/components/design/inspector/design-icons.tsx +15 -28
  38. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +4 -3
  39. package/corpus/templates/design/app/components/design/multi-screen/chrome-transitions.ts +12 -8
  40. package/corpus/templates/design/app/components/design/multi-screen/culling.ts +34 -9
  41. package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +13 -27
  42. package/corpus/templates/design/app/components/design/multi-screen/types.ts +8 -1
  43. package/corpus/templates/design/app/components/design/multi-screen/zoom-gesture.ts +95 -0
  44. package/corpus/templates/design/app/i18n-data.ts +121 -10
  45. package/corpus/templates/design/app/pages/design-editor/data-operations.ts +45 -0
  46. package/corpus/templates/design/app/pages/design-editor/layout-operations.ts +59 -0
  47. package/corpus/templates/design/app/pages/design-editor/text-edit-utils.ts +115 -40
  48. package/corpus/templates/design/changelog/2026-07-28-generated-design-variants-no-longer-overlap-each-other-on-th.md +6 -0
  49. package/corpus/templates/design/changelog/2026-07-29-auto-layout-direction-icons-show-an-arrow.md +6 -0
  50. package/corpus/templates/design/changelog/2026-07-29-breakpoints-no-longer-overlap-neighbouring-screens.md +6 -0
  51. package/corpus/templates/design/changelog/2026-07-29-canvas-is-more-responsive-when-selecting-and-adding-frames.md +6 -0
  52. package/corpus/templates/design/changelog/2026-07-29-canvas-zoom-is-smoother-and-labels-hold-their-size.md +6 -0
  53. package/corpus/templates/design/changelog/2026-07-29-double-click-a-frame-now-drills-into-its-layers.md +6 -0
  54. package/corpus/templates/design/changelog/2026-07-29-export-preview-shows-the-real-render.md +6 -0
  55. package/corpus/templates/design/changelog/2026-07-29-figma-parity-shortcuts.md +6 -0
  56. package/corpus/templates/design/changelog/2026-07-29-more-breakpoint-width-presets.md +6 -0
  57. package/corpus/templates/design/changelog/2026-07-29-resize-a-selected-frame-to-a-device-preset.md +6 -0
  58. package/corpus/templates/design/changelog/2026-07-29-text-tool-reaches-the-right-surface.md +6 -0
  59. package/corpus/templates/design/shared/canvas-frames.ts +23 -0
  60. package/corpus/templates/design/shared/code-layer.ts +54 -2
  61. package/corpus/templates/design/shared/responsive-frame-layout.ts +55 -0
  62. package/corpus/templates/slides/.agents/skills/analytics-data-for-decks/SKILL.md +48 -0
  63. package/corpus/templates/slides/AGENTS.md +3 -0
  64. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +72 -8
  65. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  66. package/corpus/templates/slides/changelog/2026-07-29-google-slides-exports-can-connect-your-google-account-direct.md +6 -0
  67. package/corpus/templates/slides/server/handlers/google-docs-auth.ts +4 -2
  68. package/dist/a2a/types.d.ts +6 -0
  69. package/dist/a2a/types.d.ts.map +1 -1
  70. package/dist/a2a/types.js.map +1 -1
  71. package/dist/action.d.ts +13 -0
  72. package/dist/action.d.ts.map +1 -1
  73. package/dist/action.js.map +1 -1
  74. package/dist/client/AgentPanel.d.ts.map +1 -1
  75. package/dist/client/AgentPanel.js +16 -5
  76. package/dist/client/AgentPanel.js.map +1 -1
  77. package/dist/collab/awareness.d.ts +2 -2
  78. package/dist/collab/awareness.d.ts.map +1 -1
  79. package/dist/collab/routes.d.ts +1 -1
  80. package/dist/localization/default-messages.d.ts +6 -0
  81. package/dist/localization/default-messages.d.ts.map +1 -1
  82. package/dist/localization/default-messages.js +6 -0
  83. package/dist/localization/default-messages.js.map +1 -1
  84. package/dist/notifications/routes.d.ts +2 -2
  85. package/dist/resources/handlers.d.ts +1 -1
  86. package/dist/server/agent-capabilities.d.ts.map +1 -1
  87. package/dist/server/agent-capabilities.js +32 -3
  88. package/dist/server/agent-capabilities.js.map +1 -1
  89. package/dist/server/agent-chat/action-filters-a2a.d.ts +17 -1
  90. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  91. package/dist/server/agent-chat/action-filters-a2a.js +50 -1
  92. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  93. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  94. package/package.json +1 -1
  95. package/src/a2a/types.ts +6 -0
  96. package/src/action.ts +13 -0
  97. package/src/client/AgentPanel.tsx +58 -34
  98. package/src/localization/default-messages.ts +6 -0
  99. package/src/server/agent-capabilities.ts +43 -8
  100. package/src/server/agent-chat/action-filters-a2a.ts +63 -1
@@ -1,7 +1,38 @@
1
1
  import { POINTER_TEXT_EDIT_ACTIVATION_DELAY_MS } from "@/components/design/design-canvas/pending-text-edit";
2
+ import { findCanvasIframeForScreen } from "@/components/design/multi-screen/iframe-targeting";
2
3
 
3
4
  import { queryUniqueSelector } from "./dom-utils";
4
5
 
6
+ /**
7
+ * Why one begin-text-edit attempt did not end up in an editing session. These
8
+ * stay distinct on purpose: a screen whose iframe is not mounted yet
9
+ * ("no-iframe") is a retry-worthy race, while an iframe that answered and does
10
+ * not have the node ("node-missing") or has it un-focused ("not-editing") is a
11
+ * different situation entirely. Collapsing them all to `false` is what made the
12
+ * board-space text bug invisible — every probe failed identically, so nothing
13
+ * distinguished "asked the wrong window" from "user has not typed yet".
14
+ */
15
+ export type BeginTextEditOutcome =
16
+ | "active"
17
+ | "done"
18
+ | "node-missing"
19
+ | "not-editing"
20
+ | "no-iframe"
21
+ | "no-reply"
22
+ /** begin-text-edit was posted; the iframe has not been re-probed yet. Never
23
+ * settle on this — it is not evidence the node was abandoned, and the
24
+ * caller's exhaustion path deletes untouched nodes. */
25
+ | "activation-requested";
26
+
27
+ export function isTextEditSessionOutcome(
28
+ outcome: BeginTextEditOutcome,
29
+ ): boolean {
30
+ return outcome === "active" || outcome === "done";
31
+ }
32
+
33
+ /** Grace period before re-probing a just-requested activation. */
34
+ const ACTIVATION_CONFIRM_DELAY_MS = 300;
35
+
5
36
  /**
6
37
  * Ask a single iframe's editor-chrome bridge whether a text-edit session for
7
38
  * `nodeId` is "active" (focused), "done" (non-empty committed text), or
@@ -13,16 +44,16 @@ import { queryUniqueSelector } from "./dom-utils";
13
44
  function queryTextEditStatus(
14
45
  iframe: HTMLIFrameElement,
15
46
  nodeId: string,
16
- ): Promise<"active" | "done" | false> {
47
+ ): Promise<"active" | "done" | "node-missing" | "not-editing" | "no-reply"> {
17
48
  const win = iframe.contentWindow;
18
- if (!win) return Promise.resolve(false);
49
+ if (!win) return Promise.resolve("no-reply");
19
50
  const correlationId = `text-edit-status-${Date.now()}-${Math.random()
20
51
  .toString(36)
21
52
  .slice(2, 6)}`;
22
53
  return new Promise((resolve) => {
23
54
  const timer = window.setTimeout(() => {
24
55
  window.removeEventListener("message", listener);
25
- resolve(false);
56
+ resolve("no-reply");
26
57
  }, 250);
27
58
  const listener = (event: MessageEvent) => {
28
59
  if (
@@ -38,7 +69,13 @@ function queryTextEditStatus(
38
69
  window.clearTimeout(timer);
39
70
  window.removeEventListener("message", listener);
40
71
  const status = event.data.status;
41
- resolve(status === "active" || status === "done" ? status : false);
72
+ if (status === "active" || status === "done") {
73
+ resolve(status);
74
+ return;
75
+ }
76
+ // Older bridges answer a bare `false` for both cases; treat that as
77
+ // "present but not editing" so the retry ladder behaves as before.
78
+ resolve(status === "missing" ? "node-missing" : "not-editing");
42
79
  };
43
80
  window.addEventListener("message", listener);
44
81
  win.postMessage(
@@ -48,31 +85,50 @@ function queryTextEditStatus(
48
85
  });
49
86
  }
50
87
 
51
- async function postBeginTextEditToPreviewIframes(
88
+ async function probeTextEdit(
89
+ screenId: string | null,
90
+ nodeId: string,
91
+ boardFileId: string | null,
92
+ ): Promise<BeginTextEditOutcome> {
93
+ if (typeof document === "undefined" || !nodeId || !screenId) {
94
+ return "no-iframe";
95
+ }
96
+ // The board surface's live iframe carries no `data-screen-iframe-id`, so the
97
+ // old `dataset.screenIframeId === screenId` filter never matched it and every
98
+ // attempt fell through to broadcasting at all the *screen* iframes — none of
99
+ // which own a board node. findCanvasIframeForScreen is the one resolver that
100
+ // already knows the board's `[data-board-surface-layer]` shape.
101
+ const iframe = findCanvasIframeForScreen(
102
+ document.body,
103
+ screenId,
104
+ boardFileId ?? undefined,
105
+ );
106
+ if (!iframe?.contentWindow) return "no-iframe";
107
+ return queryTextEditStatus(iframe, nodeId);
108
+ }
109
+
110
+ /** Probes, and asks the iframe to enter edit mode when it is not already
111
+ * editing. Reports `activation-requested` rather than the status observed
112
+ * *before* the request: that pre-activation status is not an answer about the
113
+ * session it just asked for. */
114
+ async function requestTextEdit(
52
115
  screenId: string | null,
53
116
  nodeId: string,
54
- ): Promise<"active" | "done" | false> {
55
- if (typeof document === "undefined" || !nodeId) return false;
56
- const iframes = Array.from(
57
- document.querySelectorAll<HTMLIFrameElement>(
58
- "iframe[data-design-preview-iframe]",
59
- ),
117
+ boardFileId: string | null,
118
+ ): Promise<BeginTextEditOutcome> {
119
+ const status = await probeTextEdit(screenId, nodeId, boardFileId);
120
+ if (isTextEditSessionOutcome(status) || status === "no-iframe") return status;
121
+ const iframe = findCanvasIframeForScreen(
122
+ document.body,
123
+ screenId ?? "",
124
+ boardFileId ?? undefined,
60
125
  );
61
- const targetIframes = iframes.filter(
62
- (iframe) => screenId && iframe.dataset.screenIframeId === screenId,
126
+ if (!iframe?.contentWindow) return "no-iframe";
127
+ iframe.contentWindow.postMessage(
128
+ { type: "begin-text-edit", nodeId, force: true },
129
+ "*",
63
130
  );
64
- const orderedIframes = targetIframes.length > 0 ? targetIframes : iframes;
65
- for (const iframe of orderedIframes) {
66
- const status = await queryTextEditStatus(iframe, nodeId);
67
- if (status === "active" || status === "done") return status;
68
- }
69
- orderedIframes.forEach((iframe) => {
70
- iframe.contentWindow?.postMessage(
71
- { type: "begin-text-edit", nodeId, force: true },
72
- "*",
73
- );
74
- });
75
- return false;
131
+ return "activation-requested";
76
132
  }
77
133
 
78
134
  /**
@@ -93,13 +149,20 @@ async function postBeginTextEditToPreviewIframes(
93
149
  export function scheduleBeginTextEditForScreen(
94
150
  screenId: string | null,
95
151
  nodeId: string,
96
- onExhausted?: (finalStatus: "active" | "done" | false) => void,
152
+ options?: {
153
+ /** Board file id, so a board-space text node resolves the board surface's
154
+ * iframe instead of looking for a `data-screen-iframe-id` it never has. */
155
+ boardFileId?: string | null;
156
+ onExhausted?: (finalStatus: BeginTextEditOutcome) => void;
157
+ },
97
158
  ): () => void {
98
159
  if (typeof window === "undefined") return () => {};
160
+ const onExhausted = options?.onExhausted;
161
+ const boardFileId = options?.boardFileId ?? null;
99
162
  let finished = false;
100
- let lastStatus: "active" | "done" | false = false;
163
+ let lastStatus: BeginTextEditOutcome = "no-iframe";
101
164
  const timers: number[] = [];
102
- const settle = (status: "active" | "done" | false) => {
165
+ const settle = (status: BeginTextEditOutcome) => {
103
166
  if (finished) return;
104
167
  finished = true;
105
168
  lastStatus = status;
@@ -119,19 +182,31 @@ export function scheduleBeginTextEditForScreen(
119
182
  delays.forEach((delay, index) => {
120
183
  const timer = window.setTimeout(() => {
121
184
  if (finished) return;
122
- void postBeginTextEditToPreviewIframes(screenId, nodeId).then(
123
- (status) => {
185
+ void requestTextEdit(screenId, nodeId, boardFileId).then((status) => {
186
+ if (finished) return;
187
+ lastStatus = status;
188
+ if (isTextEditSessionOutcome(status)) {
189
+ settle(status);
190
+ return;
191
+ }
192
+ if (index !== delays.length - 1) return;
193
+ if (status !== "activation-requested") {
194
+ settle(status);
195
+ return;
196
+ }
197
+ // Activation is still in flight, and the caller deletes untouched nodes
198
+ // on exhaustion. Settle on what the iframe reports, never on the request.
199
+ const confirmTimer = window.setTimeout(() => {
124
200
  if (finished) return;
125
- lastStatus = status;
126
- if (status === "active" || status === "done") {
127
- settle(status);
128
- return;
129
- }
130
- if (index === delays.length - 1) {
131
- settle(false);
132
- }
133
- },
134
- );
201
+ void probeTextEdit(screenId, nodeId, boardFileId).then(
202
+ (confirmed) => {
203
+ if (finished) return;
204
+ settle(confirmed);
205
+ },
206
+ );
207
+ }, ACTIVATION_CONFIRM_DELAY_MS);
208
+ timers.push(confirmTimer);
209
+ });
135
210
  }, delay);
136
211
  timers.push(timer);
137
212
  });
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-28
4
+ ---
5
+
6
+ Generated design variants no longer overlap each other on the overview board, and zooming or panning a board with breakpoint frames no longer flashes screen content
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-29
4
+ ---
5
+
6
+ Auto-layout direction buttons now show a direction arrow, so the flow direction is readable without hovering for the tooltip.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-29
4
+ ---
5
+
6
+ Adding a breakpoint now re-packs the board when the new frames would overlap a neighbouring screen, and the add control says it applies to all screens.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-29
4
+ ---
5
+
6
+ Selecting layers and adding frames are faster: the layer projection is reused for unchanged screens and creating a frame no longer refetches every screen's contents.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-29
4
+ ---
5
+
6
+ Canvas zoom now steps about 10% per mouse notch like Figma instead of nearly tripling, and frame labels hold a constant size throughout the gesture instead of scaling and snapping back.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-29
4
+ ---
5
+
6
+ Double-clicking a frame now selects the layer under the pointer so its styling options open, instead of switching the editor into Interact.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-29
4
+ ---
5
+
6
+ The inspector's export preview now shows the actual rendered output instead of a plain coloured rectangle, and says so when it cannot render.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-29
4
+ ---
5
+
6
+ Added Figma-parity shortcuts: A also picks the frame tool, Shift+Cmd+G ungroups, and Shift+0 resets zoom to 100%.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-29
4
+ ---
5
+
6
+ The breakpoint + menu keeps offering device widths after the default Desktop, Tablet, and Phone breakpoints are added, instead of falling back to a bare number input.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-29
4
+ ---
5
+
6
+ A selected frame's size is now editable, with a device-preset picker on the Size section, so an existing frame can be set to a phone, tablet, or desktop size.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-29
4
+ ---
5
+
6
+ The text tool now reaches the board's own editing surface, so a text box you draw on the canvas can be typed into immediately, and boxes left empty are cleaned up instead of lingering invisibly.
@@ -58,6 +58,29 @@ export function parseCanvasFrameGeometryById(
58
58
  );
59
59
  }
60
60
 
61
+ /** Y a new group must start at to clear existing frames, or 0 when the board is
62
+ * empty. Placing at y=0 unconditionally stacks each new group on the last. */
63
+ export function nextFreeCanvasRowY(
64
+ existing: unknown,
65
+ gap: number,
66
+ options: { ignoreFileIds?: readonly string[] } = {},
67
+ ): number {
68
+ const ignored = new Set(options.ignoreFileIds ?? []);
69
+ const frames = Object.entries(parseCanvasFrameGeometryById(existing)).filter(
70
+ ([id]) => !ignored.has(id),
71
+ );
72
+ let bottom = 0;
73
+ let sawFrame = false;
74
+ for (const [, frame] of frames) {
75
+ const y = frame.y ?? 0;
76
+ const height = frame.height ?? 0;
77
+ if (!Number.isFinite(y) || !Number.isFinite(height)) continue;
78
+ sawFrame = true;
79
+ bottom = Math.max(bottom, y + height);
80
+ }
81
+ return sawFrame ? bottom + gap : 0;
82
+ }
83
+
61
84
  export function mergeCanvasFramePlacements({
62
85
  existing,
63
86
  placements,
@@ -2230,6 +2230,38 @@ function buildProjection(
2230
2230
  };
2231
2231
  }
2232
2232
 
2233
+ /**
2234
+ * Most-recently-used projections, keyed by the exact document they came from.
2235
+ *
2236
+ * Projecting means a full HTML parse plus a tree walk, and the editor calls
2237
+ * this on nearly every selection and content change from dozens of call sites —
2238
+ * repeatedly, synchronously, on the main thread, for documents that have not
2239
+ * changed. That is the canvas "freezing while just clicking" and the sluggish
2240
+ * cursor.
2241
+ *
2242
+ * Keyed on the whole HTML string rather than a hash on purpose: a 32-bit hash
2243
+ * collision would hand a caller a confidently wrong layer tree, and every
2244
+ * id-keyed edit downstream would target the wrong node. The string is already
2245
+ * retained by the file/query cache, so the key costs a reference, not a copy.
2246
+ *
2247
+ * Callers must treat the result as read-only — it is shared now. Every consumer
2248
+ * only reads (`find`/`filter`/`map`); `applyCodeLayer*`-style writers build new
2249
+ * HTML and re-project rather than editing a projection in place.
2250
+ */
2251
+ const PROJECTION_CACHE_MAX = 24;
2252
+ const projectionCache = new Map<string, CodeLayerProjection>();
2253
+
2254
+ /** Every own field of the source, sorted, so adding a field to
2255
+ * CodeLayerSource later cannot silently start returning a projection whose
2256
+ * `.source` came from a different call. */
2257
+ function projectionSourceKey(source: CodeLayerSource): string {
2258
+ const record = source as unknown as Record<string, unknown>;
2259
+ return Object.keys(source)
2260
+ .sort()
2261
+ .map((field) => `${field}=${record[field] ?? ""}`)
2262
+ .join("\u0000");
2263
+ }
2264
+
2233
2265
  export function buildCodeLayerProjection(
2234
2266
  html: string,
2235
2267
  options: { source?: CodeLayerSource } = {},
@@ -2238,8 +2270,28 @@ export function buildCodeLayerProjection(
2238
2270
  // (e.g. `activeContent` is briefly undefined on first render). Projecting a
2239
2271
  // non-string must yield an empty projection, never crash the editor.
2240
2272
  const safeHtml = typeof html === "string" ? html : "";
2241
- return buildProjection(safeHtml, options.source ?? { kind: "inline-html" })
2242
- .projection;
2273
+ const source = options.source ?? { kind: "inline-html" };
2274
+ const key = `${projectionSourceKey(source)}\u0000${safeHtml}`;
2275
+ const cached = projectionCache.get(key);
2276
+ if (cached) {
2277
+ // Re-insert so the Map's insertion order stays least-recently-used first.
2278
+ projectionCache.delete(key);
2279
+ projectionCache.set(key, cached);
2280
+ return cached;
2281
+ }
2282
+ const projection = buildProjection(safeHtml, source).projection;
2283
+ projectionCache.set(key, projection);
2284
+ if (projectionCache.size > PROJECTION_CACHE_MAX) {
2285
+ const oldest = projectionCache.keys().next();
2286
+ if (!oldest.done) projectionCache.delete(oldest.value);
2287
+ }
2288
+ return projection;
2289
+ }
2290
+
2291
+ /** Drops every cached projection. Exists for tests that assert projection
2292
+ * identity/eviction; production has no reason to call it. */
2293
+ export function clearCodeLayerProjectionCache(): void {
2294
+ projectionCache.clear();
2243
2295
  }
2244
2296
 
2245
2297
  export function ensureCodeLayerNodeIdsInHtml(
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Geometry of one screen's responsive frame group: the primary frame plus the
3
+ * breakpoint previews painted to its right.
4
+ *
5
+ * This lives in `shared/` because both sides of the contract need it. Actions
6
+ * that *place* screens on the overview board (present-design-variants) must
7
+ * reserve the same width the canvas will actually *paint*, or the next screen
8
+ * lands underneath the previous screen's breakpoint row. Keeping the width in
9
+ * one place is what makes writer and renderer agree.
10
+ */
11
+
12
+ /** Gap between the primary frame and each breakpoint preview beside it. */
13
+ export const BREAKPOINT_FRAME_GAP = 24;
14
+
15
+ /** Drops any breakpoint whose width equals the primary frame's own width — a
16
+ * redundant duplicate of the base — also cleaning up designs authored before
17
+ * the default set excluded the primary width. */
18
+ export function visibleBreakpointWidths(
19
+ breakpointWidths: readonly number[] | undefined,
20
+ primaryWidthPx: number | undefined,
21
+ ): number[] {
22
+ const deduped = Array.from(
23
+ new Set(
24
+ (breakpointWidths ?? []).filter(
25
+ (width) => Number.isFinite(width) && width > 0,
26
+ ),
27
+ ),
28
+ );
29
+ if (primaryWidthPx === undefined || !Number.isFinite(primaryWidthPx)) {
30
+ return deduped;
31
+ }
32
+ return deduped.filter((width) => Math.abs(width - primaryWidthPx) > 1);
33
+ }
34
+
35
+ /**
36
+ * Total painted width of a screen's frame group: the primary box plus every
37
+ * breakpoint preview beside it, each drawn at the primary's own uniform
38
+ * `scale`. `visibleWidths` must already be filtered through
39
+ * `visibleBreakpointWidths` — callers differ in which width they dedupe
40
+ * against, but the arithmetic must not.
41
+ */
42
+ export function getResponsiveGroupWidth({
43
+ primaryWidth,
44
+ scale,
45
+ visibleWidths,
46
+ }: {
47
+ primaryWidth: number;
48
+ scale: number;
49
+ visibleWidths: readonly number[];
50
+ }): number {
51
+ return visibleWidths.reduce(
52
+ (total, width) => total + BREAKPOINT_FRAME_GAP + width * scale,
53
+ Math.max(1, primaryWidth),
54
+ );
55
+ }
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: analytics-data-for-decks
3
+ description: >-
4
+ Route customer, account, CRM, HubSpot, pipeline, renewal, usage, product
5
+ activity, Gong, and other analytics-backed deck requests through the
6
+ Analytics agent. Use when generating or updating Slides content that needs
7
+ external or first-party data.
8
+ ---
9
+
10
+ # Analytics data for decks
11
+
12
+ When a deck needs customer or product data, ask the Analytics agent over A2A
13
+ before creating or changing Slides content. The user's wording can be direct,
14
+ such as "ask the analytics agent to ...", or implicit in a request for customer,
15
+ account, CRM, HubSpot, pipeline, renewal, usage, product-activity, Gong, or
16
+ other analytics data.
17
+
18
+ ## Delegation
19
+
20
+ - For a known, read-only operation, use `call-agent` with `agent: "analytics"`,
21
+ the exact `action`, and complete `input`. This invokes Analytics directly
22
+ without starting a second model loop.
23
+ - For ambiguous, multi-source, or narrative questions, use `call-agent` with a
24
+ natural-language `message` and let Analytics decide which source or sources
25
+ to use.
26
+ - Useful Analytics operations include `hubspot-records` for bounded CRM
27
+ records and `hubspot-deals` for pipeline and deal metrics,
28
+ `account-deep-dive` for a named account or renewal/risk review, `gong-calls`
29
+ for quotes/counts/transcripts and coverage, and `gong-native-insights` for
30
+ bounded narrative synthesis. For first-party product activity, send a
31
+ natural-language message and let Analytics choose its query.
32
+ - Slides must not write SQL, choose a warehouse or provider, call HubSpot/Gong
33
+ directly, or interpret a provider schema itself. Analytics owns source
34
+ selection, data dictionary interpretation, filters, joins, and query
35
+ execution using its own instructions and connected-source status.
36
+
37
+ ## Evidence and deck behavior
38
+
39
+ - Preserve the question, source, filters, date window, coverage, counts,
40
+ pagination, and limitations from Analytics. Never invent customer data or
41
+ claim a lookup succeeded when it failed.
42
+ - If a source is unavailable, name the missing source and continue only with
43
+ clearly labeled data that Analytics actually returned. Do not expose
44
+ credentials or raw provider payloads.
45
+ - Before generation, call `get-workspace-defaults` when no reference deck or
46
+ design system is named, then follow `creative-context` for approved brand
47
+ context and provenance. For updates, preserve the existing deck structure
48
+ and make focused slide edits.
@@ -45,6 +45,8 @@ ladder.
45
45
  and `input` (or `invokeAgentAction`) instead of starting the sibling agent's
46
46
  model loop. In Analytics, use `gong-native-insights` for provider-synthesized
47
47
  briefs and `gong-calls` for quotes, counts, transcripts, and coverage claims.
48
+ - For data requests, read `.agents/skills/analytics-data-for-decks/SKILL.md` and
49
+ delegate via Analytics over A2A; do not write SQL or call providers directly.
48
50
  - When the user names no reference deck or design system, call
49
51
  `get-workspace-defaults` first so a bare "make a deck about X" is still on
50
52
  brand.
@@ -102,3 +104,4 @@ Read the relevant skill before deeper work:
102
104
  - `design-systems`, `frontend-design`, `shadcn-ui`, and `actions` as needed.
103
105
  - `creative-context` for cross-app source reuse, pinned packs, provenance, and
104
106
  context opt-out.
107
+ - `analytics-data-for-decks` for delegated data.
@@ -1,4 +1,7 @@
1
- import { appBasePath } from "@agent-native/core/client/api-path";
1
+ import {
2
+ agentNativePath,
3
+ appBasePath,
4
+ } from "@agent-native/core/client/api-path";
2
5
  import { useT } from "@agent-native/core/client/i18n";
3
6
  import {
4
7
  IconDownload,
@@ -8,6 +11,7 @@ import {
8
11
  IconCopy,
9
12
  IconShare2,
10
13
  IconBrandGoogle,
14
+ IconPlugConnected,
11
15
  } from "@tabler/icons-react";
12
16
  import { toast } from "sonner";
13
17
 
@@ -122,6 +126,57 @@ export function ExportMenu({
122
126
  }
123
127
  };
124
128
 
129
+ const handleConnectGoogle = async () => {
130
+ const authUrl = new URL(
131
+ agentNativePath("/_agent-native/google-docs/auth-url"),
132
+ window.location.origin,
133
+ );
134
+ authUrl.searchParams.set(
135
+ "return",
136
+ window.location.pathname + window.location.search,
137
+ );
138
+
139
+ const popup = window.open(
140
+ "",
141
+ "google-docs-oauth",
142
+ "popup,width=520,height=720",
143
+ );
144
+ if (!popup) {
145
+ toast.error(t("editorExport.exportFailed"), {
146
+ description: t("editorExport.exportGoogleSlidesError"),
147
+ });
148
+ return;
149
+ }
150
+
151
+ try {
152
+ const response = await fetch(authUrl.toString(), {
153
+ credentials: "same-origin",
154
+ });
155
+ if (!response.ok) {
156
+ throw new Error(
157
+ await readErrorMessage(
158
+ response,
159
+ t("editorExport.exportGoogleSlidesError"),
160
+ ),
161
+ );
162
+ }
163
+ const data = (await response.json()) as { url?: unknown };
164
+ if (typeof data.url !== "string") {
165
+ throw new Error(t("editorExport.exportGoogleSlidesError"));
166
+ }
167
+ popup.location.href = data.url;
168
+ } catch (err) {
169
+ popup?.close();
170
+ console.error("Google connection failed:", err);
171
+ toast.error(t("editorExport.exportFailed"), {
172
+ description:
173
+ err instanceof Error
174
+ ? err.message
175
+ : t("editorExport.exportGoogleSlidesError"),
176
+ });
177
+ }
178
+ };
179
+
125
180
  const handleExportHtml = async () => {
126
181
  try {
127
182
  const res = await fetch(`${appBasePath()}/api/exports/html`, {
@@ -189,13 +244,22 @@ export function ExportMenu({
189
244
  {t("editorExport.exportPptx")}
190
245
  </DropdownMenuItem>
191
246
  {onExportGoogleSlides && (
192
- <DropdownMenuItem
193
- onClick={handleExportGoogleSlides}
194
- className="cursor-pointer"
195
- >
196
- <IconBrandGoogle className="w-4 h-4 mr-2" />
197
- {t("editorExport.openInGoogleSlides")}
198
- </DropdownMenuItem>
247
+ <>
248
+ <DropdownMenuItem
249
+ onClick={handleConnectGoogle}
250
+ className="cursor-pointer"
251
+ >
252
+ <IconPlugConnected className="w-4 h-4 mr-2" />
253
+ {t("editorExport.connectGoogle")}
254
+ </DropdownMenuItem>
255
+ <DropdownMenuItem
256
+ onClick={handleExportGoogleSlides}
257
+ className="cursor-pointer"
258
+ >
259
+ <IconBrandGoogle className="w-4 h-4 mr-2" />
260
+ {t("editorExport.openInGoogleSlides")}
261
+ </DropdownMenuItem>
262
+ </>
199
263
  )}
200
264
  <DropdownMenuSeparator />
201
265
  <DropdownMenuItem onClick={onDuplicate} className="cursor-pointer">
@@ -262,6 +262,7 @@ const messages = {
262
262
  downloadHtml: "Download as HTML",
263
263
  exportPdf: "Export as PDF",
264
264
  exportPptx: "Export as PPTX",
265
+ connectGoogle: "Connect Google",
265
266
  openInGoogleSlides: "Open in Google Slides",
266
267
  googleSlidesCreated: "Opened in Google Slides",
267
268
  googleSlidesCreatedHint:
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-29
4
+ ---
5
+
6
+ Google Slides exports can connect your Google account directly from the export menu
@@ -12,7 +12,6 @@ import {
12
12
  import {
13
13
  defineEventHandler,
14
14
  getQuery,
15
- sendRedirect,
16
15
  setResponseStatus,
17
16
  type H3Event,
18
17
  } from "h3";
@@ -98,7 +97,10 @@ export const getGoogleDocsAuthUrlHandler = defineEventHandler(
98
97
  });
99
98
  const url = await getGoogleDocsAuthUrl(redirectUri, state, owner);
100
99
  if (q.redirect === "1") {
101
- return sendRedirect(event, url, 302);
100
+ return new Response(null, {
101
+ status: 302,
102
+ headers: { Location: url },
103
+ });
102
104
  }
103
105
  return { url };
104
106
  } catch (error) {