@stigmer/react 3.1.7 → 3.1.8
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/agent/AgentDetailView.d.ts +9 -1
- package/agent/AgentDetailView.d.ts.map +1 -1
- package/agent/AgentDetailView.js +15 -4
- package/agent/AgentDetailView.js.map +1 -1
- package/environment/EnvironmentListPanel.d.ts.map +1 -1
- package/environment/EnvironmentListPanel.js +11 -3
- package/environment/EnvironmentListPanel.js.map +1 -1
- package/environment/EnvironmentVariableEditor.d.ts.map +1 -1
- package/environment/EnvironmentVariableEditor.js +9 -3
- package/environment/EnvironmentVariableEditor.js.map +1 -1
- package/environment/index.d.ts +1 -0
- package/environment/index.d.ts.map +1 -1
- package/environment/index.js +1 -0
- package/environment/index.js.map +1 -1
- package/environment/shareRestriction.d.ts +13 -0
- package/environment/shareRestriction.d.ts.map +1 -0
- package/environment/shareRestriction.js +15 -0
- package/environment/shareRestriction.js.map +1 -0
- package/execution/ToolCallItem.d.ts.map +1 -1
- package/execution/ToolCallItem.js +12 -0
- package/execution/ToolCallItem.js.map +1 -1
- package/execution/ToolRunGroup.js +4 -0
- package/execution/ToolRunGroup.js.map +1 -1
- package/execution/WriteBackCard.d.ts +14 -5
- package/execution/WriteBackCard.d.ts.map +1 -1
- package/execution/WriteBackCard.js +103 -32
- package/execution/WriteBackCard.js.map +1 -1
- package/execution/WriteBacksWidget.d.ts +1 -1
- package/execution/WriteBacksWidget.js +2 -2
- package/execution/WriteBacksWidget.js.map +1 -1
- package/execution/index.d.ts +2 -0
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +1 -0
- package/execution/index.js.map +1 -1
- package/execution/write-back-utils.d.ts +57 -0
- package/execution/write-back-utils.d.ts.map +1 -0
- package/execution/write-back-utils.js +81 -0
- package/execution/write-back-utils.js.map +1 -0
- package/index.d.ts +6 -2
- package/index.d.ts.map +1 -1
- package/index.js +6 -1
- package/index.js.map +1 -1
- package/internal/dev/dom-counter.d.ts.map +1 -1
- package/internal/dev/dom-counter.js +2 -2
- package/internal/dev/dom-counter.js.map +1 -1
- package/internal/dev/enabled.d.ts +7 -0
- package/internal/dev/enabled.d.ts.map +1 -0
- package/internal/dev/enabled.js +38 -0
- package/internal/dev/enabled.js.map +1 -0
- package/internal/dev/profiler-wrapper.d.ts +3 -3
- package/internal/dev/profiler-wrapper.d.ts.map +1 -1
- package/internal/dev/profiler-wrapper.js +5 -5
- package/internal/dev/profiler-wrapper.js.map +1 -1
- package/internal/dev/use-key-stability.d.ts.map +1 -1
- package/internal/dev/use-key-stability.js +2 -2
- package/internal/dev/use-key-stability.js.map +1 -1
- package/internal/dev/use-render-tracer.d.ts +2 -2
- package/internal/dev/use-render-tracer.d.ts.map +1 -1
- package/internal/dev/use-render-tracer.js +4 -4
- package/internal/dev/use-render-tracer.js.map +1 -1
- package/internal/dev/use-stream-rate.d.ts +2 -1
- package/internal/dev/use-stream-rate.d.ts.map +1 -1
- package/internal/dev/use-stream-rate.js +4 -3
- package/internal/dev/use-stream-rate.js.map +1 -1
- package/library/ResourceVisibilityControl.d.ts +3 -0
- package/library/ResourceVisibilityControl.d.ts.map +1 -1
- package/library/ResourceVisibilityControl.js +22 -8
- package/library/ResourceVisibilityControl.js.map +1 -1
- package/library/useUpdateVisibility.d.ts +5 -3
- package/library/useUpdateVisibility.d.ts.map +1 -1
- package/library/useUpdateVisibility.js +7 -2
- package/library/useUpdateVisibility.js.map +1 -1
- package/library/visibilityLevels.d.ts +17 -0
- package/library/visibilityLevels.d.ts.map +1 -1
- package/library/visibilityLevels.js +32 -0
- package/library/visibilityLevels.js.map +1 -1
- package/package.json +4 -4
- package/session/NewSessionViewer.d.ts +11 -1
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +28 -8
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +9 -2
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +26 -12
- package/session/SessionViewer.js.map +1 -1
- package/session/audience.d.ts +12 -4
- package/session/audience.d.ts.map +1 -1
- package/session/facets/ChangesTab.d.ts.map +1 -1
- package/session/facets/ChangesTab.js +3 -1
- package/session/facets/ChangesTab.js.map +1 -1
- package/session/useNewSessionFlow.d.ts +12 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +13 -1
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +13 -0
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +7 -2
- package/session/useSessionPageFlow.js.map +1 -1
- package/sharing/ShareAgentDialog.d.ts +55 -0
- package/sharing/ShareAgentDialog.d.ts.map +1 -0
- package/sharing/ShareAgentDialog.js +393 -0
- package/sharing/ShareAgentDialog.js.map +1 -0
- package/sharing/SharedAgentChat.d.ts +84 -0
- package/sharing/SharedAgentChat.d.ts.map +1 -0
- package/sharing/SharedAgentChat.js +80 -0
- package/sharing/SharedAgentChat.js.map +1 -0
- package/sharing/index.d.ts +16 -0
- package/sharing/index.d.ts.map +1 -0
- package/sharing/index.js +11 -0
- package/sharing/index.js.map +1 -0
- package/sharing/useRotateShareLink.d.ts +42 -0
- package/sharing/useRotateShareLink.d.ts.map +1 -0
- package/sharing/useRotateShareLink.js +55 -0
- package/sharing/useRotateShareLink.js.map +1 -0
- package/sharing/useShareAgent.d.ts +68 -0
- package/sharing/useShareAgent.d.ts.map +1 -0
- package/sharing/useShareAgent.js +41 -0
- package/sharing/useShareAgent.js.map +1 -0
- package/sharing/useShareToolReadiness.d.ts +48 -0
- package/sharing/useShareToolReadiness.d.ts.map +1 -0
- package/sharing/useShareToolReadiness.js +93 -0
- package/sharing/useShareToolReadiness.js.map +1 -0
- package/sharing/useSharedAgentProfile.d.ts +84 -0
- package/sharing/useSharedAgentProfile.d.ts.map +1 -0
- package/sharing/useSharedAgentProfile.js +72 -0
- package/sharing/useSharedAgentProfile.js.map +1 -0
- package/sharing/useUpdateAgentSharing.d.ts +86 -0
- package/sharing/useUpdateAgentSharing.d.ts.map +1 -0
- package/sharing/useUpdateAgentSharing.js +81 -0
- package/sharing/useUpdateAgentSharing.js.map +1 -0
- package/src/agent/AgentDetailView.tsx +28 -3
- package/src/environment/EnvironmentListPanel.tsx +47 -18
- package/src/environment/EnvironmentVariableEditor.tsx +27 -17
- package/src/environment/__tests__/shareRestriction.test.ts +35 -0
- package/src/environment/index.ts +5 -0
- package/src/environment/shareRestriction.ts +22 -0
- package/src/execution/ToolCallItem.tsx +19 -1
- package/src/execution/ToolRunGroup.tsx +4 -0
- package/src/execution/WriteBackCard.tsx +285 -88
- package/src/execution/WriteBacksWidget.tsx +5 -5
- package/src/execution/__tests__/ToolCallItem.test.tsx +51 -0
- package/src/execution/__tests__/ToolRunGroup.test.tsx +22 -0
- package/src/execution/__tests__/WriteBackCard.test.tsx +64 -10
- package/src/execution/__tests__/write-back-utils.test.ts +135 -0
- package/src/execution/index.ts +7 -0
- package/src/execution/write-back-utils.ts +107 -0
- package/src/index.ts +34 -0
- package/src/internal/dev/__tests__/use-key-stability.test.ts +4 -0
- package/src/internal/dev/__tests__/use-render-tracer.test.ts +4 -0
- package/src/internal/dev/dom-counter.ts +2 -3
- package/src/internal/dev/enabled.ts +39 -0
- package/src/internal/dev/profiler-wrapper.tsx +5 -6
- package/src/internal/dev/use-key-stability.ts +2 -3
- package/src/internal/dev/use-render-tracer.ts +4 -5
- package/src/internal/dev/use-stream-rate.ts +4 -4
- package/src/library/ResourceVisibilityControl.tsx +23 -7
- package/src/library/__tests__/visibilityLevels.test.ts +33 -0
- package/src/library/useUpdateVisibility.ts +9 -3
- package/src/library/visibilityLevels.ts +36 -0
- package/src/session/NewSessionViewer.tsx +67 -31
- package/src/session/SessionViewer.tsx +67 -36
- package/src/session/__tests__/audienceWiring.test.tsx +99 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +75 -1
- package/src/session/__tests__/useSessionPageFlow.guest.test.tsx +112 -0
- package/src/session/audience.ts +12 -4
- package/src/session/facets/ChangesTab.tsx +6 -4
- package/src/session/useNewSessionFlow.ts +26 -1
- package/src/session/useSessionPageFlow.ts +24 -2
- package/src/sharing/ShareAgentDialog.tsx +1255 -0
- package/src/sharing/SharedAgentChat.tsx +283 -0
- package/src/sharing/__tests__/ShareAgentDialog.test.tsx +745 -0
- package/src/sharing/__tests__/SharedAgentChat.test.tsx +222 -0
- package/src/sharing/__tests__/useRotateShareLink.test.tsx +96 -0
- package/src/sharing/__tests__/useShareAgent.test.tsx +127 -0
- package/src/sharing/__tests__/useShareToolReadiness.test.tsx +182 -0
- package/src/sharing/__tests__/useSharedAgentProfile.test.tsx +201 -0
- package/src/sharing/__tests__/useUpdateAgentSharing.test.tsx +200 -0
- package/src/sharing/index.ts +30 -0
- package/src/sharing/useRotateShareLink.ts +76 -0
- package/src/sharing/useShareAgent.tsx +106 -0
- package/src/sharing/useShareToolReadiness.ts +129 -0
- package/src/sharing/useSharedAgentProfile.ts +131 -0
- package/src/sharing/useUpdateAgentSharing.ts +151 -0
- package/src/switch/Switch.tsx +92 -0
- package/src/switch/__tests__/Switch.test.tsx +88 -0
- package/src/switch/index.ts +2 -0
- package/styles.css +1 -1
- package/switch/Switch.d.ts +47 -0
- package/switch/Switch.d.ts.map +1 -0
- package/switch/Switch.js +36 -0
- package/switch/Switch.js.map +1 -0
- package/switch/index.d.ts +3 -0
- package/switch/index.d.ts.map +1 -0
- package/switch/index.js +2 -0
- package/switch/index.js.map +1 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { Code } from "@connectrpc/connect";
|
|
5
|
+
import { StigmerError } from "@stigmer/sdk";
|
|
6
|
+
import { StigmerContext } from "../../context";
|
|
7
|
+
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
8
|
+
import { useSharedAgentProfile } from "../useSharedAgentProfile";
|
|
9
|
+
|
|
10
|
+
function createMockStigmer(overrides: {
|
|
11
|
+
getSharedProfile?: (...args: unknown[]) => Promise<unknown>;
|
|
12
|
+
getSharedProfileForMember?: (...args: unknown[]) => Promise<unknown>;
|
|
13
|
+
} = {}) {
|
|
14
|
+
return {
|
|
15
|
+
agent: {
|
|
16
|
+
getSharedProfile:
|
|
17
|
+
overrides.getSharedProfile ?? vi.fn().mockResolvedValue(null),
|
|
18
|
+
getSharedProfileForMember:
|
|
19
|
+
overrides.getSharedProfileForMember ?? vi.fn().mockResolvedValue(null),
|
|
20
|
+
},
|
|
21
|
+
} as never;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function wrapper(client: unknown) {
|
|
25
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
26
|
+
return (
|
|
27
|
+
<FetchCacheContext.Provider value={null}>
|
|
28
|
+
<StigmerContext.Provider value={client as never}>
|
|
29
|
+
{children}
|
|
30
|
+
</StigmerContext.Provider>
|
|
31
|
+
</FetchCacheContext.Provider>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PROFILE = {
|
|
37
|
+
org: "acme",
|
|
38
|
+
slug: "support-agent",
|
|
39
|
+
name: "Support Agent",
|
|
40
|
+
description: "Answers support questions",
|
|
41
|
+
iconUrl: "",
|
|
42
|
+
defaultInstanceId: "inst_1",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
describe("useSharedAgentProfile", () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("fetches the profile by org and slug", async () => {
|
|
51
|
+
const getSharedProfile = vi.fn().mockResolvedValue(PROFILE);
|
|
52
|
+
const client = createMockStigmer({ getSharedProfile });
|
|
53
|
+
|
|
54
|
+
const { result } = renderHook(
|
|
55
|
+
() => useSharedAgentProfile("acme", "support-agent"),
|
|
56
|
+
{ wrapper: wrapper(client) },
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(result.current.isLoading).toBe(true);
|
|
60
|
+
|
|
61
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
62
|
+
|
|
63
|
+
expect(result.current.profile).toBe(PROFILE);
|
|
64
|
+
expect(result.current.error).toBeNull();
|
|
65
|
+
// The hook builds a GetSharedProfileRequest message; a plain link
|
|
66
|
+
// carries an empty link token.
|
|
67
|
+
expect(getSharedProfile).toHaveBeenCalledWith(
|
|
68
|
+
expect.objectContaining({
|
|
69
|
+
org: "acme",
|
|
70
|
+
slug: "support-agent",
|
|
71
|
+
linkToken: "",
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("threads the linkToken option into the public request", async () => {
|
|
77
|
+
const getSharedProfile = vi.fn().mockResolvedValue(PROFILE);
|
|
78
|
+
const client = createMockStigmer({ getSharedProfile });
|
|
79
|
+
|
|
80
|
+
const { result } = renderHook(
|
|
81
|
+
() =>
|
|
82
|
+
useSharedAgentProfile("acme", "support-agent", { linkToken: "tok123" }),
|
|
83
|
+
{ wrapper: wrapper(client) },
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
87
|
+
|
|
88
|
+
expect(getSharedProfile).toHaveBeenCalledWith(
|
|
89
|
+
expect.objectContaining({ linkToken: "tok123" }),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("resolves through getSharedProfileForMember for the org audience", async () => {
|
|
94
|
+
const getSharedProfile = vi.fn();
|
|
95
|
+
const getSharedProfileForMember = vi.fn().mockResolvedValue(PROFILE);
|
|
96
|
+
const client = createMockStigmer({
|
|
97
|
+
getSharedProfile,
|
|
98
|
+
getSharedProfileForMember,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const { result } = renderHook(
|
|
102
|
+
() =>
|
|
103
|
+
useSharedAgentProfile("acme", "support-agent", { audience: "org" }),
|
|
104
|
+
{ wrapper: wrapper(client) },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
108
|
+
|
|
109
|
+
expect(result.current.profile).toBe(PROFILE);
|
|
110
|
+
expect(getSharedProfileForMember).toHaveBeenCalledWith({
|
|
111
|
+
org: "acme",
|
|
112
|
+
slug: "support-agent",
|
|
113
|
+
});
|
|
114
|
+
// The anonymous path returns NOT_FOUND for org shares by design; the
|
|
115
|
+
// hook must never fall back to it in org mode.
|
|
116
|
+
expect(getSharedProfile).not.toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("maps a member-path NOT_FOUND (non-member) to profile === null", async () => {
|
|
120
|
+
const getSharedProfileForMember = vi.fn().mockRejectedValue(
|
|
121
|
+
new StigmerError("not-found", "agent not found", Code.NotFound),
|
|
122
|
+
);
|
|
123
|
+
const client = createMockStigmer({ getSharedProfileForMember });
|
|
124
|
+
|
|
125
|
+
const { result } = renderHook(
|
|
126
|
+
() =>
|
|
127
|
+
useSharedAgentProfile("acme", "support-agent", { audience: "org" }),
|
|
128
|
+
{ wrapper: wrapper(client) },
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
132
|
+
|
|
133
|
+
expect(result.current.profile).toBeNull();
|
|
134
|
+
expect(result.current.error).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("skips fetching when org is null", () => {
|
|
138
|
+
const getSharedProfile = vi.fn();
|
|
139
|
+
const client = createMockStigmer({ getSharedProfile });
|
|
140
|
+
|
|
141
|
+
const { result } = renderHook(
|
|
142
|
+
() => useSharedAgentProfile(null, "support-agent"),
|
|
143
|
+
{ wrapper: wrapper(client) },
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(result.current.isLoading).toBe(false);
|
|
147
|
+
expect(result.current.profile).toBeNull();
|
|
148
|
+
expect(getSharedProfile).not.toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("skips fetching when slug is null", () => {
|
|
152
|
+
const getSharedProfile = vi.fn();
|
|
153
|
+
const client = createMockStigmer({ getSharedProfile });
|
|
154
|
+
|
|
155
|
+
const { result } = renderHook(() => useSharedAgentProfile("acme", null), {
|
|
156
|
+
wrapper: wrapper(client),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(result.current.isLoading).toBe(false);
|
|
160
|
+
expect(result.current.profile).toBeNull();
|
|
161
|
+
expect(getSharedProfile).not.toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("maps NOT_FOUND (unshared or nonexistent) to null without error", async () => {
|
|
165
|
+
// The generated client rethrows every failure wrapped as a
|
|
166
|
+
// StigmerError — reject with the same shape the hook really sees.
|
|
167
|
+
const getSharedProfile = vi
|
|
168
|
+
.fn()
|
|
169
|
+
.mockRejectedValue(
|
|
170
|
+
new StigmerError("not-found", "agent not found", Code.NotFound),
|
|
171
|
+
);
|
|
172
|
+
const client = createMockStigmer({ getSharedProfile });
|
|
173
|
+
|
|
174
|
+
const { result } = renderHook(
|
|
175
|
+
() => useSharedAgentProfile("acme", "revoked-agent"),
|
|
176
|
+
{ wrapper: wrapper(client) },
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
180
|
+
|
|
181
|
+
expect(result.current.profile).toBeNull();
|
|
182
|
+
expect(result.current.error).toBeNull();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("exposes non-404 failures as errors", async () => {
|
|
186
|
+
const getSharedProfile = vi
|
|
187
|
+
.fn()
|
|
188
|
+
.mockRejectedValue(new Error("Internal server error"));
|
|
189
|
+
const client = createMockStigmer({ getSharedProfile });
|
|
190
|
+
|
|
191
|
+
const { result } = renderHook(
|
|
192
|
+
() => useSharedAgentProfile("acme", "support-agent"),
|
|
193
|
+
{ wrapper: wrapper(client) },
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
197
|
+
|
|
198
|
+
expect(result.current.error).toBeTruthy();
|
|
199
|
+
expect(result.current.profile).toBeNull();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { renderHook, act, waitFor } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { UpdateAgentSharingInput } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/io_pb";
|
|
5
|
+
import { AgentSharingAudience } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
6
|
+
import { StigmerContext } from "../../context";
|
|
7
|
+
import {
|
|
8
|
+
sharingAudienceFromProto,
|
|
9
|
+
useUpdateAgentSharing,
|
|
10
|
+
type AgentSharingDraft,
|
|
11
|
+
} from "../useUpdateAgentSharing";
|
|
12
|
+
|
|
13
|
+
function createMockStigmer(overrides: {
|
|
14
|
+
updateSharing?: (input: UpdateAgentSharingInput) => Promise<unknown>;
|
|
15
|
+
} = {}) {
|
|
16
|
+
return {
|
|
17
|
+
agent: {
|
|
18
|
+
updateSharing:
|
|
19
|
+
overrides.updateSharing ?? vi.fn().mockResolvedValue({}),
|
|
20
|
+
},
|
|
21
|
+
} as never;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function wrapper(client: unknown) {
|
|
25
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
26
|
+
return (
|
|
27
|
+
<StigmerContext.Provider value={client as never}>
|
|
28
|
+
{children}
|
|
29
|
+
</StigmerContext.Provider>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const FULL_DRAFT: AgentSharingDraft = {
|
|
35
|
+
enabled: true,
|
|
36
|
+
audience: "public",
|
|
37
|
+
allowedOrigins: ["https://example.com", "https://docs.example.com"],
|
|
38
|
+
messages: {
|
|
39
|
+
rateLimited: "Slow down, please.",
|
|
40
|
+
unavailable: "We're out of credits.",
|
|
41
|
+
conversationEnded: "This conversation has ended.",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
describe("useUpdateAgentSharing", () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("sends the complete sharing block — enabled, origins, and messages", async () => {
|
|
51
|
+
const updateSharing = vi.fn().mockResolvedValue({});
|
|
52
|
+
const client = createMockStigmer({ updateSharing });
|
|
53
|
+
|
|
54
|
+
const { result } = renderHook(() => useUpdateAgentSharing("agt_1"), {
|
|
55
|
+
wrapper: wrapper(client),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await act(() => result.current.updateSharing(FULL_DRAFT));
|
|
59
|
+
|
|
60
|
+
expect(updateSharing).toHaveBeenCalledTimes(1);
|
|
61
|
+
const input = updateSharing.mock.calls[0][0] as UpdateAgentSharingInput;
|
|
62
|
+
expect(input.resourceId).toBe("agt_1");
|
|
63
|
+
expect(input.sharing?.enabled).toBe(true);
|
|
64
|
+
expect(input.sharing?.allowedOrigins).toEqual([
|
|
65
|
+
"https://example.com",
|
|
66
|
+
"https://docs.example.com",
|
|
67
|
+
]);
|
|
68
|
+
expect(input.sharing?.messages?.rateLimited).toBe("Slow down, please.");
|
|
69
|
+
expect(input.sharing?.messages?.unavailable).toBe("We're out of credits.");
|
|
70
|
+
expect(input.sharing?.messages?.conversationEnded).toBe(
|
|
71
|
+
"This conversation has ended.",
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("disabling still carries origins, messages, and audience (toggle never clobbers config)", async () => {
|
|
76
|
+
const updateSharing = vi.fn().mockResolvedValue({});
|
|
77
|
+
const client = createMockStigmer({ updateSharing });
|
|
78
|
+
|
|
79
|
+
const { result } = renderHook(() => useUpdateAgentSharing("agt_1"), {
|
|
80
|
+
wrapper: wrapper(client),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await act(() =>
|
|
84
|
+
result.current.updateSharing({
|
|
85
|
+
...FULL_DRAFT,
|
|
86
|
+
enabled: false,
|
|
87
|
+
audience: "org",
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const input = updateSharing.mock.calls[0][0] as UpdateAgentSharingInput;
|
|
92
|
+
expect(input.sharing?.enabled).toBe(false);
|
|
93
|
+
// The RPC replaces spec.sharing wholesale — a disable that dropped
|
|
94
|
+
// these fields would silently erase the owner's configuration (an
|
|
95
|
+
// audience drop would silently revert an org-only agent to public).
|
|
96
|
+
expect(input.sharing?.allowedOrigins).toEqual(FULL_DRAFT.allowedOrigins);
|
|
97
|
+
expect(input.sharing?.messages?.rateLimited).toBe(
|
|
98
|
+
FULL_DRAFT.messages.rateLimited,
|
|
99
|
+
);
|
|
100
|
+
expect(input.sharing?.audience).toBe(AgentSharingAudience.org);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("maps the audience union to the proto enum, writing public explicitly", async () => {
|
|
104
|
+
const updateSharing = vi.fn().mockResolvedValue({});
|
|
105
|
+
const client = createMockStigmer({ updateSharing });
|
|
106
|
+
|
|
107
|
+
const { result } = renderHook(() => useUpdateAgentSharing("agt_1"), {
|
|
108
|
+
wrapper: wrapper(client),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await act(() => result.current.updateSharing(FULL_DRAFT));
|
|
112
|
+
await act(() =>
|
|
113
|
+
result.current.updateSharing({ ...FULL_DRAFT, audience: "org" }),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const first = updateSharing.mock.calls[0][0] as UpdateAgentSharingInput;
|
|
117
|
+
const second = updateSharing.mock.calls[1][0] as UpdateAgentSharingInput;
|
|
118
|
+
// "public" persists as the explicit enum value, never unspecified —
|
|
119
|
+
// a console-managed share can't be downgraded by a later manifest
|
|
120
|
+
// that relies on the unspecified-means-public default.
|
|
121
|
+
expect(first.sharing?.audience).toBe(AgentSharingAudience.public);
|
|
122
|
+
expect(second.sharing?.audience).toBe(AgentSharingAudience.org);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("is a stable no-op when agentId is null", async () => {
|
|
126
|
+
const updateSharing = vi.fn();
|
|
127
|
+
const client = createMockStigmer({ updateSharing });
|
|
128
|
+
|
|
129
|
+
const { result } = renderHook(() => useUpdateAgentSharing(null), {
|
|
130
|
+
wrapper: wrapper(client),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const returned = await act(() => result.current.updateSharing(FULL_DRAFT));
|
|
134
|
+
expect(returned).toBeUndefined();
|
|
135
|
+
expect(updateSharing).not.toHaveBeenCalled();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("exposes isPending during the request", async () => {
|
|
139
|
+
let resolveRpc: (v: unknown) => void = () => {};
|
|
140
|
+
const updateSharing = vi.fn().mockReturnValue(
|
|
141
|
+
new Promise((resolve) => {
|
|
142
|
+
resolveRpc = resolve;
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
const client = createMockStigmer({ updateSharing });
|
|
146
|
+
|
|
147
|
+
const { result } = renderHook(() => useUpdateAgentSharing("agt_1"), {
|
|
148
|
+
wrapper: wrapper(client),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
let pending: Promise<unknown>;
|
|
152
|
+
act(() => {
|
|
153
|
+
pending = result.current.updateSharing(FULL_DRAFT);
|
|
154
|
+
});
|
|
155
|
+
expect(result.current.isPending).toBe(true);
|
|
156
|
+
|
|
157
|
+
await act(async () => {
|
|
158
|
+
resolveRpc({});
|
|
159
|
+
await pending;
|
|
160
|
+
});
|
|
161
|
+
expect(result.current.isPending).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("captures and rethrows RPC errors", async () => {
|
|
165
|
+
const failure = new Error("permission denied");
|
|
166
|
+
const updateSharing = vi.fn().mockRejectedValue(failure);
|
|
167
|
+
const client = createMockStigmer({ updateSharing });
|
|
168
|
+
|
|
169
|
+
const { result } = renderHook(() => useUpdateAgentSharing("agt_1"), {
|
|
170
|
+
wrapper: wrapper(client),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
let caught: unknown;
|
|
174
|
+
await act(async () => {
|
|
175
|
+
try {
|
|
176
|
+
await result.current.updateSharing(FULL_DRAFT);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
caught = err;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(caught).toBe(failure);
|
|
183
|
+
await waitFor(() => expect(result.current.error).toBe(failure));
|
|
184
|
+
expect(result.current.isPending).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("sharingAudienceFromProto", () => {
|
|
189
|
+
it("maps unspecified to public (pre-audience shares keep their behavior)", () => {
|
|
190
|
+
expect(sharingAudienceFromProto(undefined)).toBe("public");
|
|
191
|
+
expect(sharingAudienceFromProto(AgentSharingAudience.unspecified)).toBe(
|
|
192
|
+
"public",
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("maps the explicit values", () => {
|
|
197
|
+
expect(sharingAudienceFromProto(AgentSharingAudience.public)).toBe("public");
|
|
198
|
+
expect(sharingAudienceFromProto(AgentSharingAudience.org)).toBe("org");
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export { useSharedAgentProfile } from "./useSharedAgentProfile.js";
|
|
2
|
+
export type {
|
|
3
|
+
UseSharedAgentProfileOptions,
|
|
4
|
+
UseSharedAgentProfileReturn,
|
|
5
|
+
} from "./useSharedAgentProfile.js";
|
|
6
|
+
export { SharedAgentChat } from "./SharedAgentChat.js";
|
|
7
|
+
export type { SharedAgentChatProps } from "./SharedAgentChat.js";
|
|
8
|
+
export {
|
|
9
|
+
sharingAudienceFromProto,
|
|
10
|
+
useUpdateAgentSharing,
|
|
11
|
+
} from "./useUpdateAgentSharing.js";
|
|
12
|
+
export { useRotateShareLink } from "./useRotateShareLink.js";
|
|
13
|
+
export type { UseRotateShareLinkReturn } from "./useRotateShareLink.js";
|
|
14
|
+
export type {
|
|
15
|
+
AgentSharingDraft,
|
|
16
|
+
SharingAudience,
|
|
17
|
+
UseUpdateAgentSharingReturn,
|
|
18
|
+
} from "./useUpdateAgentSharing.js";
|
|
19
|
+
export { ShareAgentDialog } from "./ShareAgentDialog.js";
|
|
20
|
+
export type { ShareAgentDialogProps } from "./ShareAgentDialog.js";
|
|
21
|
+
export { useShareAgent } from "./useShareAgent.js";
|
|
22
|
+
export type {
|
|
23
|
+
UseShareAgentArgs,
|
|
24
|
+
UseShareAgentReturn,
|
|
25
|
+
} from "./useShareAgent.js";
|
|
26
|
+
export { useShareToolReadiness } from "./useShareToolReadiness.js";
|
|
27
|
+
export type { ShareToolReadiness } from "./useShareToolReadiness.js";
|
|
28
|
+
// Origin validation moved to @stigmer/sdk (framework-free, shared with the
|
|
29
|
+
// CLI); re-exported here so existing @stigmer/react importers keep working.
|
|
30
|
+
export { validateOrigin, MAX_ALLOWED_ORIGINS } from "@stigmer/sdk";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import { create } from "@bufbuild/protobuf";
|
|
5
|
+
import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
6
|
+
import { RotateShareLinkInputSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/io_pb";
|
|
7
|
+
import { useStigmer } from "../hooks.js";
|
|
8
|
+
import { toError } from "../internal/toError.js";
|
|
9
|
+
|
|
10
|
+
/** Return value of {@link useRotateShareLink}. */
|
|
11
|
+
export interface UseRotateShareLinkReturn {
|
|
12
|
+
/**
|
|
13
|
+
* Rotate the agent's share-link token via the `rotateShareLink` RPC.
|
|
14
|
+
* Resolves with the updated agent, whose `status.shareLinkToken`
|
|
15
|
+
* carries the new token for building the fresh share URL.
|
|
16
|
+
*/
|
|
17
|
+
readonly rotateShareLink: () => Promise<Agent | undefined>;
|
|
18
|
+
/** `true` while the RPC is in flight. */
|
|
19
|
+
readonly isPending: boolean;
|
|
20
|
+
/** The last error from the RPC, or `null`. */
|
|
21
|
+
readonly error: Error | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Behavior hook that rotates an agent's share-link token.
|
|
26
|
+
*
|
|
27
|
+
* Wraps the `stigmer.agent.rotateShareLink()` RPC (authorized by
|
|
28
|
+
* `can_edit`) with loading and error state management. The server
|
|
29
|
+
* generates the token — there is nothing to supply — and the previous
|
|
30
|
+
* link stops working the moment the call resolves, including for
|
|
31
|
+
* visitors mid-conversation. The token lives in `status.shareLinkToken`
|
|
32
|
+
* (server-owned), so it is never part of a sharing draft and survives
|
|
33
|
+
* manifest applies.
|
|
34
|
+
*
|
|
35
|
+
* The hook is stateless with respect to the agent — adopt the returned
|
|
36
|
+
* agent (or refetch) so the UI shows the new tokened link, matching the
|
|
37
|
+
* {@link useUpdateAgentSharing} convention.
|
|
38
|
+
*
|
|
39
|
+
* Pass `null` for `agentId` to produce a stable no-op (useful while the
|
|
40
|
+
* agent is still loading).
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* const { rotateShareLink, isPending } = useRotateShareLink(agent.metadata.id);
|
|
45
|
+
*
|
|
46
|
+
* const updated = await rotateShareLink();
|
|
47
|
+
* // updated.status.shareLinkToken is the new token
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function useRotateShareLink(
|
|
51
|
+
agentId: string | null,
|
|
52
|
+
): UseRotateShareLinkReturn {
|
|
53
|
+
const stigmer = useStigmer();
|
|
54
|
+
const [isPending, setIsPending] = useState(false);
|
|
55
|
+
const [error, setError] = useState<Error | null>(null);
|
|
56
|
+
|
|
57
|
+
const rotateShareLink = useCallback(async (): Promise<Agent | undefined> => {
|
|
58
|
+
if (!agentId) return undefined;
|
|
59
|
+
|
|
60
|
+
setIsPending(true);
|
|
61
|
+
setError(null);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
return await stigmer.agent.rotateShareLink(
|
|
65
|
+
create(RotateShareLinkInputSchema, { resourceId: agentId }),
|
|
66
|
+
);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
setError(toError(err));
|
|
69
|
+
throw err;
|
|
70
|
+
} finally {
|
|
71
|
+
setIsPending(false);
|
|
72
|
+
}
|
|
73
|
+
}, [agentId, stigmer]);
|
|
74
|
+
|
|
75
|
+
return { rotateShareLink, isPending, error };
|
|
76
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState, type ReactNode } from "react";
|
|
4
|
+
import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
5
|
+
import { useCheckPermission } from "../iam-policy/useCheckPermission.js";
|
|
6
|
+
import type { DetailAction } from "../resource-detail/types.js";
|
|
7
|
+
import { ShareAgentDialog } from "./ShareAgentDialog.js";
|
|
8
|
+
|
|
9
|
+
/** Arguments for {@link useShareAgent}. */
|
|
10
|
+
export interface UseShareAgentArgs {
|
|
11
|
+
/**
|
|
12
|
+
* The agent whose sharing is managed, or `null` while it is still
|
|
13
|
+
* loading. When `null`, {@link UseShareAgentReturn.action} is `null`
|
|
14
|
+
* and the dialog renders nothing — safe to call before the resource
|
|
15
|
+
* is ready.
|
|
16
|
+
*/
|
|
17
|
+
readonly agent: Agent | null;
|
|
18
|
+
/**
|
|
19
|
+
* Builds the absolute public chat URL for the shared agent. The host
|
|
20
|
+
* application owns URL construction (its configured public origin may
|
|
21
|
+
* differ from the rendering origin — e.g. the desktop app). When
|
|
22
|
+
* omitted, the dialog falls back to the relative `/chat/<org>/<slug>`.
|
|
23
|
+
*/
|
|
24
|
+
readonly buildShareUrl?: (org: string, slug: string) => string;
|
|
25
|
+
/**
|
|
26
|
+
* Called after any sharing change is persisted. Hosts typically pass
|
|
27
|
+
* the agent data hook's `refetch`.
|
|
28
|
+
*/
|
|
29
|
+
readonly onSharingChanged?: () => void;
|
|
30
|
+
/** Menu-item label. @default "Share" */
|
|
31
|
+
readonly label?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Return value of {@link useShareAgent}. */
|
|
35
|
+
export interface UseShareAgentReturn {
|
|
36
|
+
/**
|
|
37
|
+
* A ready-to-spread {@link DetailAction} for a kebab/overflow menu, or
|
|
38
|
+
* `null` when the agent is unavailable or the user lacks `can_edit`.
|
|
39
|
+
* Lives in the `"sharing"` group, beside "Manage access".
|
|
40
|
+
*/
|
|
41
|
+
readonly action: DetailAction | null;
|
|
42
|
+
/** The {@link ShareAgentDialog} node — render it once in the host tree. */
|
|
43
|
+
readonly dialog: ReactNode;
|
|
44
|
+
/** Imperatively open the dialog. */
|
|
45
|
+
readonly open: () => void;
|
|
46
|
+
/** Whether the dialog is currently open. */
|
|
47
|
+
readonly isOpen: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Wires the Share dialog to a kebab/overflow menu — the same trigger
|
|
52
|
+
* shape as {@link useManageAccess}, its conceptual sibling: Manage access
|
|
53
|
+
* governs who can *read* the blueprint; Share governs who can *chat* with
|
|
54
|
+
* the running agent (billed to the owning org).
|
|
55
|
+
*
|
|
56
|
+
* Owns the open-state and the `can_edit` gate — the same permission the
|
|
57
|
+
* `updateSharing` RPC enforces, so the action never appears to a user
|
|
58
|
+
* whose changes would be rejected. Returns a `null` action while the
|
|
59
|
+
* agent is loading or the user cannot edit, so the host can
|
|
60
|
+
* unconditionally fold `action` into its actions array.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```tsx
|
|
64
|
+
* const share = useShareAgent({
|
|
65
|
+
* agent,
|
|
66
|
+
* buildShareUrl: (org, slug) => `${appOrigin}/chat/${org}/${slug}`,
|
|
67
|
+
* onSharingChanged: refetch,
|
|
68
|
+
* });
|
|
69
|
+
* // ...
|
|
70
|
+
* <ResourceDetailShell actions={share.action ? [...actions, share.action] : actions} ... />
|
|
71
|
+
* {share.dialog}
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export function useShareAgent({
|
|
75
|
+
agent,
|
|
76
|
+
buildShareUrl,
|
|
77
|
+
onSharingChanged,
|
|
78
|
+
label = "Share",
|
|
79
|
+
}: UseShareAgentArgs): UseShareAgentReturn {
|
|
80
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
81
|
+
|
|
82
|
+
const agentId = agent?.metadata?.id ?? null;
|
|
83
|
+
const { allowed: canEdit } = useCheckPermission(
|
|
84
|
+
agentId ? { kind: "agent", id: agentId } : null,
|
|
85
|
+
"can_edit",
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const open = useCallback(() => setIsOpen(true), []);
|
|
89
|
+
|
|
90
|
+
const action: DetailAction | null =
|
|
91
|
+
agent && canEdit
|
|
92
|
+
? { id: "share", label, group: "sharing", onAction: open }
|
|
93
|
+
: null;
|
|
94
|
+
|
|
95
|
+
const dialog = agent ? (
|
|
96
|
+
<ShareAgentDialog
|
|
97
|
+
open={isOpen}
|
|
98
|
+
onOpenChange={setIsOpen}
|
|
99
|
+
agent={agent}
|
|
100
|
+
buildShareUrl={buildShareUrl}
|
|
101
|
+
onSharingChanged={onSharingChanged}
|
|
102
|
+
/>
|
|
103
|
+
) : null;
|
|
104
|
+
|
|
105
|
+
return { action, dialog, open, isOpen };
|
|
106
|
+
}
|