@graph8/sdk 0.373.0 → 0.374.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 (279) hide show
  1. package/README.md +3 -3
  2. package/blocks/_foundation/README.md +47 -0
  3. package/blocks/_foundation/block.json +13 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/server/auth.ts +815 -0
  6. package/blocks/campaigns/README.md +1 -1
  7. package/blocks/campaigns/block.json +34 -1
  8. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  9. package/blocks/company-intelligence/components/research-run.tsx +6 -2
  10. package/blocks/crm/README.md +19 -0
  11. package/blocks/crm/block.json +205 -6
  12. package/blocks/crm/components/bulk-update.tsx +119 -0
  13. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  14. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  15. package/blocks/crm/components/contact-lookup-link.tsx +47 -6
  16. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  17. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  18. package/blocks/crm/components/contacts-screen.tsx +46 -5
  19. package/blocks/crm/components/owner-assign.tsx +132 -0
  20. package/blocks/crm/components/record-open-links.tsx +79 -0
  21. package/blocks/crm/components/tag-assign.tsx +112 -0
  22. package/blocks/crm/components/tag-manager.tsx +178 -0
  23. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  24. package/blocks/crm/components/verify-email-button.tsx +93 -0
  25. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  26. package/blocks/crm/lib/record-actions.ts +102 -0
  27. package/blocks/deals/README.md +1 -0
  28. package/blocks/deals/block.json +17 -0
  29. package/blocks/deals/components/create-deal-button.tsx +109 -0
  30. package/blocks/deals/components/deal-detail.tsx +5 -3
  31. package/blocks/dialer/components/call-log-screen.tsx +6 -1
  32. package/blocks/documents/README.md +86 -0
  33. package/blocks/documents/block.json +378 -0
  34. package/blocks/documents/components/account-bar.tsx +48 -0
  35. package/blocks/documents/components/correspondence-form.tsx +95 -0
  36. package/blocks/documents/components/correspondence-import.tsx +106 -0
  37. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  38. package/blocks/documents/components/documents-screen.tsx +76 -0
  39. package/blocks/documents/components/library-document-detail.tsx +175 -0
  40. package/blocks/documents/components/library-document-form.tsx +105 -0
  41. package/blocks/documents/components/library-folder-form.tsx +74 -0
  42. package/blocks/documents/components/library-panel.tsx +235 -0
  43. package/blocks/documents/components/library-upload.tsx +160 -0
  44. package/blocks/documents/components/question-row.tsx +130 -0
  45. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  46. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  47. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  48. package/blocks/documents/lib/documents-types.ts +252 -0
  49. package/blocks/documents/lib/download.ts +64 -0
  50. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  51. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  52. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  53. package/blocks/email-signatures/README.md +123 -0
  54. package/blocks/email-signatures/block.json +341 -0
  55. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  56. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  57. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  58. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  59. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  60. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  61. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  62. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  63. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  64. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  65. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  66. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  67. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  68. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  69. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  70. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  71. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  72. package/blocks/enrichment/README.md +22 -4
  73. package/blocks/enrichment/block.json +145 -5
  74. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  75. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  76. package/blocks/enrichment/components/bulk-enrich-panel.tsx +7 -7
  77. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  78. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  79. package/blocks/enrichment/components/enrichment-screen.tsx +34 -3
  80. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  81. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  82. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  83. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  84. package/blocks/enrichment/lib/enrichment-types.ts +12 -0
  85. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  86. package/blocks/forms/README.md +2 -0
  87. package/blocks/forms/components/forms-screen.tsx +6 -3
  88. package/blocks/inbox/README.md +40 -4
  89. package/blocks/inbox/block.json +130 -8
  90. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  91. package/blocks/inbox/components/compose-message.tsx +193 -0
  92. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  93. package/blocks/inbox/components/inbox-search.tsx +106 -0
  94. package/blocks/inbox/components/tag-manager.tsx +136 -0
  95. package/blocks/inbox/components/thread-actions.tsx +133 -0
  96. package/blocks/inbox/components/thread-view.tsx +2 -0
  97. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  98. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  99. package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
  100. package/blocks/pages/block.json +10 -1
  101. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  102. package/blocks/programmatic-pages/README.md +82 -0
  103. package/blocks/programmatic-pages/block.json +292 -0
  104. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  105. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  106. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  107. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  108. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  109. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  110. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  111. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  112. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  113. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  114. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  115. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  116. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  117. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  118. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  119. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  120. package/blocks/quotes/README.md +25 -1
  121. package/blocks/quotes/block.json +123 -6
  122. package/blocks/quotes/components/manual-products.tsx +144 -0
  123. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  124. package/blocks/quotes/components/quote-documents.tsx +94 -0
  125. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  126. package/blocks/quotes/components/quote-list.tsx +23 -5
  127. package/blocks/quotes/components/quote-settings.tsx +184 -0
  128. package/blocks/quotes/components/quote-templates.tsx +189 -0
  129. package/blocks/quotes/components/quote-viewer.tsx +16 -3
  130. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  131. package/blocks/quotes/lib/quote-admin.ts +264 -0
  132. package/blocks/sequencer/README.md +55 -0
  133. package/blocks/sequencer/block.json +172 -5
  134. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  135. package/blocks/sequencer/components/sequence-builder.tsx +13 -3
  136. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  137. package/blocks/sequencer/components/sequence-detail.tsx +6 -0
  138. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  139. package/blocks/sequencer/components/sequence-enrollments.tsx +2 -1
  140. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  141. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  142. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  143. package/blocks/sequencer/components/sequences-screen.tsx +36 -6
  144. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  145. package/blocks/sequencer/components/step-editor.tsx +10 -1
  146. package/blocks/sequencer/components/step-templates.tsx +224 -0
  147. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  148. package/blocks/sequencer/lib/sequencer-utils.ts +4 -0
  149. package/blocks/signals/README.md +39 -0
  150. package/blocks/signals/block.json +287 -0
  151. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  152. package/blocks/signals/components/listener-form.tsx +184 -0
  153. package/blocks/signals/components/matched-posts.tsx +195 -0
  154. package/blocks/signals/components/post-engagements.tsx +114 -0
  155. package/blocks/signals/components/record-social-tab.tsx +49 -0
  156. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  157. package/blocks/signals/components/social-settings.tsx +128 -0
  158. package/blocks/signals/components/source-lists.tsx +83 -0
  159. package/blocks/signals/components/top-engagers.tsx +68 -0
  160. package/blocks/signals/components/x-integration.tsx +129 -0
  161. package/blocks/signals/lib/signals-types.ts +228 -0
  162. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  163. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  164. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  165. package/blocks/visitors/components/traffic-chart.tsx +3 -2
  166. package/blocks/web-analytics/README.md +102 -0
  167. package/blocks/web-analytics/block.json +292 -0
  168. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  169. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  170. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  171. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  172. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  173. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  174. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  175. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  176. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  177. package/blocks/web-analytics/components/report-card.tsx +71 -0
  178. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  179. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  180. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  181. package/blocks/web-analytics/lib/page-path.ts +14 -0
  182. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  183. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  184. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  185. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  186. package/blocks/workbench/README.md +54 -2
  187. package/blocks/workbench/block.json +234 -5
  188. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  189. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  190. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  191. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  192. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  193. package/blocks/workbench/components/workbench-grid.tsx +70 -7
  194. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  195. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  196. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  197. package/blocks/workbench/components/workbench-screen.tsx +50 -17
  198. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  199. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  200. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  201. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  202. package/blocks/workbench/lib/api-status.ts +8 -0
  203. package/blocks/workbench/lib/automation-types.ts +257 -0
  204. package/dist/cli.js +257 -51
  205. package/dist/index.browser.js +1 -1
  206. package/dist/index.browser.js.map +1 -1
  207. package/dist/index.browser.mjs +1 -1
  208. package/dist/index.browser.mjs.map +1 -1
  209. package/dist/index.js +1 -1
  210. package/dist/index.js.map +1 -1
  211. package/dist/index.mjs +1 -1
  212. package/dist/index.mjs.map +1 -1
  213. package/dist/react.browser.js +1 -1
  214. package/dist/react.browser.js.map +1 -1
  215. package/dist/react.browser.mjs +1 -1
  216. package/dist/react.browser.mjs.map +1 -1
  217. package/dist/react.js +1 -1
  218. package/dist/react.js.map +1 -1
  219. package/dist/react.mjs +1 -1
  220. package/dist/react.mjs.map +1 -1
  221. package/dist/types/blocks/auth.d.ts +45 -0
  222. package/dist/types/blocks/create.d.ts +5 -2
  223. package/dist/types/version.d.ts +2 -2
  224. package/docs/api/forms.md +1 -1
  225. package/package.json +1 -1
  226. package/stacks/6sense/pages/contacts.tsx.tmpl +2 -0
  227. package/stacks/6sense.json +20 -0
  228. package/stacks/apollo/pages/calls.tsx.tmpl +9 -9
  229. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  230. package/stacks/apollo/pages/contacts.tsx.tmpl +6 -0
  231. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +3 -3
  232. package/stacks/apollo/pages/lists.tsx.tmpl +14 -2
  233. package/stacks/apollo/pages/sequences.tsx.tmpl +8 -2
  234. package/stacks/apollo.json +33 -2
  235. package/stacks/clay/pages/contacts.tsx.tmpl +2 -0
  236. package/stacks/clay/pages/lists.tsx.tmpl +14 -2
  237. package/stacks/gong/pages/calls.tsx.tmpl +9 -9
  238. package/stacks/gong/pages/contacts.tsx.tmpl +2 -0
  239. package/stacks/hootsuite/pages/contacts.tsx.tmpl +2 -0
  240. package/stacks/hootsuite.json +10 -0
  241. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  242. package/stacks/hubspot/pages/companies.tsx.tmpl +2 -0
  243. package/stacks/hubspot/pages/contacts.tsx.tmpl +6 -0
  244. package/stacks/hubspot/pages/deals.tsx.tmpl +4 -0
  245. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  246. package/stacks/hubspot/pages/inbox.tsx.tmpl +2 -2
  247. package/stacks/hubspot/pages/sequences.tsx.tmpl +8 -2
  248. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  249. package/stacks/hubspot.json +45 -2
  250. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +3 -3
  251. package/stacks/intercom.json +10 -0
  252. package/stacks/mailchimp/pages/contacts.tsx.tmpl +2 -0
  253. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  254. package/stacks/mailchimp.json +2 -1
  255. package/stacks/outreach/pages/calls.tsx.tmpl +9 -9
  256. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  257. package/stacks/outreach/pages/contacts.tsx.tmpl +2 -0
  258. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +3 -3
  259. package/stacks/outreach/pages/sequences.tsx.tmpl +8 -2
  260. package/stacks/outreach.json +13 -2
  261. package/stacks/pipedrive/pages/contacts.tsx.tmpl +2 -0
  262. package/stacks/pipedrive/pages/inbox.tsx.tmpl +2 -2
  263. package/stacks/pipedrive.json +10 -0
  264. package/stacks/salesforce/pages/companies.tsx.tmpl +2 -0
  265. package/stacks/salesforce/pages/contacts.tsx.tmpl +6 -0
  266. package/stacks/salesforce/pages/deals.tsx.tmpl +4 -0
  267. package/stacks/salesforce.json +10 -0
  268. package/stacks/zoominfo/pages/contacts.tsx.tmpl +4 -0
  269. package/stacks/zoominfo/pages/lists.tsx.tmpl +14 -2
  270. package/stacks/zoominfo.json +20 -0
  271. package/styles/styles.css +1 -1
  272. package/templates/auth/auth-route.ts.tmpl +16 -0
  273. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  274. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  275. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  276. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  277. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  278. package/templates/auth/middleware.ts.tmpl +14 -0
  279. package/templates/next-app/README.md.tmpl +5 -0
@@ -2,13 +2,17 @@
2
2
  "$schema": "../block.schema.json",
3
3
  "id": "inbox",
4
4
  "title": "AI inbox",
5
- "summary": "Unified reply inbox for email, SMS and LinkedIn: thread list, conversation view, AI-drafted replies, tagging and assignment.",
5
+ "summary": "Unified reply inbox for email, SMS and LinkedIn: threads, AI-drafted replies, new messages, search, read state, tag library, tagging and assignment.",
6
6
  "features": [
7
7
  "one queue across email, SMS and LinkedIn with channel and status filters",
8
8
  "conversation view with unsent drafts flagged",
9
9
  "AI draft (confirmed, billable) and send (confirmed, external) in one composer",
10
10
  "tag and assign teammates",
11
- "drop-in two-pane InboxScreen"
11
+ "drop-in two-pane InboxScreen",
12
+ "compose a new email or text from an explicit sender (confirmed)",
13
+ "server-side search across email and LinkedIn",
14
+ "tag library (create, rename, delete) and blocked senders",
15
+ "mark read or unread, remove a tag, unassign, block a sender"
12
16
  ],
13
17
  "dependsOn": [
14
18
  "crm",
@@ -16,17 +20,33 @@
16
20
  ],
17
21
  "operations": [
18
22
  "assign_reply_inbox__reply_id__assign_post",
23
+ "block_inbox_sender_inbox_blocked_senders_post",
24
+ "compose_inbox_email_inbox_emails_compose_post",
25
+ "compose_inbox_sms_inbox_sms_compose_post",
19
26
  "create_inbox_tag_inbox_tags_post",
27
+ "delete_inbox_tag_inbox_tags__tag_id__delete",
20
28
  "generate_inbox_draft_inbox_drafts_generate_post",
21
29
  "get_reply_inbox__reply_id__get",
30
+ "list_inbox_blocked_senders_inbox_blocked_senders_get",
22
31
  "list_inbox_inbox_get",
23
32
  "list_inbox_linkedin_accounts_inbox_linkedin_accounts_get",
24
33
  "list_inbox_mailboxes_inbox_mailboxes_get",
34
+ "list_inbox_mailboxes_paginated_inbox_mailboxes_paginated_get",
35
+ "list_inbox_tags_workflows_inbox_tags_get",
25
36
  "list_inbox_users_inbox_users_get",
26
37
  "list_sms_phone_numbers_sms_phone_numbers_get",
38
+ "mark_inbox_email_read_inbox_emails__email_id__mark_read_post",
39
+ "mark_inbox_linkedin_conversation_read_inbox_linkedin_accounts__account_id__conversations__conversation_id__read_post",
40
+ "mark_inbox_linkedin_conversation_unread_inbox_linkedin_accounts__account_id__conversations__conversation_id__unread_post",
41
+ "mark_inbox_sms_read_inbox_sms__thread_id__mark_read_post",
42
+ "remove_channel_tag_inbox_channels__channel_id__tags_delete",
27
43
  "search_inbox_emails_inbox_emails_search_post",
44
+ "search_inbox_linkedin_conversations_inbox_linkedin_conversations_search_post",
28
45
  "send_reply_inbox__reply_id__send_post",
29
- "tag_reply_inbox__reply_id__tag_post"
46
+ "tag_reply_inbox__reply_id__tag_post",
47
+ "unassign_channel_inbox_channels__channel_id__assign_delete",
48
+ "unblock_inbox_sender_inbox_blocked_senders__blocked_sender_id__delete",
49
+ "update_inbox_tag_inbox_tags__tag_id__put"
30
50
  ],
31
51
  "components": [
32
52
  {
@@ -83,10 +103,88 @@
83
103
  "external"
84
104
  ]
85
105
  },
106
+ {
107
+ "name": "ComposeMessage",
108
+ "file": "components/compose-message.tsx",
109
+ "purpose": "Write a NEW email or text (not a reply): choose a sender you may send as (never defaulted), recipients, subject and body; sending reaches a real person and is confirmed with the sender and recipient count.",
110
+ "operations": [
111
+ "compose_inbox_email_inbox_emails_compose_post",
112
+ "compose_inbox_sms_inbox_sms_compose_post",
113
+ "list_inbox_mailboxes_paginated_inbox_mailboxes_paginated_get",
114
+ "list_sms_phone_numbers_sms_phone_numbers_get"
115
+ ],
116
+ "tiers": [
117
+ "read",
118
+ "external"
119
+ ]
120
+ },
121
+ {
122
+ "name": "InboxSearch",
123
+ "file": "components/inbox-search.tsx",
124
+ "purpose": "Search every email thread or LinkedIn conversation on the server (not just the loaded page) and open a result.",
125
+ "operations": [
126
+ "list_inbox_mailboxes_paginated_inbox_mailboxes_paginated_get",
127
+ "search_inbox_emails_inbox_emails_search_post",
128
+ "search_inbox_linkedin_conversations_inbox_linkedin_conversations_search_post"
129
+ ],
130
+ "tiers": [
131
+ "read"
132
+ ]
133
+ },
134
+ {
135
+ "name": "TagManager",
136
+ "file": "components/tag-manager.tsx",
137
+ "purpose": "The tag library: list, create, rename or recolour (only changed fields are sent) and delete (confirmed) inbox tags; built-in tags follow the API's edit and delete policy.",
138
+ "operations": [
139
+ "create_inbox_tag_inbox_tags_post",
140
+ "delete_inbox_tag_inbox_tags__tag_id__delete",
141
+ "list_inbox_tags_workflows_inbox_tags_get",
142
+ "update_inbox_tag_inbox_tags__tag_id__put"
143
+ ],
144
+ "tiers": [
145
+ "read",
146
+ "write",
147
+ "destructive"
148
+ ]
149
+ },
150
+ {
151
+ "name": "ThreadActions",
152
+ "file": "components/thread-actions.tsx",
153
+ "purpose": "Per-conversation housekeeping: mark read (and unread on LinkedIn), remove a tag or unassign a teammate (confirmed), block the sender.",
154
+ "operations": [
155
+ "block_inbox_sender_inbox_blocked_senders_post",
156
+ "mark_inbox_email_read_inbox_emails__email_id__mark_read_post",
157
+ "mark_inbox_linkedin_conversation_read_inbox_linkedin_accounts__account_id__conversations__conversation_id__read_post",
158
+ "mark_inbox_linkedin_conversation_unread_inbox_linkedin_accounts__account_id__conversations__conversation_id__unread_post",
159
+ "mark_inbox_sms_read_inbox_sms__thread_id__mark_read_post",
160
+ "remove_channel_tag_inbox_channels__channel_id__tags_delete",
161
+ "unassign_channel_inbox_channels__channel_id__assign_delete"
162
+ ],
163
+ "tiers": [
164
+ "write",
165
+ "destructive"
166
+ ]
167
+ },
168
+ {
169
+ "name": "BlockedSenders",
170
+ "file": "components/blocked-senders.tsx",
171
+ "purpose": "Blocked senders per mailbox: list, block one, unblock one (confirmed).",
172
+ "operations": [
173
+ "block_inbox_sender_inbox_blocked_senders_post",
174
+ "list_inbox_blocked_senders_inbox_blocked_senders_get",
175
+ "list_inbox_mailboxes_paginated_inbox_mailboxes_paginated_get",
176
+ "unblock_inbox_sender_inbox_blocked_senders__blocked_sender_id__delete"
177
+ ],
178
+ "tiers": [
179
+ "read",
180
+ "write",
181
+ "destructive"
182
+ ]
183
+ },
86
184
  {
87
185
  "name": "InboxScreen",
88
186
  "file": "components/inbox-screen.tsx",
89
- "purpose": "Finished two-pane inbox composing the list, thread view, composer and tag/assign panel.",
187
+ "purpose": "Finished two-pane inbox composing the list, thread view, composer and tag/assign panel, plus New message, Search all and Manage (tags, blocked senders) and per-conversation housekeeping.",
90
188
  "operations": [],
91
189
  "tiers": []
92
190
  },
@@ -127,6 +225,14 @@
127
225
  "operations": [
128
226
  "get_reply_inbox__reply_id__get"
129
227
  ]
228
+ },
229
+ {
230
+ "name": "useInboxMailboxes",
231
+ "file": "hooks/use-inbox-mailboxes.ts",
232
+ "purpose": "Every mailbox the caller can see and whether they may send as it, read page by page; reports when the page cap truncated the list.",
233
+ "operations": [
234
+ "list_inbox_mailboxes_paginated_inbox_mailboxes_paginated_get"
235
+ ]
130
236
  }
131
237
  ],
132
238
  "lib": [
@@ -137,6 +243,10 @@
137
243
  {
138
244
  "file": "lib/idempotency.ts",
139
245
  "purpose": "useIdempotencyKey: one Idempotency-Key per distinct payload, rotated when the body changes so an edited retry never hits a 409."
246
+ },
247
+ {
248
+ "file": "lib/inbox-manage.ts",
249
+ "purpose": "Compose validation and HTML conversion, mailbox, tag, blocked-sender and search-result readers, per-channel route helpers and the hidden-thread 404 check."
140
250
  }
141
251
  ],
142
252
  "recipes": [
@@ -178,9 +288,11 @@
178
288
  ],
179
289
  "scopes": [
180
290
  "campaigns:read",
291
+ "inbox:delete",
181
292
  "inbox:read",
182
293
  "inbox:run",
183
- "inbox:write"
294
+ "inbox:write",
295
+ "workflows:read"
184
296
  ],
185
297
  "defaults": [
186
298
  {
@@ -199,9 +311,9 @@
199
311
  "askHuman": false
200
312
  },
201
313
  {
202
- "decision": "Where to get the list of tags the picker offers (no list-tags operation exists)",
203
- "default": "Pass availableTags from your own state; tags created in the panel are added for the session",
204
- "askHuman": true
314
+ "decision": "Where the tag picker gets its tags",
315
+ "default": "GET /workflows/inbox-tags through TagManager (the Manage sheet); pass availableTags to seed it before the sheet is opened. Tags on the loaded threads are always included",
316
+ "askHuman": false
205
317
  },
206
318
  {
207
319
  "decision": "Filter by assignee",
@@ -212,6 +324,16 @@
212
324
  "decision": "Which mailbox, number or LinkedIn account a reply is sent from",
213
325
  "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
326
  "askHuman": false
327
+ },
328
+ {
329
+ "decision": "Which sender a NEW message goes from",
330
+ "default": "Never defaulted: the person chooses a mailbox (only ones they may send as are selectable) or an SMS number, and the confirm dialog names it. The API refuses a compose without a sender",
331
+ "askHuman": false
332
+ },
333
+ {
334
+ "decision": "Snooze and archive",
335
+ "default": "Not offered: the API has no snooze or archive operation for inbox threads. Use tags and assignment to triage",
336
+ "askHuman": false
215
337
  }
216
338
  ],
217
339
  "peerDependencies": {
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ /**
4
+ * BlockedSenders: the senders whose mail is kept out of a mailbox's inbox. See them, block one, unblock one.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. GET /inbox/blocked-senders (aiInbox.listInboxBlockedSenders) READ
8
+ * 2. GET /inbox/mailboxes/paginated (aiInbox.listInboxMailboxesPaginated) READ - which mailbox the block applies to
9
+ * 3. POST /inbox/blocked-senders (aiInbox.blockInboxSender) write
10
+ * 4. DELETE /inbox/blocked-senders/{id} (aiInbox.unblockInboxSender) DESTRUCTIVE -> ConfirmSpendDialog first
11
+ * A block is per mailbox, so the form makes you choose the mailbox; nothing is guessed.
12
+ */
13
+ import { useState } from "react";
14
+ import { Ban, Trash2 } from "lucide-react";
15
+ import { Button, Input, Skeleton } from "@graph8/sdk/ui";
16
+ import { ApiError } from "@/g8/components/api-error";
17
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
18
+ import { EmptyState } from "@/g8/components/empty-state";
19
+ import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
20
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
21
+ import { FirstNotice } from "@/g8/components/paging-notices";
22
+ import { useAllPages } from "@/g8/hooks/use-all-pages";
23
+ import { hasNextOf } from "@/g8/lib/paging";
24
+ import { useInboxMailboxes } from "../hooks/use-inbox-mailboxes";
25
+ import { blockedSenders, isEmail } from "../lib/inbox-manage";
26
+ import type { BlockedSender } from "../lib/inbox-manage";
27
+
28
+ export interface BlockedSendersProps {
29
+ className?: string;
30
+ }
31
+
32
+ export function BlockedSenders({ className }: BlockedSendersProps) {
33
+ const [mailbox, setMailbox] = useState("");
34
+ const [email, setEmail] = useState("");
35
+ const [reason, setReason] = useState("");
36
+ const [removing, setRemoving] = useState<BlockedSender | null>(null);
37
+ const blocked = useAllPages<BlockedSender>(
38
+ async (page, signal) => {
39
+ const env = await g8Get("/inbox/blocked-senders", { page, page_size: 100 }, { signal });
40
+ const rows = blockedSenders(env);
41
+ return { rows, hasNext: hasNextOf(env, 100, rows.length) };
42
+ },
43
+ [],
44
+ { pageSize: 100 },
45
+ );
46
+ const list = { data: blocked.loaded ? blocked.items : undefined, loading: blocked.loading, error: blocked.error, refetch: blocked.refetch };
47
+ const boxes = useInboxMailboxes();
48
+ const block = useG8Mutation(async () => {
49
+ await g8Post("/inbox/blocked-senders", { mailbox, sender_email: email.trim(), ...(reason.trim() ? { reason: reason.trim() } : {}) });
50
+ setEmail("");
51
+ setReason("");
52
+ blocked.refetch();
53
+ }, { invalidates: ["inbox"] });
54
+ const unblock = useG8Mutation(async (s: BlockedSender) => {
55
+ await g8Delete(`/inbox/blocked-senders/${encodeURIComponent(s.id)}`);
56
+ setRemoving(null);
57
+ blocked.refetch();
58
+ }, { invalidates: ["inbox"], onError: () => setRemoving(null) });
59
+
60
+ const valid = Boolean(mailbox) && isEmail(email);
61
+ return (
62
+ <div className={`space-y-3 ${className ?? ""}`}>
63
+ <h3 className="font-medium">Blocked senders</h3>
64
+ <form className="space-y-2 rounded-md border border-border p-3" onSubmit={(e) => { e.preventDefault(); if (valid) void block.mutate(undefined); }}>
65
+ <select aria-label="Mailbox" className="h-9 w-full rounded-md border border-border bg-background px-2 text-sm" value={mailbox} disabled={boxes.loading} onChange={(e) => setMailbox(e.target.value)}>
66
+ <option value="">{boxes.loading ? "Loading mailboxes..." : "Choose the mailbox to block them in"}</option>
67
+ {(boxes.data ?? []).map((m) => <option key={m.address} value={m.address}>{m.label}</option>)}
68
+ </select>
69
+ {boxes.error ? <ApiError error={boxes.error} title="Could not load your mailboxes" onRetry={() => void boxes.refetch()} /> : null}
70
+ <Input aria-label="Sender email" placeholder="sender@company.com" value={email} onChange={(e) => setEmail(e.target.value)} />
71
+ <Input aria-label="Reason" placeholder="Reason (optional)" value={reason} onChange={(e) => setReason(e.target.value)} />
72
+ {block.error ? <ApiError error={block.error} title="Could not block the sender" /> : null}
73
+ <div className="flex justify-end"><Button type="submit" size="sm" disabled={!valid || block.loading}><Ban className="mr-1 size-4" aria-hidden />Block sender</Button></div>
74
+ </form>
75
+ {list.error ? <ApiError error={list.error} title="Could not load blocked senders" onRetry={() => void list.refetch()} /> : null}
76
+ {list.loading && !list.data ? <Skeleton className="h-10 w-full" /> : null}
77
+ {list.data && list.data.length === 0 ? <EmptyState compact title="Nobody is blocked" description="Mail from blocked senders never reaches the inbox." /> : null}
78
+ {list.data && list.data.length > 0 ? (
79
+ <ul aria-label="Blocked senders" className="divide-y rounded-md border border-border text-sm">
80
+ {list.data.map((s) => (
81
+ <li key={s.id} className="flex items-center gap-2 p-2">
82
+ <span className="min-w-0 flex-1"><span className="font-medium">{s.email}</span><span className="block truncate text-xs text-muted-foreground">{s.mailbox}{s.reason ? ` - ${s.reason}` : ""}</span></span>
83
+ <Button size="sm" variant="ghost" aria-label={`Unblock ${s.email}`} onClick={() => setRemoving(s)}><Trash2 className="size-4" aria-hidden /></Button>
84
+ </li>
85
+ ))}
86
+ </ul>
87
+ ) : null}
88
+ <FirstNotice shown={blocked.items.length} limit={0} force={blocked.truncated} noun="blocked senders" />
89
+ {unblock.error ? <ApiError error={unblock.error} title="Could not unblock the sender" /> : null}
90
+ <ConfirmSpendDialog
91
+ open={removing != null}
92
+ tier="destructive"
93
+ title={`Unblock ${removing?.email ?? ""}?`}
94
+ description="Their mail starts reaching the inbox again. Mail that was held back while they were blocked is not recovered."
95
+ confirmLabel="Unblock"
96
+ affected={1}
97
+ confirming={unblock.loading}
98
+ onConfirm={() => removing && void unblock.mutate(removing)}
99
+ onCancel={() => setRemoving(null)}
100
+ />
101
+ </div>
102
+ );
103
+ }
@@ -0,0 +1,193 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ComposeMessage: write a NEW message (not a reply) to someone, by email or by text, from a sender you choose.
5
+ *
6
+ * SDK operations, in order:
7
+ * Email: 1. GET /inbox/mailboxes/paginated (aiInbox.listInboxMailboxesPaginated) READ - the mailboxes you may send as
8
+ * (each row says `can_send`, and why not when it cannot)
9
+ * 2. POST /inbox/emails/compose (aiInbox.composeInboxEmail) EXTERNAL -> ConfirmSpendDialog first
10
+ * SMS: 1. GET /sms/phone-numbers (channels.listSmsPhoneNumbers) READ - the numbers you own
11
+ * 2. POST /inbox/sms/compose (aiInbox.composeInboxSms) EXTERNAL -> ConfirmSpendDialog first
12
+ *
13
+ * Sending reaches a real person and cannot be recalled. The sender is NEVER defaulted: the API refuses a compose
14
+ * without `from_mailbox` / `from_number` (and, for a reply, `from_address`), so this form makes the person choose one
15
+ * and shows it in the confirm dialog ("from hamza@acme.com to maya@northwind.io"). A mailbox the caller may not send
16
+ * as is listed but disabled, with the reason. A fixed Idempotency-Key per distinct payload makes a double click or a
17
+ * retry after a timeout safe; it rotates after a success.
18
+ *
19
+ * LinkedIn is not offered: `POST /inbox/linkedin/compose` only continues an EXISTING conversation (it needs the
20
+ * conversation id), which is what ReplyComposer already does from the thread.
21
+ */
22
+ import { useState } from "react";
23
+ import { Button, Input, Textarea } from "@graph8/sdk/ui";
24
+ import { ApiError } from "@/g8/components/api-error";
25
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
26
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
27
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
28
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
29
+ import { useBrand } from "@/g8/hooks/use-brand";
30
+ import { FirstNotice } from "@/g8/components/paging-notices";
31
+ import { useInboxMailboxes } from "../hooks/use-inbox-mailboxes";
32
+ import { useIdempotencyKey } from "../lib/idempotency";
33
+ import { senderOptions } from "../lib/inbox-types";
34
+ import {
35
+ emailComposeProblems,
36
+ normalizePhone,
37
+ smsComposeProblems,
38
+ splitAddresses,
39
+ textToHtml,
40
+ } from "../lib/inbox-manage";
41
+
42
+ export type ComposeChannel = "email" | "sms";
43
+
44
+ export interface ComposeMessageProps {
45
+ defaultChannel?: ComposeChannel;
46
+ /** Prefill the recipient (an email address, or a phone number for SMS). */
47
+ defaultTo?: string;
48
+ defaultSubject?: string;
49
+ /** Called after the message was accepted by the API. */
50
+ onSent?: (channel: ComposeChannel) => void;
51
+ onCancel?: () => void;
52
+ className?: string;
53
+ }
54
+
55
+ const SELECT = "h-9 w-full rounded-md border border-border bg-background px-2 text-sm";
56
+
57
+ export function ComposeMessage({ defaultChannel = "email", defaultTo = "", defaultSubject = "", onSent, onCancel, className }: ComposeMessageProps) {
58
+ const brand = useBrand();
59
+ const [channel, setChannel] = useState<ComposeChannel>(defaultChannel);
60
+ const [from, setFrom] = useState("");
61
+ const [to, setTo] = useState(defaultTo);
62
+ const [cc, setCc] = useState("");
63
+ const [subject, setSubject] = useState(defaultSubject);
64
+ const [body, setBody] = useState("");
65
+ const [signature, setSignature] = useState(true);
66
+ const [confirming, setConfirming] = useState(false);
67
+ const [sent, setSent] = useState<string | null>(null);
68
+ const idempotency = useIdempotencyKey(JSON.stringify([channel, from, to, cc, subject, body, signature]));
69
+
70
+ const mailboxes = useInboxMailboxes({ enabled: channel === "email" });
71
+ const numbers = useG8Query(
72
+ async (signal) => senderOptions("sms", (await g8Get("/sms/phone-numbers", undefined, { signal })).data),
73
+ [],
74
+ { enabled: channel === "sms" },
75
+ );
76
+
77
+ const send = useG8Mutation(async () => {
78
+ // Belt and braces: the form already blocks this, but a send without an explicit sender must never leave.
79
+ if (!from.trim()) throw new Error("Choose a sender before sending.");
80
+ const chosen = channel === "email" ? (mailboxes.data ?? []).find((m) => m.address === from) : undefined;
81
+ if (chosen && !chosen.canSend) throw new Error(`You are not allowed to send as ${chosen.address} (${chosen.reason ?? "not authorised"}).`);
82
+ const key = { idempotencyKey: idempotency.get() };
83
+ if (channel === "email") {
84
+ return g8Post("/inbox/emails/compose", {
85
+ from_mailbox: from,
86
+ to: splitAddresses(to),
87
+ cc: splitAddresses(cc),
88
+ subject: subject.trim(),
89
+ content: textToHtml(body),
90
+ include_signature: signature,
91
+ save_as_draft: false,
92
+ }, key);
93
+ }
94
+ return g8Post("/inbox/sms/compose", { from_number: from, to_number: normalizePhone(to), body: body.trim(), save_as_draft: false }, key);
95
+ }, { invalidates: ["inbox"] });
96
+
97
+ const problems = channel === "email" ? emailComposeProblems({ from, to, cc, subject, body }) : smsComposeProblems({ from, to, body });
98
+ const recipients = channel === "email" ? splitAddresses(to) : to.trim() ? [to.trim()] : [];
99
+ const switchTo = (next: ComposeChannel) => {
100
+ if (next === channel) return;
101
+ setChannel(next);
102
+ setFrom(""); // a sender of one channel is never a sender of the other
103
+ setTo("");
104
+ setCc("");
105
+ setSent(null);
106
+ send.reset();
107
+ };
108
+
109
+ const confirm = async () => {
110
+ try {
111
+ await send.mutateAsync(undefined);
112
+ idempotency.rotate();
113
+ setSent(channel === "email" ? "Email sent." : "Text sent.");
114
+ setBody("");
115
+ setSubject("");
116
+ setConfirming(false);
117
+ onSent?.(channel);
118
+ } catch {
119
+ setConfirming(false); // the error renders below; the form keeps what was typed so the person can retry
120
+ }
121
+ };
122
+
123
+ const sendersLoading = channel === "email" ? mailboxes.loading : numbers.loading;
124
+ const senderError = channel === "email" ? mailboxes.error : numbers.error;
125
+ const senderChoices =
126
+ channel === "email"
127
+ ? (mailboxes.data ?? []).map((m) => ({ value: m.address, label: m.canSend ? m.label : `${m.label} (${m.reason})`, disabled: !m.canSend }))
128
+ : (numbers.data ?? []).map((n) => ({ value: n.value, label: n.label, disabled: false }));
129
+ const noSenders = !sendersLoading && !senderError && senderChoices.length === 0;
130
+
131
+ return (
132
+ <div className={`space-y-3 ${className ?? ""}`}>
133
+ <div role="group" aria-label="Channel" className="flex gap-1">
134
+ <Button size="sm" variant={channel === "email" ? "default" : "outline"} onClick={() => switchTo("email")}>Email</Button>
135
+ <Button size="sm" variant={channel === "sms" ? "default" : "outline"} onClick={() => switchTo("sms")}>Text message</Button>
136
+ </div>
137
+
138
+ <label className="block space-y-1 text-sm">
139
+ <span className="font-medium">{channel === "email" ? "From" : "Send from"}</span>
140
+ <select aria-label={channel === "email" ? "From mailbox" : "From number"} className={SELECT} value={from} disabled={sendersLoading} onChange={(e) => setFrom(e.target.value)}>
141
+ <option value="">{sendersLoading ? "Loading senders..." : channel === "email" ? "Choose the mailbox to send from" : "Choose the number to text from"}</option>
142
+ {senderChoices.map((o) => <option key={o.value} value={o.value} disabled={o.disabled}>{o.label}</option>)}
143
+ </select>
144
+ </label>
145
+ {senderError ? <ApiError error={senderError} title="Could not load your senders" onRetry={() => void (channel === "email" ? mailboxes : numbers).refetch()} /> : null}
146
+ {channel === "email" ? <FirstNotice shown={mailboxes.items.length} limit={0} force={mailboxes.truncated} noun="mailboxes" /> : null}
147
+ {noSenders ? <p className="text-sm text-muted-foreground">{channel === "email" ? `No mailbox is connected yet. Connect one in the ${brand.linkLabel} app first.` : "No SMS number on this account yet."}</p> : null}
148
+
149
+ <label className="block space-y-1 text-sm">
150
+ <span className="font-medium">To</span>
151
+ <Input aria-label="To" placeholder={channel === "email" ? "name@company.com, another@company.com" : "+14155550100"} value={to} onChange={(e) => setTo(e.target.value)} />
152
+ </label>
153
+ {channel === "email" ? (
154
+ <>
155
+ <Input aria-label="Cc" placeholder="Cc (optional)" value={cc} onChange={(e) => setCc(e.target.value)} />
156
+ <Input aria-label="Subject" placeholder="Subject" value={subject} onChange={(e) => setSubject(e.target.value)} />
157
+ </>
158
+ ) : null}
159
+ <Textarea aria-label="Message" rows={8} placeholder="Write your message" value={body} onChange={(e) => setBody(e.target.value)} />
160
+ {channel === "email" ? (
161
+ <label className="flex items-center gap-2 text-sm">
162
+ <input type="checkbox" checked={signature} onChange={(e) => setSignature(e.target.checked)} />
163
+ Add my email signature
164
+ </label>
165
+ ) : null}
166
+
167
+ {problems.length > 0 && (to || body || from) ? (
168
+ <ul aria-label="Still needed" className="list-disc pl-5 text-xs text-muted-foreground">{problems.map((p) => <li key={p}>{p}</li>)}</ul>
169
+ ) : null}
170
+ {send.error ? <ApiError error={send.error} title="Could not send the message" onRetry={() => setConfirming(true)} /> : null}
171
+ {sent ? <p role="status" className="text-sm text-positive-foreground">{sent}</p> : null}
172
+
173
+ <div className="flex justify-end gap-2">
174
+ {onCancel ? <Button variant="outline" onClick={onCancel}>Cancel</Button> : null}
175
+ <Button disabled={problems.length > 0 || send.loading} onClick={() => { setSent(null); setConfirming(true); }}>
176
+ {channel === "email" ? "Send email" : "Send text"}
177
+ </Button>
178
+ </div>
179
+
180
+ <ConfirmSpendDialog
181
+ open={confirming}
182
+ tier="external"
183
+ title={`Send this ${channel === "email" ? "email" : "text message"}?`}
184
+ description={`It goes to ${recipients.join(", ") || "the recipient"} from ${from || "no sender chosen"}. It reaches a real person and cannot be recalled.`}
185
+ recipients={recipients.length}
186
+ confirmLabel={channel === "email" ? "Send email" : "Send text"}
187
+ confirming={send.loading}
188
+ onConfirm={() => void confirm()}
189
+ onCancel={() => setConfirming(false)}
190
+ />
191
+ </div>
192
+ );
193
+ }