@agent-native/core 0.99.0 → 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 (184) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +23 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +33 -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/cli/pr-visual-recap-workflow.ts +1 -1
  8. package/corpus/core/src/client/AssistantChat.tsx +1 -2
  9. package/corpus/core/src/client/analytics.ts +13 -0
  10. package/corpus/core/src/client/chat/message-components.tsx +15 -3
  11. package/corpus/core/src/client/chat/tool-call-display.tsx +18 -1
  12. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +5 -9
  13. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +0 -133
  14. package/corpus/core/src/client/index.ts +7 -0
  15. package/corpus/core/src/client/settings/DemoModeSection.tsx +9 -65
  16. package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
  17. package/corpus/core/src/client/use-db-sync.ts +3 -15
  18. package/corpus/core/src/client/use-demo-mode-status.ts +16 -30
  19. package/corpus/core/src/demo/browser-state.ts +47 -0
  20. package/corpus/core/src/demo/fetch-interceptor.ts +12 -59
  21. package/corpus/core/src/deploy/workspace-deploy.ts +2 -2
  22. package/corpus/core/src/server/action-discovery.ts +0 -1
  23. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +13 -0
  24. package/corpus/core/src/server/agent-teams.ts +17 -0
  25. package/corpus/core/src/server/core-routes-plugin.ts +0 -30
  26. package/corpus/core/src/server/google-oauth.ts +6 -2
  27. package/corpus/core/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  28. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +48 -7
  29. package/corpus/templates/analytics/AGENTS.md +4 -4
  30. package/corpus/templates/analytics/actions/compose-dashboard.ts +12 -1
  31. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +15 -4
  32. package/corpus/templates/analytics/actions/mutate-dashboard.ts +2 -1
  33. package/corpus/templates/analytics/actions/update-dashboard.ts +14 -0
  34. package/corpus/templates/analytics/app/assets/replay-cursor.svg +17 -0
  35. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +4 -3
  36. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -27
  37. package/corpus/templates/analytics/app/global.css +17 -9
  38. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +32 -4
  39. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -1
  40. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +21 -0
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +14 -0
  42. package/corpus/templates/analytics/app/pages/monitoring/errors/IssueDetail.tsx +1 -1
  43. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +77 -23
  44. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  45. package/corpus/templates/analytics/changelog/2026-07-13-all-time-dashboard-filters-no-longer-send-invalid-dates-to-b.md +6 -0
  46. package/corpus/templates/analytics/changelog/2026-07-13-dashboard-charts-now-honor-the-selected-time-range-by-defaul.md +6 -0
  47. package/corpus/templates/analytics/changelog/2026-07-13-error-issue-titles-wrap-instead-of-overflowing.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-07-13-keep-real-authorized-identities-in-analytics-incident-lookup.md +6 -0
  49. package/corpus/templates/analytics/changelog/2026-07-13-saving-a-new-dashboard-view-no-longer-fails-with-a-server-er.md +6 -0
  50. package/corpus/templates/analytics/changelog/2026-07-13-session-replay-cursors-stay-hidden-until-their-first-recorde.md +6 -0
  51. package/corpus/templates/analytics/changelog/2026-07-13-session-replay-links-now-support-cmd-click-and-middle-click-.md +6 -0
  52. package/corpus/templates/analytics/changelog/2026-07-13-the-analytics-agents-tab-is-available-again-from-the-main-na.md +6 -0
  53. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
  54. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +4 -0
  55. package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +170 -0
  56. package/corpus/templates/analytics/server/lib/dashboards-store.ts +43 -4
  57. package/corpus/templates/analytics/server/lib/error-capture.ts +30 -67
  58. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +20 -5
  59. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +1 -1
  60. package/corpus/templates/analytics/server/lib/session-replay.ts +5 -111
  61. package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -0
  62. package/corpus/templates/assets/app/routes/library.tsx +3 -4
  63. package/corpus/templates/assets/changelog/2026-07-13-internal-library-links-open-in-new-tabs.md +6 -0
  64. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +43 -40
  65. package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +6 -8
  66. package/corpus/templates/calendar/changelog/2026-07-13-booking-links-and-previews-open-in-new-tabs.md +6 -0
  67. package/corpus/templates/clips/app/components/library/recording-card.tsx +27 -26
  68. package/corpus/templates/clips/app/components/library/space-card.tsx +4 -6
  69. package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +4 -7
  70. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +19 -9
  71. package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -3
  72. package/corpus/templates/clips/changelog/2026-07-13-recording-links-open-in-new-tabs.md +6 -0
  73. package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +6 -8
  74. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +5 -3
  75. package/corpus/templates/content/app/components/editor/database/FormView.tsx +6 -9
  76. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +18 -16
  77. package/corpus/templates/content/changelog/2026-07-13-document-links-open-in-new-tabs.md +6 -0
  78. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +33 -16
  79. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -1
  80. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +4 -4
  81. package/corpus/templates/design/app/pages/DesignSystems.tsx +8 -10
  82. package/corpus/templates/design/app/pages/Index.tsx +6 -1
  83. package/corpus/templates/design/app/pages/Present.tsx +12 -6
  84. package/corpus/templates/design/changelog/2026-07-13-design-navigation-links-open-in-new-tabs.md +6 -0
  85. package/corpus/templates/design/changelog/2026-07-13-design-session-replays-include-preview-iframes.md +6 -0
  86. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -11
  87. package/corpus/templates/forms/app/pages/FormsListPage.tsx +6 -8
  88. package/corpus/templates/forms/changelog/2026-07-13-form-navigation-links-open-in-new-tabs.md +6 -0
  89. package/corpus/templates/mail/app/pages/DraftQueuePage.tsx +3 -8
  90. package/corpus/templates/mail/changelog/2026-07-13-settings-links-open-in-new-tabs.md +6 -0
  91. package/dist/agent/production-agent.d.ts +9 -0
  92. package/dist/agent/production-agent.d.ts.map +1 -1
  93. package/dist/agent/production-agent.js +25 -37
  94. package/dist/agent/production-agent.js.map +1 -1
  95. package/dist/audit/config.d.ts.map +1 -1
  96. package/dist/audit/config.js +0 -1
  97. package/dist/audit/config.js.map +1 -1
  98. package/dist/chat-threads/store.d.ts.map +1 -1
  99. package/dist/chat-threads/store.js +47 -33
  100. package/dist/chat-threads/store.js.map +1 -1
  101. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  102. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  103. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  104. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  105. package/dist/client/AssistantChat.d.ts.map +1 -1
  106. package/dist/client/AssistantChat.js +1 -1
  107. package/dist/client/AssistantChat.js.map +1 -1
  108. package/dist/client/analytics.d.ts.map +1 -1
  109. package/dist/client/analytics.js +11 -0
  110. package/dist/client/analytics.js.map +1 -1
  111. package/dist/client/chat/message-components.d.ts.map +1 -1
  112. package/dist/client/chat/message-components.js +9 -3
  113. package/dist/client/chat/message-components.js.map +1 -1
  114. package/dist/client/chat/tool-call-display.d.ts +3 -1
  115. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  116. package/dist/client/chat/tool-call-display.js +10 -2
  117. package/dist/client/chat/tool-call-display.js.map +1 -1
  118. package/dist/client/extensions/EmbeddedExtension.js +2 -6
  119. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  120. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  121. package/dist/client/extensions/ExtensionViewer.js +2 -59
  122. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  123. package/dist/client/index.d.ts +2 -0
  124. package/dist/client/index.d.ts.map +1 -1
  125. package/dist/client/index.js +2 -0
  126. package/dist/client/index.js.map +1 -1
  127. package/dist/client/settings/DemoModeSection.d.ts +1 -13
  128. package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
  129. package/dist/client/settings/DemoModeSection.js +5 -52
  130. package/dist/client/settings/DemoModeSection.js.map +1 -1
  131. package/dist/client/settings/SettingsPanel.js +1 -1
  132. package/dist/client/settings/SettingsPanel.js.map +1 -1
  133. package/dist/client/use-db-sync.d.ts.map +1 -1
  134. package/dist/client/use-db-sync.js +3 -7
  135. package/dist/client/use-db-sync.js.map +1 -1
  136. package/dist/client/use-demo-mode-status.d.ts +3 -4
  137. package/dist/client/use-demo-mode-status.d.ts.map +1 -1
  138. package/dist/client/use-demo-mode-status.js +8 -23
  139. package/dist/client/use-demo-mode-status.js.map +1 -1
  140. package/dist/collab/struct-routes.d.ts +1 -1
  141. package/dist/demo/browser-state.d.ts +10 -0
  142. package/dist/demo/browser-state.d.ts.map +1 -0
  143. package/dist/demo/browser-state.js +49 -0
  144. package/dist/demo/browser-state.js.map +1 -0
  145. package/dist/demo/fetch-interceptor.d.ts +2 -7
  146. package/dist/demo/fetch-interceptor.d.ts.map +1 -1
  147. package/dist/demo/fetch-interceptor.js +12 -56
  148. package/dist/demo/fetch-interceptor.js.map +1 -1
  149. package/dist/deploy/workspace-deploy.js +2 -2
  150. package/dist/deploy/workspace-deploy.js.map +1 -1
  151. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  152. package/dist/notifications/routes.d.ts +2 -2
  153. package/dist/observability/routes.d.ts +5 -5
  154. package/dist/resources/handlers.d.ts +3 -3
  155. package/dist/secrets/routes.d.ts +9 -9
  156. package/dist/server/action-discovery.d.ts.map +1 -1
  157. package/dist/server/action-discovery.js +0 -1
  158. package/dist/server/action-discovery.js.map +1 -1
  159. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  160. package/dist/server/agent-chat/action-filters-a2a.js +11 -1
  161. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  162. package/dist/server/agent-teams.d.ts.map +1 -1
  163. package/dist/server/agent-teams.js +15 -1
  164. package/dist/server/agent-teams.js.map +1 -1
  165. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  166. package/dist/server/core-routes-plugin.js +0 -27
  167. package/dist/server/core-routes-plugin.js.map +1 -1
  168. package/dist/server/google-oauth.d.ts.map +1 -1
  169. package/dist/server/google-oauth.js +6 -2
  170. package/dist/server/google-oauth.js.map +1 -1
  171. package/dist/server/transcribe-voice.d.ts +1 -1
  172. package/dist/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  173. package/package.json +2 -2
  174. package/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  175. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +0 -23
  176. package/corpus/core/src/demo/config.ts +0 -57
  177. package/dist/demo/actions/toggle-demo-mode.d.ts +0 -8
  178. package/dist/demo/actions/toggle-demo-mode.d.ts.map +0 -1
  179. package/dist/demo/actions/toggle-demo-mode.js +0 -21
  180. package/dist/demo/actions/toggle-demo-mode.js.map +0 -1
  181. package/dist/demo/config.d.ts +0 -9
  182. package/dist/demo/config.d.ts.map +0 -1
  183. package/dist/demo/config.js +0 -58
  184. package/dist/demo/config.js.map +0 -1
@@ -249,32 +249,40 @@
249
249
  The recorded visitor cursor is a core session-replay affordance: users need
250
250
  to see the Mac-style arrow where the visitor clicked, including on dark
251
251
  recordings. rrweb's bundled cursor is a small black sprite that disappears
252
- against those pages. This high-contrast arrow stays inside the replayer
253
- wrapper and scales with the fit-to-stage transform. */
252
+ against those pages. Use the normal black Mac cursor with its white outline;
253
+ the small shadow below keeps it legible on either background. */
254
254
  .an-replay-stage-root .replayer-mouse {
255
255
  position: absolute;
256
256
  z-index: 2;
257
- width: 1.125rem;
258
- height: 1.5rem;
257
+ /* Render at 2x locally, then downscale to the compact 0.72rem x 0.96rem
258
+ screen size. This keeps the small cursor's rounded outline smooth. */
259
+ width: 1.44rem;
260
+ height: 1.92rem;
259
261
  border-radius: 0;
260
262
  background: transparent;
261
263
  background-image: none;
262
264
  box-shadow: none;
263
- transform: none;
265
+ visibility: hidden;
266
+ /* The stage is fit-to-container, but the recorded cursor should stay at a
267
+ normal on-screen size. The stage supplies the inverse scale variable. */
268
+ transform: scale(calc(var(--an-replay-cursor-scale, 1) * 0.5));
269
+ transform-origin: top left;
264
270
  transition:
265
271
  left 80ms linear,
266
272
  top 80ms linear;
267
273
  pointer-events: none;
268
274
  }
269
275
 
276
+ .an-replay-stage-root .replayer-mouse.has-position {
277
+ visibility: visible;
278
+ }
279
+
270
280
  .an-replay-stage-root .replayer-mouse::before {
271
281
  content: "";
272
282
  position: absolute;
273
283
  inset: 0;
274
- background: hsl(0 0% 100%);
275
- clip-path: polygon(0 0, 0 77%, 30% 57%, 48% 100%, 68% 92%, 50% 51%, 92% 51%);
276
- filter: drop-shadow(0 0 1px hsl(0 0% 0% / 0.95))
277
- drop-shadow(0 1px 3px hsl(0 0% 0% / 0.35));
284
+ background: url("./assets/replay-cursor.svg") center / contain no-repeat;
285
+ filter: drop-shadow(0 1px 1px hsl(0 0% 0% / 0.4));
278
286
  }
279
287
 
280
288
  .an-replay-stage-root .replayer-mouse::after {
@@ -41,6 +41,12 @@ function daysAgo(n: number): string {
41
41
  return d.toISOString().slice(0, 10);
42
42
  }
43
43
 
44
+ // Keep the legacy "all" date-range sentinel out of provider queries. Analytics
45
+ // data cannot predate the Unix epoch, so this is equivalent to an unbounded
46
+ // lower date while remaining valid for BigQuery DATE/TIMESTAMP expressions.
47
+ const ALL_TIME_START = "1970-01-01";
48
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
49
+
44
50
  /** Date-valued filters whose default may use the "Nd" / "today" shorthand. */
45
51
  const DATE_FILTER_TYPES: ReadonlySet<FilterType> = new Set([
46
52
  "date",
@@ -70,6 +76,18 @@ function resolveDefault(raw: string | undefined, type: FilterType): string {
70
76
  return raw;
71
77
  }
72
78
 
79
+ function resolveDateValue(
80
+ raw: string | undefined,
81
+ allTimeValue: string,
82
+ ): string {
83
+ const value = raw?.trim();
84
+ if (!value) return "";
85
+ if (value.toLowerCase() === "all") return allTimeValue;
86
+
87
+ const resolved = resolveDefault(value, "date");
88
+ return ISO_DATE_RE.test(resolved) ? resolved : "";
89
+ }
90
+
73
91
  export function resolveFilterVars(
74
92
  filters: DashboardFilter[],
75
93
  getParam: (key: string) => string,
@@ -79,17 +97,27 @@ export function resolveFilterVars(
79
97
  if (f.type === "date-range") {
80
98
  const startKey = `${f.id}Start`;
81
99
  const endKey = `${f.id}End`;
82
- out[startKey] = getParam(startKey) || resolveDefault(f.default, f.type);
83
- out[endKey] = getParam(endKey) || daysAgo(0);
100
+ out[startKey] =
101
+ resolveDateValue(getParam(startKey), ALL_TIME_START) ||
102
+ resolveDateValue(resolveDefault(f.default, f.type), ALL_TIME_START);
103
+ out[endKey] =
104
+ resolveDateValue(getParam(endKey), daysAgo(0)) || daysAgo(0);
84
105
  } else if (f.type === "toggle" || f.type === "toggle-date") {
85
106
  // Toggles have no "off value" default — if the user hasn't opted in
86
107
  // via the URL, the SQL-side conditional block ({{?id}}...{{/id}})
87
108
  // must see an empty value so it doesn't emit. Otherwise the filter
88
109
  // looks "off" in the UI but still filters the data.
89
- out[f.id] = getParam(f.id);
110
+ out[f.id] =
111
+ f.type === "toggle-date"
112
+ ? resolveDateValue(getParam(f.id), ALL_TIME_START)
113
+ : getParam(f.id);
90
114
  } else {
91
115
  const v = getParam(f.id);
92
- out[f.id] = v || resolveDefault(f.default, f.type);
116
+ out[f.id] =
117
+ f.type === "date"
118
+ ? resolveDateValue(v, ALL_TIME_START) ||
119
+ resolveDateValue(resolveDefault(f.default, f.type), ALL_TIME_START)
120
+ : v || resolveDefault(f.default, f.type);
93
121
  }
94
122
  }
95
123
  return out;
@@ -297,7 +297,10 @@ const PanelCell = memo(function PanelCell({
297
297
  [panel, vars],
298
298
  );
299
299
  const resolvedSql = useMemo(
300
- () => interpolate(serializePanelSql(panel.sql), vars),
300
+ () =>
301
+ interpolate(serializePanelSql(panel.sql), vars, {
302
+ failClosedTimeVariables: true,
303
+ }),
301
304
  [panel.sql, vars],
302
305
  );
303
306
  const handleSelectForChat = useCallback(
@@ -14,9 +14,23 @@ function escapeSqlValue(value: string): string {
14
14
  return value.replace(/'/g, "''");
15
15
  }
16
16
 
17
+ export interface InterpolateOptions {
18
+ /**
19
+ * Replace missing/empty time variables with a value that cannot satisfy the
20
+ * canonical dashboard predicates. This keeps a stale or malformed panel
21
+ * fail-closed at render time instead of turning into an all-time query.
22
+ */
23
+ failClosedTimeVariables?: boolean;
24
+ }
25
+
26
+ function isTimeVariable(name: string): boolean {
27
+ return name === "timeRange" || /(?:Start|End)$/.test(name);
28
+ }
29
+
17
30
  export function interpolate(
18
31
  sql: string | undefined | null,
19
32
  vars: Record<string, string> = {},
33
+ options: InterpolateOptions = {},
20
34
  ): string {
21
35
  // Defensive: a malformed dashboard config (e.g. agent wrote a panel without
22
36
  // a `sql` field) should not crash the page. Treat missing/non-string SQL
@@ -33,6 +47,13 @@ export function interpolate(
33
47
 
34
48
  return withConditionals.replace(/\{\{(\w+)\}\}/g, (_match, name) => {
35
49
  const value = vars[name];
50
+ if (
51
+ options.failClosedTimeVariables &&
52
+ isTimeVariable(name) &&
53
+ (value == null || value.length === 0)
54
+ ) {
55
+ return "__missing_dashboard_time_filter__";
56
+ }
36
57
  if (value == null) return "";
37
58
  return escapeSqlValue(String(value));
38
59
  });
@@ -63,7 +63,21 @@ export interface PivotConfig {
63
63
  valueKey: string;
64
64
  }
65
65
 
66
+ /**
67
+ * Declares how a query's time coverage relates to the dashboard filter.
68
+ * `dashboard` is the default for ordinary event metrics; the other values are
69
+ * explicit exceptions that make intentional history scans visible to agents
70
+ * and reviewers.
71
+ */
72
+ export type DashboardTimeScope =
73
+ | "dashboard"
74
+ | "fixed-window"
75
+ | "cohort-history"
76
+ | "all-time";
77
+
66
78
  export interface SqlPanelConfig {
79
+ /** Time coverage contract for first-party SQL panels. */
80
+ timeScope?: DashboardTimeScope;
67
81
  xKey?: string;
68
82
  yKey?: string;
69
83
  yKeys?: string[];
@@ -102,7 +102,7 @@ export function IssueDetail({
102
102
  {statusLabel(issue.status)}
103
103
  </span>
104
104
  </div>
105
- <h2 className="truncate text-lg font-semibold text-foreground">
105
+ <h2 className="break-words text-lg font-semibold text-foreground">
106
106
  {issue.title}
107
107
  </h2>
108
108
  {issue.culprit ? (
@@ -34,6 +34,7 @@ import {
34
34
  } from "@tabler/icons-react";
35
35
  import { useQuery } from "@tanstack/react-query";
36
36
  import {
37
+ type CSSProperties,
37
38
  type ReactNode,
38
39
  useCallback,
39
40
  useEffect,
@@ -46,6 +47,13 @@ import { Link, useParams } from "react-router";
46
47
 
47
48
  import { Button } from "@/components/ui/button";
48
49
  import { Card, CardContent } from "@/components/ui/card";
50
+ import {
51
+ DropdownMenu,
52
+ DropdownMenuContent,
53
+ DropdownMenuRadioGroup,
54
+ DropdownMenuRadioItem,
55
+ DropdownMenuTrigger,
56
+ } from "@/components/ui/dropdown-menu";
49
57
  import { Input } from "@/components/ui/input";
50
58
  import {
51
59
  Popover,
@@ -711,6 +719,7 @@ function ReplayPlayer({
711
719
  if (!stageRootRef.current) return;
712
720
  let cancelled = false;
713
721
  let localReplayer: any = null;
722
+ let stopCursorVisibilityObserver = () => {};
714
723
 
715
724
  async function loadReplay() {
716
725
  const replayEvents = eventsRef.current;
@@ -761,6 +770,8 @@ function ReplayPlayer({
761
770
  // Do not mutate recorded URLs/CSS; suppress viewer-page referrer leakage
762
771
  // at the iframe boundary while retaining historical visual resources.
763
772
  localReplayer.iframe?.setAttribute?.("referrerpolicy", "no-referrer");
773
+ stopCursorVisibilityObserver =
774
+ hideReplayCursorUntilPosition(localReplayer);
764
775
  replayerRef.current = localReplayer;
765
776
  const meta = localReplayer.getMetaData?.();
766
777
  const total = Number(meta?.totalTime ?? replayDuration(replayEvents));
@@ -827,6 +838,7 @@ function ReplayPlayer({
827
838
 
828
839
  return () => {
829
840
  cancelled = true;
841
+ stopCursorVisibilityObserver();
830
842
  if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
831
843
  rafRef.current = null;
832
844
  try {
@@ -977,22 +989,26 @@ function ReplayPlayer({
977
989
  <div
978
990
  ref={stageRootRef}
979
991
  className="an-replay-stage-root absolute left-1/2 top-1/2"
980
- style={{
981
- width: playerWidth,
982
- height: playerHeight,
983
- transform: `translate(-50%, -50%) scale(${fitScale})`,
984
- transformOrigin: "center center",
985
- }}
992
+ style={
993
+ {
994
+ width: playerWidth,
995
+ height: playerHeight,
996
+ "--an-replay-cursor-scale": String(1 / fitScale),
997
+ transform: `translate(-50%, -50%) scale(${fitScale})`,
998
+ transformOrigin: "center center",
999
+ } as CSSProperties
1000
+ }
986
1001
  />
987
1002
  <button
988
1003
  type="button"
989
1004
  className={cn(
990
1005
  "absolute inset-0 z-20 rounded-[inherit] border-0 bg-transparent p-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-default",
991
- // INTENTIONAL — DO NOT REMOVE `cursor-none` while playing.
992
- // The recorded cursor is the meaningful pointer here;
993
- // hiding the viewer's stationary pointer keeps the
994
- // Mac-style replay cursor easy to follow.
995
- playing ? "cursor-none" : "cursor-pointer",
1006
+ // INTENTIONAL — KEEP THE VIEWER CURSOR VISIBLE.
1007
+ // The recorded cursor is a separate overlay, while this
1008
+ // button owns the live hover target for pause/play. Do
1009
+ // not add `cursor-none`: it makes the user's cursor
1010
+ // disappear when they move over the preview.
1011
+ "cursor-pointer",
996
1012
  )}
997
1013
  disabled={disabled}
998
1014
  aria-label={
@@ -1078,22 +1094,34 @@ function ReplayPlayer({
1078
1094
  {formatClock(totalTime)}
1079
1095
  </span>
1080
1096
 
1081
- <div className="flex items-center rounded-md bg-muted p-1">
1082
- {SPEED_OPTIONS.map((option) => (
1097
+ <DropdownMenu>
1098
+ <DropdownMenuTrigger asChild>
1083
1099
  <button
1084
- key={option}
1085
1100
  type="button"
1086
- className={cn(
1087
- "rounded px-2 py-1 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
1088
- speed === option &&
1089
- "bg-background text-foreground shadow-sm",
1090
- )}
1091
- onClick={() => updateSpeed(option)}
1101
+ disabled={disabled}
1102
+ aria-label={`${speed}x`}
1103
+ className="inline-flex h-8 min-w-11 items-center justify-center rounded-md border px-2 text-xs font-medium tabular-nums transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
1092
1104
  >
1093
- {option}x
1105
+ {speed}x
1094
1106
  </button>
1095
- ))}
1096
- </div>
1107
+ </DropdownMenuTrigger>
1108
+ <DropdownMenuContent align="center" side="top">
1109
+ <DropdownMenuRadioGroup
1110
+ value={String(speed)}
1111
+ onValueChange={(value) => updateSpeed(Number(value))}
1112
+ >
1113
+ {SPEED_OPTIONS.map((option) => (
1114
+ <DropdownMenuRadioItem
1115
+ key={option}
1116
+ value={String(option)}
1117
+ className="tabular-nums"
1118
+ >
1119
+ {option}x
1120
+ </DropdownMenuRadioItem>
1121
+ ))}
1122
+ </DropdownMenuRadioGroup>
1123
+ </DropdownMenuContent>
1124
+ </DropdownMenu>
1097
1125
 
1098
1126
  <Tooltip>
1099
1127
  <TooltipTrigger asChild>
@@ -2278,6 +2306,32 @@ function hasPlayableReplayEvents(events: unknown[]): boolean {
2278
2306
  return false;
2279
2307
  }
2280
2308
 
2309
+ function hideReplayCursorUntilPosition(replayer: any): () => void {
2310
+ const cursor = replayer?.mouse as HTMLElement | undefined;
2311
+ if (!cursor || typeof MutationObserver === "undefined") return () => {};
2312
+
2313
+ let observer: MutationObserver | null = null;
2314
+ const revealWhenPositioned = () => {
2315
+ if (!cursor.style.left || !cursor.style.top) return;
2316
+ cursor.classList.add("has-position");
2317
+ observer?.disconnect();
2318
+ observer = null;
2319
+ };
2320
+
2321
+ cursor.classList.remove("has-position");
2322
+ observer = new MutationObserver(revealWhenPositioned);
2323
+ observer.observe(cursor, {
2324
+ attributes: true,
2325
+ attributeFilter: ["style"],
2326
+ });
2327
+ revealWhenPositioned();
2328
+
2329
+ return () => {
2330
+ observer?.disconnect();
2331
+ observer = null;
2332
+ };
2333
+ }
2334
+
2281
2335
  export function replayViewportDimensions(
2282
2336
  events: AnyReplayEvent[],
2283
2337
  ): ReplayViewportDimensions | null {
@@ -22,7 +22,7 @@ import {
22
22
  } from "@tabler/icons-react";
23
23
  import { useQuery } from "@tanstack/react-query";
24
24
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
25
- import { Link, useNavigate, useSearchParams } from "react-router";
25
+ import { Link, useSearchParams } from "react-router";
26
26
  import { toast } from "sonner";
27
27
 
28
28
  import { Badge } from "@/components/ui/badge";
@@ -205,7 +205,6 @@ export function useDebouncedUrlFilter(
205
205
 
206
206
  export default function SessionsPage() {
207
207
  const t = useT();
208
- const navigate = useNavigate();
209
208
  const [searchParams, setSearchParams] = useSearchParams();
210
209
  const range = readRange(searchParams.get("range"));
211
210
  const app = searchParams.get("app") ?? "";
@@ -389,11 +388,10 @@ export default function SessionsPage() {
389
388
  recording.startedAt;
390
389
  const deviceLabel = sessionDeviceLabel(recording);
391
390
  return (
392
- <button
391
+ <Link
393
392
  key={recording.id}
394
- type="button"
393
+ to={href}
395
394
  className="analytics-session-row grid w-full gap-3 px-4 py-3 text-left transition-colors hover:bg-muted/35 focus-visible:bg-muted/35 focus-visible:outline-none"
396
- onClick={() => navigate(href)}
397
395
  aria-label={t("sessions.watchReplay")}
398
396
  >
399
397
  <span className="inline-flex h-10 w-[92px] items-center justify-center gap-2 rounded-md bg-primary/10 font-medium text-primary">
@@ -446,7 +444,7 @@ export default function SessionsPage() {
446
444
  </span>
447
445
  ) : null}
448
446
  </span>
449
- </button>
447
+ </Link>
450
448
  );
451
449
  })}
452
450
  </div>
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-13
4
+ ---
5
+
6
+ All-time dashboard filters no longer send invalid dates to BigQuery
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-13
4
+ ---
5
+
6
+ Dashboard charts now honor the selected time range by default and reject accidental unbounded first-party queries
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-13
4
+ ---
5
+
6
+ Error issue titles wrap instead of overflowing
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-13
4
+ ---
5
+
6
+ Keep real authorized identities in Analytics incident lookups when browser Demo mode is enabled
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-13
4
+ ---
5
+
6
+ Saving a new dashboard view no longer fails with a server error
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-13
4
+ ---
5
+
6
+ Session replay cursors stay hidden until their first recorded position
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-13
4
+ ---
5
+
6
+ Session replay links now support Cmd-click and middle-click to open in a new tab
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-13
4
+ ---
5
+
6
+ The Analytics Agents tab is available again from the main navigation.
@@ -226,7 +226,7 @@
226
226
  "chartType": "bar",
227
227
  "source": "first-party",
228
228
  "width": 2,
229
- "sql": "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",
229
+ "sql": "SELECT COALESCE(NULLIF(app, ''), 'unknown') AS app, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))) AND ('{{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')) GROUP BY COALESCE(NULLIF(app, ''), 'unknown') ORDER BY count DESC LIMIT 20",
230
230
  "config": {
231
231
  "xKey": "app",
232
232
  "yKey": "count",
@@ -240,7 +240,7 @@
240
240
  "chartType": "bar",
241
241
  "source": "first-party",
242
242
  "width": 1,
243
- "sql": "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",
243
+ "sql": "SELECT COALESCE(NULLIF(signed_in, ''), 'unknown') AS signed_in, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))) AND ('{{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')) GROUP BY COALESCE(NULLIF(signed_in, ''), 'unknown') ORDER BY signed_in",
244
244
  "config": {
245
245
  "xKey": "signed_in",
246
246
  "yKey": "count",
@@ -254,7 +254,7 @@
254
254
  "chartType": "area",
255
255
  "source": "first-party",
256
256
  "width": 1,
257
- "sql": "SELECT event_date AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' GROUP BY event_date ORDER BY date",
257
+ "sql": "SELECT event_date AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))) AND ('{{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')) GROUP BY event_date ORDER BY date",
258
258
  "config": {
259
259
  "xKey": "date",
260
260
  "yKey": "count",
@@ -56,6 +56,10 @@ function panelSummaries(config: Record<string, unknown>) {
56
56
  width: typeof panel.width === "number" ? panel.width : undefined,
57
57
  columns: typeof panel.columns === "number" ? panel.columns : undefined,
58
58
  tab: typeof panel.tab === "string" ? panel.tab : undefined,
59
+ timeScope:
60
+ typeof panelConfig.timeScope === "string"
61
+ ? panelConfig.timeScope
62
+ : undefined,
59
63
  description:
60
64
  typeof panelConfig.description === "string"
61
65
  ? panelConfig.description