@agent-native/core 0.98.3 → 0.98.4

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 (44) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/analytics.ts +12 -0
  5. package/corpus/core/src/client/session-replay.ts +612 -26
  6. package/corpus/core/src/client/settings/DemoModeSection.tsx +3 -3
  7. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +2 -2
  8. package/corpus/core/src/demo/fetch-interceptor.ts +25 -2
  9. package/corpus/core/src/demo/redact.ts +58 -9
  10. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +46 -4
  11. package/corpus/templates/analytics/app/global.css +4 -62
  12. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +225 -52
  13. package/corpus/templates/analytics/changelog/2026-07-12-fixed-session-replays-that-appeared-ultra-wide-hid-recorded-.md +6 -0
  14. package/corpus/templates/analytics/changelog/2026-07-12-session-replays-load-faster-in-demo-mode-while-visitor-email.md +6 -0
  15. package/corpus/templates/calendar/app/components/calendar/DeleteEventDialog.tsx +11 -2
  16. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +14 -2
  17. package/corpus/templates/calendar/changelog/2026-07-12-calendar-responses-can-now-be-saved-with-command-enter-or-ct.md +6 -0
  18. package/dist/client/analytics.d.ts.map +1 -1
  19. package/dist/client/analytics.js +11 -0
  20. package/dist/client/analytics.js.map +1 -1
  21. package/dist/client/session-replay.d.ts +12 -3
  22. package/dist/client/session-replay.d.ts.map +1 -1
  23. package/dist/client/session-replay.js +488 -23
  24. package/dist/client/session-replay.js.map +1 -1
  25. package/dist/client/settings/DemoModeSection.js +1 -1
  26. package/dist/client/settings/DemoModeSection.js.map +1 -1
  27. package/dist/collab/routes.d.ts +1 -1
  28. package/dist/collab/struct-routes.d.ts +1 -1
  29. package/dist/demo/actions/toggle-demo-mode.js +2 -2
  30. package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
  31. package/dist/demo/fetch-interceptor.d.ts +1 -0
  32. package/dist/demo/fetch-interceptor.d.ts.map +1 -1
  33. package/dist/demo/fetch-interceptor.js +19 -2
  34. package/dist/demo/fetch-interceptor.js.map +1 -1
  35. package/dist/demo/redact.d.ts +7 -0
  36. package/dist/demo/redact.d.ts.map +1 -1
  37. package/dist/demo/redact.js +19 -10
  38. package/dist/demo/redact.js.map +1 -1
  39. package/dist/notifications/routes.d.ts +1 -1
  40. package/dist/progress/routes.d.ts +1 -1
  41. package/dist/resources/handlers.d.ts +3 -3
  42. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  43. package/dist/server/transcribe-voice.d.ts +1 -1
  44. package/package.json +1 -1
@@ -168,6 +168,11 @@ const DEFAULT_PLAYER_WIDTH = 1024;
168
168
  const DEFAULT_PLAYER_HEIGHT = 640;
169
169
  const MALFORMED_REPLAY_MIN_WIDTH = 4000;
170
170
  const MALFORMED_REPLAY_MIN_ASPECT_RATIO = 4;
171
+ // Evidence-backed exception for the one pre-fix viewport shape observed in a
172
+ // stored recording. Do not broaden this to an aspect-ratio range: valid
173
+ // 3440x900 ultrawide browser viewports occupy the same range.
174
+ const LEGACY_MALFORMED_REPLAY_WIDTH = 3189;
175
+ const LEGACY_MALFORMED_REPLAY_HEIGHT = 885;
171
176
  const MIN_REPLAY_DISPLAY_DIMENSION = 240;
172
177
  const RECOVERED_REPLAY_DISPLAY_ASPECT_RATIO =
173
178
  DEFAULT_PLAYER_WIDTH / DEFAULT_PLAYER_HEIGHT;
@@ -186,10 +191,14 @@ const MIN_STAGE_HEIGHT_PX = 240;
186
191
  const SCRUBBER_MARKER_LIMIT = 500;
187
192
  const TIMELINE_MARKER_LIMIT = 300;
188
193
  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
- ];
194
+ const REPLAY_CLOCK_UPDATE_INTERVAL_MS = 100;
195
+ /**
196
+ * Keep captured overlays intact. Toasts and snackbars are product feedback,
197
+ * not recorder chrome, and can be essential to understanding the session.
198
+ * The recorder does not inject notification UI into the recorded document, so
199
+ * blanket selectors (including Sonner's data attributes) are not justified.
200
+ */
201
+ export const REPLAY_OVERLAY_STYLE_RULES: string[] = [];
193
202
  type ReplayConsoleDiagnostics = ReturnType<
194
203
  typeof extractReplayDiagnostics
195
204
  >["console"];
@@ -235,11 +244,13 @@ const RRWEB_EVENT_TYPE = {
235
244
 
236
245
  const INCREMENTAL_SOURCE = {
237
246
  Mutation: 0,
247
+ MouseMove: 1,
238
248
  MouseInteraction: 2,
239
249
  Scroll: 3,
240
250
  ViewportResize: 4,
241
251
  Input: 5,
242
252
  TouchMove: 6,
253
+ Drag: 12,
243
254
  } as const;
244
255
 
245
256
  const MOUSE_INTERACTION = {
@@ -480,6 +491,7 @@ function ReplayPlayer({
480
491
  const stageRootRef = useRef<HTMLDivElement>(null);
481
492
  const replayerRef = useRef<any>(null);
482
493
  const rafRef = useRef<number | null>(null);
494
+ const lastClockUpdateAtRef = useRef<number | null>(null);
483
495
  const [status, setStatus] = useState<ReplayPlayerStatus>("idle");
484
496
  const [error, setError] = useState<string | null>(null);
485
497
  const [playing, setPlaying] = useState(false);
@@ -520,12 +532,13 @@ function ReplayPlayer({
520
532
  const scrubResumePlayingRef = useRef(false);
521
533
 
522
534
  // IMPORTANT — DO NOT REMOVE THIS LEGACY VIEWPORT CORRECTION.
523
- // Some stored sessions contain impossible 4,000–7,500px-wide Meta/resize
535
+ // Some stored sessions contain impossible 3,000–7,500px-wide Meta/resize
524
536
  // values even though the browser was a normal desktop viewport. Rendering
525
537
  // those raw values collapses the replay into a tiny horizontal ribbon. Keep
526
538
  // 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.
539
+ // widths >=4,000px with aspect >4:1, plus the exact known 3,189x885 legacy
540
+ // shape, to the standard 16:10 viewport. The same dimensions are applied to
541
+ // rrweb's iframe so CSS breakpoints/camera agree.
529
542
  const displayDims = clampReplayDisplayDimensions(streamedDims ?? initialDims);
530
543
  const playerWidth = displayDims?.width ?? DEFAULT_PLAYER_WIDTH;
531
544
  const playerHeight = displayDims?.height ?? DEFAULT_PLAYER_HEIGHT;
@@ -624,10 +637,16 @@ function ReplayPlayer({
624
637
 
625
638
  const updateTime = useCallback(
626
639
  (next: number) => {
640
+ if (!Number.isFinite(next) || next === currentTimeRef.current) return;
641
+ // Keep the live value in sync before React commits. The animation clock
642
+ // can run again while React is still processing the previous render;
643
+ // relying on useLiveRef's effect here republishes the same value and can
644
+ // create a nested update loop in development.
645
+ currentTimeRef.current = next;
627
646
  setCurrentTime(next);
628
647
  onTimeUpdate(next);
629
648
  },
630
- [onTimeUpdate],
649
+ [currentTimeRef, onTimeUpdate],
631
650
  );
632
651
 
633
652
  const seek = useCallback(
@@ -709,27 +728,17 @@ function ReplayPlayer({
709
728
  let localReplayer: any = null;
710
729
 
711
730
  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
731
  const replayEvents = eventsRef.current;
732
+ // Replayer construction remains all-or-nothing. Progressive response
733
+ // publishes update loading progress only; rebuilding during download
734
+ // rewinds the player and can desync the scrubber/playhead.
716
735
  if (!response.isComplete) {
717
736
  setStatus("loading");
718
737
  setError(null);
719
738
  setPlaying(false);
720
739
  return;
721
740
  }
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)) {
741
+ if (replayEvents.length < 2 || !hasPlayableReplayEvents(replayEvents)) {
733
742
  throw new Error(t("sessions.noReplayEvents"));
734
743
  }
735
744
  setStatus("loading");
@@ -747,15 +756,27 @@ function ReplayPlayer({
747
756
  const rawInitialDims = replayInitialViewportDimensions(replayEvents);
748
757
  const correctedInitialDims = replayInitialDisplayDimensions(replayEvents);
749
758
  setStreamedDims(correctedInitialDims);
750
- localReplayer = new Replayer(replayEvents as any[], {
759
+ // The legacy malformed-width recordings need one paired exception to
760
+ // raw-event pass-through: pointer coordinates were captured in the same
761
+ // impossible viewport coordinate space. Correcting only the iframe makes
762
+ // rrweb cast clicks thousands of pixels off-stage. Project pointer data
763
+ // with each malformed Meta/resize while retaining every other raw field.
764
+ const playerEvents = projectReplayPointerCoordinates(replayEvents);
765
+ // Keep this loosely typed because the legacy viewport recovery below
766
+ // intentionally calls rrweb's runtime handleResize hook, which its
767
+ // TypeScript declaration marks private.
768
+ localReplayer = new Replayer(playerEvents as any[], {
751
769
  root: stageRootRef.current,
752
770
  speed: speedRef.current,
753
771
  skipInactive: false,
754
772
  showWarning: false,
755
773
  showDebug: false,
756
- triggerFocus: false,
757
774
  mouseTail: false,
758
- insertStyleRules: SUPPRESS_OVERLAYS_CSS,
775
+ // Match stock rrweb/builder-internal focus replay. Disabling focus
776
+ // drops recorded focus-visible state and can leave menus/forms looking
777
+ // unlike the source page even when the snapshot CSS is correct.
778
+ triggerFocus: true,
779
+ insertStyleRules: REPLAY_OVERLAY_STYLE_RULES,
759
780
  });
760
781
  // IMPORTANT: rrweb constructs its iframe from the raw initial Meta event.
761
782
  // When that legacy Meta width is malformed, correcting only streamedDims
@@ -766,7 +787,7 @@ function ReplayPlayer({
766
787
  (rawInitialDims?.width !== correctedInitialDims.width ||
767
788
  rawInitialDims?.height !== correctedInitialDims.height)
768
789
  ) {
769
- localReplayer.handleResize?.(correctedInitialDims);
790
+ (localReplayer as any).handleResize?.(correctedInitialDims);
770
791
  }
771
792
  // rrweb already sandboxes the replay document without script execution.
772
793
  // Do not mutate recorded URLs/CSS; suppress viewer-page referrer leakage
@@ -805,6 +826,17 @@ function ReplayPlayer({
805
826
  // rrweb handles the raw resize before notifying us. Override only
806
827
  // impossible legacy geometry so its iframe and our stage stay equal.
807
828
  // DO NOT clamp just one layer; that breaks responsive CSS and clicks.
829
+ if (
830
+ correctedDims.width !== rawDims.width ||
831
+ correctedDims.height !== rawDims.height
832
+ ) {
833
+ // rrweb applies the raw Meta/ViewportResize to its iframe before
834
+ // emitting this callback. Re-apply the correction every time the
835
+ // raw event is malformed, even when React already stores the same
836
+ // corrected dimensions. Returning early here recreates the exact
837
+ // bug where the outer stage is normal but the iframe is ultra-wide.
838
+ (localReplayer as any).handleResize?.(correctedDims);
839
+ }
808
840
  const currentDims = streamedDimsRef.current;
809
841
  if (
810
842
  currentDims?.width === correctedDims.width &&
@@ -812,12 +844,6 @@ function ReplayPlayer({
812
844
  ) {
813
845
  return;
814
846
  }
815
- if (
816
- correctedDims.width !== rawDims.width ||
817
- correctedDims.height !== rawDims.height
818
- ) {
819
- localReplayer.handleResize?.(correctedDims);
820
- }
821
847
  setStreamedDims(correctedDims);
822
848
  }
823
849
  });
@@ -851,6 +877,7 @@ function ReplayPlayer({
851
877
  try {
852
878
  localReplayer?.pause?.();
853
879
  localReplayer?.destroy?.();
880
+ replayerRef.current?.pause?.();
854
881
  replayerRef.current?.destroy?.();
855
882
  } catch {
856
883
  // rrweb cleanup is best-effort across versions.
@@ -858,9 +885,6 @@ function ReplayPlayer({
858
885
  replayerRef.current = null;
859
886
  if (stageRootRef.current) stageRootRef.current.innerHTML = "";
860
887
  };
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
888
  }, [
865
889
  currentTimeRef,
866
890
  eventsIdentity,
@@ -875,10 +899,11 @@ function ReplayPlayer({
875
899
  if (!playing || status !== "ready") {
876
900
  if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
877
901
  rafRef.current = null;
902
+ lastClockUpdateAtRef.current = null;
878
903
  return;
879
904
  }
880
905
 
881
- const tick = () => {
906
+ const tick = (frameTime: number) => {
882
907
  const replayer = replayerRef.current;
883
908
  if (replayer && !scrubbingRef.current) {
884
909
  let nextTime = Number(
@@ -901,7 +926,17 @@ function ReplayPlayer({
901
926
  }
902
927
  }
903
928
  }
904
- if (Number.isFinite(nextTime)) updateTime(nextTime);
929
+ if (
930
+ shouldPublishReplayClockUpdate(
931
+ lastClockUpdateAtRef.current,
932
+ frameTime,
933
+ currentTimeRef.current,
934
+ nextTime,
935
+ )
936
+ ) {
937
+ lastClockUpdateAtRef.current = frameTime;
938
+ updateTime(nextTime);
939
+ }
905
940
  }
906
941
  rafRef.current = requestAnimationFrame(tick);
907
942
  };
@@ -910,6 +945,7 @@ function ReplayPlayer({
910
945
  return () => {
911
946
  if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
912
947
  rafRef.current = null;
948
+ lastClockUpdateAtRef.current = null;
913
949
  };
914
950
  }, [
915
951
  currentTimeRef,
@@ -995,6 +1031,10 @@ function ReplayPlayer({
995
1031
  />
996
1032
  <button
997
1033
  type="button"
1034
+ // IMPORTANT: Keep the viewer's real OS pointer visible while
1035
+ // the synthetic rrweb pointer replays underneath it. Hiding
1036
+ // this during playback makes the page feel broken whenever
1037
+ // the viewer moves their mouse over the full-stage control.
998
1038
  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
1039
  disabled={disabled}
1000
1040
  aria-label={
@@ -1954,6 +1994,102 @@ export function normalizeReplayEvents(events: unknown[]): AnyReplayEvent[] {
1954
1994
  .sort((a, b) => Number(a.timestamp ?? 0) - Number(b.timestamp ?? 0));
1955
1995
  }
1956
1996
 
1997
+ /**
1998
+ * Pair pointer coordinates with the narrowly gated legacy viewport recovery.
1999
+ *
2000
+ * IMPORTANT: do not apply this to normal recordings and do not rewrite the
2001
+ * Meta/resize events themselves. rrweb must still rebuild the captured DOM
2002
+ * from raw events; only pointer coordinates need projection into the corrected
2003
+ * iframe camera. Returning original references for untouched events keeps this
2004
+ * compatible with stock rrweb and avoids cloning large snapshots.
2005
+ */
2006
+ export function projectReplayPointerCoordinates(
2007
+ events: AnyReplayEvent[],
2008
+ ): AnyReplayEvent[] {
2009
+ let rawDims: ReplayViewportDimensions | null = null;
2010
+ let displayDims: ReplayViewportDimensions | null = null;
2011
+
2012
+ return events.map((event) => {
2013
+ const nextDims = dimensionsFromReplayEvent(event);
2014
+ if (nextDims) {
2015
+ rawDims = nextDims;
2016
+ displayDims = clampReplayDisplayDimensions(nextDims);
2017
+ return event;
2018
+ }
2019
+ if (
2020
+ !rawDims ||
2021
+ !displayDims ||
2022
+ (rawDims.width === displayDims.width &&
2023
+ rawDims.height === displayDims.height) ||
2024
+ event.type !== RRWEB_EVENT_TYPE.IncrementalSnapshot
2025
+ ) {
2026
+ return event;
2027
+ }
2028
+
2029
+ const source = event.data?.source;
2030
+ if (source === INCREMENTAL_SOURCE.MouseInteraction) {
2031
+ const data = projectReplayPointerData(event.data, rawDims, displayDims);
2032
+ return data === event.data ? event : { ...event, data };
2033
+ }
2034
+ if (
2035
+ source !== INCREMENTAL_SOURCE.MouseMove &&
2036
+ source !== INCREMENTAL_SOURCE.TouchMove &&
2037
+ source !== INCREMENTAL_SOURCE.Drag
2038
+ ) {
2039
+ return event;
2040
+ }
2041
+ const positions = Array.isArray(event.data?.positions)
2042
+ ? event.data.positions
2043
+ : [];
2044
+ let changed = false;
2045
+ const projectedPositions = positions.map((position: unknown) => {
2046
+ if (!isRecord(position)) return position;
2047
+ const projected = projectReplayPointerData(
2048
+ position,
2049
+ rawDims!,
2050
+ displayDims!,
2051
+ );
2052
+ if (projected !== position) changed = true;
2053
+ return projected;
2054
+ });
2055
+ return changed
2056
+ ? { ...event, data: { ...event.data, positions: projectedPositions } }
2057
+ : event;
2058
+ });
2059
+ }
2060
+
2061
+ function projectReplayPointerData(
2062
+ data: AnyReplayEvent,
2063
+ rawDims: ReplayViewportDimensions,
2064
+ displayDims: ReplayViewportDimensions,
2065
+ ): AnyReplayEvent {
2066
+ const projectedX = projectReplayCoordinate(
2067
+ data.x,
2068
+ displayDims.width / rawDims.width,
2069
+ displayDims.width,
2070
+ );
2071
+ const projectedY = projectReplayCoordinate(
2072
+ data.y,
2073
+ displayDims.height / rawDims.height,
2074
+ displayDims.height,
2075
+ );
2076
+ if (projectedX === data.x && projectedY === data.y) return data;
2077
+ return {
2078
+ ...data,
2079
+ ...(projectedX !== undefined ? { x: projectedX } : {}),
2080
+ ...(projectedY !== undefined ? { y: projectedY } : {}),
2081
+ };
2082
+ }
2083
+
2084
+ function projectReplayCoordinate(
2085
+ value: unknown,
2086
+ scale: number,
2087
+ maximum: number,
2088
+ ): number | undefined {
2089
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
2090
+ return clamp(value * scale, 0, Math.max(0, maximum - 1));
2091
+ }
2092
+
1957
2093
  export function buildReplayMarkers(events: AnyReplayEvent[]): ReplayMarker[] {
1958
2094
  const startedAt = replayStartedAt(events);
1959
2095
  const markers: ReplayMarker[] = [];
@@ -2169,9 +2305,7 @@ export function buildIdleSkipRanges(events: AnyReplayEvent[]): SkipRange[] {
2169
2305
  const timestamp = Number(event.timestamp ?? 0);
2170
2306
  if (!Number.isFinite(timestamp) || timestamp <= 0) continue;
2171
2307
  lastTimestamp = Math.max(lastTimestamp, timestamp);
2172
- if (isReplayActivityEvent(event)) {
2173
- interactions.push(timestamp);
2174
- }
2308
+ interactions.push(...replayActivityTimestamps(event, timestamp));
2175
2309
  }
2176
2310
  interactions.sort((a, b) => a - b);
2177
2311
  const ranges: SkipRange[] = [];
@@ -2194,22 +2328,42 @@ export function buildIdleSkipRanges(events: AnyReplayEvent[]): SkipRange[] {
2194
2328
  return ranges;
2195
2329
  }
2196
2330
 
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;
2331
+ function replayActivityTimestamps(
2332
+ event: AnyReplayEvent,
2333
+ timestamp: number,
2334
+ ): number[] {
2335
+ if (event.type === RRWEB_EVENT_TYPE.Meta) return [timestamp];
2336
+ if (event.type !== RRWEB_EVENT_TYPE.IncrementalSnapshot) return [];
2200
2337
 
2201
2338
  const source = event.data?.source;
2339
+ if (
2340
+ source === INCREMENTAL_SOURCE.MouseMove ||
2341
+ source === INCREMENTAL_SOURCE.TouchMove ||
2342
+ source === INCREMENTAL_SOURCE.Drag
2343
+ ) {
2344
+ const positions = Array.isArray(event.data?.positions)
2345
+ ? event.data.positions
2346
+ : [];
2347
+ // IMPORTANT: rrweb batches pointer positions and schedules each one at
2348
+ // event.timestamp + timeOffset. Treat those exact moments as activity.
2349
+ // Ignoring the batch made Skip inactivity jump across visible movement,
2350
+ // which looked like a frozen cursor even though the recording was intact.
2351
+ return positions.flatMap((position: unknown) => {
2352
+ if (!isRecord(position)) return [];
2353
+ const timeOffset = Number(position.timeOffset ?? 0);
2354
+ return [timestamp + (Number.isFinite(timeOffset) ? timeOffset : 0)];
2355
+ });
2356
+ }
2202
2357
  if (
2203
2358
  source === INCREMENTAL_SOURCE.Input ||
2204
- source === INCREMENTAL_SOURCE.Scroll ||
2205
- source === INCREMENTAL_SOURCE.TouchMove
2359
+ source === INCREMENTAL_SOURCE.Scroll
2206
2360
  ) {
2207
- return true;
2361
+ return [timestamp];
2208
2362
  }
2209
- if (source !== INCREMENTAL_SOURCE.MouseInteraction) return false;
2363
+ if (source !== INCREMENTAL_SOURCE.MouseInteraction) return [];
2210
2364
 
2211
2365
  const interactionType = event.data?.type;
2212
- return (
2366
+ return [
2213
2367
  interactionType === MOUSE_INTERACTION.MouseUp ||
2214
2368
  interactionType === MOUSE_INTERACTION.MouseDown ||
2215
2369
  interactionType === MOUSE_INTERACTION.Click ||
@@ -2218,7 +2372,9 @@ function isReplayActivityEvent(event: AnyReplayEvent): boolean {
2218
2372
  interactionType === MOUSE_INTERACTION.Focus ||
2219
2373
  interactionType === MOUSE_INTERACTION.TouchStart ||
2220
2374
  interactionType === MOUSE_INTERACTION.TouchEnd
2221
- );
2375
+ ? timestamp
2376
+ : null,
2377
+ ].filter((value): value is number => value !== null);
2222
2378
  }
2223
2379
 
2224
2380
  function pushIdleRange(
@@ -2395,10 +2551,13 @@ export function clampReplayDisplayDimensions(
2395
2551
  return { width: DEFAULT_PLAYER_WIDTH, height: DEFAULT_PLAYER_HEIGHT };
2396
2552
  }
2397
2553
  const aspect = dims.width / dims.height;
2398
- if (
2554
+ const hasMalformedWideGeometry =
2399
2555
  dims.width >= MALFORMED_REPLAY_MIN_WIDTH &&
2400
- aspect > MALFORMED_REPLAY_MIN_ASPECT_RATIO
2401
- ) {
2556
+ aspect > MALFORMED_REPLAY_MIN_ASPECT_RATIO;
2557
+ const hasLegacyMalformedWideGeometry =
2558
+ dims.width === LEGACY_MALFORMED_REPLAY_WIDTH &&
2559
+ dims.height === LEGACY_MALFORMED_REPLAY_HEIGHT;
2560
+ if (hasMalformedWideGeometry || hasLegacyMalformedWideGeometry) {
2402
2561
  return {
2403
2562
  width: Math.round(dims.height * RECOVERED_REPLAY_DISPLAY_ASPECT_RATIO),
2404
2563
  height: dims.height,
@@ -2563,6 +2722,20 @@ function clamp(value: number, min: number, max: number): number {
2563
2722
  return Math.min(max, Math.max(min, value));
2564
2723
  }
2565
2724
 
2725
+ export function shouldPublishReplayClockUpdate(
2726
+ lastUpdateAt: number | null,
2727
+ frameTime: number,
2728
+ currentTime: number,
2729
+ nextTime: number,
2730
+ ): boolean {
2731
+ if (!Number.isFinite(frameTime) || !Number.isFinite(nextTime)) return false;
2732
+ if (nextTime === currentTime) return false;
2733
+ return (
2734
+ lastUpdateAt == null ||
2735
+ frameTime - lastUpdateAt >= REPLAY_CLOCK_UPDATE_INTERVAL_MS
2736
+ );
2737
+ }
2738
+
2566
2739
  function isRecord(value: unknown): value is AnyRecord {
2567
2740
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
2568
2741
  }
@@ -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.
@@ -1,6 +1,6 @@
1
1
  import { useT } from "@agent-native/core/client";
2
2
  import type { CalendarEvent, DeleteEventScope } from "@shared/api";
3
- import { useEffect, useMemo, useState } from "react";
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
4
 
5
5
  import { getGuestAttendeeCount } from "@/components/calendar/GuestNotificationDialog";
6
6
  import {
@@ -36,6 +36,7 @@ export function DeleteEventDialog({
36
36
  onConfirm,
37
37
  }: DeleteEventDialogProps) {
38
38
  const t = useT();
39
+ const confirmButtonRef = useRef<HTMLButtonElement>(null);
39
40
  const [scope, setScope] = useState<DeleteEventScope>("single");
40
41
  const [message, setMessage] = useState("");
41
42
 
@@ -108,7 +109,14 @@ export function DeleteEventDialog({
108
109
 
109
110
  return (
110
111
  <AlertDialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
111
- <AlertDialogContent className="max-w-[420px]" onKeyDown={handleKeyDown}>
112
+ <AlertDialogContent
113
+ className="max-w-[420px]"
114
+ onKeyDown={handleKeyDown}
115
+ onOpenAutoFocus={(event) => {
116
+ event.preventDefault();
117
+ confirmButtonRef.current?.focus();
118
+ }}
119
+ >
112
120
  <AlertDialogHeader>
113
121
  <AlertDialogTitle className="text-sm">{copy.title}</AlertDialogTitle>
114
122
  <AlertDialogDescription>{copy.description}</AlertDialogDescription>
@@ -174,6 +182,7 @@ export function DeleteEventDialog({
174
182
  </Button>
175
183
  )}
176
184
  <Button
185
+ ref={confirmButtonRef}
177
186
  variant="destructive"
178
187
  onClick={() => handleConfirm(canNotifyGuests ? "all" : "none")}
179
188
  >
@@ -194,6 +194,12 @@ function RsvpControls({
194
194
  doRsvp(status, undefined, "");
195
195
  };
196
196
 
197
+ const savePendingResponse = () => {
198
+ if (!pendingStatus || mutation.isPending) return;
199
+ doRsvp(pendingStatus, isRecurring ? pendingScope : undefined);
200
+ closePopover();
201
+ };
202
+
197
203
  return (
198
204
  <Popover
199
205
  open={!!pendingStatus}
@@ -255,6 +261,13 @@ function RsvpControls({
255
261
  className="w-[22rem] max-w-[calc(100vw-2rem)] overflow-hidden p-0"
256
262
  onClick={(e) => e.stopPropagation()}
257
263
  onPointerDownCapture={(e) => e.stopPropagation()}
264
+ onKeyDown={(e) => {
265
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
266
+ e.preventDefault();
267
+ e.stopPropagation();
268
+ savePendingResponse();
269
+ }
270
+ }}
258
271
  >
259
272
  {pendingStatus && (
260
273
  <div>
@@ -336,8 +349,7 @@ function RsvpControls({
336
349
  disabled={mutation.isPending}
337
350
  onClick={(e) => {
338
351
  e.stopPropagation();
339
- doRsvp(pendingStatus, isRecurring ? pendingScope : undefined);
340
- closePopover();
352
+ savePendingResponse();
341
353
  }}
342
354
  >
343
355
  {t("eventForm.saveResponse")}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-12
4
+ ---
5
+
6
+ Calendar responses can now be saved with Command+Enter or Ctrl+Enter, and delete confirmations focus the delete action for faster keyboard use.
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../src/client/analytics.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../src/client/analytics.ts"],"names":[],"mappings":"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"}
@@ -976,6 +976,17 @@ function configuredSessionReplayOptions(config, tracking = {}) {
976
976
  ...(publicKey && !options.publicKey ? { publicKey } : {}),
977
977
  ...(endpoint && !options.endpoint ? { endpoint } : {}),
978
978
  ...options,
979
+ onUploadRejected: options.onUploadRejected ??
980
+ ((details) => {
981
+ trackEvent("session replay upload rejected", {
982
+ status: details.status,
983
+ restart_attempted: details.restartAttempted,
984
+ restart_succeeded: details.restartSucceeded,
985
+ ...(details.restartReason
986
+ ? { restart_reason: details.restartReason }
987
+ : {}),
988
+ });
989
+ }),
979
990
  requireSignedInUser: options.requireSignedInUser ??
980
991
  sessionReplayRequiresSignedInUserFromEnv() ??
981
992
  true,