@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,120 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { memo, useCallback, useRef } from "react";
4
+ import { timestampDate } from "@bufbuild/protobuf/wkt";
5
+ import { MessageSquare, TriangleAlert, User } from "lucide-react";
6
+ import { cn } from "@stigmer/theme";
7
+ import { getUserMessage } from "@stigmer/sdk";
8
+ import { ChannelConversationListFilter, ConversationControl, } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/conversation_io_pb";
9
+ import { formatRelativeTime } from "../activity/format-relative-time.js";
10
+ import { channelProviderOf } from "../channel/providers.js";
11
+ import { Button } from "../button/Button.js";
12
+ import { EmptyState } from "../empty-state/EmptyState.js";
13
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "../internal/tooltip.js";
14
+ import { awaitingIndicatorOf, conversationContactOf, conversationLabelOf, } from "./conversationPresentation.js";
15
+ /**
16
+ * The conversation inbox pane (channel-conversations T04): every
17
+ * conversation across the org's channels, newest activity first, with
18
+ * control, attention, and awaiting-reply indicators.
19
+ *
20
+ * Presentational — pair with `useConversationList` and
21
+ * `useOrgAgentChannelList`, or compose via `ConversationsWorkbench`.
22
+ * The wants-human filter is SERVER-evaluated (DD-011 D-g closed the API
23
+ * gap that T04 D1 cited): a client-side tab over one fetched page would
24
+ * silently lie across pages, so the pane only reports the choice and the
25
+ * list hook sends it to the server.
26
+ */
27
+ export function ConversationListPane({ conversations, isLoading, error, hasMore, loadMore, isLoadingMore, channels, channelFilter, onChannelFilterChange, filter = ChannelConversationListFilter.channel_conversation_list_filter_unspecified, onFilterChange, selected, onSelect, now, className, }) {
28
+ const providerById = new Map(channels.map((channel) => [
29
+ channel.metadata?.id ?? "",
30
+ channelProviderOf(channel.spec?.providerConfig?.case),
31
+ ]));
32
+ return (_jsxs("div", { "aria-label": "Conversation list", className: cn("flex min-h-0 flex-col", className), children: [(onFilterChange !== undefined || channels.length > 1) && (_jsxs("div", { className: "flex flex-col gap-2 border-b border-border px-3 py-2", children: [onFilterChange !== undefined && (_jsx(ConversationFilterToggle, { value: filter, onChange: onFilterChange })), channels.length > 1 && (_jsxs(_Fragment, { children: [_jsx("label", { className: "sr-only", htmlFor: "stgm-conversation-channel-filter", children: "Filter by channel" }), _jsxs("select", { id: "stgm-conversation-channel-filter", value: channelFilter, onChange: (e) => onChannelFilterChange(e.target.value), className: cn("w-full rounded-md border border-border bg-background px-2 py-1.5", "text-sm text-foreground", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"), children: [_jsx("option", { value: "", children: "All channels" }), channels.map((channel) => (_jsx("option", { value: channel.metadata?.id, children: channel.metadata?.name || channel.metadata?.slug }, channel.metadata?.id)))] })] }))] })), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-2", children: isLoading ? (_jsx(ListSkeleton, {})) : error ? (_jsx("p", { className: "px-2 py-6 text-center text-sm text-destructive", children: getUserMessage(error) })) : conversations.length === 0 ? (channels.length === 0 ? (_jsx(EmptyState, { variant: "first-use", icon: _jsx(MessageSquare, { className: "size-8" }), title: "No channels to watch", description: "Conversations arrive through an agent's channels. Connect a channel \u2014 or ask a channel owner for access \u2014 and customer conversations appear here." })) : filter === ChannelConversationListFilter.filter_wants_human ? (
33
+ // Its own empty state on purpose: under the filter, "No
34
+ // conversations yet — when a customer messages…" would lie
35
+ // to an org with a full inbox and nothing needing a person.
36
+ _jsx(EmptyState, { variant: "first-use", icon: _jsx(MessageSquare, { className: "size-8" }), title: "Nothing needs a human right now", description: "Conversations appear here when the agent asks for attention, or when a human-held conversation has a customer message awaiting a reply." })) : (_jsx(EmptyState, { variant: "first-use", icon: _jsx(MessageSquare, { className: "size-8" }), title: "No conversations yet", description: "When a customer messages one of your channels, their conversation appears here." }))) : (_jsxs(_Fragment, { children: [_jsx(TooltipProvider, { children: _jsx("ul", { className: "space-y-0.5", children: conversations.map((conversation) => (_jsx(ConversationRow, { conversation: conversation, label: conversationLabelOf(conversation, providerById.get(conversation.agentChannelId)?.id ?? null), contact: conversationContactOf(conversation, providerById.get(conversation.agentChannelId)?.id ?? null), isSelected: selected?.agentChannelId === conversation.agentChannelId &&
37
+ selected?.conversationKey === conversation.conversationKey, onSelect: onSelect, now: now }, `${conversation.agentChannelId}:${conversation.conversationKey}`))) }) }), hasMore && (_jsx("div", { className: "flex justify-center py-2", children: _jsx(Button, { variant: "ghost", size: "xs", onClick: loadMore, disabled: isLoadingMore, children: isLoadingMore ? "Loading…" : "Show more" }) }))] })) })] }));
38
+ }
39
+ const FILTER_OPTIONS = [
40
+ {
41
+ value: ChannelConversationListFilter.channel_conversation_list_filter_unspecified,
42
+ label: "All",
43
+ ariaLabel: "All conversations",
44
+ },
45
+ {
46
+ value: ChannelConversationListFilter.filter_wants_human,
47
+ label: "Needs human",
48
+ ariaLabel: "Conversations where a human action is wanted",
49
+ },
50
+ ];
51
+ /**
52
+ * Segmented control for the server-evaluated list filter. Renders as a
53
+ * WAI-ARIA Radio Group with roving tabindex and arrow-key navigation —
54
+ * the `ScopeToggle` pattern (its second sighting; extract a shared
55
+ * primitive if a third segmented control appears).
56
+ *
57
+ * "Needs human" is exactly the nav badge's predicate (DD-011 D-f/D-g),
58
+ * so the number on the badge and the list this control opens can never
59
+ * disagree.
60
+ */
61
+ function ConversationFilterToggle({ value, onChange, }) {
62
+ const optionRefs = useRef([]);
63
+ const handleSelect = useCallback((next) => {
64
+ if (next !== value)
65
+ onChange(next);
66
+ }, [value, onChange]);
67
+ const handleKeyDown = useCallback((e, index) => {
68
+ let nextIndex = null;
69
+ if (e.key === "ArrowRight" || e.key === "ArrowDown") {
70
+ e.preventDefault();
71
+ nextIndex = (index + 1) % FILTER_OPTIONS.length;
72
+ }
73
+ else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
74
+ e.preventDefault();
75
+ nextIndex = (index - 1 + FILTER_OPTIONS.length) % FILTER_OPTIONS.length;
76
+ }
77
+ if (nextIndex !== null) {
78
+ optionRefs.current[nextIndex]?.focus();
79
+ handleSelect(FILTER_OPTIONS[nextIndex].value);
80
+ }
81
+ }, [handleSelect]);
82
+ return (_jsx("div", { role: "radiogroup", "aria-label": "Conversation filter", className: "inline-flex self-start rounded-md bg-muted p-0.5", children: FILTER_OPTIONS.map((option, index) => {
83
+ const isSelected = value === option.value;
84
+ return (_jsx("button", { ref: (el) => {
85
+ optionRefs.current[index] = el;
86
+ }, type: "button", role: "radio", "aria-checked": isSelected, "aria-label": option.ariaLabel, tabIndex: isSelected ? 0 : -1, onClick: () => handleSelect(option.value), onKeyDown: (e) => handleKeyDown(e, index), className: cn("inline-flex cursor-pointer items-center rounded-sm px-2 py-1 text-xs font-medium transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", isSelected
87
+ ? "bg-background text-foreground shadow-sm"
88
+ : "text-muted-foreground hover:text-foreground"), children: option.label }, option.value));
89
+ }) }));
90
+ }
91
+ const ConversationRow = memo(function ConversationRow({ conversation, label, contact, isSelected, onSelect, now, }) {
92
+ const humanHeld = conversation.control === ConversationControl.control_human;
93
+ const awaiting = awaitingIndicatorOf(conversation);
94
+ const lastActivity = conversation.lastActivityAt
95
+ ? timestampDate(conversation.lastActivityAt)
96
+ : null;
97
+ return (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onSelect(conversation), "aria-current": isSelected ? "true" : undefined, className: cn("flex w-full items-center gap-2.5 rounded-md px-2.5 py-2 text-left", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", isSelected ? "bg-primary-subtle" : "hover:bg-accent"), children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("p", { className: "truncate text-sm font-medium text-foreground", children: label }), conversation.needsAttention && (
98
+ // The reason rides the house tooltip (F-18). The trigger
99
+ // renders as a span — it sits INSIDE the row button, so a
100
+ // default (button) trigger would nest buttons — and stays
101
+ // out of the tab order; the sr-only text remains the
102
+ // accessible name. The full reason is also always visible
103
+ // in the open conversation's attention banner.
104
+ _jsxs(Tooltip, { children: [_jsxs(TooltipTrigger, { render: _jsx("span", { className: "shrink-0" }), children: [_jsx(TriangleAlert, { "aria-hidden": "true", className: "size-3.5 text-destructive" }), _jsx("span", { className: "sr-only", children: conversation.attentionReason
105
+ ? `Needs attention: ${conversation.attentionReason}`
106
+ : "Needs attention" })] }), _jsx(TooltipContent, { side: "top", children: conversation.attentionReason || "Needs attention" })] }))] }), contact !== null && (_jsx("p", { className: "mt-0.5 truncate text-xs text-muted-foreground-faint", children: contact })), humanHeld && (_jsxs("p", { className: "mt-0.5 flex items-center gap-1 text-xs text-muted-foreground", children: [_jsx(User, { "aria-hidden": "true", className: "size-3" }), "Human has the conversation"] }))] }), (lastActivity !== null || awaiting !== null) && (_jsxs("span", { className: "flex shrink-0 flex-col items-end gap-1", children: [lastActivity && (_jsx("span", { className: "text-xs text-muted-foreground-faint", children: formatRelativeTime(lastActivity, now) })), awaiting && (
107
+ // The WhatsApp inbox convention: an awaiting conversation
108
+ // carries a dot under its timestamp. Strength maps the
109
+ // holder (DD-011 D-a): strong when a human holds it — the
110
+ // agent will not answer, a person must — muted when the
111
+ // agent does. No tooltip and no tab stop (the F-18
112
+ // discipline); the sr-only text is the accessible meaning.
113
+ _jsxs("span", { className: "flex", children: [_jsx("span", { "aria-hidden": "true", className: cn("size-2 rounded-full", awaiting === "strong" ? "bg-primary" : "bg-muted-foreground") }), _jsx("span", { className: "sr-only", children: awaiting === "strong"
114
+ ? "Customer awaiting reply — the conversation is human-held"
115
+ : "Customer awaiting reply" })] }))] }))] }) }));
116
+ });
117
+ function ListSkeleton() {
118
+ return (_jsx("div", { className: "space-y-1 p-1", "aria-hidden": "true", children: [1, 2, 3, 4].map((i) => (_jsx("div", { className: "h-11 animate-pulse rounded-md bg-muted-faint" }, i))) }));
119
+ }
120
+ //# sourceMappingURL=ConversationListPane.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConversationListPane.js","sourceRoot":"","sources":["../../src/conversation/ConversationListPane.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import type { ConversationTimelineItem } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/conversation_io_pb";
2
+ import { type ChannelProviderId } from "../channel/providers.js";
3
+ /** Props for {@link ConversationTimelineView}. */
4
+ export interface ConversationTimelineViewProps {
5
+ /** Timeline items in chronological order (from `useConversationTimeline`). */
6
+ readonly items: readonly ConversationTimelineItem[];
7
+ /** `true` only during the first load. */
8
+ readonly isLoading: boolean;
9
+ /** Error from the timeline read, or `null`. */
10
+ readonly error: Error | null;
11
+ /** `true` while older history exists beyond what is loaded. */
12
+ readonly hasOlder: boolean;
13
+ /** Load the next older page. */
14
+ readonly loadOlder: () => void;
15
+ /** `true` while an older page is in flight. */
16
+ readonly isLoadingOlder: boolean;
17
+ /**
18
+ * The channel's provider, for the honest-timeline notice: providers
19
+ * whose inbound lane has no timeline source (Slack) render an
20
+ * explicit "customer messages aren't shown" notice instead of a
21
+ * silently one-sided thread. `null` renders no notice.
22
+ */
23
+ readonly provider: ChannelProviderId | null;
24
+ /** Frozen instant for deterministic hosts (tests, documentation tours). */
25
+ readonly now?: Date;
26
+ /** Additional classes for the view container. */
27
+ readonly className?: string;
28
+ }
29
+ /**
30
+ * One conversation's customer-visible thread, WhatsApp-style: customer
31
+ * on the left, the business (agent, teammate, platform copy) on the
32
+ * right, internal-lane events (escalations, attention clears) as inline
33
+ * system rows the customer never saw.
34
+ *
35
+ * Honesty rules, enforced here rather than hoped for: outbound items
36
+ * carry their send attempt AND the provider's receipt as two independent
37
+ * facts (DD-004 D-d — an item can be handed to the provider yet fail on
38
+ * the customer's side), failed and suppressed sends render as exactly
39
+ * that, and non-text inbound renders a typed placeholder instead of
40
+ * disappearing.
41
+ *
42
+ * Presentational — pair with `useConversationTimeline`, or compose via
43
+ * `ConversationsWorkbench`.
44
+ */
45
+ export declare function ConversationTimelineView({ items, isLoading, error, hasOlder, loadOlder, isLoadingOlder, provider, now, className, }: ConversationTimelineViewProps): import("react/jsx-runtime").JSX.Element;
46
+ //# sourceMappingURL=ConversationTimelineView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConversationTimelineView.d.ts","sourceRoot":"","sources":["../../src/conversation/ConversationTimelineView.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uEAAuE,CAAC;AACtH,OAAO,EAAqB,KAAK,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAoBpF,kDAAkD;AAClD,MAAM,WAAW,6BAA6B;IAC5C,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,SAAS,wBAAwB,EAAE,CAAC;IACpD,yCAAyC;IACzC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,gCAAgC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,IAAI,CAAC;IAC/B,+CAA+C;IAC/C,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC5C,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC;IACpB,iDAAiD;IACjD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,KAAK,EACL,SAAS,EACT,KAAK,EACL,QAAQ,EACR,SAAS,EACT,cAAc,EACd,QAAQ,EACR,GAAG,EACH,SAAS,GACV,EAAE,6BAA6B,2CAgF/B"}
@@ -0,0 +1,166 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { memo, useMemo } from "react";
4
+ import { timestampDate } from "@bufbuild/protobuf/wkt";
5
+ import { Ban, Check, CheckCheck, Clock, Info, MessageSquare, TriangleAlert } from "lucide-react";
6
+ import { cn } from "@stigmer/theme";
7
+ import { getUserMessage } from "@stigmer/sdk";
8
+ import { channelProviderOf } from "../channel/providers.js";
9
+ import { Button } from "../button/Button.js";
10
+ import { EmptyState } from "../empty-state/EmptyState.js";
11
+ import { JumpToLatestButton } from "../internal/JumpToLatestButton.js";
12
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "../internal/tooltip.js";
13
+ import { useAutoScroll } from "../internal/useAutoScroll.js";
14
+ import { authorKindOf, inboundPlaceholderOf, isInternalItem, receiptOf, sendAttemptOf, } from "./conversationPresentation.js";
15
+ /**
16
+ * One conversation's customer-visible thread, WhatsApp-style: customer
17
+ * on the left, the business (agent, teammate, platform copy) on the
18
+ * right, internal-lane events (escalations, attention clears) as inline
19
+ * system rows the customer never saw.
20
+ *
21
+ * Honesty rules, enforced here rather than hoped for: outbound items
22
+ * carry their send attempt AND the provider's receipt as two independent
23
+ * facts (DD-004 D-d — an item can be handed to the provider yet fail on
24
+ * the customer's side), failed and suppressed sends render as exactly
25
+ * that, and non-text inbound renders a typed placeholder instead of
26
+ * disappearing.
27
+ *
28
+ * Presentational — pair with `useConversationTimeline`, or compose via
29
+ * `ConversationsWorkbench`.
30
+ */
31
+ export function ConversationTimelineView({ items, isLoading, error, hasOlder, loadOlder, isLoadingOlder, provider, now, className, }) {
32
+ const { scrollRef, sentinelRef, contentRef, isFollowing, jumpToLatest } = useAutoScroll();
33
+ const days = useMemo(() => groupByDay(items, now), [items, now]);
34
+ const descriptor = channelProviderOf(provider ?? undefined);
35
+ const showsCustomerMessages = descriptor?.timelineIncludesCustomerMessages ?? true;
36
+ return (
37
+ // The provider is context-only (no DOM node); one per view arms
38
+ // every status-glyph tooltip below (the WorkspaceSidebar precedent).
39
+ _jsx(TooltipProvider, { children: _jsxs("div", { "aria-label": "Conversation timeline", className: cn("relative flex min-h-0 flex-1 flex-col", className), children: [_jsxs("div", { ref: scrollRef, className: "min-h-0 flex-1 overflow-y-auto [overflow-anchor:none] px-4 py-3", children: [!showsCustomerMessages && descriptor && (_jsxs("div", { className: "mb-3 flex items-start gap-2 rounded-md border border-border bg-muted-faint px-3 py-2 text-xs text-muted-foreground", children: [_jsx(Info, { "aria-hidden": "true", className: "mt-0.5 size-3.5 shrink-0" }), _jsxs("p", { children: ["Customer messages on ", descriptor.label, " channels aren't shown here yet \u2014 this thread has replies, sends, and internal events only."] })] })), isLoading ? (_jsx(TimelineSkeleton, {})) : error ? (_jsx("p", { className: "px-2 py-6 text-center text-sm text-destructive", children: getUserMessage(error) })) : items.length === 0 ? (_jsx(EmptyState, { variant: "first-use", icon: _jsx(MessageSquare, { className: "size-8" }), title: "No messages yet", description: "The conversation's messages appear here as they happen." })) : (_jsxs("div", { ref: contentRef, children: [hasOlder && (_jsx("div", { className: "flex justify-center pb-2", children: _jsx(Button, { variant: "ghost", size: "xs", onClick: loadOlder, disabled: isLoadingOlder, children: isLoadingOlder ? "Loading…" : "Load earlier messages" }) })), days.map((day) => (_jsxs("div", { children: [_jsx("div", { className: "my-2 text-center text-xs text-muted-foreground-faint", role: "separator", children: day.label }), _jsx("ul", { className: "space-y-1.5", children: day.items.map((item) => (_jsx(TimelineItemRow, { item: item }, item.itemId))) })] }, day.label)))] })), _jsx("div", { ref: sentinelRef, "aria-hidden": "true" })] }), _jsx(JumpToLatestButton, { visible: !isFollowing, onClick: jumpToLatest })] }) }));
40
+ }
41
+ const BUSINESS_CAPTION = {
42
+ agent: "Agent",
43
+ teammate: "Teammate",
44
+ platform: "Automated",
45
+ unknown: "Unknown sender",
46
+ };
47
+ const TimelineItemRow = memo(function TimelineItemRow({ item, }) {
48
+ if (isInternalItem(item)) {
49
+ return _jsx(InternalEventRow, { item: item });
50
+ }
51
+ const author = authorKindOf(item);
52
+ const isCustomer = author === "customer";
53
+ const body = item.text || inboundPlaceholderOf(item);
54
+ // DD-014 D-c (amended at the Sitting 2 gate, R-1): the provider's
55
+ // failure explanation is decision-bearing — window closed vs bad
56
+ // number changes what the operator does next — so it renders as
57
+ // VISIBLE text, not hover-only (the footer glyphs are deliberately
58
+ // non-focusable per F-18, which makes a tooltip mouse-only). Verbatim
59
+ // relay, never pattern-matched; the numeric twin (receipt_error_code)
60
+ // stays off the surface as machine vocabulary. Gated exactly like
61
+ // ReceiptTicks' failed arm — attempt delivered AND receipt failed —
62
+ // so the attempt-axis explanation (last_error, F-25's slice) can
63
+ // structurally never leak in here.
64
+ const receiptExplanation = sendAttemptOf(item) === "delivered" &&
65
+ receiptOf(item) === "failed" &&
66
+ item.receiptDetail !== ""
67
+ ? item.receiptDetail
68
+ : null;
69
+ return (_jsx("li", { className: cn("flex", isCustomer ? "justify-start" : "justify-end"), children: _jsxs("div", { className: cn("max-w-[80%] rounded-lg px-3 py-2", isCustomer ? "bg-muted-subtle" : "bg-primary-subtle"), children: [!isCustomer && (_jsx("p", { className: "text-xs font-medium text-muted-foreground", children: BUSINESS_CAPTION[author] })), _jsx("p", { className: cn("whitespace-pre-wrap break-words text-sm text-foreground", !item.text && "italic text-muted-foreground"), children: body || "Message content unavailable" }), receiptExplanation !== null && (_jsx("p", { className: "mt-1 break-words text-xs text-destructive", children: receiptExplanation })), _jsx(ItemFooter, { item: item })] }) }));
70
+ });
71
+ /**
72
+ * A footer status glyph with its explanation on the house tooltip
73
+ * (F-18, replacing native `title` — OS-delayed, imprecise, invisible to
74
+ * keyboard and touch). The trigger renders as a plain `<span>` and is
75
+ * deliberately NOT focusable: these sit inside every bubble's footer,
76
+ * so focusable triggers would add several tab stops per message. Their
77
+ * accessible name stays the visible or `sr-only` text; the tooltip is
78
+ * the visual description only.
79
+ */
80
+ function StatusHint({ hint, className, children, }) {
81
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx("span", { className: className }), children: children }), _jsx(TooltipContent, { side: "top", children: hint })] }));
82
+ }
83
+ /**
84
+ * The bubble footer: the item's instant plus, for outbound items, the
85
+ * two status axes — our attempt and the provider's receipt, never
86
+ * collapsed into one glyph.
87
+ */
88
+ function ItemFooter({ item }) {
89
+ const attempt = sendAttemptOf(item);
90
+ const time = item.at
91
+ ? timestampDate(item.at).toLocaleTimeString(undefined, {
92
+ hour: "2-digit",
93
+ minute: "2-digit",
94
+ })
95
+ : "";
96
+ return (_jsxs("p", { className: "mt-1 flex items-center justify-end gap-1 text-xs text-muted-foreground-faint", children: [_jsx("span", { children: time }), attempt === "failed" && (_jsxs(StatusHint, { hint: "The platform could not deliver this message", className: "flex items-center gap-0.5 text-destructive", children: [_jsx(TriangleAlert, { "aria-hidden": "true", className: "size-3" }), "Not delivered"] })), attempt === "suppressed" && (_jsxs(StatusHint, { hint: "Withheld because a human had the conversation when it came due", className: "flex items-center gap-0.5", children: [_jsx(Ban, { "aria-hidden": "true", className: "size-3" }), "Held"] })), (attempt === "pending" || attempt === "delivering") && (_jsxs(StatusHint, { hint: "Sending", className: "flex items-center gap-0.5", children: [_jsx(Clock, { "aria-hidden": "true", className: "size-3" }), _jsx("span", { className: "sr-only", children: "Sending" })] })), attempt === "delivered" && _jsx(ReceiptTicks, { item: item })] }));
97
+ }
98
+ /**
99
+ * The provider's receipt report, WhatsApp-tick style. Renders only on
100
+ * items the platform successfully handed to the provider; the receipt
101
+ * can still be `failed` — the "we sent it, it never arrived" case a
102
+ * collapsed status hides.
103
+ */
104
+ function ReceiptTicks({ item }) {
105
+ const receipt = receiptOf(item);
106
+ if (receipt === "failed") {
107
+ return (_jsxs(StatusHint, { hint: "The provider accepted this message but could not deliver it", className: "flex items-center gap-0.5 text-destructive", children: [_jsx(TriangleAlert, { "aria-hidden": "true", className: "size-3" }), "Delivery failed"] }));
108
+ }
109
+ if (receipt === "read") {
110
+ return (_jsxs(StatusHint, { hint: "Read by the customer", className: "text-primary", children: [_jsx(CheckCheck, { "aria-hidden": "true", className: "size-3.5" }), _jsx("span", { className: "sr-only", children: "Read" })] }));
111
+ }
112
+ if (receipt === "delivered") {
113
+ return (_jsxs(StatusHint, { hint: "Delivered to the customer's device", children: [_jsx(CheckCheck, { "aria-hidden": "true", className: "size-3.5" }), _jsx("span", { className: "sr-only", children: "Delivered" })] }));
114
+ }
115
+ // Handed to the provider; no (further) receipt yet.
116
+ return (_jsxs(StatusHint, { hint: "Sent", children: [_jsx(Check, { "aria-hidden": "true", className: "size-3.5" }), _jsx("span", { className: "sr-only", children: "Sent" })] }));
117
+ }
118
+ /**
119
+ * Internal-lane rows — org-internal events the customer never saw,
120
+ * rendered as centered system copy so they can never be misread as
121
+ * messages: the agent's escalation (its reason verbatim) and a
122
+ * teammate's attention clear.
123
+ */
124
+ function InternalEventRow({ item }) {
125
+ const author = authorKindOf(item);
126
+ const isEscalation = author === "agent" && item.text !== "";
127
+ return (_jsx("li", { className: "flex justify-center", children: _jsx("div", { className: cn("flex max-w-[90%] items-start gap-1.5 rounded-md px-3 py-1.5 text-center text-xs", isEscalation
128
+ ? "bg-destructive-subtle text-destructive"
129
+ : "bg-muted-faint text-muted-foreground"), children: isEscalation ? (_jsxs(_Fragment, { children: [_jsx(TriangleAlert, { "aria-hidden": "true", className: "mt-0.5 size-3.5 shrink-0" }), _jsxs("span", { className: "min-w-0 break-words", children: ["Agent escalated to a human \u2014 \u201C", item.text, "\u201D"] })] })) : item.text !== "" ? (
130
+ // A future internal note flows through with zero code change.
131
+ _jsx("span", { className: "min-w-0 break-words", children: item.text })) : (_jsx("span", { children: "A teammate resolved the attention flag" })) }) }));
132
+ }
133
+ function TimelineSkeleton() {
134
+ return (_jsxs("div", { className: "space-y-2", "aria-hidden": "true", children: [_jsx("div", { className: "h-10 w-3/5 animate-pulse rounded-lg bg-muted-faint" }), _jsx("div", { className: "ml-auto h-10 w-3/5 animate-pulse rounded-lg bg-muted-faint" }), _jsx("div", { className: "h-10 w-2/5 animate-pulse rounded-lg bg-muted-faint" })] }));
135
+ }
136
+ /** Group chronological items under day separators. */
137
+ function groupByDay(items, now) {
138
+ const ref = now ?? new Date();
139
+ const groups = [];
140
+ let currentKey = "";
141
+ for (const item of items) {
142
+ const date = item.at ? timestampDate(item.at) : null;
143
+ const key = date ? date.toDateString() : "undated";
144
+ if (key !== currentKey) {
145
+ currentKey = key;
146
+ groups.push({ label: date ? dayLabel(date, ref) : "Earlier", items: [] });
147
+ }
148
+ groups[groups.length - 1].items.push(item);
149
+ }
150
+ return groups;
151
+ }
152
+ function dayLabel(date, ref) {
153
+ if (date.toDateString() === ref.toDateString())
154
+ return "Today";
155
+ const yesterday = new Date(ref);
156
+ yesterday.setDate(ref.getDate() - 1);
157
+ if (date.toDateString() === yesterday.toDateString())
158
+ return "Yesterday";
159
+ return date.toLocaleDateString(undefined, {
160
+ weekday: "short",
161
+ month: "short",
162
+ day: "numeric",
163
+ ...(date.getFullYear() === ref.getFullYear() ? {} : { year: "numeric" }),
164
+ });
165
+ }
166
+ //# sourceMappingURL=ConversationTimelineView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConversationTimelineView.js","sourceRoot":"","sources":["../../src/conversation/ConversationTimelineView.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ import type { AgentChannel } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
2
+ import { type ConversationIdentity } from "./ConversationListPane.js";
3
+ /**
4
+ * Context handed to a function-form
5
+ * {@link ConversationsWorkbenchProps.headerAccessory}.
6
+ */
7
+ export interface ConversationHeaderContext {
8
+ /**
9
+ * The open conversation's channel, once the org's channel list
10
+ * answers — `null` while it loads. Carries the channel's name (for
11
+ * scope-explicit accessories like the "Channel access" trigger) and
12
+ * `spec.agent_ref` (for navigation), so hosts never re-fetch what the
13
+ * workbench already holds.
14
+ */
15
+ readonly channel: AgentChannel | null;
16
+ }
17
+ /** Props for {@link ConversationsWorkbench}. */
18
+ export interface ConversationsWorkbenchProps {
19
+ /** Organization whose conversations to show. */
20
+ readonly org: string;
21
+ /**
22
+ * The open conversation. Controlled by the host so selection can live
23
+ * in the URL (deep links, reloads, back button).
24
+ */
25
+ readonly selected: ConversationIdentity | null;
26
+ /** Called when the user opens or leaves a conversation. */
27
+ readonly onSelectionChange: (selection: ConversationIdentity | null) => void;
28
+ /**
29
+ * The signed-in staff member's identity account id, when the host
30
+ * knows it — used only for "You have this conversation" copy.
31
+ */
32
+ readonly currentIdentityAccountId?: string;
33
+ /**
34
+ * Rendered in the open conversation's header (the host's seam for
35
+ * page-level actions — e.g. the channel access panel trigger). The
36
+ * function form receives {@link ConversationHeaderContext} so the
37
+ * accessory can name the channel it acts on.
38
+ */
39
+ readonly headerAccessory?: React.ReactNode | ((context: ConversationHeaderContext) => React.ReactNode);
40
+ /**
41
+ * Maps the open conversation's channel to the host's channel surface
42
+ * — the owning agent's Channels tab (channels have no standalone
43
+ * page). When provided, the header's channel name renders as a link;
44
+ * `null` for a specific channel keeps it plain text. The SDK never
45
+ * assumes a routing scheme (DD-004).
46
+ */
47
+ readonly channelHref?: (channel: AgentChannel) => string | null;
48
+ /** Frozen instant for deterministic hosts (tests, documentation tours). */
49
+ readonly now?: Date;
50
+ /** Additional classes for the workbench container. */
51
+ readonly className?: string;
52
+ }
53
+ /**
54
+ * The Conversations console surface (channel-conversations T04), fully
55
+ * composed: the org-wide inbox on the left; the open conversation's
56
+ * timeline, participation banners, and staff composer on the right.
57
+ *
58
+ * Everything under it is independently importable — hosts that need a
59
+ * different layout compose `ConversationListPane`,
60
+ * `ConversationTimelineView`, the banners, and the hooks directly.
61
+ */
62
+ export declare function ConversationsWorkbench({ org, selected, onSelectionChange, currentIdentityAccountId, headerAccessory, channelHref, now, className, }: ConversationsWorkbenchProps): import("react/jsx-runtime").JSX.Element;
63
+ //# sourceMappingURL=ConversationsWorkbench.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConversationsWorkbench.d.ts","sourceRoot":"","sources":["../../src/conversation/ConversationsWorkbench.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2DAA2D,CAAC;AAY9F,OAAO,EAAwB,KAAK,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAa5F;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAAC;CACvC;AAED,gDAAgD;AAChD,MAAM,WAAW,2BAA2B;IAC1C,gDAAgD;IAChD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC/C,2DAA2D;IAC3D,QAAQ,CAAC,iBAAiB,EAAE,CAAC,SAAS,EAAE,oBAAoB,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7E;;;OAGG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,CAAC,EACrB,KAAK,CAAC,SAAS,GACf,CAAC,CAAC,OAAO,EAAE,yBAAyB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAC9D;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,MAAM,GAAG,IAAI,CAAC;IAChE,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC;IACpB,sDAAsD;IACtD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,GAAG,EACH,QAAQ,EACR,iBAAiB,EACjB,wBAAwB,EACxB,eAAe,EACf,WAAW,EACX,GAAG,EACH,SAAS,GACV,EAAE,2BAA2B,2CAmR7B"}
@@ -0,0 +1,165 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useMemo, useState } from "react";
4
+ import { MessagesSquare } from "lucide-react";
5
+ import { cn } from "@stigmer/theme";
6
+ import { ChannelConversationListFilter, } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/conversation_io_pb";
7
+ import { ChannelSendOutcome } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
8
+ import { channelProviderOf } from "../channel/providers.js";
9
+ import { useOrgAgentChannelList } from "../channel/useOrgAgentChannelList.js";
10
+ import { EmptyState } from "../empty-state/EmptyState.js";
11
+ import { ConversationAttentionBanner } from "./ConversationAttentionBanner.js";
12
+ import { ConversationComposer } from "./ConversationComposer.js";
13
+ import { ConversationControlBanner } from "./ConversationControlBanner.js";
14
+ import { ConversationListPane } from "./ConversationListPane.js";
15
+ import { ConversationTimelineView } from "./ConversationTimelineView.js";
16
+ import { conversationContactOf, conversationLabelOf, outboundItemIdOf, serviceWindowOf, } from "./conversationPresentation.js";
17
+ import { useConversation } from "./useConversation.js";
18
+ import { useConversationList } from "./useConversationList.js";
19
+ import { useConversationParticipation } from "./useConversationParticipation.js";
20
+ import { useConversationTimeline } from "./useConversationTimeline.js";
21
+ /**
22
+ * The Conversations console surface (channel-conversations T04), fully
23
+ * composed: the org-wide inbox on the left; the open conversation's
24
+ * timeline, participation banners, and staff composer on the right.
25
+ *
26
+ * Everything under it is independently importable — hosts that need a
27
+ * different layout compose `ConversationListPane`,
28
+ * `ConversationTimelineView`, the banners, and the hooks directly.
29
+ */
30
+ export function ConversationsWorkbench({ org, selected, onSelectionChange, currentIdentityAccountId, headerAccessory, channelHref, now, className, }) {
31
+ const [channelFilter, setChannelFilter] = useState("");
32
+ const [listFilter, setListFilter] = useState(ChannelConversationListFilter.channel_conversation_list_filter_unspecified);
33
+ const { channels } = useOrgAgentChannelList(org || null);
34
+ const list = useConversationList({
35
+ org: org || null,
36
+ agentChannelId: channelFilter,
37
+ filter: listFilter,
38
+ });
39
+ const detail = useConversation(selected?.agentChannelId ?? "", selected?.conversationKey ?? "");
40
+ const timeline = useConversationTimeline(selected?.agentChannelId ?? "", selected?.conversationKey ?? "");
41
+ // DD-012 D-a: a command answer is fresher than any in-flight poll on
42
+ // BOTH surfaces rendering this conversation — fan it out to the detail
43
+ // seam (banners, composer state) and the list seam (the inbox row), so
44
+ // your own takeover never lags the inbox (F-06).
45
+ const detailApply = detail.applyServerState;
46
+ const listApply = list.applyServerState;
47
+ const adoptConversation = useCallback((fresh) => {
48
+ detailApply(fresh);
49
+ listApply(fresh);
50
+ }, [detailApply, listApply]);
51
+ const participation = useConversationParticipation({
52
+ agentChannelId: selected?.agentChannelId ?? "",
53
+ conversationKey: selected?.conversationKey ?? "",
54
+ onConversation: adoptConversation,
55
+ });
56
+ const selectedChannel = useMemo(() => selected
57
+ ? channels.find((c) => c.metadata?.id === selected.agentChannelId) ?? null
58
+ : null, [channels, selected]);
59
+ const descriptor = channelProviderOf(selectedChannel?.spec?.providerConfig?.case);
60
+ // DD-007 D-e: the handback guard arms on the row's server-derived
61
+ // awaiting_reply fact — bounce-aware since T08 (a staff reply the
62
+ // provider later failed does NOT count as an answer), and platform
63
+ // acknowledgments never stamp it. Never re-derive this from timeline
64
+ // authorship: that derivation read a bounced staff reply as "answered"
65
+ // and went silent in exactly the scenario the guard exists for (F-28)
66
+ // — the client-side twin of the re-derivation DD-011 A-1 removed
67
+ // server-side. No loading gap: the Hand back button renders from this
68
+ // same row, so the guard and the control it protects appear together.
69
+ const unansweredCustomer = detail.conversation?.awaitingReply ?? false;
70
+ const composerDisabledReason = descriptor && !descriptor.supportsStaffReplies
71
+ ? `Staff replies aren't available on ${descriptor.label} channels yet — the agent keeps serving this conversation.`
72
+ : detail.awaitingCustomer
73
+ ? "The customer hasn't written yet — staff replies unlock with their first message."
74
+ : null;
75
+ // DD-014 D-b/D-e: the closed-window forecast, computed fresh on EVERY
76
+ // render — deliberately NOT memoized on [detail.conversation]: the
77
+ // row's reference is intentionally stable across identical polls
78
+ // (DD-010) while the window is wall-clock-anchored, so a memo keyed on
79
+ // the row would freeze this warning forever on a quiet conversation.
80
+ // The 5s detail poll's re-render is the refresh cadence; the workbench
81
+ // test "the 5s detail poll alone carries the advisory over the
82
+ // boundary" is the guard. The advisory names the rule (DD-014 D-c's
83
+ // one home for it); the failed tick relays the provider's verdict.
84
+ const serviceWindow = detail.conversation
85
+ ? serviceWindowOf(detail.conversation, descriptor?.id ?? null, now ?? new Date())
86
+ : null;
87
+ const composerAdvisory = serviceWindow === "closed" && descriptor
88
+ ? `${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.`
89
+ : null;
90
+ const handleSelect = useCallback((conversation) => onSelectionChange({
91
+ agentChannelId: conversation.agentChannelId,
92
+ conversationKey: conversation.conversationKey,
93
+ }), [onSelectionChange]);
94
+ // F-05: an accepted reply's ledger item is only visible once the next
95
+ // timeline read answers, so the composer's busy state must span
96
+ // click-to-visible, not end at the RPC. The settling record names the
97
+ // item the reply output promised (`ob:<outbound_message_id>`) and the
98
+ // selection it belongs to — carrying the selection makes stale state
99
+ // from a previously open conversation structurally inert (the derived
100
+ // id is null there) instead of needing a reset effect.
101
+ const [replySettling, setReplySettling] = useState(null);
102
+ const selectionKey = selected
103
+ ? `${selected.agentChannelId}:${selected.conversationKey}`
104
+ : "";
105
+ const settlingItemId = replySettling !== null && replySettling.selectionKey === selectionKey
106
+ ? replySettling.itemId
107
+ : null;
108
+ useEffect(() => {
109
+ if (settlingItemId === null)
110
+ return;
111
+ if (timeline.items.some((item) => item.itemId === settlingItemId)) {
112
+ setReplySettling(null);
113
+ }
114
+ }, [timeline.items, settlingItemId]);
115
+ const handleSend = useCallback(async (text) => {
116
+ const output = await participation.reply(text);
117
+ // The ledger row is committed before reply answers, so one head
118
+ // refresh renders the REAL item (with its true status) — the SDK
119
+ // never fabricates an optimistic item that might misstate what
120
+ // the provider did. Until that item lands, the composer stays
121
+ // busy; even if this refetch fails, the head poll delivers the
122
+ // item and clears the hold. A refusal never settles: it restores
123
+ // the draft for editing, and holding the composer busy would
124
+ // fight the correction its notice asks for.
125
+ if (output.outcome !== ChannelSendOutcome.refused &&
126
+ output.outboundMessageId !== "") {
127
+ setReplySettling({
128
+ selectionKey,
129
+ itemId: outboundItemIdOf(output.outboundMessageId),
130
+ });
131
+ }
132
+ timeline.refetch();
133
+ return output;
134
+ }, [participation.reply, timeline.refetch, selectionKey]);
135
+ const detailLabel = detail.conversation
136
+ ? conversationLabelOf(detail.conversation, descriptor?.id ?? null)
137
+ : selected?.conversationKey ?? "";
138
+ // The customer's reachable address (e.g. the WhatsApp number a display
139
+ // name hides) — the call-back path belongs beside the channel name.
140
+ const detailContact = detail.conversation
141
+ ? conversationContactOf(detail.conversation, descriptor?.id ?? null)
142
+ : null;
143
+ const channelName = selectedChannel
144
+ ? selectedChannel.metadata?.name || selectedChannel.metadata?.slug
145
+ : null;
146
+ const selectedChannelHref = selectedChannel && channelHref ? channelHref(selectedChannel) : null;
147
+ const headerAccessoryNode = typeof headerAccessory === "function"
148
+ ? headerAccessory({ channel: selectedChannel })
149
+ : headerAccessory;
150
+ return (_jsxs("div", { "aria-label": "Conversations workbench", className: cn("flex min-h-0 flex-1", className), children: [_jsx(ConversationListPane, { className: "w-80 shrink-0 border-r border-border", conversations: list.conversations, isLoading: list.isLoading, error: list.error, hasMore: list.hasMore, loadMore: list.loadMore, isLoadingMore: list.isLoadingMore, channels: channels, channelFilter: channelFilter, onChannelFilterChange: setChannelFilter, filter: listFilter, onFilterChange: setListFilter, selected: selected, onSelect: handleSelect, now: now }), selected === null ? (_jsx("div", { className: "flex min-w-0 flex-1 items-center justify-center p-6", children: _jsx(EmptyState, { variant: "first-use", icon: _jsx(MessagesSquare, { className: "size-8" }), title: "Select a conversation", description: "Pick a conversation to read its thread, reply as your business, or take over from the agent." }) })) : (
151
+ // Keyed by conversation identity (DD-014: `key` remounts are the
152
+ // clean-reset pattern): every open starts a fresh detail column, so
153
+ // auto-scroll opens following at the newest message and the
154
+ // composer's draft and notice can never travel from one customer's
155
+ // conversation into another's (F-22). The data hooks live above
156
+ // this column, so the remount refetches nothing; the inbox pane
157
+ // stays outside it so its scroll position and filter survive.
158
+ // Channel ids never contain ":", so the joined key is unambiguous.
159
+ _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-border px-4 py-2.5", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "truncate text-sm font-semibold text-foreground", children: detailLabel }), (detailContact !== null || channelName) && (_jsxs("p", { className: "truncate text-xs text-muted-foreground", children: [detailContact, detailContact !== null && channelName && " · ", channelName &&
160
+ // The path from a conversation to its channel (F-11):
161
+ // the channel name links to the host's channel surface
162
+ // when a route exists.
163
+ (selectedChannelHref ? (_jsx("a", { href: selectedChannelHref, className: "hover:text-foreground hover:underline", children: channelName })) : (channelName))] }))] }), headerAccessoryNode] }), detail.conversation && (_jsx(ConversationAttentionBanner, { conversation: detail.conversation, participation: participation })), detail.conversation && (_jsx(ConversationControlBanner, { conversation: detail.conversation, participation: participation, unansweredCustomer: unansweredCustomer, supportsStaffReplies: descriptor?.supportsStaffReplies ?? true, currentIdentityAccountId: currentIdentityAccountId })), _jsx(ConversationTimelineView, { items: timeline.items, isLoading: timeline.isLoading, error: timeline.error, hasOlder: timeline.hasOlder, loadOlder: timeline.loadOlder, isLoadingOlder: timeline.isLoadingOlder, provider: descriptor?.id ?? null, now: now }), _jsx(ConversationComposer, { onSend: handleSend, isSending: participation.pendingCommands.has("reply") || settlingItemId !== null, disabledReason: composerDisabledReason, advisory: composerAdvisory })] }, `${selected.agentChannelId}:${selected.conversationKey}`))] }));
164
+ }
165
+ //# sourceMappingURL=ConversationsWorkbench.js.map