@agent-native/core 0.130.1 → 0.131.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 (909) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +57 -0
  3. package/corpus/core/docs/content/agent-surfaces.mdx +4 -4
  4. package/corpus/core/docs/content/automations.mdx +61 -12
  5. package/corpus/core/docs/content/deployment.mdx +11 -4
  6. package/corpus/core/docs/content/extensions.mdx +39 -33
  7. package/corpus/core/docs/content/recurring-jobs.mdx +41 -19
  8. package/corpus/core/docs/content/template-dispatch-operations.mdx +19 -7
  9. package/corpus/core/package.json +3 -1
  10. package/corpus/core/src/action.ts +55 -0
  11. package/corpus/core/src/agent/engine/builder-engine.ts +8 -4
  12. package/corpus/core/src/agent/production-agent.ts +175 -100
  13. package/corpus/core/src/agent/tool-search.ts +36 -0
  14. package/corpus/core/src/automations/service.ts +506 -0
  15. package/corpus/core/src/browser-context/index.ts +392 -0
  16. package/corpus/core/src/client/AgentPanel.tsx +2 -1
  17. package/corpus/core/src/client/FeedbackButton.tsx +37 -4
  18. package/corpus/core/src/client/MultiTabAssistantChat.tsx +21 -0
  19. package/corpus/core/src/client/agent-chat.ts +31 -3
  20. package/corpus/core/src/client/agent-page/AgentJobsTab.tsx +438 -310
  21. package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +4 -4
  22. package/corpus/core/src/client/agent-page/use-jobs.ts +7 -0
  23. package/corpus/core/src/client/analytics-session.ts +13 -0
  24. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +67 -0
  25. package/corpus/core/src/client/settings/AutomationsSection.tsx +27 -499
  26. package/corpus/core/src/client/settings/SettingsPanel.tsx +32 -11
  27. package/corpus/core/src/client/settings/index.ts +2 -0
  28. package/corpus/core/src/client/use-agent-chat.ts +35 -3
  29. package/corpus/core/src/client/use-chat-threads.ts +8 -25
  30. package/corpus/core/src/deploy/build.ts +2 -0
  31. package/corpus/core/src/deploy/workspace-deploy.ts +2 -0
  32. package/corpus/core/src/extensions/actions.ts +115 -3
  33. package/corpus/core/src/extensions/fetch-tool.ts +11 -0
  34. package/corpus/core/src/extensions/routes.ts +16 -1
  35. package/corpus/core/src/extensions/web-content.ts +96 -1
  36. package/corpus/core/src/integrations/computer-supervision.ts +33 -1
  37. package/corpus/core/src/integrations/index.ts +6 -0
  38. package/corpus/core/src/integrations/integration-durable-dispatch-config.ts +10 -0
  39. package/corpus/core/src/integrations/integration-durable-dispatch.ts +4 -0
  40. package/corpus/core/src/integrations/plugin.ts +94 -41
  41. package/corpus/core/src/integrations/remote-browser-actions.ts +621 -0
  42. package/corpus/core/src/jobs/actions/list-recurring-jobs.ts +5 -7
  43. package/corpus/core/src/jobs/actions/manage-recurring-job.ts +3 -7
  44. package/corpus/core/src/jobs/background-automation-runner.ts +437 -0
  45. package/corpus/core/src/jobs/frontmatter.ts +308 -0
  46. package/corpus/core/src/jobs/scheduler.ts +89 -598
  47. package/corpus/core/src/jobs/tools.ts +22 -3
  48. package/corpus/core/src/localization/default-messages.ts +45 -45
  49. package/corpus/core/src/mcp-client/index.ts +6 -0
  50. package/corpus/core/src/notifications/actions.ts +5 -0
  51. package/corpus/core/src/progress/actions.ts +5 -0
  52. package/corpus/core/src/provider-api/actions/github-repo-files.ts +11 -0
  53. package/corpus/core/src/provider-api/actions/provider-api.ts +13 -0
  54. package/corpus/core/src/server/action-discovery.ts +7 -0
  55. package/corpus/core/src/server/agent-chat/browser-team-tools.ts +126 -3
  56. package/corpus/core/src/server/agent-chat/framework-prompts.ts +7 -11
  57. package/corpus/core/src/server/agent-chat/plugin-options.ts +3 -3
  58. package/corpus/core/src/server/agent-chat/script-entries.ts +52 -1
  59. package/corpus/core/src/server/agent-chat-plugin.ts +25 -5
  60. package/corpus/core/src/server/attribution.ts +22 -0
  61. package/corpus/core/src/server/auth.ts +20 -6
  62. package/corpus/core/src/server/better-auth-instance.ts +10 -2
  63. package/corpus/core/src/server/core-routes-plugin.ts +10 -1
  64. package/corpus/core/src/server/csrf.ts +25 -0
  65. package/corpus/core/src/server/email-template.ts +43 -13
  66. package/corpus/core/src/server/google-oauth.ts +21 -1
  67. package/corpus/core/src/server/onboarding-html.ts +18 -0
  68. package/corpus/core/src/server/prompts/framework-core-compact.ts +3 -3
  69. package/corpus/core/src/server/prompts/framework-core.ts +3 -3
  70. package/corpus/core/src/server/prompts/shared-rules.ts +3 -3
  71. package/corpus/core/src/settings/index.ts +1 -0
  72. package/corpus/core/src/shared/analytics-anonymous-id.ts +35 -0
  73. package/corpus/core/src/sharing/actions/share-resource.ts +26 -4
  74. package/corpus/core/src/sharing/index.ts +1 -0
  75. package/corpus/core/src/sharing/registry.ts +23 -0
  76. package/corpus/core/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  77. package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +26 -0
  78. package/corpus/core/src/templates/default/app/i18n/de-DE.ts +28 -0
  79. package/corpus/core/src/templates/default/app/i18n/en-US.ts +27 -0
  80. package/corpus/core/src/templates/default/app/i18n/es-ES.ts +28 -0
  81. package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +28 -0
  82. package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +25 -0
  83. package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +27 -0
  84. package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +27 -0
  85. package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +27 -0
  86. package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +24 -0
  87. package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +24 -0
  88. package/corpus/core/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  89. package/corpus/core/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  90. package/corpus/core/src/templates/workspace-core/.agents/skills/agent-page/SKILL.md +10 -7
  91. package/corpus/core/src/templates/workspace-core/.agents/skills/automations/SKILL.md +53 -8
  92. package/corpus/core/src/templates/workspace-core/.agents/skills/browser-sessions/SKILL.md +131 -0
  93. package/corpus/core/src/templates/workspace-core/.agents/skills/extension-points/SKILL.md +13 -4
  94. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +23 -15
  95. package/corpus/core/src/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +33 -18
  96. package/corpus/core/src/tracking/providers.ts +1 -0
  97. package/corpus/core/src/tracking/registry.ts +2 -1
  98. package/corpus/core/src/tracking/types.ts +1 -0
  99. package/corpus/core/src/triggers/actions/list-automations.ts +43 -44
  100. package/corpus/core/src/triggers/actions/manage-automation.ts +17 -48
  101. package/corpus/core/src/triggers/actions.ts +218 -127
  102. package/corpus/core/src/triggers/dispatcher.ts +181 -367
  103. package/corpus/core/src/triggers/routes.ts +84 -27
  104. package/corpus/core/src/triggers/types.ts +9 -23
  105. package/corpus/templates/analytics/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  106. package/corpus/templates/analytics/actions/install-dashboard-template.ts +13 -4
  107. package/corpus/templates/analytics/actions/rename-dashboard.ts +8 -3
  108. package/corpus/templates/analytics/actions/restore-dashboard-revision.ts +8 -3
  109. package/corpus/templates/analytics/actions/update-dashboard.ts +22 -5
  110. package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +7 -1
  111. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +1 -82
  112. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +4 -4
  113. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +23 -13
  114. package/corpus/templates/analytics/app/global.css +21 -0
  115. package/corpus/templates/analytics/app/i18n-data.ts +165 -101
  116. package/corpus/templates/analytics/app/lib/custom-block-promotion.ts +62 -0
  117. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +17 -2
  118. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +3 -1
  119. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +77 -12
  120. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-save-queue.ts +34 -0
  121. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +42 -26
  122. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +15 -0
  123. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +1 -1
  124. package/corpus/templates/analytics/changelog/2026-07-29-chat-history-now-closes-smoothly-and-stays-ready-when-you-re.md +6 -0
  125. package/corpus/templates/analytics/changelog/2026-07-29-chat-history-stays-visible-when-you-return-to-the-dashboard.md +6 -0
  126. package/corpus/templates/analytics/changelog/2026-07-29-custom-dashboard-blocks-now-identify-one-off-agent-patches-a.md +6 -0
  127. package/corpus/templates/analytics/changelog/2026-07-29-dashboard-chart-deletions-now-save-in-order-so-removed-rows-.md +6 -0
  128. package/corpus/templates/analytics/changelog/2026-07-29-dashboard-chart-loaders-are-easier-to-see-and-animate-more-s.md +6 -0
  129. package/corpus/templates/analytics/changelog/2026-07-29-plan-mode-can-now-query-connected-analytics-sources-while-ke.md +6 -0
  130. package/corpus/templates/analytics/changelog/2026-07-29-plan-mode-can-use-clips-as-read-only-prompt-context.md +6 -0
  131. package/corpus/templates/analytics/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  132. package/corpus/templates/analytics/changelog/2026-07-29-signup-entry-pages-now-show-the-first-page-viewed-before-eac.md +6 -0
  133. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +26 -1
  134. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +0 -33
  135. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +13 -3
  136. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +9 -1
  137. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +44 -1
  138. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +96 -16
  139. package/corpus/templates/analytics/server/plugins/agent-chat.ts +18 -27
  140. package/corpus/templates/analytics/server/plugins/core-routes.ts +1 -0
  141. package/corpus/templates/assets/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  142. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -8
  143. package/corpus/templates/assets/changelog/2026-07-29-chat-history-now-closes-smoothly-and-stays-ready-when-you-re.md +6 -0
  144. package/corpus/templates/assets/changelog/2026-07-29-chat-history-stays-visible-when-you-return-to-the-workspace.md +6 -0
  145. package/corpus/templates/assets/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  146. package/corpus/templates/brain/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  147. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +3 -7
  148. package/corpus/templates/brain/app/root.tsx +0 -3
  149. package/corpus/templates/brain/changelog/2026-07-29-chat-history-now-closes-smoothly-and-stays-ready-when-you-re.md +6 -0
  150. package/corpus/templates/brain/changelog/2026-07-29-chat-history-stays-visible-when-you-return-to-ask-brain.md +6 -0
  151. package/corpus/templates/brain/changelog/2026-07-29-extensions-are-no-longer-shown-as-a-default-command-menu-des.md +6 -0
  152. package/corpus/templates/brain/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  153. package/corpus/templates/calendar/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  154. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +25 -5
  155. package/corpus/templates/calendar/AGENTS.md +5 -0
  156. package/corpus/templates/calendar/actions/create-event.ts +43 -13
  157. package/corpus/templates/calendar/actions/event-action-helpers.ts +131 -1
  158. package/corpus/templates/calendar/actions/manage-event-draft.ts +65 -2
  159. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +437 -258
  160. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +19 -3
  161. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +24 -0
  162. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +15 -2
  163. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +1 -5
  164. package/corpus/templates/calendar/app/hooks/use-events.ts +44 -5
  165. package/corpus/templates/calendar/app/i18n-data.ts +50 -0
  166. package/corpus/templates/calendar/app/lib/all-day-layout.ts +9 -2
  167. package/corpus/templates/calendar/app/lib/event-form-initialization.ts +4 -8
  168. package/corpus/templates/calendar/app/lib/event-form-utils.ts +30 -8
  169. package/corpus/templates/calendar/app/lib/location-suggestions.ts +36 -0
  170. package/corpus/templates/calendar/app/lib/out-of-office.ts +87 -1
  171. package/corpus/templates/calendar/app/pages/CalendarView.tsx +68 -13
  172. package/corpus/templates/calendar/changelog/2026-07-26-out-of-office-events-now-default-to-full-days-with-a-ready.md +6 -0
  173. package/corpus/templates/calendar/changelog/2026-07-29-recent-calendar-locations-now-appear-as-address-suggestions-.md +6 -0
  174. package/corpus/templates/calendar/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  175. package/corpus/templates/calendar/server/lib/find-time.ts +29 -5
  176. package/corpus/templates/calendar/shared/api.ts +2 -0
  177. package/corpus/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  178. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +1 -5
  179. package/corpus/templates/chat/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  180. package/corpus/templates/clips/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  181. package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +3 -1
  182. package/corpus/templates/clips/app/components/library/library-layout.tsx +2 -6
  183. package/corpus/templates/clips/app/components/meetings/day-header.tsx +1 -1
  184. package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +1 -22
  185. package/corpus/templates/clips/app/components/workspace/branding-editor.tsx +26 -1
  186. package/corpus/templates/clips/app/components/workspace/organization-identity-card.tsx +107 -0
  187. package/corpus/templates/clips/app/i18n/en-US.ts +3 -4
  188. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +0 -10
  189. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +23 -51
  190. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +1 -8
  191. package/corpus/templates/clips/changelog/2026-07-29-clearing-all-recording-edits-no-longer-leaves-the-editor-unr.md +6 -0
  192. package/corpus/templates/clips/changelog/2026-07-29-clip-share-emails-now-open-with-who-shared-the-clip-add-a-su.md +6 -0
  193. package/corpus/templates/clips/changelog/2026-07-29-finished-meeting-pills-now-have-a-clear-close-button.md +6 -0
  194. package/corpus/templates/clips/changelog/2026-07-29-fixed-system-audio-recordings-picking-up-your-microphone-in-.md +6 -0
  195. package/corpus/templates/clips/changelog/2026-07-29-meeting-cards-no-longer-flicker-or-disappear-when-hovered-in.md +6 -0
  196. package/corpus/templates/clips/changelog/2026-07-29-organization-name-logo-brand-color-and-default-visibility-no.md +6 -0
  197. package/corpus/templates/clips/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  198. package/corpus/templates/clips/changelog/2026-07-29-the-desktop-camera-preview-now-closes-after-a-recording-fin.md +6 -0
  199. package/corpus/templates/clips/changelog/2026-07-29-transcript-lines-on-the-meeting-page-no-longer-jump-to-the-r.md +6 -0
  200. package/corpus/templates/clips/changelog/2026-07-29-when-a-meeting-wraps-up-the-recording-pill-now-shows-a-green.md +6 -0
  201. package/corpus/templates/clips/desktop/src/app.tsx +14 -15
  202. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +6 -3
  203. package/corpus/templates/clips/desktop/src/lib/bubble-session.ts +11 -0
  204. package/corpus/templates/clips/desktop/src/lib/recorder.ts +9 -4
  205. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +11 -0
  206. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +112 -32
  207. package/corpus/templates/clips/desktop/src/styles.css +42 -2
  208. package/corpus/templates/clips/server/db/index.ts +6 -0
  209. package/corpus/templates/clips/server/jobs/transactional-emails.ts +47 -23
  210. package/corpus/templates/clips/server/lib/share-email-hero.ts +33 -0
  211. package/corpus/templates/clips/server/lib/transactional-email-store.ts +16 -6
  212. package/corpus/templates/clips/shared/clips-ai-prefs.ts +0 -1
  213. package/corpus/templates/content/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  214. package/corpus/templates/content/.agents/skills/content-product-development/SKILL.md +103 -0
  215. package/corpus/templates/content/.agents/skills/content-product-development/references/verification.md +57 -0
  216. package/corpus/templates/content/AGENTS.md +2 -0
  217. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +1 -6
  218. package/corpus/templates/content/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  219. package/corpus/templates/content/docs/product/README.md +115 -0
  220. package/corpus/templates/content/docs/product/architecture.md +98 -0
  221. package/corpus/templates/content/docs/product/capabilities/content.access.page-database.md +76 -0
  222. package/corpus/templates/content/docs/product/capabilities/content.access.row-private.md +75 -0
  223. package/corpus/templates/content/docs/product/capabilities/content.access.safe-aggregate.md +75 -0
  224. package/corpus/templates/content/docs/product/capabilities/content.access.visibility-closure.md +79 -0
  225. package/corpus/templates/content/docs/product/capabilities/content.action.button.md +67 -0
  226. package/corpus/templates/content/docs/product/capabilities/content.agent.action-parity.md +73 -0
  227. package/corpus/templates/content/docs/product/capabilities/content.agent.audience-safe.md +91 -0
  228. package/corpus/templates/content/docs/product/capabilities/content.agent.automation.md +67 -0
  229. package/corpus/templates/content/docs/product/capabilities/content.agent.expression-authoring.md +71 -0
  230. package/corpus/templates/content/docs/product/capabilities/content.agent.presence.md +103 -0
  231. package/corpus/templates/content/docs/product/capabilities/content.agent.resource-consent.md +90 -0
  232. package/corpus/templates/content/docs/product/capabilities/content.agent.skill-catalog.md +67 -0
  233. package/corpus/templates/content/docs/product/capabilities/content.api.cms.md +70 -0
  234. package/corpus/templates/content/docs/product/capabilities/content.author.code.md +71 -0
  235. package/corpus/templates/content/docs/product/capabilities/content.author.document-editor.md +71 -0
  236. package/corpus/templates/content/docs/product/capabilities/content.author.footnotes.md +71 -0
  237. package/corpus/templates/content/docs/product/capabilities/content.author.math.md +71 -0
  238. package/corpus/templates/content/docs/product/capabilities/content.author.media.md +71 -0
  239. package/corpus/templates/content/docs/product/capabilities/content.author.mermaid.md +71 -0
  240. package/corpus/templates/content/docs/product/capabilities/content.automation.scheduled.md +67 -0
  241. package/corpus/templates/content/docs/product/capabilities/content.capture.enrich.md +75 -0
  242. package/corpus/templates/content/docs/product/capabilities/content.command.fabric.md +67 -0
  243. package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +75 -0
  244. package/corpus/templates/content/docs/product/capabilities/content.diff.ai-assist.md +75 -0
  245. package/corpus/templates/content/docs/product/capabilities/content.diff.filtered-review.md +79 -0
  246. package/corpus/templates/content/docs/product/capabilities/content.diff.in-place.md +80 -0
  247. package/corpus/templates/content/docs/product/capabilities/content.discussion.page.md +80 -0
  248. package/corpus/templates/content/docs/product/capabilities/content.embed.host-grant.md +70 -0
  249. package/corpus/templates/content/docs/product/capabilities/content.embed.mcp-app.md +69 -0
  250. package/corpus/templates/content/docs/product/capabilities/content.embed.surface.md +74 -0
  251. package/corpus/templates/content/docs/product/capabilities/content.event.committed.md +85 -0
  252. package/corpus/templates/content/docs/product/capabilities/content.expression.cached-result.md +71 -0
  253. package/corpus/templates/content/docs/product/capabilities/content.expression.catalog.md +71 -0
  254. package/corpus/templates/content/docs/product/capabilities/content.expression.language.md +76 -0
  255. package/corpus/templates/content/docs/product/capabilities/content.feedback.signal.md +67 -0
  256. package/corpus/templates/content/docs/product/capabilities/content.form.shared-engine.md +72 -0
  257. package/corpus/templates/content/docs/product/capabilities/content.history.queryable.md +79 -0
  258. package/corpus/templates/content/docs/product/capabilities/content.home.global.md +80 -0
  259. package/corpus/templates/content/docs/product/capabilities/content.job.durable.md +76 -0
  260. package/corpus/templates/content/docs/product/capabilities/content.knowledge.graph.md +67 -0
  261. package/corpus/templates/content/docs/product/capabilities/content.knowledge.links.md +67 -0
  262. package/corpus/templates/content/docs/product/capabilities/content.knowledge.search.md +67 -0
  263. package/corpus/templates/content/docs/product/capabilities/content.layout.responsive.md +80 -0
  264. package/corpus/templates/content/docs/product/capabilities/content.navigation.sidebar.md +86 -0
  265. package/corpus/templates/content/docs/product/capabilities/content.notification.source.md +71 -0
  266. package/corpus/templates/content/docs/product/capabilities/content.object.block.md +76 -0
  267. package/corpus/templates/content/docs/product/capabilities/content.object.blocks-field.md +79 -0
  268. package/corpus/templates/content/docs/product/capabilities/content.object.database.md +92 -0
  269. package/corpus/templates/content/docs/product/capabilities/content.object.multi-membership.md +75 -0
  270. package/corpus/templates/content/docs/product/capabilities/content.object.page.md +76 -0
  271. package/corpus/templates/content/docs/product/capabilities/content.object.reference.md +75 -0
  272. package/corpus/templates/content/docs/product/capabilities/content.object.transclusion.md +75 -0
  273. package/corpus/templates/content/docs/product/capabilities/content.organization.teams.md +71 -0
  274. package/corpus/templates/content/docs/product/capabilities/content.portability.pdf-export.md +78 -0
  275. package/corpus/templates/content/docs/product/capabilities/content.portability.roundtrip.md +154 -0
  276. package/corpus/templates/content/docs/product/capabilities/content.portability.source-representation.md +70 -0
  277. package/corpus/templates/content/docs/product/capabilities/content.portability.vault-export.md +80 -0
  278. package/corpus/templates/content/docs/product/capabilities/content.presentation.mode.md +76 -0
  279. package/corpus/templates/content/docs/product/capabilities/content.preset.catalog.md +71 -0
  280. package/corpus/templates/content/docs/product/capabilities/content.property.actor.md +75 -0
  281. package/corpus/templates/content/docs/product/capabilities/content.property.catalog.md +76 -0
  282. package/corpus/templates/content/docs/product/capabilities/content.property.constraints.md +75 -0
  283. package/corpus/templates/content/docs/product/capabilities/content.property.guarded-change.md +76 -0
  284. package/corpus/templates/content/docs/product/capabilities/content.property.location.md +71 -0
  285. package/corpus/templates/content/docs/product/capabilities/content.property.typed.md +71 -0
  286. package/corpus/templates/content/docs/product/capabilities/content.publish.public.md +75 -0
  287. package/corpus/templates/content/docs/product/capabilities/content.publish.reading.md +74 -0
  288. package/corpus/templates/content/docs/product/capabilities/content.query.object.md +114 -0
  289. package/corpus/templates/content/docs/product/capabilities/content.reader.documents.md +69 -0
  290. package/corpus/templates/content/docs/product/capabilities/content.reader.surface.md +75 -0
  291. package/corpus/templates/content/docs/product/capabilities/content.relationship.edge.md +183 -0
  292. package/corpus/templates/content/docs/product/capabilities/content.renderer.artifact-block.md +71 -0
  293. package/corpus/templates/content/docs/product/capabilities/content.renderer.custom-block.md +81 -0
  294. package/corpus/templates/content/docs/product/capabilities/content.renderer.graph.md +71 -0
  295. package/corpus/templates/content/docs/product/capabilities/content.renderer.typed.md +92 -0
  296. package/corpus/templates/content/docs/product/capabilities/content.research.annotation.md +75 -0
  297. package/corpus/templates/content/docs/product/capabilities/content.research.citation.md +70 -0
  298. package/corpus/templates/content/docs/product/capabilities/content.review.code.md +67 -0
  299. package/corpus/templates/content/docs/product/capabilities/content.revision.suggestions.md +76 -0
  300. package/corpus/templates/content/docs/product/capabilities/content.rule.deterministic.md +79 -0
  301. package/corpus/templates/content/docs/product/capabilities/content.schedule.constraints.md +81 -0
  302. package/corpus/templates/content/docs/product/capabilities/content.security.private-vault.md +71 -0
  303. package/corpus/templates/content/docs/product/capabilities/content.share.views.md +140 -0
  304. package/corpus/templates/content/docs/product/capabilities/content.source.adapters.md +154 -0
  305. package/corpus/templates/content/docs/product/capabilities/content.source.builder-codec.md +70 -0
  306. package/corpus/templates/content/docs/product/capabilities/content.source.catalog.md +149 -0
  307. package/corpus/templates/content/docs/product/capabilities/content.source.file-folder.md +71 -0
  308. package/corpus/templates/content/docs/product/capabilities/content.source.local-bridge.md +156 -0
  309. package/corpus/templates/content/docs/product/capabilities/content.source.local-project.md +70 -0
  310. package/corpus/templates/content/docs/product/capabilities/content.source.page-link.md +70 -0
  311. package/corpus/templates/content/docs/product/capabilities/content.source.row-union.md +69 -0
  312. package/corpus/templates/content/docs/product/capabilities/content.source.spaces-files.md +75 -0
  313. package/corpus/templates/content/docs/product/capabilities/content.source.sync-policy.md +161 -0
  314. package/corpus/templates/content/docs/product/capabilities/content.system.dependencies.md +80 -0
  315. package/corpus/templates/content/docs/product/capabilities/content.system.my-tasks.md +75 -0
  316. package/corpus/templates/content/docs/product/capabilities/content.system.project-status.md +77 -0
  317. package/corpus/templates/content/docs/product/capabilities/content.system.research-workspace.md +80 -0
  318. package/corpus/templates/content/docs/product/capabilities/content.system.task-project.md +76 -0
  319. package/corpus/templates/content/docs/product/capabilities/content.template.governance.md +76 -0
  320. package/corpus/templates/content/docs/product/capabilities/content.template.graph.md +76 -0
  321. package/corpus/templates/content/docs/product/capabilities/content.template.item-body.md +71 -0
  322. package/corpus/templates/content/docs/product/capabilities/content.template.update.md +71 -0
  323. package/corpus/templates/content/docs/product/capabilities/content.time.types.md +85 -0
  324. package/corpus/templates/content/docs/product/capabilities/content.version.branching.md +151 -0
  325. package/corpus/templates/content/docs/product/capabilities/content.version.field-history.md +103 -0
  326. package/corpus/templates/content/docs/product/capabilities/content.view.canvas.md +94 -0
  327. package/corpus/templates/content/docs/product/capabilities/content.view.chart.md +91 -0
  328. package/corpus/templates/content/docs/product/capabilities/content.view.dynamic-create.md +137 -0
  329. package/corpus/templates/content/docs/product/capabilities/content.view.fast-capture.md +82 -0
  330. package/corpus/templates/content/docs/product/capabilities/content.view.graph.md +95 -0
  331. package/corpus/templates/content/docs/product/capabilities/content.view.grouping-aggregation.md +83 -0
  332. package/corpus/templates/content/docs/product/capabilities/content.view.map.md +77 -0
  333. package/corpus/templates/content/docs/product/capabilities/content.view.personal-state.md +99 -0
  334. package/corpus/templates/content/docs/product/capabilities/content.view.pivot.md +76 -0
  335. package/corpus/templates/content/docs/product/capabilities/content.view.query.md +106 -0
  336. package/corpus/templates/content/docs/product/capabilities/content.view.renderer-conformance.md +77 -0
  337. package/corpus/templates/content/docs/product/capabilities/content.view.scale.md +77 -0
  338. package/corpus/templates/content/docs/product/capabilities/content.view.source-query.md +159 -0
  339. package/corpus/templates/content/docs/product/capabilities/content.view.timeline.md +86 -0
  340. package/corpus/templates/content/docs/product/capabilities/content.view.tree.md +77 -0
  341. package/corpus/templates/content/docs/product/capabilities/content.workspace.multi-scope.md +80 -0
  342. package/corpus/templates/content/docs/product/capabilities/content.workspace.session-restore.md +76 -0
  343. package/corpus/templates/content/docs/product/capabilities/content.workspace.view-instance.md +76 -0
  344. package/corpus/templates/content/docs/product/capabilities/content.workspace.working-set.md +77 -0
  345. package/corpus/templates/content/docs/product/chapters/content.chapter.capture-research.md +20 -0
  346. package/corpus/templates/content/docs/product/chapters/content.chapter.connected-sources.md +20 -0
  347. package/corpus/templates/content/docs/product/chapters/content.chapter.consensus.md +19 -0
  348. package/corpus/templates/content/docs/product/chapters/content.chapter.durable-home.md +21 -0
  349. package/corpus/templates/content/docs/product/chapters/content.chapter.publishing-portability.md +21 -0
  350. package/corpus/templates/content/docs/product/chapters/content.chapter.working-systems.md +27 -0
  351. package/corpus/templates/content/docs/product/encyclopedia.md +559 -0
  352. package/corpus/templates/content/docs/product/features/content.feature.bring-your-local-work.md +39 -0
  353. package/corpus/templates/content/docs/product/features/content.feature.build-living-dashboards.md +34 -0
  354. package/corpus/templates/content/docs/product/features/content.feature.build-new-surfaces.md +33 -0
  355. package/corpus/templates/content/docs/product/features/content.feature.capture-into-action.md +34 -0
  356. package/corpus/templates/content/docs/product/features/content.feature.cite-what-you-found.md +34 -0
  357. package/corpus/templates/content/docs/product/features/content.feature.collaborate-in-context.md +59 -0
  358. package/corpus/templates/content/docs/product/features/content.feature.collect-structured-input.md +38 -0
  359. package/corpus/templates/content/docs/product/features/content.feature.connect-your-sources.md +38 -0
  360. package/corpus/templates/content/docs/product/features/content.feature.data-that-keeps-itself-right.md +45 -0
  361. package/corpus/templates/content/docs/product/features/content.feature.durable-foundations.md +48 -0
  362. package/corpus/templates/content/docs/product/features/content.feature.evolve-systems-safely.md +38 -0
  363. package/corpus/templates/content/docs/product/features/content.feature.explore-alternatives-safely.md +39 -0
  364. package/corpus/templates/content/docs/product/features/content.feature.find-your-place-again.md +40 -0
  365. package/corpus/templates/content/docs/product/features/content.feature.keep-your-private-vault-private.md +39 -0
  366. package/corpus/templates/content/docs/product/features/content.feature.living-references.md +38 -0
  367. package/corpus/templates/content/docs/product/features/content.feature.make-the-workspace-yours.md +40 -0
  368. package/corpus/templates/content/docs/product/features/content.feature.move-without-starting-over.md +38 -0
  369. package/corpus/templates/content/docs/product/features/content.feature.plan-work-across-time.md +39 -0
  370. package/corpus/templates/content/docs/product/features/content.feature.publish-with-confidence.md +43 -0
  371. package/corpus/templates/content/docs/product/features/content.feature.put-your-organizations-know-how-to-work.md +38 -0
  372. package/corpus/templates/content/docs/product/features/content.feature.read-and-annotate-anything.md +38 -0
  373. package/corpus/templates/content/docs/product/features/content.feature.review-changes-in-place.md +39 -0
  374. package/corpus/templates/content/docs/product/features/content.feature.run-projects-your-way.md +44 -0
  375. package/corpus/templates/content/docs/product/features/content.feature.see-your-information-your-way.md +44 -0
  376. package/corpus/templates/content/docs/product/features/content.feature.share-how-your-organization-works.md +38 -0
  377. package/corpus/templates/content/docs/product/features/content.feature.sketch-connections-keep-whats-true.md +39 -0
  378. package/corpus/templates/content/docs/product/features/content.feature.take-the-whole-vault-with-you.md +39 -0
  379. package/corpus/templates/content/docs/product/features/content.feature.trust-your-connected-sources.md +40 -0
  380. package/corpus/templates/content/docs/product/features/content.feature.understand-what-your-data-says.md +39 -0
  381. package/corpus/templates/content/docs/product/features/content.feature.when-this-happens-that-follows.md +43 -0
  382. package/corpus/templates/content/docs/product/features/content.feature.work-across-every-workspace.md +37 -0
  383. package/corpus/templates/content/docs/product/features/content.feature.work-on-content-inside-another-application.md +37 -0
  384. package/corpus/templates/content/docs/product/roadmap.md +878 -0
  385. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  386. package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +1 -7
  387. package/corpus/templates/crm/app/i18n/en-US.ts +4 -0
  388. package/corpus/templates/crm/app/routes/settings.tsx +33 -8
  389. package/corpus/templates/crm/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  390. package/corpus/templates/design/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  391. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +133 -63
  392. package/corpus/templates/design/.generated/bridge/embedded-wheel.generated.ts +19 -2
  393. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +51 -14
  394. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +41 -26
  395. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +22 -4
  396. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +216 -116
  397. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +21 -2
  398. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +5 -1
  399. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbenchLoader.tsx +10 -2
  400. package/corpus/templates/design/app/components/design/code-workbench/code-workbench-shell.ts +2 -0
  401. package/corpus/templates/design/app/components/design/code-workbench/explorer/ExplorerView.tsx +60 -55
  402. package/corpus/templates/design/app/components/design/design-canvas/content-size-report.ts +88 -2
  403. package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +3 -2
  404. package/corpus/templates/design/app/components/design/types.ts +4 -0
  405. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -5
  406. package/corpus/templates/design/app/pages/design-editor/clipboard-layer-source.ts +28 -0
  407. package/corpus/templates/design/app/pages/design-editor/clone-and-pen-edit.ts +117 -0
  408. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +3 -1
  409. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +120 -74
  410. package/corpus/templates/design/app/pages/design-editor/pending-structure-verification.ts +11 -0
  411. package/corpus/templates/design/app/pages/design-editor/react-semantic-handoff.ts +2 -0
  412. package/corpus/templates/design/app/pages/design-editor/source-location.ts +1 -1
  413. package/corpus/templates/design/changelog/2026-07-29-copy-rendered-ui-between-live-local-apps-and-design-files-wh.md +6 -0
  414. package/corpus/templates/design/changelog/2026-07-29-design-boards-stay-dark-and-viewport-height-content-keeps-it.md +6 -0
  415. package/corpus/templates/design/changelog/2026-07-29-design-frames-no-longer-grow-continuously-when-their-content.md +6 -0
  416. package/corpus/templates/design/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  417. package/corpus/templates/design/changelog/2026-07-29-the-code-panel-and-apply-design-update-control-now-use-clear.md +6 -0
  418. package/corpus/templates/design/changelog/2026-07-29-the-tools-panel-now-keeps-extension-creation-and-discovery-h.md +6 -0
  419. package/corpus/templates/design/shared/source-mode.ts +7 -1
  420. package/corpus/templates/dispatch/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  421. package/corpus/templates/dispatch/app/i18n-data.ts +171 -0
  422. package/corpus/templates/dispatch/app/routes/browser-chat.tsx +4 -0
  423. package/corpus/templates/dispatch/app/routes/browser-connect.tsx +4 -0
  424. package/corpus/templates/dispatch/changelog/2026-07-29-browser-chat-can-securely-connect-the-agent-native-chrome-ex.md +6 -0
  425. package/corpus/templates/forms/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  426. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +1 -6
  427. package/corpus/templates/forms/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  428. package/corpus/templates/macros/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  429. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +1 -5
  430. package/corpus/templates/macros/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  431. package/corpus/templates/mail/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  432. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +1 -9
  433. package/corpus/templates/mail/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  434. package/corpus/templates/plan/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  435. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +3 -7
  436. package/corpus/templates/plan/app/pages/PlansPage.tsx +2 -2
  437. package/corpus/templates/plan/changelog/2026-07-29-chat-history-now-closes-smoothly-and-stays-ready-when-you-re.md +6 -0
  438. package/corpus/templates/plan/changelog/2026-07-29-chat-history-stays-visible-when-you-return-to-ask-plan.md +6 -0
  439. package/corpus/templates/plan/changelog/2026-07-29-fixed-plan-pages-crashing-when-recent-activity-is-unavailabl.md +6 -0
  440. package/corpus/templates/plan/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  441. package/corpus/templates/slides/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  442. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +60 -4
  443. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +20 -0
  444. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +30 -1
  445. package/corpus/templates/slides/.agents/skills/slide-images/SKILL.md +3 -1
  446. package/corpus/templates/slides/AGENTS.md +22 -24
  447. package/corpus/templates/slides/actions/check-image-gen.ts +8 -2
  448. package/corpus/templates/slides/actions/create-deck.ts +4 -12
  449. package/corpus/templates/slides/actions/export-pptx.ts +25 -3
  450. package/corpus/templates/slides/actions/generate-image-api.ts +4 -2
  451. package/corpus/templates/slides/actions/generate-slides-ai.ts +1 -1
  452. package/corpus/templates/slides/actions/get-deck-reference-context.ts +136 -0
  453. package/corpus/templates/slides/actions/get-workspace-defaults.ts +55 -0
  454. package/corpus/templates/slides/actions/image-gen-status.ts +10 -1
  455. package/corpus/templates/slides/actions/import-file.ts +53 -38
  456. package/corpus/templates/slides/actions/list-decks.ts +2 -0
  457. package/corpus/templates/slides/actions/patch-deck.ts +2 -0
  458. package/corpus/templates/slides/actions/set-workspace-defaults.ts +50 -0
  459. package/corpus/templates/slides/actions/view-screen.ts +6 -0
  460. package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +1 -1
  461. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +67 -3
  462. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +40 -8
  463. package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +33 -8
  464. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +6 -2
  465. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +114 -128
  466. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +2 -1
  467. package/corpus/templates/slides/app/components/editor/ImageGenPanel.tsx +1 -1
  468. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1193 -267
  469. package/corpus/templates/slides/app/components/editor/SlideStyleInspector.tsx +302 -102
  470. package/corpus/templates/slides/app/components/editor/rich-text-selection.ts +270 -0
  471. package/corpus/templates/slides/app/components/editor/slide-escape-arbiter.ts +34 -0
  472. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +368 -0
  473. package/corpus/templates/slides/app/components/editor/slide-presence.ts +13 -0
  474. package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +23 -3
  475. package/corpus/templates/slides/app/components/layout/Layout.tsx +39 -6
  476. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +1 -6
  477. package/corpus/templates/slides/app/components/layout/layout-route-policy.ts +23 -0
  478. package/corpus/templates/slides/app/components/visual-editor/DrawOverlay.tsx +0 -2
  479. package/corpus/templates/slides/app/context/DeckContext.tsx +2 -0
  480. package/corpus/templates/slides/app/global.css +60 -0
  481. package/corpus/templates/slides/app/hooks/use-agent-generating.ts +76 -5
  482. package/corpus/templates/slides/app/hooks/use-workspace-defaults.ts +26 -0
  483. package/corpus/templates/slides/app/i18n/en-US.ts +38 -3
  484. package/corpus/templates/slides/app/lib/generation-state.ts +13 -3
  485. package/corpus/templates/slides/app/lib/slide-image-replacement.ts +27 -6
  486. package/corpus/templates/slides/app/lib/text-tool-shortcut.ts +62 -0
  487. package/corpus/templates/slides/app/pages/DeckEditor.tsx +197 -83
  488. package/corpus/templates/slides/app/pages/DesignSystems.tsx +95 -0
  489. package/corpus/templates/slides/app/pages/Index.tsx +348 -53
  490. package/corpus/templates/slides/changelog/2026-07-28-pick-an-existing-deck-as-a-style-reference-when-creating-a-n.md +6 -0
  491. package/corpus/templates/slides/changelog/2026-07-29-ai-editing-status-is-now-quieter-and-only-shows-slide-presen.md +6 -0
  492. package/corpus/templates/slides/changelog/2026-07-29-design-systems-and-reference-decks-are-easier-to-choose-toge.md +6 -0
  493. package/corpus/templates/slides/changelog/2026-07-29-generated-decks-now-receive-a-concise-relevant-title-before-.md +6 -0
  494. package/corpus/templates/slides/changelog/2026-07-29-new-deck-prompts-reuse-empty-chats.md +6 -0
  495. package/corpus/templates/slides/changelog/2026-07-29-new-deck-setup-now-stays-in-the-editor-with-closable-style-a.md +6 -0
  496. package/corpus/templates/slides/changelog/2026-07-29-selected-words-and-text-runs-can-now-be-styled-independently.md +6 -0
  497. package/corpus/templates/slides/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  498. package/corpus/templates/slides/changelog/2026-07-29-slide-annotation-tools-are-clearer-and-escape-now-exits-anno.md +6 -0
  499. package/corpus/templates/slides/changelog/2026-07-29-slide-editing-now-opens-with-the-app-navigation-collapsed-fo.md +6 -0
  500. package/corpus/templates/slides/changelog/2026-07-29-slide-editing-stays-visually-clean-on-hover-without-a-canvas.md +6 -0
  501. package/corpus/templates/slides/changelog/2026-07-29-slides-now-appear-one-by-one-while-ai-builds-a-deck-with-ste.md +6 -0
  502. package/corpus/templates/slides/changelog/2026-07-29-text-boxes-now-select-move-duplicate-resize-and-style-like-a.md +6 -0
  503. package/corpus/templates/slides/changelog/2026-07-29-untitled-decks-now-use-a-clearer-this-deck-label-in-the-agen.md +6 -0
  504. package/corpus/templates/slides/server/handlers/image-providers/builder.ts +305 -0
  505. package/corpus/templates/slides/server/handlers/image-providers/index.ts +6 -3
  506. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -0
  507. package/corpus/templates/slides/server/workspace-defaults.ts +169 -0
  508. package/corpus/templates/tasks/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  509. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +31 -8
  510. package/corpus/templates/tasks/app/i18n/en-US.ts +7 -0
  511. package/corpus/templates/tasks/app/routes/settings.tsx +25 -2
  512. package/corpus/templates/tasks/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  513. package/corpus/toolkit/CHANGELOG.md +15 -0
  514. package/corpus/toolkit/README.md +2 -2
  515. package/corpus/toolkit/package.json +1 -1
  516. package/corpus/toolkit/src/app-shell/sidebar-footer-actions.tsx +0 -3
  517. package/corpus/toolkit/src/chat-history.css +1 -0
  518. package/corpus/toolkit/src/composer/ComposerPlusMenu.tsx +35 -14
  519. package/corpus/toolkit/src/composer/PromptComposer.tsx +6 -0
  520. package/corpus/toolkit/src/composer/TiptapComposer.tsx +11 -3
  521. package/corpus/toolkit/src/design-tweaks/visual-style-controls.tsx +64 -11
  522. package/dist/action.d.ts +44 -0
  523. package/dist/action.d.ts.map +1 -1
  524. package/dist/action.js +9 -0
  525. package/dist/action.js.map +1 -1
  526. package/dist/agent/engine/builder-engine.js +6 -4
  527. package/dist/agent/engine/builder-engine.js.map +1 -1
  528. package/dist/agent/production-agent.d.ts +11 -1
  529. package/dist/agent/production-agent.d.ts.map +1 -1
  530. package/dist/agent/production-agent.js +131 -90
  531. package/dist/agent/production-agent.js.map +1 -1
  532. package/dist/agent/tool-search.d.ts +4 -0
  533. package/dist/agent/tool-search.d.ts.map +1 -1
  534. package/dist/agent/tool-search.js +30 -0
  535. package/dist/agent/tool-search.js.map +1 -1
  536. package/dist/automations/service.d.ts +85 -0
  537. package/dist/automations/service.d.ts.map +1 -0
  538. package/dist/automations/service.js +332 -0
  539. package/dist/automations/service.js.map +1 -0
  540. package/dist/browser-context/index.d.ts +579 -0
  541. package/dist/browser-context/index.d.ts.map +1 -0
  542. package/dist/browser-context/index.js +331 -0
  543. package/dist/browser-context/index.js.map +1 -0
  544. package/dist/client/AgentPanel.d.ts.map +1 -1
  545. package/dist/client/AgentPanel.js +1 -1
  546. package/dist/client/AgentPanel.js.map +1 -1
  547. package/dist/client/FeedbackButton.d.ts +6 -2
  548. package/dist/client/FeedbackButton.d.ts.map +1 -1
  549. package/dist/client/FeedbackButton.js +20 -3
  550. package/dist/client/FeedbackButton.js.map +1 -1
  551. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  552. package/dist/client/MultiTabAssistantChat.js +19 -2
  553. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  554. package/dist/client/agent-chat.d.ts +15 -0
  555. package/dist/client/agent-chat.d.ts.map +1 -1
  556. package/dist/client/agent-chat.js +13 -3
  557. package/dist/client/agent-chat.js.map +1 -1
  558. package/dist/client/agent-page/AgentJobsTab.d.ts +1 -0
  559. package/dist/client/agent-page/AgentJobsTab.d.ts.map +1 -1
  560. package/dist/client/agent-page/AgentJobsTab.js +186 -98
  561. package/dist/client/agent-page/AgentJobsTab.js.map +1 -1
  562. package/dist/client/agent-page/AgentTabsPage.js +4 -4
  563. package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
  564. package/dist/client/agent-page/use-jobs.d.ts +7 -0
  565. package/dist/client/agent-page/use-jobs.d.ts.map +1 -1
  566. package/dist/client/agent-page/use-jobs.js.map +1 -1
  567. package/dist/client/analytics-session.d.ts.map +1 -1
  568. package/dist/client/analytics-session.js +13 -0
  569. package/dist/client/analytics-session.js.map +1 -1
  570. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  571. package/dist/client/extensions/ExtensionViewer.js +29 -6
  572. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  573. package/dist/client/settings/AutomationsSection.d.ts +0 -9
  574. package/dist/client/settings/AutomationsSection.d.ts.map +1 -1
  575. package/dist/client/settings/AutomationsSection.js +9 -168
  576. package/dist/client/settings/AutomationsSection.js.map +1 -1
  577. package/dist/client/settings/SettingsPanel.d.ts +9 -1
  578. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  579. package/dist/client/settings/SettingsPanel.js +19 -11
  580. package/dist/client/settings/SettingsPanel.js.map +1 -1
  581. package/dist/client/settings/index.d.ts +1 -1
  582. package/dist/client/settings/index.d.ts.map +1 -1
  583. package/dist/client/settings/index.js +1 -1
  584. package/dist/client/settings/index.js.map +1 -1
  585. package/dist/client/use-agent-chat.d.ts.map +1 -1
  586. package/dist/client/use-agent-chat.js +20 -3
  587. package/dist/client/use-agent-chat.js.map +1 -1
  588. package/dist/client/use-chat-threads.d.ts.map +1 -1
  589. package/dist/client/use-chat-threads.js +4 -17
  590. package/dist/client/use-chat-threads.js.map +1 -1
  591. package/dist/collab/struct-routes.d.ts +1 -1
  592. package/dist/deploy/build.d.ts.map +1 -1
  593. package/dist/deploy/build.js +2 -1
  594. package/dist/deploy/build.js.map +1 -1
  595. package/dist/deploy/workspace-deploy.d.ts.map +1 -1
  596. package/dist/deploy/workspace-deploy.js +2 -1
  597. package/dist/deploy/workspace-deploy.js.map +1 -1
  598. package/dist/extensions/actions.d.ts.map +1 -1
  599. package/dist/extensions/actions.js +82 -6
  600. package/dist/extensions/actions.js.map +1 -1
  601. package/dist/extensions/fetch-tool.d.ts.map +1 -1
  602. package/dist/extensions/fetch-tool.js +11 -0
  603. package/dist/extensions/fetch-tool.js.map +1 -1
  604. package/dist/extensions/routes.d.ts +5 -1
  605. package/dist/extensions/routes.d.ts.map +1 -1
  606. package/dist/extensions/routes.js +7 -1
  607. package/dist/extensions/routes.js.map +1 -1
  608. package/dist/extensions/web-content.d.ts +1 -0
  609. package/dist/extensions/web-content.d.ts.map +1 -1
  610. package/dist/extensions/web-content.js +82 -1
  611. package/dist/extensions/web-content.js.map +1 -1
  612. package/dist/integrations/computer-supervision.d.ts.map +1 -1
  613. package/dist/integrations/computer-supervision.js +22 -1
  614. package/dist/integrations/computer-supervision.js.map +1 -1
  615. package/dist/integrations/index.d.ts +3 -2
  616. package/dist/integrations/index.d.ts.map +1 -1
  617. package/dist/integrations/index.js +3 -2
  618. package/dist/integrations/index.js.map +1 -1
  619. package/dist/integrations/integration-durable-dispatch-config.d.ts +2 -0
  620. package/dist/integrations/integration-durable-dispatch-config.d.ts.map +1 -1
  621. package/dist/integrations/integration-durable-dispatch-config.js +4 -0
  622. package/dist/integrations/integration-durable-dispatch-config.js.map +1 -1
  623. package/dist/integrations/integration-durable-dispatch.d.ts +2 -2
  624. package/dist/integrations/integration-durable-dispatch.d.ts.map +1 -1
  625. package/dist/integrations/integration-durable-dispatch.js +2 -2
  626. package/dist/integrations/integration-durable-dispatch.js.map +1 -1
  627. package/dist/integrations/plugin.d.ts.map +1 -1
  628. package/dist/integrations/plugin.js +84 -39
  629. package/dist/integrations/plugin.js.map +1 -1
  630. package/dist/integrations/remote-browser-actions.d.ts +7 -0
  631. package/dist/integrations/remote-browser-actions.d.ts.map +1 -0
  632. package/dist/integrations/remote-browser-actions.js +482 -0
  633. package/dist/integrations/remote-browser-actions.js.map +1 -0
  634. package/dist/jobs/actions/list-recurring-jobs.d.ts.map +1 -1
  635. package/dist/jobs/actions/list-recurring-jobs.js +4 -6
  636. package/dist/jobs/actions/list-recurring-jobs.js.map +1 -1
  637. package/dist/jobs/actions/manage-recurring-job.js +3 -6
  638. package/dist/jobs/actions/manage-recurring-job.js.map +1 -1
  639. package/dist/jobs/background-automation-runner.d.ts +76 -0
  640. package/dist/jobs/background-automation-runner.d.ts.map +1 -0
  641. package/dist/jobs/background-automation-runner.js +288 -0
  642. package/dist/jobs/background-automation-runner.js.map +1 -0
  643. package/dist/jobs/frontmatter.d.ts +63 -0
  644. package/dist/jobs/frontmatter.d.ts.map +1 -0
  645. package/dist/jobs/frontmatter.js +216 -0
  646. package/dist/jobs/frontmatter.js.map +1 -0
  647. package/dist/jobs/scheduler.d.ts +6 -60
  648. package/dist/jobs/scheduler.d.ts.map +1 -1
  649. package/dist/jobs/scheduler.js +67 -484
  650. package/dist/jobs/scheduler.js.map +1 -1
  651. package/dist/jobs/tools.d.ts.map +1 -1
  652. package/dist/jobs/tools.js +24 -3
  653. package/dist/jobs/tools.js.map +1 -1
  654. package/dist/localization/default-messages.d.ts +28 -33
  655. package/dist/localization/default-messages.d.ts.map +1 -1
  656. package/dist/localization/default-messages.js +34 -39
  657. package/dist/localization/default-messages.js.map +1 -1
  658. package/dist/mcp-client/index.d.ts.map +1 -1
  659. package/dist/mcp-client/index.js +4 -0
  660. package/dist/mcp-client/index.js.map +1 -1
  661. package/dist/notifications/actions.d.ts.map +1 -1
  662. package/dist/notifications/actions.js +5 -0
  663. package/dist/notifications/actions.js.map +1 -1
  664. package/dist/notifications/routes.d.ts +6 -6
  665. package/dist/observability/routes.d.ts +3 -3
  666. package/dist/progress/actions.d.ts.map +1 -1
  667. package/dist/progress/actions.js +5 -0
  668. package/dist/progress/actions.js.map +1 -1
  669. package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
  670. package/dist/provider-api/actions/github-repo-files.d.ts.map +1 -1
  671. package/dist/provider-api/actions/github-repo-files.js +9 -0
  672. package/dist/provider-api/actions/github-repo-files.js.map +1 -1
  673. package/dist/provider-api/actions/provider-api.d.ts +16 -16
  674. package/dist/provider-api/actions/provider-api.d.ts.map +1 -1
  675. package/dist/provider-api/actions/provider-api.js +14 -0
  676. package/dist/provider-api/actions/provider-api.js.map +1 -1
  677. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  678. package/dist/resources/handlers.d.ts +1 -1
  679. package/dist/secrets/routes.d.ts +9 -9
  680. package/dist/server/action-discovery.d.ts.map +1 -1
  681. package/dist/server/action-discovery.js +5 -0
  682. package/dist/server/action-discovery.js.map +1 -1
  683. package/dist/server/agent-chat/browser-team-tools.d.ts.map +1 -1
  684. package/dist/server/agent-chat/browser-team-tools.js +90 -3
  685. package/dist/server/agent-chat/browser-team-tools.js.map +1 -1
  686. package/dist/server/agent-chat/framework-prompts.d.ts.map +1 -1
  687. package/dist/server/agent-chat/framework-prompts.js +7 -11
  688. package/dist/server/agent-chat/framework-prompts.js.map +1 -1
  689. package/dist/server/agent-chat/plugin-options.d.ts +3 -3
  690. package/dist/server/agent-chat/plugin-options.js.map +1 -1
  691. package/dist/server/agent-chat/script-entries.d.ts.map +1 -1
  692. package/dist/server/agent-chat/script-entries.js +43 -1
  693. package/dist/server/agent-chat/script-entries.js.map +1 -1
  694. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  695. package/dist/server/agent-chat-plugin.js +24 -5
  696. package/dist/server/agent-chat-plugin.js.map +1 -1
  697. package/dist/server/attribution.d.ts +6 -0
  698. package/dist/server/attribution.d.ts.map +1 -1
  699. package/dist/server/attribution.js +14 -14
  700. package/dist/server/attribution.js.map +1 -1
  701. package/dist/server/auth.d.ts.map +1 -1
  702. package/dist/server/auth.js +5 -2
  703. package/dist/server/auth.js.map +1 -1
  704. package/dist/server/better-auth-instance.d.ts +2 -1
  705. package/dist/server/better-auth-instance.d.ts.map +1 -1
  706. package/dist/server/better-auth-instance.js +6 -3
  707. package/dist/server/better-auth-instance.js.map +1 -1
  708. package/dist/server/core-routes-plugin.d.ts +7 -0
  709. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  710. package/dist/server/core-routes-plugin.js +3 -1
  711. package/dist/server/core-routes-plugin.js.map +1 -1
  712. package/dist/server/csrf.d.ts.map +1 -1
  713. package/dist/server/csrf.js +25 -0
  714. package/dist/server/csrf.js.map +1 -1
  715. package/dist/server/email-template.d.ts +4 -0
  716. package/dist/server/email-template.d.ts.map +1 -1
  717. package/dist/server/email-template.js +25 -8
  718. package/dist/server/email-template.js.map +1 -1
  719. package/dist/server/google-oauth.d.ts +3 -0
  720. package/dist/server/google-oauth.d.ts.map +1 -1
  721. package/dist/server/google-oauth.js +12 -1
  722. package/dist/server/google-oauth.js.map +1 -1
  723. package/dist/server/onboarding-html.d.ts.map +1 -1
  724. package/dist/server/onboarding-html.js +18 -0
  725. package/dist/server/onboarding-html.js.map +1 -1
  726. package/dist/server/prompts/framework-core-compact.js +3 -3
  727. package/dist/server/prompts/framework-core-compact.js.map +1 -1
  728. package/dist/server/prompts/framework-core.js +3 -3
  729. package/dist/server/prompts/framework-core.js.map +1 -1
  730. package/dist/server/prompts/shared-rules.js +3 -3
  731. package/dist/server/prompts/shared-rules.js.map +1 -1
  732. package/dist/server/realtime-token.d.ts +1 -1
  733. package/dist/server/transcribe-voice.d.ts +1 -1
  734. package/dist/settings/index.d.ts +1 -1
  735. package/dist/settings/index.d.ts.map +1 -1
  736. package/dist/settings/index.js +1 -1
  737. package/dist/settings/index.js.map +1 -1
  738. package/dist/shared/analytics-anonymous-id.d.ts +12 -0
  739. package/dist/shared/analytics-anonymous-id.d.ts.map +1 -0
  740. package/dist/shared/analytics-anonymous-id.js +29 -0
  741. package/dist/shared/analytics-anonymous-id.js.map +1 -0
  742. package/dist/sharing/actions/share-resource.d.ts.map +1 -1
  743. package/dist/sharing/actions/share-resource.js +23 -4
  744. package/dist/sharing/actions/share-resource.js.map +1 -1
  745. package/dist/sharing/index.d.ts +1 -1
  746. package/dist/sharing/index.d.ts.map +1 -1
  747. package/dist/sharing/index.js.map +1 -1
  748. package/dist/sharing/registry.d.ts +23 -0
  749. package/dist/sharing/registry.d.ts.map +1 -1
  750. package/dist/sharing/registry.js.map +1 -1
  751. package/dist/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  752. package/dist/templates/chat/app/components/layout/Sidebar.tsx +1 -5
  753. package/dist/templates/chat/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  754. package/dist/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  755. package/dist/templates/default/app/i18n/ar-SA.ts +26 -0
  756. package/dist/templates/default/app/i18n/de-DE.ts +28 -0
  757. package/dist/templates/default/app/i18n/en-US.ts +27 -0
  758. package/dist/templates/default/app/i18n/es-ES.ts +28 -0
  759. package/dist/templates/default/app/i18n/fr-FR.ts +28 -0
  760. package/dist/templates/default/app/i18n/hi-IN.ts +25 -0
  761. package/dist/templates/default/app/i18n/ja-JP.ts +27 -0
  762. package/dist/templates/default/app/i18n/ko-KR.ts +27 -0
  763. package/dist/templates/default/app/i18n/pt-BR.ts +27 -0
  764. package/dist/templates/default/app/i18n/zh-CN.ts +24 -0
  765. package/dist/templates/default/app/i18n/zh-TW.ts +24 -0
  766. package/dist/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  767. package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  768. package/dist/templates/workspace-core/.agents/skills/agent-page/SKILL.md +10 -7
  769. package/dist/templates/workspace-core/.agents/skills/automations/SKILL.md +53 -8
  770. package/dist/templates/workspace-core/.agents/skills/browser-sessions/SKILL.md +131 -0
  771. package/dist/templates/workspace-core/.agents/skills/extension-points/SKILL.md +13 -4
  772. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +23 -15
  773. package/dist/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +33 -18
  774. package/dist/tracking/providers.d.ts.map +1 -1
  775. package/dist/tracking/providers.js +1 -0
  776. package/dist/tracking/providers.js.map +1 -1
  777. package/dist/tracking/registry.d.ts +1 -0
  778. package/dist/tracking/registry.d.ts.map +1 -1
  779. package/dist/tracking/registry.js +1 -0
  780. package/dist/tracking/registry.js.map +1 -1
  781. package/dist/tracking/types.d.ts +1 -0
  782. package/dist/tracking/types.d.ts.map +1 -1
  783. package/dist/tracking/types.js.map +1 -1
  784. package/dist/triggers/actions/list-automations.d.ts +7 -0
  785. package/dist/triggers/actions/list-automations.d.ts.map +1 -1
  786. package/dist/triggers/actions/list-automations.js +29 -41
  787. package/dist/triggers/actions/list-automations.js.map +1 -1
  788. package/dist/triggers/actions/manage-automation.js +15 -33
  789. package/dist/triggers/actions/manage-automation.js.map +1 -1
  790. package/dist/triggers/actions.d.ts.map +1 -1
  791. package/dist/triggers/actions.js +184 -95
  792. package/dist/triggers/actions.js.map +1 -1
  793. package/dist/triggers/dispatcher.d.ts +3 -9
  794. package/dist/triggers/dispatcher.d.ts.map +1 -1
  795. package/dist/triggers/dispatcher.js +134 -304
  796. package/dist/triggers/dispatcher.js.map +1 -1
  797. package/dist/triggers/routes.d.ts.map +1 -1
  798. package/dist/triggers/routes.js +76 -17
  799. package/dist/triggers/routes.js.map +1 -1
  800. package/dist/triggers/types.d.ts +4 -23
  801. package/dist/triggers/types.d.ts.map +1 -1
  802. package/dist/triggers/types.js.map +1 -1
  803. package/docs/content/agent-surfaces.mdx +4 -4
  804. package/docs/content/automations.mdx +61 -12
  805. package/docs/content/deployment.mdx +11 -4
  806. package/docs/content/extensions.mdx +39 -33
  807. package/docs/content/recurring-jobs.mdx +41 -19
  808. package/docs/content/template-dispatch-operations.mdx +19 -7
  809. package/package.json +4 -2
  810. package/src/action.ts +55 -0
  811. package/src/agent/engine/builder-engine.ts +8 -4
  812. package/src/agent/production-agent.ts +175 -100
  813. package/src/agent/tool-search.ts +36 -0
  814. package/src/automations/service.ts +506 -0
  815. package/src/browser-context/index.ts +392 -0
  816. package/src/client/AgentPanel.tsx +2 -1
  817. package/src/client/FeedbackButton.tsx +37 -4
  818. package/src/client/MultiTabAssistantChat.tsx +21 -0
  819. package/src/client/agent-chat.ts +31 -3
  820. package/src/client/agent-page/AgentJobsTab.tsx +438 -310
  821. package/src/client/agent-page/AgentTabsPage.tsx +4 -4
  822. package/src/client/agent-page/use-jobs.ts +7 -0
  823. package/src/client/analytics-session.ts +13 -0
  824. package/src/client/extensions/ExtensionViewer.tsx +67 -0
  825. package/src/client/settings/AutomationsSection.tsx +27 -499
  826. package/src/client/settings/SettingsPanel.tsx +32 -11
  827. package/src/client/settings/index.ts +2 -0
  828. package/src/client/use-agent-chat.ts +35 -3
  829. package/src/client/use-chat-threads.ts +8 -25
  830. package/src/deploy/build.ts +2 -0
  831. package/src/deploy/workspace-deploy.ts +2 -0
  832. package/src/extensions/actions.ts +115 -3
  833. package/src/extensions/fetch-tool.ts +11 -0
  834. package/src/extensions/routes.ts +16 -1
  835. package/src/extensions/web-content.ts +96 -1
  836. package/src/integrations/computer-supervision.ts +33 -1
  837. package/src/integrations/index.ts +6 -0
  838. package/src/integrations/integration-durable-dispatch-config.ts +10 -0
  839. package/src/integrations/integration-durable-dispatch.ts +4 -0
  840. package/src/integrations/plugin.ts +94 -41
  841. package/src/integrations/remote-browser-actions.ts +621 -0
  842. package/src/jobs/actions/list-recurring-jobs.ts +5 -7
  843. package/src/jobs/actions/manage-recurring-job.ts +3 -7
  844. package/src/jobs/background-automation-runner.ts +437 -0
  845. package/src/jobs/frontmatter.ts +308 -0
  846. package/src/jobs/scheduler.ts +89 -598
  847. package/src/jobs/tools.ts +22 -3
  848. package/src/localization/default-messages.ts +45 -45
  849. package/src/mcp-client/index.ts +6 -0
  850. package/src/notifications/actions.ts +5 -0
  851. package/src/progress/actions.ts +5 -0
  852. package/src/provider-api/actions/github-repo-files.ts +11 -0
  853. package/src/provider-api/actions/provider-api.ts +13 -0
  854. package/src/server/__snapshots__/agent-chat-plugin.surface.spec.ts.snap +4 -4
  855. package/src/server/action-discovery.ts +7 -0
  856. package/src/server/agent-chat/browser-team-tools.ts +126 -3
  857. package/src/server/agent-chat/framework-prompts.ts +7 -11
  858. package/src/server/agent-chat/plugin-options.ts +3 -3
  859. package/src/server/agent-chat/script-entries.ts +52 -1
  860. package/src/server/agent-chat-plugin.ts +25 -5
  861. package/src/server/attribution.ts +22 -0
  862. package/src/server/auth.ts +20 -6
  863. package/src/server/better-auth-instance.ts +10 -2
  864. package/src/server/core-routes-plugin.ts +10 -1
  865. package/src/server/csrf.ts +25 -0
  866. package/src/server/email-template.ts +43 -13
  867. package/src/server/google-oauth.ts +21 -1
  868. package/src/server/onboarding-html.ts +18 -0
  869. package/src/server/prompts/framework-core-compact.ts +3 -3
  870. package/src/server/prompts/framework-core.ts +3 -3
  871. package/src/server/prompts/shared-rules.ts +3 -3
  872. package/src/settings/index.ts +1 -0
  873. package/src/shared/analytics-anonymous-id.ts +35 -0
  874. package/src/sharing/actions/share-resource.ts +26 -4
  875. package/src/sharing/index.ts +1 -0
  876. package/src/sharing/registry.ts +23 -0
  877. package/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  878. package/src/templates/chat/app/components/layout/Sidebar.tsx +1 -5
  879. package/src/templates/chat/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
  880. package/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  881. package/src/templates/default/app/i18n/ar-SA.ts +26 -0
  882. package/src/templates/default/app/i18n/de-DE.ts +28 -0
  883. package/src/templates/default/app/i18n/en-US.ts +27 -0
  884. package/src/templates/default/app/i18n/es-ES.ts +28 -0
  885. package/src/templates/default/app/i18n/fr-FR.ts +28 -0
  886. package/src/templates/default/app/i18n/hi-IN.ts +25 -0
  887. package/src/templates/default/app/i18n/ja-JP.ts +27 -0
  888. package/src/templates/default/app/i18n/ko-KR.ts +27 -0
  889. package/src/templates/default/app/i18n/pt-BR.ts +27 -0
  890. package/src/templates/default/app/i18n/zh-CN.ts +24 -0
  891. package/src/templates/default/app/i18n/zh-TW.ts +24 -0
  892. package/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  893. package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  894. package/src/templates/workspace-core/.agents/skills/agent-page/SKILL.md +10 -7
  895. package/src/templates/workspace-core/.agents/skills/automations/SKILL.md +53 -8
  896. package/src/templates/workspace-core/.agents/skills/browser-sessions/SKILL.md +131 -0
  897. package/src/templates/workspace-core/.agents/skills/extension-points/SKILL.md +13 -4
  898. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +23 -15
  899. package/src/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +33 -18
  900. package/src/tracking/providers.ts +1 -0
  901. package/src/tracking/registry.ts +2 -1
  902. package/src/tracking/types.ts +1 -0
  903. package/src/triggers/actions/list-automations.ts +43 -44
  904. package/src/triggers/actions/manage-automation.ts +17 -48
  905. package/src/triggers/actions.ts +218 -127
  906. package/src/triggers/dispatcher.ts +181 -367
  907. package/src/triggers/routes.ts +84 -27
  908. package/src/triggers/types.ts +9 -23
  909. package/corpus/templates/clips/app/routes/_app.settings.organization.tsx +0 -232
@@ -18,8 +18,8 @@ import {
18
18
  import { appStateKeyForBrowserTab } from "@shared/app-state-tabs";
19
19
  import {
20
20
  IconAlertTriangle,
21
- IconArrowsMove,
22
21
  IconMaximize,
22
+ IconX,
23
23
  IconZoomIn,
24
24
  IconZoomOut,
25
25
  } from "@tabler/icons-react";
@@ -28,6 +28,7 @@ import {
28
28
  useCallback,
29
29
  useRef,
30
30
  useEffect,
31
+ useLayoutEffect,
31
32
  type MouseEvent as ReactMouseEvent,
32
33
  type ReactNode,
33
34
  } from "react";
@@ -71,6 +72,37 @@ import { enterSelectionMode } from "@/root";
71
72
  import type { DesignSystemData } from "../../../shared/api";
72
73
  import { BlockBubbleMenu } from "./BlockBubbleMenu";
73
74
  import ImageOverlay from "./ImageOverlay";
75
+ import {
76
+ applyInlineTextStyle,
77
+ getInlineTextStyleSnapshot,
78
+ getInlineTextStyleSnapshotForRange,
79
+ restoreEditableTextRange,
80
+ snapshotEditableTextRange,
81
+ type InlineTextStylePatch,
82
+ type InlineTextStyleSnapshot,
83
+ } from "./rich-text-selection";
84
+ import { decideSlideEscape } from "./slide-escape-arbiter";
85
+ import {
86
+ clientPointToSlideCoordinates,
87
+ cloneSlideObject,
88
+ createSlidesSelectionState,
89
+ ensureSlideObjectId,
90
+ escapedEditingSelection,
91
+ findSlideObjectById,
92
+ freezeSlideElementForFreeform,
93
+ getSelectedObjectDragStart,
94
+ getSlideSelectionIdentity,
95
+ getSlideSelectionMode,
96
+ removeSlideObjectAndLayoutSpacer,
97
+ resolveSlideObjectContainingBlock,
98
+ resizeSlideObject,
99
+ type ResizeHandle,
100
+ type SlideObjectGeometry,
101
+ type SlidesSelectionMode,
102
+ type SlidesSelectionState as BaseSlidesSelectionState,
103
+ type SlidesSelectionTool,
104
+ } from "./slide-object-interactions";
105
+ import { getPassiveSlidePresenceUsers } from "./slide-presence";
74
106
  import {
75
107
  SlideStyleInspector,
76
108
  type SlideStylePatch,
@@ -80,14 +112,52 @@ import { SpeakerNotesPanel } from "./SpeakerNotesPanel";
80
112
 
81
113
  let builderIdCounter = 0;
82
114
  const CANVAS_ZOOM_PRESETS = [10, 25, 50, 75, 100, 125, 150, 200] as const;
115
+ // Keep the text itself a normal click-to-edit surface. Only the slim perimeter
116
+ // is reserved for object movement, matching the familiar Slides interaction.
117
+ const ELEMENT_EDGE_MOVE_BAND = 8;
118
+
119
+ function isWithinElementEdgeMoveBand(
120
+ rect: DOMRect,
121
+ clientX: number,
122
+ clientY: number,
123
+ ): boolean {
124
+ // Tiny labels still need a usable center click target. Cap the move band at
125
+ // one quarter of either dimension so its opposite edges never consume it.
126
+ const edgeBand = Math.min(
127
+ ELEMENT_EDGE_MOVE_BAND,
128
+ rect.width / 4,
129
+ rect.height / 4,
130
+ );
131
+ const outerBand = edgeBand / 2;
132
+ const withinExpandedBounds =
133
+ clientX >= rect.left - outerBand &&
134
+ clientX <= rect.right + outerBand &&
135
+ clientY >= rect.top - outerBand &&
136
+ clientY <= rect.bottom + outerBand;
137
+ if (!withinExpandedBounds) return false;
138
+
139
+ return (
140
+ Math.abs(clientX - rect.left) <= edgeBand ||
141
+ Math.abs(clientX - rect.right) <= edgeBand ||
142
+ Math.abs(clientY - rect.top) <= edgeBand ||
143
+ Math.abs(clientY - rect.bottom) <= edgeBand
144
+ );
145
+ }
146
+
147
+ function ensureBuilderId(element: HTMLElement): string {
148
+ const existing = element.getAttribute("data-builder-id");
149
+ if (existing) return existing;
150
+ const id = `b-${++builderIdCounter}`;
151
+ element.setAttribute("data-builder-id", id);
152
+ return id;
153
+ }
83
154
 
84
155
  /** Stamp all elements inside a container with unique data-builder-id attributes */
85
156
  function stampBuilderIds(container: HTMLElement) {
86
157
  const elements = container.querySelectorAll("*");
87
158
  elements.forEach((el) => {
88
- if (!el.getAttribute("data-builder-id")) {
89
- el.setAttribute("data-builder-id", `b-${++builderIdCounter}`);
90
- }
159
+ if ((el as HTMLElement).classList.contains("fmd-layout-spacer")) return;
160
+ ensureBuilderId(el as HTMLElement);
91
161
  });
92
162
  }
93
163
 
@@ -285,6 +355,38 @@ function stylePropertyName(property: string): string {
285
355
  return property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
286
356
  }
287
357
 
358
+ const INLINE_INSPECTOR_STYLE_KEYS = [
359
+ "color",
360
+ "fontSize",
361
+ "fontWeight",
362
+ ] as const satisfies readonly (keyof SlideStylePatch)[];
363
+
364
+ function inlineInspectorStylePatch(
365
+ patch: SlideStylePatch,
366
+ ): InlineTextStylePatch {
367
+ return Object.fromEntries(
368
+ INLINE_INSPECTOR_STYLE_KEYS.flatMap((key) => {
369
+ const value = patch[key];
370
+ return value === undefined ? [] : [[key, value]];
371
+ }),
372
+ );
373
+ }
374
+
375
+ function applyDescendantTextStyle(
376
+ element: HTMLElement,
377
+ patch: InlineTextStylePatch,
378
+ ) {
379
+ for (const descendant of Array.from(
380
+ element.querySelectorAll<HTMLElement>("*"),
381
+ )) {
382
+ for (const [property, value] of Object.entries(patch)) {
383
+ if (value !== undefined) {
384
+ descendant.style.setProperty(stylePropertyName(property), value);
385
+ }
386
+ }
387
+ }
388
+ }
389
+
288
390
  function elementPathFromRoot(
289
391
  root: HTMLElement,
290
392
  element: HTMLElement,
@@ -315,20 +417,36 @@ function resolveElementPath(
315
417
  function buildStyleSnapshot(
316
418
  element: HTMLElement,
317
419
  selector: string,
420
+ inlineTextStyle?: InlineTextStyleSnapshot,
318
421
  ): SlideStyleSnapshot {
319
422
  const computed = window.getComputedStyle(element);
423
+ const fmdSlide = element.closest(".fmd-slide") as HTMLElement | null;
424
+ const isAbsolute = computed.position === "absolute";
425
+ const slideWidth = fmdSlide?.offsetWidth ?? 0;
426
+ const slideHeight = fmdSlide?.offsetHeight ?? 0;
427
+ const matrix = new DOMMatrixReadOnly(computed.transform);
428
+ const rotation =
429
+ computed.transform === "none"
430
+ ? 0
431
+ : Math.round((Math.atan2(matrix.b, matrix.a) * 180) / Math.PI);
320
432
  const textPreview = (element.textContent ?? "").trim().slice(0, 80);
321
- const fontSize = cssPx(computed.fontSize);
433
+ const blockFontSize = cssPx(computed.fontSize);
322
434
  const rawLineHeight = cssPx(computed.lineHeight);
323
435
  const lineHeight =
324
- rawLineHeight > 0 && fontSize > 0
325
- ? Number((rawLineHeight / fontSize).toFixed(2))
436
+ rawLineHeight > 0 && blockFontSize > 0
437
+ ? Number((rawLineHeight / blockFontSize).toFixed(2))
326
438
  : 1.2;
327
439
  const paddingLeft = cssPx(computed.paddingLeft);
328
440
  const paddingRight = cssPx(computed.paddingRight);
329
441
  const paddingTop = cssPx(computed.paddingTop);
330
442
  const paddingBottom = cssPx(computed.paddingBottom);
331
443
 
444
+ const selectedTextStyle =
445
+ inlineTextStyle?.scope === "selection" ? inlineTextStyle : null;
446
+ const selectedColor = selectedTextStyle?.values.color;
447
+ const selectedFontSize = selectedTextStyle?.values.fontSize;
448
+ const selectedFontWeight = selectedTextStyle?.values.fontWeight;
449
+
332
450
  return {
333
451
  selector,
334
452
  label: element.getAttribute("aria-label") || element.tagName.toLowerCase(),
@@ -338,10 +456,19 @@ function buildStyleSnapshot(
338
456
  element.tagName !== "IMG" &&
339
457
  (!!textPreview || element.classList.contains("fmd-text-box")),
340
458
  isImage: element.tagName === "IMG",
341
- color: normalizedColor(computed.color),
459
+ color:
460
+ selectedColor === null || selectedColor === undefined
461
+ ? normalizedColor(computed.color)
462
+ : normalizedColor(selectedColor),
342
463
  backgroundColor: normalizedColor(computed.backgroundColor),
343
- fontSize,
344
- fontWeight: normalizedFontWeight(computed.fontWeight),
464
+ fontSize:
465
+ selectedFontSize === null || selectedFontSize === undefined
466
+ ? blockFontSize
467
+ : cssPx(selectedFontSize),
468
+ fontWeight:
469
+ selectedFontWeight === null || selectedFontWeight === undefined
470
+ ? normalizedFontWeight(computed.fontWeight)
471
+ : normalizedFontWeight(selectedFontWeight),
345
472
  lineHeight,
346
473
  textAlign: normalizedTextAlign(computed.textAlign),
347
474
  opacity: Math.round(Number(computed.opacity || 1) * 100),
@@ -350,11 +477,23 @@ function buildStyleSnapshot(
350
477
  borderColor: normalizedColor(computed.borderTopColor),
351
478
  paddingX: Math.round((paddingLeft + paddingRight) / 2),
352
479
  paddingY: Math.round((paddingTop + paddingBottom) / 2),
480
+ textStyleScope: inlineTextStyle?.scope ?? "block",
481
+ mixedTextStyles: selectedTextStyle?.mixed ?? [],
482
+ isAbsolute,
483
+ x: Math.round(element.offsetLeft),
484
+ y: Math.round(element.offsetTop),
485
+ width: Math.round(element.offsetWidth),
486
+ height: Math.round(element.offsetHeight),
487
+ rotation,
488
+ slideWidth,
489
+ slideHeight,
353
490
  };
354
491
  }
355
492
 
356
493
  interface SlideSelectionItem {
357
494
  selector: string;
495
+ runtimeSelector?: string;
496
+ objectId?: string;
358
497
  text?: string;
359
498
  kind?: string;
360
499
  tagName?: string;
@@ -362,14 +501,30 @@ interface SlideSelectionItem {
362
501
  style?: Partial<SlideStyleSnapshot>;
363
502
  }
364
503
 
365
- interface SlidesSelectionState {
366
- deckId?: string;
367
- slideId: string;
368
- slideIndex: number;
369
- slideNumber: number;
370
- mode: "single" | "multi" | "image" | "editing";
371
- activeTool?: "select" | "draw" | "pin";
372
- items: SlideSelectionItem[];
504
+ type SlidesSelectionState = BaseSlidesSelectionState<SlideSelectionItem>;
505
+
506
+ function selectionItemForElement(
507
+ element: HTMLElement,
508
+ runtimeSelector: string,
509
+ snapshot?: SlideStyleSnapshot,
510
+ ): SlideSelectionItem {
511
+ const identity = getSlideSelectionIdentity(element, runtimeSelector);
512
+ return {
513
+ ...identity,
514
+ kind: snapshot?.isImage
515
+ ? "image"
516
+ : element.tagName === "IMG"
517
+ ? "image"
518
+ : "element",
519
+ tagName: snapshot?.tagName ?? element.tagName.toLowerCase(),
520
+ text:
521
+ snapshot?.textPreview ?? (element.textContent || "").trim().slice(0, 200),
522
+ imageSrc:
523
+ element instanceof HTMLImageElement
524
+ ? (element.getAttribute("src") ?? undefined)
525
+ : undefined,
526
+ style: snapshot ? { ...snapshot, selector: identity.selector } : undefined,
527
+ };
373
528
  }
374
529
 
375
530
  function syncSelectionToAppState(state: SlidesSelectionState | null) {
@@ -416,6 +571,14 @@ interface SlideEditorProps {
416
571
  file: File,
417
572
  position?: { x: number; y: number },
418
573
  ) => void;
574
+ /** Fired when an image is dragged from elsewhere in the app (e.g. a
575
+ * generated-image preview in the agent chat panel) and dropped on the
576
+ * slide canvas, instead of a native OS file drop. */
577
+ onDropImageUrl?: (
578
+ replaceSrc: string | null,
579
+ url: string,
580
+ position?: { x: number; y: number },
581
+ ) => void;
419
582
  onToggleObjectFit: (imgSrc: string, newFit: string) => void;
420
583
  /** Current user display info for cursor caret */
421
584
  collabUser?: { name: string; color: string };
@@ -434,6 +597,10 @@ interface SlideEditorProps {
434
597
  designSystem?: DesignSystemData;
435
598
  /** Deck aspect ratio (defaults to 16:9 when omitted) */
436
599
  aspectRatio?: AspectRatio;
600
+ /** Whether the right-side style inspector is visible */
601
+ stylePanelOpen?: boolean;
602
+ /** Close the right-side style inspector */
603
+ onCloseStylePanel?: () => void;
437
604
  /** Whether the draw-to-prompt overlay is visible */
438
605
  drawMode?: boolean;
439
606
  /** Called when the draw overlay should exit (Esc, Send, close button) */
@@ -588,6 +755,7 @@ function ImageSelectionOutline({
588
755
  return (
589
756
  <SelectionOverlayPortal viewportRect={viewportRect} zIndex={50}>
590
757
  <div
758
+ data-slide-selection-outline="true"
591
759
  style={{
592
760
  position: "absolute",
593
761
  top: rect.top - pad,
@@ -606,20 +774,24 @@ function ImageSelectionOutline({
606
774
  function ElementSelectionOutline({
607
775
  rect,
608
776
  viewportRect,
609
- onDragStart,
777
+ onResizeStart,
610
778
  }: {
611
779
  rect: DOMRect;
612
780
  viewportRect: DOMRect | null;
613
- onDragStart?: (e: React.PointerEvent) => void;
781
+ onResizeStart?: (handle: ResizeHandle, e: React.PointerEvent) => void;
614
782
  }) {
615
- const t = useT();
616
783
  const pad = 2;
617
784
  const handle = 7;
618
785
  const handleClass =
619
- "absolute size-[7px] rounded-sm border border-background bg-[#609FF8] shadow-sm";
786
+ "absolute size-[7px] touch-none rounded-sm border border-background bg-[#609FF8] shadow-sm";
787
+ const edgeHandleClass =
788
+ "absolute flex touch-none items-center justify-center bg-transparent p-0";
789
+ const edgeBarClass =
790
+ "rounded-sm border border-background bg-[#609FF8] shadow-sm";
620
791
  return (
621
792
  <SelectionOverlayPortal viewportRect={viewportRect} zIndex={51}>
622
793
  <div
794
+ data-slide-selection-outline="true"
623
795
  style={{
624
796
  position: "absolute",
625
797
  top: rect.top - pad,
@@ -633,38 +805,121 @@ function ElementSelectionOutline({
633
805
  }}
634
806
  >
635
807
  <span
808
+ data-slide-resize-handle="nw"
809
+ onPointerDown={(e) => onResizeStart?.("nw", e)}
636
810
  className={handleClass}
637
- style={{ left: -handle / 2, top: -handle / 2 }}
811
+ style={{
812
+ left: -handle / 2,
813
+ top: -handle / 2,
814
+ pointerEvents: "auto",
815
+ cursor: "nwse-resize",
816
+ zIndex: 2,
817
+ }}
638
818
  />
639
819
  <span
820
+ data-slide-resize-handle="ne"
821
+ onPointerDown={(e) => onResizeStart?.("ne", e)}
640
822
  className={handleClass}
641
- style={{ right: -handle / 2, top: -handle / 2 }}
823
+ style={{
824
+ right: -handle / 2,
825
+ top: -handle / 2,
826
+ pointerEvents: "auto",
827
+ cursor: "nesw-resize",
828
+ zIndex: 2,
829
+ }}
642
830
  />
643
831
  <span
832
+ data-slide-resize-handle="sw"
833
+ onPointerDown={(e) => onResizeStart?.("sw", e)}
644
834
  className={handleClass}
645
- style={{ left: -handle / 2, bottom: -handle / 2 }}
835
+ style={{
836
+ left: -handle / 2,
837
+ bottom: -handle / 2,
838
+ pointerEvents: "auto",
839
+ cursor: "nesw-resize",
840
+ zIndex: 2,
841
+ }}
646
842
  />
647
843
  <span
844
+ data-slide-resize-handle="se"
845
+ onPointerDown={(e) => onResizeStart?.("se", e)}
648
846
  className={handleClass}
649
- style={{ right: -handle / 2, bottom: -handle / 2 }}
847
+ style={{
848
+ right: -handle / 2,
849
+ bottom: -handle / 2,
850
+ pointerEvents: "auto",
851
+ cursor: "nwse-resize",
852
+ zIndex: 2,
853
+ }}
650
854
  />
651
- {onDragStart && (
652
- <span
653
- onPointerDown={onDragStart}
654
- title={t("raw.dragToMove")}
655
- className="absolute flex items-center justify-center rounded-full border border-background bg-[#609FF8] shadow-sm cursor-move"
656
- style={{
657
- left: "50%",
658
- top: -22,
659
- width: 16,
660
- height: 16,
661
- transform: "translateX(-50%)",
662
- pointerEvents: "auto",
663
- }}
664
- >
665
- <IconArrowsMove className="size-2.5 text-background" />
666
- </span>
667
- )}
855
+ <span
856
+ data-slide-resize-handle="n"
857
+ onPointerDown={(e) => onResizeStart?.("n", e)}
858
+ className={edgeHandleClass}
859
+ style={{
860
+ width: 22,
861
+ height: 12,
862
+ left: "50%",
863
+ top: -6,
864
+ transform: "translateX(-50%)",
865
+ pointerEvents: "auto",
866
+ cursor: "ns-resize",
867
+ zIndex: 1,
868
+ }}
869
+ >
870
+ <span className={`${edgeBarClass} h-1 w-3.5`} />
871
+ </span>
872
+ <span
873
+ data-slide-resize-handle="e"
874
+ onPointerDown={(e) => onResizeStart?.("e", e)}
875
+ className={edgeHandleClass}
876
+ style={{
877
+ width: 12,
878
+ height: 22,
879
+ right: -6,
880
+ top: "50%",
881
+ transform: "translateY(-50%)",
882
+ pointerEvents: "auto",
883
+ cursor: "ew-resize",
884
+ zIndex: 1,
885
+ }}
886
+ >
887
+ <span className={`${edgeBarClass} h-3.5 w-1`} />
888
+ </span>
889
+ <span
890
+ data-slide-resize-handle="s"
891
+ onPointerDown={(e) => onResizeStart?.("s", e)}
892
+ className={edgeHandleClass}
893
+ style={{
894
+ width: 22,
895
+ height: 12,
896
+ left: "50%",
897
+ bottom: -6,
898
+ transform: "translateX(-50%)",
899
+ pointerEvents: "auto",
900
+ cursor: "ns-resize",
901
+ zIndex: 1,
902
+ }}
903
+ >
904
+ <span className={`${edgeBarClass} h-1 w-3.5`} />
905
+ </span>
906
+ <span
907
+ data-slide-resize-handle="w"
908
+ onPointerDown={(e) => onResizeStart?.("w", e)}
909
+ className={edgeHandleClass}
910
+ style={{
911
+ width: 12,
912
+ height: 22,
913
+ left: -6,
914
+ top: "50%",
915
+ transform: "translateY(-50%)",
916
+ pointerEvents: "auto",
917
+ cursor: "ew-resize",
918
+ zIndex: 1,
919
+ }}
920
+ >
921
+ <span className={`${edgeBarClass} h-3.5 w-1`} />
922
+ </span>
668
923
  </div>
669
924
  </SelectionOverlayPortal>
670
925
  );
@@ -800,12 +1055,15 @@ export default function SlideEditor({
800
1055
  onSearchImage,
801
1056
  onLogoSearch,
802
1057
  onDropImage,
1058
+ onDropImageUrl,
803
1059
  onToggleObjectFit,
804
1060
  agentActive,
805
1061
  slideIndex = 0,
806
1062
  slideCount = 1,
807
1063
  designSystem,
808
1064
  aspectRatio,
1065
+ stylePanelOpen = false,
1066
+ onCloseStylePanel,
809
1067
  drawMode,
810
1068
  onExitDrawMode,
811
1069
  pinMode,
@@ -825,7 +1083,6 @@ export default function SlideEditor({
825
1083
  content.includes('class="fmd-slide"') ||
826
1084
  ["blank", "section", "statement", "full-image"].includes(slide.layout);
827
1085
 
828
- const [isHoveringText, setIsHoveringText] = useState(false);
829
1086
  const [canvasZoom, setCanvasZoom] = useState(100);
830
1087
  const [imageOverlay, setImageOverlay] = useState<{
831
1088
  rect: DOMRect;
@@ -854,6 +1111,10 @@ export default function SlideEditor({
854
1111
  d.paths.some((p) => p === `slides.${activeSlideId}`)
855
1112
  );
856
1113
  });
1114
+ const passivePresentUsers = getPassiveSlidePresenceUsers(
1115
+ presentUsers,
1116
+ agentActive,
1117
+ );
857
1118
  const resolveCanvasRect = useCallback(
858
1119
  (): DOMRect | null =>
859
1120
  slideCanvasRef.current?.getBoundingClientRect() ?? null,
@@ -872,6 +1133,7 @@ export default function SlideEditor({
872
1133
  const [selectedElementPath, setSelectedElementPath] = useState<
873
1134
  number[] | null
874
1135
  >(null);
1136
+ const [selectedObjectId, setSelectedObjectId] = useState<string | null>(null);
875
1137
  const [selectedElementSelector, setSelectedElementSelector] = useState<
876
1138
  string | null
877
1139
  >(null);
@@ -1079,7 +1341,7 @@ export default function SlideEditor({
1079
1341
  `3. Reduce slide padding (e.g. 40px top/bottom instead of 60-80px) if the layout is genuinely tight.`,
1080
1342
  `4. If the content really can't be compressed without losing meaning, split it across two slides.`,
1081
1343
  ``,
1082
- `Do NOT solve this by adding \`transform: scale()\`, \`overflow: scroll\`, or absolute positioning the renderer no longer auto-shrinks overflowing slides, so the HTML itself has to fit ${dimsW}x${dimsH}.`,
1344
+ `Do NOT solve this by adding \`transform: scale()\` or \`overflow: scroll\`. Preserve existing manually positioned absolute objects, including text boxes; rewrite only the overflowing flow layout so the HTML itself fits ${dimsW}x${dimsH}.`,
1083
1345
  ]
1084
1346
  .filter(Boolean)
1085
1347
  .join("\n"),
@@ -1092,6 +1354,7 @@ export default function SlideEditor({
1092
1354
  * placing pointerdown doesn't fall through to click-to-select/deselect
1093
1355
  * logic and steal focus back off the freshly created box. */
1094
1356
  const suppressNextClickRef = useRef(false);
1357
+ const activeGestureCancelRef = useRef<(() => void) | null>(null);
1095
1358
  /**
1096
1359
  * If the user pressed shift/cmd before starting a marquee, additive mode
1097
1360
  * preserves the existing selection on pointerup.
@@ -1107,6 +1370,7 @@ export default function SlideEditor({
1107
1370
  * the parent's `onUpdateSlide` would update DeckProvider mid-render.
1108
1371
  */
1109
1372
  const editingElRef = useRef<HTMLElement | null>(null);
1373
+ const richTextSelectionRef = useRef<Range | null>(null);
1110
1374
  /** Latest onUpdateSlide in a ref so blur handlers always see the current version */
1111
1375
  const onUpdateSlideRef = useRef(onUpdateSlide);
1112
1376
  useEffect(() => {
@@ -1144,63 +1408,135 @@ export default function SlideEditor({
1144
1408
  );
1145
1409
  }, []);
1146
1410
 
1411
+ /**
1412
+ * Turn a normal layout block into a freeform object at its current visual
1413
+ * coordinates. A hidden same-size sibling retains the flex/grid slot, so
1414
+ * the rest of the slide does not reflow when Escape selects the block.
1415
+ */
1416
+ const freezeElementForFreeformSelection = useCallback(
1417
+ (element: HTMLElement): HTMLElement => {
1418
+ if (window.getComputedStyle(element).position === "absolute") {
1419
+ ensureSlideObjectId(element);
1420
+ return element;
1421
+ }
1422
+
1423
+ const fmdSlide = element.closest(".fmd-slide") as HTMLElement | null;
1424
+ const positioningLayer = fmdSlide
1425
+ ? (Array.from(fmdSlide.children).find(
1426
+ (child): child is HTMLElement =>
1427
+ child instanceof HTMLElement &&
1428
+ child.hasAttribute("data-fmd-autofit-content"),
1429
+ ) ?? fmdSlide)
1430
+ : null;
1431
+ if (!positioningLayer) return element;
1432
+
1433
+ const containingBlock = resolveSlideObjectContainingBlock(
1434
+ element,
1435
+ positioningLayer,
1436
+ );
1437
+ const elementRect = element.getBoundingClientRect();
1438
+ const layerRect = containingBlock.getBoundingClientRect();
1439
+ if (
1440
+ !elementRect.width ||
1441
+ !elementRect.height ||
1442
+ !layerRect.width ||
1443
+ !layerRect.height ||
1444
+ !containingBlock.offsetWidth ||
1445
+ !containingBlock.offsetHeight
1446
+ ) {
1447
+ return element;
1448
+ }
1449
+
1450
+ const { x, y } = clientPointToSlideCoordinates(
1451
+ elementRect.left,
1452
+ elementRect.top,
1453
+ layerRect,
1454
+ containingBlock.offsetWidth,
1455
+ containingBlock.offsetHeight,
1456
+ );
1457
+ const scaleX = containingBlock.offsetWidth / layerRect.width;
1458
+ const scaleY = containingBlock.offsetHeight / layerRect.height;
1459
+ const computed = window.getComputedStyle(element);
1460
+ freezeSlideElementForFreeform(
1461
+ element,
1462
+ {
1463
+ x,
1464
+ y,
1465
+ width: Math.round(elementRect.width * scaleX),
1466
+ height: Math.round(elementRect.height * scaleY),
1467
+ },
1468
+ {
1469
+ display: computed.display,
1470
+ flexGrow: computed.flexGrow,
1471
+ flexShrink: computed.flexShrink,
1472
+ flexBasis: computed.flexBasis,
1473
+ alignSelf: computed.alignSelf,
1474
+ },
1475
+ );
1476
+ return element;
1477
+ },
1478
+ [],
1479
+ );
1480
+
1147
1481
  const resolveSelectedElement = useCallback((): HTMLElement | null => {
1482
+ const slideContent = getSlideContent();
1483
+ if (!slideContent) return null;
1484
+ if (selectedObjectId) {
1485
+ const object = findSlideObjectById(slideContent, selectedObjectId);
1486
+ if (object) return object;
1487
+ }
1148
1488
  if (!selectedElementPath) return null;
1149
- return resolveElementPath(getSlideContent(), selectedElementPath);
1150
- }, [getSlideContent, selectedElementPath]);
1489
+ return resolveElementPath(slideContent, selectedElementPath);
1490
+ }, [getSlideContent, selectedElementPath, selectedObjectId]);
1151
1491
 
1152
1492
  const buildSelectionState = useCallback(
1153
1493
  (
1154
- mode: SlidesSelectionState["mode"],
1494
+ mode: SlidesSelectionMode,
1155
1495
  items: SlideSelectionItem[],
1156
- activeTool: SlidesSelectionState["activeTool"] = drawMode
1157
- ? "draw"
1158
- : pinMode
1159
- ? "pin"
1160
- : "select",
1161
- ): SlidesSelectionState => ({
1162
- deckId,
1163
- slideId: slide.id,
1164
- slideIndex,
1165
- slideNumber: slideIndex + 1,
1166
- mode,
1167
- activeTool,
1168
- items,
1169
- }),
1170
- [deckId, drawMode, pinMode, slide.id, slideIndex],
1496
+ activeTool?: SlidesSelectionTool,
1497
+ ): SlidesSelectionState =>
1498
+ createSlidesSelectionState({
1499
+ deckId,
1500
+ slideId: slide.id,
1501
+ slideIndex,
1502
+ mode,
1503
+ items,
1504
+ drawMode: Boolean(drawMode),
1505
+ pinMode: Boolean(pinMode),
1506
+ textBoxMode: Boolean(textBoxMode),
1507
+ activeTool,
1508
+ }),
1509
+ [deckId, drawMode, pinMode, slide.id, slideIndex, textBoxMode],
1171
1510
  );
1172
1511
 
1173
1512
  const clearSelectedElement = useCallback(() => {
1513
+ richTextSelectionRef.current = null;
1174
1514
  setSelectedElementPath(null);
1515
+ setSelectedObjectId(null);
1175
1516
  setSelectedElementSelector(null);
1176
1517
  setSelectedElementRect(null);
1177
1518
  setSelectedStyleSnapshot(null);
1178
1519
  }, []);
1179
1520
 
1180
1521
  const selectElementForStyling = useCallback(
1181
- (element: HTMLElement, selector: string) => {
1522
+ (
1523
+ element: HTMLElement,
1524
+ selector: string,
1525
+ selectionMode?: SlidesSelectionMode,
1526
+ ) => {
1182
1527
  const slideContent = getSlideContent();
1183
1528
  if (!slideContent) return;
1184
1529
  const path = elementPathFromRoot(slideContent, element);
1185
1530
  if (path.length === 0) return;
1186
1531
  const snapshot = buildStyleSnapshot(element, selector);
1187
1532
  setSelectedElementPath(path);
1533
+ setSelectedObjectId(element.getAttribute("data-slide-object-id"));
1188
1534
  setSelectedElementSelector(selector);
1189
1535
  setSelectedElementRect(element.getBoundingClientRect());
1190
1536
  setSelectedStyleSnapshot(snapshot);
1191
1537
  syncSelectionToAppState(
1192
- buildSelectionState(snapshot.isImage ? "image" : "single", [
1193
- {
1194
- selector,
1195
- kind: snapshot.isImage ? "image" : "element",
1196
- tagName: snapshot.tagName,
1197
- text: snapshot.textPreview,
1198
- imageSrc:
1199
- element instanceof HTMLImageElement
1200
- ? (element.getAttribute("src") ?? undefined)
1201
- : undefined,
1202
- style: snapshot,
1203
- },
1538
+ buildSelectionState(getSlideSelectionMode(snapshot, selectionMode), [
1539
+ selectionItemForElement(element, selector, snapshot),
1204
1540
  ]),
1205
1541
  );
1206
1542
  },
@@ -1212,17 +1548,31 @@ export default function SlideEditor({
1212
1548
  const el = editingElRef.current;
1213
1549
  if (!el) return;
1214
1550
  editingElRef.current = null;
1551
+ richTextSelectionRef.current = null;
1215
1552
  el.contentEditable = "false";
1216
1553
  el.removeAttribute("data-editing-block");
1217
1554
 
1555
+ const selected = freezeElementForFreeformSelection(el);
1556
+ const selector = getBuilderSelector(selected);
1557
+
1218
1558
  const html = readCurrentSlideContentHtml();
1219
1559
  if (html !== null) {
1220
1560
  onUpdateSlideRef.current({ content: html });
1221
1561
  }
1222
1562
  inlineEditDraftRef.current = null;
1223
- syncSelectionToAppState(null);
1224
- setEditingEl(null);
1225
- }, [readCurrentSlideContentHtml]);
1563
+ const next = escapedEditingSelection(selected, resolveSelectedElement());
1564
+ setEditingEl(next.editing);
1565
+ if (next.selected && selector) {
1566
+ selectElementForStyling(next.selected, selector);
1567
+ } else {
1568
+ syncSelectionToAppState(null);
1569
+ }
1570
+ }, [
1571
+ readCurrentSlideContentHtml,
1572
+ resolveSelectedElement,
1573
+ selectElementForStyling,
1574
+ freezeElementForFreeformSelection,
1575
+ ]);
1226
1576
 
1227
1577
  /** Enter edit mode on a smart block (text leaf or smart group) */
1228
1578
  const enterInlineEdit = useCallback(
@@ -1248,15 +1598,9 @@ export default function SlideEditor({
1248
1598
  editingElRef.current = el;
1249
1599
  setEditingEl(el);
1250
1600
  if (selector) {
1601
+ const item = selectionItemForElement(el, selector);
1251
1602
  syncSelectionToAppState(
1252
- buildSelectionState("editing", [
1253
- {
1254
- selector,
1255
- kind: "text",
1256
- tagName: el.tagName.toLowerCase(),
1257
- text: (el.textContent ?? "").trim().slice(0, 200),
1258
- },
1259
- ]),
1603
+ buildSelectionState("editing", [{ ...item, kind: "text" }]),
1260
1604
  );
1261
1605
  }
1262
1606
  },
@@ -1276,6 +1620,7 @@ export default function SlideEditor({
1276
1620
  editing.removeAttribute("data-editing-block");
1277
1621
  }
1278
1622
  editingElRef.current = null;
1623
+ richTextSelectionRef.current = null;
1279
1624
  setEditingEl(null);
1280
1625
 
1281
1626
  if (draft?.slideId === previousSlideId) {
@@ -1301,6 +1646,44 @@ export default function SlideEditor({
1301
1646
  return () => editingEl.removeEventListener("input", handleInput);
1302
1647
  }, [captureInlineEditDraft, editingEl, slide.id]);
1303
1648
 
1649
+ useEffect(() => {
1650
+ if (!editingEl) return;
1651
+
1652
+ const updateInspectorTextStyle = () => {
1653
+ const selection = window.getSelection();
1654
+ if (!selection || selection.rangeCount !== 1) return;
1655
+ const range = selection.getRangeAt(0);
1656
+ if (
1657
+ !editingEl.contains(range.startContainer) ||
1658
+ !editingEl.contains(range.endContainer)
1659
+ ) {
1660
+ // Inspector and portalled picker interactions move browser focus away
1661
+ // from the slide. Retain the last valid range until the user places a
1662
+ // new caret or selection inside this editable.
1663
+ return;
1664
+ }
1665
+
1666
+ richTextSelectionRef.current = snapshotEditableTextRange(
1667
+ editingEl,
1668
+ selection,
1669
+ );
1670
+ const selector = selectedElementSelector ?? getBuilderSelector(editingEl);
1671
+ if (!selector) return;
1672
+ setSelectedStyleSnapshot(
1673
+ buildStyleSnapshot(
1674
+ editingEl,
1675
+ selector,
1676
+ getInlineTextStyleSnapshot(editingEl, selection),
1677
+ ),
1678
+ );
1679
+ };
1680
+
1681
+ updateInspectorTextStyle();
1682
+ document.addEventListener("selectionchange", updateInspectorTextStyle);
1683
+ return () =>
1684
+ document.removeEventListener("selectionchange", updateInspectorTextStyle);
1685
+ }, [editingEl, selectedElementSelector]);
1686
+
1304
1687
  // Global keyboard handling while inline-editing
1305
1688
  useEffect(() => {
1306
1689
  if (!editingEl) return;
@@ -1326,12 +1709,6 @@ export default function SlideEditor({
1326
1709
  ) {
1327
1710
  return;
1328
1711
  }
1329
- if (e.key === "Escape") {
1330
- e.preventDefault();
1331
- e.stopPropagation();
1332
- exitInlineEdit();
1333
- return;
1334
- }
1335
1712
  if (e.key === "Enter") {
1336
1713
  // Smart Enter:
1337
1714
  // - Shift+Enter always inserts a <br>.
@@ -1385,8 +1762,15 @@ export default function SlideEditor({
1385
1762
  const onDocMouseDown = (e: MouseEvent) => {
1386
1763
  const target = e.target as Node;
1387
1764
  if (editingEl.contains(target)) return;
1388
- // Ignore clicks on the bubble menu (it lives in a portal)
1389
- if ((target as HTMLElement).closest?.("[data-block-bubble-menu]")) return;
1765
+ // Inspector controls and their popovers deliberately preserve the live
1766
+ // edit session so a saved text range can receive the chosen formatting.
1767
+ if (
1768
+ (target as HTMLElement).closest?.(
1769
+ "[data-block-bubble-menu], [data-slide-style-trigger], [data-slide-style-dock], [data-slide-inline-edit-surface]",
1770
+ )
1771
+ ) {
1772
+ return;
1773
+ }
1390
1774
  exitInlineEdit();
1391
1775
  };
1392
1776
  document.addEventListener("mousedown", onDocMouseDown);
@@ -1409,8 +1793,9 @@ export default function SlideEditor({
1409
1793
  };
1410
1794
  }, [selectedImg]);
1411
1795
 
1412
- useEffect(() => {
1796
+ useLayoutEffect(() => {
1413
1797
  if (!selectedElementPath || !selectedElementSelector) return;
1798
+ let observedElement: HTMLElement | null = null;
1414
1799
  const update = () => {
1415
1800
  const element = resolveSelectedElement();
1416
1801
  if (!element) {
@@ -1418,29 +1803,54 @@ export default function SlideEditor({
1418
1803
  syncSelectionToAppState(null);
1419
1804
  return;
1420
1805
  }
1421
- const snapshot = buildStyleSnapshot(element, selectedElementSelector);
1806
+ const inlineTextStyle =
1807
+ editingElRef.current === element
1808
+ ? getInlineTextStyleSnapshotForRange(
1809
+ element,
1810
+ richTextSelectionRef.current,
1811
+ )
1812
+ : undefined;
1813
+ const snapshot = buildStyleSnapshot(
1814
+ element,
1815
+ selectedElementSelector,
1816
+ inlineTextStyle,
1817
+ );
1422
1818
  setSelectedElementRect(element.getBoundingClientRect());
1423
1819
  setSelectedStyleSnapshot(snapshot);
1424
1820
  syncSelectionToAppState(
1425
- buildSelectionState(snapshot.isImage ? "image" : "single", [
1426
- {
1427
- selector: selectedElementSelector,
1428
- kind: snapshot.isImage ? "image" : "element",
1429
- tagName: snapshot.tagName,
1430
- text: snapshot.textPreview,
1431
- imageSrc:
1432
- element instanceof HTMLImageElement
1433
- ? (element.getAttribute("src") ?? undefined)
1434
- : undefined,
1435
- style: snapshot,
1436
- },
1821
+ buildSelectionState(getSlideSelectionMode(snapshot), [
1822
+ selectionItemForElement(element, selectedElementSelector, snapshot),
1437
1823
  ]),
1438
1824
  );
1439
1825
  };
1440
1826
  update();
1827
+
1828
+ observedElement = resolveSelectedElement();
1829
+ const positioningLayer = observedElement?.closest(
1830
+ "[data-fmd-autofit-content], .fmd-slide",
1831
+ ) as HTMLElement | null;
1832
+ const resizeObserver =
1833
+ typeof ResizeObserver === "undefined" ? null : new ResizeObserver(update);
1834
+ if (observedElement) resizeObserver?.observe(observedElement);
1835
+ if (positioningLayer) resizeObserver?.observe(positioningLayer);
1836
+ // Auto-fit writes transform custom properties directly on the layer. Those
1837
+ // mutations do not resize the element's CSS box, but they do change its
1838
+ // viewport rect, so selection chrome must follow the transformed DOM.
1839
+ const mutationObserver =
1840
+ typeof MutationObserver === "undefined" || !positioningLayer
1841
+ ? null
1842
+ : new MutationObserver(update);
1843
+ if (mutationObserver && positioningLayer) {
1844
+ mutationObserver.observe(positioningLayer, {
1845
+ attributes: true,
1846
+ attributeFilter: ["class", "style"],
1847
+ });
1848
+ }
1441
1849
  window.addEventListener("resize", update);
1442
1850
  window.addEventListener("scroll", update, true);
1443
1851
  return () => {
1852
+ resizeObserver?.disconnect();
1853
+ mutationObserver?.disconnect();
1444
1854
  window.removeEventListener("resize", update);
1445
1855
  window.removeEventListener("scroll", update, true);
1446
1856
  };
@@ -1511,12 +1921,7 @@ export default function SlideEditor({
1511
1921
  const selector = `[data-builder-id="${id}"]`;
1512
1922
  const text = (el.textContent || "").trim().slice(0, 200);
1513
1923
  rects.set(id, { rect: el.getBoundingClientRect(), text, selector });
1514
- items.push({
1515
- selector,
1516
- text,
1517
- kind: el.tagName === "IMG" ? "image" : "element",
1518
- tagName: el.tagName.toLowerCase(),
1519
- });
1924
+ items.push(selectionItemForElement(el, selector));
1520
1925
  });
1521
1926
  }
1522
1927
  setMultiSelection(ids);
@@ -1539,6 +1944,94 @@ export default function SlideEditor({
1539
1944
  applyMultiSelection(new Set());
1540
1945
  }, [applyMultiSelection, multiSelection.size]);
1541
1946
 
1947
+ // One Escape owner for the HTML editor. Radix dialogs/popovers and native
1948
+ // form controls retain their own Escape behavior before we arbitrate canvas
1949
+ // state. Gesture cancellation is deliberately ahead of selection clearing.
1950
+ useEffect(() => {
1951
+ const onKey = (e: KeyboardEvent) => {
1952
+ if (e.key !== "Escape") return;
1953
+ const target = e.target instanceof Element ? e.target : null;
1954
+ const editing = editingElRef.current;
1955
+ const overlayOwnsEscape = Boolean(
1956
+ document.querySelector(
1957
+ '[role="dialog"]:not([data-state="closed"]), [role="menu"]:not([data-state="closed"]), [role="listbox"]:not([data-state="closed"]), [data-radix-popper-content-wrapper]:not([data-state="closed"])',
1958
+ ),
1959
+ );
1960
+ const targetOwnsEscape = Boolean(
1961
+ target?.closest(
1962
+ '[role="dialog"], [data-radix-popper-content-wrapper], [data-radix-menu-content]',
1963
+ ) ||
1964
+ ((target instanceof HTMLInputElement ||
1965
+ target instanceof HTMLTextAreaElement ||
1966
+ target instanceof HTMLSelectElement ||
1967
+ (target instanceof HTMLElement && target.isContentEditable)) &&
1968
+ !editing?.contains(target)),
1969
+ );
1970
+ const action = decideSlideEscape({
1971
+ editing: Boolean(editing),
1972
+ activeGesture: activeGestureCancelRef.current !== null,
1973
+ activeMode: Boolean(drawMode || pinMode || textBoxMode),
1974
+ multiSelection: multiSelection.size > 0,
1975
+ singleSelection: Boolean(selectedElementSelector),
1976
+ targetOwnsEscape,
1977
+ overlayOwnsEscape,
1978
+ });
1979
+ if (action === "none" || action === "canvas") return;
1980
+
1981
+ e.preventDefault();
1982
+ e.stopImmediatePropagation();
1983
+ if (action === "edit") {
1984
+ exitInlineEdit();
1985
+ } else if (action === "gesture") {
1986
+ activeGestureCancelRef.current?.();
1987
+ } else if (action === "mode") {
1988
+ if (drawMode) onExitDrawMode?.();
1989
+ else if (pinMode) onExitPinMode?.();
1990
+ else onExitTextBoxMode?.();
1991
+ } else if (action === "multi-selection") {
1992
+ clearMultiSelection();
1993
+ } else {
1994
+ clearSelectedElement();
1995
+ syncSelectionToAppState(null);
1996
+ }
1997
+ };
1998
+ window.addEventListener("keydown", onKey, true);
1999
+ return () => window.removeEventListener("keydown", onKey, true);
2000
+ }, [
2001
+ clearMultiSelection,
2002
+ clearSelectedElement,
2003
+ drawMode,
2004
+ exitInlineEdit,
2005
+ multiSelection.size,
2006
+ onExitDrawMode,
2007
+ onExitPinMode,
2008
+ onExitTextBoxMode,
2009
+ pinMode,
2010
+ selectedElementSelector,
2011
+ textBoxMode,
2012
+ ]);
2013
+
2014
+ // Tool state is useful even before the user places an object. This keeps
2015
+ // `view-screen` truthful while the text tool is armed and after it exits.
2016
+ useEffect(() => {
2017
+ if (editingEl || multiSelection.size > 0 || selectedElementSelector) {
2018
+ return;
2019
+ }
2020
+ if (drawMode || pinMode || textBoxMode) {
2021
+ syncSelectionToAppState(buildSelectionState("canvas", []));
2022
+ } else {
2023
+ syncSelectionToAppState(null);
2024
+ }
2025
+ }, [
2026
+ buildSelectionState,
2027
+ drawMode,
2028
+ editingEl,
2029
+ multiSelection.size,
2030
+ pinMode,
2031
+ selectedElementSelector,
2032
+ textBoxMode,
2033
+ ]);
2034
+
1542
2035
  const getPlaceholderTarget = useCallback(
1543
2036
  (placeholder: HTMLElement): string => {
1544
2037
  const slideContent = getSlideContent();
@@ -1613,20 +2106,6 @@ export default function SlideEditor({
1613
2106
  syncSelectionToAppState(null);
1614
2107
  }, [clearSelectedElement, slide.id]);
1615
2108
 
1616
- // Escape key clears multi-selection (only when not inline-editing)
1617
- useEffect(() => {
1618
- if (multiSelection.size === 0) return;
1619
- if (editingEl) return; // Esc handler in editing mode owns this key
1620
- const onKey = (e: KeyboardEvent) => {
1621
- if (e.key === "Escape") {
1622
- e.preventDefault();
1623
- clearMultiSelection();
1624
- }
1625
- };
1626
- window.addEventListener("keydown", onKey);
1627
- return () => window.removeEventListener("keydown", onKey);
1628
- }, [multiSelection.size, editingEl, clearMultiSelection]);
1629
-
1630
2109
  // Delete/Backspace removes the selected shape/text box (single or
1631
2110
  // multi-select) from the slide. Only active when something is selected for
1632
2111
  // styling (not while inline-editing text, where Backspace should delete a
@@ -1647,11 +2126,15 @@ export default function SlideEditor({
1647
2126
 
1648
2127
  if (multiSelection.size > 0) {
1649
2128
  for (const id of multiSelection) {
1650
- slideContent.querySelector(`[data-builder-id="${id}"]`)?.remove();
2129
+ const element = slideContent.querySelector(
2130
+ `[data-builder-id="${id}"]`,
2131
+ ) as HTMLElement | null;
2132
+ if (element) removeSlideObjectAndLayoutSpacer(element);
1651
2133
  }
1652
2134
  clearMultiSelection();
1653
2135
  } else {
1654
- resolveSelectedElement()?.remove();
2136
+ const element = resolveSelectedElement();
2137
+ if (element) removeSlideObjectAndLayoutSpacer(element);
1655
2138
  clearSelectedElement();
1656
2139
  }
1657
2140
 
@@ -1670,6 +2153,7 @@ export default function SlideEditor({
1670
2153
  resolveSelectedElement,
1671
2154
  clearSelectedElement,
1672
2155
  readCurrentSlideContentHtml,
2156
+ removeSlideObjectAndLayoutSpacer,
1673
2157
  syncSelectionToAppState,
1674
2158
  ]);
1675
2159
 
@@ -1683,6 +2167,7 @@ export default function SlideEditor({
1683
2167
  (target: HTMLElement, slideContent: HTMLElement): string | null => {
1684
2168
  let el: HTMLElement | null = target;
1685
2169
  while (el && slideContent.contains(el) && el !== slideContent) {
2170
+ if (el.classList.contains("fmd-layout-spacer")) return null;
1686
2171
  const id = el.getAttribute("data-builder-id");
1687
2172
  if (id) return id;
1688
2173
  el = el.parentElement;
@@ -1696,6 +2181,7 @@ export default function SlideEditor({
1696
2181
  (target: HTMLElement, slideContent: HTMLElement): HTMLElement | null => {
1697
2182
  let el: HTMLElement | null = target;
1698
2183
  while (el && slideContent.contains(el) && el !== slideContent) {
2184
+ if (el.classList.contains("fmd-layout-spacer")) return null;
1699
2185
  if (el.getAttribute("data-builder-id")) return el;
1700
2186
  el = el.parentElement;
1701
2187
  }
@@ -1723,28 +2209,20 @@ export default function SlideEditor({
1723
2209
  ".fmd-slide",
1724
2210
  ) as HTMLElement | null;
1725
2211
  if (!fmdSlide) return;
1726
- // .fmd-slide is often visually scaled (canvas zoom, and the autofit
1727
- // system that shrinks overflowing slides to fit) via a CSS transform.
1728
- // A transform doesn't change the element's own layout coordinate
1729
- // space, so a pixel offset computed from its on-screen rect would be
1730
- // scaled a second time once applied to a child. Percentages resolve
1731
- // against that untransformed layout box, so they land at the actual
1732
- // click point regardless of the current scale.
1733
- const rect = fmdSlide.getBoundingClientRect();
1734
- const xPct =
1735
- rect.width > 0
1736
- ? Math.min(
1737
- 100,
1738
- Math.max(0, ((clientX - rect.left) / rect.width) * 100),
1739
- )
1740
- : 0;
1741
- const yPct =
1742
- rect.height > 0
1743
- ? Math.min(
1744
- 100,
1745
- Math.max(0, ((clientY - rect.top) / rect.height) * 100),
1746
- )
1747
- : 0;
2212
+ const positioningLayer =
2213
+ Array.from(fmdSlide.children).find(
2214
+ (child): child is HTMLElement =>
2215
+ child instanceof HTMLElement &&
2216
+ child.hasAttribute("data-fmd-autofit-content"),
2217
+ ) ?? fmdSlide;
2218
+ const rect = positioningLayer.getBoundingClientRect();
2219
+ const { x, y } = clientPointToSlideCoordinates(
2220
+ clientX,
2221
+ clientY,
2222
+ rect,
2223
+ positioningLayer.offsetWidth,
2224
+ positioningLayer.offsetHeight,
2225
+ );
1748
2226
 
1749
2227
  if (getComputedStyle(fmdSlide).position === "static") {
1750
2228
  fmdSlide.style.position = "relative";
@@ -1752,16 +2230,18 @@ export default function SlideEditor({
1752
2230
 
1753
2231
  const box = document.createElement("div");
1754
2232
  box.className = "fmd-text-box";
2233
+ ensureSlideObjectId(box);
2234
+ ensureBuilderId(box);
1755
2235
  box.style.position = "absolute";
1756
- box.style.left = `${xPct}%`;
1757
- box.style.top = `${yPct}%`;
2236
+ box.style.left = `${x}px`;
2237
+ box.style.top = `${y}px`;
1758
2238
  box.style.width = "320px";
1759
2239
  box.style.fontSize = "24px";
1760
2240
  box.style.color = "#fff";
1761
2241
  box.style.fontFamily = "'Poppins', sans-serif";
1762
2242
  box.style.lineHeight = "1.3";
1763
2243
  box.textContent = ZERO_WIDTH_SPACE;
1764
- fmdSlide.appendChild(box);
2244
+ positioningLayer.appendChild(box);
1765
2245
 
1766
2246
  enterInlineEdit(box);
1767
2247
 
@@ -1781,8 +2261,367 @@ export default function SlideEditor({
1781
2261
  [enterInlineEdit],
1782
2262
  );
1783
2263
 
2264
+ const getObjectGeometry = useCallback(
2265
+ (element: HTMLElement): SlideObjectGeometry => ({
2266
+ x: element.offsetLeft,
2267
+ y: element.offsetTop,
2268
+ width: element.offsetWidth,
2269
+ height: element.offsetHeight,
2270
+ }),
2271
+ [],
2272
+ );
2273
+
2274
+ const applyObjectGeometry = useCallback(
2275
+ (element: HTMLElement, geometry: SlideObjectGeometry) => {
2276
+ element.style.left = `${geometry.x}px`;
2277
+ element.style.top = `${geometry.y}px`;
2278
+ element.style.width = `${geometry.width}px`;
2279
+ element.style.height = `${geometry.height}px`;
2280
+ },
2281
+ [],
2282
+ );
2283
+
2284
+ const startElementDrag = useCallback(
2285
+ (
2286
+ e: React.PointerEvent,
2287
+ element: HTMLElement,
2288
+ {
2289
+ preserveClickWithoutMove = false,
2290
+ }: { preserveClickWithoutMove?: boolean } = {},
2291
+ ) => {
2292
+ if (readOnly) return;
2293
+ const fmdSlide = element.closest(".fmd-slide") as HTMLElement | null;
2294
+ if (!fmdSlide || getComputedStyle(element).position !== "absolute") {
2295
+ return;
2296
+ }
2297
+
2298
+ const slideRect = fmdSlide.getBoundingClientRect();
2299
+ const slideWidth = fmdSlide.offsetWidth;
2300
+ const slideHeight = fmdSlide.offsetHeight;
2301
+ if (
2302
+ !slideRect.width ||
2303
+ !slideRect.height ||
2304
+ !slideWidth ||
2305
+ !slideHeight
2306
+ ) {
2307
+ return;
2308
+ }
2309
+
2310
+ // Pointer-down on the selection perimeter is a move gesture, never a
2311
+ // text caret placement. A selected object's body, however, has to keep
2312
+ // an unmoved click available for inline editing. In that case wait until
2313
+ // movement crosses the drag threshold before consuming its click.
2314
+ if (!preserveClickWithoutMove) {
2315
+ e.preventDefault();
2316
+ e.stopPropagation();
2317
+ suppressNextClickRef.current = true;
2318
+ }
2319
+
2320
+ const origin = getObjectGeometry(element);
2321
+ const originalObjectId = element.getAttribute("data-slide-object-id");
2322
+ const startClientX = e.clientX;
2323
+ const startClientY = e.clientY;
2324
+ const duplicateRequested = e.altKey;
2325
+ let activeElement = element;
2326
+ let clone: HTMLElement | null = null;
2327
+ let moved = false;
2328
+
2329
+ const stop = () => {
2330
+ window.removeEventListener("pointermove", onMove);
2331
+ window.removeEventListener("pointerup", onUp);
2332
+ window.removeEventListener("pointercancel", onCancel);
2333
+ if (activeGestureCancelRef.current === onCancel) {
2334
+ activeGestureCancelRef.current = null;
2335
+ }
2336
+ };
2337
+
2338
+ const restore = () => {
2339
+ if (clone) {
2340
+ clone.remove();
2341
+ } else {
2342
+ applyObjectGeometry(element, origin);
2343
+ }
2344
+ if (originalObjectId) {
2345
+ element.setAttribute("data-slide-object-id", originalObjectId);
2346
+ } else {
2347
+ element.removeAttribute("data-slide-object-id");
2348
+ }
2349
+ const selector = getBuilderSelector(element);
2350
+ if (selector) selectElementForStyling(element, selector);
2351
+ };
2352
+
2353
+ const onMove = (moveEvent: PointerEvent) => {
2354
+ let dx =
2355
+ ((moveEvent.clientX - startClientX) / slideRect.width) * slideWidth;
2356
+ let dy =
2357
+ ((moveEvent.clientY - startClientY) / slideRect.height) * slideHeight;
2358
+ if (!moved && Math.hypot(dx, dy) < 2) return;
2359
+ if (moveEvent.shiftKey) {
2360
+ if (Math.abs(dx) >= Math.abs(dy)) dy = 0;
2361
+ else dx = 0;
2362
+ }
2363
+ if (!moved) {
2364
+ moved = true;
2365
+ // React still receives a click after a pointer drag. Suppress that
2366
+ // trailing click so a moved text box does not immediately reopen
2367
+ // inline editing.
2368
+ suppressNextClickRef.current = true;
2369
+ ensureSlideObjectId(element);
2370
+ if (duplicateRequested && moveEvent.altKey) {
2371
+ clone = cloneSlideObject(element);
2372
+ element.after(clone);
2373
+ ensureBuilderId(clone);
2374
+ stampBuilderIds(clone);
2375
+ activeElement = clone;
2376
+ }
2377
+ }
2378
+ moveEvent.preventDefault();
2379
+ applyObjectGeometry(activeElement, {
2380
+ ...origin,
2381
+ x: origin.x + dx,
2382
+ y: origin.y + dy,
2383
+ });
2384
+ const selector = getBuilderSelector(activeElement);
2385
+ if (selector) selectElementForStyling(activeElement, selector);
2386
+ };
2387
+
2388
+ const onUp = (upEvent: PointerEvent) => {
2389
+ stop();
2390
+ if (!moved) {
2391
+ if (preserveClickWithoutMove) return;
2392
+ // Pointer-up can occur outside the canvas, where React will not see
2393
+ // the click that normally clears this flag.
2394
+ window.setTimeout(function clearEdgeClickSuppression() {
2395
+ suppressNextClickRef.current = false;
2396
+ }, 0);
2397
+ return;
2398
+ }
2399
+ // A drag does not always produce a click (for example when released
2400
+ // outside the canvas), so do not leave the one-click suppression
2401
+ // armed for the user's next unrelated action.
2402
+ window.setTimeout(function clearDragClickSuppression() {
2403
+ suppressNextClickRef.current = false;
2404
+ }, 0);
2405
+ // Keeping Option pressed is the explicit duplicate commit. If it was
2406
+ // released before drop, turn the gesture back into a normal move.
2407
+ if (clone && !upEvent.altKey) {
2408
+ applyObjectGeometry(element, getObjectGeometry(clone));
2409
+ clone.remove();
2410
+ activeElement = element;
2411
+ }
2412
+ const html = readCurrentSlideContentHtml();
2413
+ if (html !== null) onUpdateSlideRef.current({ content: html });
2414
+ const selector = getBuilderSelector(activeElement);
2415
+ if (selector) selectElementForStyling(activeElement, selector);
2416
+ };
2417
+
2418
+ const onCancel = () => {
2419
+ stop();
2420
+ suppressNextClickRef.current = false;
2421
+ restore();
2422
+ };
2423
+
2424
+ window.addEventListener("pointermove", onMove);
2425
+ window.addEventListener("pointerup", onUp);
2426
+ window.addEventListener("pointercancel", onCancel);
2427
+ activeGestureCancelRef.current = onCancel;
2428
+ },
2429
+ [
2430
+ applyObjectGeometry,
2431
+ getObjectGeometry,
2432
+ readCurrentSlideContentHtml,
2433
+ readOnly,
2434
+ selectElementForStyling,
2435
+ ],
2436
+ );
2437
+
2438
+ const startElementResize = useCallback(
2439
+ (handle: ResizeHandle, e: React.PointerEvent) => {
2440
+ if (readOnly) return;
2441
+ const element = resolveSelectedElement();
2442
+ const fmdSlide = element?.closest(".fmd-slide") as HTMLElement | null;
2443
+ if (
2444
+ !element ||
2445
+ !fmdSlide ||
2446
+ getComputedStyle(element).position !== "absolute"
2447
+ ) {
2448
+ return;
2449
+ }
2450
+ e.preventDefault();
2451
+ e.stopPropagation();
2452
+
2453
+ const slideRect = fmdSlide.getBoundingClientRect();
2454
+ const slideWidth = fmdSlide.offsetWidth;
2455
+ const slideHeight = fmdSlide.offsetHeight;
2456
+ if (
2457
+ !slideRect.width ||
2458
+ !slideRect.height ||
2459
+ !slideWidth ||
2460
+ !slideHeight
2461
+ ) {
2462
+ return;
2463
+ }
2464
+
2465
+ const origin = getObjectGeometry(element);
2466
+ const originalObjectId = element.getAttribute("data-slide-object-id");
2467
+ const startClientX = e.clientX;
2468
+ const startClientY = e.clientY;
2469
+ let resized = false;
2470
+ const selector = getBuilderSelector(element);
2471
+ if (selector) selectElementForStyling(element, selector, "resizing");
2472
+
2473
+ const stop = () => {
2474
+ window.removeEventListener("pointermove", onMove);
2475
+ window.removeEventListener("pointerup", onUp);
2476
+ window.removeEventListener("pointercancel", onCancel);
2477
+ if (activeGestureCancelRef.current === onCancel) {
2478
+ activeGestureCancelRef.current = null;
2479
+ }
2480
+ };
2481
+
2482
+ const onMove = (moveEvent: PointerEvent) => {
2483
+ const dx =
2484
+ ((moveEvent.clientX - startClientX) / slideRect.width) * slideWidth;
2485
+ const dy =
2486
+ ((moveEvent.clientY - startClientY) / slideRect.height) * slideHeight;
2487
+ if (!resized && Math.hypot(dx, dy) < 1) return;
2488
+ resized = true;
2489
+ ensureSlideObjectId(element);
2490
+ applyObjectGeometry(
2491
+ element,
2492
+ resizeSlideObject(origin, {
2493
+ handle,
2494
+ dx,
2495
+ dy,
2496
+ preserveAspectRatio: moveEvent.shiftKey,
2497
+ }),
2498
+ );
2499
+ const currentSelector = getBuilderSelector(element);
2500
+ if (currentSelector) {
2501
+ selectElementForStyling(element, currentSelector, "resizing");
2502
+ }
2503
+ };
2504
+
2505
+ const onUp = () => {
2506
+ stop();
2507
+ const currentSelector = getBuilderSelector(element);
2508
+ if (currentSelector) selectElementForStyling(element, currentSelector);
2509
+ if (!resized) return;
2510
+ const html = readCurrentSlideContentHtml();
2511
+ if (html !== null) onUpdateSlideRef.current({ content: html });
2512
+ };
2513
+
2514
+ const onCancel = () => {
2515
+ stop();
2516
+ applyObjectGeometry(element, origin);
2517
+ if (originalObjectId) {
2518
+ element.setAttribute("data-slide-object-id", originalObjectId);
2519
+ } else {
2520
+ element.removeAttribute("data-slide-object-id");
2521
+ }
2522
+ const selector = getBuilderSelector(element);
2523
+ if (selector) selectElementForStyling(element, selector);
2524
+ };
2525
+
2526
+ window.addEventListener("pointermove", onMove);
2527
+ window.addEventListener("pointerup", onUp);
2528
+ window.addEventListener("pointercancel", onCancel);
2529
+ activeGestureCancelRef.current = onCancel;
2530
+ },
2531
+ [
2532
+ applyObjectGeometry,
2533
+ getObjectGeometry,
2534
+ readCurrentSlideContentHtml,
2535
+ readOnly,
2536
+ resolveSelectedElement,
2537
+ selectElementForStyling,
2538
+ ],
2539
+ );
2540
+
2541
+ useEffect(() => {
2542
+ if (
2543
+ readOnly ||
2544
+ editingEl ||
2545
+ multiSelection.size > 0 ||
2546
+ !selectedElementSelector
2547
+ ) {
2548
+ return;
2549
+ }
2550
+ const onKey = (e: KeyboardEvent) => {
2551
+ if (!e.key.startsWith("Arrow")) return;
2552
+ const active = document.activeElement;
2553
+ if (
2554
+ active?.tagName === "INPUT" ||
2555
+ active?.tagName === "TEXTAREA" ||
2556
+ active?.tagName === "SELECT" ||
2557
+ (active instanceof HTMLElement && active.isContentEditable)
2558
+ ) {
2559
+ return;
2560
+ }
2561
+ const element = resolveSelectedElement();
2562
+ if (!element || getComputedStyle(element).position !== "absolute") {
2563
+ return;
2564
+ }
2565
+ const amount = e.shiftKey ? 10 : 1;
2566
+ const geometry = getObjectGeometry(element);
2567
+ if (e.key === "ArrowLeft") geometry.x -= amount;
2568
+ else if (e.key === "ArrowRight") geometry.x += amount;
2569
+ else if (e.key === "ArrowUp") geometry.y -= amount;
2570
+ else if (e.key === "ArrowDown") geometry.y += amount;
2571
+ else return;
2572
+
2573
+ e.preventDefault();
2574
+ ensureSlideObjectId(element);
2575
+ applyObjectGeometry(element, geometry);
2576
+ const html = readCurrentSlideContentHtml();
2577
+ if (html !== null) onUpdateSlideRef.current({ content: html });
2578
+ const selector = getBuilderSelector(element);
2579
+ if (selector) selectElementForStyling(element, selector);
2580
+ };
2581
+ window.addEventListener("keydown", onKey);
2582
+ return () => window.removeEventListener("keydown", onKey);
2583
+ }, [
2584
+ applyObjectGeometry,
2585
+ editingEl,
2586
+ getObjectGeometry,
2587
+ multiSelection.size,
2588
+ readCurrentSlideContentHtml,
2589
+ readOnly,
2590
+ resolveSelectedElement,
2591
+ selectElementForStyling,
2592
+ selectedElementSelector,
2593
+ ]);
2594
+
1784
2595
  // --- Marquee drag handlers (attached to slide-content via React props) ---
1785
2596
 
2597
+ const clearEdgeMoveCursor = useCallback(() => {
2598
+ if (slideCanvasRef.current) slideCanvasRef.current.style.cursor = "";
2599
+ }, []);
2600
+
2601
+ const handleSlidePointerMove = useCallback(
2602
+ (e: React.PointerEvent) => {
2603
+ if (editingEl || readOnly) {
2604
+ clearEdgeMoveCursor();
2605
+ return;
2606
+ }
2607
+ const selected = resolveSelectedElement();
2608
+ const shouldShowMoveCursor =
2609
+ selected !== null &&
2610
+ getComputedStyle(selected).position === "absolute" &&
2611
+ isWithinElementEdgeMoveBand(
2612
+ selected.getBoundingClientRect(),
2613
+ e.clientX,
2614
+ e.clientY,
2615
+ );
2616
+ if (slideCanvasRef.current) {
2617
+ slideCanvasRef.current.style.cursor = shouldShowMoveCursor
2618
+ ? "move"
2619
+ : "";
2620
+ }
2621
+ },
2622
+ [clearEdgeMoveCursor, editingEl, readOnly, resolveSelectedElement],
2623
+ );
2624
+
1786
2625
  const handleSlidePointerDown = useCallback(
1787
2626
  (e: React.PointerEvent) => {
1788
2627
  if (e.button !== 0) return; // left click only
@@ -1806,6 +2645,25 @@ export default function SlideEditor({
1806
2645
  }
1807
2646
  if (editingEl) return; // avoid interfering with an active inline edit
1808
2647
 
2648
+ const selected = resolveSelectedElement();
2649
+ const dragStart =
2650
+ selected && getComputedStyle(selected).position === "absolute"
2651
+ ? getSelectedObjectDragStart({
2652
+ targetWithinSelectedObject: selected.contains(target),
2653
+ pointerWithinMoveBand: isWithinElementEdgeMoveBand(
2654
+ selected.getBoundingClientRect(),
2655
+ e.clientX,
2656
+ e.clientY,
2657
+ ),
2658
+ })
2659
+ : null;
2660
+ if (dragStart && selected) {
2661
+ startElementDrag(e, selected, {
2662
+ preserveClickWithoutMove: dragStart === "body",
2663
+ });
2664
+ return;
2665
+ }
2666
+
1809
2667
  // Only start a marquee from "whitespace" inside the slide. Clicks on
1810
2668
  // an actual element fall through to handleSlideClick (which handles
1811
2669
  // shift/cmd-click toggle below).
@@ -1839,6 +2697,9 @@ export default function SlideEditor({
1839
2697
  exitInlineEdit,
1840
2698
  placeTextBoxAt,
1841
2699
  onExitTextBoxMode,
2700
+ getSelectedObjectDragStart,
2701
+ resolveSelectedElement,
2702
+ startElementDrag,
1842
2703
  ],
1843
2704
  );
1844
2705
 
@@ -1882,6 +2743,7 @@ export default function SlideEditor({
1882
2743
  const candidates = slideContent.querySelectorAll("[data-builder-id]");
1883
2744
  candidates.forEach((node) => {
1884
2745
  const el = node as HTMLElement;
2746
+ if (el.classList.contains("fmd-layout-spacer")) return;
1885
2747
  const id = el.getAttribute("data-builder-id");
1886
2748
  if (!id) return;
1887
2749
  // Skip the slide-content root itself if it ever got stamped
@@ -1944,6 +2806,32 @@ export default function SlideEditor({
1944
2806
  [getPlaceholderTarget],
1945
2807
  );
1946
2808
 
2809
+ // Browsers put the dragged element's outerHTML on the "text/html" data
2810
+ // type. Sniffing specifically for an <img> tag there (rather than trusting
2811
+ // any URL on text/uri-list or text/plain) matters because those same types
2812
+ // are also populated when dragging a plain link — e.g. a citation or CTA
2813
+ // button in the agent chat panel — and that URL is not an image. Without
2814
+ // this check, dragging any link onto a slide would replace/insert a broken
2815
+ // image.
2816
+ const extractDraggedImageUrl = useCallback(
2817
+ (dataTransfer: DataTransfer): string | null => {
2818
+ const html = dataTransfer.getData("text/html");
2819
+ if (!html) return null;
2820
+ // Parse instead of regex-matching the raw src attribute text: the
2821
+ // browser HTML-entity-escapes "&" (and other characters) when
2822
+ // serializing outerHTML for the drag payload, so a signed CDN URL like
2823
+ // "...?format=webp&width=800&height=1200" would come through as
2824
+ // "...&amp;width=..." if read verbatim. getAttribute() returns the
2825
+ // already-decoded value.
2826
+ const img = new DOMParser()
2827
+ .parseFromString(html, "text/html")
2828
+ .querySelector("img");
2829
+ const url = img?.getAttribute("src") || null;
2830
+ return url && /^https?:\/\//i.test(url) ? url : null;
2831
+ },
2832
+ [],
2833
+ );
2834
+
1947
2835
  const handleSlideDragOver = useCallback((e: React.DragEvent) => {
1948
2836
  const files = Array.from(e.dataTransfer.files ?? []);
1949
2837
  const items = Array.from(e.dataTransfer.items ?? []);
@@ -1953,7 +2841,12 @@ export default function SlideEditor({
1953
2841
  files.some(imageFileLooksSupported) ||
1954
2842
  items.some(
1955
2843
  (item) => item.kind === "file" && item.type.startsWith("image/"),
1956
- );
2844
+ ) ||
2845
+ // Dragging a rendered <img> (e.g. a generated-image preview in the
2846
+ // agent chat panel) rather than a native OS file. dragover can't read
2847
+ // getData() payloads (only types) in most browsers, so this is a
2848
+ // best-effort signal; the drop handler does the real <img> check.
2849
+ (types.includes("text/html") && types.includes("text/uri-list"));
1957
2850
  if (!hasImage) return;
1958
2851
  e.preventDefault();
1959
2852
  e.dataTransfer.dropEffect = "copy";
@@ -1963,16 +2856,38 @@ export default function SlideEditor({
1963
2856
  (e: React.DragEvent) => {
1964
2857
  const files = Array.from(e.dataTransfer.files ?? []);
1965
2858
  const file = files.find(imageFileLooksSupported);
1966
- if (files.length === 0) return;
2859
+ if (files.length > 0) {
2860
+ e.preventDefault();
2861
+ e.stopPropagation();
2862
+ if (!file) return;
2863
+ onDropImage?.(
2864
+ getImageReplacementTarget(e.target as HTMLElement),
2865
+ file,
2866
+ { x: e.clientX, y: e.clientY },
2867
+ );
2868
+ return;
2869
+ }
2870
+ // No native file — check for a dragged <img> instead (e.g. one dragged
2871
+ // out of the agent chat panel's generated-image preview).
2872
+ const url = extractDraggedImageUrl(e.dataTransfer);
2873
+ if (!url) return;
1967
2874
  e.preventDefault();
1968
2875
  e.stopPropagation();
1969
- if (!file) return;
1970
- onDropImage?.(getImageReplacementTarget(e.target as HTMLElement), file, {
1971
- x: e.clientX,
1972
- y: e.clientY,
1973
- });
2876
+ onDropImageUrl?.(
2877
+ getImageReplacementTarget(e.target as HTMLElement),
2878
+ url,
2879
+ {
2880
+ x: e.clientX,
2881
+ y: e.clientY,
2882
+ },
2883
+ );
1974
2884
  },
1975
- [getImageReplacementTarget, onDropImage],
2885
+ [
2886
+ extractDraggedImageUrl,
2887
+ getImageReplacementTarget,
2888
+ onDropImage,
2889
+ onDropImageUrl,
2890
+ ],
1976
2891
  );
1977
2892
 
1978
2893
  const handleSlideClick = useCallback(
@@ -2078,13 +2993,62 @@ export default function SlideEditor({
2078
2993
  [showImageOverlay],
2079
2994
  );
2080
2995
 
2996
+ const preserveRichTextSelection = useCallback(() => {
2997
+ const editing = editingElRef.current;
2998
+ const selection = window.getSelection();
2999
+ if (!editing || !selection || selection.rangeCount !== 1) return;
3000
+ const range = selection.getRangeAt(0);
3001
+ if (
3002
+ editing.contains(range.startContainer) &&
3003
+ editing.contains(range.endContainer)
3004
+ ) {
3005
+ richTextSelectionRef.current = snapshotEditableTextRange(
3006
+ editing,
3007
+ selection,
3008
+ );
3009
+ }
3010
+ }, []);
3011
+
2081
3012
  const applySelectedStylePatch = useCallback(
2082
3013
  (patch: SlideStylePatch) => {
2083
- const element = resolveSelectedElement();
3014
+ const editing = editingElRef.current;
3015
+ const element = editing ?? resolveSelectedElement();
2084
3016
  if (!element || !selectedElementSelector) return;
2085
3017
 
3018
+ const inlinePatch = inlineInspectorStylePatch(patch);
3019
+ const inlineKeys = INLINE_INSPECTOR_STYLE_KEYS.filter(
3020
+ (key) => patch[key] !== undefined,
3021
+ );
3022
+ let styledRange = false;
3023
+ const savedRange =
3024
+ richTextSelectionRef.current ??
3025
+ (editing ? snapshotEditableTextRange(editing) : null);
3026
+ if (
3027
+ editing &&
3028
+ inlineKeys.length > 0 &&
3029
+ restoreEditableTextRange(editing, savedRange)
3030
+ ) {
3031
+ const result = applyInlineTextStyle(editing, inlinePatch);
3032
+ if (result.scope === "selection" && result.range) {
3033
+ richTextSelectionRef.current = result.range.cloneRange();
3034
+ styledRange = true;
3035
+ }
3036
+ }
3037
+
3038
+ if (!styledRange && inlineKeys.length > 0) {
3039
+ applyDescendantTextStyle(element, inlinePatch);
3040
+ }
3041
+
2086
3042
  for (const [property, value] of Object.entries(patch)) {
2087
3043
  if (value === undefined) continue;
3044
+ if (
3045
+ styledRange &&
3046
+ INLINE_INSPECTOR_STYLE_KEYS.includes(
3047
+ property as (typeof INLINE_INSPECTOR_STYLE_KEYS)[number],
3048
+ )
3049
+ ) {
3050
+ continue;
3051
+ }
2088
3052
  element.style.setProperty(stylePropertyName(property), value);
2089
3053
  }
2090
3054
 
@@ -2097,95 +3061,48 @@ export default function SlideEditor({
2097
3061
  element.style.borderStyle = "solid";
2098
3062
  }
2099
3063
 
2100
- const html = readCurrentSlideContentHtml();
2101
- if (html !== null) {
2102
- onUpdateSlideRef.current({ content: html });
3064
+ if (editing) {
3065
+ captureInlineEditDraft(slide.id);
3066
+ } else {
3067
+ const html = readCurrentSlideContentHtml();
3068
+ if (html !== null) {
3069
+ onUpdateSlideRef.current({ content: html });
3070
+ }
2103
3071
  }
2104
3072
 
2105
- const snapshot = buildStyleSnapshot(element, selectedElementSelector);
2106
- setSelectedElementRect(element.getBoundingClientRect());
2107
- setSelectedStyleSnapshot(snapshot);
2108
- syncSelectionToAppState(
2109
- buildSelectionState(snapshot.isImage ? "image" : "single", [
2110
- {
2111
- selector: selectedElementSelector,
2112
- kind: snapshot.isImage ? "image" : "element",
2113
- tagName: snapshot.tagName,
2114
- text: snapshot.textPreview,
2115
- imageSrc:
2116
- element instanceof HTMLImageElement
2117
- ? (element.getAttribute("src") ?? undefined)
2118
- : undefined,
2119
- style: snapshot,
2120
- },
2121
- ]),
3073
+ const inlineTextStyle = editing
3074
+ ? getInlineTextStyleSnapshotForRange(
3075
+ editing,
3076
+ richTextSelectionRef.current,
3077
+ )
3078
+ : undefined;
3079
+ const snapshot = buildStyleSnapshot(
3080
+ element,
3081
+ selectedElementSelector,
3082
+ inlineTextStyle,
2122
3083
  );
3084
+ if (!editing) {
3085
+ setSelectedElementRect(element.getBoundingClientRect());
3086
+ }
3087
+ setSelectedStyleSnapshot(snapshot);
3088
+ if (!editing) {
3089
+ syncSelectionToAppState(
3090
+ buildSelectionState(getSlideSelectionMode(snapshot), [
3091
+ selectionItemForElement(element, selectedElementSelector, snapshot),
3092
+ ]),
3093
+ );
3094
+ }
2123
3095
  },
2124
3096
  [
2125
3097
  buildSelectionState,
3098
+ captureInlineEditDraft,
2126
3099
  readCurrentSlideContentHtml,
2127
3100
  resolveSelectedElement,
2128
3101
  selectedElementSelector,
3102
+ slide.id,
2129
3103
  ],
2130
3104
  );
2131
3105
 
2132
- /**
2133
- * Drag-to-reposition for the selected element. Only elements that are (or
2134
- * can safely become) absolutely positioned support this — repositioning an
2135
- * in-flow element would shift the rest of the slide's layout. Percentages
2136
- * (not px) are used for left/top so the drag stays accurate under canvas
2137
- * zoom and the autofit scale transform, matching placeTextBoxAt.
2138
- */
2139
- const startElementDrag = useCallback(
2140
- (e: React.PointerEvent) => {
2141
- const element = resolveSelectedElement();
2142
- if (!element) return;
2143
- const fmdSlide = element.closest(".fmd-slide") as HTMLElement | null;
2144
- if (!fmdSlide) return;
2145
- // left/top only place an element at an absolute coordinate for
2146
- // position: absolute (or fixed). For position: relative they're an
2147
- // *offset* from the element's normal flow position instead, so
2148
- // treating a relative element as draggable here would make it jump by
2149
- // the wrong amount. Restrict dragging to elements already out of flow.
2150
- if (getComputedStyle(element).position !== "absolute") return;
2151
-
2152
- e.preventDefault();
2153
- e.stopPropagation();
2154
-
2155
- const slideRect = fmdSlide.getBoundingClientRect();
2156
- const elRect = element.getBoundingClientRect();
2157
- const startXPct =
2158
- ((elRect.left - slideRect.left) / slideRect.width) * 100;
2159
- const startYPct = ((elRect.top - slideRect.top) / slideRect.height) * 100;
2160
- const startClientX = e.clientX;
2161
- const startClientY = e.clientY;
2162
- let moved = false;
2163
-
2164
- const onMove = (moveEvent: PointerEvent) => {
2165
- moved = true;
2166
- const dxPct =
2167
- ((moveEvent.clientX - startClientX) / slideRect.width) * 100;
2168
- const dyPct =
2169
- ((moveEvent.clientY - startClientY) / slideRect.height) * 100;
2170
- element.style.left = `${startXPct + dxPct}%`;
2171
- element.style.top = `${startYPct + dyPct}%`;
2172
- setSelectedElementRect(element.getBoundingClientRect());
2173
- };
2174
-
2175
- const onUp = () => {
2176
- window.removeEventListener("pointermove", onMove);
2177
- window.removeEventListener("pointerup", onUp);
2178
- if (!moved) return;
2179
- const html = readCurrentSlideContentHtml();
2180
- if (html !== null) onUpdateSlideRef.current({ content: html });
2181
- };
2182
-
2183
- window.addEventListener("pointermove", onMove);
2184
- window.addEventListener("pointerup", onUp);
2185
- },
2186
- [resolveSelectedElement, readCurrentSlideContentHtml],
2187
- );
2188
-
2189
3106
  // --- Pending visual updates ---
2190
3107
  const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
2191
3108
 
@@ -2344,14 +3261,14 @@ export default function SlideEditor({
2344
3261
  onContextMenu={handleSlideContextMenu}
2345
3262
  onDoubleClick={handleSlideDoubleClick}
2346
3263
  onPointerDown={handleSlidePointerDown}
3264
+ onPointerMove={handleSlidePointerMove}
3265
+ onPointerLeave={clearEdgeMoveCursor}
2347
3266
  onDragOver={handleSlideDragOver}
2348
3267
  onDrop={handleSlideDrop}
2349
- onMouseEnter={() => setIsHoveringText(true)}
2350
- onMouseLeave={() => setIsHoveringText(false)}
2351
3268
  >
2352
3269
  <SlideRenderer
2353
3270
  slide={slide}
2354
- className={`shadow-2xl shadow-black/40 ${isHoveringText ? "ring-2 ring-[#609FF8]/60" : ""}`}
3271
+ className="shadow-2xl shadow-black/40"
2355
3272
  designSystem={designSystem}
2356
3273
  aspectRatio={aspectRatio}
2357
3274
  onOverflowChange={handleOverflowChange}
@@ -2365,24 +3282,14 @@ export default function SlideEditor({
2365
3282
  containerRef={slideCanvasRef}
2366
3283
  />
2367
3284
  )}
2368
- {/* Double-click hint only shown for HTML slides that support inline editing */}
2369
- {isHoveringText && !editingEl && isHtmlSlide && (
2370
- <div className="absolute bottom-2 left-1/2 -translate-x-1/2 rounded bg-black/60 px-2 py-0.5 text-xs text-white/40 pointer-events-none select-none">
2371
- {t("raw.doubleClickEdit")}
2372
- </div>
2373
- )}
2374
- {agentActive && (
2375
- <div className="absolute top-2 right-2 z-10 pointer-events-none">
2376
- <AgentPresenceChip active={agentActive} />
2377
- </div>
2378
- )}
2379
- {presentUsers.length > 0 && (
2380
- <div
2381
- className={`absolute right-2 z-10 ${
2382
- agentActive ? "top-11" : "top-2"
2383
- }`}
2384
- >
2385
- <SameSlidePresenceIndicator users={presentUsers} />
3285
+ {(agentActive || passivePresentUsers.length > 0) && (
3286
+ <div className="absolute right-2 top-2 z-10 flex items-center gap-2">
3287
+ <AgentPresenceChip active={Boolean(agentActive)} />
3288
+ {passivePresentUsers.length > 0 && (
3289
+ <SameSlidePresenceIndicator
3290
+ users={passivePresentUsers}
3291
+ />
3292
+ )}
2386
3293
  </div>
2387
3294
  )}
2388
3295
  {overflowInfo && !readOnly && !agentActive && (
@@ -2414,10 +3321,11 @@ export default function SlideEditor({
2414
3321
  )}
2415
3322
  </div>
2416
3323
 
2417
- {!readOnly && (
3324
+ {!readOnly && stylePanelOpen && (
2418
3325
  <div
2419
3326
  className="relative z-[70] hidden h-full w-[17rem] shrink-0 border-l border-border/70 bg-background/95 lg:block"
2420
3327
  data-slide-style-dock="true"
3328
+ onPointerDownCapture={preserveRichTextSelection}
2421
3329
  >
2422
3330
  {selectedStyleSnapshot ? (
2423
3331
  <SlideStyleInspector
@@ -2428,13 +3336,29 @@ export default function SlideEditor({
2428
3336
  onClose={() => {
2429
3337
  clearSelectedElement();
2430
3338
  syncSelectionToAppState(null);
3339
+ onCloseStylePanel?.();
2431
3340
  }}
2432
3341
  />
2433
3342
  ) : (
2434
- <div className="flex h-11 items-center border-b border-border/70 px-3">
3343
+ <div className="flex h-11 items-center justify-between border-b border-border/70 px-3">
2435
3344
  <span className="text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground/70">
2436
3345
  {t("styleInspector.title")}
2437
3346
  </span>
3347
+ <Tooltip>
3348
+ <TooltipTrigger asChild>
3349
+ <Button
3350
+ type="button"
3351
+ variant="ghost"
3352
+ size="icon"
3353
+ className="size-7 cursor-pointer text-muted-foreground hover:text-foreground"
3354
+ onClick={onCloseStylePanel}
3355
+ aria-label={t("styleInspector.close")}
3356
+ >
3357
+ <IconX className="size-3.5" />
3358
+ </Button>
3359
+ </TooltipTrigger>
3360
+ <TooltipContent>{t("styleInspector.close")}</TooltipContent>
3361
+ </Tooltip>
2438
3362
  </div>
2439
3363
  )}
2440
3364
  </div>
@@ -2458,8 +3382,10 @@ export default function SlideEditor({
2458
3382
  <ElementSelectionOutline
2459
3383
  rect={selectedElementRect}
2460
3384
  viewportRect={selectionViewportRect}
2461
- onDragStart={
2462
- isSelectedElementDraggable ? startElementDrag : undefined
3385
+ onResizeStart={
3386
+ !readOnly && isSelectedElementDraggable
3387
+ ? startElementResize
3388
+ : undefined
2463
3389
  }
2464
3390
  />
2465
3391
  )}