@agent-native/core 0.149.0 → 0.149.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 (134) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +2 -0
  3. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +150 -46
  4. package/corpus/templates/analytics/app/global.css +1 -1
  5. package/corpus/templates/analytics/app/i18n-data.ts +37 -0
  6. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +1 -1
  7. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -0
  8. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +2 -0
  9. package/corpus/templates/analytics/changelog/2026-08-10-dashboard-tables-can-be-copied-and-sorted-by-multiple-column.md +6 -0
  10. package/corpus/templates/analytics/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  11. package/corpus/templates/assets/app/global.css +1 -1
  12. package/corpus/templates/assets/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  13. package/corpus/templates/brain/app/global.css +1 -1
  14. package/corpus/templates/brain/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  15. package/corpus/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  16. package/corpus/templates/clips/changelog/2026-08-10-clips-desktop-sign-in-now-starts-with-magic-links-and-google.md +6 -0
  17. package/corpus/templates/clips/chrome-extension/src/overlay.css +1 -0
  18. package/corpus/templates/clips/desktop/src/app.tsx +274 -137
  19. package/corpus/templates/clips/desktop/src/styles.css +35 -0
  20. package/corpus/templates/content/app/global.css +1 -0
  21. package/corpus/templates/crm/app/global.css +1 -1
  22. package/corpus/templates/crm/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  23. package/corpus/templates/dispatch/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  24. package/corpus/templates/factory/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  25. package/corpus/templates/forms/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  26. package/corpus/templates/plan/app/global.css +1 -0
  27. package/corpus/templates/plan/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  28. package/corpus/templates/plan/server/ui-plan-html.ts +4 -1
  29. package/corpus/templates/slides/AGENTS.md +11 -10
  30. package/corpus/templates/slides/actions/add-deck.ts +2 -0
  31. package/corpus/templates/slides/actions/add-slide.ts +15 -1
  32. package/corpus/templates/slides/actions/create-deck.ts +18 -6
  33. package/corpus/templates/slides/actions/get-deck.ts +29 -1
  34. package/corpus/templates/slides/actions/patch-deck.ts +139 -10
  35. package/corpus/templates/slides/actions/save-deck.ts +24 -1
  36. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +46 -0
  37. package/corpus/templates/slides/app/components/editor/BlockBubbleMenu.tsx +194 -3
  38. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +10 -1
  39. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +134 -618
  40. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +74 -63
  41. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -1
  42. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +13 -1
  43. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +56 -174
  44. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +239 -116
  45. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -7
  46. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +45 -0
  47. package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +1 -1
  48. package/corpus/templates/slides/app/components/layout/Layout.tsx +34 -35
  49. package/corpus/templates/slides/app/components/layout/layout-route-policy.ts +2 -5
  50. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +45 -40
  51. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +63 -16
  52. package/corpus/templates/slides/app/hooks/use-generating-slide-preview.ts +25 -3
  53. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  54. package/corpus/templates/slides/app/lib/create-deck-generation.ts +5 -4
  55. package/corpus/templates/slides/app/lib/export-pdf-client.ts +84 -25
  56. package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +11 -0
  57. package/corpus/templates/slides/app/lib/recent-references.ts +23 -1
  58. package/corpus/templates/slides/app/lib/slide-animation-elements.ts +41 -3
  59. package/corpus/templates/slides/app/pages/DeckEditor.tsx +16 -27
  60. package/corpus/templates/slides/app/pages/Index.tsx +256 -98
  61. package/corpus/templates/slides/changelog/2026-08-08-google-slides-export-now-tells-you-why-a-deck-fell-back-to-a.md +6 -0
  62. package/corpus/templates/slides/changelog/2026-08-09-delete-key-now-removes-a-selected-image.md +6 -0
  63. package/corpus/templates/slides/changelog/2026-08-09-editing-a-slide-in-an-imported-deck-no-longer-fails-to-save.md +6 -0
  64. package/corpus/templates/slides/changelog/2026-08-09-fixed-pdf-and-custom-size-powerpoint-imports-rendering-with-.md +6 -0
  65. package/corpus/templates/slides/changelog/2026-08-09-images-from-sites-without-cors-no-longer-export-blank.md +6 -0
  66. package/corpus/templates/slides/changelog/2026-08-09-imported-pdf-headings-no-longer-lose-the-space-between-color.md +6 -0
  67. package/corpus/templates/slides/changelog/2026-08-09-imported-slide-text-no-longer-renders-too-large-for-its-box.md +6 -0
  68. package/corpus/templates/slides/changelog/2026-08-09-large-decks-no-longer-render-slowly-or-glitch-in-the-editor.md +6 -0
  69. package/corpus/templates/slides/changelog/2026-08-09-revise-highlighted-slide-text-with-ai.md +6 -0
  70. package/corpus/templates/slides/changelog/2026-08-09-the-deck-editor-no-longer-stays-dimmed-after-the-window-narro.md +6 -0
  71. package/corpus/templates/slides/changelog/2026-08-10-ai-editing-avatar-badge.md +6 -0
  72. package/corpus/templates/slides/changelog/2026-08-10-generated-decks-keep-a-clear-human-readable-title.md +6 -0
  73. package/corpus/templates/slides/changelog/2026-08-10-new-deck-creation-now-shows-progress-opens-the-editor-direct.md +6 -0
  74. package/corpus/templates/slides/changelog/2026-08-10-presentation-reveals-now-follow-valid-targets.md +6 -0
  75. package/corpus/templates/slides/changelog/2026-08-10-slides-editing-controls-are-consolidated-into-one-focused-me.md +6 -0
  76. package/corpus/templates/slides/changelog/2026-08-10-slides-now-verify-and-restyle-every-imported-slide-in-one-pa.md +6 -0
  77. package/corpus/templates/slides/changelog/2026-08-10-slides-only-suggests-a-google-connection-for-relevant-reques.md +6 -0
  78. package/corpus/templates/slides/changelog/2026-08-10-the-google-slides-import-option-is-now-labeled-slides.md +6 -0
  79. package/corpus/templates/slides/server/handlers/import/html-converter.ts +115 -22
  80. package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +11 -2
  81. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  82. package/corpus/templates/slides/server/lib/fetch-remote-image.ts +192 -0
  83. package/corpus/templates/slides/server/lib/image-proxy-url.ts +192 -0
  84. package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -3
  85. package/corpus/templates/slides/server/routes/api/image-proxy.get.ts +65 -0
  86. package/corpus/templates/slides/shared/api.ts +2 -1
  87. package/corpus/templates/slides/shared/deck-title.ts +135 -0
  88. package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +4 -1
  89. package/dist/agent/engine/ai-sdk-engine.js +1 -1
  90. package/dist/agent/production-agent.d.ts +21 -1
  91. package/dist/agent/production-agent.js +129 -10
  92. package/dist/agent/tool-approval-store.d.ts +18 -0
  93. package/dist/agent/tool-approval-store.js +122 -0
  94. package/dist/agent/types.d.ts +1 -1
  95. package/dist/cli/code-agent-executor.js +11 -2
  96. package/dist/client/AgentPanel.js +31 -6
  97. package/dist/client/AssistantChat.d.ts +3 -2
  98. package/dist/client/AssistantChat.js +7 -20
  99. package/dist/client/agent-chat/index.d.ts +1 -1
  100. package/dist/client/agent-chat-adapter.js +11 -0
  101. package/dist/client/agent-chat.d.ts +1 -1
  102. package/dist/client/chat/markdown-renderer.js +14 -2
  103. package/dist/client/chat/message-components.d.ts +6 -2
  104. package/dist/client/chat/message-components.js +25 -47
  105. package/dist/client/chat/tool-call-display.d.ts +3 -5
  106. package/dist/client/chat/tool-call-display.js +7 -212
  107. package/dist/client/chat-first/apps-rail.js +3 -3
  108. package/dist/client/chat-first/browser-pane.js +4 -1
  109. package/dist/client/chat-first/chat-history.d.ts +3 -1
  110. package/dist/client/chat-first/chat-history.js +2 -2
  111. package/dist/client/chat-first/surface-tabs.d.ts +1 -1
  112. package/dist/client/chat-first/surface-tabs.js +52 -41
  113. package/dist/client/chat-first/types.d.ts +3 -0
  114. package/dist/client/chat-first.d.ts +10 -0
  115. package/dist/client/chat-first.js +37 -2
  116. package/dist/client/components/ui/message-scroller.js +4 -1
  117. package/dist/client/i18n.js +3 -1
  118. package/dist/client/progress/RunsTray.js +14 -1
  119. package/dist/client/resources/McpConnectionSuggestion.js +9 -1
  120. package/dist/client/sse-event-processor.js +10 -0
  121. package/dist/client/use-chat-models.js +156 -80
  122. package/dist/integrations/webhook-handler.js +1 -1
  123. package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
  124. package/dist/secrets/routes.d.ts +6 -6
  125. package/dist/server/auth.js +262 -31
  126. package/dist/server/onboarding-html.js +56 -9
  127. package/dist/shared/reasoning-effort.d.ts +8 -8
  128. package/dist/shared/reasoning-effort.js +10 -10
  129. package/dist/shared/streaming-text-smoothing.js +41 -11
  130. package/dist/styles/agent-native.css +19 -32
  131. package/dist/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  132. package/docs/content/template-chat.mdx +1 -1
  133. package/package.json +2 -2
  134. package/src/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
@@ -3,9 +3,12 @@ import { PromptComposer } from "@agent-native/core/client/composer";
3
3
  import { ensureEmbedAuthFetchInterceptor } from "@agent-native/core/client/host";
4
4
  import { useT } from "@agent-native/core/client/i18n";
5
5
  import {
6
+ IconArrowLeft,
6
7
  IconBrandGoogle,
7
8
  IconFileTypePdf,
9
+ IconLoader2,
8
10
  IconPresentation,
11
+ IconUpload,
9
12
  } from "@tabler/icons-react";
10
13
  import { useState, useEffect, useCallback, useRef } from "react";
11
14
  import { createPortal } from "react-dom";
@@ -86,7 +89,7 @@ interface PromptPopoverProps {
86
89
  placeholder?: string;
87
90
  onSkip?: () => void;
88
91
  skipLabel?: string;
89
- onSubmit: (prompt: string, files: UploadedFile[]) => void;
92
+ onSubmit: (prompt: string, files: UploadedFile[]) => void | Promise<void>;
90
93
  loading?: boolean;
91
94
  anchorRef?: React.RefObject<HTMLElement | null>;
92
95
  centered?: boolean;
@@ -125,10 +128,14 @@ export default function PromptPopover({
125
128
  }: PromptPopoverProps) {
126
129
  const t = useT();
127
130
  const [uploading, setUploading] = useState(false);
131
+ const [submitting, setSubmitting] = useState(false);
128
132
  const [promptText, setPromptText] = useState("");
129
133
  const [googleDocContext, setGoogleDocContext] = useState("");
130
134
  const [googleSlidesUrl, setGoogleSlidesUrl] = useState("");
131
- const [googleSlidesInputOpen, setGoogleSlidesInputOpen] = useState(false);
135
+ const [importMode, setImportMode] = useState<PromptImportSource | null>(null);
136
+ const [selectedImportFile, setSelectedImportFile] = useState<File | null>(
137
+ null,
138
+ );
132
139
  const [importingSource, setImportingSource] =
133
140
  useState<PromptImportSource | null>(null);
134
141
  const pdfInputRef = useRef<HTMLInputElement>(null);
@@ -216,16 +223,20 @@ export default function PromptPopover({
216
223
  if (files.length > 0 && onBeforeUpload?.(enrichedText, files) === false) {
217
224
  return;
218
225
  }
226
+ setSubmitting(true);
219
227
  try {
220
228
  const uploaded = await uploadFiles(files);
221
- onSubmit(enrichedText, uploaded);
229
+ await onSubmit(enrichedText, uploaded);
230
+ setSubmitting(false);
222
231
  } catch (error) {
232
+ setSubmitting(false);
223
233
  toast.error(t("raw.uploadFailed"), {
224
234
  description:
225
235
  error instanceof Error
226
236
  ? error.message
227
237
  : t("raw.uploadAttachedFailed"),
228
238
  });
239
+ throw error;
229
240
  }
230
241
  },
231
242
  [googleDocContext, onBeforeUpload, onSubmit, uploadFiles, t],
@@ -255,11 +266,25 @@ export default function PromptPopover({
255
266
  const handleFileImport = useCallback(
256
267
  (kind: "pdf" | "pptx", file: File | undefined) => {
257
268
  if (!file) return;
269
+ setSelectedImportFile(file);
258
270
  void runImport({ kind, files: [file] });
259
271
  },
260
272
  [runImport],
261
273
  );
262
274
 
275
+ const chooseImportMode = useCallback((kind: PromptImportSource) => {
276
+ setImportMode(kind);
277
+ setSelectedImportFile(null);
278
+ setGoogleSlidesUrl("");
279
+ }, []);
280
+
281
+ const returnToPrompt = useCallback(() => {
282
+ if (importingSource) return;
283
+ setImportMode(null);
284
+ setSelectedImportFile(null);
285
+ setGoogleSlidesUrl("");
286
+ }, [importingSource]);
287
+
263
288
  const handleGoogleSlidesImport = useCallback(() => {
264
289
  const url = googleSlidesUrl.trim();
265
290
  if (!url) return;
@@ -271,13 +296,29 @@ export default function PromptPopover({
271
296
  setPromptText("");
272
297
  setGoogleDocContext("");
273
298
  setGoogleSlidesUrl("");
274
- setGoogleSlidesInputOpen(false);
299
+ setImportMode(null);
300
+ setSelectedImportFile(null);
275
301
  setImportingSource(null);
302
+ setSubmitting(false);
276
303
  }
277
304
  }, [open]);
278
305
 
279
306
  if (!open) return null;
280
307
 
308
+ const importEnabled = Boolean(onImport && importFromLabel);
309
+ const importFromCopy = importFromLabel?.trim().toLowerCase().startsWith("or ")
310
+ ? importFromLabel.trim()
311
+ : importFromLabel
312
+ ? `or ${importFromLabel.trim().toLowerCase()}`
313
+ : "";
314
+ const importModeLabel =
315
+ importMode === "google-slides"
316
+ ? t("home.googleSlidesReferenceTitle")
317
+ : importMode === "pdf"
318
+ ? "PDF"
319
+ : "PPT";
320
+ const importInputRef = importMode === "pdf" ? pdfInputRef : pptxInputRef;
321
+
281
322
  const popover = (
282
323
  <>
283
324
  {centered && (
@@ -296,7 +337,7 @@ export default function PromptPopover({
296
337
  >
297
338
  <div className="flex items-center justify-between gap-3 px-4 pb-2.5 pt-3.5">
298
339
  <span className="text-sm font-medium text-foreground">{title}</span>
299
- {onSkip && (
340
+ {onSkip && !importMode && !submitting && (
300
341
  <button
301
342
  type="button"
302
343
  onClick={() => {
@@ -310,132 +351,214 @@ export default function PromptPopover({
310
351
  )}
311
352
  </div>
312
353
 
313
- <div className="px-2.5 pb-2.5">
314
- <PromptComposer
315
- autoFocus
316
- attachmentsEnabled
317
- maxDocumentAttachmentBytes={MAX_REFERENCE_FILE_BYTES}
318
- documentAttachmentLimitLabel="Slides reference files"
319
- disabled={loading || uploading}
320
- placeholder={placeholder}
321
- onSubmit={handleSubmit}
322
- onTextChange={setPromptText}
323
- draftScope={draftScope}
324
- initialText={initialText}
325
- initialTextKey={initialTextKey}
354
+ {importEnabled && (
355
+ <input
356
+ ref={pdfInputRef}
357
+ type="file"
358
+ accept=".pdf,application/pdf"
359
+ className="sr-only"
360
+ aria-label={t("editorToolbar.importFile")}
361
+ onChange={(event) => {
362
+ handleFileImport("pdf", event.target.files?.[0]);
363
+ event.target.value = "";
364
+ }}
326
365
  />
327
- </div>
366
+ )}
367
+ {importEnabled && (
368
+ <input
369
+ ref={pptxInputRef}
370
+ type="file"
371
+ accept=".pptx,application/vnd.openxmlformats-officedocument.presentationml.presentation"
372
+ className="sr-only"
373
+ aria-label={t("editorToolbar.importFile")}
374
+ onChange={(event) => {
375
+ handleFileImport("pptx", event.target.files?.[0]);
376
+ event.target.value = "";
377
+ }}
378
+ />
379
+ )}
328
380
 
329
- {onImport && importFromLabel && (
330
- <div className="border-t border-border/60 px-4 pb-3 pt-2.5">
331
- <div className="flex flex-wrap items-center gap-1.5">
332
- <span className="mr-1 text-xs text-muted-foreground">
333
- {importFromLabel}
334
- </span>
335
- <input
336
- ref={pdfInputRef}
337
- type="file"
338
- accept=".pdf,application/pdf"
339
- className="sr-only"
340
- aria-label={t("editorToolbar.importFile")}
341
- onChange={(event) => {
342
- handleFileImport("pdf", event.target.files?.[0]);
343
- event.target.value = "";
344
- }}
345
- />
346
- <input
347
- ref={pptxInputRef}
348
- type="file"
349
- accept=".pptx,application/vnd.openxmlformats-officedocument.presentationml.presentation"
350
- className="sr-only"
351
- aria-label={t("editorToolbar.importFile")}
352
- onChange={(event) => {
353
- handleFileImport("pptx", event.target.files?.[0]);
354
- event.target.value = "";
355
- }}
381
+ <div className={importEnabled ? "grid" : undefined}>
382
+ <div
383
+ className={[
384
+ importEnabled ? "col-start-1 row-start-1" : "",
385
+ importMode ? "invisible pointer-events-none" : "",
386
+ ]
387
+ .filter(Boolean)
388
+ .join(" ")}
389
+ aria-hidden={importMode ? true : undefined}
390
+ >
391
+ <div className="px-2.5 pb-2.5">
392
+ <PromptComposer
393
+ autoFocus
394
+ attachmentsEnabled
395
+ maxDocumentAttachmentBytes={MAX_REFERENCE_FILE_BYTES}
396
+ documentAttachmentLimitLabel="Slides reference files"
397
+ disabled={
398
+ loading || uploading || submitting || Boolean(importMode)
399
+ }
400
+ placeholder={placeholder}
401
+ onSubmit={handleSubmit}
402
+ onTextChange={setPromptText}
403
+ draftScope={draftScope}
404
+ initialText={initialText}
405
+ initialTextKey={initialTextKey}
356
406
  />
357
- <Button
358
- type="button"
359
- variant="ghost"
360
- size="sm"
361
- className="h-7 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground"
362
- disabled={importingSource !== null || loading || uploading}
363
- onClick={() => pdfInputRef.current?.click()}
364
- >
365
- <IconFileTypePdf className="size-3.5" />
366
- PDF
367
- </Button>
368
- <Button
369
- type="button"
370
- variant="ghost"
371
- size="sm"
372
- className="h-7 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground"
373
- disabled={importingSource !== null || loading || uploading}
374
- onClick={() => setGoogleSlidesInputOpen((current) => !current)}
375
- >
376
- <IconBrandGoogle className="size-3.5" />
377
- {t("home.googleSlidesReferenceTitle")}
378
- </Button>
379
- <Button
380
- type="button"
381
- variant="ghost"
382
- size="sm"
383
- className="h-7 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground"
384
- disabled={importingSource !== null || loading || uploading}
385
- onClick={() => pptxInputRef.current?.click()}
386
- >
387
- <IconPresentation className="size-3.5" />
388
- PPT
389
- </Button>
390
- {importingSource && (
391
- <span className="ml-auto text-xs text-muted-foreground">
392
- {importingLabel}
393
- </span>
394
- )}
395
407
  </div>
396
- {googleSlidesInputOpen && (
397
- <div className="mt-2 space-y-2">
398
- <GoogleDriveConnectionCta />
399
- <div className="flex gap-2">
400
- <Input
401
- autoFocus
402
- type="url"
403
- value={googleSlidesUrl}
404
- placeholder={t("home.googleSlidesReferenceUrl")}
405
- aria-label={t("home.googleSlidesReferenceUrl")}
406
- className="h-8 text-xs"
407
- disabled={importingSource !== null || loading || uploading}
408
- onChange={(event) => setGoogleSlidesUrl(event.target.value)}
409
- onKeyDown={(event) => {
410
- if (event.key === "Enter") handleGoogleSlidesImport();
411
- }}
412
- />
408
+
409
+ {submitting && (
410
+ <div
411
+ className="flex items-center gap-2 border-t border-border/60 px-4 py-2.5 text-xs text-muted-foreground"
412
+ role="status"
413
+ aria-live="polite"
414
+ >
415
+ <IconLoader2 className="size-3.5 animate-spin" />
416
+ <span>{t("raw.uploading")}</span>
417
+ </div>
418
+ )}
419
+
420
+ {importEnabled && (
421
+ <div className="border-t border-border/60 px-4 pb-3 pt-2.5">
422
+ <div className="flex flex-wrap items-center gap-1.5">
423
+ <span className="mr-1 text-xs text-muted-foreground">
424
+ {importFromCopy}
425
+ </span>
426
+ <Button
427
+ type="button"
428
+ variant="ghost"
429
+ size="sm"
430
+ className="h-7 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground"
431
+ disabled={loading || uploading || submitting}
432
+ onClick={() => chooseImportMode("pdf")}
433
+ >
434
+ <IconFileTypePdf className="size-3.5" />
435
+ PDF
436
+ </Button>
413
437
  <Button
414
438
  type="button"
439
+ variant="ghost"
415
440
  size="sm"
416
- className="h-8 shrink-0 px-3 text-xs"
417
- disabled={
418
- !googleSlidesUrl.trim() ||
419
- importingSource !== null ||
420
- loading ||
421
- uploading
422
- }
423
- onClick={handleGoogleSlidesImport}
441
+ className="h-7 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground"
442
+ disabled={loading || uploading || submitting}
443
+ onClick={() => chooseImportMode("google-slides")}
424
444
  >
425
- Import
445
+ <IconBrandGoogle className="size-3.5" />
446
+ {t("home.googleSlidesImportLabel")}
447
+ </Button>
448
+ <Button
449
+ type="button"
450
+ variant="ghost"
451
+ size="sm"
452
+ className="h-7 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground"
453
+ disabled={loading || uploading || submitting}
454
+ onClick={() => chooseImportMode("pptx")}
455
+ >
456
+ <IconPresentation className="size-3.5" />
457
+ PPT
426
458
  </Button>
427
459
  </div>
428
460
  </div>
429
461
  )}
462
+
463
+ {children}
464
+
465
+ <GoogleDocImportHint
466
+ promptText={promptText}
467
+ onSourceContextChange={setGoogleDocContext}
468
+ />
430
469
  </div>
431
- )}
432
470
 
433
- {children}
471
+ {importEnabled && importMode && (
472
+ <div className="col-start-1 row-start-1 flex min-h-full flex-col px-4 pb-4 pt-3">
473
+ <div className="flex items-center gap-2">
474
+ <button
475
+ type="button"
476
+ onClick={returnToPrompt}
477
+ disabled={importingSource !== null}
478
+ className="-ms-1 inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
479
+ aria-label={
480
+ "Back to prompt" /* i18n-ignore -- local import-flow control label */
481
+ }
482
+ >
483
+ <IconArrowLeft className="size-4" />
484
+ </button>
485
+ <span className="text-sm font-medium text-foreground">
486
+ Import {importModeLabel}
487
+ </span>
488
+ </div>
434
489
 
435
- <GoogleDocImportHint
436
- promptText={promptText}
437
- onSourceContextChange={setGoogleDocContext}
438
- />
490
+ <div className="flex flex-1 flex-col justify-center gap-3">
491
+ {importMode === "google-slides" ? (
492
+ <>
493
+ <GoogleDriveConnectionCta />
494
+ <div className="flex gap-2">
495
+ <Input
496
+ autoFocus
497
+ type="url"
498
+ value={googleSlidesUrl}
499
+ placeholder={t("home.googleSlidesReferenceUrl")}
500
+ aria-label={t("home.googleSlidesReferenceUrl")}
501
+ className="h-8 text-xs"
502
+ disabled={importingSource !== null || loading}
503
+ onChange={(event) =>
504
+ setGoogleSlidesUrl(event.target.value)
505
+ }
506
+ onKeyDown={(event) => {
507
+ if (event.key === "Enter") handleGoogleSlidesImport();
508
+ }}
509
+ />
510
+ <Button
511
+ type="button"
512
+ size="sm"
513
+ className="h-8 shrink-0 px-3 text-xs"
514
+ disabled={
515
+ !googleSlidesUrl.trim() ||
516
+ importingSource !== null ||
517
+ loading
518
+ }
519
+ onClick={handleGoogleSlidesImport}
520
+ >
521
+ Import
522
+ </Button>
523
+ </div>
524
+ </>
525
+ ) : (
526
+ <>
527
+ <p className="text-xs text-muted-foreground">
528
+ Choose a {importModeLabel} file to open it as a deck.
529
+ </p>
530
+ <Button
531
+ type="button"
532
+ variant="outline"
533
+ className="w-full justify-center gap-2"
534
+ disabled={importingSource !== null || loading}
535
+ onClick={() => importInputRef.current?.click()}
536
+ >
537
+ <IconUpload className="size-4" />
538
+ Upload {importModeLabel}
539
+ </Button>
540
+ {selectedImportFile && (
541
+ <p className="truncate text-center text-xs text-muted-foreground">
542
+ {selectedImportFile.name}
543
+ </p>
544
+ )}
545
+ </>
546
+ )}
547
+ </div>
548
+
549
+ {importingSource && (
550
+ <div
551
+ className="flex items-center justify-center gap-2 text-xs text-muted-foreground"
552
+ role="status"
553
+ aria-live="polite"
554
+ >
555
+ <IconLoader2 className="size-3.5 animate-spin" />
556
+ <span>{importingLabel}</span>
557
+ </div>
558
+ )}
559
+ </div>
560
+ )}
561
+ </div>
439
562
  </div>
440
563
  </>
441
564
  );
@@ -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,
@@ -148,7 +150,7 @@ function ExcalidrawExitButton(props: { onExit: () => void; label: string }) {
148
150
  <Button
149
151
  variant="ghost"
150
152
  size="icon"
151
- className="absolute right-3 top-3 z-20 h-8 w-8 cursor-pointer border border-border bg-popover/95 shadow-lg backdrop-blur"
153
+ className="absolute right-3 top-3 z-20 h-8 w-8 cursor-pointer border border-border bg-popover/95 shadow-lg"
152
154
  onClick={props.onExit}
153
155
  aria-label={props.label}
154
156
  >
@@ -650,7 +652,7 @@ function SameSlidePresenceIndicator({ users }: { users: CollabUser[] }) {
650
652
  ? `${users[0].name} is here`
651
653
  : `${users.length} others here`;
652
654
  return (
653
- <div className="pointer-events-auto flex items-center gap-1.5 rounded-full border border-border bg-popover/95 py-1 pl-1 pr-2.5 text-xs text-popover-foreground shadow-lg backdrop-blur">
655
+ <div className="pointer-events-auto flex items-center gap-1.5 rounded-full border border-border bg-popover/95 py-1 pl-1 pr-2.5 text-xs text-popover-foreground shadow-lg">
654
656
  <div className="flex items-center">
655
657
  {visible.map((u) => (
656
658
  <SamePresenceAvatar key={u.email} user={u} />
@@ -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
 
@@ -4229,7 +4246,7 @@ export default function SlideEditor({
4229
4246
  </div>
4230
4247
  ) : (
4231
4248
  <div className="relative h-full bg-background">
4232
- <div className="absolute right-3 top-3 z-20 flex h-8 items-center gap-0.5 rounded-md border border-border bg-popover/95 px-1 shadow-lg backdrop-blur">
4249
+ <div className="absolute right-3 top-3 z-20 flex h-8 items-center gap-0.5 rounded-md border border-border bg-popover/95 px-1 shadow-lg">
4233
4250
  <Tooltip>
4234
4251
  <TooltipTrigger asChild>
4235
4252
  <Button
@@ -4413,7 +4430,12 @@ export default function SlideEditor({
4413
4430
  onSendToAgent={sendSelectionToAgent}
4414
4431
  />
4415
4432
 
4416
- <BlockBubbleMenu editingEl={editingEl} />
4433
+ <BlockBubbleMenu
4434
+ editingEl={editingEl}
4435
+ slideId={slide.id}
4436
+ deckId={deckId}
4437
+ onCommitInlineEdit={exitInlineEdit}
4438
+ />
4417
4439
 
4418
4440
  {pendingUpdateCount > 0 && (
4419
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,
@@ -87,7 +87,7 @@ export function AgentWorkIndicator() {
87
87
 
88
88
  return (
89
89
  <div className="pointer-events-none fixed bottom-4 left-1/2 z-50 w-[calc(100vw-2rem)] max-w-sm -translate-x-1/2 md:bottom-5">
90
- <div className="pointer-events-auto flex items-center justify-between gap-3 rounded-lg border border-border bg-popover/95 px-3 py-2 text-popover-foreground shadow-xl shadow-black/20 backdrop-blur">
90
+ <div className="pointer-events-auto flex items-center justify-between gap-3 rounded-lg border border-border bg-popover/95 px-3 py-2 text-popover-foreground shadow-xl shadow-black/20">
91
91
  <div className="flex min-w-0 items-center gap-2">
92
92
  <IconLoader2 className="h-4 w-4 shrink-0 animate-spin text-[#609FF8]" />
93
93
  <span className="truncate text-sm font-medium">