@agent-native/core-corpus 0.210.2 → 0.211.0

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 (192) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +5 -3
  3. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +41 -2
  4. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +4 -2
  5. package/corpus/templates/analytics/CHANGELOG.md +18 -0
  6. package/corpus/templates/analytics/actions/build-data-index.ts +11 -2
  7. package/corpus/templates/analytics/actions/configure-content-collection.ts +29 -4
  8. package/corpus/templates/analytics/actions/configure-first-party-events-model.ts +9 -7
  9. package/corpus/templates/analytics/actions/create-analytics-public-key.ts +4 -0
  10. package/corpus/templates/analytics/actions/estimate-content-signups-cost.ts +72 -0
  11. package/corpus/templates/analytics/actions/estimate-query-cost.ts +16 -134
  12. package/corpus/templates/analytics/actions/list-sigma-workbooks.ts +123 -42
  13. package/corpus/templates/analytics/actions/migrate-sigma-workbook.ts +20 -17
  14. package/corpus/templates/analytics/actions/revoke-analytics-public-key.ts +4 -0
  15. package/corpus/templates/analytics/app/components/dashboard/SigmaMigrationDialog.tsx +246 -680
  16. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
  17. package/corpus/templates/analytics/app/i18n/en-US.ts +54 -55
  18. package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +0 -1
  19. package/corpus/templates/analytics/app/pages/DataSources.tsx +56 -0
  20. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +4 -3
  21. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +26 -8
  22. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +3 -1
  23. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/panel-cost-notice.ts +172 -0
  24. package/corpus/templates/analytics/app/pages/data-sources/SearchConsoleDetail.tsx +441 -0
  25. package/corpus/templates/analytics/changelog/2026-10-15-a-content-dashboard-lists-every-blog-article-with-its-author.md +6 -0
  26. package/corpus/templates/analytics/changelog/2026-10-15-data-sources-has-a-google-search-console-card-that-shows-the.md +6 -0
  27. package/corpus/templates/analytics/changelog/2026-10-16-the-content-dashboard-can-show-signups-per-blog-article.md +6 -0
  28. package/corpus/templates/analytics/changelog/2026-10-19-creating-or-revoking-a-public-analytics-key-now-asks-you-to-.md +6 -0
  29. package/corpus/templates/analytics/changelog/2026-10-19-search-sigma-workbooks-by-name-and-open-ones-a-teammate-alre.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-10-20-blog-signup-attribution-excludes-signups-before-the-first-vi.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-10-20-collapsed-analytics-navigation-starts-at-the-top.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-10-20-content-keeps-successful-coverage-dates-and-missing-day-gaps.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-10-20-private-github-repositories-can-be-indexed-with-your-connect.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-10-20-search-console-preserves-backfill-and-paused-collection.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-10-20-search-console-refreshes-historical-coverage-when-properties.md +6 -0
  36. package/corpus/templates/analytics/changelog/2026-10-20-search-console-setup-keeps-saved-properties-readable-and-sup.md +6 -0
  37. package/corpus/templates/analytics/seeds/dashboards/content-view.json +182 -0
  38. package/corpus/templates/analytics/server/db/schema.ts +1 -0
  39. package/corpus/templates/analytics/server/lib/bi-migration/external-dashboards-store.ts +56 -18
  40. package/corpus/templates/analytics/server/lib/bi-migration/unfinished-import.ts +64 -0
  41. package/corpus/templates/analytics/server/lib/builtin-dashboard-pass.ts +17 -2
  42. package/corpus/templates/analytics/server/lib/content-metrics/collect.ts +2 -0
  43. package/corpus/templates/analytics/server/lib/content-metrics/config.ts +92 -3
  44. package/corpus/templates/analytics/server/lib/content-metrics/legacy-content-view.json +172 -0
  45. package/corpus/templates/analytics/server/lib/content-metrics/migration.ts +2 -0
  46. package/corpus/templates/analytics/server/lib/content-metrics/steps/daily-metrics.ts +8 -3
  47. package/corpus/templates/analytics/server/lib/content-metrics/steps/search-console.ts +16 -3
  48. package/corpus/templates/analytics/server/lib/content-metrics/steps/signups.ts +240 -0
  49. package/corpus/templates/analytics/server/lib/content-metrics/store.ts +5 -0
  50. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +21 -0
  51. package/corpus/templates/analytics/server/lib/dashboard-panel-verification.ts +300 -5
  52. package/corpus/templates/analytics/server/lib/dashboard-seeds.ts +2 -0
  53. package/corpus/templates/analytics/server/lib/dashboards-store.ts +28 -0
  54. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +128 -24
  55. package/corpus/templates/analytics/server/lib/google-analytics.ts +56 -16
  56. package/corpus/templates/analytics/server/lib/index-build-github.ts +5 -0
  57. package/corpus/templates/analytics/server/lib/provider-credentials.ts +47 -2
  58. package/corpus/templates/analytics/server/lib/query-cost-estimate.ts +184 -5
  59. package/corpus/templates/analytics/server/plugins/db.ts +9 -1
  60. package/corpus/templates/analytics/shared/builtin-dashboard-ids.ts +2 -0
  61. package/corpus/templates/analytics/shared/search-console-property.ts +78 -0
  62. package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +5 -3
  63. package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +5 -3
  64. package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +5 -3
  65. package/corpus/templates/calendar/actions/delete-event.ts +1 -3
  66. package/corpus/templates/calendar/actions/delete-events.ts +1 -2
  67. package/corpus/templates/calendar/actions/event-action-helpers.ts +0 -4
  68. package/corpus/templates/calendar/actions/update-event.ts +2 -6
  69. package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
  70. package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +5 -3
  71. package/corpus/templates/clips/CHANGELOG.md +11 -24
  72. package/corpus/templates/clips/actions/create-recording-upload.ts +5 -1
  73. package/corpus/templates/clips/app/components/player/chapter-list.tsx +7 -6
  74. package/corpus/templates/clips/app/components/player/comment-composer.tsx +5 -1
  75. package/corpus/templates/clips/app/components/player/player-controls.tsx +1 -0
  76. package/corpus/templates/clips/app/components/player/scrubber.tsx +4 -2
  77. package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
  78. package/corpus/templates/clips/app/lib/extension-auth.ts +70 -0
  79. package/corpus/templates/clips/app/root.tsx +22 -10
  80. package/corpus/templates/clips/app/routes/_app.notifications.tsx +3 -0
  81. package/corpus/templates/clips/app/routes/import.tsx +115 -110
  82. package/corpus/templates/clips/changelog/2026-10-16-agent-uploads-can-use-free-clips-storage.md +6 -0
  83. package/corpus/templates/clips/changelog/2026-10-19-agents-see-a-clip-s-frames-as-soon-as-it-finishes-processing.md +6 -0
  84. package/corpus/templates/clips/changelog/2026-10-19-chapter-list-times-now-match-the-player-s-clock-and-each-cha.md +6 -0
  85. package/corpus/templates/clips/changelog/2026-10-19-editing-a-comment-puts-the-cursor-after-its-text-and-reply-o.md +6 -0
  86. package/corpus/templates/clips/changelog/2026-10-19-youtube-import-followups.md +6 -0
  87. package/corpus/templates/clips/changelog/2026-10-20-locally-saved-desktop-recordings-now-appear-in-the-recovery-.md +6 -0
  88. package/corpus/templates/clips/desktop/src/app.tsx +29 -186
  89. package/corpus/templates/clips/desktop/src/components/RecordingRecovery.tsx +171 -16
  90. package/corpus/templates/clips/desktop/src/lib/record-first.ts +9 -32
  91. package/corpus/templates/clips/desktop/src/lib/recording-recovery.ts +30 -1
  92. package/corpus/templates/clips/server/lib/agent-frame-request.ts +5 -2
  93. package/corpus/templates/clips/server/lib/recording-upload-ticket.ts +8 -0
  94. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +2 -0
  95. package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +5 -3
  96. package/corpus/templates/content/CHANGELOG.md +12 -0
  97. package/corpus/templates/content/actions/_content-database-personal-view.ts +5 -15
  98. package/corpus/templates/content/actions/_database-navigation.ts +3 -3
  99. package/corpus/templates/content/actions/_database-utils.ts +10 -22
  100. package/corpus/templates/content/actions/_database-view-setup.ts +16 -76
  101. package/corpus/templates/content/actions/create-comment-ai-suggestion.ts +93 -44
  102. package/corpus/templates/content/actions/export-document.ts +2 -14
  103. package/corpus/templates/content/actions/suggest-document-edit.ts +5 -7
  104. package/corpus/templates/content/actions/update-content-database-view.ts +2 -14
  105. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +86 -30
  106. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +64 -28
  107. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -46
  108. package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +72 -7
  109. package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +292 -103
  110. package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +147 -40
  111. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +108 -11
  112. package/corpus/templates/content/app/components/editor/database/ContentTable.tsx +2 -1
  113. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +150 -166
  114. package/corpus/templates/content/app/components/editor/database/database-cell-comments.tsx +515 -0
  115. package/corpus/templates/content/app/components/editor/database/database-item-comments.tsx +5 -43
  116. package/corpus/templates/content/app/components/editor/database/filter-sort.ts +8 -783
  117. package/corpus/templates/content/app/components/editor/live-editor-session.ts +5 -1
  118. package/corpus/templates/content/app/components/editor/page-draft-journal.ts +61 -1
  119. package/corpus/templates/content/app/components/editor/property-comments.tsx +35 -4
  120. package/corpus/templates/content/app/components/editor/property-date-format.ts +45 -0
  121. package/corpus/templates/content/app/components/editor/thread-suggestions.ts +30 -0
  122. package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +23 -4
  123. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +36 -10
  124. package/corpus/templates/content/app/global.css +23 -0
  125. package/corpus/templates/content/app/i18n-data.ts +1 -0
  126. package/corpus/templates/content/changelog/2026-10-16-opening-a-page-in-a-new-tab-while-another-tab-is-still-savin.md +6 -0
  127. package/corpus/templates/content/changelog/2026-10-16-typing-steadily-in-one-tab-no-longer-makes-another-open-tab-.md +6 -0
  128. package/corpus/templates/content/changelog/2026-10-19-database-views-can-filter-the-same-property-more-than-once-n.md +6 -0
  129. package/corpus/templates/content/changelog/2026-10-19-edits-made-from-a-block-s-menu-or-a-table-s-row-and-column-m.md +6 -0
  130. package/corpus/templates/content/changelog/2026-10-19-starting-a-comment-puts-the-cursor-in-the-comment-box-ready-.md +6 -0
  131. package/corpus/templates/content/changelog/2026-10-19-table-row-and-column-options-now-appear-for-the-row-or-colum.md +6 -0
  132. package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +5 -1
  133. package/corpus/templates/content/docs/product/capabilities/content.revision.suggestions.md +3 -1
  134. package/corpus/templates/content/e2e/helpers.ts +10 -0
  135. package/corpus/templates/content/shared/api.ts +18 -12
  136. package/corpus/templates/content/shared/database-query.ts +107 -13
  137. package/corpus/templates/content/shared/suggestion-diff.ts +35 -0
  138. package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +5 -3
  139. package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +5 -3
  140. package/corpus/templates/design/CHANGELOG.md +2 -0
  141. package/corpus/templates/design/actions/delete-file.ts +1 -4
  142. package/corpus/templates/design/app/components/design/LayersPanel.tsx +10 -1
  143. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +24 -4
  144. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +14 -9
  145. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +23 -2
  146. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +29 -0
  147. package/corpus/templates/design/changelog/2026-10-19-reply-and-edit-on-a-comment-in-the-sidebar-or-on-a-canvas-pi.md +6 -0
  148. package/corpus/templates/design/changelog/2026-10-20-comments-return-to-the-mode-they-came-from-and-layers-search.md +6 -0
  149. package/corpus/templates/design/e2e/global-setup.ts +185 -85
  150. package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +5 -3
  151. package/corpus/templates/dispatch/CHANGELOG.md +4 -0
  152. package/corpus/templates/dispatch/actions/apply-workspace-connection-setup.ts +4 -0
  153. package/corpus/templates/dispatch/actions/delete-workspace-connection.ts +4 -0
  154. package/corpus/templates/dispatch/actions/set-workspace-connection-grant.ts +4 -0
  155. package/corpus/templates/dispatch/actions/upsert-workspace-connection.ts +4 -0
  156. package/corpus/templates/dispatch/changelog/2026-10-19-vault-secrets-grants-link-tokens-app-access-and-workspace-co.md +6 -0
  157. package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +5 -3
  158. package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
  159. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
  160. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
  161. package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +5 -3
  162. package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +5 -3
  163. package/corpus/templates/mail/.agents/skills/contacts-and-crm/SKILL.md +22 -17
  164. package/corpus/templates/mail/AGENTS.md +1 -1
  165. package/corpus/templates/mail/CHANGELOG.md +14 -0
  166. package/corpus/templates/mail/actions/provider-api-request.ts +3 -3
  167. package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +211 -54
  168. package/corpus/templates/mail/app/hooks/use-inbox-threads.ts +26 -2
  169. package/corpus/templates/mail/app/i18n/en-US.ts +5 -1
  170. package/corpus/templates/mail/app/lib/gong-connection.ts +27 -0
  171. package/corpus/templates/mail/app/lib/thread-cache.ts +110 -9
  172. package/corpus/templates/mail/app/root.tsx +10 -2
  173. package/corpus/templates/mail/changelog/2026-10-20-connected-gong-settings-now-open-the-shared-integration-mana.md +6 -0
  174. package/corpus/templates/mail/changelog/2026-10-20-gong-connection-details-now-match-the-call-data-available-in.md +6 -0
  175. package/corpus/templates/mail/changelog/2026-10-20-improve-gong-setup-with-a-click-to-connect-flow-and-permissi.md +6 -0
  176. package/corpus/templates/mail/changelog/2026-10-20-mail-lets-you-retry-when-it-cannot-check-your-gong-access.md +6 -0
  177. package/corpus/templates/mail/changelog/2026-10-20-mail-thread-cache-stays-isolated-when-mailbox-scope-changes.md +6 -0
  178. package/corpus/templates/mail/changelog/2026-10-20-the-inbox-and-recently-opened-threads-now-open-instantly-fro.md +6 -0
  179. package/corpus/templates/mail/server/handlers/emails.ts +9 -0
  180. package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +5 -3
  181. package/corpus/templates/plan/CHANGELOG.md +4 -0
  182. package/corpus/templates/plan/app/pages/PlansPage.tsx +173 -33
  183. package/corpus/templates/plan/changelog/2026-10-19-opening-a-comment-thread-puts-the-cursor-in-the-reply-box-an.md +6 -0
  184. package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +5 -3
  185. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -42
  186. package/corpus/templates/slides/app/components/editor/FloatingToolbar.tsx +3 -7
  187. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +5 -14
  188. package/corpus/templates/slides/app/i18n/en-US.ts +0 -2
  189. package/corpus/templates/slides/app/pages/DeckEditor.tsx +27 -33
  190. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +1 -3
  191. package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +5 -3
  192. package/package.json +1 -1
@@ -2450,7 +2450,7 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
2450
2450
  collapsed={true}
2451
2451
  />
2452
2452
  <nav className="flex min-h-0 flex-1 flex-col items-center overflow-y-auto px-2 py-3">
2453
- <div className="my-auto flex flex-col items-center gap-2">
2453
+ <div className="flex flex-col items-center gap-3">
2454
2454
  {collapsedNavItems.map((item) => {
2455
2455
  const Icon = item.icon;
2456
2456
  return (
@@ -321,65 +321,19 @@ export default {
321
321
  sigmaRetryConnection: "Check the connection and try again.",
322
322
  sigmaRetry: "Retry",
323
323
  sigmaNoWorkbooks: "No Sigma workbooks are available.",
324
- sigmaWorkbook: "Workbook",
325
- sigmaChooseWorkbook: "Choose a workbook",
326
- sigmaStatusNotImported: "Not imported",
327
- sigmaStatusImporting: "Importing",
328
- sigmaStatusImported: "Imported",
329
- sigmaStatusFailed: "Import failed",
324
+ sigmaSearchWorkbooks: "Search workbooks…",
325
+ sigmaNoMatchingWorkbooks: "No matching workbooks",
326
+ sigmaNoMatchingWorkbooksLoadMore:
327
+ "No matching workbooks loaded yet. Load more workbooks to search the rest.",
328
+ sigmaOpenMigratedDashboard: "Open the imported dashboard in a new tab",
329
+ sigmaOwnedBy: "Owned by",
330
+ sigmaMigratingTooltip: "Being migrated by {{name}}",
331
+ sigmaMigrate: "Migrate",
332
+ sigmaMigrateComingSoon: "Coming soon",
330
333
  sigmaArchived: "Archived",
331
- sigmaPreviewTitle: "Workbook contents",
332
- sigmaPreviewFailed: "Couldn't preview this workbook",
333
- sigmaRetryPreview: "Try again to load the workbook preview.",
334
- sigmaImportableCount: "{{count}} importable",
335
- sigmaUnsupportedCount: "{{count}} unsupported",
336
- sigmaImportable: "Importable",
337
- sigmaUnsupported: "Unsupported",
338
- sigmaNoElements: "No workbook elements were found.",
339
- sigmaPreviewOnly:
340
- "Preview only for now: Sigma does not expose a BigQuery project mapping Analytics can verify, so importing is disabled until that mapping is available.",
341
- sigmaPreviewTruncated: "Showing {{shown}} of {{total}} elements.",
342
- sigmaImportWorkbook: "Import workbook",
343
- sigmaImporting: "Importing workbook…",
344
- sigmaImportInProgress:
345
- "An import for this workbook is already in progress.",
346
- sigmaMigrationFailed: "Couldn't import this workbook. Try again.",
347
- sigmaMigrationResultInvalid:
348
- "The import response was incomplete. Reload the workbook list to check its status.",
349
- sigmaImportComplete: "Dashboard created",
350
- sigmaImportSummary:
351
- "{{imported}} elements imported; {{unsupported}} unsupported elements retained as review callouts.",
352
- sigmaAlreadyImported: "This workbook has already been imported.",
353
- sigmaImportedNoDashboard:
354
- "The imported dashboard isn't available to open from this account.",
355
- sigmaReviewRequiredCount: "{{count}} need review",
356
- sigmaBetaRepresentationWarning:
357
- "This beta import may represent some visualizations differently from Sigma.",
358
- sigmaReasonUnsupportedElement:
359
- "This element type is not supported for import.",
360
- sigmaReasonUnsupportedSource:
361
- "This element uses a source that is not supported for import.",
362
- sigmaReasonSourceMappingRequired:
363
- "Sigma doesn't expose a BigQuery project mapping Analytics can verify, so this SQL can't be imported yet.",
364
- sigmaReasonSourceUnavailable:
365
- "Sigma didn't allow access to this source connection. Check its permissions.",
366
- sigmaReasonQueryUnavailable:
367
- "Sigma did not return an importable query for this element.",
368
- sigmaReasonUnsafeSql: "This query pattern cannot be imported safely.",
369
- sigmaReasonVisualizationReview:
370
- "The data can be imported, but verify its visualization after import.",
371
- sigmaRetryOwnerOnly:
372
- "Only the person who started this import can retry it.",
373
- sigmaImportedByTeammate: "Imported by a teammate",
374
- sigmaFlaggedCount: "{{count}} flagged for review",
375
334
  sigmaMoreWorkbooksFailed: "Could not load more workbooks",
376
335
  sigmaLoadingMore: "Loading workbooks…",
377
336
  sigmaLoadMore: "Load more workbooks",
378
- sigmaOpenDashboard: "Open dashboard",
379
- sigmaReviewWithAgent: "Review with agent",
380
- sigmaReviewContext: "Imported Sigma dashboard",
381
- sigmaReviewPrompt:
382
- "Review the imported Sigma dashboard {{name}}. Check the imported panels and unsupported elements, then suggest any corrections.",
383
337
  },
384
338
  sidebar: {
385
339
  collapseSidebar: "Collapse sidebar",
@@ -938,6 +892,13 @@ export default {
938
892
  viewOnly: "You have view-only access to this dashboard.",
939
893
  saveFailed: "Couldn't save dashboard",
940
894
  saveFailedWithMessage: "Couldn't save dashboard: {{message}}",
895
+ panelCostTitle: 'BigQuery cost of "{{title}}"',
896
+ panelCostOverTitle: '"{{title}}" is expensive to refresh',
897
+ panelCostScheduled:
898
+ "~{{size}} scanned · {{perRun}} per refresh · ~{{perMonth}}/month in email reports",
899
+ panelCostUnscheduled:
900
+ "~{{size}} scanned · {{perRun}} per refresh · no email reports",
901
+ panelCostOverHint: "A narrower date range or fewer columns scans less.",
941
902
  archived: 'Archived "{{name}}"',
942
903
  unhid: 'Unhid "{{name}}"',
943
904
  archiveFailed: "Couldn't archive dashboard",
@@ -1314,6 +1275,44 @@ export default {
1314
1275
  "Couldn't check for existing entries with this metric.",
1315
1276
  },
1316
1277
  dataSources: {
1278
+ searchConsole: {
1279
+ name: "Google Search Console",
1280
+ description:
1281
+ "Daily clicks and impressions per page for the Content view.",
1282
+ serviceAccount: "Service account",
1283
+ copyEmail: "Copy email",
1284
+ grantHint:
1285
+ "Add this email as a user on each property in Search Console, under Settings → Users and permissions.",
1286
+ noServiceAccount: "Add a Google service account key first.",
1287
+ openGoogleCloud: "Open Google Cloud",
1288
+ invalidServiceAccount: "The saved service account key can't be read.",
1289
+ properties: "Properties",
1290
+ siteLabel: "Property",
1291
+ pathLabel: "Path prefix",
1292
+ pathInvalid: "Start the path prefix with /.",
1293
+ addProperty: "Add property",
1294
+ removeProperty: "Remove property",
1295
+ save: "Save",
1296
+ collectDaily: "Collect daily",
1297
+ adminOnly: "Only organization owners and admins can change this.",
1298
+ statusCollecting: "Collecting",
1299
+ statusNeedsAccess: "Needs access",
1300
+ statusFailed: "Last run failed",
1301
+ runSucceeded: "Collected {{when}}",
1302
+ runNeedsAccess: "No access as of {{when}}",
1303
+ runRunning: "Collecting since {{when}}",
1304
+ runFailed: "Failed {{when}}",
1305
+ unavailable: "Collection status is unavailable: {{message}}",
1306
+ keyUnreadable:
1307
+ "Couldn't read the saved service account key. Re-save it under Google Cloud.",
1308
+ propertyBareDomain: "For a Domain property, use {{suggestion}}.",
1309
+ propertyInvalidDomain:
1310
+ "Put only the domain after sc-domain:, as in {{suggestion}}.",
1311
+ propertyMissingSlash:
1312
+ "URL-prefix properties end in /. Use {{suggestion}}.",
1313
+ propertyUnrecognized:
1314
+ "Use sc-domain:example.com, or a URL-prefix property ending in /.",
1315
+ },
1317
1316
  uploadFile: "Upload file",
1318
1317
  googleSheetsExport: "Google Sheets export",
1319
1318
  googleSheetsExportDescription:
@@ -432,7 +432,6 @@ export default function DashboardOverview() {
432
432
  setSigmaMigrationOpen(open);
433
433
  if (!open) setPreferredSigmaConnectionId(null);
434
434
  }}
435
- onImported={() => void dashboardsQuery.refetch()}
436
435
  />
437
436
  ) : null}
438
437
 
@@ -116,6 +116,11 @@ import {
116
116
  type DataSourceCategory,
117
117
  type WalkthroughStep,
118
118
  } from "@/lib/data-sources";
119
+ import {
120
+ SearchConsoleDetail,
121
+ searchConsoleState,
122
+ useContentCollectionStatus,
123
+ } from "@/pages/data-sources/SearchConsoleDetail";
119
124
 
120
125
  import {
121
126
  ConnectionTestStatus,
@@ -2214,6 +2219,7 @@ export default function DataSources() {
2214
2219
  statusData.workspaceConnections?.available === false);
2215
2220
 
2216
2221
  const firstParty = useFirstPartyAnalyticsData();
2222
+ const contentCollection = useContentCollectionStatus();
2217
2223
  const googleSheetsConnected =
2218
2224
  getGoogleSheetsConnection(statusData)?.grantState === "connected";
2219
2225
 
@@ -2377,6 +2383,55 @@ export default function DataSources() {
2377
2383
  ),
2378
2384
  };
2379
2385
 
2386
+ const searchConsoleLabel = {
2387
+ unavailable: {
2388
+ label: t("dataSources.statusUnknown"),
2389
+ className: "text-destructive",
2390
+ },
2391
+ collecting: {
2392
+ label: t("dataSources.searchConsole.statusCollecting"),
2393
+ className: "text-emerald-500",
2394
+ },
2395
+ needs_access: {
2396
+ label: t("dataSources.searchConsole.statusNeedsAccess"),
2397
+ className: "text-amber-600 dark:text-amber-400",
2398
+ },
2399
+ failed: {
2400
+ label: t("dataSources.searchConsole.statusFailed"),
2401
+ className: "text-destructive",
2402
+ },
2403
+ off: {
2404
+ label: t("dataSources.notConfigured"),
2405
+ className: "text-muted-foreground",
2406
+ },
2407
+ }[searchConsoleState(contentCollection.data, contentCollection.error)];
2408
+ const searchConsoleEntry: SourceEntry = {
2409
+ id: "google-search-console",
2410
+ name: t("dataSources.searchConsole.name"),
2411
+ description: t("dataSources.searchConsole.description"),
2412
+ category: "seo",
2413
+ logo: (
2414
+ <SourceLogo
2415
+ id="google-search-console"
2416
+ name={t("dataSources.searchConsole.name")}
2417
+ fallbackIcon={IconSearch}
2418
+ />
2419
+ ),
2420
+ status: contentCollection.isLoading ? undefined : searchConsoleLabel.label,
2421
+ statusClassName: searchConsoleLabel.className,
2422
+ connected:
2423
+ searchConsoleState(contentCollection.data, contentCollection.error) ===
2424
+ "collecting",
2425
+ searchText: [
2426
+ t("dataSources.searchConsole.name"),
2427
+ t("dataSources.searchConsole.description"),
2428
+ "google search console gsc seo clicks impressions",
2429
+ ]
2430
+ .join(" ")
2431
+ .toLowerCase(),
2432
+ detail: <SearchConsoleDetail />,
2433
+ };
2434
+
2380
2435
  const customApiEntry: SourceEntry = {
2381
2436
  id: "custom-api",
2382
2437
  name: t("dataSources.customApi.title"),
@@ -2403,6 +2458,7 @@ export default function DataSources() {
2403
2458
  firstPartyEntry,
2404
2459
  ...sourceEntries,
2405
2460
  googleSheetsEntry,
2461
+ searchConsoleEntry,
2406
2462
  customApiEntry,
2407
2463
  ];
2408
2464
  const activeEntry = entries.find((entry) => entry.id === activeId);
@@ -52,7 +52,8 @@ function isRecord(value: unknown): value is Record<string, unknown> {
52
52
  return typeof value === "object" && value !== null && !Array.isArray(value);
53
53
  }
54
54
 
55
- export const MAX_FILTER_OPTIONS = 100;
55
+ export const MAX_FILTER_OPTIONS = 500;
56
+ export const MAX_FILTER_PRESET_VALUES = 100;
56
57
  // Each preset is a button in the dropdown, so a filter offers a short list.
57
58
  export const MAX_FILTER_PRESETS = 20;
58
59
 
@@ -110,9 +111,9 @@ export function filterPresetIssues(
110
111
  if (
111
112
  isRecord(preset) &&
112
113
  Array.isArray(preset.values) &&
113
- preset.values.length > MAX_FILTER_OPTIONS
114
+ preset.values.length > MAX_FILTER_PRESET_VALUES
114
115
  ) {
115
- return `has more than ${MAX_FILTER_OPTIONS} values`;
116
+ return `has more than ${MAX_FILTER_PRESET_VALUES} values`;
116
117
  }
117
118
  if (
118
119
  !isFilterPreset(preset) ||
@@ -208,6 +208,7 @@ import {
208
208
  type FetchedDashboard,
209
209
  } from "./fetch-dashboard";
210
210
  import { interpolate, interpolateDashboardPanelSql } from "./interpolate";
211
+ import { panelCostChange, usePanelCostNotice } from "./panel-cost-notice";
211
212
  import { serializePanelSql } from "./panel-sql";
212
213
  import { AddPanelPopover, PanelEditorDialog } from "./PanelEditorDialog";
213
214
  import { listReportablePanelIds } from "./report-panel-window";
@@ -1190,6 +1191,7 @@ function SqlDashboardPageContent({
1190
1191
  otherViewers: otherViewerCount(activeUsers, session?.email),
1191
1192
  onUndo: undoLatestSave,
1192
1193
  });
1194
+ const panelCost = usePanelCostNotice(dashboardId);
1193
1195
 
1194
1196
  const updateCachedDashboardConfig = useCallback(
1195
1197
  (updated: SqlDashboardConfig) => {
@@ -2039,8 +2041,10 @@ function SqlDashboardPageContent({
2039
2041
  clampDashboardColumns(current.columns ?? DEFAULT_DASHBOARD_COLUMNS),
2040
2042
  ),
2041
2043
  }));
2044
+ // A removed panel's cost notice would outlive it.
2045
+ panelCost.clear(panelId);
2042
2046
  },
2043
- [persist],
2047
+ [panelCost, persist],
2044
2048
  );
2045
2049
 
2046
2050
  const handleResizePanel = useCallback(
@@ -2073,6 +2077,7 @@ function SqlDashboardPageContent({
2073
2077
  );
2074
2078
  const handleSavePanel = useCallback(
2075
2079
  async (panel: SqlPanel, editorBaseline?: SqlPanel | null) => {
2080
+ const cost: { change?: ReturnType<typeof panelCostChange> } = {};
2076
2081
  await persistThrow((current) => {
2077
2082
  const idx = current.panels.findIndex((p) => p.id === panel.id);
2078
2083
  if (editorBaseline && idx < 0) {
@@ -2086,30 +2091,43 @@ function SqlDashboardPageContent({
2086
2091
  current.panels[idx],
2087
2092
  )
2088
2093
  : panel;
2094
+ cost.change = panelCostChange(
2095
+ idx >= 0 ? current.panels[idx] : null,
2096
+ nextPanel,
2097
+ );
2089
2098
  const nextPanels =
2090
2099
  idx >= 0
2091
2100
  ? current.panels.map((p, i) => (i === idx ? nextPanel : p))
2092
2101
  : [...current.panels, nextPanel];
2093
2102
  return { ...current, panels: nextPanels };
2094
2103
  });
2104
+ if (cost.change === "notify")
2105
+ void panelCost.notify(panel.id, panel.title);
2106
+ // A panel turned into a section no longer costs what its notice said.
2107
+ if (cost.change === "clear") panelCost.clear(panel.id);
2095
2108
  },
2096
- [persistThrow],
2109
+ [panelCost, persistThrow],
2097
2110
  );
2098
2111
  const handleSaveSql = useCallback(
2099
2112
  async (panelId: string, sql: string) => {
2113
+ const edit: { title?: string } = {};
2100
2114
  await persistThrow((current) => {
2101
- if (!current.panels.some((panel) => panel.id === panelId)) {
2102
- return current;
2103
- }
2115
+ const panel = current.panels.find((p) => p.id === panelId);
2116
+ edit.title =
2117
+ panel && serializePanelSql(panel.sql) !== serializePanelSql(sql)
2118
+ ? panel.title
2119
+ : undefined;
2120
+ if (!panel) return current;
2104
2121
  return {
2105
2122
  ...current,
2106
- panels: current.panels.map((panel) =>
2107
- panel.id === panelId ? { ...panel, sql } : panel,
2123
+ panels: current.panels.map((p) =>
2124
+ p.id === panelId ? { ...p, sql } : p,
2108
2125
  ),
2109
2126
  };
2110
2127
  });
2128
+ if (edit.title !== undefined) void panelCost.notify(panelId, edit.title);
2111
2129
  },
2112
- [persistThrow],
2130
+ [panelCost, persistThrow],
2113
2131
  );
2114
2132
  const handleSaveEditedPanel = useCallback(
2115
2133
  async (panel: SqlPanel) => {
@@ -158,7 +158,9 @@ export function interpolate(
158
158
  return "SELECT __invalid_custom_date_range__";
159
159
  }
160
160
  if (
161
- /generate_series|generate_date_array/i.test(sourceSql) &&
161
+ /generate_series|generate_date_array|\bwith\s+recursive\b/i.test(
162
+ sourceSql,
163
+ ) &&
162
164
  (Date.parse(`${end}T00:00:00.000Z`) -
163
165
  Date.parse(`${start}T00:00:00.000Z`)) /
164
166
  DAY_MILLISECONDS +
@@ -0,0 +1,172 @@
1
+ import { callAction } from "@agent-native/core/client/hooks";
2
+ import { useFormatters, useT } from "@agent-native/core/client/i18n";
3
+ import { useCallback, useEffect, useMemo, useRef } from "react";
4
+ import { toast } from "sonner";
5
+
6
+ import { serializePanelSql } from "./panel-sql";
7
+ import type { SqlPanel } from "./types";
8
+
9
+ const MIB = 1024 ** 2;
10
+ const GIB = 1024 ** 3;
11
+ const TIB = 1024 ** 4;
12
+ const NOTICE_MS = 8000;
13
+
14
+ type Formatters = ReturnType<typeof useFormatters>;
15
+
16
+ export function formatScanSize(bytes: number, formatters: Formatters): string {
17
+ const [value, unit] =
18
+ bytes >= TIB
19
+ ? [bytes / TIB, "TiB"]
20
+ : bytes >= GIB
21
+ ? [bytes / GIB, "GiB"]
22
+ : [bytes / MIB, "MiB"];
23
+ return `${formatters.formatNumber(value, { maximumFractionDigits: 1 })} ${unit}`;
24
+ }
25
+
26
+ export function formatQueryUsd(usd: number, formatters: Formatters): string {
27
+ const money = (value: number) =>
28
+ formatters.formatNumber(value, {
29
+ style: "currency",
30
+ currency: "USD",
31
+ maximumFractionDigits: 2,
32
+ });
33
+ return usd > 0 && usd < 0.01 ? `<${money(0.01)}` : money(usd);
34
+ }
35
+
36
+ type CostPanel = Pick<SqlPanel, "chartType" | "source" | "sql">;
37
+
38
+ /** Section and extension panels draw config, so they run no query to price. */
39
+ function runsQuery(panel: CostPanel): boolean {
40
+ return panel.chartType !== "section" && panel.chartType !== "extension";
41
+ }
42
+
43
+ /**
44
+ * What saving a panel means for its cost notice: "notify" to price a new or
45
+ * changed query, including a section turned into a chart, and "clear" when the
46
+ * panel stopped running a query. Any other chart-type or config edit reads the
47
+ * same bytes and changes nothing.
48
+ */
49
+ export function panelCostChange(
50
+ before: CostPanel | null,
51
+ next: CostPanel,
52
+ ): "notify" | "clear" | null {
53
+ if (!runsQuery(next)) return before && runsQuery(before) ? "clear" : null;
54
+ const changed =
55
+ !before ||
56
+ !runsQuery(before) ||
57
+ before.source !== next.source ||
58
+ serializePanelSql(before.sql) !== serializePanelSql(next.sql);
59
+ return changed ? "notify" : null;
60
+ }
61
+
62
+ async function estimatePanel(dashboardId: string, panelId: string) {
63
+ try {
64
+ return await callAction("estimate-query-cost", { dashboardId, panelId });
65
+ } catch {
66
+ // coercion-ok: the notice is informational; a failed estimate leaves the save as it was.
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * After a save that changed a panel's query, shows what one refresh of that
73
+ * panel scans and costs in BigQuery, from the same free dry run as
74
+ * estimate-query-cost. Panels outside BigQuery, and estimates that fail, show
75
+ * nothing and close the panel's earlier notice. An estimate a newer save of the
76
+ * panel has replaced shows nothing either. Over
77
+ * the per-run or per-month threshold the notice stays up until dismissed or
78
+ * until you leave the dashboard. `clear` closes a panel's notice, and drops its
79
+ * pending estimate, when the panel stops running a query.
80
+ */
81
+ export function usePanelCostNotice(dashboardId: string | undefined) {
82
+ const t = useT();
83
+ const formatters = useFormatters();
84
+ // An estimate that lands after leaving the dashboard must not show there,
85
+ // and its warnings close when you leave.
86
+ const openDashboardRef = useRef(dashboardId);
87
+ const warningsRef = useRef(new Set<string>());
88
+ useEffect(() => {
89
+ openDashboardRef.current = dashboardId;
90
+ const warnings = warningsRef.current;
91
+ return () => {
92
+ for (const id of warnings) toast.dismiss(id);
93
+ warnings.clear();
94
+ };
95
+ }, [dashboardId]);
96
+ // Nor may one that lands after a newer save of the same panel.
97
+ const latestRequestRef = useRef(new Map<string, number>());
98
+ const requestCountRef = useRef(0);
99
+ const closeNotice = useCallback((id: string) => {
100
+ toast.dismiss(id);
101
+ warningsRef.current.delete(id);
102
+ }, []);
103
+
104
+ const notify = useCallback(
105
+ async (panelId: string, title: string) => {
106
+ if (!dashboardId) return;
107
+ const key = `${dashboardId}:${panelId}`;
108
+ const request = ++requestCountRef.current;
109
+ latestRequestRef.current.set(key, request);
110
+ const result = await estimatePanel(dashboardId, panelId);
111
+ if (
112
+ openDashboardRef.current !== dashboardId ||
113
+ latestRequestRef.current.get(key) !== request
114
+ ) {
115
+ return;
116
+ }
117
+ const id = `panel-cost:${key}`;
118
+ const estimate =
119
+ result && "panels" in result ? result.panels[0] : undefined;
120
+ if (estimate?.status !== "estimated") {
121
+ // An earlier notice priced the query this save replaced.
122
+ closeNotice(id);
123
+ return;
124
+ }
125
+ const over =
126
+ estimate.overPerRunThreshold || estimate.overPerMonthThreshold;
127
+ const values = {
128
+ size: formatScanSize(estimate.estimatedBytes, formatters),
129
+ perRun: formatQueryUsd(estimate.usdPerRun, formatters),
130
+ perMonth:
131
+ estimate.usdPerMonth === null
132
+ ? ""
133
+ : formatQueryUsd(estimate.usdPerMonth, formatters),
134
+ };
135
+ const cost = t(
136
+ estimate.usdPerMonth === null
137
+ ? "sqlDashboard.panelCostUnscheduled"
138
+ : "sqlDashboard.panelCostScheduled",
139
+ values,
140
+ );
141
+ const options = {
142
+ id,
143
+ // The hint goes on its own line under the cost.
144
+ description: over
145
+ ? `${cost}\n${t("sqlDashboard.panelCostOverHint")}`
146
+ : cost,
147
+ classNames: { description: "whitespace-pre-line" },
148
+ duration: over ? Number.POSITIVE_INFINITY : NOTICE_MS,
149
+ closeButton: over,
150
+ };
151
+ if (over) {
152
+ warningsRef.current.add(options.id);
153
+ toast.warning(t("sqlDashboard.panelCostOverTitle", { title }), options);
154
+ } else {
155
+ toast.message(t("sqlDashboard.panelCostTitle", { title }), options);
156
+ }
157
+ },
158
+ [closeNotice, dashboardId, formatters, t],
159
+ );
160
+
161
+ const clear = useCallback(
162
+ (panelId: string) => {
163
+ if (!dashboardId) return;
164
+ const key = `${dashboardId}:${panelId}`;
165
+ latestRequestRef.current.set(key, ++requestCountRef.current);
166
+ closeNotice(`panel-cost:${key}`);
167
+ },
168
+ [closeNotice, dashboardId],
169
+ );
170
+
171
+ return useMemo(() => ({ notify, clear }), [notify, clear]);
172
+ }