@stigmer/react 3.7.0 → 3.9.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 (219) hide show
  1. package/attachment/AttachmentChipList.d.ts.map +1 -1
  2. package/attachment/AttachmentChipList.js +30 -1
  3. package/attachment/AttachmentChipList.js.map +1 -1
  4. package/attachment/attachment-utils.d.ts +11 -0
  5. package/attachment/attachment-utils.d.ts.map +1 -1
  6. package/attachment/attachment-utils.js +23 -0
  7. package/attachment/attachment-utils.js.map +1 -1
  8. package/attachment/clipboard.d.ts +51 -0
  9. package/attachment/clipboard.d.ts.map +1 -0
  10. package/attachment/clipboard.js +89 -0
  11. package/attachment/clipboard.js.map +1 -0
  12. package/attachment/index.d.ts +6 -1
  13. package/attachment/index.d.ts.map +1 -1
  14. package/attachment/index.js +4 -1
  15. package/attachment/index.js.map +1 -1
  16. package/attachment/prepare-image.d.ts +43 -0
  17. package/attachment/prepare-image.d.ts.map +1 -0
  18. package/attachment/prepare-image.js +162 -0
  19. package/attachment/prepare-image.js.map +1 -0
  20. package/attachment/useAttachments.d.ts.map +1 -1
  21. package/attachment/useAttachments.js +23 -3
  22. package/attachment/useAttachments.js.map +1 -1
  23. package/attachment/vision-fit.d.ts +50 -0
  24. package/attachment/vision-fit.d.ts.map +1 -0
  25. package/attachment/vision-fit.js +76 -0
  26. package/attachment/vision-fit.js.map +1 -0
  27. package/channel/AgentChannelsPanel.d.ts.map +1 -1
  28. package/channel/AgentChannelsPanel.js +21 -2
  29. package/channel/AgentChannelsPanel.js.map +1 -1
  30. package/channel/ChannelConversationsDialog.d.ts +12 -4
  31. package/channel/ChannelConversationsDialog.d.ts.map +1 -1
  32. package/channel/ChannelConversationsDialog.js +18 -24
  33. package/channel/ChannelConversationsDialog.js.map +1 -1
  34. package/channel/providers.d.ts +23 -0
  35. package/channel/providers.d.ts.map +1 -1
  36. package/channel/providers.js +4 -0
  37. package/channel/providers.js.map +1 -1
  38. package/channel/useSaveAgentChannel.d.ts +5 -4
  39. package/channel/useSaveAgentChannel.d.ts.map +1 -1
  40. package/channel/useSaveAgentChannel.js +18 -4
  41. package/channel/useSaveAgentChannel.js.map +1 -1
  42. package/composer/ComposerToolbar.d.ts +1 -1
  43. package/composer/ComposerToolbar.d.ts.map +1 -1
  44. package/composer/SessionComposer.d.ts +4 -2
  45. package/composer/SessionComposer.d.ts.map +1 -1
  46. package/composer/SessionComposer.js +51 -5
  47. package/composer/SessionComposer.js.map +1 -1
  48. package/conversation/ConversationAttentionBanner.d.ts +22 -0
  49. package/conversation/ConversationAttentionBanner.d.ts.map +1 -0
  50. package/conversation/ConversationAttentionBanner.js +25 -0
  51. package/conversation/ConversationAttentionBanner.js.map +1 -0
  52. package/conversation/ConversationComposer.d.ts +41 -0
  53. package/conversation/ConversationComposer.d.ts.map +1 -0
  54. package/conversation/ConversationComposer.js +57 -0
  55. package/conversation/ConversationComposer.js.map +1 -0
  56. package/conversation/ConversationControlBanner.d.ts +52 -0
  57. package/conversation/ConversationControlBanner.d.ts.map +1 -0
  58. package/conversation/ConversationControlBanner.js +59 -0
  59. package/conversation/ConversationControlBanner.js.map +1 -0
  60. package/conversation/ConversationListPane.d.ts +68 -0
  61. package/conversation/ConversationListPane.d.ts.map +1 -0
  62. package/conversation/ConversationListPane.js +120 -0
  63. package/conversation/ConversationListPane.js.map +1 -0
  64. package/conversation/ConversationTimelineView.d.ts +46 -0
  65. package/conversation/ConversationTimelineView.d.ts.map +1 -0
  66. package/conversation/ConversationTimelineView.js +166 -0
  67. package/conversation/ConversationTimelineView.js.map +1 -0
  68. package/conversation/ConversationsWorkbench.d.ts +63 -0
  69. package/conversation/ConversationsWorkbench.d.ts.map +1 -0
  70. package/conversation/ConversationsWorkbench.js +165 -0
  71. package/conversation/ConversationsWorkbench.js.map +1 -0
  72. package/conversation/conversationPresentation.d.ts +137 -0
  73. package/conversation/conversationPresentation.d.ts.map +1 -0
  74. package/conversation/conversationPresentation.js +230 -0
  75. package/conversation/conversationPresentation.js.map +1 -0
  76. package/conversation/index.d.ts +26 -0
  77. package/conversation/index.d.ts.map +1 -0
  78. package/conversation/index.js +14 -0
  79. package/conversation/index.js.map +1 -0
  80. package/conversation/polling.d.ts +39 -0
  81. package/conversation/polling.d.ts.map +1 -0
  82. package/conversation/polling.js +39 -0
  83. package/conversation/polling.js.map +1 -0
  84. package/conversation/useConversation.d.ts +61 -0
  85. package/conversation/useConversation.d.ts.map +1 -0
  86. package/conversation/useConversation.js +153 -0
  87. package/conversation/useConversation.js.map +1 -0
  88. package/conversation/useConversationList.d.ts +98 -0
  89. package/conversation/useConversationList.d.ts.map +1 -0
  90. package/conversation/useConversationList.js +200 -0
  91. package/conversation/useConversationList.js.map +1 -0
  92. package/conversation/useConversationParticipation.d.ts +63 -0
  93. package/conversation/useConversationParticipation.d.ts.map +1 -0
  94. package/conversation/useConversationParticipation.js +84 -0
  95. package/conversation/useConversationParticipation.js.map +1 -0
  96. package/conversation/useConversationTimeline.d.ts +71 -0
  97. package/conversation/useConversationTimeline.d.ts.map +1 -0
  98. package/conversation/useConversationTimeline.js +152 -0
  99. package/conversation/useConversationTimeline.js.map +1 -0
  100. package/conversation/useConversationsWantsHumanCount.d.ts +42 -0
  101. package/conversation/useConversationsWantsHumanCount.d.ts.map +1 -0
  102. package/conversation/useConversationsWantsHumanCount.js +40 -0
  103. package/conversation/useConversationsWantsHumanCount.js.map +1 -0
  104. package/index.d.ts +6 -2
  105. package/index.d.ts.map +1 -1
  106. package/index.js +11 -2
  107. package/index.js.map +1 -1
  108. package/internal/tooltip.d.ts.map +1 -1
  109. package/internal/tooltip.js +7 -1
  110. package/internal/tooltip.js.map +1 -1
  111. package/internal/useAutoScroll.d.ts +25 -4
  112. package/internal/useAutoScroll.d.ts.map +1 -1
  113. package/internal/useAutoScroll.js +55 -15
  114. package/internal/useAutoScroll.js.map +1 -1
  115. package/internal/useFetch.d.ts.map +1 -1
  116. package/internal/useFetch.js +14 -4
  117. package/internal/useFetch.js.map +1 -1
  118. package/models/ModelSelector.d.ts +24 -5
  119. package/models/ModelSelector.d.ts.map +1 -1
  120. package/models/ModelSelector.js +31 -9
  121. package/models/ModelSelector.js.map +1 -1
  122. package/models/registry.d.ts +3 -2
  123. package/models/registry.d.ts.map +1 -1
  124. package/models/registry.js.map +1 -1
  125. package/models/service-tier.d.ts +1 -1
  126. package/models/service-tier.d.ts.map +1 -1
  127. package/models/service-tier.js +1 -1
  128. package/models/service-tier.js.map +1 -1
  129. package/package.json +4 -4
  130. package/schedule/ScheduleDetailView.js +1 -1
  131. package/schedule/ScheduleDetailView.js.map +1 -1
  132. package/schedule/ScheduleForm.d.ts.map +1 -1
  133. package/schedule/ScheduleForm.js +7 -6
  134. package/schedule/ScheduleForm.js.map +1 -1
  135. package/sharing/useSaveAgentShare.d.ts +10 -1
  136. package/sharing/useSaveAgentShare.d.ts.map +1 -1
  137. package/sharing/useSaveAgentShare.js +9 -0
  138. package/sharing/useSaveAgentShare.js.map +1 -1
  139. package/sidebar/WorkspaceSidebar.d.ts +12 -4
  140. package/sidebar/WorkspaceSidebar.d.ts.map +1 -1
  141. package/sidebar/WorkspaceSidebar.js +19 -7
  142. package/sidebar/WorkspaceSidebar.js.map +1 -1
  143. package/src/attachment/AttachmentChipList.tsx +45 -1
  144. package/src/attachment/__tests__/AttachmentChipList.test.tsx +103 -0
  145. package/src/attachment/__tests__/attachment-utils.test.ts +54 -0
  146. package/src/attachment/__tests__/clipboard.test.ts +110 -0
  147. package/src/attachment/__tests__/prepare-image.browser.test.ts +166 -0
  148. package/src/attachment/__tests__/prepare-image.test.ts +32 -0
  149. package/src/attachment/__tests__/vision-fit.test.ts +93 -0
  150. package/src/attachment/attachment-utils.ts +27 -0
  151. package/src/attachment/clipboard.ts +102 -0
  152. package/src/attachment/index.ts +13 -0
  153. package/src/attachment/prepare-image.ts +179 -0
  154. package/src/attachment/useAttachments.ts +26 -2
  155. package/src/attachment/vision-fit.ts +90 -0
  156. package/src/channel/AgentChannelsPanel.tsx +33 -2
  157. package/src/channel/ChannelConversationsDialog.tsx +22 -25
  158. package/src/channel/__tests__/AgentChannelsPanel.test.tsx +36 -5
  159. package/src/channel/__tests__/ChannelConversationsDialog.test.tsx +38 -1
  160. package/src/channel/__tests__/useSaveAgentChannel.test.tsx +26 -0
  161. package/src/channel/providers.ts +27 -0
  162. package/src/channel/useSaveAgentChannel.ts +18 -4
  163. package/src/composer/ComposerToolbar.tsx +1 -1
  164. package/src/composer/SessionComposer.tsx +75 -6
  165. package/src/composer/__tests__/SessionComposer-paste.test.tsx +263 -0
  166. package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +4 -3
  167. package/src/composer/__tests__/SessionComposer-uploadGate.test.tsx +239 -0
  168. package/src/conversation/ConversationAttentionBanner.tsx +77 -0
  169. package/src/conversation/ConversationComposer.tsx +156 -0
  170. package/src/conversation/ConversationControlBanner.tsx +193 -0
  171. package/src/conversation/ConversationListPane.tsx +459 -0
  172. package/src/conversation/ConversationTimelineView.tsx +435 -0
  173. package/src/conversation/ConversationsWorkbench.tsx +378 -0
  174. package/src/conversation/__tests__/ConversationAttentionBanner.test.tsx +82 -0
  175. package/src/conversation/__tests__/ConversationComposer.test.tsx +160 -0
  176. package/src/conversation/__tests__/ConversationControlBanner.test.tsx +190 -0
  177. package/src/conversation/__tests__/ConversationListPane.test.tsx +372 -0
  178. package/src/conversation/__tests__/ConversationTimelineView.layout.test.tsx +160 -0
  179. package/src/conversation/__tests__/ConversationTimelineView.test.tsx +405 -0
  180. package/src/conversation/__tests__/ConversationsWorkbench.test.tsx +860 -0
  181. package/src/conversation/__tests__/a11y/conversation-surfaces.a11y.test.tsx +235 -0
  182. package/src/conversation/__tests__/a11y/harness.tsx +100 -0
  183. package/src/conversation/__tests__/conversation-tooltips.layout.test.tsx +167 -0
  184. package/src/conversation/__tests__/conversationPollingComposition.test.tsx +138 -0
  185. package/src/conversation/__tests__/conversationPresentation.test.ts +289 -0
  186. package/src/conversation/__tests__/useConversation.test.tsx +243 -0
  187. package/src/conversation/__tests__/useConversationList.test.tsx +492 -0
  188. package/src/conversation/__tests__/useConversationParticipation.test.tsx +190 -0
  189. package/src/conversation/__tests__/useConversationTimeline.test.tsx +258 -0
  190. package/src/conversation/__tests__/useConversationsWantsHumanCount.test.tsx +102 -0
  191. package/src/conversation/conversationPresentation.ts +305 -0
  192. package/src/conversation/index.ts +80 -0
  193. package/src/conversation/polling.ts +41 -0
  194. package/src/conversation/useConversation.ts +221 -0
  195. package/src/conversation/useConversationList.ts +328 -0
  196. package/src/conversation/useConversationParticipation.ts +178 -0
  197. package/src/conversation/useConversationTimeline.ts +244 -0
  198. package/src/conversation/useConversationsWantsHumanCount.ts +86 -0
  199. package/src/index.ts +78 -1
  200. package/src/internal/__tests__/fake-timer-slices.ts +29 -0
  201. package/src/internal/__tests__/useAutoScroll.layout.test.tsx +143 -0
  202. package/src/internal/__tests__/useAutoScroll.test.tsx +62 -5
  203. package/src/internal/__tests__/useFetch-poll-under-render-pressure.test.tsx +79 -0
  204. package/src/internal/tooltip.tsx +6 -1
  205. package/src/internal/useAutoScroll.ts +66 -20
  206. package/src/internal/useFetch.ts +15 -4
  207. package/src/models/ModelSelector.tsx +81 -49
  208. package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +105 -21
  209. package/src/models/registry.ts +3 -2
  210. package/src/models/service-tier.ts +1 -1
  211. package/src/schedule/ScheduleDetailView.tsx +1 -1
  212. package/src/schedule/ScheduleForm.tsx +7 -6
  213. package/src/schedule/__tests__/scheduleCreation.test.tsx +2 -2
  214. package/src/sharing/__tests__/useSaveAgentShare.test.tsx +55 -1
  215. package/src/sharing/__tests__/useShareToolReadiness.test.tsx +1 -0
  216. package/src/sharing/useSaveAgentShare.ts +19 -1
  217. package/src/sidebar/WorkspaceSidebar.tsx +62 -3
  218. package/src/sidebar/__tests__/WorkspaceSidebar.test.tsx +56 -1
  219. package/styles.css +1 -1
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Resolution policy for images attached to agent executions — the pure math
3
+ * behind {@link prepareImageForVision}.
4
+ *
5
+ * Vision providers cap what they will actually look at: Anthropic's standard
6
+ * tier downscales anything beyond a 1568 px long edge or ~1.15 megapixels
7
+ * before the model sees it (OpenAI's high-detail tiling lands in the same
8
+ * range). Shrinking to that ceiling in the browser is therefore
9
+ * quality-neutral — the provider would do it anyway — while cutting a 4-10 MB
10
+ * screenshot paste to a few hundred KB before it ever hits the wire.
11
+ *
12
+ * Deliberately NOT named "budget": in this codebase *vision budget* means the
13
+ * runner's per-turn byte/count budget (`VisionBudget` in the runner's
14
+ * `shared/attachment-vision.ts`, 3 MiB per image raw). That byte cap stays
15
+ * the runner's concern alone; this module bounds pixels, the provider's
16
+ * concern. Two limits, one owner each, no constants to drift.
17
+ */
18
+
19
+ /**
20
+ * Maximum long-edge length, mirroring Anthropic's standard-tier limit.
21
+ * @see https://platform.claude.com/docs/en/build-with-claude/vision
22
+ */
23
+ export const MAX_VISION_LONG_EDGE_PX = 1568;
24
+
25
+ /**
26
+ * Maximum total pixels, mirroring Anthropic's standard-tier visual-token
27
+ * ceiling (1568 tokens ≈ 1.15 megapixels). For nearly all photos and
28
+ * screenshots this — not the edge limit — is the binding constraint: a
29
+ * 1920×1080 screenshot fits the edge limit but still resizes to 1456×819.
30
+ */
31
+ export const MAX_VISION_PIXELS = 1_150_000;
32
+
33
+ /** Integer pixel dimensions produced by {@link fitToVisionResolution}. */
34
+ export interface VisionFitSize {
35
+ readonly width: number;
36
+ readonly height: number;
37
+ }
38
+
39
+ /**
40
+ * True when an image of these dimensions would be downscaled by the
41
+ * provider — i.e. when resizing it ourselves loses nothing.
42
+ */
43
+ export function exceedsVisionResolution(width: number, height: number): boolean {
44
+ return (
45
+ width * height > MAX_VISION_PIXELS ||
46
+ Math.max(width, height) > MAX_VISION_LONG_EDGE_PX
47
+ );
48
+ }
49
+
50
+ /**
51
+ * The largest size that fits both vision limits while preserving aspect
52
+ * ratio. Never upscales: dimensions already within the limits come back
53
+ * unchanged. Mirrors Anthropic's published "max API fit" computation.
54
+ *
55
+ * Degenerate inputs (zero, negative, or non-finite dimensions) come back
56
+ * unchanged — the caller's decode has already failed or will fail, and this
57
+ * module never turns a bad input into a crash.
58
+ */
59
+ export function fitToVisionResolution(width: number, height: number): VisionFitSize {
60
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
61
+ return { width, height };
62
+ }
63
+ if (!exceedsVisionResolution(width, height)) {
64
+ return { width, height };
65
+ }
66
+
67
+ const aspect = width / height;
68
+
69
+ // Largest size under the pixel ceiling at this aspect ratio.
70
+ let fitHeight = Math.sqrt(MAX_VISION_PIXELS / aspect);
71
+ let fitWidth = fitHeight * aspect;
72
+
73
+ // The edge limit takes over only for extreme aspect ratios (panoramas,
74
+ // tall phone screenshots), where the pixel-fitted size still has a long
75
+ // edge beyond the cap.
76
+ if (Math.max(fitWidth, fitHeight) > MAX_VISION_LONG_EDGE_PX) {
77
+ if (fitWidth >= fitHeight) {
78
+ fitWidth = MAX_VISION_LONG_EDGE_PX;
79
+ fitHeight = fitWidth / aspect;
80
+ } else {
81
+ fitHeight = MAX_VISION_LONG_EDGE_PX;
82
+ fitWidth = fitHeight * aspect;
83
+ }
84
+ }
85
+
86
+ return {
87
+ width: Math.max(1, Math.floor(Math.min(fitWidth, width))),
88
+ height: Math.max(1, Math.floor(Math.min(fitHeight, height))),
89
+ };
90
+ }
@@ -1,14 +1,16 @@
1
1
  "use client";
2
2
 
3
3
  import { useCallback, useState } from "react";
4
- import { FileCode2, KeyRound, LayoutTemplate, MessageSquare, MoreHorizontal, Trash2 } from "lucide-react";
4
+ import { FileCode2, KeyRound, LayoutTemplate, MessageSquare, MoreHorizontal, Share2, Trash2 } from "lucide-react";
5
5
  import { cn } from "@stigmer/theme";
6
6
  import { getUserMessage } from "@stigmer/sdk";
7
7
  import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
8
8
  import type { AgentChannel } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
9
9
  import type { ChannelApp } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/api_pb";
10
10
  import { AgentChannelInstallState } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/status_pb";
11
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
11
12
  import { toast } from "../feedback/toast.js";
13
+ import { useManageAccess } from "../access/useManageAccess.js";
12
14
  import { ActionMenu } from "../action-menu/index.js";
13
15
  import { Button } from "../button/Button.js";
14
16
  import { EmptyState } from "../empty-state/EmptyState.js";
@@ -467,6 +469,24 @@ function ChannelCard({
467
469
  "can_delete",
468
470
  );
469
471
 
472
+ // The channel's canonical access-management home (channel-conversations
473
+ // F-11): a participant grant covers every conversation on the channel,
474
+ // so the affordance belongs on the channel card. The conversation
475
+ // header's "Channel access" button is the point-of-need shortcut to
476
+ // the same dialog. Self-gates on can_view_access — `action` is null
477
+ // for viewers who may not see the access list.
478
+ const access = useManageAccess({
479
+ resource: id
480
+ ? {
481
+ kind: ApiResourceKind.agent_channel,
482
+ kindString: "agent_channel",
483
+ id,
484
+ org: meta?.org ?? "",
485
+ name: meta?.name || meta?.slug,
486
+ }
487
+ : null,
488
+ });
489
+
470
490
  // On/off is a full-input save with only `enabled` flipped —
471
491
  // agentChannelToInput guarantees the toggle can never wipe the agent
472
492
  // reference or the provider marker (the fails-closed posture shares use).
@@ -538,10 +558,20 @@ function ChannelCard({
538
558
  <ActionMenu.Item
539
559
  icon={<MessageSquare />}
540
560
  onSelect={onViewConversations}
561
+ /* "Sessions", deliberately not "Conversations": this
562
+ dialog is the session-level forensics view (which
563
+ sessions served a conversation). The word
564
+ Conversations belongs to the top-level customer
565
+ surface (channel-conversations DD-004 D-g). */
541
566
  data-cursor-target="channel-conversations"
542
567
  >
543
- Conversations
568
+ Sessions
544
569
  </ActionMenu.Item>
570
+ {access.action && (
571
+ <ActionMenu.Item icon={<Share2 />} onSelect={access.open}>
572
+ Manage access
573
+ </ActionMenu.Item>
574
+ )}
545
575
  {/* Gated on the bar the server enforces for listTemplates
546
576
  (ChannelMessagingReach: can_edit), and on the provider
547
577
  declaring a template registry at all — hidden, not
@@ -591,6 +621,7 @@ function ChannelCard({
591
621
  canEdit={canEdit}
592
622
  onEditCredentials={onEditCredentials}
593
623
  />
624
+ {access.dialog}
594
625
  </div>
595
626
  );
596
627
  }
@@ -7,6 +7,7 @@ import { cn } from "@stigmer/theme";
7
7
  import { getUserMessage } from "@stigmer/sdk";
8
8
  import type { AgentChannel } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
9
9
  import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
10
+ import { formatRelativeTime } from "../activity/format-relative-time.js";
10
11
  import { Button } from "../button/Button.js";
11
12
  import { EmptyState } from "../empty-state/EmptyState.js";
12
13
  import { channelSessionExternalUserKey } from "../session/channelOrigin.js";
@@ -37,11 +38,19 @@ export interface ChannelConversationsDialogProps {
37
38
  }
38
39
 
39
40
  /**
40
- * Lists the conversations a channel created — the Slack/WhatsApp sessions
41
- * the channel runtime owns, visible to exactly the channel's viewers (the
42
- * connector and org admins; design decision 012).
41
+ * Lists the SESSIONS a channel created — the session-level forensics view
42
+ * (which execution containers served the channel's traffic), visible to
43
+ * exactly the channel's viewers (the connector and org admins; design
44
+ * decision 012).
43
45
  *
44
- * Each row shows the conversation subject, the external platform user it
46
+ * Deliberately titled "Sessions", not "Conversations": the customer-facing
47
+ * Conversations surface is the top-level `ConversationsWorkbench` over the
48
+ * conversation timeline API, which supersedes this read for that purpose
49
+ * (channel-conversations DD-004 D-g). This dialog remains what it actually
50
+ * is — the observability view underneath a conversation. The component
51
+ * name keeps its historical export for API stability.
52
+ *
53
+ * Each row shows the session subject, the external platform user it
45
54
  * belongs to (an opaque provider id in v1), and the last activity time.
46
55
  * Rows link to the host's session route, where `SessionViewer` renders the
47
56
  * transcript read-only (the observer audience — channel viewers hold
@@ -126,10 +135,10 @@ function ChannelConversationsDialogBody({
126
135
  id="channel-conversations-title"
127
136
  className="text-sm font-semibold text-popover-foreground"
128
137
  >
129
- Conversations
138
+ Sessions
130
139
  </h2>
131
140
  <p className="mt-0.5 truncate text-xs text-muted-foreground">
132
- What people asked {channelName} — read-only, visible to the
141
+ The sessions {channelName} created — read-only, visible to the
133
142
  channel&apos;s owner and org admins.
134
143
  </p>
135
144
  </div>
@@ -137,7 +146,7 @@ function ChannelConversationsDialogBody({
137
146
  variant="ghost"
138
147
  size="xs"
139
148
  onClick={onClose}
140
- aria-label="Close conversations"
149
+ aria-label="Close sessions"
141
150
  >
142
151
  <X className="size-4" />
143
152
  </Button>
@@ -154,8 +163,8 @@ function ChannelConversationsDialogBody({
154
163
  <EmptyState
155
164
  variant="first-use"
156
165
  icon={<MessageSquare className="size-8" />}
157
- title="No conversations yet"
158
- description="When someone messages this channel, their conversation appears here."
166
+ title="No sessions yet"
167
+ description="When someone messages this channel, the sessions serving them appear here."
159
168
  />
160
169
  ) : (
161
170
  <ul className="space-y-1">
@@ -184,7 +193,7 @@ function ConversationRow({
184
193
  readonly session: Session;
185
194
  readonly href?: string;
186
195
  }) {
187
- const subject = session.spec?.subject || "Untitled conversation";
196
+ const subject = session.spec?.subject || "Untitled session";
188
197
  const externalUser = channelSessionExternalUserKey(session);
189
198
  const lastActivity = lastActivityDate(session);
190
199
 
@@ -242,9 +251,9 @@ function ConversationsSkeleton() {
242
251
  }
243
252
 
244
253
  /**
245
- * The conversation's last activity: `statusAudit.updatedAt` (bumped on
246
- * every meaningful status change), falling back to `specAudit.createdAt`
247
- * the recent-activity sort key, so the dialog agrees with the server's
254
+ * The session's last activity: `statusAudit.updatedAt` (bumped on every
255
+ * meaningful status change), falling back to `specAudit.createdAt`
256
+ * the recent-activity sort key, so the dialog agrees with the server's
248
257
  * ordering.
249
258
  */
250
259
  function lastActivityDate(session: Session): Date | null {
@@ -253,15 +262,3 @@ function lastActivityDate(session: Session): Date | null {
253
262
  session.status?.audit?.specAudit?.createdAt;
254
263
  return ts ? timestampDate(ts) : null;
255
264
  }
256
-
257
- function formatRelativeTime(date: Date): string {
258
- const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
259
- if (seconds < 60) return "just now";
260
- const minutes = Math.floor(seconds / 60);
261
- if (minutes < 60) return `${minutes}m ago`;
262
- const hours = Math.floor(minutes / 60);
263
- if (hours < 24) return `${hours}h ago`;
264
- const days = Math.floor(hours / 24);
265
- if (days < 30) return `${days}d ago`;
266
- return date.toLocaleDateString();
267
- }
@@ -419,16 +419,47 @@ describe("AgentChannelsPanel", () => {
419
419
  expect(screen.queryByRole("switch")).toBeNull();
420
420
  expect(screen.queryByRole("button", { name: /connect to slack/i })).toBeNull();
421
421
 
422
- // The card menu still renders: Conversations is a viewer-level action
422
+ // The card menu still renders: Sessions is a viewer-level action
423
423
  // (everyone who sees the card holds can_view on the channel — the same
424
- // bar as viewing its conversations, DD-012). Mutation items stay
424
+ // bar as viewing its sessions, DD-012). Mutation items stay
425
425
  // permission-gated and absent.
426
426
  fireEvent.click(screen.getByRole("button", { name: /actions for/i }));
427
- expect(screen.getByRole("menuitem", { name: /conversations/i })).toBeTruthy();
427
+ expect(screen.getByRole("menuitem", { name: /sessions/i })).toBeTruthy();
428
428
  expect(screen.queryByRole("menuitem", { name: /tool credentials/i })).toBeNull();
429
429
  expect(screen.queryByRole("menuitem", { name: /disconnect/i })).toBeNull();
430
430
  });
431
431
 
432
+ it("offers Manage access from the card menu — the channel's canonical access home (F-11)", async () => {
433
+ const client = createMockStigmer({
434
+ channels: [makeChannel({ teamName: "Acme HQ" })],
435
+ });
436
+ // The dialog's People section lists existing grants on open.
437
+ (
438
+ client as { iamPolicy: Record<string, unknown> }
439
+ ).iamPolicy.listResourceAccessByPrincipal = vi
440
+ .fn()
441
+ .mockResolvedValue({ entries: [] });
442
+ render(
443
+ <Providers client={client}>
444
+ <AgentChannelsPanel agent={makeAgent()} />
445
+ </Providers>,
446
+ );
447
+ await waitFor(() => expect(screen.getByText("Support Slack")).toBeTruthy());
448
+
449
+ fireEvent.click(screen.getByRole("button", { name: /actions for/i }));
450
+ fireEvent.click(screen.getByRole("menuitem", { name: /manage access/i }));
451
+
452
+ // The one canonical dialog opens, and its subtitle names the channel
453
+ // — the scope every grant covers (a participant grant is per
454
+ // channel, never per conversation).
455
+ await waitFor(() =>
456
+ expect(screen.getByRole("heading", { name: "Manage access" })).toBeTruthy(),
457
+ );
458
+ const dialog = document.querySelector("dialog");
459
+ expect(dialog).not.toBeNull();
460
+ expect(within(dialog as HTMLElement).getByText("Support Slack")).toBeTruthy();
461
+ });
462
+
432
463
  it("warns on an installed card when a tool-using agent has no credentials bound", async () => {
433
464
  const client = createMockStigmer({
434
465
  channels: [makeChannel({ teamName: "Acme HQ" })],
@@ -655,7 +686,7 @@ describe("AgentChannelsPanel", () => {
655
686
  // Slack has no template concept: hidden, not disabled — a disabled
656
687
  // item would imply the capability might one day exist there.
657
688
  expect(
658
- await screen.findByRole("menuitem", { name: /conversations/i }),
689
+ await screen.findByRole("menuitem", { name: /sessions/i }),
659
690
  ).toBeTruthy();
660
691
  expect(screen.queryByRole("menuitem", { name: "Templates" })).toBeNull();
661
692
  });
@@ -681,7 +712,7 @@ describe("AgentChannelsPanel", () => {
681
712
  fireEvent.click(screen.getByRole("button", { name: /actions for/i }));
682
713
 
683
714
  expect(
684
- await screen.findByRole("menuitem", { name: /conversations/i }),
715
+ await screen.findByRole("menuitem", { name: /sessions/i }),
685
716
  ).toBeTruthy();
686
717
  expect(screen.queryByRole("menuitem", { name: "Templates" })).toBeNull();
687
718
  });
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
2
  import { render, screen, cleanup, waitFor } from "@testing-library/react";
3
3
  import type { ReactNode } from "react";
4
+ import { timestampFromDate } from "@bufbuild/protobuf/wkt";
4
5
  import type { AgentChannel } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
5
6
  import { StigmerContext } from "../../context";
6
7
  import { FetchCacheContext } from "../../internal/FetchCacheProvider";
@@ -92,11 +93,47 @@ describe("ChannelConversationsDialog", () => {
92
93
  expect(screen.queryAllByRole("link")).toHaveLength(0);
93
94
  });
94
95
 
96
+ it("titles itself Sessions and falls back to Untitled session (F-04, DD-004 D-g)", async () => {
97
+ // The word "conversation" belongs to the customer-facing surface;
98
+ // this dialog is the session-level forensics view.
99
+ const listByChannel = vi.fn().mockResolvedValue({
100
+ entries: [makeChannelSession("ses_1", "")],
101
+ });
102
+
103
+ renderDialog(listByChannel);
104
+
105
+ expect(screen.getByRole("heading", { name: "Sessions" })).toBeDefined();
106
+ await waitFor(() => expect(screen.getByText("Untitled session")).toBeDefined());
107
+ expect(screen.queryByText("Untitled conversation")).toBeNull();
108
+ });
109
+
110
+ it("formats activity in the shared compact style, matching the inbox (F-04)", async () => {
111
+ const fiveMinutesAgo = new Date(Date.now() - 5 * 60_000);
112
+ const listByChannel = vi.fn().mockResolvedValue({
113
+ entries: [
114
+ {
115
+ ...makeChannelSession("ses_1", "Deploy help"),
116
+ status: {
117
+ audit: { statusAudit: { updatedAt: timestampFromDate(fiveMinutesAgo) } },
118
+ },
119
+ },
120
+ ],
121
+ });
122
+
123
+ renderDialog(listByChannel);
124
+
125
+ // The shared helper's compact single-unit style ("5m"), not the
126
+ // retired local formatter's "5m ago" — one time vocabulary across
127
+ // the channel dialog and the conversations inbox.
128
+ await waitFor(() => expect(screen.getByText("5m")).toBeDefined());
129
+ expect(screen.queryByText("5m ago")).toBeNull();
130
+ });
131
+
95
132
  it("shows the empty state when nobody has messaged the channel", async () => {
96
133
  renderDialog(vi.fn().mockResolvedValue({ entries: [] }));
97
134
 
98
135
  await waitFor(() =>
99
- expect(screen.getByText("No conversations yet")).toBeDefined(),
136
+ expect(screen.getByText("No sessions yet")).toBeDefined(),
100
137
  );
101
138
  });
102
139
 
@@ -124,6 +124,32 @@ describe("agentChannelToInput", () => {
124
124
  expect(input).not.toHaveProperty("appRef");
125
125
  });
126
126
 
127
+ it("carries run_config — the toggle must never wipe an API-set execution override", () => {
128
+ const channel = makeChannel({
129
+ spec: {
130
+ agentRef: { org: "acme", slug: "support-agent" },
131
+ enabled: true,
132
+ providerConfig: { case: "slack", value: {} },
133
+ // Set via CLI/API (stigmer/stigmer#360); no console surface edits
134
+ // it, so it must ride through the full-input rebuild opaquely.
135
+ runConfig: { modelName: "gpt-5-mini", maxCostUsd: 0.25, maxToolRounds: 8 },
136
+ },
137
+ });
138
+
139
+ const input = agentChannelToInput(channel);
140
+ expect(input.runConfig).toEqual({
141
+ modelName: "gpt-5-mini",
142
+ maxCostUsd: 0.25,
143
+ maxToolRounds: 8,
144
+ serviceTier: undefined,
145
+ });
146
+ });
147
+
148
+ it("omits runConfig entirely when the channel carries none", () => {
149
+ const input = agentChannelToInput(makeChannel());
150
+ expect(input).not.toHaveProperty("runConfig");
151
+ });
152
+
127
153
  it("omits the slack marker when the provider config is absent", () => {
128
154
  const channel = makeChannel({
129
155
  spec: {
@@ -55,6 +55,29 @@ export interface ChannelProviderDescriptor {
55
55
  * there one day.
56
56
  */
57
57
  readonly supportsMessageTemplates: boolean;
58
+ /**
59
+ * Whether staff can reply into and take over live conversations on
60
+ * this provider. Mirrors the server's `ProactiveMessageSender`
61
+ * registry — the same registry behind `reply`/`takeOver` refusing
62
+ * senderless providers with FAILED_PRECONDITION ("this channel's
63
+ * provider has no send lane for staff messages").
64
+ *
65
+ * Unlike templates, `false` here renders as DISABLED-with-explanation
66
+ * rather than hidden: the send lane is a planned capability (the
67
+ * suppression doors are already wired provider-blind server-side), so
68
+ * the affordance should say why it is off, not pretend it cannot
69
+ * exist. The server refusal stays the authoritative backstop.
70
+ */
71
+ readonly supportsStaffReplies: boolean;
72
+ /**
73
+ * Whether the conversation timeline includes the CUSTOMER's own
74
+ * messages on this provider. Mirrors the cloud stitcher's inbound
75
+ * sources: WhatsApp inbound rides a per-conversation event store;
76
+ * Slack's inbound lane has no timeline source yet, so its timeline
77
+ * shows only replies, sends, and internal events. Consumers render an
78
+ * honest notice instead of a silently one-sided thread.
79
+ */
80
+ readonly timelineIncludesCustomerMessages: boolean;
58
81
  }
59
82
 
60
83
  /**
@@ -73,6 +96,8 @@ export const CHANNEL_PROVIDERS: readonly ChannelProviderDescriptor[] = [
73
96
  Icon: SlackMarkIcon,
74
97
  installStyle: "redirect",
75
98
  supportsMessageTemplates: false,
99
+ supportsStaffReplies: false,
100
+ timelineIncludesCustomerMessages: false,
76
101
  },
77
102
  {
78
103
  id: "whatsapp",
@@ -80,6 +105,8 @@ export const CHANNEL_PROVIDERS: readonly ChannelProviderDescriptor[] = [
80
105
  Icon: WhatsAppMarkIcon,
81
106
  installStyle: "direct",
82
107
  supportsMessageTemplates: true,
108
+ supportsStaffReplies: true,
109
+ timelineIncludesCustomerMessages: true,
83
110
  },
84
111
  ];
85
112
 
@@ -23,10 +23,11 @@ import { toError } from "../internal/toError.js";
23
23
  * carries its declared `phone_number_id`. Dropping the arm is never an
24
24
  * option — the provider oneof is immutable server-side, so a save
25
25
  * without it would be refused outright. The channel's bound tool
26
- * credentials (`environment_refs`) and its channel-app binding
27
- * (`app_ref`) carry over too apply semantics would otherwise silently
28
- * unbind them on every toggle (and an installed channel's app_ref is
29
- * frozen server-side, so dropping it would refuse the save outright).
26
+ * credentials (`environment_refs`), its channel-app binding
27
+ * (`app_ref`), and its per-channel execution override (`run_config`)
28
+ * carry over too apply semantics would otherwise silently unbind
29
+ * them on every toggle (and an installed channel's app_ref is frozen
30
+ * server-side, so dropping it would refuse the save outright).
30
31
  */
31
32
  export function agentChannelToInput(channel: AgentChannel): AgentChannelInput {
32
33
  const metadata = channel.metadata;
@@ -65,6 +66,19 @@ export function agentChannelToInput(channel: AgentChannel): AgentChannelInput {
65
66
  })),
66
67
  }
67
68
  : {}),
69
+ // The console does not edit run_config (yet) — it rides through
70
+ // opaquely so a settings save or pause/resume toggle never wipes an
71
+ // override set via the CLI or API.
72
+ ...(spec?.runConfig
73
+ ? {
74
+ runConfig: {
75
+ modelName: spec.runConfig.modelName,
76
+ maxCostUsd: spec.runConfig.maxCostUsd,
77
+ maxToolRounds: spec.runConfig.maxToolRounds,
78
+ serviceTier: spec.runConfig.serviceTier,
79
+ },
80
+ }
81
+ : {}),
68
82
  };
69
83
  }
70
84
 
@@ -45,7 +45,7 @@ export interface ComposerToolbarProps {
45
45
  readonly onModelChange: (id: string) => void;
46
46
  /** Current service tier for the selected model (#357). */
47
47
  readonly serviceTier?: ServiceTierOption;
48
- /** Enables the fast-tier toggle inside the model selector. */
48
+ /** Enables the fast-tier switch inside the model selector's options area. */
49
49
  readonly onServiceTierChange?: (tier: ServiceTierOption) => void;
50
50
 
51
51
  // -- Right group: Secondary actions (icon-only) ---------------------------