@agent-native/core 0.149.6 → 0.151.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 (173) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  3. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +83 -45
  4. package/corpus/templates/analytics/changelog/2026-08-11-analytics-dashboards-keep-the-selected-tab-when-reopened.md +6 -0
  5. package/corpus/templates/assets/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  6. package/corpus/templates/brain/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  7. package/corpus/templates/calendar/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  8. package/corpus/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  9. package/corpus/templates/clips/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  10. package/corpus/templates/clips/changelog/2026-08-11-clips-recovers-from-transient-updates-and-avoids-macos-priva.md +6 -0
  11. package/corpus/templates/clips/desktop/src/app.tsx +32 -176
  12. package/corpus/templates/clips/desktop/src/lib/updater.ts +52 -37
  13. package/corpus/templates/clips/desktop/src/styles.css +1 -166
  14. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +2 -0
  15. package/corpus/templates/clips/desktop/src-tauri/Info.plist +4 -0
  16. package/corpus/templates/clips/desktop/src-tauri/src/eventkit.rs +28 -1
  17. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
  18. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +48 -1
  19. package/corpus/templates/clips/desktop/src-tauri/src/permission_status.rs +3 -0
  20. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +5 -0
  21. package/corpus/templates/clips/server/plugins/auth.ts +3 -0
  22. package/corpus/templates/content/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  23. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +2 -1
  24. package/corpus/templates/design/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  25. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +5 -1
  26. package/corpus/templates/design/changelog/2026-08-11-design-agent-prompts-copy-successfully-in-the-desktop-app.md +6 -0
  27. package/corpus/templates/dispatch/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  28. package/corpus/templates/dispatch/app/global.css +2 -2
  29. package/corpus/templates/dispatch/changelog/2026-08-10-desktop-workspace-sign-in.md +6 -0
  30. package/corpus/templates/dispatch/changelog/2026-08-11-dispatch-no-longer-shows-the-generic-chat-starter-among-defa.md +6 -0
  31. package/corpus/templates/dispatch/changelog/2026-08-11-integrations-and-scheduled-work-open-as-first-class-electron-pages.md +6 -0
  32. package/corpus/templates/dispatch/changelog/2026-08-11-sidebar-text-is-slightly-easier-to-read-in-light-and-dark-mo.md +6 -0
  33. package/corpus/templates/dispatch/server/lib/identity-sso.ts +57 -31
  34. package/corpus/templates/dispatch/server/plugins/feature-flags.ts +5 -0
  35. package/corpus/templates/dispatch/server/plugins/identity-sso.ts +82 -33
  36. package/corpus/templates/dispatch/server/plugins/org-apps-directory.ts +7 -15
  37. package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +9 -1
  38. package/corpus/templates/dispatch/shared/feature-flags.ts +15 -0
  39. package/corpus/templates/factory/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  40. package/corpus/templates/forms/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  41. package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +8 -7
  42. package/corpus/templates/forms/app/hooks/use-forms.ts +8 -1
  43. package/corpus/templates/forms/changelog/2026-08-11-fixed-public-forms-so-conditional-questions-stay-hidden-unti.md +6 -0
  44. package/corpus/templates/forms/server/handlers/submissions.ts +2 -1
  45. package/corpus/templates/forms/server/lib/public-form-ssr.ts +37 -2
  46. package/corpus/templates/forms/shared/page-url.ts +54 -0
  47. package/corpus/templates/forms/shared/types.ts +4 -4
  48. package/corpus/templates/mail/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  49. package/corpus/templates/plan/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  50. package/corpus/templates/slides/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  51. package/corpus/templates/slides/actions/add-slide.ts +0 -27
  52. package/corpus/templates/slides/actions/import-file.ts +28 -9
  53. package/corpus/templates/slides/actions/view-screen.ts +4 -4
  54. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +8 -1
  55. package/corpus/templates/slides/app/components/editor/AiEditingMarker.tsx +25 -0
  56. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +43 -48
  57. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +9 -6
  58. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -9
  59. package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +2 -0
  60. package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +0 -26
  61. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +904 -217
  62. package/corpus/templates/slides/app/components/editor/canvas-interactions/index.ts +1 -0
  63. package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +11 -0
  64. package/corpus/templates/slides/app/components/editor/inline-edit-session.ts +16 -0
  65. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +22 -6
  66. package/corpus/templates/slides/app/components/editor/style-clipboard.ts +61 -0
  67. package/corpus/templates/slides/app/components/layout/Layout.tsx +26 -2
  68. package/corpus/templates/slides/app/components/ui/context-menu.tsx +1 -0
  69. package/corpus/templates/slides/app/context/DeckContext.tsx +75 -25
  70. package/corpus/templates/slides/app/global.css +1 -1
  71. package/corpus/templates/slides/app/i18n/en-US.ts +6 -1
  72. package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -1
  73. package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +12 -12
  74. package/corpus/templates/slides/app/lib/slide-agent-context.ts +52 -0
  75. package/corpus/templates/slides/app/lib/slide-image-replacement.ts +61 -0
  76. package/corpus/templates/slides/app/pages/DeckEditor.tsx +55 -116
  77. package/corpus/templates/slides/app/pages/Index.tsx +28 -3
  78. package/corpus/templates/slides/changelog/2026-08-11-agent-chat-now-follows-the-current-slide-and-selection.md +6 -0
  79. package/corpus/templates/slides/changelog/2026-08-11-deck-titles-now-use-slide-content-instead-of-placeholder-filenames.md +6 -0
  80. package/corpus/templates/slides/changelog/2026-08-11-history-now-opens-from-the-deck-overflow-menu.md +6 -0
  81. package/corpus/templates/slides/changelog/2026-08-11-images-can-be-dropped-directly-onto-slides-and-resized-or-re.md +6 -0
  82. package/corpus/templates/slides/changelog/2026-08-11-new-decks-now-transition-directly-to-a-full-page-editor-load.md +6 -0
  83. package/corpus/templates/slides/changelog/2026-08-11-selected-slide-layers-can-be-nudged-precisely-with-arrow-key.md +6 -0
  84. package/corpus/templates/slides/changelog/2026-08-11-slides-can-copy-and-paste-element-styles-with-cmd-option-c-v.md +6 -0
  85. package/corpus/templates/slides/changelog/2026-08-11-slides-shows-one-compact-ai-marker-and-keeps-your-current-sl.md +6 -0
  86. package/corpus/templates/slides/changelog/2026-08-11-text-edits-preserve-slide-layout-when-no-changes-are-made.md +6 -0
  87. package/corpus/templates/slides/changelog/2026-08-11-the-style-toolbar-now-uses-the-full-available-width.md +6 -0
  88. package/corpus/templates/slides/changelog/2026-08-11-undo-and-redo-now-move-one-visible-slide-state-at-a-time.md +6 -0
  89. package/corpus/templates/slides/server/lib/chat-attachments.ts +6 -2
  90. package/corpus/templates/slides/shared/deck-title.ts +56 -0
  91. package/corpus/templates/tasks/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  92. package/dist/agent/run-store.js +6 -1
  93. package/dist/agent/thread-data-builder.js +19 -44
  94. package/dist/agent/types.d.ts +13 -0
  95. package/dist/catalog.json +1 -0
  96. package/dist/cli/create.js +1 -1
  97. package/dist/client/AssistantChat.js +58 -10
  98. package/dist/client/FileStorageSetupCard.d.ts +6 -0
  99. package/dist/client/FileStorageSetupCard.js +18 -0
  100. package/dist/client/NewWorkspaceAppFlow.js +10 -0
  101. package/dist/client/agent-chat-adapter.js +87 -11
  102. package/dist/client/agent-chat.d.ts +1 -1
  103. package/dist/client/agent-chat.js +8 -3
  104. package/dist/client/chat/attachment-adapters.js +10 -0
  105. package/dist/client/chat/message-components.d.ts +4 -1
  106. package/dist/client/chat/message-components.js +37 -13
  107. package/dist/client/chat/tool-call-display.js +14 -0
  108. package/dist/client/chat-first/apps-rail.d.ts +1 -1
  109. package/dist/client/chat-first/apps-rail.js +6 -6
  110. package/dist/client/chat-first/copy.js +1 -0
  111. package/dist/client/chat-first/types.d.ts +1 -0
  112. package/dist/client/extensions/EmbeddedExtension.js +1 -1
  113. package/dist/client/extensions/ExtensionViewer.js +1 -1
  114. package/dist/client/extensions/InlineExtensionFrame.js +1 -1
  115. package/dist/client/onboarding/FirstRunOnboarding.js +17 -2
  116. package/dist/client/settings/FileStorageSettingsForm.d.ts +2 -0
  117. package/dist/client/settings/FileStorageSettingsForm.js +113 -0
  118. package/dist/client/settings/SettingsPanel.js +2 -1
  119. package/dist/client/sse-event-processor.js +21 -0
  120. package/dist/collab/struct-routes.d.ts +1 -1
  121. package/dist/extensions/actions.js +2 -2
  122. package/dist/extensions/html-shell.js +3 -1
  123. package/dist/file-upload/pre-upload-attachments.d.ts +15 -10
  124. package/dist/file-upload/pre-upload-attachments.js +68 -24
  125. package/dist/file-upload/registry.d.ts +4 -5
  126. package/dist/file-upload/registry.js +7 -14
  127. package/dist/file-upload/s3.d.ts +9 -0
  128. package/dist/file-upload/s3.js +168 -0
  129. package/dist/file-upload/types.d.ts +3 -3
  130. package/dist/file-upload/types.js +2 -2
  131. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  132. package/dist/notifications/routes.d.ts +3 -3
  133. package/dist/onboarding/app-profile.js +23 -1
  134. package/dist/onboarding/default-steps.js +69 -0
  135. package/dist/onboarding/types.d.ts +2 -0
  136. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  137. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  138. package/dist/secrets/routes.d.ts +3 -3
  139. package/dist/server/agent-chat/browser-team-tools.js +20 -0
  140. package/dist/server/agent-chat-plugin.js +12 -3
  141. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  142. package/dist/server/auth.js +45 -21
  143. package/dist/server/better-auth-instance.d.ts +2 -0
  144. package/dist/server/better-auth-instance.js +2 -2
  145. package/dist/server/core-routes-plugin.d.ts +1 -0
  146. package/dist/server/core-routes-plugin.js +26 -16
  147. package/dist/server/identity-sso-store.d.ts +3 -0
  148. package/dist/server/identity-sso-store.js +45 -0
  149. package/dist/server/identity-sso.d.ts +20 -10
  150. package/dist/server/identity-sso.js +79 -31
  151. package/dist/server/index.d.ts +1 -1
  152. package/dist/server/index.js +1 -1
  153. package/dist/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  154. package/dist/templates/default/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  155. package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +24 -1
  156. package/dist/templates/workspace-core/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  157. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -2
  158. package/dist/templates/workspace-core/.agents/skills/extensions/references/api.md +1 -1
  159. package/dist/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +3 -1
  160. package/docs/content/authentication.mdx +27 -0
  161. package/docs/content/cross-app-sso.mdx +38 -8
  162. package/docs/content/extensions.mdx +15 -15
  163. package/docs/content/file-uploads.mdx +10 -12
  164. package/docs/content/generative-ui.mdx +6 -1
  165. package/docs/content/key-concepts.mdx +141 -83
  166. package/package.json +2 -2
  167. package/src/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  168. package/src/templates/default/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  169. package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +24 -1
  170. package/src/templates/workspace-core/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  171. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -2
  172. package/src/templates/workspace-core/.agents/skills/extensions/references/api.md +1 -1
  173. package/src/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +3 -1
@@ -4,6 +4,14 @@ interface ReplaceOptions {
4
4
  alt?: string;
5
5
  }
6
6
 
7
+ export interface SlideImageDropPosition {
8
+ x: number;
9
+ y: number;
10
+ }
11
+
12
+ const DROPPED_IMAGE_WIDTH = 320;
13
+ const DROPPED_IMAGE_HEIGHT = 180;
14
+
7
15
  interface PlaceholderTarget {
8
16
  index: number | null;
9
17
  label: string;
@@ -53,6 +61,13 @@ function cleanAlt(value: string | undefined): string {
53
61
  return (value || "Uploaded image").replace(/\s+/g, " ").trim();
54
62
  }
55
63
 
64
+ function createSlideObjectId(): string {
65
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
66
+ return crypto.randomUUID();
67
+ }
68
+ return `slide-object-${Date.now()}-${Math.random().toString(36).slice(2)}`;
69
+ }
70
+
56
71
  function hasStyleProperty(style: string, property: string): boolean {
57
72
  return new RegExp(`(?:^|;)\\s*${property}\\s*:`, "i").test(style);
58
73
  }
@@ -188,6 +203,52 @@ export function insertImageIntoSlideHtml(
188
203
  return serializeFragment(doc);
189
204
  }
190
205
 
206
+ /** Insert a desktop drop as a durable, independently movable canvas object. */
207
+ export function insertDroppedImageIntoSlideHtml(
208
+ content: string,
209
+ newSrc: string,
210
+ options: ReplaceOptions & { position?: SlideImageDropPosition } = {},
211
+ ): string {
212
+ const doc = parseFragment(content);
213
+ const img = doc.createElement("img");
214
+ const position = options.position ?? {
215
+ x: 640,
216
+ y: 360,
217
+ };
218
+ const left = Math.max(0, Math.round(position.x - DROPPED_IMAGE_WIDTH / 2));
219
+ const top = Math.max(0, Math.round(position.y - DROPPED_IMAGE_HEIGHT / 2));
220
+
221
+ img.setAttribute("src", newSrc);
222
+ img.setAttribute("alt", cleanAlt(options.alt));
223
+ img.setAttribute("data-slide-object-id", createSlideObjectId());
224
+ img.className = "fmd-img-uploaded";
225
+ img.setAttribute(
226
+ "style",
227
+ `position: absolute; left: ${left}px; top: ${top}px; width: ${DROPPED_IMAGE_WIDTH}px; height: ${DROPPED_IMAGE_HEIGHT}px; max-width: none; max-height: none; margin: 0; border-radius: 8px; object-fit: contain; box-sizing: border-box; z-index: 1;`,
228
+ );
229
+
230
+ const slideRoot = doc.body.querySelector<HTMLElement>(".fmd-slide");
231
+ if (slideRoot) {
232
+ if (!hasStyleProperty(slideRoot.getAttribute("style") ?? "", "position")) {
233
+ slideRoot.setAttribute(
234
+ "style",
235
+ `${(slideRoot.getAttribute("style") ?? "").trim().replace(/;+\s*$/, "")}; position: relative;`.replace(
236
+ /^;\s*/,
237
+ "",
238
+ ),
239
+ );
240
+ }
241
+ slideRoot.appendChild(img);
242
+ } else {
243
+ // Markdown-backed slides keep their source text. Appending a raw image tag
244
+ // lets ReactMarkdown preserve the text while the slide canvas supplies the
245
+ // positioned containing block at render time.
246
+ doc.body.append(doc.createTextNode("\n\n"), img);
247
+ }
248
+
249
+ return serializeFragment(doc);
250
+ }
251
+
191
252
  export function replaceImageTargetInSlideHtml(
192
253
  content: string,
193
254
  oldSrc: string,
@@ -36,10 +36,7 @@ import EditorSidebar from "@/components/editor/EditorSidebar";
36
36
  import EditorToolbar from "@/components/editor/EditorToolbar";
37
37
  import GeneratingSlidePreview from "@/components/editor/GeneratingSlidePreview";
38
38
  import HistoryPanel from "@/components/editor/HistoryPanel";
39
- import ImageDropPromptPopover from "@/components/editor/ImageDropPromptPopover";
40
39
  import ImageGenPanel from "@/components/editor/ImageGenPanel";
41
- import ImageSearchPanel from "@/components/editor/ImageSearchPanel";
42
- import LogoSearchPanel from "@/components/editor/LogoSearchPanel";
43
40
  import { QuestionFlow } from "@/components/editor/QuestionFlow";
44
41
  import SlideEditor from "@/components/editor/SlideEditor";
45
42
  import { TweaksPanel } from "@/components/editor/TweaksPanel";
@@ -49,7 +46,6 @@ import { useAgentGenerating } from "@/hooks/use-agent-generating";
49
46
  import { useDeckDesignSystem } from "@/hooks/use-deck-design-system";
50
47
  import { useDeckPresence } from "@/hooks/use-deck-presence";
51
48
  import { useDeckRole } from "@/hooks/use-deck-role";
52
- import { useGeneratingSlidePreview } from "@/hooks/use-generating-slide-preview";
53
49
  import {
54
50
  useSlideComments,
55
51
  type CommentThread,
@@ -71,7 +67,7 @@ import {
71
67
  import { isMissingUploadProviderError } from "@/lib/image-drop-to-agent";
72
68
  import { imageFileLooksSupported } from "@/lib/slide-image-replacement";
73
69
  import {
74
- insertImageIntoSlideHtml,
70
+ insertDroppedImageIntoSlideHtml,
75
71
  replaceImageTargetInSlideHtml,
76
72
  } from "@/lib/slide-image-replacement";
77
73
  import { TAB_ID } from "@/lib/tab-id";
@@ -203,8 +199,6 @@ export default function DeckEditor() {
203
199
  // Dialog/popover states
204
200
  const [imageGenOpen, setImageGenOpen] = useState(false);
205
201
  const [assetLibraryOpen, setAssetLibraryOpen] = useState(false);
206
- const [imageSearchOpen, setImageSearchOpen] = useState(false);
207
- const [logoSearchOpen, setLogoSearchOpen] = useState(false);
208
202
  const [historyOpen, setHistoryOpen] = useState(false);
209
203
  const historyButtonRef = useRef<HTMLButtonElement>(null);
210
204
  const [sidePanel, setSidePanel] = useState<EditorSidePanel>(null);
@@ -246,19 +240,6 @@ export default function DeckEditor() {
246
240
  // Hidden file input for direct upload
247
241
  const uploadInputRef = useRef<HTMLInputElement>(null);
248
242
 
249
- // Drop-to-prompt popover state. Opens when a user drops an image somewhere
250
- // other than an existing image/placeholder on the slide — so we ask "what
251
- // should we do with this?" and hand the image off to the agent chat instead
252
- // of guessing (or worse, letting the browser navigate to the file).
253
- const [imageDropPopover, setImageDropPopover] = useState<{
254
- open: boolean;
255
- file: File | null;
256
- position: { x: number; y: number } | null;
257
- }>({ open: false, file: null, position: null });
258
- const closeImageDropPopover = useCallback(() => {
259
- setImageDropPopover({ open: false, file: null, position: null });
260
- }, []);
261
-
262
243
  const deck = getDeck(id || "");
263
244
  const fitDims = getAspectRatioDims(deck?.aspectRatio);
264
245
  const currentDeckAccessKey = deckAccessCheckKey(id, org?.orgId);
@@ -267,11 +248,6 @@ export default function DeckEditor() {
267
248
  ((org?.pendingInvitations?.length ?? 0) > 0 ||
268
249
  (org?.domainMatches?.length ?? 0) > 0);
269
250
  const slideCount = deck?.slides.length ?? 0;
270
- const streamedGeneratingSlideContent = useGeneratingSlidePreview({
271
- deckId: id ?? "",
272
- slideCount,
273
- generating,
274
- });
275
251
  // Mirror Google Slides: viewers see the editor shell with edit affordances
276
252
  // disabled (rather than a separate "viewer" route). Owners/Editors/Admins
277
253
  // get the full editor. Only assume edit access while the role is still
@@ -365,26 +341,17 @@ export default function DeckEditor() {
365
341
  );
366
342
  const slideWasAdded = currentSlideIds.length > previousSlideIds.length;
367
343
 
368
- if (
369
- slideWasAdded &&
370
- addedSlide &&
371
- (generating ||
372
- isNewDeckGenerating ||
373
- addSlideGenerating ||
374
- generatingSlideSelected)
375
- ) {
344
+ previousSlideIdsRef.current = currentSlideIds;
345
+ if (!slideWasAdded) return;
346
+
347
+ // Keep the user's current slide stable while AI appends slides. The only
348
+ // exception is an explicit click on the synthetic generating-slide row,
349
+ // which opts the user into following that one generated slide.
350
+ if (addedSlide && generatingSlideSelected) {
376
351
  setActiveSlideId(addedSlide.id);
377
352
  }
378
-
379
- previousSlideIdsRef.current = currentSlideIds;
380
- if (slideWasAdded) setGeneratingSlideSelected(false);
381
- }, [
382
- addSlideGenerating,
383
- deck,
384
- generating,
385
- generatingSlideSelected,
386
- isNewDeckGenerating,
387
- ]);
353
+ setGeneratingSlideSelected(false);
354
+ }, [deck, generatingSlideSelected]);
388
355
 
389
356
  useEffect(() => {
390
357
  if (
@@ -541,22 +508,43 @@ export default function DeckEditor() {
541
508
  position?: { x: number; y: number },
542
509
  ) => {
543
510
  if (!id || !currentSlideRef.current) return;
544
- // When there's no concrete target (drop landed on slide whitespace, the
545
- // canvas, or the editor chrome), defer to the user: open the popover so
546
- // they can tell the agent what to do with the image. The agent can then
547
- // decide which slide / placeholder / element to update, generate a
548
- // matching layout, or add a new slide.
511
+ const targetSlideId = currentSlideRef.current.id;
549
512
  if (!replaceSrc) {
550
- setImageDropPopover({
551
- open: true,
552
- file,
553
- position: position ?? null,
554
- });
513
+ try {
514
+ const newUrl = await uploadImageAsset(file);
515
+ const targetSlide =
516
+ currentSlideRef.current?.id === targetSlideId
517
+ ? currentSlideRef.current
518
+ : getDeck(id)?.slides.find((slide) => slide.id === targetSlideId);
519
+ if (!targetSlide) return;
520
+ const updatedContent = insertDroppedImageIntoSlideHtml(
521
+ targetSlide.content,
522
+ newUrl,
523
+ { alt: file.name, position },
524
+ );
525
+ if (updatedContent !== targetSlide.content) {
526
+ updateSlide(id, targetSlide.id, { content: updatedContent });
527
+ }
528
+ toast.success(t("deckEditor.imageAdded"), {
529
+ description: file.name,
530
+ });
531
+ } catch (error) {
532
+ toast.error(t("deckEditor.imageUploadFailed"), {
533
+ description:
534
+ error instanceof Error
535
+ ? error.message
536
+ : t("deckEditor.imageUploadError"),
537
+ });
538
+ }
555
539
  return;
556
540
  }
557
- const targetSlide = currentSlideRef.current;
558
541
  try {
559
542
  const newUrl = await uploadImageAsset(file);
543
+ const targetSlide =
544
+ currentSlideRef.current?.id === targetSlideId
545
+ ? currentSlideRef.current
546
+ : getDeck(id)?.slides.find((slide) => slide.id === targetSlideId);
547
+ if (!targetSlide) return;
560
548
  const updatedContent = replaceImageTargetInSlideHtml(
561
549
  targetSlide.content,
562
550
  replaceSrc,
@@ -578,7 +566,7 @@ export default function DeckEditor() {
578
566
  });
579
567
  }
580
568
  },
581
- [id, updateSlide, uploadImageAsset],
569
+ [getDeck, id, t, updateSlide, uploadImageAsset],
582
570
  );
583
571
 
584
572
  // Drag an already-hosted image (e.g. dragged out of a generated-image
@@ -586,17 +574,18 @@ export default function DeckEditor() {
586
574
  // uploadAndApplyImage there's nothing to upload — the URL is already a
587
575
  // live asset — so this just swaps it into the target image/placeholder.
588
576
  const dropImageUrlOnSlide = useCallback(
589
- (replaceSrc: string | null, url: string) => {
577
+ (
578
+ replaceSrc: string | null,
579
+ url: string,
580
+ position?: { x: number; y: number },
581
+ ) => {
590
582
  if (!id || !currentSlideRef.current) return;
591
583
  if (!replaceSrc) {
592
- // No existing image/placeholder under the drop point (e.g. an empty
593
- // slide) — the URL is already a known, hosted asset (unlike an
594
- // arbitrary local file), so just add it to the slide directly
595
- // instead of routing through the agent-prompt popover.
596
584
  const targetSlide = currentSlideRef.current;
597
- const updatedContent = insertImageIntoSlideHtml(
585
+ const updatedContent = insertDroppedImageIntoSlideHtml(
598
586
  targetSlide.content,
599
587
  url,
588
+ { position },
600
589
  );
601
590
  if (updatedContent !== targetSlide.content) {
602
591
  updateSlide(id, targetSlide.id, { content: updatedContent });
@@ -981,10 +970,9 @@ export default function DeckEditor() {
981
970
 
982
971
  // Editor-wide drag-and-drop catch-all. SlideEditor's own drop handler runs
983
972
  // first for drops landing on a slide (it calls stopPropagation), so this
984
- // only fires for drops that landed in the surrounding chrome — sidebar,
985
- // toolbar, deck thumbnails, or empty space. Without this, the browser's
986
- // default kicks in and navigates to the dropped image file, which
987
- // surprises users who expect drop-to-attach behavior everywhere.
973
+ // only fires for drops that landed in the surrounding chrome. Prevent the
974
+ // browser from navigating to the dropped file, and add it to the active
975
+ // slide at the default canvas position.
988
976
  const editorDragOver = (e: React.DragEvent) => {
989
977
  if (!Array.from(e.dataTransfer?.types ?? []).includes("Files")) return;
990
978
  e.preventDefault();
@@ -996,15 +984,8 @@ export default function DeckEditor() {
996
984
  if (!file) return;
997
985
  e.preventDefault();
998
986
  e.stopPropagation();
999
- setImageDropPopover({
1000
- open: true,
1001
- file,
1002
- position: { x: e.clientX, y: e.clientY },
1003
- });
987
+ void uploadAndApplyImage(null, file);
1004
988
  };
1005
- const contextHintForDrop = currentSlide
1006
- ? `Current slide: ${currentSlide.id} (index ${currentIndex >= 0 ? currentIndex : 0}). Deck: ${id}.`
1007
- : `Deck: ${id}.`;
1008
989
 
1009
990
  const handleAddEmptySlide = () => {
1010
991
  const activeIdx = deck.slides.findIndex((s) => s.id === activeSlideId);
@@ -1034,7 +1015,7 @@ export default function DeckEditor() {
1034
1015
  setReplaceImageSrc(null);
1035
1016
  setAssetLibraryOpen(true);
1036
1017
  }}
1037
- onShowHistory={() => setHistoryOpen(!historyOpen)}
1018
+ onShowHistory={() => setHistoryOpen((open) => !open)}
1038
1019
  historyButtonRef={historyButtonRef}
1039
1020
  currentSlide={currentSlide}
1040
1021
  onWideContextToolbarSlotChange={setWideContextToolbarSlot}
@@ -1159,7 +1140,6 @@ export default function DeckEditor() {
1159
1140
  generatingSlideVisible
1160
1141
  ? {
1161
1142
  index: deck.slides.length,
1162
- content: streamedGeneratingSlideContent,
1163
1143
  }
1164
1144
  : undefined
1165
1145
  }
@@ -1191,7 +1171,6 @@ export default function DeckEditor() {
1191
1171
  <div className="flex min-h-0 flex-1 overflow-auto bg-[var(--slides-editor-surface)] p-4 md:p-8">
1192
1172
  <div className="m-auto w-full max-w-6xl">
1193
1173
  <GeneratingSlidePreview
1194
- content={streamedGeneratingSlideContent}
1195
1174
  aspectRatio={deck.aspectRatio}
1196
1175
  designSystem={designSystem}
1197
1176
  thumbnail={false}
@@ -1207,7 +1186,6 @@ export default function DeckEditor() {
1207
1186
  <div className="flex min-h-0 flex-1 overflow-auto bg-[var(--slides-editor-surface)] p-4 md:p-8">
1208
1187
  <div className="m-auto w-full max-w-6xl">
1209
1188
  <GeneratingSlidePreview
1210
- content={streamedGeneratingSlideContent}
1211
1189
  aspectRatio={deck.aspectRatio}
1212
1190
  designSystem={designSystem}
1213
1191
  thumbnail={false}
@@ -1260,14 +1238,6 @@ export default function DeckEditor() {
1260
1238
  setReplaceImageSrc(src);
1261
1239
  uploadInputRef.current?.click();
1262
1240
  }}
1263
- onSearchImage={(src) => {
1264
- setReplaceImageSrc(src);
1265
- setImageSearchOpen(true);
1266
- }}
1267
- onLogoSearch={(src) => {
1268
- setReplaceImageSrc(src);
1269
- setLogoSearchOpen(true);
1270
- }}
1271
1241
  onDropImage={uploadAndApplyImage}
1272
1242
  onDropImageUrl={dropImageUrlOnSlide}
1273
1243
  onToggleObjectFit={toggleObjectFit}
@@ -1388,30 +1358,6 @@ export default function DeckEditor() {
1388
1358
  : undefined
1389
1359
  }
1390
1360
  />
1391
- <ImageSearchPanel
1392
- open={imageSearchOpen}
1393
- onOpenChange={setImageSearchOpen}
1394
- onSelectImage={
1395
- replaceImageSrc
1396
- ? (newUrl) => {
1397
- replaceImageInSlide(replaceImageSrc, newUrl);
1398
- setReplaceImageSrc(null);
1399
- }
1400
- : undefined
1401
- }
1402
- />
1403
- <LogoSearchPanel
1404
- open={logoSearchOpen}
1405
- onOpenChange={setLogoSearchOpen}
1406
- onSelectLogo={
1407
- replaceImageSrc
1408
- ? (newUrl) => {
1409
- replaceImageInSlide(replaceImageSrc, newUrl);
1410
- setReplaceImageSrc(null);
1411
- }
1412
- : undefined
1413
- }
1414
- />
1415
1361
  <HistoryPanel
1416
1362
  deckId={id}
1417
1363
  open={historyOpen}
@@ -1419,13 +1365,6 @@ export default function DeckEditor() {
1419
1365
  canRestore={canEdit}
1420
1366
  anchorRef={historyButtonRef}
1421
1367
  />
1422
- <ImageDropPromptPopover
1423
- open={imageDropPopover.open}
1424
- file={imageDropPopover.file}
1425
- position={imageDropPopover.position}
1426
- contextHint={contextHintForDrop}
1427
- onClose={closeImageDropPopover}
1428
- />
1429
1368
  </div>
1430
1369
  );
1431
1370
  }
@@ -26,6 +26,7 @@ import { useLocation, useNavigate, useSearchParams } from "react-router";
26
26
  import { toast } from "sonner";
27
27
 
28
28
  import DeckCard from "@/components/deck/DeckCard";
29
+ import { DeckEditorSkeleton } from "@/components/editor/DeckEditorSkeleton";
29
30
  import {
30
31
  NewDeckReferenceStep,
31
32
  type ImportedReference,
@@ -271,6 +272,7 @@ export default function Index() {
271
272
  } | null>(null);
272
273
  const [showNewDeckReferenceStep, setShowNewDeckReferenceStep] =
273
274
  useState(false);
275
+ const [isStartingNewDeck, setIsStartingNewDeck] = useState(false);
274
276
  const [recentReferences, setRecentReferences] = useState<RecentReference[]>(
275
277
  [],
276
278
  );
@@ -544,11 +546,15 @@ export default function Index() {
544
546
  : undefined;
545
547
  let deck: ReturnType<typeof createDeck> | undefined;
546
548
  flushSync(() => {
549
+ setIsStartingNewDeck(true);
547
550
  deck = createDeck(undefined, {
548
551
  designSystemId: selectedDesignSystem?.id ?? null,
549
552
  });
550
553
  });
551
- if (!deck) return;
554
+ if (!deck) {
555
+ setIsStartingNewDeck(false);
556
+ return;
557
+ }
552
558
  navigate(`/deck/${deck.id}`);
553
559
  };
554
560
 
@@ -588,12 +594,20 @@ export default function Index() {
588
594
  : undefined;
589
595
  let deck: ReturnType<typeof createDeck> | undefined;
590
596
  flushSync(() => {
597
+ // Commit the destination-shaped shell before navigating. React Router
598
+ // keeps the current outlet mounted while a cold route chunk loads, and
599
+ // showing the deck grid during that handoff makes the route indicator
600
+ // look like a second app.
601
+ setIsStartingNewDeck(true);
591
602
  deck = createDeck(undefined, {
592
603
  noDefaultSlides: true,
593
604
  designSystemId: selectedDesignSystem?.id ?? null,
594
605
  });
595
606
  });
596
- if (!deck) return;
607
+ if (!deck) {
608
+ setIsStartingNewDeck(false);
609
+ return;
610
+ }
597
611
  const deckId = deck.id;
598
612
  setNewDeckPromptOpen(false);
599
613
 
@@ -735,7 +749,7 @@ export default function Index() {
735
749
  "Start a `manage-progress` run so progress appears in the app header. Add the first slide as soon as it is ready, then continue one slide at a time so the editor visibly fills in.",
736
750
  "After reading any requested or attached reference material, but before adding the first slide, choose a concise, specific deck title from the user's request and source material. Never use the deck id, run id, file id, or another opaque alphanumeric token as the title. Call `patch-deck` with `deckId: \"" +
737
751
  deckId +
738
- '\"` and `operations: [{ "op": "patch-deck-fields", "fields": { "title": "<generated title>" } }]`. Include only `title` in `fields`; omit all other optional fields. Never leave a generated deck named "Untitled Deck" or another placeholder.',
752
+ '\"` and `operations: [{ "op": "patch-deck-fields", "fields": { "title": "<generated title>" } }]`. Include only `title` in `fields`; omit all other optional fields. Never leave a generated deck named "Untitled Deck" or another placeholder, and do not reuse the uploaded filename or a generic label like "Untitled scene" when the content can describe the deck better.',
739
753
  "If the user asks for a standalone visual, diagram, hero, one-pager, poster, or a couple of visuals, create only the requested one/few polished visual slides. Do not pad the result into a full presentation.",
740
754
  "If the request is for a presentation or deck and does not explicitly ask for one slide, infer a coherent multi-slide outline from the scope and keep adding slides until that outline is complete. Do not stop after the first slide just because the prompt has few explicit instructions.",
741
755
  "Add slides ONE AT A TIME using the `add-slide` action with --deckId=" +
@@ -1290,6 +1304,17 @@ export default function Index() {
1290
1304
  ),
1291
1305
  );
1292
1306
 
1307
+ if (isStartingNewDeck) {
1308
+ return (
1309
+ <div
1310
+ className="fixed inset-0 z-[300] min-h-screen bg-background"
1311
+ data-testid="new-deck-loading"
1312
+ >
1313
+ <DeckEditorSkeleton label={t("deckEditor.lookingForDeck")} />
1314
+ </div>
1315
+ );
1316
+ }
1317
+
1293
1318
  return (
1294
1319
  <main className="min-w-0 flex-1 overflow-y-auto px-4 pb-6 pt-0 sm:px-6 sm:pb-10">
1295
1320
  {loading ? (
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ Agent chat now follows the current slide and selection
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ Imported decks now use slide content for their title instead of a placeholder filename.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ History now opens from the deck overflow menu
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ Images can be dropped directly onto slides and resized or repositioned freely
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ New decks now transition directly to a full-page editor loading state
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ Selected slide layers can be nudged precisely with arrow keys
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ Slides can copy and paste element styles with Cmd+Option+C/V and the context menu
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ Slides shows one compact AI marker and keeps your current slide selected while new slides load as skeletons
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ Text edits preserve slide layout when no changes are made
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ The style toolbar now uses the full available width
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ Undo and redo now move one visible slide state at a time
@@ -158,11 +158,15 @@ function stripForwardedAttachmentData(
158
158
  ): AgentChatAttachment {
159
159
  const next = { ...attachment };
160
160
  // Keep visual data for the current model turn so uploaded screenshots remain
161
- // available for vision analysis; non-visual files only need their path/URL.
161
+ // available for vision analysis. Keep non-visual bytes until core's shared
162
+ // pre-upload boundary has created the durable public object-storage URL;
163
+ // `slidesUploadPath` is a private import handle, not a chat attachment URL.
162
164
  const inlineImage = isVisualAttachment(attachment)
163
165
  ? decodeDataUrl(attachment.data)
164
166
  : null;
165
- if (!inlineImage || inlineImage.bytes.length > MAX_INLINE_IMAGE_BYTES) {
167
+ if (!inlineImage && (saved.url || !attachment.data)) {
168
+ delete next.data;
169
+ } else if (inlineImage && inlineImage.bytes.length > MAX_INLINE_IMAGE_BYTES) {
166
170
  delete next.data;
167
171
  }
168
172
  (next as any).slidesUploadPath = saved.path;
@@ -11,6 +11,14 @@ const GENERATED_TITLE_PLACEHOLDERS = new Set([
11
11
  "your name",
12
12
  ]);
13
13
 
14
+ const IMPORTED_TITLE_PLACEHOLDERS = new Set([
15
+ "untitled deck",
16
+ "untitled file",
17
+ "untitled presentation",
18
+ "untitled scene",
19
+ "untitled slide",
20
+ ]);
21
+
14
22
  /**
15
23
  * Generated deck ids should never become user-facing titles. Keep this
16
24
  * deliberately narrow so normal titles with spaces and punctuation remain
@@ -63,6 +71,15 @@ function plainText(value: string): string {
63
71
  .trim();
64
72
  }
65
73
 
74
+ function plainTextLines(value: string): string[] {
75
+ return decodeHtmlEntities(
76
+ value.replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/gi, "\n"),
77
+ )
78
+ .split(/\r?\n/)
79
+ .map((line) => line.replace(/\s+/g, " ").trim())
80
+ .filter(Boolean);
81
+ }
82
+
66
83
  function usableCandidate(value: string): string | null {
67
84
  const candidate = plainText(value).replace(/^[•●▪‣\-\s]+/, "");
68
85
  if (!candidate || candidate.length > 140) return null;
@@ -101,6 +118,17 @@ export function deriveDeckTitleFromSlideContent(
101
118
  if (text) candidates.push({ score: fontSize, text });
102
119
  }
103
120
 
121
+ const fallbackLines = plainTextLines(content);
122
+ if (fallbackLines.length > 1) {
123
+ for (const line of fallbackLines) {
124
+ const text = usableCandidate(line);
125
+ if (text) {
126
+ candidates.push({ score: 1, text });
127
+ break;
128
+ }
129
+ }
130
+ }
131
+
104
132
  return candidates.sort((a, b) => b.score - a.score)[0]?.text ?? null;
105
133
  }
106
134
 
@@ -126,6 +154,34 @@ export function repairGeneratedDeckTitle(
126
154
  );
127
155
  }
128
156
 
157
+ /**
158
+ * Imported files often fall back to filenames when they do not have a
159
+ * meaningful title in their own metadata. Prefer the first slide's content
160
+ * when it can produce a real deck title; otherwise keep a human-readable
161
+ * fallback instead of a source filename placeholder.
162
+ */
163
+ export function resolveImportedDeckTitle(
164
+ requestedTitle: unknown,
165
+ firstSlideContent: unknown,
166
+ ): string {
167
+ if (typeof requestedTitle === "string") {
168
+ const title = requestedTitle.trim();
169
+ const opaqueTitle = isOpaqueDeckTitle(requestedTitle);
170
+ if (
171
+ title &&
172
+ !opaqueTitle &&
173
+ !IMPORTED_TITLE_PLACEHOLDERS.has(title.toLowerCase())
174
+ ) {
175
+ return title;
176
+ }
177
+ }
178
+
179
+ const derivedTitle = deriveDeckTitleFromSlideContent(firstSlideContent);
180
+ if (derivedTitle) return derivedTitle;
181
+
182
+ return "Imported File";
183
+ }
184
+
129
185
  export function assertHumanReadableDeckTitle(title: string): void {
130
186
  if (isOpaqueDeckTitle(title)) {
131
187
  throw new Error(