@agent-native/core 0.144.0 → 0.144.2

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 (88) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +1 -1
  3. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +36 -14
  4. package/corpus/templates/clips/app/components/player/player-controls.tsx +0 -3
  5. package/corpus/templates/clips/app/components/player/scrubber.tsx +2 -33
  6. package/corpus/templates/clips/app/components/player/video-player.tsx +58 -11
  7. package/corpus/templates/clips/changelog/2026-08-06-trimmed-clips-play-on-a-clean-timeline-without-cut-markers.md +6 -0
  8. package/corpus/templates/dispatch/AGENTS.md +8 -1
  9. package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -0
  10. package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +30 -15
  11. package/corpus/templates/dispatch/app/i18n-data.ts +131 -0
  12. package/corpus/templates/dispatch/app/routes/admin._index.tsx +4 -0
  13. package/corpus/templates/dispatch/app/routes/admin.agents.tsx +1 -0
  14. package/corpus/templates/dispatch/app/routes/admin.approvals.tsx +1 -0
  15. package/corpus/templates/dispatch/app/routes/admin.apps.$appId.tsx +1 -0
  16. package/corpus/templates/dispatch/app/routes/admin.apps.tsx +1 -0
  17. package/corpus/templates/dispatch/app/routes/admin.audit.tsx +1 -0
  18. package/corpus/templates/dispatch/app/routes/admin.automations.tsx +1 -0
  19. package/corpus/templates/dispatch/app/routes/admin.destinations.tsx +4 -0
  20. package/corpus/templates/dispatch/app/routes/admin.dreams.tsx +1 -0
  21. package/corpus/templates/dispatch/app/routes/admin.identities.tsx +1 -0
  22. package/corpus/templates/dispatch/app/routes/admin.integrations.tsx +4 -0
  23. package/corpus/templates/dispatch/app/routes/admin.messaging.tsx +1 -0
  24. package/corpus/templates/dispatch/app/routes/admin.metrics.tsx +1 -0
  25. package/corpus/templates/dispatch/app/routes/admin.new-app.tsx +1 -0
  26. package/corpus/templates/dispatch/app/routes/admin.operations.tsx +1 -0
  27. package/corpus/templates/dispatch/app/routes/admin.thread-debug.tsx +4 -0
  28. package/corpus/templates/dispatch/app/routes/admin.transactional-email.$appId.$id.tsx +4 -0
  29. package/corpus/templates/dispatch/app/routes/admin.transactional-email.tsx +4 -0
  30. package/corpus/templates/dispatch/app/routes/admin.tsx +1 -0
  31. package/corpus/templates/dispatch/app/routes/admin.vault.tsx +1 -0
  32. package/corpus/templates/dispatch/app/routes/admin.workspace.tsx +1 -0
  33. package/corpus/templates/dispatch/changelog/2026-08-06-dispatch-keeps-everyday-navigation-focused-while-management-.md +6 -0
  34. package/corpus/templates/slides/AGENTS.md +10 -12
  35. package/corpus/templates/slides/actions/export-pptx.ts +68 -0
  36. package/corpus/templates/slides/actions/get-deck.ts +12 -0
  37. package/corpus/templates/slides/actions/import-file.ts +130 -55
  38. package/corpus/templates/slides/actions/import-pptx.ts +108 -8
  39. package/corpus/templates/slides/actions/patch-deck.ts +51 -0
  40. package/corpus/templates/slides/actions/update-slide.ts +29 -3
  41. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +11 -0
  42. package/corpus/templates/slides/app/lib/create-deck-generation.ts +72 -11
  43. package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +89 -0
  44. package/corpus/templates/slides/app/pages/Index.tsx +85 -18
  45. package/corpus/templates/slides/changelog/2026-08-06-uploaded-decks-keep-their-slides-and-visuals-when-you-improv.md +6 -0
  46. package/corpus/templates/slides/server/handlers/import/html-converter.ts +13 -2
  47. package/corpus/templates/slides/server/lib/chat-attachments.ts +2 -2
  48. package/corpus/templates/slides/server/lib/source-import.ts +167 -0
  49. package/corpus/templates/slides/server/plugins/agent-chat.ts +8 -0
  50. package/dist/client/analytics/index.d.ts +1 -1
  51. package/dist/client/analytics/index.js +1 -1
  52. package/dist/client/analytics-session.d.ts +13 -0
  53. package/dist/client/analytics-session.js +57 -0
  54. package/dist/client/analytics.d.ts +1 -0
  55. package/dist/client/analytics.js +1 -0
  56. package/dist/client/index.d.ts +1 -1
  57. package/dist/client/index.js +1 -1
  58. package/dist/client/track.js +7 -0
  59. package/dist/client/use-action.js +10 -1
  60. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  61. package/dist/jobs/scheduler.js +79 -23
  62. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  63. package/dist/notifications/routes.d.ts +3 -3
  64. package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
  65. package/dist/resources/handlers.d.ts +1 -1
  66. package/dist/secrets/routes.d.ts +9 -9
  67. package/dist/server/action-routes.js +4 -2
  68. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  69. package/dist/server/agent-run-context.d.ts +4 -2
  70. package/dist/server/agent-run-context.js +5 -3
  71. package/dist/server/core-routes-plugin.js +2 -0
  72. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +5 -1
  73. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +20 -3
  74. package/dist/tracking/index.d.ts +1 -1
  75. package/dist/tracking/providers.js +12 -1
  76. package/dist/tracking/registry.d.ts +12 -2
  77. package/dist/tracking/registry.js +27 -3
  78. package/dist/tracking/types.d.ts +8 -0
  79. package/docs/content/creating-templates.mdx +1 -1
  80. package/docs/content/getting-started-actions.mdx +38 -189
  81. package/docs/content/getting-started-database.mdx +80 -164
  82. package/docs/content/getting-started-pages.mdx +44 -155
  83. package/docs/content/getting-started.mdx +26 -106
  84. package/docs/content/observability.mdx +16 -0
  85. package/docs/content/tracking.mdx +17 -2
  86. package/package.json +1 -1
  87. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +5 -1
  88. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +20 -3
@@ -18,6 +18,12 @@ import { notifyClients } from "../server/handlers/decks.js";
18
18
  import { uploadPptxSlideImages } from "../server/handlers/import/pptx-assets.js";
19
19
  import { upsertBuilderProxyDesignSystem } from "../server/lib/builder-design-system-proxy.js";
20
20
  import { setupPdfParse } from "../server/lib/pdf-parse-setup.js";
21
+ import {
22
+ buildSourceImportMetadata,
23
+ mergeSourceImportMetadata,
24
+ sourceImportForDeck,
25
+ type SourceImportSlideSnapshot,
26
+ } from "../server/lib/source-import.js";
21
27
  import {
22
28
  ASPECT_RATIOS,
23
29
  DEFAULT_ASPECT_RATIO,
@@ -31,7 +37,7 @@ const DEFAULT_MAX_SOURCE_CHARS = 60_000;
31
37
  export default defineAction({
32
38
  description:
33
39
  "Import a file (PPTX, DOCX, PDF, FIG) and extract content for creating slides or slide design systems. " +
34
- "For PPTX files, returns parsed slides with text and layout info ready for conversion. " +
40
+ "For PPTX files, returns parsed slides with text and layout info ready for conversion, or writes positioned source-preserving slides when importIntoDeck is true. " +
35
41
  "For DOCX files, returns structured sections extracted from the document. " +
36
42
  "For PDF files, returns extracted text organized by page, or source-faithful page-image slides when importIntoDeck is true. " +
37
43
  "For Figma .fig files, requires Builder.io (free tier available) and starts Builder design-system indexing; the returned Builder job/design-system ids are the source of truth. " +
@@ -153,7 +159,37 @@ export default defineAction({
153
159
  (total, r) => total + r.imageSkippedCount,
154
160
  0,
155
161
  );
156
- await appendDeckSlides(deckId, title, slides, "import-file:pptx");
162
+ const sourceImport = buildSourceImportMetadata({
163
+ format: "pptx",
164
+ slides: pptxResults.map((result) => ({
165
+ id: result.slide.id,
166
+ text: result.sourceText,
167
+ notes: result.slide.notes ?? "",
168
+ imageUrls: result.imageUrls,
169
+ editableText: true,
170
+ })),
171
+ imagesSkipped,
172
+ });
173
+ if (imagesSkipped > 0) {
174
+ throw new Error(
175
+ `Source-faithful PPTX import could not preserve ${imagesSkipped} image(s). No slides were written. Retry with browser-renderable images or use a PDF export for page-faithful preservation.`,
176
+ );
177
+ }
178
+ const aspectRatio =
179
+ presentation.slides[0]?.widthEmu && presentation.slides[0]?.heightEmu
180
+ ? nearestAspectRatio(
181
+ presentation.slides[0].widthEmu,
182
+ presentation.slides[0].heightEmu,
183
+ )
184
+ : undefined;
185
+ await appendDeckSlides(
186
+ deckId,
187
+ title,
188
+ slides,
189
+ "import-file:pptx",
190
+ aspectRatio,
191
+ sourceImport,
192
+ );
157
193
  return {
158
194
  format: "pptx",
159
195
  title,
@@ -241,9 +277,15 @@ export default defineAction({
241
277
  // typography) bake their visuals into vector/image page content with
242
278
  // no reliable text/shape structure to reconstruct, so importing into a
243
279
  // deck rasterizes each page instead of flattening it to generic bullet
244
- // text. Falls through to the text-only path below when the optional
245
- // canvas renderer isn't available in this runtime.
246
- if (importIntoDeck && canvasFactory) {
280
+ // text. Do not silently fall back to text reconstruction when the
281
+ // renderer is unavailable: that is how a visually complete source deck
282
+ // turns into a misleading, content-lossy import.
283
+ if (importIntoDeck) {
284
+ if (!canvasFactory) {
285
+ throw new Error(
286
+ "Source-faithful PDF import is unavailable in this runtime because the page renderer is not configured. Retry after connecting file storage or use a PDF export with a supported Slides deployment.",
287
+ );
288
+ }
247
289
  if (!deckId) throw new Error("deckId is required to import into deck");
248
290
  return importPdfPagesAsFullBleedSlides({
249
291
  fileBuffer,
@@ -254,8 +296,6 @@ export default defineAction({
254
296
  });
255
297
  }
256
298
 
257
- const { convertSectionsToSlides } =
258
- await import("../server/handlers/import/html-converter.js");
259
299
  const pdf = new PDFParse({
260
300
  data: new Uint8Array(fileBuffer),
261
301
  CanvasFactory: canvasFactory,
@@ -270,29 +310,6 @@ export default defineAction({
270
310
  );
271
311
  }
272
312
 
273
- if (importIntoDeck) {
274
- if (!deckId) throw new Error("deckId is required to import into deck");
275
- const sections = textPages.map((p) => ({
276
- heading: `Page ${p.num}`,
277
- content: p.text,
278
- }));
279
- const slideHtmlArray = convertSectionsToSlides(sections);
280
- const slides = slideHtmlArray.map((content) => ({
281
- id: newSlideId(),
282
- content,
283
- layout: "content",
284
- notes: "",
285
- }));
286
- await appendDeckSlides(deckId, title, slides, "import-file:pdf");
287
- return {
288
- format: "pdf",
289
- title,
290
- pageCount: pages.length,
291
- slideCount: slides.length,
292
- deckId,
293
- imported: true,
294
- };
295
- }
296
313
  const totalTextLength = textPages.reduce(
297
314
  (sum, p) => sum + p.text.length,
298
315
  0,
@@ -342,7 +359,7 @@ async function importPdfPagesAsFullBleedSlides(args: {
342
359
  canvasFactory: object;
343
360
  }) {
344
361
  const { fileBuffer, title, deckId, PDFParse, canvasFactory } = args;
345
- const { buildFullPageImageSlideHtml, convertSectionsToSlides } =
362
+ const { buildFullPageImageSlideHtml } =
346
363
  await import("../server/handlers/import/html-converter.js");
347
364
 
348
365
  const pdf = new PDFParse({
@@ -355,7 +372,15 @@ async function importPdfPagesAsFullBleedSlides(args: {
355
372
  { data: Uint8Array; width: number; height: number }
356
373
  >;
357
374
  try {
358
- pages = normalizePdfPages(await pdf.getText());
375
+ let textPages: { num: number; text: string }[] = [];
376
+ try {
377
+ textPages = normalizePdfPages(await pdf.getText());
378
+ } catch (error) {
379
+ console.warn(
380
+ "[import-file] PDF text extraction failed; preserving rendered pages and leaving notes empty:",
381
+ error,
382
+ );
383
+ }
359
384
  // Rasterize the complete page so positioned text, vector shapes, logos,
360
385
  // photos, and the source page proportions survive the import together.
361
386
  const screenshotResult = await pdf.getScreenshot({
@@ -371,10 +396,23 @@ async function importPdfPagesAsFullBleedSlides(args: {
371
396
  height: page.height,
372
397
  });
373
398
  }
399
+ if (screenshotsByPage.size === 0) {
400
+ throw new Error("The PDF renderer returned no pages.");
401
+ }
402
+ pages = screenshotResult.pages.map((page) => ({
403
+ num: page.pageNumber,
404
+ text:
405
+ textPages.find((textPage) => textPage.num === page.pageNumber)?.text ??
406
+ "",
407
+ }));
374
408
  } finally {
375
409
  await pdf.destroy();
376
410
  }
377
411
 
412
+ if (pages.length === 0) {
413
+ throw new Error("The PDF renderer returned no importable pages.");
414
+ }
415
+
378
416
  // Source decks (e.g. Instagram carousel exports) are commonly portrait or
379
417
  // square, not the deck editor's 16:9 default — match the canvas to the
380
418
  // first rendered page's own proportions instead of stretching/cropping it.
@@ -386,24 +424,14 @@ async function importPdfPagesAsFullBleedSlides(args: {
386
424
  : undefined;
387
425
 
388
426
  const ownerEmail = getRequestUserEmail();
389
- const slides = await Promise.all(
427
+ const imported = await Promise.all(
390
428
  pages.map(async (page) => {
391
- const lines = page.text
392
- .split(/\r?\n/)
393
- .map((line) => line.trim())
394
- .filter(Boolean);
395
429
  const screenshot = screenshotsByPage.get(page.num);
396
430
 
397
431
  if (!screenshot) {
398
- const [content] = convertSectionsToSlides([
399
- { heading: lines[0] ?? `Page ${page.num}`, content: page.text },
400
- ]);
401
- return {
402
- id: newSlideId(),
403
- content: content ?? '<div class="fmd-slide"></div>',
404
- layout: "content",
405
- notes: "",
406
- };
432
+ throw new Error(
433
+ `The PDF renderer did not return page ${page.num}; refusing a partial source import.`,
434
+ );
407
435
  }
408
436
 
409
437
  const uploadResult = await uploadFile({
@@ -418,16 +446,42 @@ async function importPdfPagesAsFullBleedSlides(args: {
418
446
  "File storage is not configured. Connect Builder.io (free tier available) or another upload provider before importing PDF slides.",
419
447
  );
420
448
  }
449
+ const id = newSlideId();
421
450
  return {
422
- id: newSlideId(),
423
- content: buildFullPageImageSlideHtml(uploadResult.url),
424
- layout: "full-image",
425
- notes: page.text,
451
+ slide: {
452
+ id,
453
+ content: buildFullPageImageSlideHtml(
454
+ uploadResult.url,
455
+ screenshot.width,
456
+ screenshot.height,
457
+ ),
458
+ layout: "full-image",
459
+ notes: page.text,
460
+ },
461
+ snapshot: {
462
+ id,
463
+ text: page.text,
464
+ notes: page.text,
465
+ imageUrls: [uploadResult.url],
466
+ editableText: false,
467
+ } satisfies SourceImportSlideSnapshot,
426
468
  };
427
469
  }),
428
470
  );
471
+ const slides = imported.map((entry) => entry.slide);
472
+ const sourceImport = buildSourceImportMetadata({
473
+ format: "pdf",
474
+ slides: imported.map((entry) => entry.snapshot),
475
+ });
429
476
 
430
- await appendDeckSlides(deckId, title, slides, "import-file:pdf", aspectRatio);
477
+ await appendDeckSlides(
478
+ deckId,
479
+ title,
480
+ slides,
481
+ "import-file:pdf",
482
+ aspectRatio,
483
+ sourceImport,
484
+ );
431
485
 
432
486
  return {
433
487
  format: "pdf",
@@ -459,6 +513,8 @@ async function buildPptxSlide(
459
513
  splitByParagraph?: boolean;
460
514
  };
461
515
  imageSkippedCount: number;
516
+ sourceText: string;
517
+ imageUrls: string[];
462
518
  }> {
463
519
  const { convertToSlideHtml } =
464
520
  await import("../server/handlers/import/html-converter.js");
@@ -467,9 +523,10 @@ async function buildPptxSlide(
467
523
  slideIndex,
468
524
  ownerEmail,
469
525
  });
526
+ const id = newSlideId();
470
527
  return {
471
528
  slide: {
472
- id: newSlideId(),
529
+ id,
473
530
  content: convertToSlideHtml(slide, uploadedImages.urls, themeFont),
474
531
  layout: slide.layoutHint ?? "content",
475
532
  notes: slide.notes,
@@ -477,6 +534,8 @@ async function buildPptxSlide(
477
534
  ...(slide.splitByParagraph ? { splitByParagraph: true } : {}),
478
535
  },
479
536
  imageSkippedCount: uploadedImages.imageSkippedCount,
537
+ sourceText: slide.texts.map((text) => text.content).join("\n"),
538
+ imageUrls: Object.values(uploadedImages.urls),
480
539
  };
481
540
  }
482
541
 
@@ -568,6 +627,7 @@ async function appendDeckSlides(
568
627
  }>,
569
628
  source: string,
570
629
  aspectRatio?: AspectRatio,
630
+ sourceImport?: ReturnType<typeof buildSourceImportMetadata>,
571
631
  ) {
572
632
  await assertAccess("deck", deckId, "editor");
573
633
 
@@ -601,11 +661,18 @@ async function appendDeckSlides(
601
661
  // every slide already on it.
602
662
  const hadExistingSlides = previousSlides.length > 0;
603
663
  const nextTitle = hadExistingSlides ? (existing[0].title ?? title) : title;
664
+ const nextSourceImport = sourceImport
665
+ ? mergeSourceImportMetadata(
666
+ sourceImportForDeck(previousData.sourceImport),
667
+ sourceImport,
668
+ )
669
+ : sourceImportForDeck(previousData.sourceImport);
604
670
  const data = {
605
671
  ...previousData,
606
672
  title: nextTitle,
607
673
  slides: [...previousSlides, ...slides],
608
674
  ...(!hadExistingSlides && aspectRatio ? { aspectRatio } : {}),
675
+ ...(nextSourceImport ? { sourceImport: nextSourceImport } : {}),
609
676
  updatedAt: writeNow,
610
677
  };
611
678
 
@@ -626,12 +693,20 @@ async function appendDeckSlides(
626
693
  }
627
694
 
628
695
  function safeParseDeckData(raw: string): Record<string, unknown> {
696
+ let parsed: unknown;
629
697
  try {
630
- const parsed = JSON.parse(raw);
631
- return parsed && typeof parsed === "object" ? parsed : {};
698
+ parsed = JSON.parse(raw);
632
699
  } catch {
633
- return {};
700
+ throw new Error(
701
+ "The target deck contains invalid JSON; refusing to overwrite it.",
702
+ );
703
+ }
704
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
705
+ throw new Error(
706
+ "The target deck data is invalid; refusing to overwrite it.",
707
+ );
634
708
  }
709
+ return parsed as Record<string, unknown>;
635
710
  }
636
711
 
637
712
  function stripTags(html: string): string {
@@ -19,6 +19,12 @@ import {
19
19
  type ParsedImage,
20
20
  type ParsedPresentation,
21
21
  } from "../server/handlers/import/pptx-parser.js";
22
+ import { buildSourceImportMetadata } from "../server/lib/source-import.js";
23
+ import {
24
+ ASPECT_RATIOS,
25
+ DEFAULT_ASPECT_RATIO,
26
+ type AspectRatio,
27
+ } from "../shared/aspect-ratios.js";
22
28
  import { getDeckUrl } from "./_app-url.js";
23
29
  import { readUserUploadedFile } from "./_uploaded-files.js";
24
30
 
@@ -82,6 +88,7 @@ export async function importPptxBufferToDeck(args: {
82
88
  fileBuffer: Buffer;
83
89
  title?: string;
84
90
  deckId?: string;
91
+ designSystemId?: string | null;
85
92
  source?: string;
86
93
  imageFallbacks?: ImportedImageFallback[];
87
94
  parsedPresentation?: ParsedPresentation;
@@ -98,6 +105,7 @@ export async function importPptxBufferToDeck(args: {
98
105
  fileBuffer,
99
106
  title,
100
107
  deckId,
108
+ designSystemId,
101
109
  source = "import-pptx",
102
110
  imageFallbacks,
103
111
  parsedPresentation,
@@ -133,9 +141,10 @@ export async function importPptxBufferToDeck(args: {
133
141
  uploadedImages.urls,
134
142
  themeFont,
135
143
  );
144
+ const id = `slide-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
136
145
  return {
137
146
  slide: {
138
- id: `slide-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
147
+ id,
139
148
  content: html,
140
149
  layout: parsedSlide.layoutHint ?? "content",
141
150
  notes: parsedSlide.notes,
@@ -144,6 +153,8 @@ export async function importPptxBufferToDeck(args: {
144
153
  : {}),
145
154
  ...(parsedSlide.splitByParagraph ? { splitByParagraph: true } : {}),
146
155
  },
156
+ sourceText: parsedSlide.texts.map((text) => text.content).join("\n"),
157
+ imageUrls: Object.values(uploadedImages.urls),
147
158
  imageSkippedCount: uploadedImages.imageSkippedCount,
148
159
  };
149
160
  }),
@@ -154,6 +165,26 @@ export async function importPptxBufferToDeck(args: {
154
165
  (total, r) => total + r.imageSkippedCount,
155
166
  0,
156
167
  );
168
+ if (imagesSkipped > 0) {
169
+ throw new Error(
170
+ `Source-faithful PPTX import could not preserve ${imagesSkipped} image(s). No deck was written. Retry with browser-renderable images or use a PDF export for page-faithful preservation.`,
171
+ );
172
+ }
173
+ const sourceImport = buildSourceImportMetadata({
174
+ format: "pptx",
175
+ slides: results.map((result) => ({
176
+ id: result.slide.id,
177
+ text: result.sourceText,
178
+ notes: result.slide.notes ?? "",
179
+ imageUrls: result.imageUrls,
180
+ editableText: true,
181
+ })),
182
+ imagesSkipped,
183
+ });
184
+ const aspectRatio = nearestAspectRatio(
185
+ presentation.slides[0]?.widthEmu,
186
+ presentation.slides[0]?.heightEmu,
187
+ );
157
188
 
158
189
  const db = getDb();
159
190
  const now = new Date().toISOString();
@@ -168,10 +199,25 @@ export async function importPptxBufferToDeck(args: {
168
199
  throw new Error(`Deck ${deckId} not found`);
169
200
  }
170
201
 
171
- const data = { title: deckTitle, slides, updatedAt: now };
202
+ const previousData = safeParseDeckData(existing[0].data);
203
+ const data = {
204
+ ...previousData,
205
+ title: deckTitle,
206
+ slides,
207
+ ...(aspectRatio ? { aspectRatio } : {}),
208
+ sourceImport,
209
+ updatedAt: now,
210
+ };
172
211
  await db
173
212
  .update(schema.decks)
174
- .set({ title: deckTitle, data: JSON.stringify(data), updatedAt: now })
213
+ .set({
214
+ title: deckTitle,
215
+ data: JSON.stringify(data),
216
+ ...(designSystemId !== undefined
217
+ ? { designSystemId }
218
+ : { designSystemId: existing[0].designSystemId }),
219
+ updatedAt: now,
220
+ })
175
221
  .where(eq(schema.decks.id, deckId));
176
222
 
177
223
  notifyClients(deckId);
@@ -193,13 +239,21 @@ export async function importPptxBufferToDeck(args: {
193
239
 
194
240
  // Create new deck
195
241
  const id = `deck-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
196
- const data = { title: deckTitle, slides, createdAt: now, updatedAt: now };
242
+ const data = {
243
+ title: deckTitle,
244
+ slides,
245
+ ...(aspectRatio ? { aspectRatio } : {}),
246
+ sourceImport,
247
+ createdAt: now,
248
+ updatedAt: now,
249
+ };
197
250
  await db.insert(schema.decks).values({
198
251
  id,
199
252
  title: deckTitle,
200
253
  data: JSON.stringify(data),
201
254
  ownerEmail,
202
255
  orgId: getRequestOrgId(),
256
+ designSystemId: designSystemId ?? undefined,
203
257
  createdAt: now,
204
258
  updatedAt: now,
205
259
  });
@@ -222,8 +276,8 @@ export default defineAction({
222
276
  description:
223
277
  "Import a PPTX file and create a slide deck from it. " +
224
278
  "Parses the PowerPoint file, extracts text and layout information, " +
225
- "converts each slide to the app's HTML format, and creates or updates a deck. " +
226
- "Returns the deck ID and slide count.",
279
+ "converts each slide to the app's positioned HTML format, records source-preservation metadata, and creates or updates a deck. " +
280
+ "If an embedded image cannot be preserved, the action fails before writing a partial deck. Returns the deck ID and slide count.",
227
281
  schema: z.object({
228
282
  filePath: z
229
283
  .string()
@@ -234,6 +288,11 @@ export default defineAction({
234
288
  .describe(
235
289
  "If provided, import slides into this existing deck (replaces all slides)",
236
290
  ),
291
+ designSystemId: z
292
+ .string()
293
+ .nullable()
294
+ .optional()
295
+ .describe("Optional design system to link when creating a new deck"),
237
296
  title: z
238
297
  .string()
239
298
  .optional()
@@ -241,8 +300,49 @@ export default defineAction({
241
300
  "Deck title — defaults to the title extracted from the presentation",
242
301
  ),
243
302
  }),
244
- run: async ({ filePath, deckId, title }) => {
303
+ run: async ({ filePath, deckId, title, designSystemId }) => {
245
304
  const { data: fileBuffer } = await readUserUploadedFile(filePath);
246
- return importPptxBufferToDeck({ fileBuffer, deckId, title });
305
+ return importPptxBufferToDeck({
306
+ fileBuffer,
307
+ deckId,
308
+ title,
309
+ designSystemId,
310
+ });
247
311
  },
248
312
  });
313
+
314
+ function nearestAspectRatio(
315
+ width: number | undefined,
316
+ height: number | undefined,
317
+ ): AspectRatio | undefined {
318
+ if (!width || !height || width <= 0 || height <= 0) return undefined;
319
+ const target = width / height;
320
+ let best: AspectRatio = DEFAULT_ASPECT_RATIO;
321
+ let bestDiff = Infinity;
322
+ for (const key of Object.keys(ASPECT_RATIOS) as AspectRatio[]) {
323
+ const preset = ASPECT_RATIOS[key];
324
+ const diff = Math.abs(preset.width / preset.height - target);
325
+ if (diff < bestDiff) {
326
+ best = key;
327
+ bestDiff = diff;
328
+ }
329
+ }
330
+ return best;
331
+ }
332
+
333
+ function safeParseDeckData(raw: string): Record<string, unknown> {
334
+ let parsed: unknown;
335
+ try {
336
+ parsed = JSON.parse(raw);
337
+ } catch {
338
+ throw new Error(
339
+ "The target deck contains invalid JSON; refusing to overwrite it.",
340
+ );
341
+ }
342
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
343
+ throw new Error(
344
+ "The target deck data is invalid; refusing to overwrite it.",
345
+ );
346
+ }
347
+ return parsed as Record<string, unknown>;
348
+ }
@@ -26,6 +26,11 @@ import { z } from "zod";
26
26
  import { normalizeSlidePadding } from "../app/lib/normalize-slide-padding.js";
27
27
  import { getDb, schema } from "../server/db/index.js";
28
28
  import { notifyClients } from "../server/handlers/decks.js";
29
+ import {
30
+ assertSourceSlidePreserved,
31
+ sourceImportForDeck,
32
+ type SourceImportMetadata,
33
+ } from "../server/lib/source-import.js";
29
34
  import { ASPECT_RATIO_VALUES } from "../shared/aspect-ratios.js";
30
35
 
31
36
  // ---------------------------------------------------------------------------
@@ -79,6 +84,13 @@ const PatchSlideOp = z.object({
79
84
  op: z.literal("patch-slide"),
80
85
  slideId: z.string(),
81
86
  fields: SlideFieldsSchema,
87
+ preserveSource: z
88
+ .boolean()
89
+ .optional()
90
+ .default(true)
91
+ .describe(
92
+ "Keep source-imported images and factual copy (default true). Set false only for an explicit rewrite.",
93
+ ),
82
94
  });
83
95
 
84
96
  /** Delete a single slide by ID */
@@ -141,6 +153,24 @@ export const OperationSchema = z.discriminatedUnion("op", [
141
153
 
142
154
  export type Operation = z.infer<typeof OperationSchema>;
143
155
 
156
+ export function assertSourceImportOperationsPreserved(
157
+ metadata: SourceImportMetadata | null,
158
+ operations: Operation[],
159
+ ): void {
160
+ if (!metadata) return;
161
+ const structuralOperation = operations.find(
162
+ (operation) =>
163
+ operation.op === "delete-slide" ||
164
+ operation.op === "reorder-slides" ||
165
+ operation.op === "add-slide",
166
+ );
167
+ if (!structuralOperation) return;
168
+
169
+ throw new Error(
170
+ `Cannot ${structuralOperation.op} on a source-imported deck while source preservation is enabled. Preserve the imported slide structure, or use an explicit source rewrite workflow.`,
171
+ );
172
+ }
173
+
144
174
  // The browser uses the full operation union above. Agents additionally use
145
175
  // this action for one bounded, deck-wide layout repair: one patch-slide per
146
176
  // slide in a single SQL transaction, followed by a fresh read for verification.
@@ -404,6 +434,27 @@ export default defineAction({
404
434
  );
405
435
  }
406
436
 
437
+ const sourceImport = sourceImportForDeck(deck.sourceImport);
438
+ assertSourceImportOperationsPreserved(sourceImport, operations);
439
+ for (const op of operations) {
440
+ if (
441
+ op.op !== "patch-slide" ||
442
+ (op.fields.content === undefined && op.fields.notes === undefined)
443
+ ) {
444
+ continue;
445
+ }
446
+ assertSourceSlidePreserved({
447
+ metadata: sourceImport,
448
+ slideId: op.slideId,
449
+ nextContent:
450
+ op.fields.content === undefined
451
+ ? undefined
452
+ : normalizeSlidePadding(op.fields.content),
453
+ nextNotes: op.fields.notes,
454
+ preserveSource: op.preserveSource,
455
+ });
456
+ }
457
+
407
458
  for (const op of operations) {
408
459
  applyOperation(deck, op);
409
460
  }
@@ -16,6 +16,10 @@ import { normalizeSlidePadding } from "../app/lib/normalize-slide-padding.js";
16
16
  import { getDb, schema } from "../server/db/index.js"; // ensure registerShareableResource runs
17
17
  import { notifyClients } from "../server/handlers/decks.js";
18
18
  import { createDeckVersionSnapshot } from "../server/lib/deck-versions.js";
19
+ import {
20
+ assertSourceSlidePreserved,
21
+ sourceImportForDeck,
22
+ } from "../server/lib/source-import.js";
19
23
  import { hashSlideContent } from "../shared/slide-fit.js";
20
24
  import { slideLabelFor, touchAgentSlidePresence } from "./_agent-presence.js";
21
25
  import {
@@ -89,7 +93,7 @@ function storedCreativeContext(value: unknown): {
89
93
  export default defineAction({
90
94
  description:
91
95
  "Surgically edit a slide's content using search-replace or full replacement. " +
92
- "Syncs live to open editors. Prefer this over full deck rewrites.",
96
+ "Syncs live to open editors. Prefer this over full deck rewrites. Source-imported slides preserve their original images and factual copy by default.",
93
97
  schema: z.object({
94
98
  deckId: z.string().describe("Deck ID"),
95
99
  slideId: z.string().describe("Slide ID"),
@@ -105,6 +109,13 @@ export default defineAction({
105
109
  .string()
106
110
  .optional()
107
111
  .describe("Full HTML to replace entire slide content"),
112
+ preserveSource: z
113
+ .boolean()
114
+ .optional()
115
+ .default(true)
116
+ .describe(
117
+ "Keep source-imported images and factual copy (default true). Set false only when the user explicitly asks to rewrite the source slide.",
118
+ ),
108
119
  contextPackId: z
109
120
  .string()
110
121
  .optional()
@@ -131,6 +142,7 @@ export default defineAction({
131
142
  find,
132
143
  replace,
133
144
  fullContent,
145
+ preserveSource,
134
146
  contextPackId,
135
147
  contextModeOverride,
136
148
  reuseLabels,
@@ -205,17 +217,31 @@ export default defineAction({
205
217
  let notFound = false;
206
218
 
207
219
  if (fullContent) {
208
- slide.content = normalizeSlidePadding(fullContent);
220
+ const nextContent = normalizeSlidePadding(fullContent);
221
+ assertSourceSlidePreserved({
222
+ metadata: sourceImportForDeck(deck.sourceImport),
223
+ slideId,
224
+ nextContent,
225
+ preserveSource,
226
+ });
227
+ slide.content = nextContent;
209
228
  applied = true;
210
229
  } else if (find) {
211
230
  const idx = (slide.content as string).indexOf(find);
212
231
  if (idx === -1) {
213
232
  notFound = true;
214
233
  } else {
215
- slide.content =
234
+ const nextContent =
216
235
  slide.content.slice(0, idx) +
217
236
  (replace ?? "") +
218
237
  slide.content.slice(idx + find.length);
238
+ assertSourceSlidePreserved({
239
+ metadata: sourceImportForDeck(deck.sourceImport),
240
+ slideId,
241
+ nextContent,
242
+ preserveSource,
243
+ });
244
+ slide.content = nextContent;
219
245
  applied = true;
220
246
  }
221
247
  }
@@ -157,6 +157,17 @@ export function FirstDeckOnboardingFlow({
157
157
  ),
158
158
  });
159
159
  },
160
+ onSetupFailure: (failedPrompt, _failedFiles, failure) => {
161
+ setPromptInitialText(failedPrompt);
162
+ setPromptInitialTextKey(Date.now());
163
+ setStep("prompt");
164
+ toast.error(t("home.generationStartFailed"), {
165
+ description:
166
+ failure instanceof Error
167
+ ? failure.message
168
+ : t("home.generationStartFailedDescription"),
169
+ });
170
+ },
160
171
  });
161
172
  if (result === "started") onComplete();
162
173
  },