@stigmer/react 3.6.0 → 3.8.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 (260) hide show
  1. package/billing/CreditLedgerTable.d.ts.map +1 -1
  2. package/billing/CreditLedgerTable.js +2 -7
  3. package/billing/CreditLedgerTable.js.map +1 -1
  4. package/channel/AgentChannelsPanel.d.ts.map +1 -1
  5. package/channel/AgentChannelsPanel.js +21 -2
  6. package/channel/AgentChannelsPanel.js.map +1 -1
  7. package/channel/ChannelConversationsDialog.d.ts +12 -4
  8. package/channel/ChannelConversationsDialog.d.ts.map +1 -1
  9. package/channel/ChannelConversationsDialog.js +18 -24
  10. package/channel/ChannelConversationsDialog.js.map +1 -1
  11. package/channel/providers.d.ts +23 -0
  12. package/channel/providers.d.ts.map +1 -1
  13. package/channel/providers.js +4 -0
  14. package/channel/providers.js.map +1 -1
  15. package/channel/useSaveAgentChannel.d.ts +5 -4
  16. package/channel/useSaveAgentChannel.d.ts.map +1 -1
  17. package/channel/useSaveAgentChannel.js +18 -4
  18. package/channel/useSaveAgentChannel.js.map +1 -1
  19. package/composer/ComposerToolbar.d.ts +6 -1
  20. package/composer/ComposerToolbar.d.ts.map +1 -1
  21. package/composer/ComposerToolbar.js +2 -2
  22. package/composer/ComposerToolbar.js.map +1 -1
  23. package/composer/SessionComposer.d.ts +9 -0
  24. package/composer/SessionComposer.d.ts.map +1 -1
  25. package/composer/SessionComposer.js +13 -2
  26. package/composer/SessionComposer.js.map +1 -1
  27. package/conversation/ConversationAttentionBanner.d.ts +22 -0
  28. package/conversation/ConversationAttentionBanner.d.ts.map +1 -0
  29. package/conversation/ConversationAttentionBanner.js +25 -0
  30. package/conversation/ConversationAttentionBanner.js.map +1 -0
  31. package/conversation/ConversationComposer.d.ts +41 -0
  32. package/conversation/ConversationComposer.d.ts.map +1 -0
  33. package/conversation/ConversationComposer.js +57 -0
  34. package/conversation/ConversationComposer.js.map +1 -0
  35. package/conversation/ConversationControlBanner.d.ts +52 -0
  36. package/conversation/ConversationControlBanner.d.ts.map +1 -0
  37. package/conversation/ConversationControlBanner.js +59 -0
  38. package/conversation/ConversationControlBanner.js.map +1 -0
  39. package/conversation/ConversationListPane.d.ts +68 -0
  40. package/conversation/ConversationListPane.d.ts.map +1 -0
  41. package/conversation/ConversationListPane.js +120 -0
  42. package/conversation/ConversationListPane.js.map +1 -0
  43. package/conversation/ConversationTimelineView.d.ts +46 -0
  44. package/conversation/ConversationTimelineView.d.ts.map +1 -0
  45. package/conversation/ConversationTimelineView.js +166 -0
  46. package/conversation/ConversationTimelineView.js.map +1 -0
  47. package/conversation/ConversationsWorkbench.d.ts +63 -0
  48. package/conversation/ConversationsWorkbench.d.ts.map +1 -0
  49. package/conversation/ConversationsWorkbench.js +165 -0
  50. package/conversation/ConversationsWorkbench.js.map +1 -0
  51. package/conversation/conversationPresentation.d.ts +137 -0
  52. package/conversation/conversationPresentation.d.ts.map +1 -0
  53. package/conversation/conversationPresentation.js +230 -0
  54. package/conversation/conversationPresentation.js.map +1 -0
  55. package/conversation/index.d.ts +26 -0
  56. package/conversation/index.d.ts.map +1 -0
  57. package/conversation/index.js +14 -0
  58. package/conversation/index.js.map +1 -0
  59. package/conversation/polling.d.ts +39 -0
  60. package/conversation/polling.d.ts.map +1 -0
  61. package/conversation/polling.js +39 -0
  62. package/conversation/polling.js.map +1 -0
  63. package/conversation/useConversation.d.ts +61 -0
  64. package/conversation/useConversation.d.ts.map +1 -0
  65. package/conversation/useConversation.js +153 -0
  66. package/conversation/useConversation.js.map +1 -0
  67. package/conversation/useConversationList.d.ts +98 -0
  68. package/conversation/useConversationList.d.ts.map +1 -0
  69. package/conversation/useConversationList.js +200 -0
  70. package/conversation/useConversationList.js.map +1 -0
  71. package/conversation/useConversationParticipation.d.ts +63 -0
  72. package/conversation/useConversationParticipation.d.ts.map +1 -0
  73. package/conversation/useConversationParticipation.js +84 -0
  74. package/conversation/useConversationParticipation.js.map +1 -0
  75. package/conversation/useConversationTimeline.d.ts +71 -0
  76. package/conversation/useConversationTimeline.d.ts.map +1 -0
  77. package/conversation/useConversationTimeline.js +152 -0
  78. package/conversation/useConversationTimeline.js.map +1 -0
  79. package/conversation/useConversationsWantsHumanCount.d.ts +42 -0
  80. package/conversation/useConversationsWantsHumanCount.d.ts.map +1 -0
  81. package/conversation/useConversationsWantsHumanCount.js +40 -0
  82. package/conversation/useConversationsWantsHumanCount.js.map +1 -0
  83. package/execution/useCreateAgentExecution.d.ts +14 -0
  84. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  85. package/execution/useCreateAgentExecution.js +5 -0
  86. package/execution/useCreateAgentExecution.js.map +1 -1
  87. package/index.d.ts +6 -2
  88. package/index.d.ts.map +1 -1
  89. package/index.js +14 -5
  90. package/index.js.map +1 -1
  91. package/internal/Pagination.d.ts +15 -0
  92. package/internal/Pagination.d.ts.map +1 -0
  93. package/internal/Pagination.js +20 -0
  94. package/internal/Pagination.js.map +1 -0
  95. package/internal/tooltip.d.ts.map +1 -1
  96. package/internal/tooltip.js +7 -1
  97. package/internal/tooltip.js.map +1 -1
  98. package/internal/useAutoScroll.d.ts +25 -4
  99. package/internal/useAutoScroll.d.ts.map +1 -1
  100. package/internal/useAutoScroll.js +55 -15
  101. package/internal/useAutoScroll.js.map +1 -1
  102. package/internal/useFetch.d.ts.map +1 -1
  103. package/internal/useFetch.js +14 -4
  104. package/internal/useFetch.js.map +1 -1
  105. package/models/ModelSelector.d.ts +45 -1
  106. package/models/ModelSelector.d.ts.map +1 -1
  107. package/models/ModelSelector.js +49 -6
  108. package/models/ModelSelector.js.map +1 -1
  109. package/models/index.d.ts +2 -0
  110. package/models/index.d.ts.map +1 -1
  111. package/models/index.js +1 -0
  112. package/models/index.js.map +1 -1
  113. package/models/registry.d.ts +11 -0
  114. package/models/registry.d.ts.map +1 -1
  115. package/models/registry.js +3 -0
  116. package/models/registry.js.map +1 -1
  117. package/models/service-tier.d.ts +27 -0
  118. package/models/service-tier.d.ts.map +1 -0
  119. package/models/service-tier.js +34 -0
  120. package/models/service-tier.js.map +1 -0
  121. package/package.json +4 -4
  122. package/schedule/ScheduleDetailView.d.ts +31 -8
  123. package/schedule/ScheduleDetailView.d.ts.map +1 -1
  124. package/schedule/ScheduleDetailView.js +441 -29
  125. package/schedule/ScheduleDetailView.js.map +1 -1
  126. package/schedule/ScheduleForm.d.ts +7 -4
  127. package/schedule/ScheduleForm.d.ts.map +1 -1
  128. package/schedule/ScheduleForm.js +94 -8
  129. package/schedule/ScheduleForm.js.map +1 -1
  130. package/schedule/ScheduleRunsTable.d.ts +72 -0
  131. package/schedule/ScheduleRunsTable.d.ts.map +1 -0
  132. package/schedule/ScheduleRunsTable.js +161 -0
  133. package/schedule/ScheduleRunsTable.js.map +1 -0
  134. package/schedule/index.d.ts +6 -0
  135. package/schedule/index.d.ts.map +1 -1
  136. package/schedule/index.js +3 -0
  137. package/schedule/index.js.map +1 -1
  138. package/schedule/useScheduleRuns.d.ts +44 -0
  139. package/schedule/useScheduleRuns.d.ts.map +1 -0
  140. package/schedule/useScheduleRuns.js +48 -0
  141. package/schedule/useScheduleRuns.js.map +1 -0
  142. package/schedule/useTriggerSchedule.d.ts +23 -10
  143. package/schedule/useTriggerSchedule.d.ts.map +1 -1
  144. package/schedule/useTriggerSchedule.js +28 -9
  145. package/schedule/useTriggerSchedule.js.map +1 -1
  146. package/schedule/useUpdateScheduleSpec.d.ts +49 -0
  147. package/schedule/useUpdateScheduleSpec.d.ts.map +1 -0
  148. package/schedule/useUpdateScheduleSpec.js +70 -0
  149. package/schedule/useUpdateScheduleSpec.js.map +1 -0
  150. package/session/useNewSessionFlow.d.ts.map +1 -1
  151. package/session/useNewSessionFlow.js +1 -0
  152. package/session/useNewSessionFlow.js.map +1 -1
  153. package/session/useSessionConversation.d.ts +11 -0
  154. package/session/useSessionConversation.d.ts.map +1 -1
  155. package/session/useSessionConversation.js +1 -0
  156. package/session/useSessionConversation.js.map +1 -1
  157. package/session/useSessionPageFlow.d.ts.map +1 -1
  158. package/session/useSessionPageFlow.js +1 -0
  159. package/session/useSessionPageFlow.js.map +1 -1
  160. package/sharing/useSaveAgentShare.d.ts +10 -1
  161. package/sharing/useSaveAgentShare.d.ts.map +1 -1
  162. package/sharing/useSaveAgentShare.js +9 -0
  163. package/sharing/useSaveAgentShare.js.map +1 -1
  164. package/sidebar/WorkspaceSidebar.d.ts +12 -4
  165. package/sidebar/WorkspaceSidebar.d.ts.map +1 -1
  166. package/sidebar/WorkspaceSidebar.js +19 -7
  167. package/sidebar/WorkspaceSidebar.js.map +1 -1
  168. package/src/billing/CreditLedgerTable.tsx +3 -42
  169. package/src/channel/AgentChannelsPanel.tsx +33 -2
  170. package/src/channel/ChannelConversationsDialog.tsx +22 -25
  171. package/src/channel/__tests__/AgentChannelsPanel.test.tsx +36 -5
  172. package/src/channel/__tests__/ChannelConversationsDialog.test.tsx +38 -1
  173. package/src/channel/__tests__/useSaveAgentChannel.test.tsx +26 -0
  174. package/src/channel/providers.ts +27 -0
  175. package/src/channel/useSaveAgentChannel.ts +18 -4
  176. package/src/composer/ComposerToolbar.tsx +9 -0
  177. package/src/composer/SessionComposer.tsx +24 -1
  178. package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +157 -0
  179. package/src/conversation/ConversationAttentionBanner.tsx +77 -0
  180. package/src/conversation/ConversationComposer.tsx +156 -0
  181. package/src/conversation/ConversationControlBanner.tsx +193 -0
  182. package/src/conversation/ConversationListPane.tsx +459 -0
  183. package/src/conversation/ConversationTimelineView.tsx +435 -0
  184. package/src/conversation/ConversationsWorkbench.tsx +378 -0
  185. package/src/conversation/__tests__/ConversationAttentionBanner.test.tsx +82 -0
  186. package/src/conversation/__tests__/ConversationComposer.test.tsx +160 -0
  187. package/src/conversation/__tests__/ConversationControlBanner.test.tsx +190 -0
  188. package/src/conversation/__tests__/ConversationListPane.test.tsx +372 -0
  189. package/src/conversation/__tests__/ConversationTimelineView.layout.test.tsx +160 -0
  190. package/src/conversation/__tests__/ConversationTimelineView.test.tsx +405 -0
  191. package/src/conversation/__tests__/ConversationsWorkbench.test.tsx +860 -0
  192. package/src/conversation/__tests__/a11y/conversation-surfaces.a11y.test.tsx +235 -0
  193. package/src/conversation/__tests__/a11y/harness.tsx +100 -0
  194. package/src/conversation/__tests__/conversation-tooltips.layout.test.tsx +167 -0
  195. package/src/conversation/__tests__/conversationPollingComposition.test.tsx +138 -0
  196. package/src/conversation/__tests__/conversationPresentation.test.ts +289 -0
  197. package/src/conversation/__tests__/useConversation.test.tsx +243 -0
  198. package/src/conversation/__tests__/useConversationList.test.tsx +492 -0
  199. package/src/conversation/__tests__/useConversationParticipation.test.tsx +190 -0
  200. package/src/conversation/__tests__/useConversationTimeline.test.tsx +258 -0
  201. package/src/conversation/__tests__/useConversationsWantsHumanCount.test.tsx +102 -0
  202. package/src/conversation/conversationPresentation.ts +305 -0
  203. package/src/conversation/index.ts +80 -0
  204. package/src/conversation/polling.ts +41 -0
  205. package/src/conversation/useConversation.ts +221 -0
  206. package/src/conversation/useConversationList.ts +328 -0
  207. package/src/conversation/useConversationParticipation.ts +178 -0
  208. package/src/conversation/useConversationTimeline.ts +244 -0
  209. package/src/conversation/useConversationsWantsHumanCount.ts +86 -0
  210. package/src/execution/useCreateAgentExecution.ts +18 -0
  211. package/src/index.ts +76 -4
  212. package/src/internal/Pagination.tsx +72 -0
  213. package/src/internal/__tests__/fake-timer-slices.ts +29 -0
  214. package/src/internal/__tests__/useAutoScroll.layout.test.tsx +143 -0
  215. package/src/internal/__tests__/useAutoScroll.test.tsx +62 -5
  216. package/src/internal/__tests__/useFetch-poll-under-render-pressure.test.tsx +79 -0
  217. package/src/internal/tooltip.tsx +6 -1
  218. package/src/internal/useAutoScroll.ts +66 -20
  219. package/src/internal/useFetch.ts +15 -4
  220. package/src/models/ModelSelector.tsx +133 -4
  221. package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +256 -0
  222. package/src/models/__tests__/useModelRegistry.test.tsx +15 -0
  223. package/src/models/index.ts +2 -0
  224. package/src/models/registry.ts +17 -0
  225. package/src/models/service-tier.ts +49 -0
  226. package/src/schedule/ScheduleDetailView.tsx +1179 -111
  227. package/src/schedule/ScheduleForm.tsx +218 -7
  228. package/src/schedule/ScheduleRunsTable.tsx +424 -0
  229. package/src/schedule/__tests__/ScheduleDetailView.test.tsx +431 -8
  230. package/src/schedule/__tests__/scheduleCreation.test.tsx +197 -0
  231. package/src/schedule/__tests__/scheduleHooks.test.tsx +94 -7
  232. package/src/schedule/index.ts +12 -0
  233. package/src/schedule/useScheduleRuns.ts +93 -0
  234. package/src/schedule/useTriggerSchedule.ts +39 -14
  235. package/src/schedule/useUpdateScheduleSpec.ts +108 -0
  236. package/src/session/useNewSessionFlow.ts +1 -0
  237. package/src/session/useSessionConversation.ts +12 -0
  238. package/src/session/useSessionPageFlow.ts +1 -0
  239. package/src/sharing/__tests__/useSaveAgentShare.test.tsx +55 -1
  240. package/src/sharing/__tests__/useShareToolReadiness.test.tsx +1 -0
  241. package/src/sharing/useSaveAgentShare.ts +19 -1
  242. package/src/sidebar/WorkspaceSidebar.tsx +62 -3
  243. package/src/sidebar/__tests__/WorkspaceSidebar.test.tsx +56 -1
  244. package/src/workflow/__tests__/inspector-forms.test.tsx +119 -2
  245. package/src/workflow/inspector/forms/AgentCallForm.tsx +260 -37
  246. package/src/workflow/inspector/tabs/RuntimeTab.tsx +72 -33
  247. package/src/workflow/starter-workflow-yaml.ts +2 -1
  248. package/styles.css +1 -1
  249. package/workflow/inspector/forms/AgentCallForm.d.ts +18 -2
  250. package/workflow/inspector/forms/AgentCallForm.d.ts.map +1 -1
  251. package/workflow/inspector/forms/AgentCallForm.js +87 -9
  252. package/workflow/inspector/forms/AgentCallForm.js.map +1 -1
  253. package/workflow/inspector/tabs/RuntimeTab.d.ts +2 -1
  254. package/workflow/inspector/tabs/RuntimeTab.d.ts.map +1 -1
  255. package/workflow/inspector/tabs/RuntimeTab.js +31 -5
  256. package/workflow/inspector/tabs/RuntimeTab.js.map +1 -1
  257. package/workflow/starter-workflow-yaml.d.ts +1 -1
  258. package/workflow/starter-workflow-yaml.d.ts.map +1 -1
  259. package/workflow/starter-workflow-yaml.js +2 -1
  260. package/workflow/starter-workflow-yaml.js.map +1 -1
@@ -0,0 +1,156 @@
1
+ "use client";
2
+
3
+ import { useId, useState } from "react";
4
+ import { Send, TriangleAlert } 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 { SendChannelMessageOutput } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
9
+ import { Button } from "../button/Button.js";
10
+ import { SpinnerIcon } from "../composer/icons.js";
11
+ import { useComposer } from "../composer/useComposer.js";
12
+
13
+ /** Props for {@link ConversationComposer}. */
14
+ export interface ConversationComposerProps {
15
+ /** Send a staff reply (from `useConversationParticipation().reply`). */
16
+ readonly onSend: (text: string) => Promise<SendChannelMessageOutput>;
17
+ /** `true` while a reply is in flight. */
18
+ readonly isSending: boolean;
19
+ /**
20
+ * Why replying is unavailable, when it is — rendered in place of the
21
+ * input (a senderless provider, or a conversation the customer has
22
+ * not started yet). `null` enables the composer.
23
+ */
24
+ readonly disabledReason: string | null;
25
+ /**
26
+ * A pre-send forecast that ANNOTATES the enabled input — the
27
+ * `disabledReason` pattern's sibling with the opposite contract
28
+ * (channel-conversations DD-014 D-e): a closed service window is a
29
+ * forecast, not a structural block, so the input stays usable and the
30
+ * send engines remain the authority. Rendered above the input and
31
+ * associated via `aria-describedby` (never a live region: the
32
+ * advisory is state a reader meets on open, not an event). Compute it
33
+ * with `serviceWindowOf`; `null` or omitted renders nothing.
34
+ */
35
+ readonly advisory?: string | null;
36
+ /** Additional classes for the composer container. */
37
+ readonly className?: string;
38
+ }
39
+
40
+ /**
41
+ * The staff reply composer. Sending is an implicit takeover — the agent
42
+ * goes quiet the moment a staff reply lands, before the send itself —
43
+ * so this composer is only ever rendered alongside the control banner
44
+ * that shows that state change.
45
+ *
46
+ * Outcome honesty: `refused` keeps the draft (the words never left) and
47
+ * renders the server's refusal detail verbatim; `queued` clears it and
48
+ * says the platform is retrying; a thrown failure keeps the draft and
49
+ * renders the error. The timeline shows the delivered item via its own
50
+ * read — this component never fabricates one.
51
+ */
52
+ export function ConversationComposer({
53
+ onSend,
54
+ isSending,
55
+ disabledReason,
56
+ advisory,
57
+ className,
58
+ }: ConversationComposerProps) {
59
+ const advisoryId = useId();
60
+ const hasAdvisory = advisory != null && advisory !== "";
61
+ const [notice, setNotice] = useState<{
62
+ readonly kind: "refused" | "queued" | "error";
63
+ readonly text: string;
64
+ } | null>(null);
65
+
66
+ const composer = useComposer({
67
+ onSubmit: (message) => {
68
+ setNotice(null);
69
+ onSend(message).then(
70
+ (output) => {
71
+ if (output.outcome === ChannelSendOutcome.refused) {
72
+ // The words never left; keep them so the user can adjust.
73
+ composer.setMessage(message);
74
+ setNotice({
75
+ kind: "refused",
76
+ text: output.detail || "The provider refused this message.",
77
+ });
78
+ } else if (output.outcome === ChannelSendOutcome.queued) {
79
+ setNotice({
80
+ kind: "queued",
81
+ text: "Delivery hit a transient issue — the platform is retrying in the background.",
82
+ });
83
+ }
84
+ },
85
+ (err) => {
86
+ composer.setMessage(message);
87
+ setNotice({ kind: "error", text: getUserMessage(err) });
88
+ },
89
+ );
90
+ },
91
+ disabled: disabledReason !== null || isSending,
92
+ });
93
+
94
+ if (disabledReason !== null) {
95
+ return (
96
+ <div className={cn("border-t border-border px-4 py-3", className)}>
97
+ <p className="text-center text-xs text-muted-foreground">{disabledReason}</p>
98
+ </div>
99
+ );
100
+ }
101
+
102
+ return (
103
+ <div className={cn("border-t border-border px-4 py-3", className)}>
104
+ {/* The persistent forecast sits above the transient send-outcome
105
+ notice: the notice is the more recent event and belongs closest
106
+ to the input it reacts to. */}
107
+ {hasAdvisory && (
108
+ <p
109
+ id={advisoryId}
110
+ className="mb-2 flex items-start gap-1.5 rounded-md bg-status-degraded-subtle px-2.5 py-1.5 text-xs text-status-degraded"
111
+ >
112
+ <TriangleAlert aria-hidden="true" className="mt-0.5 size-3.5 shrink-0" />
113
+ <span className="min-w-0">{advisory}</span>
114
+ </p>
115
+ )}
116
+ {notice && (
117
+ <p
118
+ className={cn(
119
+ "mb-2 text-xs",
120
+ notice.kind === "queued" ? "text-muted-foreground" : "text-destructive",
121
+ )}
122
+ role="status"
123
+ >
124
+ {notice.text}
125
+ </p>
126
+ )}
127
+ <div className="flex items-end gap-2">
128
+ <textarea
129
+ {...composer.textareaProps}
130
+ rows={1}
131
+ placeholder="Reply as your business…"
132
+ aria-label="Reply to the customer"
133
+ aria-describedby={hasAdvisory ? advisoryId : undefined}
134
+ className={cn(
135
+ "min-h-9 flex-1 resize-none rounded-md border border-border bg-background",
136
+ "px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground-faint",
137
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
138
+ )}
139
+ />
140
+ <Button
141
+ variant="primary"
142
+ size="sm"
143
+ onClick={composer.submit}
144
+ disabled={!composer.canSubmit || isSending}
145
+ aria-label="Send reply"
146
+ >
147
+ {/* The house in-flight glyph (the session composer's pattern):
148
+ the spinner replaces the Send icon so the button itself
149
+ reports progress, not just its label. */}
150
+ {isSending ? <SpinnerIcon /> : <Send aria-hidden="true" className="size-4" />}
151
+ {isSending ? "Sending…" : "Send"}
152
+ </Button>
153
+ </div>
154
+ </div>
155
+ );
156
+ }
@@ -0,0 +1,193 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Bot, User } from "lucide-react";
5
+ import { cn } from "@stigmer/theme";
6
+ import { getUserMessage } from "@stigmer/sdk";
7
+ import {
8
+ ConversationControl,
9
+ type ChannelConversation,
10
+ } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/conversation_io_pb";
11
+ import { Button } from "../button/Button.js";
12
+ import type { UseConversationParticipationReturn } from "./useConversationParticipation.js";
13
+
14
+ /** Props for {@link ConversationControlBanner}. */
15
+ export interface ConversationControlBannerProps {
16
+ /** The conversation's fresh participation state. */
17
+ readonly conversation: ChannelConversation;
18
+ /** The participation commands (from `useConversationParticipation`). */
19
+ readonly participation: UseConversationParticipationReturn;
20
+ /**
21
+ * `true` when the customer's last message has not been answered —
22
+ * the conversation row's server-derived `awaiting_reply` fact, which
23
+ * is bounce-aware: a staff reply the provider later failed does not
24
+ * count as an answer (channel-conversations F-28/DD-015). Arms the
25
+ * handback confirm guard (DD-007 D-e): handing back runs NO turn, so
26
+ * the agent stays quiet until the customer next speaks, and the
27
+ * unanswered state must be unmissable. Never derive this from
28
+ * timeline authorship — the send attempt's presence on the timeline
29
+ * says nothing about whether the customer received it.
30
+ */
31
+ readonly unansweredCustomer: boolean;
32
+ /**
33
+ * Whether the channel's provider has a staff send lane. Senderless
34
+ * providers disable takeover with the reason — a takeover there would
35
+ * silence the agent while no human reply could reach the customer,
36
+ * which is exactly what the server refuses.
37
+ */
38
+ readonly supportsStaffReplies: boolean;
39
+ /**
40
+ * The signed-in staff member's identity account id, when the host
41
+ * knows it — lets the banner attribute a human hold: "You have this
42
+ * conversation" when it matches, "A teammate has this conversation"
43
+ * when it differs. When omitted the banner only states that a human
44
+ * holds it — the holder may be the viewer themself, so "a teammate"
45
+ * would be a guess. Optional: the SDK never assumes a host's auth
46
+ * state.
47
+ */
48
+ readonly currentIdentityAccountId?: string;
49
+ /** Additional classes for the banner container. */
50
+ readonly className?: string;
51
+ }
52
+
53
+ /**
54
+ * Who may speak to the customer, and the controls that move it: take
55
+ * over (the agent goes quiet) and hand back (the agent resumes, with
56
+ * the context it missed injected on its next turn).
57
+ *
58
+ * Truth rules: state renders from the conversation row the server
59
+ * answered — a takeover that lost its race renders the WINNER's hold,
60
+ * because that is what happened. Failures render verbatim beside the
61
+ * control that caused them.
62
+ */
63
+ export function ConversationControlBanner({
64
+ conversation,
65
+ participation,
66
+ unansweredCustomer,
67
+ supportsStaffReplies,
68
+ currentIdentityAccountId,
69
+ className,
70
+ }: ConversationControlBannerProps) {
71
+ const [confirmingHandBack, setConfirmingHandBack] = useState(false);
72
+
73
+ const humanHeld = conversation.control === ConversationControl.control_human;
74
+ const heldByMe =
75
+ humanHeld &&
76
+ currentIdentityAccountId !== undefined &&
77
+ conversation.controlledBy === currentIdentityAccountId;
78
+
79
+ const { takeOver, handBack, pendingCommands, commandErrors } = participation;
80
+ const error = commandErrors.get("takeOver") ?? commandErrors.get("handBack");
81
+
82
+ const requestHandBack = () => {
83
+ if (unansweredCustomer) {
84
+ setConfirmingHandBack(true);
85
+ return;
86
+ }
87
+ void handBack().catch(() => {
88
+ // Recorded in commandErrors and rendered below.
89
+ });
90
+ };
91
+
92
+ return (
93
+ <div
94
+ className={cn(
95
+ "border-b border-border px-4 py-2",
96
+ humanHeld ? "bg-primary-subtle" : "bg-muted-faint",
97
+ className,
98
+ )}
99
+ >
100
+ <div className="flex flex-wrap items-center justify-between gap-2">
101
+ <p className="flex items-center gap-1.5 text-sm text-foreground">
102
+ {humanHeld ? (
103
+ <>
104
+ <User aria-hidden="true" className="size-4 text-muted-foreground" />
105
+ {heldByMe
106
+ ? "You have this conversation — the agent is quiet until you hand it back."
107
+ : currentIdentityAccountId !== undefined
108
+ ? "A teammate has this conversation — the agent is quiet until handback."
109
+ : // Without the host's identity the holder may be the
110
+ // viewer themself — claiming "a teammate" would be a
111
+ // guess. State only what the row proves: a human
112
+ // holds it (channel-conversations F-01).
113
+ "This conversation is with a human — the agent is quiet until handback."}
114
+ </>
115
+ ) : (
116
+ <>
117
+ <Bot aria-hidden="true" className="size-4 text-muted-foreground" />
118
+ The agent is serving this conversation.
119
+ </>
120
+ )}
121
+ </p>
122
+
123
+ {confirmingHandBack ? (
124
+ <div className="flex items-center gap-2">
125
+ <span className="text-xs text-destructive">
126
+ The customer&apos;s last message has no reply — the agent stays
127
+ quiet until they write again.
128
+ </span>
129
+ <Button
130
+ variant="outline"
131
+ size="xs"
132
+ onClick={() => {
133
+ setConfirmingHandBack(false);
134
+ void handBack().catch(() => {
135
+ // Recorded in commandErrors and rendered below.
136
+ });
137
+ }}
138
+ disabled={pendingCommands.has("handBack")}
139
+ >
140
+ Hand back anyway
141
+ </Button>
142
+ <Button
143
+ variant="primary"
144
+ size="xs"
145
+ onClick={() => setConfirmingHandBack(false)}
146
+ >
147
+ Keep the conversation
148
+ </Button>
149
+ </div>
150
+ ) : humanHeld ? (
151
+ <Button
152
+ variant="outline"
153
+ size="xs"
154
+ onClick={requestHandBack}
155
+ disabled={pendingCommands.has("handBack")}
156
+ >
157
+ {pendingCommands.has("handBack") ? "Handing back…" : "Hand back to agent"}
158
+ </Button>
159
+ ) : (
160
+ <div className="flex items-center gap-2">
161
+ {!supportsStaffReplies && (
162
+ // Visible text, never a native title (F-18): a disabled
163
+ // Button swallows hover (disabled:pointer-events-none) and
164
+ // leaves the tab order, so a title on it is unreachable by
165
+ // every input method. Kept short — the composer's own
166
+ // notice already explains the missing staff reply lane.
167
+ <span className="text-xs text-muted-foreground">
168
+ This channel&apos;s provider has no staff send lane yet — a
169
+ takeover would silence the agent with no way to reply.
170
+ </span>
171
+ )}
172
+ <Button
173
+ variant="outline"
174
+ size="xs"
175
+ onClick={() =>
176
+ void takeOver().catch(() => {
177
+ // Recorded in commandErrors and rendered below.
178
+ })
179
+ }
180
+ disabled={!supportsStaffReplies || pendingCommands.has("takeOver")}
181
+ >
182
+ {pendingCommands.has("takeOver") ? "Taking over…" : "Take over"}
183
+ </Button>
184
+ </div>
185
+ )}
186
+ </div>
187
+
188
+ {error && (
189
+ <p className="mt-1 text-xs text-destructive">{getUserMessage(error)}</p>
190
+ )}
191
+ </div>
192
+ );
193
+ }