@graph8/sdk 0.370.0 → 0.373.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 (466) hide show
  1. package/README.md +19 -0
  2. package/blocks/_foundation/README.md +14 -0
  3. package/blocks/_foundation/block.json +44 -0
  4. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  5. package/blocks/_foundation/components/empty-state.tsx +5 -2
  6. package/blocks/_foundation/components/list-select.tsx +112 -0
  7. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  8. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  9. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  10. package/blocks/_foundation/components/screen/index.ts +1 -0
  11. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  12. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  13. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  14. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  15. package/blocks/_foundation/lib/paging.ts +57 -0
  16. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  17. package/blocks/agents/README.md +81 -4
  18. package/blocks/agents/block.json +311 -2
  19. package/blocks/agents/components/agent-activity.tsx +112 -0
  20. package/blocks/agents/components/agent-approvals.tsx +135 -0
  21. package/blocks/agents/components/agent-builder.tsx +17 -3
  22. package/blocks/agents/components/agent-conversations.tsx +143 -0
  23. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  24. package/blocks/agents/components/agent-operators.tsx +164 -0
  25. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  26. package/blocks/agents/components/agent-runs.tsx +3 -3
  27. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  28. package/blocks/agents/components/agent-test-history.tsx +121 -0
  29. package/blocks/agents/components/agents-screen.tsx +53 -11
  30. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  31. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  32. package/blocks/agents/components/from-template-button.tsx +65 -0
  33. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  34. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  35. package/blocks/agents/components/knowledge-form.tsx +69 -0
  36. package/blocks/agents/components/name-dialog.tsx +32 -0
  37. package/blocks/agents/components/receptionist-button.tsx +46 -0
  38. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  39. package/blocks/agents/components/skill-editor.tsx +147 -0
  40. package/blocks/agents/components/skill-library.tsx +111 -0
  41. package/blocks/agents/lib/agent-types.ts +7 -1
  42. package/blocks/agents/lib/facts.tsx +13 -0
  43. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  44. package/blocks/agents/lib/load-more.tsx +141 -0
  45. package/blocks/agents/lib/skill-utils.ts +91 -0
  46. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  47. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  48. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  49. package/blocks/analytics/components/report-builder.tsx +15 -2
  50. package/blocks/appointments/README.md +6 -0
  51. package/blocks/appointments/block.json +27 -0
  52. package/blocks/appointments/components/booking-page.tsx +2 -2
  53. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  54. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  55. package/blocks/appointments/components/meeting-list.tsx +29 -13
  56. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  57. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  58. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  59. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  60. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  61. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  62. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  63. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  64. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  65. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  66. package/blocks/coach/components/call-scorecard.tsx +7 -7
  67. package/blocks/company-intelligence/README.md +132 -0
  68. package/blocks/company-intelligence/block.json +318 -0
  69. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  70. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  71. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  72. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  73. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  74. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  75. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  76. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  77. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  78. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  79. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  80. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  81. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  82. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  83. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  84. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  85. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  86. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  87. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  88. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  89. package/blocks/crm/README.md +6 -0
  90. package/blocks/crm/block.json +53 -4
  91. package/blocks/crm/components/companies-screen.tsx +23 -9
  92. package/blocks/crm/components/company-detail.tsx +2 -2
  93. package/blocks/crm/components/contact-detail.tsx +4 -4
  94. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  95. package/blocks/crm/components/contacts-screen.tsx +31 -13
  96. package/blocks/crm/components/dedupe-review.tsx +6 -1
  97. package/blocks/crm/components/import-wizard.tsx +3 -7
  98. package/blocks/crm/components/list-select.tsx +20 -0
  99. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  100. package/blocks/crm/components/record-form.tsx +1 -1
  101. package/blocks/crm/components/tasks-screen.tsx +35 -15
  102. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  103. package/blocks/crm/hooks/use-page-query.ts +3 -1
  104. package/blocks/crm/lib/crm-types.ts +14 -0
  105. package/blocks/crm/lib/read-lists.ts +16 -0
  106. package/blocks/deals/README.md +24 -6
  107. package/blocks/deals/block.json +151 -2
  108. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  109. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  110. package/blocks/deals/components/deal-detail.tsx +2 -2
  111. package/blocks/deals/components/deal-filters.tsx +65 -0
  112. package/blocks/deals/components/deal-line-items.tsx +120 -0
  113. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  114. package/blocks/deals/components/deal-related.tsx +30 -9
  115. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  116. package/blocks/deals/components/deals-screen.tsx +57 -22
  117. package/blocks/deals/components/forecast-view.tsx +4 -4
  118. package/blocks/deals/components/pipeline-board.tsx +8 -6
  119. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  120. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  121. package/blocks/deals/components/stage-editor.tsx +1 -1
  122. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  123. package/blocks/deals/lib/deal-extras.ts +156 -0
  124. package/blocks/dialer/README.md +14 -5
  125. package/blocks/dialer/block.json +17 -2
  126. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  127. package/blocks/dialer/components/call-log-screen.tsx +4 -1
  128. package/blocks/dialer/components/call-log.tsx +16 -4
  129. package/blocks/dialer/components/call-queue.tsx +3 -3
  130. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  131. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  132. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  133. package/blocks/dialer/components/session-start-form.tsx +58 -11
  134. package/blocks/dialer/components/softphone.tsx +1 -1
  135. package/blocks/enrichment/README.md +16 -6
  136. package/blocks/enrichment/block.json +19 -7
  137. package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
  138. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  139. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  140. package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
  141. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  142. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  143. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  144. package/blocks/forms/README.md +8 -0
  145. package/blocks/forms/block.json +107 -0
  146. package/blocks/forms/components/form-activity.tsx +71 -0
  147. package/blocks/forms/components/form-backfill.tsx +108 -0
  148. package/blocks/forms/components/form-builder.tsx +1 -1
  149. package/blocks/forms/components/form-embed.tsx +1 -1
  150. package/blocks/forms/components/form-groups.tsx +151 -0
  151. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  152. package/blocks/forms/components/form-route-history.tsx +72 -0
  153. package/blocks/forms/components/form-saved-views.tsx +81 -0
  154. package/blocks/forms/components/forms-screen.tsx +51 -17
  155. package/blocks/forms/components/more-rows.tsx +30 -0
  156. package/blocks/forms/components/submissions-export.tsx +67 -0
  157. package/blocks/forms/components/submissions-table.tsx +15 -6
  158. package/blocks/forms/lib/forms-extra.ts +67 -0
  159. package/blocks/inbox/README.md +5 -0
  160. package/blocks/inbox/block.json +15 -0
  161. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  162. package/blocks/inbox/components/thread-badge.tsx +2 -2
  163. package/blocks/inbox/components/thread-view.tsx +1 -1
  164. package/blocks/integrations/README.md +54 -1
  165. package/blocks/integrations/block.json +189 -15
  166. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  167. package/blocks/integrations/components/connection-health.tsx +59 -0
  168. package/blocks/integrations/components/crm-detail.tsx +20 -1
  169. package/blocks/integrations/components/crm-records.tsx +103 -0
  170. package/blocks/integrations/components/field-mappings.tsx +61 -0
  171. package/blocks/integrations/components/integration-list.tsx +4 -4
  172. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  173. package/blocks/integrations/components/migration-panel.tsx +133 -0
  174. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  175. package/blocks/integrations/components/sync-status.tsx +6 -6
  176. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  177. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  178. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  179. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  180. package/blocks/integrations/lib/crm-utils.ts +39 -0
  181. package/blocks/integrations/lib/facts.tsx +13 -0
  182. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  183. package/blocks/integrations/lib/load-more.tsx +141 -0
  184. package/blocks/intent/components/account-intent-score.tsx +5 -5
  185. package/blocks/intent/components/intent-feed.tsx +8 -8
  186. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  187. package/blocks/intent/lib/intent-utils.ts +4 -4
  188. package/blocks/lifecycle/components/account-related.tsx +8 -1
  189. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  190. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  191. package/blocks/lists/README.md +7 -1
  192. package/blocks/lists/block.json +31 -3
  193. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  194. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  195. package/blocks/lists/components/list-picker.tsx +13 -3
  196. package/blocks/lists/components/list-table.tsx +1 -1
  197. package/blocks/lists/components/lists-screen.tsx +4 -2
  198. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  199. package/blocks/lists/components/suppression-manager.tsx +27 -6
  200. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  201. package/blocks/mailboxes/block.json +4 -0
  202. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  203. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  204. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  205. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  206. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  207. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  208. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  209. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  210. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  211. package/blocks/newsletter/README.md +12 -0
  212. package/blocks/newsletter/block.json +117 -0
  213. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  214. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  215. package/blocks/newsletter/components/more-rows.tsx +30 -0
  216. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  217. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  218. package/blocks/newsletter/components/newsletter-screen.tsx +36 -13
  219. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  220. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  221. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  222. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  223. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  224. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  225. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  226. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  227. package/blocks/nurture/components/nurture-form.tsx +5 -8
  228. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  229. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  230. package/blocks/objects/README.md +1 -1
  231. package/blocks/objects/block.json +13 -5
  232. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  233. package/blocks/objects/components/object-record-detail.tsx +8 -1
  234. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  235. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  236. package/blocks/pages/README.md +14 -0
  237. package/blocks/pages/block.json +113 -1
  238. package/blocks/pages/components/more-rows.tsx +30 -0
  239. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  240. package/blocks/pages/components/page-analytics.tsx +176 -0
  241. package/blocks/pages/components/page-clone.tsx +99 -0
  242. package/blocks/pages/components/page-domains.tsx +167 -0
  243. package/blocks/pages/components/page-editor.tsx +1 -1
  244. package/blocks/pages/components/page-generation-status.tsx +48 -0
  245. package/blocks/pages/components/page-tracking.tsx +137 -0
  246. package/blocks/pages/components/pages-screen.tsx +57 -16
  247. package/blocks/pages/components/seo-report.tsx +6 -1
  248. package/blocks/pages/lib/studio.ts +7 -0
  249. package/blocks/phone-numbers/README.md +74 -0
  250. package/blocks/phone-numbers/block.json +262 -0
  251. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  252. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  253. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  254. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  255. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  256. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  257. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  258. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  259. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  260. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  261. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  262. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  263. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  264. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  265. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  266. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  267. package/blocks/pickers/README.md +23 -0
  268. package/blocks/pickers/block.json +103 -0
  269. package/blocks/pickers/components/company-picker.tsx +90 -0
  270. package/blocks/pickers/components/contact-picker.tsx +131 -0
  271. package/blocks/pickers/components/member-picker.tsx +99 -0
  272. package/blocks/pickers/components/search-picker.tsx +98 -0
  273. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  274. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  275. package/blocks/pickers/lib/use-debounced.ts +13 -0
  276. package/blocks/quotes/README.md +13 -0
  277. package/blocks/quotes/block.json +48 -1
  278. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  279. package/blocks/quotes/components/quote-builder.tsx +1 -1
  280. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  281. package/blocks/quotes/components/quote-list.tsx +5 -3
  282. package/blocks/quotes/components/quote-related.tsx +3 -2
  283. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  284. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  285. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  286. package/blocks/radar/components/radar-overview.tsx +4 -4
  287. package/blocks/radar/components/radar-screen.tsx +2 -2
  288. package/blocks/sequencer/README.md +6 -0
  289. package/blocks/sequencer/block.json +28 -1
  290. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  291. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  292. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  293. package/blocks/sequencer/components/sequence-builder.tsx +19 -20
  294. package/blocks/sequencer/components/sequence-detail.tsx +4 -2
  295. package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
  296. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  297. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  298. package/blocks/sequencer/components/sequences-screen.tsx +3 -0
  299. package/blocks/sequencer/components/step-editor.tsx +11 -11
  300. package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
  301. package/blocks/social-ads/README.md +8 -0
  302. package/blocks/social-ads/block.json +137 -4
  303. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  304. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  305. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  306. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  307. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  308. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  309. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  310. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  311. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  312. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  313. package/blocks/social-ads/components/post-composer.tsx +4 -2
  314. package/blocks/social-ads/components/post-detail.tsx +3 -0
  315. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  316. package/blocks/social-ads/components/social-performance.tsx +80 -0
  317. package/blocks/social-ads/components/social-screen.tsx +33 -6
  318. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  319. package/blocks/surveys/README.md +91 -0
  320. package/blocks/surveys/block.json +299 -0
  321. package/blocks/surveys/components/account-nps.tsx +69 -0
  322. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  323. package/blocks/surveys/components/org-summary.tsx +82 -0
  324. package/blocks/surveys/components/sending-domains.tsx +113 -0
  325. package/blocks/surveys/components/survey-form.tsx +126 -0
  326. package/blocks/surveys/components/survey-responses.tsx +145 -0
  327. package/blocks/surveys/components/survey-results.tsx +72 -0
  328. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  329. package/blocks/surveys/components/survey-send.tsx +162 -0
  330. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  331. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  332. package/blocks/surveys/lib/surveys-types.ts +228 -0
  333. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  334. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  335. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  336. package/blocks/visitors/components/snippet-install.tsx +18 -9
  337. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  338. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  339. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  340. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  341. package/blocks/webchat/components/conversation-list.tsx +1 -1
  342. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  343. package/blocks/workbench/components/run-progress.tsx +1 -1
  344. package/blocks/workbench/components/workbench-grid.tsx +1 -1
  345. package/blocks/workbench/components/workbench-screen.tsx +15 -5
  346. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  347. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  348. package/dist/cli.js +536 -392
  349. package/dist/index.browser.js +1 -1
  350. package/dist/index.browser.js.map +1 -1
  351. package/dist/index.browser.mjs +1 -1
  352. package/dist/index.browser.mjs.map +1 -1
  353. package/dist/index.d.mts +2 -2
  354. package/dist/index.d.ts +2 -2
  355. package/dist/index.js +1 -1
  356. package/dist/index.js.map +1 -1
  357. package/dist/index.mjs +1 -1
  358. package/dist/index.mjs.map +1 -1
  359. package/dist/react.browser.js +1 -1
  360. package/dist/react.browser.js.map +1 -1
  361. package/dist/react.browser.mjs +1 -1
  362. package/dist/react.browser.mjs.map +1 -1
  363. package/dist/react.d.mts +2 -2
  364. package/dist/react.d.ts +2 -2
  365. package/dist/react.js +1 -1
  366. package/dist/react.js.map +1 -1
  367. package/dist/react.mjs +1 -1
  368. package/dist/react.mjs.map +1 -1
  369. package/dist/types/blocks/brand.d.ts +11 -2
  370. package/dist/types/blocks/create.d.ts +7 -3
  371. package/dist/types/blocks/doctor.d.ts +1 -0
  372. package/dist/types/blocks/flows.d.ts +7 -1
  373. package/dist/types/blocks/orgBrand.d.ts +34 -0
  374. package/dist/types/cli.d.ts +3 -0
  375. package/dist/types/version.d.ts +2 -2
  376. package/docs/api/desk.d.ts +2 -2
  377. package/docs/api/desk.md +3 -3
  378. package/package.json +1 -1
  379. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  380. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  381. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  382. package/stacks/6sense.json +17 -4
  383. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  384. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  385. package/stacks/apollo/pages/contacts.tsx.tmpl +29 -95
  386. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  387. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  388. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  389. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  390. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  391. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  392. package/stacks/apollo.json +36 -8
  393. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  394. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  395. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  396. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  397. package/stacks/calendly.json +8 -4
  398. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  399. package/stacks/clay/pages/contacts.tsx.tmpl +12 -0
  400. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  401. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  402. package/stacks/clay.json +16 -3
  403. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  404. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  405. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  406. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  407. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  408. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  409. package/stacks/gong.json +23 -6
  410. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  411. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  412. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  413. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  414. package/stacks/hootsuite.json +9 -4
  415. package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
  416. package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
  417. package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
  418. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  419. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  420. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  421. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  422. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  423. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  424. package/stacks/hubspot.json +21 -5
  425. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  426. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  427. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  428. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  429. package/stacks/intercom.json +8 -4
  430. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  431. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  432. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  433. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  434. package/stacks/mailchimp.json +20 -5
  435. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  436. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  437. package/stacks/outreach/pages/contacts.tsx.tmpl +27 -66
  438. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  439. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  440. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  441. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  442. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  443. package/stacks/outreach.json +25 -7
  444. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  445. package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -35
  446. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  447. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  448. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  449. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  450. package/stacks/pipedrive.json +4 -2
  451. package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
  452. package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
  453. package/stacks/salesforce/pages/deals.tsx.tmpl +10 -1
  454. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  455. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  456. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  457. package/stacks/salesforce.json +7 -3
  458. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  459. package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
  460. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  461. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  462. package/stacks/zoominfo.json +16 -3
  463. package/styles/styles.css +1 -1
  464. package/templates/next-app/app/globals.css.tmpl +349 -64
  465. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  466. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -0,0 +1,111 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SkillLibrary: every skill in the workspace (your own plus graph8's defaults) in one list. Search by name, filter by kind
5
+ * and by enabled, open one to edit it (SkillEditor), write a new LLM or API skill, or start from a template. A skill that no
6
+ * agent uses yet can be attached from an agent's Skills tab.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. listSkills GET /skills?runtime_type&enabled&include_system (read) returns every skill, no cap
10
+ * 2. listTemplates GET /skills/templates (read)
11
+ * 3. createFromTemplate POST /skills/from-template { template_id, name } (write)
12
+ * 4. SkillEditor see skill-editor.tsx (getSkill, updateSkill, deleteSkill, createSkill, validateTemplate, extractVariables, listModels)
13
+ */
14
+ import { useMemo, useRef, useState } from "react";
15
+ import { Wand2 } from "lucide-react";
16
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Skeleton } from "@graph8/sdk/ui";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { EmptyState } from "@/g8/components/empty-state";
19
+ import { StatusPill } from "@/g8/components/screen/status-pill";
20
+ import { Toolbar } from "@/g8/components/screen/toolbar";
21
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
22
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
23
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
24
+ import { listOf } from "../lib/agent-types";
25
+ import type { SkillDetail } from "../lib/skill-utils";
26
+ import { skillOf } from "../lib/skill-utils";
27
+ import { SkillEditor } from "./skill-editor";
28
+ import type { SkillEditorTarget } from "./skill-editor";
29
+
30
+ export interface SkillLibraryProps {
31
+ className?: string;
32
+ }
33
+
34
+ interface Template { template_id?: string; action_id?: string; id?: string; name: string; description?: string | null; runtime_type?: string }
35
+ const tid = (t: Template): string => t.template_id ?? t.action_id ?? t.id ?? "";
36
+ const KINDS = [{ id: "", label: "All" }, { id: "llm", label: "LLM" }, { id: "api", label: "API" }];
37
+ const STATES = [{ id: "", label: "Any state" }, { id: "true", label: "Enabled" }, { id: "false", label: "Disabled" }];
38
+
39
+ function FromTemplate({ open, onClose, onCreated }: { open: boolean; onClose: () => void; onCreated: (id?: string) => void }) {
40
+ const [picked, setPicked] = useState<Template | null>(null);
41
+ const [name, setName] = useState("");
42
+ const made = useRef<{ id?: string } | null>(null);
43
+ const templates = useG8Query((signal) => g8Get("/skills/templates", undefined, { signal }), [], { enabled: open });
44
+ const create = useG8Mutation(async (v: { id: string; name: string }) => {
45
+ if (made.current) return made.current.id;
46
+ const id = skillOf(await g8Post("/skills/from-template", { template_id: v.id, name: v.name }))?.action_id;
47
+ made.current = { id };
48
+ return id;
49
+ }, { onSuccess: (id) => { made.current = null; setPicked(null); onCreated(id); } });
50
+ const rows = listOf<Template>(templates.data, "actions", "templates", "items");
51
+ return (
52
+ <Dialog open={open} onOpenChange={(o) => !o && !create.loading && onClose()}>
53
+ <DialogContent>
54
+ <DialogHeader><DialogTitle>New skill from a template</DialogTitle><DialogDescription>Templates are ready-made skills. Name yours and edit it afterwards.</DialogDescription></DialogHeader>
55
+ {templates.error ? <ApiError error={templates.error} title="Could not load templates" onRetry={() => void templates.refetch()} /> : null}
56
+ {templates.loading && !templates.data ? <Skeleton className="h-16 w-full" /> : null}
57
+ <ul className="max-h-64 divide-y overflow-y-auto rounded-md border">
58
+ {rows.map((t) => (
59
+ <li key={tid(t)}><button type="button" aria-pressed={picked != null && tid(picked) === tid(t)} className={`block w-full px-3 py-2 text-left text-sm ${picked != null && tid(picked) === tid(t) ? "bg-accent" : ""}`} onClick={() => { made.current = null; setPicked(t); setName(t.name); }}>
60
+ <span className="font-medium">{t.name}</span>{t.runtime_type ? <span className="ml-2 text-xs text-muted-foreground">{t.runtime_type}</span> : null}
61
+ {t.description ? <span className="block text-xs text-muted-foreground">{t.description}</span> : null}
62
+ </button></li>
63
+ ))}
64
+ </ul>
65
+ {!templates.loading && !templates.error && rows.length === 0 ? <p className="text-sm text-muted-foreground">There are no templates yet.</p> : null}
66
+ {picked ? <div className="flex flex-col gap-1"><Label htmlFor="skill-from-template">Name</Label><Input id="skill-from-template" value={name} onChange={(e) => setName(e.target.value)} /></div> : null}
67
+ {create.error ? <ApiError error={create.error} title="Could not create the skill" /> : null}
68
+ <DialogFooter><Button variant="ghost" disabled={create.loading} onClick={onClose}>Cancel</Button><Button loading={create.loading} disabled={!picked || !name.trim()} onClick={() => picked && void create.mutate({ id: tid(picked), name: name.trim() })}>Create skill</Button></DialogFooter>
69
+ </DialogContent>
70
+ </Dialog>
71
+ );
72
+ }
73
+
74
+ export function SkillLibrary({ className }: SkillLibraryProps) {
75
+ const [search, setSearch] = useState("");
76
+ const [kind, setKind] = useState("");
77
+ const [state, setState] = useState("");
78
+ const [target, setTarget] = useState<SkillEditorTarget | null>(null);
79
+ const [fromTemplate, setFromTemplate] = useState(false);
80
+ const skills = useG8Query((signal) => g8Get("/skills", { runtime_type: kind || undefined, enabled: state || undefined, include_system: true }, { signal }), [kind, state]);
81
+ const all = listOf<SkillDetail>(skills.data, "actions", "skills", "items");
82
+ const q = search.trim().toLowerCase();
83
+ const rows = useMemo(() => (q ? all.filter((s) => `${s.name} ${s.description ?? ""} ${s.category ?? ""}`.toLowerCase().includes(q)) : all), [all, q]);
84
+
85
+ return (
86
+ <div className={`flex flex-col gap-3 ${className ?? ""}`}>
87
+ <Toolbar search={{ value: search, onChange: setSearch, placeholder: "Search skills" }}
88
+ filters={<><div role="group" aria-label="Skill kind" className="flex gap-1">{KINDS.map((k) => <Button key={k.id || "all"} size="sm" variant={kind === k.id ? "default" : "outline"} onClick={() => setKind(k.id)}>{k.label}</Button>)}</div>
89
+ <select aria-label="Skill state" className="h-8 rounded-md border bg-background px-2 text-sm" value={state} onChange={(e) => setState(e.target.value)}>{STATES.map((s) => <option key={s.id} value={s.id}>{s.label}</option>)}</select></>}
90
+ actions={<><Button size="sm" variant="outline" onClick={() => setFromTemplate(true)}>From a template</Button><Button size="sm" variant="outline" onClick={() => setTarget({ kind: "new", runtime: "api" })}>New API skill</Button><Button size="sm" onClick={() => setTarget({ kind: "new", runtime: "llm" })}>New LLM skill</Button></>} />
91
+ {skills.error ? <ApiError error={skills.error} title="Could not load skills" onRetry={() => void skills.refetch()} /> : null}
92
+ {skills.loading && !skills.data ? <div className="space-y-2">{[0, 1, 2].map((i) => <Skeleton key={i} className="h-12 w-full" />)}</div> : null}
93
+ <ul className="divide-y rounded-md border">
94
+ {rows.map((s) => (
95
+ <li key={s.action_id}>
96
+ <button type="button" data-testid={`skill-${s.action_id}`} className="flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm" onClick={() => setTarget({ kind: "edit", id: s.action_id })}>
97
+ <span className="min-w-0"><span className="block truncate font-medium">{s.name}</span>{s.description ? <span className="block truncate text-xs text-muted-foreground">{s.description}</span> : null}</span>
98
+ <span className="flex shrink-0 items-center gap-2">{s.category ? <span className="text-xs text-muted-foreground">{s.category}</span> : null}<StatusPill tone="neutral">{s.runtime_type ?? "skill"}</StatusPill><StatusPill tone={s.enabled === false ? "warning" : "positive"} dot>{s.enabled === false ? "disabled" : "enabled"}</StatusPill></span>
99
+ </button>
100
+ </li>
101
+ ))}
102
+ </ul>
103
+ {!skills.loading && !skills.error && rows.length === 0 ? (
104
+ <EmptyState icon={<Wand2 strokeWidth={1.5} />} title={all.length ? "No skills match" : "No skills yet"} description="A skill is one thing an agent can do: write a summary, call an API."
105
+ action={all.length ? undefined : <Button onClick={() => setTarget({ kind: "new", runtime: "llm" })}>New LLM skill</Button>} />
106
+ ) : null}
107
+ <SkillEditor target={target} onClose={() => setTarget(null)} onChanged={() => void skills.refetch()} />
108
+ <FromTemplate open={fromTemplate} onClose={() => setFromTemplate(false)} onCreated={(id) => { setFromTemplate(false); void skills.refetch(); if (id) setTarget({ kind: "edit", id }); }} />
109
+ </div>
110
+ );
111
+ }
@@ -58,7 +58,7 @@ export interface SkillRow {
58
58
  description?: string | null;
59
59
  runtime_type?: "llm" | "api" | string;
60
60
  category?: string | null;
61
- llm_config?: { prompt?: string; model?: string } | null;
61
+ llm_config?: { prompt_template?: string; model?: string } | null;
62
62
  }
63
63
 
64
64
  export interface AgentRun {
@@ -112,3 +112,9 @@ export function pretty(value: unknown): string {
112
112
  return String(value);
113
113
  }
114
114
  }
115
+
116
+ /** The id of the agent a create or clone answered with, whatever envelope it came in. */
117
+ export function agentIdOf(res: G8Envelope<unknown> | null | undefined): string {
118
+ const b = bodyOf<{ agent?: { agent_id?: string }; agent_id?: string; new_agent_id?: string }>(res);
119
+ return b.agent?.agent_id ?? b.agent_id ?? b.new_agent_id ?? "";
120
+ }
@@ -0,0 +1,13 @@
1
+ "use client";
2
+
3
+ /** Renders an open-shaped API object (health, scorecard, status) as labelled facts without guessing its fields. */
4
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
5
+
6
+ const label = (k: string): string => { const t = k.replace(/_/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); };
7
+ const show = (v: unknown): string => (v == null ? "" : typeof v === "string" ? v : typeof v === "number" || typeof v === "boolean" ? String(v) : JSON.stringify(v));
8
+
9
+ export function FactList({ data, empty = "Nothing reported yet." }: { data: unknown; empty?: string }) {
10
+ const entries = Object.entries((data && typeof data === "object" && !Array.isArray(data) ? data : {}) as Record<string, unknown>).filter(([, v]) => v != null && v !== "");
11
+ if (entries.length === 0) return <p className="text-sm text-muted-foreground">{empty}</p>;
12
+ return <KeyValueList columns={2} items={entries.map(([k, v]) => ({ label: label(k), value: show(v), wide: typeof v === "object", mono: typeof v === "object" }))} />;
13
+ }
@@ -0,0 +1,49 @@
1
+ "use client";
2
+
3
+ /** Shapes of the company knowledge (the Knowledge Brain) the agents block reads, and the body helpers for authoring. */
4
+
5
+ export interface KnowledgeSummary {
6
+ concept_id: string;
7
+ concept_type: string;
8
+ title: string;
9
+ source_type?: string;
10
+ knowledge_status?: string;
11
+ trust_tier?: string;
12
+ visibility?: string;
13
+ stale?: boolean;
14
+ logical_path?: string;
15
+ }
16
+
17
+ export interface KnowledgeConcept {
18
+ summary: KnowledgeSummary;
19
+ markdown?: string;
20
+ source_refs?: Array<{ resource?: string; title?: string | null; locator?: string | null; checksum?: string | null }>;
21
+ relationships?: string[];
22
+ warnings?: string[];
23
+ capabilities?: Record<string, boolean>;
24
+ }
25
+
26
+ export interface KnowledgeInput {
27
+ title: string;
28
+ concept_type: string;
29
+ body: string;
30
+ }
31
+
32
+ export const LIFECYCLES = ["draft", "stable", "deprecated"] as const;
33
+
34
+ /** Rendered markdown can start with a `---` frontmatter block; the authoring body must not carry it. */
35
+ export function bodyFromMarkdown(markdown: string | undefined): string {
36
+ const text = markdown ?? "";
37
+ const m = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/.exec(text);
38
+ return (m ? text.slice(m[0].length) : text).replace(/^\s+/, "");
39
+ }
40
+
41
+ /** Only authored entries can be edited, amended, verified or deprecated; generated ones come from the product's own records. */
42
+ export function isAuthored(s: KnowledgeSummary | undefined): boolean {
43
+ return Boolean(s?.source_type && /authored/i.test(s.source_type));
44
+ }
45
+
46
+ export function conceptOf(res: unknown): KnowledgeConcept | undefined {
47
+ const d = ((res as { data?: unknown } | null)?.data ?? res) as Partial<KnowledgeConcept> | null;
48
+ return d && d.summary && d.summary.concept_id ? (d as KnowledgeConcept) : undefined;
49
+ }
@@ -0,0 +1,141 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Paging helpers for the agents block, in the same shape as the foundation's paging primitives (a list never stops silently at a
5
+ * cap: it shows "Load more" while the last page was full).
6
+ *
7
+ * useLoadMoreList offset + limit APIs (the next page starts at items.length).
8
+ * useGrowingList limit-only APIs (a bigger limit refetches the list from the start).
9
+ * LoadMore the footer button plus the error of a failed "more" request.
10
+ */
11
+ import { useCallback, useEffect, useRef, useState } from "react";
12
+ import { Button } from "@graph8/sdk/ui";
13
+ import { ApiError } from "@/g8/components/api-error";
14
+
15
+ export interface LoadMoreList<T> {
16
+ items: T[];
17
+ /** The first page is loading (or reloading after the deps changed). */
18
+ loading: boolean;
19
+ loadingMore: boolean;
20
+ error: unknown;
21
+ moreError: unknown;
22
+ hasMore: boolean;
23
+ /** A limit-only API stopped at its own maximum: there are more rows the API will not return. */
24
+ capped?: boolean;
25
+ loadMore: () => void;
26
+ reload: () => void;
27
+ }
28
+
29
+ interface Options {
30
+ pageSize?: number;
31
+ enabled?: boolean;
32
+ /** useGrowingList: the largest `limit` the API accepts. */
33
+ max?: number;
34
+ }
35
+
36
+ /** Offset paging. `fetchPage(offset, limit)` returns that page's rows; a full page means there may be more. */
37
+ export function useLoadMoreList<T>(
38
+ fetchPage: (offset: number, limit: number, signal: AbortSignal) => Promise<T[]>,
39
+ deps: readonly unknown[],
40
+ { pageSize = 50, enabled = true }: Options = {},
41
+ ): LoadMoreList<T> {
42
+ const [items, setItems] = useState<T[]>([]);
43
+ const [loading, setLoading] = useState(enabled);
44
+ const [loadingMore, setLoadingMore] = useState(false);
45
+ const [error, setError] = useState<unknown>(null);
46
+ const [moreError, setMoreError] = useState<unknown>(null);
47
+ const [hasMore, setHasMore] = useState(false);
48
+ const [tick, setTick] = useState(0);
49
+ const fetchRef = useRef(fetchPage);
50
+ fetchRef.current = fetchPage;
51
+ const gen = useRef(0);
52
+ const count = useRef(0);
53
+ const sig = JSON.stringify([...deps, enabled, pageSize]);
54
+ const lastSig = useRef(sig);
55
+
56
+ useEffect(() => {
57
+ if (!enabled) { setLoading(false); return; }
58
+ const mine = ++gen.current;
59
+ const ctl = new AbortController();
60
+ // A reload() keeps showing the rows it has until the fresh first page lands; a changed filter starts from nothing.
61
+ const fresh = lastSig.current !== sig;
62
+ lastSig.current = sig;
63
+ setLoading(true); setError(null); setMoreError(null);
64
+ if (fresh) { setItems([]); setHasMore(false); count.current = 0; }
65
+ // A reload() refetches as many pages as were showing, so a row opened from page 2 does not vanish; a changed filter starts at page 1.
66
+ const want = fresh ? pageSize : Math.max(count.current, pageSize);
67
+ const load = async (): Promise<T[]> => {
68
+ const all: T[] = [];
69
+ let last = pageSize;
70
+ while (all.length < want && last >= pageSize) {
71
+ const page = await fetchRef.current(all.length, pageSize, ctl.signal);
72
+ all.push(...page);
73
+ last = page.length;
74
+ }
75
+ return all;
76
+ };
77
+ load().then((rows) => {
78
+ if (gen.current !== mine) return;
79
+ setItems(rows); count.current = rows.length; setHasMore(rows.length > 0 && rows.length % pageSize === 0); setLoading(false);
80
+ }, (e) => { if (gen.current === mine) { setError(e); setLoading(false); } });
81
+ return () => ctl.abort();
82
+ }, [...deps, enabled, pageSize, tick]); // eslint-disable-line react-hooks/exhaustive-deps
83
+
84
+ const loadMore = useCallback(() => {
85
+ const mine = gen.current;
86
+ setLoadingMore(true); setMoreError(null);
87
+ fetchRef.current(count.current, pageSize, new AbortController().signal).then((rows) => {
88
+ if (gen.current !== mine) return;
89
+ count.current += rows.length;
90
+ setItems((prev) => [...prev, ...rows]); setHasMore(rows.length >= pageSize); setLoadingMore(false);
91
+ }, (e) => { if (gen.current === mine) { setMoreError(e); setLoadingMore(false); } });
92
+ }, [pageSize]);
93
+
94
+ return { items, loading, loadingMore, error, moreError, hasMore, loadMore, reload: () => setTick((t) => t + 1) };
95
+ }
96
+
97
+ /** Limit-only APIs: asks for `step` rows, and "Load more" asks again for `step` more from the start. */
98
+ export function useGrowingList<T>(
99
+ fetchRows: (limit: number, signal: AbortSignal) => Promise<T[]>,
100
+ deps: readonly unknown[],
101
+ { pageSize = 50, enabled = true, max = Infinity }: Options = {},
102
+ ): LoadMoreList<T> {
103
+ const [limit, setLimit] = useState(pageSize);
104
+ const [rows, setRows] = useState<T[]>([]);
105
+ const [loading, setLoading] = useState(enabled);
106
+ const [error, setError] = useState<unknown>(null);
107
+ const [tick, setTick] = useState(0);
108
+ const [growing, setGrowing] = useState(false);
109
+ const fetchRef = useRef(fetchRows);
110
+ fetchRef.current = fetchRows;
111
+ const depKey = JSON.stringify(deps);
112
+ const lastDeps = useRef(depKey);
113
+
114
+ useEffect(() => { if (lastDeps.current !== depKey) { lastDeps.current = depKey; setLimit(pageSize); } }, [depKey, pageSize]);
115
+ useEffect(() => {
116
+ if (!enabled) { setLoading(false); return; }
117
+ let live = true;
118
+ const ctl = new AbortController();
119
+ setLoading(true); setError(null);
120
+ fetchRef.current(limit, ctl.signal).then((r) => { if (live) { setRows(r); setLoading(false); setGrowing(false); } }, (e) => { if (live) { setError(e); setLoading(false); setGrowing(false); } });
121
+ return () => { live = false; ctl.abort(); };
122
+ }, [limit, depKey, enabled, tick]); // eslint-disable-line react-hooks/exhaustive-deps
123
+
124
+ return {
125
+ items: rows, loading: loading && !growing, loadingMore: loading && growing, error: growing ? null : error, moreError: growing ? error : null,
126
+ hasMore: !loading && rows.length >= limit && limit < max, capped: !loading && rows.length >= limit && limit >= max,
127
+ loadMore: () => { setGrowing(true); setLimit((l) => Math.min(max, l + pageSize)); }, reload: () => setTick((t) => t + 1),
128
+ };
129
+ }
130
+
131
+ /** The footer of a paged list. Renders nothing when everything is shown. */
132
+ export function LoadMore<T>({ list, noun = "rows" }: { list: LoadMoreList<T>; noun?: string }) {
133
+ if (!list.hasMore && !list.moreError && !list.capped) return null;
134
+ return (
135
+ <div className="flex flex-col items-center gap-2 py-2">
136
+ {list.moreError ? <ApiError compact error={list.moreError} title={`Could not load more ${noun}`} onRetry={list.loadMore} /> : null}
137
+ {list.capped ? <p className="text-xs text-muted-foreground">Showing the latest {list.items.length} {noun}. The API returns no more than this.</p> : null}
138
+ {list.hasMore && !list.moreError ? <Button size="sm" variant="outline" loading={list.loadingMore} onClick={list.loadMore}>Load more {noun}</Button> : null}
139
+ </div>
140
+ );
141
+ }
@@ -0,0 +1,91 @@
1
+ "use client";
2
+
3
+ /** Shapes and diffing for the skill library. Skills answer un-enveloped or enveloped, flat or under `action`; `skillOf` reads all of them. */
4
+ import type { G8Envelope } from "@/g8/lib/g8-client";
5
+ import { bodyOf } from "./agent-types";
6
+
7
+ export interface SkillDetail {
8
+ action_id: string;
9
+ name: string;
10
+ description?: string | null;
11
+ category?: string | null;
12
+ runtime_type?: "llm" | "api" | string;
13
+ enabled?: boolean;
14
+ requires_approval?: boolean;
15
+ llm_config?: { model?: string; temperature?: number; max_tokens?: number; prompt_template?: string; prompt?: string } | null;
16
+ api_config?: { endpoint?: string; method?: string; headers?: Record<string, string> | null; body_template?: string | null; requires_auth?: boolean } | null;
17
+ }
18
+
19
+ export interface SkillDraft {
20
+ name: string;
21
+ description: string;
22
+ category: string;
23
+ enabled: boolean;
24
+ model: string;
25
+ prompt_template: string;
26
+ temperature: string;
27
+ max_tokens: string;
28
+ endpoint: string;
29
+ method: string;
30
+ headers: string;
31
+ body_template: string;
32
+ requires_auth: boolean;
33
+ }
34
+
35
+ export const BLANK_DRAFT: SkillDraft = { name: "", description: "", category: "", enabled: true, model: "", prompt_template: "", temperature: "0.7", max_tokens: "1000", endpoint: "", method: "POST", headers: "", body_template: "", requires_auth: false };
36
+ export const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const;
37
+
38
+ export function skillOf(res: G8Envelope<unknown> | null | undefined): SkillDetail | undefined {
39
+ const b = bodyOf<{ action?: SkillDetail } & Partial<SkillDetail>>(res);
40
+ const s = (b.action ?? b) as SkillDetail;
41
+ return s && s.action_id ? s : undefined;
42
+ }
43
+
44
+ export function draftOf(s: SkillDetail): SkillDraft {
45
+ const l = s.llm_config ?? {}, a = s.api_config ?? {};
46
+ return {
47
+ name: s.name, description: s.description ?? "", category: s.category ?? "", enabled: s.enabled !== false,
48
+ model: l.model ?? "", prompt_template: l.prompt_template ?? l.prompt ?? "", temperature: String(l.temperature ?? 0.7), max_tokens: String(l.max_tokens ?? 1000),
49
+ endpoint: a.endpoint ?? "", method: a.method ?? "POST", headers: a.headers && Object.keys(a.headers).length ? JSON.stringify(a.headers, null, 2) : "", body_template: a.body_template ?? "", requires_auth: Boolean(a.requires_auth),
50
+ };
51
+ }
52
+
53
+ /** The headers textarea as an object; undefined when blank, null when it is not a JSON object of strings. */
54
+ export function parseHeaders(text: string): Record<string, string> | undefined | null {
55
+ if (!text.trim()) return undefined;
56
+ try {
57
+ const v = JSON.parse(text) as unknown;
58
+ if (!v || typeof v !== "object" || Array.isArray(v) || !Object.values(v).every((x) => typeof x === "string")) return null;
59
+ return v as Record<string, string>;
60
+ } catch { return null; }
61
+ }
62
+
63
+ /**
64
+ * The PUT body: only what changed against the LOADED skill. A runtime config is sent whole when any field in it changed
65
+ * (voice validates it as a unit: a model and a prompt, or an endpoint).
66
+ */
67
+ export function skillPatch(loaded: SkillDetail, d: SkillDraft): Record<string, unknown> {
68
+ const before = draftOf(loaded);
69
+ const changed = (...keys: (keyof SkillDraft)[]) => keys.some((k) => before[k] !== d[k]);
70
+ const out: Record<string, unknown> = {};
71
+ if (changed("name")) out.name = d.name.trim();
72
+ if (changed("description")) out.description = d.description.trim();
73
+ if (changed("category")) out.category = d.category.trim();
74
+ if (changed("enabled")) out.enabled = d.enabled;
75
+ if (loaded.runtime_type === "llm" && changed("model", "prompt_template", "temperature", "max_tokens")) {
76
+ out.llm_config = { model: d.model, prompt_template: d.prompt_template, temperature: Number(d.temperature), max_tokens: Number(d.max_tokens) };
77
+ }
78
+ if (loaded.runtime_type === "api" && changed("endpoint", "method", "headers", "body_template", "requires_auth")) {
79
+ const headers = parseHeaders(d.headers);
80
+ out.api_config = { endpoint: d.endpoint.trim(), method: d.method, body_template: d.body_template || undefined, requires_auth: d.requires_auth, ...(headers ? { headers } : {}) };
81
+ }
82
+ return out;
83
+ }
84
+
85
+ /** The POST body for a new skill. */
86
+ export function newSkillBody(runtime: "llm" | "api", d: SkillDraft): Record<string, unknown> {
87
+ const base = { name: d.name.trim(), description: d.description.trim() || undefined, category: d.category.trim() || undefined, runtime_type: runtime };
88
+ if (runtime === "llm") return { ...base, llm_config: { model: d.model, prompt_template: d.prompt_template, temperature: Number(d.temperature), max_tokens: Number(d.max_tokens) } };
89
+ const headers = parseHeaders(d.headers);
90
+ return { ...base, api_config: { endpoint: d.endpoint.trim(), method: d.method, body_template: d.body_template || undefined, requires_auth: d.requires_auth, ...(headers ? { headers } : {}) } };
91
+ }
@@ -34,7 +34,7 @@ export async function buildAgent() {
34
34
  body: {
35
35
  name: "Summarise the lead",
36
36
  runtime_type: "llm",
37
- llm_config: { prompt: "Summarise {contact_name} in two sentences for the sales team.", model: "gpt-4o-mini" },
37
+ llm_config: { model: "gpt-4o-mini", prompt_template: "Summarise {contact_name} in two sentences for the sales team." },
38
38
  },
39
39
  });
40
40
  const actionId = (skill as { action_id?: string }).action_id;
@@ -23,10 +23,13 @@
23
23
  * stale-cache notice instead of silent old numbers.
24
24
  * The tab and range are kept in the URL (?tab=, ?days=) so a link opens the same view.
25
25
  */
26
- import { useState, type ReactNode } from "react";
26
+ import { useEffect, useState, type ReactNode } from "react";
27
27
  import { BarChart3, FileText, Plus } from "lucide-react";
28
28
  import { Button, Skeleton } from "@graph8/sdk/ui";
29
29
  import { ApiError } from "@/g8/components/api-error";
30
+ import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
31
+ import { byOffset, hasNextOf } from "@/g8/lib/paging";
32
+ import { LoadMore } from "@/g8/components/paging-notices";
30
33
  import { EmptyState } from "@/g8/components/empty-state";
31
34
  import { DetailPanel } from "@/g8/components/screen/detail-panel";
32
35
  import { FormSheet } from "@/g8/components/screen/form-sheet";
@@ -128,6 +131,9 @@ function Overview({ days, nameFor }: { days: number; nameFor?: (email: string) =
128
131
  );
129
132
  }
130
133
 
134
+ /** Reports per read; "Load more" appends the next page (the list used to stop silently at 50). */
135
+ const REPORT_PAGE = 50;
136
+
131
137
  function ReportsPane({ objectSlug }: { objectSlug?: string }) {
132
138
  const brand = useBrand();
133
139
  const [selected, setSelected] = useUrlState("id", "");
@@ -136,11 +142,17 @@ function ReportsPane({ objectSlug }: { objectSlug?: string }) {
136
142
  const [creating, setCreating] = useState(false);
137
143
  // Results live here so switching between Result and Edit never runs the report again (each run is a write).
138
144
  const [results, setResults] = useState<Record<string, ReportResultData | null>>({});
139
- const list = useG8Query(async (signal) => reportBody<{ reports?: SavedReport[] }>(await g8Get("/reports", { object_slug: objectSlug || undefined, limit: 50 }, { signal })).reports ?? [], [objectSlug]);
145
+ const list = useLoadMoreList(byOffset<SavedReport>(REPORT_PAGE, async (offset, signal) => {
146
+ const env = await g8Get("/reports", { object_slug: objectSlug || undefined, limit: REPORT_PAGE, offset: offset || undefined }, { signal });
147
+ const rows = reportBody<{ reports?: SavedReport[] }>(env).reports ?? [];
148
+ return { rows, hasNext: hasNextOf(env, REPORT_PAGE, rows.length) };
149
+ }), [objectSlug]);
140
150
  const switchedOff = (list.error as { status?: number } | undefined)?.status === 404;
141
151
  const q = search.trim().toLowerCase();
142
152
  const rows = (list.data ?? []).filter((r) => !q || `${r.title} ${r.source.plural_noun}`.toLowerCase().includes(q));
143
153
  const open = (list.data ?? []).find((r) => r.id === selected);
154
+ // A deep link to a report on a later page: keep loading pages until it is found.
155
+ useEffect(() => { if (selected && !open && list.hasMore && !list.loading && !list.loadingMore) list.loadMore(); }, [selected, open, list.hasMore, list.loading, list.loadingMore]); // eslint-disable-line react-hooks/exhaustive-deps
144
156
 
145
157
  const detail = open ? (
146
158
  <DetailPanel title={open.title} subtitle={open.source.plural_noun} badges={<StatusPill tone={open.visibility === "private" ? "neutral" : "info"}>{open.visibility}</StatusPill>} onClose={() => { setSelected(""); setView(""); }} onExpand={() => setView("full")}
@@ -148,7 +160,7 @@ function ReportsPane({ objectSlug }: { objectSlug?: string }) {
148
160
  { id: "result", label: "Result", content: <ReportDetail key={open.id} reportId={open.id} result={results[open.id]} onResult={(r) => setResults((m) => ({ ...m, [open.id]: r }))} onDeleted={() => { setSelected(""); void list.refetch(); }} /> },
149
161
  { id: "edit", label: "Edit", content: <ReportBuilder key={open.id} hideList reportId={open.id} autoRun={false} onSaved={() => { setResults((m) => Object.fromEntries(Object.entries(m).filter(([k]) => k !== open.id))); void list.refetch(); }} /> }, // a saved edit changes the query: drop the stale result
150
162
  ]} />
151
- ) : selected && !list.loading ? <EmptyState title="Report not found" description="It may have been deleted." /> : null;
163
+ ) : selected && !list.loading && !list.hasMore ? <EmptyState title="Report not found" description="It may have been deleted." /> : null;
152
164
 
153
165
  const listView = (
154
166
  <div className="flex flex-col gap-3">
@@ -166,6 +178,7 @@ function ReportsPane({ objectSlug }: { objectSlug?: string }) {
166
178
  </li>
167
179
  ))}
168
180
  </ul>
181
+ <LoadMore shown={(list.data ?? []).length} noun="reports" hasMore={list.hasMore} loading={list.loadingMore} onLoadMore={list.loadMore} />
169
182
  {!list.loading && !list.error && rows.length === 0 ? <EmptyState title={list.data?.length ? "No reports match" : "No saved reports yet"} description="Pick an object, a measure and a grouping, preview it, then save it."
170
183
  action={list.data?.length ? undefined : <Button onClick={() => setCreating(true)}>New report</Button>} /> : null}
171
184
  </div>
@@ -61,7 +61,7 @@ export function KpiTiles({ days: initialDays = 30, tiles = DEFAULT_TILES, classN
61
61
  return (
62
62
  <div key={t.key} className="rounded-lg border p-4">
63
63
  <div className="text-xs uppercase text-muted-foreground">{t.label}</div>
64
- <div className="mt-1 text-2xl font-semibold">{t.kind === "percent" ? formatPercent(raw) : formatNumber(raw)}</div>
64
+ <div className="mt-1 text-2xl font-medium">{t.kind === "percent" ? formatPercent(raw) : formatNumber(raw)}</div>
65
65
  {data && t.hint ? <div className="mt-1 text-xs text-muted-foreground">{t.hint(data)}</div> : null}
66
66
  </div>
67
67
  );
@@ -19,6 +19,9 @@
19
19
  import { useEffect, useRef, useState } from "react";
20
20
  import { Button, Input, Label, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
21
21
  import { ApiError } from "@/g8/components/api-error";
22
+ import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
23
+ import { byOffset, hasNextOf } from "@/g8/lib/paging";
24
+ import { LoadMore } from "@/g8/components/paging-notices";
22
25
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
23
26
  import { EmptyState } from "@/g8/components/empty-state";
24
27
  import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
@@ -114,6 +117,9 @@ export function ResultTable({ result }: { result: ReportResultData }) {
114
117
  );
115
118
  }
116
119
 
120
+ /** Reports per read; "Load more" appends the next page (the list used to stop silently at 50). */
121
+ const REPORT_PAGE = 50;
122
+
117
123
  export function ReportBuilder({ objectSlug: initialSlug = "", onSaved, reportId, hideList, autoRun = true, className }: ReportBuilderProps) {
118
124
  const brand = useBrand();
119
125
  const [slug, setSlug] = useState(initialSlug);
@@ -124,7 +130,11 @@ export function ReportBuilder({ objectSlug: initialSlug = "", onSaved, reportId,
124
130
  const set = <K extends keyof Draft>(k: K, v: Draft[K]) => setDraft((d) => ({ ...d, [k]: v }));
125
131
 
126
132
  const listSlug = useDebounced(slug.trim());
127
- const saved = useG8Query(async (signal) => reportBody<{ reports?: SavedReport[] }>(await g8Get("/reports", { object_slug: listSlug || undefined, limit: 50 }, { signal })).reports ?? [], [listSlug]);
133
+ const saved = useLoadMoreList(byOffset<SavedReport>(REPORT_PAGE, async (offset, signal) => {
134
+ const env = await g8Get("/reports", { object_slug: listSlug || undefined, limit: REPORT_PAGE, offset: offset || undefined }, { signal });
135
+ const rows = reportBody<{ reports?: SavedReport[] }>(env).reports ?? [];
136
+ return { rows, hasNext: hasNextOf(env, REPORT_PAGE, rows.length) };
137
+ }), [listSlug]);
128
138
  const body = () => ({ object_slug: slug, chart_type: draft.chart, config: configOf(draft, loaded?.config as Cfg) });
129
139
  const preview = useG8Mutation(async () => { const r = reportBody<ReportResultData>(await g8Post("/reports/preview", body())); setResult(r); return r; });
130
140
  const run = useG8Mutation(async (r: SavedReport) => { const out = reportBody<ReportResultData>(await g8Post(`/reports/${encodeURIComponent(r.id)}/execute`, {})); setResult(out); return out; });
@@ -162,6 +172,8 @@ export function ReportBuilder({ objectSlug: initialSlug = "", onSaved, reportId,
162
172
  useEffect(() => {
163
173
  const row = reportId ? (saved.data ?? []).find((r) => r.id === reportId) : undefined;
164
174
  if (row && preloaded.current !== reportId) { preloaded.current = reportId ?? null; open(row, autoRun); }
175
+ // The report sits on a later page: keep loading pages until it is found.
176
+ else if (reportId && !row && saved.hasMore && !saved.loading && !saved.loadingMore) saved.loadMore();
165
177
  }); // eslint-disable-line react-hooks/exhaustive-deps
166
178
  const failure = preview.error ?? run.error ?? save.error ?? remove.error;
167
179
  const switchedOff = (saved.error as { status?: number } | undefined)?.status === 404;
@@ -169,7 +181,7 @@ export function ReportBuilder({ objectSlug: initialSlug = "", onSaved, reportId,
169
181
  return (
170
182
  <div className={`grid gap-4 ${hideList ? "" : "lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]"} ${className ?? ""}`}>
171
183
  {hideList ? null : <section aria-label="Saved reports" className="space-y-2">
172
- <h3 className="text-sm font-semibold">Saved reports</h3>
184
+ <h3 className="text-sm font-medium">Saved reports</h3>
173
185
  {switchedOff ? <EmptyState title="Reports are not enabled" description={`Custom-object reports are switched on per organisation in ${brand.linkLabel}.`} /> : saved.error ? <ApiError error={saved.error} title="Could not load saved reports" onRetry={() => void saved.refetch()} /> : null}
174
186
  {(saved.data ?? []).map((r) => (
175
187
  <div key={r.id} className="flex items-center justify-between rounded border p-2 text-sm">
@@ -177,6 +189,7 @@ export function ReportBuilder({ objectSlug: initialSlug = "", onSaved, reportId,
177
189
  {r.can_delete ? <Button size="sm" variant="ghost" aria-label={`Delete ${r.title}`} onClick={() => setDeleting(r)}>Delete</Button> : null}
178
190
  </div>
179
191
  ))}
192
+ <LoadMore shown={(saved.data ?? []).length} noun="reports" hasMore={saved.hasMore} loading={saved.loadingMore} onLoadMore={saved.loadMore} />
180
193
  {!saved.loading && !saved.error && (saved.data ?? []).length === 0 ? <p className="text-sm text-muted-foreground">Nothing saved yet.</p> : null}
181
194
  </section>}
182
195
 
@@ -98,3 +98,9 @@ with the full API key.
98
98
  - `03-bookings-digest.ts`: upcoming bookings, pending approvals, recorded meetings.
99
99
  - `04-calendar-invite.ts`: ad-hoc invite via `calendarInvites.createCalendarInvite` (not part of the block's
100
100
  UI; add the operation to your own proxy allowlist if you call it from the browser).
101
+
102
+ ## Cross-block glue
103
+
104
+ - `MeetingContactLinks` (`components/meeting-contact-links.tsx`): "Open <attendee>" buttons for a booked or recorded meeting, one per attendee who is a CRM contact. Use it in `MeetingsScreen`'s `renderDetailActions`.
105
+
106
+ - `RecordMeetingsTab` (`components/record-meetings-tab.tsx`): meetings booked with a contact (`getContactMeetings`, the API's 50 most recent booking events), each opening `/meetings?sel=booking:<id>`. `extraDetailTabs` entry for ContactsScreen.
@@ -23,10 +23,12 @@
23
23
  "get_availability_appointments_schedules__schedule_id__availability_get",
24
24
  "get_available_slots_appointments_slots_get",
25
25
  "get_booking_appointments_bookings__booking_uid__get",
26
+ "get_contact_meetings_contacts__contact_id__meetings_get",
26
27
  "get_event_type_appointments_event_types__event_type_id__get",
27
28
  "get_meeting_inbox_meetings__meeting_id__get",
28
29
  "list_bookings_appointments_bookings_get",
29
30
  "list_calendars_appointments_calendars_get",
31
+ "list_contacts_contacts_get",
30
32
  "list_event_types_event_types_get",
31
33
  "list_meetings_inbox_meetings_get",
32
34
  "list_schedules_appointments_schedules_get",
@@ -138,6 +140,30 @@
138
140
  "tiers": [
139
141
  "read"
140
142
  ]
143
+ },
144
+ {
145
+ "name": "MeetingContactLinks",
146
+ "file": "components/meeting-contact-links.tsx",
147
+ "purpose": "\"Open <attendee>\" buttons for a booked or recorded meeting: one per attendee who is a CRM contact; for MeetingsScreen renderDetailActions.",
148
+ "operations": [
149
+ "get_booking_appointments_bookings__booking_uid__get",
150
+ "get_meeting_inbox_meetings__meeting_id__get",
151
+ "list_contacts_contacts_get"
152
+ ],
153
+ "tiers": [
154
+ "read"
155
+ ]
156
+ },
157
+ {
158
+ "name": "RecordMeetingsTab",
159
+ "file": "components/record-meetings-tab.tsx",
160
+ "purpose": "The meetings booked with one contact (booked, rescheduled, cancelled), each opening in the meetings screen; an extraDetailTabs entry for ContactsScreen.",
161
+ "operations": [
162
+ "get_contact_meetings_contacts__contact_id__meetings_get"
163
+ ],
164
+ "tiers": [
165
+ "read"
166
+ ]
141
167
  }
142
168
  ],
143
169
  "hooks": [
@@ -220,6 +246,7 @@
220
246
  ],
221
247
  "scopes": [
222
248
  "campaigns:read",
249
+ "contacts:read",
223
250
  "meetings:delete",
224
251
  "meetings:read",
225
252
  "meetings:run",