@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
@@ -64,9 +64,9 @@ export function DemoModeSection() {
64
64
  Enable demo mode
65
65
  </div>
66
66
  <p className="text-[10px] text-muted-foreground mt-0.5">
67
- Replace emails and numbers with realistic fake data everywhere in
68
- the UI and what the agent sees. Names, free text, labels, IDs, and
69
- structure are preserved so the app keeps working.
67
+ Anonymize emails in the UI and reshape supported dashboard charts for
68
+ presentations. Agent-visible emails and numbers are also anonymized.
69
+ Names, labels, IDs, and structure stay intact.
70
70
  </p>
71
71
  {forced && (
72
72
  <p className="text-[10px] text-muted-foreground mt-0.5">
@@ -5,7 +5,7 @@ import { writeAppState } from "../../application-state/script-helpers.js";
5
5
 
6
6
  export default defineAction({
7
7
  description:
8
- "Turn demo mode on or off. When demo mode is on, the app replaces email addresses and numbers with realistic fake data everywhere in the UI and in what you (the agent) see while keeping names, free text, labels, IDs, dates, and structure intact so everything still works. Use when the user asks to 'hide my data', 'turn on demo mode', 'anonymize this for a screen share / recording', or similar. This is the same toggle as the Demo mode switch in settings.",
8
+ "Turn demo mode on or off. When demo mode is on, the UI replaces email addresses with realistic fake data and supported dashboards reshape chart values for presentations. Agent-visible results replace emails and numbers while keeping names, free text, labels, IDs, dates, and structure intact. Use when the user asks to 'hide my data', 'turn on demo mode', 'anonymize this for a screen share / recording', or similar. This is the same toggle as the Demo mode switch in settings.",
9
9
  schema: z.object({
10
10
  enabled: z
11
11
  .boolean()
@@ -16,7 +16,7 @@ export default defineAction({
16
16
  return {
17
17
  enabled,
18
18
  message: enabled
19
- ? "Demo mode is ON — emails and numbers are now replaced with deterministic fake data everywhere (UI and agent). Names, free text, labels, IDs, and structure are preserved so everything keeps working."
19
+ ? "Demo mode is ON — UI emails are anonymized and supported dashboard charts are reshaped for presentation. Agent-visible emails and numbers are also anonymized. Names, free text, labels, IDs, and structure are preserved."
20
20
  : "Demo mode is OFF — real data is shown again.",
21
21
  };
22
22
  },
@@ -46,6 +46,23 @@ const SKIP_SUBSTRINGS = [
46
46
  "/_agent-native/runs",
47
47
  ];
48
48
 
49
+ // Raw rrweb payloads are playback data, not ordinary app UI records. Walking
50
+ // and cloning their huge DOM/event trees on the main thread stalls playback
51
+ // and can corrupt replay fidelity. Small list/summary/manifest responses stay
52
+ // eligible so rendered visitor identities are still anonymized.
53
+ const RAW_REPLAY_PAYLOAD_RE =
54
+ /\/api\/session-replay\/recordings\/[^/?#]+\/(?:chunks(?:\/[^/?#]+)?|events)(?:[/?#]|$)/;
55
+ const RAW_AGENT_REPLAY_EVENTS_RE =
56
+ /\/api\/session-replay\/agent-events\.json(?:[?#]|$)/;
57
+
58
+ export function shouldSkipDemoResponseRedaction(url: string): boolean {
59
+ return (
60
+ SKIP_SUBSTRINGS.some((substring) => url.includes(substring)) ||
61
+ RAW_REPLAY_PAYLOAD_RE.test(url) ||
62
+ RAW_AGENT_REPLAY_EVENTS_RE.test(url)
63
+ );
64
+ }
65
+
49
66
  let installed = false;
50
67
  let demoEnabled = false;
51
68
  let originalFetch: typeof fetch | null = null;
@@ -148,7 +165,7 @@ export function ensureDemoModeFetchInterceptor(): void {
148
165
 
149
166
  try {
150
167
  const url = urlOf(input);
151
- if (SKIP_SUBSTRINGS.some((s) => url.includes(s))) return res;
168
+ if (shouldSkipDemoResponseRedaction(url)) return res;
152
169
 
153
170
  // Only buffered, finite JSON. SSE / streaming / chunked-forever bodies
154
171
  // never reach `redactDemoData`: streaming content-types are excluded,
@@ -167,7 +184,13 @@ export function ensureDemoModeFetchInterceptor(): void {
167
184
  if (res.bodyUsed) return res;
168
185
 
169
186
  const data = await withTimeout(res.clone().json(), 3_000);
170
- const redacted = redactDemoData(data);
187
+ // Frontend reads only need identity privacy. Dashboard charts apply
188
+ // their purpose-built demo trend transform at render time, so mutating
189
+ // every numeric field in every JSON response is unnecessary work.
190
+ const redacted = redactDemoData(data, {
191
+ redactNumbers: false,
192
+ redactProtectedEmails: true,
193
+ });
171
194
 
172
195
  const headers = new Headers(res.headers);
173
196
  // Body is re-serialized — these would be wrong now.
@@ -14,6 +14,13 @@
14
14
 
15
15
  export interface RedactOptions {
16
16
  salt?: string;
17
+ /** Redact numeric values. Defaults to true for backward compatibility. */
18
+ redactNumbers?: boolean;
19
+ /**
20
+ * Redact emails stored in otherwise protected structural fields such as
21
+ * `userId` and `userKey`. Intended for display-only frontend responses.
22
+ */
23
+ redactProtectedEmails?: boolean;
17
24
  }
18
25
 
19
26
  /* ------------------------------------------------------------------ *
@@ -532,18 +539,29 @@ function transformNumbers(text: string, salt: string): string {
532
539
  * Public: string redactor
533
540
  * ------------------------------------------------------------------ */
534
541
 
535
- function redactDemoStringInternal(text: string, salt: string): string {
542
+ function redactDemoStringInternal(
543
+ text: string,
544
+ salt: string,
545
+ redactNumbers: boolean,
546
+ ): string {
536
547
  if (typeof text !== "string" || text.length === 0) return text;
548
+ if (!text.includes("@") && (!redactNumbers || !/[\d$€£¥]/.test(text))) {
549
+ return text;
550
+ }
537
551
 
538
552
  const { text: masked, restore } = protect(text);
539
553
  let out = masked;
540
554
  out = transformEmails(out, salt);
541
- out = transformNumbers(out, salt);
555
+ if (redactNumbers) out = transformNumbers(out, salt);
542
556
  return unprotect(out, restore);
543
557
  }
544
558
 
545
559
  export function redactDemoString(text: string, opts?: RedactOptions): string {
546
- return redactDemoStringInternal(text, opts?.salt ?? "");
560
+ return redactDemoStringInternal(
561
+ text,
562
+ opts?.salt ?? "",
563
+ opts?.redactNumbers !== false,
564
+ );
547
565
  }
548
566
 
549
567
  /* ------------------------------------------------------------------ *
@@ -586,6 +604,8 @@ function walk(
586
604
  salt: string,
587
605
  depth: number,
588
606
  seen: WeakSet<object>,
607
+ redactNumbers: boolean,
608
+ redactProtectedEmails: boolean,
589
609
  ): unknown {
590
610
  if (depth > MAX_DEPTH) return value;
591
611
 
@@ -594,11 +614,11 @@ function walk(
594
614
  const t = typeof value;
595
615
 
596
616
  if (t === "string") {
597
- return redactDemoStringInternal(value as string, salt);
617
+ return redactDemoStringInternal(value as string, salt, redactNumbers);
598
618
  }
599
619
 
600
620
  if (t === "number") {
601
- return redactNumberLeaf(value as number, salt);
621
+ return redactNumbers ? redactNumberLeaf(value as number, salt) : value;
602
622
  }
603
623
 
604
624
  if (t === "boolean" || t === "bigint" || t === "function" || t === "symbol") {
@@ -610,7 +630,9 @@ function walk(
610
630
  if (Array.isArray(value)) {
611
631
  if (seen.has(value)) return value;
612
632
  seen.add(value);
613
- const out = value.map((item) => walk(item, salt, depth + 1, seen));
633
+ const out = value.map((item) =>
634
+ walk(item, salt, depth + 1, seen, redactNumbers, redactProtectedEmails),
635
+ );
614
636
  seen.delete(value);
615
637
  return out;
616
638
  }
@@ -629,14 +651,34 @@ function walk(
629
651
  ) {
630
652
  // Still recurse into nested structures, but the protected key does
631
653
  // not transform its own leaf value.
632
- out[key] = walk(entry, salt, depth + 1, seen);
654
+ out[key] = walk(
655
+ entry,
656
+ salt,
657
+ depth + 1,
658
+ seen,
659
+ redactNumbers,
660
+ redactProtectedEmails,
661
+ );
662
+ } else if (
663
+ redactProtectedEmails &&
664
+ typeof entry === "string" &&
665
+ entry.includes("@")
666
+ ) {
667
+ out[key] = redactDemoStringInternal(entry, salt, false);
633
668
  } else {
634
669
  // Leaf under a protected key: pass through completely untouched.
635
670
  out[key] = entry;
636
671
  }
637
672
  continue;
638
673
  }
639
- out[key] = walk(entry, salt, depth + 1, seen);
674
+ out[key] = walk(
675
+ entry,
676
+ salt,
677
+ depth + 1,
678
+ seen,
679
+ redactNumbers,
680
+ redactProtectedEmails,
681
+ );
640
682
  }
641
683
  seen.delete(value);
642
684
  return out;
@@ -648,7 +690,14 @@ function walk(
648
690
 
649
691
  export function redactDemoData<T>(value: T, opts?: RedactOptions): T {
650
692
  const salt = opts?.salt ?? "";
651
- return walk(value, salt, 0, new WeakSet<object>()) as T;
693
+ return walk(
694
+ value,
695
+ salt,
696
+ 0,
697
+ new WeakSet<object>(),
698
+ opts?.redactNumbers !== false,
699
+ opts?.redactProtectedEmails === true,
700
+ ) as T;
652
701
  }
653
702
 
654
703
  /**
@@ -102,9 +102,9 @@ agent answers about browser recordings in the Analytics template.
102
102
  - Wait for all replay chunks (`isComplete`) before constructing the rrweb
103
103
  `Replayer`. Progressive chunk publishes should only update the loading bar;
104
104
  rebuilding the player mid-load desyncs the scrubber and playhead.
105
- - Pass events to `Replayer` untouched. rrweb rebuilds them in a sandboxed iframe;
106
- pre-processing DOM, stylesheet, resource, or mutation payloads makes playback
107
- diverge from the captured page. In particular, never rewrite `href`, `src`,
105
+ - Pass normal events to `Replayer` untouched. rrweb rebuilds them in a sandboxed
106
+ iframe; pre-processing DOM, stylesheet, resource, or mutation payloads makes
107
+ playback diverge from the captured page. In particular, never rewrite `href`, `src`,
108
108
  `_cssText`, CSS `url()`, or Meta URLs to `about:blank`; that exact remediation
109
109
  broke historical replay CSS in PR #2040. Handle request privacy at capture or
110
110
  the sandbox boundary instead of mutating stored rrweb events. Historical
@@ -112,15 +112,46 @@ agent answers about browser recordings in the Analytics template.
112
112
  requests for accurate rendering; the viewer accepts that fidelity tradeoff,
113
113
  uses rrweb's script-disabled sandbox plus `referrerpolicy="no-referrer"`, and
114
114
  must never add credentials or proxy those URLs through a privileged server.
115
+ - Capture-time URL scrubbing must preserve load-bearing DOM resource attributes:
116
+ `src`, `srcset`, `poster`, `data`, and `href` only on resource links such as
117
+ stylesheets, preloads, and icons. Signed CDN query parameters are part of the
118
+ resource identity; redacting them produces missing CSS, fonts, images, and
119
+ oversized fallback icons. Keep scrubbing Meta/navigation URLs, anchor hrefs,
120
+ and console/network diagnostics. Captured `_cssText` and CSS `@import`/`url()`
121
+ values must remain byte-identical.
122
+ - rrweb rebuilds into an `about:srcdoc` iframe, which inherits the Analytics
123
+ document's CSP. Analytics currently sends no CSP header; if a future change
124
+ adds restrictive `style-src`, `font-src`, or `img-src` directives, verify
125
+ historical replays and resolve external imports/fonts at capture before
126
+ blocking the recorded resource origins. Do not diagnose current font loss as
127
+ CSP without checking the deployed response headers first.
115
128
  - Let rrweb own normal iframe sizing via Meta / ViewportResize, and keep the
116
129
  outer wrapper in that same coordinate system for fit-to-stage scaling. Some
117
130
  legacy recordings contain demonstrably corrupt 4,000–7,500px-wide viewport
118
131
  values from ordinary desktop sessions. Only widths of at least 4,000px with
119
132
  aspect ratios above 4:1 are recovered to a 16:10 viewport; real 32:9 and
120
133
  mobile portrait captures remain untouched. The resize handler applies the
121
- recovery to **both** rrweb's iframe and the outer stage.
134
+ recovery to **both** rrweb's iframe and the outer stage. This includes the
135
+ historical 3,000–3,999px-wide, sub-1,000px-high malformed shape; do not
136
+ narrow the guard back to only 4,000px+ recordings.
122
137
  Never remove this recovery, clamp only one layer, or broadly rewrite event
123
138
  geometry: those changes repeatedly recreated the ribbon/clipping regression.
139
+ - The malformed-viewport recovery's sole event-data exception is pointer
140
+ geometry: project MouseMove, TouchMove, Drag, and MouseInteraction x/y into
141
+ the corrected camera and clamp impossible coordinates. Correcting the iframe
142
+ without its paired pointer projection makes rrweb cast valid target ids
143
+ thousands of pixels off-stage. Normal-viewport pointer events must retain
144
+ their original object references and exact coordinates.
145
+ - Keep rrweb's stock cursor stylesheet and its hotspot transform. During
146
+ playback, hide the viewer's native pointer over Analytics' transparent
147
+ click-to-pause overlay so it cannot masquerade as a frozen recorded cursor.
148
+ - Keep rrweb's recorded focus handling enabled. Focus and focus-visible state
149
+ affect menus, forms, and keyboard UX; disabling `triggerFocus` makes a valid
150
+ snapshot diverge from the source page.
151
+ - `insertStyleRules` may suppress known toast/snackbar containers only. Never
152
+ hide generic framework primitives such as
153
+ `[data-radix-popper-content-wrapper]`: Radix dropdowns, selects, tooltips,
154
+ and other real recorded product UI all share that wrapper.
124
155
  - Keep the realistic fidelity and malformed-viewport tests in
125
156
  `SessionDetailPage.spec.ts`. Do not change their expectations merely to bless
126
157
  a new sanitizer or raw ultra-wide sizing; validate the affected replay in a
@@ -161,3 +192,14 @@ agent answers about browser recordings in the Analytics template.
161
192
  `.an-mask` or `data-an-mask`.
162
193
  - Use `.an-block`, `.an-ignore`, `data-an-block`, or `data-an-ignore` for
163
194
  sensitive zones that should not be captured.
195
+ - A definitive upload `409` abandons only the conflicted replay identity and
196
+ immediately starts rrweb again under a fresh per-tab id, producing a new
197
+ Meta + FullSnapshot for long-lived SPA tabs. Recovery is limited to one
198
+ restart until an upload succeeds so a misconfigured endpoint cannot loop;
199
+ Analytics tracks the content-free `session replay upload rejected` lifecycle
200
+ event so conflicts and recovery success are measurable.
201
+ - Do not label an old recording "corrupt" from pointer coordinates, unknown
202
+ mutation node ids, or changing Meta geometry alone. Those shapes can be
203
+ legitimate with scrolling, iframes/shadow DOM, navigation, and resize. A
204
+ historical-artifact notice needs a durable capture/ingest marker or another
205
+ low-false-positive invariant; do not guess from playback heuristics.
@@ -245,72 +245,14 @@
245
245
  transform-origin: top left;
246
246
  }
247
247
 
248
- /* First-party visitor-cursor styles for rrweb playback. These are bundled
249
- statically so the cursor stays visible even if the lazily imported
250
- `@rrweb/replay/dist/style.css` sheet fails to load, and they replace the
251
- default black arrow sprite with an app-styled indicator. The cursor lives
252
- inside `.replayer-wrapper`, so it scales with the fit-to-stage transform. */
253
- .an-replay-stage-root .replayer-mouse {
254
- position: absolute;
255
- z-index: 2;
256
- width: 1.125rem;
257
- height: 1.5rem;
258
- border-radius: 0;
259
- background: transparent;
260
- background-image: none;
261
- box-shadow: none;
262
- transform: none;
263
- transition:
264
- left 80ms linear,
265
- top 80ms linear;
266
- pointer-events: none;
267
- }
268
-
269
- .an-replay-stage-root .replayer-mouse::before {
270
- content: "";
271
- position: absolute;
272
- inset: 0;
273
- background: hsl(0 0% 100%);
274
- clip-path: polygon(0 0, 0 77%, 30% 57%, 48% 100%, 68% 92%, 50% 51%, 92% 51%);
275
- filter: drop-shadow(0 0 1px hsl(0 0% 0% / 0.95))
276
- drop-shadow(0 1px 3px hsl(0 0% 0% / 0.35));
277
- }
278
-
248
+ /* IMPORTANT: Keep rrweb's stock SVG arrow. Only suppress its permanently
249
+ visible purple ::after disc, which can otherwise read as the cursor itself
250
+ when the arrow is small. The stock .active animation still supplies the
251
+ brief click halo. Do not replace the arrow with an app-colored dot. */
279
252
  .an-replay-stage-root .replayer-mouse::after {
280
- content: "";
281
- position: absolute;
282
- left: -0.5rem;
283
- top: -0.5rem;
284
- width: 1rem;
285
- height: 1rem;
286
- border-radius: 9999px;
287
- border: 2px solid hsl(217 91% 60%);
288
- background: transparent;
289
253
  opacity: 0;
290
254
  }
291
255
 
292
- /* rrweb toggles `.active` on mouse interactions; pulse a click ripple. */
293
- .an-replay-stage-root .replayer-mouse.active::after {
294
- animation: an-replay-mouse-click 0.4s ease-out 1;
295
- }
296
-
297
- @keyframes an-replay-mouse-click {
298
- 0% {
299
- opacity: 0.5;
300
- transform: scale(1);
301
- }
302
- 100% {
303
- opacity: 0;
304
- transform: scale(3.5);
305
- }
306
- }
307
-
308
- .an-replay-stage-root .replayer-mouse-tail {
309
- position: absolute;
310
- inset: 0;
311
- pointer-events: none;
312
- }
313
-
314
256
  .analytics-session-snippet .plan-code-surface {
315
257
  border: 0;
316
258
  border-radius: 0;