@graph8/sdk 0.362.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 (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -7,6 +7,23 @@ copy `InboxScreen`, the finished two-pane page.
7
7
  Depends on: `crm` (the contact on each thread), `mailboxes` (the connected sending mailboxes). Install with
8
8
  `npx @graph8/sdk add inbox`.
9
9
 
10
+ ## Screens
11
+
12
+ `InboxScreen` (`components/inbox-screen.tsx`) is the finished page; it needs no props. Stat strip, channel / status /
13
+ tag filters, search of the loaded page, paged list, and the open conversation (reply composer with confirmed AI draft
14
+ and send, tag and assign). Open conversations deep-link as `?id=<channel>:<thread id>`. Optional slots:
15
+ `toolbarActions`, `renderRowActions(thread)`, `renderDetailActions(thread)`, `extraDetailTabs`, `pageSize`.
16
+ Needs scopes `inbox:read`, `inbox:write`, `inbox:run` (plus `campaigns:read` for the SMS sender picker) and a mailbox
17
+ connected in the graph8 app (a link-out card says so).
18
+
19
+ **Behaviour change.** Compared with the original two-pane `InboxScreen`, the finished page now writes `?id=` and
20
+ `?view=` to the host page URL and renders its own header (an "Inbox" h1, description and stats). If your page
21
+ already has a title or owns those query keys, pass `urlState={false}` (the open conversation lives in component
22
+ state, the URL is untouched) and/or `hideHeader` (no title or stats, just search, filters, Refresh and the two panes).
23
+ Defaults are unchanged from this release: both are on.
24
+
25
+ Every detail opens as a full page with `&view=full` (the Open full page button).
26
+
10
27
  ## The flow, in order
11
28
 
12
29
  1. **List** `GET /inbox?channel=&status=&page=&page_size=` (`InboxList`). Each row has `id` and `channel`.
@@ -18,8 +35,12 @@ Depends on: `crm` (the contact on each thread), `mailboxes` (the connected sendi
18
35
  `shorten` / `expand` / `fix_grammar_spelling` rework the text you pass in `message_html`. The response
19
36
  `data.content` is HTML and `data.plain_content` is text. It sends nothing.
20
37
  4. **Send** `POST /inbox/{reply_id}/send` (`ReplyComposer`, **external**, confirm first) with
21
- `{ channel, body, subject?, to? }`. The composer sends an `Idempotency-Key` so a double click cannot
22
- send twice.
38
+ `{ channel, body, subject?, to, from_address }`. `from_address` is the thread's own sender: `thread.mailbox`
39
+ (email), `thread.from_number` (SMS) or `thread.linkedin_account_id` (LinkedIn); `to` is the thread's contact
40
+ (`contact.email`, or `thread.to_number` for SMS). If an SMS or LinkedIn thread names no sender, the composer
41
+ shows a picker filled from `GET /sms/phone-numbers` or `GET /inbox/linkedin/accounts`. The API fills both
42
+ from the thread when omitted (older API versions refused the send with 400). The composer sends an
43
+ `Idempotency-Key` so a double click cannot send twice.
23
44
  5. **Route** (`TagAndAssign`): `POST /inbox/tags` creates a tag (write), `POST /inbox/{id}/tag?channel=`
24
45
  applies tag ids and `POST /inbox/{id}/assign?channel=` hands the thread to a teammate by email. Both of
25
46
  those are **external** (confirm first) because they can fire automations and notify people.
@@ -31,13 +52,14 @@ IDs passed between steps: `thread.id` + `thread.channel` (list -> view -> draft/
31
52
 
32
53
  - Connect a mailbox in the graph8 app (Google / Microsoft OAuth exists only there; see the `mailboxes` block).
33
54
  Until then the inbox has no email threads. `where: "app"`.
34
- - Give the proxy's API key `inbox:read`, `inbox:write` and `inbox:run`.
55
+ - Give the proxy's API key `inbox:read`, `inbox:write` and `inbox:run` (and `campaigns:read` for the SMS sender picker).
35
56
  - Credits for AI drafts (402 when none; there is no balance read, so the confirm dialog shows no estimate).
36
57
 
37
58
  ## Defaults (the agent does not need to ask)
38
59
 
39
60
  - AI drafts are only ever written into the reply box. Sending is always a separate, confirmed click.
40
61
  - Tone `professional`, goal `follow_up`.
62
+ - A reply leaves from the thread's own mailbox, number or LinkedIn account and goes to the thread's contact.
41
63
  - No assignee filter: the API cannot filter by an arbitrary assignee email (an org key owns no threads), so
42
64
  filter `thread.assignees` client-side if you need it.
43
65
  - Ask the human only about **tags**: there is no list-tags operation. Pass `availableTags` from your own
@@ -60,6 +82,8 @@ IDs passed between steps: `thread.id` + `thread.channel` (list -> view -> draft/
60
82
  | `inbox.getReply` | GET /inbox/{reply_id} | read | ThreadView, useInboxThread |
61
83
  | `aiInbox.generateInboxDraft` | POST /inbox/drafts/generate | billable | ReplyComposer |
62
84
  | `inbox.sendReply` | POST /inbox/{reply_id}/send | external | ReplyComposer |
85
+ | `channels.listSmsPhoneNumbers` | GET /sms/phone-numbers | read | ReplyComposer (SMS sender picker) |
86
+ | `aiInbox.listInboxLinkedinAccounts` | GET /inbox/linkedin/accounts | read | ReplyComposer (LinkedIn sender picker) |
63
87
  | `aiInbox.listInboxUsers` | GET /inbox/users | read | TagAndAssign |
64
88
  | `aiInbox.createInboxTag` | POST /inbox/tags | write | TagAndAssign |
65
89
  | `inbox.tagReply` | POST /inbox/{reply_id}/tag | external | TagAndAssign |
@@ -70,3 +94,8 @@ IDs passed between steps: `thread.id` + `thread.channel` (list -> view -> draft/
70
94
  - `01-triage-and-reply.ts`: list, read, AI draft (billable), send (external).
71
95
  - `02-tag-and-assign.ts`: create a tag, apply it, assign a teammate.
72
96
  - `03-hot-contacts-digest.ts`: read-only digest of engaging contacts and unowned threads.
97
+
98
+ ## Cross-block glue
99
+
100
+
101
+ - `RecordConversationsTab` (`components/record-conversations-tab.tsx`): the email conversations with a contact. The inbox has no filter by contact id, so it matches the contact's email through `searchInboxEmails` (`search_emails`) across the mailboxes from `listInboxMailboxes`, 10 per page with "Load more"; opening a row reads `getReply`, and the API's uniform 404 for a hidden thread shows "not available to you" (no retry). `extraDetailTabs` entry for ContactsScreen.
@@ -20,7 +20,11 @@
20
20
  "generate_inbox_draft_inbox_drafts_generate_post",
21
21
  "get_reply_inbox__reply_id__get",
22
22
  "list_inbox_inbox_get",
23
+ "list_inbox_linkedin_accounts_inbox_linkedin_accounts_get",
24
+ "list_inbox_mailboxes_inbox_mailboxes_get",
23
25
  "list_inbox_users_inbox_users_get",
26
+ "list_sms_phone_numbers_sms_phone_numbers_get",
27
+ "search_inbox_emails_inbox_emails_search_post",
24
28
  "send_reply_inbox__reply_id__send_post",
25
29
  "tag_reply_inbox__reply_id__tag_post"
26
30
  ],
@@ -50,12 +54,15 @@
50
54
  {
51
55
  "name": "ReplyComposer",
52
56
  "file": "components/reply-composer.tsx",
53
- "purpose": "Write a reply, draft or reshape it with AI (confirm, billable) and send it on the thread's channel (confirm, external).",
57
+ "purpose": "Write a reply, draft or reshape it with AI (confirm, billable) and send it from the thread's own mailbox, number or LinkedIn account to its contact (confirm, external); picks a sender when an SMS or LinkedIn thread names none.",
54
58
  "operations": [
55
59
  "generate_inbox_draft_inbox_drafts_generate_post",
60
+ "list_inbox_linkedin_accounts_inbox_linkedin_accounts_get",
61
+ "list_sms_phone_numbers_sms_phone_numbers_get",
56
62
  "send_reply_inbox__reply_id__send_post"
57
63
  ],
58
64
  "tiers": [
65
+ "read",
59
66
  "billable",
60
67
  "external"
61
68
  ]
@@ -89,6 +96,19 @@
89
96
  "purpose": "Small status pill shared by the thread view and tag panel.",
90
97
  "operations": [],
91
98
  "tiers": []
99
+ },
100
+ {
101
+ "name": "RecordConversationsTab",
102
+ "file": "components/record-conversations-tab.tsx",
103
+ "purpose": "The email conversations with one contact (matched by address), with Load more and a calm answer for a thread that 404s; an extraDetailTabs entry for ContactsScreen.",
104
+ "operations": [
105
+ "list_inbox_mailboxes_inbox_mailboxes_get",
106
+ "search_inbox_emails_inbox_emails_search_post",
107
+ "get_reply_inbox__reply_id__get"
108
+ ],
109
+ "tiers": [
110
+ "read"
111
+ ]
92
112
  }
93
113
  ],
94
114
  "hooks": [
@@ -157,6 +177,7 @@
157
177
  }
158
178
  ],
159
179
  "scopes": [
180
+ "campaigns:read",
160
181
  "inbox:read",
161
182
  "inbox:run",
162
183
  "inbox:write"
@@ -186,9 +207,15 @@
186
207
  "decision": "Filter by assignee",
187
208
  "default": "Not offered: the API cannot filter by an arbitrary assignee email (an org key owns no threads). Filter thread.assignees client-side",
188
209
  "askHuman": false
210
+ },
211
+ {
212
+ "decision": "Which mailbox, number or LinkedIn account a reply is sent from",
213
+ "default": "The thread's own (mailbox / from_number / linkedin_account_id on the thread); an SMS or LinkedIn thread without one shows a sender picker",
214
+ "askHuman": false
189
215
  }
190
216
  ],
191
217
  "peerDependencies": {
192
- "react": ">=18"
218
+ "react": ">=18",
219
+ "lucide-react": ">=0.400.0"
193
220
  }
194
221
  }
@@ -25,8 +25,12 @@ import {
25
25
  type InboxChannel,
26
26
  type InboxThread,
27
27
  } from "../lib/inbox-types";
28
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
29
+ import { RecordLink } from "@/g8/components/screen/record-link";
28
30
 
29
31
  export interface InboxListProps {
32
+ /** URL param the host screen reads for this record type. With it the name is a real link (Cmd-click opens it in a new tab); without it a plain button. */
33
+ linkParam?: string;
30
34
  onSelect?: (thread: InboxThread) => void;
31
35
  selectedId?: string | null;
32
36
  /** Start with these filters (for example a tag id from the URL). */
@@ -62,7 +66,7 @@ function LoadingRows() {
62
66
  );
63
67
  }
64
68
 
65
- export function InboxList({ onSelect, selectedId, initialFilters, pageSize = 25, onFiltersChange, className }: InboxListProps) {
69
+ export function InboxList({ linkParam, onSelect, selectedId, initialFilters, pageSize = 25, onFiltersChange, className }: InboxListProps) {
66
70
  const [filters, setFilters] = useState<InboxFilters>(initialFilters ?? {});
67
71
  const [page, setPage] = useState(1);
68
72
  const { threads, loading, error, hasNext, total, refetch } = useInboxThreads(filters, page, pageSize);
@@ -123,9 +127,9 @@ export function InboxList({ onSelect, selectedId, initialFilters, pageSize = 25,
123
127
  key={`${thread.channel}:${thread.id}`}
124
128
  data-state={selectedId === thread.id ? "selected" : undefined}
125
129
  className="cursor-pointer"
126
- onClick={() => onSelect?.(thread)}
130
+ {...rowActivationProps(() => onSelect?.(thread))}
127
131
  >
128
- <TableCell className="max-w-48 truncate font-medium">{contactLabel(thread)}</TableCell>
132
+ <TableCell className="max-w-48 truncate font-medium">{onSelect ? <RecordLink param={linkParam} id={thread.id} onOpen={() => onSelect(thread)}>{contactLabel(thread)}</RecordLink> : contactLabel(thread)}</TableCell>
129
133
  <TableCell className="max-w-80 truncate text-muted-foreground">
130
134
  {thread.subject ? <span className="text-foreground">{thread.subject} </span> : null}
131
135
  {previewText(thread)}
@@ -1,49 +1,316 @@
1
1
  "use client";
2
2
 
3
3
  /**
4
- * InboxScreen: the finished two-pane inbox. List on the left; on the right the open conversation with its
5
- * reply composer and tag/assign panel. Copy it as the page, or take the four parts it composes.
4
+ * InboxScreen: the finished inbox. A stat strip, a searchable and filterable conversation list on the left, and on the
5
+ * right the open conversation with its reply composer and tag/assign panel. Deep link: `?id=<channel>:<thread id>`.
6
6
  *
7
- * SDK operations: none of its own. It composes InboxList (read), ThreadView (read), ReplyComposer
8
- * (billable draft, external send) and TagAndAssign (external). The dialogs live in those parts.
7
+ * Flows: browse and page the queue; filter by channel, status and tag; search the loaded page; open a conversation;
8
+ * read it (unsent drafts flagged); draft with AI (billable, confirmed) and send (external, confirmed); tag and assign
9
+ * (external, confirmed); clear filters; connect a mailbox (link out to graph8, OAuth only exists there).
10
+ *
11
+ * SDK operations: none of its own. It composes useInboxThreads (GET /inbox, read), ThreadView (read), ReplyComposer
12
+ * (billable draft, external send) and TagAndAssign (external). The confirm dialogs live in those parts.
13
+ *
14
+ * BEHAVIOUR CHANGE (versus the original two-pane InboxScreen): it now writes `?id=` and `?view=` to the host page URL and
15
+ * renders its own ScreenLayout header ("Inbox" h1, stats). Opt out with `urlState={false}` (selection kept in component
16
+ * state, the URL untouched) and/or `hideHeader` (no title or stats, just the toolbar and the two panes). Defaults keep the new look.
17
+ *
18
+ * Props are backwards compatible: `initialFilters`, `availableTags`, `className` as before; everything else is optional glue.
9
19
  */
10
- import { useMemo, useState } from "react";
20
+ import { useMemo, useState, type ReactNode } from "react";
21
+ import { Inbox as InboxIcon, Mail, RefreshCw } from "lucide-react";
22
+ import { Button, Skeleton } from "@graph8/sdk/ui";
23
+ import { ApiError } from "@/g8/components/api-error";
11
24
  import { EmptyState } from "@/g8/components/empty-state";
12
- import type { InboxFilters } from "../hooks/use-inbox-threads";
13
- import type { InboxTag, InboxThread } from "../lib/inbox-types";
14
- import { InboxList } from "./inbox-list";
25
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
26
+ import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
27
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
28
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
29
+ import { StatusPill } from "@/g8/components/screen/status-pill";
30
+ import { Toolbar } from "@/g8/components/screen/toolbar";
31
+ import { useUrlState } from "@/g8/hooks/use-url-state";
32
+ import { useInboxThread } from "../hooks/use-inbox-thread";
33
+ import { useInboxThreads, type InboxFilters } from "../hooks/use-inbox-threads";
34
+ import {
35
+ INBOX_CHANNELS,
36
+ INBOX_STATUSES,
37
+ contactLabel,
38
+ formatWhen,
39
+ latestMessage,
40
+ previewText,
41
+ type InboxChannel,
42
+ type InboxTag,
43
+ type InboxThread,
44
+ } from "../lib/inbox-types";
15
45
  import { ReplyComposer } from "./reply-composer";
16
46
  import { TagAndAssign } from "./tag-and-assign";
17
47
  import { ThreadView } from "./thread-view";
48
+ import { useBrand } from "@/g8/hooks/use-brand";
18
49
 
19
50
  export interface InboxScreenProps {
20
51
  initialFilters?: InboxFilters;
21
52
  /** Tags the person can apply. There is no list-tags operation; keep this list in your app. */
22
53
  availableTags?: InboxTag[];
23
54
  className?: string;
55
+ /** Extra buttons next to Refresh. */
56
+ toolbarActions?: ReactNode;
57
+ /** Rendered at the end of each conversation row. */
58
+ renderRowActions?: (thread: InboxThread) => ReactNode;
59
+ /** Rendered in the header of the open conversation. */
60
+ renderDetailActions?: (thread: InboxThread) => ReactNode;
61
+ /** Extra tabs next to "Conversation" (for example the contact's CRM record). */
62
+ extraDetailTabs?: Array<{ id: string; label: string; render: (thread: InboxThread) => ReactNode }>;
63
+ /** Page size of the list. Default 25. */
64
+ pageSize?: number;
65
+ /** Keep the open conversation and full-page view in the page URL (`?id=`, `?view=`). Default true; false keeps them in component state and leaves the URL alone. */
66
+ urlState?: boolean;
67
+ /** Skip the screen header (title, description, stats) when the host page already has one. Default false. */
68
+ hideHeader?: boolean;
69
+ }
70
+
71
+ /** `useUrlState` or plain state, chosen once per mount (the flag must not change while mounted). */
72
+ function useSelectionState(key: string, urlState: boolean): [string | undefined, (v: string | null) => void] {
73
+ const [fromUrl, setFromUrl] = useUrlState(key);
74
+ const [local, setLocal] = useState<string | null>(null);
75
+ return urlState ? [fromUrl, setFromUrl] : [local ?? undefined, setLocal];
24
76
  }
25
77
 
26
- export function InboxScreen({ initialFilters, availableTags, className }: InboxScreenProps) {
27
- const [open, setOpen] = useState<InboxThread | null>(null);
28
- const tags = useMemo(() => availableTags ?? [], [availableTags]);
78
+ type Selected = { channel: string; id: string };
79
+ const keyOf = (t: Selected) => `${t.channel}:${t.id}`;
80
+ function parseKey(value: string): Selected | null {
81
+ const at = value.indexOf(":");
82
+ return at > 0 && at < value.length - 1 ? { channel: value.slice(0, at), id: value.slice(at + 1) } : null;
83
+ }
84
+
85
+ function statusTone(status?: string | null) {
86
+ const s = (status ?? "").toUpperCase();
87
+ return s === "DRAFT" ? "warning" : s === "SENT" ? "positive" : s === "AI_RESPONDED" ? "info" : "neutral";
88
+ }
29
89
 
90
+ function ThreadRow({ thread, selected, onOpen, actions }: { thread: InboxThread; selected: boolean; onOpen: () => void; actions?: ReactNode }) {
91
+ const hasDraft = thread.messages.some((m) => m.is_draft);
30
92
  return (
31
- <div className={`grid gap-4 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)] ${className ?? ""}`}>
32
- <InboxList onFiltersChange={() => setOpen(null)} initialFilters={initialFilters} selectedId={open?.id} onSelect={setOpen} />
33
- <div className="min-w-0">
34
- {open ? (
35
- <ThreadView key={`${open.channel}:${open.id}`} thread={open}>
36
- {(thread, reload) => (
37
- <div className="space-y-3">
38
- <ReplyComposer thread={thread} onSent={reload} />
39
- <TagAndAssign thread={thread} availableTags={tags} onChanged={reload} />
40
- </div>
41
- )}
42
- </ThreadView>
43
- ) : (
44
- <EmptyState title="Select a conversation" description="Pick a reply on the left to read it and respond." />
45
- )}
46
- </div>
93
+ <li className={`flex items-start gap-1 border-b border-border ${selected ? "bg-muted" : "hover:bg-muted/50"}`}>
94
+ <button type="button" aria-current={selected ? "true" : undefined} onClick={onOpen} className="flex min-w-0 flex-1 items-start gap-3 p-3 text-left">
95
+ <EntityAvatar name={contactLabel(thread)} kind="person" size="sm" />
96
+ <span className="min-w-0 flex-1">
97
+ <span className="flex items-center justify-between gap-2">
98
+ <span className="truncate text-sm font-medium">{contactLabel(thread)}</span>
99
+ <span className="shrink-0 text-xs text-muted-foreground">{formatWhen(thread.updated_at ?? latestMessage(thread)?.date)}</span>
100
+ </span>
101
+ <span className="block truncate text-sm">{thread.subject ?? ""}</span>
102
+ <span className="block truncate text-xs text-muted-foreground">{previewText(thread)}</span>
103
+ <span className="mt-1 flex flex-wrap items-center gap-1">
104
+ <StatusPill tone="neutral">{String(thread.channel)}</StatusPill>
105
+ {thread.status ? <StatusPill tone={statusTone(thread.status)}>{thread.status.toLowerCase().replace("_", " ")}</StatusPill> : null}
106
+ {hasDraft ? <StatusPill tone="warning" dot>Draft waiting</StatusPill> : null}
107
+ {thread.tags.map((t) => <StatusPill key={t.id ?? t.name} tone="info">{t.name ?? t.id}</StatusPill>)}
108
+ </span>
109
+ </span>
110
+ </button>
111
+ {actions ? <div className="shrink-0 p-2">{actions}</div> : null}
112
+ </li>
113
+ );
114
+ }
115
+
116
+ function ChannelFilter({ value, onChange }: { value?: InboxChannel; onChange: (next?: InboxChannel) => void }) {
117
+ return (
118
+ <div role="group" aria-label="Channel" className="flex gap-1">
119
+ <Button size="sm" variant={value ? "outline" : "default"} onClick={() => onChange(undefined)}>All</Button>
120
+ {INBOX_CHANNELS.map((c) => (
121
+ <Button key={c.value} size="sm" variant={value === c.value ? "default" : "outline"} onClick={() => onChange(c.value)}>{c.label}</Button>
122
+ ))}
47
123
  </div>
48
124
  );
49
125
  }
126
+
127
+ function Filters({ filters, tags, update }: { filters: InboxFilters; tags: InboxTag[]; update: (patch: Partial<InboxFilters>) => void }) {
128
+ const selectClass = "h-8 rounded-md border border-border bg-background px-2 text-sm";
129
+ return (
130
+ <>
131
+ <ChannelFilter value={filters.channel} onChange={(channel) => update({ channel })} />
132
+ <select aria-label="Status" className={selectClass} value={filters.status ?? ""} onChange={(e) => update({ status: e.target.value || undefined })}>
133
+ <option value="">Any status</option>
134
+ {INBOX_STATUSES.map((s) => <option key={s} value={s}>{s.replace("_", " ").toLowerCase()}</option>)}
135
+ </select>
136
+ {tags.length > 0 ? (
137
+ <select aria-label="Tag" className={selectClass} value={filters.tag ?? ""} onChange={(e) => update({ tag: e.target.value || undefined })}>
138
+ <option value="">Any tag</option>
139
+ {tags.map((t) => <option key={t.id} value={t.id ?? ""}>{t.name ?? t.id}</option>)}
140
+ </select>
141
+ ) : null}
142
+ </>
143
+ );
144
+ }
145
+
146
+ function ListEmpty({ filtered, onClear }: { filtered: boolean; onClear: () => void }) {
147
+ const brand = useBrand();
148
+ if (filtered) {
149
+ return <EmptyState title="No conversations match" description="Nothing fits these filters or this search." action={<Button size="sm" variant="outline" onClick={onClear}>Clear filters</Button>} />;
150
+ }
151
+ return (
152
+ <EmptyState
153
+ icon={<Mail className="size-6" />}
154
+ title="No conversations yet"
155
+ description={`Replies from sequences, SMS and LinkedIn land here. Connect a mailbox in ${brand.linkLabel} (Google and Microsoft sign-in only exist there), then enroll contacts in a sequence.`}
156
+ action={<a className="text-sm underline" href={brand.appUrl} target="_blank" rel="noreferrer">Connect in {brand.linkLabel}</a>}
157
+ />
158
+ );
159
+ }
160
+
161
+ function ThreadDetails({ thread }: { thread: Selected }) {
162
+ const { data: t, loading, error, refetch } = useInboxThread(thread.id, thread.channel);
163
+ if (error) return <ApiError error={error} title="Could not load this conversation" onRetry={() => void refetch()} />;
164
+ if (!t) return loading ? <Skeleton className="h-24 w-full" /> : null;
165
+ const dash = "";
166
+ return (
167
+ <KeyValueList
168
+ columns={1}
169
+ items={[
170
+ { label: "Contact", value: t.contact?.name || dash },
171
+ { label: "Email", value: t.contact?.email || dash },
172
+ { label: "Phone", value: t.contact?.phone || dash },
173
+ { label: "Company", value: t.contact?.company || dash },
174
+ { label: "Channel", value: String(t.channel) },
175
+ { label: "Status", value: t.status ? t.status.toLowerCase().replace("_", " ") : dash },
176
+ { label: "Tags", value: t.tags.length ? t.tags.map((x) => x.name ?? x.id).join(", ") : dash },
177
+ { label: "Assigned to", value: t.assignees.length ? t.assignees.map((a) => a.name || a.email).join(", ") : dash },
178
+ { label: "Messages", value: `${t.messages.length}${t.messages.some((m) => m.is_draft) ? " (includes an unsent draft)" : ""}` },
179
+ { label: "Started", value: formatWhen(t.created_at) || dash },
180
+ { label: "Last activity", value: formatWhen(t.updated_at) || dash },
181
+ ]}
182
+ />
183
+ );
184
+ }
185
+
186
+ function ExtraTab({ thread, render }: { thread: Selected; render: (thread: InboxThread) => ReactNode }) {
187
+ const { data, loading, error, refetch } = useInboxThread(thread.id, thread.channel);
188
+ if (error) return <ApiError error={error} title="Could not load this conversation" onRetry={() => void refetch()} />;
189
+ if (!data) return loading ? <Skeleton className="h-24 w-full" /> : null;
190
+ return <>{render(data)}</>;
191
+ }
192
+
193
+ export function InboxScreen({ initialFilters, availableTags, className, toolbarActions, renderRowActions, renderDetailActions, extraDetailTabs, pageSize = 25, urlState = true, hideHeader = false }: InboxScreenProps) {
194
+ const [filters, setFilters] = useState<InboxFilters>(initialFilters ?? {});
195
+ const [page, setPage] = useState(1);
196
+ const [search, setSearch] = useState("");
197
+ const [openKey, setOpenKey] = useSelectionState("id", urlState);
198
+ const [view, setView] = useSelectionState("view", urlState);
199
+ const { threads, loading, error, hasNext, total, refetch } = useInboxThreads(filters, page, pageSize);
200
+ const open = openKey ? parseKey(openKey) : null;
201
+ const expanded = view === "full" && open != null;
202
+
203
+ const tags = useMemo(() => {
204
+ const seen = new Map<string, InboxTag>();
205
+ for (const t of [...(availableTags ?? []), ...threads.flatMap((th) => th.tags)]) if (t.id) seen.set(t.id, t);
206
+ return [...seen.values()];
207
+ }, [availableTags, threads]);
208
+ const needle = search.trim().toLowerCase();
209
+ const rows = useMemo(
210
+ () => (needle ? threads.filter((t) => `${contactLabel(t)} ${t.contact?.email ?? ""} ${t.contact?.company ?? ""} ${t.subject ?? ""} ${previewText(t, 300)}`.toLowerCase().includes(needle)) : threads),
211
+ [threads, needle],
212
+ );
213
+
214
+ const update = (patch: Partial<InboxFilters>) => {
215
+ setFilters((f) => ({ ...f, ...patch }));
216
+ setPage(1);
217
+ setOpenKey(null);
218
+ };
219
+ const clear = () => { setSearch(""); setFilters({}); setPage(1); setOpenKey(null); };
220
+ const filtered = Boolean(needle || filters.channel || filters.status || filters.tag || filters.sequenceId);
221
+ const drafts = threads.filter((t) => t.messages.some((m) => m.is_draft)).length;
222
+ const count = (c: string) => threads.filter((t) => t.channel === c).length;
223
+ const first = open ? threads.find((t) => t.id === open.id && t.channel === open.channel) : undefined;
224
+
225
+ const panes = (
226
+ <>
227
+ <div className={expanded ? "grid gap-4" : "grid gap-4 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]"}>
228
+ <div className={expanded ? "hidden" : "min-w-0 space-y-2"}>
229
+ {error ? (
230
+ <ApiError error={error} title="Could not load the inbox" onRetry={() => void refetch()} />
231
+ ) : loading && threads.length === 0 ? (
232
+ <div aria-busy="true" className="space-y-2">{Array.from({ length: 5 }, (_, i) => <Skeleton key={i} className="h-16 w-full" />)}</div>
233
+ ) : rows.length === 0 ? (
234
+ <ListEmpty filtered={filtered} onClear={clear} />
235
+ ) : (
236
+ <ul aria-label="Conversations" className="rounded-md border border-border">
237
+ {rows.map((t) => (
238
+ <ThreadRow key={keyOf(t)} thread={t} selected={open != null && keyOf(open) === keyOf(t)} onOpen={() => setOpenKey(keyOf(t))} actions={renderRowActions?.(t)} />
239
+ ))}
240
+ </ul>
241
+ )}
242
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
243
+ <span>{total != null ? `${total.toLocaleString()} conversations` : `Page ${page}`}</span>
244
+ <div className="flex gap-2">
245
+ <Button size="sm" variant="outline" disabled={page <= 1 || loading} onClick={() => { setPage((p) => p - 1); setOpenKey(null); }}>Previous</Button>
246
+ <Button size="sm" variant="outline" disabled={!hasNext || loading} onClick={() => { setPage((p) => p + 1); setOpenKey(null); }}>Next</Button>
247
+ </div>
248
+ </div>
249
+ </div>
250
+ <div className="min-w-0">
251
+ {open ? (
252
+ <DetailPanel
253
+ key={keyOf(open)}
254
+ title={first ? first.subject || contactLabel(first) : "Conversation"}
255
+ subtitle={first ? `${contactLabel(first)}${first.contact?.company ? ` at ${first.contact.company}` : ""}` : undefined}
256
+ avatar={first ? <EntityAvatar name={contactLabel(first)} kind="person" /> : undefined}
257
+ onClose={() => { setOpenKey(null); setView(null); }}
258
+ onExpand={expanded ? undefined : () => setView("full")}
259
+ onCollapse={expanded ? () => setView(null) : undefined}
260
+ tabs={[
261
+ {
262
+ id: "conversation",
263
+ label: "Conversation",
264
+ content: (
265
+ <ThreadView thread={first ?? open}>
266
+ {(thread, reload) => (
267
+ <div className="space-y-3">
268
+ {renderDetailActions ? <div className="flex flex-wrap gap-2">{renderDetailActions(thread)}</div> : null}
269
+ <ReplyComposer thread={thread} onSent={reload} />
270
+ <TagAndAssign thread={thread} availableTags={tags} onChanged={reload} />
271
+ </div>
272
+ )}
273
+ </ThreadView>
274
+ ),
275
+ },
276
+ { id: "details", label: "Details", content: <ThreadDetails thread={open} /> },
277
+ ...(extraDetailTabs ?? []).map((tab) => ({ id: tab.id, label: tab.label, content: <ExtraTab thread={open} render={tab.render} /> })),
278
+ ]}
279
+ />
280
+ ) : (
281
+ <EmptyState title="Select a conversation" description="Pick a reply on the left to read it and respond." />
282
+ )}
283
+ </div>
284
+ </div>
285
+ </>
286
+ );
287
+ if (hideHeader) {
288
+ return (
289
+ <div className={className ? `space-y-4 ${className}` : "space-y-4"}>
290
+ <div className="flex flex-wrap items-center justify-between gap-2">
291
+ <Toolbar search={{ value: search, onChange: setSearch, placeholder: "Search this page" }} filters={<Filters filters={filters} tags={tags} update={update} />} />
292
+ <div className="flex gap-2"><Button size="sm" variant="outline" onClick={() => void refetch()}><RefreshCw className="mr-1 size-4" />Refresh</Button>{toolbarActions}</div>
293
+ </div>
294
+ {panes}
295
+ </div>
296
+ );
297
+ }
298
+ return (
299
+ <ScreenLayout
300
+ className={className}
301
+ title="Inbox"
302
+ description="Every reply across email, SMS and LinkedIn in one queue."
303
+ icon={<InboxIcon className="size-5" />}
304
+ actions={<><Button size="sm" variant="outline" onClick={() => void refetch()}><RefreshCw className="mr-1 size-4" />Refresh</Button>{toolbarActions}</>}
305
+ stats={[
306
+ { label: "Conversations", value: total != null ? total.toLocaleString() : threads.length, hint: total != null ? "in this view" : "on this page" },
307
+ { label: "Drafts waiting", value: drafts, hint: "on this page", tone: drafts > 0 ? "warning" : "neutral" },
308
+ { label: "Email threads", value: count("email"), hint: "on this page" },
309
+ { label: "Text and LinkedIn", value: count("sms") + count("linkedin"), hint: "on this page" },
310
+ ]}
311
+ toolbar={<Toolbar search={{ value: search, onChange: setSearch, placeholder: "Search this page" }} filters={<Filters filters={filters} tags={tags} update={update} />} />}
312
+ >
313
+ {panes}
314
+ </ScreenLayout>
315
+ );
316
+ }