@stigmer/react 3.5.3 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/agent/AgentPicker.d.ts +10 -1
  2. package/agent/AgentPicker.d.ts.map +1 -1
  3. package/agent/AgentPicker.js +2 -2
  4. package/agent/AgentPicker.js.map +1 -1
  5. package/billing/CreditLedgerTable.d.ts.map +1 -1
  6. package/billing/CreditLedgerTable.js +2 -7
  7. package/billing/CreditLedgerTable.js.map +1 -1
  8. package/channel/AgentChannelsPanel.d.ts.map +1 -1
  9. package/channel/AgentChannelsPanel.js +22 -5
  10. package/channel/AgentChannelsPanel.js.map +1 -1
  11. package/channel/ChannelTemplatesDialog.d.ts +53 -0
  12. package/channel/ChannelTemplatesDialog.d.ts.map +1 -0
  13. package/channel/ChannelTemplatesDialog.js +150 -0
  14. package/channel/ChannelTemplatesDialog.js.map +1 -0
  15. package/channel/index.d.ts +8 -0
  16. package/channel/index.d.ts.map +1 -1
  17. package/channel/index.js +4 -0
  18. package/channel/index.js.map +1 -1
  19. package/channel/providers.d.ts +12 -0
  20. package/channel/providers.d.ts.map +1 -1
  21. package/channel/providers.js +14 -2
  22. package/channel/providers.js.map +1 -1
  23. package/channel/templatePresentation.d.ts +37 -0
  24. package/channel/templatePresentation.d.ts.map +1 -0
  25. package/channel/templatePresentation.js +69 -0
  26. package/channel/templatePresentation.js.map +1 -0
  27. package/channel/useChannelTemplateList.d.ts +64 -0
  28. package/channel/useChannelTemplateList.d.ts.map +1 -0
  29. package/channel/useChannelTemplateList.js +69 -0
  30. package/channel/useChannelTemplateList.js.map +1 -0
  31. package/channel/useChannelTemplateReadiness.d.ts +53 -0
  32. package/channel/useChannelTemplateReadiness.d.ts.map +1 -0
  33. package/channel/useChannelTemplateReadiness.js +42 -0
  34. package/channel/useChannelTemplateReadiness.js.map +1 -0
  35. package/composer/ComposerToolbar.d.ts +6 -1
  36. package/composer/ComposerToolbar.d.ts.map +1 -1
  37. package/composer/ComposerToolbar.js +2 -2
  38. package/composer/ComposerToolbar.js.map +1 -1
  39. package/composer/SessionComposer.d.ts +9 -0
  40. package/composer/SessionComposer.d.ts.map +1 -1
  41. package/composer/SessionComposer.js +13 -2
  42. package/composer/SessionComposer.js.map +1 -1
  43. package/dashboard/useDashboardFailedRuns.d.ts.map +1 -1
  44. package/dashboard/useDashboardFailedRuns.js +2 -0
  45. package/dashboard/useDashboardFailedRuns.js.map +1 -1
  46. package/execution/useCreateAgentExecution.d.ts +14 -0
  47. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  48. package/execution/useCreateAgentExecution.js +5 -0
  49. package/execution/useCreateAgentExecution.js.map +1 -1
  50. package/index.d.ts +4 -2
  51. package/index.d.ts.map +1 -1
  52. package/index.js +10 -1
  53. package/index.js.map +1 -1
  54. package/internal/Pagination.d.ts +15 -0
  55. package/internal/Pagination.d.ts.map +1 -0
  56. package/internal/Pagination.js +20 -0
  57. package/internal/Pagination.js.map +1 -0
  58. package/library/useExportResource.d.ts +4 -3
  59. package/library/useExportResource.d.ts.map +1 -1
  60. package/library/useExportResource.js +1 -1
  61. package/library/useExportResource.js.map +1 -1
  62. package/models/ModelSelector.d.ts +26 -1
  63. package/models/ModelSelector.d.ts.map +1 -1
  64. package/models/ModelSelector.js +27 -6
  65. package/models/ModelSelector.js.map +1 -1
  66. package/models/index.d.ts +2 -0
  67. package/models/index.d.ts.map +1 -1
  68. package/models/index.js +1 -0
  69. package/models/index.js.map +1 -1
  70. package/models/registry.d.ts +10 -0
  71. package/models/registry.d.ts.map +1 -1
  72. package/models/registry.js +3 -0
  73. package/models/registry.js.map +1 -1
  74. package/models/service-tier.d.ts +27 -0
  75. package/models/service-tier.d.ts.map +1 -0
  76. package/models/service-tier.js +34 -0
  77. package/models/service-tier.js.map +1 -0
  78. package/organization/OrgProvider.d.ts +11 -4
  79. package/organization/OrgProvider.d.ts.map +1 -1
  80. package/organization/OrgProvider.js +28 -4
  81. package/organization/OrgProvider.js.map +1 -1
  82. package/package.json +4 -4
  83. package/resource-detail/useDeleteResource.d.ts +1 -1
  84. package/resource-detail/useDeleteResource.d.ts.map +1 -1
  85. package/resource-detail/useDeleteResource.js +5 -0
  86. package/resource-detail/useDeleteResource.js.map +1 -1
  87. package/resource-workbench/components/ResourceWorkbench.d.ts +10 -1
  88. package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
  89. package/resource-workbench/components/ResourceWorkbench.js +6 -2
  90. package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
  91. package/resource-workbench/hooks/useResourceCollection.d.ts +8 -0
  92. package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
  93. package/resource-workbench/hooks/useResourceCollection.js +2 -2
  94. package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
  95. package/schedule/CadenceField.d.ts +31 -0
  96. package/schedule/CadenceField.d.ts.map +1 -0
  97. package/schedule/CadenceField.js +116 -0
  98. package/schedule/CadenceField.js.map +1 -0
  99. package/schedule/ScheduleDetailView.d.ts +81 -0
  100. package/schedule/ScheduleDetailView.d.ts.map +1 -0
  101. package/schedule/ScheduleDetailView.js +648 -0
  102. package/schedule/ScheduleDetailView.js.map +1 -0
  103. package/schedule/ScheduleForm.d.ts +46 -0
  104. package/schedule/ScheduleForm.d.ts.map +1 -0
  105. package/schedule/ScheduleForm.js +214 -0
  106. package/schedule/ScheduleForm.js.map +1 -0
  107. package/schedule/ScheduleRowActions.d.ts +20 -0
  108. package/schedule/ScheduleRowActions.d.ts.map +1 -0
  109. package/schedule/ScheduleRowActions.js +54 -0
  110. package/schedule/ScheduleRowActions.js.map +1 -0
  111. package/schedule/ScheduleRunsTable.d.ts +72 -0
  112. package/schedule/ScheduleRunsTable.d.ts.map +1 -0
  113. package/schedule/ScheduleRunsTable.js +161 -0
  114. package/schedule/ScheduleRunsTable.js.map +1 -0
  115. package/schedule/TimeZoneField.d.ts +32 -0
  116. package/schedule/TimeZoneField.d.ts.map +1 -0
  117. package/schedule/TimeZoneField.js +104 -0
  118. package/schedule/TimeZoneField.js.map +1 -0
  119. package/schedule/cadence.d.ts +95 -0
  120. package/schedule/cadence.d.ts.map +1 -0
  121. package/schedule/cadence.js +242 -0
  122. package/schedule/cadence.js.map +1 -0
  123. package/schedule/index.d.ts +37 -0
  124. package/schedule/index.d.ts.map +1 -0
  125. package/schedule/index.js +19 -0
  126. package/schedule/index.js.map +1 -0
  127. package/schedule/scheduleColumns.d.ts +26 -0
  128. package/schedule/scheduleColumns.d.ts.map +1 -0
  129. package/schedule/scheduleColumns.js +77 -0
  130. package/schedule/scheduleColumns.js.map +1 -0
  131. package/schedule/scheduleListFn.d.ts +50 -0
  132. package/schedule/scheduleListFn.d.ts.map +1 -0
  133. package/schedule/scheduleListFn.js +65 -0
  134. package/schedule/scheduleListFn.js.map +1 -0
  135. package/schedule/scheduleState.d.ts +43 -0
  136. package/schedule/scheduleState.d.ts.map +1 -0
  137. package/schedule/scheduleState.js +67 -0
  138. package/schedule/scheduleState.js.map +1 -0
  139. package/schedule/useCreateSchedule.d.ts +57 -0
  140. package/schedule/useCreateSchedule.d.ts.map +1 -0
  141. package/schedule/useCreateSchedule.js +61 -0
  142. package/schedule/useCreateSchedule.js.map +1 -0
  143. package/schedule/useResumeSchedule.d.ts +30 -0
  144. package/schedule/useResumeSchedule.d.ts.map +1 -0
  145. package/schedule/useResumeSchedule.js +45 -0
  146. package/schedule/useResumeSchedule.js.map +1 -0
  147. package/schedule/useSchedule.d.ts +39 -0
  148. package/schedule/useSchedule.d.ts.map +1 -0
  149. package/schedule/useSchedule.js +44 -0
  150. package/schedule/useSchedule.js.map +1 -0
  151. package/schedule/useScheduleCount.d.ts +38 -0
  152. package/schedule/useScheduleCount.d.ts.map +1 -0
  153. package/schedule/useScheduleCount.js +30 -0
  154. package/schedule/useScheduleCount.js.map +1 -0
  155. package/schedule/useScheduleList.d.ts +48 -0
  156. package/schedule/useScheduleList.d.ts.map +1 -0
  157. package/schedule/useScheduleList.js +43 -0
  158. package/schedule/useScheduleList.js.map +1 -0
  159. package/schedule/useScheduleRuns.d.ts +44 -0
  160. package/schedule/useScheduleRuns.d.ts.map +1 -0
  161. package/schedule/useScheduleRuns.js +48 -0
  162. package/schedule/useScheduleRuns.js.map +1 -0
  163. package/schedule/useSetScheduleEnabled.d.ts +31 -0
  164. package/schedule/useSetScheduleEnabled.d.ts.map +1 -0
  165. package/schedule/useSetScheduleEnabled.js +53 -0
  166. package/schedule/useSetScheduleEnabled.js.map +1 -0
  167. package/schedule/useTriggerSchedule.d.ts +41 -0
  168. package/schedule/useTriggerSchedule.d.ts.map +1 -0
  169. package/schedule/useTriggerSchedule.js +62 -0
  170. package/schedule/useTriggerSchedule.js.map +1 -0
  171. package/schedule/useUpdateScheduleSpec.d.ts +49 -0
  172. package/schedule/useUpdateScheduleSpec.d.ts.map +1 -0
  173. package/schedule/useUpdateScheduleSpec.js +70 -0
  174. package/schedule/useUpdateScheduleSpec.js.map +1 -0
  175. package/search/useResourceCount.d.ts +2 -1
  176. package/search/useResourceCount.d.ts.map +1 -1
  177. package/search/useResourceCount.js +6 -1
  178. package/search/useResourceCount.js.map +1 -1
  179. package/session/useNewSessionFlow.d.ts.map +1 -1
  180. package/session/useNewSessionFlow.js +1 -0
  181. package/session/useNewSessionFlow.js.map +1 -1
  182. package/session/useSessionConversation.d.ts +11 -0
  183. package/session/useSessionConversation.d.ts.map +1 -1
  184. package/session/useSessionConversation.js +1 -0
  185. package/session/useSessionConversation.js.map +1 -1
  186. package/session/useSessionPageFlow.d.ts.map +1 -1
  187. package/session/useSessionPageFlow.js +1 -0
  188. package/session/useSessionPageFlow.js.map +1 -1
  189. package/src/agent/AgentPicker.tsx +13 -1
  190. package/src/billing/CreditLedgerTable.tsx +3 -42
  191. package/src/channel/AgentChannelsPanel.tsx +43 -1
  192. package/src/channel/ChannelTemplatesDialog.tsx +433 -0
  193. package/src/channel/__tests__/AgentChannelsPanel.test.tsx +73 -0
  194. package/src/channel/__tests__/ChannelTemplatesDialog.test.tsx +292 -0
  195. package/src/channel/__tests__/providers.test.ts +9 -0
  196. package/src/channel/__tests__/templatePresentation.test.ts +96 -0
  197. package/src/channel/__tests__/useChannelTemplateList.test.tsx +216 -0
  198. package/src/channel/__tests__/useChannelTemplateReadiness.test.tsx +124 -0
  199. package/src/channel/index.ts +15 -0
  200. package/src/channel/providers.ts +26 -2
  201. package/src/channel/templatePresentation.ts +82 -0
  202. package/src/channel/useChannelTemplateList.ts +123 -0
  203. package/src/channel/useChannelTemplateReadiness.ts +69 -0
  204. package/src/composer/ComposerToolbar.tsx +9 -0
  205. package/src/composer/SessionComposer.tsx +24 -1
  206. package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +156 -0
  207. package/src/dashboard/__tests__/useDashboardFailedRuns.test.tsx +106 -0
  208. package/src/dashboard/useDashboardFailedRuns.ts +2 -0
  209. package/src/execution/useCreateAgentExecution.ts +18 -0
  210. package/src/index.ts +70 -0
  211. package/src/internal/Pagination.tsx +72 -0
  212. package/src/library/useExportResource.ts +4 -3
  213. package/src/models/ModelSelector.tsx +100 -3
  214. package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +172 -0
  215. package/src/models/__tests__/useModelRegistry.test.tsx +15 -0
  216. package/src/models/index.ts +2 -0
  217. package/src/models/registry.ts +16 -0
  218. package/src/models/service-tier.ts +49 -0
  219. package/src/organization/OrgProvider.tsx +29 -4
  220. package/src/organization/__tests__/OrgProvider.orgSwitch.test.tsx +89 -0
  221. package/src/resource-detail/useDeleteResource.ts +11 -1
  222. package/src/resource-workbench/__tests__/ResourceWorkbench.test.tsx +112 -0
  223. package/src/resource-workbench/components/ResourceWorkbench.tsx +32 -15
  224. package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
  225. package/src/schedule/CadenceField.tsx +365 -0
  226. package/src/schedule/ScheduleDetailView.tsx +1651 -0
  227. package/src/schedule/ScheduleForm.tsx +615 -0
  228. package/src/schedule/ScheduleRowActions.tsx +124 -0
  229. package/src/schedule/ScheduleRunsTable.tsx +424 -0
  230. package/src/schedule/TimeZoneField.tsx +199 -0
  231. package/src/schedule/__tests__/ScheduleDetailView.test.tsx +683 -0
  232. package/src/schedule/__tests__/cadence.test.ts +278 -0
  233. package/src/schedule/__tests__/scheduleColumnsRowActions.test.tsx +176 -0
  234. package/src/schedule/__tests__/scheduleCreation.test.tsx +537 -0
  235. package/src/schedule/__tests__/scheduleHooks.test.tsx +399 -0
  236. package/src/schedule/__tests__/scheduleListFn.test.ts +128 -0
  237. package/src/schedule/__tests__/scheduleState.test.ts +133 -0
  238. package/src/schedule/cadence.ts +298 -0
  239. package/src/schedule/index.ts +69 -0
  240. package/src/schedule/scheduleColumns.tsx +118 -0
  241. package/src/schedule/scheduleListFn.ts +108 -0
  242. package/src/schedule/scheduleState.ts +96 -0
  243. package/src/schedule/useCreateSchedule.ts +88 -0
  244. package/src/schedule/useResumeSchedule.ts +67 -0
  245. package/src/schedule/useSchedule.ts +67 -0
  246. package/src/schedule/useScheduleCount.ts +57 -0
  247. package/src/schedule/useScheduleList.ts +83 -0
  248. package/src/schedule/useScheduleRuns.ts +93 -0
  249. package/src/schedule/useSetScheduleEnabled.ts +84 -0
  250. package/src/schedule/useTriggerSchedule.ts +90 -0
  251. package/src/schedule/useUpdateScheduleSpec.ts +108 -0
  252. package/src/search/__tests__/useResourceCount.test.tsx +89 -0
  253. package/src/search/useResourceCount.ts +8 -2
  254. package/src/session/useNewSessionFlow.ts +1 -0
  255. package/src/session/useSessionConversation.ts +12 -0
  256. package/src/session/useSessionPageFlow.ts +1 -0
  257. package/src/workflow/WorkflowDashboard.tsx +1 -0
  258. package/src/workflow/__tests__/inspector-forms.test.tsx +119 -2
  259. package/src/workflow/inspector/forms/AgentCallForm.tsx +260 -37
  260. package/src/workflow/inspector/tabs/RuntimeTab.tsx +72 -33
  261. package/src/workflow/starter-workflow-yaml.ts +2 -1
  262. package/src/workflow/useWorkflowExecutionList.ts +13 -2
  263. package/styles.css +1 -1
  264. package/workflow/CanvasTransitionEdge.d.ts +1 -1
  265. package/workflow/WorkflowDashboard.d.ts.map +1 -1
  266. package/workflow/WorkflowDashboard.js +1 -0
  267. package/workflow/WorkflowDashboard.js.map +1 -1
  268. package/workflow/inspector/forms/AgentCallForm.d.ts +18 -2
  269. package/workflow/inspector/forms/AgentCallForm.d.ts.map +1 -1
  270. package/workflow/inspector/forms/AgentCallForm.js +87 -9
  271. package/workflow/inspector/forms/AgentCallForm.js.map +1 -1
  272. package/workflow/inspector/tabs/RuntimeTab.d.ts +2 -1
  273. package/workflow/inspector/tabs/RuntimeTab.d.ts.map +1 -1
  274. package/workflow/inspector/tabs/RuntimeTab.js +31 -5
  275. package/workflow/inspector/tabs/RuntimeTab.js.map +1 -1
  276. package/workflow/starter-workflow-yaml.d.ts +1 -1
  277. package/workflow/starter-workflow-yaml.d.ts.map +1 -1
  278. package/workflow/starter-workflow-yaml.js +2 -1
  279. package/workflow/starter-workflow-yaml.js.map +1 -1
  280. package/workflow/useWorkflowExecutionList.d.ts +10 -0
  281. package/workflow/useWorkflowExecutionList.d.ts.map +1 -1
  282. package/workflow/useWorkflowExecutionList.js +3 -2
  283. package/workflow/useWorkflowExecutionList.js.map +1 -1
@@ -0,0 +1,433 @@
1
+ "use client";
2
+
3
+ import { useCallback, useRef, type ReactNode } from "react";
4
+ import { ExternalLink, LayoutTemplate, X } from "lucide-react";
5
+ import { cn } from "@stigmer/theme";
6
+ import { getUserMessage } from "@stigmer/sdk";
7
+ import type { AgentChannel } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
8
+ import type { ChannelTemplate } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
9
+ import { Button } from "../button/Button.js";
10
+ import { EmptyState } from "../empty-state/EmptyState.js";
11
+ import { CloudFeatureNotice } from "../internal/CloudFeatureNotice.js";
12
+ import { StatusBadge } from "../resource-workbench/components/StatusBadge.js";
13
+ import {
14
+ channelPresentationOf,
15
+ DEFAULT_CHANNEL_PRESENTATION,
16
+ } from "./providerPresentation.js";
17
+ import {
18
+ splitTemplateBody,
19
+ templateStatusPhase,
20
+ } from "./templatePresentation.js";
21
+ import { useChannelTemplateList } from "./useChannelTemplateList.js";
22
+ import { useChannelTemplateReadiness } from "./useChannelTemplateReadiness.js";
23
+
24
+ /**
25
+ * Where businesses author and manage WhatsApp message templates — the
26
+ * provider's own console. Stigmer keeps no copy of the list (DD-003:
27
+ * the provider is the registry), so this surface links out rather than
28
+ * offering any editing of its own.
29
+ */
30
+ const WHATSAPP_MANAGER_TEMPLATES_URL =
31
+ "https://business.facebook.com/wa/manage/message-templates/";
32
+
33
+ /** Props for {@link ChannelTemplatesDialog}. */
34
+ export interface ChannelTemplatesDialogProps {
35
+ /** Whether the dialog is open. */
36
+ readonly open: boolean;
37
+ /** Called when the dialog requests an open-state change. */
38
+ readonly onOpenChange: (open: boolean) => void;
39
+ /** The channel whose provider template registry is listed. */
40
+ readonly channel: AgentChannel;
41
+ /**
42
+ * Opens the channel's YAML editor — the only place
43
+ * `proactive_messaging_enabled` can be granted today. Wired by
44
+ * {@link AgentChannelsPanel} to its Edit YAML dialog; when absent,
45
+ * the teaching state renders without the affordance.
46
+ */
47
+ readonly onEditYaml?: () => void;
48
+ /**
49
+ * When `false`, renders as an in-flow open dialog instead of a
50
+ * top-layer modal — for embedding in constrained surfaces
51
+ * (documentation demos, visual tests). Interactive hosts keep the
52
+ * default.
53
+ * @default true
54
+ */
55
+ readonly modal?: boolean;
56
+ }
57
+
58
+ /**
59
+ * Lists a channel's message templates as the provider's registry
60
+ * reports them — for WhatsApp, the templates a business authored in
61
+ * WhatsApp Manager, each with its approval status.
62
+ *
63
+ * This is the diagnosis surface for business-initiated messaging: it is
64
+ * the one place that shows the provider's rejection copy AND Stigmer's
65
+ * own `unsupported_reason` (why an approved template still cannot be
66
+ * sent by the agent). The runner's prompt deliberately filters
67
+ * unsendable templates rather than annotating them, so a template
68
+ * "missing" from the agent's repertoire is explained here and nowhere
69
+ * else.
70
+ *
71
+ * The dialog is a window onto the provider, never an editor: templates
72
+ * are authored and approved in WhatsApp Manager, and the empty state
73
+ * says so. Before fetching, a readiness pre-check answers the
74
+ * preconditions the channel resource already knows (cloud runtime,
75
+ * installed, serving, proactive grant) as teaching states instead of
76
+ * relayed refusals; once a fetch does fail, the server's message
77
+ * renders verbatim.
78
+ *
79
+ * Built on the native `<dialog>` element, matching the SDK's modal
80
+ * convention. Most hosts mount it via {@link AgentChannelsPanel}'s
81
+ * channel card action menu, which shows the action only for providers
82
+ * whose descriptor declares `supportsMessageTemplates`.
83
+ */
84
+ export function ChannelTemplatesDialog({
85
+ open,
86
+ onOpenChange,
87
+ channel,
88
+ onEditYaml,
89
+ modal = true,
90
+ }: ChannelTemplatesDialogProps) {
91
+ const dialogRef = useRef<HTMLDialogElement>(null);
92
+
93
+ const handleClose = useCallback(() => {
94
+ dialogRef.current?.close();
95
+ onOpenChange(false);
96
+ }, [onOpenChange]);
97
+
98
+ // Sync native dialog open state (matches the SDK dialog convention).
99
+ const prevOpenRef = useRef(false);
100
+ if (modal && open !== prevOpenRef.current) {
101
+ prevOpenRef.current = open;
102
+ if (open) {
103
+ requestAnimationFrame(() => {
104
+ if (dialogRef.current && !dialogRef.current.open) {
105
+ dialogRef.current.showModal();
106
+ }
107
+ });
108
+ } else if (dialogRef.current?.open) {
109
+ dialogRef.current.close();
110
+ }
111
+ }
112
+
113
+ return (
114
+ <dialog
115
+ ref={dialogRef}
116
+ open={modal ? undefined : open}
117
+ onClose={handleClose}
118
+ className={cn(
119
+ // Wider than the conversations dialog on purpose: template
120
+ // bodies are the content, and a narrow column shreds them.
121
+ "w-full max-w-2xl rounded-xl border border-border bg-popover p-0 shadow-xl",
122
+ modal ? "fixed inset-0 m-auto backdrop:bg-black/50" : "relative",
123
+ )}
124
+ aria-labelledby="channel-templates-title"
125
+ >
126
+ {/* Body mounts only while open so each opening fetches fresh —
127
+ approval statuses change on the provider's side at any time. */}
128
+ {open && (
129
+ <ChannelTemplatesDialogBody
130
+ channel={channel}
131
+ onEditYaml={onEditYaml}
132
+ onClose={handleClose}
133
+ />
134
+ )}
135
+ </dialog>
136
+ );
137
+ }
138
+
139
+ function ChannelTemplatesDialogBody({
140
+ channel,
141
+ onEditYaml,
142
+ onClose,
143
+ }: {
144
+ readonly channel: AgentChannel;
145
+ readonly onEditYaml?: () => void;
146
+ readonly onClose: () => void;
147
+ }) {
148
+ const channelName =
149
+ channel.metadata?.name || channel.metadata?.slug || "this channel";
150
+
151
+ return (
152
+ <div className="flex max-h-[75vh] flex-col">
153
+ <div className="flex items-start justify-between gap-3 border-b border-border px-5 py-4">
154
+ <div className="min-w-0">
155
+ <h2
156
+ id="channel-templates-title"
157
+ className="text-sm font-semibold text-popover-foreground"
158
+ >
159
+ Templates
160
+ </h2>
161
+ <p className="mt-0.5 text-xs text-muted-foreground">
162
+ The message templates WhatsApp holds for {channelName} — authored
163
+ and approved in WhatsApp Manager, listed here as WhatsApp reports
164
+ them.
165
+ </p>
166
+ </div>
167
+ <Button
168
+ variant="ghost"
169
+ size="xs"
170
+ onClick={onClose}
171
+ aria-label="Close templates"
172
+ >
173
+ <X className="size-4" />
174
+ </Button>
175
+ </div>
176
+
177
+ <div className="min-h-0 flex-1 overflow-y-auto p-4">
178
+ <ChannelTemplatesContent channel={channel} onEditYaml={onEditYaml} />
179
+ </div>
180
+ </div>
181
+ );
182
+ }
183
+
184
+ /**
185
+ * The readiness gate: each precondition the channel resource already
186
+ * answers renders as a teaching state without a network call — the
187
+ * courtesy-pre-check posture (project DD-007 D4). Only `ready` fetches;
188
+ * the server stays authoritative for everything past this point.
189
+ */
190
+ function ChannelTemplatesContent({
191
+ channel,
192
+ onEditYaml,
193
+ }: {
194
+ readonly channel: AgentChannel;
195
+ readonly onEditYaml?: () => void;
196
+ }) {
197
+ const readiness = useChannelTemplateReadiness(channel);
198
+ const presentation =
199
+ channelPresentationOf(channel.spec?.providerConfig?.case) ??
200
+ DEFAULT_CHANNEL_PRESENTATION;
201
+
202
+ switch (readiness.status) {
203
+ case "cloud-only":
204
+ return (
205
+ <CloudFeatureNotice>
206
+ Message templates come from the provider through Stigmer
207
+ Cloud&apos;s channel runtime. Channel configuration still works
208
+ here, but the template registry can only be read on a cloud
209
+ deployment.
210
+ </CloudFeatureNotice>
211
+ );
212
+ case "not-installed":
213
+ // describeChannel already carries the right sentence per provider
214
+ // and per install state (pending vs revoked) — the card's copy,
215
+ // reused so the two surfaces cannot drift.
216
+ return (
217
+ <EmptyState
218
+ variant="first-use"
219
+ icon={<LayoutTemplate className="size-8" />}
220
+ title="Connect this channel first"
221
+ description={`${presentation.describeChannel(channel)} Templates become readable once the connection is complete.`}
222
+ />
223
+ );
224
+ case "channel-off":
225
+ return (
226
+ <EmptyState
227
+ variant="first-use"
228
+ icon={<LayoutTemplate className="size-8" />}
229
+ title="This channel is turned off"
230
+ description="Turn the channel on with the switch on its card, then check back here."
231
+ />
232
+ );
233
+ case "not-proactive":
234
+ return (
235
+ <EmptyState
236
+ variant="first-use"
237
+ icon={<LayoutTemplate className="size-8" />}
238
+ title="Business-initiated messaging is not enabled"
239
+ description={
240
+ "Templates power messages your agent sends first — reminders, " +
241
+ "invoices, notifications. To allow them, set " +
242
+ "proactive_messaging_enabled on this channel's definition."
243
+ }
244
+ >
245
+ {onEditYaml && (
246
+ <Button variant="primary" onClick={onEditYaml}>
247
+ Edit channel YAML
248
+ </Button>
249
+ )}
250
+ </EmptyState>
251
+ );
252
+ case "ready":
253
+ return <ChannelTemplatesList channel={channel} />;
254
+ }
255
+ }
256
+
257
+ function ChannelTemplatesList({
258
+ channel,
259
+ }: {
260
+ readonly channel: AgentChannel;
261
+ }) {
262
+ const { templates, isLoading, error } = useChannelTemplateList(
263
+ channel.metadata?.slug ?? "",
264
+ channel.metadata?.org ?? "",
265
+ );
266
+
267
+ if (isLoading) {
268
+ return <TemplatesSkeleton />;
269
+ }
270
+
271
+ if (error) {
272
+ // The server's copy is the teaching state past the pre-check —
273
+ // its refusals name the fix (scope, token, app binding) verbatim.
274
+ return (
275
+ <p className="px-2 py-6 text-center text-sm text-destructive">
276
+ {getUserMessage(error)}
277
+ </p>
278
+ );
279
+ }
280
+
281
+ if (templates.length === 0) {
282
+ // "found", never "you have none": an empty success is
283
+ // indistinguishable from a filtered or truncated one on the wire.
284
+ return (
285
+ <EmptyState
286
+ variant="first-use"
287
+ icon={<LayoutTemplate className="size-8" />}
288
+ title="No templates found for this channel"
289
+ description={
290
+ "Templates are authored in WhatsApp Manager and approved by " +
291
+ "WhatsApp — Stigmer keeps no copy. Once a template is approved, " +
292
+ "it appears here and in your agent's repertoire automatically."
293
+ }
294
+ >
295
+ <WhatsAppManagerLink label="Open WhatsApp Manager" />
296
+ </EmptyState>
297
+ );
298
+ }
299
+
300
+ const sendable = templates.filter(
301
+ (t) => t.status === "APPROVED" && t.unsupportedReason === "",
302
+ ).length;
303
+
304
+ return (
305
+ <div>
306
+ <div className="flex items-center justify-between gap-3 px-1 pb-3">
307
+ <p className="text-xs text-muted-foreground">
308
+ {templates.length} {templates.length === 1 ? "template" : "templates"}
309
+ {" \u00b7 "}
310
+ {sendable} ready to send
311
+ </p>
312
+ <WhatsAppManagerLink label="Manage in WhatsApp Manager" />
313
+ </div>
314
+ <ul className="space-y-2">
315
+ {templates.map((template) => (
316
+ <TemplateRow
317
+ key={`${template.name}\u0000${template.language}`}
318
+ template={template}
319
+ />
320
+ ))}
321
+ </ul>
322
+ </div>
323
+ );
324
+ }
325
+
326
+ // ---------------------------------------------------------------------------
327
+ // Row — one (name, language) template as the provider reports it
328
+ // ---------------------------------------------------------------------------
329
+
330
+ function TemplateRow({ template }: { readonly template: ChannelTemplate }) {
331
+ return (
332
+ <li className="rounded-lg border border-border p-3">
333
+ <div className="flex items-start justify-between gap-3">
334
+ <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
335
+ <span
336
+ className="truncate text-sm font-medium text-foreground"
337
+ title={template.name}
338
+ >
339
+ {template.name}
340
+ </span>
341
+ {/* (name, language) is the template's identity — each
342
+ language is approved independently, so it always shows. */}
343
+ <span className="shrink-0 text-xs text-muted-foreground">
344
+ {template.language}
345
+ </span>
346
+ {template.category && <FactChip>{template.category}</FactChip>}
347
+ {template.headerFormat && (
348
+ <FactChip>{template.headerFormat} header</FactChip>
349
+ )}
350
+ </div>
351
+ <StatusBadge
352
+ phase={templateStatusPhase(template.status)}
353
+ label={template.status}
354
+ className="shrink-0"
355
+ />
356
+ </div>
357
+
358
+ {template.bodyText && <TemplateBody body={template.bodyText} />}
359
+
360
+ {template.rejectionReason && (
361
+ <p className="mt-2 text-xs text-destructive">
362
+ Rejected: {template.rejectionReason}
363
+ </p>
364
+ )}
365
+ {template.unsupportedReason && (
366
+ <p className="mt-2 text-xs text-warning" role="status">
367
+ Not sendable: {template.unsupportedReason}
368
+ </p>
369
+ )}
370
+ </li>
371
+ );
372
+ }
373
+
374
+ /**
375
+ * The body verbatim, with `{{...}}` placeholders set off visually —
376
+ * they are the parts the agent fills at send time, and the reason the
377
+ * full text is on the wire at all.
378
+ */
379
+ function TemplateBody({ body }: { readonly body: string }) {
380
+ return (
381
+ <p className="mt-2 whitespace-pre-wrap text-xs leading-relaxed text-muted-foreground">
382
+ {splitTemplateBody(body).map((segment, i) =>
383
+ segment.kind === "placeholder" ? (
384
+ <code
385
+ key={i}
386
+ className="rounded bg-muted-subtle px-1 font-mono text-foreground"
387
+ >
388
+ {segment.value}
389
+ </code>
390
+ ) : (
391
+ <span key={i}>{segment.value}</span>
392
+ ),
393
+ )}
394
+ </p>
395
+ );
396
+ }
397
+
398
+ /** Provider-verbatim fact rendered small and quiet (category, header). */
399
+ function FactChip({ children }: { readonly children: ReactNode }) {
400
+ return (
401
+ <span className="shrink-0 rounded bg-muted-subtle px-1.5 py-0.5 text-xs text-muted-foreground">
402
+ {children}
403
+ </span>
404
+ );
405
+ }
406
+
407
+ function WhatsAppManagerLink({ label }: { readonly label: string }) {
408
+ return (
409
+ <a
410
+ href={WHATSAPP_MANAGER_TEMPLATES_URL}
411
+ target="_blank"
412
+ rel="noreferrer noopener"
413
+ className={cn(
414
+ "inline-flex shrink-0 items-center gap-1 text-xs font-medium text-foreground",
415
+ "underline underline-offset-2 hover:text-muted-foreground",
416
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded",
417
+ )}
418
+ >
419
+ {label}
420
+ <ExternalLink aria-hidden="true" className="size-3" />
421
+ </a>
422
+ );
423
+ }
424
+
425
+ function TemplatesSkeleton() {
426
+ return (
427
+ <div className="space-y-2 p-1">
428
+ {[1, 2, 3].map((i) => (
429
+ <div key={i} className="h-20 animate-pulse rounded-lg bg-muted-faint" />
430
+ ))}
431
+ </div>
432
+ );
433
+ }
@@ -613,6 +613,79 @@ describe("AgentChannelsPanel", () => {
613
613
  expect(screen.queryByText(/including credentials/i)).toBeNull();
614
614
  });
615
615
 
616
+ it("offers Templates on WhatsApp cards and opens the dialog from the menu", async () => {
617
+ const client = createMockStigmer({
618
+ channels: [makeWhatsAppChannel()],
619
+ });
620
+ render(
621
+ <Providers client={client}>
622
+ <AgentChannelsPanel agent={makeAgent()} />
623
+ </Providers>,
624
+ );
625
+
626
+ await waitFor(() =>
627
+ expect(screen.getByText("Support WhatsApp")).toBeTruthy(),
628
+ );
629
+ fireEvent.click(screen.getByRole("button", { name: /actions for/i }));
630
+ fireEvent.click(await screen.findByRole("menuitem", { name: "Templates" }));
631
+
632
+ expect(
633
+ await screen.findByRole("heading", { name: "Templates" }),
634
+ ).toBeTruthy();
635
+ // This channel has no proactive grant — the dialog teaches instead
636
+ // of firing a doomed listTemplates call (project DD-007 D4).
637
+ expect(
638
+ screen.getByText("Business-initiated messaging is not enabled"),
639
+ ).toBeTruthy();
640
+ });
641
+
642
+ it("offers no Templates item on providers without a template registry", async () => {
643
+ const client = createMockStigmer({
644
+ channels: [makeChannel({ teamName: "Acme HQ" })],
645
+ });
646
+ render(
647
+ <Providers client={client}>
648
+ <AgentChannelsPanel agent={makeAgent()} />
649
+ </Providers>,
650
+ );
651
+
652
+ await waitFor(() => expect(screen.getByText("Support Slack")).toBeTruthy());
653
+ fireEvent.click(screen.getByRole("button", { name: /actions for/i }));
654
+
655
+ // Slack has no template concept: hidden, not disabled — a disabled
656
+ // item would imply the capability might one day exist there.
657
+ expect(
658
+ await screen.findByRole("menuitem", { name: /conversations/i }),
659
+ ).toBeTruthy();
660
+ expect(screen.queryByRole("menuitem", { name: "Templates" })).toBeNull();
661
+ });
662
+
663
+ it("hides Templates from viewers — the server's listTemplates bar is can_edit", async () => {
664
+ const checkMyPermission = vi
665
+ .fn()
666
+ .mockResolvedValue({ isAuthorized: false });
667
+ const client = createMockStigmer({
668
+ channels: [makeWhatsAppChannel()],
669
+ checkMyPermission,
670
+ });
671
+ render(
672
+ <Providers client={client}>
673
+ <AgentChannelsPanel agent={makeAgent()} />
674
+ </Providers>,
675
+ );
676
+
677
+ await waitFor(() =>
678
+ expect(screen.getByText("Support WhatsApp")).toBeTruthy(),
679
+ );
680
+ await waitFor(() => expect(checkMyPermission).toHaveBeenCalled());
681
+ fireEvent.click(screen.getByRole("button", { name: /actions for/i }));
682
+
683
+ expect(
684
+ await screen.findByRole("menuitem", { name: /conversations/i }),
685
+ ).toBeTruthy();
686
+ expect(screen.queryByRole("menuitem", { name: "Templates" })).toBeNull();
687
+ });
688
+
616
689
  it("renders an error state when the list fails to load", async () => {
617
690
  const client = {
618
691
  agentChannel: {