@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
@@ -119,11 +119,11 @@ details live in `.agents/skills/`.
119
119
  without raw replay events or storage references. Do not add replay blob or
120
120
  dashboard mutation actions to this catalog without an explicit security
121
121
  review.
122
- - While Demo mode is enabled, session and error identities display anonymized
123
- (`anonymous@builder.io`), so incident lookups must filter by the stable
122
+ - Demo mode is a browser-local presentation preference. Analytics backend
123
+ actions, agent/MCP reads, session replays, and error issues always return real
124
+ authorized identities, so incident lookups should use the actual
124
125
  `userId`/email parameter (e.g. `userId: "user@example.com"`) or recording/
125
- issue id — never by matching emails read back from previously displayed
126
- output.
126
+ issue id.
127
127
  - Analytics keeps its direct MCP surface explicitly curated, so external
128
128
  agents should use `ask_app` for multi-step investigation and changes. The
129
129
  six incident reads above are bounded, user/org-scoped fallback tools for
@@ -11,6 +11,7 @@ import {
11
11
  } from "@agent-native/core/server";
12
12
  import { z } from "zod";
13
13
 
14
+ import { validateFirstPartyDashboardTimeScope } from "../server/lib/dashboard-time-scope";
14
15
  import {
15
16
  getDashboard,
16
17
  upsertDashboard,
@@ -138,7 +139,7 @@ export default defineAction({
138
139
  "If the dashboard already exists and `overwrite` is false (default), the new panels are APPENDED (panels whose id is already present are skipped); with `overwrite: true` the config is replaced. " +
139
140
  "Returns { dashboardId, panelCount, createdMetrics, unknownMetrics, invalidMetrics, urlPath, deepLink, message } — use panelCount as proof-of-done. " +
140
141
  `Available metric keys: ${METRIC_KEYS.join(", ")}. ` +
141
- "Each metric accepts an optional per-metric `window` of '30d' | '90d' | 'all' (only affects windowed virality/time metrics).",
142
+ "Each metric accepts an optional per-metric `window` of '30d' | '90d' | 'all' (only affects windowed virality/time metrics). Catalog panels are time-scoped by construction; custom first-party SQL added later must use `{{timeRange}}` or declare `config.timeScope`.",
142
143
  schema: z.object({
143
144
  dashboardId: z
144
145
  .string()
@@ -200,6 +201,7 @@ export default defineAction({
200
201
  const unknownMetrics: string[] = [];
201
202
  const invalidMetrics: Array<{ metric: string; reason: string }> = [];
202
203
  const composedPanels: ComposedPanel[] = [];
204
+ const catalogDashboard = withFirstPartyDashboardFilters({ filters: [] });
203
205
 
204
206
  for (const req of requests) {
205
207
  const panel = buildPanel(req.metric, {
@@ -220,6 +222,15 @@ export default defineAction({
220
222
  // rest of the dashboard still builds. (Catalog SQL is known-good, so this
221
223
  // is a defensive net, e.g. if a future window/override produces bad SQL.)
222
224
  try {
225
+ const timeScopeError = validateFirstPartyDashboardTimeScope(
226
+ panel,
227
+ catalogDashboard,
228
+ 0,
229
+ );
230
+ if (timeScopeError) {
231
+ invalidMetrics.push({ metric: req.metric, reason: timeScopeError });
232
+ continue;
233
+ }
223
234
  validateFirstPartyAnalyticsSql(panel.sql);
224
235
  } catch (e: any) {
225
236
  invalidMetrics.push({
@@ -32,6 +32,17 @@ type DashboardPatch = {
32
32
  parentId?: string;
33
33
  };
34
34
 
35
+ type PanelTimeScope =
36
+ | "dashboard"
37
+ | "fixed-window"
38
+ | "cohort-history"
39
+ | "all-time";
40
+
41
+ type PanelConfig = Record<string, unknown> & {
42
+ /** Use "dashboard" for AI-generated first-party panels by default. */
43
+ timeScope?: PanelTimeScope;
44
+ };
45
+
35
46
  type PanelPatch = {
36
47
  title?: string;
37
48
  sql?: string;
@@ -40,7 +51,7 @@ type PanelPatch = {
40
51
  width?: number;
41
52
  columns?: number;
42
53
  tab?: string;
43
- config?: Record<string, unknown>;
54
+ config?: PanelConfig;
44
55
  description?: string; // shorthand for config.description
45
56
  };
46
57
 
@@ -112,9 +123,9 @@ export const DASHBOARD_MUTATION_EXAMPLES = [
112
123
  'dashboard.panelsMatching({"source":"first-party"}).setWidth(2);',
113
124
  'dashboard.panel("retention").setConfigPath("yAxis.format","percent");',
114
125
  'dashboard.section("retention-activity-section").append(["repeat-users","retention-over-time"]);',
115
- 'dashboard.insertPanel({"id":"new-kpi","title":"New KPI","source":"first-party","chartType":"metric","width":1,"sql":"SELECT COUNT(*) AS value FROM analytics_events"}).atTop();',
116
- 'dashboard.insertPanel({"id":"new-chart","title":"New Chart","source":"first-party","chartType":"line","width":1,"sql":"SELECT date, COUNT(*) AS value FROM analytics_events GROUP BY date ORDER BY date"}).nextTo("retention-over-time");',
117
- 'dashboard.insertPanel({"id":"row-chart","title":"Row Chart","source":"first-party","chartType":"bar","width":1,"sql":"SELECT name, COUNT(*) AS value FROM analytics_events GROUP BY name"}).atRow(2);',
126
+ `dashboard.insertPanel({"id":"new-kpi","title":"New KPI","source":"first-party","chartType":"metric","width":1,"config":{"timeScope":"dashboard"},"sql":"SELECT COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'"}).atTop();`,
127
+ `dashboard.insertPanel({"id":"new-chart","title":"New Chart","source":"first-party","chartType":"line","width":1,"config":{"timeScope":"dashboard"},"sql":"SELECT event_date AS date, COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}' GROUP BY event_date ORDER BY event_date"}).nextTo("retention-over-time");`,
128
+ `dashboard.insertPanel({"id":"row-chart","title":"Row Chart","source":"first-party","chartType":"bar","width":1,"config":{"timeScope":"dashboard"},"sql":"SELECT event_name AS name, COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}' GROUP BY event_name"}).atRow(2);`,
118
129
  'dashboard.insertPanel({"id":"pipeline-widget","title":"Pipeline Widget","chartType":"extension","width":3,"config":{"extensionId":"<existing-extension-id>"}}).atBottom();',
119
130
  ] as const;
120
131
 
@@ -122,6 +122,7 @@ const apiHelp =
122
122
  "Subjects: dashboard.set, dashboard.panel, dashboard.panels, dashboard.panelsMatching, dashboard.section, dashboard.insertPanel. " +
123
123
  "Selection methods: moveToTop, moveToBottom, moveBefore, moveAfter, moveToIndex, moveNextTo, moveToRow, remove, set, setTitle, setSql, setWidth, setConfig, setConfigPath, duplicate. " +
124
124
  "Inserted panels support atTop, atBottom, before, after, atIndex, nextTo, atRow, atRowStart, and atRowEnd. Use nextTo(panelId) or atRow(rowNumber) for visible row placement. " +
125
+ "AI-generated first-party panels are dashboard-time-bound by default: set config.timeScope to dashboard and include a matching dashboard time filter in SQL. Allowed 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. A {{timeRange}} token requires the timeRange select filter; {{<id>Start}}/{{<id>End}} require a matching date-range filter. Server validation rejects unbound first-party SQL. " +
125
126
  `Examples: ${DASHBOARD_MUTATION_EXAMPLES.slice(0, 5).join(" ")}`;
126
127
 
127
128
  function resolveScope() {
@@ -198,7 +199,7 @@ export default defineAction({
198
199
  "Apply general SQL dashboard edits through a small typed mutation API in ONE atomic save. " +
199
200
  "Prefer this for dashboard layout and panel edits: move panels by id, edit titles/SQL/width/config, remove panels, duplicate panels, insert panels, or patch dashboard fields. " +
200
201
  "For user placement requests like 'second row' or 'next to return rates', use row-aware placement such as `dashboard.insertPanel(...).nextTo(\"retention-over-time\")` or `.atRow(2)`, then verify rendered rows from `get-sql-dashboard.layout.groups`. " +
201
- "This is code-shaped but not arbitrary code execution: the server parses the allowed dashboard methods, validates the resulting config with the same invariants as update-dashboard, saves once, syncs collab, and returns compact proof. " +
202
+ "This is code-shaped but not arbitrary code execution: the server parses the allowed dashboard methods, validates the resulting config with the same invariants as update-dashboard, saves once, syncs collab, and returns compact proof. First-party SQL must be explicitly time-bound as described in the API help; server validation rejects unbound first-party SQL. " +
202
203
  "The main code argument is a string, so it avoids brittle JSON-pointer indexes and native-array serialization issues. " +
203
204
  `Common example: ${DASHBOARD_MUTATION_EXAMPLES[0]}`,
204
205
  schema: z.object({
@@ -13,6 +13,7 @@ import { z } from "zod";
13
13
 
14
14
  import { interpolate } from "../app/pages/adhoc/sql-dashboard/interpolate";
15
15
  import { dryRunQuery } from "../server/lib/bigquery";
16
+ import { validateFirstPartyDashboardTimeScope } from "../server/lib/dashboard-time-scope";
16
17
  import {
17
18
  upsertDashboard,
18
19
  upsertDashboardWithRetry,
@@ -421,8 +422,20 @@ export async function validatePanelSql(
421
422
  const raw = typeof p.sql === "string" ? p.sql : "";
422
423
  if (raw.trim()) {
423
424
  try {
425
+ const timeScopeError = validateFirstPartyDashboardTimeScope(
426
+ p,
427
+ config,
428
+ i,
429
+ );
430
+ if (timeScopeError) return timeScopeError;
424
431
  validateFirstPartyAnalyticsSql(interpolate(raw, vars));
425
432
  } catch (e: any) {
433
+ if (
434
+ typeof e?.message === "string" &&
435
+ e.message.startsWith("panel[")
436
+ ) {
437
+ return e.message;
438
+ }
426
439
  return `panel[${i}] "${p.title || p.id}" first-party analytics SQL is invalid: ${e?.message ?? e}`;
427
440
  }
428
441
  }
@@ -552,6 +565,7 @@ export default defineAction({
552
565
  "Use this action when creating a brand-new dashboard from a complete config, for the UI full-config save path, or for an explicitly requested low-level JSON-pointer compatibility edit. " +
553
566
  "Do not use `ops` or `panelOrder` for ordinary agent edits like moving charts, adding panels to an existing dashboard, changing widths, or updating panel config; call `mutate-dashboard` once with the full edit script. " +
554
567
  "When this action is appropriate, provide only one of `ops`, `panelOrder`, or `config`; `config` replaces the whole dashboard config. " +
568
+ "First-party event panels must bind to a declared dashboard time filter with `{{timeRange}}` or date-range variables. Intentional fixed-window, cohort-history, and all-time exceptions must be explicit in `panel.config.timeScope`; unbounded first-party SQL is rejected at save time. " +
555
569
  "To add a shipped catalog template's panels to an existing dashboard, prefer `install-dashboard-template` with `mergePanels: true` — it appends the template's panels in one call without you having to author each panel. " +
556
570
  "The result is compact by default: `panelCount`, `appliedOps`, `panelOrder`, `firstPanelIds`, and `summary`. Set `returnConfig: true` only when you truly need the full config in the tool result. " +
557
571
  "The UI auto-refreshes after this action — do NOT call `refresh-screen`.",
@@ -14,6 +14,7 @@ import {
14
14
  IconReportAnalytics,
15
15
  IconSearch,
16
16
  IconArchive,
17
+ IconActivity,
17
18
  IconHeartbeat,
18
19
  IconPin,
19
20
  IconPlus,
@@ -26,7 +27,6 @@ import {
26
27
  IconPlayerPlay,
27
28
  IconLayoutSidebarLeftCollapse,
28
29
  IconLayoutSidebarLeftExpand,
29
- IconShieldCheck,
30
30
  } from "@tabler/icons-react";
31
31
  import {
32
32
  useQuery,
@@ -97,7 +97,7 @@ import {
97
97
  type ChatThreadSummary,
98
98
  } from "@agent-native/core/client";
99
99
  import { ExtensionsSidebarSection } from "@agent-native/core/client/extensions";
100
- import { OrgSwitcher, useOrgRole } from "@agent-native/core/client/org";
100
+ import { OrgSwitcher } from "@agent-native/core/client/org";
101
101
 
102
102
  import {
103
103
  AlertDialog,
@@ -1593,7 +1593,6 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
1593
1593
  const navigate = useNavigate();
1594
1594
  const t = useT();
1595
1595
  const queryClient = useQueryClient();
1596
- const { canManageOrg } = useOrgRole();
1597
1596
  const { setTheme } = useTheme();
1598
1597
 
1599
1598
  const isAskRoute = location.pathname === "/ask";
@@ -2428,16 +2427,12 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
2428
2427
  href: "/monitoring",
2429
2428
  active: location.pathname.startsWith("/monitoring"),
2430
2429
  },
2431
- ...(canManageOrg
2432
- ? [
2433
- {
2434
- icon: IconShieldCheck,
2435
- label: t("navigation.admin"),
2436
- href: "/agents?view=dashboards",
2437
- active: location.pathname.startsWith("/agents"),
2438
- },
2439
- ]
2440
- : []),
2430
+ {
2431
+ icon: IconActivity,
2432
+ label: t("navigation.agents"),
2433
+ href: "/agents",
2434
+ active: location.pathname.startsWith("/agents"),
2435
+ },
2441
2436
  {
2442
2437
  icon: IconDatabase,
2443
2438
  label: t("navigation.dataSources"),
@@ -2641,20 +2636,19 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
2641
2636
  {t("navigation.monitoring")}
2642
2637
  </Link>
2643
2638
 
2644
- {canManageOrg ? (
2645
- <Link
2646
- to="/agents?view=dashboards"
2647
- className={cn(
2648
- "order-6 flex items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:text-primary",
2649
- location.pathname.startsWith("/agents")
2650
- ? "bg-sidebar-accent text-sidebar-accent-foreground"
2651
- : "text-muted-foreground hover:bg-sidebar-accent/50",
2652
- )}
2653
- >
2654
- <IconShieldCheck className="h-4 w-4" />
2655
- {t("navigation.admin")}
2656
- </Link>
2657
- ) : null}
2639
+ {/* Agents link */}
2640
+ <Link
2641
+ to="/agents"
2642
+ className={cn(
2643
+ "order-6 flex items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:text-primary",
2644
+ location.pathname.startsWith("/agents")
2645
+ ? "bg-sidebar-accent text-sidebar-accent-foreground"
2646
+ : "text-muted-foreground hover:bg-sidebar-accent/50",
2647
+ )}
2648
+ >
2649
+ <IconActivity className="h-4 w-4" />
2650
+ {t("navigation.agents")}
2651
+ </Link>
2658
2652
 
2659
2653
  {/* Data Sources link */}
2660
2654
  <Link
@@ -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[];
@@ -47,6 +47,13 @@ import { Link, useParams } from "react-router";
47
47
 
48
48
  import { Button } from "@/components/ui/button";
49
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";
50
57
  import { Input } from "@/components/ui/input";
51
58
  import {
52
59
  Popover,
@@ -1087,22 +1094,34 @@ function ReplayPlayer({
1087
1094
  {formatClock(totalTime)}
1088
1095
  </span>
1089
1096
 
1090
- <div className="flex items-center rounded-md bg-muted p-1">
1091
- {SPEED_OPTIONS.map((option) => (
1097
+ <DropdownMenu>
1098
+ <DropdownMenuTrigger asChild>
1092
1099
  <button
1093
- key={option}
1094
1100
  type="button"
1095
- className={cn(
1096
- "rounded px-2 py-1 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
1097
- speed === option &&
1098
- "bg-background text-foreground shadow-sm",
1099
- )}
1100
- 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"
1101
1104
  >
1102
- {option}x
1105
+ {speed}x
1103
1106
  </button>
1104
- ))}
1105
- </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>
1106
1125
 
1107
1126
  <Tooltip>
1108
1127
  <TooltipTrigger asChild>
@@ -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
+ 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
+ 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
@@ -0,0 +1,170 @@
1
+ import type { DashboardTimeScope } from "../../app/pages/adhoc/sql-dashboard/types";
2
+
3
+ export const DASHBOARD_TIME_SCOPES: readonly DashboardTimeScope[] = [
4
+ "dashboard",
5
+ "fixed-window",
6
+ "cohort-history",
7
+ "all-time",
8
+ ];
9
+
10
+ type DashboardFilterLike = {
11
+ id?: unknown;
12
+ key?: unknown;
13
+ type?: unknown;
14
+ default?: unknown;
15
+ };
16
+
17
+ type DashboardPanelLike = {
18
+ id?: unknown;
19
+ title?: unknown;
20
+ chartType?: unknown;
21
+ source?: unknown;
22
+ sql?: unknown;
23
+ config?: unknown;
24
+ };
25
+
26
+ type DashboardConfigLike = {
27
+ filters?: unknown;
28
+ };
29
+
30
+ const TEMPORAL_VARIABLE_RE = /\{\{(timeRange|[A-Za-z_]\w*(?:Start|End))\}\}/g;
31
+
32
+ /** Return the time variables a panel actually references. */
33
+ export function extractDashboardTimeVariables(sql: string): string[] {
34
+ const variables = new Set<string>();
35
+ for (const match of sql.matchAll(TEMPORAL_VARIABLE_RE)) {
36
+ variables.add(match[1]);
37
+ }
38
+ return [...variables];
39
+ }
40
+
41
+ function filterId(filter: DashboardFilterLike): string {
42
+ const value = filter.id ?? filter.key;
43
+ return typeof value === "string" ? value.trim() : "";
44
+ }
45
+
46
+ function filtersFrom(config: DashboardConfigLike): DashboardFilterLike[] {
47
+ return Array.isArray(config.filters)
48
+ ? (config.filters as DashboardFilterLike[])
49
+ : [];
50
+ }
51
+
52
+ function panelConfig(panel: DashboardPanelLike): Record<string, unknown> {
53
+ return panel.config && typeof panel.config === "object"
54
+ ? (panel.config as Record<string, unknown>)
55
+ : {};
56
+ }
57
+
58
+ function hasExplicitLowerBound(sql: string): boolean {
59
+ // This intentionally recognizes the bounded shapes used by the shipped
60
+ // first-party catalog. It is a compatibility escape hatch for fixed-window
61
+ // catalog metrics; new ad-hoc panels should use a dashboard placeholder.
62
+ return /\b(?:event_date|timestamp|started_at|ended_at|cohort_date|created_at|date)\b[\s\S]{0,100}?(?:>=|>)\s*[\s\S]{0,160}?(?:CURRENT_DATE|CURRENT_TIMESTAMP|NOW\s*\(|INTERVAL\s*['"]|DATE\s*['"]|TIMESTAMP\s*['"]|\b20\d{2}-\d{2}-\d{2}\b)/i.test(
63
+ sql,
64
+ );
65
+ }
66
+
67
+ function hasIntentionalHistoryDescription(
68
+ panel: DashboardPanelLike,
69
+ config: Record<string, unknown>,
70
+ ): boolean {
71
+ const values = [panel.title, config.description].filter(
72
+ (value): value is string => typeof value === "string",
73
+ );
74
+ return /all[- ]?time|lifetime|histor(?:y|ical)/i.test(values.join(" "));
75
+ }
76
+
77
+ function scopeValue(panel: DashboardPanelLike): DashboardTimeScope | undefined {
78
+ const value = panelConfig(panel).timeScope;
79
+ return typeof value === "string" &&
80
+ (DASHBOARD_TIME_SCOPES as readonly string[]).includes(value)
81
+ ? (value as DashboardTimeScope)
82
+ : undefined;
83
+ }
84
+
85
+ /**
86
+ * Validate the temporal contract for a first-party dashboard panel.
87
+ *
88
+ * Ordinary panels must bind to a dashboard filter. Intentional exceptions are
89
+ * explicit in `config.timeScope`, while legacy fixed-window catalog SQL is
90
+ * accepted when its lower bound is visible in the SQL itself.
91
+ */
92
+ export function validateFirstPartyDashboardTimeScope(
93
+ panel: DashboardPanelLike,
94
+ dashboard: DashboardConfigLike,
95
+ index: number,
96
+ ): string | null {
97
+ if (panel.source !== "first-party") return null;
98
+ if (panel.chartType === "section" || panel.chartType === "extension") {
99
+ return null;
100
+ }
101
+
102
+ const sql = typeof panel.sql === "string" ? panel.sql : "";
103
+ const variables = extractDashboardTimeVariables(sql);
104
+ const filters = filtersFrom(dashboard);
105
+ const label =
106
+ typeof panel.title === "string" && panel.title.trim()
107
+ ? `"${panel.title}"`
108
+ : `at index ${index}`;
109
+
110
+ for (const variable of variables) {
111
+ const expectedFilterId =
112
+ variable === "timeRange"
113
+ ? "timeRange"
114
+ : variable.replace(/(?:Start|End)$/, "");
115
+ const filter = filters.find(
116
+ (candidate) => filterId(candidate) === expectedFilterId,
117
+ );
118
+ if (!filter) {
119
+ return `panel[${index}] ${label} uses {{${variable}}} but config.filters has no matching "${expectedFilterId}" filter; bind first-party SQL to a declared dashboard time filter`;
120
+ }
121
+ if (variable === "timeRange") {
122
+ if (filter.type !== "select") {
123
+ return `panel[${index}] ${label} uses {{timeRange}}, but filter "timeRange" must have type "select"`;
124
+ }
125
+ } else if (filter.type !== "date-range") {
126
+ return `panel[${index}] ${label} uses {{${variable}}}, but filter "${expectedFilterId}" must have type "date-range"`;
127
+ }
128
+ if (typeof filter.default !== "string" || !filter.default.trim()) {
129
+ return `panel[${index}] ${label} uses a time filter without a non-empty default; choose a bounded default such as "90d" so missing filter state cannot become all-time`;
130
+ }
131
+ }
132
+
133
+ const configuredScope = panelConfig(panel).timeScope;
134
+ if (
135
+ configuredScope !== undefined &&
136
+ (typeof configuredScope !== "string" ||
137
+ !(DASHBOARD_TIME_SCOPES as readonly string[]).includes(configuredScope))
138
+ ) {
139
+ return `panel[${index}] ${label} config.timeScope must be one of ${DASHBOARD_TIME_SCOPES.join(", ")}`;
140
+ }
141
+
142
+ const scope = scopeValue(panel);
143
+ const hasDashboardBinding = variables.length > 0;
144
+ const hasLowerBound = hasExplicitLowerBound(sql);
145
+
146
+ if (scope === "dashboard" && !hasDashboardBinding) {
147
+ return `panel[${index}] ${label} declares timeScope "dashboard" but does not reference {{timeRange}} or a date-range variable`;
148
+ }
149
+ if (scope === "fixed-window" && hasDashboardBinding) {
150
+ return `panel[${index}] ${label} declares timeScope "fixed-window" but uses dashboard time variables; use timeScope "dashboard" or remove the placeholders`;
151
+ }
152
+ if (scope === "fixed-window" && !hasLowerBound) {
153
+ return `panel[${index}] ${label} declares timeScope "fixed-window" without a recognizable lower date bound`;
154
+ }
155
+ if (scope === "all-time" && hasDashboardBinding) {
156
+ return `panel[${index}] ${label} declares timeScope "all-time" but uses dashboard time variables; use timeScope "dashboard" for filter-bound output`;
157
+ }
158
+ if (
159
+ scope === "all-time" &&
160
+ !hasIntentionalHistoryDescription(panel, panelConfig(panel))
161
+ ) {
162
+ return `panel[${index}] ${label} declares timeScope "all-time"; add a description or title that says lifetime, historical, or all-time so the exception is explicit`;
163
+ }
164
+
165
+ if (!scope && !hasDashboardBinding && !hasLowerBound) {
166
+ return `panel[${index}] ${label} reads first-party analytics without a time bound; use {{timeRange}} with a non-empty default filter, or explicitly set config.timeScope to "cohort-history" or "all-time" for intentional history scans`;
167
+ }
168
+
169
+ return null;
170
+ }