@agent-native/core 0.119.1 → 0.119.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 (230) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +15 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/client.ts +4 -0
  5. package/corpus/core/src/agent/production-agent.ts +1 -0
  6. package/corpus/core/src/client/AssistantChat.tsx +11 -50
  7. package/corpus/core/src/client/ErrorBoundary.tsx +25 -19
  8. package/corpus/core/src/client/agent-page/AgentTabFrame.tsx +1 -1
  9. package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +23 -0
  10. package/corpus/core/src/client/components/ui/message-scroller.tsx +1 -9
  11. package/corpus/core/src/client/resources/ResourceTree.tsx +3 -3
  12. package/corpus/core/src/client/resources/ResourcesPanel.tsx +64 -60
  13. package/corpus/core/src/client/settings/AccountSettingsCard.tsx +228 -0
  14. package/corpus/core/src/client/settings/SecretsSection.tsx +7 -5
  15. package/corpus/core/src/client/settings/SettingsPanel.tsx +67 -244
  16. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +88 -21
  17. package/corpus/core/src/client/settings/agent-settings-search.ts +0 -2
  18. package/corpus/core/src/client/settings/index.ts +7 -0
  19. package/corpus/core/src/client/vite-dev-recovery-script.ts +18 -1
  20. package/corpus/core/src/db/client.ts +65 -15
  21. package/corpus/core/src/db/index.ts +2 -0
  22. package/corpus/core/src/localization/default-messages.ts +1 -0
  23. package/corpus/core/src/server/email.ts +33 -1
  24. package/corpus/core/src/styles/agent-native.css +23 -0
  25. package/corpus/core/src/templates/default/.agents/skills/frontend-design/SKILL.md +28 -0
  26. package/corpus/core/src/templates/default/app/routes/settings.tsx +2 -0
  27. package/corpus/core/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +28 -0
  28. package/corpus/core/src/vite/client.ts +76 -8
  29. package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +28 -0
  30. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +3 -1
  31. package/corpus/templates/analytics/app/components/layout/Layout.tsx +9 -13
  32. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +2 -2
  33. package/corpus/templates/analytics/app/global.css +0 -5
  34. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +0 -13
  35. package/corpus/templates/analytics/app/lib/sql-query.ts +15 -2
  36. package/corpus/templates/analytics/app/pages/Settings.tsx +2 -24
  37. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +6 -0
  38. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -0
  39. package/corpus/templates/analytics/app/pages/settings/settings-search.ts +1 -1
  40. package/corpus/templates/analytics/changelog/2026-07-23-ask-analytics-is-better-centered-with-a-quieter-chat-rail-an.md +6 -0
  41. package/corpus/templates/analytics/changelog/2026-07-23-daily-dashboard-emails-load-read-heavy-panels-reliably.md +6 -0
  42. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +2 -2
  43. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +39 -5
  44. package/corpus/templates/analytics/server/lib/dashboard-report.ts +186 -62
  45. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +3 -0
  46. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +18 -2
  47. package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +3 -0
  48. package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +28 -0
  49. package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -4
  50. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +1 -1
  51. package/corpus/templates/assets/app/global.css +0 -5
  52. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +0 -22
  53. package/corpus/templates/assets/app/routes/settings.tsx +2 -0
  54. package/corpus/templates/assets/changelog/2026-07-23-create-and-chat-surfaces-are-better-centered-with-quieter-ch.md +6 -0
  55. package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +28 -0
  56. package/corpus/templates/brain/app/components/layout/Layout.tsx +1 -1
  57. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +1 -1
  58. package/corpus/templates/brain/app/global.css +0 -9
  59. package/corpus/templates/brain/app/hooks/use-navigation-state.ts +0 -12
  60. package/corpus/templates/brain/app/i18n-data.ts +1 -0
  61. package/corpus/templates/brain/app/lib/settings-navigation.ts +1 -1
  62. package/corpus/templates/brain/app/routes/settings.tsx +21 -22
  63. package/corpus/templates/brain/changelog/2026-07-23-ask-brain-is-better-centered-with-quieter-chat-history-and-a.md +6 -0
  64. package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +28 -0
  65. package/corpus/templates/calendar/app/pages/Settings.tsx +2 -0
  66. package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +28 -0
  67. package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
  68. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +1 -1
  69. package/corpus/templates/chat/app/hooks/use-navigation-state.ts +0 -15
  70. package/corpus/templates/chat/app/routes/settings.tsx +2 -0
  71. package/corpus/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +6 -0
  72. package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +28 -0
  73. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +2 -0
  74. package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +28 -0
  75. package/corpus/templates/content/app/routes/_app.settings.tsx +2 -0
  76. package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +28 -0
  77. package/corpus/templates/design/app/routes/settings.tsx +2 -0
  78. package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +28 -0
  79. package/corpus/templates/dispatch/app/routes/settings.tsx +2 -0
  80. package/corpus/templates/dispatch/changelog/2026-07-23-quieter-progressive-disclosure.md +6 -0
  81. package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +28 -0
  82. package/corpus/templates/forms/app/global.css +0 -5
  83. package/corpus/templates/forms/app/hooks/use-navigation-state.ts +0 -7
  84. package/corpus/templates/forms/app/root.tsx +1 -7
  85. package/corpus/templates/forms/app/routes/_app.settings.tsx +2 -0
  86. package/corpus/templates/forms/changelog/2026-07-23-ask-forms-is-better-centered-with-quieter-chat-history-and-a.md +6 -0
  87. package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +28 -0
  88. package/corpus/templates/macros/app/routes/settings.tsx +2 -0
  89. package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +28 -0
  90. package/corpus/templates/mail/app/pages/SettingsPage.tsx +3 -1
  91. package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +28 -0
  92. package/corpus/templates/plan/app/components/layout/Layout.tsx +2 -2
  93. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +1 -13
  94. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +0 -4
  95. package/corpus/templates/plan/app/routes/settings.tsx +2 -0
  96. package/corpus/templates/plan/changelog/2026-07-23-ask-plan-is-now-a-clean-full-page-chat-surface-with-better-c.md +6 -0
  97. package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +28 -0
  98. package/corpus/templates/slides/app/routes/settings.tsx +2 -0
  99. package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +28 -0
  100. package/corpus/toolkit/CHANGELOG.md +6 -0
  101. package/corpus/toolkit/package.json +1 -1
  102. package/corpus/toolkit/src/chat-history.css +2 -2
  103. package/corpus/toolkit/src/composer/PromptComposer.tsx +0 -31
  104. package/corpus/toolkit/src/composer/RealtimeVoiceMode.tsx +7 -2
  105. package/corpus/toolkit/src/styles.css +183 -36
  106. package/dist/a2a/client.d.ts +2 -0
  107. package/dist/a2a/client.d.ts.map +1 -1
  108. package/dist/a2a/client.js +2 -0
  109. package/dist/a2a/client.js.map +1 -1
  110. package/dist/agent/production-agent.d.ts.map +1 -1
  111. package/dist/agent/production-agent.js +1 -0
  112. package/dist/agent/production-agent.js.map +1 -1
  113. package/dist/client/AssistantChat.d.ts.map +1 -1
  114. package/dist/client/AssistantChat.js +11 -42
  115. package/dist/client/AssistantChat.js.map +1 -1
  116. package/dist/client/ErrorBoundary.d.ts.map +1 -1
  117. package/dist/client/ErrorBoundary.js +3 -2
  118. package/dist/client/ErrorBoundary.js.map +1 -1
  119. package/dist/client/agent-page/AgentTabFrame.js +1 -1
  120. package/dist/client/agent-page/AgentTabFrame.js.map +1 -1
  121. package/dist/client/agent-page/AgentTabsPage.d.ts.map +1 -1
  122. package/dist/client/agent-page/AgentTabsPage.js +13 -1
  123. package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
  124. package/dist/client/components/ui/message-scroller.d.ts.map +1 -1
  125. package/dist/client/components/ui/message-scroller.js +2 -2
  126. package/dist/client/components/ui/message-scroller.js.map +1 -1
  127. package/dist/client/resources/ResourceTree.js +2 -2
  128. package/dist/client/resources/ResourceTree.js.map +1 -1
  129. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  130. package/dist/client/resources/ResourcesPanel.js +11 -11
  131. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  132. package/dist/client/settings/AccountSettingsCard.d.ts +9 -0
  133. package/dist/client/settings/AccountSettingsCard.d.ts.map +1 -0
  134. package/dist/client/settings/AccountSettingsCard.js +78 -0
  135. package/dist/client/settings/AccountSettingsCard.js.map +1 -0
  136. package/dist/client/settings/SecretsSection.d.ts.map +1 -1
  137. package/dist/client/settings/SecretsSection.js +2 -2
  138. package/dist/client/settings/SecretsSection.js.map +1 -1
  139. package/dist/client/settings/SettingsPanel.d.ts +3 -0
  140. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  141. package/dist/client/settings/SettingsPanel.js +25 -77
  142. package/dist/client/settings/SettingsPanel.js.map +1 -1
  143. package/dist/client/settings/SettingsTabsPage.d.ts +5 -1
  144. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
  145. package/dist/client/settings/SettingsTabsPage.js +35 -9
  146. package/dist/client/settings/SettingsTabsPage.js.map +1 -1
  147. package/dist/client/settings/agent-settings-search.d.ts.map +1 -1
  148. package/dist/client/settings/agent-settings-search.js +0 -2
  149. package/dist/client/settings/agent-settings-search.js.map +1 -1
  150. package/dist/client/settings/index.d.ts +2 -1
  151. package/dist/client/settings/index.d.ts.map +1 -1
  152. package/dist/client/settings/index.js +2 -1
  153. package/dist/client/settings/index.js.map +1 -1
  154. package/dist/client/vite-dev-recovery-script.d.ts.map +1 -1
  155. package/dist/client/vite-dev-recovery-script.js +18 -1
  156. package/dist/client/vite-dev-recovery-script.js.map +1 -1
  157. package/dist/collab/awareness.d.ts +2 -2
  158. package/dist/collab/awareness.d.ts.map +1 -1
  159. package/dist/collab/routes.d.ts +1 -1
  160. package/dist/collab/struct-routes.d.ts +1 -1
  161. package/dist/db/client.d.ts +18 -8
  162. package/dist/db/client.d.ts.map +1 -1
  163. package/dist/db/client.js +34 -11
  164. package/dist/db/client.js.map +1 -1
  165. package/dist/db/index.d.ts +1 -1
  166. package/dist/db/index.d.ts.map +1 -1
  167. package/dist/db/index.js.map +1 -1
  168. package/dist/localization/default-messages.d.ts +1 -0
  169. package/dist/localization/default-messages.d.ts.map +1 -1
  170. package/dist/localization/default-messages.js +1 -0
  171. package/dist/localization/default-messages.js.map +1 -1
  172. package/dist/notifications/routes.d.ts +2 -2
  173. package/dist/observability/routes.d.ts +3 -3
  174. package/dist/progress/routes.d.ts +1 -1
  175. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  176. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  177. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  178. package/dist/resources/handlers.d.ts +1 -1
  179. package/dist/secrets/routes.d.ts +9 -9
  180. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  181. package/dist/server/email.d.ts +1 -0
  182. package/dist/server/email.d.ts.map +1 -1
  183. package/dist/server/email.js +29 -1
  184. package/dist/server/email.js.map +1 -1
  185. package/dist/server/realtime-token.d.ts +1 -1
  186. package/dist/styles/agent-native.css +23 -0
  187. package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +28 -0
  188. package/dist/templates/chat/app/components/layout/Layout.tsx +1 -1
  189. package/dist/templates/chat/app/components/layout/Sidebar.tsx +1 -1
  190. package/dist/templates/chat/app/hooks/use-navigation-state.ts +0 -15
  191. package/dist/templates/chat/app/routes/settings.tsx +2 -0
  192. package/dist/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +6 -0
  193. package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +28 -0
  194. package/dist/templates/default/app/routes/settings.tsx +2 -0
  195. package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +28 -0
  196. package/dist/vite/client.d.ts.map +1 -1
  197. package/dist/vite/client.js +68 -6
  198. package/dist/vite/client.js.map +1 -1
  199. package/package.json +2 -2
  200. package/src/a2a/client.ts +4 -0
  201. package/src/agent/production-agent.ts +1 -0
  202. package/src/client/AssistantChat.tsx +11 -50
  203. package/src/client/ErrorBoundary.tsx +25 -19
  204. package/src/client/agent-page/AgentTabFrame.tsx +1 -1
  205. package/src/client/agent-page/AgentTabsPage.tsx +23 -0
  206. package/src/client/components/ui/message-scroller.tsx +1 -9
  207. package/src/client/resources/ResourceTree.tsx +3 -3
  208. package/src/client/resources/ResourcesPanel.tsx +64 -60
  209. package/src/client/settings/AccountSettingsCard.tsx +228 -0
  210. package/src/client/settings/SecretsSection.tsx +7 -5
  211. package/src/client/settings/SettingsPanel.tsx +67 -244
  212. package/src/client/settings/SettingsTabsPage.tsx +88 -21
  213. package/src/client/settings/agent-settings-search.ts +0 -2
  214. package/src/client/settings/index.ts +7 -0
  215. package/src/client/vite-dev-recovery-script.ts +18 -1
  216. package/src/db/client.ts +65 -15
  217. package/src/db/index.ts +2 -0
  218. package/src/localization/default-messages.ts +1 -0
  219. package/src/server/email.ts +33 -1
  220. package/src/styles/agent-native.css +23 -0
  221. package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +28 -0
  222. package/src/templates/chat/app/components/layout/Layout.tsx +1 -1
  223. package/src/templates/chat/app/components/layout/Sidebar.tsx +1 -1
  224. package/src/templates/chat/app/hooks/use-navigation-state.ts +0 -15
  225. package/src/templates/chat/app/routes/settings.tsx +2 -0
  226. package/src/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +6 -0
  227. package/src/templates/default/.agents/skills/frontend-design/SKILL.md +28 -0
  228. package/src/templates/default/app/routes/settings.tsx +2 -0
  229. package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +28 -0
  230. package/src/vite/client.ts +76 -8
@@ -9,10 +9,26 @@ import {
9
9
 
10
10
  let running = false;
11
11
  const DEFAULT_MAX_REPORTS_PER_SWEEP = 5;
12
+ const SERVERLESS_REPORT_DELIVERY_BUDGET_MS = 220_000;
13
+
14
+ async function persistDashboardReportResult(
15
+ ...args: Parameters<typeof markDashboardReportResult>
16
+ ): Promise<boolean> {
17
+ try {
18
+ await markDashboardReportResult(...args);
19
+ return true;
20
+ } catch (err) {
21
+ console.error(
22
+ `[dashboard-report] Failed to persist subscription ${args[0].id} result:`,
23
+ err instanceof Error ? err.message : String(err),
24
+ );
25
+ return false;
26
+ }
27
+ }
12
28
 
13
29
  function maxReportsPerSweep(): number {
30
+ if (process.env.NETLIFY === "true") return 1;
14
31
  const raw = process.env.DASHBOARD_REPORT_SWEEP_LIMIT?.trim();
15
- if (!raw && process.env.NETLIFY === "true") return 1;
16
32
  if (!raw) return DEFAULT_MAX_REPORTS_PER_SWEEP;
17
33
  const parsed = Number.parseInt(raw, 10);
18
34
  return Number.isFinite(parsed) && parsed > 0
@@ -31,6 +47,10 @@ export async function runDashboardReportsOnce(): Promise<{
31
47
  }> {
32
48
  if (running) return { processed: 0, failed: 0, remaining: 0 };
33
49
  running = true;
50
+ const deliveryDeadlineAt =
51
+ process.env.NETLIFY === "true"
52
+ ? Date.now() + SERVERLESS_REPORT_DELIVERY_BUDGET_MS
53
+ : undefined;
34
54
  let processed = 0;
35
55
  let failed = 0;
36
56
  let remaining = 0;
@@ -51,8 +71,21 @@ export async function runDashboardReportsOnce(): Promise<{
51
71
  () =>
52
72
  sendDashboardReportSubscription(sub, {
53
73
  skipEmailWithoutScreenshot: retryAt !== null,
74
+ ...(deliveryDeadlineAt ? { deadlineAt: deliveryDeadlineAt } : {}),
54
75
  }),
55
76
  );
77
+ if (result.screenshotMode === "partial" && result.emailsSent) {
78
+ failed++;
79
+ const message = result.screenshotError
80
+ ? `Dashboard screenshot partially available: ${result.screenshotError}`
81
+ : "Dashboard screenshot partially available";
82
+ console.error(
83
+ `[dashboard-report] Subscription ${sub.id} sent with a partial screenshot:`,
84
+ message,
85
+ );
86
+ await persistDashboardReportResult(sub, "error", message);
87
+ continue;
88
+ }
56
89
  if (!result.screenshotAttached) {
57
90
  const message = result.screenshotError
58
91
  ? `Dashboard screenshot unavailable: ${result.screenshotError}`
@@ -62,12 +95,13 @@ export async function runDashboardReportsOnce(): Promise<{
62
95
  `[dashboard-report] Subscription ${sub.id} skipped sending without a screenshot, will retry:`,
63
96
  message,
64
97
  );
65
- await markDashboardReportResult(
98
+ const persisted = await persistDashboardReportResult(
66
99
  sub,
67
100
  "error",
68
101
  `${message} (retry scheduled)`,
69
102
  { nextRunAt: retryAt },
70
103
  );
104
+ if (!persisted) failed++;
71
105
  continue;
72
106
  }
73
107
  failed++;
@@ -75,10 +109,10 @@ export async function runDashboardReportsOnce(): Promise<{
75
109
  `[dashboard-report] Subscription ${sub.id} sent without a screenshot:`,
76
110
  message,
77
111
  );
78
- await markDashboardReportResult(sub, "error", message);
112
+ await persistDashboardReportResult(sub, "error", message);
79
113
  continue;
80
114
  }
81
- await markDashboardReportResult(sub, "success");
115
+ if (!(await persistDashboardReportResult(sub, "success"))) failed++;
82
116
  } catch (err: any) {
83
117
  failed++;
84
118
  const message = err?.message ?? String(err);
@@ -86,7 +120,7 @@ export async function runDashboardReportsOnce(): Promise<{
86
120
  `[dashboard-report] Subscription ${sub.id} failed:`,
87
121
  message,
88
122
  );
89
- await markDashboardReportResult(sub, "error", message);
123
+ await persistDashboardReportResult(sub, "error", message);
90
124
  }
91
125
  }
92
126
  } finally {
@@ -24,6 +24,7 @@ import type {
24
24
  FilterType,
25
25
  SqlDashboardConfig,
26
26
  } from "../../app/pages/adhoc/sql-dashboard/types";
27
+ import { DASHBOARD_REPORT_READY_TIMEOUT_MS } from "../../shared/dashboard-report-timeouts.js";
27
28
  import {
28
29
  getReportDashboard,
29
30
  normalizeDashboardReportRecipients,
@@ -55,10 +56,12 @@ const DASHBOARD_REPORT_CID = "dashboard-report-snapshot";
55
56
  const LOCAL_SCREENSHOT_TIMEOUT_MS = 90_000;
56
57
  const SERVERLESS_SCREENSHOT_TIMEOUT_MS = 90_000;
57
58
  const SERVERLESS_SECOND_READY_TIMEOUT_MS = 45_000;
58
- // Reserve 90s under Netlify's 300s background-function limit for bounded
59
- // browser cleanup plus delivery to the capped recipient list.
60
- const SERVERLESS_CHUNKED_ATTEMPT_TIMEOUT_MS = 210_000;
59
+ // Cap browser work separately from the sweep-level delivery deadline.
60
+ const SERVERLESS_CHUNKED_ATTEMPT_TIMEOUT_MS = 150_000;
61
61
  const BROWSER_CLEANUP_TIMEOUT_MS = 10_000;
62
+ const DASHBOARD_REPORT_EMAIL_TIMEOUT_MS = 10_000;
63
+ const DASHBOARD_REPORT_EMAIL_OVERHEAD_RESERVE_MS = 5_000;
64
+ const MAX_CAPTURE_CLEANUP_RESERVE_MS = BROWSER_CLEANUP_TIMEOUT_MS * 3;
62
65
  const SCREENSHOT_VIEWPORT_PADDING = 64;
63
66
  const MAX_DASHBOARD_REPORT_CHUNKS = 10;
64
67
  const MAX_DASHBOARD_REPORT_ATTACHMENT_BYTES = 14 * 1024 * 1024;
@@ -78,6 +81,14 @@ type LaunchedScreenshotBrowser = {
78
81
  newPage: () => Promise<any>;
79
82
  };
80
83
 
84
+ class DashboardReportPanelMismatchError extends Error {}
85
+
86
+ type DashboardPngCapture = {
87
+ pngs: Array<Buffer | null> | null;
88
+ mode: "full" | "partial" | "none";
89
+ error?: string;
90
+ };
91
+
81
92
  function daysAgo(n: number): string {
82
93
  const d = new Date();
83
94
  d.setDate(d.getDate() - n);
@@ -387,6 +398,33 @@ async function runWithinCaptureDeadline<T>(
387
398
  }
388
399
  }
389
400
 
401
+ async function runWithinReportDeadline<T>(
402
+ label: string,
403
+ operation: () => Promise<T>,
404
+ deadlineAt?: number,
405
+ ): Promise<T> {
406
+ if (!deadlineAt) return operation();
407
+ const timeoutMs = deadlineAt - Date.now();
408
+ if (timeoutMs <= 0)
409
+ throw new Error(`${label} exceeded the report delivery deadline`);
410
+
411
+ let timeout: ReturnType<typeof setTimeout> | undefined;
412
+ try {
413
+ return await Promise.race([
414
+ operation(),
415
+ new Promise<never>((_, reject) => {
416
+ timeout = setTimeout(
417
+ () =>
418
+ reject(new Error(`${label} exceeded the report delivery deadline`)),
419
+ timeoutMs,
420
+ );
421
+ }),
422
+ ]);
423
+ } finally {
424
+ if (timeout) clearTimeout(timeout);
425
+ }
426
+ }
427
+
390
428
  function positiveIntEnv(name: string): number | null {
391
429
  const raw = process.env[name]?.trim();
392
430
  if (!raw) return null;
@@ -397,6 +435,8 @@ function positiveIntEnv(name: string): number | null {
397
435
  async function waitForDashboardReportReady(
398
436
  page: any,
399
437
  timeout: number,
438
+ consoleErrors: string[] = [],
439
+ failedRequests: string[] = [],
400
440
  ): Promise<boolean> {
401
441
  try {
402
442
  await page.waitForFunction(
@@ -440,9 +480,13 @@ async function waitForDashboardReportReady(
440
480
  };
441
481
  })()`)
442
482
  .catch(() => null);
483
+ const captureDiagnostics = { consoleErrors, failedRequests };
443
484
  const message = detail
444
- ? `${err?.message ?? String(err)}; dashboard state: ${JSON.stringify(detail)}`
445
- : `${err?.message ?? String(err)}; dashboard page was not inspectable`;
485
+ ? `${err?.message ?? String(err)}; dashboard state: ${JSON.stringify({
486
+ ...detail,
487
+ ...captureDiagnostics,
488
+ })}`
489
+ : `${err?.message ?? String(err)}; dashboard page was not inspectable; capture diagnostics: ${JSON.stringify(captureDiagnostics)}`;
446
490
  throw new Error(message);
447
491
  }
448
492
  }
@@ -469,7 +513,7 @@ async function assertDashboardReportPanelWindow(
469
513
  actualPanelIds.length !== expectedPanelIds.length ||
470
514
  actualPanelIds.some((panelId, index) => panelId !== expectedPanelIds[index])
471
515
  ) {
472
- throw new Error(
516
+ throw new DashboardReportPanelMismatchError(
473
517
  `report chunk panel mismatch; expected=${JSON.stringify(expectedPanelIds)} actual=${JSON.stringify(actualPanelIds)}`,
474
518
  );
475
519
  }
@@ -765,6 +809,8 @@ async function captureDashboardChunk(
765
809
  await waitForDashboardReportReady(
766
810
  page,
767
811
  boundedStageTimeout(attempt.readyTimeout ?? timeout, deadlineAt),
812
+ consoleErrors,
813
+ failedRequests,
768
814
  );
769
815
  captureStage = "validating the report chunk panels";
770
816
  await assertDashboardReportPanelWindow(page, expectedPanelIds);
@@ -777,6 +823,8 @@ async function captureDashboardChunk(
777
823
  attempt.secondReadyTimeout ?? secondReadyTimeoutMs(),
778
824
  deadlineAt,
779
825
  ),
826
+ consoleErrors,
827
+ failedRequests,
780
828
  );
781
829
  captureStage = "revalidating the report chunk panels";
782
830
  await assertDashboardReportPanelWindow(page, expectedPanelIds);
@@ -793,6 +841,7 @@ async function captureDashboardChunk(
793
841
  if (!image?.length) throw new Error("Dashboard screenshot was empty");
794
842
  return Buffer.from(image);
795
843
  } catch (err) {
844
+ if (err instanceof DashboardReportPanelMismatchError) throw err;
796
845
  throw new Error(`${captureStage}: ${errorMessage(err)}`);
797
846
  }
798
847
  }
@@ -800,7 +849,9 @@ async function captureDashboardChunk(
800
849
  async function captureDashboardPngChunks(
801
850
  sub: DashboardReportSubscription,
802
851
  snapshot: ReportSnapshot,
803
- ): Promise<Buffer[]> {
852
+ allowPartialCapture: boolean,
853
+ totalTimeoutMs?: number,
854
+ ): Promise<DashboardPngCapture> {
804
855
  const offsets = reportChunkOffsets(snapshot.panelIds.length);
805
856
  if (offsets.length > MAX_DASHBOARD_REPORT_CHUNKS) {
806
857
  throw new Error(
@@ -814,9 +865,9 @@ async function captureDashboardPngChunks(
814
865
  captureScale: 0.7,
815
866
  ...(serverless
816
867
  ? {
817
- readyTimeout: 35_000,
868
+ readyTimeout: DASHBOARD_REPORT_READY_TIMEOUT_MS,
818
869
  secondReadyTimeout: 15_000,
819
- totalTimeout: SERVERLESS_CHUNKED_ATTEMPT_TIMEOUT_MS,
870
+ totalTimeout: totalTimeoutMs ?? SERVERLESS_CHUNKED_ATTEMPT_TIMEOUT_MS,
820
871
  }
821
872
  : {}),
822
873
  };
@@ -834,6 +885,7 @@ async function captureDashboardPngChunks(
834
885
  if (browser) void browser.close().catch(() => {});
835
886
  }, attempt.totalTimeout)
836
887
  : null;
888
+ const images: Array<Buffer | null> = Array(offsets.length).fill(null);
837
889
  try {
838
890
  launchPromise = launchScreenshotBrowser(attempt.viewport);
839
891
  const launched = attempt.totalTimeout
@@ -854,9 +906,8 @@ async function captureDashboardPngChunks(
854
906
  : await launchPromise;
855
907
  browser = launched.browser;
856
908
  cleanup = launched.cleanup;
857
- const images: Buffer[] = [];
858
909
  let attachmentBytes = 0;
859
- for (const offset of offsets) {
910
+ for (const [index, offset] of offsets.entries()) {
860
911
  if (attemptTimedOut)
861
912
  throw new Error(`capture exceeded ${attempt.totalTimeout}ms`);
862
913
  const page = await runWithinCaptureDeadline(
@@ -887,21 +938,35 @@ async function captureDashboardPngChunks(
887
938
  `complete dashboard images total ${attachmentBytes} bytes; email delivery supports at most ${MAX_DASHBOARD_REPORT_ATTACHMENT_BYTES} bytes`,
888
939
  );
889
940
  }
890
- images.push(image);
941
+ images[index] = image;
891
942
  } finally {
892
943
  await runBoundedBrowserCleanup("Failed to close screenshot page", () =>
893
944
  page.close(),
894
945
  );
895
946
  }
896
947
  }
897
- return images;
948
+ return { pngs: images, mode: "full" };
898
949
  } catch (err) {
950
+ const partialCapture =
951
+ allowPartialCapture &&
952
+ !(err instanceof DashboardReportPanelMismatchError) &&
953
+ images.some((image) => image !== null);
954
+ const suffix = attemptTimedOut
955
+ ? `${memoryDiagnostics()} ${attempt.label} capture exceeded ${attempt.totalTimeout}ms: ${errorMessage(err)}`
956
+ : `${attempt.label}: ${errorMessage(err)}`;
957
+ if (partialCapture) {
958
+ return {
959
+ pngs: images,
960
+ mode: "partial",
961
+ error: suffix.includes("lambdaMemoryMb=")
962
+ ? suffix
963
+ : `${suffix}; ${memoryDiagnostics()}`,
964
+ };
965
+ }
899
966
  if (attemptTimedOut) {
900
- throw new Error(
901
- `${memoryDiagnostics()} ${attempt.label} capture exceeded ${attempt.totalTimeout}ms: ${errorMessage(err)}`,
902
- );
967
+ throw new Error(suffix);
903
968
  }
904
- throw new Error(`${attempt.label}: ${errorMessage(err)}`);
969
+ throw new Error(suffix);
905
970
  } finally {
906
971
  if (attemptTimeout) clearTimeout(attemptTimeout);
907
972
  if (launchTimeout) clearTimeout(launchTimeout);
@@ -940,22 +1005,35 @@ function errorMessage(err: unknown): string {
940
1005
  async function captureDashboardPngWithFallback(
941
1006
  sub: DashboardReportSubscription,
942
1007
  snapshot: ReportSnapshot,
943
- ): Promise<{
944
- pngs: Buffer[] | null;
945
- mode: "full" | "none";
946
- error?: string;
947
- }> {
1008
+ allowPartialCapture: boolean,
1009
+ totalTimeoutMs?: number,
1010
+ ): Promise<DashboardPngCapture> {
1011
+ if (totalTimeoutMs !== undefined && totalTimeoutMs <= 0) {
1012
+ return {
1013
+ pngs: null,
1014
+ mode: "none",
1015
+ error: `report delivery deadline left no time for capture; ${memoryDiagnostics()}`,
1016
+ };
1017
+ }
948
1018
  try {
949
- const pngs = await captureDashboardPngChunks(sub, snapshot);
1019
+ const capture = await captureDashboardPngChunks(
1020
+ sub,
1021
+ snapshot,
1022
+ allowPartialCapture,
1023
+ totalTimeoutMs,
1024
+ );
950
1025
  if (isServerlessBrowserRuntime()) {
951
1026
  console.info(
952
- `[dashboard-report] complete chunked capture succeeded: chunks=${pngs.length} ${memoryDiagnostics()}`,
1027
+ `[dashboard-report] ${capture.mode} chunked capture finished: chunks=${capture.pngs?.filter(Boolean).length ?? 0}/${capture.pngs?.length ?? 0} ${memoryDiagnostics()}`,
953
1028
  );
954
1029
  }
955
- return {
956
- pngs,
957
- mode: "full",
958
- };
1030
+ if (capture.mode === "partial") {
1031
+ console.error(
1032
+ `[dashboard-report] partial chunked screenshot capture for subscription ${sub.id}:`,
1033
+ capture.error,
1034
+ );
1035
+ }
1036
+ return capture;
959
1037
  } catch (err) {
960
1038
  const rawError = errorMessage(err).replace(/\s+/g, " ").trim();
961
1039
  const error = rawError.includes("lambdaMemoryMb=")
@@ -980,24 +1058,25 @@ function reportDate(snapshot: ReportSnapshot): string {
980
1058
  function renderReportEmailHtml(
981
1059
  snapshot: ReportSnapshot,
982
1060
  options: {
983
- screenshotAttached: boolean;
984
- screenshotCount: number;
1061
+ screenshotParts: Array<Buffer | null>;
985
1062
  },
986
1063
  ): string {
987
1064
  const title = escapeHtml(snapshot.title);
988
1065
  const dashboardUrl = escapeHtml(snapshot.dashboardUrl);
989
1066
  const reportSettingsUrl = escapeHtml(snapshot.reportSettingsUrl);
990
1067
  const date = escapeHtml(reportDate(snapshot));
991
- const screenshotBlock = options.screenshotAttached
992
- ? Array.from(
993
- { length: options.screenshotCount },
994
- (
995
- _,
996
- index,
997
- ) => `<a href="${dashboardUrl}" style="display:block;text-decoration:none;">
998
- <img src="cid:${DASHBOARD_REPORT_CID}-${index + 1}" alt="${title}${options.screenshotCount > 1 ? ` (part ${index + 1})` : ""}" width="100%" style="display:block;width:100%;max-width:1280px;height:auto;border:0;outline:0;border-radius:0;" />
999
- </a>`,
1000
- ).join("\n")
1068
+ const screenshotBlock = options.screenshotParts.some(Boolean)
1069
+ ? options.screenshotParts
1070
+ .map((screenshot, index) =>
1071
+ screenshot
1072
+ ? `<a href="${dashboardUrl}" style="display:block;text-decoration:none;">
1073
+ <img src="cid:${DASHBOARD_REPORT_CID}-${index + 1}" alt="${title}${options.screenshotParts.length > 1 ? ` (part ${index + 1})` : ""}" width="100%" style="display:block;width:100%;max-width:1280px;height:auto;border:0;outline:0;border-radius:0;" />
1074
+ </a>`
1075
+ : `<div style="margin:18px 0;padding:14px 16px;border:1px solid #e5e7eb;border-radius:8px;background:#f9fafb;color:#374151;font-size:14px;line-height:1.5;">
1076
+ Dashboard image part ${index + 1} was unavailable for this run. Open the live dashboard to view the latest report.
1077
+ </div>`,
1078
+ )
1079
+ .join("\n")
1001
1080
  : `<div style="margin:18px 0;padding:14px 16px;border:1px solid #e5e7eb;border-radius:8px;background:#f9fafb;color:#374151;font-size:14px;line-height:1.5;">
1002
1081
  The dashboard image was unavailable for this run. Open the live dashboard to view the latest report.
1003
1082
  </div>`;
@@ -1021,7 +1100,7 @@ function renderReportEmailHtml(
1021
1100
  function renderReportText(
1022
1101
  snapshot: ReportSnapshot,
1023
1102
  options: {
1024
- screenshotAttached: boolean;
1103
+ screenshotParts: Array<Buffer | null>;
1025
1104
  },
1026
1105
  ): string {
1027
1106
  const lines = [
@@ -1030,8 +1109,15 @@ function renderReportText(
1030
1109
  `Open dashboard: ${snapshot.dashboardUrl}`,
1031
1110
  `Edit subscription settings: ${snapshot.reportSettingsUrl}`,
1032
1111
  ];
1033
- if (!options.screenshotAttached) {
1112
+ if (!options.screenshotParts.some(Boolean)) {
1034
1113
  lines.push("Dashboard image unavailable for this run.");
1114
+ } else {
1115
+ for (const [index, screenshot] of options.screenshotParts.entries()) {
1116
+ if (!screenshot)
1117
+ lines.push(
1118
+ `Dashboard image part ${index + 1} unavailable for this run.`,
1119
+ );
1120
+ }
1035
1121
  }
1036
1122
  return lines.join("\n");
1037
1123
  }
@@ -1051,26 +1137,46 @@ export async function sendDashboardReportSubscription(
1051
1137
  options: {
1052
1138
  requireScreenshot?: boolean;
1053
1139
  skipEmailWithoutScreenshot?: boolean;
1140
+ deadlineAt?: number;
1054
1141
  } = {},
1055
1142
  ): Promise<{
1056
1143
  dashboardUrl: string;
1057
1144
  recipientCount: number;
1058
1145
  screenshotAttached: boolean;
1059
- screenshotMode: "full" | "none";
1146
+ screenshotMode: "full" | "partial" | "none";
1060
1147
  screenshotError?: string;
1061
1148
  emailsSent: boolean;
1062
1149
  }> {
1063
1150
  const recipients = normalizeDashboardReportRecipients(sub.recipients);
1064
- const snapshot = await collectReportSnapshot(sub);
1065
- const capture = await captureDashboardPngWithFallback(sub, snapshot);
1066
- if (!capture.pngs && options.requireScreenshot) {
1151
+ const snapshot = await runWithinReportDeadline(
1152
+ "Dashboard report snapshot",
1153
+ () => collectReportSnapshot(sub),
1154
+ options.deadlineAt,
1155
+ );
1156
+ const captureTimeoutMs = options.deadlineAt
1157
+ ? Math.min(
1158
+ SERVERLESS_CHUNKED_ATTEMPT_TIMEOUT_MS,
1159
+ options.deadlineAt -
1160
+ Date.now() -
1161
+ MAX_CAPTURE_CLEANUP_RESERVE_MS -
1162
+ recipients.length * DASHBOARD_REPORT_EMAIL_TIMEOUT_MS -
1163
+ DASHBOARD_REPORT_EMAIL_OVERHEAD_RESERVE_MS,
1164
+ )
1165
+ : undefined;
1166
+ const capture = await captureDashboardPngWithFallback(
1167
+ sub,
1168
+ snapshot,
1169
+ !options.skipEmailWithoutScreenshot && !options.requireScreenshot,
1170
+ captureTimeoutMs,
1171
+ );
1172
+ if (capture.mode !== "full" && options.requireScreenshot) {
1067
1173
  throw new Error(
1068
1174
  capture.error
1069
1175
  ? `Dashboard screenshot unavailable: ${capture.error}`
1070
1176
  : "Dashboard screenshot unavailable",
1071
1177
  );
1072
1178
  }
1073
- if (!capture.pngs && options.skipEmailWithoutScreenshot) {
1179
+ if (capture.mode !== "full" && options.skipEmailWithoutScreenshot) {
1074
1180
  return {
1075
1181
  dashboardUrl: snapshot.dashboardUrl,
1076
1182
  recipientCount: recipients.length,
@@ -1080,35 +1186,53 @@ export async function sendDashboardReportSubscription(
1080
1186
  ...(capture.error ? { screenshotError: capture.error } : {}),
1081
1187
  };
1082
1188
  }
1083
- const screenshotAttached = Boolean(capture.pngs?.length);
1189
+ const screenshotAttached = Boolean(capture.pngs?.some(Boolean));
1190
+ const screenshotParts = capture.pngs ?? [];
1084
1191
  const html = renderReportEmailHtml(snapshot, {
1085
- screenshotAttached,
1086
- screenshotCount: capture.pngs?.length ?? 0,
1192
+ screenshotParts,
1087
1193
  });
1088
1194
  const text = renderReportText(snapshot, {
1089
- screenshotAttached,
1195
+ screenshotParts,
1090
1196
  });
1091
1197
  const subject = `Daily dashboard: ${snapshot.title}`;
1092
1198
 
1093
1199
  for (const to of recipients) {
1200
+ const emailTimeoutMs = options.deadlineAt
1201
+ ? Math.min(
1202
+ DASHBOARD_REPORT_EMAIL_TIMEOUT_MS,
1203
+ options.deadlineAt - Date.now(),
1204
+ )
1205
+ : DASHBOARD_REPORT_EMAIL_TIMEOUT_MS;
1206
+ if (emailTimeoutMs <= 0) {
1207
+ throw new Error(
1208
+ "Dashboard email delivery exceeded the report delivery deadline",
1209
+ );
1210
+ }
1094
1211
  await sendEmail({
1095
1212
  to,
1096
1213
  subject,
1097
1214
  html,
1098
1215
  text,
1099
1216
  attachments: capture.pngs
1100
- ? capture.pngs.map((png, index) => ({
1101
- filename: reportFilename(
1102
- snapshot.title,
1103
- index + 1,
1104
- capture.pngs!.length,
1105
- ),
1106
- content: png,
1107
- contentType: "image/png",
1108
- contentId: `${DASHBOARD_REPORT_CID}-${index + 1}`,
1109
- disposition: "inline",
1110
- }))
1217
+ ? capture.pngs.flatMap((png, index) =>
1218
+ png
1219
+ ? [
1220
+ {
1221
+ filename: reportFilename(
1222
+ snapshot.title,
1223
+ index + 1,
1224
+ capture.pngs!.length,
1225
+ ),
1226
+ content: png,
1227
+ contentType: "image/png",
1228
+ contentId: `${DASHBOARD_REPORT_CID}-${index + 1}`,
1229
+ disposition: "inline",
1230
+ },
1231
+ ]
1232
+ : [],
1233
+ )
1111
1234
  : undefined,
1235
+ timeoutMs: emailTimeoutMs,
1112
1236
  });
1113
1237
  }
1114
1238
 
@@ -1,6 +1,7 @@
1
1
  import { getDbExec } from "@agent-native/core/db";
2
2
  import { and, eq, isNull, or } from "drizzle-orm";
3
3
 
4
+ import { FIRST_PARTY_ANALYTICS_QUERY_TIMEOUT_MS } from "../../shared/dashboard-report-timeouts.js";
4
5
  import { getDb, schema } from "../db/index.js";
5
6
  import {
6
7
  EXCEPTION_EVENT_NAME,
@@ -642,6 +643,8 @@ export async function queryFirstPartyAnalytics(
642
643
  const result = await exec.execute({
643
644
  sql: `SELECT * FROM (${scoped.sql}) AS first_party_analytics_query LIMIT ${MAX_QUERY_ROWS}`,
644
645
  args: scoped.args,
646
+ timeoutMs: FIRST_PARTY_ANALYTICS_QUERY_TIMEOUT_MS,
647
+ maxAttempts: 1,
645
648
  });
646
649
  const rows = result.rows as Record<string, unknown>[];
647
650
  return { rows, schema: inferSchema(rows) };
@@ -393,6 +393,20 @@ export function repairFirstPartyObservedRetentionPanels(
393
393
  description: RECURRING_USERS_WEEKLY_DESCRIPTION,
394
394
  },
395
395
  ],
396
+ [
397
+ "dau-over-time",
398
+ {
399
+ legacySql: [LEGACY_DAU_BY_TEMPLATE_SQL],
400
+ sql: DAU_BY_TEMPLATE_SQL,
401
+ },
402
+ ],
403
+ [
404
+ "wau-over-time",
405
+ {
406
+ legacySql: [LEGACY_WAU_BY_TEMPLATE_SQL],
407
+ sql: WAU_BY_TEMPLATE_SQL,
408
+ },
409
+ ],
396
410
  ...additionalReplacements.map(
397
411
  (replacement) => [replacement.id, replacement] as const,
398
412
  ),
@@ -437,8 +451,10 @@ export function repairFirstPartyObservedRetentionPanels(
437
451
  ? { config: { ...config, panels }, changed }
438
452
  : { config, changed };
439
453
  }
440
- const DAU_BY_TEMPLATE_SQL = `SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template, COUNT(DISTINCT ${SIGNED_IN_ACTIVITY_KEY_SQL}) AS visitors FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} GROUP BY ${EVENT_DATE_SQL}, ${TEMPLATE_EXPR} ORDER BY date, template`;
441
- const WAU_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS visitor_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${DASHBOARD_WAU_BASE_RANGE_FILTER}), days AS (SELECT DISTINCT event_date AS date FROM base WHERE ${DASHBOARD_EVENT_DATE_RANGE_FILTER}) SELECT d.date, b.template, COUNT(DISTINCT b.visitor_key) AS visitors FROM days d JOIN base b ON b.event_date >= to_char(d.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND b.event_date <= d.date GROUP BY d.date, b.template ORDER BY d.date, b.template`;
454
+ export const LEGACY_DAU_BY_TEMPLATE_SQL = `SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template, COUNT(DISTINCT ${SIGNED_IN_ACTIVITY_KEY_SQL}) AS visitors FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} GROUP BY ${EVENT_DATE_SQL}, ${TEMPLATE_EXPR} ORDER BY date, template`;
455
+ const DAU_BY_TEMPLATE_SQL = `SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template, COUNT(DISTINCT user_key) AS visitors FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} GROUP BY ${EVENT_DATE_SQL}, ${TEMPLATE_EXPR} ORDER BY date, template`;
456
+ export const LEGACY_WAU_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS visitor_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${DASHBOARD_WAU_BASE_RANGE_FILTER}), days AS (SELECT DISTINCT event_date AS date FROM base WHERE ${DASHBOARD_EVENT_DATE_RANGE_FILTER}) SELECT d.date, b.template, COUNT(DISTINCT b.visitor_key) AS visitors FROM days d JOIN base b ON b.event_date >= to_char(d.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND b.event_date <= d.date GROUP BY d.date, b.template ORDER BY d.date, b.template`;
457
+ const WAU_BY_TEMPLATE_SQL = `WITH base AS (SELECT user_key AS visitor_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${DASHBOARD_WAU_BASE_RANGE_FILTER}), days AS (SELECT DISTINCT event_date AS date FROM base WHERE ${DASHBOARD_EVENT_DATE_RANGE_FILTER}) SELECT d.date, b.template, COUNT(DISTINCT b.visitor_key) AS visitors FROM days d JOIN base b ON b.event_date >= to_char(d.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND b.event_date <= d.date GROUP BY d.date, b.template ORDER BY d.date, b.template`;
442
458
 
443
459
  /**
444
460
  * Catalog entries. Order here is the default panel order when a caller passes
@@ -0,0 +1,3 @@
1
+ export const FIRST_PARTY_ANALYTICS_QUERY_TIMEOUT_MS = 30_000;
2
+ export const DASHBOARD_REPORT_ACTION_TIMEOUT_MS = 38_000;
3
+ export const DASHBOARD_REPORT_READY_TIMEOUT_MS = 45_000;
@@ -35,6 +35,34 @@ Then implement working code that is cohesive, accessible, responsive, and polish
35
35
 
36
36
  Default to Apple/Linear-level restraint: make the primary workflow obvious, then remove everything that does not help that workflow right now. A polished UI often has fewer visible controls, fewer borders, fewer labels, and fewer explanatory surfaces than the first reasonable implementation.
37
37
 
38
+ ### Progressive Disclosure Is A Design Requirement
39
+
40
+ Treat an all-at-once interface as a defect to fix during design, not as a
41
+ styling preference. Before coding, inventory every piece of content and action
42
+ on the surface, then assign each one to the smallest useful visibility level:
43
+
44
+ 1. **Immediate** — the page title, current state, one primary action, and the
45
+ compact context needed to choose what to do next.
46
+ 2. **Expanded** — the details needed for the selected item or active workflow.
47
+ 3. **On demand** — advanced settings, diagnostics, credentials, metadata,
48
+ destructive actions, documentation, and rarely used tools.
49
+
50
+ Use single-select accordions or simple disclosure rows for sibling panels when
51
+ the user is choosing one item at a time. Inside an expanded panel, keep another
52
+ layer for independent concerns instead of dumping every form, explanation, and
53
+ secondary action into the first reveal. Prefer one flat panel with alignment,
54
+ dividers, and whitespace over nested cards; provider or product icons should
55
+ not receive decorative borders or containers unless the container communicates
56
+ state or interaction. A collapsed row should still show the item name, status,
57
+ and a concise summary so the user can scan the whole surface without opening
58
+ everything.
59
+
60
+ Before shipping, ask: “What can disappear until the user asks for it?” Then
61
+ verify collapsed, expanded, loading, empty, error, and narrow-width states.
62
+ If the first viewport contains multiple forms, repeated explanatory copy,
63
+ documentation links, and controls for unrelated tasks, the surface has not
64
+ passed this requirement yet.
65
+
38
66
  - **Start by subtracting**: Before adding a visible control, banner, toolbar row, card, or explanatory block, ask what can be removed, merged, renamed, or moved into an existing affordance.
39
67
  - **One primary action**: Each surface should have one dominant next action. Secondary actions belong in menus, popovers, command palettes, disclosure rows, or contextual hover/focus states unless they are used constantly.
40
68
  - **Progressively disclose rare work**: Advanced options, diagnostics, metadata, settings, import/export, destructive actions, and inspection tools should stay tucked away until requested. Prefer small icon triggers with tooltips, popovers, drawers, or detail panels over permanent chrome.
@@ -68,10 +68,9 @@ export function Layout({ children }: LayoutProps) {
68
68
  useAgentChatHomeHandoffLinks({
69
69
  storageKey: ASSETS_CHAT_STORAGE_KEY,
70
70
  isChatPath: (pathname) => pathname === "/" || pathname.startsWith("/chat/"),
71
- // A direct /chat/:id link already names the thread and must preserve the
72
- // normal chat-to-sidebar transition. Only the empty home chat needs the
73
- // recent marker that proves an agent handoff is in flight.
74
- requireActiveHandoff: false,
71
+ // Only preserve the transition when chat activity has recorded an active
72
+ // handoff; an empty home chat should keep the destination sidebar closed.
73
+ requireActiveHandoff: true,
75
74
  });
76
75
 
77
76
  useEffect(() => {
@@ -240,7 +240,7 @@ function AssetsChatsSection() {
240
240
  }
241
241
 
242
242
  return (
243
- <div className="mt-2 border-s border-border/70 ps-3">
243
+ <div className="mt-2 ms-4">
244
244
  <ChatHistoryRail
245
245
  items={chatItems}
246
246
  activeId={displayedActiveThreadId}
@@ -92,11 +92,6 @@
92
92
  background: hsl(var(--muted-foreground) / 0.4);
93
93
  }
94
94
 
95
- .assets-create-chat-panel [data-agent-empty-state="centered"] {
96
- justify-content: center;
97
- padding-bottom: clamp(4rem, 12vh, 8rem);
98
- }
99
-
100
95
  .assets-create-chat-intro {
101
96
  display: none;
102
97
  }