@agent-native/core 0.99.1 → 0.99.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 (148) 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/agent/production-agent.ts +12 -38
  5. package/corpus/core/src/audit/config.ts +0 -1
  6. package/corpus/core/src/chat-threads/store.ts +57 -41
  7. package/corpus/core/src/client/AssistantChat.tsx +1 -2
  8. package/corpus/core/src/client/chat/message-components.tsx +15 -3
  9. package/corpus/core/src/client/chat/tool-call-display.tsx +18 -1
  10. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +5 -9
  11. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +0 -133
  12. package/corpus/core/src/client/index.ts +7 -0
  13. package/corpus/core/src/client/settings/DemoModeSection.tsx +9 -65
  14. package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
  15. package/corpus/core/src/client/use-db-sync.ts +3 -15
  16. package/corpus/core/src/client/use-demo-mode-status.ts +16 -30
  17. package/corpus/core/src/demo/browser-state.ts +47 -0
  18. package/corpus/core/src/demo/fetch-interceptor.ts +12 -59
  19. package/corpus/core/src/deploy/workspace-deploy.ts +2 -2
  20. package/corpus/core/src/server/action-discovery.ts +0 -1
  21. package/corpus/core/src/server/core-routes-plugin.ts +0 -30
  22. package/corpus/core/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  23. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +48 -7
  24. package/corpus/templates/analytics/AGENTS.md +4 -4
  25. package/corpus/templates/analytics/actions/compose-dashboard.ts +12 -1
  26. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +15 -4
  27. package/corpus/templates/analytics/actions/mutate-dashboard.ts +2 -1
  28. package/corpus/templates/analytics/actions/update-dashboard.ts +14 -0
  29. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -27
  30. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -1
  31. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +21 -0
  32. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +14 -0
  33. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +31 -12
  34. package/corpus/templates/analytics/changelog/2026-07-13-dashboard-charts-now-honor-the-selected-time-range-by-defaul.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-07-13-keep-real-authorized-identities-in-analytics-incident-lookup.md +6 -0
  36. package/corpus/templates/analytics/changelog/2026-07-13-the-analytics-agents-tab-is-available-again-from-the-main-na.md +6 -0
  37. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
  38. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +4 -0
  39. package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +170 -0
  40. package/corpus/templates/analytics/server/lib/error-capture.ts +6 -62
  41. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +20 -5
  42. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +1 -1
  43. package/corpus/templates/analytics/server/lib/session-replay.ts +5 -111
  44. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -0
  45. package/corpus/templates/assets/app/routes/library.tsx +3 -4
  46. package/corpus/templates/assets/changelog/2026-07-13-internal-library-links-open-in-new-tabs.md +6 -0
  47. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +43 -40
  48. package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +6 -8
  49. package/corpus/templates/calendar/changelog/2026-07-13-booking-links-and-previews-open-in-new-tabs.md +6 -0
  50. package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +4 -7
  51. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +19 -9
  52. package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -3
  53. package/corpus/templates/clips/changelog/2026-07-13-recording-links-open-in-new-tabs.md +6 -0
  54. package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +6 -8
  55. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +5 -3
  56. package/corpus/templates/content/app/components/editor/database/FormView.tsx +6 -9
  57. package/corpus/templates/content/changelog/2026-07-13-document-links-open-in-new-tabs.md +6 -0
  58. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +33 -16
  59. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -1
  60. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +4 -4
  61. package/corpus/templates/design/app/pages/DesignSystems.tsx +8 -10
  62. package/corpus/templates/design/app/pages/Index.tsx +6 -1
  63. package/corpus/templates/design/app/pages/Present.tsx +12 -6
  64. package/corpus/templates/design/changelog/2026-07-13-design-navigation-links-open-in-new-tabs.md +6 -0
  65. package/corpus/templates/design/changelog/2026-07-13-design-session-replays-include-preview-iframes.md +6 -0
  66. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -11
  67. package/corpus/templates/forms/app/pages/FormsListPage.tsx +6 -8
  68. package/corpus/templates/forms/changelog/2026-07-13-form-navigation-links-open-in-new-tabs.md +6 -0
  69. package/corpus/templates/mail/app/pages/DraftQueuePage.tsx +3 -8
  70. package/corpus/templates/mail/changelog/2026-07-13-settings-links-open-in-new-tabs.md +6 -0
  71. package/dist/agent/production-agent.d.ts.map +1 -1
  72. package/dist/agent/production-agent.js +13 -37
  73. package/dist/agent/production-agent.js.map +1 -1
  74. package/dist/audit/config.d.ts.map +1 -1
  75. package/dist/audit/config.js +0 -1
  76. package/dist/audit/config.js.map +1 -1
  77. package/dist/chat-threads/store.d.ts.map +1 -1
  78. package/dist/chat-threads/store.js +47 -33
  79. package/dist/chat-threads/store.js.map +1 -1
  80. package/dist/client/AssistantChat.d.ts.map +1 -1
  81. package/dist/client/AssistantChat.js +1 -1
  82. package/dist/client/AssistantChat.js.map +1 -1
  83. package/dist/client/chat/message-components.d.ts.map +1 -1
  84. package/dist/client/chat/message-components.js +9 -3
  85. package/dist/client/chat/message-components.js.map +1 -1
  86. package/dist/client/chat/tool-call-display.d.ts +3 -1
  87. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  88. package/dist/client/chat/tool-call-display.js +10 -2
  89. package/dist/client/chat/tool-call-display.js.map +1 -1
  90. package/dist/client/extensions/EmbeddedExtension.js +2 -6
  91. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  92. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  93. package/dist/client/extensions/ExtensionViewer.js +2 -59
  94. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  95. package/dist/client/index.d.ts +2 -0
  96. package/dist/client/index.d.ts.map +1 -1
  97. package/dist/client/index.js +2 -0
  98. package/dist/client/index.js.map +1 -1
  99. package/dist/client/settings/DemoModeSection.d.ts +1 -13
  100. package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
  101. package/dist/client/settings/DemoModeSection.js +5 -52
  102. package/dist/client/settings/DemoModeSection.js.map +1 -1
  103. package/dist/client/settings/SettingsPanel.js +1 -1
  104. package/dist/client/settings/SettingsPanel.js.map +1 -1
  105. package/dist/client/use-db-sync.d.ts.map +1 -1
  106. package/dist/client/use-db-sync.js +3 -7
  107. package/dist/client/use-db-sync.js.map +1 -1
  108. package/dist/client/use-demo-mode-status.d.ts +3 -4
  109. package/dist/client/use-demo-mode-status.d.ts.map +1 -1
  110. package/dist/client/use-demo-mode-status.js +8 -23
  111. package/dist/client/use-demo-mode-status.js.map +1 -1
  112. package/dist/collab/awareness.d.ts +2 -2
  113. package/dist/collab/awareness.d.ts.map +1 -1
  114. package/dist/collab/routes.d.ts +1 -1
  115. package/dist/collab/struct-routes.d.ts +1 -1
  116. package/dist/demo/browser-state.d.ts +10 -0
  117. package/dist/demo/browser-state.d.ts.map +1 -0
  118. package/dist/demo/browser-state.js +49 -0
  119. package/dist/demo/browser-state.js.map +1 -0
  120. package/dist/demo/fetch-interceptor.d.ts +2 -7
  121. package/dist/demo/fetch-interceptor.d.ts.map +1 -1
  122. package/dist/demo/fetch-interceptor.js +12 -56
  123. package/dist/demo/fetch-interceptor.js.map +1 -1
  124. package/dist/deploy/workspace-deploy.js +2 -2
  125. package/dist/deploy/workspace-deploy.js.map +1 -1
  126. package/dist/notifications/routes.d.ts +2 -2
  127. package/dist/progress/routes.d.ts +1 -1
  128. package/dist/resources/handlers.d.ts +3 -3
  129. package/dist/server/action-discovery.d.ts.map +1 -1
  130. package/dist/server/action-discovery.js +0 -1
  131. package/dist/server/action-discovery.js.map +1 -1
  132. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  133. package/dist/server/core-routes-plugin.js +0 -27
  134. package/dist/server/core-routes-plugin.js.map +1 -1
  135. package/dist/server/transcribe-voice.d.ts +1 -1
  136. package/dist/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  137. package/package.json +1 -1
  138. package/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  139. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +0 -23
  140. package/corpus/core/src/demo/config.ts +0 -57
  141. package/dist/demo/actions/toggle-demo-mode.d.ts +0 -8
  142. package/dist/demo/actions/toggle-demo-mode.d.ts.map +0 -1
  143. package/dist/demo/actions/toggle-demo-mode.js +0 -21
  144. package/dist/demo/actions/toggle-demo-mode.js.map +0 -1
  145. package/dist/demo/config.d.ts +0 -9
  146. package/dist/demo/config.d.ts.map +0 -1
  147. package/dist/demo/config.js +0 -58
  148. package/dist/demo/config.js.map +0 -1
@@ -18,7 +18,6 @@ import { fileURLToPath } from "node:url";
18
18
  * reads go through `accessFilter` so an org-scoped key surfaces its issues to
19
19
  * the whole org exactly like session recordings.
20
20
  */
21
- import { appStateGet } from "@agent-native/core/application-state";
22
21
  import { notifyWithDelivery } from "@agent-native/core/notifications";
23
22
  import { recordChange } from "@agent-native/core/server";
24
23
  import { accessFilter } from "@agent-native/core/sharing";
@@ -1099,56 +1098,6 @@ export interface ListErrorIssuesFilters {
1099
1098
  limit?: number;
1100
1099
  }
1101
1100
 
1102
- export const ERROR_REPORTING_ANONYMOUS_EMAIL = "anonymous@builder.io";
1103
- const ERROR_REPORTING_EMAIL_PATTERN = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
1104
-
1105
- /**
1106
- * Error reports contain emails in identity fields, messages, stacks, URLs,
1107
- * tags, extra context, and breadcrumbs. Redact every string at this owning
1108
- * read seam so demo mode never depends on browser interception timing.
1109
- */
1110
- export function anonymizeErrorReportingEmails<T>(value: T): T {
1111
- if (typeof value === "string") {
1112
- return value.replace(
1113
- ERROR_REPORTING_EMAIL_PATTERN,
1114
- ERROR_REPORTING_ANONYMOUS_EMAIL,
1115
- ) as T;
1116
- }
1117
- if (Array.isArray(value)) {
1118
- return value.map((item) => anonymizeErrorReportingEmails(item)) as T;
1119
- }
1120
- if (value && typeof value === "object") {
1121
- if (value instanceof Date) return value;
1122
- const output: Record<string, unknown> = {};
1123
- for (const [key, entry] of Object.entries(value)) {
1124
- output[key] = anonymizeErrorReportingEmails(entry);
1125
- }
1126
- return output as T;
1127
- }
1128
- return value;
1129
- }
1130
-
1131
- async function isErrorDemoModeEnabled(
1132
- userEmail?: string | null,
1133
- ): Promise<boolean> {
1134
- if (process.env.DEMO_MODE === "true") return true;
1135
- if (!userEmail) return false;
1136
- try {
1137
- // Keep demo state tied to the caller whose issues are being read.
1138
- const state = await appStateGet(userEmail, "demo-mode");
1139
- return state?.enabled === true;
1140
- } catch {
1141
- return false;
1142
- }
1143
- }
1144
-
1145
- function anonymizeErrorReportingEmailsInDemoMode<T>(
1146
- value: T,
1147
- enabled: boolean,
1148
- ): T {
1149
- return enabled ? anonymizeErrorReportingEmails(value) : value;
1150
- }
1151
-
1152
1101
  export interface ErrorIssueSummary {
1153
1102
  id: string;
1154
1103
  fingerprint: string;
@@ -1221,7 +1170,6 @@ export async function listErrorIssues(
1221
1170
  scope: ErrorReadScope,
1222
1171
  filters: ListErrorIssuesFilters = {},
1223
1172
  ): Promise<ErrorIssueSummary[]> {
1224
- const demoModeEnabled = isErrorDemoModeEnabled(scope.userEmail);
1225
1173
  const db = getDb() as any;
1226
1174
  const limit = Math.min(
1227
1175
  MAX_ISSUE_LIMIT,
@@ -1312,7 +1260,7 @@ export async function listErrorIssues(
1312
1260
  template: row.template ?? null,
1313
1261
  sparkline: sparklines.get(row.id) ?? new Array(SPARKLINE_DAYS).fill(0),
1314
1262
  }));
1315
- return anonymizeErrorReportingEmailsInDemoMode(issues, await demoModeEnabled);
1263
+ return issues;
1316
1264
  }
1317
1265
 
1318
1266
  export interface ErrorEventDetail {
@@ -1404,7 +1352,6 @@ export async function getErrorIssue(
1404
1352
  issueId: string,
1405
1353
  options: { eventsLimit?: number } = {},
1406
1354
  ): Promise<ErrorIssueDetail> {
1407
- const demoModeEnabled = isErrorDemoModeEnabled(scope.userEmail);
1408
1355
  const db = getDb() as any;
1409
1356
  const [issueRow] = await db
1410
1357
  .select()
@@ -1530,14 +1477,11 @@ export async function getErrorIssue(
1530
1477
  sparkline: sparklines.get(issueId) ?? new Array(SPARKLINE_DAYS).fill(0),
1531
1478
  };
1532
1479
 
1533
- return anonymizeErrorReportingEmailsInDemoMode(
1534
- {
1535
- issue,
1536
- events,
1537
- sessions: Array.from(sessions.values()),
1538
- },
1539
- await demoModeEnabled,
1540
- );
1480
+ return {
1481
+ issue,
1482
+ events,
1483
+ sessions: Array.from(sessions.values()),
1484
+ };
1541
1485
  }
1542
1486
 
1543
1487
  export interface UpdateErrorIssueInput {
@@ -163,6 +163,7 @@ const DASHBOARD_WAU_BASE_RANGE_FILTER = dashboardLookbackTimeRangeFilter(
163
163
  );
164
164
  const DASHBOARD_EMAIL_FILTER =
165
165
  "('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io'))";
166
+ const SESSION_STATUS_FILTER = `event_name = 'session status' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`;
166
167
  const SIGNED_IN_ACTIVITY_KEY_SQL = USER_KEY_SQL;
167
168
  const SIGNED_IN_ACTIVITY_FILTER = `event_name = 'session status' AND signed_in = 'true' AND ${SIGNED_IN_ACTIVITY_KEY_SQL} IS NOT NULL`;
168
169
  const SIGNED_IN_PRODUCT_ACTIVITY_FILTER = `${SIGNED_IN_ACTIVITY_FILTER} AND ${PRODUCT_ACTIVITY_TEMPLATE_FILTER}`;
@@ -292,7 +293,7 @@ const ENTRIES: FirstPartyMetric[] = [
292
293
  width: 2,
293
294
  windowed: false,
294
295
  buildSql: fixed(
295
- "SELECT COALESCE(NULLIF(app, ''), 'unknown') AS app, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' GROUP BY COALESCE(NULLIF(app, ''), 'unknown') ORDER BY count DESC LIMIT 20",
296
+ `SELECT COALESCE(NULLIF(app, ''), 'unknown') AS app, COUNT(*) AS count FROM analytics_events WHERE ${SESSION_STATUS_FILTER} GROUP BY COALESCE(NULLIF(app, ''), 'unknown') ORDER BY count DESC LIMIT 20`,
296
297
  ),
297
298
  config: {
298
299
  xKey: "app",
@@ -310,7 +311,7 @@ const ENTRIES: FirstPartyMetric[] = [
310
311
  width: 1,
311
312
  windowed: false,
312
313
  buildSql: fixed(
313
- `SELECT ${EVENT_DATE_SQL} AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' GROUP BY ${EVENT_DATE_SQL} ORDER BY date`,
314
+ `SELECT ${EVENT_DATE_SQL} AS date, COUNT(*) AS count FROM analytics_events WHERE ${SESSION_STATUS_FILTER} GROUP BY ${EVENT_DATE_SQL} ORDER BY date`,
314
315
  ),
315
316
  config: { xKey: "date", yKey: "count", color: "#10b981" },
316
317
  },
@@ -380,7 +381,7 @@ const ENTRIES: FirstPartyMetric[] = [
380
381
  width: 1,
381
382
  windowed: false,
382
383
  buildSql: fixed(
383
- "SELECT COALESCE(NULLIF(signed_in, ''), 'unknown') AS signed_in, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' GROUP BY COALESCE(NULLIF(signed_in, ''), 'unknown') ORDER BY signed_in",
384
+ `SELECT COALESCE(NULLIF(signed_in, ''), 'unknown') AS signed_in, COUNT(*) AS count FROM analytics_events WHERE ${SESSION_STATUS_FILTER} GROUP BY COALESCE(NULLIF(signed_in, ''), 'unknown') ORDER BY signed_in`,
384
385
  ),
385
386
  config: {
386
387
  xKey: "signed_in",
@@ -1007,14 +1008,28 @@ export function buildPanel(
1007
1008
  overrides.width <= 6
1008
1009
  ? overrides.width
1009
1010
  : metric.width;
1011
+ const title =
1012
+ overrides.title?.trim() ||
1013
+ (window === "all"
1014
+ ? metric.title.replace(/\(\d+d\)$/i, "(all-time)")
1015
+ : metric.title);
1016
+ const config = { ...metric.config };
1017
+ if (metric.windowed) {
1018
+ config.timeScope = window === "all" ? "all-time" : "fixed-window";
1019
+ if (window === "all") {
1020
+ const description =
1021
+ typeof config.description === "string" ? config.description.trim() : "";
1022
+ config.description = `${description ? `${description} ` : ""}This panel is explicitly configured for an all-time window.`;
1023
+ }
1024
+ }
1010
1025
  return {
1011
1026
  id: overrides.id?.trim() || metric.key,
1012
- title: overrides.title?.trim() || metric.title,
1027
+ title,
1013
1028
  chartType: overrides.chartType?.trim() || metric.chartType,
1014
1029
  source: "first-party",
1015
1030
  width,
1016
1031
  sql: metric.buildSql(window),
1017
1032
  // Clone so callers can't mutate the shared catalog config object.
1018
- config: { ...metric.config },
1033
+ config,
1019
1034
  };
1020
1035
  }
@@ -663,7 +663,7 @@ export function resolveSessionReplayAgentAccess(
663
663
  resourceId: recordingId,
664
664
  });
665
665
  // Replay grants are always minted by an authenticated viewer. Fail closed
666
- // for grants without that signed identity: demo-mode policy is viewer-scoped,
666
+ // for grants without that signed identity: access policy is viewer-scoped,
667
667
  // so guessing from the ambient request could expose identities to agents.
668
668
  if (!result.ok || !result.viewerEmail) return null;
669
669
  return { viewerEmail: result.viewerEmail };
@@ -2,7 +2,6 @@ import { Buffer } from "node:buffer";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { gzipSync, gunzipSync } from "node:zlib";
4
4
 
5
- import { appStateGet } from "@agent-native/core/application-state";
6
5
  import {
7
6
  deletePrivateBlob,
8
7
  putPrivateBlob,
@@ -258,8 +257,6 @@ const DEFAULT_REPLAY_MAX_REQUESTS_PER_MINUTE = 120;
258
257
  const RETENTION_DELETE_BATCH_SIZE = 500;
259
258
  const REPLAY_PRIVATE_BLOB_REF_KIND = "agent-native.session-replay.private-blob";
260
259
  const REPLAY_PRIVATE_BLOB_REF_VERSION = 1;
261
- const SESSION_DEMO_EMAIL_PATTERN = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
262
- const SESSION_DEMO_ANONYMOUS_EMAIL = "anonymous@builder.io";
263
260
  let inlineReplayFallbackWarned = false;
264
261
 
265
262
  function replayError(message: string, statusCode: number): Error {
@@ -297,89 +294,6 @@ function replayEmail(value: unknown): string | null {
297
294
  return raw && raw.includes("@") ? raw : null;
298
295
  }
299
296
 
300
- function normalizedEmail(value: unknown): string | null {
301
- return replayEmail(value)?.toLowerCase() ?? null;
302
- }
303
-
304
- function isBuilderEmail(value: unknown): boolean {
305
- return normalizedEmail(value)?.endsWith("@builder.io") === true;
306
- }
307
-
308
- async function isSessionDemoModeEnabled(
309
- userEmail?: string | null,
310
- ): Promise<boolean> {
311
- if (process.env.DEMO_MODE === "true") return true;
312
- if (!userEmail) return false;
313
- try {
314
- // These reads also run from custom routes and actions, so use the
315
- // authenticated scope explicitly instead of ambient request state.
316
- const state = await appStateGet(userEmail, "demo-mode");
317
- return state?.enabled === true;
318
- } catch {
319
- return false;
320
- }
321
- }
322
-
323
- function demoBuilderSessionCondition() {
324
- return or(
325
- sql`lower(${schema.sessionRecordings.userId}) like ${"%@builder.io"}`,
326
- sql`lower(${schema.sessionRecordings.userKey}) like ${"%@builder.io"}`,
327
- );
328
- }
329
-
330
- class SessionDemoAnonymizer {
331
- aliasFor(value: unknown): string | null {
332
- const raw = replayString(value);
333
- if (!raw || raw.match(SESSION_DEMO_EMAIL_PATTERN)?.[0] !== raw) {
334
- return null;
335
- }
336
- return SESSION_DEMO_ANONYMOUS_EMAIL;
337
- }
338
-
339
- summarize(row: any, role?: SessionReplayAccessRole): SessionRecordingSummary {
340
- const summary = rowToSessionRecordingSummary(row, role);
341
- return {
342
- ...summary,
343
- userId: this.aliasFor(row.userId) ?? summary.userId,
344
- userKey: this.aliasFor(row.userKey) ?? summary.userKey,
345
- ownerEmail: this.aliasFor(summary.ownerEmail) ?? summary.ownerEmail,
346
- metadata: anonymizeEmailValue(summary.metadata, this) as Record<
347
- string,
348
- unknown
349
- >,
350
- };
351
- }
352
- }
353
-
354
- function anonymizeEmailValue(
355
- value: unknown,
356
- anonymizer: SessionDemoAnonymizer,
357
- ): unknown {
358
- const alias = anonymizer.aliasFor(value);
359
- if (alias) return alias;
360
- if (Array.isArray(value)) {
361
- return value.map((item) => anonymizeEmailValue(item, anonymizer));
362
- }
363
- if (typeof value === "string") {
364
- return value.replace(SESSION_DEMO_EMAIL_PATTERN, (email) => {
365
- return anonymizer.aliasFor(email) ?? email;
366
- });
367
- }
368
- if (value && typeof value === "object") {
369
- const out: Record<string, unknown> = {};
370
- for (const [key, item] of Object.entries(value)) {
371
- out[key] = anonymizeEmailValue(item, anonymizer);
372
- }
373
- return out;
374
- }
375
- return value;
376
- }
377
-
378
- function assertDemoVisibleSession(row: any): void {
379
- if (isBuilderEmail(row.userId) || isBuilderEmail(row.userKey)) return;
380
- throw replayError("Session recording not found", 404);
381
- }
382
-
383
297
  function replayInteger(value: unknown): number | null {
384
298
  if (typeof value === "number" && Number.isInteger(value)) return value;
385
299
  if (typeof value === "string" && /^-?\d+$/.test(value.trim())) {
@@ -1686,7 +1600,6 @@ export async function listSessionRecordings(
1686
1600
  filters: SessionReplayListFilters = {},
1687
1601
  ): Promise<SessionRecordingSummary[]> {
1688
1602
  const db = getDb() as any;
1689
- const demoMode = await isSessionDemoModeEnabled(scope.userEmail);
1690
1603
  const limit = Math.min(
1691
1604
  MAX_SESSION_RECORDINGS_LIMIT,
1692
1605
  Math.max(1, filters.limit ?? DEFAULT_SESSION_RECORDINGS_LIMIT),
@@ -1699,7 +1612,6 @@ export async function listSessionRecordings(
1699
1612
  replayVisibleIdentityCondition(),
1700
1613
  replayPlayableEventsCondition(),
1701
1614
  ];
1702
- if (demoMode) conditions.push(demoBuilderSessionCondition());
1703
1615
  if (filters.app)
1704
1616
  conditions.push(eq(schema.sessionRecordings.app, filters.app));
1705
1617
  if (filters.template) {
@@ -1753,22 +1665,13 @@ export async function listSessionRecordings(
1753
1665
  .where(and(...conditions))
1754
1666
  .orderBy(desc(schema.sessionRecordings.startedAt))
1755
1667
  .limit(limit);
1756
- if (!demoMode) {
1757
- return rows.map((row: any) => rowToSessionRecordingSummary(row));
1758
- }
1759
- const anonymizer = new SessionDemoAnonymizer();
1760
- return rows
1761
- .filter(
1762
- (row: any) => isBuilderEmail(row.userId) || isBuilderEmail(row.userKey),
1763
- )
1764
- .map((row: any) => anonymizer.summarize(row));
1668
+ return rows.map((row: any) => rowToSessionRecordingSummary(row));
1765
1669
  }
1766
1670
 
1767
1671
  export async function getSessionReplaySummary(
1768
1672
  recordingId: string,
1769
1673
  scope: SessionReplayScope,
1770
1674
  ): Promise<SessionRecordingSummary> {
1771
- const demoMode = await isSessionDemoModeEnabled(scope.userEmail);
1772
1675
  const access = await resolveAccess("session-recording", recordingId, {
1773
1676
  userEmail: scope.userEmail,
1774
1677
  orgId: scope.orgId ?? undefined,
@@ -1777,22 +1680,15 @@ export async function getSessionReplaySummary(
1777
1680
  if (!isVisibleSessionRecording(access.resource)) {
1778
1681
  throw replayError("Session recording not found", 404);
1779
1682
  }
1780
- if (!demoMode) {
1781
- return rowToSessionRecordingSummary(access.resource, access.role);
1782
- }
1783
- assertDemoVisibleSession(access.resource);
1784
- return new SessionDemoAnonymizer().summarize(access.resource, access.role);
1683
+ return rowToSessionRecordingSummary(access.resource, access.role);
1785
1684
  }
1786
1685
 
1787
1686
  export async function getSessionReplayTokenizedSummary(
1788
1687
  recordingId: string,
1789
1688
  viewerEmail: string,
1790
1689
  ): Promise<SessionRecordingSummary> {
1791
- // Tokenized reads often run without an authenticated request session. Use
1792
- // the viewer identity embedded in the signed, recording-scoped grant so a
1793
- // link minted while demo mode is enabled cannot reveal real identities when
1794
- // it is opened in a signed-out browser or by an external agent.
1795
- const demoMode = await isSessionDemoModeEnabled(viewerEmail);
1690
+ // Tokenized reads often run without an authenticated request session. The
1691
+ // viewer identity is still required by the signed, recording-scoped grant.
1796
1692
  const db = getDb() as any;
1797
1693
  // guard:allow-unscoped -- called only after verifySessionReplayAgentAccess(recordingId, token) verifies a signed, recording-scoped agent_access token.
1798
1694
  const [row] = await db
@@ -1803,9 +1699,7 @@ export async function getSessionReplayTokenizedSummary(
1803
1699
  if (!row || !isVisibleSessionRecording(row)) {
1804
1700
  throw replayError("Session recording not found", 404);
1805
1701
  }
1806
- if (!demoMode) return rowToSessionRecordingSummary(row, "viewer");
1807
- assertDemoVisibleSession(row);
1808
- return new SessionDemoAnonymizer().summarize(row, "viewer");
1702
+ return rowToSessionRecordingSummary(row, "viewer");
1809
1703
  }
1810
1704
 
1811
1705
  function parseInlineReplayEvents(inlineData: string): unknown[] {
@@ -286,6 +286,7 @@ export default createAgentChatPlugin({
286
286
  analyticsSourceGuidanceOpening() +
287
287
  "DASHBOARD CREATION RULE — You may create dashboards, analyses, SQL panels, or other resources only when the user explicitly asks you to (e.g. 'build me a dashboard for...', 'create a new analysis', 'add a chart for...'). Never create any resource proactively during research, trend analysis, or answering questions. If you think a dashboard would be useful, suggest it and wait for explicit confirmation before creating anything. Never add new items to the sidebar or modify existing dashboards without an explicit user directive. " +
288
288
  "DASHBOARD MUTATION RULE — For dashboard edits, default to `mutate-dashboard` with the typed `dashboard.*` script API so the main payload is a string and avoids native-array serialization traps. It can move panels by id, edit titles/SQL/config, insert, duplicate, remove, and patch dashboard fields in one atomic save. The script API is constrained: no variables/imports/loops/functions, only JSON-compatible arguments on documented dashboard methods. Do not count shifting `/panels/<index>` positions for ordinary dashboard edits unless the user specifically asks for low-level JSON-pointer operations. " +
289
+ 'FIRST-PARTY DASHBOARD TIME RULE — AI-generated `source: "first-party"` panels are dashboard-time-bound by default: set `config.timeScope` to `dashboard` and include a matching dashboard time predicate. `{{timeRange}}` requires a matching `filters` entry with `id: "timeRange"` and `type: "select"`; `{{<id>Start}}`/`{{<id>End}}` require a matching `type: "date-range"` filter with that id. Allowed `timeScope` values are `dashboard`, `fixed-window`, `cohort-history`, and `all-time`; use `all-time` only when the user requests full available history and put all-time, lifetime, or historical in the title or description. Server validation rejects unbound first-party SQL. ' +
289
290
  "DASHBOARD READ RULE — `get-sql-dashboard` is compact by default: use its `panels` summaries plus `layout.panelOrder`, `layout.firstPanelIds`, and `layout.groups[].rows[].rowNumber/panelIds` for orientation and verification. Pass `includeConfig: true` only when you truly need full panel SQL/config. " +
290
291
  'DASHBOARD REORDER RULE — For simple chart/section moves, use `mutate-dashboard` code such as `dashboard.panels(["panel-a","panel-b"]).moveToTop();`. For visible placement requests like "second row" or "next to return rates", use row-aware placement such as `dashboard.insertPanel({...}).nextTo("retention-over-time")`, `.atRow(2)`, or `dashboard.panel("panel-a").moveNextTo("panel-b")`; these keep panels in the intended rendered row and expand/rebalance that row when needed. Never count shifting `/panels/<index>` positions for ordinary \'move this chart\' requests. Use `get-sql-dashboard.layout.groups[].rows` as proof of visible row placement, not only flat `panelOrder`. ' +
291
292
  "Use configured data sources and actions only. Call `data-source-status` when you need to know which providers are connected, and treat provider actions as unavailable for analysis if they return missing credentials, permission, syntax, quota, or network errors. " +
@@ -1339,13 +1339,12 @@ function AllAssetsBrowser() {
1339
1339
  </div>
1340
1340
  </button>
1341
1341
  {(asset as any).libraryTitle ? (
1342
- <button
1343
- type="button"
1344
- onClick={() => navigate(`/library/${asset.libraryId}`)}
1342
+ <Link
1343
+ to={`/library/${asset.libraryId}`}
1345
1344
  className="absolute bottom-2 left-2 z-10 max-w-[calc(100%-1rem)] truncate rounded-full bg-background/95 px-2.5 py-1 text-[11px] font-medium shadow-sm transition hover:bg-background"
1346
1345
  >
1347
1346
  {(asset as any).libraryTitle}
1348
- </button>
1347
+ </Link>
1349
1348
  ) : null}
1350
1349
  <TooltipProvider>
1351
1350
  <Tooltip>
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-13
4
+ ---
5
+
6
+ Internal library links now support opening in a new tab.
@@ -48,7 +48,7 @@ import {
48
48
  } from "date-fns";
49
49
  import { nanoid } from "nanoid";
50
50
  import { useEffect, useMemo, useRef, useState } from "react";
51
- import { useNavigate, useSearchParams } from "react-router";
51
+ import { Link, useNavigate, useSearchParams } from "react-router";
52
52
  import { toast } from "sonner";
53
53
 
54
54
  import { CloudUpgrade } from "@/components/CloudUpgrade";
@@ -915,12 +915,10 @@ export default function BookingLinksPage({
915
915
  toast.error(t("common.clipboardUnavailable"));
916
916
  }
917
917
 
918
- function openPreview(slug: string) {
919
- // For local preview, use the local path
920
- const localPath = bookingUsername
918
+ function bookingPreviewPath(slug: string) {
919
+ return bookingUsername
921
920
  ? `/book/${bookingUsername}/${slug}`
922
921
  : `/book/${slug}`;
923
- window.open(localPath, "_blank", "noopener,noreferrer");
924
922
  }
925
923
 
926
924
  const handleSaveRef = useRef(handleSave);
@@ -952,14 +950,13 @@ export default function BookingLinksPage({
952
950
  }
953
951
  return {
954
952
  left: (
955
- <button
956
- type="button"
957
- onClick={() => navigate("/booking-links")}
953
+ <Link
954
+ to="/booking-links"
958
955
  className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground"
959
956
  >
960
957
  <IconChevronLeft className="h-4 w-4" />
961
958
  {t("bookingLinks.back")}
962
- </button>
959
+ </Link>
963
960
  ),
964
961
  right: selectedLink ? (
965
962
  <div className="flex items-center gap-1.5">
@@ -1008,14 +1005,19 @@ export default function BookingLinksPage({
1008
1005
  <Tooltip>
1009
1006
  <TooltipTrigger asChild>
1010
1007
  <Button
1011
- type="button"
1008
+ asChild
1012
1009
  variant="ghost"
1013
1010
  size="icon"
1014
- onClick={() => openPreview(draft.slug)}
1015
1011
  className={cn("h-8 w-8", BRAND_ICON_LINK_CLASS)}
1016
1012
  aria-label={t("bookingLinks.openBookingLink")}
1017
1013
  >
1018
- <IconExternalLink className="h-4 w-4" />
1014
+ <a
1015
+ href={bookingPreviewPath(draft.slug)}
1016
+ target="_blank"
1017
+ rel="noopener noreferrer"
1018
+ >
1019
+ <IconExternalLink className="h-4 w-4" />
1020
+ </a>
1019
1021
  </Button>
1020
1022
  </TooltipTrigger>
1021
1023
  <TooltipContent>{t("bookingLinks.openLink")}</TooltipContent>
@@ -1300,14 +1302,19 @@ export default function BookingLinksPage({
1300
1302
  <Tooltip>
1301
1303
  <TooltipTrigger asChild>
1302
1304
  <Button
1303
- type="button"
1305
+ asChild
1304
1306
  variant="ghost"
1305
1307
  size="icon"
1306
- onClick={() => openPreview(draft.slug)}
1307
1308
  className={cn("h-8 w-8", BRAND_ICON_LINK_CLASS)}
1308
1309
  aria-label={t("bookingLinks.openBookingPageNewTab")}
1309
1310
  >
1310
- <IconExternalLink className="h-4 w-4" />
1311
+ <a
1312
+ href={bookingPreviewPath(draft.slug)}
1313
+ target="_blank"
1314
+ rel="noopener noreferrer"
1315
+ >
1316
+ <IconExternalLink className="h-4 w-4" />
1317
+ </a>
1311
1318
  </Button>
1312
1319
  </TooltipTrigger>
1313
1320
  <TooltipContent>
@@ -1499,7 +1506,7 @@ export default function BookingLinksPage({
1499
1506
  availability={availability ?? undefined}
1500
1507
  bookingUrl={previewUrl}
1501
1508
  onCopy={() => void copyPreviewUrl(draft.slug)}
1502
- onOpen={() => openPreview(draft.slug)}
1509
+ openHref={bookingPreviewPath(draft.slug)}
1503
1510
  onCollapse={() => setIsPreviewCollapsed(true)}
1504
1511
  />
1505
1512
  )}
@@ -1597,9 +1604,8 @@ export default function BookingLinksPage({
1597
1604
  >
1598
1605
  <div className="flex flex-col gap-3 px-4 py-4 sm:flex-row sm:items-center sm:gap-4 sm:px-5">
1599
1606
  {/* Info — clickable to edit */}
1600
- <button
1601
- type="button"
1602
- onClick={() => navigate(`/booking-links/${link.id}`)}
1607
+ <Link
1608
+ to={`/booking-links/${link.id}`}
1603
1609
  className="min-w-0 flex-1 text-left"
1604
1610
  >
1605
1611
  <div className="flex items-center gap-2">
@@ -1622,7 +1628,7 @@ export default function BookingLinksPage({
1622
1628
  {availability.timezone}
1623
1629
  </p>
1624
1630
  )}
1625
- </button>
1631
+ </Link>
1626
1632
 
1627
1633
  {/* Actions */}
1628
1634
  <div className="flex shrink-0 items-center gap-2">
@@ -1642,19 +1648,17 @@ export default function BookingLinksPage({
1642
1648
  <IconLink className="h-3.5 w-3.5" />
1643
1649
  {t("bookingLinks.copyLink")}
1644
1650
  </button>
1645
- <button
1646
- type="button"
1647
- onClick={(e) => {
1648
- e.stopPropagation();
1649
- openPreview(link.slug);
1650
- }}
1651
+ <a
1652
+ href={bookingPreviewPath(link.slug)}
1653
+ target="_blank"
1654
+ rel="noopener noreferrer"
1651
1655
  className={cn(
1652
1656
  "flex h-9 w-9 items-center justify-center rounded-full border",
1653
1657
  BRAND_PILL_LINK_CLASS,
1654
1658
  )}
1655
1659
  >
1656
1660
  <IconExternalLink className="h-4 w-4" />
1657
- </button>
1661
+ </a>
1658
1662
  </>
1659
1663
  )}
1660
1664
 
@@ -1669,12 +1673,10 @@ export default function BookingLinksPage({
1669
1673
  </button>
1670
1674
  </DropdownMenuTrigger>
1671
1675
  <DropdownMenuContent align="end">
1672
- <DropdownMenuItem
1673
- onClick={() =>
1674
- navigate(`/booking-links/${link.id}`)
1675
- }
1676
- >
1677
- {t("eventForm.edit")}
1676
+ <DropdownMenuItem asChild>
1677
+ <Link to={`/booking-links/${link.id}`}>
1678
+ {t("eventForm.edit")}
1679
+ </Link>
1678
1680
  </DropdownMenuItem>
1679
1681
  <DropdownMenuItem
1680
1682
  onClick={() => {
@@ -1949,7 +1951,7 @@ function BookingPreview({
1949
1951
  availability,
1950
1952
  bookingUrl,
1951
1953
  onCopy,
1952
- onOpen,
1954
+ openHref,
1953
1955
  onCollapse,
1954
1956
  }: {
1955
1957
  title: string;
@@ -1961,7 +1963,7 @@ function BookingPreview({
1961
1963
  availability?: AvailabilityConfig;
1962
1964
  bookingUrl?: string;
1963
1965
  onCopy?: () => void;
1964
- onOpen?: () => void;
1966
+ openHref?: string;
1965
1967
  onCollapse?: () => void;
1966
1968
  }) {
1967
1969
  const t = useT();
@@ -2138,19 +2140,20 @@ function BookingPreview({
2138
2140
  <TooltipContent>{t("bookingLinks.copyLink")}</TooltipContent>
2139
2141
  </Tooltip>
2140
2142
  )}
2141
- {onOpen && (
2143
+ {openHref && (
2142
2144
  <Tooltip>
2143
2145
  <TooltipTrigger asChild>
2144
- <button
2145
- type="button"
2146
- onClick={onOpen}
2146
+ <a
2147
+ href={openHref}
2148
+ target="_blank"
2149
+ rel="noopener noreferrer"
2147
2150
  className={cn(
2148
2151
  "flex h-6 w-6 items-center justify-center rounded",
2149
2152
  BRAND_ICON_LINK_CLASS,
2150
2153
  )}
2151
2154
  >
2152
2155
  <IconExternalLink className="h-3.5 w-3.5" />
2153
- </button>
2156
+ </a>
2154
2157
  </TooltipTrigger>
2155
2158
  <TooltipContent>
2156
2159
  {t("bookingLinks.openInteractiveBookingLink")}
@@ -9,7 +9,7 @@ import {
9
9
  import { useQuery, useMutation } from "@tanstack/react-query";
10
10
  import { format, parseISO } from "date-fns";
11
11
  import { useState } from "react";
12
- import { useParams, useNavigate } from "react-router";
12
+ import { Link, useParams, useNavigate } from "react-router";
13
13
  import { toast } from "sonner";
14
14
 
15
15
  import {
@@ -118,13 +118,11 @@ export function ManageBookingPage() {
118
118
  {t("manageBooking.cancelledSuffix")}
119
119
  </p>
120
120
  {booking.slug && (
121
- <Button
122
- variant="outline"
123
- className="mt-6 gap-2"
124
- onClick={() => navigate(`/book/${booking.slug}`)}
125
- >
126
- <IconCalendarPlus className="h-4 w-4" />
127
- {t("manageBooking.reschedule")}
121
+ <Button asChild variant="outline" className="mt-6 gap-2">
122
+ <Link to={`/book/${booking.slug}`}>
123
+ <IconCalendarPlus className="h-4 w-4" />
124
+ {t("manageBooking.reschedule")}
125
+ </Link>
128
126
  </Button>
129
127
  )}
130
128
  </div>
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-13
4
+ ---
5
+
6
+ Booking links and previews now support opening in a new tab.
@@ -58,13 +58,10 @@ export function SignInPromptDialog({
58
58
  <Button variant="ghost" onClick={() => onOpenChange(false)}>
59
59
  {t("signInPrompt.notNow")}
60
60
  </Button>
61
- <Button
62
- onClick={() => {
63
- onSignIn?.();
64
- window.location.href = buildSignInHref(returnTo);
65
- }}
66
- >
67
- {t("signInPrompt.signIn")}
61
+ <Button asChild>
62
+ <a href={buildSignInHref(returnTo)} onClick={() => onSignIn?.()}>
63
+ {t("signInPrompt.signIn")}
64
+ </a>
68
65
  </Button>
69
66
  </DialogFooter>
70
67
  </DialogContent>