@agent-native/core 0.99.1 → 0.99.3

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 (170) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/docs/content/a2a-protocol.mdx +42 -0
  4. package/corpus/core/docs/content/locales/ar-SA/a2a-protocol.mdx +42 -0
  5. package/corpus/core/docs/content/locales/de-DE/a2a-protocol.mdx +42 -0
  6. package/corpus/core/docs/content/locales/es-ES/a2a-protocol.mdx +42 -0
  7. package/corpus/core/docs/content/locales/fr-FR/a2a-protocol.mdx +42 -0
  8. package/corpus/core/docs/content/locales/hi-IN/a2a-protocol.mdx +42 -0
  9. package/corpus/core/docs/content/locales/ja-JP/a2a-protocol.mdx +42 -0
  10. package/corpus/core/docs/content/locales/ko-KR/a2a-protocol.mdx +42 -0
  11. package/corpus/core/docs/content/locales/pt-BR/a2a-protocol.mdx +42 -0
  12. package/corpus/core/docs/content/locales/zh-CN/a2a-protocol.mdx +42 -0
  13. package/corpus/core/docs/content/locales/zh-TW/a2a-protocol.mdx +42 -0
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/agent/production-agent.ts +12 -38
  16. package/corpus/core/src/audit/config.ts +0 -1
  17. package/corpus/core/src/chat-threads/store.ts +57 -41
  18. package/corpus/core/src/client/AssistantChat.tsx +1 -2
  19. package/corpus/core/src/client/chat/message-components.tsx +15 -3
  20. package/corpus/core/src/client/chat/tool-call-display.tsx +18 -1
  21. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +5 -9
  22. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +0 -133
  23. package/corpus/core/src/client/index.ts +7 -0
  24. package/corpus/core/src/client/settings/DemoModeSection.tsx +9 -65
  25. package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
  26. package/corpus/core/src/client/use-db-sync.ts +3 -15
  27. package/corpus/core/src/client/use-demo-mode-status.ts +16 -30
  28. package/corpus/core/src/demo/browser-state.ts +47 -0
  29. package/corpus/core/src/demo/fetch-interceptor.ts +12 -59
  30. package/corpus/core/src/deploy/workspace-deploy.ts +2 -2
  31. package/corpus/core/src/server/action-discovery.ts +0 -1
  32. package/corpus/core/src/server/core-routes-plugin.ts +0 -30
  33. package/corpus/core/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  34. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +48 -7
  35. package/corpus/templates/analytics/AGENTS.md +4 -4
  36. package/corpus/templates/analytics/actions/compose-dashboard.ts +12 -1
  37. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +15 -4
  38. package/corpus/templates/analytics/actions/mutate-dashboard.ts +2 -1
  39. package/corpus/templates/analytics/actions/update-dashboard.ts +14 -0
  40. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -27
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -1
  42. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +21 -0
  43. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +14 -0
  44. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +31 -12
  45. package/corpus/templates/analytics/changelog/2026-07-13-dashboard-charts-now-honor-the-selected-time-range-by-defaul.md +6 -0
  46. package/corpus/templates/analytics/changelog/2026-07-13-keep-real-authorized-identities-in-analytics-incident-lookup.md +6 -0
  47. package/corpus/templates/analytics/changelog/2026-07-13-the-analytics-agents-tab-is-available-again-from-the-main-na.md +6 -0
  48. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
  49. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +4 -0
  50. package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +170 -0
  51. package/corpus/templates/analytics/server/lib/error-capture.ts +6 -62
  52. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +20 -5
  53. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +1 -1
  54. package/corpus/templates/analytics/server/lib/session-replay.ts +5 -111
  55. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -0
  56. package/corpus/templates/assets/app/routes/library.tsx +3 -4
  57. package/corpus/templates/assets/changelog/2026-07-13-internal-library-links-open-in-new-tabs.md +6 -0
  58. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +43 -40
  59. package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +6 -8
  60. package/corpus/templates/calendar/changelog/2026-07-13-booking-links-and-previews-open-in-new-tabs.md +6 -0
  61. package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +4 -7
  62. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +19 -9
  63. package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -3
  64. package/corpus/templates/clips/changelog/2026-07-13-recording-links-open-in-new-tabs.md +6 -0
  65. package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +6 -8
  66. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +5 -3
  67. package/corpus/templates/content/app/components/editor/database/FormView.tsx +6 -9
  68. package/corpus/templates/content/changelog/2026-07-13-document-links-open-in-new-tabs.md +6 -0
  69. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +33 -16
  70. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -1
  71. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +4 -4
  72. package/corpus/templates/design/app/pages/DesignSystems.tsx +8 -10
  73. package/corpus/templates/design/app/pages/Index.tsx +6 -1
  74. package/corpus/templates/design/app/pages/Present.tsx +12 -6
  75. package/corpus/templates/design/changelog/2026-07-13-design-navigation-links-open-in-new-tabs.md +6 -0
  76. package/corpus/templates/design/changelog/2026-07-13-design-session-replays-include-preview-iframes.md +6 -0
  77. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -11
  78. package/corpus/templates/forms/app/pages/FormsListPage.tsx +6 -8
  79. package/corpus/templates/forms/changelog/2026-07-13-form-navigation-links-open-in-new-tabs.md +6 -0
  80. package/corpus/templates/mail/app/pages/DraftQueuePage.tsx +3 -8
  81. package/corpus/templates/mail/changelog/2026-07-13-settings-links-open-in-new-tabs.md +6 -0
  82. package/dist/agent/production-agent.d.ts.map +1 -1
  83. package/dist/agent/production-agent.js +13 -37
  84. package/dist/agent/production-agent.js.map +1 -1
  85. package/dist/audit/config.d.ts.map +1 -1
  86. package/dist/audit/config.js +0 -1
  87. package/dist/audit/config.js.map +1 -1
  88. package/dist/chat-threads/store.d.ts.map +1 -1
  89. package/dist/chat-threads/store.js +47 -33
  90. package/dist/chat-threads/store.js.map +1 -1
  91. package/dist/client/AssistantChat.d.ts.map +1 -1
  92. package/dist/client/AssistantChat.js +1 -1
  93. package/dist/client/AssistantChat.js.map +1 -1
  94. package/dist/client/chat/message-components.d.ts.map +1 -1
  95. package/dist/client/chat/message-components.js +9 -3
  96. package/dist/client/chat/message-components.js.map +1 -1
  97. package/dist/client/chat/tool-call-display.d.ts +3 -1
  98. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  99. package/dist/client/chat/tool-call-display.js +10 -2
  100. package/dist/client/chat/tool-call-display.js.map +1 -1
  101. package/dist/client/extensions/EmbeddedExtension.js +2 -6
  102. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  103. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  104. package/dist/client/extensions/ExtensionViewer.js +2 -59
  105. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  106. package/dist/client/index.d.ts +2 -0
  107. package/dist/client/index.d.ts.map +1 -1
  108. package/dist/client/index.js +2 -0
  109. package/dist/client/index.js.map +1 -1
  110. package/dist/client/settings/DemoModeSection.d.ts +1 -13
  111. package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
  112. package/dist/client/settings/DemoModeSection.js +5 -52
  113. package/dist/client/settings/DemoModeSection.js.map +1 -1
  114. package/dist/client/settings/SettingsPanel.js +1 -1
  115. package/dist/client/settings/SettingsPanel.js.map +1 -1
  116. package/dist/client/use-db-sync.d.ts.map +1 -1
  117. package/dist/client/use-db-sync.js +3 -7
  118. package/dist/client/use-db-sync.js.map +1 -1
  119. package/dist/client/use-demo-mode-status.d.ts +3 -4
  120. package/dist/client/use-demo-mode-status.d.ts.map +1 -1
  121. package/dist/client/use-demo-mode-status.js +8 -23
  122. package/dist/client/use-demo-mode-status.js.map +1 -1
  123. package/dist/collab/awareness.d.ts +2 -2
  124. package/dist/collab/awareness.d.ts.map +1 -1
  125. package/dist/collab/routes.d.ts +2 -2
  126. package/dist/collab/struct-routes.d.ts +1 -1
  127. package/dist/demo/browser-state.d.ts +10 -0
  128. package/dist/demo/browser-state.d.ts.map +1 -0
  129. package/dist/demo/browser-state.js +49 -0
  130. package/dist/demo/browser-state.js.map +1 -0
  131. package/dist/demo/fetch-interceptor.d.ts +2 -7
  132. package/dist/demo/fetch-interceptor.d.ts.map +1 -1
  133. package/dist/demo/fetch-interceptor.js +12 -56
  134. package/dist/demo/fetch-interceptor.js.map +1 -1
  135. package/dist/deploy/workspace-deploy.js +2 -2
  136. package/dist/deploy/workspace-deploy.js.map +1 -1
  137. package/dist/notifications/routes.d.ts +2 -2
  138. package/dist/observability/routes.d.ts +5 -5
  139. package/dist/resources/handlers.d.ts +3 -3
  140. package/dist/secrets/routes.d.ts +9 -9
  141. package/dist/server/action-discovery.d.ts.map +1 -1
  142. package/dist/server/action-discovery.js +0 -1
  143. package/dist/server/action-discovery.js.map +1 -1
  144. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  145. package/dist/server/core-routes-plugin.js +0 -27
  146. package/dist/server/core-routes-plugin.js.map +1 -1
  147. package/dist/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  148. package/docs/content/a2a-protocol.mdx +42 -0
  149. package/docs/content/locales/ar-SA/a2a-protocol.mdx +42 -0
  150. package/docs/content/locales/de-DE/a2a-protocol.mdx +42 -0
  151. package/docs/content/locales/es-ES/a2a-protocol.mdx +42 -0
  152. package/docs/content/locales/fr-FR/a2a-protocol.mdx +42 -0
  153. package/docs/content/locales/hi-IN/a2a-protocol.mdx +42 -0
  154. package/docs/content/locales/ja-JP/a2a-protocol.mdx +42 -0
  155. package/docs/content/locales/ko-KR/a2a-protocol.mdx +42 -0
  156. package/docs/content/locales/pt-BR/a2a-protocol.mdx +42 -0
  157. package/docs/content/locales/zh-CN/a2a-protocol.mdx +42 -0
  158. package/docs/content/locales/zh-TW/a2a-protocol.mdx +42 -0
  159. package/package.json +1 -1
  160. package/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  161. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +0 -23
  162. package/corpus/core/src/demo/config.ts +0 -57
  163. package/dist/demo/actions/toggle-demo-mode.d.ts +0 -8
  164. package/dist/demo/actions/toggle-demo-mode.d.ts.map +0 -1
  165. package/dist/demo/actions/toggle-demo-mode.js +0 -21
  166. package/dist/demo/actions/toggle-demo-mode.js.map +0 -1
  167. package/dist/demo/config.d.ts +0 -9
  168. package/dist/demo/config.d.ts.map +0 -1
  169. package/dist/demo/config.js +0 -58
  170. package/dist/demo/config.js.map +0 -1
@@ -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>
@@ -35,7 +35,13 @@ import {
35
35
  } from "@tabler/icons-react";
36
36
  import { useQuery } from "@tanstack/react-query";
37
37
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
38
- import { useParams, useNavigate, NavLink, useSearchParams } from "react-router";
38
+ import {
39
+ Link,
40
+ useParams,
41
+ useNavigate,
42
+ NavLink,
43
+ useSearchParams,
44
+ } from "react-router";
39
45
  import { toast } from "sonner";
40
46
 
41
47
  import { EditableRecordingTitle } from "@/components/editable-recording-title";
@@ -712,8 +718,8 @@ export default function RecordingPage() {
712
718
  {(playerDataQ.error as Error | undefined)?.message ??
713
719
  t("recordingPage.noAccess")}
714
720
  </p>
715
- <Button onClick={() => navigate("/")} variant="outline">
716
- {t("recordingPage.backToLibrary")}
721
+ <Button asChild variant="outline">
722
+ <Link to="/">{t("recordingPage.backToLibrary")}</Link>
717
723
  </Button>
718
724
  </div>
719
725
  );
@@ -772,13 +778,15 @@ export default function RecordingPage() {
772
778
  <div className="flex min-h-screen w-full flex-col bg-background">
773
779
  <header className="flex min-w-0 shrink-0 items-center gap-3 border-b border-border px-3 py-2 sm:px-4 sm:py-3">
774
780
  <Button
781
+ asChild
775
782
  variant="ghost"
776
783
  size="icon"
777
784
  className="shrink-0"
778
- onClick={() => navigate("/")}
779
785
  aria-label={t("recordingPage.back")}
780
786
  >
781
- <IconArrowLeft className="h-4 w-4 rtl:-scale-x-100" />
787
+ <Link to="/">
788
+ <IconArrowLeft className="h-4 w-4 rtl:-scale-x-100" />
789
+ </Link>
782
790
  </Button>
783
791
  <div className="min-w-0 flex-1">
784
792
  <p className="truncate text-sm font-medium">{visibleTitle}</p>
@@ -947,8 +955,8 @@ export default function RecordingPage() {
947
955
  : t("recordingPage.retryUpload")
948
956
  : t("recordingPage.checkAgain")}
949
957
  </Button>
950
- <Button onClick={() => navigate("/")} variant="ghost" size="sm">
951
- {t("recordingPage.backToLibrary")}
958
+ <Button asChild variant="ghost" size="sm">
959
+ <Link to="/">{t("recordingPage.backToLibrary")}</Link>
952
960
  </Button>
953
961
  </div>
954
962
  </div>
@@ -1122,13 +1130,15 @@ export default function RecordingPage() {
1122
1130
  <div className="flex w-full min-w-0 flex-col xl:flex-1">
1123
1131
  <header className="flex min-w-0 shrink-0 items-center gap-2 border-b border-border px-3 py-2 sm:gap-3 sm:px-4 sm:py-3">
1124
1132
  <Button
1133
+ asChild
1125
1134
  variant="ghost"
1126
1135
  size="icon"
1127
1136
  className="shrink-0"
1128
- onClick={() => navigate("/")}
1129
1137
  aria-label={t("recordingPage.back")}
1130
1138
  >
1131
- <IconArrowLeft className="h-4 w-4 rtl:-scale-x-100" />
1139
+ <Link to="/">
1140
+ <IconArrowLeft className="h-4 w-4 rtl:-scale-x-100" />
1141
+ </Link>
1132
1142
  </Button>
1133
1143
  <div className="flex-1 min-w-0">
1134
1144
  <EditableRecordingTitle
@@ -32,7 +32,7 @@ import {
32
32
  type LoaderFunctionArgs,
33
33
  type MetaFunction,
34
34
  } from "react-router";
35
- import { useLoaderData, useNavigate, useParams } from "react-router";
35
+ import { Link, useLoaderData, useNavigate, useParams } from "react-router";
36
36
  import { toast } from "sonner";
37
37
 
38
38
  import { CaptureInstallButton } from "@/components/capture-install-options";
@@ -817,12 +817,14 @@ export default function ShareRoute() {
817
817
  <header className="flex min-w-0 shrink-0 flex-wrap items-center gap-2 border-b border-border px-3 py-2 sm:gap-3 sm:px-4 sm:py-3 lg:flex-nowrap">
818
818
  {session ? (
819
819
  <Button
820
+ asChild
820
821
  variant="ghost"
821
822
  size="icon"
822
- onClick={() => navigate("/")}
823
823
  aria-label={t("sharePage.backToHome")}
824
824
  >
825
- <IconArrowLeft className="h-4 w-4 rtl:-scale-x-100" />
825
+ <Link to="/">
826
+ <IconArrowLeft className="h-4 w-4 rtl:-scale-x-100" />
827
+ </Link>
826
828
  </Button>
827
829
  ) : null}
828
830
  <div className="min-w-0 flex-1">
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-13
4
+ ---
5
+
6
+ Recording cards and player back links now support opening in a new tab.