@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
@@ -12,13 +12,12 @@ import { useSidebarCollapsed } from "@/hooks/use-sidebar-collapsed";
12
12
  import { TAB_ID } from "@/lib/tab-id";
13
13
  import { cn } from "@/lib/utils";
14
14
 
15
- import { GoogleDriveConnectionCta } from "../editor/GoogleDriveConnectionCta";
16
15
  import { AgentWorkIndicator } from "./AgentWorkIndicator";
17
16
  import { Header } from "./Header";
18
17
  import {
19
- getDeckChatScopeLabel,
20
18
  getEffectiveSlidesSidebarCollapsed,
21
19
  isSlidesEditorRoute,
20
+ shouldShowSlidesAppSidebar,
22
21
  } from "./layout-route-policy";
23
22
  import { Sidebar } from "./Sidebar";
24
23
 
@@ -50,26 +49,24 @@ export function Layout({ children }: LayoutProps) {
50
49
  useState<EditorSidebarOverride | null>(null);
51
50
  const { collapsed: sidebarCollapsed, setCollapsed: setSidebarCollapsed } =
52
51
  useSidebarCollapsed();
53
- const { decks, getDeck, loading: decksLoading } = useDecks();
52
+ const { decks, loading: decksLoading } = useDecks();
54
53
  const isEmptyDecksState =
55
54
  location.pathname === "/" && !decksLoading && decks.length === 0;
56
55
 
57
56
  // Scope new chats to the deck the user is currently editing. The route
58
57
  // is `/deck/:id`; everywhere else (list, presentation) leaves
59
- // scope null so chats stay in the general pool. Falling back to the
60
- // raw deck id keeps the chat bound even before the deck object has
61
- // streamed in — once the title arrives the badge updates in place.
58
+ // scope null so chats stay in the general pool. Keep the visible label
59
+ // semantic so an imported deck id never leaks into the composer chip.
62
60
  const deckScope = useMemo(() => {
63
61
  const match = location.pathname.match(/^\/deck\/([^/]+)/);
64
62
  const deckId = match?.[1];
65
63
  if (!deckId) return null;
66
- const deck = getDeck(deckId);
67
64
  return {
68
65
  type: "deck" as const,
69
66
  id: deckId,
70
- label: getDeckChatScopeLabel(deck?.title, t("agent.thisDeck")),
67
+ label: t("agent.thisDeck"),
71
68
  };
72
- }, [location.pathname, getDeck, t]);
69
+ }, [location.pathname, t]);
73
70
 
74
71
  useEffect(() => {
75
72
  setSidebarOpen(false);
@@ -84,6 +81,7 @@ export function Layout({ children }: LayoutProps) {
84
81
  }, []);
85
82
 
86
83
  const ownToolbar = pageHasOwnToolbar(location.pathname);
84
+ const showAppSidebar = shouldShowSlidesAppSidebar(location.pathname);
87
85
  const editorSidebarOverrideForLocation =
88
86
  editorSidebarOverride?.locationKey === location.key
89
87
  ? editorSidebarOverride.collapsed
@@ -120,35 +118,36 @@ export function Layout({ children }: LayoutProps) {
120
118
  agentPageHref="/settings/agent"
121
119
  suppressFirstRunOnboarding={isSlidesEditorRoute(location.pathname)}
122
120
  composerSlot={<CreativeContextComposerChip />}
123
- threadFooterSlot={<GoogleDriveConnectionCta />}
124
121
  >
125
122
  <div className="agent-layout-shell flex h-screen w-full overflow-hidden bg-background text-foreground">
126
- {sidebarOpen && (
127
- <div
128
- className="fixed inset-0 z-40 bg-black/50 md:hidden"
129
- onClick={() => setSidebarOpen(false)}
130
- />
131
- )}
132
- {!isEmptyDecksState && (
133
- <div
134
- className={cn(
135
- "agent-layout-left-drawer fixed inset-y-0 start-0 z-50 transition-transform duration-200 ease-out md:static md:z-auto md:transition-none",
136
- sidebarOpen
137
- ? "translate-x-0"
138
- : "-translate-x-full rtl:translate-x-full md:translate-x-0 md:rtl:translate-x-0",
123
+ {!isEmptyDecksState && showAppSidebar && (
124
+ <>
125
+ {sidebarOpen && (
126
+ <div
127
+ className="fixed inset-0 z-40 bg-foreground/50 md:hidden"
128
+ onClick={() => setSidebarOpen(false)}
129
+ />
139
130
  )}
140
- >
141
- <Sidebar
142
- collapsed={effectiveSidebarCollapsed && !sidebarOpen}
143
- // In the mobile drawer the sidebar is forced expanded, so the
144
- // desktop collapse toggle would be a silent no-op (worse: it'd
145
- // mutate the desktop preference). Hide it while the drawer is
146
- // open.
147
- onToggleCollapsed={
148
- sidebarOpen ? undefined : toggleSidebarCollapsed
149
- }
150
- />
151
- </div>
131
+ <div
132
+ className={cn(
133
+ "agent-layout-left-drawer fixed inset-y-0 start-0 z-50 transition-transform duration-200 ease-out md:static md:z-auto md:transition-none",
134
+ sidebarOpen
135
+ ? "translate-x-0"
136
+ : "-translate-x-full rtl:translate-x-full md:translate-x-0 md:rtl:translate-x-0",
137
+ )}
138
+ >
139
+ <Sidebar
140
+ collapsed={effectiveSidebarCollapsed && !sidebarOpen}
141
+ // In the mobile drawer the sidebar is forced expanded, so the
142
+ // desktop collapse toggle would be a silent no-op (worse: it'd
143
+ // mutate the desktop preference). Hide it while the drawer is
144
+ // open.
145
+ onToggleCollapsed={
146
+ sidebarOpen ? undefined : toggleSidebarCollapsed
147
+ }
148
+ />
149
+ </div>
150
+ </>
152
151
  )}
153
152
  <div className="agent-layout-main-surface flex h-full min-w-0 flex-1 flex-col overflow-hidden">
154
153
  {/* Mobile-only nav strip with hamburger — only when there's no page toolbar */}
@@ -2,11 +2,8 @@ export function isSlidesEditorRoute(pathname: string): boolean {
2
2
  return /^\/deck\/[^/]+\/?$/.test(pathname);
3
3
  }
4
4
 
5
- export function getDeckChatScopeLabel(
6
- deckTitle: string | undefined,
7
- untitledDeckLabel: string,
8
- ): string {
9
- return deckTitle === "Untitled Deck" ? untitledDeckLabel : deckTitle || "";
5
+ export function shouldShowSlidesAppSidebar(pathname: string): boolean {
6
+ return !isSlidesEditorRoute(pathname);
10
7
  }
11
8
 
12
9
  export function getEffectiveSlidesSidebarCollapsed({
@@ -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.`,