@agent-native/core 0.120.2 → 0.120.4

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 (278) hide show
  1. package/corpus/README.md +3 -3
  2. package/corpus/core/CHANGELOG.md +28 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/context-xray-local.ts +1 -1
  5. package/corpus/core/src/cli/index.ts +63 -18
  6. package/corpus/core/src/cli/skills.ts +5 -0
  7. package/corpus/core/src/cli/telemetry.ts +118 -1
  8. package/corpus/core/src/client/AgentPanel.tsx +10 -0
  9. package/corpus/core/src/client/CommandMenu.tsx +13 -1
  10. package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +39 -4
  11. package/corpus/core/src/client/analytics.ts +13 -0
  12. package/corpus/core/src/client/error-capture.ts +6 -1
  13. package/corpus/core/src/client/extensions/ExtensionsListPage.tsx +12 -5
  14. package/corpus/core/src/client/extensions/agent-native-extension-runtime.ts +1 -1
  15. package/corpus/core/src/client/navigation/index.ts +2 -0
  16. package/corpus/core/src/client/org/OrgSwitcher.tsx +14 -32
  17. package/corpus/core/src/client/settings/ExtensionsSettingsContent.tsx +1 -1
  18. package/corpus/core/src/extensions/theme.ts +1 -1
  19. package/corpus/core/src/localization/default-messages.ts +2 -0
  20. package/corpus/core/src/server/core-routes-plugin.ts +10 -1
  21. package/corpus/core/src/server/framework-request-handler.ts +17 -26
  22. package/corpus/core/src/styles/agent-native.css +13 -0
  23. package/corpus/core/src/templates/chat/app/components/layout/Layout.tsx +3 -0
  24. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +78 -45
  25. package/corpus/core/src/templates/chat/app/global.css +1 -1
  26. package/corpus/core/src/templates/default/app/global.css +1 -1
  27. package/corpus/core/src/tracking/error-capture.ts +151 -0
  28. package/corpus/core/src/tracking/index.ts +5 -0
  29. package/corpus/templates/analytics/app/components/dashboard/CumulativeNetChart.tsx +1 -1
  30. package/corpus/templates/analytics/app/components/dashboard/DashboardHistoryPanel.tsx +1 -4
  31. package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +1 -1
  32. package/corpus/templates/analytics/app/components/dashboard/DataTable.tsx +1 -1
  33. package/corpus/templates/analytics/app/components/dashboard/DateRangePicker.tsx +2 -2
  34. package/corpus/templates/analytics/app/components/dashboard/MetricCard.tsx +1 -1
  35. package/corpus/templates/analytics/app/components/dashboard/RecentActivity.tsx +1 -1
  36. package/corpus/templates/analytics/app/components/dashboard/RevenueChart.tsx +1 -1
  37. package/corpus/templates/analytics/app/components/dashboard/RevenueComparisonChart.tsx +1 -1
  38. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +41 -9
  39. package/corpus/templates/analytics/app/components/dashboard/StatsCard.tsx +1 -1
  40. package/corpus/templates/analytics/app/components/dashboard/TimeSeriesChart.tsx +1 -1
  41. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +2 -0
  42. package/corpus/templates/analytics/app/components/layout/Layout.tsx +28 -14
  43. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +152 -124
  44. package/corpus/templates/analytics/app/global.css +5 -5
  45. package/corpus/templates/analytics/app/lib/dashboard-report-capture.ts +42 -0
  46. package/corpus/templates/analytics/app/pages/adhoc/_shared/KpiChart.tsx +1 -1
  47. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/ChartTypePicker.tsx +1 -1
  48. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/DateRangePicker.tsx +1 -1
  49. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/EventRow.tsx +1 -1
  50. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/SqlPreview.tsx +2 -2
  51. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +3 -3
  52. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +2 -2
  53. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +1 -1
  54. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +1 -1
  55. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +162 -51
  56. package/corpus/templates/analytics/app/root.tsx +28 -0
  57. package/corpus/templates/analytics/changelog/2026-07-24-analytics-charts-now-explain-slow-loads-and-offer-a-direct-r.md +6 -0
  58. package/corpus/templates/analytics/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  59. package/corpus/templates/analytics/changelog/2026-07-24-daily-dashboard-emails-now-load-signed-reports-more-reliably.md +6 -0
  60. package/corpus/templates/analytics/changelog/2026-07-24-manage-agent-is-clearly-marked-as-a-link-to-its-dedicated-pa.md +6 -0
  61. package/corpus/templates/analytics/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  62. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +1 -1
  63. package/corpus/templates/analytics/server/db/schema.ts +5 -0
  64. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +21 -0
  65. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +11 -0
  66. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +54 -0
  67. package/corpus/templates/analytics/server/lib/dashboard-report.ts +235 -26
  68. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +4 -1
  69. package/corpus/templates/analytics/server/plugins/db.ts +9 -0
  70. package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +1 -1
  71. package/corpus/templates/assets/app/components/asset/AssetPreviewDialog.tsx +376 -0
  72. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +14 -4
  73. package/corpus/templates/assets/app/components/layout/Layout.tsx +5 -0
  74. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +118 -74
  75. package/corpus/templates/assets/app/global.css +1 -1
  76. package/corpus/templates/assets/app/i18n-data.ts +10 -0
  77. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +99 -10
  78. package/corpus/templates/assets/app/routes/library.tsx +21 -198
  79. package/corpus/templates/assets/changelog/2026-07-21-unified-asset-preview-click-any-asset-to-open-one-preview-wi.md +6 -0
  80. package/corpus/templates/assets/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  81. package/corpus/templates/assets/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  82. package/corpus/templates/brain/app/components/layout/Layout.tsx +3 -0
  83. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +76 -43
  84. package/corpus/templates/brain/app/global.css +1 -1
  85. package/corpus/templates/brain/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  86. package/corpus/templates/brain/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  87. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +67 -52
  88. package/corpus/templates/calendar/app/global.css +1 -1
  89. package/corpus/templates/calendar/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  90. package/corpus/templates/calendar/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  91. package/corpus/templates/chat/app/components/layout/Layout.tsx +3 -0
  92. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +78 -45
  93. package/corpus/templates/chat/app/global.css +1 -1
  94. package/corpus/templates/chat/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  95. package/corpus/templates/chat/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  96. package/corpus/templates/clips/app/components/library/library-layout.tsx +65 -31
  97. package/corpus/templates/clips/app/components/library/organization-switcher.tsx +2 -2
  98. package/corpus/templates/clips/app/components/player/video-player.tsx +16 -1
  99. package/corpus/templates/clips/app/global.css +1 -1
  100. package/corpus/templates/clips/app/hooks/use-view-tracking.ts +55 -12
  101. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +6 -5
  102. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +7 -7
  103. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -5
  104. package/corpus/templates/clips/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  105. package/corpus/templates/clips/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  106. package/corpus/templates/clips/server/plugins/db.ts +113 -68
  107. package/corpus/templates/content/actions/update-document.ts +145 -91
  108. package/corpus/templates/content/app/components/editor/CommentComposer.tsx +4 -1
  109. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +366 -164
  110. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +51 -28
  111. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +19 -8
  112. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +66 -42
  113. package/corpus/templates/content/app/global.css +1 -1
  114. package/corpus/templates/content/app/i18n-data.ts +15 -0
  115. package/corpus/templates/content/changelog/2026-07-23-shared-pages-now-keep-durable-content-visible-and-comment-th.md +6 -0
  116. package/corpus/templates/content/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  117. package/corpus/templates/content/changelog/2026-07-24-comment-drafts-now-stay-open-when-saving-fails-and-long-live.md +6 -0
  118. package/corpus/templates/content/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  119. package/corpus/templates/design/actions/update-file.ts +85 -18
  120. package/corpus/templates/design/app/components/layout/Sidebar.tsx +65 -30
  121. package/corpus/templates/design/app/entry.client.tsx +14 -1
  122. package/corpus/templates/design/app/global.css +1 -1
  123. package/corpus/templates/design/app/lib/design-save-outbox.ts +39 -0
  124. package/corpus/templates/design/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  125. package/corpus/templates/design/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  126. package/corpus/templates/dispatch/app/global.css +1 -1
  127. package/corpus/templates/dispatch/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  128. package/corpus/templates/dispatch/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  129. package/corpus/templates/forms/app/components/layout/Layout.tsx +3 -0
  130. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +72 -37
  131. package/corpus/templates/forms/app/global.css +1 -1
  132. package/corpus/templates/forms/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  133. package/corpus/templates/forms/changelog/2026-07-24-forms-sidebar-actions-now-keep-search-language-feedback-and-.md +6 -0
  134. package/corpus/templates/forms/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  135. package/corpus/templates/macros/app/components/DailyProgress.tsx +5 -5
  136. package/corpus/templates/macros/app/components/ExerciseCard.tsx +2 -2
  137. package/corpus/templates/macros/app/components/MealCard.tsx +2 -2
  138. package/corpus/templates/macros/app/components/WeightCard.tsx +2 -2
  139. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +56 -29
  140. package/corpus/templates/macros/app/components/ui/button.tsx +1 -2
  141. package/corpus/templates/macros/app/components/ui/card.tsx +1 -1
  142. package/corpus/templates/macros/app/global.css +1 -1
  143. package/corpus/templates/macros/app/routes/_index.tsx +1 -1
  144. package/corpus/templates/macros/app/routes/analytics.tsx +4 -7
  145. package/corpus/templates/macros/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  146. package/corpus/templates/macros/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  147. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +125 -37
  148. package/corpus/templates/mail/app/global.css +1 -1
  149. package/corpus/templates/mail/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  150. package/corpus/templates/mail/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  151. package/corpus/templates/plan/app/components/layout/Layout.tsx +3 -0
  152. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +90 -60
  153. package/corpus/templates/plan/app/global.css +1 -1
  154. package/corpus/templates/plan/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  155. package/corpus/templates/plan/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  156. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +69 -32
  157. package/corpus/templates/slides/app/global.css +1 -1
  158. package/corpus/templates/slides/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  159. package/corpus/templates/slides/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  160. package/corpus/templates/tasks/app/global.css +1 -1
  161. package/corpus/templates/tasks/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  162. package/corpus/toolkit/CHANGELOG.md +14 -0
  163. package/corpus/toolkit/README.md +8 -1
  164. package/corpus/toolkit/agent-native.eject.json +1 -1
  165. package/corpus/toolkit/package.json +1 -1
  166. package/corpus/toolkit/src/app-shell/index.ts +1 -0
  167. package/corpus/toolkit/src/app-shell/sidebar-footer-actions.tsx +48 -0
  168. package/corpus/toolkit/src/chat-history.css +52 -1
  169. package/corpus/toolkit/src/design-system/default-adapter.tsx +1 -1
  170. package/corpus/toolkit/src/editor/useCollabReconcile.ts +83 -15
  171. package/corpus/toolkit/src/ui/button.tsx +1 -2
  172. package/corpus/toolkit/src/ui/card.tsx +1 -1
  173. package/corpus/toolkit/src/ui/table.tsx +4 -11
  174. package/dist/cli/context-xray-local.js +1 -1
  175. package/dist/cli/context-xray-local.js.map +1 -1
  176. package/dist/cli/index.js +56 -17
  177. package/dist/cli/index.js.map +1 -1
  178. package/dist/cli/skills.d.ts.map +1 -1
  179. package/dist/cli/skills.js +5 -0
  180. package/dist/cli/skills.js.map +1 -1
  181. package/dist/cli/telemetry.d.ts +7 -0
  182. package/dist/cli/telemetry.d.ts.map +1 -1
  183. package/dist/cli/telemetry.js +91 -1
  184. package/dist/cli/telemetry.js.map +1 -1
  185. package/dist/client/AgentPanel.d.ts +7 -1
  186. package/dist/client/AgentPanel.d.ts.map +1 -1
  187. package/dist/client/AgentPanel.js +2 -2
  188. package/dist/client/AgentPanel.js.map +1 -1
  189. package/dist/client/CommandMenu.d.ts +2 -0
  190. package/dist/client/CommandMenu.d.ts.map +1 -1
  191. package/dist/client/CommandMenu.js +12 -1
  192. package/dist/client/CommandMenu.js.map +1 -1
  193. package/dist/client/agent-page/AgentTabsPage.d.ts +4 -0
  194. package/dist/client/agent-page/AgentTabsPage.d.ts.map +1 -1
  195. package/dist/client/agent-page/AgentTabsPage.js +8 -4
  196. package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
  197. package/dist/client/analytics.d.ts.map +1 -1
  198. package/dist/client/analytics.js +7 -0
  199. package/dist/client/analytics.js.map +1 -1
  200. package/dist/client/error-capture.d.ts.map +1 -1
  201. package/dist/client/error-capture.js +8 -1
  202. package/dist/client/error-capture.js.map +1 -1
  203. package/dist/client/extensions/ExtensionsListPage.d.ts.map +1 -1
  204. package/dist/client/extensions/ExtensionsListPage.js +2 -2
  205. package/dist/client/extensions/ExtensionsListPage.js.map +1 -1
  206. package/dist/client/extensions/agent-native-extension-runtime.js +1 -1
  207. package/dist/client/extensions/agent-native-extension-runtime.js.map +1 -1
  208. package/dist/client/navigation/index.d.ts +1 -1
  209. package/dist/client/navigation/index.d.ts.map +1 -1
  210. package/dist/client/navigation/index.js +1 -1
  211. package/dist/client/navigation/index.js.map +1 -1
  212. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  213. package/dist/client/org/OrgSwitcher.js +9 -13
  214. package/dist/client/org/OrgSwitcher.js.map +1 -1
  215. package/dist/client/settings/ExtensionsSettingsContent.js +1 -1
  216. package/dist/client/settings/ExtensionsSettingsContent.js.map +1 -1
  217. package/dist/collab/awareness.d.ts +2 -2
  218. package/dist/collab/awareness.d.ts.map +1 -1
  219. package/dist/collab/struct-routes.d.ts +1 -1
  220. package/dist/extensions/theme.js +1 -1
  221. package/dist/extensions/theme.js.map +1 -1
  222. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  223. package/dist/localization/default-messages.d.ts +2 -0
  224. package/dist/localization/default-messages.d.ts.map +1 -1
  225. package/dist/localization/default-messages.js +2 -0
  226. package/dist/localization/default-messages.js.map +1 -1
  227. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  228. package/dist/secrets/routes.d.ts +9 -9
  229. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  230. package/dist/server/core-routes-plugin.js +8 -1
  231. package/dist/server/core-routes-plugin.js.map +1 -1
  232. package/dist/server/framework-request-handler.d.ts.map +1 -1
  233. package/dist/server/framework-request-handler.js +17 -27
  234. package/dist/server/framework-request-handler.js.map +1 -1
  235. package/dist/server/realtime-token.d.ts +1 -1
  236. package/dist/styles/agent-native.css +13 -0
  237. package/dist/templates/chat/app/components/layout/Layout.tsx +3 -0
  238. package/dist/templates/chat/app/components/layout/Sidebar.tsx +78 -45
  239. package/dist/templates/chat/app/global.css +1 -1
  240. package/dist/templates/chat/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  241. package/dist/templates/chat/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  242. package/dist/templates/default/app/global.css +1 -1
  243. package/dist/tracking/error-capture.d.ts +14 -0
  244. package/dist/tracking/error-capture.d.ts.map +1 -0
  245. package/dist/tracking/error-capture.js +113 -0
  246. package/dist/tracking/error-capture.js.map +1 -0
  247. package/dist/tracking/index.d.ts +1 -0
  248. package/dist/tracking/index.d.ts.map +1 -1
  249. package/dist/tracking/index.js +1 -0
  250. package/dist/tracking/index.js.map +1 -1
  251. package/package.json +2 -2
  252. package/src/cli/context-xray-local.ts +1 -1
  253. package/src/cli/index.ts +63 -18
  254. package/src/cli/skills.ts +5 -0
  255. package/src/cli/telemetry.ts +118 -1
  256. package/src/client/AgentPanel.tsx +10 -0
  257. package/src/client/CommandMenu.tsx +13 -1
  258. package/src/client/agent-page/AgentTabsPage.tsx +39 -4
  259. package/src/client/analytics.ts +13 -0
  260. package/src/client/error-capture.ts +6 -1
  261. package/src/client/extensions/ExtensionsListPage.tsx +12 -5
  262. package/src/client/extensions/agent-native-extension-runtime.ts +1 -1
  263. package/src/client/navigation/index.ts +2 -0
  264. package/src/client/org/OrgSwitcher.tsx +14 -32
  265. package/src/client/settings/ExtensionsSettingsContent.tsx +1 -1
  266. package/src/extensions/theme.ts +1 -1
  267. package/src/localization/default-messages.ts +2 -0
  268. package/src/server/core-routes-plugin.ts +10 -1
  269. package/src/server/framework-request-handler.ts +17 -26
  270. package/src/styles/agent-native.css +13 -0
  271. package/src/templates/chat/app/components/layout/Layout.tsx +3 -0
  272. package/src/templates/chat/app/components/layout/Sidebar.tsx +78 -45
  273. package/src/templates/chat/app/global.css +1 -1
  274. package/src/templates/chat/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  275. package/src/templates/chat/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  276. package/src/templates/default/app/global.css +1 -1
  277. package/src/tracking/error-capture.ts +151 -0
  278. package/src/tracking/index.ts +5 -0
@@ -22,7 +22,7 @@
22
22
  --muted-foreground: 0 0% 45%;
23
23
  --accent: 0 0% 95%;
24
24
  --accent-foreground: 0 0% 15%;
25
- --destructive: 0 84% 60%;
25
+ --destructive: 0 91% 71%;
26
26
  --destructive-foreground: 0 0% 98%;
27
27
  --border: 0 0% 90%;
28
28
  --input: 0 0% 90%;
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-24
4
+ ---
5
+
6
+ Secondary controls and dashboard surfaces now use quieter borderless styling.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-24
4
+ ---
5
+
6
+ Sidebar utility controls now follow a consistent footer order.
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AgentSidebar,
3
3
  focusAgentChat,
4
+ isAgentChatHomeHandoffActive,
4
5
  navigateWithAgentChatViewTransition,
5
6
  useAgentChatHomeHandoff,
6
7
  useAgentChatHomeHandoffLinks,
@@ -57,6 +58,7 @@ export function Layout({ children }: LayoutProps) {
57
58
  activePath: location.pathname,
58
59
  enabled: !isChatRoute,
59
60
  });
61
+ const chatHomeHandoffPending = isAgentChatHomeHandoffActive("chat");
60
62
  useAgentChatHomeHandoffLinks({
61
63
  storageKey: "chat",
62
64
  isChatPath: (pathname) => pathname === "/" || pathname.startsWith("/chat/"),
@@ -164,6 +166,7 @@ export function Layout({ children }: LayoutProps) {
164
166
  <AgentSidebar
165
167
  position="right"
166
168
  chatViewTransition
169
+ chatViewTransitionHandoff={chatHomeHandoffPending}
167
170
  storageKey="chat"
168
171
  browserTabId={TAB_ID}
169
172
  openOnChatRunning={chatHomeHandoffActive}
@@ -4,9 +4,11 @@ import {
4
4
  type ChatThreadSummary,
5
5
  } from "@agent-native/core/client/agent-chat";
6
6
  import { appPath } from "@agent-native/core/client/api-path";
7
- import { useT } from "@agent-native/core/client/i18n";
7
+ import { LanguagePicker, useT } from "@agent-native/core/client/i18n";
8
+ import { openCommandMenu } from "@agent-native/core/client/navigation";
8
9
  import { OrgSwitcher } from "@agent-native/core/client/org";
9
10
  import { FeedbackButton } from "@agent-native/core/client/ui";
11
+ import { SidebarFooterActions } from "@agent-native/toolkit/app-shell";
10
12
  import {
11
13
  ChatHistoryRail,
12
14
  type ChatHistoryItem,
@@ -16,6 +18,7 @@ import {
16
18
  IconLayoutSidebarLeftCollapse,
17
19
  IconLayoutSidebarLeftExpand,
18
20
  IconMessageCircle,
21
+ IconSearch,
19
22
  IconSettings,
20
23
  } from "@tabler/icons-react";
21
24
  import { useEffect, useMemo } from "react";
@@ -126,7 +129,7 @@ function chatThreadPath(threadId: string) {
126
129
  return `/chat/${encodeURIComponent(threadId)}`;
127
130
  }
128
131
 
129
- function ChatThreadsSection() {
132
+ function ChatThreadsSection({ open }: { open: boolean }) {
130
133
  const navigate = useNavigate();
131
134
  const location = useLocation();
132
135
  const t = useT();
@@ -230,36 +233,42 @@ function ChatThreadsSection() {
230
233
  }
231
234
 
232
235
  return (
233
- <div className="mt-2 ms-4">
234
- <ChatHistoryRail
235
- items={chatItems}
236
- activeId={displayedActiveThreadId}
237
- onSelect={(threadId) => openThread(threadId)}
238
- onNewChat={() => void handleNewChat()}
239
- railLabels={{
240
- newChat: t("chat.newChat"),
241
- showMore: t("chat.chats"),
242
- showLess: t("chat.chats"),
243
- }}
244
- renameMaxLength={160}
245
- onTogglePin={(threadId) => {
246
- const thread = visibleThreads.find((item) => item.id === threadId);
247
- if (thread) void pinThread(threadId, !thread.pinnedAt);
248
- }}
249
- onRename={handleRenameThread}
250
- onDelete={(threadId) => void handleArchiveThread(threadId)}
251
- labels={{
252
- options: (item) =>
253
- t("chat.optionsFor", { title: item.titleText ?? "" }),
254
- renameInput: (item) =>
255
- t("chat.renameThread", { title: item.titleText ?? "" }),
256
- rename: t("chat.renameChat"),
257
- pin: t("chat.pinChat"),
258
- unpin: t("chat.unpinChat"),
259
- delete: t("chat.archiveChat"),
260
- }}
261
- className="min-w-0"
262
- />
236
+ <div
237
+ className="an-chat-history-rail__collapse"
238
+ data-state={open ? "open" : "closed"}
239
+ aria-hidden={!open}
240
+ >
241
+ <div className="ms-4">
242
+ <ChatHistoryRail
243
+ items={chatItems}
244
+ activeId={displayedActiveThreadId}
245
+ onSelect={(threadId) => openThread(threadId)}
246
+ onNewChat={() => void handleNewChat()}
247
+ railLabels={{
248
+ newChat: t("chat.newChat"),
249
+ showMore: t("chat.chats"),
250
+ showLess: t("chat.chats"),
251
+ }}
252
+ renameMaxLength={160}
253
+ onTogglePin={(threadId) => {
254
+ const thread = visibleThreads.find((item) => item.id === threadId);
255
+ if (thread) void pinThread(threadId, !thread.pinnedAt);
256
+ }}
257
+ onRename={handleRenameThread}
258
+ onDelete={(threadId) => void handleArchiveThread(threadId)}
259
+ labels={{
260
+ options: (item) =>
261
+ t("chat.optionsFor", { title: item.titleText ?? "" }),
262
+ renameInput: (item) =>
263
+ t("chat.renameThread", { title: item.titleText ?? "" }),
264
+ rename: t("chat.renameChat"),
265
+ pin: t("chat.pinChat"),
266
+ unpin: t("chat.unpinChat"),
267
+ delete: t("chat.archiveChat"),
268
+ }}
269
+ className="min-w-0"
270
+ />
271
+ </div>
263
272
  </div>
264
273
  );
265
274
  }
@@ -281,14 +290,14 @@ export function Sidebar({
281
290
  cn(
282
291
  "flex items-center text-sm transition-colors",
283
292
  collapsed
284
- ? "relative h-10 w-full justify-center rounded-none border-s-2 px-0"
293
+ ? "relative h-10 w-full justify-center rounded-none px-0"
285
294
  : "h-9 rounded-md gap-3 px-3",
286
295
  isActive
287
296
  ? collapsed
288
- ? "border-s-sidebar-accent-foreground/80 bg-sidebar-accent text-sidebar-accent-foreground"
297
+ ? "bg-sidebar-accent text-sidebar-accent-foreground"
289
298
  : "bg-sidebar-accent text-sidebar-accent-foreground"
290
299
  : collapsed
291
- ? "border-s-transparent text-sidebar-foreground/70 hover:bg-sidebar-accent/55 hover:text-sidebar-accent-foreground"
300
+ ? "text-sidebar-foreground/70 hover:bg-sidebar-accent/55 hover:text-sidebar-accent-foreground"
292
301
  : "text-sidebar-foreground hover:bg-sidebar-accent/65 hover:text-sidebar-accent-foreground",
293
302
  );
294
303
  const collapseButton = collapsible ? (
@@ -317,6 +326,31 @@ export function Sidebar({
317
326
  </TooltipContent>
318
327
  </Tooltip>
319
328
  ) : null;
329
+ const searchButton = (
330
+ <Tooltip>
331
+ <TooltipTrigger asChild>
332
+ <button
333
+ type="button"
334
+ onClick={openCommandMenu}
335
+ aria-label={t("root.commandSearch")}
336
+ className="flex size-8 items-center justify-center rounded-md text-sidebar-foreground/65 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
337
+ >
338
+ <IconSearch className="size-4" />
339
+ </button>
340
+ </TooltipTrigger>
341
+ <TooltipContent side="right">{t("root.commandSearch")}</TooltipContent>
342
+ </Tooltip>
343
+ );
344
+ const translateButton = (
345
+ <LanguagePicker variant="ghost-icon" label={t("settings.languageLabel")} />
346
+ );
347
+ const feedbackButton = (
348
+ <FeedbackButton
349
+ variant={collapsed ? "icon" : "sidebar"}
350
+ side="right"
351
+ className={collapsed ? "h-8 w-8" : "min-w-0"}
352
+ />
353
+ );
320
354
 
321
355
  return (
322
356
  <aside
@@ -411,8 +445,8 @@ export function Sidebar({
411
445
  ) : (
412
446
  link
413
447
  )}
414
- {!collapsed && item.view === "chat" && isChatRoute ? (
415
- <ChatThreadsSection />
448
+ {!collapsed && item.view === "chat" ? (
449
+ <ChatThreadsSection open={isChatRoute} />
416
450
  ) : null}
417
451
  </div>
418
452
  );
@@ -465,14 +499,13 @@ export function Sidebar({
465
499
  />
466
500
  </div>
467
501
 
468
- {!collapsed ? (
469
- <div className="flex items-center justify-end gap-1 px-3 py-2">
470
- <FeedbackButton className="min-w-0 flex-1" side="right" />
471
- {collapseButton}
472
- </div>
473
- ) : collapseButton ? (
474
- <div className="flex justify-center px-1 py-1">{collapseButton}</div>
475
- ) : null}
502
+ <SidebarFooterActions
503
+ collapsed={collapsed}
504
+ feedback={feedbackButton}
505
+ translate={translateButton}
506
+ search={searchButton}
507
+ collapse={collapseButton}
508
+ />
476
509
  </div>
477
510
  </aside>
478
511
  );
@@ -22,7 +22,7 @@
22
22
  --muted-foreground: 0 0% 45%;
23
23
  --accent: 0 0% 95%;
24
24
  --accent-foreground: 0 0% 15%;
25
- --destructive: 0 84% 60%;
25
+ --destructive: 0 91% 71%;
26
26
  --destructive-foreground: 0 0% 98%;
27
27
  --border: 0 0% 90%;
28
28
  --input: 0 0% 90%;
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-24
4
+ ---
5
+
6
+ Secondary controls and dashboard surfaces now use quieter borderless styling.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-24
4
+ ---
5
+
6
+ Sidebar utility controls now follow a consistent footer order.
@@ -5,13 +5,15 @@ import {
5
5
  import { appPath } from "@agent-native/core/client/api-path";
6
6
  import { DevDatabaseLink } from "@agent-native/core/client/db-admin";
7
7
  import { getBrowserTabId } from "@agent-native/core/client/hooks";
8
- import { useT } from "@agent-native/core/client/i18n";
8
+ import { LanguagePicker, useT } from "@agent-native/core/client/i18n";
9
+ import { openCommandMenu } from "@agent-native/core/client/navigation";
9
10
  import {
10
11
  InvitationBanner,
11
12
  OrgSwitcher,
12
13
  useOrgRole,
13
14
  } from "@agent-native/core/client/org";
14
15
  import { FeedbackButton } from "@agent-native/core/client/ui";
16
+ import { SidebarFooterActions } from "@agent-native/toolkit/app-shell";
15
17
  import {
16
18
  IconInbox,
17
19
  IconArchive,
@@ -30,6 +32,7 @@ import {
30
32
  IconShare,
31
33
  IconHierarchy2,
32
34
  IconSettings,
35
+ IconSearch,
33
36
  } from "@tabler/icons-react";
34
37
  import { ReactNode, useEffect, useMemo, useState } from "react";
35
38
  import { NavLink, useLocation, useParams } from "react-router";
@@ -154,6 +157,59 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
154
157
  );
155
158
  const [headerSlot, setHeaderSlot] = useState<HTMLElement | null>(null);
156
159
  const showCollapsedSidebar = sidebarCollapsed && !isMobile;
160
+
161
+ const collapseButton = !isMobile ? (
162
+ <Tooltip>
163
+ <TooltipTrigger asChild>
164
+ <button
165
+ type="button"
166
+ aria-label={
167
+ showCollapsedSidebar
168
+ ? t("navigation.expandSidebar")
169
+ : t("navigation.collapseSidebar")
170
+ }
171
+ className="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent/60 hover:text-foreground"
172
+ onClick={() => setSidebarCollapsed((value) => !value)}
173
+ >
174
+ {showCollapsedSidebar ? (
175
+ <IconLayoutSidebarLeftExpand className="h-4 w-4" />
176
+ ) : (
177
+ <IconLayoutSidebarLeftCollapse className="h-4 w-4" />
178
+ )}
179
+ </button>
180
+ </TooltipTrigger>
181
+ <TooltipContent side="right">
182
+ {showCollapsedSidebar
183
+ ? t("navigation.expandSidebar")
184
+ : t("navigation.collapseSidebar")}
185
+ </TooltipContent>
186
+ </Tooltip>
187
+ ) : null;
188
+ const searchButton = (
189
+ <Tooltip>
190
+ <TooltipTrigger asChild>
191
+ <button
192
+ type="button"
193
+ aria-label={t("root.commandSearch")}
194
+ className="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent/60 hover:text-foreground"
195
+ onClick={openCommandMenu}
196
+ >
197
+ <IconSearch className="h-4 w-4" />
198
+ </button>
199
+ </TooltipTrigger>
200
+ <TooltipContent side="right">{t("root.commandSearch")}</TooltipContent>
201
+ </Tooltip>
202
+ );
203
+ const translateButton = (
204
+ <LanguagePicker variant="ghost-icon" label={t("settings.languageLabel")} />
205
+ );
206
+ const feedbackButton = (
207
+ <FeedbackButton
208
+ variant={showCollapsedSidebar ? "icon" : "sidebar"}
209
+ side="right"
210
+ className={showCollapsedSidebar ? "size-8" : "min-w-0"}
211
+ />
212
+ );
157
213
  const sidebarHasNewRecordingAction = isMobile
158
214
  ? sidebarOpen
159
215
  : !sidebarCollapsed;
@@ -314,35 +370,6 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
314
370
  </>
315
371
  )}
316
372
  </div>
317
-
318
- <Tooltip>
319
- <TooltipTrigger asChild>
320
- <Button
321
- type="button"
322
- variant="ghost"
323
- size="icon"
324
- className="hidden h-7 w-7 shrink-0 text-muted-foreground hover:text-foreground md:inline-flex"
325
- aria-label={
326
- showCollapsedSidebar
327
- ? t("navigation.expandSidebar")
328
- : t("navigation.collapseSidebar")
329
- }
330
- aria-expanded={!showCollapsedSidebar}
331
- onClick={() => setSidebarCollapsed((value) => !value)}
332
- >
333
- {showCollapsedSidebar ? (
334
- <IconLayoutSidebarLeftExpand className="h-4 w-4" />
335
- ) : (
336
- <IconLayoutSidebarLeftCollapse className="h-4 w-4" />
337
- )}
338
- </Button>
339
- </TooltipTrigger>
340
- <TooltipContent side="right">
341
- {showCollapsedSidebar
342
- ? t("navigation.expandSidebar")
343
- : t("navigation.collapseSidebar")}
344
- </TooltipContent>
345
- </Tooltip>
346
373
  </div>
347
374
  <div className="min-h-0 flex-1 overflow-y-auto">
348
375
  {showCollapsedSidebar ? (
@@ -592,10 +619,17 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
592
619
  <div className="shrink-0 space-y-2 px-3 py-2">
593
620
  <OrgSwitcher settingsPath="/settings/organization" />
594
621
  <DevDatabaseLink />
595
- <FeedbackButton />
596
622
  </div>
597
623
  </>
598
624
  )}
625
+ <SidebarFooterActions
626
+ collapsed={showCollapsedSidebar}
627
+ feedback={feedbackButton}
628
+ translate={translateButton}
629
+ search={searchButton}
630
+ collapse={collapseButton}
631
+ className={showCollapsedSidebar ? undefined : "px-0 py-0"}
632
+ />
599
633
  </aside>
600
634
 
601
635
  <AgentSidebar
@@ -59,8 +59,8 @@ export function OrganizationSwitcher({ className }: OrganizationSwitcherProps) {
59
59
  <button
60
60
  type="button"
61
61
  className={cn(
62
- "flex w-full items-center gap-2 rounded-md border border-border bg-background px-2 py-1.5 text-start",
63
- "hover:bg-accent",
62
+ "flex w-full items-center gap-2 rounded-md border-0 bg-accent/50 px-2 py-1.5 text-start",
63
+ "hover:bg-accent/70",
64
64
  className,
65
65
  )}
66
66
  >
@@ -209,6 +209,13 @@ export interface VideoPlayerProps {
209
209
  * a visible frame for missing or blank auto-generated library thumbnails.
210
210
  */
211
211
  role?: "owner" | "admin" | "editor" | "viewer";
212
+ /**
213
+ * Called with the live `<video>` DOM node whenever it is created or
214
+ * destroyed (e.g. swapping to/from the Loom iframe or unsupported-format
215
+ * placeholder). Lets a caller key an effect off the actual element
216
+ * lifecycle instead of polling an imperative-handle getter.
217
+ */
218
+ onVideoElementChange?: (video: HTMLVideoElement | null) => void;
212
219
  }
213
220
 
214
221
  export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
@@ -245,6 +252,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
245
252
  cover,
246
253
  recordingId,
247
254
  role,
255
+ onVideoElementChange,
248
256
  } = props;
249
257
 
250
258
  const resolvedVideoSrc = useMemo(
@@ -252,6 +260,13 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
252
260
  [videoUrl],
253
261
  );
254
262
  const videoRef = useRef<HTMLVideoElement | null>(null);
263
+ const setVideoNode = useCallback(
264
+ (el: HTMLVideoElement | null) => {
265
+ videoRef.current = el;
266
+ onVideoElementChange?.(el);
267
+ },
268
+ [onVideoElementChange],
269
+ );
255
270
  const containerRef = useRef<HTMLDivElement | null>(null);
256
271
  const idleTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
257
272
  const touchTapCandidateRef = useRef<{
@@ -1262,7 +1277,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
1262
1277
  </div>
1263
1278
  ) : activeVideoSrc ? (
1264
1279
  <video
1265
- ref={videoRef}
1280
+ ref={setVideoNode}
1266
1281
  src={domVideoSrc}
1267
1282
  poster={resolveLocalUrl(thumbnailUrl)}
1268
1283
  // `crossOrigin` is only needed so the owner's canvas thumbnail
@@ -22,7 +22,7 @@
22
22
  --muted-foreground: 0 0% 45%;
23
23
  --accent: 0 0% 95%;
24
24
  --accent-foreground: 0 0% 15%;
25
- --destructive: 0 84% 60%;
25
+ --destructive: 0 91% 71%;
26
26
  --destructive-foreground: 0 0% 98%;
27
27
  --border: 0 0% 90%;
28
28
  --input: 0 0% 90%;
@@ -34,7 +34,14 @@ function createViewSessionId(recordingId: string): string {
34
34
 
35
35
  export interface UseViewTrackingOpts {
36
36
  recordingId: string;
37
- videoRef: React.RefObject<HTMLVideoElement | null>;
37
+ /**
38
+ * The live `<video>` DOM node, or `null` when there is none (e.g. a Loom
39
+ * iframe embed). Pass the actual element — not a ref wrapper — so this
40
+ * hook's effect can depend on it directly and React's own dependency
41
+ * comparison decides when to reattach, instead of hand-rolled identity
42
+ * bookkeeping.
43
+ */
44
+ videoEl: HTMLVideoElement | null;
38
45
  durationMs: number;
39
46
  /** Disable tracking entirely (e.g. for the recording's owner viewing their own clip). */
40
47
  disabled?: boolean;
@@ -46,10 +53,24 @@ export interface UseViewTrackingOpts {
46
53
  * Wires up the view-event tracker for a player instance. Fires a "view-start"
47
54
  * on mount, then throttled "watch-progress" every 5s while playing, plus
48
55
  * seek/pause/resume events and a final flush on unmount.
56
+ *
57
+ * The effect depends on `[recordingId, videoEl, trackOpenWithoutVideo,
58
+ * disabled]`, so React naturally creates a fresh closure — and runs the
59
+ * previous one's cleanup — exactly when any of those actually change (a
60
+ * different video element, a different recording, or the no-video/embed
61
+ * mode flipping). Each closure captures its own `recordingId` and `videoEl`,
62
+ * so a cleanup's final flush always describes the session it belonged to,
63
+ * never a session that has since replaced it.
64
+ *
65
+ * `durationMs` is intentionally excluded from the dependency array — it can
66
+ * load asynchronously after the video/recording is already attached, and
67
+ * reattaching just for that would be wasted work. It's kept in a ref that's
68
+ * synced every render and read fresh inside `post()`.
49
69
  */
50
70
  export function useViewTracking(opts: UseViewTrackingOpts) {
51
- const { recordingId, videoRef, durationMs, disabled, trackOpenWithoutVideo } =
71
+ const { recordingId, videoEl, durationMs, disabled, trackOpenWithoutVideo } =
52
72
  opts;
73
+
53
74
  const watchMsRef = useRef(0);
54
75
  const lastTickRef = useRef<number | null>(null);
55
76
  const startedRef = useRef(false);
@@ -57,11 +78,25 @@ export function useViewTracking(opts: UseViewTrackingOpts) {
57
78
  const lastSentProgressRef = useRef(0);
58
79
  const maxPctRef = useRef(0);
59
80
  const viewSessionRef = useRef<string | null>(null);
81
+ const durationMsRef = useRef(durationMs);
82
+ const recordingIdRef = useRef(recordingId);
83
+
84
+ durationMsRef.current = durationMs;
85
+ recordingIdRef.current = recordingId;
60
86
 
61
87
  useEffect(() => {
62
88
  if (disabled) return;
63
- const video = videoRef.current;
64
- if (!video) {
89
+
90
+ // Reset per-session counters — this effect only reruns when the video
91
+ // element, recording, or embed mode actually change.
92
+ watchMsRef.current = 0;
93
+ lastTickRef.current = null;
94
+ startedRef.current = false;
95
+ lastSentProgressRef.current = 0;
96
+ maxPctRef.current = 0;
97
+ viewSessionRef.current = null;
98
+
99
+ if (!videoEl) {
65
100
  if (
66
101
  !trackOpenWithoutVideo ||
67
102
  !recordingId ||
@@ -69,6 +104,10 @@ export function useViewTracking(opts: UseViewTrackingOpts) {
69
104
  ) {
70
105
  return;
71
106
  }
107
+ // Persists for the hook's lifetime (never reset on cleanup): this is
108
+ // what stops a React StrictMode dev mount->cleanup->remount cycle
109
+ // from double-posting the same iframe-open view-start, since — unlike
110
+ // the with-video path below — there's no native DOM event gating it.
72
111
  openTrackedRecordingRef.current = recordingId;
73
112
  viewSessionRef.current = createViewSessionId(recordingId);
74
113
  fetch(`${appBasePath()}/api/view-event`, {
@@ -90,6 +129,7 @@ export function useViewTracking(opts: UseViewTrackingOpts) {
90
129
  return;
91
130
  }
92
131
 
132
+ const video = videoEl;
93
133
  const sessionId = getSessionId();
94
134
  viewSessionRef.current = createViewSessionId(recordingId);
95
135
  let progressTimer: ReturnType<typeof setInterval> | null = null;
@@ -105,8 +145,7 @@ export function useViewTracking(opts: UseViewTrackingOpts) {
105
145
  | "reaction",
106
146
  extra?: Record<string, unknown>,
107
147
  ) {
108
- const v = videoRef.current;
109
- if (!v) return;
148
+ const durationMs = durationMsRef.current;
110
149
  const completedPct =
111
150
  durationMs > 0 ? (watchMsRef.current / durationMs) * 100 : 0;
112
151
  maxPctRef.current = Math.max(
@@ -120,12 +159,13 @@ export function useViewTracking(opts: UseViewTrackingOpts) {
120
159
  body: JSON.stringify({
121
160
  recordingId,
122
161
  kind,
123
- timestampMs: Math.floor(v.currentTime * 1000),
162
+ timestampMs: Math.floor(video.currentTime * 1000),
124
163
  sessionId,
125
164
  viewSessionId: viewSessionRef.current,
126
165
  totalWatchMs: Math.floor(watchMsRef.current),
127
166
  completedPct: Math.floor(maxPctRef.current),
128
- scrubbedToEnd: v.duration > 0 && v.currentTime >= v.duration - 0.5,
167
+ scrubbedToEnd:
168
+ video.duration > 0 && video.currentTime >= video.duration - 0.5,
129
169
  payload: extra,
130
170
  }),
131
171
  }).catch(() => {});
@@ -186,10 +226,13 @@ export function useViewTracking(opts: UseViewTrackingOpts) {
186
226
  video.removeEventListener("seeked", onSeek);
187
227
  video.removeEventListener("ended", onEnded);
188
228
  if (progressTimer) clearInterval(progressTimer);
189
- // Flush final progress.
229
+ // Flush final progress, still scoped to this closure's own video and
230
+ // recordingId — never one a later render has since moved on to.
190
231
  if (startedRef.current) post("watch-progress");
191
232
  };
192
- }, [recordingId, videoRef, durationMs, disabled, trackOpenWithoutVideo]);
233
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- durationMs is
234
+ // deliberately excluded; it's read live from durationMsRef inside post().
235
+ }, [recordingId, videoEl, trackOpenWithoutVideo, disabled]);
193
236
 
194
237
  return {
195
238
  reportCtaClick: () => {
@@ -198,7 +241,7 @@ export function useViewTracking(opts: UseViewTrackingOpts) {
198
241
  keepalive: true,
199
242
  headers: { "Content-Type": "application/json" },
200
243
  body: JSON.stringify({
201
- recordingId,
244
+ recordingId: recordingIdRef.current,
202
245
  kind: "cta-click",
203
246
  sessionId: getSessionId(),
204
247
  }),
@@ -210,7 +253,7 @@ export function useViewTracking(opts: UseViewTrackingOpts) {
210
253
  keepalive: true,
211
254
  headers: { "Content-Type": "application/json" },
212
255
  body: JSON.stringify({
213
- recordingId,
256
+ recordingId: recordingIdRef.current,
214
257
  kind: "reaction",
215
258
  sessionId: getSessionId(),
216
259
  payload: { emoji },
@@ -131,13 +131,13 @@ export default function EmbedRoute() {
131
131
  const firstCta = ctas[0] ?? null;
132
132
  const isLoomEmbedBacked = isLoomEmbedBackedRecording(recording);
133
133
 
134
+ const [trackedVideoEl, setTrackedVideoEl] = useState<HTMLVideoElement | null>(
135
+ null,
136
+ );
137
+
134
138
  const tracking = useViewTracking({
135
139
  recordingId: shareId ?? "",
136
- videoRef: {
137
- get current() {
138
- return playerRef.current?.video ?? null;
139
- },
140
- } as any,
140
+ videoEl: trackedVideoEl,
141
141
  durationMs: recording?.durationMs ?? 0,
142
142
  trackOpenWithoutVideo: isLoomEmbedBacked,
143
143
  });
@@ -193,6 +193,7 @@ export default function EmbedRoute() {
193
193
  <div className="fixed inset-0 h-dvh w-dvw overflow-hidden bg-black">
194
194
  <VideoPlayer
195
195
  ref={playerRef}
196
+ onVideoElementChange={setTrackedVideoEl}
196
197
  recordingId={recording.id}
197
198
  videoUrl={recording.videoUrl}
198
199
  videoFormat={recording.videoFormat}
@@ -800,16 +800,15 @@ export default function RecordingPage() {
800
800
 
801
801
  usePlayerShortcuts({ playerRef, chapters });
802
802
 
803
+ const [trackedVideoEl, setTrackedVideoEl] = useState<HTMLVideoElement | null>(
804
+ null,
805
+ );
806
+
803
807
  const tracking = useViewTracking({
804
808
  recordingId: recordingId ?? "",
805
- videoRef: {
806
- get current() {
807
- return playerRef.current?.video ?? null;
808
- },
809
- } as any,
809
+ videoEl: trackedVideoEl,
810
810
  durationMs: recording?.durationMs ?? 0,
811
- // Skip tracking for the owner they shouldn't inflate their own views.
812
- disabled: role === "owner",
811
+ disabled: role === "owner", // Skip tracking for the owner: they shouldn't inflate their own views.
813
812
  });
814
813
 
815
814
  if (!recordingId) return null;
@@ -1545,6 +1544,7 @@ export default function RecordingPage() {
1545
1544
  <div className="relative aspect-video w-full xl:min-h-0 xl:flex-1 xl:aspect-auto">
1546
1545
  <VideoPlayer
1547
1546
  ref={playerRef}
1547
+ onVideoElementChange={setTrackedVideoEl}
1548
1548
  recordingId={recording.id}
1549
1549
  videoUrl={recording.videoUrl}
1550
1550
  videoFormat={recording.videoFormat}