@graph8/sdk 0.370.0 → 0.373.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (466) hide show
  1. package/README.md +19 -0
  2. package/blocks/_foundation/README.md +14 -0
  3. package/blocks/_foundation/block.json +44 -0
  4. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  5. package/blocks/_foundation/components/empty-state.tsx +5 -2
  6. package/blocks/_foundation/components/list-select.tsx +112 -0
  7. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  8. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  9. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  10. package/blocks/_foundation/components/screen/index.ts +1 -0
  11. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  12. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  13. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  14. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  15. package/blocks/_foundation/lib/paging.ts +57 -0
  16. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  17. package/blocks/agents/README.md +81 -4
  18. package/blocks/agents/block.json +311 -2
  19. package/blocks/agents/components/agent-activity.tsx +112 -0
  20. package/blocks/agents/components/agent-approvals.tsx +135 -0
  21. package/blocks/agents/components/agent-builder.tsx +17 -3
  22. package/blocks/agents/components/agent-conversations.tsx +143 -0
  23. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  24. package/blocks/agents/components/agent-operators.tsx +164 -0
  25. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  26. package/blocks/agents/components/agent-runs.tsx +3 -3
  27. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  28. package/blocks/agents/components/agent-test-history.tsx +121 -0
  29. package/blocks/agents/components/agents-screen.tsx +53 -11
  30. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  31. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  32. package/blocks/agents/components/from-template-button.tsx +65 -0
  33. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  34. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  35. package/blocks/agents/components/knowledge-form.tsx +69 -0
  36. package/blocks/agents/components/name-dialog.tsx +32 -0
  37. package/blocks/agents/components/receptionist-button.tsx +46 -0
  38. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  39. package/blocks/agents/components/skill-editor.tsx +147 -0
  40. package/blocks/agents/components/skill-library.tsx +111 -0
  41. package/blocks/agents/lib/agent-types.ts +7 -1
  42. package/blocks/agents/lib/facts.tsx +13 -0
  43. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  44. package/blocks/agents/lib/load-more.tsx +141 -0
  45. package/blocks/agents/lib/skill-utils.ts +91 -0
  46. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  47. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  48. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  49. package/blocks/analytics/components/report-builder.tsx +15 -2
  50. package/blocks/appointments/README.md +6 -0
  51. package/blocks/appointments/block.json +27 -0
  52. package/blocks/appointments/components/booking-page.tsx +2 -2
  53. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  54. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  55. package/blocks/appointments/components/meeting-list.tsx +29 -13
  56. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  57. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  58. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  59. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  60. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  61. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  62. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  63. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  64. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  65. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  66. package/blocks/coach/components/call-scorecard.tsx +7 -7
  67. package/blocks/company-intelligence/README.md +132 -0
  68. package/blocks/company-intelligence/block.json +318 -0
  69. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  70. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  71. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  72. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  73. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  74. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  75. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  76. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  77. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  78. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  79. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  80. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  81. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  82. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  83. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  84. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  85. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  86. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  87. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  88. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  89. package/blocks/crm/README.md +6 -0
  90. package/blocks/crm/block.json +53 -4
  91. package/blocks/crm/components/companies-screen.tsx +23 -9
  92. package/blocks/crm/components/company-detail.tsx +2 -2
  93. package/blocks/crm/components/contact-detail.tsx +4 -4
  94. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  95. package/blocks/crm/components/contacts-screen.tsx +31 -13
  96. package/blocks/crm/components/dedupe-review.tsx +6 -1
  97. package/blocks/crm/components/import-wizard.tsx +3 -7
  98. package/blocks/crm/components/list-select.tsx +20 -0
  99. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  100. package/blocks/crm/components/record-form.tsx +1 -1
  101. package/blocks/crm/components/tasks-screen.tsx +35 -15
  102. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  103. package/blocks/crm/hooks/use-page-query.ts +3 -1
  104. package/blocks/crm/lib/crm-types.ts +14 -0
  105. package/blocks/crm/lib/read-lists.ts +16 -0
  106. package/blocks/deals/README.md +24 -6
  107. package/blocks/deals/block.json +151 -2
  108. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  109. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  110. package/blocks/deals/components/deal-detail.tsx +2 -2
  111. package/blocks/deals/components/deal-filters.tsx +65 -0
  112. package/blocks/deals/components/deal-line-items.tsx +120 -0
  113. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  114. package/blocks/deals/components/deal-related.tsx +30 -9
  115. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  116. package/blocks/deals/components/deals-screen.tsx +57 -22
  117. package/blocks/deals/components/forecast-view.tsx +4 -4
  118. package/blocks/deals/components/pipeline-board.tsx +8 -6
  119. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  120. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  121. package/blocks/deals/components/stage-editor.tsx +1 -1
  122. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  123. package/blocks/deals/lib/deal-extras.ts +156 -0
  124. package/blocks/dialer/README.md +14 -5
  125. package/blocks/dialer/block.json +17 -2
  126. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  127. package/blocks/dialer/components/call-log-screen.tsx +4 -1
  128. package/blocks/dialer/components/call-log.tsx +16 -4
  129. package/blocks/dialer/components/call-queue.tsx +3 -3
  130. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  131. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  132. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  133. package/blocks/dialer/components/session-start-form.tsx +58 -11
  134. package/blocks/dialer/components/softphone.tsx +1 -1
  135. package/blocks/enrichment/README.md +16 -6
  136. package/blocks/enrichment/block.json +19 -7
  137. package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
  138. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  139. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  140. package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
  141. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  142. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  143. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  144. package/blocks/forms/README.md +8 -0
  145. package/blocks/forms/block.json +107 -0
  146. package/blocks/forms/components/form-activity.tsx +71 -0
  147. package/blocks/forms/components/form-backfill.tsx +108 -0
  148. package/blocks/forms/components/form-builder.tsx +1 -1
  149. package/blocks/forms/components/form-embed.tsx +1 -1
  150. package/blocks/forms/components/form-groups.tsx +151 -0
  151. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  152. package/blocks/forms/components/form-route-history.tsx +72 -0
  153. package/blocks/forms/components/form-saved-views.tsx +81 -0
  154. package/blocks/forms/components/forms-screen.tsx +51 -17
  155. package/blocks/forms/components/more-rows.tsx +30 -0
  156. package/blocks/forms/components/submissions-export.tsx +67 -0
  157. package/blocks/forms/components/submissions-table.tsx +15 -6
  158. package/blocks/forms/lib/forms-extra.ts +67 -0
  159. package/blocks/inbox/README.md +5 -0
  160. package/blocks/inbox/block.json +15 -0
  161. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  162. package/blocks/inbox/components/thread-badge.tsx +2 -2
  163. package/blocks/inbox/components/thread-view.tsx +1 -1
  164. package/blocks/integrations/README.md +54 -1
  165. package/blocks/integrations/block.json +189 -15
  166. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  167. package/blocks/integrations/components/connection-health.tsx +59 -0
  168. package/blocks/integrations/components/crm-detail.tsx +20 -1
  169. package/blocks/integrations/components/crm-records.tsx +103 -0
  170. package/blocks/integrations/components/field-mappings.tsx +61 -0
  171. package/blocks/integrations/components/integration-list.tsx +4 -4
  172. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  173. package/blocks/integrations/components/migration-panel.tsx +133 -0
  174. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  175. package/blocks/integrations/components/sync-status.tsx +6 -6
  176. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  177. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  178. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  179. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  180. package/blocks/integrations/lib/crm-utils.ts +39 -0
  181. package/blocks/integrations/lib/facts.tsx +13 -0
  182. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  183. package/blocks/integrations/lib/load-more.tsx +141 -0
  184. package/blocks/intent/components/account-intent-score.tsx +5 -5
  185. package/blocks/intent/components/intent-feed.tsx +8 -8
  186. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  187. package/blocks/intent/lib/intent-utils.ts +4 -4
  188. package/blocks/lifecycle/components/account-related.tsx +8 -1
  189. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  190. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  191. package/blocks/lists/README.md +7 -1
  192. package/blocks/lists/block.json +31 -3
  193. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  194. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  195. package/blocks/lists/components/list-picker.tsx +13 -3
  196. package/blocks/lists/components/list-table.tsx +1 -1
  197. package/blocks/lists/components/lists-screen.tsx +4 -2
  198. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  199. package/blocks/lists/components/suppression-manager.tsx +27 -6
  200. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  201. package/blocks/mailboxes/block.json +4 -0
  202. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  203. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  204. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  205. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  206. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  207. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  208. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  209. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  210. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  211. package/blocks/newsletter/README.md +12 -0
  212. package/blocks/newsletter/block.json +117 -0
  213. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  214. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  215. package/blocks/newsletter/components/more-rows.tsx +30 -0
  216. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  217. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  218. package/blocks/newsletter/components/newsletter-screen.tsx +36 -13
  219. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  220. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  221. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  222. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  223. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  224. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  225. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  226. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  227. package/blocks/nurture/components/nurture-form.tsx +5 -8
  228. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  229. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  230. package/blocks/objects/README.md +1 -1
  231. package/blocks/objects/block.json +13 -5
  232. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  233. package/blocks/objects/components/object-record-detail.tsx +8 -1
  234. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  235. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  236. package/blocks/pages/README.md +14 -0
  237. package/blocks/pages/block.json +113 -1
  238. package/blocks/pages/components/more-rows.tsx +30 -0
  239. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  240. package/blocks/pages/components/page-analytics.tsx +176 -0
  241. package/blocks/pages/components/page-clone.tsx +99 -0
  242. package/blocks/pages/components/page-domains.tsx +167 -0
  243. package/blocks/pages/components/page-editor.tsx +1 -1
  244. package/blocks/pages/components/page-generation-status.tsx +48 -0
  245. package/blocks/pages/components/page-tracking.tsx +137 -0
  246. package/blocks/pages/components/pages-screen.tsx +57 -16
  247. package/blocks/pages/components/seo-report.tsx +6 -1
  248. package/blocks/pages/lib/studio.ts +7 -0
  249. package/blocks/phone-numbers/README.md +74 -0
  250. package/blocks/phone-numbers/block.json +262 -0
  251. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  252. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  253. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  254. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  255. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  256. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  257. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  258. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  259. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  260. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  261. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  262. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  263. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  264. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  265. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  266. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  267. package/blocks/pickers/README.md +23 -0
  268. package/blocks/pickers/block.json +103 -0
  269. package/blocks/pickers/components/company-picker.tsx +90 -0
  270. package/blocks/pickers/components/contact-picker.tsx +131 -0
  271. package/blocks/pickers/components/member-picker.tsx +99 -0
  272. package/blocks/pickers/components/search-picker.tsx +98 -0
  273. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  274. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  275. package/blocks/pickers/lib/use-debounced.ts +13 -0
  276. package/blocks/quotes/README.md +13 -0
  277. package/blocks/quotes/block.json +48 -1
  278. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  279. package/blocks/quotes/components/quote-builder.tsx +1 -1
  280. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  281. package/blocks/quotes/components/quote-list.tsx +5 -3
  282. package/blocks/quotes/components/quote-related.tsx +3 -2
  283. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  284. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  285. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  286. package/blocks/radar/components/radar-overview.tsx +4 -4
  287. package/blocks/radar/components/radar-screen.tsx +2 -2
  288. package/blocks/sequencer/README.md +6 -0
  289. package/blocks/sequencer/block.json +28 -1
  290. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  291. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  292. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  293. package/blocks/sequencer/components/sequence-builder.tsx +19 -20
  294. package/blocks/sequencer/components/sequence-detail.tsx +4 -2
  295. package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
  296. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  297. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  298. package/blocks/sequencer/components/sequences-screen.tsx +3 -0
  299. package/blocks/sequencer/components/step-editor.tsx +11 -11
  300. package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
  301. package/blocks/social-ads/README.md +8 -0
  302. package/blocks/social-ads/block.json +137 -4
  303. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  304. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  305. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  306. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  307. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  308. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  309. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  310. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  311. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  312. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  313. package/blocks/social-ads/components/post-composer.tsx +4 -2
  314. package/blocks/social-ads/components/post-detail.tsx +3 -0
  315. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  316. package/blocks/social-ads/components/social-performance.tsx +80 -0
  317. package/blocks/social-ads/components/social-screen.tsx +33 -6
  318. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  319. package/blocks/surveys/README.md +91 -0
  320. package/blocks/surveys/block.json +299 -0
  321. package/blocks/surveys/components/account-nps.tsx +69 -0
  322. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  323. package/blocks/surveys/components/org-summary.tsx +82 -0
  324. package/blocks/surveys/components/sending-domains.tsx +113 -0
  325. package/blocks/surveys/components/survey-form.tsx +126 -0
  326. package/blocks/surveys/components/survey-responses.tsx +145 -0
  327. package/blocks/surveys/components/survey-results.tsx +72 -0
  328. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  329. package/blocks/surveys/components/survey-send.tsx +162 -0
  330. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  331. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  332. package/blocks/surveys/lib/surveys-types.ts +228 -0
  333. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  334. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  335. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  336. package/blocks/visitors/components/snippet-install.tsx +18 -9
  337. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  338. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  339. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  340. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  341. package/blocks/webchat/components/conversation-list.tsx +1 -1
  342. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  343. package/blocks/workbench/components/run-progress.tsx +1 -1
  344. package/blocks/workbench/components/workbench-grid.tsx +1 -1
  345. package/blocks/workbench/components/workbench-screen.tsx +15 -5
  346. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  347. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  348. package/dist/cli.js +536 -392
  349. package/dist/index.browser.js +1 -1
  350. package/dist/index.browser.js.map +1 -1
  351. package/dist/index.browser.mjs +1 -1
  352. package/dist/index.browser.mjs.map +1 -1
  353. package/dist/index.d.mts +2 -2
  354. package/dist/index.d.ts +2 -2
  355. package/dist/index.js +1 -1
  356. package/dist/index.js.map +1 -1
  357. package/dist/index.mjs +1 -1
  358. package/dist/index.mjs.map +1 -1
  359. package/dist/react.browser.js +1 -1
  360. package/dist/react.browser.js.map +1 -1
  361. package/dist/react.browser.mjs +1 -1
  362. package/dist/react.browser.mjs.map +1 -1
  363. package/dist/react.d.mts +2 -2
  364. package/dist/react.d.ts +2 -2
  365. package/dist/react.js +1 -1
  366. package/dist/react.js.map +1 -1
  367. package/dist/react.mjs +1 -1
  368. package/dist/react.mjs.map +1 -1
  369. package/dist/types/blocks/brand.d.ts +11 -2
  370. package/dist/types/blocks/create.d.ts +7 -3
  371. package/dist/types/blocks/doctor.d.ts +1 -0
  372. package/dist/types/blocks/flows.d.ts +7 -1
  373. package/dist/types/blocks/orgBrand.d.ts +34 -0
  374. package/dist/types/cli.d.ts +3 -0
  375. package/dist/types/version.d.ts +2 -2
  376. package/docs/api/desk.d.ts +2 -2
  377. package/docs/api/desk.md +3 -3
  378. package/package.json +1 -1
  379. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  380. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  381. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  382. package/stacks/6sense.json +17 -4
  383. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  384. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  385. package/stacks/apollo/pages/contacts.tsx.tmpl +29 -95
  386. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  387. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  388. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  389. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  390. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  391. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  392. package/stacks/apollo.json +36 -8
  393. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  394. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  395. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  396. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  397. package/stacks/calendly.json +8 -4
  398. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  399. package/stacks/clay/pages/contacts.tsx.tmpl +12 -0
  400. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  401. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  402. package/stacks/clay.json +16 -3
  403. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  404. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  405. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  406. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  407. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  408. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  409. package/stacks/gong.json +23 -6
  410. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  411. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  412. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  413. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  414. package/stacks/hootsuite.json +9 -4
  415. package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
  416. package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
  417. package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
  418. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  419. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  420. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  421. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  422. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  423. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  424. package/stacks/hubspot.json +21 -5
  425. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  426. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  427. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  428. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  429. package/stacks/intercom.json +8 -4
  430. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  431. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  432. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  433. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  434. package/stacks/mailchimp.json +20 -5
  435. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  436. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  437. package/stacks/outreach/pages/contacts.tsx.tmpl +27 -66
  438. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  439. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  440. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  441. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  442. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  443. package/stacks/outreach.json +25 -7
  444. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  445. package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -35
  446. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  447. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  448. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  449. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  450. package/stacks/pipedrive.json +4 -2
  451. package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
  452. package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
  453. package/stacks/salesforce/pages/deals.tsx.tmpl +10 -1
  454. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  455. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  456. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  457. package/stacks/salesforce.json +7 -3
  458. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  459. package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
  460. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  461. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  462. package/stacks/zoominfo.json +16 -3
  463. package/styles/styles.css +1 -1
  464. package/templates/next-app/app/globals.css.tmpl +349 -64
  465. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  466. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -21,6 +21,9 @@
21
21
  import { useEffect, useState } from "react";
22
22
  import { Button, Input, Label, Skeleton, Switch, Textarea } from "@graph8/sdk/ui";
23
23
  import { ApiError } from "@/g8/components/api-error";
24
+ import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
25
+ import { byPage, hasNextOf } from "@/g8/lib/paging";
26
+ import { FirstNotice, LoadMore } from "@/g8/components/paging-notices";
24
27
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
28
  import { EmptyState } from "@/g8/components/empty-state";
26
29
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
@@ -62,6 +65,11 @@ function Dial({ label, value, onChange, low, high }: { label: string; value: num
62
65
  );
63
66
  }
64
67
 
68
+ /** Agents per read; "Load more" appends the next page (the list used to stop silently at 50). */
69
+ const AGENT_PAGE = 50;
70
+ /** The voice presets API has no paging: a full read is announced under the select. */
71
+ const VOICE_LIMIT = 100;
72
+
65
73
  export function AgentBuilder({ agentId: controlled, onSelect, onChanged, hideList, className }: AgentBuilderProps) {
66
74
  const [inner, setInner] = useState<string | null>(null);
67
75
  const selected = controlled !== undefined ? controlled : inner;
@@ -72,9 +80,13 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, hideLis
72
80
  const [deleting, setDeleting] = useState(false);
73
81
 
74
82
  const choose = (id: string | null) => { setInner(id); setCreating(false); onSelect?.(id); };
75
- const agents = useG8Query((signal) => g8Get("/voice/agents", { search: search.trim() || undefined, page_size: 50 }, { signal }), [search], { enabled: !hideList });
83
+ const agents = useLoadMoreList(byPage<AgentSummary>(AGENT_PAGE, async (page, signal) => {
84
+ const env = await g8Get("/voice/agents", { search: search.trim() || undefined, page, page_size: AGENT_PAGE }, { signal });
85
+ const rows = listOf<AgentSummary>(env, "agents");
86
+ return { rows, hasNext: hasNextOf(env, AGENT_PAGE, rows.length) };
87
+ }), [search], { enabled: !hideList });
76
88
  const detail = useG8Query((signal) => g8Get(`/voice/agents/${selected}`, undefined, { signal }), [selected], { enabled: Boolean(selected) && !creating });
77
- const voices = useG8Query((signal) => g8Get("/voice/voice-library/presets", { limit: 100 }, { signal }), []);
89
+ const voices = useG8Query((signal) => g8Get("/voice/voice-library/presets", { limit: VOICE_LIMIT }, { signal }), []);
78
90
 
79
91
  useEffect(() => {
80
92
  const agent = bodyOf<{ agent?: AgentDetail | null }>(detail.data).agent;
@@ -101,7 +113,7 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, hideLis
101
113
  onSuccess: () => { setDeleting(false); setDraft(null); choose(null); if (!hideList) void agents.refetch(); onChanged?.(); },
102
114
  });
103
115
 
104
- const rows = listOf<AgentSummary>(agents.data, "agents");
116
+ const rows = agents.data ?? [];
105
117
  const presets = listOf<{ voice_id?: string; id?: string; name?: string; label?: string }>(voices.data, "items");
106
118
  const setPersona = (patch: Partial<AgentDetail["persona"]>) => setDraft((d) => d && { ...d, persona: { ...d.persona, ...patch } });
107
119
  // 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.
@@ -128,6 +140,7 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, hideLis
128
140
  </li>
129
141
  ))}
130
142
  </ul>
143
+ <LoadMore shown={rows.length} noun="agents" hasMore={agents.hasMore} loading={agents.loadingMore} onLoadMore={agents.loadMore} />
131
144
  {!agents.loading && !agents.error && rows.length === 0 ? (
132
145
  <EmptyState title="No agents yet" description="An agent has a persona, a voice, knowledge and skills." />
133
146
  ) : null}
@@ -170,6 +183,7 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, hideLis
170
183
  <option value="">Default voice</option>
171
184
  {presets.map((v) => { const id = v.voice_id ?? v.id ?? ""; return <option key={id} value={id}>{v.name ?? v.label ?? id}</option>; })}
172
185
  </select>
186
+ <FirstNotice shown={presets.length} limit={VOICE_LIMIT} noun="voices" />
173
187
  </div>
174
188
  <div className="flex flex-col gap-1"><Label htmlFor="agent-inbound">Inbound call instructions</Label><Textarea id="agent-inbound" rows={3} value={draft.persona.inbound_instructions ?? ""} onChange={(e) => setPersona({ inbound_instructions: e.target.value || null })} /></div>
175
189
  <div className="flex flex-col gap-1"><Label htmlFor="agent-outbound">Outbound call instructions</Label><Textarea id="agent-outbound" rows={3} value={draft.persona.outbound_instructions ?? ""} onChange={(e) => setPersona({ outbound_instructions: e.target.value || null })} /></div>
@@ -0,0 +1,143 @@
1
+ "use client";
2
+
3
+ /**
4
+ * AgentConversations: the chat history of one agent. The list of its conversations (newest first, "Load more" up to the API's 100),
5
+ * a conversation's messages, rename, hand-over to a human and delete, plus a message box to talk to the agent yourself.
6
+ * Sending a message is billable and the agent may run its attached skills while answering, so it goes through ConfirmSpendDialog.
7
+ * Deleting a conversation is destructive and confirmed.
8
+ *
9
+ * SDK operations, in order:
10
+ * 1. listChatAgentConversations POST /voice/chat-agent/conversations { agent_id, limit } (read, a POST with a body)
11
+ * 2. getChatAgentHistory POST /voice/chat-agent/history { agent_id, contact_id, session_id } (read)
12
+ * 3. renameChatAgentConversation PUT /voice/chat-agent/conversations/{chat_id}/name?agent_id { name } (write)
13
+ * 4. transferChatAgentConversation POST /voice/chat-agent/conversations/{chat_id}/transfer-to-human (write)
14
+ * 5. deleteChatAgentConversation DELETE /voice/chat-agent/conversations/{chat_id}?agent_id (destructive, ConfirmSpendDialog)
15
+ * 6. sendChatAgentMessage POST /voice/chat-agent/messages { agent_id, contact_id, message, session_id? } (billable, ConfirmSpendDialog)
16
+ *
17
+ * The history route does not match on contact_id, so any stable string works; `contactId` lets a host pass the person's own id.
18
+ */
19
+ import { useEffect, useState } from "react";
20
+ import { MessageSquare } from "lucide-react";
21
+ import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
22
+ import { ApiError } from "@/g8/components/api-error";
23
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
24
+ import { EmptyState } from "@/g8/components/empty-state";
25
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
26
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
27
+ import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
28
+ import { bodyOf, listOf } from "../lib/agent-types";
29
+ import { LoadMore, useGrowingList } from "../lib/load-more";
30
+ import { useBrand } from "@/g8/hooks/use-brand";
31
+
32
+ export interface AgentConversationsProps {
33
+ agentId: string;
34
+ /** Sent as contact_id on messages and history reads. Default "sdk". */
35
+ contactId?: string;
36
+ className?: string;
37
+ }
38
+
39
+ interface Chat { id: string; name?: string; message_count?: number; updated_at?: string | null; created_at?: string | null }
40
+ interface Msg { role?: string; content?: unknown; created_at?: string }
41
+
42
+ const text = (v: unknown): string => (typeof v === "string" ? v : v == null ? "" : JSON.stringify(v));
43
+ const when = (v?: string | null): string => (v ? new Date(v).toLocaleString() : "");
44
+
45
+ function Transcript({ agentId, chat, contactId, refreshKey }: { agentId: string; chat: Chat; contactId: string; refreshKey: number }) {
46
+ const history = useG8Query((signal) => g8Post(`/voice/chat-agent/history`, { agent_id: agentId, contact_id: contactId, session_id: chat.id }, { signal }), [agentId, chat.id, contactId, refreshKey]);
47
+ const body = bodyOf<{ messages?: Msg[]; success?: boolean; error?: string }>(history.data);
48
+ const messages = listOf<Msg>(history.data, "messages", "items");
49
+ if (history.error) return <ApiError error={history.error} title="Could not load the conversation" onRetry={() => void history.refetch()} />;
50
+ if (history.loading && !history.data) return <Skeleton className="h-24 w-full" aria-label="Loading conversation" />;
51
+ if (body.success === false && body.error) return <p className="text-sm text-muted-foreground">{body.error}</p>;
52
+ if (!messages.length) return <p className="text-sm text-muted-foreground">No messages in this conversation yet.</p>;
53
+ return (
54
+ <ol className="flex flex-col gap-2" aria-label="Messages">
55
+ {messages.map((m, i) => (
56
+ <li key={i} className={`max-w-[85%] rounded-md border px-3 py-2 text-sm ${m.role === "user" ? "self-end bg-accent" : "self-start"}`}>
57
+ <span className="block text-xs text-muted-foreground">{m.role === "user" ? "Contact" : m.role === "assistant" ? "Agent" : (m.role ?? "")}</span>
58
+ <span className="whitespace-pre-wrap">{text(m.content)}</span>
59
+ </li>
60
+ ))}
61
+ </ol>
62
+ );
63
+ }
64
+
65
+ export function AgentConversations({ agentId, contactId = "sdk", className }: AgentConversationsProps) {
66
+ const brand = useBrand();
67
+ const [openId, setOpenId] = useState<string | null>(null);
68
+ const [renaming, setRenaming] = useState<{ id: string; name: string } | null>(null);
69
+ const [deleting, setDeleting] = useState<Chat | null>(null);
70
+ const [draft, setDraft] = useState("");
71
+ const [sending, setSending] = useState<string | null>(null);
72
+ const [turn, setTurn] = useState(0);
73
+ const list = useGrowingList<Chat>(async (limit, signal) => listOf<Chat>(await g8Post("/voice/chat-agent/conversations", { agent_id: agentId, limit }, { signal }), "chats", "items", "conversations"), [agentId], { pageSize: 25, max: 100 });
74
+ useEffect(() => { setOpenId(null); setRenaming(null); setDeleting(null); setDraft(""); setSending(null); }, [agentId]);
75
+ const open = list.items.find((c) => c.id === openId) ?? (openId ? { id: openId } : null);
76
+ const enc = encodeURIComponent;
77
+
78
+ const rename = useG8Mutation((v: { id: string; name: string }) => g8Put(`/voice/chat-agent/conversations/${enc(v.id)}/name`, { name: v.name }, { query: { agent_id: agentId } }), { onSuccess: () => { setRenaming(null); list.reload(); } });
79
+ const handover = useG8Mutation((id: string) => g8Post(`/voice/chat-agent/conversations/${enc(id)}/transfer-to-human`, {}), { onSuccess: () => list.reload() });
80
+ const remove = useG8Mutation((id: string) => g8Delete(`/voice/chat-agent/conversations/${enc(id)}`, { query: { agent_id: agentId } }), { onSuccess: (_r, id) => { setDeleting(null); if (openId === id) setOpenId(null); list.reload(); } });
81
+ const send = useG8Mutation(async (message: string) => {
82
+ const res = await g8Post("/voice/chat-agent/messages", { agent_id: agentId, contact_id: contactId, message, ...(openId ? { session_id: openId } : {}) });
83
+ return bodyOf<{ session_id?: string }>(res).session_id ?? null;
84
+ }, { onSuccess: (sid) => { setSending(null); setDraft(""); if (sid) setOpenId(sid); setTurn((t) => t + 1); list.reload(); }, onError: () => setSending(null) });
85
+
86
+ return (
87
+ <div className={`grid gap-4 lg:grid-cols-[18rem_1fr] ${className ?? ""}`}>
88
+ <div className="flex flex-col gap-2">
89
+ <div className="flex items-center justify-between"><h3 className="text-sm font-medium">Conversations</h3><Button size="sm" variant="outline" onClick={() => setOpenId(null)}>New chat</Button></div>
90
+ {list.error ? <ApiError error={list.error} title="Could not load conversations" onRetry={list.reload} /> : null}
91
+ {list.loading ? <Skeleton className="h-16 w-full" aria-label="Loading conversations" /> : null}
92
+ <ul className="divide-y rounded-md border">
93
+ {list.items.map((c) => (
94
+ <li key={c.id} className={openId === c.id ? "bg-accent" : ""}>
95
+ <button type="button" data-testid={`chat-${c.id}`} className="block w-full px-3 py-2 text-left text-sm" onClick={() => setOpenId(c.id)}>
96
+ <span className="block truncate font-medium">{c.name || "Chat"}</span>
97
+ <span className="block text-xs text-muted-foreground">{c.message_count ?? 0} messages, {when(c.updated_at ?? c.created_at)}</span>
98
+ </button>
99
+ </li>
100
+ ))}
101
+ </ul>
102
+ {!list.loading && !list.error && list.items.length === 0 ? <EmptyState compact icon={<MessageSquare strokeWidth={1.5} />} title="No conversations yet" description="Send a message to start one." /> : null}
103
+ <LoadMore list={list} noun="conversations" />
104
+ </div>
105
+ <div className="flex min-w-0 flex-col gap-3">
106
+ {open ? (
107
+ <>
108
+ <div className="flex flex-wrap items-center gap-2">
109
+ {renaming?.id === open.id ? (
110
+ <>
111
+ <Input aria-label="Conversation name" className="max-w-xs" value={renaming.name} onChange={(e) => setRenaming({ id: open.id, name: e.target.value })} />
112
+ <Button size="sm" loading={rename.loading} disabled={!renaming.name.trim()} onClick={() => void rename.mutate(renaming)}>Save name</Button>
113
+ <Button size="sm" variant="ghost" onClick={() => setRenaming(null)}>Cancel</Button>
114
+ </>
115
+ ) : (
116
+ <>
117
+ <h3 className="text-sm font-medium">{(open as Chat).name || "Conversation"}</h3>
118
+ <Button size="sm" variant="outline" onClick={() => setRenaming({ id: open.id, name: (open as Chat).name ?? "" })}>Rename</Button>
119
+ <Button size="sm" variant="outline" loading={handover.loading} onClick={() => void handover.mutate(open.id)}>Hand over to a human</Button>
120
+ <Button size="sm" variant="ghost" onClick={() => setDeleting(open as Chat)}>Delete</Button>
121
+ </>
122
+ )}
123
+ </div>
124
+ {rename.error ? <ApiError error={rename.error} title="Could not rename the conversation" /> : null}
125
+ {handover.error ? <ApiError error={handover.error} title="Could not hand the conversation over" /> : null}
126
+ {handover.data ? <p role="status" className="text-sm text-muted-foreground">A person will pick this conversation up.</p> : null}
127
+ <Transcript agentId={agentId} chat={open as Chat} contactId={contactId} refreshKey={turn} />
128
+ </>
129
+ ) : <p className="text-sm text-muted-foreground">Pick a conversation, or type below to start a new one.</p>}
130
+ <div className="flex flex-col gap-2 border-t pt-3">
131
+ <Textarea aria-label="Message to the agent" rows={3} value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Say something to this agent" />
132
+ {send.error ? <ApiError error={send.error} title="Could not send the message" /> : null}
133
+ <Button className="self-end" disabled={!draft.trim()} onClick={() => setSending(draft.trim())}>Send message</Button>
134
+ </div>
135
+ </div>
136
+ {remove.error ? <ApiError error={remove.error} title="Could not delete the conversation" /> : null}
137
+ <ConfirmSpendDialog open={sending != null} tier="billable" title="Send a message to this agent" description={`The agent answers with the model and may run the skills attached to it, which can reach outside ${brand.name}. Each turn spends credits.`}
138
+ confirmLabel="Send" confirming={send.loading} onConfirm={() => sending && void send.mutate(sending)} onCancel={() => setSending(null)} />
139
+ <ConfirmSpendDialog open={deleting != null} tier="destructive" title={`Delete "${deleting?.name || "this conversation"}"`} description="The messages are removed for everyone." affected={1}
140
+ confirmLabel="Delete" confirming={remove.loading} onConfirm={() => deleting && void remove.mutate(deleting.id)} onCancel={() => setDeleting(null)} />
141
+ </div>
142
+ );
143
+ }
@@ -90,7 +90,7 @@ export function AgentKnowledgePanel({ agentId, useCompanyKnowledge, onToggleComp
90
90
 
91
91
  <div className="flex flex-col gap-2">
92
92
  <div className="flex items-center justify-between">
93
- <h3 className="text-sm font-semibold">This agent&apos;s documents</h3>
93
+ <h3 className="text-sm font-medium">This agent&apos;s documents</h3>
94
94
  <Button size="sm" variant="outline" onClick={() => fileInput.current?.click()}>Upload documents</Button>
95
95
  <input ref={fileInput} type="file" multiple hidden aria-label="Choose documents" accept=".pdf,.docx,.txt,.md" onChange={(e) => onPick(e.target.files)} />
96
96
  </div>
@@ -113,7 +113,7 @@ export function AgentKnowledgePanel({ agentId, useCompanyKnowledge, onToggleComp
113
113
  </div>
114
114
 
115
115
  <div className="flex flex-col gap-2">
116
- <h3 className="text-sm font-semibold">Company knowledge</h3>
116
+ <h3 className="text-sm font-medium">Company knowledge</h3>
117
117
  <form className="flex gap-2" onSubmit={(e) => { e.preventDefault(); setSubmitted(query.trim()); }}>
118
118
  <Input aria-label="Search company knowledge" placeholder="Search what the agent can draw on" value={query} onChange={(e) => setQuery(e.target.value)} />
119
119
  <Button type="submit" size="sm" variant="outline" disabled={!query.trim()}>Search</Button>
@@ -0,0 +1,164 @@
1
+ "use client";
2
+
3
+ /**
4
+ * AgentOperators: the autonomous operators that work in your workspace without being asked (each monitors something on a schedule,
5
+ * records findings, and acts only as far as its trust dial allows). A summary, the bench (turn each on or off), and for the open
6
+ * operator: Skills (the trust dial per skill: off, detect, propose or auto), Findings, Activity, Health and Scorecard.
7
+ * Turning an operator on, and raising a trust dial, start work that proceeds on its own and spends credits, so both go through
8
+ * ConfirmSpendDialog (billable). Deciding an operator's proposed fix (which changes your data) is not offered here; it stays in graph8.
9
+ *
10
+ * SDK operations, in order:
11
+ * 1. getAgentOperatorsSummary GET /agent/operators/summary (read)
12
+ * 2. listAgentOperators GET /agent/operators (read)
13
+ * 3. getAgentOperatorStatus GET /agent/operators/{operator}/status (read)
14
+ * 4. setAgentOperatorEnabled PUT /agent/operators/{operator}/enabled { enabled } (billable, ConfirmSpendDialog)
15
+ * 5. listAgentOperatorSkills GET /agent/operators/{operator}/skills (read)
16
+ * 6. setAgentOperatorTrustDial PUT /agent/operators/{operator}/skills/{skill_key} { autonomy } (billable, ConfirmSpendDialog)
17
+ * 7. listAgentOperatorFindings GET /agent/operators/{operator}/findings (read)
18
+ * 8. listAgentOperatorActivity GET /agent/operators/{operator}/activity?limit (read) "Load more" grows the limit
19
+ * 9. getAgentOperatorHealth GET /agent/operators/{operator}/health (read)
20
+ * 10. getAgentOperatorScorecard GET /agent/operators/{operator}/scorecard (read)
21
+ */
22
+ import { useState } from "react";
23
+ import { Cpu } from "lucide-react";
24
+ import { Button, Skeleton } from "@graph8/sdk/ui";
25
+ import { ApiError } from "@/g8/components/api-error";
26
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
+ import { EmptyState } from "@/g8/components/empty-state";
28
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
29
+ import { SplitView } from "@/g8/components/screen/split-view";
30
+ import { StatStrip } from "@/g8/components/screen/stat-strip";
31
+ import { StatusPill } from "@/g8/components/screen/status-pill";
32
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
33
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
34
+ import { useUrlState } from "@/g8/hooks/use-url-state";
35
+ import { g8Get, g8Put } from "@/g8/lib/g8-client";
36
+ import { FactList } from "../lib/facts";
37
+ import { bodyOf, listOf } from "../lib/agent-types";
38
+ import { LoadMore, useGrowingList } from "../lib/load-more";
39
+
40
+ export interface AgentOperatorsProps {
41
+ className?: string;
42
+ }
43
+
44
+ interface Operator { operator: string; title?: string; persona?: string; enabled?: boolean; has_run?: boolean; findings_total?: number; findings_critical?: number; detection?: boolean }
45
+ interface OperatorSkill { key: string; name?: string; title?: string; description?: string; autonomy?: string; default_autonomy?: string }
46
+ const LEVELS = ["OFF", "DETECT", "PROPOSE", "AUTO"] as const;
47
+ type Level = (typeof LEVELS)[number];
48
+ const enc = encodeURIComponent;
49
+
50
+ function Skills({ operator, onAsk }: { operator: string; onAsk: (v: { key: string; label: string; autonomy: Level }) => void }) {
51
+ const skills = useG8Query((signal) => g8Get(`/agent/operators/${enc(operator)}/skills`, undefined, { signal }), [operator]);
52
+ const rows = listOf<OperatorSkill>(skills.data, "items", "skills");
53
+ if (skills.error) return <ApiError error={skills.error} title="Could not load the skills" onRetry={() => void skills.refetch()} />;
54
+ if (skills.loading && !skills.data) return <Skeleton className="h-16 w-full" />;
55
+ if (rows.length === 0) return <p className="text-sm text-muted-foreground">This operator has no skills to tune.</p>;
56
+ return (
57
+ <ul className="divide-y rounded-md border" aria-label="Operator skills">
58
+ {rows.map((s) => (
59
+ <li key={s.key} className="flex flex-wrap items-center justify-between gap-2 px-3 py-2 text-sm">
60
+ <span><span className="font-medium">{s.name ?? s.title ?? s.key}</span>{s.description ? <span className="block text-xs text-muted-foreground">{s.description}</span> : null}</span>
61
+ <select aria-label={`Autonomy for ${s.name ?? s.title ?? s.key}`} className="h-8 rounded-md border bg-background px-2 text-sm" value={(s.autonomy ?? s.default_autonomy ?? "OFF").toUpperCase()}
62
+ onChange={(e) => onAsk({ key: s.key, label: s.name ?? s.title ?? s.key, autonomy: e.target.value as Level })}>
63
+ {LEVELS.map((l) => <option key={l} value={l}>{l.toLowerCase()}</option>)}
64
+ </select>
65
+ </li>
66
+ ))}
67
+ </ul>
68
+ );
69
+ }
70
+
71
+ function Activity({ operator }: { operator: string }) {
72
+ const list = useGrowingList<Record<string, unknown>>(async (limit, signal) => listOf<Record<string, unknown>>(await g8Get(`/agent/operators/${enc(operator)}/activity`, { limit }, { signal }), "items"), [operator], { pageSize: 25, max: 200 });
73
+ if (list.error) return <ApiError error={list.error} title="Could not load the activity" onRetry={list.reload} />;
74
+ if (list.loading) return <Skeleton className="h-16 w-full" />;
75
+ if (list.items.length === 0) return <p className="text-sm text-muted-foreground">Nothing has been decided or fixed yet.</p>;
76
+ return (
77
+ <div className="flex flex-col gap-2">
78
+ <ul className="divide-y rounded-md border" aria-label="Operator activity">
79
+ {list.items.map((r, i) => <li key={String(r.task_id ?? r.id ?? i)} className="px-3 py-2 text-sm">{String(r.title ?? r.summary ?? r.check_id ?? "Activity")}<span className="block text-xs text-muted-foreground">{String(r.resolution ?? r.status ?? "")} {r.at || r.created_at ? new Date(String(r.at ?? r.created_at)).toLocaleString() : ""}</span></li>)}
80
+ </ul>
81
+ <LoadMore list={list} noun="events" />
82
+ </div>
83
+ );
84
+ }
85
+
86
+ function Findings({ operator }: { operator: string }) {
87
+ const q = useG8Query((signal) => g8Get(`/agent/operators/${enc(operator)}/findings`, undefined, { signal }), [operator]);
88
+ const rows = listOf<Record<string, unknown>>(q.data, "items", "findings");
89
+ if (q.error) return <ApiError error={q.error} title="Could not load the findings" onRetry={() => void q.refetch()} />;
90
+ if (q.loading && !q.data) return <Skeleton className="h-16 w-full" />;
91
+ if (rows.length === 0) return <p className="text-sm text-muted-foreground">No open findings.</p>;
92
+ return <ul className="divide-y rounded-md border" aria-label="Findings">{rows.map((r, i) => <li key={String(r.id ?? r.check_id ?? i)} className="flex items-center justify-between gap-2 px-3 py-2 text-sm"><span>{String(r.title ?? r.summary ?? r.check_id ?? "Finding")}</span>{r.severity ? <StatusPill tone={String(r.severity) === "critical" ? "negative" : "warning"}>{String(r.severity)}</StatusPill> : null}</li>)}</ul>;
93
+ }
94
+
95
+ function Facts({ path }: { path: string }) {
96
+ const q = useG8Query((signal) => g8Get(path, undefined, { signal }), [path]);
97
+ if (q.error) return <ApiError error={q.error} title="Could not load this" onRetry={() => void q.refetch()} />;
98
+ if (q.loading && !q.data) return <Skeleton className="h-16 w-full" />;
99
+ return <FactList data={bodyOf(q.data)} />;
100
+ }
101
+
102
+ export function AgentOperators({ className }: AgentOperatorsProps) {
103
+ const [open, setOpen] = useUrlState("operator", "");
104
+ const [enabling, setEnabling] = useState<{ operator: Operator; enabled: boolean } | null>(null);
105
+ const [dial, setDial] = useState<{ key: string; label: string; autonomy: Level } | null>(null);
106
+ const [dialKey, setDialKey] = useState(0);
107
+ const summary = useG8Query((signal) => g8Get("/agent/operators/summary", undefined, { signal }), []);
108
+ const bench = useG8Query((signal) => g8Get("/agent/operators", undefined, { signal }), []);
109
+ const rows = listOf<Operator>(bench.data, "items", "operators");
110
+ const current = rows.find((o) => o.operator === open);
111
+ const sum = bodyOf<Record<string, number>>(summary.data);
112
+
113
+ const toggle = useG8Mutation((v: { operator: string; enabled: boolean }) => g8Put(`/agent/operators/${enc(v.operator)}/enabled`, { enabled: v.enabled }), { onSuccess: () => { setEnabling(null); void bench.refetch(); void summary.refetch(); }, onError: () => setEnabling(null) });
114
+ const setLevel = useG8Mutation((v: { operator: string; key: string; autonomy: Level }) => g8Put(`/agent/operators/${enc(v.operator)}/skills/${enc(v.key)}`, { autonomy: v.autonomy }), { onSuccess: () => { setDial(null); setDialKey((k) => k + 1); }, onError: () => { setDial(null); setDialKey((k) => k + 1); } });
115
+
116
+ const list = (
117
+ <div className="flex flex-col gap-3">
118
+ {bench.error ? <ApiError error={bench.error} title="Could not load the operators" onRetry={() => void bench.refetch()} /> : null}
119
+ {bench.loading && !bench.data ? <Skeleton className="h-20 w-full" aria-label="Loading operators" /> : null}
120
+ <ul className="divide-y rounded-md border">
121
+ {rows.map((o) => (
122
+ <li key={o.operator} className={`flex items-center gap-2 px-3 py-2 ${open === o.operator ? "bg-accent" : ""}`}>
123
+ <button type="button" data-testid={`operator-${o.operator}`} className="min-w-0 flex-1 text-left text-sm" onClick={() => setOpen(o.operator)}>
124
+ <span className="block truncate font-medium">{o.title ?? o.operator}</span>
125
+ <span className="block truncate text-xs text-muted-foreground">{o.detection === false ? "Always on" : `${o.findings_total ?? 0} findings, ${o.findings_critical ?? 0} critical`}</span>
126
+ </button>
127
+ {o.detection === false ? <StatusPill tone="positive" dot>on</StatusPill> : (
128
+ <Button size="sm" variant={o.enabled ? "outline" : "default"} onClick={() => setEnabling({ operator: o, enabled: !o.enabled })}>{o.enabled ? "Turn off" : "Turn on"}</Button>
129
+ )}
130
+ </li>
131
+ ))}
132
+ </ul>
133
+ {!bench.loading && !bench.error && rows.length === 0 ? <EmptyState icon={<Cpu strokeWidth={1.5} />} title="No operators" description="Operators appear here once the workspace has them." /> : null}
134
+ {toggle.error ? <ApiError error={toggle.error} title="Could not change the operator" /> : null}
135
+ </div>
136
+ );
137
+
138
+ const detail = open ? (
139
+ <DetailPanel title={current?.title ?? open} subtitle={current?.persona} onClose={() => setOpen("")}
140
+ badges={current ? <StatusPill tone={current.enabled ? "positive" : "neutral"} dot>{current.enabled ? "on" : "off"}</StatusPill> : undefined}
141
+ tabs={current?.detection === false ? [{ id: "status", label: "Status", content: <Facts path={`/agent/operators/${enc(open)}/status`} /> }] : [
142
+ { id: "skills", label: "Skills", content: <><Skills key={`${open}:${dialKey}`} operator={open} onAsk={setDial} />{setLevel.error ? <ApiError error={setLevel.error} title="Could not change the trust dial" /> : null}</> },
143
+ { id: "findings", label: "Findings", content: <Findings operator={open} /> },
144
+ { id: "activity", label: "Activity", content: <Activity operator={open} /> },
145
+ { id: "health", label: "Health", content: <Facts path={`/agent/operators/${enc(open)}/health`} /> },
146
+ { id: "scorecard", label: "Scorecard", content: <Facts path={`/agent/operators/${enc(open)}/scorecard`} /> },
147
+ { id: "status", label: "Status", content: <Facts path={`/agent/operators/${enc(open)}/status`} /> },
148
+ ]} />
149
+ ) : null;
150
+
151
+ return (
152
+ <div className={`flex flex-col gap-4 ${className ?? ""}`}>
153
+ <StatStrip loading={summary.loading && !summary.data} items={summary.error ? [] : Object.entries(sum).filter(([, v]) => typeof v === "number").slice(0, 5).map(([k, v]) => ({ label: k.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase()), value: String(v) }))} />
154
+ {summary.error ? <ApiError compact error={summary.error} title="Could not load the summary" onRetry={() => void summary.refetch()} /> : null}
155
+ <SplitView list={list} detail={detail} detailOpen={Boolean(open)} onCloseDetail={() => setOpen("")} detailWidth="lg" />
156
+ <ConfirmSpendDialog open={enabling != null} tier="billable" title={enabling?.enabled ? `Turn on ${enabling.operator.title ?? enabling.operator.operator}` : `Turn off ${enabling?.operator.title ?? enabling?.operator.operator ?? "operator"}`}
157
+ description={enabling?.enabled ? "Its monitors start running on a schedule and spend credits as they work, without being asked." : "Its monitors stop. Findings already recorded stay."}
158
+ confirmLabel={enabling?.enabled ? "Turn on" : "Turn off"} confirming={toggle.loading} onConfirm={() => enabling && void toggle.mutate({ operator: enabling.operator.operator, enabled: enabling.enabled })} onCancel={() => setEnabling(null)} />
159
+ <ConfirmSpendDialog open={dial != null} tier="billable" title={`Set ${dial?.label ?? "skill"} to ${dial?.autonomy.toLowerCase() ?? ""}`}
160
+ description="The trust dial decides how far this skill may go on its own when it finds something. Higher settings act without asking and spend credits."
161
+ confirmLabel="Set dial" confirming={setLevel.loading} onConfirm={() => dial && open && void setLevel.mutate({ operator: open, key: dial.key, autonomy: dial.autonomy })} onCancel={() => { setDial(null); setDialKey((k) => k + 1); }} />
162
+ </div>
163
+ );
164
+ }
@@ -10,10 +10,12 @@
10
10
  * 2. listAgentApprovals GET /agent/approvals (read) this run's pending ones
11
11
  * 3. decideAgentApproval POST /agent/approvals/{approval_id}/decide { decision } (write; approving lets the run act: ConfirmSpendDialog, external)
12
12
  * 4. cancelAgentRun POST /agent/runs/{run_id}/cancel (destructive, ConfirmSpendDialog)
13
+ * 5. steerAgentWorkItem POST /agent/work-items/{work_item_id}/steer { client_turn_id, instruction } (write; while the run is live)
14
+ * The same client_turn_id is reused when a send is retried, so an instruction is never applied twice.
13
15
  * Approvals that gate a Work coordination action cannot be decided with an API key: the error is shown as is.
14
16
  */
15
- import { useEffect, useState } from "react";
16
- import { Button, Skeleton } from "@graph8/sdk/ui";
17
+ import { useEffect, useRef, useState } from "react";
18
+ import { Button, Skeleton, Textarea } from "@graph8/sdk/ui";
17
19
  import { ApiError } from "@/g8/components/api-error";
18
20
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
19
21
  import { KeyValueList } from "@/g8/components/screen/key-value-list";
@@ -43,6 +45,8 @@ export function AgentRunDetail({ runId, pollMs = 3000, onChanged, className }: A
43
45
  const [cancelling, setCancelling] = useState(false);
44
46
  const [approving, setApproving] = useState<Approval | null>(null);
45
47
  const [deciding, setDeciding] = useState<string | null>(null);
48
+ const [steerText, setSteerText] = useState("");
49
+ const turnId = useRef<string | null>(null);
46
50
  const poll = useJobPoll<AgentRun>((signal) => g8Get<AgentRun>(`/agent/runs/${encodeURIComponent(runId)}`, undefined, { signal }).then((r) => bodyOf<AgentRun>(r)), {
47
51
  isTerminal: isTerminalRun, intervalMs: pollMs, maxIntervalMs: pollMs * 4, onDone: () => onChanged?.(),
48
52
  });
@@ -56,6 +60,12 @@ export function AgentRunDetail({ runId, pollMs = 3000, onChanged, className }: A
56
60
  try { return await g8Post(`/agent/approvals/${encodeURIComponent(v.id)}/decide`, { decision: v.decision }); } finally { setDeciding(null); }
57
61
  }, { onSuccess: () => { setApproving(null); void approvals.refetch(); onChanged?.(); }, onError: () => setApproving(null) });
58
62
 
63
+ const steer = useG8Mutation((instruction: string) => {
64
+ turnId.current ??= globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
65
+ return g8Post(`/agent/work-items/${encodeURIComponent(runId)}/steer`, { client_turn_id: turnId.current, instruction });
66
+ }, { onSuccess: () => { turnId.current = null; setSteerText(""); poll.start(); } });
67
+ useEffect(() => { setSteerText(""); turnId.current = null; steer.reset(); }, [runId]); // eslint-disable-line react-hooks/exhaustive-deps
68
+
59
69
  const run = poll.status?.work_item_id === runId ? poll.status : undefined;
60
70
  const pending = listOf<Approval>(approvals.data).filter((a) => a.work_item_id === runId && !a.decision && !a.is_expired);
61
71
  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" />;
@@ -79,7 +89,7 @@ export function AgentRunDetail({ runId, pollMs = 3000, onChanged, className }: A
79
89
  ]} />
80
90
  {pending.length > 0 ? (
81
91
  <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>
92
+ <h4 className="text-sm font-medium">Waiting for your decision ({pending.length})</h4>
83
93
  {pending.map((a) => (
84
94
  <div key={a.id} className="flex flex-wrap items-center justify-between gap-2 text-sm">
85
95
  <span>{a.summary}<span className="ml-2 text-xs text-muted-foreground">risk: {a.risk_level}</span></span>
@@ -91,6 +101,13 @@ export function AgentRunDetail({ runId, pollMs = 3000, onChanged, className }: A
91
101
  ))}
92
102
  </section>
93
103
  ) : null}
104
+ {!isTerminalRun(run) ? (
105
+ <section className="space-y-2" aria-label="Steer the run">
106
+ <Textarea aria-label="Instruction for the agent" rows={2} placeholder="Tell the agent what to do differently" value={steerText} onChange={(e) => { if (e.target.value !== steerText) turnId.current = null; setSteerText(e.target.value); }} />
107
+ <div className="flex justify-end"><Button size="sm" variant="outline" loading={steer.loading} disabled={!steerText.trim()} onClick={() => void steer.mutate(steerText.trim())}>Send instruction</Button></div>
108
+ {steer.error ? <ApiError error={steer.error} title="Could not send the instruction" /> : null}
109
+ </section>
110
+ ) : null}
94
111
  {decide.error ? <ApiError error={decide.error} title="Could not record the decision" /> : null}
95
112
  {cancel.error ? <ApiError error={cancel.error} title="Could not cancel the run" /> : null}
96
113
  <ConfirmSpendDialog open={cancelling} tier="destructive" title={`Cancel "${run.title}"`} description="Work stops where it is. Steps already taken are not undone."
@@ -100,8 +100,8 @@ export function AgentRuns({ agentId, pageSize = 20, pollMs = 3000, selectedId, o
100
100
  return (
101
101
  <div className={`flex flex-col gap-4 ${className ?? ""}`}>
102
102
  {pending.length > 0 ? (
103
- <div className="rounded-md border border-amber-300 p-3" data-testid="approvals">
104
- <h3 className="mb-2 text-sm font-semibold">Waiting for your decision ({pending.length})</h3>
103
+ <div className="rounded-md border border-warning-foreground/30 p-3" data-testid="approvals">
104
+ <h3 className="mb-2 text-sm font-medium">Waiting for your decision ({pending.length})</h3>
105
105
  <ul className="flex flex-col gap-2">
106
106
  {pending.map((a) => (
107
107
  <li key={a.id} className="flex flex-wrap items-center justify-between gap-2 text-sm">
@@ -160,7 +160,7 @@ export function AgentRuns({ agentId, pageSize = 20, pollMs = 3000, selectedId, o
160
160
  <div className="rounded-md border p-3" data-testid="run-detail">
161
161
  {current ? (
162
162
  <>
163
- <div className="flex items-center justify-between"><h3 className="font-semibold">{current.title}</h3><span className="text-sm">{current.status}{detail.polling ? " (updating)" : ""}</span></div>
163
+ <div className="flex items-center justify-between"><h3 className="font-medium">{current.title}</h3><span className="text-sm">{current.status}{detail.polling ? " (updating)" : ""}</span></div>
164
164
  <dl className="mt-2 grid grid-cols-2 gap-1 text-sm">
165
165
  <dt className="text-muted-foreground">Current step</dt><dd>{current.current_step ?? "-"}</dd>
166
166
  <dt className="text-muted-foreground">Next action</dt><dd>{current.next_action ?? "-"}</dd>
@@ -10,7 +10,7 @@
10
10
  * 3. attachSkillsToAgent POST /voice/agents/{agent_id}/skills { action_ids } (write)
11
11
  * 4. detachSkillFromAgent DELETE /voice/agents/{agent_id}/skills/{action_id} (destructive, ConfirmSpendDialog)
12
12
  * 5. listModels GET /skills/models (read)
13
- * 6. createSkill POST /skills { name, runtime_type: "llm", llm_config } (write)
13
+ * 6. createSkill POST /skills { name, runtime_type: "llm", llm_config: { model, prompt_template } } (write)
14
14
  *
15
15
  * Detaching never deletes the skill; it only stops this agent from using it.
16
16
  */
@@ -42,16 +42,16 @@ export function AgentSkillsPanel({ agentId, onChanged, className }: AgentSkillsP
42
42
  const [attachingId, setAttachingId] = useState<string | null>(null);
43
43
  useEffect(() => { setDetaching(null); setCreating(null); setAttachingId(null); }, [agentId]);
44
44
 
45
- const attached = useG8Query((signal) => g8Get(`/voice/agents/${agentId}/skills`, undefined, { signal }), [agentId]);
45
+ const attached = useG8Query((signal) => g8Get(`/voice/agents/${encodeURIComponent(agentId)}/skills`, undefined, { signal }), [agentId]);
46
46
  const all = useG8Query((signal) => g8Get("/skills", undefined, { signal }), []);
47
47
  const models = useG8Query((signal) => g8Get("/skills/models", undefined, { signal }), [], { enabled: creating != null });
48
48
 
49
49
  const changed = () => { void attached.refetch(); void all.refetch(); onChanged?.(); };
50
- const attach = useG8Mutation((actionId: string) => g8Post(`/voice/agents/${agentId}/skills`, { action_ids: [actionId] }), { onSuccess: changed });
50
+ const attach = useG8Mutation((actionId: string) => g8Post(`/voice/agents/${encodeURIComponent(agentId)}/skills`, { action_ids: [actionId] }), { onSuccess: changed });
51
51
  const attachOne = async (id: string) => { setAttachingId(id); await attach.mutate(id); setAttachingId(null); };
52
- const detach = useG8Mutation((actionId: string) => g8Delete(`/voice/agents/${agentId}/skills/${actionId}`), { onSuccess: () => { setDetaching(null); changed(); } });
52
+ const detach = useG8Mutation((actionId: string) => g8Delete(`/voice/agents/${encodeURIComponent(agentId)}/skills/${encodeURIComponent(actionId)}`), { onSuccess: () => { setDetaching(null); changed(); } });
53
53
  const create = useG8Mutation(
54
- (v: NonNullable<typeof creating>) => g8Post("/skills", { name: v.name, description: v.description || undefined, runtime_type: "llm", llm_config: { prompt: v.prompt, model: v.model } }),
54
+ (v: NonNullable<typeof creating>) => g8Post("/skills", { name: v.name, description: v.description || undefined, runtime_type: "llm", llm_config: { model: v.model, prompt_template: v.prompt } }),
55
55
  { onSuccess: () => { setCreating(null); changed(); } },
56
56
  );
57
57
 
@@ -63,7 +63,7 @@ export function AgentSkillsPanel({ agentId, onChanged, className }: AgentSkillsP
63
63
  return (
64
64
  <div className={`flex flex-col gap-5 ${className ?? ""}`}>
65
65
  <div className="flex flex-col gap-2">
66
- <h3 className="text-sm font-semibold">Attached skills</h3>
66
+ <h3 className="text-sm font-medium">Attached skills</h3>
67
67
  {attached.error ? <ApiError error={attached.error} title="Could not load the agent's skills" onRetry={() => void attached.refetch()} /> : null}
68
68
  {attached.loading && mine.length === 0 ? <Skeleton className="h-10 w-full" /> : null}
69
69
  <ul className="divide-y rounded-md border">
@@ -79,7 +79,7 @@ export function AgentSkillsPanel({ agentId, onChanged, className }: AgentSkillsP
79
79
 
80
80
  <div className="flex flex-col gap-2">
81
81
  <div className="flex items-center justify-between">
82
- <h3 className="text-sm font-semibold">Available skills</h3>
82
+ <h3 className="text-sm font-medium">Available skills</h3>
83
83
  <Button size="sm" variant="outline" onClick={() => setCreating({ name: "", description: "", prompt: "", model: "" })}>New skill</Button>
84
84
  </div>
85
85
  {all.error ? <ApiError error={all.error} title="Could not load skills" onRetry={() => void all.refetch()} /> : null}