@agent-native/core 0.98.3 → 0.98.5

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 (230) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +29 -0
  3. package/corpus/core/docs/content/durable-background-runs.mdx +37 -0
  4. package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +2 -0
  5. package/corpus/core/docs/content/locales/de-DE/tracking.mdx +2 -0
  6. package/corpus/core/docs/content/locales/es-ES/tracking.mdx +2 -0
  7. package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +4 -2
  8. package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +2 -0
  9. package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +2 -0
  10. package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +2 -0
  11. package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +2 -0
  12. package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +2 -0
  13. package/corpus/core/docs/content/locales/zh-TW/durable-background-runs.mdx +14 -0
  14. package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +2 -0
  15. package/corpus/core/docs/content/tracking.mdx +2 -0
  16. package/corpus/core/package.json +1 -1
  17. package/corpus/core/src/agent/durable-background.ts +16 -16
  18. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +25 -8
  19. package/corpus/core/src/agent/engine/anthropic-engine.ts +17 -3
  20. package/corpus/core/src/agent/engine/output-tokens.ts +6 -6
  21. package/corpus/core/src/agent/production-agent.ts +200 -27
  22. package/corpus/core/src/agent/run-manager.ts +81 -1
  23. package/corpus/core/src/agent/run-store.ts +301 -41
  24. package/corpus/core/src/client/AgentPanel.tsx +6 -2
  25. package/corpus/core/src/client/AssistantChat.tsx +364 -69
  26. package/corpus/core/src/client/agent-chat-adapter.ts +169 -1
  27. package/corpus/core/src/client/analytics.ts +12 -0
  28. package/corpus/core/src/client/chat/message-components.tsx +3 -0
  29. package/corpus/core/src/client/chat/tool-call-display.tsx +20 -36
  30. package/corpus/core/src/client/conversation/use-near-bottom-autoscroll.ts +7 -0
  31. package/corpus/core/src/client/extensions/AgentNativeExtensionFrame.tsx +2 -0
  32. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +2 -0
  33. package/corpus/core/src/client/extensions/ExtensionEditor.tsx +9 -2
  34. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +2 -0
  35. package/corpus/core/src/client/extensions/InlineExtensionFrame.tsx +2 -0
  36. package/corpus/core/src/client/extensions/portable-extension.ts +2 -0
  37. package/corpus/core/src/client/session-replay.ts +971 -53
  38. package/corpus/core/src/client/settings/DemoModeSection.tsx +3 -3
  39. package/corpus/core/src/client/sse-event-processor.ts +43 -1
  40. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +2 -2
  41. package/corpus/core/src/demo/config.ts +6 -6
  42. package/corpus/core/src/demo/fetch-interceptor.ts +34 -2
  43. package/corpus/core/src/demo/redact.ts +71 -158
  44. package/corpus/core/src/extensions/html-shell.ts +3 -0
  45. package/corpus/core/src/extensions/session-replay-iframe.ts +131 -0
  46. package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +1 -20
  47. package/corpus/core/src/session-replay-iframe-protocol.ts +58 -0
  48. package/corpus/core/src/shared/reasoning-effort.ts +35 -0
  49. package/corpus/core/src/styles/agent-conversation.css +26 -0
  50. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  51. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +67 -16
  52. package/corpus/templates/analytics/AGENTS.md +12 -0
  53. package/corpus/templates/analytics/actions/get-error-issue.ts +1 -0
  54. package/corpus/templates/analytics/actions/get-session-replay-events.ts +1 -0
  55. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +1 -0
  56. package/corpus/templates/analytics/actions/list-error-issues.ts +1 -0
  57. package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -0
  58. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +2 -1
  59. package/corpus/templates/analytics/app/global.css +6 -64
  60. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +41 -4
  61. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +143 -121
  62. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +34 -12
  63. package/corpus/templates/analytics/changelog/2026-07-12-analytics-chat-retries-before-showing-a-generic-no-data-mess.md +6 -0
  64. package/corpus/templates/analytics/changelog/2026-07-12-analytics-sessions-search-no-longer-loses-fast-keystrokes-an.md +6 -0
  65. package/corpus/templates/analytics/changelog/2026-07-12-chart-selection-respects-chat-state.md +6 -0
  66. package/corpus/templates/analytics/changelog/2026-07-12-demo-mode-anonymizes-error-reporting-emails.md +6 -0
  67. package/corpus/templates/analytics/changelog/2026-07-12-fixed-session-replays-that-appeared-ultra-wide-hid-recorded-.md +6 -0
  68. package/corpus/templates/analytics/changelog/2026-07-12-session-replays-load-faster-in-demo-mode-while-visitor-email.md +6 -0
  69. package/corpus/templates/analytics/changelog/2026-07-12-session-replays-show-iframe-content.md +6 -0
  70. package/corpus/templates/analytics/netlify.toml +0 -3
  71. package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +14 -0
  72. package/corpus/templates/analytics/server/lib/error-capture.ts +59 -2
  73. package/corpus/templates/analytics/server/lib/real-data-actions.ts +24 -0
  74. package/corpus/templates/analytics/server/lib/session-replay.ts +2 -9
  75. package/corpus/templates/analytics/server/plugins/agent-chat.ts +39 -6
  76. package/corpus/templates/assets/app/global.css +1 -1
  77. package/corpus/templates/brain/app/global.css +2 -2
  78. package/corpus/templates/brain/changelog/2026-07-12-fix-long-response-progress.md +6 -0
  79. package/corpus/templates/calendar/app/components/calendar/DeleteEventDialog.tsx +11 -2
  80. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +14 -2
  81. package/corpus/templates/calendar/changelog/2026-07-12-calendar-responses-can-now-be-saved-with-command-enter-or-ct.md +6 -0
  82. package/corpus/templates/forms/app/components/CloudUpgrade.tsx +1 -1
  83. package/corpus/templates/forms/app/components/ThemeToggle.tsx +22 -6
  84. package/corpus/templates/forms/app/components/builder/FieldPropertiesPanel.tsx +5 -5
  85. package/corpus/templates/forms/app/components/builder/FieldRenderer.tsx +3 -3
  86. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +16 -16
  87. package/corpus/templates/forms/app/global.css +110 -5
  88. package/corpus/templates/forms/app/pages/AskPage.tsx +3 -3
  89. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +134 -52
  90. package/corpus/templates/forms/app/pages/FormFillPage.tsx +44 -11
  91. package/corpus/templates/forms/app/pages/FormsListPage.tsx +42 -24
  92. package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +28 -15
  93. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +38 -15
  94. package/corpus/templates/forms/app/routes/_app.settings.tsx +1 -1
  95. package/corpus/templates/forms/changelog/2026-07-12-form-builder-surfaces-are-calmer-fields-stay-stable-on-hover.md +6 -0
  96. package/corpus/templates/forms/changelog/2026-07-12-smoother-more-tactile-controls-throughout-animated-icon-and-.md +6 -0
  97. package/corpus/templates/mail/app/components/email/EmailThread.tsx +21 -13
  98. package/corpus/templates/mail/app/components/email/email-iframe-document.ts +18 -0
  99. package/dist/agent/durable-background.d.ts +10 -10
  100. package/dist/agent/durable-background.d.ts.map +1 -1
  101. package/dist/agent/durable-background.js +16 -16
  102. package/dist/agent/durable-background.js.map +1 -1
  103. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  104. package/dist/agent/engine/ai-sdk-engine.js +19 -8
  105. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  106. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  107. package/dist/agent/engine/anthropic-engine.js +11 -3
  108. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  109. package/dist/agent/engine/output-tokens.js +6 -6
  110. package/dist/agent/engine/output-tokens.js.map +1 -1
  111. package/dist/agent/production-agent.d.ts +52 -0
  112. package/dist/agent/production-agent.d.ts.map +1 -1
  113. package/dist/agent/production-agent.js +154 -26
  114. package/dist/agent/production-agent.js.map +1 -1
  115. package/dist/agent/run-manager.d.ts +9 -0
  116. package/dist/agent/run-manager.d.ts.map +1 -1
  117. package/dist/agent/run-manager.js +71 -1
  118. package/dist/agent/run-manager.js.map +1 -1
  119. package/dist/agent/run-store.d.ts +9 -0
  120. package/dist/agent/run-store.d.ts.map +1 -1
  121. package/dist/agent/run-store.js +243 -48
  122. package/dist/agent/run-store.js.map +1 -1
  123. package/dist/client/AgentPanel.d.ts.map +1 -1
  124. package/dist/client/AgentPanel.js +6 -2
  125. package/dist/client/AgentPanel.js.map +1 -1
  126. package/dist/client/AssistantChat.d.ts +21 -0
  127. package/dist/client/AssistantChat.d.ts.map +1 -1
  128. package/dist/client/AssistantChat.js +298 -67
  129. package/dist/client/AssistantChat.js.map +1 -1
  130. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  131. package/dist/client/agent-chat-adapter.js +146 -1
  132. package/dist/client/agent-chat-adapter.js.map +1 -1
  133. package/dist/client/analytics.d.ts.map +1 -1
  134. package/dist/client/analytics.js +11 -0
  135. package/dist/client/analytics.js.map +1 -1
  136. package/dist/client/chat/message-components.d.ts.map +1 -1
  137. package/dist/client/chat/message-components.js +2 -1
  138. package/dist/client/chat/message-components.js.map +1 -1
  139. package/dist/client/chat/tool-call-display.d.ts +3 -1
  140. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  141. package/dist/client/chat/tool-call-display.js +13 -37
  142. package/dist/client/chat/tool-call-display.js.map +1 -1
  143. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts +1 -0
  144. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -1
  145. package/dist/client/conversation/use-near-bottom-autoscroll.js +6 -0
  146. package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -1
  147. package/dist/client/extensions/AgentNativeExtensionFrame.d.ts.map +1 -1
  148. package/dist/client/extensions/AgentNativeExtensionFrame.js +2 -1
  149. package/dist/client/extensions/AgentNativeExtensionFrame.js.map +1 -1
  150. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  151. package/dist/client/extensions/EmbeddedExtension.js +2 -1
  152. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  153. package/dist/client/extensions/ExtensionEditor.d.ts.map +1 -1
  154. package/dist/client/extensions/ExtensionEditor.js +5 -2
  155. package/dist/client/extensions/ExtensionEditor.js.map +1 -1
  156. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  157. package/dist/client/extensions/ExtensionViewer.js +2 -1
  158. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  159. package/dist/client/extensions/InlineExtensionFrame.d.ts.map +1 -1
  160. package/dist/client/extensions/InlineExtensionFrame.js +2 -1
  161. package/dist/client/extensions/InlineExtensionFrame.js.map +1 -1
  162. package/dist/client/extensions/portable-extension.d.ts.map +1 -1
  163. package/dist/client/extensions/portable-extension.js +2 -0
  164. package/dist/client/extensions/portable-extension.js.map +1 -1
  165. package/dist/client/session-replay.d.ts +12 -3
  166. package/dist/client/session-replay.d.ts.map +1 -1
  167. package/dist/client/session-replay.js +793 -49
  168. package/dist/client/session-replay.js.map +1 -1
  169. package/dist/client/settings/DemoModeSection.js +1 -1
  170. package/dist/client/settings/DemoModeSection.js.map +1 -1
  171. package/dist/client/sse-event-processor.d.ts +5 -0
  172. package/dist/client/sse-event-processor.d.ts.map +1 -1
  173. package/dist/client/sse-event-processor.js +37 -1
  174. package/dist/client/sse-event-processor.js.map +1 -1
  175. package/dist/demo/actions/toggle-demo-mode.js +2 -2
  176. package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
  177. package/dist/demo/config.js +6 -6
  178. package/dist/demo/config.js.map +1 -1
  179. package/dist/demo/fetch-interceptor.d.ts +1 -0
  180. package/dist/demo/fetch-interceptor.d.ts.map +1 -1
  181. package/dist/demo/fetch-interceptor.js +28 -2
  182. package/dist/demo/fetch-interceptor.js.map +1 -1
  183. package/dist/demo/redact.d.ts +14 -7
  184. package/dist/demo/redact.d.ts.map +1 -1
  185. package/dist/demo/redact.js +27 -155
  186. package/dist/demo/redact.js.map +1 -1
  187. package/dist/extensions/html-shell.d.ts.map +1 -1
  188. package/dist/extensions/html-shell.js +2 -0
  189. package/dist/extensions/html-shell.js.map +1 -1
  190. package/dist/extensions/session-replay-iframe.d.ts +10 -0
  191. package/dist/extensions/session-replay-iframe.d.ts.map +1 -0
  192. package/dist/extensions/session-replay-iframe.js +121 -0
  193. package/dist/extensions/session-replay-iframe.js.map +1 -0
  194. package/dist/notifications/routes.d.ts +1 -1
  195. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  196. package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
  197. package/dist/scripts/agent-engines/list-agent-engines.js +1 -19
  198. package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
  199. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  200. package/dist/server/transcribe-voice.d.ts +1 -1
  201. package/dist/session-replay-iframe-protocol.d.ts +36 -0
  202. package/dist/session-replay-iframe-protocol.d.ts.map +1 -0
  203. package/dist/session-replay-iframe-protocol.js +24 -0
  204. package/dist/session-replay-iframe-protocol.js.map +1 -0
  205. package/dist/shared/reasoning-effort.d.ts +11 -0
  206. package/dist/shared/reasoning-effort.d.ts.map +1 -1
  207. package/dist/shared/reasoning-effort.js +36 -0
  208. package/dist/shared/reasoning-effort.js.map +1 -1
  209. package/dist/styles/agent-conversation.css +26 -0
  210. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  211. package/docs/content/durable-background-runs.mdx +37 -0
  212. package/docs/content/locales/ar-SA/tracking.mdx +2 -0
  213. package/docs/content/locales/de-DE/tracking.mdx +2 -0
  214. package/docs/content/locales/es-ES/tracking.mdx +2 -0
  215. package/docs/content/locales/fr-FR/tracking.mdx +4 -2
  216. package/docs/content/locales/hi-IN/tracking.mdx +2 -0
  217. package/docs/content/locales/ja-JP/tracking.mdx +2 -0
  218. package/docs/content/locales/ko-KR/tracking.mdx +2 -0
  219. package/docs/content/locales/pt-BR/tracking.mdx +2 -0
  220. package/docs/content/locales/zh-CN/tracking.mdx +2 -0
  221. package/docs/content/locales/zh-TW/durable-background-runs.mdx +14 -0
  222. package/docs/content/locales/zh-TW/tracking.mdx +2 -0
  223. package/docs/content/tracking.mdx +2 -0
  224. package/package.json +2 -2
  225. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  226. package/corpus/core/src/observability/hosted-model-experiment.ts +0 -118
  227. package/dist/observability/hosted-model-experiment.d.ts +0 -39
  228. package/dist/observability/hosted-model-experiment.d.ts.map +0 -1
  229. package/dist/observability/hosted-model-experiment.js +0 -90
  230. package/dist/observability/hosted-model-experiment.js.map +0 -1
@@ -166,11 +166,6 @@ type ReplayViewportChange = ReplayViewportDimensions & {
166
166
 
167
167
  const DEFAULT_PLAYER_WIDTH = 1024;
168
168
  const DEFAULT_PLAYER_HEIGHT = 640;
169
- const MALFORMED_REPLAY_MIN_WIDTH = 4000;
170
- const MALFORMED_REPLAY_MIN_ASPECT_RATIO = 4;
171
- const MIN_REPLAY_DISPLAY_DIMENSION = 240;
172
- const RECOVERED_REPLAY_DISPLAY_ASPECT_RATIO =
173
- DEFAULT_PLAYER_WIDTH / DEFAULT_PLAYER_HEIGHT;
174
169
  const DEFAULT_SPEED = 2;
175
170
  const SPEED_OPTIONS = [0.5, 1, 2, 4, 8];
176
171
  const SKIP_STEP_MS = 5000;
@@ -186,10 +181,14 @@ const MIN_STAGE_HEIGHT_PX = 240;
186
181
  const SCRUBBER_MARKER_LIMIT = 500;
187
182
  const TIMELINE_MARKER_LIMIT = 300;
188
183
  const TIMELINE_FOLLOW_PAUSE_MS = 4000;
189
- /** Toast/snackbar noise only — keep insertStyleRules minimal like builder-internal. */
190
- const SUPPRESS_OVERLAYS_CSS = [
191
- "[data-radix-popper-content-wrapper], .Toastify, [class*='toast'], [class*='Toast'], [class*='Snackbar'] { display: none !important; }",
192
- ];
184
+ const REPLAY_CLOCK_UPDATE_INTERVAL_MS = 100;
185
+ /**
186
+ * Keep captured overlays intact. Toasts and snackbars are product feedback,
187
+ * not recorder chrome, and can be essential to understanding the session.
188
+ * The recorder does not inject notification UI into the recorded document, so
189
+ * blanket selectors (including Sonner's data attributes) are not justified.
190
+ */
191
+ export const REPLAY_OVERLAY_STYLE_RULES: string[] = [];
193
192
  type ReplayConsoleDiagnostics = ReturnType<
194
193
  typeof extractReplayDiagnostics
195
194
  >["console"];
@@ -235,11 +234,13 @@ const RRWEB_EVENT_TYPE = {
235
234
 
236
235
  const INCREMENTAL_SOURCE = {
237
236
  Mutation: 0,
237
+ MouseMove: 1,
238
238
  MouseInteraction: 2,
239
239
  Scroll: 3,
240
240
  ViewportResize: 4,
241
241
  Input: 5,
242
242
  TouchMove: 6,
243
+ Drag: 12,
243
244
  } as const;
244
245
 
245
246
  const MOUSE_INTERACTION = {
@@ -480,6 +481,7 @@ function ReplayPlayer({
480
481
  const stageRootRef = useRef<HTMLDivElement>(null);
481
482
  const replayerRef = useRef<any>(null);
482
483
  const rafRef = useRef<number | null>(null);
484
+ const lastClockUpdateAtRef = useRef<number | null>(null);
483
485
  const [status, setStatus] = useState<ReplayPlayerStatus>("idle");
484
486
  const [error, setError] = useState<string | null>(null);
485
487
  const [playing, setPlaying] = useState(false);
@@ -497,7 +499,7 @@ function ReplayPlayer({
497
499
  } | null>(null);
498
500
  const [fitScale, setFitScale] = useState(1);
499
501
  const initialDims = useMemo(
500
- () => replayInitialDisplayDimensions(events),
502
+ () => replayInitialViewportDimensions(events),
501
503
  [events],
502
504
  );
503
505
  const viewportTimeline = useMemo(
@@ -519,16 +521,14 @@ function ReplayPlayer({
519
521
  const scrubbingRef = useRef(false);
520
522
  const scrubResumePlayingRef = useRef(false);
521
523
 
522
- // IMPORTANT DO NOT REMOVE THIS LEGACY VIEWPORT CORRECTION.
523
- // Some stored sessions contain impossible 4,000–7,500px-wide Meta/resize
524
- // values even though the browser was a normal desktop viewport. Rendering
525
- // those raw values collapses the replay into a tiny horizontal ribbon. Keep
526
- // normal recordings exact—including 32:9 and mobile portrait. Recover only
527
- // widths >=4,000px with aspect >4:1 to the standard 16:10 viewport. The same
528
- // dimensions are applied to rrweb's iframe so CSS breakpoints/camera agree.
529
- const displayDims = clampReplayDisplayDimensions(streamedDims ?? initialDims);
530
- const playerWidth = displayDims?.width ?? DEFAULT_PLAYER_WIDTH;
531
- const playerHeight = displayDims?.height ?? DEFAULT_PLAYER_HEIGHT;
524
+ // Fall back to the default player size only when the viewport is unknown;
525
+ // otherwise render the raw recorded dimensions untouched. See the Replayer
526
+ // construction below for why "recovery" heuristics were removed here.
527
+ const displayDims = resolveReplayDisplayDimensions(
528
+ streamedDims ?? initialDims,
529
+ );
530
+ const playerWidth = displayDims.width;
531
+ const playerHeight = displayDims.height;
532
532
  const skipRanges = useMemo(() => buildIdleSkipRanges(events), [events]);
533
533
  const skipRangesRef = useLiveRef(skipRanges);
534
534
  const skipInactiveRef = useLiveRef(skipInactive);
@@ -624,10 +624,16 @@ function ReplayPlayer({
624
624
 
625
625
  const updateTime = useCallback(
626
626
  (next: number) => {
627
+ if (!Number.isFinite(next) || next === currentTimeRef.current) return;
628
+ // Keep the live value in sync before React commits. The animation clock
629
+ // can run again while React is still processing the previous render;
630
+ // relying on useLiveRef's effect here republishes the same value and can
631
+ // create a nested update loop in development.
632
+ currentTimeRef.current = next;
627
633
  setCurrentTime(next);
628
634
  onTimeUpdate(next);
629
635
  },
630
- [onTimeUpdate],
636
+ [currentTimeRef, onTimeUpdate],
631
637
  );
632
638
 
633
639
  const seek = useCallback(
@@ -652,18 +658,16 @@ function ReplayPlayer({
652
658
  clamped,
653
659
  );
654
660
  if (seekDims) {
655
- // rrweb 2.1 does not reliably re-emit Resize when seeking backwards.
656
- // IMPORTANT: correct both layers together. Correcting only the outer
657
- // stage or only rrweb's iframe recreates the ultra-wide/clipped bug.
658
- const correctedSeekDims = clampReplayDisplayDimensions(seekDims);
661
+ // rrweb 2.1 does not reliably re-emit its resize event when seeking
662
+ // backwards, so mirror the raw viewport-timeline lookup onto both
663
+ // rrweb's iframe and the outer stage state to keep them in sync.
659
664
  const currentDims = streamedDimsRef.current;
660
665
  if (
661
- correctedSeekDims &&
662
- (currentDims?.width !== correctedSeekDims.width ||
663
- currentDims?.height !== correctedSeekDims.height)
666
+ currentDims?.width !== seekDims.width ||
667
+ currentDims?.height !== seekDims.height
664
668
  ) {
665
- replayer.handleResize?.(correctedSeekDims);
666
- setStreamedDims(correctedSeekDims);
669
+ replayer.handleResize?.(seekDims);
670
+ setStreamedDims(seekDims);
667
671
  }
668
672
  }
669
673
  updateTime(clamped);
@@ -709,27 +713,17 @@ function ReplayPlayer({
709
713
  let localReplayer: any = null;
710
714
 
711
715
  async function loadReplay() {
712
- // Wait for the full recording before creating the Replayer. Progressive
713
- // chunk publishes used to rebuild the player on every append, which
714
- // reset the clock, disabled the scrubber, and desynced the playhead.
715
716
  const replayEvents = eventsRef.current;
717
+ // Replayer construction remains all-or-nothing. Progressive response
718
+ // publishes update loading progress only; rebuilding during download
719
+ // rewinds the player and can desync the scrubber/playhead.
716
720
  if (!response.isComplete) {
717
721
  setStatus("loading");
718
722
  setError(null);
719
723
  setPlaying(false);
720
724
  return;
721
725
  }
722
- if (replayEvents.length < 2) {
723
- throw new Error(t("sessions.noReplayEvents"));
724
- }
725
- if (
726
- !replayEvents.some(
727
- (event) => event.type === RRWEB_EVENT_TYPE.FullSnapshot,
728
- )
729
- ) {
730
- throw new Error(t("sessions.noReplayEvents"));
731
- }
732
- if (!hasPlayableReplayEvents(replayEvents)) {
726
+ if (replayEvents.length < 2 || !hasPlayableReplayEvents(replayEvents)) {
733
727
  throw new Error(t("sessions.noReplayEvents"));
734
728
  }
735
729
  setStatus("loading");
@@ -739,35 +733,30 @@ function ReplayPlayer({
739
733
  if (cancelled || !stageRootRef.current) return;
740
734
 
741
735
  stageRootRef.current.innerHTML = "";
742
- // Match builder-internal: pass events through untouched and let rrweb own
743
- // normal iframe sizing via Meta / ViewportResize. Never rewrite Meta or
744
- // force arbitrary iframe dimensions (that mismatches the FullSnapshot DOM
745
- // and blanks the stage). The one exception below mirrors the narrowly
746
- // detected legacy malformed viewport onto both the iframe and outer stage.
747
- const rawInitialDims = replayInitialViewportDimensions(replayEvents);
748
- const correctedInitialDims = replayInitialDisplayDimensions(replayEvents);
749
- setStreamedDims(correctedInitialDims);
736
+ // Viewport and pointer events must pass through to rrweb untouched.
737
+ // The 2026-07 "ultra-wide replay" bugs (e.g. a stored 1,152px-wide
738
+ // recording rendering as a 4,491px Meta width) were caused by demo
739
+ // mode's fetch redaction faking numbers >= 1000 in raw replay JSON at
740
+ // view time, not by malformed stored geometry fixed in
741
+ // packages/core/src/demo/fetch-interceptor.ts. Stored recordings were
742
+ // always sane, so never add viewport "recovery" heuristics here; they
743
+ // can only corrupt genuine recordings (e.g. a real ultrawide window).
744
+ setStreamedDims(replayInitialViewportDimensions(replayEvents));
745
+ // Keep this loosely typed: our internal AnyReplayEvent shape doesn't
746
+ // exactly match rrweb's declared eventWithTime type.
750
747
  localReplayer = new Replayer(replayEvents as any[], {
751
748
  root: stageRootRef.current,
752
749
  speed: speedRef.current,
753
750
  skipInactive: false,
754
751
  showWarning: false,
755
752
  showDebug: false,
756
- triggerFocus: false,
757
753
  mouseTail: false,
758
- insertStyleRules: SUPPRESS_OVERLAYS_CSS,
754
+ // Match stock rrweb/builder-internal focus replay. Disabling focus
755
+ // drops recorded focus-visible state and can leave menus/forms looking
756
+ // unlike the source page even when the snapshot CSS is correct.
757
+ triggerFocus: true,
758
+ insertStyleRules: REPLAY_OVERLAY_STYLE_RULES,
759
759
  });
760
- // IMPORTANT: rrweb constructs its iframe from the raw initial Meta event.
761
- // When that legacy Meta width is malformed, correcting only streamedDims
762
- // fixes the outer stage but leaves CSS breakpoints and clicks ultra-wide.
763
- // Apply the same correction to rrweb immediately, before first playback.
764
- if (
765
- correctedInitialDims &&
766
- (rawInitialDims?.width !== correctedInitialDims.width ||
767
- rawInitialDims?.height !== correctedInitialDims.height)
768
- ) {
769
- localReplayer.handleResize?.(correctedInitialDims);
770
- }
771
760
  // rrweb already sandboxes the replay document without script execution.
772
761
  // Do not mutate recorded URLs/CSS; suppress viewer-page referrer leakage
773
762
  // at the iframe boundary while retaining historical visual resources.
@@ -800,25 +789,17 @@ function ReplayPlayer({
800
789
  width: Math.round(dims.width),
801
790
  height: Math.round(dims.height),
802
791
  };
803
- const correctedDims = clampReplayDisplayDimensions(rawDims);
804
- if (!correctedDims) return;
805
- // rrweb handles the raw resize before notifying us. Override only
806
- // impossible legacy geometry so its iframe and our stage stay equal.
807
- // DO NOT clamp just one layer; that breaks responsive CSS and clicks.
792
+ // rrweb owns its iframe geometry from raw Meta/ViewportResize
793
+ // events. Only mirror the raw dims into React state here, for the
794
+ // outer stage's fit-to-container scaling.
808
795
  const currentDims = streamedDimsRef.current;
809
796
  if (
810
- currentDims?.width === correctedDims.width &&
811
- currentDims?.height === correctedDims.height
797
+ currentDims?.width === rawDims.width &&
798
+ currentDims?.height === rawDims.height
812
799
  ) {
813
800
  return;
814
801
  }
815
- if (
816
- correctedDims.width !== rawDims.width ||
817
- correctedDims.height !== rawDims.height
818
- ) {
819
- localReplayer.handleResize?.(correctedDims);
820
- }
821
- setStreamedDims(correctedDims);
802
+ setStreamedDims(rawDims);
822
803
  }
823
804
  });
824
805
  updateTime(startAt);
@@ -851,6 +832,7 @@ function ReplayPlayer({
851
832
  try {
852
833
  localReplayer?.pause?.();
853
834
  localReplayer?.destroy?.();
835
+ replayerRef.current?.pause?.();
854
836
  replayerRef.current?.destroy?.();
855
837
  } catch {
856
838
  // rrweb cleanup is best-effort across versions.
@@ -858,9 +840,6 @@ function ReplayPlayer({
858
840
  replayerRef.current = null;
859
841
  if (stageRootRef.current) stageRootRef.current.innerHTML = "";
860
842
  };
861
- // Key only on isComplete + a stable events identity. Including the events
862
- // array reference would rebuild the Replayer when the final publish()
863
- // replaces the chunks object even though the event set is unchanged.
864
843
  }, [
865
844
  currentTimeRef,
866
845
  eventsIdentity,
@@ -875,10 +854,11 @@ function ReplayPlayer({
875
854
  if (!playing || status !== "ready") {
876
855
  if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
877
856
  rafRef.current = null;
857
+ lastClockUpdateAtRef.current = null;
878
858
  return;
879
859
  }
880
860
 
881
- const tick = () => {
861
+ const tick = (frameTime: number) => {
882
862
  const replayer = replayerRef.current;
883
863
  if (replayer && !scrubbingRef.current) {
884
864
  let nextTime = Number(
@@ -901,7 +881,17 @@ function ReplayPlayer({
901
881
  }
902
882
  }
903
883
  }
904
- if (Number.isFinite(nextTime)) updateTime(nextTime);
884
+ if (
885
+ shouldPublishReplayClockUpdate(
886
+ lastClockUpdateAtRef.current,
887
+ frameTime,
888
+ currentTimeRef.current,
889
+ nextTime,
890
+ )
891
+ ) {
892
+ lastClockUpdateAtRef.current = frameTime;
893
+ updateTime(nextTime);
894
+ }
905
895
  }
906
896
  rafRef.current = requestAnimationFrame(tick);
907
897
  };
@@ -910,6 +900,7 @@ function ReplayPlayer({
910
900
  return () => {
911
901
  if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
912
902
  rafRef.current = null;
903
+ lastClockUpdateAtRef.current = null;
913
904
  };
914
905
  }, [
915
906
  currentTimeRef,
@@ -995,6 +986,10 @@ function ReplayPlayer({
995
986
  />
996
987
  <button
997
988
  type="button"
989
+ // IMPORTANT: Keep the viewer's real OS pointer visible while
990
+ // the synthetic rrweb pointer replays underneath it. Hiding
991
+ // this during playback makes the page feel broken whenever
992
+ // the viewer moves their mouse over the full-stage control.
998
993
  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"
999
994
  disabled={disabled}
1000
995
  aria-label={
@@ -2169,9 +2164,7 @@ export function buildIdleSkipRanges(events: AnyReplayEvent[]): SkipRange[] {
2169
2164
  const timestamp = Number(event.timestamp ?? 0);
2170
2165
  if (!Number.isFinite(timestamp) || timestamp <= 0) continue;
2171
2166
  lastTimestamp = Math.max(lastTimestamp, timestamp);
2172
- if (isReplayActivityEvent(event)) {
2173
- interactions.push(timestamp);
2174
- }
2167
+ interactions.push(...replayActivityTimestamps(event, timestamp));
2175
2168
  }
2176
2169
  interactions.sort((a, b) => a - b);
2177
2170
  const ranges: SkipRange[] = [];
@@ -2194,22 +2187,42 @@ export function buildIdleSkipRanges(events: AnyReplayEvent[]): SkipRange[] {
2194
2187
  return ranges;
2195
2188
  }
2196
2189
 
2197
- function isReplayActivityEvent(event: AnyReplayEvent): boolean {
2198
- if (event.type === RRWEB_EVENT_TYPE.Meta) return true;
2199
- if (event.type !== RRWEB_EVENT_TYPE.IncrementalSnapshot) return false;
2190
+ function replayActivityTimestamps(
2191
+ event: AnyReplayEvent,
2192
+ timestamp: number,
2193
+ ): number[] {
2194
+ if (event.type === RRWEB_EVENT_TYPE.Meta) return [timestamp];
2195
+ if (event.type !== RRWEB_EVENT_TYPE.IncrementalSnapshot) return [];
2200
2196
 
2201
2197
  const source = event.data?.source;
2198
+ if (
2199
+ source === INCREMENTAL_SOURCE.MouseMove ||
2200
+ source === INCREMENTAL_SOURCE.TouchMove ||
2201
+ source === INCREMENTAL_SOURCE.Drag
2202
+ ) {
2203
+ const positions = Array.isArray(event.data?.positions)
2204
+ ? event.data.positions
2205
+ : [];
2206
+ // IMPORTANT: rrweb batches pointer positions and schedules each one at
2207
+ // event.timestamp + timeOffset. Treat those exact moments as activity.
2208
+ // Ignoring the batch made Skip inactivity jump across visible movement,
2209
+ // which looked like a frozen cursor even though the recording was intact.
2210
+ return positions.flatMap((position: unknown) => {
2211
+ if (!isRecord(position)) return [];
2212
+ const timeOffset = Number(position.timeOffset ?? 0);
2213
+ return [timestamp + (Number.isFinite(timeOffset) ? timeOffset : 0)];
2214
+ });
2215
+ }
2202
2216
  if (
2203
2217
  source === INCREMENTAL_SOURCE.Input ||
2204
- source === INCREMENTAL_SOURCE.Scroll ||
2205
- source === INCREMENTAL_SOURCE.TouchMove
2218
+ source === INCREMENTAL_SOURCE.Scroll
2206
2219
  ) {
2207
- return true;
2220
+ return [timestamp];
2208
2221
  }
2209
- if (source !== INCREMENTAL_SOURCE.MouseInteraction) return false;
2222
+ if (source !== INCREMENTAL_SOURCE.MouseInteraction) return [];
2210
2223
 
2211
2224
  const interactionType = event.data?.type;
2212
- return (
2225
+ return [
2213
2226
  interactionType === MOUSE_INTERACTION.MouseUp ||
2214
2227
  interactionType === MOUSE_INTERACTION.MouseDown ||
2215
2228
  interactionType === MOUSE_INTERACTION.Click ||
@@ -2218,7 +2231,9 @@ function isReplayActivityEvent(event: AnyReplayEvent): boolean {
2218
2231
  interactionType === MOUSE_INTERACTION.Focus ||
2219
2232
  interactionType === MOUSE_INTERACTION.TouchStart ||
2220
2233
  interactionType === MOUSE_INTERACTION.TouchEnd
2221
- );
2234
+ ? timestamp
2235
+ : null,
2236
+ ].filter((value): value is number => value !== null);
2222
2237
  }
2223
2238
 
2224
2239
  function pushIdleRange(
@@ -2291,14 +2306,6 @@ export function replayInitialViewportDimensions(
2291
2306
  return null;
2292
2307
  }
2293
2308
 
2294
- export function replayInitialDisplayDimensions(
2295
- events: AnyReplayEvent[],
2296
- ): ReplayViewportDimensions | null {
2297
- // Keep the initial outer stage and rrweb iframe on one corrected camera.
2298
- // DO NOT use the raw initial Meta dimensions for only one of those layers.
2299
- return clampReplayDisplayDimensions(replayInitialViewportDimensions(events));
2300
- }
2301
-
2302
2309
  export function buildReplayViewportTimeline(
2303
2310
  events: AnyReplayEvent[],
2304
2311
  ): ReplayViewportChange[] {
@@ -2384,27 +2391,28 @@ export function normalizeReplayDimensions(
2384
2391
  };
2385
2392
  }
2386
2393
 
2387
- export function clampReplayDisplayDimensions(
2394
+ /**
2395
+ * Fall back to the default player size only when the viewport is entirely
2396
+ * unknown (no Meta/ViewportResize event yet). This is not a "recovery"
2397
+ * heuristic for real recorded geometry — it never rewrites valid dims, no
2398
+ * matter how wide, narrow, or unusual the aspect ratio. See the Replayer
2399
+ * construction in ReplayPlayer for why viewport "correction" was removed:
2400
+ * the 2026-07 ultra-wide replay bugs were caused by demo mode's fetch
2401
+ * redaction faking numbers at view time, not by malformed stored geometry.
2402
+ */
2403
+ export function resolveReplayDisplayDimensions(
2388
2404
  dims: ReplayViewportDimensions | null,
2389
- ): ReplayViewportDimensions | null {
2390
- if (!dims) return null;
2405
+ ): ReplayViewportDimensions {
2391
2406
  if (
2392
- dims.width < MIN_REPLAY_DISPLAY_DIMENSION ||
2393
- dims.height < MIN_REPLAY_DISPLAY_DIMENSION
2407
+ dims &&
2408
+ Number.isFinite(dims.width) &&
2409
+ Number.isFinite(dims.height) &&
2410
+ dims.width > 0 &&
2411
+ dims.height > 0
2394
2412
  ) {
2395
- return { width: DEFAULT_PLAYER_WIDTH, height: DEFAULT_PLAYER_HEIGHT };
2413
+ return dims;
2396
2414
  }
2397
- const aspect = dims.width / dims.height;
2398
- if (
2399
- dims.width >= MALFORMED_REPLAY_MIN_WIDTH &&
2400
- aspect > MALFORMED_REPLAY_MIN_ASPECT_RATIO
2401
- ) {
2402
- return {
2403
- width: Math.round(dims.height * RECOVERED_REPLAY_DISPLAY_ASPECT_RATIO),
2404
- height: dims.height,
2405
- };
2406
- }
2407
- return dims;
2415
+ return { width: DEFAULT_PLAYER_WIDTH, height: DEFAULT_PLAYER_HEIGHT };
2408
2416
  }
2409
2417
 
2410
2418
  export function filterReplayMarkers(
@@ -2563,6 +2571,20 @@ function clamp(value: number, min: number, max: number): number {
2563
2571
  return Math.min(max, Math.max(min, value));
2564
2572
  }
2565
2573
 
2574
+ export function shouldPublishReplayClockUpdate(
2575
+ lastUpdateAt: number | null,
2576
+ frameTime: number,
2577
+ currentTime: number,
2578
+ nextTime: number,
2579
+ ): boolean {
2580
+ if (!Number.isFinite(frameTime) || !Number.isFinite(nextTime)) return false;
2581
+ if (nextTime === currentTime) return false;
2582
+ return (
2583
+ lastUpdateAt == null ||
2584
+ frameTime - lastUpdateAt >= REPLAY_CLOCK_UPDATE_INTERVAL_MS
2585
+ );
2586
+ }
2587
+
2566
2588
  function isRecord(value: unknown): value is AnyRecord {
2567
2589
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
2568
2590
  }
@@ -21,7 +21,7 @@ import {
21
21
  IconSettings,
22
22
  } from "@tabler/icons-react";
23
23
  import { useQuery } from "@tanstack/react-query";
24
- import { useMemo, useState } from "react";
24
+ import { useCallback, useEffect, useMemo, useState } from "react";
25
25
  import { Link, useNavigate, useSearchParams } from "react-router";
26
26
  import { toast } from "sonner";
27
27
 
@@ -167,6 +167,7 @@ type SessionRecordingIdentity = Pick<
167
167
  type SessionRecordingDevice = Pick<SessionRecordingSummary, "metadata">;
168
168
 
169
169
  const RANGE_OPTIONS: ReplayRange[] = ["24h", "7d", "30d", "90d", "all"];
170
+ const SESSION_QUERY_DEBOUNCE_MS = 250;
170
171
 
171
172
  export default function SessionsPage() {
172
173
  const t = useT();
@@ -175,8 +176,38 @@ export default function SessionsPage() {
175
176
  const range = readRange(searchParams.get("range"));
176
177
  const app = searchParams.get("app") ?? "";
177
178
  const query = searchParams.get("q") ?? "";
179
+ const [queryInput, setQueryInput] = useState(query);
178
180
  const from = useMemo(() => rangeToFrom(range), [range]);
179
181
 
182
+ useEffect(() => {
183
+ setQueryInput(query);
184
+ }, [query]);
185
+
186
+ const updateFilter = useCallback(
187
+ (key: string, value: string) => {
188
+ setSearchParams(
189
+ (current) => {
190
+ const next = new URLSearchParams(current);
191
+ const emptyDefault =
192
+ (key === "range" && value === "30d") || value.trim() === "";
193
+ if (emptyDefault) next.delete(key);
194
+ else next.set(key, value);
195
+ return next;
196
+ },
197
+ { replace: true },
198
+ );
199
+ },
200
+ [setSearchParams],
201
+ );
202
+
203
+ useEffect(() => {
204
+ if (queryInput === query) return;
205
+ const timeout = window.setTimeout(() => {
206
+ updateFilter("q", queryInput);
207
+ }, SESSION_QUERY_DEBOUNCE_MS);
208
+ return () => window.clearTimeout(timeout);
209
+ }, [query, queryInput, updateFilter]);
210
+
180
211
  const { data, isLoading, isFetching, refetch, error } = useActionQuery<
181
212
  SessionRecordingSummary[]
182
213
  >(
@@ -192,15 +223,6 @@ export default function SessionsPage() {
192
223
 
193
224
  const recordings = data ?? [];
194
225
 
195
- function updateFilter(key: string, value: string) {
196
- const next = new URLSearchParams(searchParams);
197
- const emptyDefault =
198
- (key === "range" && value === "30d") || value.trim() === "";
199
- if (emptyDefault) next.delete(key);
200
- else next.set(key, value);
201
- setSearchParams(next, { replace: true });
202
- }
203
-
204
226
  return (
205
227
  <div className="analytics-sessions-page mx-auto flex w-full max-w-7xl flex-col gap-4 px-4 py-5">
206
228
  <Card>
@@ -213,8 +235,8 @@ export default function SessionsPage() {
213
235
  <div className="analytics-sessions-filter-search relative min-w-0">
214
236
  <IconSearch className="pointer-events-none absolute start-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
215
237
  <Input
216
- value={query}
217
- onChange={(event) => updateFilter("q", event.target.value)}
238
+ value={queryInput}
239
+ onChange={(event) => setQueryInput(event.target.value)}
218
240
  placeholder={t("sessions.searchPlaceholder")}
219
241
  className="h-9 ps-9"
220
242
  />
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-12
4
+ ---
5
+
6
+ Analytics chat retries before showing a generic no-data message
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-12
4
+ ---
5
+
6
+ Analytics sessions search no longer loses fast keystrokes, and agent incident lookups no longer stall after background dispatch failures.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-12
4
+ ---
5
+
6
+ Dashboard charts only select for chat when the chat sidebar is already open.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: security
3
+ date: 2026-07-12
4
+ ---
5
+
6
+ Demo mode now reliably anonymizes email addresses throughout error reports.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-12
4
+ ---
5
+
6
+ Fixed session replays that appeared ultra-wide, hid recorded menus, or skipped captured cursor movement.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-12
4
+ ---
5
+
6
+ Session replays load faster in demo mode while visitor emails stay anonymized.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-12
4
+ ---
5
+
6
+ Session replays now show activity inside extensions and email content.
@@ -9,14 +9,11 @@ GA_MEASUREMENT_ID = "G-ESF7FYXGN9"
9
9
  NITRO_PRESET = "netlify"
10
10
  NPM_CONFIG_PRODUCTION = "false"
11
11
  AGENT_PROD_CODE_EXECUTION = "sandboxed"
12
- AGENT_CHAT_DURABLE_BACKGROUND = "true"
13
12
 
14
13
  # A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
15
14
  # Hosted agent chat uses a soft timeout before this function timeout so the
16
15
  # hidden auto-continuation path can hand off before Netlify kills the function.
17
16
  # If AGENT_RUN_SOFT_TIMEOUT_MS is customized, keep it below this timeout.
18
- # Agent chat runs are dispatched to the emitted Netlify background function;
19
- # the normal server timeout still protects non-background HTTP handlers.
20
17
  [functions."*"]
21
18
  timeout = 75
22
19
 
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The deliberately small authenticated MCP surface for Analytics.
3
+ *
4
+ * Keep this list read-only and incident-focused. In particular, do not add
5
+ * replay-event/blob actions or dashboard/data mutation actions here.
6
+ */
7
+ export const ANALYTICS_CONNECTOR_CATALOG = [
8
+ "list-session-recordings",
9
+ "get-session-replay-summary",
10
+ "get-session-replay-events",
11
+ "query-agent-native-analytics",
12
+ "list-error-issues",
13
+ "get-error-issue",
14
+ ] as const;