@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
@@ -0,0 +1,90 @@
1
+ "use client";
2
+
3
+ /**
4
+ * PipelineTemplates: pick a ready-made pipeline for a list (for example "Find + verify missing emails") and add it.
5
+ * The catalog is filtered by the API to the recipes that fit this list's kind (people or companies).
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listPipelineTemplates GET /enrichment/lists/pipeline-templates?list_id (read, free) recipes + whether the workspace has a
9
+ * qualification template (recipes that score records first need one)
10
+ * 2. createListPipelineFromTemplate POST /enrichment/lists/{list_id}/pipelines/from-template { template_key } (write, free)
11
+ * saves the recipe TURNED OFF and creates any columns it needs. Adding a recipe the list already has REPLACES that
12
+ * pipeline's steps with the recipe's (it is saved by name), so that case goes through ConfirmSpendDialog (destructive).
13
+ *
14
+ * Nothing runs or charges here: open the new pipeline to price and run it.
15
+ */
16
+
17
+ import { useState } from "react";
18
+ import { Button } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
+ import { EmptyState } from "@/g8/components/empty-state";
22
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
23
+ import { StatusPill } from "@/g8/components/screen/status-pill";
24
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
25
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
26
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
27
+ import { itemsOf, templateExists } from "../lib/pipeline-types";
28
+ import type { ListPipeline, PipelineTemplate } from "../lib/pipeline-types";
29
+
30
+ export interface PipelineTemplatesProps {
31
+ listId: number;
32
+ open: boolean;
33
+ onOpenChange: (open: boolean) => void;
34
+ /** The pipelines already on the list, to warn before a recipe replaces one. */
35
+ existing: ListPipeline[];
36
+ /** Called with the saved pipeline. */
37
+ onAdded: (pipeline: ListPipeline) => void;
38
+ }
39
+
40
+ interface Catalog { items?: PipelineTemplate[]; has_qualification_templates?: boolean }
41
+
42
+ export function PipelineTemplates({ listId, open, onOpenChange, existing, onAdded }: PipelineTemplatesProps) {
43
+ const [replacing, setReplacing] = useState<PipelineTemplate | null>(null);
44
+ const [working, setWorking] = useState<string | null>(null);
45
+ const catalog = useG8Query(async (signal) => (await g8Get<Catalog>("/enrichment/lists/pipeline-templates", { list_id: listId }, { signal })).data, [listId], { enabled: open });
46
+ const add = useG8Mutation(
47
+ async (t: PipelineTemplate) => (await g8Post<ListPipeline>(`/enrichment/lists/${listId}/pipelines/from-template`, { template_key: t.key })).data,
48
+ { onSuccess: (p) => { setReplacing(null); setWorking(null); onAdded(p); onOpenChange(false); }, onError: () => { setWorking(null); setReplacing(null); } },
49
+ );
50
+ const templates = itemsOf<PipelineTemplate>(catalog.data);
51
+ const hasQualification = catalog.data?.has_qualification_templates === true;
52
+ const choose = (t: PipelineTemplate) => {
53
+ if (templateExists(t, existing)) { setReplacing(t); return; }
54
+ setWorking(t.key);
55
+ void add.mutate(t);
56
+ };
57
+
58
+ return (
59
+ <FormSheet open={open} onOpenChange={onOpenChange} title="Start from a template" description="Ready-made pipelines for this list. They are saved turned off; nothing runs until you run one." width="lg"
60
+ footer={<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>Close</Button>}>
61
+ {catalog.error ? <ApiError error={catalog.error} title="Could not load templates" onRetry={() => void catalog.refetch()} /> : null}
62
+ {catalog.loading && !catalog.data ? <p className="text-sm text-muted-foreground">Loading templates...</p> : null}
63
+ {!catalog.loading && !catalog.error && templates.length === 0 ? <EmptyState title="No templates for this list" description="The API has no ready-made pipeline for this kind of list." /> : null}
64
+ {add.error ? <ApiError error={add.error} title="Could not add the template" /> : null}
65
+ <ul className="flex flex-col gap-3">
66
+ {templates.map((t) => {
67
+ const needsQualification = t.requires_qualification && !hasQualification;
68
+ return (
69
+ <li key={t.key} className="rounded-md border p-3" data-testid="template">
70
+ <div className="flex items-start justify-between gap-3">
71
+ <div className="min-w-0">
72
+ <p className="text-sm font-medium">{t.name}</p>
73
+ <p className="text-xs text-muted-foreground">{t.description}</p>
74
+ <ol className="mt-2 flex flex-wrap gap-1" aria-label={`${t.name} steps`}>{t.step_names.map((n, i) => <li key={`${n}-${i}`}><StatusPill tone="neutral">{i + 1}. {n}</StatusPill></li>)}</ol>
75
+ {needsQualification ? <p className="mt-2 text-xs text-warning-foreground">Needs a qualification template, and this workspace has none yet.</p> : null}
76
+ {templateExists(t, existing) ? <p className="mt-2 text-xs text-muted-foreground">This list already has it. Adding it again resets its steps to the recipe.</p> : null}
77
+ </div>
78
+ <Button type="button" size="sm" variant="outline" disabled={needsQualification || working != null} loading={working === t.key} aria-label={`Add ${t.name}`} onClick={() => choose(t)}>{templateExists(t, existing) ? "Reset" : "Add"}</Button>
79
+ </div>
80
+ </li>
81
+ );
82
+ })}
83
+ </ul>
84
+ <ConfirmSpendDialog open={replacing != null} tier="destructive" title={`Reset "${replacing?.name ?? ""}" to the template`}
85
+ description="The pipeline's steps are replaced with the template's. Any edits you made to its steps are lost. Values it already wrote stay on the records."
86
+ affected={1} confirmLabel="Reset pipeline" confirming={add.loading}
87
+ onConfirm={() => replacing && void add.mutate(replacing)} onCancel={() => setReplacing(null)} />
88
+ </FormSheet>
89
+ );
90
+ }
@@ -104,6 +104,7 @@ export interface EnrichJob {
104
104
  successful_enrichments?: number | null;
105
105
  failed_enrichments?: number | null;
106
106
  total_credits_used?: number | null;
107
+ paused_at?: string | null;
107
108
  warnings?: string[] | null;
108
109
  provider_errors?: Array<{ record_id?: number; provider?: string; error?: string }> | null;
109
110
  }
@@ -129,6 +130,17 @@ export function isJobTerminal(status: string | undefined | null): boolean {
129
130
  }
130
131
 
131
132
  /**
133
+ * The API's own quote for a run (`max_total_credits` from the preflight), or null when it could not price it. Null means
134
+ * "no total": callers show that, never a made-up number.
135
+ */
136
+ export function quotedCredits(preflight: Pick<Preflight, "max_total_credits"> | undefined): number | null {
137
+ return preflight?.max_total_credits ?? null;
138
+ }
139
+
140
+ /**
141
+ * @deprecated Guesses 2 credits per record when the preflight cannot price a run, which is an invented price. The bulk panel
142
+ * no longer uses it (see `quotedCredits`); kept exported so existing imports keep working.
143
+ *
132
144
  * What to show as the cost of a run: the API's own quote when the preflight priced it, otherwise the flat
133
145
  * per-record estimate times the contacts that are really on the list. Never a balance.
134
146
  */
@@ -0,0 +1,234 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Shapes and pure helpers for list pipelines, auto-enrichment and the enrichment activity view.
5
+ *
6
+ * Everything here is read off the API's own responses (enrichment/interfaces/fastapi/list_pipelines_router.py,
7
+ * auto_enrichment_router.py, router.py). The contract types these payloads as open objects, so the shapes below are the
8
+ * handlers' documented fields; a field the API leaves out is `undefined`/`null` here and the UI shows a dash, never a
9
+ * guessed default.
10
+ *
11
+ * No prices live here: the cost of a pipeline run is whatever `POST .../estimate` answers for that exact list and
12
+ * pipeline (`required_credits`, an upper bound that ignores run conditions).
13
+ */
14
+
15
+ /** The step kinds a saved pipeline step can have. */
16
+ export type StepType = "ai" | "waterfall";
17
+
18
+ export interface PipelineStep {
19
+ id: string;
20
+ name?: string;
21
+ type: StepType;
22
+ config_ref: string;
23
+ run_condition?: string | null;
24
+ skip_existing_values?: boolean;
25
+ skip_recently_enriched?: boolean;
26
+ enabled?: boolean;
27
+ ui_meta?: Record<string, unknown> | null;
28
+ [extra: string]: unknown;
29
+ }
30
+
31
+ export interface StepProgress {
32
+ step_index: number;
33
+ step_id?: string | null;
34
+ name?: string | null;
35
+ job_id?: string | null;
36
+ status?: string | null;
37
+ total_records?: number;
38
+ processed_records?: number;
39
+ successful?: number;
40
+ failed?: number;
41
+ skipped_by_reason?: Record<string, number>;
42
+ note?: string | null;
43
+ }
44
+
45
+ export interface PipelineRun {
46
+ run_id: string;
47
+ status: string;
48
+ current_index: number;
49
+ total_steps: number;
50
+ started_at?: string | null;
51
+ error?: string | null;
52
+ cancelled_by?: string | null;
53
+ step_progress?: StepProgress[];
54
+ /** Present on a run read by id (GET .../pipeline-runs/{run_id}). */
55
+ pipeline_id?: string;
56
+ pipeline_name?: string;
57
+ list_id?: number;
58
+ current_job_id?: string | null;
59
+ }
60
+
61
+ export interface ListPipeline {
62
+ id: string;
63
+ list_id: number;
64
+ name: string;
65
+ steps: PipelineStep[];
66
+ enabled: boolean;
67
+ created_at?: string | null;
68
+ last_run?: PipelineRun | null;
69
+ }
70
+
71
+ export interface PipelineEstimate {
72
+ can_run: boolean;
73
+ subscription_valid?: boolean;
74
+ estimated_evaluations: number;
75
+ required_credits: number;
76
+ available_credits: number;
77
+ reason?: string | null;
78
+ steps: Array<{ step_id: string; name: string; type: string; estimated_records: number; required_credits: number }>;
79
+ }
80
+
81
+ export interface StepOption {
82
+ type: StepType;
83
+ config_ref: string;
84
+ name: string;
85
+ is_qualification?: boolean;
86
+ qualification_entity?: string | null;
87
+ uses_existing_field?: boolean;
88
+ target_column_name?: string | null;
89
+ }
90
+
91
+ export interface PipelineTemplate {
92
+ key: string;
93
+ name: string;
94
+ description: string;
95
+ target_type: string;
96
+ requires_qualification: boolean;
97
+ step_names: string[];
98
+ }
99
+
100
+ export interface AutoConfig {
101
+ id: string;
102
+ audience_id: number;
103
+ enrichment_type: StepType;
104
+ column_id: string;
105
+ trigger_on_insert: boolean;
106
+ created_at?: string | null;
107
+ created_by?: string | null;
108
+ }
109
+
110
+ export const MERGE_RULES = ["sticky", "user_wins", "mashup_wins"] as const;
111
+ export type MergeRule = (typeof MERGE_RULES)[number];
112
+
113
+ /** What each merge rule does, from enrichment/usecases/apply_mashup_merge/handler.py. */
114
+ export const MERGE_RULE_COPY: Record<MergeRule, { label: string; help: string }> = {
115
+ sticky: { label: "Keep edits, fill the rest", help: "Fields a person edited are left alone; every other field takes the matched value." },
116
+ user_wins: { label: "Never overwrite", help: "No matched value is copied onto the record." },
117
+ mashup_wins: { label: "Matched data wins", help: "Every field takes the matched value, including fields a person edited." },
118
+ };
119
+
120
+ export interface AutoSettings {
121
+ entity_type: "contact" | "company";
122
+ enabled: boolean;
123
+ default_rule: MergeRule;
124
+ per_field_overrides: Record<string, MergeRule>;
125
+ sync_policy_version?: number;
126
+ }
127
+
128
+ export interface JobControlResult {
129
+ job_id?: string;
130
+ success?: boolean;
131
+ message?: string;
132
+ previous_status?: string;
133
+ paused_at?: string;
134
+ resumed_at?: string;
135
+ }
136
+
137
+ /** The analytics windows the API accepts (anything else is a 422). */
138
+ export const PERIODS = [
139
+ { value: "last_7_days", label: "Last 7 days" },
140
+ { value: "last_30_days", label: "Last 30 days" },
141
+ { value: "last_90_days", label: "Last 90 days" },
142
+ { value: "current_month", label: "This month" },
143
+ { value: "last_month", label: "Last month" },
144
+ ] as const;
145
+ export type Period = (typeof PERIODS)[number]["value"];
146
+
147
+ export const GRANULARITIES = ["day", "week", "month"] as const;
148
+ export type Granularity = (typeof GRANULARITIES)[number];
149
+
150
+ /** Rows of an open `{ items }` payload, or of a bare array; anything else is an empty list. */
151
+ export function itemsOf<T>(data: unknown, key = "items"): T[] {
152
+ if (Array.isArray(data)) return data as T[];
153
+ const v = (data as Record<string, unknown> | null | undefined)?.[key];
154
+ return Array.isArray(v) ? (v as T[]) : [];
155
+ }
156
+
157
+ /** The only states a run never leaves (the app's useListPipelines treats everything else as in progress). */
158
+ export const RUN_TERMINAL = ["completed", "failed", "cancelled"] as const;
159
+
160
+ /**
161
+ * A run that can still change: poll it, offer Stop, block another Run. The backend creates every run as "pending", so
162
+ * "active" is "has a status and it is not terminal", never "equals running": a second run priced and confirmed while the
163
+ * first is pending would charge twice.
164
+ */
165
+ export function runIsActive(status: string | null | undefined): boolean {
166
+ const s = String(status ?? "").toLowerCase();
167
+ return s !== "" && !(RUN_TERMINAL as readonly string[]).includes(s);
168
+ }
169
+
170
+ /** The pipeline's steps that will run (a disabled step is skipped). */
171
+ export function enabledSteps(p: Pick<ListPipeline, "steps">): PipelineStep[] {
172
+ return p.steps.filter((s) => s.enabled !== false);
173
+ }
174
+
175
+ /** A unique, stable step id (1 to 64 characters) for a step added in the editor. */
176
+ export function newStepId(taken: readonly string[]): string {
177
+ let n = taken.length + 1;
178
+ while (taken.includes(`step-${n}`)) n += 1;
179
+ return `step-${n}`;
180
+ }
181
+
182
+ /** A step for a picked config. Defaults mirror the API's own: fill only empty values, skip recently enriched records. */
183
+ export function stepFromOption(option: StepOption, taken: readonly string[]): PipelineStep {
184
+ return { id: newStepId(taken), name: option.name, type: option.type, config_ref: option.config_ref, skip_existing_values: true, skip_recently_enriched: true, enabled: true };
185
+ }
186
+
187
+ /** Move the item at `index` by `delta` (clamped), returning a new array. */
188
+ export function moveItem<T>(items: readonly T[], index: number, delta: number): T[] {
189
+ const to = index + delta;
190
+ if (to < 0 || to >= items.length) return [...items];
191
+ const next = [...items];
192
+ [next[index], next[to]] = [next[to], next[index]];
193
+ return next;
194
+ }
195
+
196
+ /** The body of a save: the name and the FULL step list (the API replaces steps), keeping every field of an edited step. */
197
+ export function savePayload(name: string, steps: readonly PipelineStep[]): { name: string; steps: PipelineStep[] } {
198
+ return { name: name.trim(), steps: steps.map((s) => ({ ...s })) };
199
+ }
200
+
201
+ /** Did the editor change anything the API would store? */
202
+ export function pipelineChanged(original: Pick<ListPipeline, "name" | "steps">, name: string, steps: readonly PipelineStep[]): boolean {
203
+ return original.name !== name.trim() || JSON.stringify(original.steps) !== JSON.stringify(steps);
204
+ }
205
+
206
+ /** The label of a step: its own name, else the name of the config it points at, else its kind. Never the raw id. */
207
+ export function stepLabel(step: PipelineStep, options: readonly StepOption[]): string {
208
+ const hit = options.find((o) => o.config_ref === step.config_ref && o.type === step.type);
209
+ return step.name?.trim() || hit?.name || (step.type === "ai" ? "AI step" : "Provider waterfall step");
210
+ }
211
+
212
+ /** `n / total` as a whole percent, or null when there is nothing to divide by (shown as a dash, never 0%). */
213
+ export function percentOf(part: number | null | undefined, total: number | null | undefined): number | null {
214
+ if (part == null || !total) return null;
215
+ return Math.min(100, Math.round((part / total) * 100));
216
+ }
217
+
218
+ /** The reason a run reads as "skipped" in plain words. */
219
+ export function skipReasonLabel(reason: string): string {
220
+ if (reason === "run_condition_not_met") return "skipped by the step condition";
221
+ return reason.replace(/_/g, " ");
222
+ }
223
+
224
+ /** True when the pipeline named like this template already exists on the list (re-adding it replaces that pipeline's steps). */
225
+ export function templateExists(template: Pick<PipelineTemplate, "name">, pipelines: readonly Pick<ListPipeline, "name">[]): boolean {
226
+ return pipelines.some((p) => p.name === template.name);
227
+ }
228
+
229
+ /** The most useful single sentence about why a pipeline cannot be run right now, or null when it can. */
230
+ export function runBlockedReason(p: Pick<ListPipeline, "steps" | "last_run">): string | null {
231
+ if (enabledSteps(p).length === 0) return "Every step is turned off, so there is nothing to run.";
232
+ if (runIsActive(p.last_run?.status)) return "A run is already in progress.";
233
+ return null;
234
+ }
@@ -130,3 +130,5 @@ graph8 captures forms from your site; there is **no create-form operation in the
130
130
  New on `FormsScreen`: a **Groups** tab (`FormGroups`: create, rename, re-type, add and remove forms, delete), saved views (`FormSavedViews`), "Load more" past the first 100 forms, and on an open form: a **Routing** tab (`FormBackfill`: preview, then a confirmed delivery; `FormRouteHistory`: restore the previous route or stop routing, both destructive confirms that send the version they read), an **Activity** tab (`FormActivity`: audit trail and pages), and **Export CSV** on Submissions (`SubmissionsExport`, formula-safe, says when the file is not the whole set).
131
131
 
132
132
  Not covered (and why): `saveFormRoute` (the mapping editor's save already writes the route); `captureFormPreview` and `getFormPreview` return untyped image data; unidentified-form grouping (`previewUnidentifiedGrouping`, `setUnidentifiedGrouping`, `syncUnidentifiedGroup`) needs a list picker (owned by the pickers PR). The mapping editor's list select keeps its existing cap until that PR lands.
133
+
134
+ Slot prop `renderSubmissionActions(submission, form)`: extra buttons on an opened submission, next to its delivery state. The submission carries no contact id, so the stack pages look the submitter up by the email in `form_data` (`ContactLookupLink`) and offer "Add to contacts" when nobody has it.
@@ -66,6 +66,8 @@ import { useBrand } from "@/g8/hooks/use-brand";
66
66
  export interface FormsScreenProps {
67
67
  renderRowActions?: (form: FormListItem) => ReactNode;
68
68
  renderDetailActions?: (form: FormListItem) => ReactNode;
69
+ /** Extra buttons on an opened submission, next to its delivery state (open or add the person who submitted it). */
70
+ renderSubmissionActions?: (submission: FormSubmission, form: FormListItem) => ReactNode;
69
71
  extraDetailTabs?: { id: string; label: string; render: (form: FormListItem) => ReactNode }[];
70
72
  toolbarActions?: ReactNode;
71
73
  className?: string;
@@ -164,7 +166,7 @@ function SubmissionNotFound({ form, submissionId }: { form: FormRef; submissionI
164
166
  }
165
167
 
166
168
  /** One submission in full: every submitted field, where it came from, its delivery, and the trace. */
167
- function SubmissionDetail({ form, submission, onChanged }: { form: FormRef; submission: FormSubmission; onChanged: () => void }) {
169
+ function SubmissionDetail({ form, submission, onChanged, actions }: { form: FormRef; submission: FormSubmission; onChanged: () => void; actions?: ReactNode }) {
168
170
  const [retrying, setRetrying] = useState(false);
169
171
  const trace = useG8Query(
170
172
  (signal) => g8Get<{ capture_state?: string; receipts?: unknown[] }>(`/forms/${encodeURIComponent(form.form_key)}/submissions/${encodeURIComponent(submission.id)}/trace`, { source_id: form.source_id }, { signal }).then((r) => r.data),
@@ -180,6 +182,7 @@ function SubmissionDetail({ form, submission, onChanged }: { form: FormRef; subm
180
182
  <StatusPill tone={failed ? "negative" : submissionState(submission) === "delivered" ? "positive" : "neutral"}>{submissionState(submission)}</StatusPill>
181
183
  {submission.is_test ? <StatusPill tone="info">Test</StatusPill> : null}
182
184
  {failed && submission.delivery?.receipt_id ? <Button size="sm" variant="outline" onClick={() => { retry.reset(); setRetrying(true); }}>Retry delivery</Button> : null}
185
+ {actions}
183
186
  </div>
184
187
  <SectionCard title="Submitted fields">
185
188
  {fields.length === 0 ? <p className="text-muted-foreground text-sm">No fields were captured. Identifying fields may be redacted for your key.</p> : (
@@ -218,7 +221,7 @@ function SubmissionDetail({ form, submission, onChanged }: { form: FormRef; subm
218
221
  );
219
222
  }
220
223
 
221
- export function FormsScreen({ renderRowActions, renderDetailActions, extraDetailTabs, toolbarActions, className }: FormsScreenProps) {
224
+ export function FormsScreen({ renderRowActions, renderDetailActions, renderSubmissionActions, extraDetailTabs, toolbarActions, className }: FormsScreenProps) {
222
225
  const brand = useBrand();
223
226
  const [tabParam, setTabParam] = useUrlState("tab", "forms");
224
227
  const tab: Tab = tabParam === "install" || tabParam === "audit" || tabParam === "groups" ? tabParam : "forms";
@@ -369,7 +372,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
369
372
  >
370
373
  {selected && submissionId ? (
371
374
  linkedSubmission ? (
372
- <SubmissionDetail key={submissionId} form={selected} submission={linkedSubmission} onChanged={() => setSubsRefresh((n) => n + 1)} />
375
+ <SubmissionDetail key={submissionId} form={selected} submission={linkedSubmission} onChanged={() => setSubsRefresh((n) => n + 1)} actions={renderSubmissionActions?.(linkedSubmission, selected)} />
373
376
  ) : lookup.error ? (
374
377
  <ApiError error={lookup.error} title="Could not load this submission" onRetry={() => void lookup.refetch()} />
375
378
  ) : lookup.loading || !lookup.data ? (
@@ -24,6 +24,21 @@ Defaults are unchanged from this release: both are on.
24
24
 
25
25
  Every detail opens as a full page with `&view=full` (the Open full page button).
26
26
 
27
+ **Management (new, on by default; `showManagement={false}` hides the three buttons).** The toolbar gains:
28
+
29
+ - **New message** (`ComposeMessage`): write a NEW email or text. The sender is never defaulted: you choose a mailbox
30
+ (from `GET /inbox/mailboxes/paginated`; ones you may not send as are listed disabled with the reason) or an SMS number,
31
+ and the confirm dialog names it ("from hamza@acme.com to maya@northwind.io, 2 recipients"). Sending is **external**.
32
+ - **Search all** (`InboxSearch`): server-side search of every email thread (across all your mailboxes) or LinkedIn
33
+ conversation, beyond the page the list loaded; pick a result to open it. SMS has no search route in the API. A LinkedIn result opens by the row's conversation id (`conversation_id`, else `id`); that this equals the unified inbox's LinkedIn thread id has not been checked against a live HeyReach payload, so if a result opens as "Conversation not found", the id differs.
34
+ - **Manage**: the tag library (`TagManager`: create, rename or recolour, delete; built-in tags follow the API's policy) and
35
+ `BlockedSenders` (block per mailbox, unblock). The tag list it loads also feeds the Tag filter and the tag picker.
36
+ - In an open conversation, **Manage conversation** (`ThreadActions`): mark read (and unread on LinkedIn), remove a tag,
37
+ unassign a teammate, block the sender.
38
+
39
+ A conversation this key cannot see (an org key sees shared threads only) or that was removed answers a uniform 404; the
40
+ conversation view says "Conversation not found" instead of showing a generic failure.
41
+
27
42
  ## The flow, in order
28
43
 
29
44
  1. **List** `GET /inbox?channel=&status=&page=&page_size=` (`InboxList`). Each row has `id` and `channel`.
@@ -52,7 +67,8 @@ IDs passed between steps: `thread.id` + `thread.channel` (list -> view -> draft/
52
67
 
53
68
  - Connect a mailbox in the graph8 app (Google / Microsoft OAuth exists only there; see the `mailboxes` block).
54
69
  Until then the inbox has no email threads. `where: "app"`.
55
- - Give the proxy's API key `inbox:read`, `inbox:write` and `inbox:run` (and `campaigns:read` for the SMS sender picker).
70
+ - Give the proxy's API key `inbox:read`, `inbox:write` and `inbox:run` (and `campaigns:read` for the SMS sender picker,
71
+ `inbox:delete` to delete a tag, remove a tag or teammate from a conversation or unblock a sender, `workflows:read` to list tags).
56
72
  - Credits for AI drafts (402 when none; there is no balance read, so the confirm dialog shows no estimate).
57
73
 
58
74
  ## Defaults (the agent does not need to ask)
@@ -62,9 +78,13 @@ IDs passed between steps: `thread.id` + `thread.channel` (list -> view -> draft/
62
78
  - A reply leaves from the thread's own mailbox, number or LinkedIn account and goes to the thread's contact.
63
79
  - No assignee filter: the API cannot filter by an arbitrary assignee email (an org key owns no threads), so
64
80
  filter `thread.assignees` client-side if you need it.
65
- - Ask the human only about **tags**: there is no list-tags operation. Pass `availableTags` from your own
66
- state (collect them from `thread.tags` of loaded rows, or keep a list); tags created in the panel are
67
- offered for the session.
81
+ - Tags: `TagManager` lists them (`GET /workflows/inbox-tags`, scope `workflows:read`), so no input is needed. Pass
82
+ `availableTags` only to seed the pickers before the Manage sheet is opened; tags on loaded threads are always offered.
83
+ - A NEW message is never sent without an explicit sender; the API refuses a compose without `from_mailbox` or
84
+ `from_number`, and this block does not guess one.
85
+ - Not offered, because the API has no such operation: snooze, archive, mark an email or SMS unread. Not offered by
86
+ choice (see the PR): composing a NEW LinkedIn message (`/inbox/linkedin/compose` only continues an existing
87
+ conversation, which `ReplyComposer` already does), starring chats, internal comments, saving compose drafts.
68
88
 
69
89
  ## Gotchas
70
90
 
@@ -88,6 +108,22 @@ IDs passed between steps: `thread.id` + `thread.channel` (list -> view -> draft/
88
108
  | `aiInbox.createInboxTag` | POST /inbox/tags | write | TagAndAssign |
89
109
  | `inbox.tagReply` | POST /inbox/{reply_id}/tag | external | TagAndAssign |
90
110
  | `inbox.assignReply` | POST /inbox/{reply_id}/assign | external | TagAndAssign |
111
+ | `aiInbox.listInboxMailboxesPaginated` | GET /inbox/mailboxes/paginated | read | ComposeMessage, InboxSearch, BlockedSenders |
112
+ | `aiInbox.composeInboxEmail` | POST /inbox/emails/compose | external | ComposeMessage |
113
+ | `aiInbox.composeInboxSms` | POST /inbox/sms/compose | external | ComposeMessage |
114
+ | `inbox.searchInboxEmails` | POST /inbox/emails/search | read | InboxSearch |
115
+ | `aiInbox.searchInboxLinkedinConversations` | POST /inbox/linkedin/conversations/search | read | InboxSearch |
116
+ | `workflows.listInboxTags` | GET /workflows/inbox-tags | read | TagManager |
117
+ | `aiInbox.updateInboxTag` | PUT /inbox/tags/{tag_id} | write | TagManager (changed fields only) |
118
+ | `aiInbox.deleteInboxTag` | DELETE /inbox/tags/{tag_id} | destructive | TagManager |
119
+ | `aiInbox.markInboxEmailRead` | POST /inbox/emails/{email_id}/mark-read | write | ThreadActions |
120
+ | `aiInbox.markInboxSmsRead` | POST /inbox/sms/{thread_id}/mark-read | write | ThreadActions |
121
+ | `aiInbox.markInboxLinkedinConversationRead` / `Unread` | POST /inbox/linkedin/accounts/{account_id}/conversations/{id}/read, /unread | write | ThreadActions |
122
+ | `aiInbox.removeChannelTag` | DELETE /inbox/channels/{channel_id}/tags | destructive | ThreadActions |
123
+ | `aiInbox.unassignChannel` | DELETE /inbox/channels/{channel_id}/assign | destructive | ThreadActions |
124
+ | `aiInbox.blockInboxSender` | POST /inbox/blocked-senders | write | ThreadActions, BlockedSenders |
125
+ | `aiInbox.listInboxBlockedSenders` | GET /inbox/blocked-senders | read | BlockedSenders |
126
+ | `aiInbox.unblockInboxSender` | DELETE /inbox/blocked-senders/{blocked_sender_id} | destructive | BlockedSenders |
91
127
 
92
128
  ## Recipes (server side)
93
129