@agent-native/core 0.98.1 → 0.98.2

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 (122) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +8 -0
  3. package/corpus/core/docs/content/locales/ar-SA/pr-visual-recap.mdx +25 -1
  4. package/corpus/core/docs/content/locales/ar-SA/template-plan.mdx +1 -1
  5. package/corpus/core/docs/content/locales/de-DE/pr-visual-recap.mdx +25 -1
  6. package/corpus/core/docs/content/locales/de-DE/template-plan.mdx +1 -1
  7. package/corpus/core/docs/content/locales/es-ES/pr-visual-recap.mdx +25 -1
  8. package/corpus/core/docs/content/locales/es-ES/template-plan.mdx +1 -1
  9. package/corpus/core/docs/content/locales/fr-FR/pr-visual-recap.mdx +25 -1
  10. package/corpus/core/docs/content/locales/fr-FR/template-plan.mdx +1 -1
  11. package/corpus/core/docs/content/locales/hi-IN/pr-visual-recap.mdx +25 -1
  12. package/corpus/core/docs/content/locales/hi-IN/template-plan.mdx +1 -1
  13. package/corpus/core/docs/content/locales/ja-JP/pr-visual-recap.mdx +25 -1
  14. package/corpus/core/docs/content/locales/ja-JP/template-plan.mdx +1 -1
  15. package/corpus/core/docs/content/locales/ko-KR/pr-visual-recap.mdx +25 -1
  16. package/corpus/core/docs/content/locales/ko-KR/template-plan.mdx +1 -1
  17. package/corpus/core/docs/content/locales/pt-BR/pr-visual-recap.mdx +25 -1
  18. package/corpus/core/docs/content/locales/pt-BR/template-plan.mdx +1 -1
  19. package/corpus/core/docs/content/locales/zh-CN/pr-visual-recap.mdx +25 -1
  20. package/corpus/core/docs/content/locales/zh-CN/template-plan.mdx +1 -1
  21. package/corpus/core/docs/content/locales/zh-TW/pr-visual-recap.mdx +25 -1
  22. package/corpus/core/docs/content/locales/zh-TW/template-plan.mdx +1 -1
  23. package/corpus/core/docs/content/pr-visual-recap.mdx +50 -1
  24. package/corpus/core/docs/content/template-plan.mdx +1 -1
  25. package/corpus/core/package.json +1 -1
  26. package/corpus/core/src/cli/plan-local.ts +43 -11
  27. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +2 -9
  28. package/corpus/core/src/cli/recap.ts +356 -4
  29. package/corpus/core/src/cli/skills-content/local-files.ts +17 -12
  30. package/corpus/core/src/client/analytics.ts +96 -14
  31. package/corpus/core/src/client/index.ts +5 -0
  32. package/corpus/core/src/client/session-replay.ts +10 -0
  33. package/corpus/core/src/client/settings/DemoModeSection.tsx +4 -28
  34. package/corpus/core/src/client/url-scrub.ts +13 -0
  35. package/corpus/core/src/client/use-demo-mode-status.ts +44 -0
  36. package/corpus/templates/analytics/AGENTS.md +8 -0
  37. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +18 -4
  38. package/corpus/templates/analytics/app/lib/demo-chart-trend.ts +156 -0
  39. package/corpus/templates/analytics/changelog/2026-07-12-daily-dashboard-emails-reliably-include-the-complete-dashboa.md +6 -0
  40. package/corpus/templates/analytics/changelog/2026-07-12-demo-mode-charts-trend-upward.md +6 -0
  41. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +2 -0
  42. package/corpus/templates/analytics/server/lib/dashboard-report.ts +17 -33
  43. package/corpus/templates/clips/app/components/player/video-player.tsx +5 -1
  44. package/corpus/templates/clips/changelog/2026-07-11-clips-no-longer-stay-stuck-on-preparing-clip-when-they-are-r.md +6 -0
  45. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +2 -2
  46. package/corpus/templates/content/changelog/2026-07-12-the-left-sidebar-stays-fixed-horizontally-while-scrolling-on.md +6 -0
  47. package/corpus/templates/plan/.agents/skills/visual-plan/references/local-files.md +17 -12
  48. package/corpus/templates/plan/.agents/skills/visual-recap/references/local-files.md +17 -12
  49. package/corpus/templates/plan/actions/validate-local-plan-source.ts +4 -4
  50. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +2 -1
  51. package/corpus/templates/plan/app/lib/plan-local-bridge.ts +41 -0
  52. package/corpus/templates/plan/app/lib/plan-tracking.ts +3 -0
  53. package/corpus/templates/plan/app/pages/PlansPage.tsx +9 -9
  54. package/corpus/templates/plan/app/root.tsx +13 -3
  55. package/corpus/templates/plan/changelog/2026-07-12-local-plan-source-now-stays-on-device.md +6 -0
  56. package/dist/cli/plan-local.d.ts +6 -6
  57. package/dist/cli/plan-local.d.ts.map +1 -1
  58. package/dist/cli/plan-local.js +36 -7
  59. package/dist/cli/plan-local.js.map +1 -1
  60. package/dist/cli/pr-visual-recap-workflow.d.ts +2 -8
  61. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  62. package/dist/cli/pr-visual-recap-workflow.js +2 -8
  63. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  64. package/dist/cli/recap.d.ts +22 -1
  65. package/dist/cli/recap.d.ts.map +1 -1
  66. package/dist/cli/recap.js +286 -2
  67. package/dist/cli/recap.js.map +1 -1
  68. package/dist/cli/skills-content/local-files.d.ts +1 -1
  69. package/dist/cli/skills-content/local-files.d.ts.map +1 -1
  70. package/dist/cli/skills-content/local-files.js +17 -12
  71. package/dist/cli/skills-content/local-files.js.map +1 -1
  72. package/dist/client/analytics.d.ts +8 -0
  73. package/dist/client/analytics.d.ts.map +1 -1
  74. package/dist/client/analytics.js +77 -13
  75. package/dist/client/analytics.js.map +1 -1
  76. package/dist/client/index.d.ts +2 -1
  77. package/dist/client/index.d.ts.map +1 -1
  78. package/dist/client/index.js +2 -1
  79. package/dist/client/index.js.map +1 -1
  80. package/dist/client/session-replay.d.ts +2 -0
  81. package/dist/client/session-replay.d.ts.map +1 -1
  82. package/dist/client/session-replay.js +7 -0
  83. package/dist/client/session-replay.js.map +1 -1
  84. package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
  85. package/dist/client/settings/DemoModeSection.js +4 -23
  86. package/dist/client/settings/DemoModeSection.js.map +1 -1
  87. package/dist/client/url-scrub.d.ts.map +1 -1
  88. package/dist/client/url-scrub.js +14 -0
  89. package/dist/client/url-scrub.js.map +1 -1
  90. package/dist/client/use-demo-mode-status.d.ts +12 -0
  91. package/dist/client/use-demo-mode-status.d.ts.map +1 -0
  92. package/dist/client/use-demo-mode-status.js +30 -0
  93. package/dist/client/use-demo-mode-status.js.map +1 -0
  94. package/dist/collab/struct-routes.d.ts +1 -1
  95. package/dist/notifications/routes.d.ts +3 -3
  96. package/dist/observability/routes.d.ts +5 -5
  97. package/dist/progress/routes.d.ts +1 -1
  98. package/dist/resources/handlers.d.ts +3 -3
  99. package/dist/secrets/routes.d.ts +9 -9
  100. package/docs/content/locales/ar-SA/pr-visual-recap.mdx +25 -1
  101. package/docs/content/locales/ar-SA/template-plan.mdx +1 -1
  102. package/docs/content/locales/de-DE/pr-visual-recap.mdx +25 -1
  103. package/docs/content/locales/de-DE/template-plan.mdx +1 -1
  104. package/docs/content/locales/es-ES/pr-visual-recap.mdx +25 -1
  105. package/docs/content/locales/es-ES/template-plan.mdx +1 -1
  106. package/docs/content/locales/fr-FR/pr-visual-recap.mdx +25 -1
  107. package/docs/content/locales/fr-FR/template-plan.mdx +1 -1
  108. package/docs/content/locales/hi-IN/pr-visual-recap.mdx +25 -1
  109. package/docs/content/locales/hi-IN/template-plan.mdx +1 -1
  110. package/docs/content/locales/ja-JP/pr-visual-recap.mdx +25 -1
  111. package/docs/content/locales/ja-JP/template-plan.mdx +1 -1
  112. package/docs/content/locales/ko-KR/pr-visual-recap.mdx +25 -1
  113. package/docs/content/locales/ko-KR/template-plan.mdx +1 -1
  114. package/docs/content/locales/pt-BR/pr-visual-recap.mdx +25 -1
  115. package/docs/content/locales/pt-BR/template-plan.mdx +1 -1
  116. package/docs/content/locales/zh-CN/pr-visual-recap.mdx +25 -1
  117. package/docs/content/locales/zh-CN/template-plan.mdx +1 -1
  118. package/docs/content/locales/zh-TW/pr-visual-recap.mdx +25 -1
  119. package/docs/content/locales/zh-TW/template-plan.mdx +1 -1
  120. package/docs/content/pr-visual-recap.mdx +50 -1
  121. package/docs/content/template-plan.mdx +1 -1
  122. package/package.json +1 -1
@@ -89,6 +89,13 @@ export type ConfigureTrackingOptions = {
89
89
  /** First-party analytics track endpoint. */
90
90
  endpoint?: string;
91
91
  getDefaultProps?: GetDefaultProps;
92
+ /**
93
+ * Disable content-capturing analytics such as interaction autocapture and
94
+ * session replay while retaining pageviews, explicit events, and Sentry.
95
+ */
96
+ contentCapture?: boolean;
97
+ /** Resolve content capture synchronously for each browser pathname. */
98
+ contentCaptureForPath?: (pathname: string) => boolean;
92
99
  sessionReplay?: boolean | SessionReplayOptions;
93
100
  /**
94
101
  * First-party, Sentry-style error capture. Auto-captures uncaught errors
@@ -132,6 +139,8 @@ let _errorCaptureDisposer: (() => void) | null = null;
132
139
  let _sessionReplayModuleForCapture:
133
140
  | typeof import("./session-replay.js")
134
141
  | null = null;
142
+ let _trackingContentCaptureEnabled = true;
143
+ let _contentCaptureForPath: ((pathname: string) => boolean) | null = null;
135
144
  // Buffer for setSentryUser calls made before Sentry has initialized.
136
145
  // `undefined` means "no pending update"; `null` means "pending clear".
137
146
  let _pendingSentryUser: SentryUser | null | undefined = undefined;
@@ -575,7 +584,10 @@ function ensureAmplitude(): boolean {
575
584
  const key = (import.meta.env as Record<string, string | undefined>)
576
585
  ?.VITE_AMPLITUDE_API_KEY;
577
586
  if (!key) return false;
578
- amplitude.init(key, { autocapture: true });
587
+ // Standard pageviews and explicit events are emitted below. Keep SDK-level
588
+ // DOM/network autocapture off so rendered user content is never collected as
589
+ // an implicit analytics side effect.
590
+ amplitude.init(key, { autocapture: false });
579
591
  _amplitudeInitialized = true;
580
592
  return true;
581
593
  }
@@ -896,7 +908,11 @@ export function setSentryUser(
896
908
  clearTrackingIdentity();
897
909
  }
898
910
  _trackingIdentityResolved = true;
899
- if (shouldRetryReplay && _sessionReplayOptions?.requireSignedInUser) {
911
+ if (
912
+ shouldRetryReplay &&
913
+ _trackingContentCaptureEnabled &&
914
+ _sessionReplayOptions?.requireSignedInUser
915
+ ) {
900
916
  void startConfiguredSessionReplay(_sessionReplayOptions);
901
917
  }
902
918
  if (_sentryInitialized) {
@@ -1004,6 +1020,11 @@ export function configureTracking(options: ConfigureTrackingOptions): void {
1004
1020
  if (options.getDefaultProps) {
1005
1021
  _getDefaultProps = options.getDefaultProps;
1006
1022
  }
1023
+ _contentCaptureForPath = options.contentCaptureForPath ?? null;
1024
+ _trackingContentCaptureEnabled =
1025
+ _contentCaptureForPath && typeof window !== "undefined"
1026
+ ? _contentCaptureForPath(window.location.pathname)
1027
+ : options.contentCapture !== false;
1007
1028
  if (typeof window !== "undefined") {
1008
1029
  ensureSentry();
1009
1030
  ensureAmplitude();
@@ -1011,14 +1032,37 @@ export function configureTracking(options: ConfigureTrackingOptions): void {
1011
1032
  installLlmConnectionRefresh();
1012
1033
  installTrackingAuthSessionRefresh();
1013
1034
  installPageviewTracking();
1014
- maybeInstallSessionReplay(options.sessionReplay, {
1015
- endpoint: options.endpoint,
1016
- publicKey,
1017
- });
1035
+ maybeInstallSessionReplay(
1036
+ options.sessionReplay,
1037
+ {
1038
+ endpoint: options.endpoint,
1039
+ publicKey,
1040
+ },
1041
+ _trackingContentCaptureEnabled,
1042
+ );
1018
1043
  maybeInstallErrorCapture(options.errorCapture);
1019
1044
  }
1020
1045
  }
1021
1046
 
1047
+ export function setTrackingContentCaptureEnabled(enabled: boolean): void {
1048
+ if (_trackingContentCaptureEnabled === enabled) return;
1049
+ _trackingContentCaptureEnabled = enabled;
1050
+ if (enabled) {
1051
+ if (_sessionReplayOptions) {
1052
+ void startConfiguredSessionReplay(_sessionReplayOptions);
1053
+ }
1054
+ } else {
1055
+ void stopSessionReplay("content-capture-disabled");
1056
+ }
1057
+ }
1058
+
1059
+ function syncTrackingContentCaptureForLocation(): void {
1060
+ if (!_contentCaptureForPath) return;
1061
+ setTrackingContentCaptureEnabled(
1062
+ _contentCaptureForPath(window.location.pathname),
1063
+ );
1064
+ }
1065
+
1022
1066
  /**
1023
1067
  * Lazily load the session-replay module so error capture can read the active
1024
1068
  * replay id and surface manual captures on the replay timeline without a
@@ -1254,11 +1298,12 @@ function replayEndpointFromTrackingEndpoint(value: string): string | undefined {
1254
1298
  function maybeInstallSessionReplay(
1255
1299
  config: boolean | SessionReplayOptions | undefined,
1256
1300
  tracking?: { endpoint?: string; publicKey?: string },
1301
+ start = true,
1257
1302
  ): void {
1258
1303
  if (typeof window === "undefined") return;
1259
1304
  const options = configuredSessionReplayOptions(config, tracking);
1260
- if (!options) return;
1261
1305
  _sessionReplayOptions = options;
1306
+ if (!options || !start) return;
1262
1307
  void startConfiguredSessionReplay(options);
1263
1308
  }
1264
1309
 
@@ -1284,11 +1329,24 @@ async function startConfiguredSessionReplay(
1284
1329
  ): Promise<SessionReplayStartResult | null> {
1285
1330
  if (_sessionReplayStartPromise) return _sessionReplayStartPromise;
1286
1331
  _sessionReplayStartPromise = (async () => {
1332
+ if (!_trackingContentCaptureEnabled) {
1333
+ return { started: false, reason: "disabled" as const };
1334
+ }
1287
1335
  if (!(await waitForSessionReplayAuthIfRequired(options))) {
1288
1336
  return { started: false, reason: "missing-user-id" as const };
1289
1337
  }
1338
+ if (!_trackingContentCaptureEnabled) {
1339
+ return { started: false, reason: "disabled" as const };
1340
+ }
1290
1341
  const mod = await import("./session-replay.js");
1291
- return mod.startSessionReplay(options);
1342
+ if (!_trackingContentCaptureEnabled) {
1343
+ return { started: false, reason: "disabled" as const };
1344
+ }
1345
+ return mod.startSessionReplay({
1346
+ ...options,
1347
+ shouldStart: () =>
1348
+ _trackingContentCaptureEnabled && (options.shouldStart?.() ?? true),
1349
+ });
1292
1350
  })()
1293
1351
  .catch(() => ({ started: false, reason: "import-failed" as const }))
1294
1352
  .finally(() => {
@@ -1300,23 +1358,37 @@ async function startConfiguredSessionReplay(
1300
1358
  export async function startSessionReplay(
1301
1359
  options: SessionReplayOptions = {},
1302
1360
  ): Promise<SessionReplayStartResult> {
1361
+ if (!_trackingContentCaptureEnabled) {
1362
+ return { started: false, reason: "disabled" };
1363
+ }
1303
1364
  const configured = configuredSessionReplayOptions(options) ?? options;
1304
1365
  if (!(await waitForSessionReplayAuthIfRequired(configured))) {
1305
1366
  return { started: false, reason: "missing-user-id" };
1306
1367
  }
1307
1368
  const mod = await import("./session-replay.js");
1308
- return mod.startSessionReplay(configured);
1369
+ return mod.startSessionReplay({
1370
+ ...configured,
1371
+ shouldStart: () =>
1372
+ _trackingContentCaptureEnabled && (configured.shouldStart?.() ?? true),
1373
+ });
1309
1374
  }
1310
1375
 
1311
1376
  export async function maybeStartSessionReplay(
1312
1377
  options: SessionReplayOptions = {},
1313
1378
  ): Promise<SessionReplayStartResult> {
1379
+ if (!_trackingContentCaptureEnabled) {
1380
+ return { started: false, reason: "disabled" };
1381
+ }
1314
1382
  const configured = configuredSessionReplayOptions(options) ?? options;
1315
1383
  if (!(await waitForSessionReplayAuthIfRequired(configured))) {
1316
1384
  return { started: false, reason: "missing-user-id" };
1317
1385
  }
1318
1386
  const mod = await import("./session-replay.js");
1319
- return mod.maybeStartSessionReplay(configured);
1387
+ return mod.maybeStartSessionReplay({
1388
+ ...configured,
1389
+ shouldStart: () =>
1390
+ _trackingContentCaptureEnabled && (configured.shouldStart?.() ?? true),
1391
+ });
1320
1392
  }
1321
1393
 
1322
1394
  export async function stopSessionReplay(reason = "manual"): Promise<void> {
@@ -1371,15 +1443,17 @@ function pageviewKey(): string {
1371
1443
 
1372
1444
  function pageviewProperties(reason: string): Record<string, unknown> {
1373
1445
  const properties: Record<string, unknown> = {
1374
- url: scrubUrl(window.location.href),
1446
+ url: !_trackingContentCaptureEnabled
1447
+ ? window.location.origin + window.location.pathname
1448
+ : scrubUrl(window.location.href),
1375
1449
  path: window.location.pathname,
1376
1450
  hostname: window.location.hostname,
1377
1451
  navigation_type: reason,
1378
1452
  };
1379
- if (window.location.search) {
1453
+ if (_trackingContentCaptureEnabled && window.location.search) {
1380
1454
  properties.search = scrubUrl(window.location.search);
1381
1455
  }
1382
- if (typeof document !== "undefined") {
1456
+ if (_trackingContentCaptureEnabled && typeof document !== "undefined") {
1383
1457
  if (document.referrer) {
1384
1458
  properties.referrer = scrubUrl(document.referrer);
1385
1459
  }
@@ -1400,6 +1474,9 @@ function emitPageview(reason: string): void {
1400
1474
  }
1401
1475
 
1402
1476
  function schedulePageview(reason: string): void {
1477
+ if (!_trackingContentCaptureEnabled) {
1478
+ void stopSessionReplay("local-plan-privacy");
1479
+ }
1403
1480
  const run = () => emitPageview(reason);
1404
1481
  const pendingStartupContext: Array<Promise<void>> = [];
1405
1482
  if (_llmConnectionRefresh && !_llmConnectionStatus) {
@@ -1436,17 +1513,22 @@ function installPageviewTracking(): void {
1436
1513
 
1437
1514
  window.history.pushState = function pushState(...args) {
1438
1515
  const result = originalPushState.apply(this, args);
1516
+ syncTrackingContentCaptureForLocation();
1439
1517
  schedulePageview("pushState");
1440
1518
  return result;
1441
1519
  };
1442
1520
 
1443
1521
  window.history.replaceState = function replaceState(...args) {
1444
1522
  const result = originalReplaceState.apply(this, args);
1523
+ syncTrackingContentCaptureForLocation();
1445
1524
  schedulePageview("replaceState");
1446
1525
  return result;
1447
1526
  };
1448
1527
 
1449
- window.addEventListener("popstate", () => schedulePageview("popstate"));
1528
+ window.addEventListener("popstate", () => {
1529
+ syncTrackingContentCaptureForLocation();
1530
+ schedulePageview("popstate");
1531
+ });
1450
1532
  }
1451
1533
 
1452
1534
  function sendAgentNativeAnalytics(
@@ -192,6 +192,10 @@ export {
192
192
  getChangeVersion,
193
193
  bumpChangeVersion,
194
194
  } from "./use-change-version.js";
195
+ export {
196
+ useDemoModeStatus,
197
+ type DemoModeStatus,
198
+ } from "./use-demo-mode-status.js";
195
199
  export { useReconciledState } from "./use-external-value.js";
196
200
  export {
197
201
  buildDynamicAgentSuggestions,
@@ -751,6 +755,7 @@ export {
751
755
  trackEvent,
752
756
  trackSessionStatus,
753
757
  configureTracking,
758
+ setTrackingContentCaptureEnabled,
754
759
  maybeStartSessionReplay,
755
760
  startSessionReplay,
756
761
  stopSessionReplay,
@@ -109,6 +109,8 @@ export interface SessionReplayNetworkOptions {
109
109
 
110
110
  export interface SessionReplayOptions {
111
111
  enabled?: boolean;
112
+ /** Rechecked immediately before rrweb starts to cancel deferred startup. */
113
+ shouldStart?: () => boolean;
112
114
  publicKey?: string;
113
115
  endpoint?: string;
114
116
  requireSignedInUser?: boolean;
@@ -208,6 +210,7 @@ interface NormalizedSessionReplayOptions {
208
210
  /** Null disables network capture entirely. */
209
211
  network: NormalizedCaptureOptions | null;
210
212
  extraProperties?: SessionReplayOptions["extraProperties"];
213
+ shouldStart?: SessionReplayOptions["shouldStart"];
211
214
  }
212
215
 
213
216
  interface NormalizedCaptureOptions {
@@ -656,6 +659,7 @@ function normalizeOptions(
656
659
  DEFAULT_MAX_NETWORK_EVENTS,
657
660
  ),
658
661
  extraProperties: options.extraProperties,
662
+ shouldStart: options.shouldStart,
659
663
  };
660
664
  }
661
665
 
@@ -1950,6 +1954,9 @@ export async function startSessionReplay(
1950
1954
  options: SessionReplayOptions = {},
1951
1955
  ): Promise<SessionReplayStartResult> {
1952
1956
  if (options.enabled === false) return { started: false, reason: "disabled" };
1957
+ if (options.shouldStart && !options.shouldStart()) {
1958
+ return { started: false, reason: "disabled" };
1959
+ }
1953
1960
  if (typeof window === "undefined" || typeof document === "undefined") {
1954
1961
  return { started: false, reason: "not-browser" };
1955
1962
  }
@@ -2032,6 +2039,9 @@ async function startSessionReplayRecorder(
2032
2039
  } catch {
2033
2040
  return { started: false, reason: "import-failed", sessionId, sampled };
2034
2041
  }
2042
+ if (normalized.shouldStart && !normalized.shouldStart()) {
2043
+ return { started: false, reason: "disabled", sessionId, sampled };
2044
+ }
2035
2045
 
2036
2046
  const replaySession = getOrCreateReplaySession(sessionId);
2037
2047
  state.options = normalized;
@@ -13,40 +13,18 @@
13
13
  */
14
14
 
15
15
  import { IconEyeOff } from "@tabler/icons-react";
16
- import { useQuery } from "@tanstack/react-query";
17
16
  import { useEffect, useState } from "react";
18
17
 
19
18
  import { agentNativePath } from "../api-path.js";
20
- import { useChangeVersion } from "../use-change-version.js";
19
+ import { useDemoModeStatus } from "../use-demo-mode-status.js";
21
20
 
22
- interface DemoStatus {
23
- enabled?: boolean;
24
- forced?: boolean;
25
- }
26
-
27
- const DEMO_STATUS_URL = agentNativePath("/_agent-native/demo/status");
28
21
  const DEMO_MODE_WRITE_URL = agentNativePath(
29
22
  "/_agent-native/application-state/demo-mode",
30
23
  );
31
24
 
32
25
  export function DemoModeSection() {
33
26
  const [enabled, setEnabled] = useState<boolean | null>(null);
34
- const demoModeVersion = useChangeVersion("app-state:demo-mode");
35
-
36
- const { data } = useQuery({
37
- queryKey: ["agent-native", "demo-mode", demoModeVersion],
38
- queryFn: async () => {
39
- const res = await fetch(DEMO_STATUS_URL, {
40
- credentials: "same-origin",
41
- });
42
- if (!res.ok) return null;
43
- return (await res.json()) as DemoStatus | null;
44
- },
45
- staleTime: Infinity,
46
- });
47
-
48
- const serverEnabled = data?.enabled;
49
- const forced = data?.forced === true;
27
+ const { enabled: serverEnabled, forced, isLoading } = useDemoModeStatus();
50
28
 
51
29
  // Surface the server value once it arrives (and on subsequent polls), but
52
30
  // never clobber an in-flight optimistic toggle with a stale read. When
@@ -54,12 +32,10 @@ export function DemoModeSection() {
54
32
  useEffect(() => {
55
33
  if (forced) {
56
34
  setEnabled(true);
57
- } else if (typeof serverEnabled === "boolean") {
35
+ } else if (!isLoading) {
58
36
  setEnabled((prev) => (prev === null ? serverEnabled : prev));
59
- } else if (serverEnabled === undefined && data !== undefined) {
60
- setEnabled((prev) => (prev === null ? false : prev));
61
37
  }
62
- }, [serverEnabled, forced, data]);
38
+ }, [serverEnabled, forced, isLoading]);
63
39
 
64
40
  const toggle = async (next: boolean) => {
65
41
  if (forced) return;
@@ -12,6 +12,7 @@ const SENSITIVE_QUERY_PARAMS = new Set([
12
12
  "code",
13
13
  "share",
14
14
  "share_token",
15
+ "bridge",
15
16
  ]);
16
17
 
17
18
  export function scrubUrl(url: string | undefined): string | undefined {
@@ -26,6 +27,18 @@ export function scrubUrl(url: string | undefined): string | undefined {
26
27
  mutated = true;
27
28
  }
28
29
  }
30
+ if (u.hash.includes("=")) {
31
+ const hashParams = new URLSearchParams(u.hash.slice(1));
32
+ let hashMutated = false;
33
+ for (const key of Array.from(hashParams.keys())) {
34
+ if (SENSITIVE_QUERY_PARAMS.has(key.toLowerCase())) {
35
+ hashParams.set(key, "<redacted>");
36
+ mutated = true;
37
+ hashMutated = true;
38
+ }
39
+ }
40
+ if (hashMutated) u.hash = hashParams.toString();
41
+ }
29
42
  if (!mutated) return url;
30
43
  // If the original URL was relative, return only the path/query/fragment.
31
44
  if (u.origin === "http://placeholder.local") {
@@ -0,0 +1,44 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+
3
+ import { agentNativePath } from "./api-path.js";
4
+ import { useChangeVersion } from "./use-change-version.js";
5
+
6
+ interface DemoModeStatusResponse {
7
+ enabled?: boolean;
8
+ forced?: boolean;
9
+ }
10
+
11
+ export interface DemoModeStatus {
12
+ enabled: boolean;
13
+ forced: boolean;
14
+ isLoading: boolean;
15
+ }
16
+
17
+ const DEMO_STATUS_URL = agentNativePath("/_agent-native/demo/status");
18
+
19
+ /**
20
+ * Reads the effective Demo mode status for the current user. The shared
21
+ * change-version stream advances after Demo mode application state changes,
22
+ * so mounted consumers refresh without polling or duplicating the route call.
23
+ */
24
+ export function useDemoModeStatus(): DemoModeStatus {
25
+ const demoModeVersion = useChangeVersion("app-state:demo-mode");
26
+
27
+ const { data, isLoading } = useQuery({
28
+ queryKey: ["agent-native", "demo-mode", demoModeVersion],
29
+ queryFn: async () => {
30
+ const res = await fetch(DEMO_STATUS_URL, {
31
+ credentials: "same-origin",
32
+ });
33
+ if (!res.ok) return null;
34
+ return (await res.json()) as DemoModeStatusResponse | null;
35
+ },
36
+ staleTime: Infinity,
37
+ });
38
+
39
+ return {
40
+ enabled: data?.enabled === true,
41
+ forced: data?.forced === true,
42
+ isLoading,
43
+ };
44
+ }
@@ -39,6 +39,14 @@ details live in `.agents/skills/`.
39
39
  Prometheus credential slot. Treat it as demo-environment data: do not use it
40
40
  for `REAL_DATA_REQUIRED`, saved analyses, or real user analytics answers
41
41
  unless the user explicitly asks to inspect the demo dashboard.
42
+ - Framework Demo mode is separate from the built-in `demo` source. While Demo
43
+ mode is enabled, Analytics line and area charts reshape each returned numeric
44
+ series into a deterministic, natural-looking upward trend using that series'
45
+ actual minimum, maximum, and normalized volatility. Smooth source series stay
46
+ smooth; spiky source series keep larger swings. This is presentation-only:
47
+ query results, tables, metrics, exports, and non-time-series charts remain
48
+ unchanged, so do not cite the displayed intermediate line/area points as
49
+ retrieved source values.
42
50
  - Every analytical answer should include enough audit context for the user to
43
51
  trust it: source(s), time window, filters, sample size or row count,
44
52
  join/match method when relevant, and caveats/gaps.
@@ -1,4 +1,4 @@
1
- import { useT } from "@agent-native/core/client";
1
+ import { useDemoModeStatus, useT } from "@agent-native/core/client";
2
2
  import { EmbeddedExtension } from "@agent-native/core/client/extensions";
3
3
  import {
4
4
  IconArrowsSort,
@@ -54,6 +54,7 @@ import {
54
54
  import { Skeleton } from "@/components/ui/skeleton";
55
55
 
56
56
  const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
57
+ import { createDemoChartTrendRows } from "@/lib/demo-chart-trend";
57
58
  import { useSqlQuery } from "@/lib/sql-query";
58
59
  import { serializePanelSql } from "@/pages/adhoc/sql-dashboard/panel-sql";
59
60
  import { pivotRows } from "@/pages/adhoc/sql-dashboard/pivot";
@@ -994,6 +995,7 @@ export function SqlChart({
994
995
  onExportCsvChange,
995
996
  }: SqlChartProps) {
996
997
  const t = useT();
998
+ const { enabled: demoModeEnabled } = useDemoModeStatus();
997
999
  // Hooks must be called unconditionally before any early return.
998
1000
  const isSection = panel.chartType === "section";
999
1001
  const isExtension = panel.chartType === "extension";
@@ -1024,7 +1026,7 @@ export function SqlChart({
1024
1026
  const error =
1025
1027
  rawRows.length === 0 ? (result?.error ?? queryErrorMessage) : undefined;
1026
1028
 
1027
- const { rows, forcedYKeys } = useMemo(() => {
1029
+ const { rows: queryRows, forcedYKeys } = useMemo(() => {
1028
1030
  if (panel.config?.pivot && rawRows.length) {
1029
1031
  const pivoted = pivotRows(rawRows, panel.config.pivot);
1030
1032
  return { rows: pivoted.rows, forcedYKeys: pivoted.seriesKeys };
@@ -1033,8 +1035,20 @@ export function SqlChart({
1033
1035
  }, [rawRows, panel.config?.pivot]);
1034
1036
 
1035
1037
  const { xKey, yKeys } = useMemo(
1036
- () => detectKeys(rows, panel.config, forcedYKeys),
1037
- [rows, panel.config, forcedYKeys],
1038
+ () => detectKeys(queryRows, panel.config, forcedYKeys),
1039
+ [queryRows, panel.config, forcedYKeys],
1040
+ );
1041
+ const shouldCreateDemoTrend =
1042
+ demoModeEnabled &&
1043
+ (panel.chartType === "line" ||
1044
+ panel.chartType === "area" ||
1045
+ (panel.chartType as string) === "stacked-area");
1046
+ const rows = useMemo(
1047
+ () =>
1048
+ shouldCreateDemoTrend
1049
+ ? createDemoChartTrendRows(queryRows, yKeys, panel.id)
1050
+ : queryRows,
1051
+ [queryRows, yKeys, panel.id, shouldCreateDemoTrend],
1038
1052
  );
1039
1053
 
1040
1054
  // Section panels are pure layout — no query, no chart. Render a header with
@@ -0,0 +1,156 @@
1
+ type DemoTrendSeed = string | number;
2
+
3
+ function hashSeed(seed: string): number {
4
+ let hash = 2166136261;
5
+ for (let index = 0; index < seed.length; index += 1) {
6
+ hash ^= seed.charCodeAt(index);
7
+ hash = Math.imul(hash, 16777619);
8
+ }
9
+ return hash >>> 0;
10
+ }
11
+
12
+ function createRandom(seed: string): () => number {
13
+ let state = hashSeed(seed);
14
+ return () => {
15
+ state += 0x6d2b79f5;
16
+ let value = state;
17
+ value = Math.imul(value ^ (value >>> 15), value | 1);
18
+ value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
19
+ return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
20
+ };
21
+ }
22
+
23
+ function clamp(value: number, minimum: number, maximum: number): number {
24
+ return Math.min(maximum, Math.max(minimum, value));
25
+ }
26
+
27
+ function numericValue(value: unknown): number | null {
28
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
29
+ if (typeof value !== "string" || value.trim() === "") return null;
30
+ const numeric = Number(value);
31
+ return Number.isFinite(numeric) ? numeric : null;
32
+ }
33
+
34
+ function normalizedVolatility(
35
+ series: number[],
36
+ minimum: number,
37
+ range: number,
38
+ ) {
39
+ if (series.length < 3 || range === 0) return 0;
40
+ const normalized = series.map((value) => (value - minimum) / range);
41
+ const averageStep =
42
+ (normalized[normalized.length - 1] - normalized[0]) /
43
+ (normalized.length - 1);
44
+ const deviation = normalized
45
+ .slice(1)
46
+ .reduce(
47
+ (total, value, index) =>
48
+ total + Math.abs(value - normalized[index] - averageStep),
49
+ 0,
50
+ );
51
+ return clamp((deviation / (normalized.length - 1)) * 1.4, 0, 1);
52
+ }
53
+
54
+ function normalizedTrend(
55
+ length: number,
56
+ volatilityScore: number,
57
+ random: () => number,
58
+ ): number[] {
59
+ if (length <= 1) return [0];
60
+ if (length === 2) return [0, 1];
61
+
62
+ const volatility = 0.015 + volatilityScore * (0.22 + random() * 0.12);
63
+ const noiseMemory = 0.78 - volatilityScore * 0.58;
64
+ let noise = 0;
65
+ const values = Array.from({ length }, (_, index) => {
66
+ if (index === 0) return 0;
67
+ if (index === length - 1) return 1;
68
+
69
+ const progress = index / (length - 1);
70
+ noise = noise * noiseMemory + (random() * 2 - 1) * (1 - noiseMemory);
71
+ const taperedNoise = noise * volatility * Math.sin(Math.PI * progress);
72
+ return clamp(progress + taperedNoise, 0.025, 0.975);
73
+ });
74
+
75
+ // Pullback count and depth follow the source's normalized volatility. A
76
+ // smooth source remains a gentle rise; a jagged source gets several seeded
77
+ // reversals. Spacing the reversals across the series prevents them from
78
+ // collapsing into one noisy patch.
79
+ if (length >= 5 && volatilityScore >= 0.12) {
80
+ const maximumPullbacks = Math.min(3, Math.floor((length - 2) / 2));
81
+ const pullbackCount = Math.max(
82
+ 1,
83
+ Math.round(volatilityScore * maximumPullbacks),
84
+ );
85
+ for (let pullback = 0; pullback < pullbackCount; pullback += 1) {
86
+ const segmentCenter =
87
+ ((pullback + 1) * (length - 3)) / (pullbackCount + 1) + 1;
88
+ const jitter = (random() - 0.5) * Math.max(1, length / 10);
89
+ const dipIndex = Math.round(clamp(segmentCenter + jitter, 2, length - 2));
90
+ const center = dipIndex / (length - 1);
91
+ const amplitude = 0.025 + volatilityScore * (0.075 + random() * 0.12);
92
+ values[dipIndex - 1] = clamp(center + amplitude, 0.075, 0.925);
93
+ values[dipIndex] = clamp(center - amplitude, 0.05, 0.9);
94
+ }
95
+ }
96
+
97
+ return values;
98
+ }
99
+
100
+ /**
101
+ * Replace numeric chart series with a stable, seeded upward trend while
102
+ * retaining the query's original range and every non-series field. This is a
103
+ * presentation-only demo-mode transform: callers keep the real query result
104
+ * and opt individual line/area renderers into the returned rows.
105
+ */
106
+ export function createDemoChartTrendRows(
107
+ rows: Record<string, unknown>[],
108
+ yKeys: string[],
109
+ seed: DemoTrendSeed,
110
+ ): Record<string, unknown>[] {
111
+ if (rows.length === 0 || yKeys.length === 0) return rows;
112
+
113
+ let output: Record<string, unknown>[] | null = null;
114
+
115
+ for (const yKey of yKeys) {
116
+ const points = rows.flatMap((row, rowIndex) => {
117
+ const numeric = numericValue(row[yKey]);
118
+ return numeric === null
119
+ ? []
120
+ : [{ rowIndex, numeric, original: row[yKey] }];
121
+ });
122
+
123
+ if (points.length < 2) continue;
124
+
125
+ const minimum = Math.min(...points.map((point) => point.numeric));
126
+ const maximum = Math.max(...points.map((point) => point.numeric));
127
+ if (minimum === maximum) continue;
128
+
129
+ const random = createRandom(`${String(seed)}:${yKey}`);
130
+ const range = maximum - minimum;
131
+ const volatilityScore = normalizedVolatility(
132
+ points.map((point) => point.numeric),
133
+ minimum,
134
+ range,
135
+ );
136
+ const trend = normalizedTrend(points.length, volatilityScore, random);
137
+ const useStringValues = points.every(
138
+ (point) => typeof point.original === "string",
139
+ );
140
+
141
+ output ??= rows.map((row) => ({ ...row }));
142
+ points.forEach((point, pointIndex) => {
143
+ const numeric =
144
+ pointIndex === 0
145
+ ? minimum
146
+ : pointIndex === points.length - 1
147
+ ? maximum
148
+ : minimum + trend[pointIndex] * range;
149
+ output![point.rowIndex][yKey] = useStringValues
150
+ ? String(numeric)
151
+ : numeric;
152
+ });
153
+ }
154
+
155
+ return output ?? rows;
156
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-12
4
+ ---
5
+
6
+ Daily dashboard emails reliably include the complete dashboard image.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-12
4
+ ---
5
+
6
+ Demo mode line and area charts now show unique upward trends while preserving each series' original range and volatility.
@@ -56,6 +56,8 @@ export async function runDashboardReportsOnce(): Promise<{
56
56
  `[dashboard-report] Subscription ${sub.id} sent without a screenshot:`,
57
57
  message,
58
58
  );
59
+ await markDashboardReportResult(sub, "success", message);
60
+ continue;
59
61
  }
60
62
  await markDashboardReportResult(sub, "success");
61
63
  } catch (err: any) {