@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
@@ -416,7 +416,7 @@ function writeCloudflareRoutingManifest(distDir: string, apps: string[]): void {
416
416
  const dispatch = apps
417
417
  .map(
418
418
  (a) =>
419
- ` if (pathname === "/${a}" || pathname.startsWith("/${a}/")) return ${moduleIdent(a)}.fetch(requestForMountedApp(request, "/${a}"), env, ctx);`,
419
+ ` if (pathname === "/${a}" || pathname === "/${a}.data" || pathname.startsWith("/${a}/")) return ${moduleIdent(a)}.fetch(requestForMountedApp(request, "/${a}"), env, ctx);`,
420
420
  )
421
421
  .join("\n");
422
422
  const dispatchRootFrameworkRoutes = apps.includes("dispatch")
@@ -880,7 +880,7 @@ function patchNetlifyFunctionEntry(
880
880
  const pathConfig =
881
881
  app === "dispatch"
882
882
  ? ["/_agent-native/*", "/.well-known/*", `${basePath}/*`]
883
- : [basePath, `${basePath}/*`];
883
+ : [basePath, `${basePath}.data`, `${basePath}/*`];
884
884
  const normalizeBasePathHelper =
885
885
  app === "dispatch"
886
886
  ? ""
@@ -599,7 +599,6 @@ export async function mergeCoreSharingActions(
599
599
  "change-appearance",
600
600
  () => import("../appearance/actions/change-appearance.js"),
601
601
  ],
602
- ["toggle-demo-mode", () => import("../demo/actions/toggle-demo-mode.js")],
603
602
  // Audit log — read surface (who changed what, when, agent vs human).
604
603
  [
605
604
  "list-audit-events",
@@ -6,9 +6,11 @@ import {
6
6
  type A2AToolResultSummary,
7
7
  } from "../../a2a/artifact-response.js";
8
8
  import { collectFinalResponseTextFromAgentEvents } from "../../a2a/response-text.js";
9
+ import { resolveMainChatMaxOutputTokens } from "../../agent/engine/output-tokens.js";
9
10
  import type { EngineTool } from "../../agent/engine/types.js";
10
11
  import {
11
12
  filterInitialEngineTools,
13
+ resolveAgentRequestReasoningEffort,
12
14
  type ActionEntry,
13
15
  } from "../../agent/production-agent.js";
14
16
  import { runAgentLoopDirectWithSoftTimeout } from "../../agent/run-loop-with-resume.js";
@@ -160,6 +162,17 @@ function runDelegatedAgentLoop(
160
162
  return runner(
161
163
  {
162
164
  ...runOptions,
165
+ // Delegated runs resolve their own model and do not pass through the
166
+ // interactive request handler's output-token setup. Use the same
167
+ // model-aware headroom here so reasoning models (notably GPT-5.x) do
168
+ // not spend the small internal default entirely on reasoning before
169
+ // emitting a tool call or answer. Preserve explicit test/caller values.
170
+ maxOutputTokens:
171
+ runOptions.maxOutputTokens ??
172
+ resolveMainChatMaxOutputTokens(runOptions.model),
173
+ reasoningEffort:
174
+ runOptions.reasoningEffort ??
175
+ resolveAgentRequestReasoningEffort({ model: runOptions.model }),
163
176
  finalResponseGuard: pluginOptions.finalResponseGuard,
164
177
  },
165
178
  pluginOptions.runSoftTimeoutMs,
@@ -18,6 +18,7 @@
18
18
  import { AsyncLocalStorage } from "node:async_hooks";
19
19
 
20
20
  import { applyAgentTextEventToBuffer } from "../a2a/response-text.js";
21
+ import { resolveMainChatMaxOutputTokens } from "../agent/engine/output-tokens.js";
21
22
  import type { AgentEngine, EngineMessage } from "../agent/engine/types.js";
22
23
  import type {
23
24
  ActionEntry,
@@ -26,6 +27,7 @@ import type {
26
27
  import {
27
28
  actionsToEngineTools,
28
29
  filterInitialEngineTools,
30
+ resolveAgentRequestReasoningEffort,
29
31
  } from "../agent/production-agent.js";
30
32
  import {
31
33
  runAgentLoop,
@@ -589,6 +591,11 @@ function createTaskMessageFinalGuard(
589
591
  retryMessage: formatQueuedTaskMessages(queuedMessages),
590
592
  fallbackMessage:
591
593
  "I received an orchestrator update while finishing, but could not continue from it. Please check the task status and send the update again if needed.",
594
+ // A queued update can introduce a request for an action that was
595
+ // deferred behind tool-search. The retry is already a corrective turn,
596
+ // so expose the full authorized registry rather than making the
597
+ // sub-agent spend that turn rediscovering the same tool.
598
+ expandToolSurface: true,
592
599
  };
593
600
  };
594
601
  }
@@ -1835,6 +1842,16 @@ export async function processAgentTeamRun(
1835
1842
  chunkUsage = await runAgentLoop({
1836
1843
  engine: config.engine,
1837
1844
  model: config.model,
1845
+ // Agent-team runs are delegated turns too. Keep their
1846
+ // first attempt on the same model-aware budget as A2A /
1847
+ // MCP so reasoning models do not spend the old 4K
1848
+ // default before emitting a tool call or answer.
1849
+ maxOutputTokens: resolveMainChatMaxOutputTokens(
1850
+ config.model,
1851
+ ),
1852
+ reasoningEffort: resolveAgentRequestReasoningEffort({
1853
+ model: config.model,
1854
+ }),
1838
1855
  systemPrompt,
1839
1856
  tools,
1840
1857
  availableTools,
@@ -1205,36 +1205,6 @@ export function createCoreRoutesPlugin(
1205
1205
  }),
1206
1206
  );
1207
1207
 
1208
- // Demo-mode status — read by the client fetch interceptor and the
1209
- // Demo mode settings toggle. `forced` reflects the DEMO_MODE env (a
1210
- // hosted demo deployment); `enabled` ORs that with the per-user
1211
- // application_state toggle. No request-context dependency: the env case
1212
- // needs nothing, and the per-user case resolves the session straight
1213
- // off the request cookie via getSession(event).
1214
- getH3App(nitroApp).use(
1215
- `${P}/demo/status`,
1216
- defineEventHandler(async (event) => {
1217
- const { isDemoModeForced } = await import("../demo/config.js");
1218
- const forced = isDemoModeForced();
1219
- if (forced) return { enabled: true, forced: true };
1220
- try {
1221
- const session = await getSession(event);
1222
- const email = session?.email;
1223
- if (!email) return { enabled: false, forced: false };
1224
- const { appStateGet } =
1225
- await import("../application-state/store.js");
1226
- const state = await appStateGet(email, "demo-mode");
1227
- return {
1228
- enabled:
1229
- (state as { enabled?: boolean } | null)?.enabled === true,
1230
- forced: false,
1231
- };
1232
- } catch {
1233
- return { enabled: false, forced: false };
1234
- }
1235
- }),
1236
- );
1237
-
1238
1208
  // Polling
1239
1209
  getH3App(nitroApp).use(`${P}/poll`, createPollHandler());
1240
1210
 
@@ -17,7 +17,7 @@ import {
17
17
  type H3Event,
18
18
  } from "h3";
19
19
 
20
- import { getConfiguredAppBasePath } from "./app-base-path.js";
20
+ import { getAppBasePathFromViteEnv } from "./app-base-path.js";
21
21
  import { getAppName } from "./app-name.js";
22
22
  import { signupAttributionFromCookieHeader } from "./attribution.js";
23
23
  import {
@@ -281,7 +281,11 @@ export function getOrigin(event: H3Event): string {
281
281
 
282
282
  /** App mount prefix, if the template is served under APP_BASE_PATH. */
283
283
  export function getAppBasePath(): string {
284
- return getConfiguredAppBasePath();
284
+ // Vite statically replaces VITE_* values in the server bundle during the
285
+ // build, but Netlify/Nitro does not necessarily expose those build vars at
286
+ // runtime. Keep auth and OAuth path matching aligned with the SSR handler by
287
+ // falling back to import.meta.env (including BASE_URL).
288
+ return getAppBasePathFromViteEnv();
285
289
  }
286
290
 
287
291
  /** Build an absolute same-origin URL that preserves APP_BASE_PATH. */
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: native-navigation
3
+ description: >-
4
+ Use when adding or reviewing UI navigation so internal routes preserve
5
+ normal SPA behavior plus Cmd/Ctrl-click and middle-click new-tab semantics.
6
+ scope: dev
7
+ ---
8
+
9
+ # Native Navigation
10
+
11
+ ## Rule
12
+
13
+ Pure navigation must render a real React Router `<Link>` for same-SPA routes,
14
+ a native `<a href>` for external/public or explicit new-tab destinations, or a
15
+ shadcn `Button asChild` wrapper around one. Do not use a button whose only job
16
+ is to call `navigate()`, `window.open()`, or `window.location`.
17
+
18
+ ## Why
19
+
20
+ Browser modifier-click, middle-click, keyboard activation, accessibility, and
21
+ normal client-side routing come from real links. Imperative JavaScript
22
+ navigation hides the destination and blocks those browser semantics.
23
+
24
+ ## How
25
+
26
+ ```tsx
27
+ import { Link } from "react-router";
28
+
29
+ <Link to={`/items/${item.id}`}>...</Link>
30
+
31
+ <Button asChild>
32
+ <Link to="/settings">Settings</Link>
33
+ </Button>
34
+
35
+ <a href={previewUrl} target="_blank" rel="noopener noreferrer">
36
+ Preview
37
+ </a>
38
+ ```
39
+
40
+ - Use `Link` for same-SPA internal routes.
41
+ - Use `<a>` when the URL is external, crosses an app/public boundary, is an
42
+ auth or sign-in destination, or intentionally opens in a new tab.
43
+ - Preserve query strings and hashes in `to` or `href`.
44
+ - For cards with nested actions, link only the non-interactive card region or
45
+ use a transparent Link overlay; never nest links or buttons.
46
+ - If selection mode changes click behavior, prevent default only for an
47
+ unmodified primary click and preserve modified clicks.
48
+
49
+ Keep imperative navigation for post-mutation redirects, OAuth and popup flows,
50
+ downloads, media/blob URLs, playback, keyboard shortcuts, and stateful tabs.
51
+
52
+ ## Review
53
+
54
+ Search for `onClick={() => navigate`, `window.open`, `window.location`,
55
+ `onAuxClick`, and buttons whose only purpose is routing. For each candidate,
56
+ confirm whether it is pure navigation or a stateful workflow before changing
57
+ it.
58
+
59
+ Verify the rendered DOM exposes an `href`, then test normal click, Cmd/Ctrl-click,
60
+ middle-click, keyboard Enter, and nested controls where applicable. Run oxfmt
61
+ and focused app tests or typechecks for the touched package.
62
+
63
+ ## Related Skills
64
+
65
+ - `client-side-routing`
66
+ - `frontend-design`
67
+ - `shadcn-ui`
@@ -58,6 +58,23 @@ once — do not switch to db-patch or raw SQL.
58
58
 
59
59
  Do not use `app-db` as a dashboard source. For first-party events collected through `/track`, use `source: "first-party"` or the `query-agent-native-analytics` action rather than raw internal `db-query`.
60
60
 
61
+ AI-generated first-party panels are dashboard-time-bound by default. Set
62
+ `config.timeScope` to `"dashboard"` and include the matching dashboard time
63
+ filter in the SQL. The allowed values are:
64
+
65
+ - `dashboard`: use the dashboard-selected time range; the default for ordinary metrics.
66
+ - `fixed-window`: use an explicit bounded window independent of the dashboard filter.
67
+ - `cohort-history`: use the bounded history of an explicitly defined cohort.
68
+ - `all-time`: scan all available history; use only when the user requests it and
69
+ put `all-time`, `lifetime`, or `historical` in the title or description.
70
+
71
+ `{{timeRange}}` requires an explicit matching `filters` entry with
72
+ `id: "timeRange"` and `type: "select"`. `{{<id>Start}}` and `{{<id>End}}`
73
+ require a matching `filters` entry with that id and `type: "date-range"`.
74
+ Do not rely on undeclared time variables. Server validation rejects unbound
75
+ first-party SQL, so declare the filter or choose an explicit non-dashboard
76
+ scope before saving.
77
+
61
78
  ## Creating A Dashboard
62
79
 
63
80
  When the user asks for a dashboard:
@@ -154,7 +171,8 @@ Notes:
154
171
  "source": "first-party",
155
172
  "chartType": "metric",
156
173
  "width": 1,
157
- "sql": "SELECT COUNT(*) AS value FROM analytics_events WHERE event_name = 'click'",
174
+ "config": { "timeScope": "dashboard" },
175
+ "sql": "SELECT COUNT(*) AS value FROM analytics_events WHERE event_name = 'click' AND event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'",
158
176
  },
159
177
  {
160
178
  "id": "kpi-signups",
@@ -162,7 +180,8 @@ Notes:
162
180
  "source": "first-party",
163
181
  "chartType": "metric",
164
182
  "width": 1,
165
- "sql": "SELECT COUNT(*) AS value FROM analytics_events WHERE event_name = 'signup'",
183
+ "config": { "timeScope": "dashboard" },
184
+ "sql": "SELECT COUNT(*) AS value FROM analytics_events WHERE event_name = 'signup' AND event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'",
166
185
  },
167
186
  {
168
187
  "id": "kpi-active",
@@ -170,7 +189,8 @@ Notes:
170
189
  "source": "first-party",
171
190
  "chartType": "metric",
172
191
  "width": 1,
173
- "sql": "SELECT COUNT(DISTINCT user_id) AS value FROM analytics_events",
192
+ "config": { "timeScope": "dashboard" },
193
+ "sql": "SELECT COUNT(DISTINCT user_id) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'",
174
194
  },
175
195
  // Section header switches the grid to 2 columns for the panels below it.
176
196
  {
@@ -186,7 +206,8 @@ Notes:
186
206
  "source": "first-party",
187
207
  "chartType": "line",
188
208
  "width": 2,
189
- "sql": "SELECT DATE(timestamp) AS date, COUNT(*) AS value FROM analytics_events WHERE timestamp >= '{{dateStart}}' AND timestamp < '{{dateEnd}}' GROUP BY 1 ORDER BY 1",
209
+ "config": { "timeScope": "dashboard" },
210
+ "sql": "SELECT event_date AS date, COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}' GROUP BY 1 ORDER BY 1",
190
211
  },
191
212
  ],
192
213
  }
@@ -196,6 +217,13 @@ Notes:
196
217
 
197
218
  `filters[]` defines dashboard-wide controls. Filter values are available in panel SQL through `{{var}}` interpolation. Date ranges emit `{{<id>Start}}` and `{{<id>End}}`.
198
219
 
220
+ For dashboard-time-bound first-party SQL, use `config.timeScope: "dashboard"`
221
+ and a predicate that consumes the declared filter, such as
222
+ `event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'`. A
223
+ `{{timeRange}}` token must have a matching select filter and SQL branches for
224
+ its options; date variables must have a matching date-range filter. The server
225
+ rejects unbound first-party SQL during dashboard validation.
226
+
199
227
  **Filter ids must be unique.** Two filters with the same `id` collide on the same URL param, so changing one visibly updates the other in the UI. The dashboard save endpoint rejects duplicates with a 400.
200
228
 
201
229
  **Use `type: "date-range"` for paired start/end dates.** Don't add two `type: "date"` filters labeled "Start" and "End" — even with distinct ids, that ships the wrong UX. A single date-range filter renders as the "from … to …" pair and exposes both halves to SQL via `{{<id>Start}}` / `{{<id>End}}`.
@@ -244,6 +272,17 @@ type DashboardPatch = {
244
272
  variables?: Record<string, string>;
245
273
  };
246
274
 
275
+ type PanelTimeScope =
276
+ | "dashboard"
277
+ | "fixed-window"
278
+ | "cohort-history"
279
+ | "all-time";
280
+
281
+ type PanelConfig = Record<string, unknown> & {
282
+ // Use "dashboard" for AI-generated first-party panels by default.
283
+ timeScope?: PanelTimeScope;
284
+ };
285
+
247
286
  type PanelPatch = {
248
287
  title?: string;
249
288
  sql?: string;
@@ -268,7 +307,7 @@ type PanelPatch = {
268
307
  width?: number;
269
308
  columns?: number;
270
309
  tab?: string;
271
- config?: Record<string, unknown>;
310
+ config?: PanelConfig;
272
311
  description?: string;
273
312
  };
274
313
 
@@ -346,7 +385,9 @@ dashboard
346
385
  source: "first-party",
347
386
  chartType: "metric",
348
387
  width: 1,
349
- sql: "SELECT COUNT(*) AS value FROM analytics_events",
388
+ config: { timeScope: "dashboard" },
389
+ // Assumes filters includes { id: "date", type: "date-range", default: "30d" }.
390
+ sql: "SELECT COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'",
350
391
  })
351
392
  .atTop();
352
393
  ```
@@ -441,7 +482,7 @@ For a **first-party analytics** dashboard, prefer `compose-dashboard` over hand-
441
482
  - **Never hand-author large first-party configs panel-by-panel.** Call `compose-dashboard` with the metric keys instead.
442
483
  - Unknown metric keys are skipped and reported in `unknownMetrics` (not fatal). Each panel's SQL is validated independently — valid panels save, invalid ones are reported in `invalidMetrics`.
443
484
  - By default (no `overwrite`), composing into an existing dashboard APPENDS the new panels and skips ids already present. `overwrite: true` replaces the whole config.
444
- - Each metric accepts an optional per-metric `window` of `'30d' | '90d' | 'all'` (only affects windowed virality/time metrics) and `title` / `chartType` / `width` overrides.
485
+ - Each metric accepts an optional per-metric `window` of `'30d' | '90d' | 'all'` (only affects windowed virality/time metrics) and `title` / `chartType` / `width` overrides. Request `'all'` only when the user asks for all-time coverage, and describe it as full available history.
445
486
  - Returns `{ dashboardId, panelCount, createdMetrics, unknownMetrics, invalidMetrics, skippedExistingIds }` — report `panelCount` as proof-of-done.
446
487
 
447
488
  Available metric keys: `total-signups`, `signups-over-time`, `signups-by-template`, `sessions-by-app`, `sessions-over-time`, `replay-sessions`, `replay-chunks-over-time`, `recent-replay-sessions`, `signed-in-vs-anon`, `total-template-clicks`, `total-demo-clicks`, `total-cli-copies`, `template-interest-over-time`, `clicks-by-template`, `demo-clicks-by-template`, `cli-copies-by-template`, `cli-copies-over-time`, `pageviews-over-time`, `top-referrer-domains`, `referred-signups-30d`, `viral-signup-share-30d`, `clip-share-signups-30d`, `signups-by-referral-source`, `referred-signups-over-time`, `top-referrers`, `share-funnel-30d`, `viral-participation-rate-90d`, `viral-coefficient-90d`, `activated-referrers-90d`.
@@ -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`.",
@@ -0,0 +1,17 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <svg
3
+ version="1.1"
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ viewBox="-0.8 -0.8 22.8 30.4"
6
+ shape-rendering="geometricPrecision"
7
+ >
8
+ <path
9
+ d="M 2.5 1.8 C 2.2 1.5 1.8 1.7 1.8 2.1 V 21.9 C 1.8 22.5 2.5 22.8 2.9 22.4 L 7.4 17.5 L 11.5 26.2 C 11.7 26.7 12.3 26.9 12.8 26.7 L 15.7 25.3 C 16.2 25.1 16.4 24.5 16.2 24 L 12.4 15.4 H 18.4 C 19.1 15.4 19.4 14.5 18.9 14 Z"
10
+ fill="#000000"
11
+ stroke="#FFFFFF"
12
+ stroke-width="4.35"
13
+ stroke-linecap="round"
14
+ stroke-linejoin="round"
15
+ paint-order="stroke fill"
16
+ />
17
+ </svg>
@@ -24,6 +24,7 @@ import {
24
24
  type ReactNode,
25
25
  } from "react";
26
26
  import { createPortal } from "react-dom";
27
+ import { Link } from "react-router";
27
28
  import {
28
29
  Area,
29
30
  AreaChart,
@@ -1579,12 +1580,12 @@ function TableRenderer({
1579
1580
  {replayHref ? (
1580
1581
  <span className="inline-flex flex-col gap-0.5">
1581
1582
  <span>{content}</span>
1582
- <a
1583
- href={replayHref}
1583
+ <Link
1584
+ to={replayHref}
1584
1585
  className="text-xs font-medium text-primary hover:underline"
1585
1586
  >
1586
1587
  {t("sessions.watchReplay")}
1587
- </a>
1588
+ </Link>
1588
1589
  </span>
1589
1590
  ) : (
1590
1591
  content
@@ -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