@agent-native/core 0.79.12 → 0.79.15

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 (285) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/docs/content/locales/zh-TW/a2a-protocol.md +68 -68
  4. package/corpus/core/docs/content/locales/zh-TW/actions.md +82 -82
  5. package/corpus/core/docs/content/locales/zh-TW/agent-mentions.md +22 -22
  6. package/corpus/core/docs/content/locales/zh-TW/agent-surfaces.md +82 -82
  7. package/corpus/core/docs/content/locales/zh-TW/agent-teams.md +29 -29
  8. package/corpus/core/docs/content/locales/zh-TW/agent-web-surfaces.md +11 -11
  9. package/corpus/core/docs/content/locales/zh-TW/audit-log.md +16 -16
  10. package/corpus/core/docs/content/locales/zh-TW/authentication.md +32 -32
  11. package/corpus/core/docs/content/locales/zh-TW/automations.md +32 -32
  12. package/corpus/core/docs/content/locales/zh-TW/blueprint-installer.md +19 -19
  13. package/corpus/core/docs/content/locales/zh-TW/cli-adapters.md +17 -17
  14. package/corpus/core/docs/content/locales/zh-TW/client.md +62 -62
  15. package/corpus/core/docs/content/locales/zh-TW/cloneable-saas.md +32 -32
  16. package/corpus/core/docs/content/locales/zh-TW/code-agents-ui.md +91 -91
  17. package/corpus/core/docs/content/locales/zh-TW/components.md +51 -51
  18. package/corpus/core/docs/content/locales/zh-TW/context-awareness.md +66 -66
  19. package/corpus/core/docs/content/locales/zh-TW/creating-templates.md +39 -39
  20. package/corpus/core/docs/content/locales/zh-TW/cross-app-sso.md +29 -29
  21. package/corpus/core/docs/content/locales/zh-TW/database.md +21 -21
  22. package/corpus/core/docs/content/locales/zh-TW/deployment.md +37 -37
  23. package/corpus/core/docs/content/locales/zh-TW/dispatch.md +43 -43
  24. package/corpus/core/docs/content/locales/zh-TW/drop-in-agent.md +34 -34
  25. package/corpus/core/docs/content/locales/zh-TW/durable-resume.md +18 -18
  26. package/corpus/core/docs/content/locales/zh-TW/embedding-sdk.md +73 -73
  27. package/corpus/core/docs/content/locales/zh-TW/evals.md +16 -16
  28. package/corpus/core/docs/content/locales/zh-TW/extensions.md +81 -81
  29. package/corpus/core/docs/content/locales/zh-TW/external-agents.md +146 -146
  30. package/corpus/core/docs/content/locales/zh-TW/faq.md +43 -43
  31. package/corpus/core/docs/content/locales/zh-TW/file-uploads.md +11 -11
  32. package/corpus/core/docs/content/locales/zh-TW/frames.md +33 -33
  33. package/corpus/core/docs/content/locales/zh-TW/getting-started.md +28 -28
  34. package/corpus/core/docs/content/locales/zh-TW/harness-agents.md +78 -78
  35. package/corpus/core/docs/content/locales/zh-TW/human-approval.md +27 -27
  36. package/corpus/core/docs/content/locales/zh-TW/internationalization.md +19 -19
  37. package/corpus/core/docs/content/locales/zh-TW/key-concepts.md +68 -68
  38. package/corpus/core/docs/content/locales/zh-TW/local-file-mode.md +50 -50
  39. package/corpus/core/docs/content/locales/zh-TW/mcp-apps.md +38 -38
  40. package/corpus/core/docs/content/locales/zh-TW/mcp-clients.md +67 -67
  41. package/corpus/core/docs/content/locales/zh-TW/mcp-protocol.md +50 -50
  42. package/corpus/core/docs/content/locales/zh-TW/messaging.md +99 -99
  43. package/corpus/core/docs/content/locales/zh-TW/migration-workbench.md +6 -6
  44. package/corpus/core/docs/content/locales/zh-TW/multi-app-workspace.md +39 -39
  45. package/corpus/core/docs/content/locales/zh-TW/multi-tenancy.md +7 -7
  46. package/corpus/core/docs/content/locales/zh-TW/native-chat-ui.md +38 -38
  47. package/corpus/core/docs/content/locales/zh-TW/notifications.md +23 -23
  48. package/corpus/core/docs/content/locales/zh-TW/observability.md +49 -49
  49. package/corpus/core/docs/content/locales/zh-TW/observational-memory.md +23 -23
  50. package/corpus/core/docs/content/locales/zh-TW/onboarding.md +40 -40
  51. package/corpus/core/docs/content/locales/zh-TW/plan-plugin.md +37 -37
  52. package/corpus/core/docs/content/locales/zh-TW/pr-visual-recap.md +72 -72
  53. package/corpus/core/docs/content/locales/zh-TW/processors.md +17 -17
  54. package/corpus/core/docs/content/locales/zh-TW/progress.md +15 -15
  55. package/corpus/core/docs/content/locales/zh-TW/pure-agent-apps.md +10 -10
  56. package/corpus/core/docs/content/locales/zh-TW/real-time-collaboration.md +103 -103
  57. package/corpus/core/docs/content/locales/zh-TW/recurring-jobs.md +19 -19
  58. package/corpus/core/docs/content/locales/zh-TW/routing.md +9 -9
  59. package/corpus/core/docs/content/locales/zh-TW/sandbox-adapters.md +48 -48
  60. package/corpus/core/docs/content/locales/zh-TW/security.md +49 -49
  61. package/corpus/core/docs/content/locales/zh-TW/server.md +47 -47
  62. package/corpus/core/docs/content/locales/zh-TW/sharing.md +22 -22
  63. package/corpus/core/docs/content/locales/zh-TW/skills-guide.md +46 -46
  64. package/corpus/core/docs/content/locales/zh-TW/template-analytics.md +40 -40
  65. package/corpus/core/docs/content/locales/zh-TW/template-assets.md +69 -69
  66. package/corpus/core/docs/content/locales/zh-TW/template-brain.md +81 -81
  67. package/corpus/core/docs/content/locales/zh-TW/template-calendar.md +40 -40
  68. package/corpus/core/docs/content/locales/zh-TW/template-chat.md +27 -27
  69. package/corpus/core/docs/content/locales/zh-TW/template-clips.md +85 -85
  70. package/corpus/core/docs/content/locales/zh-TW/template-content.md +65 -66
  71. package/corpus/core/docs/content/locales/zh-TW/template-design.md +29 -29
  72. package/corpus/core/docs/content/locales/zh-TW/template-dispatch.md +51 -51
  73. package/corpus/core/docs/content/locales/zh-TW/template-forms.md +25 -25
  74. package/corpus/core/docs/content/locales/zh-TW/template-mail.md +71 -71
  75. package/corpus/core/docs/content/locales/zh-TW/template-plan.md +92 -92
  76. package/corpus/core/docs/content/locales/zh-TW/template-slides.md +52 -52
  77. package/corpus/core/docs/content/locales/zh-TW/template-videos.md +30 -30
  78. package/corpus/core/docs/content/locales/zh-TW/tracking.md +29 -29
  79. package/corpus/core/docs/content/locales/zh-TW/using-your-agent.md +18 -18
  80. package/corpus/core/docs/content/locales/zh-TW/voice-input.md +19 -19
  81. package/corpus/core/docs/content/locales/zh-TW/what-is-agent-native.md +61 -61
  82. package/corpus/core/docs/content/locales/zh-TW/workspace-connections.md +41 -41
  83. package/corpus/core/docs/content/locales/zh-TW/workspace-management.md +27 -27
  84. package/corpus/core/docs/content/locales/zh-TW/workspace.md +58 -58
  85. package/corpus/core/docs/content/locales/zh-TW/writing-agent-instructions.md +41 -41
  86. package/corpus/core/docs/content/template-clips.md +48 -2
  87. package/corpus/core/package.json +2 -1
  88. package/corpus/core/src/client/AgentPanel.tsx +27 -3
  89. package/corpus/core/src/deploy/build.ts +417 -42
  90. package/corpus/core/src/deploy/route-discovery.ts +1 -1
  91. package/corpus/core/src/server/edge.ts +50 -0
  92. package/corpus/core/src/styles/agent-native.css +93 -0
  93. package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +44 -44
  94. package/corpus/core/src/vite/client.ts +1 -0
  95. package/corpus/templates/analytics/app/components/layout/Layout.tsx +5 -3
  96. package/corpus/templates/analytics/app/i18n/zh-TW.ts +69 -70
  97. package/corpus/templates/analytics/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  98. package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -3
  99. package/corpus/templates/assets/app/i18n/zh-TW.ts +82 -82
  100. package/corpus/templates/assets/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  101. package/corpus/templates/brain/app/components/layout/Layout.tsx +6 -4
  102. package/corpus/templates/brain/app/i18n/zh-TW.ts +90 -90
  103. package/corpus/templates/brain/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  104. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +1 -1
  105. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +1 -1
  106. package/corpus/templates/calendar/app/i18n/zh-TW.ts +57 -57
  107. package/corpus/templates/calendar/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  108. package/corpus/templates/calendar/server/lib/booking-og-fonts.ts +20 -6
  109. package/corpus/templates/calendar/server/lib/booking-og-image.ts +17 -9
  110. package/corpus/templates/chat/app/components/layout/Layout.tsx +5 -3
  111. package/corpus/templates/chat/app/i18n/zh-TW.ts +3 -3
  112. package/corpus/templates/chat/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  113. package/corpus/templates/clips/AGENTS.md +6 -0
  114. package/corpus/templates/clips/actions/delete-recording-permanent.ts +7 -1
  115. package/corpus/templates/clips/actions/get-recording-player-data.ts +44 -2
  116. package/corpus/templates/clips/actions/navigate.ts +2 -0
  117. package/corpus/templates/clips/actions/save-bug-report-context.ts +173 -0
  118. package/corpus/templates/clips/actions/view-screen.ts +58 -7
  119. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +9 -3
  120. package/corpus/templates/clips/app/hooks/use-navigation-state.ts +17 -0
  121. package/corpus/templates/clips/app/i18n/ar-SA.ts +31 -0
  122. package/corpus/templates/clips/app/i18n/de-DE.ts +32 -0
  123. package/corpus/templates/clips/app/i18n/en-US.ts +32 -0
  124. package/corpus/templates/clips/app/i18n/es-ES.ts +32 -0
  125. package/corpus/templates/clips/app/i18n/fr-FR.ts +32 -0
  126. package/corpus/templates/clips/app/i18n/hi-IN.ts +32 -0
  127. package/corpus/templates/clips/app/i18n/ja-JP.ts +31 -0
  128. package/corpus/templates/clips/app/i18n/ko-KR.ts +32 -0
  129. package/corpus/templates/clips/app/i18n/pt-BR.ts +32 -0
  130. package/corpus/templates/clips/app/i18n/zh-CN.ts +30 -0
  131. package/corpus/templates/clips/app/i18n/zh-TW.ts +461 -451
  132. package/corpus/templates/clips/app/routes/bug-report.done.tsx +126 -0
  133. package/corpus/templates/clips/app/routes/bug-report.tsx +222 -0
  134. package/corpus/templates/clips/app/routes/record.tsx +104 -12
  135. package/corpus/templates/clips/changelog/2026-06-26-brief-network-blips-no-longer-block-recording-or-wrongly-pro.md +6 -0
  136. package/corpus/templates/clips/changelog/2026-06-26-bug-report-recordings-can-keep-redacted-host-app-context-wit.md +6 -0
  137. package/corpus/templates/clips/changelog/2026-06-26-bug-report-widgets-can-launch-clips-recordings-from-embedded.md +6 -0
  138. package/corpus/templates/clips/changelog/2026-06-26-desktop-recording-keeps-connected-users-out-of-the-storage-s.md +6 -0
  139. package/corpus/templates/clips/changelog/2026-06-26-if-a-recording-can-t-be-uploaded-the-chrome-extension-now-sa.md +6 -0
  140. package/corpus/templates/clips/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  141. package/corpus/templates/clips/chrome-extension/public/manifest.json +9 -2
  142. package/corpus/templates/clips/chrome-extension/src/background.ts +43 -0
  143. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +111 -6
  144. package/corpus/templates/clips/chrome-extension/src/popup.ts +18 -7
  145. package/corpus/templates/clips/desktop/src/app.tsx +53 -19
  146. package/corpus/templates/clips/server/db/schema.ts +26 -0
  147. package/corpus/templates/clips/server/lib/public-agent-context.ts +51 -0
  148. package/corpus/templates/clips/server/plugins/auth.ts +1 -0
  149. package/corpus/templates/clips/server/plugins/db.ts +28 -0
  150. package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +4 -1
  151. package/corpus/templates/clips/shared/bug-report.ts +161 -0
  152. package/corpus/templates/content/app/components/layout/Layout.tsx +10 -8
  153. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +1 -1
  154. package/corpus/templates/content/app/i18n/zh-TW.ts +67 -66
  155. package/corpus/templates/content/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  156. package/corpus/templates/design/app/components/layout/Layout.tsx +3 -3
  157. package/corpus/templates/design/app/i18n/zh-TW.ts +44 -44
  158. package/corpus/templates/design/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  159. package/corpus/templates/dispatch/app/i18n/zh-TW.ts +5 -4
  160. package/corpus/templates/dispatch/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  161. package/corpus/templates/forms/app/components/layout/Layout.tsx +4 -2
  162. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +1 -1
  163. package/corpus/templates/forms/app/i18n/zh-TW.ts +48 -48
  164. package/corpus/templates/forms/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  165. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +3 -3
  166. package/corpus/templates/macros/app/i18n/zh-TW.ts +5 -5
  167. package/corpus/templates/macros/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  168. package/corpus/templates/mail/app/i18n/zh-TW.ts +83 -83
  169. package/corpus/templates/mail/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  170. package/corpus/templates/plan/app/components/layout/Layout.tsx +5 -3
  171. package/corpus/templates/plan/app/i18n/zh-TW.ts +69 -69
  172. package/corpus/templates/plan/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  173. package/corpus/templates/slides/app/components/layout/Layout.tsx +3 -3
  174. package/corpus/templates/slides/app/i18n/zh-TW.ts +64 -64
  175. package/corpus/templates/slides/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  176. package/corpus/templates/videos/app/components/layout/Layout.tsx +2 -2
  177. package/corpus/templates/videos/app/i18n/zh-TW.ts +54 -54
  178. package/corpus/templates/videos/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  179. package/dist/client/AgentPanel.d.ts.map +1 -1
  180. package/dist/client/AgentPanel.js +14 -4
  181. package/dist/client/AgentPanel.js.map +1 -1
  182. package/dist/collab/routes.d.ts +1 -1
  183. package/dist/deploy/build.d.ts +3 -1
  184. package/dist/deploy/build.d.ts.map +1 -1
  185. package/dist/deploy/build.js +361 -37
  186. package/dist/deploy/build.js.map +1 -1
  187. package/dist/deploy/route-discovery.d.ts +1 -1
  188. package/dist/deploy/route-discovery.js +1 -1
  189. package/dist/deploy/route-discovery.js.map +1 -1
  190. package/dist/resources/handlers.d.ts +2 -2
  191. package/dist/server/edge.d.ts +11 -0
  192. package/dist/server/edge.d.ts.map +1 -0
  193. package/dist/server/edge.js +11 -0
  194. package/dist/server/edge.js.map +1 -0
  195. package/dist/styles/agent-native.css +93 -0
  196. package/dist/templates/default/app/i18n/zh-TW.ts +44 -44
  197. package/dist/vite/client.d.ts.map +1 -1
  198. package/dist/vite/client.js +1 -0
  199. package/dist/vite/client.js.map +1 -1
  200. package/docs/content/locales/zh-TW/a2a-protocol.md +68 -68
  201. package/docs/content/locales/zh-TW/actions.md +82 -82
  202. package/docs/content/locales/zh-TW/agent-mentions.md +22 -22
  203. package/docs/content/locales/zh-TW/agent-surfaces.md +82 -82
  204. package/docs/content/locales/zh-TW/agent-teams.md +29 -29
  205. package/docs/content/locales/zh-TW/agent-web-surfaces.md +11 -11
  206. package/docs/content/locales/zh-TW/audit-log.md +16 -16
  207. package/docs/content/locales/zh-TW/authentication.md +32 -32
  208. package/docs/content/locales/zh-TW/automations.md +32 -32
  209. package/docs/content/locales/zh-TW/blueprint-installer.md +19 -19
  210. package/docs/content/locales/zh-TW/cli-adapters.md +17 -17
  211. package/docs/content/locales/zh-TW/client.md +62 -62
  212. package/docs/content/locales/zh-TW/cloneable-saas.md +32 -32
  213. package/docs/content/locales/zh-TW/code-agents-ui.md +91 -91
  214. package/docs/content/locales/zh-TW/components.md +51 -51
  215. package/docs/content/locales/zh-TW/context-awareness.md +66 -66
  216. package/docs/content/locales/zh-TW/creating-templates.md +39 -39
  217. package/docs/content/locales/zh-TW/cross-app-sso.md +29 -29
  218. package/docs/content/locales/zh-TW/database.md +21 -21
  219. package/docs/content/locales/zh-TW/deployment.md +37 -37
  220. package/docs/content/locales/zh-TW/dispatch.md +43 -43
  221. package/docs/content/locales/zh-TW/drop-in-agent.md +34 -34
  222. package/docs/content/locales/zh-TW/durable-resume.md +18 -18
  223. package/docs/content/locales/zh-TW/embedding-sdk.md +73 -73
  224. package/docs/content/locales/zh-TW/evals.md +16 -16
  225. package/docs/content/locales/zh-TW/extensions.md +81 -81
  226. package/docs/content/locales/zh-TW/external-agents.md +146 -146
  227. package/docs/content/locales/zh-TW/faq.md +43 -43
  228. package/docs/content/locales/zh-TW/file-uploads.md +11 -11
  229. package/docs/content/locales/zh-TW/frames.md +33 -33
  230. package/docs/content/locales/zh-TW/getting-started.md +28 -28
  231. package/docs/content/locales/zh-TW/harness-agents.md +78 -78
  232. package/docs/content/locales/zh-TW/human-approval.md +27 -27
  233. package/docs/content/locales/zh-TW/internationalization.md +19 -19
  234. package/docs/content/locales/zh-TW/key-concepts.md +68 -68
  235. package/docs/content/locales/zh-TW/local-file-mode.md +50 -50
  236. package/docs/content/locales/zh-TW/mcp-apps.md +38 -38
  237. package/docs/content/locales/zh-TW/mcp-clients.md +67 -67
  238. package/docs/content/locales/zh-TW/mcp-protocol.md +50 -50
  239. package/docs/content/locales/zh-TW/messaging.md +99 -99
  240. package/docs/content/locales/zh-TW/migration-workbench.md +6 -6
  241. package/docs/content/locales/zh-TW/multi-app-workspace.md +39 -39
  242. package/docs/content/locales/zh-TW/multi-tenancy.md +7 -7
  243. package/docs/content/locales/zh-TW/native-chat-ui.md +38 -38
  244. package/docs/content/locales/zh-TW/notifications.md +23 -23
  245. package/docs/content/locales/zh-TW/observability.md +49 -49
  246. package/docs/content/locales/zh-TW/observational-memory.md +23 -23
  247. package/docs/content/locales/zh-TW/onboarding.md +40 -40
  248. package/docs/content/locales/zh-TW/plan-plugin.md +37 -37
  249. package/docs/content/locales/zh-TW/pr-visual-recap.md +72 -72
  250. package/docs/content/locales/zh-TW/processors.md +17 -17
  251. package/docs/content/locales/zh-TW/progress.md +15 -15
  252. package/docs/content/locales/zh-TW/pure-agent-apps.md +10 -10
  253. package/docs/content/locales/zh-TW/real-time-collaboration.md +103 -103
  254. package/docs/content/locales/zh-TW/recurring-jobs.md +19 -19
  255. package/docs/content/locales/zh-TW/routing.md +9 -9
  256. package/docs/content/locales/zh-TW/sandbox-adapters.md +48 -48
  257. package/docs/content/locales/zh-TW/security.md +49 -49
  258. package/docs/content/locales/zh-TW/server.md +47 -47
  259. package/docs/content/locales/zh-TW/sharing.md +22 -22
  260. package/docs/content/locales/zh-TW/skills-guide.md +46 -46
  261. package/docs/content/locales/zh-TW/template-analytics.md +40 -40
  262. package/docs/content/locales/zh-TW/template-assets.md +69 -69
  263. package/docs/content/locales/zh-TW/template-brain.md +81 -81
  264. package/docs/content/locales/zh-TW/template-calendar.md +40 -40
  265. package/docs/content/locales/zh-TW/template-chat.md +27 -27
  266. package/docs/content/locales/zh-TW/template-clips.md +85 -85
  267. package/docs/content/locales/zh-TW/template-content.md +65 -66
  268. package/docs/content/locales/zh-TW/template-design.md +29 -29
  269. package/docs/content/locales/zh-TW/template-dispatch.md +51 -51
  270. package/docs/content/locales/zh-TW/template-forms.md +25 -25
  271. package/docs/content/locales/zh-TW/template-mail.md +71 -71
  272. package/docs/content/locales/zh-TW/template-plan.md +92 -92
  273. package/docs/content/locales/zh-TW/template-slides.md +52 -52
  274. package/docs/content/locales/zh-TW/template-videos.md +30 -30
  275. package/docs/content/locales/zh-TW/tracking.md +29 -29
  276. package/docs/content/locales/zh-TW/using-your-agent.md +18 -18
  277. package/docs/content/locales/zh-TW/voice-input.md +19 -19
  278. package/docs/content/locales/zh-TW/what-is-agent-native.md +61 -61
  279. package/docs/content/locales/zh-TW/workspace-connections.md +41 -41
  280. package/docs/content/locales/zh-TW/workspace-management.md +27 -27
  281. package/docs/content/locales/zh-TW/workspace.md +58 -58
  282. package/docs/content/locales/zh-TW/writing-agent-instructions.md +41 -41
  283. package/docs/content/template-clips.md +48 -2
  284. package/package.json +2 -1
  285. package/src/templates/default/app/i18n/zh-TW.ts +44 -44
@@ -146,6 +146,10 @@ type NativeRecording = {
146
146
  status: NativeRecordingStatus;
147
147
  recordingUrl: string;
148
148
  error: string | null;
149
+ // When an upload fails, the recording is saved to the user's Downloads as a
150
+ // fallback so it is never lost; these describe that saved file.
151
+ savedToDisk?: boolean;
152
+ savedFilename?: string;
149
153
  };
150
154
 
151
155
  type OffscreenStatusMessage = {
@@ -156,6 +160,8 @@ type OffscreenStatusMessage = {
156
160
  result?: Record<string, unknown>;
157
161
  error?: string;
158
162
  storageSetupRequired?: boolean;
163
+ savedToDisk?: boolean;
164
+ savedFilename?: string;
159
165
  };
160
166
 
161
167
  type ExtensionErrorMessage = {
@@ -2032,6 +2038,36 @@ async function dispatchRuntimeMessage(message: unknown): Promise<unknown> {
2032
2038
  return { ok: true };
2033
2039
  }
2034
2040
 
2041
+ // The offscreen recorder asks us to save a recording to disk when its upload
2042
+ // fails (storage not connected, network drop, size cap), so the recording is
2043
+ // never lost. The offscreen document holds the blob URL alive until its next
2044
+ // acquire(); we only need the download to be accepted, not fully written.
2045
+ if (type === "CLIPS_SAVE_RECORDING_TO_DISK") {
2046
+ const { url, filename } = message as { url?: string; filename?: string };
2047
+ if (typeof url !== "string" || !url) {
2048
+ return { ok: false, error: "Missing recording URL." };
2049
+ }
2050
+ try {
2051
+ const options: chrome.downloads.DownloadOptions = {
2052
+ url,
2053
+ saveAs: false,
2054
+ conflictAction: "uniquify",
2055
+ };
2056
+ if (typeof filename === "string" && filename) options.filename = filename;
2057
+ const downloadId = await chrome.downloads.download(options);
2058
+ return { ok: typeof downloadId === "number" && downloadId >= 0 };
2059
+ } catch (err) {
2060
+ console.error("[clips-bg] save-to-disk download failed", err);
2061
+ captureExtensionError(err, {
2062
+ tags: { surface: "background", messageType: type },
2063
+ });
2064
+ return {
2065
+ ok: false,
2066
+ error: err instanceof Error ? err.message : "Download failed.",
2067
+ };
2068
+ }
2069
+ }
2070
+
2035
2071
  // Status updates streamed from the offscreen recorder.
2036
2072
  if (type === "CLIPS_NATIVE_STATUS") {
2037
2073
  const status = message as OffscreenStatusMessage;
@@ -2060,6 +2096,13 @@ async function dispatchRuntimeMessage(message: unknown): Promise<unknown> {
2060
2096
  activeNativeRecording,
2061
2097
  );
2062
2098
  }
2099
+ if (status.status === "error") {
2100
+ activeNativeRecording.savedToDisk = status.savedToDisk === true;
2101
+ activeNativeRecording.savedFilename =
2102
+ typeof status.savedFilename === "string"
2103
+ ? status.savedFilename
2104
+ : undefined;
2105
+ }
2063
2106
  await saveActiveNativeRecording();
2064
2107
  // The offscreen recorder finished its pre-roll and actually started —
2065
2108
  // advance from countdown to recording (the reliable phase flip).
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { scheduleReadyChime } from "@shared/recording-audio";
20
20
  import { chunkUploadUrl, pickMimeType } from "@shared/recording-core";
21
+ import { MAX_UPLOAD_BYTES } from "@shared/upload-limits";
21
22
 
22
23
  import { captureExtensionError, initExtensionSentry } from "./sentry";
23
24
 
@@ -110,6 +111,15 @@ type ActiveRecording = {
110
111
  chunkIndex: number;
111
112
  uploadPromises: Promise<unknown>[];
112
113
  uploadFailure: Error | null;
114
+ // Local safety buffer: every recorded blob is kept here (browser-managed,
115
+ // disk-backed Blob refs — not raw heap) so that if the upload fails (storage
116
+ // not connected, network drop, size cap) we can still save the finished
117
+ // recording to disk instead of losing it. Mirrors the web/desktop recorders.
118
+ recordedBlobs: Blob[];
119
+ recordedBytes: number;
120
+ // Set if the recording grew past the buffer ceiling and we stopped retaining
121
+ // — at that point a local save can't be guaranteed, so we don't promise one.
122
+ localBufferOverflow: boolean;
113
123
  cancelled: boolean;
114
124
  // Set when the recorder is being torn down to start over on the same source
115
125
  // streams, so the stop handler skips the usual track cleanup.
@@ -133,8 +143,24 @@ type ActiveRecording = {
133
143
 
134
144
  const UPLOAD_SLICE_BYTES = 3 * 1024 * 1024;
135
145
 
146
+ // Don't retain more than the upload ceiling — past it the server rejects the
147
+ // recording anyway, so there is nothing a local save could recover.
148
+ const MAX_LOCAL_BUFFER_BYTES = MAX_UPLOAD_BYTES;
149
+ // Below this, a failed recording is too short to be worth dumping a file into
150
+ // the user's Downloads (e.g. the storage gate was bypassed and the very first
151
+ // chunk was rejected ~2s in). The connect-storage message is enough there.
152
+ const MIN_LOCAL_SAVE_BYTES = 2 * 1024 * 1024;
153
+
136
154
  let prepared: PreparedStreams | null = null;
137
155
  let activeRecording: ActiveRecording | null = null;
156
+ // Blob URLs handed to the background for save-to-disk recovery downloads. Kept
157
+ // alive until the next recording starts so the download can finish reading.
158
+ const pendingSaveUrls = new Set<string>();
159
+
160
+ function releasePendingSaveUrls(): void {
161
+ for (const url of pendingSaveUrls) URL.revokeObjectURL(url);
162
+ pendingSaveUrls.clear();
163
+ }
138
164
 
139
165
  function reportStatus(
140
166
  sessionId: string,
@@ -454,6 +480,20 @@ async function uploadChunk(
454
480
  return data;
455
481
  }
456
482
 
483
+ // Keep a local copy of every recorded blob so a failed upload can still be
484
+ // saved to disk. Blob references are browser-managed (often disk-backed), so
485
+ // this is far cheaper than holding ArrayBuffers. We stop retaining past the
486
+ // upload ceiling (a larger recording can't be uploaded anyway).
487
+ function retainRecordedBlob(recording: ActiveRecording, blob: Blob): void {
488
+ if (recording.localBufferOverflow) return;
489
+ if (recording.recordedBytes + blob.size > MAX_LOCAL_BUFFER_BYTES) {
490
+ recording.localBufferOverflow = true;
491
+ return;
492
+ }
493
+ recording.recordedBlobs.push(blob);
494
+ recording.recordedBytes += blob.size;
495
+ }
496
+
457
497
  async function uploadBlobInSlices(
458
498
  recording: ActiveRecording,
459
499
  blob: Blob,
@@ -501,6 +541,8 @@ async function acquire(message: AcquireMessage): Promise<{
501
541
  // Discard any half-prepared capture from a cancelled attempt.
502
542
  stopPreparedStreams();
503
543
  disposePrepared();
544
+ // A prior recording's recovery download has finished by now; free its URL.
545
+ releasePendingSaveUrls();
504
546
 
505
547
  let displayStream: MediaStream | null = null;
506
548
  let micStream: MediaStream | null = null;
@@ -649,6 +691,9 @@ async function begin(message: BeginMessage): Promise<{
649
691
  chunkIndex: 0,
650
692
  uploadPromises: [],
651
693
  uploadFailure: null,
694
+ recordedBlobs: [],
695
+ recordedBytes: 0,
696
+ localBufferOverflow: false,
652
697
  cancelled: false,
653
698
  restarting: false,
654
699
  startTimer: null,
@@ -671,14 +716,15 @@ async function begin(message: BeginMessage): Promise<{
671
716
  activeRecording = recording;
672
717
 
673
718
  recorder.addEventListener("dataavailable", (event) => {
674
- if (
675
- recording.cancelled ||
676
- !event.data ||
677
- event.data.size === 0 ||
678
- recording.uploadFailure
679
- ) {
719
+ if (recording.cancelled || !event.data || event.data.size === 0) {
680
720
  return;
681
721
  }
722
+ // Always keep a local copy first — even after an upload failure — so the
723
+ // saved-to-disk fallback can assemble the COMPLETE recording. Without this,
724
+ // a recording whose upload is rejected (storage disconnected, network drop,
725
+ // size cap) would be lost; the extension has no other on-disk copy.
726
+ retainRecordedBlob(recording, event.data);
727
+ if (recording.uploadFailure) return;
682
728
  // Record the failure and stop, but do NOT re-throw: re-throwing leaves a
683
729
  // rejected promise that surfaces as an "Uncaught (in promise)" error (bad
684
730
  // look in a Chrome Web Store review). finalizeStop reads recording.upload-
@@ -782,6 +828,61 @@ function startRecorderNow(recording: ActiveRecording): void {
782
828
  }
783
829
  }
784
830
 
831
+ function recordingDownloadFilename(recording: ActiveRecording): string {
832
+ const ext = /mp4/i.test(recording.mimeType) ? "mp4" : "webm";
833
+ const stamp = new Date(recording.startedAtMs || Date.now())
834
+ .toISOString()
835
+ .replace(/[:.]/g, "-")
836
+ .replace("T", "_")
837
+ .slice(0, 19);
838
+ return `clip-${stamp}.${ext}`;
839
+ }
840
+
841
+ // Last-resort recovery: if a finished recording can't be uploaded, save the
842
+ // locally-buffered bytes to the user's Downloads so the recording is never
843
+ // lost. The offscreen document can't call chrome.downloads, so it hands the
844
+ // background a blob URL to download. The URL is revoked on the next acquire().
845
+ async function saveRecordingToDisk(
846
+ recording: ActiveRecording,
847
+ ): Promise<{ savedToDisk: boolean; savedFilename?: string }> {
848
+ if (
849
+ recording.cancelled ||
850
+ recording.localBufferOverflow ||
851
+ recording.recordedBlobs.length === 0 ||
852
+ recording.recordedBytes < MIN_LOCAL_SAVE_BYTES
853
+ ) {
854
+ return { savedToDisk: false };
855
+ }
856
+ try {
857
+ const blob = new Blob(recording.recordedBlobs, {
858
+ type: recording.mimeType,
859
+ });
860
+ const objectUrl = URL.createObjectURL(blob);
861
+ pendingSaveUrls.add(objectUrl);
862
+ const filename = recordingDownloadFilename(recording);
863
+ const response = (await chrome.runtime.sendMessage({
864
+ type: "CLIPS_SAVE_RECORDING_TO_DISK",
865
+ sessionId: recording.sessionId,
866
+ url: objectUrl,
867
+ filename,
868
+ })) as { ok?: boolean } | undefined;
869
+ if (response?.ok) return { savedToDisk: true, savedFilename: filename };
870
+ // The download was not accepted — drop the URL we just created.
871
+ pendingSaveUrls.delete(objectUrl);
872
+ URL.revokeObjectURL(objectUrl);
873
+ return { savedToDisk: false };
874
+ } catch (err) {
875
+ captureExtensionError(err, {
876
+ tags: { surface: "offscreen", recordingStep: "save-to-disk" },
877
+ extra: {
878
+ recordingId: recording.recordingId,
879
+ bytes: recording.recordedBytes,
880
+ },
881
+ });
882
+ return { savedToDisk: false };
883
+ }
884
+ }
885
+
785
886
  async function finalizeStop(recording: ActiveRecording): Promise<void> {
786
887
  if (recording.restarting) {
787
888
  // restart() re-homes the source streams; do not stop or upload anything.
@@ -866,11 +967,15 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
866
967
  durationMs: Math.max(0, Date.now() - recording.startedAtMs),
867
968
  },
868
969
  });
970
+ // The upload failed — save the buffered recording to disk so it isn't lost.
971
+ const saved = await saveRecordingToDisk(recording);
869
972
  reportStatus(recording.sessionId, "error", {
870
973
  recordingId: recording.recordingId,
871
974
  error: error.message,
872
975
  storageSetupRequired: (error as { storageSetupRequired?: boolean })
873
976
  .storageSetupRequired,
977
+ savedToDisk: saved.savedToDisk,
978
+ savedFilename: saved.savedFilename,
874
979
  });
875
980
  recording.rejectStopped(error);
876
981
  }
@@ -45,6 +45,8 @@ type NativeRecording = {
45
45
  status: NativeRecordingStatus;
46
46
  recordingUrl: string;
47
47
  error: string | null;
48
+ savedToDisk?: boolean;
49
+ savedFilename?: string;
48
50
  };
49
51
 
50
52
  type PopupStatusResponse = {
@@ -719,22 +721,31 @@ function renderActiveRecording(recording: NativeRecording | null): void {
719
721
  titleKey && hostKey && (titleKey === hostKey || hostKey.includes(titleKey));
720
722
  recordingUrl.textContent = duplicate ? "" : host;
721
723
  recordingUrl.hidden = !host || Boolean(duplicate);
724
+ const storageFailure = isStorageSetupFailureMessage(recording.error);
725
+ let errorText = storageFailure
726
+ ? STORAGE_SETUP_REQUIRED_MESSAGE
727
+ : recording.error || "Recording needs attention";
728
+ // If the upload failed but we saved the recording to disk, lead with the
729
+ // reassurance (it's not lost) and the re-upload action.
730
+ if (recording.status === "error" && recording.savedToDisk) {
731
+ const named = recording.savedFilename
732
+ ? ` (${recording.savedFilename})`
733
+ : "";
734
+ errorText = storageFailure
735
+ ? `Couldn't upload — storage isn't connected. Your clip is saved to your downloads${named}. Connect storage, then re-upload it with "Upload video".`
736
+ : `Couldn't upload your clip. It's saved to your downloads${named} — try re-uploading it with "Upload video".`;
737
+ }
722
738
  recordingStatus.textContent =
723
739
  recording.status === "uploading"
724
740
  ? "Saving..."
725
741
  : recording.status === "stopping"
726
742
  ? "Stopping..."
727
743
  : recording.status === "error"
728
- ? isStorageSetupFailureMessage(recording.error)
729
- ? STORAGE_SETUP_REQUIRED_MESSAGE
730
- : recording.error || "Recording needs attention"
744
+ ? errorText
731
745
  : `Recording ${formatDuration(recording.startedAtMs)}`;
732
746
  recordingStatus.dataset.kind =
733
747
  recording.status === "error" ? "error" : "info";
734
- setStorageHelp(
735
- recording.status === "error" &&
736
- isStorageSetupFailureMessage(recording.error),
737
- );
748
+ setStorageHelp(recording.status === "error" && storageFailure);
738
749
  }
739
750
 
740
751
  async function init(): Promise<void> {
@@ -204,9 +204,21 @@ function serverUrlForPendingUpload(
204
204
  return normalizedCurrent || normalizeServerUrl(upload.serverUrl || "");
205
205
  }
206
206
 
207
- async function hasConfiguredVideoStorage(serverUrl: string): Promise<boolean> {
207
+ // "configured"/"missing" are definitive answers from the server; "unknown"
208
+ // means the check could not be completed (network error, unreachable server, or
209
+ // an unparseable/non-OK response). An "unknown" result must never downgrade an
210
+ // already-connected user to the setup flow.
211
+ type VideoStorageProbe = "configured" | "missing" | "unknown";
212
+
213
+ async function hasConfiguredVideoStorage(
214
+ serverUrl: string,
215
+ ): Promise<VideoStorageProbe> {
208
216
  const base = serverUrl.replace(/\/+$/, "");
209
217
 
218
+ // Track whether any endpoint gave a definitive answer. If both checks throw
219
+ // or return non-OK/unparseable responses, we can't tell and return "unknown".
220
+ let sawDefinitiveAnswer = false;
221
+
210
222
  try {
211
223
  const uploadStatus = await fetch(
212
224
  `${base}/_agent-native/file-upload/status`,
@@ -215,12 +227,15 @@ async function hasConfiguredVideoStorage(serverUrl: string): Promise<boolean> {
215
227
  cache: "no-store",
216
228
  },
217
229
  );
218
- const body = uploadStatus.ok
219
- ? ((await uploadStatus.json().catch(() => null)) as {
220
- configured?: boolean;
221
- } | null)
222
- : null;
223
- if (body?.configured) return true;
230
+ if (uploadStatus.ok) {
231
+ const body = (await uploadStatus.json().catch(() => null)) as {
232
+ configured?: boolean;
233
+ } | null;
234
+ if (body) {
235
+ sawDefinitiveAnswer = true;
236
+ if (body.configured) return "configured";
237
+ }
238
+ }
224
239
  } catch {
225
240
  // Fall through to the Builder status endpoint.
226
241
  }
@@ -230,15 +245,20 @@ async function hasConfiguredVideoStorage(serverUrl: string): Promise<boolean> {
230
245
  credentials: "include",
231
246
  cache: "no-store",
232
247
  });
233
- const body = builderStatus.ok
234
- ? ((await builderStatus.json().catch(() => null)) as {
235
- configured?: boolean;
236
- } | null)
237
- : null;
238
- return !!body?.configured;
248
+ if (builderStatus.ok) {
249
+ const body = (await builderStatus.json().catch(() => null)) as {
250
+ configured?: boolean;
251
+ } | null;
252
+ if (body) {
253
+ sawDefinitiveAnswer = true;
254
+ if (body.configured) return "configured";
255
+ }
256
+ }
239
257
  } catch {
240
- return false;
258
+ // Network error or unreachable server — treat as indeterminate below.
241
259
  }
260
+
261
+ return sawDefinitiveAnswer ? "missing" : "unknown";
242
262
  }
243
263
 
244
264
  function authTokenStorageKey(serverUrl: string): string {
@@ -658,10 +678,21 @@ export function App() {
658
678
  return true;
659
679
  }
660
680
 
661
- setVideoStorageStatus("checking");
662
- const configured = await hasConfiguredVideoStorage(serverUrl);
663
- setVideoStorageStatus(configured ? "configured" : "missing");
664
- return configured;
681
+ setVideoStorageStatus((prev) => (prev === "missing" ? prev : "checking"));
682
+ const probe = await hasConfiguredVideoStorage(serverUrl);
683
+ if (probe === "unknown") {
684
+ // The check couldn't be completed (offline/unreachable). Never downgrade
685
+ // an already-connected user to "missing" on an indeterminate result;
686
+ // preserve the last known status and let the poll retry. If we never
687
+ // determined a status, fall back to "checking" so the poll keeps trying
688
+ // rather than hard-blocking the record button.
689
+ setVideoStorageStatus((prev) =>
690
+ prev === "configured" || prev === "missing" ? prev : "checking",
691
+ );
692
+ return false;
693
+ }
694
+ setVideoStorageStatus(probe);
695
+ return probe === "configured";
665
696
  }, [authStatus, localRecordingMode, serverUrl]);
666
697
 
667
698
  useEffect(() => {
@@ -672,7 +703,10 @@ export function App() {
672
703
  if (
673
704
  authStatus !== "authed" ||
674
705
  localRecordingMode !== "off" ||
675
- videoStorageStatus !== "missing"
706
+ // Re-poll while storage is "missing" (server may become configured) and
707
+ // while still "checking" (an indeterminate/unreachable first probe should
708
+ // keep retrying instead of hard-blocking the record button).
709
+ (videoStorageStatus !== "missing" && videoStorageStatus !== "checking")
676
710
  ) {
677
711
  return;
678
712
  }
@@ -253,6 +253,32 @@ export const recordingBrowserDiagnostics = table(
253
253
  },
254
254
  );
255
255
 
256
+ export const recordingBugReports = table("recording_bug_reports", {
257
+ recordingId: text("recording_id").primaryKey(),
258
+ ownerEmail: text("owner_email").notNull().default("local@localhost"),
259
+ organizationId: text("workspace_id").notNull(),
260
+ orgId: text("org_id"),
261
+ projectId: text("project_id"),
262
+ title: text("title"),
263
+ description: text("description").notNull().default(""),
264
+ severity: text("severity", {
265
+ enum: ["low", "normal", "high", "urgent"],
266
+ })
267
+ .notNull()
268
+ .default("normal"),
269
+ sourceUrl: text("source_url"),
270
+ pageTitle: text("page_title"),
271
+ appVersion: text("app_version"),
272
+ environment: text("environment"),
273
+ reporterEmail: text("reporter_email"),
274
+ reporterName: text("reporter_name"),
275
+ reporterId: text("reporter_id"),
276
+ metadataJson: text("metadata_json").notNull().default("{}"),
277
+ submittedAt: text("submitted_at").notNull().default(now()),
278
+ createdAt: text("created_at").notNull().default(now()),
279
+ updatedAt: text("updated_at").notNull().default(now()),
280
+ });
281
+
256
282
  export const recordingCtas = table("recording_ctas", {
257
283
  id: text("id").primaryKey(),
258
284
  recordingId: text("recording_id").notNull(),
@@ -33,6 +33,9 @@ export type PublicAgentRecording = typeof schema.recordings._.inferSelect;
33
33
  export type PublicAgentTranscript =
34
34
  | typeof schema.recordingTranscripts._.inferSelect
35
35
  | null;
36
+ export type PublicAgentBugReport =
37
+ | typeof schema.recordingBugReports._.inferSelect
38
+ | null;
36
39
 
37
40
  export interface PublicAgentAccess {
38
41
  recording: PublicAgentRecording;
@@ -337,6 +340,17 @@ export async function loadAgentBrowserDiagnostics(recordingId: string) {
337
340
  return parseBrowserDiagnosticsRow(row);
338
341
  }
339
342
 
343
+ export async function loadAgentBugReport(
344
+ recordingId: string,
345
+ ): Promise<PublicAgentBugReport> {
346
+ const [row] = await getDb()
347
+ .select()
348
+ .from(schema.recordingBugReports)
349
+ .where(eq(schema.recordingBugReports.recordingId, recordingId))
350
+ .limit(1);
351
+ return row ?? null;
352
+ }
353
+
340
354
  // Most-recent console logs / network requests surfaced in the public agent
341
355
  // context. Bounded to keep the agent payload small; the summary still reports
342
356
  // the true total counts.
@@ -412,6 +426,35 @@ function compactBrowserDiagnostics(diagnostics: BrowserDiagnosticsData | null) {
412
426
  };
413
427
  }
414
428
 
429
+ function safeMetadataObject(raw: string | null | undefined) {
430
+ if (!raw) return {};
431
+ try {
432
+ const parsed = JSON.parse(raw);
433
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
434
+ ? parsed
435
+ : {};
436
+ } catch {
437
+ return {};
438
+ }
439
+ }
440
+
441
+ function compactBugReport(report: PublicAgentBugReport) {
442
+ if (!report) return null;
443
+ return {
444
+ projectId: report.projectId,
445
+ title: report.title,
446
+ description: report.description,
447
+ severity: report.severity,
448
+ sourceUrl: report.sourceUrl,
449
+ pageTitle: report.pageTitle,
450
+ appVersion: report.appVersion,
451
+ environment: report.environment,
452
+ metadata: safeMetadataObject(report.metadataJson),
453
+ submittedAt: report.submittedAt,
454
+ note: "Reporter identity fields are omitted from public agent context. Host metadata is redacted and bounded at capture time.",
455
+ };
456
+ }
457
+
415
458
  export function buildPublicAgentContext({
416
459
  event,
417
460
  access,
@@ -420,6 +463,7 @@ export function buildPublicAgentContext({
420
463
  chapters,
421
464
  ctas,
422
465
  browserDiagnostics,
466
+ bugReport,
423
467
  }: {
424
468
  event: H3Event;
425
469
  access: PublicAgentAccess;
@@ -428,6 +472,7 @@ export function buildPublicAgentContext({
428
472
  chapters: ReturnType<typeof parseAgentChapters>;
429
473
  ctas: Awaited<ReturnType<typeof loadAgentCtas>>;
430
474
  browserDiagnostics?: BrowserDiagnosticsData | null;
475
+ bugReport?: PublicAgentBugReport;
431
476
  }) {
432
477
  const recording = access.recording;
433
478
  const requestUrl = getRequestURL(event);
@@ -451,6 +496,11 @@ export function buildPublicAgentContext({
451
496
  }));
452
497
  const instructions = [
453
498
  "Use transcript.segments for timestamped spoken context.",
499
+ ...(bugReport
500
+ ? [
501
+ "Use bugReport for the submitted product context: source URL, page title, app version, environment, severity, and redacted host metadata.",
502
+ ]
503
+ : []),
454
504
  ...(browserDiagnostics
455
505
  ? [
456
506
  "Use browserDiagnostics.consoleLogs for the redacted console stream (all levels: debug/log/info/warn/error) and browserDiagnostics.networkRequests for the fetch/XHR requests (method, sanitized URL, status, duration) captured during the recording. browserDiagnostics.consoleIssues highlights just the warnings/errors, and browserDiagnostics.failedNetworkRequests highlights failed requests.",
@@ -515,6 +565,7 @@ export function buildPublicAgentContext({
515
565
  },
516
566
  chapters,
517
567
  recommendedFrames: suggestedFrames,
568
+ bugReport: compactBugReport(bugReport ?? null),
518
569
  browserDiagnostics: compactBrowserDiagnostics(browserDiagnostics ?? null),
519
570
  ctas: ctas.map((cta) => ({
520
571
  label: cta.label,
@@ -22,6 +22,7 @@ export default createAuthPlugin({
22
22
  "/r",
23
23
  "/embed",
24
24
  "/download",
25
+ "/bug-report",
25
26
  // React Router's lazy route-discovery endpoint. If this is gated by
26
27
  // auth it returns an HTML login page; the client tries to parse it
27
28
  // as JSON, fails, and can't resolve any public route the user lands
@@ -721,6 +721,34 @@ const migrations = runMigrations(
721
721
  `CREATE INDEX IF NOT EXISTS slack_installations_org_idx ON slack_installations (org_id, created_at)`,
722
722
  ].join("; "),
723
723
  },
724
+ {
725
+ version: 44,
726
+ sql: [
727
+ `CREATE TABLE IF NOT EXISTS recording_bug_reports (
728
+ recording_id TEXT PRIMARY KEY,
729
+ owner_email TEXT NOT NULL DEFAULT 'local@localhost',
730
+ workspace_id TEXT NOT NULL,
731
+ org_id TEXT,
732
+ project_id TEXT,
733
+ title TEXT,
734
+ description TEXT NOT NULL DEFAULT '',
735
+ severity TEXT NOT NULL DEFAULT 'normal',
736
+ source_url TEXT,
737
+ page_title TEXT,
738
+ app_version TEXT,
739
+ environment TEXT,
740
+ reporter_email TEXT,
741
+ reporter_name TEXT,
742
+ reporter_id TEXT,
743
+ metadata_json TEXT NOT NULL DEFAULT '{}',
744
+ submitted_at TEXT NOT NULL DEFAULT (datetime('now')),
745
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
746
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
747
+ )`,
748
+ `CREATE INDEX IF NOT EXISTS recording_bug_reports_owner_idx ON recording_bug_reports (owner_email, updated_at)`,
749
+ `CREATE INDEX IF NOT EXISTS recording_bug_reports_project_idx ON recording_bug_reports (project_id, updated_at)`,
750
+ ].join("; "),
751
+ },
724
752
  ],
725
753
  { table: "clips_migrations" },
726
754
  );
@@ -16,6 +16,7 @@ import {
16
16
  import {
17
17
  applyAgentJsonHeaders,
18
18
  buildPublicAgentContext,
19
+ loadAgentBugReport,
19
20
  loadAgentBrowserDiagnostics,
20
21
  loadAgentCtas,
21
22
  loadAgentTranscript,
@@ -40,11 +41,12 @@ export default defineEventHandler(async (event: H3Event) => {
40
41
  }
41
42
 
42
43
  const recording = accessResult.access.recording;
43
- const [{ transcript, agentSegments }, ctas, browserDiagnostics] =
44
+ const [{ transcript, agentSegments }, ctas, browserDiagnostics, bugReport] =
44
45
  await Promise.all([
45
46
  loadAgentTranscript(recording.id, recording.durationMs),
46
47
  loadAgentCtas(recording.id),
47
48
  loadAgentBrowserDiagnostics(recording.id),
49
+ loadAgentBugReport(recording.id),
48
50
  ]);
49
51
  const chapters = parseAgentChapters(recording);
50
52
 
@@ -56,5 +58,6 @@ export default defineEventHandler(async (event: H3Event) => {
56
58
  chapters,
57
59
  ctas,
58
60
  browserDiagnostics,
61
+ bugReport,
59
62
  });
60
63
  });