@agent-native/core 0.148.1 → 0.149.1

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 (165) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/changelog/2026-08-10-wide-tables-in-analytics-chat-now-scroll-horizontally-within.md +6 -0
  3. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +13 -53
  4. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +11 -70
  5. package/corpus/templates/calendar/app/lib/event-layout.ts +122 -0
  6. package/corpus/templates/calendar/app/routes/_app.availability.tsx +1 -1
  7. package/corpus/templates/calendar/changelog/2026-08-10-opening-availability-settings-from-the-agent-lands-on-the-av.md +6 -0
  8. package/corpus/templates/calendar/vitest.config.ts +6 -1
  9. package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +3 -1
  10. package/corpus/templates/clips/app/components/player/video-player.tsx +44 -4
  11. package/corpus/templates/clips/changelog/2026-08-10-fixed-transcription-failures-for-recordings-without-captured.md +6 -0
  12. package/corpus/templates/clips/changelog/2026-08-10-mobile-clips-can-now-enter-fullscreen-on-iphone-safari.md +6 -0
  13. package/corpus/templates/content/actions/_property-utils.ts +11 -1
  14. package/corpus/templates/content/actions/view-screen.ts +62 -12
  15. package/corpus/templates/content/server/plugins/agent-chat.ts +2 -0
  16. package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +22 -0
  17. package/corpus/templates/design/AGENTS.md +3 -0
  18. package/corpus/templates/design/actions/create-design-from-template.ts +19 -0
  19. package/corpus/templates/design/actions/get-design-snapshot.ts +18 -0
  20. package/corpus/templates/design/actions/get-design-template.ts +178 -0
  21. package/corpus/templates/design/actions/list-designs.ts +3 -1
  22. package/corpus/templates/design/actions/view-screen.ts +31 -0
  23. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +225 -214
  24. package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -0
  25. package/corpus/templates/design/app/i18n-data.ts +78 -0
  26. package/corpus/templates/design/app/lib/design-authors.ts +64 -0
  27. package/corpus/templates/design/app/pages/Index.tsx +114 -4
  28. package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +1 -0
  29. package/corpus/templates/design/changelog/2026-08-07-alt-dragging-an-element-to-copy-it-now-keeps-the-copy-visibl.md +6 -0
  30. package/corpus/templates/design/changelog/2026-08-07-design-cards-now-show-who-created-each-design-with-a-filter-.md +6 -0
  31. package/corpus/templates/design/changelog/2026-08-07-drawing-a-shape-on-the-overview-canvas-now-keeps-the-shape-s.md +6 -0
  32. package/corpus/templates/design/changelog/2026-08-07-hold-option-alt-while-dragging-a-frame-on-the-overview-canva.md +6 -0
  33. package/corpus/templates/design/server/lib/design-template-data.ts +143 -0
  34. package/corpus/templates/design/server/plugins/agent-chat.ts +1 -0
  35. package/corpus/templates/forms/AGENTS.md +2 -2
  36. package/corpus/templates/forms/actions/create-form.ts +1 -1
  37. package/corpus/templates/forms/actions/update-form.ts +5 -5
  38. package/corpus/templates/forms/changelog/2026-08-10-forms-now-keep-agent-applied-field-changes-visible-after-an-.md +6 -0
  39. package/corpus/templates/forms/changelog/2026-08-10-published-form-links-always-use-the-working-public-form-url.md +6 -0
  40. package/corpus/templates/forms/server/plugins/agent-chat.ts +2 -2
  41. package/corpus/templates/slides/AGENTS.md +4 -3
  42. package/corpus/templates/slides/actions/import-google-slides-reference.ts +56 -10
  43. package/corpus/templates/slides/actions/patch-deck.ts +18 -2
  44. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +46 -0
  45. package/corpus/templates/slides/app/components/editor/BlockBubbleMenu.tsx +194 -3
  46. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +22 -75
  47. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +5 -3
  48. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +5 -1
  49. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +15 -4
  50. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +116 -55
  51. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +27 -4
  52. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +45 -0
  53. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +143 -18
  54. package/corpus/templates/slides/app/i18n/en-US.ts +6 -0
  55. package/corpus/templates/slides/app/lib/export-pdf-client.ts +84 -25
  56. package/corpus/templates/slides/app/lib/generation-state.ts +7 -1
  57. package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +11 -0
  58. package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -14
  59. package/corpus/templates/slides/app/pages/Index.tsx +124 -135
  60. package/corpus/templates/slides/changelog/2026-08-08-google-slides-export-now-tells-you-why-a-deck-fell-back-to-a.md +6 -0
  61. package/corpus/templates/slides/changelog/2026-08-09-delete-key-now-removes-a-selected-image.md +6 -0
  62. package/corpus/templates/slides/changelog/2026-08-09-editing-a-slide-in-an-imported-deck-no-longer-fails-to-save.md +6 -0
  63. package/corpus/templates/slides/changelog/2026-08-09-fixed-pdf-and-custom-size-powerpoint-imports-rendering-with-.md +6 -0
  64. package/corpus/templates/slides/changelog/2026-08-09-images-from-sites-without-cors-no-longer-export-blank.md +6 -0
  65. package/corpus/templates/slides/changelog/2026-08-09-imported-pdf-headings-no-longer-lose-the-space-between-color.md +6 -0
  66. package/corpus/templates/slides/changelog/2026-08-09-imported-slide-text-no-longer-renders-too-large-for-its-box.md +6 -0
  67. package/corpus/templates/slides/changelog/2026-08-09-large-decks-no-longer-render-slowly-or-glitch-in-the-editor.md +6 -0
  68. package/corpus/templates/slides/changelog/2026-08-09-revise-highlighted-slide-text-with-ai.md +6 -0
  69. package/corpus/templates/slides/changelog/2026-08-09-the-deck-editor-no-longer-stays-dimmed-after-the-window-narro.md +6 -0
  70. package/corpus/templates/slides/changelog/2026-08-10-imported-reference-decks-now-show-clear-success-and-stay-sel.md +6 -0
  71. package/corpus/templates/slides/changelog/2026-08-10-pasted-google-slides-links-import-after-reconnecting-google-.md +6 -0
  72. package/corpus/templates/slides/server/handlers/google-docs-auth.ts +4 -0
  73. package/corpus/templates/slides/server/handlers/import/html-converter.ts +115 -22
  74. package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +11 -2
  75. package/corpus/templates/slides/server/lib/fetch-remote-image.ts +192 -0
  76. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +13 -0
  77. package/corpus/templates/slides/server/lib/image-proxy-url.ts +192 -0
  78. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
  79. package/corpus/templates/slides/server/routes/api/image-proxy.get.ts +65 -0
  80. package/dist/agent/engine/registry.d.ts +8 -2
  81. package/dist/agent/engine/registry.js +63 -29
  82. package/dist/agent/production-agent.js +7 -0
  83. package/dist/agent/run-manager.d.ts +5 -4
  84. package/dist/agent/run-manager.js +6 -5
  85. package/dist/cli/claude-code-participant.d.ts +2 -1
  86. package/dist/cli/claude-code-participant.js +19 -0
  87. package/dist/cli/code-agent-executor.js +223 -0
  88. package/dist/cli/create-workspace.js +3 -3
  89. package/dist/cli/create.d.ts +3 -2
  90. package/dist/cli/create.js +20 -24
  91. package/dist/client/AgentPanel.js +8 -4
  92. package/dist/client/AssistantChat.js +34 -1
  93. package/dist/client/agent-chat-adapter.d.ts +1 -0
  94. package/dist/client/agent-chat-adapter.js +10 -2
  95. package/dist/client/chat/markdown-renderer.d.ts +1 -0
  96. package/dist/client/chat/markdown-renderer.js +4 -0
  97. package/dist/client/chat/message-components.d.ts +1 -0
  98. package/dist/client/chat/message-components.js +9 -1
  99. package/dist/client/chat/tool-call-display.d.ts +8 -0
  100. package/dist/client/chat/tool-call-display.js +30 -21
  101. package/dist/client/chat-first/app-pane.js +1 -1
  102. package/dist/client/chat-first/apps-rail.d.ts +1 -1
  103. package/dist/client/chat-first/apps-rail.js +11 -11
  104. package/dist/client/chat-first/chat-history.d.ts +1 -1
  105. package/dist/client/chat-first/chat-history.js +2 -0
  106. package/dist/client/chat-first/primary-nav.js +1 -1
  107. package/dist/client/progress/RunsTray.js +14 -1
  108. package/dist/client/resources/McpConnectionSuggestion.js +2 -2
  109. package/dist/client/settings/SettingsTabsPage.d.ts +3 -1
  110. package/dist/client/settings/SettingsTabsPage.js +2 -2
  111. package/dist/client/sse-event-processor.d.ts +8 -0
  112. package/dist/client/sse-event-processor.js +13 -6
  113. package/dist/collab/awareness.d.ts +2 -2
  114. package/dist/collab/routes.d.ts +1 -1
  115. package/dist/deploy/build.d.ts +5 -7
  116. package/dist/deploy/build.js +19 -12
  117. package/dist/localization/default-messages.js +1 -1
  118. package/dist/notifications/routes.d.ts +3 -3
  119. package/dist/observability/routes.d.ts +6 -6
  120. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  121. package/dist/scripts/call-agent.js +3 -3
  122. package/dist/scripts/describe-workspace-apps.js +1 -1
  123. package/dist/server/agent-capabilities.js +0 -0
  124. package/dist/server/agent-chat/context-tools.js +1 -1
  125. package/dist/server/agent-chat/prompt-resources.js +1 -1
  126. package/dist/server/agent-chat-plugin.js +1 -1
  127. package/dist/server/auth.js +190 -42
  128. package/dist/server/better-auth-migrations.js +53 -0
  129. package/dist/server/http-response-telemetry.js +7 -1
  130. package/dist/server/onboarding-html.js +119 -33
  131. package/dist/server/transcribe-voice.d.ts +1 -1
  132. package/dist/shared/password-policy.d.ts +2 -1
  133. package/dist/shared/password-policy.js +2 -1
  134. package/dist/styles/agent-conversation.css +20 -3
  135. package/dist/styles/agent-native.css +14 -2
  136. package/dist/templates/default/app/i18n/ar-SA.ts +1 -1
  137. package/dist/templates/default/app/i18n/de-DE.ts +1 -1
  138. package/dist/templates/default/app/i18n/en-US.ts +1 -1
  139. package/dist/templates/default/app/i18n/es-ES.ts +1 -1
  140. package/dist/templates/default/app/i18n/fr-FR.ts +1 -1
  141. package/dist/templates/default/app/i18n/hi-IN.ts +1 -1
  142. package/dist/templates/default/app/i18n/ja-JP.ts +1 -1
  143. package/dist/templates/default/app/i18n/ko-KR.ts +1 -1
  144. package/dist/templates/default/app/i18n/pt-BR.ts +1 -1
  145. package/dist/templates/default/app/i18n/zh-CN.ts +1 -1
  146. package/dist/templates/default/app/i18n/zh-TW.ts +1 -1
  147. package/dist/templates/default/pnpm-workspace.yaml +1 -0
  148. package/dist/templates/workspace-root/pnpm-workspace.yaml +2 -0
  149. package/docs/content/cloneable-saas.mdx +6 -6
  150. package/docs/content/deployment.mdx +10 -8
  151. package/docs/content/environment-variables.mdx +4 -3
  152. package/package.json +2 -2
  153. package/src/templates/default/app/i18n/ar-SA.ts +1 -1
  154. package/src/templates/default/app/i18n/de-DE.ts +1 -1
  155. package/src/templates/default/app/i18n/en-US.ts +1 -1
  156. package/src/templates/default/app/i18n/es-ES.ts +1 -1
  157. package/src/templates/default/app/i18n/fr-FR.ts +1 -1
  158. package/src/templates/default/app/i18n/hi-IN.ts +1 -1
  159. package/src/templates/default/app/i18n/ja-JP.ts +1 -1
  160. package/src/templates/default/app/i18n/ko-KR.ts +1 -1
  161. package/src/templates/default/app/i18n/pt-BR.ts +1 -1
  162. package/src/templates/default/app/i18n/zh-CN.ts +1 -1
  163. package/src/templates/default/app/i18n/zh-TW.ts +1 -1
  164. package/src/templates/default/pnpm-workspace.yaml +1 -0
  165. package/src/templates/workspace-root/pnpm-workspace.yaml +2 -0
@@ -119,6 +119,8 @@ import {
119
119
  getSlideTextBoxDefaultColor,
120
120
  getSlideSelectionIdentity,
121
121
  getSlideSelectionMode,
122
+ findPersistedImageObject,
123
+ isDeletableFlowImage,
122
124
  removeSlideObjectAndLayoutSpacer,
123
125
  resolveSlideObjectContainingBlock,
124
126
  type CopiedSlideObjects,
@@ -2404,9 +2406,24 @@ export default function SlideEditor({
2404
2406
  clearMultiSelection();
2405
2407
  } else {
2406
2408
  const element = resolveSelectedElement();
2407
- if (!element || !isPersistedFreeformObject(element)) return;
2408
- e.preventDefault();
2409
- removeSlideObjectAndLayoutSpacer(element);
2409
+ if (!element) return;
2410
+ if (isPersistedFreeformObject(element)) {
2411
+ e.preventDefault();
2412
+ removeSlideObjectAndLayoutSpacer(element);
2413
+ } else if (isDeletableFlowImage(element)) {
2414
+ e.preventDefault();
2415
+ // An imported PPTX/PDF image is an <img> (or placeholder) nested in
2416
+ // an absolutely positioned wrapper that carries the persisted object
2417
+ // id. Removing only the inner node would leave that wrapper and its
2418
+ // durable metadata behind as an invisible ghost object.
2419
+ const owner = findPersistedImageObject(element, slideContent);
2420
+ if (owner) removeSlideObjectAndLayoutSpacer(owner);
2421
+ else element.remove();
2422
+ setSelectedImg(null);
2423
+ setImageOverlay(null);
2424
+ } else {
2425
+ return;
2426
+ }
2410
2427
  clearSelectedElement();
2411
2428
  }
2412
2429
 
@@ -4328,6 +4345,7 @@ export default function SlideEditor({
4328
4345
  edits={activeSlideEdits}
4329
4346
  resolveRect={resolveCanvasRect}
4330
4347
  containerRef={slideCanvasRef}
4348
+ outlineOnly
4331
4349
  />
4332
4350
  )}
4333
4351
  {(agentActive || passivePresentUsers.length > 0) && (
@@ -4412,7 +4430,12 @@ export default function SlideEditor({
4412
4430
  onSendToAgent={sendSelectionToAgent}
4413
4431
  />
4414
4432
 
4415
- <BlockBubbleMenu editingEl={editingEl} />
4433
+ <BlockBubbleMenu
4434
+ editingEl={editingEl}
4435
+ slideId={slide.id}
4436
+ deckId={deckId}
4437
+ onCommitInlineEdit={exitInlineEdit}
4438
+ />
4416
4439
 
4417
4440
  {pendingUpdateCount > 0 && (
4418
4441
  <div className="absolute top-4 right-4 z-50">
@@ -521,6 +521,51 @@ export function removeSlideObjectAndLayoutSpacer(element: HTMLElement): void {
521
521
  element.remove();
522
522
  }
523
523
 
524
+ /**
525
+ * Whether Delete should remove `element` even though it is not a freeform
526
+ * canvas object.
527
+ *
528
+ * Flow-layout nodes are deliberately excluded from object operations, because
529
+ * deleting an arbitrary one collapses the layout around it. An image is the
530
+ * exception: it is a leaf, users select it directly, and removing it leaves
531
+ * the surrounding grid or card intact. Without this, selecting a picture in a
532
+ * card grid and pressing Delete silently did nothing.
533
+ */
534
+ export function isDeletableFlowImage(element: HTMLElement): boolean {
535
+ return (
536
+ element.tagName === "IMG" ||
537
+ element.classList.contains("fmd-img-placeholder")
538
+ );
539
+ }
540
+
541
+ /**
542
+ * The persisted image object that owns `element`, if any.
543
+ *
544
+ * PPTX/PDF import wraps each picture in an absolutely positioned
545
+ * `.fmd-pptx-image` div carrying the durable `data-slide-object-id`, with the
546
+ * `<img>` (or an empty placeholder) inside it. Deleting the inner node alone
547
+ * leaves that wrapper behind as an invisible object that still occupies its
548
+ * slot and still round-trips through save. Matching on the image wrapper
549
+ * specifically — rather than any positioned ancestor — keeps this from
550
+ * swallowing a whole card or column that merely contains a picture.
551
+ */
552
+ export function findPersistedImageObject(
553
+ element: HTMLElement,
554
+ root: HTMLElement,
555
+ ): HTMLElement | null {
556
+ let current: HTMLElement | null = element;
557
+ while (current && current !== root && root.contains(current)) {
558
+ const isImageWrapper =
559
+ current.classList.contains("fmd-pptx-image") ||
560
+ current.getAttribute("data-pptx-element-kind") === "image";
561
+ if (isImageWrapper && current.getAttribute("data-slide-object-id")) {
562
+ return current;
563
+ }
564
+ current = current.parentElement;
565
+ }
566
+ return null;
567
+ }
568
+
524
569
  /** Convert a viewport click into the unscaled fmd-slide coordinate system. */
525
570
  export function clientPointToSlideCoordinates(
526
571
  clientX: number,
@@ -9,7 +9,9 @@ import { toast } from "sonner";
9
9
 
10
10
  import {
11
11
  NewDeckReferenceStep,
12
+ type ImportedReference,
12
13
  type NewDeckReferenceSelection,
14
+ type NewDeckReferenceSource,
13
15
  } from "@/components/editor/NewDeckReferenceStep";
14
16
  import {
15
17
  uploadPromptFiles,
@@ -204,37 +206,159 @@ export function FirstDeckOnboardingFlow({
204
206
  );
205
207
 
206
208
  const handleReferenceImport = useCallback(
207
- async (files: File[]) => {
209
+ async (files: File[]): Promise<ImportedReference | null> => {
208
210
  setReferenceImporting(true);
209
211
  try {
210
212
  const uploaded = await uploadPromptFiles(files);
211
213
  const pptxReference = uploaded.find((file) =>
212
214
  file.originalName.toLowerCase().endsWith(".pptx"),
213
215
  );
214
- let referenceSelection: NewDeckReferenceSelection = {
215
- designSystemId: null,
216
- referenceDeckId: null,
217
- };
216
+ const pdfReference = uploaded.find((file) =>
217
+ file.originalName.toLowerCase().endsWith(".pdf"),
218
+ );
219
+ let importedReference: ImportedReference | null = null;
218
220
  let generationFiles = uploaded;
219
221
  if (pptxReference) {
220
222
  const imported = (await callAction("import-pptx", {
221
223
  filePath: pptxReference.path,
222
- })) as { id?: unknown };
223
- if (typeof imported.id !== "string" || !imported.id) {
224
+ })) as {
225
+ id?: unknown;
226
+ imported?: unknown;
227
+ slideCount?: unknown;
228
+ title?: unknown;
229
+ };
230
+ if (
231
+ typeof imported.id !== "string" ||
232
+ !imported.id ||
233
+ imported.imported !== true ||
234
+ typeof imported.slideCount !== "number" ||
235
+ imported.slideCount < 1
236
+ ) {
224
237
  throw new Error("The imported presentation did not create a deck.");
225
238
  }
226
- await reloadDecks();
227
- rememberReference({ id: imported.id, kind: "deck" });
228
- referenceSelection = {
229
- designSystemId: null,
230
- referenceDeckId: imported.id,
239
+ importedReference = {
240
+ id: imported.id,
241
+ title:
242
+ typeof imported.title === "string" && imported.title
243
+ ? imported.title
244
+ : t("home.importedReferenceDeck"),
245
+ source: "pptx",
231
246
  };
232
247
  generationFiles = uploaded.filter((file) => file !== pptxReference);
248
+ } else if (pdfReference) {
249
+ const referenceDeck = createDeck(undefined, {
250
+ noDefaultSlides: true,
251
+ });
252
+ const persisted = await ensureDeckPersisted(referenceDeck.id);
253
+ if (!persisted.persisted) {
254
+ deleteDeck(referenceDeck.id);
255
+ throw new Error(
256
+ describeDeckPersistenceFailure(
257
+ persisted,
258
+ "The PDF reference deck could not be saved.",
259
+ ),
260
+ );
261
+ }
262
+ try {
263
+ const imported = (await callAction("import-file", {
264
+ filePath: pdfReference.path,
265
+ format: "pdf",
266
+ deckId: referenceDeck.id,
267
+ importIntoDeck: true,
268
+ })) as {
269
+ imported?: unknown;
270
+ deckId?: unknown;
271
+ pageCount?: unknown;
272
+ title?: unknown;
273
+ };
274
+ if (
275
+ imported.imported !== true ||
276
+ imported.deckId !== referenceDeck.id ||
277
+ typeof imported.pageCount !== "number" ||
278
+ imported.pageCount < 1
279
+ ) {
280
+ throw new Error("The PDF reference deck could not be imported.");
281
+ }
282
+ importedReference = {
283
+ id: referenceDeck.id,
284
+ title:
285
+ typeof imported.title === "string" && imported.title
286
+ ? imported.title
287
+ : t("home.importedReferenceDeck"),
288
+ source: "pdf",
289
+ };
290
+ generationFiles = uploaded.filter((file) => file !== pdfReference);
291
+ } catch (error) {
292
+ deleteDeck(referenceDeck.id);
293
+ throw error;
294
+ }
233
295
  }
234
- await startGeneration(
235
- [...promptFiles, ...generationFiles],
236
- referenceSelection,
237
- );
296
+ setPromptFiles((current) => [...current, ...generationFiles]);
297
+ if (importedReference) {
298
+ await reloadDecks();
299
+ rememberReference({ id: importedReference.id, kind: "deck" });
300
+ }
301
+ return importedReference;
302
+ } catch (error) {
303
+ toast.error(t("editorToolbar.uploadFailed"), {
304
+ description:
305
+ error instanceof Error
306
+ ? error.message
307
+ : t("editorToolbar.importFailedDescription"),
308
+ });
309
+ return null;
310
+ } finally {
311
+ setReferenceImporting(false);
312
+ }
313
+ },
314
+ [
315
+ callAction,
316
+ createDeck,
317
+ deleteDeck,
318
+ ensureDeckPersisted,
319
+ reloadDecks,
320
+ rememberReference,
321
+ t,
322
+ ],
323
+ );
324
+
325
+ const handleReferenceSourceImport = useCallback(
326
+ async (
327
+ source: NewDeckReferenceSource,
328
+ ): Promise<ImportedReference | null> => {
329
+ if (source.kind !== "google-docs") return null;
330
+ setReferenceImporting(true);
331
+ try {
332
+ const imported = (await callAction("import-google-slides-reference", {
333
+ presentationUrl: source.value,
334
+ })) as {
335
+ id?: unknown;
336
+ imported?: unknown;
337
+ slideCount?: unknown;
338
+ title?: unknown;
339
+ };
340
+ if (
341
+ typeof imported.id !== "string" ||
342
+ !imported.id ||
343
+ imported.imported !== true ||
344
+ typeof imported.slideCount !== "number" ||
345
+ imported.slideCount < 1
346
+ ) {
347
+ throw new Error(
348
+ "The Google Slides presentation did not create a deck.",
349
+ );
350
+ }
351
+ const importedReference: ImportedReference = {
352
+ id: imported.id,
353
+ title:
354
+ typeof imported.title === "string" && imported.title
355
+ ? imported.title
356
+ : t("home.importedReferenceDeck"),
357
+ source: "google-slides",
358
+ };
359
+ await reloadDecks();
360
+ rememberReference({ id: importedReference.id, kind: "deck" });
361
+ return importedReference;
238
362
  } catch (error) {
239
363
  toast.error(t("editorToolbar.uploadFailed"), {
240
364
  description:
@@ -242,11 +366,12 @@ export function FirstDeckOnboardingFlow({
242
366
  ? error.message
243
367
  : t("editorToolbar.importFailedDescription"),
244
368
  });
369
+ return null;
245
370
  } finally {
246
371
  setReferenceImporting(false);
247
372
  }
248
373
  },
249
- [promptFiles, reloadDecks, rememberReference, startGeneration, t],
374
+ [callAction, reloadDecks, rememberReference, t],
250
375
  );
251
376
 
252
377
  const handleReferenceSkip = useCallback(() => {
@@ -267,6 +392,7 @@ export function FirstDeckOnboardingFlow({
267
392
  recentReferences={recentReferences}
268
393
  onSelect={handleReferenceSelect}
269
394
  onImport={handleReferenceImport}
395
+ onImportSource={handleReferenceSourceImport}
270
396
  onSkip={handleReferenceSkip}
271
397
  onOpenChange={(open) => {
272
398
  if (!open) setStep("prompt");
@@ -278,7 +404,6 @@ export function FirstDeckOnboardingFlow({
278
404
  chooseDeckLabel={t("home.referenceDeckPlaceholder")}
279
405
  importingLabel={t("raw.uploading")}
280
406
  skipLabel={t("home.referenceDeckNone")}
281
- defaultSuffix={t("home.defaultSuffix")}
282
407
  starredLabel={t("home.referenceDeckStarredGroup")}
283
408
  otherDecksLabel={t("home.referenceDeckOtherGroup")}
284
409
  searchDecksLabel={t("root.searchDecks")}
@@ -672,6 +672,12 @@ const messages = {
672
672
  chooseReferences: "Choose references",
673
673
  addDesignSystem: "+ Design system",
674
674
  importFrom: "Import from",
675
+ imported: "Imported",
676
+ importedReferenceDeck: "Imported reference deck",
677
+ referenceImportSuccess: "Imported successfully",
678
+ referenceImportSelected: '"{{title}}" is now the reference deck.',
679
+ continue: "Continue",
680
+ continueToGenerate: "Continue to generate",
675
681
  designSystem: "Design system",
676
682
  referenceDeck: "Reference deck",
677
683
  referenceDeckPlaceholder: "Match the style of an existing deck",
@@ -10,9 +10,16 @@
10
10
  * largest rendered element so a thumbnail's transform: scale(0.25)
11
11
  * doesn't shrink the captured pixels.
12
12
  */
13
+ import { appBasePath } from "@agent-native/core/client/api-path";
14
+
13
15
  import { type AspectRatio, getAspectRatioDims } from "./aspect-ratios";
14
16
  import { importExportModule } from "./dynamic-import";
15
17
 
18
+ /** Same-origin URL that re-serves a remote image, bypassing its missing CORS. */
19
+ export function imageProxyUrl(src: string): string {
20
+ return `${appBasePath()}/api/image-proxy?url=${encodeURIComponent(src)}`;
21
+ }
22
+
16
23
  /**
17
24
  * Cross-origin <img> elements without an explicit `crossOrigin="anonymous"`
18
25
  * attribute taint the canvas when rasterized via <foreignObject>, producing
@@ -21,8 +28,15 @@ import { importExportModule } from "./dynamic-import";
21
28
  * setting the attribute and re-assigning the same src. This is the root
22
29
  * cause of the "blank images in exported PDF" bug Rochkind reported.
23
30
  */
24
- export async function preloadImagesWithCors(root: HTMLElement): Promise<void> {
31
+ export async function preloadImagesWithCors(
32
+ root: HTMLElement,
33
+ ): Promise<() => void> {
25
34
  const imgs = Array.from(root.querySelectorAll<HTMLImageElement>("img"));
35
+ // The slide DOM is the live editor canvas. Anything rewritten here would
36
+ // otherwise be picked up by the next save and persisted into the deck, so
37
+ // every mutation is recorded and undone once the capture is done.
38
+ const restores: Array<() => void> = [];
39
+
26
40
  await Promise.all(
27
41
  imgs.map(async (img) => {
28
42
  const src = img.currentSrc || img.src;
@@ -35,30 +49,59 @@ export async function preloadImagesWithCors(root: HTMLElement): Promise<void> {
35
49
  isCrossOrigin = false;
36
50
  }
37
51
  if (!isCrossOrigin) return;
52
+
53
+ const originalCrossOrigin = img.getAttribute("crossorigin");
54
+ const originalSrc = img.getAttribute("src");
55
+ const restore = () => {
56
+ if (originalCrossOrigin === null) img.removeAttribute("crossorigin");
57
+ else img.setAttribute("crossorigin", originalCrossOrigin);
58
+ if (originalSrc === null) img.removeAttribute("src");
59
+ else img.setAttribute("src", originalSrc);
60
+ };
61
+
38
62
  if (img.crossOrigin === "anonymous") {
39
63
  // Already CORS-enabled; just make sure it's decoded.
40
64
  try {
41
65
  await img.decode();
66
+ return;
42
67
  } catch {
43
- /* ignore */
68
+ // coercion-ok: a failed direct load is the signal to try the proxy,
69
+ // and the proxy attempt below reports its own failure.
70
+ }
71
+ } else {
72
+ img.crossOrigin = "anonymous";
73
+ // Re-set src to retrigger the load with the new CORS attribute.
74
+ img.src = src;
75
+ restores.push(restore);
76
+ try {
77
+ await img.decode();
78
+ return;
79
+ } catch {
80
+ // coercion-ok: same as above — this is the CORS probe, not the
81
+ // final outcome.
44
82
  }
45
- return;
46
83
  }
84
+
85
+ // The host does not send Access-Control-Allow-Origin, and no client-side
86
+ // flag can override that. Re-serve the image from our own origin so the
87
+ // canvas stays clean instead of rasterizing a blank rect.
88
+ if (!restores.includes(restore)) restores.push(restore);
47
89
  img.crossOrigin = "anonymous";
48
- // Re-set src to retrigger the load with the new CORS attribute.
49
- img.src = src;
90
+ img.src = imageProxyUrl(src);
50
91
  try {
51
92
  await img.decode();
52
93
  } catch (err) {
53
- // Server didn't return Access-Control-Allow-Origin. The screenshot
54
- // will be blank for this image — log so the user can swap the host.
55
94
  console.warn(
56
- `[export-pdf] CORS-tainted image likely caused blank render: ${src}`,
95
+ `[export-pdf] image could not be loaded for export: ${src}`,
57
96
  err,
58
97
  );
59
98
  }
60
99
  }),
61
100
  );
101
+
102
+ return () => {
103
+ for (const restore of restores) restore();
104
+ };
62
105
  }
63
106
 
64
107
  export function findSlideExportSource(
@@ -132,23 +175,39 @@ export async function exportDeckAsPdf(
132
175
  // Force CORS-enabled re-fetch on every cross-origin <img> before
133
176
  // capture — otherwise the canvas tainting check inside modern-screenshot
134
177
  // produces a blank rect for the image.
135
- await preloadImagesWithCors(source);
136
-
137
- const dataUrl = await domToJpeg(source, {
138
- width: dims.width,
139
- height: dims.height,
140
- scale: 2, // 2x for crisp text
141
- backgroundColor: "#000000",
142
- quality: 0.92,
143
- // Pair with the in-DOM CORS preload above. modern-screenshot's
144
- // internal image fetcher needs no-cache so re-issued requests don't
145
- // get served the original tainted (no-CORS) response from the HTTP
146
- // cache, and an anonymous-CORS request mode so the response itself
147
- // is usable on a clean canvas.
148
- fetch: {
149
- requestInit: { cache: "no-cache", mode: "cors", credentials: "omit" },
150
- },
151
- });
178
+ const restoreImages = await preloadImagesWithCors(source);
179
+
180
+ let dataUrl: string;
181
+ try {
182
+ dataUrl = await domToJpeg(source, {
183
+ width: dims.width,
184
+ height: dims.height,
185
+ scale: 2, // 2x for crisp text
186
+ // guard:allow-raw-color — a PDF page has no theme to follow.
187
+ backgroundColor: "#000000",
188
+ quality: 0.92,
189
+ // Pair with the in-DOM CORS preload above. modern-screenshot's
190
+ // internal image fetcher needs no-cache so re-issued requests don't
191
+ // get served the original tainted (no-CORS) response from the HTTP
192
+ // cache, and an anonymous-CORS request mode so the response itself
193
+ // is usable on a clean canvas.
194
+ //
195
+ // `same-origin` rather than `omit`: images the preload rewrote to
196
+ // /api/image-proxy are same-origin and that route needs the session
197
+ // cookie, so omitting credentials would 401 exactly the images this
198
+ // is meant to rescue. Cross-origin requests still go out anonymously,
199
+ // which is what CORS mode requires.
200
+ fetch: {
201
+ requestInit: {
202
+ cache: "no-cache",
203
+ mode: "cors",
204
+ credentials: "same-origin",
205
+ },
206
+ },
207
+ });
208
+ } finally {
209
+ restoreImages();
210
+ }
152
211
 
153
212
  if (i > 0) pdf.addPage([dims.width, dims.height], orientation);
154
213
  pdf.addImage(dataUrl, "JPEG", 0, 0, dims.width, dims.height);
@@ -2,12 +2,18 @@ export function shouldShowNewDeckGeneratingOverlay({
2
2
  generating,
3
3
  isNewDeckCreation,
4
4
  slideCount,
5
+ generationStarted,
5
6
  }: {
6
7
  generating: boolean;
7
8
  isNewDeckCreation: boolean;
8
9
  slideCount?: number | null;
10
+ generationStarted: boolean;
9
11
  }): boolean {
10
- return generating && isNewDeckCreation && (slideCount ?? 0) === 0;
12
+ return (
13
+ isNewDeckCreation &&
14
+ (slideCount ?? 0) === 0 &&
15
+ (generating || !generationStarted)
16
+ );
11
17
  }
12
18
 
13
19
  export function shouldShowNewDeckGeneratingProgress({
@@ -9,6 +9,16 @@ export type ImportedSourceDeck = {
9
9
  imagesSkipped: number;
10
10
  };
11
11
 
12
+ /**
13
+ * PDF/PPTX import renders every page (image extraction, per-page fidelity
14
+ * parsing) and can run well past the client's default 60s action timeout on
15
+ * large or image-heavy files. A timeout here only aborts the client's wait —
16
+ * the server keeps importing and the deck still ends up with slides — so the
17
+ * default made the editor silently fail on a deck that had, or was about to
18
+ * have, real content.
19
+ */
20
+ export const IMPORT_ACTION_TIMEOUT_MS = 5 * 60 * 1000;
21
+
12
22
  function sourceFormat(file: UploadedFile): "pdf" | "pptx" | null {
13
23
  const name = file.originalName.toLowerCase();
14
24
  if (name.endsWith(".pptx")) return "pptx";
@@ -57,6 +67,7 @@ export async function importUploadedDeckIntoDeck(
57
67
  deckId,
58
68
  importIntoDeck: true,
59
69
  },
70
+ { timeoutMs: IMPORT_ACTION_TIMEOUT_MS },
60
71
  ),
61
72
  );
62
73
  if (result.imported !== true || result.deckId !== deckId) {
@@ -287,6 +287,7 @@ export default function DeckEditor() {
287
287
  generating,
288
288
  isNewDeckCreation: wasNewDeckCreation.current,
289
289
  slideCount,
290
+ generationStarted: newDeckGenerationStarted.current,
290
291
  });
291
292
  const { designSystem, imageStyleReferenceUrls } = useDeckDesignSystem(
292
293
  deck?.designSystemId,
@@ -326,7 +327,9 @@ export default function DeckEditor() {
326
327
 
327
328
  const showQuestionFlow = Boolean(questionFlowQuestions?.length);
328
329
  const generatingSlideVisible =
329
- canEdit && !showQuestionFlow && (isNewDeckGenerating || addSlideGenerating);
330
+ canEdit &&
331
+ !showQuestionFlow &&
332
+ (isNewDeckGenerating || addSlideGenerating || showNewDeckGeneratingOverlay);
330
333
  const showCurrentSlideEditor =
331
334
  !generatingSlideSelected &&
332
335
  !showNewDeckGeneratingOverlay &&
@@ -342,12 +345,47 @@ export default function DeckEditor() {
342
345
  if (!generating) setAddSlideGenerating(false);
343
346
  }, [generating]);
344
347
 
345
- const previousSlideCountRef = useRef(slideCount);
348
+ // Below `md` the rail is a drawer behind a full-viewport dimming scrim; at
349
+ // `md` and up it's docked with no scrim. `sidebarOpen` is seeded from the
350
+ // width at mount only, so a window that starts wide and is then narrowed
351
+ // (or an editor opened in a resizable preview pane) keeps `sidebarOpen`
352
+ // true while the scrim stops being `md:hidden` — dimming the whole editor
353
+ // with no way to dismiss it.
346
354
  useEffect(() => {
347
- if (previousSlideCountRef.current === slideCount) return;
348
- previousSlideCountRef.current = slideCount;
349
- setGeneratingSlideSelected(false);
350
- }, [slideCount]);
355
+ const onResize = () => setSidebarOpen(window.innerWidth >= 768);
356
+ window.addEventListener("resize", onResize);
357
+ return () => window.removeEventListener("resize", onResize);
358
+ }, []);
359
+
360
+ const previousSlideIdsRef = useRef<string[]>([]);
361
+ useEffect(() => {
362
+ const currentSlideIds = deck?.slides.map((slide) => slide.id) ?? [];
363
+ const previousSlideIds = previousSlideIdsRef.current;
364
+ const addedSlide = deck?.slides.find(
365
+ (slide) => !previousSlideIds.includes(slide.id),
366
+ );
367
+ const slideWasAdded = currentSlideIds.length > previousSlideIds.length;
368
+
369
+ if (
370
+ slideWasAdded &&
371
+ addedSlide &&
372
+ (generating ||
373
+ isNewDeckGenerating ||
374
+ addSlideGenerating ||
375
+ generatingSlideSelected)
376
+ ) {
377
+ setActiveSlideId(addedSlide.id);
378
+ }
379
+
380
+ previousSlideIdsRef.current = currentSlideIds;
381
+ if (slideWasAdded) setGeneratingSlideSelected(false);
382
+ }, [
383
+ addSlideGenerating,
384
+ deck,
385
+ generating,
386
+ generatingSlideSelected,
387
+ isNewDeckGenerating,
388
+ ]);
351
389
 
352
390
  useEffect(() => {
353
391
  if (
@@ -1128,14 +1166,6 @@ export default function DeckEditor() {
1128
1166
  setActiveSlideId(slideId);
1129
1167
  if (window.innerWidth < 768) setSidebarOpen(false);
1130
1168
  }}
1131
- onDuplicateSlide={(slideId) => duplicateSlide(id, slideId)}
1132
- onDeleteSlide={(slideId) => {
1133
- const idx = deck.slides.findIndex((s) => s.id === slideId);
1134
- const nextSlide =
1135
- deck.slides[idx + 1] || deck.slides[idx - 1];
1136
- deleteSlideWithUndo(id, slideId);
1137
- if (nextSlide) setActiveSlideId(nextSlide.id);
1138
- }}
1139
1169
  readOnly={!canEdit}
1140
1170
  slidePresence={slidePresence}
1141
1171
  recentEdits={deckRecentEdits}