@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,441 @@
1
+ "use client";
2
+
3
+ import { useId, useMemo, useState } from "react";
4
+ import { ArrowLeft, LayoutTemplate, Send, X } from "lucide-react";
5
+ import { cn } from "@stigmer/theme";
6
+ import { getUserMessage } from "@stigmer/sdk";
7
+ import { ChannelSendOutcome } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
8
+ import type {
9
+ ChannelTemplate,
10
+ SendChannelMessageOutput,
11
+ } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
12
+ import { Button } from "../button/Button.js";
13
+ import { EmptyState } from "../empty-state/EmptyState.js";
14
+ import { DialogShell } from "../internal/DialogShell.js";
15
+ import { SpinnerIcon } from "../internal/SpinnerIcon.js";
16
+ import { UNSTYLED_LIST } from "../internal/element-resets.js";
17
+ import { TruncatedText } from "../internal/truncated-text.js";
18
+ import { splitTemplateBody } from "../channel/templatePresentation.js";
19
+ import { useChannelTemplateList } from "../channel/useChannelTemplateList.js";
20
+ import type { ConversationReplyPayload } from "./useConversationParticipation.js";
21
+
22
+ /** Props for {@link ConversationTemplatePickerDialog}. */
23
+ export interface ConversationTemplatePickerDialogProps {
24
+ /** Whether the dialog is open. */
25
+ readonly open: boolean;
26
+ /** Called when the dialog requests an open-state change. */
27
+ readonly onOpenChange: (open: boolean) => void;
28
+ /**
29
+ * The conversation's channel, addressed as the template registry
30
+ * addresses it: `metadata.slug` + `metadata.org` (the registry is
31
+ * derived from the channel's phone number, not stored on a resource).
32
+ */
33
+ readonly channelSlug: string;
34
+ readonly org: string;
35
+ /**
36
+ * Send the chosen template as a staff reply — the SAME seam the
37
+ * composer's textarea uses (`useConversationParticipation().reply`
38
+ * or the host's wrapper around it), so a template send takes over,
39
+ * settles, and reports outcomes exactly like a text send.
40
+ */
41
+ readonly onSend: (payload: ConversationReplyPayload) => Promise<SendChannelMessageOutput>;
42
+ /** `true` while a reply is in flight. */
43
+ readonly isSending: boolean;
44
+ /**
45
+ * When `false`, renders as an in-flow open dialog instead of a
46
+ * top-layer modal — for embedding in constrained surfaces
47
+ * (documentation demos, visual tests). Interactive hosts keep the
48
+ * default.
49
+ * @default true
50
+ */
51
+ readonly modal?: boolean;
52
+ }
53
+
54
+ /**
55
+ * Pick a provider-approved template, fill its variables, and send it as
56
+ * a staff reply — the one lane WhatsApp offers a business once the
57
+ * customer's 24-hour service window has closed (cloud#260).
58
+ *
59
+ * Where {@link ChannelTemplatesDialog} is the channel surface's
60
+ * read-only registry *viewer* (every status, diagnosis copy), this is
61
+ * the conversation surface's *picker*: it lists only approved
62
+ * templates, marks the ones this platform version cannot send (an
63
+ * `unsupported_reason` from the server, or an IMAGE header — the send
64
+ * payload needs a public HTTPS asset no console surface can supply
65
+ * yet), and walks the two steps of a send: choose, then fill the
66
+ * `{{...}}` variables with a live preview of the exact body the
67
+ * customer will receive.
68
+ *
69
+ * Outcome honesty mirrors the composer's contract: a `refused` outcome
70
+ * keeps the dialog open with every filled value intact and renders the
71
+ * server's corrective detail verbatim (the pre-check names missing
72
+ * parameters, wrong languages, paused templates); a thrown failure does
73
+ * the same with the transport error. `accepted` and `queued` close the
74
+ * dialog — the ledger row exists, so the timeline shows the real item
75
+ * with its true delivery status (the SDK never fabricates one).
76
+ *
77
+ * The body mounts only while open, so each opening fetches the registry
78
+ * fresh (approval statuses change on the provider's side at any time)
79
+ * and a closed dialog holds no draft — the same clean-reset rule that
80
+ * keys the workbench's detail column (F-22).
81
+ */
82
+ export function ConversationTemplatePickerDialog({
83
+ open,
84
+ onOpenChange,
85
+ channelSlug,
86
+ org,
87
+ onSend,
88
+ isSending,
89
+ modal = true,
90
+ }: ConversationTemplatePickerDialogProps) {
91
+ const titleId = useId();
92
+
93
+ return (
94
+ <DialogShell
95
+ open={open}
96
+ onOpenChange={onOpenChange}
97
+ modal={modal}
98
+ // Template bodies are the content — the templates-dialog width.
99
+ width="2xl"
100
+ aria-labelledby={titleId}
101
+ >
102
+ {open && (
103
+ <PickerBody
104
+ titleId={titleId}
105
+ channelSlug={channelSlug}
106
+ org={org}
107
+ onSend={onSend}
108
+ isSending={isSending}
109
+ onClose={() => onOpenChange(false)}
110
+ />
111
+ )}
112
+ </DialogShell>
113
+ );
114
+ }
115
+
116
+ /** Why a listed template cannot be sent from here, or null when it can. */
117
+ function unsendableReasonOf(template: ChannelTemplate): string | null {
118
+ if (template.unsupportedReason) {
119
+ return `Not sendable: ${template.unsupportedReason}`;
120
+ }
121
+ if (template.headerFormat === "IMAGE") {
122
+ // The wire's header_image_link needs a public HTTPS asset at send
123
+ // time; no console surface can supply one yet (v1 scope cut,
124
+ // recorded on cloud#260).
125
+ return "Not sendable from the console yet: this template's image header needs a hosted image at send time.";
126
+ }
127
+ return null;
128
+ }
129
+
130
+ /** The verbatim `{{ key }}` placeholder's inner key. */
131
+ function placeholderKeyOf(placeholder: string): string {
132
+ return placeholder.replace(/^\{\{\s*|\s*\}\}$/g, "");
133
+ }
134
+
135
+ function PickerBody({
136
+ titleId,
137
+ channelSlug,
138
+ org,
139
+ onSend,
140
+ isSending,
141
+ onClose,
142
+ }: {
143
+ readonly titleId: string;
144
+ readonly channelSlug: string;
145
+ readonly org: string;
146
+ readonly onSend: (payload: ConversationReplyPayload) => Promise<SendChannelMessageOutput>;
147
+ readonly isSending: boolean;
148
+ readonly onClose: () => void;
149
+ }) {
150
+ // Approved-only is the wire filter; unsupported/image-header entries
151
+ // still list (marked unsendable) so "why is my template missing"
152
+ // never needs a second surface.
153
+ const { templates, isLoading, error } = useChannelTemplateList(channelSlug, org, {
154
+ approvedOnly: true,
155
+ });
156
+ const [selected, setSelected] = useState<ChannelTemplate | null>(null);
157
+
158
+ return (
159
+ <div className="stg:flex stg:max-h-[75vh] stg:flex-col">
160
+ <div className="stg:flex stg:items-start stg:justify-between stg:gap-3 stg:border-b stg:border-border stg:px-5 stg:py-4">
161
+ <div className="stg:flex stg:min-w-0 stg:items-center stg:gap-2">
162
+ {selected !== null && (
163
+ <Button
164
+ variant="ghost"
165
+ size="xs"
166
+ onClick={() => setSelected(null)}
167
+ disabled={isSending}
168
+ aria-label="Back to the template list"
169
+ >
170
+ <ArrowLeft className="stg:size-4" />
171
+ </Button>
172
+ )}
173
+ <div className="stg:min-w-0">
174
+ <h2
175
+ id={titleId}
176
+ className="stg:text-sm stg:font-semibold stg:text-popover-foreground"
177
+ >
178
+ {selected === null ? "Send a template" : selected.name}
179
+ </h2>
180
+ <p className="stg:mt-0.5 stg:text-xs stg:text-muted-foreground">
181
+ {selected === null
182
+ ? "Approved templates deliver even after the 24-hour reply window closes."
183
+ : "Fill the template's variables — the preview is the exact message the customer receives."}
184
+ </p>
185
+ </div>
186
+ </div>
187
+ <Button variant="ghost" size="xs" onClick={onClose} aria-label="Close template picker">
188
+ <X className="stg:size-4" />
189
+ </Button>
190
+ </div>
191
+
192
+ <div className="stg:min-h-0 stg:flex-1 stg:overflow-y-auto stg:p-4">
193
+ {selected === null ? (
194
+ <TemplateChoiceList
195
+ templates={templates}
196
+ isLoading={isLoading}
197
+ error={error}
198
+ onChoose={setSelected}
199
+ />
200
+ ) : (
201
+ <TemplateFillForm
202
+ template={selected}
203
+ onSend={onSend}
204
+ isSending={isSending}
205
+ onClose={onClose}
206
+ />
207
+ )}
208
+ </div>
209
+ </div>
210
+ );
211
+ }
212
+
213
+ function TemplateChoiceList({
214
+ templates,
215
+ isLoading,
216
+ error,
217
+ onChoose,
218
+ }: {
219
+ readonly templates: readonly ChannelTemplate[];
220
+ readonly isLoading: boolean;
221
+ readonly error: Error | null;
222
+ readonly onChoose: (template: ChannelTemplate) => void;
223
+ }) {
224
+ if (isLoading) {
225
+ return (
226
+ <div className="stg:space-y-2 stg:p-1">
227
+ {[1, 2, 3].map((i) => (
228
+ <div key={i} className="stg:h-16 stg:animate-pulse stg:rounded-lg stg:bg-muted-faint" />
229
+ ))}
230
+ </div>
231
+ );
232
+ }
233
+ if (error) {
234
+ // Past the fetch, the server's copy is the teaching state — its
235
+ // refusals name the fix verbatim (the templates-dialog posture).
236
+ return (
237
+ <p className="stg:px-2 stg:py-6 stg:text-center stg:text-sm stg:text-destructive">
238
+ {getUserMessage(error)}
239
+ </p>
240
+ );
241
+ }
242
+ if (templates.length === 0) {
243
+ // "found", never "you have none": an empty success is
244
+ // indistinguishable from a filtered or truncated one on the wire.
245
+ return (
246
+ <EmptyState
247
+ variant="first-use"
248
+ icon={<LayoutTemplate className="stg:size-8" />}
249
+ title="No approved templates found"
250
+ description={
251
+ "Templates are authored and approved in WhatsApp Manager — " +
252
+ "Stigmer keeps no copy. Approved templates appear here " +
253
+ "automatically; the channel card's Templates dialog shows every " +
254
+ "status, including pending and rejected ones."
255
+ }
256
+ />
257
+ );
258
+ }
259
+
260
+ return (
261
+ <ul className={cn(UNSTYLED_LIST, "stg:space-y-2")}>
262
+ {templates.map((template) => {
263
+ const unsendable = unsendableReasonOf(template);
264
+ return (
265
+ <li key={`${template.name}\u0000${template.language}`}>
266
+ <button
267
+ type="button"
268
+ onClick={() => onChoose(template)}
269
+ disabled={unsendable !== null}
270
+ className={cn(
271
+ "stg:w-full stg:rounded-lg stg:border stg:border-border stg:p-3 stg:text-left",
272
+ "stg:focus-visible:outline-none stg:focus-visible:ring-2 stg:focus-visible:ring-ring",
273
+ unsendable === null
274
+ ? "stg:hover:border-ring stg:hover:bg-muted-faint"
275
+ : "stg:opacity-60",
276
+ )}
277
+ >
278
+ <div className="stg:flex stg:min-w-0 stg:flex-wrap stg:items-center stg:gap-x-2 stg:gap-y-1">
279
+ <TruncatedText
280
+ text={template.name}
281
+ className="stg:text-sm stg:font-medium stg:text-foreground"
282
+ />
283
+ {/* (name, language) is the template's identity — each
284
+ language is approved independently. */}
285
+ <span className="stg:shrink-0 stg:text-xs stg:text-muted-foreground">
286
+ {template.language}
287
+ </span>
288
+ {template.headerFormat && (
289
+ <span className="stg:shrink-0 stg:rounded stg:bg-muted-subtle stg:px-1.5 stg:py-0.5 stg:text-xs stg:text-muted-foreground">
290
+ {template.headerFormat} header
291
+ </span>
292
+ )}
293
+ </div>
294
+ {template.bodyText && (
295
+ <p className="stg:mt-1.5 stg:line-clamp-2 stg:whitespace-pre-wrap stg:text-xs stg:leading-relaxed stg:text-muted-foreground">
296
+ {template.bodyText}
297
+ </p>
298
+ )}
299
+ {unsendable !== null && (
300
+ <p className="stg:mt-1.5 stg:text-xs stg:text-warning">{unsendable}</p>
301
+ )}
302
+ </button>
303
+ </li>
304
+ );
305
+ })}
306
+ </ul>
307
+ );
308
+ }
309
+
310
+ function TemplateFillForm({
311
+ template,
312
+ onSend,
313
+ isSending,
314
+ onClose,
315
+ }: {
316
+ readonly template: ChannelTemplate;
317
+ readonly onSend: (payload: ConversationReplyPayload) => Promise<SendChannelMessageOutput>;
318
+ readonly isSending: boolean;
319
+ readonly onClose: () => void;
320
+ }) {
321
+ const fieldIdPrefix = useId();
322
+ const [values, setValues] = useState<Readonly<Record<string, string>>>({});
323
+ const [notice, setNotice] = useState<{
324
+ readonly kind: "refused" | "error";
325
+ readonly text: string;
326
+ } | null>(null);
327
+
328
+ const segments = useMemo(() => splitTemplateBody(template.bodyText), [template.bodyText]);
329
+ // The declared parameters are the contract: names for NAMED
330
+ // templates, positions ("1", "2", …) for POSITIONAL — the pre-check
331
+ // refuses anything missing or extra, so the form asks for exactly
332
+ // this list.
333
+ const parameterKeys = template.parameterNames;
334
+ const allFilled = parameterKeys.every((key) => (values[key] ?? "").trim() !== "");
335
+
336
+ const handleSend = () => {
337
+ setNotice(null);
338
+ const parameters = Object.fromEntries(
339
+ parameterKeys.map((key) => [key, (values[key] ?? "").trim()]),
340
+ );
341
+ onSend({
342
+ kind: "template",
343
+ name: template.name,
344
+ // Always the chosen entry's language: (name, language) is the
345
+ // identity we listed, and sending it structurally avoids the
346
+ // wire's ambiguous-language refusal.
347
+ language: template.language,
348
+ parameters,
349
+ }).then(
350
+ (output) => {
351
+ if (output.outcome === ChannelSendOutcome.refused) {
352
+ // The words never left; every filled value stays for the
353
+ // correction the detail asks for (the composer's contract).
354
+ setNotice({
355
+ kind: "refused",
356
+ text: output.detail || "The provider refused this template.",
357
+ });
358
+ } else {
359
+ // accepted or queued: the ledger row exists — the timeline
360
+ // shows the real item with its true delivery status.
361
+ onClose();
362
+ }
363
+ },
364
+ (err) => {
365
+ setNotice({ kind: "error", text: getUserMessage(err) });
366
+ },
367
+ );
368
+ };
369
+
370
+ return (
371
+ <div className="stg:space-y-4">
372
+ {notice && (
373
+ <p className="stg:text-xs stg:text-destructive" role="status">
374
+ {notice.text}
375
+ </p>
376
+ )}
377
+
378
+ {parameterKeys.length > 0 && (
379
+ <div className="stg:space-y-3">
380
+ {parameterKeys.map((key) => (
381
+ <div key={key}>
382
+ <label
383
+ htmlFor={`${fieldIdPrefix}-${key}`}
384
+ className="stg:mb-1 stg:block stg:text-xs stg:font-medium stg:text-foreground"
385
+ >
386
+ {key}
387
+ </label>
388
+ <input
389
+ id={`${fieldIdPrefix}-${key}`}
390
+ type="text"
391
+ value={values[key] ?? ""}
392
+ onChange={(e) => setValues((v) => ({ ...v, [key]: e.target.value }))}
393
+ disabled={isSending}
394
+ className={cn(
395
+ "stg:w-full stg:rounded-md stg:border stg:border-border stg:bg-background",
396
+ "stg:px-3 stg:py-2 stg:text-sm stg:text-foreground stg:placeholder:text-muted-foreground-faint",
397
+ "stg:focus-visible:outline-none stg:focus-visible:ring-2 stg:focus-visible:ring-ring",
398
+ )}
399
+ />
400
+ </div>
401
+ ))}
402
+ </div>
403
+ )}
404
+
405
+ <div>
406
+ <p className="stg:mb-1 stg:text-xs stg:font-medium stg:text-foreground">Preview</p>
407
+ <p className="stg:whitespace-pre-wrap stg:rounded-md stg:border stg:border-border stg:bg-muted-faint stg:p-3 stg:text-sm stg:leading-relaxed stg:text-foreground">
408
+ {segments.map((segment, i) => {
409
+ if (segment.kind === "text") {
410
+ return <span key={i}>{segment.value}</span>;
411
+ }
412
+ const value = (values[placeholderKeyOf(segment.value)] ?? "").trim();
413
+ return value !== "" ? (
414
+ <span key={i}>{value}</span>
415
+ ) : (
416
+ <code
417
+ key={i}
418
+ className="stg:rounded stg:bg-muted-subtle stg:px-1 stg:font-mono stg:text-muted-foreground"
419
+ >
420
+ {segment.value}
421
+ </code>
422
+ );
423
+ })}
424
+ </p>
425
+ </div>
426
+
427
+ <div className="stg:flex stg:justify-end">
428
+ <Button
429
+ variant="primary"
430
+ size="sm"
431
+ onClick={handleSend}
432
+ disabled={!allFilled || isSending}
433
+ aria-label="Send template"
434
+ >
435
+ {isSending ? <SpinnerIcon /> : <Send aria-hidden="true" className="stg:size-4" />}
436
+ {isSending ? "Sending…" : "Send template"}
437
+ </Button>
438
+ </div>
439
+ </div>
440
+ );
441
+ }
@@ -16,6 +16,7 @@ import { ConversationAttentionBanner } from "./ConversationAttentionBanner.js";
16
16
  import { ConversationComposer } from "./ConversationComposer.js";
17
17
  import { ConversationControlBanner } from "./ConversationControlBanner.js";
18
18
  import { ConversationListPane, type ConversationIdentity } from "./ConversationListPane.js";
19
+ import { ConversationTemplatePickerDialog } from "./ConversationTemplatePickerDialog.js";
19
20
  import { ConversationTimelineView } from "./ConversationTimelineView.js";
20
21
  import {
21
22
  conversationContactOf,
@@ -25,7 +26,10 @@ import {
25
26
  } from "./conversationPresentation.js";
26
27
  import { useConversation } from "./useConversation.js";
27
28
  import { useConversationList } from "./useConversationList.js";
28
- import { useConversationParticipation } from "./useConversationParticipation.js";
29
+ import {
30
+ useConversationParticipation,
31
+ type ConversationReplyPayload,
32
+ } from "./useConversationParticipation.js";
29
33
  import { useConversationTimeline } from "./useConversationTimeline.js";
30
34
 
31
35
  /**
@@ -179,9 +183,20 @@ export function ConversationsWorkbench({
179
183
  const serviceWindow = detail.conversation
180
184
  ? serviceWindowOf(detail.conversation, descriptor?.id ?? null, now ?? new Date())
181
185
  : null;
186
+ // cloud#260: templates are the lane that SUCCEEDS on a closed window,
187
+ // offered from the persistent affordance beside Send — the advisory
188
+ // points at it in copy only (it is the input's description with zero
189
+ // tab stops, F-18; a control inside it would be unreachable context).
190
+ const [templatePickerOpen, setTemplatePickerOpen] = useState(false);
191
+ const templatesAvailable =
192
+ (descriptor?.supportsMessageTemplates ?? false) && selectedChannel !== null;
182
193
  const composerAdvisory =
183
194
  serviceWindow === "closed" && descriptor
184
- ? `${descriptor.label} closes free-form replies 24 hours after the customer's last message — a reply sent now will probably fail. The window reopens when the customer writes again.`
195
+ ? `${descriptor.label} closes free-form replies 24 hours after the customer's last message — a reply sent now will probably fail. The window reopens when the customer writes again.${
196
+ templatesAvailable
197
+ ? " Approved templates still deliver — use the template button beside Send."
198
+ : ""
199
+ }`
185
200
  : null;
186
201
 
187
202
  const handleSelect = useCallback(
@@ -219,8 +234,8 @@ export function ConversationsWorkbench({
219
234
  }, [timeline.items, settlingItemId]);
220
235
 
221
236
  const handleSend = useCallback(
222
- async (text: string) => {
223
- const output = await participation.reply(text);
237
+ async (payload: ConversationReplyPayload) => {
238
+ const output = await participation.reply(payload);
224
239
  // The ledger row is committed before reply answers, so one head
225
240
  // refresh renders the REAL item (with its true status) — the SDK
226
241
  // never fabricates an optimistic item that might misstate what
@@ -371,9 +386,30 @@ export function ConversationsWorkbench({
371
386
  isSending={
372
387
  participation.pendingCommands.has("reply") || settlingItemId !== null
373
388
  }
389
+ onOpenTemplatePicker={
390
+ templatesAvailable && composerDisabledReason === null
391
+ ? () => setTemplatePickerOpen(true)
392
+ : undefined
393
+ }
374
394
  disabledReason={composerDisabledReason}
375
395
  advisory={composerAdvisory}
376
396
  />
397
+
398
+ {/* Inside the keyed column on purpose: switching conversations
399
+ remounts and closes it, so a half-filled template can never
400
+ travel into another customer's conversation (F-22). */}
401
+ {templatesAvailable && (
402
+ <ConversationTemplatePickerDialog
403
+ open={templatePickerOpen}
404
+ onOpenChange={setTemplatePickerOpen}
405
+ channelSlug={selectedChannel?.metadata?.slug ?? ""}
406
+ org={selectedChannel?.metadata?.org ?? ""}
407
+ onSend={handleSend}
408
+ isSending={
409
+ participation.pendingCommands.has("reply") || settlingItemId !== null
410
+ }
411
+ />
412
+ )}
377
413
  </div>
378
414
  )}
379
415
  </div>
@@ -40,10 +40,45 @@ describe("ConversationComposer", () => {
40
40
  const input = screen.getByLabelText("Reply to the customer");
41
41
  await user.type(input, "on my way{Enter}");
42
42
 
43
- expect(onSend).toHaveBeenCalledWith("on my way");
43
+ // One reply command, two lanes (cloud#260): the textarea always
44
+ // submits the text lane of the payload union.
45
+ expect(onSend).toHaveBeenCalledWith({ kind: "text", body: "on my way" });
44
46
  await waitFor(() => expect((input as HTMLTextAreaElement).value).toBe(""));
45
47
  });
46
48
 
49
+ it("renders the template affordance only when a picker is wired, disabled while sending", () => {
50
+ const onOpenTemplatePicker = vi.fn();
51
+ const { rerender } = render(
52
+ <ConversationComposer onSend={vi.fn()} isSending={false} disabledReason={null} />,
53
+ );
54
+ expect(screen.queryByRole("button", { name: "Send a template reply" })).toBeNull();
55
+
56
+ rerender(
57
+ <ConversationComposer
58
+ onSend={vi.fn()}
59
+ isSending={false}
60
+ disabledReason={null}
61
+ onOpenTemplatePicker={onOpenTemplatePicker}
62
+ />,
63
+ );
64
+ const affordance = screen.getByRole("button", { name: "Send a template reply" });
65
+ affordance.click();
66
+ expect(onOpenTemplatePicker).toHaveBeenCalledTimes(1);
67
+
68
+ rerender(
69
+ <ConversationComposer
70
+ onSend={vi.fn()}
71
+ isSending={true}
72
+ disabledReason={null}
73
+ onOpenTemplatePicker={onOpenTemplatePicker}
74
+ />,
75
+ );
76
+ expect(
77
+ (screen.getByRole("button", { name: "Send a template reply" }) as HTMLButtonElement)
78
+ .disabled,
79
+ ).toBe(true);
80
+ });
81
+
47
82
  it("keeps the draft and shows the refusal detail verbatim when the send is refused", async () => {
48
83
  const user = userEvent.setup();
49
84
  const onSend = vi
@@ -118,6 +153,26 @@ describe("ConversationComposer", () => {
118
153
  expect(advisoryEl?.querySelector("button, a, [tabindex]")).toBeNull();
119
154
  });
120
155
 
156
+ it("keeps the advisory interaction-free even with a template picker wired (F-18)", () => {
157
+ render(
158
+ <ConversationComposer
159
+ onSend={vi.fn()}
160
+ isSending={false}
161
+ disabledReason={null}
162
+ advisory="The window has closed — send an approved template with the template button."
163
+ onOpenTemplatePicker={vi.fn()}
164
+ />,
165
+ );
166
+ const input = screen.getByLabelText("Reply to the customer");
167
+ const advisoryEl = document.getElementById(
168
+ input.getAttribute("aria-describedby") as string,
169
+ );
170
+ // The way forward lives on the persistent affordance BESIDE Send —
171
+ // the description itself must never grow a control.
172
+ expect(advisoryEl?.querySelector("button, a, [tabindex]")).toBeNull();
173
+ expect(screen.getByRole("button", { name: "Send a template reply" })).toBeDefined();
174
+ });
175
+
121
176
  it("renders no advisory and no dangling description when there is no claim", () => {
122
177
  render(
123
178
  <ConversationComposer onSend={vi.fn()} isSending={false} disabledReason={null} advisory={null} />,