@graph8/sdk 0.361.0 → 0.370.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 (388) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +172 -1
  3. package/blocks/_foundation/block.json +234 -13
  4. package/blocks/_foundation/components/api-error.tsx +71 -25
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  7. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  8. package/blocks/_foundation/components/empty-state.tsx +39 -7
  9. package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
  10. package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
  11. package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
  12. package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
  13. package/blocks/_foundation/components/screen/index.ts +22 -0
  14. package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
  17. package/blocks/_foundation/components/screen/section-card.tsx +47 -0
  18. package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
  19. package/blocks/_foundation/components/screen/split-view.tsx +125 -0
  20. package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
  21. package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
  22. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  23. package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
  24. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  25. package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
  26. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  27. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  28. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  29. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  30. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  31. package/blocks/_foundation/hooks/use-url-state.ts +74 -0
  32. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  33. package/blocks/_foundation/lib/g8-client.ts +1 -1
  34. package/blocks/_foundation/lib/plural.ts +23 -0
  35. package/blocks/_foundation/server/connect-route.ts +15 -0
  36. package/blocks/_foundation/server/connect.ts +127 -0
  37. package/blocks/_foundation/server/local-key.ts +70 -0
  38. package/blocks/_foundation/server/proxy.ts +103 -3
  39. package/blocks/_foundation/server/route.ts +16 -4
  40. package/blocks/agents/README.md +14 -0
  41. package/blocks/agents/block.json +33 -1
  42. package/blocks/agents/components/agent-builder.tsx +13 -8
  43. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  44. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  45. package/blocks/agents/components/agent-runs.tsx +16 -7
  46. package/blocks/agents/components/agents-screen.tsx +184 -0
  47. package/blocks/analytics/README.md +13 -0
  48. package/blocks/analytics/block.json +45 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  50. package/blocks/analytics/components/leaderboard.tsx +14 -7
  51. package/blocks/analytics/components/report-builder.tsx +23 -9
  52. package/blocks/analytics/components/report-detail.tsx +84 -0
  53. package/blocks/analytics/components/trend-chart.tsx +62 -0
  54. package/blocks/appointments/README.md +21 -1
  55. package/blocks/appointments/block.json +43 -2
  56. package/blocks/appointments/components/availability-editor.tsx +17 -5
  57. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  58. package/blocks/appointments/components/event-type-related.tsx +77 -0
  59. package/blocks/appointments/components/meeting-list.tsx +14 -6
  60. package/blocks/appointments/components/meetings-screen.tsx +157 -0
  61. package/blocks/appointments/components/scheduling-screen.tsx +122 -0
  62. package/blocks/appointments/hooks/use-event-types.ts +17 -0
  63. package/blocks/block.schema.json +18 -0
  64. package/blocks/block.types.ts +26 -0
  65. package/blocks/campaigns/README.md +41 -11
  66. package/blocks/campaigns/block.json +105 -42
  67. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  68. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  69. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  70. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  71. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  72. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  73. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  74. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  75. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  76. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  77. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  78. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  79. package/blocks/coach/README.md +10 -0
  80. package/blocks/coach/block.json +16 -1
  81. package/blocks/coach/components/coach-screen.tsx +222 -0
  82. package/blocks/copilot/README.md +13 -0
  83. package/blocks/copilot/block.json +14 -1
  84. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  85. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  87. package/blocks/crm/README.md +28 -0
  88. package/blocks/crm/block.json +58 -2
  89. package/blocks/crm/components/companies-screen.tsx +345 -0
  90. package/blocks/crm/components/company-detail.tsx +8 -6
  91. package/blocks/crm/components/company-table.tsx +11 -4
  92. package/blocks/crm/components/contact-detail.tsx +6 -3
  93. package/blocks/crm/components/contact-table.tsx +10 -3
  94. package/blocks/crm/components/contacts-screen.tsx +359 -0
  95. package/blocks/crm/components/dedupe-review.tsx +7 -5
  96. package/blocks/crm/components/import-wizard.tsx +4 -2
  97. package/blocks/crm/components/notes-panel.tsx +4 -4
  98. package/blocks/crm/components/record-form.tsx +5 -3
  99. package/blocks/crm/components/task-list.tsx +4 -4
  100. package/blocks/crm/components/tasks-screen.tsx +379 -0
  101. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  102. package/blocks/crm/hooks/use-page-query.ts +7 -1
  103. package/blocks/crm/lib/crm-types.ts +12 -2
  104. package/blocks/crm/lib/screen-parts.tsx +105 -0
  105. package/blocks/deals/README.md +34 -3
  106. package/blocks/deals/block.json +45 -4
  107. package/blocks/deals/components/deal-detail.tsx +27 -13
  108. package/blocks/deals/components/deal-related.tsx +87 -0
  109. package/blocks/deals/components/deals-screen.tsx +298 -0
  110. package/blocks/deals/components/pipeline-board.tsx +4 -2
  111. package/blocks/deals/components/stage-editor.tsx +4 -4
  112. package/blocks/deals/hooks/use-default-owner.ts +60 -0
  113. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  114. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  115. package/blocks/deals/lib/deals-types.ts +14 -0
  116. package/blocks/dialer/README.md +34 -2
  117. package/blocks/dialer/block.json +67 -1
  118. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  119. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  120. package/blocks/dialer/components/call-log.tsx +7 -3
  121. package/blocks/dialer/components/call-queue.tsx +1 -1
  122. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  123. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  124. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  125. package/blocks/dialer/components/session-start-form.tsx +4 -2
  126. package/blocks/dialer/components/softphone.tsx +3 -1
  127. package/blocks/enrichment/README.md +14 -2
  128. package/blocks/enrichment/block.json +32 -4
  129. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  130. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  131. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  132. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  133. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  134. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  135. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  136. package/blocks/forms/README.md +12 -0
  137. package/blocks/forms/block.json +39 -17
  138. package/blocks/forms/components/form-builder.tsx +3 -1
  139. package/blocks/forms/components/form-embed.tsx +3 -1
  140. package/blocks/forms/components/forms-screen.tsx +353 -0
  141. package/blocks/forms/components/submissions-table.tsx +16 -6
  142. package/blocks/inbox/README.md +27 -3
  143. package/blocks/inbox/block.json +14 -2
  144. package/blocks/inbox/components/inbox-list.tsx +7 -3
  145. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  146. package/blocks/inbox/components/reply-composer.tsx +65 -9
  147. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  148. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  149. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  150. package/blocks/inbox/lib/inbox-types.ts +47 -1
  151. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  152. package/blocks/integrations/README.md +9 -0
  153. package/blocks/integrations/block.json +46 -1
  154. package/blocks/integrations/components/crm-detail.tsx +100 -0
  155. package/blocks/integrations/components/integration-list.tsx +9 -4
  156. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  157. package/blocks/integrations/components/sync-status.tsx +5 -3
  158. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  159. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  160. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  161. package/blocks/intent/README.md +14 -0
  162. package/blocks/intent/block.json +27 -1
  163. package/blocks/intent/components/intent-feed.tsx +5 -2
  164. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  165. package/blocks/intent/components/intent-screen.tsx +265 -0
  166. package/blocks/intent/components/keyword-detail.tsx +124 -0
  167. package/blocks/lifecycle/README.md +16 -0
  168. package/blocks/lifecycle/block.json +34 -7
  169. package/blocks/lifecycle/components/account-related.tsx +66 -0
  170. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  171. package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
  172. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  173. package/blocks/lists/README.md +13 -0
  174. package/blocks/lists/block.json +39 -1
  175. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  176. package/blocks/lists/components/list-picker.tsx +5 -1
  177. package/blocks/lists/components/list-table.tsx +11 -5
  178. package/blocks/lists/components/lists-screen.tsx +344 -0
  179. package/blocks/lists/components/suppression-manager.tsx +6 -2
  180. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  181. package/blocks/mailboxes/README.md +17 -0
  182. package/blocks/mailboxes/block.json +65 -17
  183. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  184. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  185. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  186. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  187. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  188. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  189. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  190. package/blocks/newsletter/README.md +13 -0
  191. package/blocks/newsletter/block.json +47 -23
  192. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  193. package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
  194. package/blocks/newsletter/components/subscriber-table.tsx +12 -6
  195. package/blocks/nurture/README.md +12 -0
  196. package/blocks/nurture/block.json +35 -13
  197. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  198. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  199. package/blocks/nurture/components/nurture-form.tsx +23 -3
  200. package/blocks/nurture/components/nurture-screen.tsx +283 -0
  201. package/blocks/nurture/lib/nurture-types.ts +1 -1
  202. package/blocks/objects/README.md +17 -0
  203. package/blocks/objects/block.json +29 -16
  204. package/blocks/objects/components/object-not-found.tsx +3 -1
  205. package/blocks/objects/components/object-record-detail.tsx +2 -1
  206. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  207. package/blocks/objects/components/object-table.tsx +13 -3
  208. package/blocks/objects/components/objects-screen.tsx +147 -0
  209. package/blocks/objects/lib/object-types.ts +1 -1
  210. package/blocks/pages/README.md +12 -0
  211. package/blocks/pages/block.json +35 -22
  212. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  213. package/blocks/pages/components/content-library.tsx +46 -11
  214. package/blocks/pages/components/page-list.tsx +6 -3
  215. package/blocks/pages/components/pages-screen.tsx +154 -0
  216. package/blocks/quotes/README.md +17 -0
  217. package/blocks/quotes/block.json +38 -9
  218. package/blocks/quotes/components/product-picker.tsx +3 -1
  219. package/blocks/quotes/components/quote-list.tsx +7 -3
  220. package/blocks/quotes/components/quote-related.tsx +123 -0
  221. package/blocks/quotes/components/quotes-screen.tsx +169 -0
  222. package/blocks/radar/README.md +10 -0
  223. package/blocks/radar/block.json +41 -1
  224. package/blocks/radar/components/competitor-detail.tsx +117 -0
  225. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  226. package/blocks/radar/components/radar-report.tsx +4 -2
  227. package/blocks/radar/components/radar-screen.tsx +214 -0
  228. package/blocks/radar/components/report-detail.tsx +104 -0
  229. package/blocks/search/README.md +10 -0
  230. package/blocks/search/block.json +16 -1
  231. package/blocks/search/components/filter-builder.tsx +3 -3
  232. package/blocks/search/components/prospect-results.tsx +5 -3
  233. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  234. package/blocks/search/components/saved-searches.tsx +4 -1
  235. package/blocks/search/hooks/use-filter-options.ts +20 -6
  236. package/blocks/search/lib/search-types.ts +2 -0
  237. package/blocks/sequencer/README.md +49 -9
  238. package/blocks/sequencer/block.json +99 -19
  239. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  240. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  241. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  242. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  243. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  244. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  245. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  246. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  247. package/blocks/sequencer/components/step-editor.tsx +3 -1
  248. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  249. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  250. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  251. package/blocks/social-ads/README.md +11 -0
  252. package/blocks/social-ads/block.json +101 -2
  253. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  254. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  255. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  256. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  257. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  258. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  259. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  260. package/blocks/social-ads/components/post-composer.tsx +3 -1
  261. package/blocks/social-ads/components/post-detail.tsx +160 -0
  262. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  263. package/blocks/social-ads/components/social-screen.tsx +160 -0
  264. package/blocks/visitors/README.md +10 -0
  265. package/blocks/visitors/block.json +25 -1
  266. package/blocks/visitors/components/snippet-install.tsx +3 -1
  267. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  268. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  269. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  270. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  271. package/blocks/webchat/README.md +10 -0
  272. package/blocks/webchat/block.json +21 -7
  273. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  274. package/blocks/webchat/components/conversation-list.tsx +7 -3
  275. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  276. package/blocks/workbench/README.md +11 -0
  277. package/blocks/workbench/block.json +48 -1
  278. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  279. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  280. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  281. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  282. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  283. package/blocks/workflows/README.md +13 -0
  284. package/blocks/workflows/block.json +28 -9
  285. package/blocks/workflows/components/run-history.tsx +31 -9
  286. package/blocks/workflows/components/trigger-config.tsx +3 -1
  287. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  288. package/blocks/workflows/components/workflow-list.tsx +13 -6
  289. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  290. package/dist/cli.js +2785 -1637
  291. package/dist/errors.d.mts +34 -0
  292. package/dist/errors.d.ts +34 -0
  293. package/dist/errors.js +46 -0
  294. package/dist/errors.js.map +1 -0
  295. package/dist/errors.mjs +21 -0
  296. package/dist/errors.mjs.map +1 -0
  297. package/dist/index.browser.js +11 -5
  298. package/dist/index.browser.js.map +1 -1
  299. package/dist/index.browser.mjs +11 -5
  300. package/dist/index.browser.mjs.map +1 -1
  301. package/dist/index.d.mts +164 -25
  302. package/dist/index.d.ts +164 -25
  303. package/dist/index.js +11 -5
  304. package/dist/index.js.map +1 -1
  305. package/dist/index.mjs +11 -5
  306. package/dist/index.mjs.map +1 -1
  307. package/dist/react.browser.js +10 -4
  308. package/dist/react.browser.js.map +1 -1
  309. package/dist/react.browser.mjs +10 -4
  310. package/dist/react.browser.mjs.map +1 -1
  311. package/dist/react.d.mts +142 -5
  312. package/dist/react.d.ts +142 -5
  313. package/dist/react.js +10 -4
  314. package/dist/react.js.map +1 -1
  315. package/dist/react.mjs +10 -4
  316. package/dist/react.mjs.map +1 -1
  317. package/dist/types/blocks/add.d.ts +11 -4
  318. package/dist/types/blocks/brand.d.ts +113 -0
  319. package/dist/types/blocks/compose.d.ts +53 -0
  320. package/dist/types/blocks/create.d.ts +25 -8
  321. package/dist/types/blocks/doctor.d.ts +6 -1
  322. package/dist/types/blocks/flows.d.ts +20 -0
  323. package/dist/types/blocks/navfile.d.ts +35 -0
  324. package/dist/types/blocks/paths.d.ts +4 -0
  325. package/dist/types/blocks/prefixes.d.ts +37 -0
  326. package/dist/types/blocks/registry.d.ts +32 -0
  327. package/dist/types/blocks/screens.d.ts +53 -0
  328. package/dist/types/cli.d.ts +2 -0
  329. package/dist/types/errors.d.ts +33 -0
  330. package/dist/types/http.d.ts +2 -18
  331. package/dist/types/version.d.ts +2 -2
  332. package/docs/agents.md +15 -4
  333. package/docs/api/README.md +2 -2
  334. package/docs/api/by-path.md +1 -0
  335. package/docs/api/desk.d.ts +98 -2
  336. package/docs/api/desk.md +123 -1
  337. package/docs/api/inbox.d.ts +10 -2
  338. package/docs/api/inbox.md +10 -2
  339. package/docs/api/seo.d.ts +19 -1
  340. package/docs/api/seo.md +24 -1
  341. package/package.json +16 -2
  342. package/stacks/6sense.json +41 -16
  343. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  344. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  345. package/stacks/apollo.json +118 -36
  346. package/stacks/calendly.json +60 -0
  347. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  348. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  349. package/stacks/clay.json +59 -26
  350. package/stacks/gong.json +81 -35
  351. package/stacks/hootsuite.json +99 -0
  352. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  353. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  354. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  355. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  356. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  357. package/stacks/hubspot.json +167 -66
  358. package/stacks/intercom.json +48 -20
  359. package/stacks/mailchimp.json +52 -33
  360. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  361. package/stacks/outreach.json +88 -25
  362. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  363. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  364. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  365. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  366. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  367. package/stacks/pipedrive.json +115 -0
  368. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  369. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  370. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  371. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  372. package/stacks/salesforce.json +81 -31
  373. package/stacks/stack.schema.json +6 -1
  374. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  375. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  376. package/stacks/zoominfo.json +102 -0
  377. package/styles/styles.css +1 -1
  378. package/templates/next-app/README.md.tmpl +55 -8
  379. package/templates/next-app/app/globals.css.tmpl +105 -0
  380. package/templates/next-app/app/layout.tsx.tmpl +18 -4
  381. package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
  382. package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
  383. package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
  384. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
  385. package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
  386. package/templates/next-app/dot-gitignore.tmpl +2 -0
  387. package/templates/next-app/lib/nav.ts.tmpl +1 -1
  388. package/templates/next-app/package.json.tmpl +1 -1
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The graph8 API key for local preview, kept on the SERVER: the Connect graph8 screen posts a pasted key to
3
+ * `app/api/g8-connect/route.ts`, which validates it and stores it here, in `.g8/local-key` at the project root
4
+ * (file mode 0600, folder 0700, with its own `.gitignore` so it is never committed). The browser never holds it.
5
+ *
6
+ * resolveApiKey(): G8_API_KEY from the environment wins; otherwise, outside production, the stored key.
7
+ *
8
+ * Node only (reads the file system); the Next route that imports it runs on the Node runtime (the default).
9
+ * In a production build the file is never read: set G8_API_KEY in the deployment's environment.
10
+ */
11
+
12
+ import { promises as fs } from "node:fs";
13
+ import path from "node:path";
14
+
15
+ /** Where the key lives, relative to the project root (the directory `next dev` runs in). */
16
+ export const LOCAL_KEY_FILE = path.join(".g8", "local-key");
17
+
18
+ export type KeySource = "env" | "file";
19
+
20
+ const isProduction = (): boolean => process.env.NODE_ENV === "production";
21
+
22
+ function keyPath(root: string = process.cwd()): string {
23
+ return path.join(root, LOCAL_KEY_FILE);
24
+ }
25
+
26
+ /** The stored key, or undefined (none, unreadable, or a production build). */
27
+ export async function readLocalKey(root?: string): Promise<string | undefined> {
28
+ if (isProduction()) return undefined;
29
+ try {
30
+ const key = (await fs.readFile(keyPath(root), "utf8")).trim();
31
+ return key || undefined;
32
+ } catch {
33
+ return undefined;
34
+ }
35
+ }
36
+
37
+ /** The key the proxy uses, and where it came from. The environment always wins. */
38
+ export async function currentKey(root?: string): Promise<{ key: string; source: KeySource } | null> {
39
+ const env = process.env.G8_API_KEY?.trim();
40
+ if (env) return { key: env, source: "env" };
41
+ const stored = await readLocalKey(root);
42
+ return stored ? { key: stored, source: "file" } : null;
43
+ }
44
+
45
+ /** For `createG8Proxy({ apiKey: resolveApiKey })`. */
46
+ export async function resolveApiKey(): Promise<string | undefined> {
47
+ return (await currentKey())?.key;
48
+ }
49
+
50
+ /** Store a key (validated by the caller): 0700 folder, 0600 file, and a `.gitignore` that ignores the folder. */
51
+ export async function writeLocalKey(key: string, root?: string): Promise<void> {
52
+ const file = keyPath(root);
53
+ const dir = path.dirname(file);
54
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
55
+ await fs.writeFile(path.join(dir, ".gitignore"), "# Written by the graph8 Connect screen: never commit this folder.\n*\n", { mode: 0o644 });
56
+ const tmp = `${file}.${process.pid}.tmp`;
57
+ await fs.writeFile(tmp, `${key}\n`, { mode: 0o600 });
58
+ await fs.chmod(tmp, 0o600);
59
+ await fs.rename(tmp, file);
60
+ }
61
+
62
+ /** Remove the stored key. Missing is fine. */
63
+ export async function removeLocalKey(root?: string): Promise<void> {
64
+ await fs.rm(keyPath(root), { force: true });
65
+ }
66
+
67
+ /** `...wxyz`: enough for a person to recognise which key is in use, never enough to use it. */
68
+ export function keyHint(key: string): string {
69
+ return key.length > 8 ? `...${key.slice(-4)}` : "...";
70
+ }
@@ -14,6 +14,11 @@
14
14
  * and `allowedPrefixes` (only the areas the UI needs). The opt-outs
15
15
  * `dangerouslyAllowUnauthenticated` and `dangerouslyAllowAllPaths` exist for
16
16
  * local prototypes only.
17
+ *
18
+ * LOCAL PREVIEW (`localPreview: true`, set in apps made by `graph8-sdk create`):
19
+ * until `authorize` is set, the route answers ONLY `next dev` requests made
20
+ * from this machine (see `isLocalPreviewRequest`). A production build refuses
21
+ * every request (500) until `authorize` is set; the setting then does nothing.
17
22
  */
18
23
 
19
24
  export interface G8ProxyConfig {
@@ -42,6 +47,19 @@ export interface G8ProxyConfig {
42
47
  * `targetOrgId` is set.
43
48
  */
44
49
  allowTargetOrgHeader?: boolean;
50
+ /**
51
+ * Local preview, for `npm run dev` before your app has sign-in. Only applies while `authorize` is NOT set:
52
+ * the proxy then answers a request only when NODE_ENV is not "production" AND the request comes from this
53
+ * machine (Host localhost, 127.0.0.1 or [::1], same-origin, no forwarding proxy in between). Anything else
54
+ * gets 403; a production build gets 500 until you set `authorize`. Default false.
55
+ */
56
+ localPreview?: boolean;
57
+ /**
58
+ * Where the server's graph8 API key comes from. Default: `process.env.G8_API_KEY`, read on every request.
59
+ * Created apps pass `resolveApiKey` (g8/server/local-key.ts): the environment first, then, in local
60
+ * preview only, the key pasted in the Connect graph8 screen.
61
+ */
62
+ apiKey?: () => string | undefined | Promise<string | undefined>;
45
63
  /** Prototype opt-out: run without `authorize`. Anyone who can reach the route can use your key. */
46
64
  dangerouslyAllowUnauthenticated?: boolean;
47
65
  /** Prototype opt-out: run without `allowedPrefixes` (every /api/v1 path is reachable). */
@@ -158,9 +176,81 @@ async function readBodyCapped(request: Request, max: number): Promise<ArrayBuffe
158
176
  return out;
159
177
  }
160
178
 
179
+ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
180
+ const LOOPBACK_ADDRESSES = /^(127\.\d{1,3}\.\d{1,3}\.\d{1,3}|::1|::ffff:127\.\d{1,3}\.\d{1,3}\.\d{1,3}|localhost)$/i;
181
+
182
+ /** `localhost:3000` -> `localhost`, `[::1]:3000` -> `[::1]`. */
183
+ function hostnameOf(host: string): string {
184
+ const h = host.trim().toLowerCase();
185
+ if (h.startsWith("[")) return h.slice(0, h.indexOf("]") + 1);
186
+ return h.replace(/:\d+$/, "");
187
+ }
188
+
189
+ function isLoopbackOrigin(value: string): boolean {
190
+ try {
191
+ return LOOPBACK_HOSTS.has(new URL(value).hostname.toLowerCase());
192
+ } catch {
193
+ return false;
194
+ }
195
+ }
196
+
197
+ /**
198
+ * True when a request was made on this machine, by this app's own page (or typed in the address bar):
199
+ * - Host (and X-Forwarded-Host, if any) is localhost, 127.0.0.1 or [::1], which also blocks DNS rebinding;
200
+ * - every X-Forwarded-For hop is a loopback address (tunnels such as ngrok add the real client);
201
+ * - the browser's Sec-Fetch-Site is same-origin or none, and Origin (if any) is a loopback origin,
202
+ * so another website open in the same browser cannot drive the proxy (CSRF).
203
+ * Headers can be forged by a client that reaches the dev server directly, so created apps also bind
204
+ * `next dev` to 127.0.0.1 (package.json). Never rely on this check in production: set `authorize`.
205
+ */
206
+ export function isLocalPreviewRequest(request: Request): boolean {
207
+ const host = request.headers.get("host") ?? new URL(request.url).host;
208
+ if (!LOOPBACK_HOSTS.has(hostnameOf(host))) return false;
209
+ const fwdHost = request.headers.get("x-forwarded-host");
210
+ if (fwdHost && !fwdHost.split(",").every((h) => LOOPBACK_HOSTS.has(hostnameOf(h)))) return false;
211
+ const fwdFor = request.headers.get("x-forwarded-for");
212
+ if (fwdFor && !fwdFor.split(",").every((a) => LOOPBACK_ADDRESSES.test(a.trim()))) return false;
213
+ const site = request.headers.get("sec-fetch-site");
214
+ if (site && site !== "same-origin" && site !== "none") return false;
215
+ const origin = request.headers.get("origin");
216
+ if (origin && !isLoopbackOrigin(origin)) return false;
217
+ return true;
218
+ }
219
+
220
+ const isProduction = (): boolean => process.env.NODE_ENV === "production";
221
+
222
+ let previewLogged = false;
223
+
224
+ /**
225
+ * The local-preview verdict for a request when `authorize` is unset: null to continue, or the refusal.
226
+ * Only called when `localPreview` is on and `authorize` is not set.
227
+ */
228
+ function localPreviewGate(request: Request): Response | null {
229
+ if (isProduction()) {
230
+ return json(500, {
231
+ error: "g8_proxy_misconfigured",
232
+ message:
233
+ "Local preview only runs under `next dev`. This is a production build, so set `authorize` (check your app's session) in app/api/g8/[...path]/route.ts before going live. `npx @graph8/sdk doctor --production` lists what is left.",
234
+ });
235
+ }
236
+ if (!isLocalPreviewRequest(request)) {
237
+ return json(403, {
238
+ error: "local_preview_only",
239
+ message: "Local preview only answers requests from this machine (http://localhost). Set `authorize` in app/api/g8/[...path]/route.ts to serve anyone else.",
240
+ });
241
+ }
242
+ if (!previewLogged) {
243
+ previewLogged = true;
244
+ console.warn(
245
+ "[graph8] Local preview: /api/g8 answers requests from this machine only, with no sign-in check. Set `authorize` in app/api/g8/[...path]/route.ts before you deploy.",
246
+ );
247
+ }
248
+ return null;
249
+ }
250
+
161
251
  /** The first reason this config is unsafe to serve, or null. Checked on every request. */
162
252
  function configProblem(config: G8ProxyConfig): string | null {
163
- if (!config.authorize && !config.dangerouslyAllowUnauthenticated) {
253
+ if (!config.authorize && !config.dangerouslyAllowUnauthenticated && !config.localPreview) {
164
254
  return "Set `authorize` (check your app's session) in app/api/g8/[...path]/route.ts. The proxy refuses to run without it.";
165
255
  }
166
256
  if (!config.allowedPrefixes && !config.dangerouslyAllowAllPaths) {
@@ -177,11 +267,21 @@ export function createG8Proxy(config: G8ProxyConfig = {}) {
177
267
  async function handle(request: Request, context: RouteContext): Promise<Response> {
178
268
  const problem = configProblem(config);
179
269
  if (problem) return json(500, { error: "g8_proxy_misconfigured", message: problem });
270
+ const preview = Boolean(config.localPreview && !config.authorize && !config.dangerouslyAllowUnauthenticated);
271
+ if (preview) {
272
+ const refused = localPreviewGate(request);
273
+ if (refused) return refused;
274
+ }
180
275
  const denied = await config.authorize?.(request);
181
276
  if (denied) return denied;
182
277
 
183
- const apiKey = process.env.G8_API_KEY;
184
- if (!apiKey) return json(500, { error: "g8_proxy_misconfigured", message: "G8_API_KEY is not set on the server." });
278
+ const apiKey = config.apiKey ? await config.apiKey() : process.env.G8_API_KEY;
279
+ if (!apiKey) {
280
+ const message = preview
281
+ ? "No graph8 API key yet: open this app in the browser and paste one in the Connect graph8 screen, or set G8_API_KEY in .env.local."
282
+ : "G8_API_KEY is not set on the server.";
283
+ return json(500, { error: "g8_proxy_misconfigured", message });
284
+ }
185
285
 
186
286
  const params = await (context.params as Promise<{ path?: string[] }> | { path?: string[] });
187
287
  const path = normalizePath(params?.path);
@@ -4,18 +4,30 @@
4
4
  *
5
5
  * Next.js only allows HTTP-method names and a few config exports from a route
6
6
  * file, so the proxy configuration is a plain constant here, not an export.
7
- * Edit it. The proxy REFUSES every request (500) until BOTH `authorize` and
8
- * `allowedPrefixes` are set, because this route carries the full-power API key.
7
+ * Edit it. Before you deploy, set `authorize`: a production build refuses every
8
+ * request (500) until it is set, because this route carries the full-power API key.
9
+ * `npx @graph8/sdk doctor --production` checks this file for you.
9
10
  */
10
11
 
11
12
  import { createG8Proxy } from "@/g8/server/proxy";
13
+ import { resolveApiKey } from "@/g8/server/local-key";
12
14
 
13
- // 1. Only the API areas this app's UI needs, relative to /api/v1. REQUIRED.
15
+ // 1. The API areas this app's UI may call, relative to /api/v1. REQUIRED.
16
+ // The lines between the graph8:allowed-prefixes markers are GENERATED from the operations the installed blocks
17
+ // declare, and rewritten by `npx @graph8/sdk add <block>` and `npx @graph8/sdk doctor --fix-prefixes`. To own the
18
+ // list yourself, delete both marker lines; the CLI then never touches it.
19
+ // graph8:allowed-prefixes:start
14
20
  const allowedPrefixes: string[] | undefined = undefined; // e.g. ["contacts", "companies", "lists"]
21
+ // graph8:allowed-prefixes:end
15
22
 
16
23
  export const { GET, POST, PUT, PATCH, DELETE } = createG8Proxy({
17
24
  allowedPrefixes,
18
- // 2. Check YOUR session and return a Response (401/403) to refuse. REQUIRED:
25
+ // 2. Local preview: until `authorize` is set, answer only `npm run dev` requests from this machine
26
+ // (http://localhost). Ignored once `authorize` is set; a production build never runs without `authorize`.
27
+ localPreview: true,
28
+ // The key: G8_API_KEY from the environment, else (local preview only) the one pasted in the Connect graph8 screen.
29
+ apiKey: resolveApiKey,
30
+ // 3. Check YOUR session and return a Response (401/403) to refuse. REQUIRED before you deploy:
19
31
  // authorize: async (request) => { if (!(await getSession(request))) return new Response("Unauthorized", { status: 401 }); },
20
32
  //
21
33
  // Agency keys only: pick the client org from the session, never from the browser:
@@ -4,6 +4,20 @@ An agent is a persona (how it talks), a voice, knowledge (documents and company
4
4
  (single-purpose actions). This block gives you the screens to build one, prove it works before anyone talks
5
5
  to it, and follow the runs agents do for you.
6
6
 
7
+ ## Screens
8
+
9
+ `AgentsScreen` (`components/agents-screen.tsx`) is the finished agents workspace; mount it as the page.
10
+
11
+ - **Agents tab**: stat strip (agents, active, inactive), search, status filter, the list, **New agent** (form sheet; the
12
+ agent starts inactive), activate / deactivate. Opening an agent (deep link `?id=`) shows a detail panel with
13
+ **Configure** (persona, voice, knowledge, skills, delete), **Test** (free dry runs, saved cases, the activation gate,
14
+ run a skill live) and **Runs** (this agent's runs).
15
+ - **Runs tab**: every run as a list; opening one (`?run=`) shows `AgentRunDetail` (status, step, credits, times, its approvals to approve or reject, cancel). Start a run here. `&view=full` opens any record as a full page.
16
+ - Uploading documents, running a skill, starting or cancelling a run, approving and deleting an agent are confirmed
17
+ with `ConfirmSpendDialog` inside the parts. Needs `voice:read|write|delete`, `knowledge:read`, `workflows:*`.
18
+ - Slots: `renderRowActions`, `renderDetailActions`, `extraDetailTabs`, `toolbarActions`. `AgentBuilder` gained
19
+ `hideList`; `AgentRuns` gained `selectedId`, `onSelectRun`, `hideDetail`, `refreshKey`; new `AgentRunDetail`.
20
+
7
21
  Install: `npx @graph8/sdk add agents` (installs `foundation` first).
8
22
 
9
23
  ## What you get
@@ -47,6 +47,37 @@
47
47
  "upload_agent_documents_voice_agents__agent_id__documents_upload_post"
48
48
  ],
49
49
  "components": [
50
+ {
51
+ "name": "AgentsScreen",
52
+ "file": "components/agents-screen.tsx",
53
+ "purpose": "Finished agents workspace: stats, search, list, create sheet, and a detail panel with configure, test and runs, plus a workspace-wide Runs tab.",
54
+ "operations": [
55
+ "create_voice_agent_voice_agents_post",
56
+ "get_voice_agent_voice_agents__agent_id__get",
57
+ "list_voice_agents_voice_agents_get",
58
+ "update_voice_agent_voice_agents__agent_id__put"
59
+ ],
60
+ "tiers": [
61
+ "read",
62
+ "write"
63
+ ]
64
+ },
65
+ {
66
+ "name": "AgentRunDetail",
67
+ "file": "components/agent-run-detail.tsx",
68
+ "purpose": "One agent run in full: status, step, credits, times, its pending approvals and cancel.",
69
+ "operations": [
70
+ "get_agent_run_agent_runs__run_id__get",
71
+ "list_agent_approvals_agent_approvals_get",
72
+ "decide_agent_approval_agent_approvals__approval_id__decide_post",
73
+ "cancel_agent_run_agent_runs__run_id__cancel_post"
74
+ ],
75
+ "tiers": [
76
+ "read",
77
+ "write",
78
+ "destructive"
79
+ ]
80
+ },
50
81
  {
51
82
  "name": "AgentBuilder",
52
83
  "file": "components/agent-builder.tsx",
@@ -218,6 +249,7 @@
218
249
  }
219
250
  ],
220
251
  "peerDependencies": {
221
- "react": ">=18"
252
+ "react": ">=18",
253
+ "lucide-react": ">=0.400.0"
222
254
  }
223
255
  }
@@ -37,6 +37,8 @@ export interface AgentBuilderProps {
37
37
  onSelect?: (agentId: string | null) => void;
38
38
  /** Called after a create, update or delete so the host can refresh its own lists. */
39
39
  onChanged?: () => void;
40
+ /** Hide the built-in agent list and "New agent" button when the host (for example AgentsScreen) provides its own. */
41
+ hideList?: boolean;
40
42
  className?: string;
41
43
  }
42
44
 
@@ -60,7 +62,7 @@ function Dial({ label, value, onChange, low, high }: { label: string; value: num
60
62
  );
61
63
  }
62
64
 
63
- export function AgentBuilder({ agentId: controlled, onSelect, onChanged, className }: AgentBuilderProps) {
65
+ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, hideList, className }: AgentBuilderProps) {
64
66
  const [inner, setInner] = useState<string | null>(null);
65
67
  const selected = controlled !== undefined ? controlled : inner;
66
68
  const [creating, setCreating] = useState(false);
@@ -70,7 +72,7 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, classNa
70
72
  const [deleting, setDeleting] = useState(false);
71
73
 
72
74
  const choose = (id: string | null) => { setInner(id); setCreating(false); onSelect?.(id); };
73
- const agents = useG8Query((signal) => g8Get("/voice/agents", { search: search.trim() || undefined, page_size: 50 }, { signal }), [search]);
75
+ const agents = useG8Query((signal) => g8Get("/voice/agents", { search: search.trim() || undefined, page_size: 50 }, { signal }), [search], { enabled: !hideList });
74
76
  const detail = useG8Query((signal) => g8Get(`/voice/agents/${selected}`, undefined, { signal }), [selected], { enabled: Boolean(selected) && !creating });
75
77
  const voices = useG8Query((signal) => g8Get("/voice/voice-library/presets", { limit: 100 }, { signal }), []);
76
78
 
@@ -87,7 +89,7 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, classNa
87
89
  {
88
90
  onSuccess: (res) => {
89
91
  const id = bodyOf<{ agent?: { agent_id?: string } }>(res).agent?.agent_id;
90
- void agents.refetch();
92
+ if (!hideList) void agents.refetch();
91
93
  // Keep the new id on the draft at once: a second click (or a retry after a later step fails) must update, never create a twin.
92
94
  if (!draft?.agent_id && id) { setDraft((d) => d && { ...d, agent_id: id }); choose(id); }
93
95
  else void detail.refetch();
@@ -96,17 +98,19 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, classNa
96
98
  },
97
99
  );
98
100
  const remove = useG8Mutation((id: string) => g8Delete(`/voice/agents/${id}`), {
99
- onSuccess: () => { setDeleting(false); setDraft(null); choose(null); void agents.refetch(); onChanged?.(); },
101
+ onSuccess: () => { setDeleting(false); setDraft(null); choose(null); if (!hideList) void agents.refetch(); onChanged?.(); },
100
102
  });
101
103
 
102
104
  const rows = listOf<AgentSummary>(agents.data, "agents");
103
105
  const presets = listOf<{ voice_id?: string; id?: string; name?: string; label?: string }>(voices.data, "items");
104
106
  const setPersona = (patch: Partial<AgentDetail["persona"]>) => setDraft((d) => d && { ...d, persona: { ...d.persona, ...patch } });
107
+ // The hook keeps the previous agent's data while the next one loads: until it arrives the old form must not be shown (or saved) as the new agent.
108
+ const stale = Boolean(draft?.agent_id && selected && draft.agent_id !== selected && !creating);
105
109
  const valid = Boolean(draft?.persona.agent_name.trim() && draft?.persona.persona.trim() && draft?.persona.description.trim() && draft?.role.trim());
106
110
 
107
111
  return (
108
- <div className={`grid gap-4 md:grid-cols-[16rem_1fr] ${className ?? ""}`}>
109
- <aside className="flex flex-col gap-2">
112
+ <div className={`grid gap-4 ${hideList ? "" : "md:grid-cols-[16rem_1fr]"} ${className ?? ""}`}>
113
+ {hideList ? null : <aside className="flex flex-col gap-2">
110
114
  <div className="flex gap-2">
111
115
  <Input aria-label="Search agents" placeholder="Search agents" value={search} onChange={(e) => setSearch(e.target.value)} />
112
116
  <Button size="sm" onClick={() => { setCreating(true); setDraft(blankDraft()); setSection("persona"); onSelect?.(null); }}>New agent</Button>
@@ -127,12 +131,13 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, classNa
127
131
  {!agents.loading && !agents.error && rows.length === 0 ? (
128
132
  <EmptyState title="No agents yet" description="An agent has a persona, a voice, knowledge and skills." />
129
133
  ) : null}
130
- </aside>
134
+ </aside>}
131
135
 
132
136
  <section className="flex min-w-0 flex-col gap-3">
133
137
  {!draft && !creating ? <EmptyState title="Pick an agent" description="Choose one on the left or create a new agent." /> : null}
134
138
  {detail.error ? <ApiError error={detail.error} title="Could not load the agent" onRetry={() => void detail.refetch()} /> : null}
135
- {draft ? (
139
+ {stale && !detail.error ? <Skeleton className="h-64 w-full" aria-label="Loading agent" /> : null}
140
+ {draft && !stale ? (
136
141
  <>
137
142
  <div role="tablist" className="flex gap-1 border-b">
138
143
  {SECTIONS.map((s) => (
@@ -25,6 +25,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
25
25
  import { useG8Query } from "@/g8/hooks/use-g8-query";
26
26
  import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
27
27
  import { listOf } from "../lib/agent-types";
28
+ import { plural } from "@/g8/lib/plural";
28
29
 
29
30
  export interface AgentKnowledgePanelProps {
30
31
  agentId: string;
@@ -132,7 +133,7 @@ export function AgentKnowledgePanel({ agentId, useCompanyKnowledge, onToggleComp
132
133
 
133
134
  {upload.error ? <ApiError error={upload.error} title="Could not upload" /> : null}
134
135
  {remove.error ? <ApiError error={remove.error} title="Could not remove the document" /> : null}
135
- <ConfirmSpendDialog open={pending != null} tier="billable" title={`Index ${pending?.length ?? 0} document${pending?.length === 1 ? "" : "s"}`}
136
+ <ConfirmSpendDialog open={pending != null} tier="billable" title={`Index ${plural(pending?.length ?? 0, "document")}`}
136
137
  description="The text is extracted and embedded with a paid model so the agent can search it."
137
138
  estimatedCredits={(pending?.length ?? 0) * creditsPerFile} confirmLabel="Upload" confirming={upload.loading}
138
139
  onConfirm={() => pending && void upload.mutate(pending)} onCancel={() => setPending(null)} />
@@ -0,0 +1,102 @@
1
+ "use client";
2
+
3
+ /**
4
+ * AgentRunDetail: one agent run in full. Title, status, agent, current step, next action, credits against the budget,
5
+ * created and completed times, the approvals this run is waiting on (approve or reject), and Cancel while it is live.
6
+ * The run is polled until it is terminal (the API has no streams).
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. getAgentRun GET /agent/runs/{run_id} (read) polled
10
+ * 2. listAgentApprovals GET /agent/approvals (read) this run's pending ones
11
+ * 3. decideAgentApproval POST /agent/approvals/{approval_id}/decide { decision } (write; approving lets the run act: ConfirmSpendDialog, external)
12
+ * 4. cancelAgentRun POST /agent/runs/{run_id}/cancel (destructive, ConfirmSpendDialog)
13
+ * Approvals that gate a Work coordination action cannot be decided with an API key: the error is shown as is.
14
+ */
15
+ import { useEffect, useState } from "react";
16
+ import { Button, Skeleton } from "@graph8/sdk/ui";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
19
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
20
+ import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
21
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
22
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
23
+ import { useJobPoll } from "@/g8/hooks/use-job-poll";
24
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
25
+ import { bodyOf, listOf } from "../lib/agent-types";
26
+ import type { AgentRun } from "../lib/agent-types";
27
+ import { isTerminalRun } from "./agent-runs";
28
+
29
+ export interface AgentRunDetailProps {
30
+ runId: string;
31
+ /** Poll interval in ms. Default 3000. */
32
+ pollMs?: number;
33
+ /** Called after a cancel or a decision so the host can refresh its run list. */
34
+ onChanged?: () => void;
35
+ className?: string;
36
+ }
37
+
38
+ interface Approval { id: string; summary: string; risk_level: string; work_item_id: string; decision?: string | null; is_expired?: boolean }
39
+
40
+ const when = (v?: string | null): string => (v ? new Date(v).toLocaleString() : "");
41
+
42
+ export function AgentRunDetail({ runId, pollMs = 3000, onChanged, className }: AgentRunDetailProps) {
43
+ const [cancelling, setCancelling] = useState(false);
44
+ const [approving, setApproving] = useState<Approval | null>(null);
45
+ const [deciding, setDeciding] = useState<string | null>(null);
46
+ const poll = useJobPoll<AgentRun>((signal) => g8Get<AgentRun>(`/agent/runs/${encodeURIComponent(runId)}`, undefined, { signal }).then((r) => bodyOf<AgentRun>(r)), {
47
+ isTerminal: isTerminalRun, intervalMs: pollMs, maxIntervalMs: pollMs * 4, onDone: () => onChanged?.(),
48
+ });
49
+ const approvals = useG8Query((signal) => g8Get("/agent/approvals", undefined, { signal }), [runId]);
50
+ useEffect(() => { poll.start(); setCancelling(false); setApproving(null); }, [runId]); // eslint-disable-line react-hooks/exhaustive-deps
51
+ useEffect(() => { if (poll.attempts > 0) void approvals.refetch(); }, [poll.attempts]); // eslint-disable-line react-hooks/exhaustive-deps
52
+
53
+ const cancel = useG8Mutation(() => g8Post(`/agent/runs/${encodeURIComponent(runId)}/cancel`, {}), { onSuccess: () => { setCancelling(false); poll.start(); onChanged?.(); }, onError: () => setCancelling(false) });
54
+ const decide = useG8Mutation(async (v: { id: string; decision: "approve" | "reject" }) => {
55
+ setDeciding(v.id);
56
+ try { return await g8Post(`/agent/approvals/${encodeURIComponent(v.id)}/decide`, { decision: v.decision }); } finally { setDeciding(null); }
57
+ }, { onSuccess: () => { setApproving(null); void approvals.refetch(); onChanged?.(); }, onError: () => setApproving(null) });
58
+
59
+ const run = poll.status?.work_item_id === runId ? poll.status : undefined;
60
+ const pending = listOf<Approval>(approvals.data).filter((a) => a.work_item_id === runId && !a.decision && !a.is_expired);
61
+ if (!run) return poll.error ? <ApiError error={poll.error} title="Could not load the run" onRetry={() => poll.start()} /> : <Skeleton className="h-40 w-full" aria-label="Loading run" />;
62
+
63
+ return (
64
+ <div className={`space-y-4 ${className ?? ""}`} data-testid="run-detail">
65
+ <div className="flex flex-wrap items-center gap-2">
66
+ <StatusPill tone={toneForStatus(run.status)} dot>{run.status}</StatusPill>
67
+ {poll.polling ? <span className="text-xs text-muted-foreground">updating</span> : null}
68
+ {!isTerminalRun(run) ? <Button className="ml-auto" size="sm" variant="outline" onClick={() => setCancelling(true)}>Cancel run</Button> : null}
69
+ </div>
70
+ <KeyValueList columns={2} items={[
71
+ { label: "Title", value: run.title, wide: true },
72
+ { label: "Agent", value: run.agent_id, mono: true },
73
+ { label: "Run id", value: run.work_item_id, mono: true },
74
+ { label: "Current step", value: run.current_step },
75
+ { label: "Next action", value: run.next_action },
76
+ { label: "Credits", value: `${run.credits_spent ?? 0}${run.credit_budget ? ` of ${run.credit_budget}` : ""}` },
77
+ { label: "Started", value: when(run.created_at) },
78
+ { label: "Finished", value: when(run.completed_at) },
79
+ ]} />
80
+ {pending.length > 0 ? (
81
+ <section className="space-y-2 rounded-md border p-3" aria-label="Approvals" data-testid="run-approvals">
82
+ <h4 className="text-sm font-semibold">Waiting for your decision ({pending.length})</h4>
83
+ {pending.map((a) => (
84
+ <div key={a.id} className="flex flex-wrap items-center justify-between gap-2 text-sm">
85
+ <span>{a.summary}<span className="ml-2 text-xs text-muted-foreground">risk: {a.risk_level}</span></span>
86
+ <span className="flex gap-2">
87
+ <Button size="sm" variant="outline" loading={deciding === a.id} disabled={deciding != null && deciding !== a.id} onClick={() => void decide.mutate({ id: a.id, decision: "reject" })}>Reject</Button>
88
+ <Button size="sm" disabled={deciding != null} onClick={() => setApproving(a)}>Approve</Button>
89
+ </span>
90
+ </div>
91
+ ))}
92
+ </section>
93
+ ) : null}
94
+ {decide.error ? <ApiError error={decide.error} title="Could not record the decision" /> : null}
95
+ {cancel.error ? <ApiError error={cancel.error} title="Could not cancel the run" /> : null}
96
+ <ConfirmSpendDialog open={cancelling} tier="destructive" title={`Cancel "${run.title}"`} description="Work stops where it is. Steps already taken are not undone."
97
+ confirmLabel="Cancel run" confirming={cancel.loading} onConfirm={() => void cancel.mutate(undefined)} onCancel={() => setCancelling(false)} />
98
+ <ConfirmSpendDialog open={approving != null} tier="external" title="Approve this action" description={approving?.summary}
99
+ confirmLabel="Approve" confirming={decide.loading} onConfirm={() => approving && void decide.mutate({ id: approving.id, decision: "approve" })} onCancel={() => setApproving(null)} />
100
+ </div>
101
+ );
102
+ }
@@ -36,6 +36,13 @@ export interface AgentRunsProps {
36
36
  pageSize?: number;
37
37
  /** Poll interval for the selected run, in ms. Default 3000. */
38
38
  pollMs?: number;
39
+ /** Controlled selection (a run id). With it, the host owns which run is open (for example from the URL). */
40
+ selectedId?: string | null;
41
+ onSelectRun?: (runId: string | null) => void;
42
+ /** Hide the built-in detail box when the host shows the run elsewhere (see AgentRunDetail). */
43
+ hideDetail?: boolean;
44
+ /** Change this number to reload the list (for example after AgentRunDetail cancelled a run). */
45
+ refreshKey?: number;
39
46
  className?: string;
40
47
  }
41
48
 
@@ -46,28 +53,30 @@ const TERMINAL = new Set(["completed", "succeeded", "failed", "cancelled", "canc
46
53
  export const isTerminalRun = (r: Pick<AgentRun, "status"> | undefined): boolean => TERMINAL.has(String(r?.status ?? "").toLowerCase());
47
54
  const STATUSES = ["running", "queued", "completed", "failed", "cancelled"];
48
55
 
49
- export function AgentRuns({ agentId, pageSize = 20, pollMs = 3000, className }: AgentRunsProps) {
56
+ export function AgentRuns({ agentId, pageSize = 20, pollMs = 3000, selectedId, onSelectRun, hideDetail, refreshKey, className }: AgentRunsProps) {
50
57
  const [status, setStatus] = useState("");
51
58
  const [limit, setLimit] = useState(pageSize);
52
- const [selected, setSelected] = useState<string | null>(null);
59
+ const [innerSel, setInnerSel] = useState<string | null>(null);
60
+ const selected = selectedId !== undefined ? selectedId : innerSel;
61
+ const setSelected = (id: string | null) => { setInnerSel(id); onSelectRun?.(id); };
53
62
  const [form, setForm] = useState<{ agent_id: string; title: string; goal: string } | null>(null);
54
63
  const [confirmStart, setConfirmStart] = useState(false);
55
64
  const [cancelling, setCancelling] = useState<AgentRun | null>(null);
56
65
  const [approving, setApproving] = useState<Approval | null>(null);
57
66
  const [deciding, setDeciding] = useState<string | null>(null);
58
67
 
59
- const runs = useG8Query((signal) => g8Get("/agent/runs", { statuses: status || undefined, agent_id: agentId, limit }, { signal }), [status, agentId, limit]);
68
+ const runs = useG8Query((signal) => g8Get("/agent/runs", { statuses: status || undefined, agent_id: agentId, limit }, { signal }), [status, agentId, limit, refreshKey]);
60
69
  const approvals = useG8Query((signal) => g8Get("/agent/approvals", undefined, { signal }), []);
61
70
  const specialists = useG8Query((signal) => g8Get("/agent/specialists", undefined, { signal }), [], { enabled: form != null });
62
71
  const detail = useJobPoll<AgentRun>((signal) => g8Get<AgentRun>(`/agent/runs/${selected}`, undefined, { signal }).then((r) => bodyOf<AgentRun>(r)), {
63
- enabled: selected != null, isTerminal: isTerminalRun, intervalMs: pollMs, maxIntervalMs: pollMs * 4, onDone: () => void runs.refetch(),
72
+ enabled: selected != null && !hideDetail, isTerminal: isTerminalRun, intervalMs: pollMs, maxIntervalMs: pollMs * 4, onDone: () => void runs.refetch(),
64
73
  });
65
74
 
66
75
  // Switching runs keeps `enabled` true, so restart the poll explicitly.
67
- useEffect(() => { if (selected) detail.start(); }, [selected]); // eslint-disable-line react-hooks/exhaustive-deps
76
+ useEffect(() => { if (selected && !hideDetail) detail.start(); }, [selected]); // eslint-disable-line react-hooks/exhaustive-deps
68
77
 
69
78
  // A run that stops on an approval keeps polling: refresh the approvals on every poll that lands.
70
- useEffect(() => { if (selected && detail.attempts > 0) void approvals.refetch(); }, [detail.attempts]); // eslint-disable-line react-hooks/exhaustive-deps
79
+ useEffect(() => { if (selected && !hideDetail && detail.attempts > 0) void approvals.refetch(); }, [detail.attempts]); // eslint-disable-line react-hooks/exhaustive-deps
71
80
 
72
81
  const start = useG8Mutation(() => g8Post<AgentRun>("/agent/runs", { agent_id: form?.agent_id ?? agentId, title: form?.title.trim(), goal: form?.goal.trim() }), {
73
82
  onSuccess: (res) => { setConfirmStart(false); setForm(null); setSelected(bodyOf<AgentRun>(res).work_item_id ?? null); void runs.refetch(); },
@@ -147,7 +156,7 @@ export function AgentRuns({ agentId, pageSize = 20, pollMs = 3000, className }:
147
156
  {!runs.loading && !runs.error && rows.length === 0 ? <EmptyState title="No runs yet" description="A run is one goal worked on by an agent, step by step, with approvals where it matters." /> : null}
148
157
  {rows.length < total ? <Button variant="outline" size="sm" onClick={() => setLimit(limit + pageSize)}>Load more</Button> : null}
149
158
 
150
- {selected ? (
159
+ {selected && !hideDetail ? (
151
160
  <div className="rounded-md border p-3" data-testid="run-detail">
152
161
  {current ? (
153
162
  <>