@agent-native/core 0.141.7 → 0.144.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/agent-native.eject.json +9 -0
  2. package/corpus/README.md +1 -1
  3. package/corpus/templates/analytics/.agents/skills/provider-api/SKILL.md +19 -0
  4. package/corpus/templates/analytics/.agents/skills/turn-into-app/SKILL.md +296 -0
  5. package/corpus/templates/analytics/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  6. package/corpus/templates/analytics/.agents/skills/turn-into-skill/SKILL.md +112 -0
  7. package/corpus/templates/analytics/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  8. package/corpus/templates/analytics/AGENTS.md +35 -38
  9. package/corpus/templates/analytics/actions/ensure-native-v2-dashboards.ts +1 -1
  10. package/corpus/templates/analytics/actions/list-dashboard-usage-stats.ts +17 -22
  11. package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +1 -1
  12. package/corpus/templates/analytics/actions/navigate.ts +4 -5
  13. package/corpus/templates/analytics/actions/provider-api-register.ts +50 -0
  14. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +44 -11
  15. package/corpus/templates/analytics/actions/test-custom-api-connection.ts +106 -0
  16. package/corpus/templates/analytics/app/components/CustomApiCard.tsx +430 -0
  17. package/corpus/templates/analytics/app/i18n-data.ts +680 -0
  18. package/corpus/templates/analytics/app/lib/native-mcp-exclusions.ts +6 -0
  19. package/corpus/templates/analytics/app/lib/sql-query.ts +33 -6
  20. package/corpus/templates/analytics/app/pages/DataSources.tsx +4 -0
  21. package/corpus/templates/analytics/changelog/2026-08-06-analytics-dashboards-load-reliably-during-background-data-sw.md +6 -0
  22. package/corpus/templates/analytics/changelog/2026-08-06-analytics-dashboards-remain-responsive-while-historical-data.md +6 -0
  23. package/corpus/templates/analytics/changelog/2026-08-06-analytics-query-results-now-render-as-a-table-with-csv-downl.md +6 -0
  24. package/corpus/templates/analytics/changelog/2026-08-06-connect-public-https-custom-apis-and-hand-tested-endpoints-t.md +6 -0
  25. package/corpus/templates/analytics/changelog/2026-08-06-durable-agent-worker-memory.md +6 -0
  26. package/corpus/templates/analytics/changelog/2026-08-06-live-dashboard-panels-use-the-shared-query-timeout.md +6 -0
  27. package/corpus/templates/analytics/changelog/2026-08-06-on-demand-billing-is-available-as-a-native-dashboard-with-ta.md +6 -0
  28. package/corpus/templates/analytics/changelog/2026-08-06-visible-dashboard-names-are-unique.md +6 -0
  29. package/corpus/templates/analytics/netlify.toml +7 -0
  30. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +4 -0
  31. package/corpus/templates/analytics/server/db/index.ts +27 -0
  32. package/corpus/templates/analytics/server/db/schema.ts +10 -0
  33. package/corpus/templates/analytics/server/lib/dashboards-store.ts +227 -60
  34. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +156 -45
  35. package/corpus/templates/analytics/server/lib/native-v2-dashboards.ts +227 -1
  36. package/corpus/templates/analytics/server/plugins/agent-chat.ts +5 -0
  37. package/corpus/templates/analytics/server/plugins/analytics-rollup-backfill-jobs.ts +1 -0
  38. package/corpus/templates/analytics/server/plugins/db.ts +52 -18
  39. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +28 -2
  40. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +16 -1
  41. package/corpus/templates/assets/.agents/skills/turn-into-app/SKILL.md +296 -0
  42. package/corpus/templates/assets/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  43. package/corpus/templates/assets/.agents/skills/turn-into-skill/SKILL.md +112 -0
  44. package/corpus/templates/assets/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  45. package/corpus/templates/brain/.agents/skills/turn-into-app/SKILL.md +296 -0
  46. package/corpus/templates/brain/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  47. package/corpus/templates/brain/.agents/skills/turn-into-skill/SKILL.md +112 -0
  48. package/corpus/templates/brain/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  49. package/corpus/templates/brain/actions/list-connection-providers.ts +24 -3
  50. package/corpus/templates/brain/app/lib/dispatch-links.ts +44 -0
  51. package/corpus/templates/brain/app/routes/sources.tsx +15 -7
  52. package/corpus/templates/brain/changelog/2026-08-06-brain-provider-setup-links-open-dispatch-correctly-in-standa.md +6 -0
  53. package/corpus/templates/brain/server/plugins/agent-chat.ts +1 -0
  54. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +26 -0
  55. package/corpus/templates/calendar/.agents/skills/turn-into-app/SKILL.md +296 -0
  56. package/corpus/templates/calendar/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  57. package/corpus/templates/calendar/.agents/skills/turn-into-skill/SKILL.md +112 -0
  58. package/corpus/templates/calendar/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  59. package/corpus/templates/calendar/AGENTS.md +11 -32
  60. package/corpus/templates/calendar/actions/update-event.ts +107 -1
  61. package/corpus/templates/calendar/app/components/calendar/EventCalendarSelect.tsx +83 -0
  62. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +57 -0
  63. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +59 -73
  64. package/corpus/templates/calendar/app/hooks/use-events.ts +4 -1
  65. package/corpus/templates/calendar/app/i18n-data.ts +20 -0
  66. package/corpus/templates/calendar/changelog/2026-08-06-delegated-requests-continue-in-the-background.md +6 -0
  67. package/corpus/templates/calendar/changelog/2026-08-06-existing-events-can-move-between-connected-google-account-ca.md +6 -0
  68. package/corpus/templates/calendar/server/lib/google-calendar.ts +101 -0
  69. package/corpus/templates/calendar/server/plugins/agent-chat.ts +7 -1
  70. package/corpus/templates/chat/.agents/skills/turn-into-app/SKILL.md +296 -0
  71. package/corpus/templates/chat/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  72. package/corpus/templates/chat/.agents/skills/turn-into-skill/SKILL.md +112 -0
  73. package/corpus/templates/chat/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  74. package/corpus/templates/clips/.agents/skills/turn-into-app/SKILL.md +296 -0
  75. package/corpus/templates/clips/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  76. package/corpus/templates/clips/.agents/skills/turn-into-skill/SKILL.md +112 -0
  77. package/corpus/templates/clips/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  78. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +9 -24
  79. package/corpus/templates/clips/app/components/editor/timeline-geometry.ts +15 -0
  80. package/corpus/templates/clips/app/components/editor/waveform.tsx +4 -17
  81. package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +14 -3
  82. package/corpus/templates/clips/app/components/player/player-controls.tsx +0 -3
  83. package/corpus/templates/clips/app/components/player/scrubber.tsx +2 -33
  84. package/corpus/templates/clips/app/components/player/video-player.tsx +58 -11
  85. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +28 -38
  86. package/corpus/templates/clips/app/routes/share.$shareId.tsx +21 -27
  87. package/corpus/templates/clips/changelog/2026-08-06-clips-recovers-from-stalled-recording-setup-without-requirin.md +6 -0
  88. package/corpus/templates/clips/changelog/2026-08-06-clips-starts-faster-after-a-deploy-or-idle-period-startup-no.md +6 -0
  89. package/corpus/templates/clips/changelog/2026-08-06-deleting-a-recording-returns-cleanly-to-the-library-and-fit-.md +6 -0
  90. package/corpus/templates/clips/changelog/2026-08-06-hide-clip-insights-from-viewers-without-analytics-access.md +6 -0
  91. package/corpus/templates/clips/changelog/2026-08-06-trimmed-clips-play-on-a-clean-timeline-without-cut-markers.md +6 -0
  92. package/corpus/templates/clips/desktop/src/app.tsx +48 -20
  93. package/corpus/templates/clips/desktop/src/lib/recorder.ts +275 -74
  94. package/corpus/templates/clips/desktop/src/lib/recording-start-guard.ts +84 -0
  95. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +14 -4
  96. package/corpus/templates/clips/server/plugins/db.ts +214 -26
  97. package/corpus/templates/content/.agents/skills/turn-into-app/SKILL.md +296 -0
  98. package/corpus/templates/content/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  99. package/corpus/templates/content/.agents/skills/turn-into-skill/SKILL.md +112 -0
  100. package/corpus/templates/content/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  101. package/corpus/templates/crm/.agents/skills/turn-into-app/SKILL.md +296 -0
  102. package/corpus/templates/crm/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  103. package/corpus/templates/crm/.agents/skills/turn-into-skill/SKILL.md +112 -0
  104. package/corpus/templates/crm/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  105. package/corpus/templates/crm/changelog/2026-08-06-crm-starts-faster-after-a-deploy-or-idle-period-one-time-dat.md +6 -0
  106. package/corpus/templates/crm/server/plugins/db.ts +11 -8
  107. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +36 -1
  108. package/corpus/templates/design/.agents/skills/turn-into-app/SKILL.md +296 -0
  109. package/corpus/templates/design/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  110. package/corpus/templates/design/.agents/skills/turn-into-skill/SKILL.md +112 -0
  111. package/corpus/templates/design/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  112. package/corpus/templates/design/actions/import-from-url.ts +1 -1
  113. package/corpus/templates/design/app/components/design/LayersPanel.tsx +27 -18
  114. package/corpus/templates/design/app/pages/Index.tsx +6 -2
  115. package/corpus/templates/design/changelog/2026-08-05-deleting-a-design-from-the-home-page-no-longer-leaves-the-ap.md +6 -0
  116. package/corpus/templates/design/changelog/2026-08-05-renaming-a-layer-now-focuses-its-name-field-automatically.md +6 -0
  117. package/corpus/templates/design/server/plugins/agent-chat.ts +2 -0
  118. package/corpus/templates/dispatch/.agents/skills/turn-into-app/SKILL.md +296 -0
  119. package/corpus/templates/dispatch/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  120. package/corpus/templates/dispatch/.agents/skills/turn-into-skill/SKILL.md +112 -0
  121. package/corpus/templates/dispatch/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  122. package/corpus/templates/dispatch/AGENTS.md +8 -1
  123. package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -0
  124. package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +30 -15
  125. package/corpus/templates/dispatch/app/i18n-data.ts +131 -0
  126. package/corpus/templates/dispatch/app/routes/admin._index.tsx +4 -0
  127. package/corpus/templates/dispatch/app/routes/admin.agents.tsx +1 -0
  128. package/corpus/templates/dispatch/app/routes/admin.approvals.tsx +1 -0
  129. package/corpus/templates/dispatch/app/routes/admin.apps.$appId.tsx +1 -0
  130. package/corpus/templates/dispatch/app/routes/admin.apps.tsx +1 -0
  131. package/corpus/templates/dispatch/app/routes/admin.audit.tsx +1 -0
  132. package/corpus/templates/dispatch/app/routes/admin.automations.tsx +1 -0
  133. package/corpus/templates/dispatch/app/routes/admin.destinations.tsx +4 -0
  134. package/corpus/templates/dispatch/app/routes/admin.dreams.tsx +1 -0
  135. package/corpus/templates/dispatch/app/routes/admin.identities.tsx +1 -0
  136. package/corpus/templates/dispatch/app/routes/admin.integrations.tsx +4 -0
  137. package/corpus/templates/dispatch/app/routes/admin.messaging.tsx +1 -0
  138. package/corpus/templates/dispatch/app/routes/admin.metrics.tsx +1 -0
  139. package/corpus/templates/dispatch/app/routes/admin.new-app.tsx +1 -0
  140. package/corpus/templates/dispatch/app/routes/admin.operations.tsx +1 -0
  141. package/corpus/templates/dispatch/app/routes/admin.thread-debug.tsx +4 -0
  142. package/corpus/templates/dispatch/app/routes/admin.transactional-email.$appId.$id.tsx +4 -0
  143. package/corpus/templates/dispatch/app/routes/admin.transactional-email.tsx +4 -0
  144. package/corpus/templates/dispatch/app/routes/admin.tsx +1 -0
  145. package/corpus/templates/dispatch/app/routes/admin.vault.tsx +1 -0
  146. package/corpus/templates/dispatch/app/routes/admin.workspace.tsx +1 -0
  147. package/corpus/templates/dispatch/changelog/2026-08-06-dispatch-keeps-everyday-navigation-focused-while-management-.md +6 -0
  148. package/corpus/templates/dispatch/changelog/2026-08-06-ready-workspace-apps-stay-one-click-away-in-a-compact-dispat.md +6 -0
  149. package/corpus/templates/dispatch/changelog/2026-08-06-shared-vault-values-and-management-are-now-restricted-to-wor.md +6 -0
  150. package/corpus/templates/factory/.agents/skills/turn-into-app/SKILL.md +296 -0
  151. package/corpus/templates/factory/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  152. package/corpus/templates/factory/.agents/skills/turn-into-skill/SKILL.md +112 -0
  153. package/corpus/templates/factory/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  154. package/corpus/templates/forms/.agents/skills/turn-into-app/SKILL.md +296 -0
  155. package/corpus/templates/forms/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  156. package/corpus/templates/forms/.agents/skills/turn-into-skill/SKILL.md +112 -0
  157. package/corpus/templates/forms/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  158. package/corpus/templates/forms/changelog/2026-08-06-delegated-requests-continue-in-the-background.md +6 -0
  159. package/corpus/templates/forms/server/plugins/agent-chat.ts +4 -0
  160. package/corpus/templates/macros/.agents/skills/turn-into-app/SKILL.md +296 -0
  161. package/corpus/templates/macros/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  162. package/corpus/templates/macros/.agents/skills/turn-into-skill/SKILL.md +112 -0
  163. package/corpus/templates/macros/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  164. package/corpus/templates/mail/.agents/skills/turn-into-app/SKILL.md +296 -0
  165. package/corpus/templates/mail/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  166. package/corpus/templates/mail/.agents/skills/turn-into-skill/SKILL.md +112 -0
  167. package/corpus/templates/mail/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  168. package/corpus/templates/mail/changelog/2026-08-06-delegated-requests-continue-in-the-background.md +6 -0
  169. package/corpus/templates/mail/server/plugins/agent-chat.ts +6 -0
  170. package/corpus/templates/plan/.agents/skills/turn-into-app/SKILL.md +296 -0
  171. package/corpus/templates/plan/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  172. package/corpus/templates/plan/.agents/skills/turn-into-skill/SKILL.md +112 -0
  173. package/corpus/templates/plan/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  174. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +48 -5
  175. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +37 -1
  176. package/corpus/templates/slides/.agents/skills/image-generation-via-a2a/SKILL.md +6 -0
  177. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +15 -2
  178. package/corpus/templates/slides/.agents/skills/slide-images/SKILL.md +29 -24
  179. package/corpus/templates/slides/.agents/skills/turn-into-app/SKILL.md +296 -0
  180. package/corpus/templates/slides/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  181. package/corpus/templates/slides/.agents/skills/turn-into-skill/SKILL.md +112 -0
  182. package/corpus/templates/slides/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  183. package/corpus/templates/slides/AGENTS.md +17 -21
  184. package/corpus/templates/slides/_gitignore +1 -0
  185. package/corpus/templates/slides/actions/export-pptx.ts +470 -36
  186. package/corpus/templates/slides/actions/get-deck.ts +12 -0
  187. package/corpus/templates/slides/actions/import-file.ts +175 -139
  188. package/corpus/templates/slides/actions/import-google-slides-reference.ts +271 -12
  189. package/corpus/templates/slides/actions/import-pptx.ts +193 -68
  190. package/corpus/templates/slides/actions/patch-deck.ts +51 -0
  191. package/corpus/templates/slides/actions/update-slide.ts +29 -3
  192. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +10 -13
  193. package/corpus/templates/slides/app/components/editor/AddSlidePopover.tsx +5 -1
  194. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +25 -12
  195. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +112 -57
  196. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +202 -0
  197. package/corpus/templates/slides/app/components/editor/GoogleSlidesReferenceImport.tsx +26 -14
  198. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +173 -0
  199. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +182 -141
  200. package/corpus/templates/slides/app/components/editor/canvas-interactions/index.ts +1 -0
  201. package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +18 -0
  202. package/corpus/templates/slides/app/components/editor/slide-text-targets.ts +85 -0
  203. package/corpus/templates/slides/app/components/layout/Layout.tsx +3 -0
  204. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +15 -0
  205. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +27 -1
  206. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  207. package/corpus/templates/slides/app/lib/create-deck-generation.ts +72 -11
  208. package/corpus/templates/slides/app/lib/deck-preview-frame.ts +15 -0
  209. package/corpus/templates/slides/app/lib/deck-share-links.ts +9 -0
  210. package/corpus/templates/slides/app/lib/export-pptx-client.ts +89 -0
  211. package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +89 -0
  212. package/corpus/templates/slides/app/lib/slide-animation-elements.ts +3 -0
  213. package/corpus/templates/slides/app/pages/Index.tsx +172 -15
  214. package/corpus/templates/slides/changelog/2026-08-06-clicking-the-gray-canvas-around-a-slide-clears-its-selection.md +6 -0
  215. package/corpus/templates/slides/changelog/2026-08-06-deck-thumbnails-keep-a-consistent-listing-frame-across-aspec.md +6 -0
  216. package/corpus/templates/slides/changelog/2026-08-06-deep-linked-decks-open-directly-instead-of-being-interrupted.md +6 -0
  217. package/corpus/templates/slides/changelog/2026-08-06-google-slides-exports-now-show-clear-import-steps-when-direc.md +6 -0
  218. package/corpus/templates/slides/changelog/2026-08-06-import-pdf-google-slides-and-powerpoint-decks-directly-from-.md +6 -0
  219. package/corpus/templates/slides/changelog/2026-08-06-imported-powerpoint-exports-keep-text-and-images-editable-an.md +6 -0
  220. package/corpus/templates/slides/changelog/2026-08-06-presentation-prompts-continue-adding-rendered-slides-until-t.md +6 -0
  221. package/corpus/templates/slides/changelog/2026-08-06-public-decks-now-share-read-only-presentation-links-by-defau.md +6 -0
  222. package/corpus/templates/slides/changelog/2026-08-06-slides-offers-a-direct-connect-google-button-when-a-drive-co.md +6 -0
  223. package/corpus/templates/slides/changelog/2026-08-06-slides-preserve-uploaded-pdf-layouts-accept-larger-reference.md +6 -0
  224. package/corpus/templates/slides/changelog/2026-08-06-styled-words-stay-part-of-their-text-box-instead-of-becoming.md +6 -0
  225. package/corpus/templates/slides/changelog/2026-08-06-uploaded-decks-keep-their-slides-and-visuals-when-you-improv.md +6 -0
  226. package/corpus/templates/slides/server/handlers/google-docs-auth.ts +7 -20
  227. package/corpus/templates/slides/server/handlers/import/html-converter.ts +247 -13
  228. package/corpus/templates/slides/server/handlers/import/pptx-assets.ts +67 -0
  229. package/corpus/templates/slides/server/handlers/import/pptx-parser.ts +2 -0
  230. package/corpus/templates/slides/server/handlers/uploads.ts +7 -3
  231. package/corpus/templates/slides/server/lib/chat-attachments.ts +7 -4
  232. package/corpus/templates/slides/server/lib/emails.ts +7 -2
  233. package/corpus/templates/slides/server/lib/google-docs-error.ts +50 -0
  234. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +1 -0
  235. package/corpus/templates/slides/server/lib/import-asset-storage.ts +127 -0
  236. package/corpus/templates/slides/server/lib/source-import.ts +167 -0
  237. package/corpus/templates/slides/server/plugins/agent-chat.ts +12 -3
  238. package/corpus/templates/slides/server/routes/api/import-assets/[token].get.ts +70 -0
  239. package/corpus/templates/slides/shared/upload-types.ts +3 -0
  240. package/corpus/templates/tasks/.agents/skills/turn-into-app/SKILL.md +296 -0
  241. package/corpus/templates/tasks/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  242. package/corpus/templates/tasks/.agents/skills/turn-into-skill/SKILL.md +112 -0
  243. package/corpus/templates/tasks/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  244. package/dist/agent/production-agent.d.ts +17 -0
  245. package/dist/agent/production-agent.js +72 -1
  246. package/dist/cli/agent-plugin.d.ts +112 -0
  247. package/dist/cli/agent-plugin.js +1081 -0
  248. package/dist/cli/app-skill.js +39 -1
  249. package/dist/cli/code-agent-executor.d.ts +3 -0
  250. package/dist/cli/code-agent-executor.js +51 -13
  251. package/dist/cli/index.js +13 -0
  252. package/dist/cli/templates-meta.js +1 -1
  253. package/dist/client/AgentPanel.d.ts +3 -1
  254. package/dist/client/AgentPanel.js +9 -2
  255. package/dist/client/AssistantChat.d.ts +8 -0
  256. package/dist/client/AssistantChat.js +23 -8
  257. package/dist/client/NewWorkspaceAppFlow.js +1 -0
  258. package/dist/client/agent-chat-adapter.js +123 -4
  259. package/dist/client/assistant-ui-recovery.d.ts +2 -1
  260. package/dist/client/assistant-ui-recovery.js +7 -3
  261. package/dist/client/onboarding/FirstRunOnboarding.js +11 -4
  262. package/dist/client/org/TeamPage.js +1 -1
  263. package/dist/client/require-session.js +1 -1
  264. package/dist/client/resources/McpConnectionSuggestion.d.ts +1 -0
  265. package/dist/client/resources/McpConnectionSuggestion.js +27 -10
  266. package/dist/client/resources/McpIntegrationDialog.js +77 -45
  267. package/dist/client/resources/mcp-integration-catalog.d.ts +13 -1
  268. package/dist/client/resources/mcp-integration-catalog.js +255 -2
  269. package/dist/client/resources/mcp-integration-logos.js +18 -2
  270. package/dist/client/settings/AccountSettingsCard.js +2 -1
  271. package/dist/collab/struct-routes.d.ts +1 -1
  272. package/dist/db/client.d.ts +9 -0
  273. package/dist/db/client.js +51 -16
  274. package/dist/db/create-get-db.js +2 -2
  275. package/dist/db/ddl-guard.d.ts +11 -10
  276. package/dist/db/ddl-guard.js +40 -12
  277. package/dist/deploy/build.d.ts +2 -0
  278. package/dist/deploy/build.js +97 -4
  279. package/dist/deploy/workspace-deploy.js +0 -2
  280. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  281. package/dist/guards/no-env-credentials.js +9 -0
  282. package/dist/ingestion/index.d.ts +1 -1
  283. package/dist/ingestion/pptx.d.ts +68 -0
  284. package/dist/ingestion/pptx.js +660 -213
  285. package/dist/integrations/plugin.js +9 -6
  286. package/dist/jobs/scheduler.js +79 -23
  287. package/dist/localization/default-messages.d.ts +54 -0
  288. package/dist/localization/default-messages.js +57 -2
  289. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  290. package/dist/mcp-client/oauth-routes.d.ts +2 -0
  291. package/dist/mcp-client/oauth-routes.js +73 -6
  292. package/dist/observability/routes.d.ts +5 -5
  293. package/dist/onboarding/app-profile.js +11 -0
  294. package/dist/progress/routes.d.ts +1 -1
  295. package/dist/resources/handlers.d.ts +1 -1
  296. package/dist/resources/store.js +15 -1
  297. package/dist/scripts/call-agent.js +34 -4
  298. package/dist/secrets/routes.d.ts +3 -3
  299. package/dist/server/agent-chat/action-filters-a2a.d.ts +1 -1
  300. package/dist/server/agent-chat/action-filters-a2a.js +36 -1
  301. package/dist/server/agent-chat-plugin.js +29 -8
  302. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  303. package/dist/server/auth.js +47 -11
  304. package/dist/server/better-auth-instance.js +5 -3
  305. package/dist/server/core-routes-plugin.d.ts +8 -1
  306. package/dist/server/core-routes-plugin.js +28 -3
  307. package/dist/server/credential-provider.d.ts +3 -3
  308. package/dist/server/credential-provider.js +90 -4
  309. package/dist/server/onboarding-html.d.ts +2 -0
  310. package/dist/server/onboarding-html.js +115 -29
  311. package/dist/server/realtime-token.d.ts +1 -1
  312. package/dist/server/self-dispatch.d.ts +5 -4
  313. package/dist/server/self-dispatch.js +25 -14
  314. package/dist/server/ssr-handler.d.ts +1 -1
  315. package/dist/server/ssr-handler.js +1 -1
  316. package/dist/server/transcribe-voice.d.ts +1 -1
  317. package/dist/shared/cache-control.d.ts +26 -2
  318. package/dist/shared/cache-control.js +43 -2
  319. package/dist/shared/password-policy.d.ts +3 -0
  320. package/dist/shared/password-policy.js +3 -0
  321. package/dist/sharing/actions/set-resource-visibility.js +16 -4
  322. package/dist/sharing/registry.d.ts +14 -0
  323. package/dist/styles/agent-native.css +6 -0
  324. package/dist/templates/chat/.agents/skills/turn-into-app/SKILL.md +296 -0
  325. package/dist/templates/chat/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  326. package/dist/templates/chat/.agents/skills/turn-into-skill/SKILL.md +112 -0
  327. package/dist/templates/chat/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  328. package/dist/templates/default/.agents/skills/turn-into-app/SKILL.md +296 -0
  329. package/dist/templates/default/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  330. package/dist/templates/default/.agents/skills/turn-into-skill/SKILL.md +112 -0
  331. package/dist/templates/default/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  332. package/dist/templates/headless/.agents/skills/turn-into-app/SKILL.md +296 -0
  333. package/dist/templates/headless/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  334. package/dist/templates/headless/.agents/skills/turn-into-skill/SKILL.md +112 -0
  335. package/dist/templates/headless/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  336. package/dist/templates/workspace-core/.agents/skills/adding-workspace-apps/SKILL.md +6 -0
  337. package/dist/templates/workspace-core/.agents/skills/performance/SKILL.md +51 -0
  338. package/dist/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +296 -0
  339. package/dist/templates/workspace-core/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  340. package/dist/templates/workspace-core/.agents/skills/turn-into-skill/SKILL.md +112 -0
  341. package/dist/templates/workspace-core/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  342. package/dist/templates/workspace-root/AGENTS.md +7 -0
  343. package/dist/user-profile/actions/change-password.js +8 -3
  344. package/dist/user-profile/actions/set-password.js +5 -1
  345. package/dist/vite/action-types-plugin.js +59 -0
  346. package/docs/content/creating-templates.mdx +1 -1
  347. package/docs/content/getting-started-actions.mdx +38 -189
  348. package/docs/content/getting-started-database.mdx +80 -164
  349. package/docs/content/getting-started-pages.mdx +44 -155
  350. package/docs/content/getting-started.mdx +26 -106
  351. package/docs/content/skills-guide.mdx +13 -0
  352. package/package.json +6 -2
  353. package/src/templates/chat/.agents/skills/turn-into-app/SKILL.md +296 -0
  354. package/src/templates/chat/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  355. package/src/templates/chat/.agents/skills/turn-into-skill/SKILL.md +112 -0
  356. package/src/templates/chat/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  357. package/src/templates/default/.agents/skills/turn-into-app/SKILL.md +296 -0
  358. package/src/templates/default/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  359. package/src/templates/default/.agents/skills/turn-into-skill/SKILL.md +112 -0
  360. package/src/templates/default/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  361. package/src/templates/headless/.agents/skills/turn-into-app/SKILL.md +296 -0
  362. package/src/templates/headless/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  363. package/src/templates/headless/.agents/skills/turn-into-skill/SKILL.md +112 -0
  364. package/src/templates/headless/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  365. package/src/templates/workspace-core/.agents/skills/adding-workspace-apps/SKILL.md +6 -0
  366. package/src/templates/workspace-core/.agents/skills/performance/SKILL.md +51 -0
  367. package/src/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +296 -0
  368. package/src/templates/workspace-core/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  369. package/src/templates/workspace-core/.agents/skills/turn-into-skill/SKILL.md +112 -0
  370. package/src/templates/workspace-core/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  371. package/src/templates/workspace-root/AGENTS.md +7 -0
@@ -1,10 +1,19 @@
1
1
  import { defineAction } from "@agent-native/core";
2
+ import { ssrfSafeFetch } from "@agent-native/core/extensions/url-safety";
2
3
  import { buildDeepLink } from "@agent-native/core/server";
3
4
  import { getRequestUserEmail } from "@agent-native/core/server/request-context";
5
+ import pLimit from "p-limit";
4
6
  import { z } from "zod";
5
7
 
8
+ import {
9
+ parsePptx,
10
+ type ParsedPresentation,
11
+ } from "../server/handlers/import/pptx-parser.js";
6
12
  import { getGoogleDocsAccessToken } from "../server/lib/google-docs-oauth.js";
7
- import { importPptxBufferToDeck } from "./import-pptx.js";
13
+ import {
14
+ importPptxBufferToDeck,
15
+ type ImportedImageFallback,
16
+ } from "./import-pptx.js";
8
17
 
9
18
  function deckDeepLink(deckId: string): string {
10
19
  return buildDeepLink({
@@ -14,6 +23,38 @@ function deckDeepLink(deckId: string): string {
14
23
  });
15
24
  }
16
25
 
26
+ export function extractGoogleSlidesPresentationId(value: string): string {
27
+ const candidate = value.trim();
28
+ if (!candidate)
29
+ throw new Error("A Google Slides file ID or URL is required.");
30
+
31
+ if (!/^https?:\/\//i.test(candidate)) {
32
+ if (!/^[a-zA-Z0-9_-]+$/.test(candidate)) {
33
+ throw new Error(
34
+ "Use a Google Slides file ID or a docs.google.com presentation URL.",
35
+ );
36
+ }
37
+ return candidate;
38
+ }
39
+
40
+ let url: URL;
41
+ try {
42
+ url = new URL(candidate);
43
+ } catch {
44
+ throw new Error("Use a valid Google Slides presentation URL.");
45
+ }
46
+ if (url.hostname !== "docs.google.com") {
47
+ throw new Error("Use a docs.google.com Google Slides presentation URL.");
48
+ }
49
+ const match = url.pathname.match(
50
+ /^\/presentation\/d\/([a-zA-Z0-9_-]+)(?:\/|$)/,
51
+ );
52
+ if (!match) {
53
+ throw new Error("That URL is not a Google Slides presentation link.");
54
+ }
55
+ return match[1];
56
+ }
57
+
17
58
  async function exportGoogleSlidesAsPptx(fileId: string, accessToken: string) {
18
59
  const response = await fetch(
19
60
  `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}/export?mimeType=application/vnd.openxmlformats-officedocument.presentationml.presentation`,
@@ -36,36 +77,254 @@ async function exportGoogleSlidesAsPptx(fileId: string, accessToken: string) {
36
77
  return Buffer.from(bytes);
37
78
  }
38
79
 
80
+ interface GoogleSlidesImageElement {
81
+ objectId?: string;
82
+ size?: {
83
+ width?: { magnitude?: number };
84
+ height?: { magnitude?: number };
85
+ };
86
+ transform?: {
87
+ scaleX?: number;
88
+ scaleY?: number;
89
+ translateX?: number;
90
+ translateY?: number;
91
+ };
92
+ image?: {
93
+ contentUrl?: string;
94
+ imageProperties?: {
95
+ cropProperties?: {
96
+ leftOffset?: number;
97
+ rightOffset?: number;
98
+ topOffset?: number;
99
+ bottomOffset?: number;
100
+ };
101
+ };
102
+ };
103
+ }
104
+
105
+ interface GoogleSlidesPresentationImages {
106
+ slides?: Array<{
107
+ pageElements?: GoogleSlidesImageElement[];
108
+ }>;
109
+ }
110
+
111
+ interface GoogleSlidesImageCandidate {
112
+ slideIndex: number;
113
+ imageIndex: number;
114
+ objectId?: string;
115
+ contentUrl: string;
116
+ x: number;
117
+ y: number;
118
+ width: number;
119
+ height: number;
120
+ crop?: ImportedImageFallback["crop"];
121
+ }
122
+
123
+ function finiteNumber(value: number | undefined): number | undefined {
124
+ return value != null && Number.isFinite(value) ? value : undefined;
125
+ }
126
+
127
+ /**
128
+ * Google Drive's PPTX export can omit image page elements that remain present
129
+ * in the native Slides document. Read those native objects as a fidelity
130
+ * fallback so a direct Google Slides import does not silently lose artwork.
131
+ */
132
+ async function fetchGoogleSlidesImageFallbacks(
133
+ fileId: string,
134
+ accessToken: string,
135
+ presentation: ParsedPresentation,
136
+ ): Promise<ImportedImageFallback[]> {
137
+ const fields =
138
+ "slides(pageElements(objectId,size,transform,image(contentUrl,imageProperties(cropProperties))))";
139
+ const response = await ssrfSafeFetch(
140
+ `https://slides.googleapis.com/v1/presentations/${encodeURIComponent(fileId)}?fields=${encodeURIComponent(fields)}`,
141
+ { headers: { Authorization: `Bearer ${accessToken}` } },
142
+ { httpsOnly: true, maxRedirects: 2 },
143
+ );
144
+ if (!response.ok) {
145
+ throw new Error(
146
+ `Google Slides returned HTTP ${response.status} while reading native image elements. Reconnect Google Drive and try the import again.`,
147
+ );
148
+ }
149
+ const googlePresentation =
150
+ (await response.json()) as GoogleSlidesPresentationImages;
151
+ const candidates: GoogleSlidesImageCandidate[] = [];
152
+ const candidateKeys = new Set<string>();
153
+
154
+ for (const [slideIndex, slide] of (
155
+ googlePresentation.slides ?? []
156
+ ).entries()) {
157
+ for (const [imageIndex, element] of (slide.pageElements ?? []).entries()) {
158
+ const contentUrl = element.image?.contentUrl;
159
+ const baseWidth = finiteNumber(element.size?.width?.magnitude);
160
+ const baseHeight = finiteNumber(element.size?.height?.magnitude);
161
+ const scaleX = finiteNumber(element.transform?.scaleX) ?? 1;
162
+ const scaleY = finiteNumber(element.transform?.scaleY) ?? 1;
163
+ const x = finiteNumber(element.transform?.translateX);
164
+ const y = finiteNumber(element.transform?.translateY);
165
+ const width =
166
+ baseWidth == null ? undefined : baseWidth * Math.abs(scaleX);
167
+ const height =
168
+ baseHeight == null ? undefined : baseHeight * Math.abs(scaleY);
169
+ if (
170
+ !contentUrl ||
171
+ x == null ||
172
+ y == null ||
173
+ width == null ||
174
+ height == null ||
175
+ width <= 0 ||
176
+ height <= 0
177
+ ) {
178
+ continue;
179
+ }
180
+
181
+ const existingImage = presentation.slides[slideIndex]?.elements.some(
182
+ (existing) =>
183
+ existing.kind === "image" &&
184
+ Math.abs(existing.x - x) < 1000 &&
185
+ Math.abs(existing.y - y) < 1000 &&
186
+ Math.abs(existing.width - width) < 1000 &&
187
+ Math.abs(existing.height - height) < 1000,
188
+ );
189
+ if (existingImage) continue;
190
+
191
+ const cropProperties = element.image?.imageProperties?.cropProperties;
192
+ const crop = cropProperties
193
+ ? {
194
+ left: cropProperties.leftOffset ?? 0,
195
+ top: cropProperties.topOffset ?? 0,
196
+ right: cropProperties.rightOffset ?? 0,
197
+ bottom: cropProperties.bottomOffset ?? 0,
198
+ }
199
+ : undefined;
200
+ const candidateKey = `${slideIndex}:${x}:${y}:${width}:${height}`;
201
+ if (candidateKeys.has(candidateKey)) continue;
202
+ candidateKeys.add(candidateKey);
203
+ candidates.push({
204
+ slideIndex,
205
+ imageIndex,
206
+ ...(element.objectId ? { objectId: element.objectId } : {}),
207
+ contentUrl,
208
+ x,
209
+ y,
210
+ width,
211
+ height,
212
+ ...(crop ? { crop } : {}),
213
+ });
214
+ }
215
+ }
216
+
217
+ const downloadLimit = pLimit(4);
218
+ const imageDownloads = new Map<
219
+ string,
220
+ Promise<{ data: Uint8Array; mimeType: string }>
221
+ >();
222
+ const downloadImage = (candidate: GoogleSlidesImageCandidate) => {
223
+ const existing = imageDownloads.get(candidate.contentUrl);
224
+ if (existing) return existing;
225
+
226
+ const download = downloadLimit(async () => {
227
+ const imageResponse = await ssrfSafeFetch(
228
+ candidate.contentUrl,
229
+ { headers: { Authorization: `Bearer ${accessToken}` } },
230
+ { httpsOnly: true, maxRedirects: 2 },
231
+ );
232
+ if (!imageResponse.ok) {
233
+ throw new Error(
234
+ `Google Slides returned HTTP ${imageResponse.status} while downloading image ${candidate.imageIndex + 1} on slide ${candidate.slideIndex + 1}.`,
235
+ );
236
+ }
237
+ const mimeType =
238
+ imageResponse.headers.get("content-type")?.split(";", 1)[0] ||
239
+ "image/png";
240
+ if (!mimeType.startsWith("image/")) {
241
+ throw new Error(
242
+ `Google Slides returned a non-image asset for image ${candidate.imageIndex + 1} on slide ${candidate.slideIndex + 1}.`,
243
+ );
244
+ }
245
+ return {
246
+ data: new Uint8Array(await imageResponse.arrayBuffer()),
247
+ mimeType,
248
+ };
249
+ });
250
+ imageDownloads.set(candidate.contentUrl, download);
251
+ return download;
252
+ };
253
+
254
+ return Promise.all(
255
+ candidates.map(async (candidate) => {
256
+ const { data, mimeType } = await downloadImage(candidate);
257
+ return {
258
+ slideIndex: candidate.slideIndex,
259
+ x: candidate.x,
260
+ y: candidate.y,
261
+ width: candidate.width,
262
+ height: candidate.height,
263
+ data,
264
+ mimeType,
265
+ name: `google-slides-${candidate.slideIndex + 1}-${candidate.objectId ?? candidate.imageIndex}.png`,
266
+ ...(candidate.crop ? { crop: candidate.crop } : {}),
267
+ };
268
+ }),
269
+ );
270
+ }
271
+
39
272
  export default defineAction({
40
273
  description:
41
- "Import a Google Slides deck selected from Google Picker and save it as a reusable Slides reference deck. " +
274
+ "Import a Google Slides deck selected from Google Picker or provided as a Google Slides URL and save it as a reusable Slides reference deck. " +
42
275
  "The selected presentation is exported from Drive as PPTX, parsed, and stored as a normal deck in the caller's Slides workspace.",
43
- schema: z.object({
44
- fileId: z.string().describe("Google Slides file ID from Google Picker"),
45
- title: z
46
- .string()
47
- .optional()
48
- .describe("Optional title for the imported reference deck"),
49
- }),
50
- run: async ({ fileId, title }) => {
276
+ schema: z
277
+ .object({
278
+ fileId: z
279
+ .string()
280
+ .optional()
281
+ .describe("Google Slides file ID from Google Picker"),
282
+ presentationUrl: z
283
+ .string()
284
+ .url()
285
+ .optional()
286
+ .describe("A docs.google.com Google Slides presentation URL"),
287
+ title: z
288
+ .string()
289
+ .optional()
290
+ .describe("Optional title for the imported reference deck"),
291
+ })
292
+ .refine(
293
+ ({ fileId, presentationUrl }) => Boolean(fileId || presentationUrl),
294
+ {
295
+ message: "Provide either fileId or presentationUrl.",
296
+ },
297
+ ),
298
+ run: async ({ fileId, presentationUrl, title }) => {
51
299
  const owner = getRequestUserEmail();
52
300
  if (!owner) throw new Error("no authenticated user");
53
301
 
54
302
  const connection = await getGoogleDocsAccessToken(owner);
55
303
  if (!connection) {
56
304
  throw new Error(
57
- "Connect Google Docs before importing a Google Slides deck.",
305
+ "Google Drive is not connected. Use the Connect Google button in Slides, then try again.",
58
306
  );
59
307
  }
60
308
 
309
+ const presentationId = extractGoogleSlidesPresentationId(
310
+ presentationUrl ?? fileId ?? "",
311
+ );
61
312
  const fileBuffer = await exportGoogleSlidesAsPptx(
62
- fileId,
313
+ presentationId,
314
+ connection.accessToken,
315
+ );
316
+ const parsedPresentation = await parsePptx(fileBuffer);
317
+ const imageFallbacks = await fetchGoogleSlidesImageFallbacks(
318
+ presentationId,
63
319
  connection.accessToken,
320
+ parsedPresentation,
64
321
  );
65
322
  return importPptxBufferToDeck({
66
323
  fileBuffer,
324
+ parsedPresentation,
67
325
  title,
68
326
  source: "import-google-slides-reference",
327
+ imageFallbacks,
69
328
  });
70
329
  },
71
330
  link: ({ result }) => {
@@ -1,6 +1,5 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { writeAppState } from "@agent-native/core/application-state";
3
- import { uploadFile } from "@agent-native/core/file-upload";
4
3
  import {
5
4
  getRequestUserEmail,
6
5
  getRequestOrgId,
@@ -13,58 +12,86 @@ import { z } from "zod";
13
12
  import { getDb, schema } from "../server/db/index.js";
14
13
  import { notifyClients } from "../server/handlers/decks.js";
15
14
  import { convertToSlideHtml } from "../server/handlers/import/html-converter.js";
15
+ import { uploadPptxSlideImages } from "../server/handlers/import/pptx-assets.js";
16
16
  import {
17
17
  parsePptx,
18
- type ParsedSlide,
18
+ type ParsedElement,
19
+ type ParsedImage,
20
+ type ParsedPresentation,
19
21
  } from "../server/handlers/import/pptx-parser.js";
22
+ import { buildSourceImportMetadata } from "../server/lib/source-import.js";
23
+ import {
24
+ ASPECT_RATIOS,
25
+ DEFAULT_ASPECT_RATIO,
26
+ type AspectRatio,
27
+ } from "../shared/aspect-ratios.js";
20
28
  import { getDeckUrl } from "./_app-url.js";
21
29
  import { readUserUploadedFile } from "./_uploaded-files.js";
22
30
 
23
- // EMF/WMF (Windows metafiles) and TIFF are valid PPTX embed formats but
24
- // browsers can't render them in an <img> tag — uploading and linking one
25
- // would just produce a broken image icon.
26
- const BROWSER_RENDERABLE_IMAGE_MIME_TYPES = new Set([
27
- "image/png",
28
- "image/jpeg",
29
- "image/gif",
30
- "image/webp",
31
- "image/svg+xml",
32
- "image/bmp",
33
- ]);
34
-
35
- async function uploadFirstSlideImage(
36
- slide: ParsedSlide,
37
- slideIndex: number,
38
- ownerEmail: string,
39
- ): Promise<string | undefined> {
40
- const image = slide.images[0];
41
- if (!image || !BROWSER_RENDERABLE_IMAGE_MIME_TYPES.has(image.mimeType)) {
42
- return undefined;
43
- }
44
- const filename =
45
- "pptx-import-" + Date.now() + "-s" + slideIndex + "-" + image.name;
46
- try {
47
- const result = await uploadFile({
48
- data: Buffer.from(image.data),
49
- filename,
50
- mimeType: image.mimeType,
51
- ownerEmail,
52
- recordAsset: false,
53
- });
54
- return result?.url;
55
- } catch {
56
- // A single slide's upload failing (network/API/rate-limit) shouldn't
57
- // abort the whole deck import — that slide's text still imports fine,
58
- // it just falls back to a placeholder like an unsupported format would.
59
- return undefined;
31
+ export interface ImportedImageFallback {
32
+ slideIndex: number;
33
+ x: number;
34
+ y: number;
35
+ width: number;
36
+ height: number;
37
+ data: Uint8Array;
38
+ mimeType: string;
39
+ name: string;
40
+ crop?: ParsedImage["crop"];
41
+ }
42
+
43
+ /** Add source-native image objects that a Google Slides PPTX export omitted. */
44
+ export function applyImageFallbacks(
45
+ presentation: Awaited<ReturnType<typeof parsePptx>>,
46
+ fallbacks: ImportedImageFallback[] = [],
47
+ ): number {
48
+ let added = 0;
49
+ for (const [fallbackIndex, fallback] of fallbacks.entries()) {
50
+ const slide = presentation.slides[fallback.slideIndex];
51
+ if (!slide || fallback.width <= 0 || fallback.height <= 0) continue;
52
+
53
+ const duplicate = slide.elements.some(
54
+ (element) =>
55
+ element.kind === "image" &&
56
+ Math.abs(element.x - fallback.x) < 1000 &&
57
+ Math.abs(element.y - fallback.y) < 1000 &&
58
+ Math.abs(element.width - fallback.width) < 1000 &&
59
+ Math.abs(element.height - fallback.height) < 1000,
60
+ );
61
+ if (duplicate) continue;
62
+
63
+ const image: ParsedImage = {
64
+ data: fallback.data,
65
+ mimeType: fallback.mimeType,
66
+ name: fallback.name,
67
+ aspectRatio: fallback.width / fallback.height,
68
+ ...(fallback.crop ? { crop: fallback.crop } : {}),
69
+ };
70
+ const element: ParsedElement = {
71
+ id: `image-fallback-${fallback.slideIndex}-${fallbackIndex}`,
72
+ name: fallback.name,
73
+ kind: "image",
74
+ x: fallback.x,
75
+ y: fallback.y,
76
+ width: fallback.width,
77
+ height: fallback.height,
78
+ image,
79
+ };
80
+ slide.images.push(image);
81
+ slide.elements.push(element);
82
+ added++;
60
83
  }
84
+ return added;
61
85
  }
62
86
 
63
87
  export async function importPptxBufferToDeck(args: {
64
88
  fileBuffer: Buffer;
65
89
  title?: string;
66
90
  deckId?: string;
91
+ designSystemId?: string | null;
67
92
  source?: string;
93
+ imageFallbacks?: ImportedImageFallback[];
94
+ parsedPresentation?: ParsedPresentation;
68
95
  }): Promise<{
69
96
  id: string;
70
97
  title: string;
@@ -74,8 +101,17 @@ export async function importPptxBufferToDeck(args: {
74
101
  url: string;
75
102
  imagesSkipped?: number;
76
103
  }> {
77
- const { fileBuffer, title, deckId, source = "import-pptx" } = args;
78
- const presentation = await parsePptx(fileBuffer);
104
+ const {
105
+ fileBuffer,
106
+ title,
107
+ deckId,
108
+ designSystemId,
109
+ source = "import-pptx",
110
+ imageFallbacks,
111
+ parsedPresentation,
112
+ } = args;
113
+ const presentation = parsedPresentation ?? (await parsePptx(fileBuffer));
114
+ applyImageFallbacks(presentation, imageFallbacks);
79
115
  const deckTitle = title || presentation.title || "Imported Presentation";
80
116
  const ownerEmail = getRequestUserEmail();
81
117
  if (!ownerEmail) throw new Error("no authenticated user");
@@ -88,38 +124,38 @@ export async function importPptxBufferToDeck(args: {
88
124
  await assertAccess("deck", deckId, "editor");
89
125
  }
90
126
 
91
- // Convert each parsed slide to our HTML format, uploading the first
92
- // embedded image (if any) so it renders as a real image instead of a
93
- // text placeholder. Concurrency is capped so a large deck doesn't fire
94
- // one outbound upload per slide at once. An image can end up unused
95
- // (unsupported format, or upload storage not configured) without
96
- // failing the whole import — the slide's text still imports fine — but
97
- // that shouldn't be a silent, invisible degradation, so it's counted
98
- // and returned to the caller.
127
+ // Convert each parsed slide to its positioned scene graph, uploading every
128
+ // browser-renderable image so the imported deck keeps the source layering
129
+ // and media instead of collapsing to a one-image approximation.
99
130
  const uploadLimit = pLimit(4);
100
131
  const results = await Promise.all(
101
132
  presentation.slides.map((parsedSlide, i) =>
102
133
  uploadLimit(async () => {
103
- const imageUrl = await uploadFirstSlideImage(
104
- parsedSlide,
105
- i,
134
+ const uploadedImages = await uploadPptxSlideImages({
135
+ slide: parsedSlide,
136
+ slideIndex: i,
106
137
  ownerEmail,
138
+ });
139
+ const html = convertToSlideHtml(
140
+ parsedSlide,
141
+ uploadedImages.urls,
142
+ themeFont,
107
143
  );
108
- const html = convertToSlideHtml(parsedSlide, imageUrl, themeFont);
144
+ const id = `slide-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
109
145
  return {
110
146
  slide: {
111
- id: `slide-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
147
+ id,
112
148
  content: html,
113
149
  layout: parsedSlide.layoutHint ?? "content",
114
150
  notes: parsedSlide.notes,
151
+ ...(parsedSlide.transition
152
+ ? { transition: parsedSlide.transition }
153
+ : {}),
154
+ ...(parsedSlide.splitByParagraph ? { splitByParagraph: true } : {}),
115
155
  },
116
- // Only the first image on a slide is ever uploaded, so every
117
- // other image on that slide is unconditionally dropped too —
118
- // not just the first one when it's unsupported.
119
- imageSkippedCount: Math.max(
120
- 0,
121
- parsedSlide.images.length - (imageUrl ? 1 : 0),
122
- ),
156
+ sourceText: parsedSlide.texts.map((text) => text.content).join("\n"),
157
+ imageUrls: Object.values(uploadedImages.urls),
158
+ imageSkippedCount: uploadedImages.imageSkippedCount,
123
159
  };
124
160
  }),
125
161
  ),
@@ -129,6 +165,26 @@ export async function importPptxBufferToDeck(args: {
129
165
  (total, r) => total + r.imageSkippedCount,
130
166
  0,
131
167
  );
168
+ if (imagesSkipped > 0) {
169
+ throw new Error(
170
+ `Source-faithful PPTX import could not preserve ${imagesSkipped} image(s). No deck was written. Retry with browser-renderable images or use a PDF export for page-faithful preservation.`,
171
+ );
172
+ }
173
+ const sourceImport = buildSourceImportMetadata({
174
+ format: "pptx",
175
+ slides: results.map((result) => ({
176
+ id: result.slide.id,
177
+ text: result.sourceText,
178
+ notes: result.slide.notes ?? "",
179
+ imageUrls: result.imageUrls,
180
+ editableText: true,
181
+ })),
182
+ imagesSkipped,
183
+ });
184
+ const aspectRatio = nearestAspectRatio(
185
+ presentation.slides[0]?.widthEmu,
186
+ presentation.slides[0]?.heightEmu,
187
+ );
132
188
 
133
189
  const db = getDb();
134
190
  const now = new Date().toISOString();
@@ -143,10 +199,25 @@ export async function importPptxBufferToDeck(args: {
143
199
  throw new Error(`Deck ${deckId} not found`);
144
200
  }
145
201
 
146
- const data = { title: deckTitle, slides, updatedAt: now };
202
+ const previousData = safeParseDeckData(existing[0].data);
203
+ const data = {
204
+ ...previousData,
205
+ title: deckTitle,
206
+ slides,
207
+ ...(aspectRatio ? { aspectRatio } : {}),
208
+ sourceImport,
209
+ updatedAt: now,
210
+ };
147
211
  await db
148
212
  .update(schema.decks)
149
- .set({ title: deckTitle, data: JSON.stringify(data), updatedAt: now })
213
+ .set({
214
+ title: deckTitle,
215
+ data: JSON.stringify(data),
216
+ ...(designSystemId !== undefined
217
+ ? { designSystemId }
218
+ : { designSystemId: existing[0].designSystemId }),
219
+ updatedAt: now,
220
+ })
150
221
  .where(eq(schema.decks.id, deckId));
151
222
 
152
223
  notifyClients(deckId);
@@ -168,13 +239,21 @@ export async function importPptxBufferToDeck(args: {
168
239
 
169
240
  // Create new deck
170
241
  const id = `deck-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
171
- const data = { title: deckTitle, slides, createdAt: now, updatedAt: now };
242
+ const data = {
243
+ title: deckTitle,
244
+ slides,
245
+ ...(aspectRatio ? { aspectRatio } : {}),
246
+ sourceImport,
247
+ createdAt: now,
248
+ updatedAt: now,
249
+ };
172
250
  await db.insert(schema.decks).values({
173
251
  id,
174
252
  title: deckTitle,
175
253
  data: JSON.stringify(data),
176
254
  ownerEmail,
177
255
  orgId: getRequestOrgId(),
256
+ designSystemId: designSystemId ?? undefined,
178
257
  createdAt: now,
179
258
  updatedAt: now,
180
259
  });
@@ -197,8 +276,8 @@ export default defineAction({
197
276
  description:
198
277
  "Import a PPTX file and create a slide deck from it. " +
199
278
  "Parses the PowerPoint file, extracts text and layout information, " +
200
- "converts each slide to the app's HTML format, and creates or updates a deck. " +
201
- "Returns the deck ID and slide count.",
279
+ "converts each slide to the app's positioned HTML format, records source-preservation metadata, and creates or updates a deck. " +
280
+ "If an embedded image cannot be preserved, the action fails before writing a partial deck. Returns the deck ID and slide count.",
202
281
  schema: z.object({
203
282
  filePath: z
204
283
  .string()
@@ -209,6 +288,11 @@ export default defineAction({
209
288
  .describe(
210
289
  "If provided, import slides into this existing deck (replaces all slides)",
211
290
  ),
291
+ designSystemId: z
292
+ .string()
293
+ .nullable()
294
+ .optional()
295
+ .describe("Optional design system to link when creating a new deck"),
212
296
  title: z
213
297
  .string()
214
298
  .optional()
@@ -216,8 +300,49 @@ export default defineAction({
216
300
  "Deck title — defaults to the title extracted from the presentation",
217
301
  ),
218
302
  }),
219
- run: async ({ filePath, deckId, title }) => {
303
+ run: async ({ filePath, deckId, title, designSystemId }) => {
220
304
  const { data: fileBuffer } = await readUserUploadedFile(filePath);
221
- return importPptxBufferToDeck({ fileBuffer, deckId, title });
305
+ return importPptxBufferToDeck({
306
+ fileBuffer,
307
+ deckId,
308
+ title,
309
+ designSystemId,
310
+ });
222
311
  },
223
312
  });
313
+
314
+ function nearestAspectRatio(
315
+ width: number | undefined,
316
+ height: number | undefined,
317
+ ): AspectRatio | undefined {
318
+ if (!width || !height || width <= 0 || height <= 0) return undefined;
319
+ const target = width / height;
320
+ let best: AspectRatio = DEFAULT_ASPECT_RATIO;
321
+ let bestDiff = Infinity;
322
+ for (const key of Object.keys(ASPECT_RATIOS) as AspectRatio[]) {
323
+ const preset = ASPECT_RATIOS[key];
324
+ const diff = Math.abs(preset.width / preset.height - target);
325
+ if (diff < bestDiff) {
326
+ best = key;
327
+ bestDiff = diff;
328
+ }
329
+ }
330
+ return best;
331
+ }
332
+
333
+ function safeParseDeckData(raw: string): Record<string, unknown> {
334
+ let parsed: unknown;
335
+ try {
336
+ parsed = JSON.parse(raw);
337
+ } catch {
338
+ throw new Error(
339
+ "The target deck contains invalid JSON; refusing to overwrite it.",
340
+ );
341
+ }
342
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
343
+ throw new Error(
344
+ "The target deck data is invalid; refusing to overwrite it.",
345
+ );
346
+ }
347
+ return parsed as Record<string, unknown>;
348
+ }