@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.
- package/billing/CreditLedgerTable.d.ts.map +1 -1
- package/billing/CreditLedgerTable.js +2 -7
- package/billing/CreditLedgerTable.js.map +1 -1
- 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 +6 -1
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +2 -2
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/SessionComposer.d.ts +9 -0
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +13 -2
- 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/execution/useCreateAgentExecution.d.ts +14 -0
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +5 -0
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/index.d.ts +6 -2
- package/index.d.ts.map +1 -1
- package/index.js +14 -5
- package/index.js.map +1 -1
- package/internal/Pagination.d.ts +15 -0
- package/internal/Pagination.d.ts.map +1 -0
- package/internal/Pagination.js +20 -0
- package/internal/Pagination.js.map +1 -0
- 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 +45 -1
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +49 -6
- package/models/ModelSelector.js.map +1 -1
- package/models/index.d.ts +2 -0
- package/models/index.d.ts.map +1 -1
- package/models/index.js +1 -0
- package/models/index.js.map +1 -1
- package/models/registry.d.ts +11 -0
- package/models/registry.d.ts.map +1 -1
- package/models/registry.js +3 -0
- package/models/registry.js.map +1 -1
- package/models/service-tier.d.ts +27 -0
- package/models/service-tier.d.ts.map +1 -0
- package/models/service-tier.js +34 -0
- package/models/service-tier.js.map +1 -0
- package/package.json +4 -4
- package/schedule/ScheduleDetailView.d.ts +31 -8
- package/schedule/ScheduleDetailView.d.ts.map +1 -1
- package/schedule/ScheduleDetailView.js +441 -29
- package/schedule/ScheduleDetailView.js.map +1 -1
- package/schedule/ScheduleForm.d.ts +7 -4
- package/schedule/ScheduleForm.d.ts.map +1 -1
- package/schedule/ScheduleForm.js +94 -8
- package/schedule/ScheduleForm.js.map +1 -1
- package/schedule/ScheduleRunsTable.d.ts +72 -0
- package/schedule/ScheduleRunsTable.d.ts.map +1 -0
- package/schedule/ScheduleRunsTable.js +161 -0
- package/schedule/ScheduleRunsTable.js.map +1 -0
- package/schedule/index.d.ts +6 -0
- package/schedule/index.d.ts.map +1 -1
- package/schedule/index.js +3 -0
- package/schedule/index.js.map +1 -1
- package/schedule/useScheduleRuns.d.ts +44 -0
- package/schedule/useScheduleRuns.d.ts.map +1 -0
- package/schedule/useScheduleRuns.js +48 -0
- package/schedule/useScheduleRuns.js.map +1 -0
- package/schedule/useTriggerSchedule.d.ts +23 -10
- package/schedule/useTriggerSchedule.d.ts.map +1 -1
- package/schedule/useTriggerSchedule.js +28 -9
- package/schedule/useTriggerSchedule.js.map +1 -1
- package/schedule/useUpdateScheduleSpec.d.ts +49 -0
- package/schedule/useUpdateScheduleSpec.d.ts.map +1 -0
- package/schedule/useUpdateScheduleSpec.js +70 -0
- package/schedule/useUpdateScheduleSpec.js.map +1 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +1 -0
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionConversation.d.ts +11 -0
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +1 -0
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +1 -0
- package/session/useSessionPageFlow.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/billing/CreditLedgerTable.tsx +3 -42
- 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 +9 -0
- package/src/composer/SessionComposer.tsx +24 -1
- package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +157 -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/execution/useCreateAgentExecution.ts +18 -0
- package/src/index.ts +76 -4
- package/src/internal/Pagination.tsx +72 -0
- 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 +133 -4
- package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +256 -0
- package/src/models/__tests__/useModelRegistry.test.tsx +15 -0
- package/src/models/index.ts +2 -0
- package/src/models/registry.ts +17 -0
- package/src/models/service-tier.ts +49 -0
- package/src/schedule/ScheduleDetailView.tsx +1179 -111
- package/src/schedule/ScheduleForm.tsx +218 -7
- package/src/schedule/ScheduleRunsTable.tsx +424 -0
- package/src/schedule/__tests__/ScheduleDetailView.test.tsx +431 -8
- package/src/schedule/__tests__/scheduleCreation.test.tsx +197 -0
- package/src/schedule/__tests__/scheduleHooks.test.tsx +94 -7
- package/src/schedule/index.ts +12 -0
- package/src/schedule/useScheduleRuns.ts +93 -0
- package/src/schedule/useTriggerSchedule.ts +39 -14
- package/src/schedule/useUpdateScheduleSpec.ts +108 -0
- package/src/session/useNewSessionFlow.ts +1 -0
- package/src/session/useSessionConversation.ts +12 -0
- package/src/session/useSessionPageFlow.ts +1 -0
- 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/src/workflow/__tests__/inspector-forms.test.tsx +119 -2
- package/src/workflow/inspector/forms/AgentCallForm.tsx +260 -37
- package/src/workflow/inspector/tabs/RuntimeTab.tsx +72 -33
- package/src/workflow/starter-workflow-yaml.ts +2 -1
- package/styles.css +1 -1
- package/workflow/inspector/forms/AgentCallForm.d.ts +18 -2
- package/workflow/inspector/forms/AgentCallForm.d.ts.map +1 -1
- package/workflow/inspector/forms/AgentCallForm.js +87 -9
- package/workflow/inspector/forms/AgentCallForm.js.map +1 -1
- package/workflow/inspector/tabs/RuntimeTab.d.ts +2 -1
- package/workflow/inspector/tabs/RuntimeTab.d.ts.map +1 -1
- package/workflow/inspector/tabs/RuntimeTab.js +31 -5
- package/workflow/inspector/tabs/RuntimeTab.js.map +1 -1
- package/workflow/starter-workflow-yaml.d.ts +1 -1
- package/workflow/starter-workflow-yaml.d.ts.map +1 -1
- package/workflow/starter-workflow-yaml.js +2 -1
- package/workflow/starter-workflow-yaml.js.map +1 -1
|
@@ -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
|
|
|
@@ -6,6 +6,7 @@ import { ConfigureMenu, type ConfigureMenuItem } from "./ConfigureMenu.js";
|
|
|
6
6
|
import { ModelSelector } from "../models/ModelSelector.js";
|
|
7
7
|
import { HarnessSelector } from "../models/HarnessSelector.js";
|
|
8
8
|
import type { HarnessOption } from "../models/harness.js";
|
|
9
|
+
import type { ServiceTierOption } from "../models/service-tier.js";
|
|
9
10
|
import { InteractionModePicker, type InteractionModeOption } from "./InteractionModePicker.js";
|
|
10
11
|
import {
|
|
11
12
|
PaperclipIcon,
|
|
@@ -42,6 +43,10 @@ export interface ComposerToolbarProps {
|
|
|
42
43
|
readonly showModelSelector: boolean;
|
|
43
44
|
readonly modelId?: string;
|
|
44
45
|
readonly onModelChange: (id: string) => void;
|
|
46
|
+
/** Current service tier for the selected model (#357). */
|
|
47
|
+
readonly serviceTier?: ServiceTierOption;
|
|
48
|
+
/** Enables the fast-tier switch inside the model selector's options area. */
|
|
49
|
+
readonly onServiceTierChange?: (tier: ServiceTierOption) => void;
|
|
45
50
|
|
|
46
51
|
// -- Right group: Secondary actions (icon-only) ---------------------------
|
|
47
52
|
|
|
@@ -115,6 +120,8 @@ export function ComposerToolbar({
|
|
|
115
120
|
showModelSelector,
|
|
116
121
|
modelId,
|
|
117
122
|
onModelChange,
|
|
123
|
+
serviceTier,
|
|
124
|
+
onServiceTierChange,
|
|
118
125
|
}: ComposerToolbarProps) {
|
|
119
126
|
const showHarnessSeparate = showHarnessSelector && !showModelSelector;
|
|
120
127
|
|
|
@@ -146,6 +153,8 @@ export function ComposerToolbar({
|
|
|
146
153
|
harness={showHarnessSelector ? undefined : harness}
|
|
147
154
|
initialHarness={showHarnessSelector ? harness : undefined}
|
|
148
155
|
onHarnessChange={showHarnessSelector ? onHarnessChange : undefined}
|
|
156
|
+
serviceTier={serviceTier}
|
|
157
|
+
onServiceTierChange={onServiceTierChange}
|
|
149
158
|
disabled={disabled}
|
|
150
159
|
/>
|
|
151
160
|
)}
|
|
@@ -7,6 +7,7 @@ import { useComposer } from "./useComposer.js";
|
|
|
7
7
|
import { ComposerToolbar } from "./ComposerToolbar.js";
|
|
8
8
|
import { type ConfigureMenuItem } from "./ConfigureMenu.js";
|
|
9
9
|
import type { HarnessOption } from "../models/harness.js";
|
|
10
|
+
import type { ServiceTierOption } from "../models/service-tier.js";
|
|
10
11
|
import type { InteractionModeOption } from "./InteractionModePicker.js";
|
|
11
12
|
import { parseModelKey } from "../models/registry.js";
|
|
12
13
|
import { WorkspaceEditor } from "../workspace/WorkspaceEditor.js";
|
|
@@ -147,6 +148,14 @@ export interface SessionComposerSubmitContext {
|
|
|
147
148
|
* Pass to execution creation as `execution_config.interaction_mode`.
|
|
148
149
|
*/
|
|
149
150
|
readonly interactionMode?: InteractionModeOption;
|
|
151
|
+
/**
|
|
152
|
+
* Service tier the user actively selected for this execution
|
|
153
|
+
* (stigmer/stigmer#357). Only ever `"fast"` — an untouched tier means
|
|
154
|
+
* "platform default" and is carried as `undefined`, preserving the
|
|
155
|
+
* unspecified-vs-explicit distinction all the way to the ledger.
|
|
156
|
+
* Pass to execution creation as `execution_config.service_tier`.
|
|
157
|
+
*/
|
|
158
|
+
readonly serviceTier?: ServiceTierOption;
|
|
150
159
|
/**
|
|
151
160
|
* Workspace-relative file paths the user referenced via drag-to-reference.
|
|
152
161
|
*
|
|
@@ -622,6 +631,11 @@ const SessionComposerInner = forwardRef<SessionComposerHandle, SessionComposerPr
|
|
|
622
631
|
setModelIdRaw(id);
|
|
623
632
|
}, []);
|
|
624
633
|
|
|
634
|
+
// Service tier (#357): composer-local state like modelId. "standard" is
|
|
635
|
+
// the resting default; the ModelSelector's fail-safe resets it when the
|
|
636
|
+
// user switches to a model without a fast tier.
|
|
637
|
+
const [serviceTier, setServiceTier] = useState<ServiceTierOption>("standard");
|
|
638
|
+
|
|
625
639
|
const [displayNames, setDisplayNames] = useState<Map<string, string>>(
|
|
626
640
|
() => new Map(),
|
|
627
641
|
);
|
|
@@ -850,13 +864,20 @@ const SessionComposerInner = forwardRef<SessionComposerHandle, SessionComposerPr
|
|
|
850
864
|
: undefined);
|
|
851
865
|
const hasFileRefs = enableFileReferences && fileRefs.hasRefs;
|
|
852
866
|
const buildFromPlan = overrides?.buildFromPlan;
|
|
867
|
+
// Carried only when the user actively chose fast: an untouched
|
|
868
|
+
// tier means "platform default" (which resolves to standard in
|
|
869
|
+
// the runner), and the UNSPECIFIED-vs-explicit distinction is
|
|
870
|
+
// load-bearing telemetry (#357).
|
|
871
|
+
const effectiveServiceTier = serviceTier === "fast" ? serviceTier : undefined;
|
|
853
872
|
|
|
854
873
|
const context: SessionComposerSubmitContext | undefined =
|
|
855
874
|
hasEnv || hasAttachments || effectiveMode || hasFileRefs || buildFromPlan
|
|
875
|
+
|| effectiveServiceTier
|
|
856
876
|
? {
|
|
857
877
|
runtimeEnv: hasEnv ? env : undefined,
|
|
858
878
|
attachments: hasAttachments ? attachmentInputs : undefined,
|
|
859
879
|
interactionMode: effectiveMode,
|
|
880
|
+
serviceTier: effectiveServiceTier,
|
|
860
881
|
workspaceFileRefs: hasFileRefs ? [...fileRefs.refs] : undefined,
|
|
861
882
|
buildFromPlan,
|
|
862
883
|
}
|
|
@@ -874,7 +895,7 @@ const SessionComposerInner = forwardRef<SessionComposerHandle, SessionComposerPr
|
|
|
874
895
|
fileRefs.clear();
|
|
875
896
|
}
|
|
876
897
|
},
|
|
877
|
-
[onSubmit, modelId, stigmer, agentSetup.state, mcpSetup.pendingRuntimeEnv, sessionVariables, enableAttachments, attachments, personalEnv, showInteractionModePicker, interactionMode],
|
|
898
|
+
[onSubmit, modelId, serviceTier, stigmer, agentSetup.state, mcpSetup.pendingRuntimeEnv, sessionVariables, enableAttachments, attachments, personalEnv, showInteractionModePicker, interactionMode],
|
|
878
899
|
);
|
|
879
900
|
|
|
880
901
|
const composer = useComposer({
|
|
@@ -1614,6 +1635,8 @@ const SessionComposerInner = forwardRef<SessionComposerHandle, SessionComposerPr
|
|
|
1614
1635
|
showModelSelector={showModelSelector}
|
|
1615
1636
|
modelId={modelId}
|
|
1616
1637
|
onModelChange={handleModelChange}
|
|
1638
|
+
serviceTier={serviceTier}
|
|
1639
|
+
onServiceTierChange={setServiceTier}
|
|
1617
1640
|
/>
|
|
1618
1641
|
</div>
|
|
1619
1642
|
</div>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeAll } from "vitest";
|
|
2
|
+
import { render, screen, cleanup, fireEvent, waitFor } from "@testing-library/react";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
5
|
+
import { StigmerContext } from "../../context";
|
|
6
|
+
import { ModelRegistryContext } from "../../models/ModelRegistryContext";
|
|
7
|
+
import type { ModelInfo } from "../../models/registry";
|
|
8
|
+
import { SessionComposer } from "../SessionComposer";
|
|
9
|
+
|
|
10
|
+
// Portal into document.body so the popover mounts under happy-dom, and shim
|
|
11
|
+
// ResizeObserver for Base UI's positioner (the scheduleCreation.test.tsx
|
|
12
|
+
// pattern).
|
|
13
|
+
vi.mock("../../portal-container", () => ({
|
|
14
|
+
useStigmerPortalContainer: () => document.body,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
if (!("ResizeObserver" in globalThis)) {
|
|
19
|
+
(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver =
|
|
20
|
+
class {
|
|
21
|
+
observe() {}
|
|
22
|
+
unobserve() {}
|
|
23
|
+
disconnect() {}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* End-to-end composer wiring for the service tier (stigmer/stigmer#357):
|
|
30
|
+
* the toolbar's ModelSelector toggle must reach composer state, and an
|
|
31
|
+
* active fast selection must ride the submit context to execution create.
|
|
32
|
+
* This seam (SessionComposer → ComposerToolbar props → submit context) is
|
|
33
|
+
* exactly where the feature can silently die while every layer on either
|
|
34
|
+
* side unit-tests green.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const FAST_CAPABLE: ModelInfo = {
|
|
38
|
+
modelId: "composer-2.5",
|
|
39
|
+
provider: "cursor",
|
|
40
|
+
displayName: "Composer 2.5",
|
|
41
|
+
shortDescription: "Fast agentic model",
|
|
42
|
+
speedTier: "fastest",
|
|
43
|
+
costTier: "economy",
|
|
44
|
+
harness: "cursor",
|
|
45
|
+
featured: true,
|
|
46
|
+
serviceTiers: ["fast"],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function createMinimalStigmerMock(): Stigmer {
|
|
50
|
+
return {
|
|
51
|
+
agentExecution: { uploadAttachment: vi.fn() },
|
|
52
|
+
environment: { getPersonal: vi.fn().mockResolvedValue(null) },
|
|
53
|
+
baseUrl: "http://localhost:8080",
|
|
54
|
+
getAuthCredential: vi.fn().mockResolvedValue("test-token"),
|
|
55
|
+
config: {
|
|
56
|
+
baseUrl: "http://localhost:8080",
|
|
57
|
+
getAccessToken: vi.fn().mockResolvedValue(""),
|
|
58
|
+
},
|
|
59
|
+
} as unknown as Stigmer;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createWrapper(client: Stigmer) {
|
|
63
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
64
|
+
return (
|
|
65
|
+
<StigmerContext.Provider value={client}>
|
|
66
|
+
<ModelRegistryContext.Provider
|
|
67
|
+
value={{
|
|
68
|
+
models: [FAST_CAPABLE],
|
|
69
|
+
isLoading: false,
|
|
70
|
+
error: null,
|
|
71
|
+
refetch: vi.fn(),
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</ModelRegistryContext.Provider>
|
|
76
|
+
</StigmerContext.Provider>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function renderComposer() {
|
|
82
|
+
const client = createMinimalStigmerMock();
|
|
83
|
+
const onSubmit = vi.fn();
|
|
84
|
+
|
|
85
|
+
const result = render(
|
|
86
|
+
<SessionComposer
|
|
87
|
+
onSubmit={onSubmit}
|
|
88
|
+
harness="cursor"
|
|
89
|
+
defaultModelId="composer-2.5"
|
|
90
|
+
/>,
|
|
91
|
+
{ wrapper: createWrapper(client) },
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return { ...result, onSubmit };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function submitMessage(message: string) {
|
|
98
|
+
const textarea = screen.getByRole("textbox");
|
|
99
|
+
fireEvent.change(textarea, { target: { value: message } });
|
|
100
|
+
fireEvent.keyDown(textarea, { key: "Enter", shiftKey: false });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Open the model selector popover (Base UI portals mount asynchronously in
|
|
105
|
+
* jsdom — the switch is awaited) and flip the fast-tier switch in the
|
|
106
|
+
* options area, then close the popover (toggling keeps it open).
|
|
107
|
+
*/
|
|
108
|
+
async function toggleFastTier() {
|
|
109
|
+
fireEvent.click(screen.getByRole("button", { name: /Composer 2\.5/ }));
|
|
110
|
+
const toggle = await screen.findByRole("switch", { name: "Fast tier" });
|
|
111
|
+
fireEvent.click(toggle);
|
|
112
|
+
fireEvent.keyDown(document.activeElement ?? document.body, { key: "Escape" });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
afterEach(cleanup);
|
|
116
|
+
|
|
117
|
+
describe("SessionComposer — service tier submit contract", () => {
|
|
118
|
+
it("renders the tier switch through the toolbar for a fast-capable model", async () => {
|
|
119
|
+
renderComposer();
|
|
120
|
+
|
|
121
|
+
fireEvent.click(screen.getByRole("button", { name: /Composer 2\.5/ }));
|
|
122
|
+
expect(await screen.findByRole("switch", { name: "Fast tier" })).toBeTruthy();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("carries serviceTier 'fast' on the submit context after toggling", async () => {
|
|
126
|
+
const { onSubmit } = renderComposer();
|
|
127
|
+
|
|
128
|
+
await toggleFastTier();
|
|
129
|
+
submitMessage("Run it fast");
|
|
130
|
+
|
|
131
|
+
await waitFor(() => expect(onSubmit).toHaveBeenCalledOnce());
|
|
132
|
+
const context = onSubmit.mock.calls[0][2];
|
|
133
|
+
expect(context?.serviceTier).toBe("fast");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("leaves serviceTier undefined when the switch is untouched", async () => {
|
|
137
|
+
const { onSubmit } = renderComposer();
|
|
138
|
+
|
|
139
|
+
submitMessage("Run it normally");
|
|
140
|
+
|
|
141
|
+
await waitFor(() => expect(onSubmit).toHaveBeenCalledOnce());
|
|
142
|
+
const context = onSubmit.mock.calls[0][2];
|
|
143
|
+
expect(context?.serviceTier).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("leaves serviceTier undefined after toggling fast on and back off", async () => {
|
|
147
|
+
const { onSubmit } = renderComposer();
|
|
148
|
+
|
|
149
|
+
await toggleFastTier();
|
|
150
|
+
await toggleFastTier();
|
|
151
|
+
submitMessage("Changed my mind");
|
|
152
|
+
|
|
153
|
+
await waitFor(() => expect(onSubmit).toHaveBeenCalledOnce());
|
|
154
|
+
const context = onSubmit.mock.calls[0][2];
|
|
155
|
+
expect(context?.serviceTier).toBeUndefined();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { TriangleAlert } from "lucide-react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { getUserMessage } from "@stigmer/sdk";
|
|
6
|
+
import type { ChannelConversation } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/conversation_io_pb";
|
|
7
|
+
import { Button } from "../button/Button.js";
|
|
8
|
+
import type { UseConversationParticipationReturn } from "./useConversationParticipation.js";
|
|
9
|
+
|
|
10
|
+
/** Props for {@link ConversationAttentionBanner}. */
|
|
11
|
+
export interface ConversationAttentionBannerProps {
|
|
12
|
+
/** The conversation's fresh participation state. */
|
|
13
|
+
readonly conversation: ChannelConversation;
|
|
14
|
+
/** The participation commands (from `useConversationParticipation`). */
|
|
15
|
+
readonly participation: UseConversationParticipationReturn;
|
|
16
|
+
/** Additional classes for the banner container. */
|
|
17
|
+
readonly className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The needs-attention banner (channel-conversations DD-008): the
|
|
22
|
+
* escalating agent's reason, verbatim, with the false-alarm dismissal.
|
|
23
|
+
*
|
|
24
|
+
* Dismiss is `clearAttention` — attention clears in place and control
|
|
25
|
+
* never moves (taking over also clears it, structurally, because the
|
|
26
|
+
* human arriving IS the answer to an escalation). Renders nothing while
|
|
27
|
+
* the conversation is unflagged.
|
|
28
|
+
*/
|
|
29
|
+
export function ConversationAttentionBanner({
|
|
30
|
+
conversation,
|
|
31
|
+
participation,
|
|
32
|
+
className,
|
|
33
|
+
}: ConversationAttentionBannerProps) {
|
|
34
|
+
if (!conversation.needsAttention) return null;
|
|
35
|
+
|
|
36
|
+
const { clearAttention, pendingCommands, commandErrors } = participation;
|
|
37
|
+
const error = commandErrors.get("clearAttention");
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
className={cn(
|
|
42
|
+
"border-b border-border bg-destructive-subtle px-4 py-2",
|
|
43
|
+
className,
|
|
44
|
+
)}
|
|
45
|
+
role="status"
|
|
46
|
+
>
|
|
47
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
48
|
+
<p className="flex min-w-0 items-start gap-1.5 text-sm text-destructive">
|
|
49
|
+
<TriangleAlert aria-hidden="true" className="mt-0.5 size-4 shrink-0" />
|
|
50
|
+
<span className="min-w-0 break-words">
|
|
51
|
+
The agent asked for a human
|
|
52
|
+
{conversation.attentionReason ? (
|
|
53
|
+
<> — “{conversation.attentionReason}”</>
|
|
54
|
+
) : (
|
|
55
|
+
"."
|
|
56
|
+
)}
|
|
57
|
+
</span>
|
|
58
|
+
</p>
|
|
59
|
+
<Button
|
|
60
|
+
variant="outline"
|
|
61
|
+
size="xs"
|
|
62
|
+
onClick={() =>
|
|
63
|
+
void clearAttention().catch(() => {
|
|
64
|
+
// Recorded in commandErrors and rendered below.
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
disabled={pendingCommands.has("clearAttention")}
|
|
68
|
+
>
|
|
69
|
+
{pendingCommands.has("clearAttention") ? "Dismissing…" : "Dismiss"}
|
|
70
|
+
</Button>
|
|
71
|
+
</div>
|
|
72
|
+
{error && (
|
|
73
|
+
<p className="mt-1 text-xs text-destructive">{getUserMessage(error)}</p>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|