@stigmer/react 3.12.4 → 3.12.6

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 (369) hide show
  1. package/agent/AgentDetailView.js +2 -2
  2. package/agent/AgentDetailView.js.map +1 -1
  3. package/agent/index.d.ts +0 -1
  4. package/agent/index.d.ts.map +1 -1
  5. package/agent/index.js +0 -1
  6. package/agent/index.js.map +1 -1
  7. package/agent/useUpdateAgent.d.ts +3 -3
  8. package/agent/useUpdateAgent.js +3 -3
  9. package/agent-instance/AgentInstanceDetailPanel.d.ts.map +1 -1
  10. package/agent-instance/AgentInstanceDetailPanel.js +5 -7
  11. package/agent-instance/AgentInstanceDetailPanel.js.map +1 -1
  12. package/agent-instance/useUpdateAgentInstance.d.ts +1 -1
  13. package/agent-instance/useUpdateAgentInstance.js +1 -1
  14. package/attachment/AttachmentChipList.js +16 -6
  15. package/attachment/AttachmentChipList.js.map +1 -1
  16. package/attachment/index.d.ts +1 -1
  17. package/attachment/index.d.ts.map +1 -1
  18. package/attachment/index.js.map +1 -1
  19. package/attachment/useAttachments.d.ts +51 -8
  20. package/attachment/useAttachments.d.ts.map +1 -1
  21. package/attachment/useAttachments.js +74 -3
  22. package/attachment/useAttachments.js.map +1 -1
  23. package/channel/AgentChannelsPanel.d.ts.map +1 -1
  24. package/channel/AgentChannelsPanel.js +10 -5
  25. package/channel/AgentChannelsPanel.js.map +1 -1
  26. package/channel/ChannelRunConfigDialog.d.ts +51 -0
  27. package/channel/ChannelRunConfigDialog.d.ts.map +1 -0
  28. package/channel/ChannelRunConfigDialog.js +121 -0
  29. package/channel/ChannelRunConfigDialog.js.map +1 -0
  30. package/channel/index.d.ts +2 -0
  31. package/channel/index.d.ts.map +1 -1
  32. package/channel/index.js +1 -0
  33. package/channel/index.js.map +1 -1
  34. package/channel/useSaveAgentChannel.d.ts.map +1 -1
  35. package/channel/useSaveAgentChannel.js +6 -3
  36. package/channel/useSaveAgentChannel.js.map +1 -1
  37. package/channel-app/ChannelAppDetailPanel.js +9 -9
  38. package/channel-app/ChannelAppDetailPanel.js.map +1 -1
  39. package/composer/ComposerToolbar.d.ts +6 -1
  40. package/composer/ComposerToolbar.d.ts.map +1 -1
  41. package/composer/ComposerToolbar.js +2 -2
  42. package/composer/ComposerToolbar.js.map +1 -1
  43. package/composer/SessionComposer.d.ts +9 -0
  44. package/composer/SessionComposer.d.ts.map +1 -1
  45. package/composer/SessionComposer.js +39 -13
  46. package/composer/SessionComposer.js.map +1 -1
  47. package/conversation/ConversationComposer.d.ts +19 -3
  48. package/conversation/ConversationComposer.d.ts.map +1 -1
  49. package/conversation/ConversationComposer.js +4 -4
  50. package/conversation/ConversationComposer.js.map +1 -1
  51. package/conversation/ConversationTemplatePickerDialog.d.ts +63 -0
  52. package/conversation/ConversationTemplatePickerDialog.d.ts.map +1 -0
  53. package/conversation/ConversationTemplatePickerDialog.js +151 -0
  54. package/conversation/ConversationTemplatePickerDialog.js.map +1 -0
  55. package/conversation/ConversationsWorkbench.d.ts.map +1 -1
  56. package/conversation/ConversationsWorkbench.js +16 -5
  57. package/conversation/ConversationsWorkbench.js.map +1 -1
  58. package/conversation/index.d.ts +3 -1
  59. package/conversation/index.d.ts.map +1 -1
  60. package/conversation/index.js +1 -0
  61. package/conversation/index.js.map +1 -1
  62. package/conversation/useConversationParticipation.d.ts +41 -4
  63. package/conversation/useConversationParticipation.d.ts.map +1 -1
  64. package/conversation/useConversationParticipation.js +13 -2
  65. package/conversation/useConversationParticipation.js.map +1 -1
  66. package/cursor-accounts/CursorAccountsConsole.d.ts.map +1 -1
  67. package/cursor-accounts/CursorAccountsConsole.js +12 -3
  68. package/cursor-accounts/CursorAccountsConsole.js.map +1 -1
  69. package/cursor-accounts/cursor-account-coverage.d.ts +30 -1
  70. package/cursor-accounts/cursor-account-coverage.d.ts.map +1 -1
  71. package/cursor-accounts/cursor-account-coverage.js +21 -0
  72. package/cursor-accounts/cursor-account-coverage.js.map +1 -1
  73. package/execution/useCreateAgentExecution.d.ts +15 -0
  74. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  75. package/execution/useCreateAgentExecution.js +5 -0
  76. package/execution/useCreateAgentExecution.js.map +1 -1
  77. package/identity-account/AccountPreferencesPanel.d.ts +36 -0
  78. package/identity-account/AccountPreferencesPanel.d.ts.map +1 -0
  79. package/identity-account/AccountPreferencesPanel.js +202 -0
  80. package/identity-account/AccountPreferencesPanel.js.map +1 -0
  81. package/identity-account/index.d.ts +8 -0
  82. package/identity-account/index.d.ts.map +1 -1
  83. package/identity-account/index.js +4 -0
  84. package/identity-account/index.js.map +1 -1
  85. package/identity-account/useAccountExecutionDefaults.d.ts +36 -0
  86. package/identity-account/useAccountExecutionDefaults.d.ts.map +1 -0
  87. package/identity-account/useAccountExecutionDefaults.js +46 -0
  88. package/identity-account/useAccountExecutionDefaults.js.map +1 -0
  89. package/identity-account/useMyIdentityAccount.d.ts +55 -0
  90. package/identity-account/useMyIdentityAccount.d.ts.map +1 -0
  91. package/identity-account/useMyIdentityAccount.js +37 -0
  92. package/identity-account/useMyIdentityAccount.js.map +1 -0
  93. package/identity-account/useUpdateIdentityAccount.d.ts +42 -0
  94. package/identity-account/useUpdateIdentityAccount.d.ts.map +1 -0
  95. package/identity-account/useUpdateIdentityAccount.js +53 -0
  96. package/identity-account/useUpdateIdentityAccount.js.map +1 -0
  97. package/identity-provider/IdentityProviderDetailPanel.d.ts.map +1 -1
  98. package/identity-provider/IdentityProviderDetailPanel.js +16 -15
  99. package/identity-provider/IdentityProviderDetailPanel.js.map +1 -1
  100. package/index.d.ts +15 -13
  101. package/index.d.ts.map +1 -1
  102. package/index.js +11 -8
  103. package/index.js.map +1 -1
  104. package/internal/StandingContextField.d.ts +37 -0
  105. package/internal/StandingContextField.d.ts.map +1 -0
  106. package/internal/StandingContextField.js +24 -0
  107. package/internal/StandingContextField.js.map +1 -0
  108. package/library/InstanceVisibilitySelector.d.ts.map +1 -1
  109. package/library/InstanceVisibilitySelector.js +9 -2
  110. package/library/InstanceVisibilitySelector.js.map +1 -1
  111. package/library/ResourceVisibilityControl.d.ts +13 -3
  112. package/library/ResourceVisibilityControl.d.ts.map +1 -1
  113. package/library/ResourceVisibilityControl.js +33 -5
  114. package/library/ResourceVisibilityControl.js.map +1 -1
  115. package/library/VisibilityOptionRow.d.ts +6 -0
  116. package/library/VisibilityOptionRow.d.ts.map +1 -1
  117. package/library/VisibilityOptionRow.js +11 -1
  118. package/library/VisibilityOptionRow.js.map +1 -1
  119. package/library/VisibilitySelector.d.ts.map +1 -1
  120. package/library/VisibilitySelector.js +14 -2
  121. package/library/VisibilitySelector.js.map +1 -1
  122. package/library/index.d.ts +4 -2
  123. package/library/index.d.ts.map +1 -1
  124. package/library/index.js +2 -1
  125. package/library/index.js.map +1 -1
  126. package/library/useCanSetPublicVisibility.d.ts +30 -0
  127. package/library/useCanSetPublicVisibility.d.ts.map +1 -0
  128. package/library/useCanSetPublicVisibility.js +41 -0
  129. package/library/useCanSetPublicVisibility.js.map +1 -0
  130. package/library/visibilityLevels.d.ts +32 -1
  131. package/library/visibilityLevels.d.ts.map +1 -1
  132. package/library/visibilityLevels.js +34 -14
  133. package/library/visibilityLevels.js.map +1 -1
  134. package/mcp-server/McpServerDetailView.js +2 -2
  135. package/mcp-server/McpServerDetailView.js.map +1 -1
  136. package/mcp-server/index.d.ts +0 -1
  137. package/mcp-server/index.d.ts.map +1 -1
  138. package/mcp-server/index.js +0 -1
  139. package/mcp-server/index.js.map +1 -1
  140. package/mcp-server/internal/definitionDrift.d.ts +14 -12
  141. package/mcp-server/internal/definitionDrift.d.ts.map +1 -1
  142. package/mcp-server/internal/definitionDrift.js +22 -19
  143. package/mcp-server/internal/definitionDrift.js.map +1 -1
  144. package/mcp-server/useMcpServerConnect.d.ts +8 -5
  145. package/mcp-server/useMcpServerConnect.d.ts.map +1 -1
  146. package/mcp-server/useMcpServerConnect.js +2 -1
  147. package/mcp-server/useMcpServerConnect.js.map +1 -1
  148. package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -1
  149. package/mcp-server/useMcpServerOAuthConnect.js +7 -2
  150. package/mcp-server/useMcpServerOAuthConnect.js.map +1 -1
  151. package/mcp-server/useUpdateMcpServer.d.ts +3 -3
  152. package/mcp-server/useUpdateMcpServer.js +3 -3
  153. package/models/ModelSelector.d.ts +33 -1
  154. package/models/ModelSelector.d.ts.map +1 -1
  155. package/models/ModelSelector.js +37 -4
  156. package/models/ModelSelector.js.map +1 -1
  157. package/models/index.d.ts +2 -0
  158. package/models/index.d.ts.map +1 -1
  159. package/models/index.js +1 -0
  160. package/models/index.js.map +1 -1
  161. package/models/registry.d.ts +16 -0
  162. package/models/registry.d.ts.map +1 -1
  163. package/models/registry.js +13 -0
  164. package/models/registry.js.map +1 -1
  165. package/models/thinking-mode.d.ts +27 -0
  166. package/models/thinking-mode.d.ts.map +1 -0
  167. package/models/thinking-mode.js +32 -0
  168. package/models/thinking-mode.js.map +1 -0
  169. package/oauth-app/CreateOAuthAppForm.d.ts +2 -2
  170. package/oauth-app/CreateOAuthAppForm.d.ts.map +1 -1
  171. package/oauth-app/CreateOAuthAppForm.js +13 -4
  172. package/oauth-app/CreateOAuthAppForm.js.map +1 -1
  173. package/oauth-app/OAuthAppDetailPanel.d.ts.map +1 -1
  174. package/oauth-app/OAuthAppDetailPanel.js +50 -22
  175. package/oauth-app/OAuthAppDetailPanel.js.map +1 -1
  176. package/organization/OrgPreferencesPanel.d.ts +32 -0
  177. package/organization/OrgPreferencesPanel.d.ts.map +1 -0
  178. package/organization/OrgPreferencesPanel.js +90 -0
  179. package/organization/OrgPreferencesPanel.js.map +1 -0
  180. package/organization/OrgProfilePanel.d.ts.map +1 -1
  181. package/organization/OrgProfilePanel.js +5 -4
  182. package/organization/OrgProfilePanel.js.map +1 -1
  183. package/organization/index.d.ts +2 -0
  184. package/organization/index.d.ts.map +1 -1
  185. package/organization/index.js +1 -0
  186. package/organization/index.js.map +1 -1
  187. package/package.json +4 -4
  188. package/platform-client/PlatformClientDetailPanel.d.ts.map +1 -1
  189. package/platform-client/PlatformClientDetailPanel.js +12 -13
  190. package/platform-client/PlatformClientDetailPanel.js.map +1 -1
  191. package/schedule/ScheduleDetailView.d.ts.map +1 -1
  192. package/schedule/ScheduleDetailView.js +21 -10
  193. package/schedule/ScheduleDetailView.js.map +1 -1
  194. package/schedule/ScheduleForm.d.ts.map +1 -1
  195. package/schedule/ScheduleForm.js +13 -3
  196. package/schedule/ScheduleForm.js.map +1 -1
  197. package/session/NewSessionViewer.d.ts +10 -1
  198. package/session/NewSessionViewer.d.ts.map +1 -1
  199. package/session/NewSessionViewer.js +2 -1
  200. package/session/NewSessionViewer.js.map +1 -1
  201. package/session/run-config.d.ts +9 -0
  202. package/session/run-config.d.ts.map +1 -1
  203. package/session/run-config.js +5 -0
  204. package/session/run-config.js.map +1 -1
  205. package/session/useNewSessionFlow.d.ts +27 -1
  206. package/session/useNewSessionFlow.d.ts.map +1 -1
  207. package/session/useNewSessionFlow.js +51 -8
  208. package/session/useNewSessionFlow.js.map +1 -1
  209. package/session/useSessionConversation.d.ts +12 -1
  210. package/session/useSessionConversation.d.ts.map +1 -1
  211. package/session/useSessionConversation.js +15 -24
  212. package/session/useSessionConversation.js.map +1 -1
  213. package/session/useSessionPageFlow.d.ts.map +1 -1
  214. package/session/useSessionPageFlow.js +14 -10
  215. package/session/useSessionPageFlow.js.map +1 -1
  216. package/session/useUpdateSession.d.ts +18 -24
  217. package/session/useUpdateSession.d.ts.map +1 -1
  218. package/session/useUpdateSession.js +18 -24
  219. package/session/useUpdateSession.js.map +1 -1
  220. package/settings/AccountPreferencesSection.d.ts +3 -0
  221. package/settings/AccountPreferencesSection.d.ts.map +1 -0
  222. package/settings/AccountPreferencesSection.js +10 -0
  223. package/settings/AccountPreferencesSection.js.map +1 -0
  224. package/settings/OrgPreferencesSection.d.ts +3 -0
  225. package/settings/OrgPreferencesSection.d.ts.map +1 -0
  226. package/settings/OrgPreferencesSection.js +13 -0
  227. package/settings/OrgPreferencesSection.js.map +1 -0
  228. package/settings/index.d.ts +2 -0
  229. package/settings/index.d.ts.map +1 -1
  230. package/settings/index.js +2 -0
  231. package/settings/index.js.map +1 -1
  232. package/settings/settings-nav.d.ts.map +1 -1
  233. package/settings/settings-nav.js +17 -1
  234. package/settings/settings-nav.js.map +1 -1
  235. package/src/agent/AgentDetailView.tsx +2 -2
  236. package/src/agent/index.ts +0 -2
  237. package/src/agent/useUpdateAgent.ts +3 -3
  238. package/src/agent-instance/AgentInstanceDetailPanel.tsx +5 -7
  239. package/src/agent-instance/__tests__/AgentInstanceDetailPanel.test.tsx +75 -0
  240. package/src/agent-instance/useUpdateAgentInstance.ts +1 -1
  241. package/src/attachment/AttachmentChipList.tsx +19 -8
  242. package/src/attachment/__tests__/AttachmentChipList.test.tsx +12 -0
  243. package/src/attachment/__tests__/useAttachments.test.ts +293 -0
  244. package/src/attachment/index.ts +1 -0
  245. package/src/attachment/useAttachments.ts +143 -11
  246. package/src/channel/AgentChannelsPanel.tsx +26 -1
  247. package/src/channel/ChannelRunConfigDialog.tsx +351 -0
  248. package/src/channel/__tests__/ChannelRunConfigDialog.test.tsx +210 -0
  249. package/src/channel/index.ts +2 -0
  250. package/src/channel/useSaveAgentChannel.ts +6 -3
  251. package/src/channel-app/ChannelAppDetailPanel.tsx +9 -9
  252. package/src/channel-app/__tests__/channelAppWhatsApp.test.tsx +2 -0
  253. package/src/channel-app/__tests__/channelAppWipeSafety.test.tsx +87 -0
  254. package/src/composer/ComposerToolbar.tsx +9 -0
  255. package/src/composer/SessionComposer.tsx +59 -16
  256. package/src/composer/__tests__/SessionComposer-thinkingMode.test.tsx +176 -0
  257. package/src/conversation/ConversationComposer.tsx +38 -4
  258. package/src/conversation/ConversationTemplatePickerDialog.tsx +441 -0
  259. package/src/conversation/ConversationsWorkbench.tsx +40 -4
  260. package/src/conversation/__tests__/ConversationComposer.test.tsx +56 -1
  261. package/src/conversation/__tests__/ConversationTemplatePickerDialog.test.tsx +224 -0
  262. package/src/conversation/__tests__/useConversationParticipation.test.tsx +45 -4
  263. package/src/conversation/index.ts +4 -0
  264. package/src/conversation/useConversationParticipation.ts +58 -6
  265. package/src/cursor-accounts/CursorAccountsConsole.tsx +40 -5
  266. package/src/cursor-accounts/__tests__/CursorAccountsConsole.test.tsx +32 -0
  267. package/src/cursor-accounts/__tests__/cursor-account-coverage.test.ts +58 -1
  268. package/src/cursor-accounts/cursor-account-coverage.ts +55 -0
  269. package/src/execution/useCreateAgentExecution.ts +19 -0
  270. package/src/identity-account/AccountPreferencesPanel.tsx +532 -0
  271. package/src/identity-account/__tests__/AccountPreferencesPanel.test.tsx +267 -0
  272. package/src/identity-account/__tests__/useAccountExecutionDefaults.test.tsx +116 -0
  273. package/src/identity-account/__tests__/useMyIdentityAccount.test.tsx +121 -0
  274. package/src/identity-account/index.ts +11 -0
  275. package/src/identity-account/useAccountExecutionDefaults.ts +63 -0
  276. package/src/identity-account/useMyIdentityAccount.ts +75 -0
  277. package/src/identity-account/useUpdateIdentityAccount.ts +73 -0
  278. package/src/identity-provider/IdentityProviderDetailPanel.tsx +18 -15
  279. package/src/identity-provider/__tests__/IdentityProviderDetailPanel.test.tsx +80 -0
  280. package/src/index.ts +26 -4
  281. package/src/internal/StandingContextField.tsx +92 -0
  282. package/src/library/InstanceVisibilitySelector.tsx +9 -2
  283. package/src/library/ResourceVisibilityControl.tsx +46 -13
  284. package/src/library/VisibilityOptionRow.tsx +28 -6
  285. package/src/library/VisibilitySelector.tsx +14 -2
  286. package/src/library/__tests__/VisibilitySelector.test.tsx +48 -1
  287. package/src/library/__tests__/visibilityLevels.test.ts +67 -1
  288. package/src/library/index.ts +6 -1
  289. package/src/library/useCanSetPublicVisibility.ts +56 -0
  290. package/src/library/visibilityLevels.ts +65 -14
  291. package/src/mcp-server/McpServerDetailView.tsx +2 -2
  292. package/src/mcp-server/__tests__/useMcpServerConnect.asyncLane.test.tsx +117 -0
  293. package/src/mcp-server/index.ts +0 -2
  294. package/src/mcp-server/internal/definitionDrift.ts +22 -20
  295. package/src/mcp-server/useMcpServerConnect.ts +9 -6
  296. package/src/mcp-server/useMcpServerOAuthConnect.ts +7 -2
  297. package/src/mcp-server/useUpdateMcpServer.ts +3 -3
  298. package/src/models/ModelSelector.tsx +94 -1
  299. package/src/models/index.ts +2 -0
  300. package/src/models/registry.ts +29 -0
  301. package/src/models/thinking-mode.ts +48 -0
  302. package/src/oauth-app/CreateOAuthAppForm.tsx +58 -3
  303. package/src/oauth-app/OAuthAppDetailPanel.tsx +103 -20
  304. package/src/oauth-app/__tests__/OAuthAppDetailPanel.test.tsx +94 -0
  305. package/src/organization/OrgPreferencesPanel.tsx +221 -0
  306. package/src/organization/OrgProfilePanel.tsx +5 -4
  307. package/src/organization/__tests__/OrgPreferencesPanel.test.tsx +145 -0
  308. package/src/organization/__tests__/OrgProfilePanel.test.tsx +108 -0
  309. package/src/organization/index.ts +2 -0
  310. package/src/platform-client/PlatformClientDetailPanel.tsx +14 -13
  311. package/src/platform-client/__tests__/PlatformClientDetailPanel.test.tsx +82 -0
  312. package/src/schedule/ScheduleDetailView.tsx +29 -4
  313. package/src/schedule/ScheduleForm.tsx +14 -1
  314. package/src/session/NewSessionViewer.tsx +12 -0
  315. package/src/session/__tests__/useNewSessionFlow.test.tsx +151 -0
  316. package/src/session/run-config.ts +16 -0
  317. package/src/session/useNewSessionFlow.ts +82 -9
  318. package/src/session/useSessionConversation.ts +32 -40
  319. package/src/session/useSessionPageFlow.ts +14 -11
  320. package/src/session/useUpdateSession.ts +18 -24
  321. package/src/settings/AccountPreferencesSection.tsx +25 -0
  322. package/src/settings/OrgPreferencesSection.tsx +35 -0
  323. package/src/settings/__tests__/settings-nav.test.ts +27 -0
  324. package/src/settings/index.ts +2 -0
  325. package/src/settings/settings-nav.ts +18 -0
  326. package/src/workflow/ApprovalFormBuilder.tsx +20 -27
  327. package/src/workflow/WorkflowDetailView.tsx +2 -2
  328. package/src/workflow/__tests__/WorkflowInstanceDetailPanel.test.tsx +83 -0
  329. package/src/workflow/index.ts +0 -2
  330. package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +6 -7
  331. package/src/workflow/useUpdateWorkflow.ts +3 -3
  332. package/styles.css +1 -1
  333. package/workflow/ApprovalFormBuilder.d.ts.map +1 -1
  334. package/workflow/ApprovalFormBuilder.js +10 -5
  335. package/workflow/ApprovalFormBuilder.js.map +1 -1
  336. package/workflow/WorkflowDetailView.js +2 -2
  337. package/workflow/WorkflowDetailView.js.map +1 -1
  338. package/workflow/index.d.ts +0 -1
  339. package/workflow/index.d.ts.map +1 -1
  340. package/workflow/index.js +0 -1
  341. package/workflow/index.js.map +1 -1
  342. package/workflow/instance/WorkflowInstanceDetailPanel.d.ts.map +1 -1
  343. package/workflow/instance/WorkflowInstanceDetailPanel.js +6 -7
  344. package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
  345. package/workflow/useUpdateWorkflow.d.ts +3 -3
  346. package/workflow/useUpdateWorkflow.js +3 -3
  347. package/agent/internal/agentToInput.d.ts +0 -15
  348. package/agent/internal/agentToInput.d.ts.map +0 -1
  349. package/agent/internal/agentToInput.js +0 -84
  350. package/agent/internal/agentToInput.js.map +0 -1
  351. package/mcp-server/internal/mcpServerToInput.d.ts +0 -12
  352. package/mcp-server/internal/mcpServerToInput.d.ts.map +0 -1
  353. package/mcp-server/internal/mcpServerToInput.js +0 -84
  354. package/mcp-server/internal/mcpServerToInput.js.map +0 -1
  355. package/session/session-spec-converters.d.ts +0 -24
  356. package/session/session-spec-converters.d.ts.map +0 -1
  357. package/session/session-spec-converters.js +0 -72
  358. package/session/session-spec-converters.js.map +0 -1
  359. package/src/agent/internal/__tests__/agentToInput.test.ts +0 -145
  360. package/src/agent/internal/agentToInput.ts +0 -102
  361. package/src/mcp-server/__tests__/mcpServerToInput.test.ts +0 -201
  362. package/src/mcp-server/internal/mcpServerToInput.ts +0 -98
  363. package/src/session/__tests__/session-spec-converters.test.ts +0 -183
  364. package/src/session/session-spec-converters.ts +0 -86
  365. package/src/workflow/internal/workflowToInput.ts +0 -96
  366. package/workflow/internal/workflowToInput.d.ts +0 -12
  367. package/workflow/internal/workflowToInput.d.ts.map +0 -1
  368. package/workflow/internal/workflowToInput.js +0 -73
  369. package/workflow/internal/workflowToInput.js.map +0 -1
@@ -0,0 +1,224 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { cleanup, render, screen, waitFor } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import type { ReactNode } from "react";
5
+ import { create } from "@bufbuild/protobuf";
6
+ import {
7
+ ChannelSendOutcome,
8
+ SendChannelMessageOutputSchema,
9
+ } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
10
+ import { StigmerContext } from "../../context";
11
+ import { FetchCacheContext } from "../../internal/FetchCacheProvider";
12
+ import { ConversationTemplatePickerDialog } from "../ConversationTemplatePickerDialog";
13
+
14
+ function makeTemplate(overrides?: Partial<Record<string, unknown>>) {
15
+ return {
16
+ name: "fee_reminder",
17
+ language: "en_US",
18
+ category: "UTILITY",
19
+ status: "APPROVED",
20
+ parameterFormat: "POSITIONAL",
21
+ parameterNames: ["1", "2"],
22
+ bodyText: "Hi {{1}}, your fee of {{2}} is due.",
23
+ headerFormat: "",
24
+ rejectionReason: "",
25
+ unsupportedReason: "",
26
+ ...overrides,
27
+ };
28
+ }
29
+
30
+ function output(outcome: ChannelSendOutcome, detail = "") {
31
+ return create(SendChannelMessageOutputSchema, {
32
+ outcome,
33
+ outboundMessageId: outcome === ChannelSendOutcome.refused ? "" : "obm_1",
34
+ detail,
35
+ });
36
+ }
37
+
38
+ function renderPicker(options?: {
39
+ templates?: unknown[];
40
+ onSend?: ReturnType<typeof vi.fn>;
41
+ onOpenChange?: ReturnType<typeof vi.fn>;
42
+ isSending?: boolean;
43
+ }) {
44
+ const listTemplates = vi
45
+ .fn()
46
+ .mockResolvedValue({ entries: options?.templates ?? [makeTemplate()] });
47
+ const onSend =
48
+ options?.onSend ?? vi.fn().mockResolvedValue(output(ChannelSendOutcome.accepted));
49
+ const client = { agentChannel: { listTemplates } } as never;
50
+ function Wrapper({ children }: { children: ReactNode }) {
51
+ return (
52
+ <FetchCacheContext.Provider value={null}>
53
+ <StigmerContext.Provider value={client}>{children}</StigmerContext.Provider>
54
+ </FetchCacheContext.Provider>
55
+ );
56
+ }
57
+ const view = render(
58
+ <ConversationTemplatePickerDialog
59
+ open
60
+ onOpenChange={options?.onOpenChange ?? vi.fn()}
61
+ channelSlug="wa-main"
62
+ org="acme"
63
+ onSend={onSend}
64
+ isSending={options?.isSending ?? false}
65
+ modal={false}
66
+ />,
67
+ { wrapper: Wrapper },
68
+ );
69
+ return { view, listTemplates, onSend };
70
+ }
71
+
72
+ describe("ConversationTemplatePickerDialog", () => {
73
+ afterEach(() => cleanup());
74
+
75
+ it("lists approved templates and asks the wire for approved only", async () => {
76
+ const { listTemplates } = renderPicker();
77
+
78
+ await waitFor(() => expect(screen.getByText("fee_reminder")).toBeDefined());
79
+ expect(listTemplates).toHaveBeenCalledWith(
80
+ expect.objectContaining({ channel: "wa-main", org: "acme", approvedOnly: true }),
81
+ );
82
+ });
83
+
84
+ it("marks unsupported and image-header templates unsendable, with the reason", async () => {
85
+ renderPicker({
86
+ templates: [
87
+ makeTemplate({
88
+ name: "flow_template",
89
+ unsupportedReason: "FLOW buttons are not supported",
90
+ }),
91
+ makeTemplate({ name: "hero_image", headerFormat: "IMAGE" }),
92
+ makeTemplate(),
93
+ ],
94
+ });
95
+
96
+ await waitFor(() => expect(screen.getByText("fee_reminder")).toBeDefined());
97
+ const unsupported = screen
98
+ .getByText("flow_template")
99
+ .closest("button") as HTMLButtonElement;
100
+ expect(unsupported.disabled).toBe(true);
101
+ expect(screen.getByText(/FLOW buttons are not supported/)).toBeDefined();
102
+
103
+ // The v1 scope cut (cloud#260): the send payload's image link has no
104
+ // console-side source yet, so the template shows but cannot be picked.
105
+ const imageHeader = screen
106
+ .getByText("hero_image")
107
+ .closest("button") as HTMLButtonElement;
108
+ expect(imageHeader.disabled).toBe(true);
109
+ expect(screen.getByText(/image header needs a hosted image/)).toBeDefined();
110
+
111
+ const sendable = screen
112
+ .getByText("fee_reminder")
113
+ .closest("button") as HTMLButtonElement;
114
+ expect(sendable.disabled).toBe(false);
115
+ });
116
+
117
+ it("phrases an empty registry answer as none FOUND, never none exist", async () => {
118
+ renderPicker({ templates: [] });
119
+ await waitFor(() =>
120
+ expect(screen.getByText("No approved templates found")).toBeDefined(),
121
+ );
122
+ });
123
+
124
+ it("fills variables with a live preview and only then enables Send", async () => {
125
+ const user = userEvent.setup();
126
+ renderPicker();
127
+
128
+ await waitFor(() => expect(screen.getByText("fee_reminder")).toBeDefined());
129
+ await user.click(screen.getByText("fee_reminder"));
130
+
131
+ const send = screen.getByRole("button", { name: "Send template" }) as HTMLButtonElement;
132
+ expect(send.disabled).toBe(true);
133
+
134
+ await user.type(screen.getByLabelText("1"), "Noor");
135
+ await user.type(screen.getByLabelText("2"), "$40");
136
+
137
+ // The preview substitutes the filled values into the verbatim body —
138
+ // the exact message the customer receives. Segments render as
139
+ // separate spans, so assert on the whole paragraph.
140
+ const preview = screen.getByText("Preview").nextElementSibling;
141
+ expect(preview?.textContent).toBe("Hi Noor, your fee of $40 is due.");
142
+ expect(send.disabled).toBe(false);
143
+ });
144
+
145
+ it("sends the chosen entry's name, language, and trimmed parameters, and closes on accepted", async () => {
146
+ const user = userEvent.setup();
147
+ const onOpenChange = vi.fn();
148
+ const { onSend } = renderPicker({ onOpenChange });
149
+
150
+ await waitFor(() => expect(screen.getByText("fee_reminder")).toBeDefined());
151
+ await user.click(screen.getByText("fee_reminder"));
152
+ await user.type(screen.getByLabelText("1"), " Noor ");
153
+ await user.type(screen.getByLabelText("2"), "$40");
154
+ await user.click(screen.getByRole("button", { name: "Send template" }));
155
+
156
+ await waitFor(() =>
157
+ expect(onSend).toHaveBeenCalledWith({
158
+ kind: "template",
159
+ name: "fee_reminder",
160
+ language: "en_US",
161
+ parameters: { "1": "Noor", "2": "$40" },
162
+ }),
163
+ );
164
+ await waitFor(() => expect(onOpenChange).toHaveBeenCalledWith(false));
165
+ });
166
+
167
+ it("keeps the dialog open with every value intact and shows the detail verbatim on refusal", async () => {
168
+ const user = userEvent.setup();
169
+ const onOpenChange = vi.fn();
170
+ const onSend = vi
171
+ .fn()
172
+ .mockResolvedValue(
173
+ output(ChannelSendOutcome.refused, "template 'fee_reminder' is PAUSED on WhatsApp"),
174
+ );
175
+ renderPicker({ onSend, onOpenChange });
176
+
177
+ await waitFor(() => expect(screen.getByText("fee_reminder")).toBeDefined());
178
+ await user.click(screen.getByText("fee_reminder"));
179
+ await user.type(screen.getByLabelText("1"), "Noor");
180
+ await user.type(screen.getByLabelText("2"), "$40");
181
+ await user.click(screen.getByRole("button", { name: "Send template" }));
182
+
183
+ await waitFor(() =>
184
+ expect(
185
+ screen.getByText("template 'fee_reminder' is PAUSED on WhatsApp"),
186
+ ).toBeDefined(),
187
+ );
188
+ // The words never left; the correction the detail asks for starts
189
+ // from the values already filled (the composer's draft contract).
190
+ expect(onOpenChange).not.toHaveBeenCalledWith(false);
191
+ expect((screen.getByLabelText("1") as HTMLInputElement).value).toBe("Noor");
192
+ expect((screen.getByLabelText("2") as HTMLInputElement).value).toBe("$40");
193
+ });
194
+
195
+ it("keeps the dialog open and renders a thrown failure", async () => {
196
+ const user = userEvent.setup();
197
+ const onSend = vi.fn().mockRejectedValue(new Error("transport exploded"));
198
+ renderPicker({ onSend });
199
+
200
+ await waitFor(() => expect(screen.getByText("fee_reminder")).toBeDefined());
201
+ await user.click(screen.getByText("fee_reminder"));
202
+ await user.type(screen.getByLabelText("1"), "Noor");
203
+ await user.type(screen.getByLabelText("2"), "$40");
204
+ await user.click(screen.getByRole("button", { name: "Send template" }));
205
+
206
+ await waitFor(() => expect(screen.getByText(/transport exploded/)).toBeDefined());
207
+ expect((screen.getByLabelText("1") as HTMLInputElement).value).toBe("Noor");
208
+ });
209
+
210
+ it("navigates back to the list, discarding the selection", async () => {
211
+ const user = userEvent.setup();
212
+ renderPicker({
213
+ templates: [makeTemplate(), makeTemplate({ name: "order_update" })],
214
+ });
215
+
216
+ await waitFor(() => expect(screen.getByText("fee_reminder")).toBeDefined());
217
+ await user.click(screen.getByText("fee_reminder"));
218
+ expect(screen.getByRole("button", { name: "Send template" })).toBeDefined();
219
+
220
+ await user.click(screen.getByRole("button", { name: "Back to the template list" }));
221
+ expect(screen.getByText("order_update")).toBeDefined();
222
+ expect(screen.queryByRole("button", { name: "Send template" })).toBeNull();
223
+ });
224
+ });
@@ -100,7 +100,9 @@ describe("useConversationParticipation", () => {
100
100
  { wrapper: wrapper(client) },
101
101
  );
102
102
 
103
- const output = await act(() => result.current.reply("on my way"));
103
+ const output = await act(() =>
104
+ result.current.reply({ kind: "text", body: "on my way" }),
105
+ );
104
106
 
105
107
  expect(output.outboundMessageId).toBe("obm_1");
106
108
  const agentChannel = (client as { agentChannel: Record<string, ReturnType<typeof vi.fn>> })
@@ -109,7 +111,10 @@ describe("useConversationParticipation", () => {
109
111
  expect.objectContaining({
110
112
  ...IDENTITY,
111
113
  payload: expect.objectContaining({
112
- kind: expect.objectContaining({ case: "text" }),
114
+ kind: expect.objectContaining({
115
+ case: "text",
116
+ value: expect.objectContaining({ body: "on my way" }),
117
+ }),
113
118
  }),
114
119
  }),
115
120
  );
@@ -121,6 +126,40 @@ describe("useConversationParticipation", () => {
121
126
  );
122
127
  });
123
128
 
129
+ it("reply sends the template lane through the same command — name, language, and parameters verbatim", async () => {
130
+ const client = createMockStigmer();
131
+ const { result } = renderHook(() => useConversationParticipation(IDENTITY), {
132
+ wrapper: wrapper(client),
133
+ });
134
+
135
+ await act(() =>
136
+ result.current.reply({
137
+ kind: "template",
138
+ name: "order_update",
139
+ language: "en_US",
140
+ parameters: { "1": "Noor", "2": "tomorrow" },
141
+ }),
142
+ );
143
+
144
+ const agentChannel = (client as { agentChannel: Record<string, ReturnType<typeof vi.fn>> })
145
+ .agentChannel;
146
+ expect(agentChannel.reply).toHaveBeenCalledWith(
147
+ expect.objectContaining({
148
+ ...IDENTITY,
149
+ payload: expect.objectContaining({
150
+ kind: expect.objectContaining({
151
+ case: "template",
152
+ value: expect.objectContaining({
153
+ name: "order_update",
154
+ language: "en_US",
155
+ parameters: { "1": "Noor", "2": "tomorrow" },
156
+ }),
157
+ }),
158
+ }),
159
+ }),
160
+ );
161
+ });
162
+
124
163
  it("still resolves the reply when the follow-up row read fails — the send outcome is the answer", async () => {
125
164
  const client = createMockStigmer({
126
165
  getConversation: vi.fn().mockRejectedValue(new Error("blip")),
@@ -131,7 +170,9 @@ describe("useConversationParticipation", () => {
131
170
  { wrapper: wrapper(client) },
132
171
  );
133
172
 
134
- const output = await act(() => result.current.reply("on my way"));
173
+ const output = await act(() =>
174
+ result.current.reply({ kind: "text", body: "on my way" }),
175
+ );
135
176
 
136
177
  expect(output.outboundMessageId).toBe("obm_1");
137
178
  expect(onConversation).not.toHaveBeenCalled();
@@ -144,7 +185,7 @@ describe("useConversationParticipation", () => {
144
185
  wrapper: wrapper(client),
145
186
  });
146
187
 
147
- await act(() => result.current.reply("on my way"));
188
+ await act(() => result.current.reply({ kind: "text", body: "on my way" }));
148
189
 
149
190
  const agentChannel = (client as { agentChannel: Record<string, ReturnType<typeof vi.fn>> })
150
191
  .agentChannel;
@@ -22,6 +22,9 @@ export type { ConversationAttentionBannerProps } from "./ConversationAttentionBa
22
22
  export { ConversationComposer } from "./ConversationComposer.js";
23
23
  export type { ConversationComposerProps } from "./ConversationComposer.js";
24
24
 
25
+ export { ConversationTemplatePickerDialog } from "./ConversationTemplatePickerDialog.js";
26
+ export type { ConversationTemplatePickerDialogProps } from "./ConversationTemplatePickerDialog.js";
27
+
25
28
  export { useConversation } from "./useConversation.js";
26
29
  export type {
27
30
  UseConversationOptions,
@@ -49,6 +52,7 @@ export type {
49
52
  export { useConversationParticipation } from "./useConversationParticipation.js";
50
53
  export type {
51
54
  ConversationCommand,
55
+ ConversationReplyPayload,
52
56
  UseConversationParticipationOptions,
53
57
  UseConversationParticipationReturn,
54
58
  } from "./useConversationParticipation.js";
@@ -17,6 +17,43 @@ import { useKeyedSubmission } from "../internal/useKeyedSubmission.js";
17
17
  /** The four staff-facing participation commands. */
18
18
  export type ConversationCommand = "reply" | "takeOver" | "handBack" | "clearAttention";
19
19
 
20
+ /**
21
+ * A staff reply's payload — the client-side face of the wire's
22
+ * `ChannelOutboundPayload` oneof. Reply is ONE command with two lanes,
23
+ * never two commands: both lanes share the implicit takeover, the
24
+ * `"reply"` pending key, and the outcome contract.
25
+ *
26
+ * - `text` — free-form; requires an open 24-hour customer-service
27
+ * window on providers that enforce one (WhatsApp).
28
+ * - `template` — a provider-approved message template, the only lane
29
+ * for a closed window. `language` is always required here (the wire
30
+ * allows omitting it for single-language names, but a caller that
31
+ * picked a template knows its language — sending it structurally
32
+ * avoids the ambiguity refusal). Image-header templates are not yet
33
+ * sendable from the SDK: the wire's `header_image_link` needs a
34
+ * public HTTPS asset no console surface can supply today.
35
+ */
36
+ export type ConversationReplyPayload =
37
+ | {
38
+ readonly kind: "text";
39
+ /** The message body, as the customer will receive it. */
40
+ readonly body: string;
41
+ }
42
+ | {
43
+ readonly kind: "template";
44
+ /** Template name on the channel provider's registry. */
45
+ readonly name: string;
46
+ /** Template language code (e.g. `"en"`, `"en_US"`). */
47
+ readonly language: string;
48
+ /**
49
+ * Variable values: keyed by parameter name for NAMED templates,
50
+ * by position (`"1"`, `"2"`, …) for POSITIONAL ones — exactly the
51
+ * template's declared parameters, no more, no fewer (the server's
52
+ * pre-check refuses mismatches with a corrective detail).
53
+ */
54
+ readonly parameters: Readonly<Record<string, string>>;
55
+ };
56
+
20
57
  /** Options for {@link useConversationParticipation}. */
21
58
  export interface UseConversationParticipationOptions {
22
59
  /** AgentChannel the conversation belongs to. */
@@ -37,13 +74,16 @@ export interface UseConversationParticipationOptions {
37
74
  /** Return value of {@link useConversationParticipation}. */
38
75
  export interface UseConversationParticipationReturn {
39
76
  /**
40
- * Send a staff text reply. Resolves with the truthful outcome of the
41
- * inline attempt: `accepted` (with `outboundMessageId` — the item will
42
- * appear on the timeline as `ob:<that id>`), `queued`, or `refused`.
77
+ * Send a staff reply text or template (see
78
+ * {@link ConversationReplyPayload}). Resolves with the truthful
79
+ * outcome of the inline attempt: `accepted` (with
80
+ * `outboundMessageId` — the item will appear on the timeline as
81
+ * `ob:<that id>`), `queued`, or `refused` (template pre-check
82
+ * refusals land here too, with the server's corrective detail).
43
83
  * A `refused` outcome WITHOUT an `outboundMessageId` left no ledger
44
84
  * row and no timeline item will ever exist for it.
45
85
  */
46
- readonly reply: (text: string) => Promise<SendChannelMessageOutput>;
86
+ readonly reply: (payload: ConversationReplyPayload) => Promise<SendChannelMessageOutput>;
47
87
  /**
48
88
  * Take the conversation over: the agent goes quiet until handBack.
49
89
  * Resolves with the fresh row — for the LOSER of a concurrent
@@ -120,13 +160,25 @@ export function useConversationParticipation(
120
160
  );
121
161
 
122
162
  const reply = useCallback(
123
- (text: string) =>
163
+ (payload: ConversationReplyPayload) =>
124
164
  run("reply", async () => {
125
165
  const output = await stigmer.agentChannel.reply(
126
166
  create(ReplyToConversationInputSchema, {
127
167
  agentChannelId,
128
168
  conversationKey,
129
- payload: { kind: { case: "text", value: { body: text } } },
169
+ payload: {
170
+ kind:
171
+ payload.kind === "text"
172
+ ? { case: "text", value: { body: payload.body } }
173
+ : {
174
+ case: "template",
175
+ value: {
176
+ name: payload.name,
177
+ language: payload.language,
178
+ parameters: { ...payload.parameters },
179
+ },
180
+ },
181
+ },
130
182
  }),
131
183
  );
132
184
  // The implicit takeover flipped control BEFORE the send ran —
@@ -10,7 +10,7 @@ import { Button } from "../button/index.js";
10
10
  import { StateBadge } from "./badges.js";
11
11
  import { CursorAccountEditor } from "./CursorAccountEditor.js";
12
12
  import { CursorAccountsAccessNotice } from "./CursorAccountsAccessNotice.js";
13
- import { deriveCoverage } from "./cursor-account-coverage.js";
13
+ import { deriveCoverage, derivePoolHealth } from "./cursor-account-coverage.js";
14
14
  import { formatSyncTime } from "./cursor-account-format.js";
15
15
  import { MemberKeysPanel } from "./MemberKeysPanel.js";
16
16
  import { useCursorAccounts } from "./useCursorAccounts.js";
@@ -119,11 +119,38 @@ export function CursorAccountsConsole({ className }: CursorAccountsConsoleProps)
119
119
  }
120
120
 
121
121
  const accounts = list.accounts?.accounts ?? [];
122
+ const health = derivePoolHealth(accounts);
122
123
 
123
124
  return (
124
125
  <div className={cn("stg:space-y-3", className)}>
125
126
  <div className="stg:flex stg:items-center stg:justify-between stg:gap-3">
126
- <h3 className="stg:text-sm stg:font-semibold stg:text-foreground">Cursor accounts</h3>
127
+ <div>
128
+ <h3 className="stg:text-sm stg:font-semibold stg:text-foreground">Cursor accounts</h3>
129
+ {/* Fleet health in one line (server-computed routability): the
130
+ 2026-08-15 drain hid behind per-account enabled counts — this
131
+ is the fact the operator needed on THIS screen. */}
132
+ {accounts.length > 0 && (
133
+ <p
134
+ className={cn(
135
+ "stg:text-[11px]",
136
+ health.totalRoutableKeys === 0
137
+ ? "stg:text-destructive"
138
+ : "stg:text-muted-foreground",
139
+ )}
140
+ role={health.totalRoutableKeys === 0 ? "alert" : undefined}
141
+ >
142
+ {health.totalRoutableKeys === 0
143
+ ? "No routable execution key anywhere - executions on API-pool models are failing"
144
+ : `${health.totalRoutableKeys} routable ${health.totalRoutableKeys === 1 ? "key" : "keys"} across ${health.totalAccounts} ${health.totalAccounts === 1 ? "account" : "accounts"}`}
145
+ {health.drainedAccounts > 0
146
+ ? ` · ${health.drainedAccounts} ${health.drainedAccounts === 1 ? "account" : "accounts"} drained`
147
+ : ""}
148
+ {health.guardTrippedKeys > 0
149
+ ? ` · ${health.guardTrippedKeys} ${health.guardTrippedKeys === 1 ? "key" : "keys"} usage-drained`
150
+ : ""}
151
+ </p>
152
+ )}
153
+ </div>
127
154
  <Button size="sm" onClick={() => setFlow({ phase: "create" })}>
128
155
  Add account
129
156
  </Button>
@@ -137,7 +164,7 @@ export function CursorAccountsConsole({ className }: CursorAccountsConsoleProps)
137
164
  <span>Account</span>
138
165
  <span>Orgs</span>
139
166
  <span>Status</span>
140
- <span>Execution keys</span>
167
+ <span>Routable keys</span>
141
168
  <span className="stg:text-right">Last synced</span>
142
169
  </div>
143
170
  {accounts.length === 0 ? (
@@ -214,8 +241,16 @@ function AccountRow({
214
241
  />
215
242
  </span>
216
243
  <span>
217
- {summary.enabledKeyCount > 0 ? (
218
- <span className="stg:text-muted-foreground">{summary.enabledKeyCount}</span>
244
+ {/* Routability, not enabled-ness: what key selection would DO.
245
+ "Drained" (usage guard) is recoverable at the cycle reset;
246
+ "Not routable" (no enabled key, or owners removed) needs
247
+ operator action. */}
248
+ {summary.routableKeyCount > 0 ? (
249
+ <span className="stg:text-muted-foreground">
250
+ {summary.routableKeyCount} of {summary.enabledKeyCount}
251
+ </span>
252
+ ) : summary.guardTrippedKeyCount > 0 ? (
253
+ <StateBadge tone="warn" label="Drained" />
219
254
  ) : (
220
255
  <StateBadge tone="warn" label="Not routable" />
221
256
  )}
@@ -86,6 +86,7 @@ function scenarSummary() {
86
86
  return create(CursorAccountSummarySchema, {
87
87
  account: scenarAccount(),
88
88
  enabledKeyCount: 1,
89
+ routableKeyCount: 1,
89
90
  lastSyncedAt: timestampFromDate(new Date("2026-07-22T12:00:00Z")),
90
91
  });
91
92
  }
@@ -194,6 +195,37 @@ describe("CursorAccountsConsole", () => {
194
195
  expect(screen.getByText("empty team")).toBeTruthy();
195
196
  expect(screen.getByText("Not routable")).toBeTruthy();
196
197
  expect(screen.getByText("shared pool")).toBeTruthy();
198
+ // Routable count renders as "N of M" (server-computed routability),
199
+ // and the fleet-health line aggregates it.
200
+ expect(screen.getByText("1 of 1")).toBeTruthy();
201
+ expect(screen.getByText(/1 routable key across 2 accounts/)).toBeTruthy();
202
+ });
203
+
204
+ it("surfaces a fully guard-drained account and the fleet drain alert", async () => {
205
+ // The 2026-08-15 shape: enabled keys exist, none routable — the one
206
+ // key left is usage-guard-tripped. The row must read "Drained" (a
207
+ // cycle-reset condition), not the misleading enabled count, and with
208
+ // zero routable keys fleet-wide the health line must alarm.
209
+ const drained = create(CursorAccountSummarySchema, {
210
+ account: scenarAccount({
211
+ accountId: "acc-3",
212
+ displayName: "drained team",
213
+ orgIds: [],
214
+ }),
215
+ enabledKeyCount: 1,
216
+ routableKeyCount: 0,
217
+ guardTrippedKeyCount: 1,
218
+ });
219
+ const client = createMockStigmer({
220
+ listAccounts: vi.fn().mockResolvedValue({ accounts: [drained] }),
221
+ });
222
+ render(<CursorAccountsConsole />, { wrapper: wrapper(client) });
223
+
224
+ await waitFor(() => expect(screen.getByText("drained team")).toBeTruthy());
225
+ expect(screen.getByText("Drained")).toBeTruthy();
226
+ expect(
227
+ screen.getByText(/No routable execution key anywhere/),
228
+ ).toBeTruthy();
197
229
  });
198
230
 
199
231
  it("shows the designed access notice on PERMISSION_DENIED", async () => {
@@ -7,12 +7,14 @@ import {
7
7
  CursorTeamMemberSchema,
8
8
  } from "@stigmer/protos/ai/stigmer/platform/cursoraccount/v1/cursor_account_pb";
9
9
  import {
10
+ CursorAccountSummarySchema,
10
11
  CursorAccountViewSchema,
11
12
  CursorMemberKeyState,
12
13
  CursorMemberKeyViewSchema,
13
14
  CursorTeamMemberViewSchema,
14
15
  } from "@stigmer/protos/ai/stigmer/platform/cursoraccount/v1/io_pb";
15
- import { deriveCoverage } from "../cursor-account-coverage";
16
+ import { CursorAccountSchema } from "@stigmer/protos/ai/stigmer/platform/cursoraccount/v1/cursor_account_pb";
17
+ import { deriveCoverage, derivePoolHealth } from "../cursor-account-coverage";
16
18
 
17
19
  function keyView(keyId: string, state: CursorMemberKeyState) {
18
20
  return create(CursorMemberKeyViewSchema, {
@@ -71,3 +73,58 @@ describe("deriveCoverage", () => {
71
73
  expect(coverage.onTeamWithKey).toEqual([]);
72
74
  });
73
75
  });
76
+
77
+ describe("derivePoolHealth", () => {
78
+ function summary(opts: {
79
+ enabled?: boolean;
80
+ enabledKeyCount: number;
81
+ routableKeyCount: number;
82
+ guardTrippedKeyCount?: number;
83
+ }) {
84
+ return create(CursorAccountSummarySchema, {
85
+ account: create(CursorAccountSchema, {
86
+ accountId: "acc",
87
+ enabled: opts.enabled ?? true,
88
+ }),
89
+ enabledKeyCount: opts.enabledKeyCount,
90
+ routableKeyCount: opts.routableKeyCount,
91
+ guardTrippedKeyCount: opts.guardTrippedKeyCount ?? 0,
92
+ });
93
+ }
94
+
95
+ it("reproduces the 2026-08-15 drain as a one-line fact", () => {
96
+ // Four accounts with enabled keys but zero routable (guard-tripped or
97
+ // owner-removed), one fresh account with a single routable key — the
98
+ // fleet state the per-account enabled counts hid.
99
+ const health = derivePoolHealth([
100
+ summary({ enabledKeyCount: 1, routableKeyCount: 0, guardTrippedKeyCount: 1 }),
101
+ summary({ enabledKeyCount: 10, routableKeyCount: 0 }),
102
+ summary({ enabledKeyCount: 1, routableKeyCount: 0 }),
103
+ summary({ enabledKeyCount: 0, routableKeyCount: 0 }),
104
+ summary({ enabledKeyCount: 2, routableKeyCount: 1, guardTrippedKeyCount: 0 }),
105
+ ]);
106
+
107
+ expect(health.totalAccounts).toBe(5);
108
+ expect(health.totalRoutableKeys).toBe(1);
109
+ // The zero-key account is not "drained" — it never had capacity.
110
+ expect(health.drainedAccounts).toBe(3);
111
+ expect(health.guardTrippedKeys).toBe(1);
112
+ });
113
+
114
+ it("does not count disabled accounts as drained (disable means drain-by-choice)", () => {
115
+ const health = derivePoolHealth([
116
+ summary({ enabled: false, enabledKeyCount: 3, routableKeyCount: 0 }),
117
+ ]);
118
+
119
+ expect(health.drainedAccounts).toBe(0);
120
+ });
121
+
122
+ it("is all zeros for an empty list", () => {
123
+ const health = derivePoolHealth([]);
124
+
125
+ expect(health.totalAccounts).toBe(0);
126
+ expect(health.totalRoutableKeys).toBe(0);
127
+ expect(health.drainedAccounts).toBe(0);
128
+ expect(health.guardTrippedKeys).toBe(0);
129
+ });
130
+ });
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  CursorMemberKeyState,
3
+ type CursorAccountSummary,
3
4
  type CursorAccountView,
4
5
  type CursorMemberKeyView,
5
6
  type CursorTeamMemberView,
@@ -69,3 +70,57 @@ export function deriveCoverage(view: CursorAccountView): CursorAccountCoverage {
69
70
  unclassified: [],
70
71
  };
71
72
  }
73
+
74
+ /**
75
+ * Fleet-wide health rolled up from the list summaries — the console's
76
+ * answer to "can the platform serve right now, and from where?". Pure
77
+ * aggregation of the server-computed routable/guard counts (the same
78
+ * routability rule key selection uses); this module must never
79
+ * re-derive routability client-side.
80
+ *
81
+ * <p>Why this exists: in the 2026-08-15 pool drain, the list showed
82
+ * five accounts with healthy-looking enabled-key counts while exactly
83
+ * one guard-tripped key could serve — the operator had to open every
84
+ * account detail to see it. The rollup makes fleet exhaustion a
85
+ * one-line fact.
86
+ */
87
+ export interface CursorPoolHealth {
88
+ /** Total accounts in the list. */
89
+ readonly totalAccounts: number;
90
+ /** Keys the routability rule would select for a new session, fleet-wide. */
91
+ readonly totalRoutableKeys: number;
92
+ /**
93
+ * Enabled accounts with enabled keys but zero routable ones — every
94
+ * key is dead (owner removed) or usage-drained. These are the rows
95
+ * that look healthy at a glance and are not.
96
+ */
97
+ readonly drainedAccounts: number;
98
+ /** Enabled keys excluded only by the usage guard, fleet-wide. */
99
+ readonly guardTrippedKeys: number;
100
+ }
101
+
102
+ /** Aggregate the server-computed summary counts across the account list. */
103
+ export function derivePoolHealth(
104
+ summaries: readonly CursorAccountSummary[],
105
+ ): CursorPoolHealth {
106
+ let totalRoutableKeys = 0;
107
+ let drainedAccounts = 0;
108
+ let guardTrippedKeys = 0;
109
+ for (const summary of summaries) {
110
+ totalRoutableKeys += summary.routableKeyCount;
111
+ guardTrippedKeys += summary.guardTrippedKeyCount;
112
+ if (
113
+ summary.account?.enabled === true
114
+ && summary.enabledKeyCount > 0
115
+ && summary.routableKeyCount === 0
116
+ ) {
117
+ drainedAccounts += 1;
118
+ }
119
+ }
120
+ return {
121
+ totalAccounts: summaries.length,
122
+ totalRoutableKeys,
123
+ drainedAccounts,
124
+ guardTrippedKeys,
125
+ };
126
+ }