@agent-native/core 0.146.0 → 0.146.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 (120) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/turn-into-app/SKILL.md +4 -0
  3. package/corpus/templates/analytics/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  4. package/corpus/templates/analytics/actions/compose-dashboard.ts +2 -27
  5. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +7 -8
  6. package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +18 -2
  7. package/corpus/templates/analytics/actions/mutate-dashboard.ts +16 -24
  8. package/corpus/templates/analytics/actions/rename-dashboard.ts +2 -24
  9. package/corpus/templates/analytics/actions/reorder-dashboard-panels.ts +2 -24
  10. package/corpus/templates/analytics/actions/restore-dashboard-revision.ts +2 -25
  11. package/corpus/templates/analytics/actions/update-dashboard.ts +4 -32
  12. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +1 -40
  13. package/corpus/templates/analytics/changelog/2026-08-07-language-selection-now-lives-in-settings-instead-of-the-comm.md +6 -0
  14. package/corpus/templates/analytics/netlify.toml +7 -0
  15. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +13 -13
  16. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +4 -2
  17. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +0 -35
  18. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +5 -3
  19. package/corpus/templates/analytics/server/lib/dashboard-collab-sync.ts +77 -0
  20. package/corpus/templates/analytics/server/lib/demo-dashboards.ts +2 -23
  21. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +28 -74
  22. package/corpus/templates/analytics/server/routes/api/dashboard-agent-context.json.get.ts +5 -9
  23. package/corpus/templates/assets/.agents/skills/turn-into-app/SKILL.md +4 -0
  24. package/corpus/templates/assets/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  25. package/corpus/templates/brain/.agents/skills/turn-into-app/SKILL.md +4 -0
  26. package/corpus/templates/brain/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  27. package/corpus/templates/calendar/.agents/skills/turn-into-app/SKILL.md +4 -0
  28. package/corpus/templates/calendar/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  29. package/corpus/templates/chat/.agents/skills/turn-into-app/SKILL.md +4 -0
  30. package/corpus/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  31. package/corpus/templates/clips/.agents/skills/turn-into-app/SKILL.md +4 -0
  32. package/corpus/templates/clips/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  33. package/corpus/templates/clips/app/components/player/video-player.tsx +1 -1
  34. package/corpus/templates/clips/changelog/2026-08-07-fixed-trimmed-clips-durations-so-the-main-player-display-mat.md +6 -0
  35. package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +1 -1
  36. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  37. package/corpus/templates/content/.agents/skills/turn-into-app/SKILL.md +4 -0
  38. package/corpus/templates/content/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  39. package/corpus/templates/crm/.agents/skills/turn-into-app/SKILL.md +4 -0
  40. package/corpus/templates/crm/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  41. package/corpus/templates/design/.agents/skills/turn-into-app/SKILL.md +4 -0
  42. package/corpus/templates/design/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  43. package/corpus/templates/dispatch/.agents/skills/turn-into-app/SKILL.md +4 -0
  44. package/corpus/templates/dispatch/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  45. package/corpus/templates/dispatch/changelog/2026-08-07-language-selection-now-lives-in-settings-instead-of-the-disp.md +6 -0
  46. package/corpus/templates/factory/.agents/skills/turn-into-app/SKILL.md +4 -0
  47. package/corpus/templates/factory/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  48. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +59 -22
  49. package/corpus/templates/forms/.agents/skills/turn-into-app/SKILL.md +4 -0
  50. package/corpus/templates/forms/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  51. package/corpus/templates/macros/.agents/skills/turn-into-app/SKILL.md +4 -0
  52. package/corpus/templates/macros/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  53. package/corpus/templates/mail/.agents/skills/turn-into-app/SKILL.md +4 -0
  54. package/corpus/templates/mail/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  55. package/corpus/templates/plan/.agents/skills/turn-into-app/SKILL.md +4 -0
  56. package/corpus/templates/plan/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  57. package/corpus/templates/slides/.agents/skills/image-generation-via-a2a/SKILL.md +10 -1
  58. package/corpus/templates/slides/.agents/skills/slide-images/SKILL.md +10 -3
  59. package/corpus/templates/slides/.agents/skills/turn-into-app/SKILL.md +4 -0
  60. package/corpus/templates/slides/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  61. package/corpus/templates/slides/actions/generate-image-api.ts +123 -0
  62. package/corpus/templates/slides/actions/import-file.ts +144 -99
  63. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +10 -2
  64. package/corpus/templates/slides/app/components/editor/ImageGenPanel.tsx +1 -1
  65. package/corpus/templates/slides/changelog/2026-08-07-deleting-a-deck-no-longer-freezes-the-slides-browser.md +6 -0
  66. package/corpus/templates/slides/changelog/2026-08-07-slides-verify-generated-images-are-persisted-on-the-target-s.md +6 -0
  67. package/corpus/templates/slides/package.json +1 -0
  68. package/corpus/templates/slides/server/handlers/import/html-converter.ts +9 -0
  69. package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +960 -0
  70. package/corpus/templates/slides/server/lib/slide-image-insertion.ts +141 -0
  71. package/corpus/templates/slides/server/plugins/agent-chat.ts +2 -0
  72. package/corpus/templates/tasks/.agents/skills/turn-into-app/SKILL.md +4 -0
  73. package/corpus/templates/tasks/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  74. package/dist/client/AgentPanel.d.ts +10 -4
  75. package/dist/client/AgentPanel.js +29 -82
  76. package/dist/collab/awareness.d.ts +2 -2
  77. package/dist/collab/struct-routes.d.ts +1 -1
  78. package/dist/db/migrations.js +22 -4
  79. package/dist/deploy/build.d.ts +1 -0
  80. package/dist/deploy/build.js +27 -22
  81. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  82. package/dist/ingestion/pptx.d.ts +1 -0
  83. package/dist/ingestion/pptx.js +353 -28
  84. package/dist/notifications/routes.d.ts +2 -2
  85. package/dist/observability/routes.d.ts +1 -1
  86. package/dist/progress/routes.d.ts +1 -1
  87. package/dist/resources/handlers.d.ts +1 -1
  88. package/dist/secrets/routes.d.ts +6 -6
  89. package/dist/server/builder-browser.d.ts +34 -0
  90. package/dist/server/builder-browser.js +179 -0
  91. package/dist/server/core-routes-plugin.d.ts +5 -0
  92. package/dist/server/core-routes-plugin.js +24 -8
  93. package/dist/server/index.d.ts +1 -1
  94. package/dist/server/index.js +1 -1
  95. package/dist/shared/cache-control.d.ts +2 -2
  96. package/dist/shared/cache-control.js +2 -2
  97. package/dist/templates/chat/.agents/skills/turn-into-app/SKILL.md +4 -0
  98. package/dist/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  99. package/dist/templates/default/.agents/skills/turn-into-app/SKILL.md +4 -0
  100. package/dist/templates/default/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  101. package/dist/templates/headless/.agents/skills/turn-into-app/SKILL.md +4 -0
  102. package/dist/templates/headless/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  103. package/dist/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +4 -0
  104. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  105. package/dist/vite/client.js +13 -1
  106. package/docs/content/drop-in-agent.mdx +3 -3
  107. package/docs/content/getting-started-actions.mdx +1 -2
  108. package/docs/content/getting-started-pages.mdx +1 -2
  109. package/docs/content/getting-started.mdx +0 -4
  110. package/docs/content/what-is-agent-native.mdx +4 -4
  111. package/package.json +3 -3
  112. package/src/templates/chat/.agents/skills/turn-into-app/SKILL.md +4 -0
  113. package/src/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  114. package/src/templates/default/.agents/skills/turn-into-app/SKILL.md +4 -0
  115. package/src/templates/default/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  116. package/src/templates/headless/.agents/skills/turn-into-app/SKILL.md +4 -0
  117. package/src/templates/headless/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  118. package/src/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +4 -0
  119. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +8 -0
  120. package/docs/content/getting-started-database.mdx +0 -173
@@ -2,7 +2,6 @@ import path from "path";
2
2
 
3
3
  import { defineAction } from "@agent-native/core";
4
4
  import { writeAppState } from "@agent-native/core/application-state";
5
- import { uploadFile } from "@agent-native/core/file-upload";
6
5
  import { startBuilderDesignSystemIndex } from "@agent-native/core/server";
7
6
  import {
8
7
  getRequestOrgId,
@@ -273,21 +272,16 @@ export default defineAction({
273
272
  const { PDFParse, canvasFactory } = await setupPdfParse();
274
273
  const title = titleFromPath(filename);
275
274
 
276
- // Designed slide PDFs (photo backgrounds, gradients, custom
277
- // typography) bake their visuals into vector/image page content with
278
- // no reliable text/shape structure to reconstruct, so importing into a
279
- // deck rasterizes each page instead of flattening it to generic bullet
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.
275
+ // Reconstruct each page's real layout — positioned text blocks at
276
+ // their actual sizes plus every embedded image at its actual
277
+ // placement — instead of flattening the page to one guessed
278
+ // background photo and a canned text template. Image placement needs
279
+ // the optional canvas renderer; text positioning does not, so this
280
+ // still beats the old bullet-text fallback even when canvasFactory is
281
+ // unavailable in this runtime.
283
282
  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
- }
289
283
  if (!deckId) throw new Error("deckId is required to import into deck");
290
- return importPdfPagesAsFullBleedSlides({
284
+ return importPdfPagesWithFidelity({
291
285
  fileBuffer,
292
286
  title,
293
287
  deckId,
@@ -351,60 +345,61 @@ function nearestAspectRatio(width: number, height: number): AspectRatio {
351
345
  return best;
352
346
  }
353
347
 
354
- async function importPdfPagesAsFullBleedSlides(args: {
348
+ async function importPdfPagesWithFidelity(args: {
355
349
  fileBuffer: Buffer;
356
350
  title: string;
357
351
  deckId: string;
358
352
  PDFParse: Awaited<ReturnType<typeof setupPdfParse>>["PDFParse"];
359
- canvasFactory: object;
353
+ canvasFactory: object | undefined;
360
354
  }) {
361
355
  const { fileBuffer, title, deckId, PDFParse, canvasFactory } = args;
362
- const { buildFullPageImageSlideHtml } =
356
+ const { convertToSlideHtml, convertSectionsToSlides } =
363
357
  await import("../server/handlers/import/html-converter.js");
358
+ const { parsePdfFidelity } =
359
+ await import("../server/handlers/import/pdf-fidelity-parser.js");
364
360
 
365
361
  const pdf = new PDFParse({
366
362
  data: new Uint8Array(fileBuffer),
367
363
  CanvasFactory: canvasFactory,
368
364
  });
369
365
  let pages: { num: number; text: string }[];
370
- let screenshotsByPage: Map<
371
- number,
372
- { data: Uint8Array; width: number; height: number }
373
- >;
366
+ let fidelityPages: Awaited<ReturnType<typeof parsePdfFidelity>>;
374
367
  try {
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
- }
384
- // Rasterize the complete page so positioned text, vector shapes, logos,
385
- // photos, and the source page proportions survive the import together.
386
- const screenshotResult = await pdf.getScreenshot({
387
- desiredWidth: 1600,
388
- imageBuffer: true,
389
- imageDataUrl: false,
390
- });
391
- screenshotsByPage = new Map();
392
- for (const page of screenshotResult.pages) {
393
- screenshotsByPage.set(page.pageNumber, {
394
- data: page.data,
395
- width: page.width,
396
- height: page.height,
397
- });
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
- }));
368
+ pages = normalizePdfPages(await pdf.getText());
369
+ // Image placement needs the optional canvas renderer to decode pixel
370
+ // data; skip it (text still gets real positions/sizes) when the native
371
+ // canvas binding isn't available in this runtime.
372
+ const imageResult = canvasFactory
373
+ ? await pdf
374
+ .getImage({
375
+ imageBuffer: true,
376
+ imageDataUrl: false,
377
+ imageThreshold: 0,
378
+ })
379
+ .catch((err) => {
380
+ console.warn(
381
+ "[import-file] PDF image extraction failed, importing text-only fidelity:",
382
+ err instanceof Error ? err.message : String(err),
383
+ );
384
+ return undefined;
385
+ })
386
+ : undefined;
387
+
388
+ // `pdf-parse` memoizes the loaded pdfjs document behind `load()` (a
389
+ // TS-only `private` method — a real, callable runtime property).
390
+ // Reaching into it reuses the exact document `getText`/`getImage` above
391
+ // already parsed instead of parsing the file a second time.
392
+ const doc = await (
393
+ pdf as unknown as {
394
+ load(): Promise<
395
+ import("pdfjs-dist/legacy/build/pdf.mjs").PDFDocumentProxy
396
+ >;
397
+ }
398
+ ).load();
399
+ // coercion-ok: undefined here means either canvasFactory was absent or
400
+ // getImage() already failed and logged a warning above — text-only
401
+ // fidelity is the intended degrade, not a swallowed failure.
402
+ fidelityPages = await parsePdfFidelity(doc, imageResult?.pages ?? []);
408
403
  } finally {
409
404
  await pdf.destroy();
410
405
  }
@@ -413,60 +408,110 @@ async function importPdfPagesAsFullBleedSlides(args: {
413
408
  throw new Error("The PDF renderer returned no importable pages.");
414
409
  }
415
410
 
411
+ // A page with neither extracted text nor a fidelity element is only ever
412
+ // produced when nothing on it could be recovered (e.g. a scanned/image
413
+ // page and canvas rendering was unavailable or failed) — if that's true of
414
+ // every page, importing anyway would silently create a deck of blank
415
+ // placeholder slides and report success, matching the earlier
416
+ // text-extraction path's "needs OCR" failure keeps that lossy import from
417
+ // going unnoticed.
418
+ const hasRecoverableContent = pages.some((page) => {
419
+ const fidelity = fidelityPages.find((p) => p.pageNumber === page.num);
420
+ return page.text.trim().length > 0 || (fidelity?.elements.length ?? 0) > 0;
421
+ });
422
+ if (!hasRecoverableContent) {
423
+ throw new Error(
424
+ "No importable text or images found in this PDF. Scanned PDFs need OCR first.",
425
+ );
426
+ }
427
+
416
428
  // Source decks (e.g. Instagram carousel exports) are commonly portrait or
417
429
  // square, not the deck editor's 16:9 default — match the canvas to the
418
- // first rendered page's own proportions instead of stretching/cropping it.
419
- const firstScreenshot = pages
420
- .map((page) => screenshotsByPage.get(page.num))
421
- .find((image): image is NonNullable<typeof image> => Boolean(image));
422
- const aspectRatio = firstScreenshot
423
- ? nearestAspectRatio(firstScreenshot.width, firstScreenshot.height)
430
+ // PDF's own real page proportions instead of stretching/cropping it.
431
+ const firstSizedPage = fidelityPages.find((p) => p.widthEmu > 0);
432
+ const aspectRatio = firstSizedPage
433
+ ? nearestAspectRatio(firstSizedPage.widthEmu, firstSizedPage.heightEmu)
424
434
  : undefined;
425
435
 
426
436
  const ownerEmail = getRequestUserEmail();
437
+ if (!ownerEmail) throw new Error("no authenticated user");
438
+
439
+ // Bounded the same way as the PPTX upload path below — an unbounded
440
+ // `Promise.all` here would fire one image-upload batch per page at once,
441
+ // and a large deck can be dozens of pages.
442
+ const uploadLimit = pLimit(4);
427
443
  const imported = await Promise.all(
428
- pages.map(async (page) => {
429
- const screenshot = screenshotsByPage.get(page.num);
444
+ pages.map((page, index) =>
445
+ uploadLimit(async () => {
446
+ const fidelity = fidelityPages.find((p) => p.pageNumber === page.num);
447
+
448
+ if (!fidelity || fidelity.elements.length === 0) {
449
+ // Fidelity parsing failed or found nothing placeable on this page
450
+ // (e.g. a fully blank page) — fall back to plain extracted text
451
+ // instead of producing a silently blank slide.
452
+ const firstLine = page.text.split(/\r?\n/)[0]?.trim();
453
+ const [content] = convertSectionsToSlides([
454
+ { heading: firstLine || `Page ${page.num}`, content: page.text },
455
+ ]);
456
+ const id = newSlideId();
457
+ return {
458
+ slide: {
459
+ id,
460
+ content: content ?? '<div class="fmd-slide"></div>',
461
+ layout: "content",
462
+ notes: page.text,
463
+ },
464
+ snapshot: {
465
+ id,
466
+ text: page.text,
467
+ notes: page.text,
468
+ imageUrls: [],
469
+ editableText: true,
470
+ } satisfies SourceImportSlideSnapshot,
471
+ };
472
+ }
430
473
 
431
- if (!screenshot) {
432
- throw new Error(
433
- `The PDF renderer did not return page ${page.num}; refusing a partial source import.`,
474
+ const slideForUpload = {
475
+ texts: [],
476
+ images: [],
477
+ elements: fidelity.elements,
478
+ };
479
+ const uploaded = await uploadPptxSlideImages({
480
+ slide: slideForUpload,
481
+ slideIndex: index,
482
+ ownerEmail,
483
+ });
484
+ const content = convertToSlideHtml(
485
+ {
486
+ texts: [],
487
+ images: [],
488
+ elements: fidelity.elements,
489
+ widthEmu: fidelity.widthEmu,
490
+ heightEmu: fidelity.heightEmu,
491
+ // A page with no detected full-page fill is plain paper.
492
+ backgroundColor: fidelity.backgroundColor ?? "#ffffff", // guard:allow-raw-color - fallback plain-paper background, not a design-system token
493
+ },
494
+ uploaded.urls,
434
495
  );
435
- }
436
496
 
437
- const uploadResult = await uploadFile({
438
- data: Buffer.from(screenshot.data),
439
- filename: `slide-import-${Date.now()}-p${page.num}.png`,
440
- mimeType: "image/png",
441
- ownerEmail: ownerEmail ?? undefined,
442
- recordAsset: false,
443
- });
444
- if (!uploadResult?.url) {
445
- throw new Error(
446
- "File storage is not configured. Connect Builder.io (free tier available) or another upload provider before importing PDF slides.",
447
- );
448
- }
449
- const id = newSlideId();
450
- return {
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,
468
- };
469
- }),
497
+ const id = newSlideId();
498
+ return {
499
+ slide: {
500
+ id,
501
+ content,
502
+ layout: "content",
503
+ notes: page.text,
504
+ },
505
+ snapshot: {
506
+ id,
507
+ text: page.text,
508
+ notes: page.text,
509
+ imageUrls: Object.values(uploaded.urls),
510
+ editableText: true,
511
+ } satisfies SourceImportSlideSnapshot,
512
+ };
513
+ }),
514
+ ),
470
515
  );
471
516
  const slides = imported.map((entry) => entry.slide);
472
517
  const sourceImport = buildSourceImportMetadata({
@@ -59,6 +59,7 @@ export default function DeckCard({
59
59
  const [contextOpen, setContextOpen] = useState(false);
60
60
  const inputRef = useRef<HTMLInputElement>(null);
61
61
  const pendingRenameRef = useRef(false);
62
+ const pendingDeleteRef = useRef(false);
62
63
  const pendingWorkspaceDefaultRef = useRef(false);
63
64
 
64
65
  useEffect(() => {
@@ -227,6 +228,11 @@ export default function DeckCard({
227
228
  pendingWorkspaceDefaultRef.current = false;
228
229
  onSetWorkspaceDefault?.(deck.id, !isWorkspaceDefault);
229
230
  }
231
+ if (pendingDeleteRef.current) {
232
+ e.preventDefault();
233
+ pendingDeleteRef.current = false;
234
+ setTimeout(() => onDelete(deck.id), 0);
235
+ }
230
236
  }}
231
237
  >
232
238
  <DropdownMenuItem
@@ -268,8 +274,10 @@ export default function DeckCard({
268
274
  )}
269
275
  <DropdownMenuSeparator />
270
276
  <DropdownMenuItem
271
- onSelect={() => {
272
- onDelete(deck.id);
277
+ onSelect={(event) => {
278
+ event.preventDefault();
279
+ pendingDeleteRef.current = true;
280
+ setMenuOpen(false);
273
281
  }}
274
282
  className="text-red-400 focus:text-red-400"
275
283
  >
@@ -80,7 +80,7 @@ export function buildImageGenerationContext({
80
80
  }
81
81
 
82
82
  contextParts.push(
83
- '\nGenerate 3 variations. Show each as an inline rendered image preview using markdown image syntax (![Variation 1](url)), not a plain text link — the chat renders "![]()" as an actual image but "[]()" as a bare link. Let the user pick their favorite, then insert the chosen generated image into the slide content in the right place.',
83
+ '\nGenerate 3 preview-only variations. Show each as an inline rendered image preview using markdown image syntax (![Variation 1](url)), not a plain text link — the chat renders "![]()" as an actual image but "[]()" as a bare link. After the user chooses one, place that preview URL with update-slide and then get-deck to verify the persisted slide HTML contains it before claiming success. For a direct one-image request instead, call `generate-image-api` with insertIntoSlide: true plus deckId and slideId; claim it was added only if the action returns inserted: true.',
84
84
  );
85
85
 
86
86
  return contextParts.join("\n");
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-07
4
+ ---
5
+
6
+ Deleting a deck no longer freezes the Slides browser
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-07
4
+ ---
5
+
6
+ Slides verify generated images are persisted on the target slide
@@ -53,6 +53,7 @@
53
53
  "nanoid": "^5.1.9",
54
54
  "p-limit": "^7.3.0",
55
55
  "pdf-parse": "^2.4.5",
56
+ "pdfjs-dist": "5.4.296",
56
57
  "pptxgenjs": "4.0.1",
57
58
  "react-markdown": "^10.1.0",
58
59
  "rehype-raw": "^7.0.0",
@@ -334,9 +334,18 @@ function formatFidelityRun(
334
334
  `font-style:${run.italic ? "italic" : "normal"}`,
335
335
  `text-decoration:${run.underline ? "underline" : "none"}`,
336
336
  ].join(";");
337
+ const href = run.href && isSafeLinkHref(run.href) ? run.href : undefined;
338
+ if (href) {
339
+ return `<a href="${esc(href)}" target="_blank" rel="noopener noreferrer" style="${styles};">${esc(run.content)}</a>`;
340
+ }
337
341
  return `<span style="${styles};">${esc(run.content)}</span>`;
338
342
  }
339
343
 
344
+ /** A source PDF/PPTX link annotation is untrusted input — only render schemes a browser treats as navigation, never `javascript:`/`data:`/etc. */
345
+ function isSafeLinkHref(href: string): boolean {
346
+ return /^(https?:|mailto:)/i.test(href);
347
+ }
348
+
340
349
  function fontWeightForFamily(
341
350
  fontFamily: string | undefined,
342
351
  fallback: number,