@agent-native/core 0.91.1 → 0.92.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 (383) hide show
  1. package/blueprints/action/crud.md +1 -1
  2. package/blueprints/channel/discord.md +1 -1
  3. package/blueprints/provider/stripe.md +1 -1
  4. package/blueprints/sandbox/docker.md +1 -1
  5. package/corpus/README.md +2 -2
  6. package/corpus/core/CHANGELOG.md +31 -0
  7. package/corpus/core/blueprints/action/crud.md +1 -1
  8. package/corpus/core/blueprints/channel/discord.md +1 -1
  9. package/corpus/core/blueprints/provider/stripe.md +1 -1
  10. package/corpus/core/blueprints/sandbox/docker.md +1 -1
  11. package/corpus/core/docs/content/faq.mdx +17 -0
  12. package/corpus/core/docs/content/notifications.mdx +12 -6
  13. package/corpus/core/package.json +6 -5
  14. package/corpus/core/src/agent/production-agent.ts +140 -46
  15. package/corpus/core/src/agent/run-manager.ts +23 -2
  16. package/corpus/core/src/agent/run-store.ts +2 -2
  17. package/corpus/core/src/agent/tool-search.ts +71 -4
  18. package/corpus/core/src/cli/add.ts +1 -1
  19. package/corpus/core/src/cli/create.ts +8 -3
  20. package/corpus/core/src/cli/index.ts +25 -4
  21. package/corpus/core/src/cli/upgrade.ts +798 -0
  22. package/corpus/core/src/client/AssistantChat.tsx +1 -1
  23. package/corpus/core/src/client/HighlightedCodeBlock.tsx +20 -3
  24. package/corpus/core/src/client/agent-chat-adapter.ts +31 -6
  25. package/corpus/core/src/client/chat/message-components.tsx +85 -8
  26. package/corpus/core/src/client/chat/tool-call-display.tsx +355 -144
  27. package/corpus/core/src/client/code-agent-chat-adapter.ts +2 -9
  28. package/corpus/core/src/client/extensions/ExtensionSlot.tsx +7 -0
  29. package/corpus/core/src/client/settings/SettingsPanel.tsx +533 -389
  30. package/corpus/core/src/client/settings/SettingsSection.tsx +154 -17
  31. package/corpus/core/src/client/sse-event-processor.ts +29 -2
  32. package/corpus/core/src/extensions/actions.ts +113 -5
  33. package/corpus/core/src/jobs/scheduler.ts +30 -1
  34. package/corpus/core/src/localization/default-messages.ts +21 -0
  35. package/corpus/core/src/notifications/channels.ts +88 -25
  36. package/corpus/core/src/notifications/registry.ts +17 -3
  37. package/corpus/core/src/notifications/types.ts +4 -1
  38. package/corpus/core/src/server/agent-chat-plugin.ts +7 -3
  39. package/corpus/core/src/server/request-context.ts +7 -0
  40. package/corpus/core/src/styles/agent-conversation.css +1 -1
  41. package/corpus/core/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +20 -2
  42. package/corpus/core/src/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  43. package/corpus/core/src/templates/default/AGENTS.md +10 -3
  44. package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +21 -0
  45. package/corpus/core/src/templates/default/app/i18n/de-DE.ts +21 -0
  46. package/corpus/core/src/templates/default/app/i18n/en-US.ts +21 -0
  47. package/corpus/core/src/templates/default/app/i18n/es-ES.ts +21 -0
  48. package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +21 -0
  49. package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +20 -0
  50. package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +21 -0
  51. package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +20 -0
  52. package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +21 -0
  53. package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +20 -0
  54. package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +19 -0
  55. package/corpus/core/src/templates/default/package.json +4 -3
  56. package/corpus/core/src/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  57. package/corpus/core/src/templates/headless/AGENTS.md +9 -3
  58. package/corpus/core/src/templates/headless/package.json +4 -3
  59. package/corpus/core/src/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +20 -2
  60. package/corpus/core/src/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  61. package/corpus/core/src/templates/workspace-core/package.json +6 -6
  62. package/corpus/core/src/templates/workspace-root/AGENTS.md +10 -4
  63. package/corpus/core/src/templates/workspace-root/package.json +3 -2
  64. package/corpus/core/src/triggers/dispatcher.ts +30 -2
  65. package/corpus/core/src/vite/client.ts +96 -1
  66. package/corpus/templates/analytics/.agents/skills/self-modifying-code/SKILL.md +20 -2
  67. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +20 -2
  68. package/corpus/templates/analytics/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  69. package/corpus/templates/analytics/AGENTS.md +7 -5
  70. package/corpus/templates/analytics/actions/save-monitor.ts +14 -0
  71. package/corpus/templates/analytics/app/components/dashboard/DataTable.tsx +41 -6
  72. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +122 -11
  73. package/corpus/templates/analytics/app/global.css +26 -10
  74. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  75. package/corpus/templates/analytics/app/i18n-data.ts +25 -0
  76. package/corpus/templates/analytics/app/pages/monitoring/MonitoringPage.tsx +8 -2
  77. package/corpus/templates/analytics/app/pages/monitoring/uptime/MonitorFormPage.tsx +151 -32
  78. package/corpus/templates/analytics/app/pages/monitoring/uptime/i18n.ts +17 -2
  79. package/corpus/templates/analytics/app/pages/monitoring/uptime/types.ts +4 -0
  80. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +461 -286
  81. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +285 -294
  82. package/corpus/templates/analytics/changelog/2026-07-08-console-details-expand-inline-under-each-row-and-the-event-t.md +6 -0
  83. package/corpus/templates/analytics/changelog/2026-07-08-dashboard-tables-keep-their-size-while-loading.md +6 -0
  84. package/corpus/templates/analytics/changelog/2026-07-08-monitoring-uptime-and-errors-tabs-now-clearly-show-which-view-is-active.md +6 -0
  85. package/corpus/templates/analytics/changelog/2026-07-08-session-replay-keeps-a-usable-stage-when-dev-tools-is-open-a.md +6 -0
  86. package/corpus/templates/analytics/changelog/2026-07-08-session-replay-resumes-after-scrubbing-and-expands-console-r.md +6 -0
  87. package/corpus/templates/analytics/changelog/2026-07-08-session-replay-uses-stock-rrweb-sizing-again-so-recorded.md +6 -0
  88. package/corpus/templates/analytics/changelog/2026-07-08-session-replays-wait-for-full-load-keep-a-normal-viewport-an.md +6 -0
  89. package/corpus/templates/analytics/changelog/2026-07-08-sidebar-dashboard-menus-now-open-above-the-dashboard-canvas.md +6 -0
  90. package/corpus/templates/analytics/changelog/2026-07-08-uptime-alerting-clarifies-in-app-inbox-vs-email-and-lets-you-paste-slack-or-webhook-urls-per-monitor.md +6 -0
  91. package/corpus/templates/analytics/docs/uptime-monitoring.md +13 -11
  92. package/corpus/templates/analytics/package.json +2 -2
  93. package/corpus/templates/analytics/server/db/schema-monitoring.ts +4 -0
  94. package/corpus/templates/analytics/server/lib/uptime-monitors.ts +60 -12
  95. package/corpus/templates/analytics/server/plugins/db.ts +10 -0
  96. package/corpus/templates/assets/.agents/skills/self-modifying-code/SKILL.md +20 -2
  97. package/corpus/templates/assets/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  98. package/corpus/templates/assets/package.json +2 -2
  99. package/corpus/templates/brain/.agents/skills/self-modifying-code/SKILL.md +20 -2
  100. package/corpus/templates/brain/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  101. package/corpus/templates/brain/package.json +2 -2
  102. package/corpus/templates/calendar/.agents/skills/self-modifying-code/SKILL.md +20 -2
  103. package/corpus/templates/calendar/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  104. package/corpus/templates/calendar/AGENTS.md +10 -0
  105. package/corpus/templates/calendar/actions/create-event.ts +5 -1
  106. package/corpus/templates/calendar/actions/event-action-helpers.ts +41 -0
  107. package/corpus/templates/calendar/actions/get-attendee-timezones.ts +28 -0
  108. package/corpus/templates/calendar/actions/set-attendee-timezone.ts +68 -0
  109. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +150 -11
  110. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +36 -1
  111. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +32 -0
  112. package/corpus/templates/calendar/app/hooks/use-attendee-timezones.ts +24 -0
  113. package/corpus/templates/calendar/app/i18n/zh-TW.ts +2 -0
  114. package/corpus/templates/calendar/app/i18n-data.ts +20 -0
  115. package/corpus/templates/calendar/app/lib/attendee-local-time.ts +122 -0
  116. package/corpus/templates/calendar/changelog/2026-07-08-event-details-now-support-extension-widgets-and-you-can-see-.md +6 -0
  117. package/corpus/templates/calendar/changelog/2026-07-08-when-you-invite-guests-to-a-new-event-you-now-appear-in.md +6 -0
  118. package/corpus/templates/calendar/package.json +2 -2
  119. package/corpus/templates/calendar/server/handlers/events.ts +5 -1
  120. package/corpus/templates/calendar/shared/api.ts +6 -0
  121. package/corpus/templates/chat/.agents/skills/self-modifying-code/SKILL.md +20 -2
  122. package/corpus/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  123. package/corpus/templates/chat/package.json +2 -2
  124. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +6 -2
  125. package/corpus/templates/clips/.agents/skills/self-modifying-code/SKILL.md +20 -2
  126. package/corpus/templates/clips/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  127. package/corpus/templates/clips/actions/create-meeting.ts +8 -3
  128. package/corpus/templates/clips/actions/list-meetings.ts +31 -3
  129. package/corpus/templates/clips/changelog/2026-07-08-after-finishing-a-desktop-recording-reopening-clips-restor.md +6 -0
  130. package/corpus/templates/clips/changelog/2026-07-08-clips-now-pops-up-to-take-notes-when-you-join-an-adhoc-zoom-.md +6 -0
  131. package/corpus/templates/clips/changelog/2026-07-08-desktop-clip-recordings-are-louder-after-capture-mic-only.md +6 -0
  132. package/corpus/templates/clips/changelog/2026-07-08-desktop-meeting-notes-stay-active-from-tray-and-shortcut-controls.md +6 -0
  133. package/corpus/templates/clips/changelog/2026-07-08-desktop-meeting-popovers-can-start-notes-and-join-the-meetin.md +6 -0
  134. package/corpus/templates/clips/changelog/2026-07-08-meeting-notes-keep-their-recording-state-when-the-desktop.md +6 -0
  135. package/corpus/templates/clips/changelog/2026-07-08-meeting-reminder-dropdowns-now-expand-fully-instead-of-being.md +6 -0
  136. package/corpus/templates/clips/changelog/2026-07-08-meeting-reminders-now-appear-1-minute-before-start-stay-unti.md +6 -0
  137. package/corpus/templates/clips/changelog/2026-07-08-the-desktop-meetings-panel-has-room-for-its-drop-shadow-and-.md +6 -0
  138. package/corpus/templates/clips/chrome-extension/package.json +2 -2
  139. package/corpus/templates/clips/desktop/package.json +2 -2
  140. package/corpus/templates/clips/desktop/src/app.tsx +214 -37
  141. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +30 -6
  142. package/corpus/templates/clips/desktop/src/lib/meeting-notification-timing.ts +83 -0
  143. package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +21 -0
  144. package/corpus/templates/clips/desktop/src/lib/recorder.ts +98 -20
  145. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +197 -50
  146. package/corpus/templates/clips/desktop/src/styles.css +150 -53
  147. package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +362 -0
  148. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +41 -7
  149. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +9 -2
  150. package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +89 -11
  151. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +71 -17
  152. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +36 -1
  153. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +62 -10
  154. package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +3 -3
  155. package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +1 -25
  156. package/corpus/templates/clips/desktop/src-tauri/src/state.rs +5 -0
  157. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +18 -6
  158. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +36 -1
  159. package/corpus/templates/clips/package.json +2 -2
  160. package/corpus/templates/content/.agents/skills/self-modifying-code/SKILL.md +20 -2
  161. package/corpus/templates/content/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  162. package/corpus/templates/content/actions/_batch-utils.ts +8 -0
  163. package/corpus/templates/content/actions/_builder-cms-read-client.ts +7 -5
  164. package/corpus/templates/content/actions/_content-database-personal-view.ts +80 -0
  165. package/corpus/templates/content/actions/_database-source-utils.ts +776 -319
  166. package/corpus/templates/content/actions/_database-utils.ts +6 -1
  167. package/corpus/templates/content/actions/_property-utils.ts +168 -0
  168. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +19 -12
  169. package/corpus/templates/content/actions/bind-content-database-source-field.ts +19 -12
  170. package/corpus/templates/content/actions/delete-document.ts +9 -13
  171. package/corpus/templates/content/actions/get-content-database-personal-view.ts +27 -0
  172. package/corpus/templates/content/actions/refresh-content-database-source.ts +5 -5
  173. package/corpus/templates/content/actions/update-content-database-personal-view.ts +31 -0
  174. package/corpus/templates/content/actions/update-content-database-view.ts +3 -0
  175. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +1 -1
  176. package/corpus/templates/content/app/components/editor/body-hydration.ts +35 -11
  177. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2631 -551
  178. package/corpus/templates/content/app/components/editor/database/filter-sort.ts +220 -18
  179. package/corpus/templates/content/app/hooks/use-content-database.ts +32 -0
  180. package/corpus/templates/content/app/hooks/use-documents.ts +8 -2
  181. package/corpus/templates/content/app/i18n/zh-TW.ts +16 -0
  182. package/corpus/templates/content/app/i18n-data.ts +154 -0
  183. package/corpus/templates/content/changelog/2026-07-08-builder-source-sync-now-completes-reliably-on-hosted-databases.md +6 -0
  184. package/corpus/templates/content/changelog/2026-07-08-database-filters-now-support-notion-style-reset-save-for-eve.md +6 -0
  185. package/corpus/templates/content/changelog/2026-07-08-date-filters-can-now-target-a-between-range.md +6 -0
  186. package/corpus/templates/content/changelog/2026-07-08-fixed-builder-database-sync-so-large-sources-continue-past-t.md +6 -0
  187. package/corpus/templates/content/package.json +2 -2
  188. package/corpus/templates/content/parity/matrix.md +1 -1
  189. package/corpus/templates/content/parity/matrix.ts +2 -0
  190. package/corpus/templates/content/server/plugins/db.ts +19 -0
  191. package/corpus/templates/content/shared/api.ts +24 -0
  192. package/corpus/templates/design/.agents/skills/self-modifying-code/SKILL.md +20 -2
  193. package/corpus/templates/design/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  194. package/corpus/templates/design/package.json +2 -2
  195. package/corpus/templates/dispatch/.agents/skills/self-modifying-code/SKILL.md +20 -2
  196. package/corpus/templates/dispatch/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  197. package/corpus/templates/dispatch/app/i18n/zh-TW.ts +34 -1
  198. package/corpus/templates/dispatch/app/i18n-data.ts +387 -10
  199. package/corpus/templates/dispatch/app/routes/automations.tsx +1 -0
  200. package/corpus/templates/dispatch/changelog/2026-07-08-added-an-automations-page-to-review-pause-and-create.md +6 -0
  201. package/corpus/templates/dispatch/changelog/2026-07-08-destinations-now-shows-outbound-delivery-queue-health.md +6 -0
  202. package/corpus/templates/dispatch/package.json +2 -2
  203. package/corpus/templates/forms/.agents/skills/self-modifying-code/SKILL.md +20 -2
  204. package/corpus/templates/forms/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  205. package/corpus/templates/forms/package.json +2 -2
  206. package/corpus/templates/macros/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  207. package/corpus/templates/macros/package.json +2 -2
  208. package/corpus/templates/mail/.agents/skills/self-modifying-code/SKILL.md +20 -2
  209. package/corpus/templates/mail/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  210. package/corpus/templates/mail/app/hooks/use-emails.ts +56 -44
  211. package/corpus/templates/mail/app/lib/gmail-mutation-queue.ts +325 -0
  212. package/corpus/templates/mail/changelog/2026-07-08-rapid-archive-and-mark-read-now-batch-gmail-updates-so-the-i.md +6 -0
  213. package/corpus/templates/mail/package.json +2 -2
  214. package/corpus/templates/plan/.agents/skills/self-modifying-code/SKILL.md +20 -2
  215. package/corpus/templates/plan/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  216. package/corpus/templates/plan/package.json +2 -2
  217. package/corpus/templates/slides/.agents/skills/self-modifying-code/SKILL.md +20 -2
  218. package/corpus/templates/slides/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  219. package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +46 -29
  220. package/corpus/templates/slides/app/context/DeckContext.tsx +45 -18
  221. package/corpus/templates/slides/app/i18n/ar-SA.ts +4 -0
  222. package/corpus/templates/slides/app/i18n/de-DE.ts +4 -0
  223. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  224. package/corpus/templates/slides/app/i18n/es-ES.ts +4 -0
  225. package/corpus/templates/slides/app/i18n/fr-FR.ts +4 -0
  226. package/corpus/templates/slides/app/i18n/hi-IN.ts +4 -0
  227. package/corpus/templates/slides/app/i18n/ja-JP.ts +4 -0
  228. package/corpus/templates/slides/app/i18n/ko-KR.ts +4 -0
  229. package/corpus/templates/slides/app/i18n/pt-BR.ts +4 -0
  230. package/corpus/templates/slides/app/i18n/zh-CN.ts +4 -0
  231. package/corpus/templates/slides/app/i18n/zh-TW.ts +4 -0
  232. package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +115 -0
  233. package/corpus/templates/slides/app/pages/DeckEditor.tsx +22 -13
  234. package/corpus/templates/slides/changelog/2026-07-08-dropping-an-image-onto-a-slide-and-sending-it-to-the-agent-w.md +6 -0
  235. package/corpus/templates/slides/changelog/2026-07-08-undo-works-after-the-agent-edits-a-slide-so-you-can-reverse.md +6 -0
  236. package/corpus/templates/slides/package.json +2 -2
  237. package/corpus/templates/videos/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  238. package/corpus/templates/voice/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  239. package/dist/agent/production-agent.d.ts +5 -1
  240. package/dist/agent/production-agent.d.ts.map +1 -1
  241. package/dist/agent/production-agent.js +112 -42
  242. package/dist/agent/production-agent.js.map +1 -1
  243. package/dist/agent/run-manager.d.ts.map +1 -1
  244. package/dist/agent/run-manager.js +23 -2
  245. package/dist/agent/run-manager.js.map +1 -1
  246. package/dist/agent/run-store.js +2 -2
  247. package/dist/agent/run-store.js.map +1 -1
  248. package/dist/agent/tool-search.d.ts +2 -0
  249. package/dist/agent/tool-search.d.ts.map +1 -1
  250. package/dist/agent/tool-search.js +60 -4
  251. package/dist/agent/tool-search.js.map +1 -1
  252. package/dist/catalog.json +2 -2
  253. package/dist/cli/add.js +1 -1
  254. package/dist/cli/add.js.map +1 -1
  255. package/dist/cli/create.d.ts.map +1 -1
  256. package/dist/cli/create.js +8 -3
  257. package/dist/cli/create.js.map +1 -1
  258. package/dist/cli/index.js +24 -4
  259. package/dist/cli/index.js.map +1 -1
  260. package/dist/cli/upgrade.d.ts +100 -0
  261. package/dist/cli/upgrade.d.ts.map +1 -0
  262. package/dist/cli/upgrade.js +678 -0
  263. package/dist/cli/upgrade.js.map +1 -0
  264. package/dist/client/AssistantChat.js +1 -1
  265. package/dist/client/AssistantChat.js.map +1 -1
  266. package/dist/client/HighlightedCodeBlock.d.ts +2 -0
  267. package/dist/client/HighlightedCodeBlock.d.ts.map +1 -1
  268. package/dist/client/HighlightedCodeBlock.js +10 -1
  269. package/dist/client/HighlightedCodeBlock.js.map +1 -1
  270. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  271. package/dist/client/agent-chat-adapter.js +32 -4
  272. package/dist/client/agent-chat-adapter.js.map +1 -1
  273. package/dist/client/chat/message-components.d.ts.map +1 -1
  274. package/dist/client/chat/message-components.js +61 -8
  275. package/dist/client/chat/message-components.js.map +1 -1
  276. package/dist/client/chat/tool-call-display.d.ts +12 -0
  277. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  278. package/dist/client/chat/tool-call-display.js +136 -42
  279. package/dist/client/chat/tool-call-display.js.map +1 -1
  280. package/dist/client/code-agent-chat-adapter.js +2 -9
  281. package/dist/client/code-agent-chat-adapter.js.map +1 -1
  282. package/dist/client/extensions/ExtensionSlot.js +6 -0
  283. package/dist/client/extensions/ExtensionSlot.js.map +1 -1
  284. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  285. package/dist/client/settings/SettingsPanel.js +52 -19
  286. package/dist/client/settings/SettingsPanel.js.map +1 -1
  287. package/dist/client/settings/SettingsSection.d.ts +19 -4
  288. package/dist/client/settings/SettingsSection.d.ts.map +1 -1
  289. package/dist/client/settings/SettingsSection.js +33 -4
  290. package/dist/client/settings/SettingsSection.js.map +1 -1
  291. package/dist/client/sse-event-processor.d.ts +9 -0
  292. package/dist/client/sse-event-processor.d.ts.map +1 -1
  293. package/dist/client/sse-event-processor.js +20 -2
  294. package/dist/client/sse-event-processor.js.map +1 -1
  295. package/dist/collab/routes.d.ts +1 -1
  296. package/dist/collab/struct-routes.d.ts +1 -1
  297. package/dist/extensions/actions.d.ts.map +1 -1
  298. package/dist/extensions/actions.js +85 -5
  299. package/dist/extensions/actions.js.map +1 -1
  300. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  301. package/dist/jobs/scheduler.js +26 -1
  302. package/dist/jobs/scheduler.js.map +1 -1
  303. package/dist/localization/default-messages.d.ts +19 -0
  304. package/dist/localization/default-messages.d.ts.map +1 -1
  305. package/dist/localization/default-messages.js +19 -0
  306. package/dist/localization/default-messages.js.map +1 -1
  307. package/dist/notifications/channels.d.ts +8 -3
  308. package/dist/notifications/channels.d.ts.map +1 -1
  309. package/dist/notifications/channels.js +61 -22
  310. package/dist/notifications/channels.js.map +1 -1
  311. package/dist/notifications/registry.d.ts.map +1 -1
  312. package/dist/notifications/registry.js +14 -3
  313. package/dist/notifications/registry.js.map +1 -1
  314. package/dist/notifications/routes.d.ts +1 -1
  315. package/dist/notifications/types.d.ts +4 -1
  316. package/dist/notifications/types.d.ts.map +1 -1
  317. package/dist/notifications/types.js.map +1 -1
  318. package/dist/observability/routes.d.ts +1 -1
  319. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  320. package/dist/resources/handlers.d.ts +3 -3
  321. package/dist/server/agent-chat-plugin.d.ts +5 -1
  322. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  323. package/dist/server/agent-chat-plugin.js +2 -2
  324. package/dist/server/agent-chat-plugin.js.map +1 -1
  325. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  326. package/dist/server/request-context.d.ts +7 -0
  327. package/dist/server/request-context.d.ts.map +1 -1
  328. package/dist/server/request-context.js.map +1 -1
  329. package/dist/styles/agent-conversation.css +1 -1
  330. package/dist/templates/default/.agents/skills/self-modifying-code/SKILL.md +20 -2
  331. package/dist/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  332. package/dist/templates/default/AGENTS.md +10 -3
  333. package/dist/templates/default/app/i18n/ar-SA.ts +21 -0
  334. package/dist/templates/default/app/i18n/de-DE.ts +21 -0
  335. package/dist/templates/default/app/i18n/en-US.ts +21 -0
  336. package/dist/templates/default/app/i18n/es-ES.ts +21 -0
  337. package/dist/templates/default/app/i18n/fr-FR.ts +21 -0
  338. package/dist/templates/default/app/i18n/hi-IN.ts +20 -0
  339. package/dist/templates/default/app/i18n/ja-JP.ts +21 -0
  340. package/dist/templates/default/app/i18n/ko-KR.ts +20 -0
  341. package/dist/templates/default/app/i18n/pt-BR.ts +21 -0
  342. package/dist/templates/default/app/i18n/zh-CN.ts +20 -0
  343. package/dist/templates/default/app/i18n/zh-TW.ts +19 -0
  344. package/dist/templates/default/package.json +4 -3
  345. package/dist/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  346. package/dist/templates/headless/AGENTS.md +9 -3
  347. package/dist/templates/headless/package.json +4 -3
  348. package/dist/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +20 -2
  349. package/dist/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  350. package/dist/templates/workspace-core/package.json +6 -6
  351. package/dist/templates/workspace-root/AGENTS.md +10 -4
  352. package/dist/templates/workspace-root/package.json +3 -2
  353. package/dist/triggers/dispatcher.js +27 -2
  354. package/dist/triggers/dispatcher.js.map +1 -1
  355. package/dist/vite/client.d.ts.map +1 -1
  356. package/dist/vite/client.js +70 -1
  357. package/dist/vite/client.js.map +1 -1
  358. package/docs/content/faq.mdx +17 -0
  359. package/docs/content/notifications.mdx +12 -6
  360. package/package.json +7 -6
  361. package/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +20 -2
  362. package/src/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  363. package/src/templates/default/AGENTS.md +10 -3
  364. package/src/templates/default/app/i18n/ar-SA.ts +21 -0
  365. package/src/templates/default/app/i18n/de-DE.ts +21 -0
  366. package/src/templates/default/app/i18n/en-US.ts +21 -0
  367. package/src/templates/default/app/i18n/es-ES.ts +21 -0
  368. package/src/templates/default/app/i18n/fr-FR.ts +21 -0
  369. package/src/templates/default/app/i18n/hi-IN.ts +20 -0
  370. package/src/templates/default/app/i18n/ja-JP.ts +21 -0
  371. package/src/templates/default/app/i18n/ko-KR.ts +20 -0
  372. package/src/templates/default/app/i18n/pt-BR.ts +21 -0
  373. package/src/templates/default/app/i18n/zh-CN.ts +20 -0
  374. package/src/templates/default/app/i18n/zh-TW.ts +19 -0
  375. package/src/templates/default/package.json +4 -3
  376. package/src/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  377. package/src/templates/headless/AGENTS.md +9 -3
  378. package/src/templates/headless/package.json +4 -3
  379. package/src/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +20 -2
  380. package/src/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
  381. package/src/templates/workspace-core/package.json +6 -6
  382. package/src/templates/workspace-root/AGENTS.md +10 -4
  383. package/src/templates/workspace-root/package.json +3 -2
@@ -23,6 +23,7 @@ import {
23
23
  IconPlayerSkipForward,
24
24
  IconPlayerTrackNext,
25
25
  IconRoute,
26
+ IconSearch,
26
27
  IconTerminal2,
27
28
  IconTimelineEvent,
28
29
  } from "@tabler/icons-react";
@@ -34,11 +35,13 @@ import {
34
35
  useMemo,
35
36
  useRef,
36
37
  useState,
38
+ type FormEvent,
37
39
  } from "react";
38
40
  import { Link, useParams } from "react-router";
39
41
 
40
42
  import { Button } from "@/components/ui/button";
41
43
  import { Card, CardContent } from "@/components/ui/card";
44
+ import { Input } from "@/components/ui/input";
42
45
  import {
43
46
  Popover,
44
47
  PopoverContent,
@@ -153,6 +156,8 @@ type ReplayViewportDimensions = {
153
156
 
154
157
  const DEFAULT_PLAYER_WIDTH = 1024;
155
158
  const DEFAULT_PLAYER_HEIGHT = 640;
159
+ const MIN_REPLAY_DISPLAY_ASPECT_RATIO = 0.45;
160
+ const MAX_REPLAY_DISPLAY_ASPECT_RATIO = 3;
156
161
  const DEFAULT_SPEED = 2;
157
162
  const SPEED_OPTIONS = [0.5, 1, 2, 4, 8];
158
163
  const SKIP_STEP_MS = 5000;
@@ -160,8 +165,15 @@ const MIN_IDLE_SKIP_MS = 8000;
160
165
  const IDLE_EDGE_PAD_MS = 1200;
161
166
  const REPLAY_CHUNK_FETCH_CONCURRENCY = 6;
162
167
  const REPLAY_CHUNK_UNAVAILABLE_MESSAGE = "Session replay chunk is unavailable";
163
- const DEFAULT_DEVTOOLS_HEIGHT = 280;
168
+ const DEFAULT_DEVTOOLS_HEIGHT = 220;
169
+ const MIN_STAGE_HEIGHT_PX = 240;
164
170
  const SCRUBBER_MARKER_LIMIT = 500;
171
+ const TIMELINE_MARKER_LIMIT = 300;
172
+ const TIMELINE_FOLLOW_PAUSE_MS = 4000;
173
+ /** Toast/snackbar noise only — keep insertStyleRules minimal like builder-internal. */
174
+ const SUPPRESS_OVERLAYS_CSS = [
175
+ "[data-radix-popper-content-wrapper], .Toastify, [class*='toast'], [class*='Toast'], [class*='Snackbar'] { display: none !important; }",
176
+ ];
165
177
  const SESSION_REPLAY_CONSOLE_EVENT_TAG = "agent-native.console";
166
178
  const SESSION_REPLAY_NETWORK_EVENT_TAG = "agent-native.network";
167
179
 
@@ -219,6 +231,7 @@ const INCREMENTAL_SOURCE = {
219
231
  Mutation: 0,
220
232
  MouseInteraction: 2,
221
233
  Scroll: 3,
234
+ ViewportResize: 4,
222
235
  Input: 5,
223
236
  TouchMove: 6,
224
237
  } as const;
@@ -471,17 +484,30 @@ function ReplayPlayer({
471
484
  const [skipInactive, setSkipInactive] = useState(true);
472
485
  const [devToolsOpen, setDevToolsOpen] = useState(false);
473
486
  const [devToolsHeight, setDevToolsHeight] = useState(DEFAULT_DEVTOOLS_HEIGHT);
487
+ const [maxDevToolsHeight, setMaxDevToolsHeight] = useState(420);
488
+ const playerShellRef = useRef<HTMLDivElement>(null);
474
489
  const [streamedDims, setStreamedDims] = useState<{
475
490
  width: number;
476
491
  height: number;
477
492
  } | null>(null);
478
493
  const [fitScale, setFitScale] = useState(1);
479
494
  const initialDims = useMemo(() => replayViewportDimensions(events), [events]);
495
+ const eventsRef = useLiveRef(events);
496
+ // Stable identity for the loaded event set so progressive chunk publishes
497
+ // that only grow the array do not tear down a healthy Replayer mid-playback.
498
+ const eventsIdentity = useMemo(
499
+ () =>
500
+ `${events.length}:${Number(events[0]?.timestamp ?? 0)}:${Number(
501
+ events[events.length - 1]?.timestamp ?? 0,
502
+ )}`,
503
+ [events],
504
+ );
505
+ const scrubbingRef = useRef(false);
506
+ const scrubResumePlayingRef = useRef(false);
480
507
 
481
- const playerWidth =
482
- streamedDims?.width ?? initialDims?.width ?? DEFAULT_PLAYER_WIDTH;
483
- const playerHeight =
484
- streamedDims?.height ?? initialDims?.height ?? DEFAULT_PLAYER_HEIGHT;
508
+ const displayDims = clampReplayDisplayDimensions(streamedDims ?? initialDims);
509
+ const playerWidth = displayDims?.width ?? DEFAULT_PLAYER_WIDTH;
510
+ const playerHeight = displayDims?.height ?? DEFAULT_PLAYER_HEIGHT;
485
511
  const skipRanges = useMemo(() => buildIdleSkipRanges(events), [events]);
486
512
  const skipRangesRef = useLiveRef(skipRanges);
487
513
  const skipInactiveRef = useLiveRef(skipInactive);
@@ -556,6 +582,26 @@ function ReplayPlayer({
556
582
  return () => observer.disconnect();
557
583
  }, [playerHeight, playerWidth]);
558
584
 
585
+ // Keep Dev Tools from eating the stage. On short viewports the panel used to
586
+ // shrink the replay area into a ribbon even when Meta dimensions were fine.
587
+ useEffect(() => {
588
+ const el = playerShellRef.current;
589
+ if (!el) return;
590
+ const update = () => {
591
+ const chromeBudget = 140;
592
+ const available = Math.max(
593
+ 160,
594
+ el.clientHeight - MIN_STAGE_HEIGHT_PX - chromeBudget,
595
+ );
596
+ setMaxDevToolsHeight(available);
597
+ setDevToolsHeight((current) => Math.min(current, available));
598
+ };
599
+ update();
600
+ const observer = new ResizeObserver(update);
601
+ observer.observe(el);
602
+ return () => observer.disconnect();
603
+ }, [devToolsOpen]);
604
+
559
605
  const updateTime = useCallback(
560
606
  (next: number) => {
561
607
  setCurrentTime(next);
@@ -586,6 +632,27 @@ function ReplayPlayer({
586
632
  [playingRef, status, totalTime, updateTime],
587
633
  );
588
634
 
635
+ const beginScrub = useCallback(
636
+ (ms: number) => {
637
+ if (!scrubbingRef.current) {
638
+ scrubResumePlayingRef.current = playingRef.current;
639
+ }
640
+ scrubbingRef.current = true;
641
+ seek(ms, false);
642
+ },
643
+ [playingRef, seek],
644
+ );
645
+
646
+ const endScrub = useCallback(
647
+ (ms: number) => {
648
+ const resume = scrubResumePlayingRef.current;
649
+ scrubbingRef.current = false;
650
+ scrubResumePlayingRef.current = false;
651
+ seek(ms, resume);
652
+ },
653
+ [seek],
654
+ );
655
+
589
656
  useEffect(() => {
590
657
  registerSeek(seek);
591
658
  }, [registerSeek, seek]);
@@ -596,21 +663,29 @@ function ReplayPlayer({
596
663
  let localReplayer: any = null;
597
664
 
598
665
  async function loadReplay() {
599
- const hasPlayableEvents = hasPlayableReplayEvents(events);
600
- if (!hasPlayableEvents && !response.isComplete) {
666
+ // Wait for the full recording before creating the Replayer. Progressive
667
+ // chunk publishes used to rebuild the player on every append, which
668
+ // reset the clock, disabled the scrubber, and desynced the playhead.
669
+ const replayEvents = eventsRef.current;
670
+ if (!response.isComplete) {
601
671
  setStatus("loading");
602
672
  setError(null);
603
673
  setPlaying(false);
604
674
  return;
605
675
  }
606
- if (events.length < 2) {
676
+ if (replayEvents.length < 2) {
607
677
  throw new Error(t("sessions.noReplayEvents"));
608
678
  }
609
679
  if (
610
- !events.some((event) => event.type === RRWEB_EVENT_TYPE.FullSnapshot)
680
+ !replayEvents.some(
681
+ (event) => event.type === RRWEB_EVENT_TYPE.FullSnapshot,
682
+ )
611
683
  ) {
612
684
  throw new Error(t("sessions.noReplayEvents"));
613
685
  }
686
+ if (!hasPlayableReplayEvents(replayEvents)) {
687
+ throw new Error(t("sessions.noReplayEvents"));
688
+ }
614
689
  setStatus("loading");
615
690
  setError(null);
616
691
  await import("@rrweb/replay/dist/style.css");
@@ -618,36 +693,31 @@ function ReplayPlayer({
618
693
  if (cancelled || !stageRootRef.current) return;
619
694
 
620
695
  stageRootRef.current.innerHTML = "";
621
- setStreamedDims(initialDims);
622
- localReplayer = new Replayer(events as any[], {
696
+ // Match builder-internal: pass events through untouched and let rrweb own
697
+ // iframe sizing via Meta / ViewportResize. Only use dimensions for CSS
698
+ // fit-to-stage scaling of the outer wrapper — never rewrite Meta or force
699
+ // iframe width/height (that mismatches the FullSnapshot DOM and blanks
700
+ // the stage).
701
+ setStreamedDims(replayViewportDimensions(replayEvents));
702
+ localReplayer = new Replayer(replayEvents as any[], {
623
703
  root: stageRootRef.current,
624
704
  speed: speedRef.current,
625
705
  skipInactive: false,
626
706
  showWarning: false,
627
707
  showDebug: false,
628
708
  triggerFocus: false,
629
- // Subtle FullStory-style trail behind the visitor cursor. The canvas
630
- // is resized alongside the iframe in applyReplayFrameDimensions.
631
- mouseTail: {
632
- strokeStyle: "rgba(59, 130, 246, 0.35)",
633
- lineWidth: 2,
634
- duration: 600,
635
- lineCap: "round",
636
- },
637
- insertStyleRules: [
638
- "[data-radix-popper-content-wrapper], .Toastify, [class*='toast'], [class*='Toast'] { display: none !important; }",
639
- ],
709
+ mouseTail: false,
710
+ insertStyleRules: SUPPRESS_OVERLAYS_CSS,
640
711
  });
641
712
  replayerRef.current = localReplayer;
642
713
  const meta = localReplayer.getMetaData?.();
643
- const total = Number(meta?.totalTime ?? replayDuration(events));
714
+ const total = Number(meta?.totalTime ?? replayDuration(replayEvents));
644
715
  setTotalTime(Number.isFinite(total) ? total : 0);
645
716
  const startAt = clamp(
646
717
  currentTimeRef.current,
647
718
  0,
648
719
  Number.isFinite(total) ? total : 0,
649
720
  );
650
- applyReplayFrameDimensions(localReplayer, initialDims);
651
721
  localReplayer.on?.("finish", () => {
652
722
  setPlaying(false);
653
723
  const finalTime = Number(localReplayer.getCurrentTime?.() ?? total);
@@ -655,20 +725,20 @@ function ReplayPlayer({
655
725
  });
656
726
  localReplayer.on?.("resize", (payload: unknown) => {
657
727
  const dims = payload as { width?: unknown; height?: unknown };
658
- const nextDims = normalizeReplayDimensions(dims.width, dims.height);
659
- if (nextDims) {
660
- applyReplayFrameDimensions(localReplayer, nextDims);
661
- revealReplayFrame(localReplayer);
662
- setStreamedDims(nextDims);
728
+ if (
729
+ typeof dims.width === "number" &&
730
+ typeof dims.height === "number" &&
731
+ Number.isFinite(dims.width) &&
732
+ Number.isFinite(dims.height) &&
733
+ dims.width > 0 &&
734
+ dims.height > 0
735
+ ) {
736
+ setStreamedDims({
737
+ width: Math.round(dims.width),
738
+ height: Math.round(dims.height),
739
+ });
663
740
  }
664
741
  });
665
- localReplayer.on?.("fullsnapshot-rebuilded", () => {
666
- applyReplayFrameDimensions(
667
- localReplayer,
668
- replayViewportDimensions(events) ?? initialDims,
669
- );
670
- revealReplayFrame(localReplayer);
671
- });
672
742
  updateTime(startAt);
673
743
  setStatus("ready");
674
744
  try {
@@ -683,13 +753,6 @@ function ReplayPlayer({
683
753
  }
684
754
  setPlaying(false);
685
755
  }
686
- window.requestAnimationFrame(() => {
687
- applyReplayFrameDimensions(
688
- localReplayer,
689
- replayViewportDimensions(events) ?? initialDims,
690
- );
691
- revealReplayFrame(localReplayer);
692
- });
693
756
  }
694
757
 
695
758
  void loadReplay().catch((loadError: any) => {
@@ -713,10 +776,13 @@ function ReplayPlayer({
713
776
  replayerRef.current = null;
714
777
  if (stageRootRef.current) stageRootRef.current.innerHTML = "";
715
778
  };
779
+ // Key only on isComplete + a stable events identity. Including the events
780
+ // array reference would rebuild the Replayer when the final publish()
781
+ // replaces the chunks object even though the event set is unchanged.
716
782
  }, [
717
783
  currentTimeRef,
718
- events,
719
- initialDims,
784
+ eventsIdentity,
785
+ eventsRef,
720
786
  response.isComplete,
721
787
  speedRef,
722
788
  t,
@@ -732,7 +798,7 @@ function ReplayPlayer({
732
798
 
733
799
  const tick = () => {
734
800
  const replayer = replayerRef.current;
735
- if (replayer) {
801
+ if (replayer && !scrubbingRef.current) {
736
802
  let nextTime = Number(
737
803
  replayer.getCurrentTime?.() ?? currentTimeRef.current,
738
804
  );
@@ -812,210 +878,218 @@ function ReplayPlayer({
812
878
 
813
879
  return (
814
880
  <TooltipProvider>
815
- <Card className="flex min-h-0 overflow-hidden">
816
- <CardContent className="flex min-h-0 flex-1 flex-col p-0">
817
- <div className="flex min-h-0 flex-1 flex-col bg-muted/20 p-2">
818
- {currentUrl ? (
881
+ <div ref={playerShellRef} className="flex min-h-0 flex-1 flex-col">
882
+ <Card className="flex min-h-0 flex-1 overflow-hidden">
883
+ <CardContent className="flex min-h-0 flex-1 flex-col p-0">
884
+ <div className="flex min-h-0 flex-1 flex-col bg-muted/20 p-2">
885
+ {currentUrl ? (
886
+ <div
887
+ className="flex h-8 shrink-0 items-center rounded-t-md border border-b-0 bg-background px-3 font-mono text-xs text-muted-foreground"
888
+ title={currentUrl}
889
+ >
890
+ <span className="truncate">{currentUrl}</span>
891
+ </div>
892
+ ) : null}
819
893
  <div
820
- className="flex h-8 shrink-0 items-center rounded-t-md border border-b-0 bg-background px-3 font-mono text-xs text-muted-foreground"
821
- title={currentUrl}
894
+ ref={stageAreaRef}
895
+ className={cn(
896
+ "relative min-h-[200px] flex-1 overflow-hidden border bg-white dark:bg-zinc-950",
897
+ currentUrl ? "rounded-b-md" : "rounded-md",
898
+ !devToolsOpen && "min-h-[320px]",
899
+ )}
822
900
  >
823
- <span className="truncate">{currentUrl}</span>
901
+ <div
902
+ ref={stageRootRef}
903
+ className="an-replay-stage-root absolute left-1/2 top-1/2"
904
+ style={{
905
+ width: playerWidth,
906
+ height: playerHeight,
907
+ transform: `translate(-50%, -50%) scale(${fitScale})`,
908
+ transformOrigin: "center center",
909
+ }}
910
+ />
911
+ <button
912
+ type="button"
913
+ className="absolute inset-0 z-20 cursor-pointer rounded-[inherit] border-0 bg-transparent p-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-default"
914
+ disabled={disabled}
915
+ aria-label={
916
+ playing ? t("sessions.pause") : t("sessions.play")
917
+ }
918
+ onClick={togglePlay}
919
+ />
920
+ {status === "loading" ? (
921
+ <div className="absolute inset-0 z-30 grid place-items-center bg-background/70 p-6 text-center text-sm text-muted-foreground">
922
+ <div className="w-full max-w-sm">
923
+ <p>{t("sessions.replayLoading")}</p>
924
+ <div className="mt-3 h-1.5 overflow-hidden rounded-full bg-muted">
925
+ <div
926
+ className="h-full rounded-full bg-primary transition-[width]"
927
+ style={{
928
+ width: `${Math.round(loadingPercent * 100)}%`,
929
+ }}
930
+ />
931
+ </div>
932
+ {response.totalChunks > 0 ? (
933
+ <p className="mt-2 font-mono text-[11px]">
934
+ {t("sessions.replayLoadingProgress", {
935
+ loaded: String(response.loadedChunks),
936
+ total: String(response.totalChunks),
937
+ })}
938
+ </p>
939
+ ) : null}
940
+ </div>
941
+ </div>
942
+ ) : null}
943
+ {status === "error" && error ? (
944
+ <div className="absolute inset-0 z-30 grid place-items-center bg-background/85 p-6 text-center text-sm text-destructive">
945
+ {t("sessions.loadFailed", { message: error })}
946
+ </div>
947
+ ) : null}
824
948
  </div>
825
- ) : null}
826
- <div
827
- ref={stageAreaRef}
828
- className={cn(
829
- "relative min-h-[320px] flex-1 overflow-hidden border bg-white dark:bg-zinc-950",
830
- currentUrl ? "rounded-b-md" : "rounded-md",
831
- )}
832
- >
833
- <div
834
- ref={stageRootRef}
835
- className="an-replay-stage-root absolute left-1/2 top-1/2"
836
- style={{
837
- width: playerWidth,
838
- height: playerHeight,
839
- transform: `translate(-50%, -50%) scale(${fitScale})`,
840
- transformOrigin: "center center",
841
- }}
842
- />
843
- <button
949
+ </div>
950
+
951
+ <div className="flex shrink-0 flex-wrap items-center gap-2 border-t px-3 py-2">
952
+ <ReplayIconButton
953
+ label={t("sessions.skipBack")}
954
+ disabled={disabled}
955
+ onClick={() => seek(currentTime - SKIP_STEP_MS)}
956
+ >
957
+ <IconPlayerSkipBack className="h-4 w-4" />
958
+ </ReplayIconButton>
959
+ <Button
844
960
  type="button"
845
- className="absolute inset-0 z-20 cursor-pointer rounded-[inherit] border-0 bg-transparent p-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-default"
961
+ size="icon"
846
962
  disabled={disabled}
847
- aria-label={playing ? t("sessions.pause") : t("sessions.play")}
848
963
  onClick={togglePlay}
849
- />
850
- {status === "loading" ? (
851
- <div className="absolute inset-0 z-30 grid place-items-center bg-background/70 p-6 text-center text-sm text-muted-foreground">
852
- <div className="w-full max-w-sm">
853
- <p>{t("sessions.replayLoading")}</p>
854
- <div className="mt-3 h-1.5 overflow-hidden rounded-full bg-muted">
855
- <div
856
- className="h-full rounded-full bg-primary transition-[width]"
857
- style={{
858
- width: `${Math.round(loadingPercent * 100)}%`,
859
- }}
860
- />
861
- </div>
862
- {response.totalChunks > 0 ? (
863
- <p className="mt-2 font-mono text-[11px]">
864
- {t("sessions.replayLoadingProgress", {
865
- loaded: String(response.loadedChunks),
866
- total: String(response.totalChunks),
867
- })}
868
- </p>
869
- ) : null}
870
- </div>
871
- </div>
872
- ) : null}
873
- {status === "error" && error ? (
874
- <div className="absolute inset-0 z-30 grid place-items-center bg-background/85 p-6 text-center text-sm text-destructive">
875
- {t("sessions.loadFailed", { message: error })}
876
- </div>
877
- ) : null}
878
- </div>
879
- </div>
964
+ aria-label={playing ? t("sessions.pause") : t("sessions.play")}
965
+ className="h-8 w-8"
966
+ >
967
+ {playing ? (
968
+ <IconPlayerPause className="h-4 w-4" />
969
+ ) : (
970
+ <IconPlayerPlay className="h-4 w-4" />
971
+ )}
972
+ </Button>
973
+ <ReplayIconButton
974
+ label={t("sessions.skipForward")}
975
+ disabled={disabled}
976
+ onClick={() => seek(currentTime + SKIP_STEP_MS)}
977
+ >
978
+ <IconPlayerSkipForward className="h-4 w-4" />
979
+ </ReplayIconButton>
880
980
 
881
- <div className="flex shrink-0 flex-wrap items-center gap-2 border-t px-3 py-2">
882
- <ReplayIconButton
883
- label={t("sessions.skipBack")}
884
- disabled={disabled}
885
- onClick={() => seek(currentTime - SKIP_STEP_MS)}
886
- >
887
- <IconPlayerSkipBack className="h-4 w-4" />
888
- </ReplayIconButton>
889
- <Button
890
- type="button"
891
- size="icon"
892
- disabled={disabled}
893
- onClick={togglePlay}
894
- aria-label={playing ? t("sessions.pause") : t("sessions.play")}
895
- className="h-8 w-8"
896
- >
897
- {playing ? (
898
- <IconPlayerPause className="h-4 w-4" />
899
- ) : (
900
- <IconPlayerPlay className="h-4 w-4" />
901
- )}
902
- </Button>
903
- <ReplayIconButton
904
- label={t("sessions.skipForward")}
905
- disabled={disabled}
906
- onClick={() => seek(currentTime + SKIP_STEP_MS)}
907
- >
908
- <IconPlayerSkipForward className="h-4 w-4" />
909
- </ReplayIconButton>
910
-
911
- <span className="w-12 text-center font-mono text-xs text-muted-foreground">
912
- {formatClock(currentTime)}
913
- </span>
914
- <ReplayScrubber
915
- currentTime={currentTime}
916
- totalTime={totalTime}
917
- markers={markers}
918
- skipRanges={skipRanges}
919
- skipInactive={skipInactive}
920
- disabled={disabled}
921
- onSeek={(ms) => seek(ms)}
922
- />
923
- <span className="w-12 text-center font-mono text-xs text-muted-foreground">
924
- {formatClock(totalTime)}
925
- </span>
981
+ <span className="w-12 text-center font-mono text-xs text-muted-foreground">
982
+ {formatClock(currentTime)}
983
+ </span>
984
+ <ReplayScrubber
985
+ currentTime={currentTime}
986
+ totalTime={totalTime}
987
+ markers={markers}
988
+ skipRanges={skipRanges}
989
+ skipInactive={skipInactive}
990
+ disabled={disabled}
991
+ onScrub={beginScrub}
992
+ onScrubEnd={endScrub}
993
+ />
994
+ <span className="w-12 text-center font-mono text-xs text-muted-foreground">
995
+ {formatClock(totalTime)}
996
+ </span>
926
997
 
927
- <div className="flex items-center rounded-md bg-muted p-1">
928
- {SPEED_OPTIONS.map((option) => (
929
- <button
930
- key={option}
931
- type="button"
932
- className={cn(
933
- "rounded px-2 py-1 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
934
- speed === option &&
935
- "bg-background text-foreground shadow-sm",
936
- )}
937
- onClick={() => updateSpeed(option)}
938
- >
939
- {option}x
940
- </button>
941
- ))}
942
- </div>
998
+ <div className="flex items-center rounded-md bg-muted p-1">
999
+ {SPEED_OPTIONS.map((option) => (
1000
+ <button
1001
+ key={option}
1002
+ type="button"
1003
+ className={cn(
1004
+ "rounded px-2 py-1 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
1005
+ speed === option &&
1006
+ "bg-background text-foreground shadow-sm",
1007
+ )}
1008
+ onClick={() => updateSpeed(option)}
1009
+ >
1010
+ {option}x
1011
+ </button>
1012
+ ))}
1013
+ </div>
943
1014
 
944
- <Tooltip>
945
- <TooltipTrigger asChild>
946
- <button
947
- type="button"
948
- className={cn(
949
- "inline-flex h-8 items-center gap-1.5 rounded-md border px-2 text-xs font-medium transition-colors hover:bg-muted",
950
- skipInactive &&
951
- "border-primary/40 bg-primary/10 text-primary",
952
- )}
953
- onClick={() => setSkipInactive((value) => !value)}
954
- aria-pressed={skipInactive}
955
- >
956
- <IconPlayerTrackNext className="h-4 w-4" />
957
- {t("sessions.skipInactive")}
958
- </button>
959
- </TooltipTrigger>
960
- <TooltipContent>
961
- {skipInactive
962
- ? t("sessions.skipInactiveOn")
963
- : t("sessions.skipInactiveOff")}
964
- </TooltipContent>
965
- </Tooltip>
966
-
967
- <button
968
- type="button"
969
- className={cn(
970
- "inline-flex h-8 items-center gap-1.5 rounded-md border px-2 text-xs font-medium transition-colors hover:bg-muted",
971
- devToolsOpen && "border-primary/40 bg-primary/10 text-primary",
972
- )}
973
- onClick={() => setDevToolsOpen((value) => !value)}
974
- aria-pressed={devToolsOpen}
975
- aria-expanded={devToolsOpen}
976
- >
977
- <IconTerminal2 className="h-4 w-4" />
978
- {t("sessions.devtools")}
979
- {devToolsIssueCount > 0 ? (
980
- <span
981
- className="inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-red-500 px-1 font-mono text-[10px] font-semibold leading-none text-white"
982
- aria-label={t("sessions.devtoolsIssueCount", {
983
- count: String(devToolsIssueCount),
984
- })}
985
- >
986
- {devToolsIssueCount > 99 ? "99+" : devToolsIssueCount}
987
- </span>
988
- ) : null}
989
- </button>
1015
+ <Tooltip>
1016
+ <TooltipTrigger asChild>
1017
+ <button
1018
+ type="button"
1019
+ className={cn(
1020
+ "inline-flex h-8 items-center gap-1.5 rounded-md border px-2 text-xs font-medium transition-colors hover:bg-muted",
1021
+ skipInactive &&
1022
+ "border-primary/40 bg-primary/10 text-primary",
1023
+ )}
1024
+ onClick={() => setSkipInactive((value) => !value)}
1025
+ aria-pressed={skipInactive}
1026
+ >
1027
+ <IconPlayerTrackNext className="h-4 w-4" />
1028
+ {t("sessions.skipInactive")}
1029
+ </button>
1030
+ </TooltipTrigger>
1031
+ <TooltipContent>
1032
+ {skipInactive
1033
+ ? t("sessions.skipInactiveOn")
1034
+ : t("sessions.skipInactiveOff")}
1035
+ </TooltipContent>
1036
+ </Tooltip>
990
1037
 
991
- <span className="ms-auto hidden text-xs text-muted-foreground lg:inline">
992
- {t("sessions.replayEventCount", {
993
- events: String(response.eventCount),
994
- })}
995
- {response.truncated ? ` ${t("sessions.truncated")}` : ""}
996
- </span>
997
- </div>
1038
+ <button
1039
+ type="button"
1040
+ className={cn(
1041
+ "inline-flex h-8 items-center gap-1.5 rounded-md border px-2 text-xs font-medium transition-colors hover:bg-muted",
1042
+ devToolsOpen &&
1043
+ "border-primary/40 bg-primary/10 text-primary",
1044
+ )}
1045
+ onClick={() => setDevToolsOpen((value) => !value)}
1046
+ aria-pressed={devToolsOpen}
1047
+ aria-expanded={devToolsOpen}
1048
+ >
1049
+ <IconTerminal2 className="h-4 w-4" />
1050
+ {t("sessions.devtools")}
1051
+ {devToolsIssueCount > 0 ? (
1052
+ <span
1053
+ className="inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-red-500 px-1 font-mono text-[10px] font-semibold leading-none text-white"
1054
+ aria-label={t("sessions.devtoolsIssueCount", {
1055
+ count: String(devToolsIssueCount),
1056
+ })}
1057
+ >
1058
+ {devToolsIssueCount > 99 ? "99+" : devToolsIssueCount}
1059
+ </span>
1060
+ ) : null}
1061
+ </button>
1062
+
1063
+ <span className="ms-auto hidden text-xs text-muted-foreground lg:inline">
1064
+ {t("sessions.replayEventCount", {
1065
+ events: String(response.eventCount),
1066
+ })}
1067
+ {response.truncated ? ` ${t("sessions.truncated")}` : ""}
1068
+ </span>
1069
+ </div>
998
1070
 
999
- {devToolsOpen ? (
1000
- <SessionDevToolsPanel
1001
- diagnostics={diagnostics}
1002
- currentTime={currentTime}
1003
- height={devToolsHeight}
1004
- onHeightChange={setDevToolsHeight}
1005
- onSeek={(ms) => seek(ms, true)}
1006
- issueMatches={issueMatches}
1007
- />
1008
- ) : null}
1071
+ {devToolsOpen ? (
1072
+ <SessionDevToolsPanel
1073
+ diagnostics={diagnostics}
1074
+ currentTime={currentTime}
1075
+ height={Math.min(devToolsHeight, maxDevToolsHeight)}
1076
+ maxHeight={maxDevToolsHeight}
1077
+ onHeightChange={setDevToolsHeight}
1078
+ onSeek={(ms) => seek(ms, true)}
1079
+ issueMatches={issueMatches}
1080
+ />
1081
+ ) : null}
1009
1082
 
1010
- {response.unavailableChunks > 0 ? (
1011
- <div className="border-t px-4 py-2 text-xs text-muted-foreground">
1012
- {t("sessions.unavailableChunks", {
1013
- count: String(response.unavailableChunks),
1014
- })}
1015
- </div>
1016
- ) : null}
1017
- </CardContent>
1018
- </Card>
1083
+ {response.unavailableChunks > 0 ? (
1084
+ <div className="border-t px-4 py-2 text-xs text-muted-foreground">
1085
+ {t("sessions.unavailableChunks", {
1086
+ count: String(response.unavailableChunks),
1087
+ })}
1088
+ </div>
1089
+ ) : null}
1090
+ </CardContent>
1091
+ </Card>
1092
+ </div>
1019
1093
  </TooltipProvider>
1020
1094
  );
1021
1095
  }
@@ -1027,7 +1101,8 @@ function ReplayScrubber({
1027
1101
  skipRanges,
1028
1102
  skipInactive,
1029
1103
  disabled,
1030
- onSeek,
1104
+ onScrub,
1105
+ onScrubEnd,
1031
1106
  }: {
1032
1107
  currentTime: number;
1033
1108
  totalTime: number;
@@ -1035,13 +1110,19 @@ function ReplayScrubber({
1035
1110
  skipRanges: SkipRange[];
1036
1111
  skipInactive: boolean;
1037
1112
  disabled: boolean;
1038
- onSeek: (ms: number) => void;
1113
+ onScrub: (ms: number) => void;
1114
+ onScrubEnd: (ms: number) => void;
1039
1115
  }) {
1040
1116
  const t = useT();
1041
1117
  const scrubberMarkers = useMemo(
1042
1118
  () => visibleScrubberMarkers(markers, totalTime),
1043
1119
  [markers, totalTime],
1044
1120
  );
1121
+
1122
+ function handleScrub(event: FormEvent<HTMLInputElement>) {
1123
+ onScrub(Number(event.currentTarget.value));
1124
+ }
1125
+
1045
1126
  return (
1046
1127
  <div className="relative min-h-8 min-w-[180px] flex-1">
1047
1128
  <input
@@ -1050,9 +1131,16 @@ function ReplayScrubber({
1050
1131
  min={0}
1051
1132
  max={Math.max(0, totalTime)}
1052
1133
  step={50}
1053
- value={Math.min(currentTime, totalTime)}
1134
+ value={Math.min(currentTime, Math.max(totalTime, 0))}
1054
1135
  disabled={disabled}
1055
- onChange={(event) => onSeek(Number(event.target.value))}
1136
+ onInput={handleScrub}
1137
+ onChange={handleScrub}
1138
+ onPointerUp={(event) =>
1139
+ onScrubEnd(Number((event.target as HTMLInputElement).value))
1140
+ }
1141
+ onKeyUp={(event) =>
1142
+ onScrubEnd(Number((event.target as HTMLInputElement).value))
1143
+ }
1056
1144
  aria-label={t("sessions.replayTimeline")}
1057
1145
  />
1058
1146
  <div className="pointer-events-none absolute inset-x-0 top-1/2 z-10 h-2 -translate-y-1/2 overflow-hidden rounded-full">
@@ -1081,7 +1169,7 @@ function ReplayScrubber({
1081
1169
  <span
1082
1170
  key={marker.id}
1083
1171
  className={cn(
1084
- "absolute top-1/2 h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full ring-2 ring-background",
1172
+ "absolute top-1/2 h-1.5 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full opacity-90 ring-1 ring-background/80",
1085
1173
  marker.kind === "navigation"
1086
1174
  ? "bg-amber-500"
1087
1175
  : marker.kind === "input"
@@ -1147,38 +1235,94 @@ function ReplayTimeline({
1147
1235
  }) {
1148
1236
  const t = useT();
1149
1237
  const [expandedMarkerId, setExpandedMarkerId] = useState<string | null>(null);
1150
- const visibleMarkers = markers.slice(0, 300);
1238
+ const [query, setQuery] = useState("");
1239
+ const listRef = useRef<HTMLDivElement>(null);
1240
+ const lastManualScrollAtRef = useRef(0);
1241
+ const activeRowRef = useRef<HTMLDivElement | null>(null);
1242
+
1243
+ const filteredMarkers = useMemo(
1244
+ () => filterReplayMarkers(markers, query).slice(0, TIMELINE_MARKER_LIMIT),
1245
+ [markers, query],
1246
+ );
1247
+
1248
+ useEffect(() => {
1249
+ if (!activeMarkerId) return;
1250
+ if (Date.now() - lastManualScrollAtRef.current < TIMELINE_FOLLOW_PAUSE_MS) {
1251
+ return;
1252
+ }
1253
+ const row = activeRowRef.current;
1254
+ const list = listRef.current;
1255
+ if (!row || !list) return;
1256
+ const rowTop = row.offsetTop;
1257
+ const rowBottom = rowTop + row.offsetHeight;
1258
+ if (
1259
+ rowTop >= list.scrollTop &&
1260
+ rowBottom <= list.scrollTop + list.clientHeight
1261
+ ) {
1262
+ return;
1263
+ }
1264
+ list.scrollTo({
1265
+ top: Math.max(0, rowTop - list.clientHeight / 3),
1266
+ behavior: "smooth",
1267
+ });
1268
+ }, [activeMarkerId, filteredMarkers]);
1269
+
1151
1270
  return (
1152
1271
  <Card className="analytics-session-detail-timeline min-h-0 overflow-hidden">
1153
1272
  <CardContent className="flex min-h-0 flex-1 flex-col p-0">
1154
- <div className="shrink-0 border-b px-3 py-2">
1273
+ <div className="shrink-0 space-y-2 border-b px-3 py-2">
1155
1274
  <div className="flex items-center gap-2 text-sm font-semibold">
1156
1275
  <IconTimelineEvent className="h-4 w-4" />
1157
1276
  {t("sessions.timeline")}
1158
1277
  </div>
1159
- <p className="mt-0.5 text-xs text-muted-foreground">
1160
- {visibleMarkers.length
1278
+ <p className="text-xs text-muted-foreground">
1279
+ {filteredMarkers.length
1161
1280
  ? t("sessions.timelineDescription", {
1162
- count: String(visibleMarkers.length),
1281
+ count: String(filteredMarkers.length),
1163
1282
  total: String(markers.length),
1164
1283
  })
1165
1284
  : t("sessions.noTimelineEvents")}
1166
1285
  </p>
1286
+ <div className="relative">
1287
+ <IconSearch className="pointer-events-none absolute start-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
1288
+ <Input
1289
+ type="search"
1290
+ className="h-7 ps-7 text-xs"
1291
+ value={query}
1292
+ placeholder={t("sessions.timelineSearch")}
1293
+ onChange={(event) => setQuery(event.target.value)}
1294
+ />
1295
+ </div>
1167
1296
  </div>
1168
- <div className="min-h-0 flex-1 overflow-auto">
1169
- {visibleMarkers.length ? (
1297
+ <div
1298
+ ref={listRef}
1299
+ className="min-h-0 flex-1 overflow-auto"
1300
+ onWheel={() => {
1301
+ lastManualScrollAtRef.current = Date.now();
1302
+ }}
1303
+ onPointerDown={() => {
1304
+ lastManualScrollAtRef.current = Date.now();
1305
+ }}
1306
+ >
1307
+ {filteredMarkers.length ? (
1170
1308
  <div className="divide-y">
1171
- {visibleMarkers.map((marker) => {
1309
+ {filteredMarkers.map((marker) => {
1172
1310
  const expanded = marker.id === expandedMarkerId;
1311
+ const active = marker.id === activeMarkerId;
1173
1312
  return (
1174
1313
  <div
1175
1314
  key={marker.id}
1176
- className={cn(marker.id === activeMarkerId && "bg-muted")}
1315
+ ref={active ? activeRowRef : undefined}
1316
+ className={cn(
1317
+ "transition-colors duration-300",
1318
+ active && "bg-primary/[0.06] dark:bg-primary/[0.09]",
1319
+ )}
1177
1320
  >
1178
1321
  <button
1179
1322
  type="button"
1180
1323
  className="flex w-full gap-2.5 px-3 py-2.5 text-left transition-colors hover:bg-muted/50"
1181
1324
  aria-expanded={expanded}
1325
+ aria-current={active ? "true" : undefined}
1182
1326
  onClick={() => {
1183
1327
  onSeek(marker.offsetMs);
1184
1328
  setExpandedMarkerId((current) =>
@@ -1236,7 +1380,9 @@ function ReplayTimeline({
1236
1380
  </div>
1237
1381
  ) : (
1238
1382
  <div className="p-4 text-sm text-muted-foreground">
1239
- {t("sessions.noTimelineEvents")}
1383
+ {query.trim()
1384
+ ? t("sessions.timelineNoMatches")
1385
+ : t("sessions.noTimelineEvents")}
1240
1386
  </div>
1241
1387
  )}
1242
1388
  </div>
@@ -1323,21 +1469,18 @@ function useSessionReplayPlayback(recordingId: string) {
1323
1469
  let loadedBytes = 0;
1324
1470
  let unavailableChunks = 0;
1325
1471
  let nextIndex = 0;
1326
- let firstPlayablePublished = false;
1327
1472
 
1328
1473
  const publish = (force = false) => {
1329
1474
  const complete = loadedCount >= manifest.chunks.length;
1330
1475
  const chunks = loadedChunks.filter(
1331
1476
  (chunk): chunk is ReplayChunkEvents => Boolean(chunk),
1332
1477
  );
1333
- const shouldPublishEvents =
1334
- force ||
1335
- complete ||
1336
- (!firstPlayablePublished &&
1337
- hasPlayableReplayEvents(chunks.flatMap((chunk) => chunk.events)));
1478
+ // Only hand events to the player once every chunk is in. Partial
1479
+ // publishes used to rebuild the Replayer mid-playback and break the
1480
+ // scrubber; the loading bar still updates while chunks stream in.
1481
+ const shouldPublishEvents = force || complete;
1338
1482
 
1339
1483
  if (shouldPublishEvents) {
1340
- firstPlayablePublished = true;
1341
1484
  setState({
1342
1485
  data: playbackResponseFromChunks(manifest, chunks, {
1343
1486
  isComplete: complete,
@@ -1365,6 +1508,8 @@ function useSessionReplayPlayback(recordingId: string) {
1365
1508
  loadedBytes,
1366
1509
  unavailableChunks,
1367
1510
  }),
1511
+ // Keep the page shell mounted so the player loading bar can show
1512
+ // chunk progress; the Replayer itself still waits for isComplete.
1368
1513
  isLoading: false,
1369
1514
  error: null,
1370
1515
  }));
@@ -2008,18 +2153,33 @@ function hasPlayableReplayEvents(events: unknown[]): boolean {
2008
2153
  export function replayViewportDimensions(
2009
2154
  events: AnyReplayEvent[],
2010
2155
  ): ReplayViewportDimensions | null {
2156
+ // Latest Meta / ViewportResize for CSS fit-to-stage only. Never rewrite these
2157
+ // into the event stream — rrweb must keep Meta in sync with the FullSnapshot.
2158
+ let best: ReplayViewportDimensions | null = null;
2011
2159
  for (const event of events) {
2012
- if (event.type !== RRWEB_EVENT_TYPE.Meta) continue;
2013
- const dims = normalizeReplayDimensions(
2014
- event.data?.width,
2015
- event.data?.height,
2016
- );
2017
- if (dims) return dims;
2160
+ const dims = dimensionsFromReplayEvent(event);
2161
+ if (dims) best = dims;
2162
+ }
2163
+ return best;
2164
+ }
2165
+
2166
+ function dimensionsFromReplayEvent(
2167
+ event: AnyReplayEvent,
2168
+ ): ReplayViewportDimensions | null {
2169
+ if (event.type === RRWEB_EVENT_TYPE.Meta) {
2170
+ return normalizeReplayDimensions(event.data?.width, event.data?.height);
2171
+ }
2172
+ if (
2173
+ event.type === RRWEB_EVENT_TYPE.IncrementalSnapshot &&
2174
+ event.data?.source === INCREMENTAL_SOURCE.ViewportResize
2175
+ ) {
2176
+ return normalizeReplayDimensions(event.data?.width, event.data?.height);
2018
2177
  }
2019
2178
  return null;
2020
2179
  }
2021
2180
 
2022
- function normalizeReplayDimensions(
2181
+ /** Read raw positive finite dimensions; no aspect clamping. */
2182
+ export function normalizeReplayDimensions(
2023
2183
  width: unknown,
2024
2184
  height: unknown,
2025
2185
  ): ReplayViewportDimensions | null {
@@ -2039,30 +2199,45 @@ function normalizeReplayDimensions(
2039
2199
  };
2040
2200
  }
2041
2201
 
2042
- function applyReplayFrameDimensions(
2043
- replayer: any,
2202
+ export function clampReplayDisplayDimensions(
2044
2203
  dims: ReplayViewportDimensions | null,
2045
- ) {
2046
- const width = dims?.width ?? DEFAULT_PLAYER_WIDTH;
2047
- const height = dims?.height ?? DEFAULT_PLAYER_HEIGHT;
2048
- const wrapper = replayer?.wrapper as HTMLElement | undefined;
2049
- const iframe = replayer?.iframe as HTMLIFrameElement | undefined;
2050
- const mouseTail = replayer?.mouseTail as HTMLCanvasElement | undefined;
2051
- for (const element of [wrapper, iframe]) {
2052
- if (!element) continue;
2053
- element.style.width = `${width}px`;
2054
- element.style.height = `${height}px`;
2204
+ ): ReplayViewportDimensions | null {
2205
+ if (!dims) return null;
2206
+ const aspect = dims.width / dims.height;
2207
+ if (aspect > MAX_REPLAY_DISPLAY_ASPECT_RATIO) {
2208
+ return {
2209
+ width: Math.round(dims.height * MAX_REPLAY_DISPLAY_ASPECT_RATIO),
2210
+ height: dims.height,
2211
+ };
2055
2212
  }
2056
- for (const element of [iframe, mouseTail]) {
2057
- if (!element) continue;
2058
- element.setAttribute("width", String(width));
2059
- element.setAttribute("height", String(height));
2213
+ if (aspect < MIN_REPLAY_DISPLAY_ASPECT_RATIO) {
2214
+ return {
2215
+ width: dims.width,
2216
+ height: Math.round(dims.width / MIN_REPLAY_DISPLAY_ASPECT_RATIO),
2217
+ };
2060
2218
  }
2219
+ return dims;
2061
2220
  }
2062
2221
 
2063
- function revealReplayFrame(replayer: any) {
2064
- const iframe = replayer?.iframe as HTMLIFrameElement | undefined;
2065
- if (iframe) iframe.style.display = "inherit";
2222
+ export function filterReplayMarkers(
2223
+ markers: ReplayMarker[],
2224
+ query: string,
2225
+ ): ReplayMarker[] {
2226
+ const needle = query.trim().toLowerCase();
2227
+ if (!needle) return markers;
2228
+ return markers.filter((marker) => {
2229
+ const haystack = [
2230
+ marker.label,
2231
+ marker.detail,
2232
+ marker.kind,
2233
+ marker.severity,
2234
+ ...(marker.fields?.flatMap((field) => [field.label, field.value]) ?? []),
2235
+ ]
2236
+ .filter(Boolean)
2237
+ .join(" ")
2238
+ .toLowerCase();
2239
+ return haystack.includes(needle);
2240
+ });
2066
2241
  }
2067
2242
 
2068
2243
  function replayStartedAt(events: AnyReplayEvent[]): number {