@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.
- package/attachment/AttachmentChipList.d.ts.map +1 -1
- package/attachment/AttachmentChipList.js +30 -1
- package/attachment/AttachmentChipList.js.map +1 -1
- package/attachment/attachment-utils.d.ts +11 -0
- package/attachment/attachment-utils.d.ts.map +1 -1
- package/attachment/attachment-utils.js +23 -0
- package/attachment/attachment-utils.js.map +1 -1
- package/attachment/clipboard.d.ts +51 -0
- package/attachment/clipboard.d.ts.map +1 -0
- package/attachment/clipboard.js +89 -0
- package/attachment/clipboard.js.map +1 -0
- package/attachment/index.d.ts +6 -1
- package/attachment/index.d.ts.map +1 -1
- package/attachment/index.js +4 -1
- package/attachment/index.js.map +1 -1
- package/attachment/prepare-image.d.ts +43 -0
- package/attachment/prepare-image.d.ts.map +1 -0
- package/attachment/prepare-image.js +162 -0
- package/attachment/prepare-image.js.map +1 -0
- package/attachment/useAttachments.d.ts.map +1 -1
- package/attachment/useAttachments.js +23 -3
- package/attachment/useAttachments.js.map +1 -1
- package/attachment/vision-fit.d.ts +50 -0
- package/attachment/vision-fit.d.ts.map +1 -0
- package/attachment/vision-fit.js +76 -0
- package/attachment/vision-fit.js.map +1 -0
- package/channel/AgentChannelsPanel.d.ts.map +1 -1
- package/channel/AgentChannelsPanel.js +21 -2
- package/channel/AgentChannelsPanel.js.map +1 -1
- package/channel/ChannelConversationsDialog.d.ts +12 -4
- package/channel/ChannelConversationsDialog.d.ts.map +1 -1
- package/channel/ChannelConversationsDialog.js +18 -24
- package/channel/ChannelConversationsDialog.js.map +1 -1
- package/channel/providers.d.ts +23 -0
- package/channel/providers.d.ts.map +1 -1
- package/channel/providers.js +4 -0
- package/channel/providers.js.map +1 -1
- package/channel/useSaveAgentChannel.d.ts +5 -4
- package/channel/useSaveAgentChannel.d.ts.map +1 -1
- package/channel/useSaveAgentChannel.js +18 -4
- package/channel/useSaveAgentChannel.js.map +1 -1
- package/composer/ComposerToolbar.d.ts +1 -1
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/SessionComposer.d.ts +4 -2
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +51 -5
- package/composer/SessionComposer.js.map +1 -1
- package/conversation/ConversationAttentionBanner.d.ts +22 -0
- package/conversation/ConversationAttentionBanner.d.ts.map +1 -0
- package/conversation/ConversationAttentionBanner.js +25 -0
- package/conversation/ConversationAttentionBanner.js.map +1 -0
- package/conversation/ConversationComposer.d.ts +41 -0
- package/conversation/ConversationComposer.d.ts.map +1 -0
- package/conversation/ConversationComposer.js +57 -0
- package/conversation/ConversationComposer.js.map +1 -0
- package/conversation/ConversationControlBanner.d.ts +52 -0
- package/conversation/ConversationControlBanner.d.ts.map +1 -0
- package/conversation/ConversationControlBanner.js +59 -0
- package/conversation/ConversationControlBanner.js.map +1 -0
- package/conversation/ConversationListPane.d.ts +68 -0
- package/conversation/ConversationListPane.d.ts.map +1 -0
- package/conversation/ConversationListPane.js +120 -0
- package/conversation/ConversationListPane.js.map +1 -0
- package/conversation/ConversationTimelineView.d.ts +46 -0
- package/conversation/ConversationTimelineView.d.ts.map +1 -0
- package/conversation/ConversationTimelineView.js +166 -0
- package/conversation/ConversationTimelineView.js.map +1 -0
- package/conversation/ConversationsWorkbench.d.ts +63 -0
- package/conversation/ConversationsWorkbench.d.ts.map +1 -0
- package/conversation/ConversationsWorkbench.js +165 -0
- package/conversation/ConversationsWorkbench.js.map +1 -0
- package/conversation/conversationPresentation.d.ts +137 -0
- package/conversation/conversationPresentation.d.ts.map +1 -0
- package/conversation/conversationPresentation.js +230 -0
- package/conversation/conversationPresentation.js.map +1 -0
- package/conversation/index.d.ts +26 -0
- package/conversation/index.d.ts.map +1 -0
- package/conversation/index.js +14 -0
- package/conversation/index.js.map +1 -0
- package/conversation/polling.d.ts +39 -0
- package/conversation/polling.d.ts.map +1 -0
- package/conversation/polling.js +39 -0
- package/conversation/polling.js.map +1 -0
- package/conversation/useConversation.d.ts +61 -0
- package/conversation/useConversation.d.ts.map +1 -0
- package/conversation/useConversation.js +153 -0
- package/conversation/useConversation.js.map +1 -0
- package/conversation/useConversationList.d.ts +98 -0
- package/conversation/useConversationList.d.ts.map +1 -0
- package/conversation/useConversationList.js +200 -0
- package/conversation/useConversationList.js.map +1 -0
- package/conversation/useConversationParticipation.d.ts +63 -0
- package/conversation/useConversationParticipation.d.ts.map +1 -0
- package/conversation/useConversationParticipation.js +84 -0
- package/conversation/useConversationParticipation.js.map +1 -0
- package/conversation/useConversationTimeline.d.ts +71 -0
- package/conversation/useConversationTimeline.d.ts.map +1 -0
- package/conversation/useConversationTimeline.js +152 -0
- package/conversation/useConversationTimeline.js.map +1 -0
- package/conversation/useConversationsWantsHumanCount.d.ts +42 -0
- package/conversation/useConversationsWantsHumanCount.d.ts.map +1 -0
- package/conversation/useConversationsWantsHumanCount.js +40 -0
- package/conversation/useConversationsWantsHumanCount.js.map +1 -0
- package/index.d.ts +6 -2
- package/index.d.ts.map +1 -1
- package/index.js +11 -2
- package/index.js.map +1 -1
- package/internal/tooltip.d.ts.map +1 -1
- package/internal/tooltip.js +7 -1
- package/internal/tooltip.js.map +1 -1
- package/internal/useAutoScroll.d.ts +25 -4
- package/internal/useAutoScroll.d.ts.map +1 -1
- package/internal/useAutoScroll.js +55 -15
- package/internal/useAutoScroll.js.map +1 -1
- package/internal/useFetch.d.ts.map +1 -1
- package/internal/useFetch.js +14 -4
- package/internal/useFetch.js.map +1 -1
- package/models/ModelSelector.d.ts +24 -5
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +31 -9
- package/models/ModelSelector.js.map +1 -1
- package/models/registry.d.ts +3 -2
- package/models/registry.d.ts.map +1 -1
- package/models/registry.js.map +1 -1
- package/models/service-tier.d.ts +1 -1
- package/models/service-tier.d.ts.map +1 -1
- package/models/service-tier.js +1 -1
- package/models/service-tier.js.map +1 -1
- package/package.json +4 -4
- package/schedule/ScheduleDetailView.js +1 -1
- package/schedule/ScheduleDetailView.js.map +1 -1
- package/schedule/ScheduleForm.d.ts.map +1 -1
- package/schedule/ScheduleForm.js +7 -6
- package/schedule/ScheduleForm.js.map +1 -1
- package/sharing/useSaveAgentShare.d.ts +10 -1
- package/sharing/useSaveAgentShare.d.ts.map +1 -1
- package/sharing/useSaveAgentShare.js +9 -0
- package/sharing/useSaveAgentShare.js.map +1 -1
- package/sidebar/WorkspaceSidebar.d.ts +12 -4
- package/sidebar/WorkspaceSidebar.d.ts.map +1 -1
- package/sidebar/WorkspaceSidebar.js +19 -7
- package/sidebar/WorkspaceSidebar.js.map +1 -1
- package/src/attachment/AttachmentChipList.tsx +45 -1
- package/src/attachment/__tests__/AttachmentChipList.test.tsx +103 -0
- package/src/attachment/__tests__/attachment-utils.test.ts +54 -0
- package/src/attachment/__tests__/clipboard.test.ts +110 -0
- package/src/attachment/__tests__/prepare-image.browser.test.ts +166 -0
- package/src/attachment/__tests__/prepare-image.test.ts +32 -0
- package/src/attachment/__tests__/vision-fit.test.ts +93 -0
- package/src/attachment/attachment-utils.ts +27 -0
- package/src/attachment/clipboard.ts +102 -0
- package/src/attachment/index.ts +13 -0
- package/src/attachment/prepare-image.ts +179 -0
- package/src/attachment/useAttachments.ts +26 -2
- package/src/attachment/vision-fit.ts +90 -0
- package/src/channel/AgentChannelsPanel.tsx +33 -2
- package/src/channel/ChannelConversationsDialog.tsx +22 -25
- package/src/channel/__tests__/AgentChannelsPanel.test.tsx +36 -5
- package/src/channel/__tests__/ChannelConversationsDialog.test.tsx +38 -1
- package/src/channel/__tests__/useSaveAgentChannel.test.tsx +26 -0
- package/src/channel/providers.ts +27 -0
- package/src/channel/useSaveAgentChannel.ts +18 -4
- package/src/composer/ComposerToolbar.tsx +1 -1
- package/src/composer/SessionComposer.tsx +75 -6
- package/src/composer/__tests__/SessionComposer-paste.test.tsx +263 -0
- package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +4 -3
- package/src/composer/__tests__/SessionComposer-uploadGate.test.tsx +239 -0
- package/src/conversation/ConversationAttentionBanner.tsx +77 -0
- package/src/conversation/ConversationComposer.tsx +156 -0
- package/src/conversation/ConversationControlBanner.tsx +193 -0
- package/src/conversation/ConversationListPane.tsx +459 -0
- package/src/conversation/ConversationTimelineView.tsx +435 -0
- package/src/conversation/ConversationsWorkbench.tsx +378 -0
- package/src/conversation/__tests__/ConversationAttentionBanner.test.tsx +82 -0
- package/src/conversation/__tests__/ConversationComposer.test.tsx +160 -0
- package/src/conversation/__tests__/ConversationControlBanner.test.tsx +190 -0
- package/src/conversation/__tests__/ConversationListPane.test.tsx +372 -0
- package/src/conversation/__tests__/ConversationTimelineView.layout.test.tsx +160 -0
- package/src/conversation/__tests__/ConversationTimelineView.test.tsx +405 -0
- package/src/conversation/__tests__/ConversationsWorkbench.test.tsx +860 -0
- package/src/conversation/__tests__/a11y/conversation-surfaces.a11y.test.tsx +235 -0
- package/src/conversation/__tests__/a11y/harness.tsx +100 -0
- package/src/conversation/__tests__/conversation-tooltips.layout.test.tsx +167 -0
- package/src/conversation/__tests__/conversationPollingComposition.test.tsx +138 -0
- package/src/conversation/__tests__/conversationPresentation.test.ts +289 -0
- package/src/conversation/__tests__/useConversation.test.tsx +243 -0
- package/src/conversation/__tests__/useConversationList.test.tsx +492 -0
- package/src/conversation/__tests__/useConversationParticipation.test.tsx +190 -0
- package/src/conversation/__tests__/useConversationTimeline.test.tsx +258 -0
- package/src/conversation/__tests__/useConversationsWantsHumanCount.test.tsx +102 -0
- package/src/conversation/conversationPresentation.ts +305 -0
- package/src/conversation/index.ts +80 -0
- package/src/conversation/polling.ts +41 -0
- package/src/conversation/useConversation.ts +221 -0
- package/src/conversation/useConversationList.ts +328 -0
- package/src/conversation/useConversationParticipation.ts +178 -0
- package/src/conversation/useConversationTimeline.ts +244 -0
- package/src/conversation/useConversationsWantsHumanCount.ts +86 -0
- package/src/index.ts +78 -1
- package/src/internal/__tests__/fake-timer-slices.ts +29 -0
- package/src/internal/__tests__/useAutoScroll.layout.test.tsx +143 -0
- package/src/internal/__tests__/useAutoScroll.test.tsx +62 -5
- package/src/internal/__tests__/useFetch-poll-under-render-pressure.test.tsx +79 -0
- package/src/internal/tooltip.tsx +6 -1
- package/src/internal/useAutoScroll.ts +66 -20
- package/src/internal/useFetch.ts +15 -4
- package/src/models/ModelSelector.tsx +81 -49
- package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +105 -21
- package/src/models/registry.ts +3 -2
- package/src/models/service-tier.ts +1 -1
- package/src/schedule/ScheduleDetailView.tsx +1 -1
- package/src/schedule/ScheduleForm.tsx +7 -6
- package/src/schedule/__tests__/scheduleCreation.test.tsx +2 -2
- package/src/sharing/__tests__/useSaveAgentShare.test.tsx +55 -1
- package/src/sharing/__tests__/useShareToolReadiness.test.tsx +1 -0
- package/src/sharing/useSaveAgentShare.ts +19 -1
- package/src/sidebar/WorkspaceSidebar.tsx +62 -3
- package/src/sidebar/__tests__/WorkspaceSidebar.test.tsx +56 -1
- 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
|
-
|
|
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
|
|
41
|
-
*
|
|
42
|
-
* connector and org admins; design
|
|
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
|
-
*
|
|
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
|
-
|
|
138
|
+
Sessions
|
|
130
139
|
</h2>
|
|
131
140
|
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
132
|
-
|
|
141
|
+
The sessions {channelName} created — read-only, visible to the
|
|
133
142
|
channel'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
|
|
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
|
|
158
|
-
description="When someone messages this channel,
|
|
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
|
|
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
|
|
246
|
-
*
|
|
247
|
-
*
|
|
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:
|
|
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
|
|
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: /
|
|
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: /
|
|
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: /
|
|
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
|
|
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: {
|
package/src/channel/providers.ts
CHANGED
|
@@ -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`)
|
|
27
|
-
* (`app_ref`)
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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
|
|
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) ---------------------------
|