@agent-native/core 0.115.4 → 0.116.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. package/agent-native.eject.json +1 -0
  2. package/corpus/README.md +3 -3
  3. package/corpus/core/CHANGELOG.md +28 -0
  4. package/corpus/core/agent-native.eject.json +1 -0
  5. package/corpus/core/docs/AGENTS.md +1 -1
  6. package/corpus/core/docs/content/agent-mentions.mdx +2 -2
  7. package/corpus/core/docs/content/{workspace.mdx → agent-resources.mdx} +1 -7
  8. package/corpus/core/docs/content/agent-teams.mdx +4 -4
  9. package/corpus/core/docs/content/dispatch.mdx +10 -10
  10. package/corpus/core/docs/content/drop-in-agent.mdx +4 -4
  11. package/corpus/core/docs/content/external-agents.mdx +1 -1
  12. package/corpus/core/docs/content/frames.mdx +1 -1
  13. package/corpus/core/docs/content/integrations.mdx +13 -2
  14. package/corpus/core/docs/content/key-concepts.mdx +2 -2
  15. package/corpus/core/docs/content/mcp-clients.mdx +7 -1
  16. package/corpus/core/docs/content/multi-app-workspace.mdx +3 -3
  17. package/corpus/core/docs/content/real-time-collaboration.mdx +33 -15
  18. package/corpus/core/docs/content/recurring-jobs.mdx +7 -7
  19. package/corpus/core/docs/content/skills-guide.mdx +4 -4
  20. package/corpus/core/docs/content/template-brain.mdx +1 -1
  21. package/corpus/core/docs/content/toolkit-collaboration.mdx +1 -1
  22. package/corpus/core/docs/content/using-your-agent.mdx +1 -1
  23. package/corpus/core/docs/content/what-is-agent-native.mdx +4 -4
  24. package/corpus/core/docs/content/workspace-management.mdx +4 -4
  25. package/corpus/core/package.json +1 -1
  26. package/corpus/core/src/agent/production-agent.ts +13 -3
  27. package/corpus/core/src/cli/doctor.ts +6 -1
  28. package/corpus/core/src/client/AgentPanel.tsx +79 -41
  29. package/corpus/core/src/client/AssistantChat.tsx +71 -25
  30. package/corpus/core/src/client/FeedbackButton.tsx +33 -7
  31. package/corpus/core/src/client/MultiTabAssistantChat.tsx +6 -8
  32. package/corpus/core/src/client/agent-chat-adapter.ts +11 -2
  33. package/corpus/core/src/client/agent-page/AgentTabFrame.tsx +22 -3
  34. package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +34 -19
  35. package/corpus/core/src/client/chat/message-components.tsx +43 -16
  36. package/corpus/core/src/client/chat/tool-call-display.tsx +30 -18
  37. package/corpus/core/src/client/chat/tool-render-registry.tsx +2 -0
  38. package/corpus/core/src/client/resources/McpConnectionSuggestion.tsx +19 -2
  39. package/corpus/core/src/client/resources/ResourceTree.tsx +7 -10
  40. package/corpus/core/src/client/resources/ResourcesPanel.tsx +33 -70
  41. package/corpus/core/src/client/resources/mcp-connection-resume.ts +107 -0
  42. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +65 -12
  43. package/corpus/core/src/client/resources/mcp-integration-logos.ts +4 -0
  44. package/corpus/core/src/client/sse-event-processor.ts +50 -14
  45. package/corpus/core/src/client/use-db-sync.ts +16 -6
  46. package/corpus/core/src/connections/catalog.ts +7 -1
  47. package/corpus/core/src/eject/remote-mcp-presets.ts +37 -5
  48. package/corpus/core/src/guards/explicit-collab-access.ts +229 -0
  49. package/corpus/core/src/guards/index.ts +1 -0
  50. package/corpus/core/src/localization/default-messages.ts +54 -5
  51. package/corpus/core/src/mcp/connect-route.ts +34 -51
  52. package/corpus/core/src/provider-api/index.ts +1 -1
  53. package/corpus/core/src/server/collab-plugin.ts +137 -18
  54. package/corpus/core/src/server/index.ts +2 -0
  55. package/corpus/core/src/styles/agent-native.css +40 -0
  56. package/corpus/core/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  57. package/corpus/core/src/templates/default/.agents/skills/real-time-collab/SKILL.md +35 -13
  58. package/corpus/core/src/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
  59. package/corpus/core/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  60. package/corpus/core/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  61. package/corpus/core/src/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +35 -13
  62. package/corpus/core/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
  63. package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +12 -8
  64. package/corpus/templates/analytics/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  65. package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +1 -1
  66. package/corpus/templates/analytics/AGENTS.md +7 -2
  67. package/corpus/templates/analytics/actions/export-dashboard-panel-to-google-sheet.ts +144 -0
  68. package/corpus/templates/analytics/app/components/layout/Layout.tsx +7 -88
  69. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +34 -37
  70. package/corpus/templates/analytics/app/global.css +0 -4
  71. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +2 -7
  72. package/corpus/templates/analytics/app/i18n-data.ts +107 -0
  73. package/corpus/templates/analytics/app/lib/data-source-status.ts +9 -0
  74. package/corpus/templates/analytics/app/lib/sql-query.ts +1 -2
  75. package/corpus/templates/analytics/app/pages/Ask.tsx +2 -7
  76. package/corpus/templates/analytics/app/pages/DataSources.tsx +48 -0
  77. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +59 -0
  78. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -0
  79. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +3 -1
  80. package/corpus/templates/analytics/changelog/2026-07-22-clear-background-chat-progress.md +6 -0
  81. package/corpus/templates/analytics/changelog/2026-07-22-daily-dashboard-email-query-batches.md +6 -0
  82. package/corpus/templates/analytics/changelog/2026-07-22-export-table-panels-directly-to-google-sheets.md +6 -0
  83. package/corpus/templates/analytics/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
  84. package/corpus/templates/analytics/changelog/2026-07-22-recent-chats-can-expand-in-the-sidebar.md +6 -0
  85. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +8 -8
  86. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +31 -0
  87. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +3 -5
  88. package/corpus/templates/analytics/server/lib/dashboard-report.ts +12 -0
  89. package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +3 -5
  90. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +150 -21
  91. package/corpus/templates/analytics/server/lib/google-sheets-export.ts +188 -0
  92. package/corpus/templates/analytics/server/lib/provider-api.ts +7 -2
  93. package/corpus/templates/analytics/server/plugins/collab.ts +6 -3
  94. package/corpus/templates/analytics/shared/sql-query-limits.ts +1 -0
  95. package/corpus/templates/assets/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  96. package/corpus/templates/assets/.agents/skills/real-time-collab/SKILL.md +35 -13
  97. package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +1 -1
  98. package/corpus/templates/assets/app/components/layout/Layout.tsx +1 -1
  99. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +54 -62
  100. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +2 -6
  101. package/corpus/templates/assets/app/routes/_index.tsx +8 -5
  102. package/corpus/templates/assets/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
  103. package/corpus/templates/assets/changelog/2026-07-22-recent-chats-expand-from-a-compact-list.md +6 -0
  104. package/corpus/templates/brain/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  105. package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +1 -1
  106. package/corpus/templates/brain/actions/rotate-source-ingest-token.ts +77 -0
  107. package/corpus/templates/brain/app/components/layout/Layout.tsx +5 -1
  108. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +38 -51
  109. package/corpus/templates/brain/app/hooks/use-navigation-state.ts +2 -9
  110. package/corpus/templates/brain/app/i18n-data.ts +120 -0
  111. package/corpus/templates/brain/app/routes/sources.tsx +156 -5
  112. package/corpus/templates/brain/changelog/2026-07-22-chat-runs-always-end-with-an-answer.md +6 -0
  113. package/corpus/templates/brain/changelog/2026-07-22-clear-background-chat-progress.md +6 -0
  114. package/corpus/templates/brain/changelog/2026-07-22-compact-sidebar-footer.md +6 -0
  115. package/corpus/templates/brain/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
  116. package/corpus/templates/brain/changelog/2026-07-22-long-chat-titles-stay-inside-the-sidebar.md +6 -0
  117. package/corpus/templates/brain/changelog/2026-07-22-new-clips-and-webhook-sources-now-show-their-secure-connecti.md +6 -0
  118. package/corpus/templates/brain/jobs/emit-netlify-brain-queue-cron.ts +125 -0
  119. package/corpus/templates/brain/netlify.toml +5 -1
  120. package/corpus/templates/brain/scripts/configure-prod-clips-source.ts +50 -0
  121. package/corpus/templates/brain/server/lib/capture-sanitization.ts +64 -6
  122. package/corpus/templates/brain/server/plugins/agent-chat.ts +4 -0
  123. package/corpus/templates/brain/server/plugins/brain-jobs.ts +1 -0
  124. package/corpus/templates/brain/server/routes/api/brain/queue/run.post.ts +55 -0
  125. package/corpus/templates/calendar/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  126. package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +1 -1
  127. package/corpus/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  128. package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  129. package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -0
  130. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +43 -67
  131. package/corpus/templates/chat/app/hooks/use-navigation-state.ts +2 -7
  132. package/corpus/templates/chat/app/routes/_index.tsx +8 -5
  133. package/corpus/templates/chat/changelog/2026-07-22-compact-sidebar-footer.md +6 -0
  134. package/corpus/templates/chat/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
  135. package/corpus/templates/chat/changelog/2026-07-22-recent-chats-are-easier-to-scan-and-expand.md +6 -0
  136. package/corpus/templates/clips/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  137. package/corpus/templates/clips/AGENTS.md +9 -1
  138. package/corpus/templates/clips/actions/export-to-brain.ts +82 -6
  139. package/corpus/templates/clips/actions/request-transcript.ts +25 -14
  140. package/corpus/templates/clips/changelog/2026-07-22-clips-transcripts-now-retry-brain-export.md +5 -0
  141. package/corpus/templates/clips/changelog/2026-07-22-german-umlauts-and-other-non-ascii-characters-now-paste-corr.md +6 -0
  142. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +26 -3
  143. package/corpus/templates/clips/jobs/emit-netlify-brain-export-cron.ts +118 -0
  144. package/corpus/templates/clips/netlify.toml +4 -1
  145. package/corpus/templates/clips/scripts/configure-prod-brain-export.ts +92 -0
  146. package/corpus/templates/clips/server/jobs/brain-export.ts +182 -0
  147. package/corpus/templates/clips/server/lib/brain-export-state.ts +42 -0
  148. package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +5 -1
  149. package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
  150. package/corpus/templates/clips/server/register-secrets.ts +42 -0
  151. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +10 -1
  152. package/corpus/templates/clips/server/routes/api/clips/brain-export/run.post.ts +45 -0
  153. package/corpus/templates/content/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  154. package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +1 -1
  155. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +1 -1
  156. package/corpus/templates/content/server/plugins/collab.ts +1 -1
  157. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  158. package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
  159. package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +2 -0
  160. package/corpus/templates/crm/actions/run.ts +2 -6
  161. package/corpus/templates/crm/actions/update-crm-record.ts +4 -2
  162. package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +6 -0
  163. package/corpus/templates/crm/changelog/2026-07-22-crm-field-validation-now-explains-which-fields-cannot-be-edi.md +6 -0
  164. package/corpus/templates/crm/changelog/2026-07-22-fixed-hubspot-contact-syncs-missing-records-updated-after-th.md +6 -0
  165. package/corpus/templates/crm/changelog/2026-07-22-fixed-pipeline-dashboard-panels-so-their-opportunity-data-lo.md +6 -0
  166. package/corpus/templates/crm/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
  167. package/corpus/templates/crm/server/crm/hubspot-adapter.ts +4 -1
  168. package/corpus/templates/design/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  169. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  170. package/corpus/templates/design/server/plugins/collab.ts +11 -8
  171. package/corpus/templates/dispatch/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  172. package/corpus/templates/forms/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  173. package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +1 -1
  174. package/corpus/templates/forms/app/components/layout/Layout.tsx +1 -0
  175. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +1 -1
  176. package/corpus/templates/forms/app/hooks/use-navigation-state.ts +2 -9
  177. package/corpus/templates/forms/app/root.tsx +1 -6
  178. package/corpus/templates/forms/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
  179. package/corpus/templates/macros/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  180. package/corpus/templates/mail/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  181. package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +1 -1
  182. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +2 -2
  183. package/corpus/templates/plan/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  184. package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +1 -1
  185. package/corpus/templates/plan/app/components/layout/Layout.tsx +5 -1
  186. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +57 -72
  187. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +2 -9
  188. package/corpus/templates/plan/app/lib/create-plan-routing.ts +2 -2
  189. package/corpus/templates/plan/app/pages/PlanChatPage.tsx +0 -1
  190. package/corpus/templates/plan/changelog/2026-07-22-compact-sidebar-footer.md +6 -0
  191. package/corpus/templates/plan/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
  192. package/corpus/templates/plan/changelog/2026-07-22-recent-chats-and-sidebar-controls-are-more-compact.md +6 -0
  193. package/corpus/templates/plan/server/plugins/collab.ts +5 -2
  194. package/corpus/templates/slides/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  195. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +1 -1
  196. package/corpus/templates/slides/server/plugins/collab.ts +11 -8
  197. package/corpus/templates/tasks/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  198. package/corpus/templates/tasks/.agents/skills/real-time-sync/SKILL.md +1 -1
  199. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +5 -22
  200. package/corpus/templates/tasks/changelog/2026-07-22-compact-sidebar-footer.md +6 -0
  201. package/corpus/toolkit/CHANGELOG.md +7 -0
  202. package/corpus/toolkit/README.md +12 -3
  203. package/corpus/toolkit/package.json +1 -1
  204. package/corpus/toolkit/src/chat-history/ChatHistoryRail.tsx +89 -0
  205. package/corpus/toolkit/src/chat-history/index.ts +5 -0
  206. package/corpus/toolkit/src/chat-history.css +59 -0
  207. package/corpus/toolkit/src/composer/RealtimeVoiceMode.tsx +106 -36
  208. package/corpus/toolkit/src/composer/VoiceButton.tsx +49 -0
  209. package/corpus/toolkit/src/composer/realtime-voice-audio-level.ts +2 -1
  210. package/corpus/toolkit/src/composer/useRealtimeVoiceMode.tsx +5 -3
  211. package/corpus/toolkit/src/styles.css +35 -0
  212. package/dist/agent/production-agent.d.ts.map +1 -1
  213. package/dist/agent/production-agent.js +13 -3
  214. package/dist/agent/production-agent.js.map +1 -1
  215. package/dist/cli/doctor.d.ts +1 -1
  216. package/dist/cli/doctor.d.ts.map +1 -1
  217. package/dist/cli/doctor.js +5 -2
  218. package/dist/cli/doctor.js.map +1 -1
  219. package/dist/client/AgentPanel.d.ts +1 -0
  220. package/dist/client/AgentPanel.d.ts.map +1 -1
  221. package/dist/client/AgentPanel.js +38 -18
  222. package/dist/client/AgentPanel.js.map +1 -1
  223. package/dist/client/AssistantChat.d.ts.map +1 -1
  224. package/dist/client/AssistantChat.js +55 -20
  225. package/dist/client/AssistantChat.js.map +1 -1
  226. package/dist/client/FeedbackButton.d.ts +4 -2
  227. package/dist/client/FeedbackButton.d.ts.map +1 -1
  228. package/dist/client/FeedbackButton.js +18 -2
  229. package/dist/client/FeedbackButton.js.map +1 -1
  230. package/dist/client/MultiTabAssistantChat.d.ts +1 -1
  231. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  232. package/dist/client/MultiTabAssistantChat.js +2 -3
  233. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  234. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  235. package/dist/client/agent-chat-adapter.js +11 -3
  236. package/dist/client/agent-chat-adapter.js.map +1 -1
  237. package/dist/client/agent-page/AgentTabFrame.d.ts +3 -1
  238. package/dist/client/agent-page/AgentTabFrame.d.ts.map +1 -1
  239. package/dist/client/agent-page/AgentTabFrame.js +3 -2
  240. package/dist/client/agent-page/AgentTabFrame.js.map +1 -1
  241. package/dist/client/agent-page/AgentTabsPage.d.ts +2 -0
  242. package/dist/client/agent-page/AgentTabsPage.d.ts.map +1 -1
  243. package/dist/client/agent-page/AgentTabsPage.js +26 -15
  244. package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
  245. package/dist/client/chat/message-components.d.ts +6 -1
  246. package/dist/client/chat/message-components.d.ts.map +1 -1
  247. package/dist/client/chat/message-components.js +17 -11
  248. package/dist/client/chat/message-components.js.map +1 -1
  249. package/dist/client/chat/tool-call-display.d.ts +11 -1
  250. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  251. package/dist/client/chat/tool-call-display.js +16 -17
  252. package/dist/client/chat/tool-call-display.js.map +1 -1
  253. package/dist/client/chat/tool-render-registry.d.ts +2 -0
  254. package/dist/client/chat/tool-render-registry.d.ts.map +1 -1
  255. package/dist/client/chat/tool-render-registry.js.map +1 -1
  256. package/dist/client/resources/McpConnectionSuggestion.d.ts.map +1 -1
  257. package/dist/client/resources/McpConnectionSuggestion.js +15 -1
  258. package/dist/client/resources/McpConnectionSuggestion.js.map +1 -1
  259. package/dist/client/resources/ResourceTree.d.ts.map +1 -1
  260. package/dist/client/resources/ResourceTree.js +3 -3
  261. package/dist/client/resources/ResourceTree.js.map +1 -1
  262. package/dist/client/resources/ResourcesPanel.d.ts +1 -0
  263. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  264. package/dist/client/resources/ResourcesPanel.js +34 -20
  265. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  266. package/dist/client/resources/mcp-connection-resume.d.ts +11 -0
  267. package/dist/client/resources/mcp-connection-resume.d.ts.map +1 -0
  268. package/dist/client/resources/mcp-connection-resume.js +95 -0
  269. package/dist/client/resources/mcp-connection-resume.js.map +1 -0
  270. package/dist/client/resources/mcp-integration-catalog.d.ts +1 -0
  271. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  272. package/dist/client/resources/mcp-integration-catalog.js +63 -8
  273. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  274. package/dist/client/resources/mcp-integration-logos.d.ts.map +1 -1
  275. package/dist/client/resources/mcp-integration-logos.js +4 -0
  276. package/dist/client/resources/mcp-integration-logos.js.map +1 -1
  277. package/dist/client/sse-event-processor.d.ts +5 -0
  278. package/dist/client/sse-event-processor.d.ts.map +1 -1
  279. package/dist/client/sse-event-processor.js +39 -13
  280. package/dist/client/sse-event-processor.js.map +1 -1
  281. package/dist/client/use-db-sync.d.ts +3 -2
  282. package/dist/client/use-db-sync.d.ts.map +1 -1
  283. package/dist/client/use-db-sync.js +17 -6
  284. package/dist/client/use-db-sync.js.map +1 -1
  285. package/dist/collab/routes.d.ts +1 -1
  286. package/dist/connections/catalog.d.ts +1 -1
  287. package/dist/connections/catalog.d.ts.map +1 -1
  288. package/dist/connections/catalog.js +7 -1
  289. package/dist/connections/catalog.js.map +1 -1
  290. package/dist/eject/remote-mcp-presets.d.ts.map +1 -1
  291. package/dist/eject/remote-mcp-presets.js +33 -3
  292. package/dist/eject/remote-mcp-presets.js.map +1 -1
  293. package/dist/guards/explicit-collab-access.d.ts +8 -0
  294. package/dist/guards/explicit-collab-access.d.ts.map +1 -0
  295. package/dist/guards/explicit-collab-access.js +214 -0
  296. package/dist/guards/explicit-collab-access.js.map +1 -0
  297. package/dist/guards/index.d.ts +1 -0
  298. package/dist/guards/index.d.ts.map +1 -1
  299. package/dist/guards/index.js +1 -0
  300. package/dist/guards/index.js.map +1 -1
  301. package/dist/localization/default-messages.d.ts +43 -0
  302. package/dist/localization/default-messages.d.ts.map +1 -1
  303. package/dist/localization/default-messages.js +48 -5
  304. package/dist/localization/default-messages.js.map +1 -1
  305. package/dist/mcp/connect-route.d.ts.map +1 -1
  306. package/dist/mcp/connect-route.js +34 -48
  307. package/dist/mcp/connect-route.js.map +1 -1
  308. package/dist/notifications/routes.d.ts +3 -3
  309. package/dist/observability/routes.d.ts +1 -1
  310. package/dist/progress/routes.d.ts +1 -1
  311. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  312. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  313. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  314. package/dist/provider-api/index.js +1 -1
  315. package/dist/provider-api/index.js.map +1 -1
  316. package/dist/server/collab-plugin.d.ts +34 -3
  317. package/dist/server/collab-plugin.d.ts.map +1 -1
  318. package/dist/server/collab-plugin.js +65 -12
  319. package/dist/server/collab-plugin.js.map +1 -1
  320. package/dist/server/index.d.ts +1 -1
  321. package/dist/server/index.d.ts.map +1 -1
  322. package/dist/server/index.js.map +1 -1
  323. package/dist/styles/agent-native.css +40 -0
  324. package/dist/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  325. package/dist/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  326. package/dist/templates/chat/app/components/layout/Layout.tsx +1 -0
  327. package/dist/templates/chat/app/components/layout/Sidebar.tsx +43 -67
  328. package/dist/templates/chat/app/hooks/use-navigation-state.ts +2 -7
  329. package/dist/templates/chat/app/routes/_index.tsx +8 -5
  330. package/dist/templates/chat/changelog/2026-07-22-compact-sidebar-footer.md +6 -0
  331. package/dist/templates/chat/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
  332. package/dist/templates/chat/changelog/2026-07-22-recent-chats-are-easier-to-scan-and-expand.md +6 -0
  333. package/dist/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  334. package/dist/templates/default/.agents/skills/real-time-collab/SKILL.md +35 -13
  335. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
  336. package/dist/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  337. package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  338. package/dist/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +35 -13
  339. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
  340. package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +12 -8
  341. package/docs/AGENTS.md +1 -1
  342. package/docs/content/agent-mentions.mdx +2 -2
  343. package/docs/content/{workspace.mdx → agent-resources.mdx} +1 -7
  344. package/docs/content/agent-teams.mdx +4 -4
  345. package/docs/content/dispatch.mdx +10 -10
  346. package/docs/content/drop-in-agent.mdx +4 -4
  347. package/docs/content/external-agents.mdx +1 -1
  348. package/docs/content/frames.mdx +1 -1
  349. package/docs/content/integrations.mdx +13 -2
  350. package/docs/content/key-concepts.mdx +2 -2
  351. package/docs/content/mcp-clients.mdx +7 -1
  352. package/docs/content/multi-app-workspace.mdx +3 -3
  353. package/docs/content/real-time-collaboration.mdx +33 -15
  354. package/docs/content/recurring-jobs.mdx +7 -7
  355. package/docs/content/skills-guide.mdx +4 -4
  356. package/docs/content/template-brain.mdx +1 -1
  357. package/docs/content/toolkit-collaboration.mdx +1 -1
  358. package/docs/content/using-your-agent.mdx +1 -1
  359. package/docs/content/what-is-agent-native.mdx +4 -4
  360. package/docs/content/workspace-management.mdx +4 -4
  361. package/package.json +3 -3
  362. package/src/agent/production-agent.ts +13 -3
  363. package/src/cli/doctor.ts +6 -1
  364. package/src/client/AgentPanel.tsx +79 -41
  365. package/src/client/AssistantChat.tsx +71 -25
  366. package/src/client/FeedbackButton.tsx +33 -7
  367. package/src/client/MultiTabAssistantChat.tsx +6 -8
  368. package/src/client/agent-chat-adapter.ts +11 -2
  369. package/src/client/agent-page/AgentTabFrame.tsx +22 -3
  370. package/src/client/agent-page/AgentTabsPage.tsx +34 -19
  371. package/src/client/chat/message-components.tsx +43 -16
  372. package/src/client/chat/tool-call-display.tsx +30 -18
  373. package/src/client/chat/tool-render-registry.tsx +2 -0
  374. package/src/client/resources/McpConnectionSuggestion.tsx +19 -2
  375. package/src/client/resources/ResourceTree.tsx +7 -10
  376. package/src/client/resources/ResourcesPanel.tsx +33 -70
  377. package/src/client/resources/mcp-connection-resume.ts +107 -0
  378. package/src/client/resources/mcp-integration-catalog.ts +65 -12
  379. package/src/client/resources/mcp-integration-logos.ts +4 -0
  380. package/src/client/sse-event-processor.ts +50 -14
  381. package/src/client/use-db-sync.ts +16 -6
  382. package/src/connections/catalog.ts +7 -1
  383. package/src/eject/remote-mcp-presets.ts +37 -5
  384. package/src/guards/explicit-collab-access.ts +229 -0
  385. package/src/guards/index.ts +1 -0
  386. package/src/localization/default-messages.ts +54 -5
  387. package/src/mcp/connect-route.ts +34 -51
  388. package/src/provider-api/index.ts +1 -1
  389. package/src/server/collab-plugin.ts +137 -18
  390. package/src/server/index.ts +2 -0
  391. package/src/styles/agent-native.css +40 -0
  392. package/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  393. package/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  394. package/src/templates/chat/app/components/layout/Layout.tsx +1 -0
  395. package/src/templates/chat/app/components/layout/Sidebar.tsx +43 -67
  396. package/src/templates/chat/app/hooks/use-navigation-state.ts +2 -7
  397. package/src/templates/chat/app/routes/_index.tsx +8 -5
  398. package/src/templates/chat/changelog/2026-07-22-compact-sidebar-footer.md +6 -0
  399. package/src/templates/chat/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
  400. package/src/templates/chat/changelog/2026-07-22-recent-chats-are-easier-to-scan-and-expand.md +6 -0
  401. package/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  402. package/src/templates/default/.agents/skills/real-time-collab/SKILL.md +35 -13
  403. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
  404. package/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  405. package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
  406. package/src/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +35 -13
  407. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
  408. package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +12 -8
@@ -124,7 +124,7 @@ const USER_KEY_SQL = "NULLIF(user_key, '')";
124
124
  const RETENTION_ROLLING_DAYS = 7;
125
125
  const RETENTION_MIN_COHORT_SIZE = 5;
126
126
  const PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE = 20;
127
- const RECURRING_ACTIVITY_LOOKBACK_DAYS = 365;
127
+ const OBSERVED_ACTIVITY_LOOKBACK_DAYS = 365;
128
128
 
129
129
  function daysAgoSql(days: number): string {
130
130
  const unit = days === 1 ? "day" : "days";
@@ -222,42 +222,160 @@ export function usesFirstPartyDashboardFilters(sql: string): boolean {
222
222
 
223
223
  const TOTAL_SIGNUPS_SQL = `SELECT COUNT(*) AS signups FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`;
224
224
  const SIGNUPS_OVER_TIME_SQL = `WITH offsets AS (SELECT (ROW_NUMBER() OVER (ORDER BY ${EVENT_DATE_SQL}) - 1)::int AS n FROM analytics_events LIMIT 800), signup_events AS (SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template`;
225
- const RETENTION_OVER_TIME_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS cohort_date FROM base GROUP BY user_key), anchor_dates AS (SELECT DISTINCT cohort_date AS date FROM first_seen WHERE cohort_date <= ${daysAgoSql(14)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_windows AS (SELECT a.date, f.user_key, f.cohort_date FROM anchor_dates a JOIN first_seen f ON f.cohort_date >= ${rollingWindowStartSql()} AND f.cohort_date <= a.date), cohort_sizes AS (SELECT date, COUNT(DISTINCT user_key) AS users FROM cohort_windows GROUP BY date), periods AS (SELECT '1-7d return' AS period UNION ALL SELECT '7-14d return' AS period), retained AS (SELECT cw.date, '1-7d return' AS period, COUNT(DISTINCT cw.user_key) AS retained FROM cohort_windows cw JOIN base b ON b.user_key = cw.user_key AND b.event_date > cw.cohort_date AND b.event_date <= to_char(cw.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') GROUP BY cw.date UNION ALL SELECT cw.date, '7-14d return' AS period, COUNT(DISTINCT cw.user_key) AS retained FROM cohort_windows cw JOIN base b ON b.user_key = cw.user_key AND b.event_date >= to_char(cw.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') AND b.event_date <= to_char(cw.cohort_date::date + INTERVAL '14 days', 'YYYY-MM-DD') GROUP BY cw.date) SELECT cs.date, p.period, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs CROSS JOIN periods p LEFT JOIN retained r ON r.date = cs.date AND r.period = p.period WHERE cs.users >= ${RETENTION_MIN_COHORT_SIZE} ORDER BY cs.date, p.period`;
226
- const ONE_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen AS (SELECT user_key, template, event_date AS cohort_date, ROW_NUMBER() OVER (PARTITION BY user_key ORDER BY event_date, template) AS rn FROM base), first_seen AS (SELECT user_key, template, cohort_date FROM ranked_first_seen WHERE rn = 1), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= ${daysAgoSql(7)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_sizes AS (SELECT template, COUNT(DISTINCT user_key) AS users FROM cohorts GROUP BY template), retained AS (SELECT c.template, COUNT(DISTINCT c.user_key) AS retained FROM cohorts c JOIN base b ON b.user_key = c.user_key AND b.event_date > c.cohort_date AND b.event_date <= to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') GROUP BY c.template) SELECT cs.template, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs LEFT JOIN retained r ON r.template = cs.template WHERE cs.users >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cs.users DESC, cs.template`;
227
- const SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen AS (SELECT user_key, template, event_date AS cohort_date, ROW_NUMBER() OVER (PARTITION BY user_key ORDER BY event_date, template) AS rn FROM base), first_seen AS (SELECT user_key, template, cohort_date FROM ranked_first_seen WHERE rn = 1), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= ${daysAgoSql(14)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_sizes AS (SELECT template, COUNT(DISTINCT user_key) AS users FROM cohorts GROUP BY template), retained AS (SELECT c.template, COUNT(DISTINCT c.user_key) AS retained FROM cohorts c JOIN base b ON b.user_key = c.user_key AND b.event_date >= to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') AND b.event_date <= to_char(c.cohort_date::date + INTERVAL '14 days', 'YYYY-MM-DD') GROUP BY c.template) SELECT cs.template, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs LEFT JOIN retained r ON r.template = cs.template WHERE cs.users >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cs.users DESC, cs.template`;
225
+ export const LEGACY_RETENTION_OVER_TIME_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS cohort_date FROM base GROUP BY user_key), anchor_dates AS (SELECT DISTINCT cohort_date AS date FROM first_seen WHERE cohort_date <= ${daysAgoSql(14)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_windows AS (SELECT a.date, f.user_key, f.cohort_date FROM anchor_dates a JOIN first_seen f ON f.cohort_date >= ${rollingWindowStartSql()} AND f.cohort_date <= a.date), cohort_sizes AS (SELECT date, COUNT(DISTINCT user_key) AS users FROM cohort_windows GROUP BY date), periods AS (SELECT '1-7d return' AS period UNION ALL SELECT '7-14d return' AS period), retained AS (SELECT cw.date, '1-7d return' AS period, COUNT(DISTINCT cw.user_key) AS retained FROM cohort_windows cw JOIN base b ON b.user_key = cw.user_key AND b.event_date > cw.cohort_date AND b.event_date <= to_char(cw.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') GROUP BY cw.date UNION ALL SELECT cw.date, '7-14d return' AS period, COUNT(DISTINCT cw.user_key) AS retained FROM cohort_windows cw JOIN base b ON b.user_key = cw.user_key AND b.event_date >= to_char(cw.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') AND b.event_date <= to_char(cw.cohort_date::date + INTERVAL '14 days', 'YYYY-MM-DD') GROUP BY cw.date) SELECT cs.date, p.period, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs CROSS JOIN periods p LEFT JOIN retained r ON r.date = cs.date AND r.period = p.period WHERE cs.users >= ${RETENTION_MIN_COHORT_SIZE} ORDER BY cs.date, p.period`;
226
+ export const LEGACY_ONE_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen AS (SELECT user_key, template, event_date AS cohort_date, ROW_NUMBER() OVER (PARTITION BY user_key ORDER BY event_date, template) AS rn FROM base), first_seen AS (SELECT user_key, template, cohort_date FROM ranked_first_seen WHERE rn = 1), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= ${daysAgoSql(7)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_sizes AS (SELECT template, COUNT(DISTINCT user_key) AS users FROM cohorts GROUP BY template), retained AS (SELECT c.template, COUNT(DISTINCT c.user_key) AS retained FROM cohorts c JOIN base b ON b.user_key = c.user_key AND b.event_date > c.cohort_date AND b.event_date <= to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') GROUP BY c.template) SELECT cs.template, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs LEFT JOIN retained r ON r.template = cs.template WHERE cs.users >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cs.users DESC, cs.template`;
227
+ export const LEGACY_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen AS (SELECT user_key, template, event_date AS cohort_date, ROW_NUMBER() OVER (PARTITION BY user_key ORDER BY event_date, template) AS rn FROM base), first_seen AS (SELECT user_key, template, cohort_date FROM ranked_first_seen WHERE rn = 1), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= ${daysAgoSql(14)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_sizes AS (SELECT template, COUNT(DISTINCT user_key) AS users FROM cohorts GROUP BY template), retained AS (SELECT c.template, COUNT(DISTINCT c.user_key) AS retained FROM cohorts c JOIN base b ON b.user_key = c.user_key AND b.event_date >= to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') AND b.event_date <= to_char(c.cohort_date::date + INTERVAL '14 days', 'YYYY-MM-DD') GROUP BY c.template) SELECT cs.template, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs LEFT JOIN retained r ON r.template = cs.template WHERE cs.users >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cs.users DESC, cs.template`;
228
+ export const LEGACY_V0_RETENTION_OVER_TIME_SQL =
229
+ LEGACY_RETENTION_OVER_TIME_SQL.replace(
230
+ "cohort_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ",
231
+ "",
232
+ )
233
+ .replace("AND (('{{timeRange}}", "AND ('{{timeRange}}")
234
+ .replace(
235
+ "CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))))",
236
+ "CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))",
237
+ );
238
+ export const LEGACY_V0_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL =
239
+ LEGACY_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL.replace(
240
+ "cohort_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ",
241
+ "",
242
+ )
243
+ .replace("AND (('{{timeRange}}", "AND ('{{timeRange}}")
244
+ .replace(
245
+ "CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))))",
246
+ "CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))",
247
+ );
248
+ export const LEGACY_V0_ONE_DAY_RETENTION_BY_TEMPLATE_SQL =
249
+ LEGACY_ONE_DAY_RETENTION_BY_TEMPLATE_SQL.replace(
250
+ "cohort_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ",
251
+ "",
252
+ )
253
+ .replace("AND (('{{timeRange}}", "AND ('{{timeRange}}")
254
+ .replace(
255
+ "CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))))",
256
+ "CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))",
257
+ );
228
258
  const SIGNUPS_BY_TEMPLATE_SQL = `SELECT ${TEMPLATE_EXPR} AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${TEMPLATE_EXPR} ORDER BY count DESC`;
229
259
  export const LEGACY_RECURRING_USERS_BY_TEMPLATE_SQL = `WITH all_users AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL}, user_id, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key) SELECT a.event_date AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' AND ${dashboardTimeRangeFilter("a.event_date")} GROUP BY 1, 2 ORDER BY date, template`;
230
260
  export const LEGACY_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL = `WITH all_users AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL}, user_id, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key) SELECT to_char(date_trunc('week', a.event_date::date), 'YYYY-MM-DD') AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' AND ${dashboardTimeRangeFilter("a.event_date")} GROUP BY 1, 2 ORDER BY date, template`;
231
- const RECURRING_ACTIVITY_LOOKBACK_FILTER = `${EVENT_DATE_SQL} >= ${daysAgoSql(RECURRING_ACTIVITY_LOOKBACK_DAYS)}`;
232
- const RECURRING_USERS_BY_TEMPLATE_SQL =
261
+ const OBSERVED_ACTIVITY_LOOKBACK_FILTER = `${EVENT_DATE_SQL} >= ${daysAgoSql(OBSERVED_ACTIVITY_LOOKBACK_DAYS)}`;
262
+ export const INTERMEDIATE_RECURRING_USERS_BY_TEMPLATE_SQL =
233
263
  LEGACY_RECURRING_USERS_BY_TEMPLATE_SQL.replace(
234
264
  `${DASHBOARD_EMAIL_FILTER}), first_seen`,
235
- `${DASHBOARD_EMAIL_FILTER} AND ${RECURRING_ACTIVITY_LOOKBACK_FILTER}), first_seen`,
265
+ `${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER}), first_seen`,
236
266
  );
237
- const RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL =
267
+ export const INTERMEDIATE_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL =
238
268
  LEGACY_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL.replace(
239
269
  `${DASHBOARD_EMAIL_FILTER}), first_seen`,
240
- `${DASHBOARD_EMAIL_FILTER} AND ${RECURRING_ACTIVITY_LOOKBACK_FILTER}), first_seen`,
270
+ `${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER}), first_seen`,
241
271
  );
272
+ export const DEPLOYED_RECURRING_USERS_BY_TEMPLATE_SQL =
273
+ LEGACY_RECURRING_USERS_BY_TEMPLATE_SQL.replace(
274
+ "a.event_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ",
275
+ "",
276
+ )
277
+ .replace("AND (('{{timeRange}}", "AND ('{{timeRange}}")
278
+ .replace(
279
+ "CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))))",
280
+ "CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))",
281
+ );
282
+ const RETENTION_OVER_TIME_SQL = LEGACY_RETENTION_OVER_TIME_SQL.replace(
283
+ `${DASHBOARD_EMAIL_FILTER}), first_seen`,
284
+ `${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER}), first_seen`,
285
+ );
286
+ const ONE_DAY_RETENTION_BY_TEMPLATE_SQL =
287
+ LEGACY_ONE_DAY_RETENTION_BY_TEMPLATE_SQL.replace(
288
+ `${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen`,
289
+ `${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER}), ranked_first_seen`,
290
+ );
291
+ const SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL =
292
+ LEGACY_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL.replace(
293
+ `${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen`,
294
+ `${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER}), ranked_first_seen`,
295
+ );
296
+ const RECURRING_USERS_BY_TEMPLATE_SQL = `WITH first_seen AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, MIN(${EVENT_DATE_SQL}) AS first_date FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} GROUP BY 1), activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER}) SELECT a.event_date AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM activity a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' GROUP BY 1, 2 ORDER BY date, template`;
297
+ const RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL = `WITH first_seen AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, MIN(${EVENT_DATE_SQL}) AS first_date FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} GROUP BY 1), activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER}) SELECT to_char(date_trunc('week', a.event_date::date), 'YYYY-MM-DD') AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM activity a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' GROUP BY 1, 2 ORDER BY date, template`;
242
298
  const LEGACY_RECURRING_USERS_DESCRIPTION =
243
299
  "Daily signed-in visitors who are NOT on their all-time first active day (Recurring only), stacked by inferred template/app used that day. Docs traffic and unknown template are excluded.";
244
300
  const LEGACY_RECURRING_USERS_WEEKLY_DESCRIPTION =
245
301
  "Weekly distinct signed-in visitors who are NOT on their all-time first active day (Recurring only), stacked by inferred template/app used that week. Weeks start Monday; docs traffic and unknown template are excluded.";
302
+ const LEGACY_RETENTION_OVER_TIME_DESCRIPTION =
303
+ "Trailing 7-day first-seen signed-in app session cohorts, keyed by browser identity. Counts returns within 1-7d and 7-14d windows. Docs traffic is excluded; windows under 5 identities are hidden.";
304
+ const LEGACY_ONE_DAY_RETENTION_BY_TEMPLATE_DESCRIPTION =
305
+ "Selected-range signed-in cohorts by the browser identity's first non-docs app/template. Counts returns to any non-docs app within 1-7 days. Templates with fewer than 20 mature cohort identities are hidden.";
306
+ const LEGACY_SEVEN_DAY_RETENTION_BY_TEMPLATE_DESCRIPTION =
307
+ "Selected-range signed-in cohorts by the browser identity's first non-docs app/template. Counts returns to any non-docs app within 7-14 days. Templates with fewer than 20 mature cohort identities are hidden.";
246
308
  const RECURRING_USERS_DESCRIPTION =
247
309
  "Daily signed-in visitors who are not on their first active day observed in the previous 365 days, stacked by inferred template/app used that day. Docs traffic and unknown template are excluded.";
248
310
  const RECURRING_USERS_WEEKLY_DESCRIPTION =
249
311
  "Weekly distinct signed-in visitors who are not on their first active day observed in the previous 365 days, stacked by inferred template/app used that week. Weeks start Monday; docs traffic and unknown template are excluded.";
312
+ const RETENTION_OVER_TIME_DESCRIPTION =
313
+ "Trailing 7-day cohorts whose first signed-in app session was observed in the previous 365 days, keyed by browser identity. Counts returns within 1-7d and 7-14d windows. Docs traffic is excluded; windows under 5 identities are hidden.";
314
+ const ONE_DAY_RETENTION_BY_TEMPLATE_DESCRIPTION =
315
+ "Selected-range signed-in cohorts by the browser identity's first non-docs app/template observed in the previous 365 days. Counts returns to any non-docs app within 1-7 days. Templates with fewer than 20 mature cohort identities are hidden.";
316
+ const SEVEN_DAY_RETENTION_BY_TEMPLATE_DESCRIPTION =
317
+ "Selected-range signed-in cohorts by the browser identity's first non-docs app/template observed in the previous 365 days. Counts returns to any non-docs app within 7-14 days. Templates with fewer than 20 mature cohort identities are hidden.";
250
318
 
251
- export function repairFirstPartyRecurringUserPanels(
319
+ export type ExactFirstPartyPanelReplacement = {
320
+ id: string;
321
+ legacySql: readonly string[];
322
+ sql: string;
323
+ legacyDescription?: string;
324
+ description?: string;
325
+ };
326
+ type FirstPartyPanelReplacement = Omit<ExactFirstPartyPanelReplacement, "id">;
327
+
328
+ export function repairFirstPartyObservedRetentionPanels(
252
329
  config: Record<string, unknown>,
330
+ additionalReplacements: readonly ExactFirstPartyPanelReplacement[] = [],
253
331
  ): { config: Record<string, unknown>; changed: boolean } {
254
332
  if (!Array.isArray(config.panels)) return { config, changed: false };
255
333
 
256
- const replacements = new Map([
334
+ const replacements = new Map<string, FirstPartyPanelReplacement>([
335
+ [
336
+ "retention-over-time",
337
+ {
338
+ legacySql: [
339
+ LEGACY_RETENTION_OVER_TIME_SQL,
340
+ LEGACY_V0_RETENTION_OVER_TIME_SQL,
341
+ ],
342
+ sql: RETENTION_OVER_TIME_SQL,
343
+ legacyDescription: LEGACY_RETENTION_OVER_TIME_DESCRIPTION,
344
+ description: RETENTION_OVER_TIME_DESCRIPTION,
345
+ },
346
+ ],
347
+ [
348
+ "one-day-retention-by-template",
349
+ {
350
+ legacySql: [
351
+ LEGACY_ONE_DAY_RETENTION_BY_TEMPLATE_SQL,
352
+ LEGACY_V0_ONE_DAY_RETENTION_BY_TEMPLATE_SQL,
353
+ ],
354
+ sql: ONE_DAY_RETENTION_BY_TEMPLATE_SQL,
355
+ legacyDescription: LEGACY_ONE_DAY_RETENTION_BY_TEMPLATE_DESCRIPTION,
356
+ description: ONE_DAY_RETENTION_BY_TEMPLATE_DESCRIPTION,
357
+ },
358
+ ],
359
+ [
360
+ "seven-day-retention-by-template",
361
+ {
362
+ legacySql: [
363
+ LEGACY_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL,
364
+ LEGACY_V0_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL,
365
+ ],
366
+ sql: SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL,
367
+ legacyDescription: LEGACY_SEVEN_DAY_RETENTION_BY_TEMPLATE_DESCRIPTION,
368
+ description: SEVEN_DAY_RETENTION_BY_TEMPLATE_DESCRIPTION,
369
+ },
370
+ ],
257
371
  [
258
372
  "recurring-users-by-template",
259
373
  {
260
- legacySql: LEGACY_RECURRING_USERS_BY_TEMPLATE_SQL,
374
+ legacySql: [
375
+ LEGACY_RECURRING_USERS_BY_TEMPLATE_SQL,
376
+ INTERMEDIATE_RECURRING_USERS_BY_TEMPLATE_SQL,
377
+ DEPLOYED_RECURRING_USERS_BY_TEMPLATE_SQL,
378
+ ],
261
379
  sql: RECURRING_USERS_BY_TEMPLATE_SQL,
262
380
  legacyDescription: LEGACY_RECURRING_USERS_DESCRIPTION,
263
381
  description: RECURRING_USERS_DESCRIPTION,
@@ -266,12 +384,18 @@ export function repairFirstPartyRecurringUserPanels(
266
384
  [
267
385
  "recurring-users-by-template-bar",
268
386
  {
269
- legacySql: LEGACY_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL,
387
+ legacySql: [
388
+ LEGACY_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL,
389
+ INTERMEDIATE_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL,
390
+ ],
270
391
  sql: RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL,
271
392
  legacyDescription: LEGACY_RECURRING_USERS_WEEKLY_DESCRIPTION,
272
393
  description: RECURRING_USERS_WEEKLY_DESCRIPTION,
273
394
  },
274
395
  ],
396
+ ...additionalReplacements.map(
397
+ (replacement) => [replacement.id, replacement] as const,
398
+ ),
275
399
  ]);
276
400
  let changed = false;
277
401
  const panels = config.panels.map((rawPanel) => {
@@ -279,7 +403,13 @@ export function repairFirstPartyRecurringUserPanels(
279
403
  const panel = rawPanel as Record<string, unknown>;
280
404
  const replacement =
281
405
  typeof panel.id === "string" ? replacements.get(panel.id) : undefined;
282
- if (!replacement || panel.sql !== replacement.legacySql) return rawPanel;
406
+ if (
407
+ !replacement ||
408
+ typeof panel.sql !== "string" ||
409
+ !replacement.legacySql.includes(panel.sql)
410
+ ) {
411
+ return rawPanel;
412
+ }
283
413
 
284
414
  changed = true;
285
415
  const panelConfig =
@@ -293,7 +423,9 @@ export function repairFirstPartyRecurringUserPanels(
293
423
  ? {
294
424
  config: {
295
425
  ...panelConfig,
296
- ...(panelConfig.description === replacement.legacyDescription
426
+ ...(replacement.legacyDescription !== undefined &&
427
+ replacement.description !== undefined &&
428
+ panelConfig.description === replacement.legacyDescription
297
429
  ? { description: replacement.description }
298
430
  : {}),
299
431
  },
@@ -806,8 +938,7 @@ const ENTRIES: FirstPartyMetric[] = [
806
938
  valueKey: "rate",
807
939
  },
808
940
  colors: ["#10b981", "#8b5cf6"],
809
- description:
810
- "Trailing 7-day first-seen signed-in app session cohorts, keyed by browser identity. Counts returns within 1-7d and 7-14d windows. Docs traffic is excluded; windows under 5 identities are hidden.",
941
+ description: RETENTION_OVER_TIME_DESCRIPTION,
811
942
  },
812
943
  },
813
944
  {
@@ -828,8 +959,7 @@ const ENTRIES: FirstPartyMetric[] = [
828
959
  { key: "retained_users", label: "Returned", format: "number" },
829
960
  { key: "cohort_users", label: "Cohort", format: "number" },
830
961
  ],
831
- description:
832
- "Selected-range signed-in cohorts by the browser identity's first non-docs app/template. Counts returns to any non-docs app within 1-7 days. Templates with fewer than 20 mature cohort identities are hidden.",
962
+ description: ONE_DAY_RETENTION_BY_TEMPLATE_DESCRIPTION,
833
963
  },
834
964
  },
835
965
  {
@@ -850,8 +980,7 @@ const ENTRIES: FirstPartyMetric[] = [
850
980
  { key: "retained_users", label: "Returned", format: "number" },
851
981
  { key: "cohort_users", label: "Cohort", format: "number" },
852
982
  ],
853
- description:
854
- "Selected-range signed-in cohorts by the browser identity's first non-docs app/template. Counts returns to any non-docs app within 7-14 days. Templates with fewer than 20 mature cohort identities are hidden.",
983
+ description: SEVEN_DAY_RETENTION_BY_TEMPLATE_DESCRIPTION,
855
984
  },
856
985
  },
857
986
  {
@@ -0,0 +1,188 @@
1
+ import { executeProviderApiRequest } from "./provider-api";
2
+
3
+ export interface GoogleSheetExportInput {
4
+ dashboardId: string;
5
+ dashboardTitle: string;
6
+ panelId: string;
7
+ panelTitle: string;
8
+ source: string;
9
+ rows: Record<string, unknown>[];
10
+ schema: { name: string; type: string }[];
11
+ truncated?: boolean;
12
+ bytesProcessed?: number;
13
+ }
14
+
15
+ export interface GoogleSheetExportResult {
16
+ spreadsheetUrl: string;
17
+ spreadsheetId: string;
18
+ rowCount: number;
19
+ metadata: {
20
+ dashboardId: string;
21
+ dashboardTitle: string;
22
+ panelId: string;
23
+ panelTitle: string;
24
+ source: string;
25
+ chartType: "table";
26
+ sheetTitle: string;
27
+ columns: string[];
28
+ columnCount: number;
29
+ truncated: boolean;
30
+ bytesProcessed: number | null;
31
+ provider: "google_drive";
32
+ updatedCells: number | null;
33
+ };
34
+ }
35
+
36
+ type ProviderResponse = {
37
+ ok?: boolean;
38
+ status?: number;
39
+ statusText?: string;
40
+ json?: unknown;
41
+ };
42
+
43
+ function providerResponse(result: unknown, label: string): ProviderResponse {
44
+ const response = (result as { response?: unknown } | null)?.response;
45
+ if (!response || typeof response !== "object") {
46
+ throw new Error(`${label} returned an unexpected response.`);
47
+ }
48
+ const typed = response as ProviderResponse;
49
+ if (!typed.ok) {
50
+ const status = typed.status ? ` HTTP ${typed.status}` : "";
51
+ const statusText = typed.statusText ? ` ${typed.statusText}` : "";
52
+ throw new Error(`${label} failed${status}${statusText}.`);
53
+ }
54
+ return typed;
55
+ }
56
+
57
+ function asRecord(value: unknown): Record<string, unknown> {
58
+ return value && typeof value === "object" && !Array.isArray(value)
59
+ ? (value as Record<string, unknown>)
60
+ : {};
61
+ }
62
+
63
+ function sheetCell(value: unknown): string | number | boolean {
64
+ if (value === null || value === undefined) return "";
65
+ if (typeof value === "string" || typeof value === "boolean") return value;
66
+ if (typeof value === "number") return Number.isFinite(value) ? value : "";
67
+ try {
68
+ return JSON.stringify(value);
69
+ } catch {
70
+ return String(value);
71
+ }
72
+ }
73
+
74
+ function quoteSheetTitle(title: string): string {
75
+ return `'${title.replace(/'/g, "''")}'`;
76
+ }
77
+
78
+ function normalizeSheetTitle(
79
+ dashboardTitle: string,
80
+ panelTitle: string,
81
+ ): string {
82
+ const title = `${dashboardTitle} - ${panelTitle}`
83
+ .replace(/[\u0000-\u001f\u007f]/g, " ")
84
+ .trim();
85
+ return (title || "Analytics export").slice(0, 100);
86
+ }
87
+
88
+ function sheetColumnCount(
89
+ schema: GoogleSheetExportInput["schema"],
90
+ rows: GoogleSheetExportInput["rows"],
91
+ ): string[] {
92
+ const names = schema
93
+ .map((column) => column.name.trim())
94
+ .filter((name, index, columns) => name && columns.indexOf(name) === index);
95
+ if (names.length > 0) return names;
96
+ return Object.keys(rows[0] ?? {});
97
+ }
98
+
99
+ export async function exportDashboardPanelToGoogleSheet(
100
+ input: GoogleSheetExportInput,
101
+ ): Promise<GoogleSheetExportResult> {
102
+ const columns = sheetColumnCount(input.schema, input.rows);
103
+ const sheetTitle = normalizeSheetTitle(
104
+ input.dashboardTitle,
105
+ input.panelTitle,
106
+ );
107
+ const createResult = await executeProviderApiRequest({
108
+ provider: "google_drive",
109
+ method: "POST",
110
+ path: "https://sheets.googleapis.com/v4/spreadsheets",
111
+ query: {
112
+ fields: "spreadsheetId,spreadsheetUrl,properties.title,sheets.properties",
113
+ },
114
+ body: { properties: { title: sheetTitle } },
115
+ });
116
+ const createResponse = providerResponse(
117
+ createResult,
118
+ "Google Sheet creation",
119
+ );
120
+ const created = asRecord(createResponse.json);
121
+ const spreadsheetId =
122
+ typeof created.spreadsheetId === "string" ? created.spreadsheetId : "";
123
+ if (!spreadsheetId) {
124
+ throw new Error("Google Sheet creation did not return a spreadsheet id.");
125
+ }
126
+
127
+ const sheets = Array.isArray(created.sheets) ? created.sheets : [];
128
+ const firstSheet = asRecord(sheets[0]);
129
+ const firstProperties = asRecord(firstSheet.properties);
130
+ const actualSheetTitle =
131
+ typeof firstProperties.title === "string" && firstProperties.title
132
+ ? firstProperties.title
133
+ : "Sheet1";
134
+ let updatedCells: number | null = null;
135
+
136
+ if (columns.length > 0) {
137
+ const values = [
138
+ columns,
139
+ ...input.rows.map((row) =>
140
+ columns.map((column) => sheetCell(row[column])),
141
+ ),
142
+ ];
143
+ const updateResult = await executeProviderApiRequest({
144
+ provider: "google_drive",
145
+ method: "PUT",
146
+ path: `https://sheets.googleapis.com/v4/spreadsheets/${encodeURIComponent(spreadsheetId)}/values/${encodeURIComponent(`${quoteSheetTitle(actualSheetTitle)}!A1`)}`,
147
+ query: { valueInputOption: "RAW" },
148
+ body: {
149
+ range: `${quoteSheetTitle(actualSheetTitle)}!A1`,
150
+ majorDimension: "ROWS",
151
+ values,
152
+ },
153
+ });
154
+ const updateResponse = providerResponse(
155
+ updateResult,
156
+ "Google Sheet values update",
157
+ );
158
+ const updated = asRecord(updateResponse.json);
159
+ updatedCells =
160
+ typeof updated.updatedCells === "number" ? updated.updatedCells : null;
161
+ }
162
+
163
+ const spreadsheetUrl =
164
+ typeof created.spreadsheetUrl === "string" && created.spreadsheetUrl
165
+ ? created.spreadsheetUrl
166
+ : `https://docs.google.com/spreadsheets/d/${encodeURIComponent(spreadsheetId)}/edit`;
167
+
168
+ return {
169
+ spreadsheetUrl,
170
+ spreadsheetId,
171
+ rowCount: input.rows.length,
172
+ metadata: {
173
+ dashboardId: input.dashboardId,
174
+ dashboardTitle: input.dashboardTitle,
175
+ panelId: input.panelId,
176
+ panelTitle: input.panelTitle,
177
+ source: input.source,
178
+ chartType: "table",
179
+ sheetTitle: actualSheetTitle,
180
+ columns,
181
+ columnCount: columns.length,
182
+ truncated: input.truncated === true,
183
+ bytesProcessed: input.bytesProcessed ?? null,
184
+ provider: "google_drive",
185
+ updatedCells,
186
+ },
187
+ };
188
+ }
@@ -11,8 +11,13 @@ import {
11
11
  import { requireRequestCredentialContext } from "./credentials-context";
12
12
  import { resolveAnalyticsProviderCredential } from "./provider-credentials";
13
13
 
14
- export const ANALYTICS_PROVIDER_API_IDS = listProviderApiIdsForTemplateUse(
15
- "analytics",
14
+ export const ANALYTICS_PROVIDER_API_IDS = Array.from(
15
+ new Set([
16
+ ...listProviderApiIdsForTemplateUse("analytics"),
17
+ // Google Sheets creation and writes use the existing Google Drive OAuth
18
+ // connection, whose consent is limited to drive.file.
19
+ "google_drive" as ProviderApiId,
20
+ ]),
16
21
  ) as [ProviderApiId, ...ProviderApiId[]];
17
22
  export type AnalyticsProviderApiId = ProviderApiId;
18
23
  export type { ProviderApiMethod, ProviderApiRequestArgs };
@@ -10,7 +10,10 @@ export default createCollabPlugin({
10
10
  contentColumn: "config",
11
11
  idColumn: "id",
12
12
  autoSeed: true,
13
- resourceType: "dashboard",
14
- resolveResourceId: (docId) =>
15
- docId.startsWith("dash-") ? docId.slice("dash-".length) : docId,
13
+ access: {
14
+ mode: "resource",
15
+ resourceType: "dashboard",
16
+ resolveResourceId: (docId) =>
17
+ docId.startsWith("dash-") ? docId.slice("dash-".length) : docId,
18
+ },
16
19
  });
@@ -0,0 +1 @@
1
+ export const MAX_CONCURRENT_SQL_QUERIES = 4;
@@ -85,7 +85,10 @@ in the moment of agent use.
85
85
  approval, generative UI, progress, and screen-state exposure.
86
86
  - **Chat history kit**: presentational chat lists and recent-chat rails belong
87
87
  in Toolkit; Core keeps thread persistence, agent execution, transport, and
88
- page-to-sidebar handoff. Apps inject routing, labels, and domain actions.
88
+ page-to-sidebar handoff. Use Toolkit's `ChatHistoryRail` for the standard
89
+ five-item sidebar preview and a footer row with New chat followed by an
90
+ ellipsis disclosure up to fifteen. Apps inject routing, labels, and domain
91
+ actions.
89
92
  - **Agent page kit**: the full-page `/agent` surface (`AgentTabsPage` from
90
93
  `@agent-native/core/client`) with Context, Files, Connections, Jobs, and
91
94
  Access tabs plus a Personal/Organization scope toggle. The canonical home
@@ -16,7 +16,7 @@ metadata:
16
16
 
17
17
  Collaborative editing uses Yjs CRDT via TipTap. The agent and human users are
18
18
  equal participants — both edit the same Y.Doc and changes merge cleanly without
19
- conflicts. Always set `resourceType` on `createCollabPlugin`.
19
+ conflicts. Always choose an explicit `access` mode on `createCollabPlugin`.
20
20
 
21
21
  ## How It Works
22
22
 
@@ -104,7 +104,16 @@ in **different** regions merge fine through the CRDT.
104
104
 
105
105
  ## Security
106
106
 
107
- ### Always set `resourceType`
107
+ ### Always choose an explicit access mode
108
+
109
+ Inspect the schema and sharing registration before configuring collaboration:
110
+
111
+ - For owned or shareable records, use `mode: "resource"` and the resource type
112
+ registered through `registerShareableResource`.
113
+ - For records deliberately visible to every authenticated user on the
114
+ deployment, use `mode: "all-authenticated"`.
115
+ - Never invent a `resourceType` merely to silence a warning. Resource mode is
116
+ correct only when the matching ownership and sharing model exists.
108
117
 
109
118
  ```ts
110
119
  // server/plugins/collab.ts
@@ -114,14 +123,23 @@ export default createCollabPlugin({
114
123
  table: "documents",
115
124
  contentColumn: "content",
116
125
  idColumn: "id",
117
- resourceType: "document", // required
126
+ access: { mode: "resource", resourceType: "document" },
118
127
  });
119
128
  ```
120
129
 
121
- Without `resourceType`, the server logs a one-time warning and collab push
122
- events are delivered to **all authenticated users** without document-level
123
- scoping. Set it to the resource type name registered via
124
- `registerShareableResource`.
130
+ For intentionally deployment-wide authenticated collaboration:
131
+
132
+ ```ts
133
+ createCollabPlugin({
134
+ table: "todos",
135
+ contentColumn: "content",
136
+ access: { mode: "all-authenticated" },
137
+ });
138
+ ```
139
+
140
+ Omitting `access` is supported only for legacy compatibility and is deprecated.
141
+ It behaves like `all-authenticated`, logs a warning, and is flagged by Doctor.
142
+ Choose the mode explicitly so access intent is reviewable.
125
143
 
126
144
  Non-owner sharees who have explicit access fall back to state-vector catch-up
127
145
  (safe, slightly higher latency). Awareness routes require the same viewer
@@ -133,7 +151,10 @@ Write routes reject payloads exceeding `maxPayloadBytes` (default 2 MB) with
133
151
  HTTP 413. Override:
134
152
 
135
153
  ```ts
136
- createCollabPlugin({ resourceType: "document", maxPayloadBytes: 512 * 1024 });
154
+ createCollabPlugin({
155
+ access: { mode: "resource", resourceType: "document" },
156
+ maxPayloadBytes: 512 * 1024,
157
+ });
137
158
  ```
138
159
 
139
160
  ## Enabling Collaboration
@@ -144,7 +165,7 @@ createCollabPlugin({ resourceType: "document", maxPayloadBytes: 512 * 1024 });
144
165
  pnpm add @tiptap/extension-collaboration @tiptap/extension-collaboration-caret @tiptap/y-tiptap @tiptap/core
145
166
  ```
146
167
 
147
- ### 2. Add collab server plugin (with `resourceType`)
168
+ ### 2. Add collab server plugin (with explicit access)
148
169
 
149
170
  ```ts
150
171
  // server/plugins/collab.ts
@@ -154,7 +175,7 @@ export default createCollabPlugin({
154
175
  table: "documents",
155
176
  contentColumn: "content",
156
177
  idColumn: "id",
157
- resourceType: "document",
178
+ access: { mode: "resource", resourceType: "document" },
158
179
  });
159
180
  ```
160
181
 
@@ -343,9 +364,10 @@ the whole history — external/agent edits must not wipe the user's undo stack.
343
364
 
344
365
  ## Common Pitfalls
345
366
 
346
- - **Missing `resourceType`** — The server logs a warning on startup and
347
- delivers collab events to all authenticated users without access scoping.
348
- Always set `resourceType`.
367
+ - **Missing `access`** — Legacy omission behaves like `all-authenticated`, logs
368
+ a startup warning, and is flagged by Doctor. Choose `resource` for records
369
+ backed by ownership/sharing, or explicit `all-authenticated` for intentionally
370
+ deployment-wide records. Never invent a resource type to silence the warning.
349
371
  - **Don't pass `content` as a TipTap prop** when Collaboration is enabled —
350
372
  Yjs owns the content. Seed via `editor.commands.setContent()` only when the
351
373
  Y.XmlFragment is empty.
@@ -72,7 +72,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
72
72
  });
73
73
  ```
74
74
 
75
- `useDbSync` and every `subscribeSyncEvents` subscriber share one `EventSource` and one fallback poll loop per tab — this is how collaborative documents receive doc updates and cursor/awareness events.
75
+ `useDbSync` and every `subscribeSyncEvents` subscriber share one `EventSource` and one fallback poll loop per tab — this is how collaborative documents receive doc updates and cursor/awareness events. Hidden tabs keep that shared transport alive by default: SSE stays connected and active fallback polling relaxes to a 10-second floor (idle polling remains once per minute). Pass `pauseWhenHidden: true` only for a consumer that explicitly must stop all hidden-tab sync.
76
76
 
77
77
  ## Which sources to depend on
78
78
 
@@ -71,7 +71,7 @@ export function Layout({ children }: LayoutProps) {
71
71
  // A direct /chat/:id link already names the thread and must preserve the
72
72
  // normal chat-to-sidebar transition. Only the empty home chat needs the
73
73
  // recent marker that proves an agent handoff is in flight.
74
- requireActiveHandoff: location.pathname === "/",
74
+ requireActiveHandoff: false,
75
75
  });
76
76
 
77
77
  useEffect(() => {