@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
@@ -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">
@@ -707,6 +707,22 @@ function dispatchActivityClear(tabId: string | undefined) {
707
707
  );
708
708
  }
709
709
 
710
+ // Fires once per chunk (see ProcessEventState.streamProgressDispatched) the
711
+ // moment a chunk produces real model output (visible text or a reasoning
712
+ // delta). Unlike `agent-chat:activity-clear` — which also fires for
713
+ // old-chunk `tool_done` replays and server-retry `clear` events, and so
714
+ // cannot be used to detect genuine forward progress — this event exists
715
+ // solely so listeners can distinguish "the run is actually producing new
716
+ // output" from those replay/retry cases.
717
+ function dispatchStreamProgress(tabId: string | undefined) {
718
+ if (typeof window === "undefined") return;
719
+ window.dispatchEvent(
720
+ new CustomEvent("agent-chat:stream-progress", {
721
+ detail: { tabId },
722
+ }),
723
+ );
724
+ }
725
+
710
726
  function dispatchMissingApiKey(tabId: string | undefined) {
711
727
  if (typeof window === "undefined") return;
712
728
  window.dispatchEvent(
@@ -946,6 +962,11 @@ function completedToolOnlyMessage(toolNames: string[]): string | null {
946
962
 
947
963
  interface ProcessEventState {
948
964
  completedToolsAfterLastAssistantText: Set<string>;
965
+ /** Set once `agent-chat:stream-progress` has been dispatched for the
966
+ * current chunk, so a burst of per-token text/reasoning deltas only fires
967
+ * it once. Cleared by `resetProcessEventState` on a server `clear` retry
968
+ * so the next batch of real output re-arms it. */
969
+ streamProgressDispatched: boolean;
949
970
  }
950
971
 
951
972
  function markAssistantText(state: ProcessEventState | undefined) {
@@ -961,6 +982,18 @@ function markCompletedToolAfterAssistantText(
961
982
 
962
983
  function resetProcessEventState(state: ProcessEventState | undefined) {
963
984
  state?.completedToolsAfterLastAssistantText.clear();
985
+ if (state) state.streamProgressDispatched = false;
986
+ }
987
+
988
+ // Returns true the first time it's called for a given state (or every time
989
+ // when state is unavailable, since there is nothing to dedupe against) and
990
+ // false on subsequent calls until `resetProcessEventState` re-arms it.
991
+ function shouldDispatchStreamProgress(
992
+ state: ProcessEventState | undefined,
993
+ ): boolean {
994
+ if (state?.streamProgressDispatched) return false;
995
+ if (state) state.streamProgressDispatched = true;
996
+ return true;
964
997
  }
965
998
 
966
999
  /**
@@ -1005,7 +1038,13 @@ export function processEvent(
1005
1038
  // activity label so a transient "Contacting model" / "Still generating
1006
1039
  // image" doesn't linger beside streamed text. Idempotent (clears once, then
1007
1040
  // no-ops) so per-token text deltas stay cheap.
1008
- if (ev.text) dispatchActivityClear(tabId);
1041
+ if (ev.text) {
1042
+ dispatchActivityClear(tabId);
1043
+ // Real output resumed — let listeners (e.g. the auto-resume "Resuming…"
1044
+ // indicator) clear state that must NOT be cleared by activity-clear
1045
+ // alone, since activity-clear also fires for old-chunk/retry replays.
1046
+ if (shouldDispatchStreamProgress(state)) dispatchStreamProgress(tabId);
1047
+ }
1009
1048
  if (ev.text?.trim()) markAssistantText(state);
1010
1049
  const lastPart = content[content.length - 1];
1011
1050
  if (lastPart && lastPart.type === "text") {
@@ -1024,6 +1063,7 @@ export function processEvent(
1024
1063
  // part so the UI can render a single collapsible "Thinking" cell.
1025
1064
  const delta = ev.text ?? "";
1026
1065
  if (!delta) return { action: "continue" };
1066
+ if (shouldDispatchStreamProgress(state)) dispatchStreamProgress(tabId);
1027
1067
  const lastPart = content[content.length - 1];
1028
1068
  if (lastPart && lastPart.type === "reasoning") {
1029
1069
  lastPart.text += delta;
@@ -1567,6 +1607,7 @@ export async function* readSSEStream(
1567
1607
  options?.preparingActionState ?? {};
1568
1608
  const processEventState: ProcessEventState = {
1569
1609
  completedToolsAfterLastAssistantText: new Set(),
1610
+ streamProgressDispatched: false,
1570
1611
  };
1571
1612
 
1572
1613
  const withStreamMetadata = (r: ChatModelRunResult): ChatModelRunResult => {
@@ -1769,6 +1810,7 @@ export async function readSSEStreamRaw(
1769
1810
  options?.preparingActionState ?? {};
1770
1811
  const processEventState: ProcessEventState = {
1771
1812
  completedToolsAfterLastAssistantText: new Set(),
1813
+ streamProgressDispatched: false,
1772
1814
  };
1773
1815
  // Tracks whether the most recent content state was already pushed via
1774
1816
  // onUpdate inside the loop, so the post-loop flush below doesn't emit the
@@ -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 every email address with anonymous@builder.io and supported dashboards reshape chart values for presentations. Agent-visible results use the same anonymous email and anonymize 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
  },
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Demo mode gate.
3
3
  *
4
- * Demo mode replaces email addresses and numbers in every action result with
5
- * deterministic fake data — for both the UI and what the agent sees — while
6
- * preserving names, free text, labels, IDs, dates, URLs, and structure so the
7
- * app keeps working. The redaction WALK (see ./redact.ts) is expensive on
8
- * large payloads, so callers MUST gate it behind this function and only walk
9
- * when it returns true.
4
+ * Demo mode replaces every email address with `anonymous@builder.io` and
5
+ * anonymizes numbers in every action result — for both the UI and what the
6
+ * agent sees — while preserving names, free text, labels, IDs, dates, URLs,
7
+ * and structure so the app keeps working. The redaction WALK (see ./redact.ts)
8
+ * is expensive on large payloads, so callers MUST gate it behind this function
9
+ * and only walk when it returns true.
10
10
  *
11
11
  * This gate itself is intentionally cheap:
12
12
  * - An env-forced deployment (a hosted demo site) short-circuits with zero
@@ -46,6 +46,32 @@ const SKIP_SUBSTRINGS = [
46
46
  "/_agent-native/runs",
47
47
  ];
48
48
 
49
+ // Raw rrweb payloads are playback data, not ordinary app UI records. Skipping
50
+ // these is not just a perf optimization that avoids walking/cloning huge
51
+ // DOM/event trees on the main thread: demo number
52
+ // redaction previously ran over this exact raw replay JSON and faked any
53
+ // integer >= 1000 it found — Meta/ViewportResize widths, pointer x/y
54
+ // coordinates, and numeric values inside `_cssText` and SVG attributes. That
55
+ // was the root cause of the 2026-07 "ultra-wide replay" bugs (stages
56
+ // rendered thousands of pixels wide, frozen/teleporting cursors, giant
57
+ // icons) even though every stored recording was always geometrically sane —
58
+ // the corruption happened at *view* time, not at capture/storage time. Small
59
+ // list/summary/manifest responses stay eligible so rendered visitor identities
60
+ // are still anonymized. NEVER remove the raw payload skips or broaden them to
61
+ // metadata endpoints that the UI renders directly.
62
+ const RAW_REPLAY_PAYLOAD_RE =
63
+ /\/api\/session-replay\/recordings\/[^/?#]+\/(?:chunks(?:\/[^/?#]+)?|events)(?:[/?#]|$)/;
64
+ const RAW_AGENT_REPLAY_EVENTS_RE =
65
+ /\/api\/session-replay\/agent-events\.json(?:[?#]|$)/;
66
+
67
+ export function shouldSkipDemoResponseRedaction(url: string): boolean {
68
+ return (
69
+ SKIP_SUBSTRINGS.some((substring) => url.includes(substring)) ||
70
+ RAW_REPLAY_PAYLOAD_RE.test(url) ||
71
+ RAW_AGENT_REPLAY_EVENTS_RE.test(url)
72
+ );
73
+ }
74
+
49
75
  let installed = false;
50
76
  let demoEnabled = false;
51
77
  let originalFetch: typeof fetch | null = null;
@@ -148,7 +174,7 @@ export function ensureDemoModeFetchInterceptor(): void {
148
174
 
149
175
  try {
150
176
  const url = urlOf(input);
151
- if (SKIP_SUBSTRINGS.some((s) => url.includes(s))) return res;
177
+ if (shouldSkipDemoResponseRedaction(url)) return res;
152
178
 
153
179
  // Only buffered, finite JSON. SSE / streaming / chunked-forever bodies
154
180
  // never reach `redactDemoData`: streaming content-types are excluded,
@@ -167,7 +193,13 @@ export function ensureDemoModeFetchInterceptor(): void {
167
193
  if (res.bodyUsed) return res;
168
194
 
169
195
  const data = await withTimeout(res.clone().json(), 3_000);
170
- const redacted = redactDemoData(data);
196
+ // Frontend reads only need identity privacy. Dashboard charts apply
197
+ // their purpose-built demo trend transform at render time, so mutating
198
+ // every numeric field in every JSON response is unnecessary work.
199
+ const redacted = redactDemoData(data, {
200
+ redactNumbers: false,
201
+ redactProtectedEmails: true,
202
+ });
171
203
 
172
204
  const headers = new Headers(res.headers);
173
205
  // Body is re-serialized — these would be wrong now.
@@ -1,19 +1,25 @@
1
1
  /**
2
2
  * Pure, dependency-free, deterministic demo-mode redactor.
3
3
  *
4
- * Replaces sensitive values (emails and free numbers) with stable fake
5
- * substitutes so a demo looks coherent the same input always maps to the
6
- * same fake. Crucially, it NEVER rewrites identifiers, structural tokens, or
7
- * timestamps. Names and other free text are deliberately left alone because
8
- * guessing whether arbitrary text is a person's name is too inaccurate. The
9
- * string redactor uses a protect-first strategy (mask IDs with opaque
10
- * placeholders before any transform runs, restore them byte-identical
4
+ * Replaces every email with one canonical anonymous address and free numbers
5
+ * with stable fake substitutes. Crucially, it NEVER rewrites identifiers,
6
+ * structural tokens, or timestamps. Names and other free text are deliberately
7
+ * left alone because guessing whether arbitrary text is a person's name is too
8
+ * inaccurate. The string redactor uses a protect-first strategy (mask IDs with
9
+ * opaque placeholders before any transform runs, restore them byte-identical
11
10
  * afterwards), and the structure-aware walker additionally protects leaf
12
11
  * values by key name.
13
12
  */
14
13
 
15
14
  export interface RedactOptions {
16
15
  salt?: string;
16
+ /** Redact numeric values. Defaults to true for backward compatibility. */
17
+ redactNumbers?: boolean;
18
+ /**
19
+ * Redact emails stored in otherwise protected structural fields such as
20
+ * `userId` and `userKey`. Intended for display-only frontend responses.
21
+ */
22
+ redactProtectedEmails?: boolean;
17
23
  }
18
24
 
19
25
  /* ------------------------------------------------------------------ *
@@ -51,10 +57,6 @@ function seededRng(value: string, salt: string): () => number {
51
57
  return mulberry32(seedFn());
52
58
  }
53
59
 
54
- function pick<T>(rng: () => number, pool: readonly T[]): T {
55
- return pool[Math.floor(rng() * pool.length) % pool.length];
56
- }
57
-
58
60
  /* ------------------------------------------------------------------ *
59
61
  * Stable mapping cache (bounded, TTL, leak-free)
60
62
  *
@@ -150,150 +152,19 @@ function memoFake(
150
152
  return value;
151
153
  }
152
154
 
153
- /* ------------------------------------------------------------------ *
154
- * Curated name pools
155
- * ------------------------------------------------------------------ */
156
-
157
- const FIRST_NAMES: readonly string[] = [
158
- "Jane",
159
- "John",
160
- "Alex",
161
- "Maria",
162
- "Sam",
163
- "Olivia",
164
- "Liam",
165
- "Emma",
166
- "Noah",
167
- "Ava",
168
- "Lucas",
169
- "Mia",
170
- "Ethan",
171
- "Sofia",
172
- "Mason",
173
- "Isla",
174
- "Leo",
175
- "Aria",
176
- "Henry",
177
- "Zoe",
178
- "Owen",
179
- "Nora",
180
- "Caleb",
181
- "Lily",
182
- "Ryan",
183
- "Ruby",
184
- "Adam",
185
- "Chloe",
186
- "Eli",
187
- "Hazel",
188
- "Max",
189
- "Iris",
190
- "Ben",
191
- "Clara",
192
- "Theo",
193
- "Maya",
194
- "Felix",
195
- "Elena",
196
- "Jonah",
197
- "Greta",
198
- ];
199
-
200
- const LAST_NAMES: readonly string[] = [
201
- "Doe",
202
- "Smith",
203
- "Johnson",
204
- "Lee",
205
- "Brown",
206
- "Garcia",
207
- "Miller",
208
- "Davis",
209
- "Wilson",
210
- "Moore",
211
- "Taylor",
212
- "Anderson",
213
- "Thomas",
214
- "Jackson",
215
- "White",
216
- "Harris",
217
- "Martin",
218
- "Clark",
219
- "Lewis",
220
- "Walker",
221
- "Hall",
222
- "Young",
223
- "King",
224
- "Wright",
225
- "Hill",
226
- "Green",
227
- "Adams",
228
- "Baker",
229
- "Nelson",
230
- "Carter",
231
- "Mitchell",
232
- "Perez",
233
- "Roberts",
234
- "Turner",
235
- "Phillips",
236
- "Campbell",
237
- "Parker",
238
- "Evans",
239
- "Edwards",
240
- "Collins",
241
- ];
242
-
243
155
  /* ------------------------------------------------------------------ *
244
156
  * Fake value generators (deterministic in value + salt)
245
157
  * ------------------------------------------------------------------ */
246
158
 
247
- // Realistic-looking but safe stand-in domains: real consumer providers (the
248
- // local part is fake, so no real mailbox is implied) plus the canonical
249
- // reserved fictional-company domains (Contoso/Fabrikam/Northwind), which read
250
- // as real businesses but are intentional placeholders. No example.com.
251
- const FAKE_EMAIL_DOMAINS: readonly string[] = [
252
- "gmail.com",
253
- "outlook.com",
254
- "icloud.com",
255
- "yahoo.com",
256
- "proton.me",
257
- "hey.com",
258
- "fastmail.com",
259
- "contoso.com",
260
- "fabrikam.com",
261
- "northwind.com",
262
- "acme.io",
263
- "globex.net",
264
- ];
159
+ export const DEMO_ANONYMOUS_EMAIL = "anonymous@builder.io";
265
160
 
266
161
  function fakeEmail(original: string, salt: string): string {
267
- return memoFake("email", original.toLowerCase(), salt, () => {
268
- const rng = seededRng(`email:${original.toLowerCase()}`, salt);
269
- const first = pick(rng, FIRST_NAMES).toLowerCase();
270
- const last = pick(rng, LAST_NAMES).toLowerCase();
271
- const domain = pick(rng, FAKE_EMAIL_DOMAINS);
272
- // Vary the local part so addresses don't all look templated.
273
- const shape = Math.floor(rng() * 6);
274
- let local: string;
275
- switch (shape) {
276
- case 0:
277
- local = `${first}.${last}`;
278
- break;
279
- case 1:
280
- local = `${first}${last}`;
281
- break;
282
- case 2:
283
- local = `${first[0]}${last}`;
284
- break;
285
- case 3:
286
- local = `${first}.${last}${1 + Math.floor(rng() * 89)}`;
287
- break;
288
- case 4:
289
- local = `${first}_${last}`;
290
- break;
291
- default:
292
- local = `${last}.${first}`;
293
- break;
294
- }
295
- return `${local}@${domain}`;
296
- });
162
+ return memoFake(
163
+ "email",
164
+ original.toLowerCase(),
165
+ salt,
166
+ () => DEMO_ANONYMOUS_EMAIL,
167
+ );
297
168
  }
298
169
 
299
170
  /**
@@ -532,18 +403,29 @@ function transformNumbers(text: string, salt: string): string {
532
403
  * Public: string redactor
533
404
  * ------------------------------------------------------------------ */
534
405
 
535
- function redactDemoStringInternal(text: string, salt: string): string {
406
+ function redactDemoStringInternal(
407
+ text: string,
408
+ salt: string,
409
+ redactNumbers: boolean,
410
+ ): string {
536
411
  if (typeof text !== "string" || text.length === 0) return text;
412
+ if (!text.includes("@") && (!redactNumbers || !/[\d$€£¥]/.test(text))) {
413
+ return text;
414
+ }
537
415
 
538
416
  const { text: masked, restore } = protect(text);
539
417
  let out = masked;
540
418
  out = transformEmails(out, salt);
541
- out = transformNumbers(out, salt);
419
+ if (redactNumbers) out = transformNumbers(out, salt);
542
420
  return unprotect(out, restore);
543
421
  }
544
422
 
545
423
  export function redactDemoString(text: string, opts?: RedactOptions): string {
546
- return redactDemoStringInternal(text, opts?.salt ?? "");
424
+ return redactDemoStringInternal(
425
+ text,
426
+ opts?.salt ?? "",
427
+ opts?.redactNumbers !== false,
428
+ );
547
429
  }
548
430
 
549
431
  /* ------------------------------------------------------------------ *
@@ -586,6 +468,8 @@ function walk(
586
468
  salt: string,
587
469
  depth: number,
588
470
  seen: WeakSet<object>,
471
+ redactNumbers: boolean,
472
+ redactProtectedEmails: boolean,
589
473
  ): unknown {
590
474
  if (depth > MAX_DEPTH) return value;
591
475
 
@@ -594,11 +478,11 @@ function walk(
594
478
  const t = typeof value;
595
479
 
596
480
  if (t === "string") {
597
- return redactDemoStringInternal(value as string, salt);
481
+ return redactDemoStringInternal(value as string, salt, redactNumbers);
598
482
  }
599
483
 
600
484
  if (t === "number") {
601
- return redactNumberLeaf(value as number, salt);
485
+ return redactNumbers ? redactNumberLeaf(value as number, salt) : value;
602
486
  }
603
487
 
604
488
  if (t === "boolean" || t === "bigint" || t === "function" || t === "symbol") {
@@ -610,7 +494,9 @@ function walk(
610
494
  if (Array.isArray(value)) {
611
495
  if (seen.has(value)) return value;
612
496
  seen.add(value);
613
- const out = value.map((item) => walk(item, salt, depth + 1, seen));
497
+ const out = value.map((item) =>
498
+ walk(item, salt, depth + 1, seen, redactNumbers, redactProtectedEmails),
499
+ );
614
500
  seen.delete(value);
615
501
  return out;
616
502
  }
@@ -629,14 +515,34 @@ function walk(
629
515
  ) {
630
516
  // Still recurse into nested structures, but the protected key does
631
517
  // not transform its own leaf value.
632
- out[key] = walk(entry, salt, depth + 1, seen);
518
+ out[key] = walk(
519
+ entry,
520
+ salt,
521
+ depth + 1,
522
+ seen,
523
+ redactNumbers,
524
+ redactProtectedEmails,
525
+ );
526
+ } else if (
527
+ redactProtectedEmails &&
528
+ typeof entry === "string" &&
529
+ entry.includes("@")
530
+ ) {
531
+ out[key] = redactDemoStringInternal(entry, salt, false);
633
532
  } else {
634
533
  // Leaf under a protected key: pass through completely untouched.
635
534
  out[key] = entry;
636
535
  }
637
536
  continue;
638
537
  }
639
- out[key] = walk(entry, salt, depth + 1, seen);
538
+ out[key] = walk(
539
+ entry,
540
+ salt,
541
+ depth + 1,
542
+ seen,
543
+ redactNumbers,
544
+ redactProtectedEmails,
545
+ );
640
546
  }
641
547
  seen.delete(value);
642
548
  return out;
@@ -648,7 +554,14 @@ function walk(
648
554
 
649
555
  export function redactDemoData<T>(value: T, opts?: RedactOptions): T {
650
556
  const salt = opts?.salt ?? "";
651
- return walk(value, salt, 0, new WeakSet<object>()) as T;
557
+ return walk(
558
+ value,
559
+ salt,
560
+ 0,
561
+ new WeakSet<object>(),
562
+ opts?.redactNumbers !== false,
563
+ opts?.redactProtectedEmails === true,
564
+ ) as T;
652
565
  }
653
566
 
654
567
  /**
@@ -1,3 +1,5 @@
1
+ import { buildSessionReplayIframeBootstrap } from "./session-replay-iframe.js";
2
+
1
3
  const EXTENSION_IFRAME_CSP_BASE =
2
4
  "default-src 'none'; script-src 'self' https://cdn.jsdelivr.net 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com; font-src https://fonts.gstatic.com; connect-src 'self'; img-src 'self' data: blob:; media-src 'self' data: blob:; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none';";
3
5
 
@@ -744,6 +746,7 @@ export function buildExtensionHtml(
744
746
  }
745
747
  });
746
748
  </script>
749
+ ${buildSessionReplayIframeBootstrap()}
747
750
  </head>
748
751
  <body${extensionId ? ` data-extension-id="${extensionIdAttr}" data-tool-id="${extensionIdAttr}"` : ""} class="text-foreground">
749
752
  ${content}
@@ -0,0 +1,131 @@
1
+ import {
2
+ isTrustedSessionReplayIframeParentOrigin,
3
+ SESSION_REPLAY_IFRAME_PROBE,
4
+ SESSION_REPLAY_IFRAME_START,
5
+ SESSION_REPLAY_IFRAME_STOP,
6
+ } from "../session-replay-iframe-protocol.js";
7
+
8
+ export const RRWEB_RECORD_IFRAME_CDN_URL =
9
+ "https://cdn.jsdelivr.net/npm/@rrweb/record@2.1.0/umd/record.min.js";
10
+ export const RRWEB_RECORD_IFRAME_SRI =
11
+ "sha384-MrD66HBNSykaP2N95+6hQCFlF5oH2tvL3TD/zyvHNkP/sAFWZx98DX9MEDy8MdVT";
12
+
13
+ /**
14
+ * Installs the cooperative side of rrweb's cross-origin iframe protocol.
15
+ * Nothing is fetched or recorded until a trusted first-party parent asks the
16
+ * frame to start. rrweb then forwards its events directly to that parent.
17
+ */
18
+ export function buildSessionReplayIframeBootstrap(): string {
19
+ const probeType = JSON.stringify(SESSION_REPLAY_IFRAME_PROBE);
20
+ const startType = JSON.stringify(SESSION_REPLAY_IFRAME_START);
21
+ const stopType = JSON.stringify(SESSION_REPLAY_IFRAME_STOP);
22
+ const scriptUrl = JSON.stringify(RRWEB_RECORD_IFRAME_CDN_URL);
23
+ const integrity = JSON.stringify(RRWEB_RECORD_IFRAME_SRI);
24
+
25
+ return `<script>
26
+ (function() {
27
+ var recorderStop = null;
28
+ var recorderLoad = null;
29
+ var startGeneration = 0;
30
+ var isTrustedParentOrigin = ${isTrustedSessionReplayIframeParentOrigin.toString()};
31
+
32
+ function stopRecorder() {
33
+ startGeneration += 1;
34
+ if (typeof recorderStop === 'function') {
35
+ try { recorderStop(); } catch (_) {}
36
+ }
37
+ recorderStop = null;
38
+ }
39
+
40
+ function getRecord() {
41
+ return window.rrwebRecord && typeof window.rrwebRecord.record === 'function'
42
+ ? window.rrwebRecord.record
43
+ : null;
44
+ }
45
+
46
+ function loadRecorder() {
47
+ if (recorderLoad) return recorderLoad;
48
+ recorderLoad = new Promise(function(resolve, reject) {
49
+ var script = document.createElement('script');
50
+ script.src = ${scriptUrl};
51
+ script.integrity = ${integrity};
52
+ script.crossOrigin = 'anonymous';
53
+ script.referrerPolicy = 'no-referrer';
54
+ script.async = true;
55
+ script.onload = function() {
56
+ var record = getRecord();
57
+ if (record) resolve(record);
58
+ else reject(new Error('rrweb recorder did not initialize'));
59
+ };
60
+ script.onerror = function() {
61
+ recorderLoad = null;
62
+ reject(new Error('rrweb recorder failed to load'));
63
+ };
64
+ document.head.appendChild(script);
65
+ });
66
+ return recorderLoad;
67
+ }
68
+
69
+ function validOptions(options) {
70
+ return !!options &&
71
+ typeof options === 'object' &&
72
+ typeof options.blockSelector === 'string' &&
73
+ typeof options.ignoreSelector === 'string' &&
74
+ typeof options.maskTextSelector === 'string' &&
75
+ typeof options.maskAllInputs === 'boolean' &&
76
+ typeof options.recordCanvas === 'boolean' &&
77
+ typeof options.collectFonts === 'boolean' &&
78
+ typeof options.inlineImages === 'boolean' &&
79
+ !!options.sampling && typeof options.sampling === 'object';
80
+ }
81
+
82
+ function startRecorder(options) {
83
+ if (!validOptions(options)) return;
84
+ stopRecorder();
85
+ var generation = startGeneration;
86
+ loadRecorder().then(function(record) {
87
+ if (generation !== startGeneration) return;
88
+ recorderStop = record({
89
+ blockSelector: options.blockSelector,
90
+ ignoreSelector: options.ignoreSelector,
91
+ maskTextClass: options.maskTextClass,
92
+ maskTextSelector: options.maskTextSelector,
93
+ maskAllInputs: options.maskAllInputs,
94
+ maskInputOptions: options.maskInputOptions,
95
+ recordCanvas: options.recordCanvas,
96
+ collectFonts: options.collectFonts,
97
+ inlineImages: options.inlineImages,
98
+ sampling: options.sampling,
99
+ recordCrossOriginIframes: true
100
+ }) || null;
101
+ }).catch(function() {});
102
+ }
103
+
104
+ window.addEventListener('message', function(event) {
105
+ if (event.source !== window.parent || !isTrustedParentOrigin(event.origin, window.location.href)) return;
106
+ var message = event.data;
107
+ if (!message || typeof message !== 'object') return;
108
+ if (message.type === ${startType}) startRecorder(message.options);
109
+ else if (message.type === ${stopType}) stopRecorder();
110
+ });
111
+ window.addEventListener('pagehide', stopRecorder);
112
+
113
+ if (window.parent !== window) {
114
+ window.parent.postMessage({ type: ${probeType} }, '*');
115
+ }
116
+ })();
117
+ </script>`;
118
+ }
119
+
120
+ export function injectSessionReplayIframeBootstrap(html: string): string {
121
+ const bootstrap = buildSessionReplayIframeBootstrap();
122
+ const headClose = html.search(/<\/head\s*>/i);
123
+ if (headClose >= 0) {
124
+ return `${html.slice(0, headClose)}${bootstrap}${html.slice(headClose)}`;
125
+ }
126
+ const bodyOpen = html.search(/<body(?:\s[^>]*)?>/i);
127
+ if (bodyOpen >= 0) {
128
+ return `${html.slice(0, bodyOpen)}${bootstrap}${html.slice(bodyOpen)}`;
129
+ }
130
+ return `${bootstrap}${html}`;
131
+ }