@agent-native/core 0.120.0 → 0.120.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 (207) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/docs/content/extensions.mdx +23 -4
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/agent/production-agent.ts +4 -0
  6. package/corpus/core/src/agent/run-manager.ts +36 -10
  7. package/corpus/core/src/agent/types.ts +2 -0
  8. package/corpus/core/src/client/MultiTabAssistantChat.tsx +45 -0
  9. package/corpus/core/src/client/extensions/ExtensionsListPage.tsx +16 -3
  10. package/corpus/core/src/client/settings/ExtensionsSettingsContent.tsx +24 -0
  11. package/corpus/core/src/client/settings/SettingsPanel.tsx +10 -0
  12. package/corpus/core/src/client/use-chat-models.ts +45 -0
  13. package/corpus/core/src/deploy/build.ts +3 -0
  14. package/corpus/core/src/server/analytics.ts +164 -20
  15. package/corpus/core/src/server/auth.ts +17 -13
  16. package/corpus/core/src/server/better-auth-instance.ts +13 -1
  17. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  18. package/corpus/core/src/templates/chat/app/routes/extensions._index.tsx +2 -2
  19. package/corpus/core/src/templates/chat/netlify.toml +1 -0
  20. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  21. package/corpus/core/src/vite/client.ts +6 -0
  22. package/corpus/templates/analytics/.agents/skills/adhoc-analysis/SKILL.md +27 -43
  23. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -8
  24. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +2 -2
  25. package/corpus/templates/analytics/AGENTS.md +20 -8
  26. package/corpus/templates/analytics/actions/get-explorer-dashboard.ts +3 -0
  27. package/corpus/templates/analytics/actions/list-dashboard-usage-stats.ts +140 -46
  28. package/corpus/templates/analytics/actions/migrate-analytics-artifacts.ts +48 -0
  29. package/corpus/templates/analytics/actions/save-analysis.ts +2 -3
  30. package/corpus/templates/analytics/actions/view-screen.ts +1 -1
  31. package/corpus/templates/analytics/app/components/dashboard/DashboardMetadata.tsx +77 -0
  32. package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +7 -1
  33. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +1 -0
  34. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +103 -502
  35. package/corpus/templates/analytics/app/global.css +30 -0
  36. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +2 -2
  37. package/corpus/templates/analytics/app/i18n-data.ts +80 -0
  38. package/corpus/templates/analytics/app/pages/Agents.tsx +49 -2
  39. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +54 -11
  40. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +3 -53
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +23 -25
  42. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +2 -2
  43. package/corpus/templates/analytics/app/routes/analyses._index.tsx +3 -2
  44. package/corpus/templates/analytics/app/routes/extensions._index.tsx +2 -2
  45. package/corpus/templates/analytics/changelog/2026-07-23-analytics-now-groups-saved-analyses-and-embedded-extensions-.md +6 -0
  46. package/corpus/templates/analytics/changelog/2026-07-23-dashboard-menus-show-who-created-and-last-updated-each-dashb.md +6 -0
  47. package/corpus/templates/analytics/changelog/2026-07-23-dashboard-usage-now-loads-reliably-for-organizations-with-ma.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-07-23-long-dashboard-building-requests-now-continue-without-stoppi.md +6 -0
  49. package/corpus/templates/analytics/netlify.toml +4 -0
  50. package/corpus/templates/analytics/server/db/schema.ts +4 -0
  51. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +0 -1
  52. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +1 -0
  53. package/corpus/templates/analytics/server/lib/migrate-analytics-artifacts.ts +1062 -0
  54. package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -52
  55. package/corpus/templates/analytics/server/plugins/core-routes.ts +1 -1
  56. package/corpus/templates/analytics/server/plugins/db.ts +10 -0
  57. package/corpus/templates/assets/actions/generate-image-batch.ts +36 -1
  58. package/corpus/templates/assets/actions/generate-image.ts +28 -1
  59. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +47 -9
  60. package/corpus/templates/assets/app/routes/extensions._index.tsx +2 -2
  61. package/corpus/templates/assets/changelog/2026-07-23-assets-image-generation-now-recovers-after-a-completed-tool-.md +6 -0
  62. package/corpus/templates/assets/changelog/2026-07-23-new-chat-aligns-with-sidebar-controls.md +6 -0
  63. package/corpus/templates/assets/netlify.toml +1 -0
  64. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +38 -8
  65. package/corpus/templates/brain/app/routes/extensions._index.tsx +2 -2
  66. package/corpus/templates/brain/netlify.toml +1 -0
  67. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +43 -5
  68. package/corpus/templates/calendar/app/routes/_app.extensions._index.tsx +2 -16
  69. package/corpus/templates/calendar/netlify.toml +1 -0
  70. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  71. package/corpus/templates/chat/app/routes/extensions._index.tsx +2 -2
  72. package/corpus/templates/chat/netlify.toml +1 -0
  73. package/corpus/templates/clips/app/components/library/library-layout.tsx +52 -6
  74. package/corpus/templates/clips/app/routes/_app.extensions._index.tsx +2 -16
  75. package/corpus/templates/clips/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  76. package/corpus/templates/clips/changelog/2026-07-23-meeting-recording-meters-and-dismissed-prompts.md +6 -0
  77. package/corpus/templates/clips/desktop/src/hooks/useMicMeter.ts +1 -1
  78. package/corpus/templates/clips/desktop/src/lib/meeting-notification-dismissal.ts +2 -0
  79. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +1 -3
  80. package/corpus/templates/clips/desktop/src/overlays/live-audio-bars.tsx +86 -0
  81. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +24 -0
  82. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +5 -69
  83. package/corpus/templates/clips/desktop/src/styles.css +35 -3
  84. package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +13 -44
  85. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +119 -17
  86. package/corpus/templates/clips/netlify.toml +1 -0
  87. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +3 -7
  88. package/corpus/templates/content/app/routes/_app.extensions._index.tsx +2 -2
  89. package/corpus/templates/content/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  90. package/corpus/templates/content/netlify.toml +1 -0
  91. package/corpus/templates/design/app/components/layout/Sidebar.tsx +55 -17
  92. package/corpus/templates/design/app/routes/extensions._index.tsx +2 -2
  93. package/corpus/templates/design/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  94. package/corpus/templates/design/netlify.toml +1 -0
  95. package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -1
  96. package/corpus/templates/dispatch/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  97. package/corpus/templates/dispatch/netlify.toml +1 -0
  98. package/corpus/templates/forms/.agents/skills/form-publishing/SKILL.md +9 -3
  99. package/corpus/templates/forms/AGENTS.md +11 -1
  100. package/corpus/templates/forms/README.md +24 -1
  101. package/corpus/templates/forms/actions/update-form.ts +12 -5
  102. package/corpus/templates/forms/app/components/builder/FieldPropertiesPanel.tsx +199 -1
  103. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +41 -45
  104. package/corpus/templates/forms/app/i18n/en-US.ts +13 -1
  105. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +35 -4
  106. package/corpus/templates/forms/app/pages/FormFillPage.tsx +7 -16
  107. package/corpus/templates/forms/app/routes/_app.extensions._index.tsx +2 -2
  108. package/corpus/templates/forms/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  109. package/corpus/templates/forms/changelog/2026-07-23-forms-can-branch-into-follow-up-questions-and-route-response.md +6 -0
  110. package/corpus/templates/forms/netlify.toml +1 -0
  111. package/corpus/templates/forms/server/handlers/submissions.ts +22 -31
  112. package/corpus/templates/forms/server/lib/integrations.ts +11 -2
  113. package/corpus/templates/forms/server/lib/public-form-ssr.ts +26 -5
  114. package/corpus/templates/forms/server/lib/validate-fields.ts +48 -11
  115. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -0
  116. package/corpus/templates/forms/shared/conditional.ts +67 -0
  117. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +36 -5
  118. package/corpus/templates/macros/app/routes/extensions._index.tsx +2 -2
  119. package/corpus/templates/macros/netlify.toml +1 -0
  120. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +0 -9
  121. package/corpus/templates/mail/app/routes/extensions._index.tsx +2 -2
  122. package/corpus/templates/mail/netlify.toml +1 -0
  123. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +38 -5
  124. package/corpus/templates/plan/app/routes/extensions._index.tsx +2 -2
  125. package/corpus/templates/plan/netlify.toml +1 -0
  126. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +47 -4
  127. package/corpus/templates/slides/app/routes/extensions._index.tsx +2 -2
  128. package/corpus/templates/slides/netlify.toml +1 -0
  129. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +37 -6
  130. package/corpus/templates/tasks/app/routes/extensions._index.tsx +2 -2
  131. package/corpus/templates/tasks/app/routes/settings.tsx +31 -0
  132. package/corpus/templates/tasks/netlify.toml +1 -0
  133. package/dist/agent/production-agent.d.ts.map +1 -1
  134. package/dist/agent/production-agent.js +4 -0
  135. package/dist/agent/production-agent.js.map +1 -1
  136. package/dist/agent/run-manager.d.ts +9 -0
  137. package/dist/agent/run-manager.d.ts.map +1 -1
  138. package/dist/agent/run-manager.js +25 -10
  139. package/dist/agent/run-manager.js.map +1 -1
  140. package/dist/agent/types.d.ts +2 -2
  141. package/dist/agent/types.d.ts.map +1 -1
  142. package/dist/agent/types.js.map +1 -1
  143. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  144. package/dist/client/MultiTabAssistantChat.js +35 -0
  145. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  146. package/dist/client/extensions/ExtensionsListPage.d.ts +5 -1
  147. package/dist/client/extensions/ExtensionsListPage.d.ts.map +1 -1
  148. package/dist/client/extensions/ExtensionsListPage.js +5 -3
  149. package/dist/client/extensions/ExtensionsListPage.js.map +1 -1
  150. package/dist/client/settings/ExtensionsSettingsContent.d.ts +2 -0
  151. package/dist/client/settings/ExtensionsSettingsContent.d.ts.map +1 -0
  152. package/dist/client/settings/ExtensionsSettingsContent.js +10 -0
  153. package/dist/client/settings/ExtensionsSettingsContent.js.map +1 -0
  154. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  155. package/dist/client/settings/SettingsPanel.js +10 -1
  156. package/dist/client/settings/SettingsPanel.js.map +1 -1
  157. package/dist/client/use-chat-models.d.ts +1 -0
  158. package/dist/client/use-chat-models.d.ts.map +1 -1
  159. package/dist/client/use-chat-models.js +33 -0
  160. package/dist/client/use-chat-models.js.map +1 -1
  161. package/dist/collab/awareness.d.ts +2 -2
  162. package/dist/collab/awareness.d.ts.map +1 -1
  163. package/dist/deploy/build.js +1 -0
  164. package/dist/deploy/build.js.map +1 -1
  165. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  166. package/dist/observability/routes.d.ts +3 -3
  167. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  168. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  169. package/dist/server/analytics.d.ts +17 -4
  170. package/dist/server/analytics.d.ts.map +1 -1
  171. package/dist/server/analytics.js +122 -17
  172. package/dist/server/analytics.js.map +1 -1
  173. package/dist/server/auth.d.ts.map +1 -1
  174. package/dist/server/auth.js +5 -4
  175. package/dist/server/auth.js.map +1 -1
  176. package/dist/server/better-auth-instance.d.ts +8 -0
  177. package/dist/server/better-auth-instance.d.ts.map +1 -1
  178. package/dist/server/better-auth-instance.js +10 -1
  179. package/dist/server/better-auth-instance.js.map +1 -1
  180. package/dist/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  181. package/dist/templates/chat/app/routes/extensions._index.tsx +2 -2
  182. package/dist/templates/chat/netlify.toml +1 -0
  183. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  184. package/dist/vite/client.d.ts.map +1 -1
  185. package/dist/vite/client.js +2 -0
  186. package/dist/vite/client.js.map +1 -1
  187. package/docs/content/extensions.mdx +23 -4
  188. package/package.json +1 -1
  189. package/src/agent/production-agent.ts +4 -0
  190. package/src/agent/run-manager.ts +36 -10
  191. package/src/agent/types.ts +2 -0
  192. package/src/client/MultiTabAssistantChat.tsx +45 -0
  193. package/src/client/extensions/ExtensionsListPage.tsx +16 -3
  194. package/src/client/settings/ExtensionsSettingsContent.tsx +24 -0
  195. package/src/client/settings/SettingsPanel.tsx +10 -0
  196. package/src/client/use-chat-models.ts +45 -0
  197. package/src/deploy/build.ts +3 -0
  198. package/src/server/analytics.ts +164 -20
  199. package/src/server/auth.ts +17 -13
  200. package/src/server/better-auth-instance.ts +13 -1
  201. package/src/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  202. package/src/templates/chat/app/routes/extensions._index.tsx +2 -2
  203. package/src/templates/chat/netlify.toml +1 -0
  204. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  205. package/src/vite/client.ts +6 -0
  206. package/corpus/templates/analytics/app/components/layout/NewAnalysisDialog.tsx +0 -165
  207. package/corpus/templates/analytics/app/pages/analyses/AnalysesList.tsx +0 -175
@@ -1,8 +1,9 @@
1
1
  # Analytics — Agent Guide
2
2
 
3
3
  Analytics is an agent-native BI workspace. The agent manages data sources,
4
- queries, dashboards, charts, analyses, and connected warehouse integrations
5
- through actions and SQL-backed state.
4
+ queries, dashboards, charts, and connected warehouse integrations through
5
+ actions and SQL-backed state. Dashboards are the canonical user-facing
6
+ analytics artifact; legacy analyses remain readable only for compatibility.
6
7
 
7
8
  Keep this file essential. Querying, dashboard, warehouse, and implementation
8
9
  details live in `.agents/skills/`.
@@ -175,13 +176,22 @@ membership id when its native update status reports `update-available`.
175
176
  `/agents?view=dashboards` shows the
176
177
  admin-only dashboard usage audit; call `list-dashboard-usage-stats` when
177
178
  admins ask about dashboard created/modified dates, owners, last tracked
178
- modifier, views, engagements, saved views, or cleanup candidates. The
179
+ modifier, views, edits, engagements, saved views, or cleanup candidates. The
180
+ dashboard overflow menu shows created/updated timestamps and their tracked
181
+ actors for both SQL and Explorer dashboards. The
179
182
  Advanced menu opens `/agents?view=database`, where organization owners/admins
180
183
  can connect other agent-native app databases and use the shared database admin
181
184
  tool for table browsing, row editing, and SQL inspection. This database
182
185
  surface is for connected target app databases, not broad access to all
183
186
  Analytics data. Keep future admin additions inside this route instead of
184
187
  adding many top-level sidebar tabs.
188
+ - The `migrate-analytics-artifacts` action is the organization-scoped
189
+ consolidation path. Run it first with `dryRun: true`, then use the exact
190
+ `confirm: "MIGRATE_ANALYTICS_ARTIFACTS"` token for an owner/admin-approved
191
+ write. It materializes legacy settings, turns saved analyses and standalone
192
+ extensions into dashboard blocks, archives exact duplicates, copies shares,
193
+ and keeps source rows recoverable. It intentionally covers organization-
194
+ scoped rows, not private member-only rows.
185
195
  - For dashboard edits, default to `mutate-dashboard` with its typed
186
196
  `dashboard.*` script API. It supports id-based panel moves, title/SQL/config
187
197
  edits, inserts, duplication, removal, and dashboard field patches in one
@@ -212,11 +222,13 @@ membership id when its native update status reports `update-available`.
212
222
  - `get-sql-dashboard` is compact by default for agents. Use its `panels`
213
223
  summaries and `layout.panelOrder` / `layout.firstPanelIds` for orientation and
214
224
  proof. Pass `includeConfig: true` only when full panel SQL/config is needed.
215
- - Native dashboards and saved analyses are constrained artifacts. If a requested
216
- dashboard, analysis surface, visualization, interaction model, custom layout,
217
- or bespoke workflow cannot be done faithfully with the built-in dashboard JSON
218
- config/components or saved-analysis markdown/chart format, automatically build
219
- it as an extension instead and tell the user why.
225
+ - Native dashboards are constrained artifacts. If a requested dashboard,
226
+ analysis/report surface, visualization, interaction model, custom layout, or
227
+ bespoke workflow cannot be done faithfully with the built-in dashboard JSON
228
+ config/components, automatically create an extension and embed it as one or
229
+ more `chartType: "extension"` panels using `config.extensionId`. Never leave
230
+ the extension standalone or direct the user to an Extensions page from
231
+ Analytics.
220
232
  - For an existing extension-backed dashboard or migrated surface such as Risk
221
233
  Meeting, separate data repair from visual redesign. Inspect the dashboard and
222
234
  extension first, then call `update-extension` with exactly `id`,
@@ -33,6 +33,9 @@ export default defineAction({
33
33
  ownerEmail: dash.ownerEmail,
34
34
  orgId: dash.orgId,
35
35
  visibility: dash.visibility,
36
+ createdAt: dash.createdAt,
37
+ updatedAt: dash.updatedAt,
38
+ updatedBy: dash.updatedBy,
36
39
  role: dash.role,
37
40
  canEdit: dash.canEdit,
38
41
  canManage: dash.canManage,
@@ -1,6 +1,6 @@
1
1
  import { defineAction } from "@agent-native/core/action";
2
2
  import { buildDeepLink } from "@agent-native/core/server";
3
- import { and, eq, like, or, sql } from "drizzle-orm";
3
+ import { and, eq, isNull, like, or, sql } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
@@ -21,6 +21,7 @@ type DashboardUsageStats = {
21
21
  viewCount: number;
22
22
  engagementCount: number;
23
23
  eventEngagementCount: number;
24
+ editCount: number;
24
25
  savedViewCount: number;
25
26
  uniqueUserCount: number;
26
27
  lastViewedAt: string | null;
@@ -96,6 +97,8 @@ export function dashboardIdFromEventLocation(
96
97
  if (!raw) return null;
97
98
  try {
98
99
  const parsed = new URL(raw, "https://analytics.local");
100
+ const parsedDashboardId = dashboardIdFromPath(parsed.pathname);
101
+ if (parsedDashboardId) return parsedDashboardId;
99
102
  if (
100
103
  /(?:^|\/)(?:dashboards|adhoc)\/explorer-dashboard$/.test(parsed.pathname)
101
104
  ) {
@@ -124,7 +127,7 @@ function dashboardUrl(row: { id: string; kind: string }): string {
124
127
 
125
128
  export default defineAction({
126
129
  description:
127
- "List admin-only dashboard usage and cleanup stats for every dashboard in the active organization. Includes lifecycle metadata, owner, last tracked modifier, pageviews, engagement events, unique viewers, saved view counts, archive/hidden state, and dashboard links. Requires active organization owner/admin role.",
130
+ "List admin-only dashboard usage and cleanup stats for every dashboard in the active organization. Includes lifecycle metadata, owner, last tracked modifier, pageviews, edit counts, engagement events, unique viewers, saved view counts, archive/hidden state, and dashboard links. Requires active organization owner/admin role.",
128
131
  schema: z.object({}),
129
132
  http: { method: "GET" },
130
133
  readOnly: true,
@@ -144,24 +147,120 @@ export default defineAction({
144
147
  // guard:allow-unscoped — org owner/admin audit intentionally spans all
145
148
  // dashboard rows in the active org after requireDbAdminContextFromRequest.
146
149
  const dashboardRows = await db
147
- .select()
150
+ .select({
151
+ id: schema.dashboards.id,
152
+ kind: schema.dashboards.kind,
153
+ title: schema.dashboards.title,
154
+ config: schema.dashboards.config,
155
+ ownerEmail: schema.dashboards.ownerEmail,
156
+ visibility: schema.dashboards.visibility,
157
+ createdAt: schema.dashboards.createdAt,
158
+ updatedAt: schema.dashboards.updatedAt,
159
+ updatedBy: schema.dashboards.updatedBy,
160
+ archivedAt: schema.dashboards.archivedAt,
161
+ hiddenAt: schema.dashboards.hiddenAt,
162
+ hiddenBy: schema.dashboards.hiddenBy,
163
+ })
148
164
  .from(schema.dashboards)
149
165
  .where(eq(schema.dashboards.orgId, admin.orgId));
150
166
 
167
+ if (dashboardRows.length === 0) return [];
168
+
151
169
  const dashboardIds = new Set<string>(
152
170
  dashboardRows.map((row: { id: string }) => row.id),
153
171
  );
154
172
 
155
- const savedViewRows = await db
156
- .select({
157
- dashboardId: schema.dashboardViews.dashboardId,
158
- count: sql<number>`count(*)`,
159
- lastSavedViewAt: sql<
160
- string | null
161
- >`max(${schema.dashboardViews.createdAt})`,
162
- })
163
- .from(schema.dashboardViews)
164
- .groupBy(schema.dashboardViews.dashboardId);
173
+ const analyticsUserIdentity = sql<
174
+ string | null
175
+ >`coalesce(nullif(${schema.analyticsEvents.userKey}, ''), nullif(${schema.analyticsEvents.userId}, ''), nullif(${schema.analyticsEvents.anonymousId}, ''), nullif(${schema.analyticsEvents.sessionId}, ''))`;
176
+ const indexedDashboardEventLocation = or(
177
+ like(schema.analyticsEvents.path, "/dashboards/%"),
178
+ like(schema.analyticsEvents.path, "/adhoc/%"),
179
+ );
180
+ const legacyAbsoluteUrlDashboardEventLocation = and(
181
+ or(
182
+ isNull(schema.analyticsEvents.path),
183
+ eq(schema.analyticsEvents.path, ""),
184
+ ),
185
+ or(
186
+ like(schema.analyticsEvents.url, "%/dashboards/%"),
187
+ like(schema.analyticsEvents.url, "%/adhoc/%"),
188
+ ),
189
+ );
190
+ const dashboardEventLocation = or(
191
+ indexedDashboardEventLocation,
192
+ legacyAbsoluteUrlDashboardEventLocation,
193
+ );
194
+
195
+ const [savedViewRows, revisionRows, eventRows, eventUserRows] =
196
+ await Promise.all([
197
+ db
198
+ .select({
199
+ dashboardId: schema.dashboardViews.dashboardId,
200
+ count: sql<number>`count(*)`,
201
+ lastSavedViewAt: sql<
202
+ string | null
203
+ >`max(${schema.dashboardViews.createdAt})`,
204
+ })
205
+ .from(schema.dashboardViews)
206
+ .innerJoin(
207
+ schema.dashboards,
208
+ and(
209
+ eq(schema.dashboardViews.dashboardId, schema.dashboards.id),
210
+ eq(schema.dashboards.orgId, admin.orgId),
211
+ ),
212
+ )
213
+ .groupBy(schema.dashboardViews.dashboardId),
214
+ db
215
+ .select({
216
+ dashboardId: schema.dashboardRevisions.dashboardId,
217
+ count: sql<number>`count(*)`,
218
+ })
219
+ .from(schema.dashboardRevisions)
220
+ .where(eq(schema.dashboardRevisions.orgId, admin.orgId))
221
+ .groupBy(schema.dashboardRevisions.dashboardId),
222
+ db
223
+ .select({
224
+ path: schema.analyticsEvents.path,
225
+ url: schema.analyticsEvents.url,
226
+ eventName: schema.analyticsEvents.eventName,
227
+ count: sql<number>`count(*)`,
228
+ lastSeenAt: sql<
229
+ string | null
230
+ >`max(${schema.analyticsEvents.receivedAt})`,
231
+ })
232
+ .from(schema.analyticsEvents)
233
+ .where(
234
+ and(
235
+ eq(schema.analyticsEvents.orgId, admin.orgId),
236
+ dashboardEventLocation,
237
+ ),
238
+ )
239
+ .groupBy(
240
+ schema.analyticsEvents.path,
241
+ schema.analyticsEvents.url,
242
+ schema.analyticsEvents.eventName,
243
+ ),
244
+ db
245
+ .select({
246
+ path: schema.analyticsEvents.path,
247
+ url: schema.analyticsEvents.url,
248
+ userIdentity: analyticsUserIdentity,
249
+ })
250
+ .from(schema.analyticsEvents)
251
+ .where(
252
+ and(
253
+ eq(schema.analyticsEvents.orgId, admin.orgId),
254
+ dashboardEventLocation,
255
+ sql`${analyticsUserIdentity} IS NOT NULL`,
256
+ ),
257
+ )
258
+ .groupBy(
259
+ schema.analyticsEvents.path,
260
+ schema.analyticsEvents.url,
261
+ analyticsUserIdentity,
262
+ ),
263
+ ]);
165
264
 
166
265
  const savedViewsByDashboard = new Map<
167
266
  string,
@@ -179,38 +278,15 @@ export default defineAction({
179
278
  });
180
279
  }
181
280
 
182
- const analyticsUserIdentity = sql<
183
- string | null
184
- >`coalesce(nullif(${schema.analyticsEvents.userKey}, ''), nullif(${schema.analyticsEvents.userId}, ''), nullif(${schema.analyticsEvents.anonymousId}, ''), nullif(${schema.analyticsEvents.sessionId}, ''))`;
185
- const eventRows = await db
186
- .select({
187
- path: schema.analyticsEvents.path,
188
- url: schema.analyticsEvents.url,
189
- eventName: schema.analyticsEvents.eventName,
190
- count: sql<number>`count(*)`,
191
- userIdentity: analyticsUserIdentity,
192
- lastSeenAt: sql<
193
- string | null
194
- >`max(${schema.analyticsEvents.receivedAt})`,
195
- })
196
- .from(schema.analyticsEvents)
197
- .where(
198
- and(
199
- eq(schema.analyticsEvents.orgId, admin.orgId),
200
- or(
201
- like(schema.analyticsEvents.path, "%/dashboards/%"),
202
- like(schema.analyticsEvents.path, "%/adhoc/%"),
203
- like(schema.analyticsEvents.url, "%/dashboards/%"),
204
- like(schema.analyticsEvents.url, "%/adhoc/%"),
205
- ),
206
- ),
207
- )
208
- .groupBy(
209
- schema.analyticsEvents.path,
210
- schema.analyticsEvents.url,
211
- schema.analyticsEvents.eventName,
212
- analyticsUserIdentity,
213
- );
281
+ const editsByDashboard = new Map<string, number>();
282
+ for (const row of revisionRows as Array<{
283
+ dashboardId: string;
284
+ count: unknown;
285
+ }>) {
286
+ if (dashboardIds.has(row.dashboardId)) {
287
+ editsByDashboard.set(row.dashboardId, toNumber(row.count));
288
+ }
289
+ }
214
290
 
215
291
  const eventsByDashboard = new Map<
216
292
  string,
@@ -226,7 +302,6 @@ export default defineAction({
226
302
  url: string | null;
227
303
  eventName: string;
228
304
  count: unknown;
229
- userIdentity: string | null;
230
305
  lastSeenAt: string | null;
231
306
  }>) {
232
307
  const dashboardId = dashboardIdFromEventLocation(row.path, row.url);
@@ -248,6 +323,22 @@ export default defineAction({
248
323
  } else {
249
324
  current.eventEngagementCount += count;
250
325
  }
326
+ eventsByDashboard.set(dashboardId, current);
327
+ }
328
+
329
+ for (const row of eventUserRows as Array<{
330
+ path: string | null;
331
+ url: string | null;
332
+ userIdentity: string | null;
333
+ }>) {
334
+ const dashboardId = dashboardIdFromEventLocation(row.path, row.url);
335
+ if (!dashboardId || !dashboardIds.has(dashboardId)) continue;
336
+ const current = eventsByDashboard.get(dashboardId) ?? {
337
+ viewCount: 0,
338
+ eventEngagementCount: 0,
339
+ uniqueUsers: new Set<string>(),
340
+ lastViewedAt: null,
341
+ };
251
342
  if (row.userIdentity) current.uniqueUsers.add(row.userIdentity);
252
343
  eventsByDashboard.set(dashboardId, current);
253
344
  }
@@ -281,6 +372,7 @@ export default defineAction({
281
372
  hiddenBy: row.hiddenBy ?? null,
282
373
  viewCount: events.viewCount,
283
374
  eventEngagementCount: events.eventEngagementCount,
375
+ editCount: editsByDashboard.get(row.id) ?? 0,
284
376
  savedViewCount: savedViews.count,
285
377
  engagementCount: events.eventEngagementCount + savedViews.count,
286
378
  uniqueUserCount: events.uniqueUsers.size,
@@ -293,6 +385,8 @@ export default defineAction({
293
385
  .sort((a, b) => {
294
386
  const views = b.viewCount - a.viewCount;
295
387
  if (views !== 0) return views;
388
+ const edits = b.editCount - a.editCount;
389
+ if (edits !== 0) return edits;
296
390
  return b.updatedAt.localeCompare(a.updatedAt);
297
391
  });
298
392
  },
@@ -0,0 +1,48 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server";
6
+ import { z } from "zod";
7
+
8
+ import { requireAnalyticsAdminContext } from "../server/lib/db-admin-connections";
9
+ import {
10
+ migrateAnalyticsArtifacts,
11
+ type AnalyticsArtifactMigrationContext,
12
+ } from "../server/lib/migrate-analytics-artifacts.js";
13
+
14
+ const confirmation = "MIGRATE_ANALYTICS_ARTIFACTS" as const;
15
+
16
+ export default defineAction({
17
+ description:
18
+ "Inventory or consolidate all organization-scoped Analytics dashboards, saved analyses, and extensions into the canonical Dashboards list. The default is a read-only dry run. A write requires an organization owner/admin and confirm=MIGRATE_ANALYTICS_ARTIFACTS; exact duplicates are archived/hidden, source rows remain recoverable, shares are copied, and legacy settings keys are removed only after SQL materialization.",
19
+ schema: z.object({
20
+ dryRun: z
21
+ .boolean()
22
+ .optional()
23
+ .default(true)
24
+ .describe("Inspect the migration without writing when true (default)."),
25
+ confirm: z
26
+ .literal(confirmation)
27
+ .optional()
28
+ .describe(
29
+ "Required for writes: MIGRATE_ANALYTICS_ARTIFACTS. Do not send this for a dry run.",
30
+ ),
31
+ }),
32
+ needsApproval: ({ dryRun }) => !dryRun,
33
+ run: async ({ dryRun, confirm }, ctx) => {
34
+ const userEmail = getRequestUserEmail() || ctx?.userEmail;
35
+ const orgId = getRequestOrgId() || ctx?.orgId || null;
36
+ const admin = await requireAnalyticsAdminContext({ userEmail, orgId });
37
+ if (!dryRun && confirm !== confirmation) {
38
+ throw new Error(
39
+ `Refusing the Analytics organization migration without confirm=${confirmation}. Run a dry run first, then repeat with the exact confirmation token.`,
40
+ );
41
+ }
42
+ const migrationContext: AnalyticsArtifactMigrationContext = {
43
+ userEmail: admin.userEmail,
44
+ orgId: admin.orgId,
45
+ };
46
+ return migrateAnalyticsArtifacts(migrationContext, { dryRun });
47
+ },
48
+ });
@@ -113,9 +113,8 @@ function assertCompactAnalysisPayload(args: {
113
113
 
114
114
  export default defineAction({
115
115
  description:
116
- "Save an ad-hoc analysis as a reusable artifact. Do not call this for ordinary in-chat analysis or deep-dive answers unless the user explicitly asks to save/create a reusable analysis, or this turn is re-running an existing saved analysis. Stores the analysis question, instructions for re-running, data sources used, and compact results. " +
117
- "This creates a reusable analysis that anyone can re-run later to get updated results. " +
118
- "Saved analyses appear in the Analyses sidebar, so do not use this as scratch storage, as a transient summary, or as a duplicate companion artifact when creating a dashboard or extension unless the user explicitly asked for a saved analysis too. " +
116
+ "Legacy compatibility only: use this to re-run or update an existing saved analysis when the user explicitly names its legacy analysis id or asks to preserve that legacy record. Do not call this to create a new saved analysis, report, visualization, or bespoke workflow; new requests become dashboards, using update-dashboard and embedding any bespoke extension as one or more dashboard panels. Stores the legacy analysis question, instructions for re-running, data sources used, and compact results. " +
117
+ "Do not use this as scratch storage, a transient summary, or a duplicate companion artifact for a dashboard. " +
119
118
  "Call this only after you've gathered real evidence and include non-empty, compact resultData with structured evidence from those data-source action results. For qualitative analyses, resultData may include call/message IDs, short transcript excerpts, coded themes, mention counts, and sentiment labels derived from actual source records. Never include full Gong transcripts, full tool outputs, or bulk raw provider payloads.",
120
119
  schema: z.object({
121
120
  id: z
@@ -438,7 +438,7 @@ export default defineAction({
438
438
  includes: [
439
439
  "dashboard created and modified dates",
440
440
  "last tracked modifier",
441
- "view and engagement counts",
441
+ "view, edit, and engagement counts",
442
442
  "saved view counts",
443
443
  "hidden and archived state",
444
444
  ],
@@ -0,0 +1,77 @@
1
+ import { useFormatters, useT } from "@agent-native/core/client/i18n";
2
+ import { IconCalendar, IconClock, IconUser } from "@tabler/icons-react";
3
+
4
+ interface DashboardMetadataProps {
5
+ createdAt: string | null;
6
+ createdBy: string | null;
7
+ updatedAt: string | null;
8
+ updatedBy: string | null;
9
+ }
10
+
11
+ function MetadataRow({
12
+ icon: Icon,
13
+ label,
14
+ value,
15
+ }: {
16
+ icon: typeof IconCalendar;
17
+ label: string;
18
+ value: string;
19
+ }) {
20
+ return (
21
+ <div className="flex items-start gap-1.5">
22
+ <Icon className="mt-0.5 h-3 w-3 shrink-0" />
23
+ <span className="shrink-0">{label}</span>
24
+ <span className="min-w-0 truncate text-foreground/80">{value}</span>
25
+ </div>
26
+ );
27
+ }
28
+
29
+ export function DashboardMetadata({
30
+ createdAt,
31
+ createdBy,
32
+ updatedAt,
33
+ updatedBy,
34
+ }: DashboardMetadataProps) {
35
+ const t = useT();
36
+ const { formatDate } = useFormatters();
37
+
38
+ function formatMetadataDate(value: string | null): string {
39
+ if (!value) return t("agents.notTracked");
40
+ try {
41
+ return formatDate(value, {
42
+ year: "numeric",
43
+ month: "short",
44
+ day: "numeric",
45
+ hour: "numeric",
46
+ minute: "2-digit",
47
+ });
48
+ } catch {
49
+ return value;
50
+ }
51
+ }
52
+
53
+ return (
54
+ <div className="flex flex-col gap-1.5 text-xs text-muted-foreground">
55
+ <MetadataRow
56
+ icon={IconCalendar}
57
+ label={t("agents.dashboardMetadataCreated")}
58
+ value={formatMetadataDate(createdAt)}
59
+ />
60
+ <MetadataRow
61
+ icon={IconUser}
62
+ label={t("agents.dashboardMetadataCreatedBy")}
63
+ value={createdBy || t("agents.notTracked")}
64
+ />
65
+ <MetadataRow
66
+ icon={IconClock}
67
+ label={t("agents.dashboardMetadataUpdated")}
68
+ value={formatMetadataDate(updatedAt)}
69
+ />
70
+ <MetadataRow
71
+ icon={IconUser}
72
+ label={t("agents.dashboardMetadataUpdatedBy")}
73
+ value={updatedBy || t("agents.notTracked")}
74
+ />
75
+ </div>
76
+ );
77
+ }
@@ -8,6 +8,12 @@ export function DashboardPanelSkeleton({
8
8
  ...props
9
9
  }: ComponentProps<typeof Skeleton>) {
10
10
  return (
11
- <Skeleton {...props} className={cn("bg-muted-foreground/20", className)} />
11
+ <Skeleton
12
+ {...props}
13
+ className={cn(
14
+ "dashboard-panel-skeleton bg-muted-foreground/25",
15
+ className,
16
+ )}
17
+ />
12
18
  );
13
19
  }
@@ -12,6 +12,7 @@ import {
12
12
 
13
13
  const DASHBOARD_CONTEXT =
14
14
  "The user wants to create a new analytics dashboard. " +
15
+ 'DASHBOARD ARTIFACT RULE — Dashboards are the only user-facing Analytics artifact. For deep dives, saved reports, custom visualizations, or bespoke workflows, always create or update a dashboard. If bespoke UI is needed, create the extension and immediately add it as one or more `chartType: "extension"` panels in that dashboard; never leave the extension standalone or direct the user to an Extensions page. ' +
15
16
  "TEMPLATE FIRST — If the user names an existing dashboard as a template to clone/base this on, resolve its id first (use `list-sql-dashboards` if you only have a title), then call `get-sql-dashboard` with `includeConfig: true` immediately and inspect `panels[].chartType`. " +
16
17
  'If any panel is `chartType: "extension"`, this is an extension-backed dashboard: call `get-extension` for that panel\'s `config.extensionId`, clone/adapt it with `create-extension` (apply the requested customer/org filters), then save a new dashboard via `update-dashboard` that embeds the new extension panel (`chartType: "extension"`, `config.extensionId`). Do not rebuild an extension template as guessed SQL/BigQuery panels. ' +
17
18
  "LARGE EXTENSION CLONE — Extension bodies can be very large (tens of thousands of characters). Call `get-extension` with `forceContent: true` exactly ONCE and reuse that body; a second same-run read intentionally omits `content` (you'll see `contentOmitted`), so don't treat that as the content being gone. Call `create-extension` / `update-extension` as NATIVE tools — they are mutating actions and cannot be invoked from `run-code`/`appAction`. For customer-specific clones, change only the small static config block (e.g. `ACCOUNT_USAGE_STATIC`) and prefer a focused `update-extension` edit over regenerating the whole HTML. Never shovel the full body through `run-code` or chat; if you stage it in a workspace scratch file, read it back with `workspaceRead` (which returns the whole file). " +