@agent-native/core 0.154.5 → 0.157.0

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 (263) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/account-health/SKILL.md +76 -0
  3. package/corpus/templates/analytics/.agents/skills/bigquery/SKILL.md +3 -0
  4. package/corpus/templates/analytics/.agents/skills/cross-source-analysis/SKILL.md +2 -1
  5. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +6 -5
  6. package/corpus/templates/analytics/AGENTS.md +3 -0
  7. package/corpus/templates/analytics/actions/account-deep-dive.ts +3 -2
  8. package/corpus/templates/analytics/actions/bigquery.ts +1 -1
  9. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +4 -1
  10. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +1 -0
  11. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyCombobox.tsx +12 -4
  12. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyValueCombobox.tsx +5 -2
  13. package/corpus/templates/analytics/app/pages/adhoc/explorer/property-label.ts +32 -0
  14. package/corpus/templates/analytics/changelog/2026-08-13-account-health-readouts-verify-customer-scope-and-contract-metrics.md +7 -0
  15. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +43 -17
  16. package/corpus/templates/analytics/server/lib/analytics-query-catalog.ts +17 -0
  17. package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
  18. package/corpus/templates/assets/app/components/asset/AssetPreviewDialog.tsx +3 -1
  19. package/corpus/templates/assets/app/i18n-data.ts +21 -1
  20. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +17 -13
  21. package/corpus/templates/calendar/AGENTS.md +2 -0
  22. package/corpus/templates/calendar/DEVELOPING.md +4 -4
  23. package/corpus/templates/calendar/actions/get-settings.ts +4 -11
  24. package/corpus/templates/calendar/actions/update-availability.ts +15 -4
  25. package/corpus/templates/calendar/actions/update-settings.ts +15 -4
  26. package/corpus/templates/calendar/app/components/booking/DatePicker.tsx +9 -3
  27. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +21 -19
  28. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +9 -6
  29. package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +25 -3
  30. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +10 -3
  31. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +9 -9
  32. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +11 -3
  33. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +10 -4
  34. package/corpus/templates/calendar/app/i18n-data.ts +30 -0
  35. package/corpus/templates/calendar/app/lib/availability-schedule.ts +53 -0
  36. package/corpus/templates/calendar/app/lib/event-form-utils.ts +5 -0
  37. package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +90 -28
  38. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +135 -61
  39. package/corpus/templates/calendar/app/pages/BookingPage.tsx +2 -0
  40. package/corpus/templates/calendar/app/pages/CalendarView.tsx +30 -15
  41. package/corpus/templates/calendar/app/pages/Settings.tsx +60 -1
  42. package/corpus/templates/calendar/changelog/2026-08-13-find-a-time-now-closes-from-a-clear-header-action.md +6 -0
  43. package/corpus/templates/calendar/server/handlers/bookings.ts +2 -1
  44. package/corpus/templates/calendar/server/handlers/settings.ts +12 -16
  45. package/corpus/templates/calendar/shared/api.ts +1 -0
  46. package/corpus/templates/calendar/shared/availability-schedule.ts +64 -0
  47. package/corpus/templates/calendar/shared/calendar-week.ts +21 -0
  48. package/corpus/templates/calendar/shared/settings.ts +44 -0
  49. package/corpus/templates/clips/AGENTS.md +2 -0
  50. package/corpus/templates/clips/actions/add-comment.ts +5 -2
  51. package/corpus/templates/clips/actions/finalize-recording.ts +31 -1
  52. package/corpus/templates/clips/actions/get-recording-player-data.ts +9 -0
  53. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +21 -1
  54. package/corpus/templates/clips/actions/reply-to-comment.ts +5 -2
  55. package/corpus/templates/clips/actions/update-comment.ts +9 -2
  56. package/corpus/templates/clips/app/components/library/search-bar.tsx +26 -13
  57. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +135 -3
  58. package/corpus/templates/clips/app/components/player/comments-panel.tsx +5 -99
  59. package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +7 -3
  60. package/corpus/templates/clips/app/components/player/share-dialog.tsx +102 -59
  61. package/corpus/templates/clips/app/components/player/video-player.tsx +84 -24
  62. package/corpus/templates/clips/app/components/workspace/notifications-list.tsx +11 -3
  63. package/corpus/templates/clips/app/i18n/en-US.ts +12 -0
  64. package/corpus/templates/clips/app/lib/public-meeting.ts +11 -3
  65. package/corpus/templates/clips/app/lib/search-focus.ts +15 -0
  66. package/corpus/templates/clips/app/root.tsx +2 -1
  67. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +94 -26
  68. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +24 -7
  69. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +8 -4
  70. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +29 -8
  71. package/corpus/templates/clips/app/routes/share.$shareId.tsx +24 -6
  72. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +72 -26
  73. package/corpus/templates/clips/changelog/2026-08-13-inline-markdown-comments.md +6 -0
  74. package/corpus/templates/clips/changelog/2026-08-13-private-meeting-agent-links.md +6 -0
  75. package/corpus/templates/clips/desktop/src/app.tsx +116 -19
  76. package/corpus/templates/clips/desktop/src/lib/recorder.ts +13 -29
  77. package/corpus/templates/clips/desktop/src/lib/recording-request.ts +39 -0
  78. package/corpus/templates/clips/desktop/src/lib/settings-navigation.ts +11 -0
  79. package/corpus/templates/clips/desktop/src/lib/whisper-model-picker.ts +7 -0
  80. package/corpus/templates/clips/desktop/src/styles.css +117 -0
  81. package/corpus/templates/clips/server/db/index.ts +10 -0
  82. package/corpus/templates/clips/server/db/schema.ts +1 -0
  83. package/corpus/templates/clips/server/lib/recordings.ts +3 -1
  84. package/corpus/templates/clips/server/lib/seekable-media-state.ts +90 -0
  85. package/corpus/templates/clips/server/plugins/db.ts +5 -0
  86. package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +37 -4
  87. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +16 -5
  88. package/corpus/templates/clips/shared/clips-ai-prefs.ts +3 -0
  89. package/corpus/templates/clips/shared/meeting-agent-access.ts +5 -0
  90. package/corpus/templates/content/actions/add-comment.ts +2 -1
  91. package/corpus/templates/content/actions/update-comment.ts +1 -1
  92. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +24 -15
  93. package/corpus/templates/design/actions/generate-design.ts +10 -7
  94. package/corpus/templates/design/actions/present-design-variants.ts +35 -6
  95. package/corpus/templates/design/actions/run-design-audit.ts +33 -0
  96. package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-catches-a-screen-that-renders-behind-an.md +6 -0
  97. package/corpus/templates/design/shared/design-review.ts +1 -0
  98. package/corpus/templates/design/shared/html-integrity.ts +66 -0
  99. package/corpus/templates/dispatch/.env.example +5 -0
  100. package/corpus/templates/dispatch/AGENTS.md +24 -19
  101. package/corpus/templates/dispatch/app/i18n-data.ts +20 -0
  102. package/corpus/templates/dispatch/server/lib/identity-sso.ts +21 -90
  103. package/corpus/templates/dispatch/server/plugins/identity-sso.ts +3 -12
  104. package/corpus/templates/dispatch/shared/feature-flags.ts +4 -0
  105. package/corpus/templates/factory/AGENTS.md +5 -1
  106. package/corpus/templates/factory/README.md +23 -0
  107. package/corpus/templates/factory/actions/view-screen.ts +17 -0
  108. package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +239 -0
  109. package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +2 -0
  110. package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +130 -7
  111. package/corpus/templates/factory/app/i18n/en-US.ts +20 -0
  112. package/corpus/templates/factory/app/routes/factory.tsx +41 -0
  113. package/corpus/templates/factory/package.json +1 -0
  114. package/corpus/templates/factory/server/factory-graph/contracts.ts +2 -0
  115. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -0
  116. package/corpus/templates/factory/server/plugins/dispatch-db.ts +1 -0
  117. package/corpus/templates/factory/server/plugins/setup-dispatch.ts +3 -0
  118. package/corpus/templates/mail/app/lib/native-mcp-exclusions.ts +3 -0
  119. package/corpus/templates/mail/vite.config.ts +7 -0
  120. package/corpus/templates/plan/actions/reply-to-plan-comment.ts +7 -2
  121. package/corpus/templates/plan/actions/resolve-plan-comment.ts +2 -2
  122. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  123. package/corpus/templates/plan/app/pages/PlansPage.tsx +29 -20
  124. package/corpus/templates/slides/actions/add-slide-comment.ts +1 -1
  125. package/corpus/templates/slides/actions/update-slide-comment.ts +1 -1
  126. package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +9 -6
  127. package/corpus/templates/slides/app/components/editor/AddSlidePopover.tsx +98 -29
  128. package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +45 -91
  129. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +94 -2
  130. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +17 -29
  131. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +1 -1
  132. package/corpus/templates/slides/app/context/DeckContext.tsx +42 -2
  133. package/corpus/templates/slides/app/global.css +7 -40
  134. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  135. package/corpus/templates/slides/app/pages/DeckEditor.tsx +130 -45
  136. package/dist/agent/index.d.ts +1 -1
  137. package/dist/agent/production-agent.d.ts +30 -0
  138. package/dist/agent/production-agent.js +105 -9
  139. package/dist/agent/thread-data-builder.js +17 -1
  140. package/dist/agent/types.d.ts +9 -0
  141. package/dist/cli/skills-content/help.js +6 -4
  142. package/dist/cli/skills.js +17 -2
  143. package/dist/cli/workspacify.d.ts +5 -0
  144. package/dist/cli/workspacify.js +65 -8
  145. package/dist/client/AgentPanel.d.ts +5 -0
  146. package/dist/client/AgentPanel.js +32 -13
  147. package/dist/client/AssistantChat.d.ts +36 -1
  148. package/dist/client/AssistantChat.js +175 -110
  149. package/dist/client/MultiTabAssistantChat.js +44 -27
  150. package/dist/client/active-run-state.d.ts +14 -1
  151. package/dist/client/active-run-state.js +0 -0
  152. package/dist/client/agent-chat-adapter.js +107 -5
  153. package/dist/client/analytics.js +9 -2
  154. package/dist/client/chat/markdown-renderer.js +4 -2
  155. package/dist/client/chat/message-components.d.ts +2 -2
  156. package/dist/client/chat/message-components.js +69 -35
  157. package/dist/client/chat/run-recovery.d.ts +4 -1
  158. package/dist/client/chat/run-recovery.js +62 -30
  159. package/dist/client/chat/tool-call-display.d.ts +12 -3
  160. package/dist/client/chat/tool-call-display.js +79 -40
  161. package/dist/client/chat/widgets/DataChartRenderer.js +3 -1
  162. package/dist/client/chat/widgets/DataChartWidget.js +8 -2
  163. package/dist/client/chat/widgets/DataInsightsWidget.js +3 -1
  164. package/dist/client/chat/widgets/DataTableWidget.js +23 -9
  165. package/dist/client/chat/widgets/builtin-tool-renderers.js +3 -1
  166. package/dist/client/chat-first/apps-rail.js +9 -4
  167. package/dist/client/chat-first/primary-nav.d.ts +2 -1
  168. package/dist/client/chat-first/primary-nav.js +3 -3
  169. package/dist/client/chat-first/types.d.ts +5 -1
  170. package/dist/client/components/ui/message-scroller.d.ts +1 -1
  171. package/dist/client/components/ui/message-scroller.js +7 -3
  172. package/dist/client/composer/runtime-adapters.js +3 -2
  173. package/dist/client/context-xray/ContextMeter.js +5 -1
  174. package/dist/client/context-xray/ContextXRayPanel.js +5 -5
  175. package/dist/client/error-format.d.ts +4 -0
  176. package/dist/client/error-format.js +93 -5
  177. package/dist/client/i18n.js +83 -17
  178. package/dist/client/markdown/InlineMarkdown.d.ts +23 -0
  179. package/dist/client/markdown/InlineMarkdown.js +65 -0
  180. package/dist/client/markdown/index.d.ts +1 -0
  181. package/dist/client/markdown/index.js +1 -0
  182. package/dist/client/observability/ThumbsFeedback.js +10 -8
  183. package/dist/client/review/ReviewThreadPanel.js +2 -1
  184. package/dist/client/run-stream-ownership.d.ts +39 -0
  185. package/dist/client/run-stream-ownership.js +65 -0
  186. package/dist/client/sharing/ShareButton.js +160 -54
  187. package/dist/coding-tools/run-code.js +2 -1
  188. package/dist/coding-tools/sandbox/background.d.ts +1 -0
  189. package/dist/coding-tools/sandbox/background.js +8 -1
  190. package/dist/coding-tools/sandbox/executions-store.d.ts +2 -0
  191. package/dist/coding-tools/sandbox/executions-store.js +41 -3
  192. package/dist/collab/awareness.d.ts +2 -2
  193. package/dist/collab/routes.d.ts +1 -1
  194. package/dist/collab/struct-routes.d.ts +1 -1
  195. package/dist/feature-flags/index.d.ts +1 -1
  196. package/dist/feature-flags/index.js +1 -1
  197. package/dist/feature-flags/store.d.ts +10 -0
  198. package/dist/feature-flags/store.js +112 -1
  199. package/dist/guards/no-env-credentials.js +4 -0
  200. package/dist/index.d.ts +2 -2
  201. package/dist/index.js +1 -1
  202. package/dist/integrations/adapters/slack.js +5 -1
  203. package/dist/localization/core-messages/ar-SA.d.ts +3 -0
  204. package/dist/localization/core-messages/ar-SA.js +541 -0
  205. package/dist/localization/core-messages/de-DE.d.ts +3 -0
  206. package/dist/localization/core-messages/de-DE.js +513 -0
  207. package/dist/localization/core-messages/en-US.d.ts +513 -0
  208. package/dist/localization/core-messages/en-US.js +513 -0
  209. package/dist/localization/core-messages/es-ES.d.ts +3 -0
  210. package/dist/localization/core-messages/es-ES.js +520 -0
  211. package/dist/localization/core-messages/fr-FR.d.ts +3 -0
  212. package/dist/localization/core-messages/fr-FR.js +520 -0
  213. package/dist/localization/core-messages/hi-IN.d.ts +3 -0
  214. package/dist/localization/core-messages/hi-IN.js +513 -0
  215. package/dist/localization/core-messages/ja-JP.d.ts +3 -0
  216. package/dist/localization/core-messages/ja-JP.js +506 -0
  217. package/dist/localization/core-messages/ko-KR.d.ts +3 -0
  218. package/dist/localization/core-messages/ko-KR.js +506 -0
  219. package/dist/localization/core-messages/pt-BR.d.ts +3 -0
  220. package/dist/localization/core-messages/pt-BR.js +520 -0
  221. package/dist/localization/core-messages/zh-CN.d.ts +3 -0
  222. package/dist/localization/core-messages/zh-CN.js +506 -0
  223. package/dist/localization/core-messages/zh-TW.d.ts +3 -0
  224. package/dist/localization/core-messages/zh-TW.js +506 -0
  225. package/dist/localization/core-messages.d.ts +524 -0
  226. package/dist/localization/core-messages.js +124 -0
  227. package/dist/localization/default-messages.d.ts +3 -0
  228. package/dist/localization/default-messages.js +3 -0
  229. package/dist/notifications/routes.d.ts +1 -1
  230. package/dist/observability/routes.d.ts +2 -2
  231. package/dist/progress/routes.d.ts +1 -1
  232. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  233. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  234. package/dist/resources/agents.js +40 -5
  235. package/dist/resources/metadata.d.ts +10 -0
  236. package/dist/resources/metadata.js +7 -3
  237. package/dist/review/actions/create-review-comment.js +1 -1
  238. package/dist/review/actions/reply-review-comment.js +1 -1
  239. package/dist/review/actions/resolve-review-thread.js +1 -1
  240. package/dist/secrets/routes.d.ts +6 -6
  241. package/dist/server/action-routes.js +32 -6
  242. package/dist/server/agent-chat/plugin-options.d.ts +7 -2
  243. package/dist/server/agent-chat-plugin.d.ts +10 -0
  244. package/dist/server/agent-chat-plugin.js +144 -41
  245. package/dist/server/agent-discovery.js +14 -3
  246. package/dist/server/agent-run-context.d.ts +7 -1
  247. package/dist/server/agent-run-context.js +9 -2
  248. package/dist/server/agent-teams-run-queue.d.ts +3 -0
  249. package/dist/server/agent-teams.js +38 -9
  250. package/dist/server/core-routes-plugin.js +1 -1
  251. package/dist/server/identity-sso-migrations.d.ts +7 -0
  252. package/dist/server/identity-sso-migrations.js +36 -0
  253. package/dist/server/identity-sso-store.d.ts +3 -2
  254. package/dist/server/identity-sso-store.js +8 -3
  255. package/dist/server/index.d.ts +1 -1
  256. package/dist/server/realtime-token.d.ts +1 -1
  257. package/dist/server/release-migrations.js +4 -0
  258. package/dist/server/request-context.d.ts +7 -0
  259. package/dist/server/request-context.js +15 -0
  260. package/dist/tracking/providers.js +13 -3
  261. package/dist/workspace-files/tool.js +1 -1
  262. package/docs/content/agent-surfaces.mdx +30 -0
  263. package/package.json +3 -2
@@ -1,8 +1,14 @@
1
1
  import { appBasePath } from "@agent-native/core/client/api-path";
2
2
  import { PromptComposer } from "@agent-native/core/client/composer";
3
3
  import { useT } from "@agent-native/core/client/i18n";
4
- import { IconCopy, IconSquarePlus } from "@tabler/icons-react";
5
- import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { IconCopy, IconSquarePlus, IconX } from "@tabler/icons-react";
5
+ import {
6
+ useCallback,
7
+ useEffect,
8
+ useLayoutEffect,
9
+ useRef,
10
+ useState,
11
+ } from "react";
6
12
  import { createPortal } from "react-dom";
7
13
  import { toast } from "sonner";
8
14
 
@@ -66,6 +72,8 @@ export function AddSlidePopover({
66
72
  agentSubmit,
67
73
  onDuplicateCurrent,
68
74
  onAddEmpty,
75
+ placement = "below",
76
+ targetSlideId,
69
77
  }: {
70
78
  open: boolean;
71
79
  onOpenChange: (open: boolean) => void;
@@ -78,11 +86,40 @@ export function AddSlidePopover({
78
86
  agentSubmit: (message: string, context: string) => void;
79
87
  onDuplicateCurrent?: () => void;
80
88
  onAddEmpty?: () => void;
89
+ /** "below" anchors under the trigger button; "right" sits beside a slide thumbnail. */
90
+ placement?: "below" | "right";
91
+ /** Id of a blank slide already inserted — the agent fills it in instead of
92
+ * inserting another one. Used when this popover follows a "New slide"
93
+ * click that already created the placeholder. */
94
+ targetSlideId?: string;
81
95
  }) {
82
96
  const t = useT();
83
97
  const panelRef = useRef<HTMLDivElement>(null);
84
98
  const [promptText, setPromptText] = useState("");
85
99
  const [googleDocContext, setGoogleDocContext] = useState("");
100
+ // Estimate before the panel has painted so the first frame doesn't hang
101
+ // off the bottom of the viewport; corrected once the real height is known.
102
+ const [panelHeight, setPanelHeight] = useState(320);
103
+
104
+ useLayoutEffect(() => {
105
+ if (!open || !panelRef.current) return;
106
+ setPanelHeight(panelRef.current.getBoundingClientRect().height);
107
+ });
108
+
109
+ // Content can grow after the first paint (Google Doc hint, file chips,
110
+ // an auto-growing textarea) without necessarily triggering a React
111
+ // re-render. Watch the panel directly so it keeps clamping to the
112
+ // viewport as it resizes, not just on the frame it first opens.
113
+ useEffect(() => {
114
+ if (!open || !panelRef.current) return;
115
+ const observer = new ResizeObserver(([entry]) => {
116
+ setPanelHeight(
117
+ entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height,
118
+ );
119
+ });
120
+ observer.observe(panelRef.current);
121
+ return () => observer.disconnect();
122
+ }, [open]);
86
123
 
87
124
  useEffect(() => {
88
125
  if (!open) return;
@@ -141,25 +178,39 @@ export function AddSlidePopover({
141
178
  const googleDocSourceForContext =
142
179
  truncateSourceForContext(googleDocContext);
143
180
  const fileContext = describeUploadedFilesForAgent(uploaded, deckId);
144
- const context = [
145
- `Add a new slide to deck "${deckTitle}" (id: ${deckId}).`,
146
- `Insert after slide ${activeSlideIndex + 1} of ${slideCount} (active slide id: ${activeSlideId}).`,
147
- "The visible user message above contains the user's request and/or pasted source material for the new slide(s). Treat pasted memo content as source material even if the user did not explicitly say they are pasting it.",
148
- googleDocSourceForContext.text,
149
- googleDocSourceForContext.truncated
150
- ? `The pasted source was longer than ${MAX_SOURCE_CONTEXT_CHARS} characters, so only the first ${MAX_SOURCE_CONTEXT_CHARS} characters were included to keep the agent request reliable.`
151
- : "",
152
- fileContext,
153
- "",
154
- "Create the slide content and insert it at the correct position using `add-slide` with --deckId=" +
155
- deckId +
156
- ".",
157
- "Every slide is rendered into a fixed native canvas (default 16:9 is 960x540 CSS pixels). Keep each slide within the density limits in AGENTS.md; split dense source material across more slides instead of packing it tightly.",
158
- "If the user asked for multiple slides, call `add-slide` once per slide. Use positions starting at " +
159
- (activeSlideIndex + 1) +
160
- " so the new slides land after the active slide in order.",
161
- "For larger requests, keep adding slides sequentially: wait for each add-slide result, then call add-slide for the next slide. Start slide 1 immediately; do not wait to design the entire sequence before adding it.",
162
- ].join("\n");
181
+ const context = targetSlideId
182
+ ? [
183
+ `Fill in slide ${activeSlideIndex + 1} of ${slideCount} (id: ${targetSlideId}) in deck "${deckTitle}" (id: ${deckId}).`,
184
+ "This slide already exists as a blank placeholder that the user just inserted — update it with `update-slide`, do not call `add-slide` for it.",
185
+ "The visible user message above contains the user's request and/or pasted source material for this slide. Treat pasted memo content as source material even if the user did not explicitly say they are pasting it.",
186
+ googleDocSourceForContext.text,
187
+ googleDocSourceForContext.truncated
188
+ ? `The pasted source was longer than ${MAX_SOURCE_CONTEXT_CHARS} characters, so only the first ${MAX_SOURCE_CONTEXT_CHARS} characters were included to keep the agent request reliable.`
189
+ : "",
190
+ fileContext,
191
+ "",
192
+ "Every slide is rendered into a fixed native canvas (default 16:9 is 960x540 CSS pixels). Keep the slide within the density limits in AGENTS.md; split dense source material across more slides instead of packing it tightly.",
193
+ "If the user asked for more than one slide's worth of content, update this slide with the first one, then call `add-slide` for the rest, positioned starting right after this slide.",
194
+ ].join("\n")
195
+ : [
196
+ `Add a new slide to deck "${deckTitle}" (id: ${deckId}).`,
197
+ `Insert after slide ${activeSlideIndex + 1} of ${slideCount} (active slide id: ${activeSlideId}).`,
198
+ "The visible user message above contains the user's request and/or pasted source material for the new slide(s). Treat pasted memo content as source material even if the user did not explicitly say they are pasting it.",
199
+ googleDocSourceForContext.text,
200
+ googleDocSourceForContext.truncated
201
+ ? `The pasted source was longer than ${MAX_SOURCE_CONTEXT_CHARS} characters, so only the first ${MAX_SOURCE_CONTEXT_CHARS} characters were included to keep the agent request reliable.`
202
+ : "",
203
+ fileContext,
204
+ "",
205
+ "Create the slide content and insert it at the correct position using `add-slide` with --deckId=" +
206
+ deckId +
207
+ ".",
208
+ "Every slide is rendered into a fixed native canvas (default 16:9 is 960x540 CSS pixels). Keep each slide within the density limits in AGENTS.md; split dense source material across more slides instead of packing it tightly.",
209
+ "If the user asked for multiple slides, call `add-slide` once per slide. Use positions starting at " +
210
+ (activeSlideIndex + 1) +
211
+ " so the new slides land after the active slide in order.",
212
+ "For larger requests, keep adding slides sequentially: wait for each add-slide result, then call add-slide for the next slide. Start slide 1 immediately; do not wait to design the entire sequence before adding it.",
213
+ ].join("\n");
163
214
 
164
215
  agentSubmit(addSlideAgentMessage(trimmedText), context);
165
216
  onOpenChange(false);
@@ -173,6 +224,7 @@ export function AddSlidePopover({
173
224
  googleDocContext,
174
225
  onOpenChange,
175
226
  slideCount,
227
+ targetSlideId,
176
228
  ],
177
229
  );
178
230
 
@@ -187,23 +239,40 @@ export function AddSlidePopover({
187
239
 
188
240
  const rect = anchorRef.current.getBoundingClientRect();
189
241
  const panelWidth = Math.min(420, window.innerWidth - 24);
190
- const left = Math.max(
191
- 12,
192
- Math.min(rect.left, window.innerWidth - panelWidth - 12),
193
- );
242
+ const left =
243
+ placement === "right"
244
+ ? Math.min(rect.right + 8, window.innerWidth - panelWidth - 12)
245
+ : Math.max(12, Math.min(rect.left, window.innerWidth - panelWidth - 12));
246
+ const top =
247
+ placement === "right"
248
+ ? Math.max(12, Math.min(rect.top, window.innerHeight - panelHeight - 12))
249
+ : rect.bottom + 8;
194
250
 
195
251
  return createPortal(
196
252
  <div
197
253
  ref={panelRef}
254
+ data-add-slide-popover
198
255
  className="fixed w-[min(420px,calc(100vw-24px))] rounded-xl border border-border bg-popover shadow-2xl shadow-black/60 z-[200] p-3"
199
256
  style={{
200
- top: rect.bottom + 8,
257
+ top,
201
258
  left,
202
259
  }}
203
260
  >
204
- <p className="px-1 pb-2 text-sm font-medium text-foreground/90">
205
- {t("editorSidebar.addSlides")}
206
- </p>
261
+ <div className="flex items-center justify-between px-1 pb-2">
262
+ <p className="text-sm font-medium text-foreground/90">
263
+ {targetSlideId
264
+ ? t("editorSidebar.describeThisSlide")
265
+ : t("editorSidebar.addSlides")}
266
+ </p>
267
+ <button
268
+ type="button"
269
+ onClick={() => onOpenChange(false)}
270
+ aria-label={t("editorSidebar.closeAddSlides")}
271
+ className="inline-flex size-5 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground/70"
272
+ >
273
+ <IconX className="size-3.5" />
274
+ </button>
275
+ </div>
207
276
  {(onAddEmpty || (onDuplicateCurrent && slideCount > 0)) && (
208
277
  <>
209
278
  {onAddEmpty && (
@@ -1,12 +1,11 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
2
  import {
3
- IconLoader2,
4
3
  IconPlus,
5
4
  IconTextSize,
6
5
  IconTransitionRight,
7
6
  } from "@tabler/icons-react";
8
- import { useEffect, useRef, useState } from "react";
9
7
 
8
+ import { Button } from "@/components/ui/button";
10
9
  import {
11
10
  DropdownMenu,
12
11
  DropdownMenuContent,
@@ -19,11 +18,8 @@ import {
19
18
  TooltipTrigger,
20
19
  } from "@/components/ui/tooltip";
21
20
  import type { Slide } from "@/context/DeckContext";
22
- import { useAgentGenerating } from "@/hooks/use-agent-generating";
23
21
  import { cn } from "@/lib/utils";
24
22
 
25
- import { AddSlidePopover } from "./AddSlidePopover";
26
-
27
23
  const BUTTON_CLASS =
28
24
  "inline-flex size-7 flex-shrink-0 items-center justify-center rounded-md transition-colors";
29
25
  const IDLE_CLASS =
@@ -41,121 +37,79 @@ const TRANSITIONS: { value: SlideTransition; labelKey: string }[] = [
41
37
  ];
42
38
 
43
39
  /**
44
- * Add slide, undo, redo, and add-text-box — the actions that stay put
45
- * regardless of what is selected. Rendered at the head of the contextual
46
- * toolbar, and as a fallback in the deck toolbar where that row is hidden.
40
+ * Selection-independent actions pinned to the head of the contextual
41
+ * toolbar: add-slide, add-text-box, and the slide transition picker.
42
+ * Rendered both as the `leading` slot of the element-controls row and as a
43
+ * fallback directly in the deck toolbar for when that row is hidden (narrow
44
+ * viewports) or never mounts (no current slide, e.g. an empty deck).
47
45
  */
48
46
  export function EditorActionCluster({
49
- deckId,
50
- deckTitle,
51
- currentSlideId,
52
- slideCount,
53
- currentSlideIndex,
54
- addSlideGenerating = false,
55
- onAddSlideGeneratingChange,
56
- onAddEmptySlide,
57
- onDuplicateCurrentSlide,
58
47
  textBoxMode,
59
48
  onToggleTextBoxMode,
49
+ onAddEmptySlide,
50
+ addSlideGenerating,
51
+ currentSlideId,
60
52
  slideTransition,
61
53
  onChangeSlideTransition,
62
54
  className,
63
55
  }: {
64
- deckId: string;
65
- deckTitle: string;
66
- currentSlideId?: string;
67
- slideCount: number;
68
- currentSlideIndex: number;
69
- addSlideGenerating?: boolean;
70
- onAddSlideGeneratingChange?: (generating: boolean) => void;
71
- onAddEmptySlide?: () => void;
72
- onDuplicateCurrentSlide?: () => void;
73
56
  textBoxMode?: boolean;
74
57
  onToggleTextBoxMode?: () => void;
58
+ onAddEmptySlide?: () => void;
59
+ addSlideGenerating?: boolean;
60
+ currentSlideId?: string;
75
61
  slideTransition?: Slide["transition"];
76
62
  onChangeSlideTransition?: (transition: SlideTransition) => void;
77
63
  className?: string;
78
64
  }) {
79
65
  const t = useT();
80
- const { generating, submit: agentSubmit } = useAgentGenerating();
81
- const [addSlideOpen, setAddSlideOpen] = useState(false);
82
- const addSlideRef = useRef<HTMLButtonElement>(null);
83
66
  // "none" is a legacy alias the presentation view already treats as instant.
84
67
  const activeTransition: SlideTransition =
85
68
  !slideTransition || slideTransition === "none"
86
69
  ? "instant"
87
70
  : slideTransition;
88
71
 
89
- useEffect(() => {
90
- if (!generating) onAddSlideGeneratingChange?.(false);
91
- }, [generating, onAddSlideGeneratingChange]);
72
+ if (!onToggleTextBoxMode && !onAddEmptySlide) return null;
92
73
 
93
74
  return (
94
75
  <div className={cn("flex items-center gap-1", className)}>
95
- <Tooltip>
96
- <TooltipTrigger asChild>
97
- <button
98
- ref={addSlideRef}
76
+ {onAddEmptySlide && (
77
+ <>
78
+ <Button
99
79
  type="button"
100
- onClick={() => setAddSlideOpen((open) => !open)}
80
+ variant="ghost"
81
+ size="sm"
82
+ className="h-7 shrink-0 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground"
83
+ onClick={onAddEmptySlide}
101
84
  disabled={addSlideGenerating}
102
- className={cn(
103
- BUTTON_CLASS,
104
- addSlideOpen ? ACTIVE_CLASS : IDLE_CLASS,
105
- )}
106
- aria-label={t("editorSidebar.addSlides")}
107
85
  >
108
- {addSlideGenerating ? (
109
- <IconLoader2 className="size-4 animate-spin" />
110
- ) : (
111
- <IconPlus className="size-4" />
112
- )}
113
- </button>
114
- </TooltipTrigger>
115
- <TooltipContent>{t("editorSidebar.addSlides")}</TooltipContent>
116
- </Tooltip>
117
- <AddSlidePopover
118
- open={addSlideOpen}
119
- onOpenChange={setAddSlideOpen}
120
- anchorRef={addSlideRef}
121
- deckId={deckId}
122
- deckTitle={deckTitle}
123
- activeSlideId={currentSlideId ?? ""}
124
- slideCount={slideCount}
125
- activeSlideIndex={currentSlideIndex}
126
- agentSubmit={(message, context) => {
127
- onAddSlideGeneratingChange?.(true);
128
- agentSubmit(message, context);
129
- }}
130
- onDuplicateCurrent={onDuplicateCurrentSlide}
131
- onAddEmpty={onAddEmptySlide}
132
- />
133
-
134
- {onToggleTextBoxMode && (
135
- <>
136
- <div className={DIVIDER_CLASS} />
137
- <Tooltip>
138
- <TooltipTrigger asChild>
139
- <button
140
- type="button"
141
- onClick={onToggleTextBoxMode}
142
- data-toolbar-textbox-button
143
- aria-label={t("editorToolbar.addTextBox")}
144
- aria-pressed={textBoxMode}
145
- aria-keyshortcuts="T"
146
- className={cn(
147
- BUTTON_CLASS,
148
- textBoxMode ? ACTIVE_CLASS : IDLE_CLASS,
149
- )}
150
- >
151
- <IconTextSize className="size-4" />
152
- </button>
153
- </TooltipTrigger>
154
- <TooltipContent>{t("editorToolbar.addTextBox")} (T)</TooltipContent>
155
- </Tooltip>
86
+ <IconPlus className="size-3.5" />
87
+ {t("editorSidebar.newSlide")}
88
+ </Button>
89
+ {onToggleTextBoxMode && <div className={DIVIDER_CLASS} />}
156
90
  </>
157
91
  )}
158
-
92
+ {onToggleTextBoxMode && (
93
+ <Tooltip>
94
+ <TooltipTrigger asChild>
95
+ <button
96
+ type="button"
97
+ onClick={onToggleTextBoxMode}
98
+ data-toolbar-textbox-button
99
+ aria-label={t("editorToolbar.addTextBox")}
100
+ aria-pressed={textBoxMode}
101
+ aria-keyshortcuts="T"
102
+ className={cn(
103
+ BUTTON_CLASS,
104
+ textBoxMode ? ACTIVE_CLASS : IDLE_CLASS,
105
+ )}
106
+ >
107
+ <IconTextSize className="size-4" />
108
+ </button>
109
+ </TooltipTrigger>
110
+ <TooltipContent>{t("editorToolbar.addTextBox")} (T)</TooltipContent>
111
+ </Tooltip>
112
+ )}
159
113
  {onChangeSlideTransition && currentSlideId && (
160
114
  <>
161
115
  <div className={DIVIDER_CLASS} />
@@ -18,9 +18,11 @@ import { CSS } from "@dnd-kit/utilities";
18
18
  import { appStateKeyForBrowserTab } from "@shared/app-state-tabs";
19
19
  import { hashSlideContent, type DeckFitState } from "@shared/slide-fit";
20
20
  import { useCallback, useEffect, useRef, useState } from "react";
21
+ import { toast } from "sonner";
21
22
 
22
23
  import SlideRenderer from "@/components/deck/SlideRenderer";
23
24
  import type { SlideOverflowInfo } from "@/components/deck/SlideRenderer";
25
+ import { AddSlidePopover } from "@/components/editor/AddSlidePopover";
24
26
  import { AiEditingMarker } from "@/components/editor/AiEditingMarker";
25
27
  import GeneratingSlidePreview from "@/components/editor/GeneratingSlidePreview";
26
28
  import {
@@ -28,7 +30,7 @@ import {
28
30
  TooltipContent,
29
31
  TooltipTrigger,
30
32
  } from "@/components/ui/tooltip";
31
- import type { Slide } from "@/context/DeckContext";
33
+ import { defaultSlideContent, type Slide } from "@/context/DeckContext";
32
34
  import { getAspectRatioDims, type AspectRatio } from "@/lib/aspect-ratios";
33
35
  import { TAB_ID } from "@/lib/tab-id";
34
36
 
@@ -39,6 +41,7 @@ interface EditorSidebarProps {
39
41
  slides: Slide[];
40
42
  activeSlideId: string;
41
43
  deckId: string;
44
+ deckTitle: string;
42
45
  onSelectSlide: (id: string) => void;
43
46
  /** Viewer-role decks get thumbnails only: no add, duplicate, or delete. */
44
47
  readOnly?: boolean;
@@ -54,6 +57,27 @@ interface EditorSidebarProps {
54
57
  generatingSlide?: { index: number };
55
58
  generatingSlideSelected?: boolean;
56
59
  onSelectGeneratingSlide?: () => void;
60
+ /** The slide just inserted via the toolbar's New Slide button — the rail
61
+ * anchors the "describe this slide" popover to that slide's thumbnail
62
+ * once it mounts. Owned by the parent since the button that sets it now
63
+ * lives in the toolbar, outside this component. */
64
+ describeSlideId: string | null;
65
+ /** Clears `describeSlideId` in the parent when the popover closes. */
66
+ onCloseDescribe: () => void;
67
+ /** Reports add-slide generation state up so the toolbar's New Slide button
68
+ * can disable itself while a request is in flight. */
69
+ onAddSlideGeneratingChange?: (generating: boolean) => void;
70
+ /** Resolves once a just-inserted blank slide has actually reached the
71
+ * server, so the agent's update-slide request can't race the add-slide
72
+ * persistence. */
73
+ onAwaitAddSlidePersisted?: () => Promise<void>;
74
+ /** Removes a blank placeholder slide whose persistence ultimately failed,
75
+ * so a flaky save doesn't leave a stray empty slide in the deck. */
76
+ onRemoveFailedSlide?: (slideId: string) => void;
77
+ /** Submits the add-slide agent request. Owned by the parent (rather than
78
+ * this component's own useAgentGenerating() call) so the run stays
79
+ * correctly scoped and trackable across a sidebar remount. */
80
+ addSlideAgentSubmit: (message: string, context: string) => void;
57
81
  }
58
82
 
59
83
  const DECK_FIT_STATE_KEYS = [
@@ -312,6 +336,7 @@ export default function EditorSidebar({
312
336
  slides,
313
337
  activeSlideId,
314
338
  deckId,
339
+ deckTitle,
315
340
  onSelectSlide,
316
341
  readOnly = false,
317
342
  slidePresence,
@@ -321,7 +346,16 @@ export default function EditorSidebar({
321
346
  generatingSlide,
322
347
  generatingSlideSelected = false,
323
348
  onSelectGeneratingSlide,
349
+ describeSlideId,
350
+ onCloseDescribe,
351
+ onAddSlideGeneratingChange,
352
+ onAwaitAddSlidePersisted,
353
+ onRemoveFailedSlide,
354
+ addSlideAgentSubmit,
324
355
  }: EditorSidebarProps) {
356
+ const t = useT();
357
+ const [describeAnchorEl, setDescribeAnchorEl] =
358
+ useState<HTMLButtonElement | null>(null);
325
359
  const [thumbnailListScrolled, setThumbnailListScrolled] = useState(false);
326
360
  const slideButtonRefs = useRef(new Map<string, HTMLButtonElement>());
327
361
  const measurementsRef = useRef(
@@ -331,6 +365,7 @@ export default function EditorSidebar({
331
365
  >(),
332
366
  );
333
367
  const writeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
368
+
334
369
  const aiEditedSlideIds = new Set(
335
370
  (recentEdits ?? [])
336
371
  .filter((edit) => edit.isAgent)
@@ -398,6 +433,7 @@ export default function EditorSidebar({
398
433
  }, [deckId, aspectRatio]);
399
434
 
400
435
  useEffect(() => {
436
+ setDescribeAnchorEl(null);
401
437
  setThumbnailListScrolled(false);
402
438
  }, [deckId]);
403
439
 
@@ -421,10 +457,22 @@ export default function EditorSidebar({
421
457
  } else {
422
458
  slideButtonRefs.current.delete(slideId);
423
459
  }
460
+ // The new-slide prompt anchors to the just-created slide's thumbnail,
461
+ // which doesn't exist yet at click time — pick it up as soon as it mounts.
462
+ // Center it in the scroll area first so the prompt has room on-screen
463
+ // instead of opening off the bottom edge when the new slide lands there.
464
+ if (node && slideId === describeSlideId) {
465
+ node.scrollIntoView({ block: "center" });
466
+ setDescribeAnchorEl(node);
467
+ }
424
468
  },
425
- [],
469
+ [describeSlideId],
426
470
  );
427
471
 
472
+ const describeSlideIndex = describeSlideId
473
+ ? slides.findIndex((s) => s.id === describeSlideId)
474
+ : -1;
475
+
428
476
  // Arrow key navigation for slides
429
477
  useEffect(() => {
430
478
  const handleKeyDown = (e: KeyboardEvent) => {
@@ -518,6 +566,50 @@ export default function EditorSidebar({
518
566
  )}
519
567
  </div>
520
568
  </div>
569
+ {describeSlideId && describeSlideIndex !== -1 && describeAnchorEl && (
570
+ <AddSlidePopover
571
+ open
572
+ onOpenChange={(open) => {
573
+ if (!open) {
574
+ onCloseDescribe();
575
+ setDescribeAnchorEl(null);
576
+ }
577
+ }}
578
+ anchorRef={{ current: describeAnchorEl }}
579
+ placement="right"
580
+ deckId={deckId}
581
+ deckTitle={deckTitle}
582
+ activeSlideId={describeSlideId}
583
+ activeSlideIndex={describeSlideIndex}
584
+ slideCount={slides.length}
585
+ targetSlideId={describeSlideId}
586
+ agentSubmit={async (message, context) => {
587
+ onAddSlideGeneratingChange?.(true);
588
+ try {
589
+ await onAwaitAddSlidePersisted?.();
590
+ } catch (error) {
591
+ console.error("Failed to persist new slide:", error);
592
+ onAddSlideGeneratingChange?.(false);
593
+ // The popover already closed (AddSlidePopover doesn't wait on
594
+ // this async callback), so the typed prompt is gone either
595
+ // way. Only remove the placeholder if it's still untouched —
596
+ // the save retries take long enough that the user could have
597
+ // started editing it directly on the canvas in the meantime,
598
+ // and deleting it would destroy that work.
599
+ const current = slides.find((s) => s.id === describeSlideId);
600
+ if (
601
+ current?.content === defaultSlideContent.blank &&
602
+ !current.notes
603
+ ) {
604
+ onRemoveFailedSlide?.(describeSlideId);
605
+ }
606
+ toast.error(t("editorSidebar.newSlideSaveFailed"));
607
+ return;
608
+ }
609
+ addSlideAgentSubmit(message, context);
610
+ }}
611
+ />
612
+ )}
521
613
  </div>
522
614
  );
523
615
  }
@@ -76,7 +76,6 @@ interface EditorToolbarProps {
76
76
  /** Whether the user may create and manage comments without editing slides. */
77
77
  canComment?: boolean;
78
78
  onTitleChange: (title: string) => void;
79
- slideCount: number;
80
79
  currentSlideIndex: number;
81
80
  sidebarOpen: boolean;
82
81
  onToggleSidebar: () => void;
@@ -132,16 +131,13 @@ interface EditorToolbarProps {
132
131
  onExportPptx?: () => Promise<void> | void;
133
132
  /** Create the deck in the user's Google Drive as native Google Slides */
134
133
  onExportGoogleSlides?: () => Promise<GoogleSlidesExportResult>;
135
- /** Insert a blank slide after the current one */
134
+ /** Inserts a blank slide directly below the active slide. Threaded through
135
+ * to the fallback action cluster below so an empty deck (no current
136
+ * slide, so the primary element-controls toolbar never mounts) still has
137
+ * a way to add its first slide. */
136
138
  onAddEmptySlide?: () => void;
137
- /** Duplicate the current slide */
138
- onDuplicateCurrentSlide?: () => void;
139
- /** Id of the current slide, so an agent add-slide lands in the right place */
140
- currentSlideId?: string;
141
- /** True while an agent add-slide request is in flight */
139
+ /** True while an agent add-slide request is in flight. */
142
140
  addSlideGenerating?: boolean;
143
- /** Called when an agent add-slide request is submitted */
144
- onAddSlideGeneratingChange?: (generating: boolean) => void;
145
141
  }
146
142
 
147
143
  const TOOLBAR_ICON_BUTTON_CLASS =
@@ -152,7 +148,6 @@ export default function EditorToolbar({
152
148
  deckId,
153
149
  deckTitle,
154
150
  onTitleChange,
155
- slideCount,
156
151
  currentSlideIndex,
157
152
  sidebarOpen,
158
153
  onToggleSidebar,
@@ -185,10 +180,7 @@ export default function EditorToolbar({
185
180
  onExportPptx,
186
181
  onExportGoogleSlides,
187
182
  onAddEmptySlide,
188
- onDuplicateCurrentSlide,
189
- currentSlideId,
190
- addSlideGenerating = false,
191
- onAddSlideGeneratingChange,
183
+ addSlideGenerating,
192
184
  canEdit = true,
193
185
  canComment = canEdit,
194
186
  }: EditorToolbarProps) {
@@ -523,7 +515,7 @@ export default function EditorToolbar({
523
515
  useEffect(() => registerEditorCommands(() => editorCommandsRef.current), []);
524
516
 
525
517
  return (
526
- <div className="deck-editor-toolbar flex h-11 shrink-0 items-center gap-1 overflow-x-auto whitespace-nowrap bg-background px-2 sm:px-3">
518
+ <div className="deck-editor-toolbar flex h-14 shrink-0 items-center gap-1 overflow-x-auto whitespace-nowrap bg-background px-2 sm:px-3">
527
519
  {/* Back button */}
528
520
  <Tooltip>
529
521
  <TooltipTrigger asChild>
@@ -554,23 +546,19 @@ export default function EditorToolbar({
554
546
  <TooltipContent>{t("editorToolbar.toggleSlideList")}</TooltipContent>
555
547
  </Tooltip>
556
548
 
557
- {/* Add slide and the text-box tool live at the head of the contextual
558
- * toolbar below. That row is desktop-only and needs a slide to mount on,
559
- * so keep a fallback here for narrow screens and empty decks. */}
560
- {canEdit && (
549
+ {/* New Slide and the text-box tool live at the head of the contextual
550
+ * toolbar below, which SlideEditor portals in at every viewport size
551
+ * (a wide inline row or a narrow standalone row) whenever there's a
552
+ * current slide. Render this fallback only when there isn't one — an
553
+ * empty deck — so those two rows never end up showing the same
554
+ * buttons twice. */}
555
+ {canEdit && !contextToolbarVisible && (
561
556
  <EditorActionCluster
562
- className={contextToolbarVisible ? "lg:hidden" : undefined}
563
- deckId={deckId}
564
- deckTitle={deckTitle}
565
- currentSlideId={currentSlideId}
566
- slideCount={slideCount}
567
- currentSlideIndex={currentSlideIndex}
568
- addSlideGenerating={addSlideGenerating}
569
- onAddSlideGeneratingChange={onAddSlideGeneratingChange}
570
- onAddEmptySlide={onAddEmptySlide}
571
- onDuplicateCurrentSlide={onDuplicateCurrentSlide}
572
557
  textBoxMode={textBoxMode}
573
558
  onToggleTextBoxMode={onToggleTextBoxMode}
559
+ onAddEmptySlide={onAddEmptySlide}
560
+ addSlideGenerating={addSlideGenerating}
561
+ currentSlideId={currentSlide?.id}
574
562
  slideTransition={currentSlide?.transition}
575
563
  onChangeSlideTransition={onChangeSlideTransition}
576
564
  />
@@ -926,7 +926,7 @@ export function SlideContextToolbar({
926
926
  )}
927
927
  {zoomControls && (
928
928
  <>
929
- <div className={TOOLBAR_DIVIDER} />
929
+ <div className={cn(TOOLBAR_DIVIDER, "ml-auto")} />
930
930
  <Tooltip>
931
931
  <TooltipTrigger asChild>
932
932
  <Button