@agent-native/core 0.149.1 → 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 (115) 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 +122 -9
  35. package/corpus/templates/slides/actions/save-deck.ts +24 -1
  36. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +134 -618
  37. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +69 -60
  38. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -1
  39. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +13 -1
  40. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +56 -174
  41. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +239 -116
  42. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +3 -3
  43. package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +1 -1
  44. package/corpus/templates/slides/app/components/layout/Layout.tsx +34 -35
  45. package/corpus/templates/slides/app/components/layout/layout-route-policy.ts +2 -5
  46. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +45 -40
  47. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +63 -16
  48. package/corpus/templates/slides/app/hooks/use-generating-slide-preview.ts +25 -3
  49. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  50. package/corpus/templates/slides/app/lib/create-deck-generation.ts +5 -4
  51. package/corpus/templates/slides/app/lib/recent-references.ts +23 -1
  52. package/corpus/templates/slides/app/lib/slide-animation-elements.ts +41 -3
  53. package/corpus/templates/slides/app/pages/DeckEditor.tsx +4 -27
  54. package/corpus/templates/slides/app/pages/Index.tsx +240 -89
  55. package/corpus/templates/slides/changelog/2026-08-10-ai-editing-avatar-badge.md +6 -0
  56. package/corpus/templates/slides/changelog/2026-08-10-generated-decks-keep-a-clear-human-readable-title.md +6 -0
  57. package/corpus/templates/slides/changelog/2026-08-10-new-deck-creation-now-shows-progress-opens-the-editor-direct.md +6 -0
  58. package/corpus/templates/slides/changelog/2026-08-10-presentation-reveals-now-follow-valid-targets.md +6 -0
  59. package/corpus/templates/slides/changelog/2026-08-10-slides-editing-controls-are-consolidated-into-one-focused-me.md +6 -0
  60. package/corpus/templates/slides/changelog/2026-08-10-slides-now-verify-and-restyle-every-imported-slide-in-one-pa.md +6 -0
  61. package/corpus/templates/slides/changelog/2026-08-10-slides-only-suggests-a-google-connection-for-relevant-reques.md +6 -0
  62. package/corpus/templates/slides/changelog/2026-08-10-the-google-slides-import-option-is-now-labeled-slides.md +6 -0
  63. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  64. package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -3
  65. package/corpus/templates/slides/shared/api.ts +2 -1
  66. package/corpus/templates/slides/shared/deck-title.ts +135 -0
  67. package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +4 -1
  68. package/dist/agent/engine/ai-sdk-engine.js +1 -1
  69. package/dist/agent/production-agent.d.ts +21 -1
  70. package/dist/agent/production-agent.js +129 -10
  71. package/dist/agent/tool-approval-store.d.ts +18 -0
  72. package/dist/agent/tool-approval-store.js +122 -0
  73. package/dist/agent/types.d.ts +1 -1
  74. package/dist/cli/code-agent-executor.js +11 -2
  75. package/dist/client/AgentPanel.js +31 -6
  76. package/dist/client/AssistantChat.d.ts +3 -2
  77. package/dist/client/AssistantChat.js +7 -20
  78. package/dist/client/agent-chat/index.d.ts +1 -1
  79. package/dist/client/agent-chat-adapter.js +11 -0
  80. package/dist/client/agent-chat.d.ts +1 -1
  81. package/dist/client/chat/markdown-renderer.js +14 -2
  82. package/dist/client/chat/message-components.d.ts +6 -2
  83. package/dist/client/chat/message-components.js +25 -47
  84. package/dist/client/chat/tool-call-display.d.ts +3 -5
  85. package/dist/client/chat/tool-call-display.js +7 -212
  86. package/dist/client/chat-first/apps-rail.js +3 -3
  87. package/dist/client/chat-first/browser-pane.js +4 -1
  88. package/dist/client/chat-first/chat-history.d.ts +3 -1
  89. package/dist/client/chat-first/chat-history.js +2 -2
  90. package/dist/client/chat-first/surface-tabs.d.ts +1 -1
  91. package/dist/client/chat-first/surface-tabs.js +52 -41
  92. package/dist/client/chat-first/types.d.ts +3 -0
  93. package/dist/client/chat-first.d.ts +10 -0
  94. package/dist/client/chat-first.js +37 -2
  95. package/dist/client/components/ui/message-scroller.js +4 -1
  96. package/dist/client/i18n.js +3 -1
  97. package/dist/client/resources/McpConnectionSuggestion.js +9 -1
  98. package/dist/client/sse-event-processor.js +10 -0
  99. package/dist/client/use-chat-models.js +156 -80
  100. package/dist/collab/struct-routes.d.ts +1 -1
  101. package/dist/integrations/webhook-handler.js +1 -1
  102. package/dist/observability/routes.d.ts +1 -1
  103. package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
  104. package/dist/resources/handlers.d.ts +1 -1
  105. package/dist/secrets/routes.d.ts +9 -9
  106. package/dist/server/auth.js +262 -31
  107. package/dist/server/onboarding-html.js +56 -9
  108. package/dist/shared/reasoning-effort.d.ts +8 -8
  109. package/dist/shared/reasoning-effort.js +10 -10
  110. package/dist/shared/streaming-text-smoothing.js +41 -11
  111. package/dist/styles/agent-native.css +19 -32
  112. package/dist/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  113. package/docs/content/template-chat.mdx +1 -1
  114. package/package.json +2 -2
  115. package/src/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
@@ -26,6 +26,7 @@ import { useDesignSystems } from "@/hooks/use-design-systems";
26
26
  import { useWorkspaceDefaults } from "@/hooks/use-workspace-defaults";
27
27
  import { startDeckGeneration } from "@/lib/create-deck-generation";
28
28
  import {
29
+ forgetRecentReference,
29
30
  readRecentReferences,
30
31
  rememberRecentReference,
31
32
  type RecentReference,
@@ -45,11 +46,8 @@ export function FirstDeckOnboardingFlow({
45
46
  const { session } = useSession();
46
47
  const { decks, createDeck, ensureDeckPersisted, deleteDeck, reloadDecks } =
47
48
  useDecks();
48
- const { designSystems, defaultSystem } = useDesignSystems();
49
- const {
50
- referenceDeck: workspaceReferenceDeck,
51
- designSystem: workspaceDesignSystem,
52
- } = useWorkspaceDefaults();
49
+ const { designSystems } = useDesignSystems();
50
+ const { designSystem: workspaceDesignSystem } = useWorkspaceDefaults();
53
51
  const { submit: agentSubmit } = useAgentGenerating();
54
52
  const [step, setStep] = useState<FirstDeckStep>("prompt");
55
53
  const [prompt, setPrompt] = useState("");
@@ -63,19 +61,25 @@ export function FirstDeckOnboardingFlow({
63
61
  );
64
62
 
65
63
  const initialPrompt = searchParams.get("initialPrompt")?.trim() ?? "";
66
- const workspaceReferenceDeckId =
67
- workspaceReferenceDeck && !workspaceReferenceDeck.unavailable
68
- ? workspaceReferenceDeck.id
69
- : null;
70
64
  const workspaceDesignSystemId =
71
65
  workspaceDesignSystem && !workspaceDesignSystem.unavailable
72
66
  ? workspaceDesignSystem.id
73
67
  : null;
68
+ const lastUsedDesignSystemId =
69
+ recentReferences.find(
70
+ (reference) =>
71
+ reference.kind === "design-system" &&
72
+ designSystems.some((designSystem) => designSystem.id === reference.id),
73
+ )?.id ?? null;
74
+ const lastUsedReferenceDeckId =
75
+ recentReferences.find(
76
+ (reference) =>
77
+ reference.kind === "deck" &&
78
+ decks.some((deck) => deck.id === reference.id),
79
+ )?.id ?? null;
74
80
  const initialDesignSystemId =
75
- designSystems.find((designSystem) => designSystem.isDefault)?.id ??
76
- workspaceDesignSystemId ??
77
- defaultSystem?.id ??
78
- null;
81
+ lastUsedDesignSystemId ?? workspaceDesignSystemId;
82
+ const initialReferenceDeckId = lastUsedReferenceDeckId;
79
83
 
80
84
  useEffect(() => {
81
85
  const result = readRecentReferences();
@@ -103,6 +107,10 @@ export function FirstDeckOnboardingFlow({
103
107
  },
104
108
  [],
105
109
  );
110
+ const forgetReference = useCallback((kind: RecentReference["kind"]) => {
111
+ const result = forgetRecentReference(kind);
112
+ if (result.readable) setRecentReferences(result.items);
113
+ }, []);
106
114
 
107
115
  const handlePromptSubmit = useCallback(
108
116
  async (text: string, files: File[]) => {
@@ -137,7 +145,7 @@ export function FirstDeckOnboardingFlow({
137
145
  files,
138
146
  referenceSelection: selection,
139
147
  selectedDesignSystemId: initialDesignSystemId,
140
- selectedReferenceDeckId: workspaceReferenceDeckId,
148
+ selectedReferenceDeckId: initialReferenceDeckId,
141
149
  designSystems,
142
150
  createDeck,
143
151
  ensureDeckPersisted,
@@ -185,24 +193,32 @@ export function FirstDeckOnboardingFlow({
185
193
  prompt,
186
194
  session,
187
195
  t,
188
- workspaceReferenceDeckId,
196
+ initialReferenceDeckId,
189
197
  ],
190
198
  );
191
199
 
192
200
  const handleReferenceSelect = useCallback(
193
201
  async (selection: NewDeckReferenceSelection) => {
194
- if (selection.designSystemId) {
195
- rememberReference({
196
- id: selection.designSystemId,
197
- kind: "design-system",
198
- });
202
+ if (selection.designSystemId !== undefined) {
203
+ if (selection.designSystemId) {
204
+ rememberReference({
205
+ id: selection.designSystemId,
206
+ kind: "design-system",
207
+ });
208
+ } else {
209
+ forgetReference("design-system");
210
+ }
199
211
  }
200
- if (selection.referenceDeckId) {
201
- rememberReference({ id: selection.referenceDeckId, kind: "deck" });
212
+ if (selection.referenceDeckId !== undefined) {
213
+ if (selection.referenceDeckId) {
214
+ rememberReference({ id: selection.referenceDeckId, kind: "deck" });
215
+ } else {
216
+ forgetReference("deck");
217
+ }
202
218
  }
203
219
  await startGeneration(promptFiles, selection);
204
220
  },
205
- [promptFiles, rememberReference, startGeneration],
221
+ [forgetReference, promptFiles, rememberReference, startGeneration],
206
222
  );
207
223
 
208
224
  const handleReferenceImport = useCallback(
@@ -296,7 +312,6 @@ export function FirstDeckOnboardingFlow({
296
312
  setPromptFiles((current) => [...current, ...generationFiles]);
297
313
  if (importedReference) {
298
314
  await reloadDecks();
299
- rememberReference({ id: importedReference.id, kind: "deck" });
300
315
  }
301
316
  return importedReference;
302
317
  } catch (error) {
@@ -311,15 +326,7 @@ export function FirstDeckOnboardingFlow({
311
326
  setReferenceImporting(false);
312
327
  }
313
328
  },
314
- [
315
- callAction,
316
- createDeck,
317
- deleteDeck,
318
- ensureDeckPersisted,
319
- reloadDecks,
320
- rememberReference,
321
- t,
322
- ],
329
+ [callAction, createDeck, deleteDeck, ensureDeckPersisted, reloadDecks, t],
323
330
  );
324
331
 
325
332
  const handleReferenceSourceImport = useCallback(
@@ -357,7 +364,6 @@ export function FirstDeckOnboardingFlow({
357
364
  source: "google-slides",
358
365
  };
359
366
  await reloadDecks();
360
- rememberReference({ id: importedReference.id, kind: "deck" });
361
367
  return importedReference;
362
368
  } catch (error) {
363
369
  toast.error(t("editorToolbar.uploadFailed"), {
@@ -371,15 +377,17 @@ export function FirstDeckOnboardingFlow({
371
377
  setReferenceImporting(false);
372
378
  }
373
379
  },
374
- [callAction, reloadDecks, rememberReference, t],
380
+ [callAction, reloadDecks, t],
375
381
  );
376
382
 
377
383
  const handleReferenceSkip = useCallback(() => {
384
+ forgetReference("design-system");
385
+ forgetReference("deck");
378
386
  void startGeneration(promptFiles, {
379
387
  designSystemId: null,
380
388
  referenceDeckId: null,
381
389
  });
382
- }, [promptFiles, startGeneration]);
390
+ }, [forgetReference, promptFiles, startGeneration]);
383
391
 
384
392
  if (step === "references") {
385
393
  return (
@@ -388,8 +396,7 @@ export function FirstDeckOnboardingFlow({
388
396
  decks={decks}
389
397
  designSystems={designSystems}
390
398
  defaultDesignSystemId={initialDesignSystemId}
391
- defaultReferenceDeckId={workspaceReferenceDeckId}
392
- recentReferences={recentReferences}
399
+ defaultReferenceDeckId={initialReferenceDeckId}
393
400
  onSelect={handleReferenceSelect}
394
401
  onImport={handleReferenceImport}
395
402
  onImportSource={handleReferenceSourceImport}
@@ -404,8 +411,6 @@ export function FirstDeckOnboardingFlow({
404
411
  chooseDeckLabel={t("home.referenceDeckPlaceholder")}
405
412
  importingLabel={t("raw.uploading")}
406
413
  skipLabel={t("home.referenceDeckNone")}
407
- starredLabel={t("home.referenceDeckStarredGroup")}
408
- otherDecksLabel={t("home.referenceDeckOtherGroup")}
409
414
  searchDecksLabel={t("root.searchDecks")}
410
415
  promptSummary={prompt}
411
416
  />
@@ -19,7 +19,7 @@ import type { AspectRatio } from "@/lib/aspect-ratios";
19
19
  import {
20
20
  findLegacyAnimationContainer,
21
21
  getElementPath,
22
- resolveSlideAnimationElement,
22
+ resolveSlideAnimationTargets,
23
23
  } from "@/lib/slide-animation-elements";
24
24
 
25
25
  import type { DesignSystemData } from "../../../shared/api";
@@ -40,7 +40,17 @@ interface PresentationViewProps {
40
40
  * Uses slide.animations if defined, falls back to splitByParagraph auto-detection.
41
41
  */
42
42
  function getAnimationSteps(slide: Slide): SlideAnimation[] | null {
43
- if (slide.animations && slide.animations.length > 0) return slide.animations;
43
+ if (slide.animations && slide.animations.length > 0) {
44
+ const doc = new DOMParser().parseFromString(slide.content, "text/html");
45
+ const root = doc.querySelector(".fmd-slide");
46
+ // Explicit metadata is authoritative only when every target still points
47
+ // at a unique element in the final HTML. Otherwise disable the reveal
48
+ // layer for this slide instead of counting invisible phantom steps while
49
+ // leaving the rest of the content visible.
50
+ return root && resolveSlideAnimationTargets(root, slide.animations)
51
+ ? slide.animations
52
+ : null;
53
+ }
44
54
  // Legacy splitByParagraph: auto-detect and create steps
45
55
  if (slide.splitByParagraph) {
46
56
  const doc = new DOMParser().parseFromString(slide.content, "text/html");
@@ -111,14 +121,16 @@ function annotateStepsForPresentation(
111
121
  const root = doc.querySelector(".fmd-slide");
112
122
  if (!root) return html;
113
123
 
114
- // Annotate each step element with data-pstep
115
- steps.forEach((anim, stepIdx) => {
116
- const el = resolveSlideAnimationElement(root, anim);
117
- if (el) el.setAttribute("data-pstep", String(stepIdx));
124
+ const resolvedSteps = resolveSlideAnimationTargets(root, steps);
125
+ if (!resolvedSteps) return html;
126
+
127
+ // Annotate each resolved step element with data-pstep.
128
+ resolvedSteps.forEach(({ element }, stepIdx) => {
129
+ element.setAttribute("data-pstep", String(stepIdx));
118
130
  });
119
131
 
120
- const styleLines = steps
121
- .map((anim, stepIdx) => {
132
+ const styleLines = resolvedSteps
133
+ .map(({ target }, stepIdx) => {
122
134
  if (stepIdx >= currentStep) {
123
135
  return `[data-pstep="${stepIdx}"] { opacity: 0; pointer-events: none; }`;
124
136
  } else if (stepIdx < currentStep - 1) {
@@ -126,7 +138,7 @@ function annotateStepsForPresentation(
126
138
  return `[data-pstep="${stepIdx}"] { opacity: 1; pointer-events: auto; animation: elem-appear 1ms both; }`;
127
139
  } else {
128
140
  // Newly revealed — animate with its type
129
- return `[data-pstep="${stepIdx}"] { opacity: 1; pointer-events: auto; ${getElemAnimCss(anim.type)} }`;
141
+ return `[data-pstep="${stepIdx}"] { opacity: 1; pointer-events: auto; ${getElemAnimCss(target.type)} }`;
130
142
  }
131
143
  })
132
144
  .join("\n");
@@ -219,6 +231,10 @@ export default function PresentationView({
219
231
  const [cursorVisible, setCursorVisible] = useState(true);
220
232
  const [needsFullscreenGesture, setNeedsFullscreenGesture] = useState(false);
221
233
  const enteredFullscreenRef = useRef(false);
234
+ const transitionTimerRef = useRef<number | null>(null);
235
+ const queuedNavigationRef = useRef<"next" | "prev" | null>(null);
236
+ const goNextRef = useRef<() => void>(() => {});
237
+ const goPrevRef = useRef<() => void>(() => {});
222
238
  const navigate = useNavigate();
223
239
 
224
240
  const isShared = deckId.startsWith("__shared__/");
@@ -230,9 +246,23 @@ export default function PresentationView({
230
246
  );
231
247
  }, [clampIndex, safeSlides.length]);
232
248
 
249
+ const clearTransitionTimer = useCallback(() => {
250
+ if (transitionTimerRef.current !== null) {
251
+ window.clearTimeout(transitionTimerRef.current);
252
+ transitionTimerRef.current = null;
253
+ }
254
+ }, []);
255
+
256
+ useEffect(() => clearTransitionTimer, [clearTransitionTimer]);
257
+
233
258
  useEffect(() => {
259
+ clearTransitionTimer();
260
+ queuedNavigationRef.current = null;
234
261
  setCurrentIndex(clampIndex(startIndex));
235
- }, [clampIndex, startIndex]);
262
+ setCurrentStep(0);
263
+ setPrevIndex(null);
264
+ setAnimating(false);
265
+ }, [clearTransitionTimer, clampIndex, startIndex]);
236
266
 
237
267
  const currentSlide = safeSlides[currentIndex];
238
268
  const animSteps = currentSlide ? getAnimationSteps(currentSlide) : null;
@@ -247,7 +277,11 @@ export default function PresentationView({
247
277
  const initialStep =
248
278
  dir === "prev" ? (incomingSteps ? incomingSteps.length : 0) : 0;
249
279
 
280
+ clearTransitionTimer();
281
+ queuedNavigationRef.current = null;
250
282
  if (isInstant(t)) {
283
+ setPrevIndex(null);
284
+ setAnimating(false);
251
285
  setCurrentIndex(newIndex);
252
286
  setCurrentStep(initialStep);
253
287
  return;
@@ -259,16 +293,28 @@ export default function PresentationView({
259
293
  setCurrentIndex(newIndex);
260
294
  setCurrentStep(initialStep);
261
295
 
262
- setTimeout(() => {
296
+ transitionTimerRef.current = window.setTimeout(() => {
297
+ transitionTimerRef.current = null;
263
298
  setPrevIndex(null);
264
299
  setAnimating(false);
300
+ const queued = queuedNavigationRef.current;
301
+ queuedNavigationRef.current = null;
302
+ if (queued) {
303
+ window.setTimeout(() => {
304
+ if (queued === "next") goNextRef.current();
305
+ else goPrevRef.current();
306
+ }, 0);
307
+ }
265
308
  }, 400);
266
309
  },
267
- [currentIndex, safeSlides],
310
+ [clearTransitionTimer, currentIndex, safeSlides],
268
311
  );
269
312
 
270
313
  const goNext = useCallback(() => {
271
- if (animating) return;
314
+ if (animating) {
315
+ queuedNavigationRef.current = "next";
316
+ return;
317
+ }
272
318
  // Reveal next paragraph step if enabled
273
319
  if (maxSteps > 0 && currentStep < maxSteps /* i18n-ignore */) {
274
320
  setCurrentStep((prev) => prev + 1);
@@ -286,7 +332,10 @@ export default function PresentationView({
286
332
  ]);
287
333
 
288
334
  const goPrev = useCallback(() => {
289
- if (animating) return;
335
+ if (animating) {
336
+ queuedNavigationRef.current = "prev";
337
+ return;
338
+ }
290
339
  if (currentIndex <= 0) return;
291
340
  startTransition(currentIndex - 1, "prev");
292
341
  }, [animating, currentIndex, startTransition]);
@@ -307,8 +356,6 @@ export default function PresentationView({
307
356
  // commands and mirrors whatever we echo back — so build steps stay
308
357
  // authoritative here.
309
358
  const channelRef = useRef<BroadcastChannel | null>(null);
310
- const goNextRef = useRef(goNext);
311
- const goPrevRef = useRef(goPrev);
312
359
  goNextRef.current = goNext;
313
360
  goPrevRef.current = goPrev;
314
361
 
@@ -21,6 +21,11 @@ export function useGeneratingSlidePreview({
21
21
  const [content, setContent] = useState<string | null>(null);
22
22
  const activeCallIdRef = useRef<string | null>(null);
23
23
  const previousSlideCountRef = useRef(slideCount);
24
+ // Latest parsed value awaiting a flush; setContent fires at most once per frame
25
+ // and always reads this ref, so a burst of deltas within one frame collapses
26
+ // to a single render carrying the newest value instead of the stale one.
27
+ const pendingContentRef = useRef<string | null>(null);
28
+ const rafIdRef = useRef<number | null>(null);
24
29
 
25
30
  useEffect(() => {
26
31
  if (slideCount !== previousSlideCountRef.current) {
@@ -37,6 +42,18 @@ export function useGeneratingSlidePreview({
37
42
  return;
38
43
  }
39
44
 
45
+ const flush = () => {
46
+ rafIdRef.current = null;
47
+ setContent(pendingContentRef.current);
48
+ };
49
+
50
+ const scheduleFlush = (value: string | null) => {
51
+ pendingContentRef.current = value;
52
+ if (rafIdRef.current === null) {
53
+ rafIdRef.current = requestAnimationFrame(flush);
54
+ }
55
+ };
56
+
40
57
  const handleToolInput = (event: Event) => {
41
58
  const detail = (event as CustomEvent<ToolInputEventDetail>).detail;
42
59
  if (detail?.tool !== "add-slide") return;
@@ -47,7 +64,7 @@ export function useGeneratingSlidePreview({
47
64
  if (detail.phase === "start") {
48
65
  if (detail.id && activeCallIdRef.current === detail.id) return;
49
66
  activeCallIdRef.current = detail.id ?? null;
50
- setContent(null);
67
+ scheduleFlush(null);
51
68
  return;
52
69
  }
53
70
 
@@ -60,13 +77,18 @@ export function useGeneratingSlidePreview({
60
77
  }
61
78
  if (detail.id) activeCallIdRef.current = detail.id;
62
79
  if (parsed.content !== undefined) {
63
- setContent(parsed.content || null);
80
+ scheduleFlush(parsed.content || null);
64
81
  }
65
82
  };
66
83
 
67
84
  window.addEventListener("agent-native:tool-input", handleToolInput);
68
- return () =>
85
+ return () => {
69
86
  window.removeEventListener("agent-native:tool-input", handleToolInput);
87
+ if (rafIdRef.current !== null) {
88
+ cancelAnimationFrame(rafIdRef.current);
89
+ rafIdRef.current = null;
90
+ }
91
+ };
70
92
  }, [deckId, generating]);
71
93
 
72
94
  return content;
@@ -689,6 +689,7 @@ const messages = {
689
689
  "Keep the default design system and reference deck unless you remove them. Search for another deck, import a file, or continue when you're ready.",
690
690
  chooseAnotherDeck: "Choose another deck",
691
691
  noMatchingDecks: "No matching decks found.",
692
+ googleSlidesImportLabel: "Slides",
692
693
  googleSlidesReferenceTitle: "Google Slides",
693
694
  googleSlidesReferenceConnect:
694
695
  "Connect Google Drive to import a Slides deck.",
@@ -113,7 +113,7 @@ function describeUploadedFilesForAgent(
113
113
  "",
114
114
  "File handling rules:",
115
115
  importedSourceDeck
116
- ? "- The imported source deck is the canonical source. Preserve its slide count, order, IDs, factual copy, notes, imagery, charts, tables, diagrams, and freeform objects while improving styling. Use update-slide on existing slide IDs; do not rebuild it with add-slide."
116
+ ? "- The imported source deck is the canonical source. Preserve its slide count, order, IDs, factual copy, notes, imagery, charts, tables, diagrams, and freeform objects while improving styling. For a deck-wide restyle, use one patch-deck call with requireAllSourceSlides=true; use update-slide only for a targeted one-slide edit. Do not rebuild it with add-slide."
117
117
  : `- PPTX files: call \`import-pptx --filePath \"<path>\" --deckId ${deckId}\` before adding or editing slides.`,
118
118
  importedSourceDeck
119
119
  ? "- For a PDF source, keep the original full-page image in every slide and add restrained design-system chrome around it without obscuring source content. Never OCR-reconstruct a source-faithful page from extracted text."
@@ -324,19 +324,20 @@ export async function startDeckGeneration({
324
324
  "Source-preserving improvement mode:",
325
325
  `- The target deck already contains ${importedSourceDeck.slideCount} imported source slides. Treat those slides as the user's complete source, not as inspiration for a new deck.`,
326
326
  "- Keep the exact source slide count, order, IDs, factual meaning, notes, images, charts, tables, diagrams, and freeform objects unless the user explicitly asks to change one of them.",
327
- "- Read `get-deck` before editing, load the linked design system with `get-design-system`, then use one `update-slide` call per existing slide ID for bounded visual improvements. Keep every original `<img>` source and enough original factual copy for each slide; for PDF slides, use restrained design-system chrome around the page without obscuring it.",
327
+ "- Read `get-deck` once before editing to obtain every existing slide ID and source HTML, load the linked design system with `get-design-system`, then make a deck-wide restyle with one `patch-deck` call using `requireAllSourceSlides: true` and one `patch-slide` operation with `fields.content` for every source slide ID. Do not split a full-deck restyle into arbitrary batches or fall back to one-by-one `update-slide` calls; use `update-slide` only for a targeted one-slide edit. Keep every original `<img>` source and enough original factual copy for each slide; for PDF slides, use restrained design-system chrome around the page without obscuring it.",
328
328
  "- Do not call `add-slide`, delete slides, reorder slides, or replace source images with generic cards. Do not claim success until `get-deck` verifies the same slide IDs and count after the edits.",
329
+ '- After the patch succeeds, verify with `get-deck` using `compact: "true"` so only slide IDs, count, and previews are returned. Do not report an initial or partial pass, and do not leave any source slides for a later run.',
329
330
  "- If `get-deck` reports partial source fidelity or skipped images, stop and report the exact warning instead of claiming a reliable restyle.",
330
331
  ].join("\n")
331
332
  : "";
332
333
  const sourceModeInstructions = importedSourceDeck
333
334
  ? [
334
335
  "The request is an in-place visual improvement of an imported source deck. Make a coherent style pass across every existing slide while preserving all source content and media.",
335
- "Do not use the new-deck add-slide workflow for this source-preserving request.",
336
+ "Do not use the new-deck add-slide workflow for this source-preserving request. Finish every source slide in this run; if patch-deck rejects incomplete coverage, continue with the returned missing IDs instead of reporting success with a partial deck.",
336
337
  ]
337
338
  : [
338
339
  "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.",
339
- `After reading any requested or imported source material, but before adding the first slide, choose a concise, specific deck title from the user's request and source material. Call \`patch-deck\` with \`deckId: \"${deckId}\"\` 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.`,
340
+ `After reading any requested or imported source 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: \"${deckId}\"\` 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.`,
340
341
  "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.",
341
342
  "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.",
342
343
  `Add slides ONE AT A TIME using the \`add-slide\` action with --deckId=${deckId}. Wait for each \`add-slide\` result before calling it again; do not batch or parallelize slide writes.`,
@@ -48,7 +48,10 @@ export function readRecentReferences(
48
48
  const parsed: unknown = JSON.parse(raw);
49
49
  if (!Array.isArray(parsed)) return { items: [], readable: false };
50
50
  return {
51
- items: parsed.filter(isRecentReference).slice(0, MAX_RECENT_REFERENCES),
51
+ items: parsed
52
+ .filter(isRecentReference)
53
+ .sort((first, second) => second.lastUsedAt - first.lastUsedAt)
54
+ .slice(0, MAX_RECENT_REFERENCES),
52
55
  readable: true,
53
56
  };
54
57
  } catch {
@@ -56,6 +59,25 @@ export function readRecentReferences(
56
59
  }
57
60
  }
58
61
 
62
+ export function forgetRecentReference(
63
+ kind: RecentReferenceKind,
64
+ storage:
65
+ | Pick<Storage, "getItem" | "setItem">
66
+ | null
67
+ | undefined = typeof window === "undefined" ? null : window.localStorage,
68
+ ): RecentReferencesResult {
69
+ const current = readRecentReferences(storage);
70
+ if (!storage || !current.readable) return current;
71
+
72
+ const next = current.items.filter((item) => item.kind !== kind);
73
+ try {
74
+ storage.setItem(RECENT_REFERENCES_STORAGE_KEY, JSON.stringify(next));
75
+ return { items: next, readable: true };
76
+ } catch {
77
+ return { items: current.items, readable: false };
78
+ }
79
+ }
80
+
59
81
  export function rememberRecentReference(
60
82
  reference: Omit<RecentReference, "lastUsedAt">,
61
83
  storage:
@@ -9,6 +9,14 @@ export interface AnimationTarget {
9
9
  elementPath?: number[];
10
10
  }
11
11
 
12
+ export interface ResolvedAnimationTarget<
13
+ T extends AnimationTarget = AnimationTarget,
14
+ > {
15
+ target: T;
16
+ element: Element;
17
+ key: string;
18
+ }
19
+
12
20
  const INLINE_TAGS = new Set([
13
21
  "a",
14
22
  "abbr",
@@ -192,15 +200,45 @@ export function resolveSlideAnimationElement(
192
200
  root: Element,
193
201
  target: AnimationTarget,
194
202
  ): Element | null {
195
- if (Array.isArray(target.elementPath) && target.elementPath.length > 0) {
196
- const pathTarget = resolveElementPath(root, target.elementPath);
197
- if (pathTarget) return pathTarget;
203
+ if (Array.isArray(target.elementPath)) {
204
+ // A supplied path is the identity of the target. Falling back to a
205
+ // legacy index after it goes stale can animate a different element while
206
+ // leaving the intended element visible.
207
+ return target.elementPath.length > 0
208
+ ? resolveElementPath(root, target.elementPath)
209
+ : null;
198
210
  }
199
211
 
200
212
  const legacyContainer = findLegacyAnimationContainer(root);
201
213
  return legacyContainer?.children.item(target.elementIndex) ?? null;
202
214
  }
203
215
 
216
+ /**
217
+ * Resolve an ordered animation list as one validated unit. A null result
218
+ * means the list cannot be rendered faithfully - a missing or duplicate
219
+ * target must not become a phantom click step in the presentation player.
220
+ */
221
+ export function resolveSlideAnimationTargets<T extends AnimationTarget>(
222
+ root: Element,
223
+ targets: readonly T[],
224
+ ): ResolvedAnimationTarget<T>[] | null {
225
+ const seen = new Set<string>();
226
+ const resolved: ResolvedAnimationTarget<T>[] = [];
227
+
228
+ for (const target of targets) {
229
+ const element = resolveSlideAnimationElement(root, target);
230
+ if (!element) return null;
231
+ const path = getElementPath(root, element);
232
+ if (!path) return null;
233
+ const key = animationElementKey(path);
234
+ if (seen.has(key)) return null;
235
+ seen.add(key);
236
+ resolved.push({ target, element, key });
237
+ }
238
+
239
+ return resolved;
240
+ }
241
+
204
242
  export function getSlideAnimationTargetKey(
205
243
  html: string,
206
244
  target: AnimationTarget,
@@ -5,7 +5,7 @@ import {
5
5
  emailToColor,
6
6
  emailToName,
7
7
  } from "@agent-native/core/client/collab";
8
- import { useSession, callAction } from "@agent-native/core/client/hooks";
8
+ import { useSession } from "@agent-native/core/client/hooks";
9
9
  import { useT } from "@agent-native/core/client/i18n";
10
10
  import { useOrg } from "@agent-native/core/client/org";
11
11
  import {
@@ -54,7 +54,7 @@ import {
54
54
  useSlideComments,
55
55
  type CommentThread,
56
56
  } from "@/hooks/use-slide-comments";
57
- import { getAspectRatioDims, type AspectRatio } from "@/lib/aspect-ratios";
57
+ import { getAspectRatioDims } from "@/lib/aspect-ratios";
58
58
  import {
59
59
  deckAccessCheckKey,
60
60
  shouldShowDeckEditorSkeleton,
@@ -238,9 +238,6 @@ export default function DeckEditor() {
238
238
  const [pendingComment, setPendingComment] = useState<{
239
239
  quotedText: string;
240
240
  } | null>(null);
241
- const imageGenButtonRef = useRef<HTMLButtonElement>(null);
242
- const assetsButtonRef = useRef<HTMLButtonElement>(null);
243
-
244
241
  // Track which image src to replace
245
242
  const [replaceImageSrc, setReplaceImageSrc] = useState<string | null>(null);
246
243
 
@@ -1035,16 +1032,9 @@ export default function DeckEditor() {
1035
1032
  setReplaceImageSrc(null);
1036
1033
  setAssetLibraryOpen(true);
1037
1034
  }}
1038
- imageGenButtonRef={imageGenButtonRef}
1039
- assetsButtonRef={assetsButtonRef}
1040
- historyOpen={historyOpen}
1041
1035
  onShowHistory={() => setHistoryOpen(!historyOpen)}
1042
1036
  historyButtonRef={historyButtonRef}
1043
1037
  currentSlide={currentSlide}
1044
- onUpdateSlide={(updates, slideIdOverride) => {
1045
- const targetId = slideIdOverride ?? currentSlide?.id;
1046
- if (targetId) updateSlide(id, targetId, updates);
1047
- }}
1048
1038
  activeUsers={slideActiveUsers.filter((u) => u.email !== session?.email)}
1049
1039
  agentPresent={agentPresent}
1050
1040
  agentActive={agentActive}
@@ -1118,19 +1108,6 @@ export default function DeckEditor() {
1118
1108
  }
1119
1109
  return exportDeckToGoogleSlides(deck.title, slides, deck.aspectRatio);
1120
1110
  }}
1121
- aspectRatio={deck.aspectRatio}
1122
- onSetAspectRatio={(ratio: AspectRatio) => {
1123
- const previous = deck.aspectRatio;
1124
- // Optimistic UI: update local cache immediately so canvas resizes.
1125
- updateDeck(id, { aspectRatio: ratio });
1126
- callAction("update-deck-aspect-ratio", {
1127
- deckId: id,
1128
- aspectRatio: ratio,
1129
- }).catch((err) => {
1130
- console.error("Failed to set aspect ratio:", err);
1131
- updateDeck(id, { aspectRatio: previous });
1132
- });
1133
- }}
1134
1111
  currentSlideId={currentSlide?.id}
1135
1112
  addSlideGenerating={addSlideGenerating}
1136
1113
  onAddSlideGeneratingChange={setAddSlideGenerating}
@@ -1376,7 +1353,7 @@ export default function DeckEditor() {
1376
1353
  <ImageGenPanel
1377
1354
  open={imageGenOpen}
1378
1355
  onOpenChange={setImageGenOpen}
1379
- anchorRef={imageGenButtonRef}
1356
+ anchorRef={historyButtonRef}
1380
1357
  referenceImageUrls={imageStyleReferenceUrls}
1381
1358
  slideContext={
1382
1359
  currentSlide
@@ -1394,7 +1371,7 @@ export default function DeckEditor() {
1394
1371
  <AssetLibraryPanel
1395
1372
  open={assetLibraryOpen}
1396
1373
  onOpenChange={setAssetLibraryOpen}
1397
- anchorRef={assetsButtonRef}
1374
+ anchorRef={historyButtonRef}
1398
1375
  onSelectAsset={
1399
1376
  replaceImageSrc
1400
1377
  ? (newUrl) => {