@agent-native/core 0.108.0 → 0.109.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +18 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/client.ts +140 -17
  5. package/corpus/core/src/a2a/handlers.ts +60 -0
  6. package/corpus/core/src/a2a/index.ts +6 -1
  7. package/corpus/core/src/a2a/invoke.ts +68 -1
  8. package/corpus/core/src/a2a/server.ts +13 -1
  9. package/corpus/core/src/a2a/types.ts +16 -0
  10. package/corpus/core/src/action.ts +3 -2
  11. package/corpus/core/src/agent/production-agent.ts +10 -4
  12. package/corpus/core/src/client/AssistantChat.tsx +648 -603
  13. package/corpus/core/src/client/MultiTabAssistantChat.tsx +31 -4
  14. package/corpus/core/src/client/agent-chat-adapter.ts +23 -37
  15. package/corpus/core/src/client/agent-sidebar-state.ts +4 -0
  16. package/corpus/core/src/client/analytics.ts +84 -0
  17. package/corpus/core/src/client/chat/message-components.tsx +94 -22
  18. package/corpus/core/src/client/chat/tool-call-display.tsx +170 -47
  19. package/corpus/core/src/client/chat/use-agent-chat-lifecycle-tracking.ts +74 -0
  20. package/corpus/core/src/client/chat-thread-url.ts +11 -0
  21. package/corpus/core/src/client/error-capture.ts +12 -0
  22. package/corpus/core/src/client/extensions/ExtensionSlot.tsx +37 -1
  23. package/corpus/core/src/client/index.ts +2 -0
  24. package/corpus/core/src/client/route-chunk-recovery.ts +20 -8
  25. package/corpus/core/src/client/session-replay.ts +31 -0
  26. package/corpus/core/src/extensions/actions.ts +2 -14
  27. package/corpus/core/src/extensions/store.ts +1 -1
  28. package/corpus/core/src/localization/default-messages.ts +3 -0
  29. package/corpus/core/src/mcp/build-server.ts +2 -2
  30. package/corpus/core/src/progress/actions.ts +4 -2
  31. package/corpus/core/src/provider-api/index.ts +205 -105
  32. package/corpus/core/src/scripts/call-agent.ts +137 -17
  33. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +40 -0
  34. package/corpus/core/src/server/agent-chat-plugin.ts +31 -0
  35. package/corpus/core/src/styles/agent-conversation.css +7 -0
  36. package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +20 -0
  37. package/corpus/core/src/templates/default/app/i18n/de-DE.ts +20 -0
  38. package/corpus/core/src/templates/default/app/i18n/en-US.ts +20 -0
  39. package/corpus/core/src/templates/default/app/i18n/es-ES.ts +20 -0
  40. package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +21 -0
  41. package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +19 -0
  42. package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +20 -0
  43. package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +19 -0
  44. package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +20 -0
  45. package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +19 -0
  46. package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +19 -0
  47. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +28 -0
  48. package/corpus/core/src/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +5 -0
  49. package/corpus/core/src/templates/workspace-core/.agents/skills/performance/SKILL.md +13 -0
  50. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +29 -10
  51. package/corpus/templates/analytics/.agents/skills/gong/SKILL.md +54 -23
  52. package/corpus/templates/analytics/.builder/skills/apollo/SKILL.md +19 -41
  53. package/corpus/templates/analytics/.builder/skills/commonroom/SKILL.md +20 -35
  54. package/corpus/templates/analytics/.builder/skills/github/SKILL.md +23 -67
  55. package/corpus/templates/analytics/.builder/skills/pylon/SKILL.md +25 -43
  56. package/corpus/templates/analytics/AGENTS.md +22 -13
  57. package/corpus/templates/analytics/actions/account-deep-dive.ts +1 -0
  58. package/corpus/templates/analytics/actions/configure-gong-native-insights.ts +29 -0
  59. package/corpus/templates/analytics/actions/content-calendar-schema.ts +8 -4
  60. package/corpus/templates/analytics/actions/content-calendar.ts +12 -4
  61. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +2 -1
  62. package/corpus/templates/analytics/actions/gong-calls.ts +113 -22
  63. package/corpus/templates/analytics/actions/gong-native-insights.ts +166 -0
  64. package/corpus/templates/analytics/actions/hubspot-deals.ts +190 -46
  65. package/corpus/templates/analytics/actions/pylon-issues.ts +123 -0
  66. package/corpus/templates/analytics/actions/slack-messages.ts +179 -27
  67. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +48 -152
  68. package/corpus/templates/analytics/app/components/ui/chart.tsx +1 -1
  69. package/corpus/templates/analytics/app/hooks/use-chart-tooltip-flip.ts +21 -6
  70. package/corpus/templates/analytics/app/i18n/zh-TW.ts +13 -0
  71. package/corpus/templates/analytics/app/i18n-data.ts +134 -0
  72. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +181 -30
  73. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +6 -0
  74. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +4 -3
  75. package/corpus/templates/analytics/changelog/2026-07-17-dashboard-editors-can-add-shared-extensions-or-opt-into-per-.md +6 -0
  76. package/corpus/templates/analytics/changelog/2026-07-17-dashboard-extensions-show-a-loading-skeleton.md +6 -0
  77. package/corpus/templates/analytics/changelog/2026-07-17-errors-now-group-repeat-failures-consistently-and-keep-priva.md +6 -0
  78. package/corpus/templates/analytics/changelog/2026-07-17-gong-transcript-reviews-now-batch-bounded-call-windows-for-f.md +6 -0
  79. package/corpus/templates/analytics/changelog/2026-07-17-hubspot-deal-filters-now-run-in-hubspot-before-paging-result.md +6 -0
  80. package/corpus/templates/analytics/changelog/2026-07-17-notion-content-calendars-now-discover-the-matching-database-.md +6 -0
  81. package/corpus/templates/analytics/changelog/2026-07-17-pylon-dashboard-reads-now-use-the-paginated-issue-search-api.md +6 -0
  82. package/corpus/templates/analytics/changelog/2026-07-17-slack-reads-no-longer-join-channels-and-report-pagination-coverage.md +6 -0
  83. package/corpus/templates/analytics/server/handlers/notion.ts +14 -3
  84. package/corpus/templates/analytics/server/handlers/slack.ts +182 -29
  85. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +2 -6
  86. package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +6 -2
  87. package/corpus/templates/analytics/server/lib/error-capture.ts +75 -8
  88. package/corpus/templates/analytics/server/lib/gong-native-policy.ts +72 -0
  89. package/corpus/templates/analytics/server/lib/gong.ts +54 -66
  90. package/corpus/templates/analytics/server/lib/hubspot.ts +40 -3
  91. package/corpus/templates/analytics/server/lib/notion.ts +83 -8
  92. package/corpus/templates/analytics/server/lib/real-data-actions.ts +4 -0
  93. package/corpus/templates/analytics/server/lib/slack.ts +242 -90
  94. package/corpus/templates/analytics/server/plugins/agent-chat.ts +3 -3
  95. package/corpus/templates/analytics/server/routes/api/pylon/accounts.get.ts +42 -1
  96. package/corpus/templates/analytics/server/routes/api/pylon/issues.get.ts +71 -1
  97. package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +44 -4
  98. package/corpus/templates/clips/app/components/library/bulk-action-toolbar.tsx +19 -1
  99. package/corpus/templates/clips/app/components/library/create-folder-dialog.tsx +123 -0
  100. package/corpus/templates/clips/app/components/library/library-grid.tsx +42 -4
  101. package/corpus/templates/clips/app/components/library/recording-card.tsx +30 -0
  102. package/corpus/templates/clips/app/entry.client.tsx +6 -1
  103. package/corpus/templates/clips/app/i18n/ar-SA.ts +12 -1
  104. package/corpus/templates/clips/app/i18n/de-DE.ts +12 -1
  105. package/corpus/templates/clips/app/i18n/en-US.ts +12 -1
  106. package/corpus/templates/clips/app/i18n/es-ES.ts +11 -1
  107. package/corpus/templates/clips/app/i18n/fr-FR.ts +11 -1
  108. package/corpus/templates/clips/app/i18n/hi-IN.ts +10 -1
  109. package/corpus/templates/clips/app/i18n/ja-JP.ts +12 -1
  110. package/corpus/templates/clips/app/i18n/ko-KR.ts +11 -1
  111. package/corpus/templates/clips/app/i18n/pt-BR.ts +11 -1
  112. package/corpus/templates/clips/app/i18n/zh-CN.ts +11 -1
  113. package/corpus/templates/clips/app/i18n/zh-TW.ts +11 -1
  114. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +72 -1
  115. package/corpus/templates/clips/changelog/2026-07-17-choose-which-workspace-receives-new-clips-recordings-and-des.md +6 -0
  116. package/corpus/templates/clips/changelog/2026-07-17-create-folders-directly-from-the-move-menu.md +6 -0
  117. package/corpus/templates/clips/changelog/2026-07-17-failed-desktop-uploads-now-offer-a-direct-way-to-reopen-clip.md +6 -0
  118. package/corpus/templates/clips/desktop/src/app.tsx +1 -1
  119. package/corpus/templates/content/app/entry.client.tsx +6 -1
  120. package/corpus/templates/dispatch/AGENTS.md +5 -0
  121. package/corpus/templates/dispatch/changelog/2026-07-17-apps-page-links-to-other-available-apps.md +6 -0
  122. package/corpus/templates/dispatch/changelog/2026-07-17-curated-template-remixes.md +6 -0
  123. package/corpus/templates/mail/app/entry.client.tsx +6 -1
  124. package/corpus/templates/slides/.agents/skills/slide-images/SKILL.md +11 -7
  125. package/corpus/templates/slides/.env.example +0 -1
  126. package/corpus/templates/slides/AGENTS.md +4 -0
  127. package/corpus/templates/slides/DEVELOPING.md +2 -2
  128. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +1 -1
  129. package/corpus/templates/slides/actions/logo-config.ts +6 -7
  130. package/corpus/templates/slides/actions/search-images.ts +11 -39
  131. package/corpus/templates/slides/actions/search-logos.ts +5 -52
  132. package/corpus/templates/slides/app/components/editor/ImageSearchPanel.tsx +1 -1
  133. package/corpus/templates/slides/app/components/editor/LogoSearchPanel.tsx +16 -26
  134. package/corpus/templates/slides/app/entry.client.tsx +6 -1
  135. package/corpus/templates/slides/server/lib/media-search-providers.ts +216 -0
  136. package/corpus/templates/slides/server/register-secrets.ts +30 -0
  137. package/dist/a2a/client.d.ts +38 -1
  138. package/dist/a2a/client.d.ts.map +1 -1
  139. package/dist/a2a/client.js +81 -12
  140. package/dist/a2a/client.js.map +1 -1
  141. package/dist/a2a/handlers.d.ts.map +1 -1
  142. package/dist/a2a/handlers.js +47 -0
  143. package/dist/a2a/handlers.js.map +1 -1
  144. package/dist/a2a/index.d.ts +4 -4
  145. package/dist/a2a/index.d.ts.map +1 -1
  146. package/dist/a2a/index.js +2 -2
  147. package/dist/a2a/index.js.map +1 -1
  148. package/dist/a2a/invoke.d.ts +26 -2
  149. package/dist/a2a/invoke.d.ts.map +1 -1
  150. package/dist/a2a/invoke.js +30 -1
  151. package/dist/a2a/invoke.js.map +1 -1
  152. package/dist/a2a/server.d.ts.map +1 -1
  153. package/dist/a2a/server.js +14 -1
  154. package/dist/a2a/server.js.map +1 -1
  155. package/dist/a2a/types.d.ts +12 -0
  156. package/dist/a2a/types.d.ts.map +1 -1
  157. package/dist/a2a/types.js.map +1 -1
  158. package/dist/action.d.ts +3 -2
  159. package/dist/action.d.ts.map +1 -1
  160. package/dist/action.js.map +1 -1
  161. package/dist/agent/production-agent.d.ts +5 -0
  162. package/dist/agent/production-agent.d.ts.map +1 -1
  163. package/dist/agent/production-agent.js +5 -3
  164. package/dist/agent/production-agent.js.map +1 -1
  165. package/dist/client/AssistantChat.d.ts +14 -1
  166. package/dist/client/AssistantChat.d.ts.map +1 -1
  167. package/dist/client/AssistantChat.js +158 -136
  168. package/dist/client/AssistantChat.js.map +1 -1
  169. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  170. package/dist/client/MultiTabAssistantChat.js +27 -4
  171. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  172. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  173. package/dist/client/agent-chat-adapter.js +21 -39
  174. package/dist/client/agent-chat-adapter.js.map +1 -1
  175. package/dist/client/agent-sidebar-state.d.ts +1 -0
  176. package/dist/client/agent-sidebar-state.d.ts.map +1 -1
  177. package/dist/client/agent-sidebar-state.js +4 -0
  178. package/dist/client/agent-sidebar-state.js.map +1 -1
  179. package/dist/client/analytics.d.ts +14 -0
  180. package/dist/client/analytics.d.ts.map +1 -1
  181. package/dist/client/analytics.js +67 -0
  182. package/dist/client/analytics.js.map +1 -1
  183. package/dist/client/chat/message-components.d.ts +8 -0
  184. package/dist/client/chat/message-components.d.ts.map +1 -1
  185. package/dist/client/chat/message-components.js +62 -19
  186. package/dist/client/chat/message-components.js.map +1 -1
  187. package/dist/client/chat/tool-call-display.d.ts +6 -0
  188. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  189. package/dist/client/chat/tool-call-display.js +83 -14
  190. package/dist/client/chat/tool-call-display.js.map +1 -1
  191. package/dist/client/chat/use-agent-chat-lifecycle-tracking.d.ts +8 -0
  192. package/dist/client/chat/use-agent-chat-lifecycle-tracking.d.ts.map +1 -0
  193. package/dist/client/chat/use-agent-chat-lifecycle-tracking.js +51 -0
  194. package/dist/client/chat/use-agent-chat-lifecycle-tracking.js.map +1 -0
  195. package/dist/client/chat-thread-url.d.ts +2 -0
  196. package/dist/client/chat-thread-url.d.ts.map +1 -0
  197. package/dist/client/chat-thread-url.js +12 -0
  198. package/dist/client/chat-thread-url.js.map +1 -0
  199. package/dist/client/error-capture.d.ts.map +1 -1
  200. package/dist/client/error-capture.js +7 -0
  201. package/dist/client/error-capture.js.map +1 -1
  202. package/dist/client/extensions/ExtensionSlot.d.ts +3 -1
  203. package/dist/client/extensions/ExtensionSlot.d.ts.map +1 -1
  204. package/dist/client/extensions/ExtensionSlot.js +28 -3
  205. package/dist/client/extensions/ExtensionSlot.js.map +1 -1
  206. package/dist/client/index.d.ts +1 -1
  207. package/dist/client/index.d.ts.map +1 -1
  208. package/dist/client/index.js +1 -1
  209. package/dist/client/index.js.map +1 -1
  210. package/dist/client/route-chunk-recovery.d.ts.map +1 -1
  211. package/dist/client/route-chunk-recovery.js +14 -9
  212. package/dist/client/route-chunk-recovery.js.map +1 -1
  213. package/dist/client/session-replay.d.ts +15 -0
  214. package/dist/client/session-replay.d.ts.map +1 -1
  215. package/dist/client/session-replay.js +20 -0
  216. package/dist/client/session-replay.js.map +1 -1
  217. package/dist/collab/routes.d.ts +1 -1
  218. package/dist/collab/struct-routes.d.ts +1 -1
  219. package/dist/extensions/actions.d.ts.map +1 -1
  220. package/dist/extensions/actions.js +2 -10
  221. package/dist/extensions/actions.js.map +1 -1
  222. package/dist/extensions/store.js +1 -1
  223. package/dist/extensions/store.js.map +1 -1
  224. package/dist/localization/default-messages.d.ts +3 -0
  225. package/dist/localization/default-messages.d.ts.map +1 -1
  226. package/dist/localization/default-messages.js +3 -0
  227. package/dist/localization/default-messages.js.map +1 -1
  228. package/dist/mcp/build-server.d.ts +2 -0
  229. package/dist/mcp/build-server.d.ts.map +1 -1
  230. package/dist/mcp/build-server.js +2 -2
  231. package/dist/mcp/build-server.js.map +1 -1
  232. package/dist/notifications/routes.d.ts +2 -2
  233. package/dist/observability/routes.d.ts +1 -1
  234. package/dist/progress/actions.d.ts.map +1 -1
  235. package/dist/progress/actions.js +3 -2
  236. package/dist/progress/actions.js.map +1 -1
  237. package/dist/progress/routes.d.ts +1 -1
  238. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  239. package/dist/provider-api/index.d.ts.map +1 -1
  240. package/dist/provider-api/index.js +91 -8
  241. package/dist/provider-api/index.js.map +1 -1
  242. package/dist/resources/handlers.d.ts +2 -2
  243. package/dist/scripts/call-agent.d.ts.map +1 -1
  244. package/dist/scripts/call-agent.js +107 -16
  245. package/dist/scripts/call-agent.js.map +1 -1
  246. package/dist/server/agent-chat/action-filters-a2a.d.ts +6 -0
  247. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  248. package/dist/server/agent-chat/action-filters-a2a.js +27 -0
  249. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  250. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  251. package/dist/server/agent-chat-plugin.js +27 -1
  252. package/dist/server/agent-chat-plugin.js.map +1 -1
  253. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  254. package/dist/server/transcribe-voice.d.ts +1 -1
  255. package/dist/styles/agent-conversation.css +7 -0
  256. package/dist/templates/default/app/i18n/ar-SA.ts +20 -0
  257. package/dist/templates/default/app/i18n/de-DE.ts +20 -0
  258. package/dist/templates/default/app/i18n/en-US.ts +20 -0
  259. package/dist/templates/default/app/i18n/es-ES.ts +20 -0
  260. package/dist/templates/default/app/i18n/fr-FR.ts +21 -0
  261. package/dist/templates/default/app/i18n/hi-IN.ts +19 -0
  262. package/dist/templates/default/app/i18n/ja-JP.ts +20 -0
  263. package/dist/templates/default/app/i18n/ko-KR.ts +19 -0
  264. package/dist/templates/default/app/i18n/pt-BR.ts +20 -0
  265. package/dist/templates/default/app/i18n/zh-CN.ts +19 -0
  266. package/dist/templates/default/app/i18n/zh-TW.ts +19 -0
  267. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +28 -0
  268. package/dist/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +5 -0
  269. package/dist/templates/workspace-core/.agents/skills/performance/SKILL.md +13 -0
  270. package/package.json +1 -1
  271. package/src/templates/default/app/i18n/ar-SA.ts +20 -0
  272. package/src/templates/default/app/i18n/de-DE.ts +20 -0
  273. package/src/templates/default/app/i18n/en-US.ts +20 -0
  274. package/src/templates/default/app/i18n/es-ES.ts +20 -0
  275. package/src/templates/default/app/i18n/fr-FR.ts +21 -0
  276. package/src/templates/default/app/i18n/hi-IN.ts +19 -0
  277. package/src/templates/default/app/i18n/ja-JP.ts +20 -0
  278. package/src/templates/default/app/i18n/ko-KR.ts +19 -0
  279. package/src/templates/default/app/i18n/pt-BR.ts +20 -0
  280. package/src/templates/default/app/i18n/zh-CN.ts +19 -0
  281. package/src/templates/default/app/i18n/zh-TW.ts +19 -0
  282. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +28 -0
  283. package/src/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +5 -0
  284. package/src/templates/workspace-core/.agents/skills/performance/SKILL.md +13 -0
  285. package/corpus/templates/analytics/server/handlers/github.ts +0 -200
  286. package/corpus/templates/analytics/server/handlers/pylon.ts +0 -43
  287. package/corpus/templates/analytics/server/lib/apollo.ts +0 -167
  288. package/corpus/templates/analytics/server/lib/commonroom.ts +0 -149
  289. package/corpus/templates/analytics/server/lib/github.ts +0 -660
  290. package/corpus/templates/analytics/server/lib/mixpanel.ts +0 -156
  291. package/corpus/templates/analytics/server/lib/posthog.ts +0 -194
  292. package/corpus/templates/analytics/server/lib/pylon.ts +0 -109
  293. package/corpus/templates/analytics/server/routes/api/github/graphql.post.ts +0 -1
  294. package/corpus/templates/analytics/server/routes/api/github/issue.get.ts +0 -1
  295. package/corpus/templates/analytics/server/routes/api/github/org-prs.get.ts +0 -1
  296. package/corpus/templates/analytics/server/routes/api/github/pr.get.ts +0 -1
  297. package/corpus/templates/analytics/server/routes/api/github/prs.get.ts +0 -1
  298. package/corpus/templates/analytics/server/routes/api/github/search.get.ts +0 -1
  299. package/corpus/templates/slides/actions/fetch-logos.ts +0 -90
  300. package/corpus/templates/slides/actions/image-search.ts +0 -64
  301. package/corpus/templates/slides/actions/import-github.ts +0 -182
  302. package/corpus/templates/slides/actions/logo-lookup.ts +0 -113
@@ -4,9 +4,8 @@ import { z } from "zod";
4
4
  import {
5
5
  getChannelHistory,
6
6
  getTeamInfo,
7
- listChannels,
8
- resolveUsers,
9
- searchMessages,
7
+ listChannelsWithCoverage,
8
+ resolveUsersWithCoverage,
10
9
  type SlackMessage,
11
10
  type Workspace,
12
11
  } from "../server/lib/slack";
@@ -41,7 +40,7 @@ function enrichMessages(messages: SlackMessage[]): SlackMessage[] {
41
40
 
42
41
  export default defineAction({
43
42
  description:
44
- "Query the analytics app's configured Slack workspace: team info, channels, channel history, multi-channel history, or message search. Slack messages returned by this action are real source evidence; you may count mentions, code themes, classify sentiment, and summarize qualitative patterns from them while stating the sample size.",
43
+ "Query the analytics app's configured Slack workspace: team info, channels, channel history, or multi-channel history. Slack messages returned by this action are real source evidence; you may count mentions, code themes, classify sentiment, and summarize qualitative patterns from them while stating the sample size. The legacy search mode is preserved for compatibility but returns migration guidance because the configured bot credential cannot call Slack global search; use the Slack provider corpus recipe for channel-scoped exhaustive search.",
45
44
  schema: z.object({
46
45
  mode: z
47
46
  .enum(["team", "channels", "history", "multi-history", "search"])
@@ -68,7 +67,10 @@ export default defineAction({
68
67
  .max(200)
69
68
  .default(50)
70
69
  .describe("Message limit for history"),
71
- cursor: z.string().optional().describe("Slack history cursor/latest ts"),
70
+ cursor: z
71
+ .string()
72
+ .optional()
73
+ .describe("Slack history/latest timestamp or channel-list cursor"),
72
74
  cursors: z
73
75
  .record(z.string(), z.string())
74
76
  .optional()
@@ -99,12 +101,31 @@ export default defineAction({
99
101
  const userIds = messages
100
102
  .map((message) => message.user)
101
103
  .filter((id): id is string => !!id);
102
- const users = await resolveUsers(workspace, userIds, messages);
104
+ const resolution = await resolveUsersWithCoverage(
105
+ workspace,
106
+ userIds,
107
+ messages,
108
+ );
103
109
  return {
104
110
  messages,
105
- users,
111
+ users: resolution.users,
106
112
  has_more: result.has_more,
107
113
  next_cursor: result.next_cursor,
114
+ truncated: result.truncated,
115
+ pagination: result.pagination,
116
+ coverage: {
117
+ ...result.coverage,
118
+ coverage_complete:
119
+ result.coverage.coverage_complete &&
120
+ resolution.coverage.coverage_complete,
121
+ truncated:
122
+ result.coverage.truncated || resolution.coverage.truncated,
123
+ truncation_reasons: [
124
+ ...result.coverage.truncation_reasons,
125
+ ...resolution.coverage.truncation_reasons,
126
+ ],
127
+ authors: resolution.coverage,
128
+ },
108
129
  };
109
130
  }
110
131
 
@@ -113,57 +134,188 @@ export default defineAction({
113
134
  const channelIds = args.channels.split(",").filter(Boolean);
114
135
  const channelNames = args.names ? args.names.split(",") : channelIds;
115
136
  const pageSize = Math.min(args.limit, 200);
116
- const results = await Promise.all(
117
- channelIds.map((id) =>
118
- getChannelHistory(workspace, id, pageSize, args.cursors?.[id]),
137
+ const channelResults = await Promise.allSettled(
138
+ channelIds.map((channelId) =>
139
+ getChannelHistory(
140
+ workspace,
141
+ channelId,
142
+ pageSize,
143
+ args.cursors?.[channelId],
144
+ ),
119
145
  ),
120
146
  );
147
+ const successfulResults = channelResults.flatMap((entry, index) =>
148
+ entry.status === "fulfilled"
149
+ ? [{ channelId: channelIds[index], result: entry.value }]
150
+ : [],
151
+ );
152
+ const failedChannelIds = channelResults.flatMap((entry, index) =>
153
+ entry.status === "rejected" ? [channelIds[index]] : [],
154
+ );
121
155
 
122
- const allMessages: (SlackMessage & { channel_name: string })[] = [];
156
+ const allMessages: (SlackMessage & {
157
+ channel_id: string;
158
+ channel_name: string;
159
+ })[] = [];
123
160
  const perChannelHasMore: Record<string, boolean> = {};
124
161
  const nextCursors: Record<string, string> = {};
125
162
 
126
- results.forEach((result, idx) => {
127
- const channelId = channelIds[idx];
163
+ successfulResults.forEach(({ channelId, result }) => {
128
164
  perChannelHasMore[channelId] = result.has_more;
129
- if (result.next_cursor) nextCursors[channelId] = result.next_cursor;
130
165
  for (const message of result.messages) {
131
166
  allMessages.push({
132
167
  ...message,
133
- channel_name: channelNames[idx] || channelId,
168
+ channel_id: channelId,
169
+ channel_name:
170
+ channelNames[channelIds.indexOf(channelId)] || channelId,
134
171
  });
135
172
  }
136
173
  });
174
+ failedChannelIds.forEach((channelId) => {
175
+ perChannelHasMore[channelId] = true;
176
+ });
137
177
 
138
178
  allMessages.sort((a, b) => parseFloat(b.ts) - parseFloat(a.ts));
139
- const messages = enrichMessages(allMessages.slice(0, pageSize));
179
+ const pageMessages = allMessages.slice(0, pageSize);
180
+ const emittedByChannel = new Map<string, SlackMessage[]>();
181
+ for (const message of pageMessages) {
182
+ const emitted = emittedByChannel.get(message.channel_id) ?? [];
183
+ emitted.push(message);
184
+ emittedByChannel.set(message.channel_id, emitted);
185
+ }
186
+ for (const channelId of channelIds) {
187
+ const emitted = emittedByChannel.get(channelId);
188
+ const nextCursor = emitted?.[emitted.length - 1]?.ts;
189
+ if (nextCursor) nextCursors[channelId] = nextCursor;
190
+ else if (args.cursors?.[channelId]) {
191
+ nextCursors[channelId] = args.cursors[channelId];
192
+ }
193
+ }
194
+
195
+ const messages = pageMessages.map((message) => {
196
+ const enriched = enrichMessages([message])[0] ?? message;
197
+ const { channel_id: _channelId, ...rest } =
198
+ enriched as typeof message;
199
+ return rest;
200
+ });
140
201
  const userIds = messages
141
202
  .map((message) => message.user)
142
203
  .filter((id): id is string => !!id);
143
- const users = await resolveUsers(workspace, userIds, messages);
204
+ const resolution = await resolveUsersWithCoverage(
205
+ workspace,
206
+ userIds,
207
+ messages,
208
+ );
209
+ const providerTruncated = successfulResults.some(
210
+ ({ result }) => result.truncated,
211
+ );
212
+ const mergedResultTruncated = allMessages.length > pageSize;
213
+ const truncated =
214
+ providerTruncated ||
215
+ mergedResultTruncated ||
216
+ failedChannelIds.length > 0;
217
+ const truncationReasons = [
218
+ ...(providerTruncated ? ["provider_has_more"] : []),
219
+ ...(mergedResultTruncated ? ["merged_result_limit"] : []),
220
+ ...failedChannelIds.map(
221
+ (channelId) => `channel_fetch_failed:${channelId}`,
222
+ ),
223
+ ];
224
+ const channelPagination = Object.fromEntries(
225
+ channelResults.map((entry, index) => [
226
+ channelIds[index],
227
+ entry.status === "fulfilled"
228
+ ? entry.value.pagination
229
+ : {
230
+ cursor_type: "latest_ts" as const,
231
+ ...(args.cursors?.[channelIds[index]]
232
+ ? { request_cursor: args.cursors[channelIds[index]] }
233
+ : {}),
234
+ },
235
+ ]),
236
+ );
237
+ const channelCoverage = Object.fromEntries(
238
+ channelResults.map((entry, index) => [
239
+ channelIds[index],
240
+ entry.status === "fulfilled"
241
+ ? entry.value.coverage
242
+ : {
243
+ requested: pageSize,
244
+ fetched: 0,
245
+ returned: 0,
246
+ pages_fetched: 0,
247
+ coverage_complete: false,
248
+ truncated: true,
249
+ truncation_reasons: [
250
+ `channel_fetch_failed:${channelIds[index]}`,
251
+ ],
252
+ },
253
+ ]),
254
+ );
144
255
  return {
145
256
  messages,
146
- users,
257
+ users: resolution.users,
147
258
  has_more:
148
259
  Object.values(perChannelHasMore).some(Boolean) ||
149
- allMessages.length > pageSize,
260
+ allMessages.length > pageSize ||
261
+ failedChannelIds.length > 0,
150
262
  next_cursors: nextCursors,
151
263
  total: allMessages.length,
264
+ truncated,
265
+ pagination: {
266
+ cursor_type: "per_channel_latest_ts",
267
+ request_cursors: args.cursors ?? {},
268
+ next_cursors: nextCursors,
269
+ channels: channelPagination,
270
+ },
271
+ coverage: {
272
+ requested: channelIds.length * pageSize,
273
+ fetched: allMessages.length,
274
+ returned: messages.length,
275
+ pages_fetched: successfulResults.length,
276
+ coverage_complete:
277
+ !truncated && resolution.coverage.coverage_complete,
278
+ truncated: truncated || resolution.coverage.truncated,
279
+ truncation_reasons: [
280
+ ...truncationReasons,
281
+ ...resolution.coverage.truncation_reasons,
282
+ ],
283
+ channels: channelCoverage,
284
+ authors: resolution.coverage,
285
+ },
152
286
  };
153
287
  }
154
288
 
155
289
  if (args.mode === "search") {
156
290
  if (!args.query) return { error: "query is required" };
157
- const result = await searchMessages(workspace, args.query);
158
- const userIds = result.messages
159
- .map((message) => message.user)
160
- .filter((id): id is string => !!id);
161
- const users = await resolveUsers(workspace, userIds, result.messages);
162
- return { messages: result.messages, users, total: result.total };
291
+ return {
292
+ messages: [],
293
+ users: {},
294
+ total: 0,
295
+ unsupported: true,
296
+ truncated: true,
297
+ coverage: {
298
+ requested: 0,
299
+ fetched: 0,
300
+ returned: 0,
301
+ pages_fetched: 0,
302
+ coverage_complete: false,
303
+ truncated: true,
304
+ truncation_reasons: ["bot_token_global_search_unsupported"],
305
+ },
306
+ guidance:
307
+ "Slack global search is not available with the configured bot credential. Resolve a channel id, then use provider-api-catalog(provider='slack') and provider-corpus-job with the Slack conversations.history corpus recipe so cursor pages are fetched inside one resumable operation.",
308
+ };
163
309
  }
164
310
 
165
- const channels = await listChannels(workspace);
166
- return { channels, total: channels.length };
311
+ const result = await listChannelsWithCoverage(workspace, args.cursor);
312
+ return {
313
+ channels: result.channels,
314
+ total: result.total,
315
+ truncated: result.truncated,
316
+ pagination: result.pagination,
317
+ coverage: result.coverage,
318
+ };
167
319
  } catch (err) {
168
320
  return providerError(err);
169
321
  }
@@ -19,14 +19,12 @@ import {
19
19
  useCallback,
20
20
  useContext,
21
21
  useEffect,
22
- useLayoutEffect,
23
22
  useMemo,
24
23
  useRef,
25
24
  useState,
26
25
  type CSSProperties,
27
26
  type ReactNode,
28
27
  } from "react";
29
- import { createPortal } from "react-dom";
30
28
  import { Link } from "react-router";
31
29
  import {
32
30
  Area,
@@ -61,6 +59,7 @@ import {
61
59
  SelectValue,
62
60
  } from "@/components/ui/select";
63
61
  import { Skeleton } from "@/components/ui/skeleton";
62
+ import { useChartTooltipFlip } from "@/hooks/use-chart-tooltip-flip";
64
63
 
65
64
  const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
66
65
  import { createDemoChartTrendRows } from "@/lib/demo-chart-trend";
@@ -90,11 +89,6 @@ const CHART_TOOLTIP_WRAPPER_STYLE: CSSProperties = {
90
89
  pointerEvents: "none",
91
90
  };
92
91
 
93
- const PORTAL_GUTTER_PADDING = 12;
94
- const PORTAL_CURSOR_OFFSET = 24;
95
- const useBrowserLayoutEffect =
96
- typeof window === "undefined" ? useEffect : useLayoutEffect;
97
-
98
92
  const CHART_TOOLTIP_PROPS = {
99
93
  allowEscapeViewBox: { x: true, y: true },
100
94
  wrapperStyle: CHART_TOOLTIP_WRAPPER_STYLE,
@@ -598,7 +592,7 @@ export function SeriesLegend({
598
592
  if (!shouldShowLegend(panel, keys.length)) return null;
599
593
 
600
594
  return (
601
- <div className="mt-2 min-h-16 max-h-16 overflow-y-auto overflow-x-hidden pr-1 text-[11px] leading-4 text-muted-foreground">
595
+ <div className="mt-1 max-h-16 overflow-y-auto overflow-x-hidden pr-1 text-[11px] leading-4 text-muted-foreground">
602
596
  <div className="flex flex-wrap gap-x-3 gap-y-1">
603
597
  {keys.map((key, i) => {
604
598
  const hidden = hiddenKeys?.has(key) ?? false;
@@ -612,7 +606,7 @@ export function SeriesLegend({
612
606
  >
613
607
  <PopoverAnchor asChild>
614
608
  <div
615
- className="inline-flex min-h-10 max-w-[14rem] items-center"
609
+ className="inline-flex min-h-6 max-w-[14rem] items-center"
616
610
  onPointerDown={(event) => {
617
611
  if (!hasLegendActions || event.pointerType === "mouse") {
618
612
  return;
@@ -648,7 +642,7 @@ export function SeriesLegend({
648
642
  }
649
643
  aria-haspopup={hasLegendActions ? "menu" : undefined}
650
644
  data-chart-legend-item={key}
651
- className={`inline-flex min-h-10 max-w-[14rem] min-w-0 items-center gap-1.5 rounded-md px-1.5 text-left transition-[opacity,color] touch-manipulation hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring ${
645
+ className={`inline-flex min-h-6 max-w-[14rem] min-w-0 items-center gap-1.5 rounded-md px-1.5 text-left transition-[opacity,color] touch-manipulation hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring ${
652
646
  hidden ? "opacity-35" : "opacity-100"
653
647
  } ${onToggleKey ? "cursor-pointer" : "cursor-default"}`}
654
648
  title={label}
@@ -680,18 +674,18 @@ export function SeriesLegend({
680
674
  align="center"
681
675
  sideOffset={8}
682
676
  collisionPadding={12}
683
- className="w-56 max-w-[calc(100vw-1.5rem)] rounded-lg p-1.5 shadow-lg"
677
+ className="w-auto max-w-[calc(100vw-1.5rem)] rounded-lg p-1 shadow-lg"
684
678
  onPointerEnter={clearCloseTimeout}
685
679
  onPointerLeave={scheduleCloseLegendActions}
686
680
  onFocusCapture={clearCloseTimeout}
687
681
  >
688
- <div className="flex w-full items-center gap-1">
682
+ <div className="flex items-center gap-0.5">
689
683
  {onFilterKey && (
690
684
  <button
691
685
  type="button"
692
686
  data-chart-legend-action="filter"
693
687
  aria-label={`${t("sqlDashboard.filterSeries")} ${label}`}
694
- className="min-h-11 min-w-0 flex-1 rounded-md px-3 py-2 text-xs font-medium whitespace-nowrap text-popover-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground"
688
+ className="min-h-8 rounded-md px-2.5 py-1 text-xs font-medium whitespace-nowrap text-popover-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground"
695
689
  onClick={() => {
696
690
  onFilterKey(key);
697
691
  setOpenKey(null);
@@ -706,7 +700,7 @@ export function SeriesLegend({
706
700
  data-chart-legend-action="hide"
707
701
  aria-label={`${t("sqlDashboard.hide")} ${label}`}
708
702
  disabled={hidden}
709
- className="min-h-11 min-w-0 flex-1 rounded-md px-3 py-2 text-xs font-medium whitespace-nowrap text-popover-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
703
+ className="min-h-8 rounded-md px-2.5 py-1 text-xs font-medium whitespace-nowrap text-popover-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
710
704
  onClick={() => {
711
705
  onToggleKey(key);
712
706
  setOpenKey(null);
@@ -886,9 +880,9 @@ function SqlChartLoadingSkeleton({ panel }: { panel: SqlPanel }) {
886
880
  <Skeleton
887
881
  className={`w-full ${fill ? "h-full min-h-[250px] flex-1" : "h-[250px]"}`}
888
882
  />
889
- <div className="mt-2 grid min-h-16 grid-cols-2 gap-x-3 gap-y-1 overflow-hidden pr-1 sm:grid-cols-3">
890
- {Array.from({ length: 6 }).map((_, index) => (
891
- <div key={index} className="flex items-center gap-1.5">
883
+ <div className="mt-1 flex min-h-6 flex-wrap gap-x-3 gap-y-1 overflow-hidden pr-1">
884
+ {Array.from({ length: 3 }).map((_, index) => (
885
+ <div key={index} className="flex min-h-6 items-center gap-1.5">
892
886
  <Skeleton className="h-2.5 w-3 shrink-0 rounded-sm" />
893
887
  <Skeleton className="h-3 w-20 min-w-0" />
894
888
  </div>
@@ -898,7 +892,7 @@ function SqlChartLoadingSkeleton({ panel }: { panel: SqlPanel }) {
898
892
  );
899
893
  }
900
894
 
901
- function ChartTooltip({
895
+ export function ChartTooltip({
902
896
  active,
903
897
  payload,
904
898
  label,
@@ -918,12 +912,7 @@ function ChartTooltip({
918
912
  seriesNameFormatter?: (value: string) => string;
919
913
  valueFormatter?: (value: number) => string;
920
914
  }) {
921
- const anchorRef = useRef<HTMLDivElement | null>(null);
922
- const portalRef = useRef<HTMLDivElement | null>(null);
923
- const [portalPosition, setPortalPosition] = useState<{
924
- left: number;
925
- top: number;
926
- } | null>(null);
915
+ const tooltipRef = useChartTooltipFlip<HTMLDivElement>(active);
927
916
  const items = useMemo(
928
917
  () =>
929
918
  sortTooltipPayloadItems(
@@ -932,11 +921,6 @@ function ChartTooltip({
932
921
  ),
933
922
  [payload],
934
923
  );
935
- const portalVisible = portalPosition !== null;
936
- const clearPortalPosition = useCallback(
937
- () => setPortalPosition((prev) => (prev === null ? prev : null)),
938
- [],
939
- );
940
924
 
941
925
  const labelText =
942
926
  label == null
@@ -945,96 +929,14 @@ function ChartTooltip({
945
929
  ? labelFormatter(String(label))
946
930
  : String(label);
947
931
 
948
- useBrowserLayoutEffect(() => {
949
- if (!active || items.length === 0 || typeof window === "undefined") {
950
- clearPortalPosition();
951
- return;
952
- }
953
-
954
- const anchor = anchorRef.current;
955
- const wrapper = anchor?.parentElement;
956
- if (!anchor || !wrapper) return;
957
-
958
- let frame = 0;
959
- const apply = () => {
960
- frame = 0;
961
- if (!(wrapper as HTMLElement).style.transform) return;
962
-
963
- const anchorRect = anchor.getBoundingClientRect();
964
- const portalRect = portalRef.current?.getBoundingClientRect();
965
- const width = portalRect?.width || anchorRect.width;
966
- const height = portalRect?.height || anchorRect.height;
967
- if (width === 0 || height === 0) return;
968
-
969
- const sidebar = document.querySelector(".agent-sidebar-panel");
970
- const sidebarRect = sidebar?.getBoundingClientRect();
971
- const rightLimit =
972
- sidebarRect && sidebarRect.width > 0 && sidebarRect.left > 0
973
- ? sidebarRect.left - PORTAL_GUTTER_PADDING
974
- : window.innerWidth - PORTAL_GUTTER_PADDING;
975
- const bottomLimit = window.innerHeight - PORTAL_GUTTER_PADDING;
976
-
977
- let left = anchorRect.left;
978
- let top = anchorRect.top;
979
-
980
- if (left + width > rightLimit) {
981
- left = anchorRect.left - width - PORTAL_CURSOR_OFFSET;
982
- }
983
- left = Math.max(
984
- PORTAL_GUTTER_PADDING,
985
- Math.min(left, rightLimit - width),
986
- );
987
-
988
- if (top + height > bottomLimit) {
989
- top = bottomLimit - height;
990
- }
991
- top = Math.max(PORTAL_GUTTER_PADDING, top);
992
-
993
- setPortalPosition((prev) =>
994
- prev &&
995
- Math.abs(prev.left - left) < 0.5 &&
996
- Math.abs(prev.top - top) < 0.5
997
- ? prev
998
- : { left, top },
999
- );
1000
- };
1001
-
1002
- const schedule = () => {
1003
- if (frame) return;
1004
- frame = window.requestAnimationFrame(apply);
1005
- };
1006
-
1007
- schedule();
1008
-
1009
- const mutationObserver = new MutationObserver(schedule);
1010
- mutationObserver.observe(wrapper, {
1011
- attributes: true,
1012
- attributeFilter: ["style", "class"],
1013
- });
1014
-
1015
- const resizeObserver =
1016
- typeof ResizeObserver === "undefined"
1017
- ? null
1018
- : new ResizeObserver(schedule);
1019
- resizeObserver?.observe(anchor);
1020
- if (portalRef.current) resizeObserver?.observe(portalRef.current);
1021
-
1022
- window.addEventListener("resize", schedule);
1023
- window.addEventListener("scroll", schedule, true);
1024
-
1025
- return () => {
1026
- if (frame) window.cancelAnimationFrame(frame);
1027
- mutationObserver.disconnect();
1028
- resizeObserver?.disconnect();
1029
- window.removeEventListener("resize", schedule);
1030
- window.removeEventListener("scroll", schedule, true);
1031
- };
1032
- }, [active, clearPortalPosition, items.length, portalVisible]);
1033
-
1034
932
  if (!active || items.length === 0) return null;
1035
933
 
1036
934
  const tooltip = (
1037
- <div className="min-w-40 max-w-[280px] rounded-md border border-border bg-card px-3 py-2 text-xs text-foreground shadow-lg">
935
+ <div
936
+ ref={tooltipRef}
937
+ role="tooltip"
938
+ className="min-w-40 max-w-[280px] rounded-md border border-border bg-card px-3 py-2 text-xs text-foreground shadow-lg"
939
+ >
1038
940
  {labelText && (
1039
941
  <div className="mb-1.5 truncate font-medium text-foreground">
1040
942
  {labelText}
@@ -1068,31 +970,7 @@ function ChartTooltip({
1068
970
  </div>
1069
971
  );
1070
972
 
1071
- return (
1072
- <>
1073
- <div ref={anchorRef} aria-hidden="true" className="invisible">
1074
- {tooltip}
1075
- </div>
1076
- {portalPosition && typeof document !== "undefined"
1077
- ? createPortal(
1078
- <div
1079
- ref={portalRef}
1080
- role="tooltip"
1081
- style={{
1082
- position: "fixed",
1083
- left: portalPosition.left,
1084
- top: portalPosition.top,
1085
- zIndex: 1000,
1086
- pointerEvents: "none",
1087
- }}
1088
- >
1089
- {tooltip}
1090
- </div>,
1091
- document.body,
1092
- )
1093
- : null}
1094
- </>
1095
- );
973
+ return tooltip;
1096
974
  }
1097
975
 
1098
976
  function detectKeys(
@@ -1434,16 +1312,33 @@ function DashboardExtensionPanel({
1434
1312
  // dashboard report screenshots don't capture a blank extension panel.
1435
1313
  const [ready, setReady] = useState(false);
1436
1314
  const [unavailable, setUnavailable] = useState(false);
1315
+ const loadingSkeleton = !ready ? (
1316
+ <Skeleton
1317
+ data-dashboard-extension-loading="true"
1318
+ className="absolute inset-0 z-10 h-full min-h-[180px] w-full rounded-md"
1319
+ aria-hidden="true"
1320
+ />
1321
+ ) : null;
1437
1322
 
1438
1323
  if (slotId) {
1439
1324
  return (
1440
- <ExtensionSlot
1441
- id={slotId}
1442
- context={context}
1443
- showEmptyAffordance
1444
- className="min-h-[120px] w-full"
1445
- toolClassName="w-full"
1446
- />
1325
+ <div
1326
+ className="relative min-h-[180px] w-full"
1327
+ aria-busy={!ready}
1328
+ data-dashboard-report-loading={ready ? undefined : "true"}
1329
+ >
1330
+ {loadingSkeleton}
1331
+ <div className={ready ? "opacity-100" : "opacity-0"}>
1332
+ <ExtensionSlot
1333
+ id={slotId}
1334
+ context={context}
1335
+ showEmptyAffordance
1336
+ onReady={() => setReady(true)}
1337
+ className="min-h-[120px] w-full"
1338
+ toolClassName="w-full"
1339
+ />
1340
+ </div>
1341
+ </div>
1447
1342
  );
1448
1343
  }
1449
1344
 
@@ -1462,15 +1357,16 @@ function DashboardExtensionPanel({
1462
1357
 
1463
1358
  return (
1464
1359
  <div
1465
- className="w-full"
1360
+ className="relative min-h-[180px] w-full"
1361
+ aria-busy={!ready}
1466
1362
  data-dashboard-report-loading={ready ? undefined : "true"}
1467
1363
  >
1364
+ {loadingSkeleton}
1468
1365
  <EmbeddedExtension
1469
1366
  extensionId={extensionId!}
1470
1367
  slotId={`dashboard-panel-${panelId}`}
1471
- className="w-full"
1472
- // Intentional for v1: extension panels are standalone widgets and do not
1473
- // receive the dashboard's filters/variables/date range as `context`.
1368
+ context={context}
1369
+ className={ready ? "w-full opacity-100" : "w-full opacity-0"}
1474
1370
  initialHeight={180}
1475
1371
  onReady={() => setReady(true)}
1476
1372
  onUnavailable={() => {
@@ -136,7 +136,7 @@ const ChartTooltipContent = React.forwardRef<
136
136
  ref,
137
137
  ) => {
138
138
  const { config } = useChart();
139
- const flipRef = useChartTooltipFlip<HTMLDivElement>();
139
+ const flipRef = useChartTooltipFlip<HTMLDivElement>(active);
140
140
  const setRefs = React.useCallback(
141
141
  (node: HTMLDivElement | null) => {
142
142
  flipRef.current = node;
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useRef } from "react";
2
2
 
3
- const RIGHT_GUTTER_PADDING = 12;
3
+ const VIEWPORT_GUTTER_PADDING = 12;
4
4
  const FLIP_CURSOR_OFFSET = 24;
5
5
 
6
6
  /**
@@ -10,10 +10,11 @@ const FLIP_CURSOR_OFFSET = 24;
10
10
  * `transform` (cursor moves) and translate the content left when its right
11
11
  * edge would land inside `.agent-sidebar-panel`.
12
12
  */
13
- export function useChartTooltipFlip<T extends HTMLElement>() {
13
+ export function useChartTooltipFlip<T extends HTMLElement>(active = true) {
14
14
  const ref = useRef<T | null>(null);
15
15
 
16
16
  useEffect(() => {
17
+ if (!active) return;
17
18
  const el = ref.current;
18
19
  if (!el) return;
19
20
  const wrapper = el.parentElement;
@@ -32,11 +33,25 @@ export function useChartTooltipFlip<T extends HTMLElement>() {
32
33
  sidebarRect && sidebarRect.width > 0 && sidebarRect.left > 0
33
34
  ? sidebarRect.left
34
35
  : window.innerWidth;
35
- const limit = gutterLeft - RIGHT_GUTTER_PADDING;
36
+ const rightLimit = gutterLeft - VIEWPORT_GUTTER_PADDING;
37
+ const bottomLimit = window.innerHeight - VIEWPORT_GUTTER_PADDING;
38
+ let translateX = 0;
39
+ let translateY = 0;
36
40
 
37
- if (rect.right > limit) {
38
- node.style.transform = `translateX(-${rect.width + FLIP_CURSOR_OFFSET}px)`;
41
+ if (rect.right > rightLimit) {
42
+ translateX = -(rect.width + FLIP_CURSOR_OFFSET);
39
43
  }
44
+ if (rect.bottom > bottomLimit) {
45
+ translateY = bottomLimit - rect.bottom;
46
+ } else if (rect.top < VIEWPORT_GUTTER_PADDING) {
47
+ translateY = VIEWPORT_GUTTER_PADDING - rect.top;
48
+ }
49
+
50
+ const transforms = [
51
+ translateX ? `translateX(${translateX}px)` : "",
52
+ translateY ? `translateY(${translateY}px)` : "",
53
+ ].filter(Boolean);
54
+ node.style.transform = transforms.join(" ");
40
55
  };
41
56
 
42
57
  apply();
@@ -46,7 +61,7 @@ export function useChartTooltipFlip<T extends HTMLElement>() {
46
61
  attributeFilter: ["style"],
47
62
  });
48
63
  return () => observer.disconnect();
49
- }, []);
64
+ }, [active]);
50
65
 
51
66
  return ref;
52
67
  }