@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.
Files changed (195) hide show
  1. package/agent/AgentDetailView.d.ts +9 -1
  2. package/agent/AgentDetailView.d.ts.map +1 -1
  3. package/agent/AgentDetailView.js +15 -4
  4. package/agent/AgentDetailView.js.map +1 -1
  5. package/environment/EnvironmentListPanel.d.ts.map +1 -1
  6. package/environment/EnvironmentListPanel.js +11 -3
  7. package/environment/EnvironmentListPanel.js.map +1 -1
  8. package/environment/EnvironmentVariableEditor.d.ts.map +1 -1
  9. package/environment/EnvironmentVariableEditor.js +9 -3
  10. package/environment/EnvironmentVariableEditor.js.map +1 -1
  11. package/environment/index.d.ts +1 -0
  12. package/environment/index.d.ts.map +1 -1
  13. package/environment/index.js +1 -0
  14. package/environment/index.js.map +1 -1
  15. package/environment/shareRestriction.d.ts +13 -0
  16. package/environment/shareRestriction.d.ts.map +1 -0
  17. package/environment/shareRestriction.js +15 -0
  18. package/environment/shareRestriction.js.map +1 -0
  19. package/execution/ToolCallItem.d.ts.map +1 -1
  20. package/execution/ToolCallItem.js +12 -0
  21. package/execution/ToolCallItem.js.map +1 -1
  22. package/execution/ToolRunGroup.js +4 -0
  23. package/execution/ToolRunGroup.js.map +1 -1
  24. package/execution/WriteBackCard.d.ts +14 -5
  25. package/execution/WriteBackCard.d.ts.map +1 -1
  26. package/execution/WriteBackCard.js +103 -32
  27. package/execution/WriteBackCard.js.map +1 -1
  28. package/execution/WriteBacksWidget.d.ts +1 -1
  29. package/execution/WriteBacksWidget.js +2 -2
  30. package/execution/WriteBacksWidget.js.map +1 -1
  31. package/execution/index.d.ts +2 -0
  32. package/execution/index.d.ts.map +1 -1
  33. package/execution/index.js +1 -0
  34. package/execution/index.js.map +1 -1
  35. package/execution/write-back-utils.d.ts +57 -0
  36. package/execution/write-back-utils.d.ts.map +1 -0
  37. package/execution/write-back-utils.js +81 -0
  38. package/execution/write-back-utils.js.map +1 -0
  39. package/index.d.ts +6 -2
  40. package/index.d.ts.map +1 -1
  41. package/index.js +6 -1
  42. package/index.js.map +1 -1
  43. package/internal/dev/dom-counter.d.ts.map +1 -1
  44. package/internal/dev/dom-counter.js +2 -2
  45. package/internal/dev/dom-counter.js.map +1 -1
  46. package/internal/dev/enabled.d.ts +7 -0
  47. package/internal/dev/enabled.d.ts.map +1 -0
  48. package/internal/dev/enabled.js +38 -0
  49. package/internal/dev/enabled.js.map +1 -0
  50. package/internal/dev/profiler-wrapper.d.ts +3 -3
  51. package/internal/dev/profiler-wrapper.d.ts.map +1 -1
  52. package/internal/dev/profiler-wrapper.js +5 -5
  53. package/internal/dev/profiler-wrapper.js.map +1 -1
  54. package/internal/dev/use-key-stability.d.ts.map +1 -1
  55. package/internal/dev/use-key-stability.js +2 -2
  56. package/internal/dev/use-key-stability.js.map +1 -1
  57. package/internal/dev/use-render-tracer.d.ts +2 -2
  58. package/internal/dev/use-render-tracer.d.ts.map +1 -1
  59. package/internal/dev/use-render-tracer.js +4 -4
  60. package/internal/dev/use-render-tracer.js.map +1 -1
  61. package/internal/dev/use-stream-rate.d.ts +2 -1
  62. package/internal/dev/use-stream-rate.d.ts.map +1 -1
  63. package/internal/dev/use-stream-rate.js +4 -3
  64. package/internal/dev/use-stream-rate.js.map +1 -1
  65. package/library/ResourceVisibilityControl.d.ts +3 -0
  66. package/library/ResourceVisibilityControl.d.ts.map +1 -1
  67. package/library/ResourceVisibilityControl.js +22 -8
  68. package/library/ResourceVisibilityControl.js.map +1 -1
  69. package/library/useUpdateVisibility.d.ts +5 -3
  70. package/library/useUpdateVisibility.d.ts.map +1 -1
  71. package/library/useUpdateVisibility.js +7 -2
  72. package/library/useUpdateVisibility.js.map +1 -1
  73. package/library/visibilityLevels.d.ts +17 -0
  74. package/library/visibilityLevels.d.ts.map +1 -1
  75. package/library/visibilityLevels.js +32 -0
  76. package/library/visibilityLevels.js.map +1 -1
  77. package/package.json +4 -4
  78. package/session/NewSessionViewer.d.ts +11 -1
  79. package/session/NewSessionViewer.d.ts.map +1 -1
  80. package/session/NewSessionViewer.js +28 -8
  81. package/session/NewSessionViewer.js.map +1 -1
  82. package/session/SessionViewer.d.ts +9 -2
  83. package/session/SessionViewer.d.ts.map +1 -1
  84. package/session/SessionViewer.js +26 -12
  85. package/session/SessionViewer.js.map +1 -1
  86. package/session/audience.d.ts +12 -4
  87. package/session/audience.d.ts.map +1 -1
  88. package/session/facets/ChangesTab.d.ts.map +1 -1
  89. package/session/facets/ChangesTab.js +3 -1
  90. package/session/facets/ChangesTab.js.map +1 -1
  91. package/session/useNewSessionFlow.d.ts +12 -0
  92. package/session/useNewSessionFlow.d.ts.map +1 -1
  93. package/session/useNewSessionFlow.js +13 -1
  94. package/session/useNewSessionFlow.js.map +1 -1
  95. package/session/useSessionPageFlow.d.ts +13 -0
  96. package/session/useSessionPageFlow.d.ts.map +1 -1
  97. package/session/useSessionPageFlow.js +7 -2
  98. package/session/useSessionPageFlow.js.map +1 -1
  99. package/sharing/ShareAgentDialog.d.ts +55 -0
  100. package/sharing/ShareAgentDialog.d.ts.map +1 -0
  101. package/sharing/ShareAgentDialog.js +393 -0
  102. package/sharing/ShareAgentDialog.js.map +1 -0
  103. package/sharing/SharedAgentChat.d.ts +84 -0
  104. package/sharing/SharedAgentChat.d.ts.map +1 -0
  105. package/sharing/SharedAgentChat.js +80 -0
  106. package/sharing/SharedAgentChat.js.map +1 -0
  107. package/sharing/index.d.ts +16 -0
  108. package/sharing/index.d.ts.map +1 -0
  109. package/sharing/index.js +11 -0
  110. package/sharing/index.js.map +1 -0
  111. package/sharing/useRotateShareLink.d.ts +42 -0
  112. package/sharing/useRotateShareLink.d.ts.map +1 -0
  113. package/sharing/useRotateShareLink.js +55 -0
  114. package/sharing/useRotateShareLink.js.map +1 -0
  115. package/sharing/useShareAgent.d.ts +68 -0
  116. package/sharing/useShareAgent.d.ts.map +1 -0
  117. package/sharing/useShareAgent.js +41 -0
  118. package/sharing/useShareAgent.js.map +1 -0
  119. package/sharing/useShareToolReadiness.d.ts +48 -0
  120. package/sharing/useShareToolReadiness.d.ts.map +1 -0
  121. package/sharing/useShareToolReadiness.js +93 -0
  122. package/sharing/useShareToolReadiness.js.map +1 -0
  123. package/sharing/useSharedAgentProfile.d.ts +84 -0
  124. package/sharing/useSharedAgentProfile.d.ts.map +1 -0
  125. package/sharing/useSharedAgentProfile.js +72 -0
  126. package/sharing/useSharedAgentProfile.js.map +1 -0
  127. package/sharing/useUpdateAgentSharing.d.ts +86 -0
  128. package/sharing/useUpdateAgentSharing.d.ts.map +1 -0
  129. package/sharing/useUpdateAgentSharing.js +81 -0
  130. package/sharing/useUpdateAgentSharing.js.map +1 -0
  131. package/src/agent/AgentDetailView.tsx +28 -3
  132. package/src/environment/EnvironmentListPanel.tsx +47 -18
  133. package/src/environment/EnvironmentVariableEditor.tsx +27 -17
  134. package/src/environment/__tests__/shareRestriction.test.ts +35 -0
  135. package/src/environment/index.ts +5 -0
  136. package/src/environment/shareRestriction.ts +22 -0
  137. package/src/execution/ToolCallItem.tsx +19 -1
  138. package/src/execution/ToolRunGroup.tsx +4 -0
  139. package/src/execution/WriteBackCard.tsx +285 -88
  140. package/src/execution/WriteBacksWidget.tsx +5 -5
  141. package/src/execution/__tests__/ToolCallItem.test.tsx +51 -0
  142. package/src/execution/__tests__/ToolRunGroup.test.tsx +22 -0
  143. package/src/execution/__tests__/WriteBackCard.test.tsx +64 -10
  144. package/src/execution/__tests__/write-back-utils.test.ts +135 -0
  145. package/src/execution/index.ts +7 -0
  146. package/src/execution/write-back-utils.ts +107 -0
  147. package/src/index.ts +34 -0
  148. package/src/internal/dev/__tests__/use-key-stability.test.ts +4 -0
  149. package/src/internal/dev/__tests__/use-render-tracer.test.ts +4 -0
  150. package/src/internal/dev/dom-counter.ts +2 -3
  151. package/src/internal/dev/enabled.ts +39 -0
  152. package/src/internal/dev/profiler-wrapper.tsx +5 -6
  153. package/src/internal/dev/use-key-stability.ts +2 -3
  154. package/src/internal/dev/use-render-tracer.ts +4 -5
  155. package/src/internal/dev/use-stream-rate.ts +4 -4
  156. package/src/library/ResourceVisibilityControl.tsx +23 -7
  157. package/src/library/__tests__/visibilityLevels.test.ts +33 -0
  158. package/src/library/useUpdateVisibility.ts +9 -3
  159. package/src/library/visibilityLevels.ts +36 -0
  160. package/src/session/NewSessionViewer.tsx +67 -31
  161. package/src/session/SessionViewer.tsx +67 -36
  162. package/src/session/__tests__/audienceWiring.test.tsx +99 -0
  163. package/src/session/__tests__/useNewSessionFlow.test.tsx +75 -1
  164. package/src/session/__tests__/useSessionPageFlow.guest.test.tsx +112 -0
  165. package/src/session/audience.ts +12 -4
  166. package/src/session/facets/ChangesTab.tsx +6 -4
  167. package/src/session/useNewSessionFlow.ts +26 -1
  168. package/src/session/useSessionPageFlow.ts +24 -2
  169. package/src/sharing/ShareAgentDialog.tsx +1255 -0
  170. package/src/sharing/SharedAgentChat.tsx +283 -0
  171. package/src/sharing/__tests__/ShareAgentDialog.test.tsx +745 -0
  172. package/src/sharing/__tests__/SharedAgentChat.test.tsx +222 -0
  173. package/src/sharing/__tests__/useRotateShareLink.test.tsx +96 -0
  174. package/src/sharing/__tests__/useShareAgent.test.tsx +127 -0
  175. package/src/sharing/__tests__/useShareToolReadiness.test.tsx +182 -0
  176. package/src/sharing/__tests__/useSharedAgentProfile.test.tsx +201 -0
  177. package/src/sharing/__tests__/useUpdateAgentSharing.test.tsx +200 -0
  178. package/src/sharing/index.ts +30 -0
  179. package/src/sharing/useRotateShareLink.ts +76 -0
  180. package/src/sharing/useShareAgent.tsx +106 -0
  181. package/src/sharing/useShareToolReadiness.ts +129 -0
  182. package/src/sharing/useSharedAgentProfile.ts +131 -0
  183. package/src/sharing/useUpdateAgentSharing.ts +151 -0
  184. package/src/switch/Switch.tsx +92 -0
  185. package/src/switch/__tests__/Switch.test.tsx +88 -0
  186. package/src/switch/index.ts +2 -0
  187. package/styles.css +1 -1
  188. package/switch/Switch.d.ts +47 -0
  189. package/switch/Switch.d.ts.map +1 -0
  190. package/switch/Switch.js +36 -0
  191. package/switch/Switch.js.map +1 -0
  192. package/switch/index.d.ts +3 -0
  193. package/switch/index.d.ts.map +1 -0
  194. package/switch/index.js +2 -0
  195. package/switch/index.js.map +1 -0
@@ -0,0 +1,129 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
5
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
6
+ import { useStigmer } from "../hooks.js";
7
+ import { useDeploymentMode } from "../deployment-mode.js";
8
+
9
+ /**
10
+ * Readiness of a shared agent's tool credentials for visitor (and
11
+ * teammate) executions.
12
+ *
13
+ * - `na` — nothing to check: the agent uses no MCP tools, the check is
14
+ * disabled, or the deployment cannot serve guest chat (local mode).
15
+ * - `checking` — lookups in flight.
16
+ * - `ready` — every environment bound to the agent's default instance is
17
+ * shared with the organization.
18
+ * - `blocked` — one or more bound environments are private; their
19
+ * `org/slug` references are listed for the hint copy.
20
+ */
21
+ export type ShareToolReadiness =
22
+ | { readonly status: "na" }
23
+ | { readonly status: "checking" }
24
+ | { readonly status: "ready" }
25
+ | { readonly status: "blocked"; readonly privateEnvironments: readonly string[] };
26
+
27
+ const NA: ShareToolReadiness = { status: "na" };
28
+
29
+ /**
30
+ * Data hook that checks whether a tool-using shared agent's credentials
31
+ * will work for visitors — i.e. whether the environments bound to its
32
+ * default instance are shared with the organization (`visibility_org`).
33
+ *
34
+ * Guest and teammate executions can only use org-shared environments;
35
+ * a private environment's secrets are creator-only and silently
36
+ * unavailable to them, so the agent's tools would fail at runtime. This
37
+ * hook powers the Share dialog's pre-flight hint, catching the
38
+ * misconfiguration at share time instead of at the visitor's first
39
+ * message.
40
+ *
41
+ * Scope note: environments bound via `environment_refs` are the
42
+ * supported credential path for shared agents. Credentials that only
43
+ * exist in the owner's personal environment are also invisible to
44
+ * guests, but are not detectable from the instance binding — the
45
+ * runtime's enriched FAILED_PRECONDITION error covers that case.
46
+ *
47
+ * The check runs only when `enabled` is true, the deployment is cloud
48
+ * (local mode has no guest runtime or secret gating), and the agent
49
+ * declares MCP server usages. All lookups run as the owner viewing the
50
+ * dialog, who can read the bound environments.
51
+ */
52
+ export function useShareToolReadiness(
53
+ agent: Agent,
54
+ enabled: boolean,
55
+ ): ShareToolReadiness {
56
+ const stigmer = useStigmer();
57
+ const deploymentMode = useDeploymentMode();
58
+ const [readiness, setReadiness] = useState<ShareToolReadiness>(NA);
59
+
60
+ const agentId = agent.metadata?.id ?? "";
61
+ const hasMcpTools = (agent.spec?.mcpServerUsages?.length ?? 0) > 0;
62
+ const defaultInstanceId = agent.status?.defaultInstanceId ?? "";
63
+
64
+ const shouldCheck =
65
+ enabled &&
66
+ deploymentMode === "cloud" &&
67
+ hasMcpTools &&
68
+ defaultInstanceId !== "";
69
+
70
+ useEffect(() => {
71
+ if (!shouldCheck) {
72
+ setReadiness(NA);
73
+ return;
74
+ }
75
+
76
+ let cancelled = false;
77
+ setReadiness({ status: "checking" });
78
+
79
+ (async (): Promise<ShareToolReadiness> => {
80
+ const instance = await stigmer.agentInstance.get(defaultInstanceId);
81
+ const refs = instance.spec?.environmentRefs ?? [];
82
+ if (refs.length === 0) {
83
+ // No bound environments to assess. The tools may need no
84
+ // credentials at all — do not warn on a guess.
85
+ return NA;
86
+ }
87
+
88
+ const privateRefs: string[] = [];
89
+ for (const ref of refs) {
90
+ try {
91
+ const env = await stigmer.environment.getByReference({
92
+ org: ref.org,
93
+ slug: ref.slug,
94
+ });
95
+ if (
96
+ env.metadata?.visibility !== ApiResourceVisibility.visibility_org
97
+ ) {
98
+ privateRefs.push(`${ref.org}/${ref.slug}`);
99
+ }
100
+ } catch {
101
+ // Unreadable ref (deleted, foreign): the runtime will skip it
102
+ // for visitors too — surface it as blocking.
103
+ privateRefs.push(`${ref.org}/${ref.slug}`);
104
+ }
105
+ }
106
+
107
+ return privateRefs.length === 0
108
+ ? { status: "ready" }
109
+ : { status: "blocked", privateEnvironments: privateRefs };
110
+ })().then(
111
+ (result) => {
112
+ if (!cancelled) setReadiness(result);
113
+ },
114
+ () => {
115
+ // The hint is best-effort pre-flight advice — a failed check must
116
+ // never block the dialog. The runtime error path still diagnoses.
117
+ if (!cancelled) setReadiness(NA);
118
+ },
119
+ );
120
+
121
+ return () => {
122
+ cancelled = true;
123
+ };
124
+ // agentId stands in for the agent object identity: the inputs that
125
+ // matter (instance id, MCP usage presence) are covered explicitly.
126
+ }, [shouldCheck, defaultInstanceId, agentId, stigmer]);
127
+
128
+ return readiness;
129
+ }
@@ -0,0 +1,131 @@
1
+ "use client";
2
+
3
+ import { create } from "@bufbuild/protobuf";
4
+ import {
5
+ GetSharedProfileRequestSchema,
6
+ type SharedAgentProfile,
7
+ } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/io_pb";
8
+ import { isNotFound } from "@stigmer/sdk";
9
+ import { useStigmer } from "../hooks.js";
10
+ import { useFetch } from "../internal/useFetch.js";
11
+ import type { SharingAudience } from "./useUpdateAgentSharing.js";
12
+
13
+ /** Options for {@link useSharedAgentProfile}. */
14
+ export interface UseSharedAgentProfileOptions {
15
+ /**
16
+ * Which resolution path to use.
17
+ *
18
+ * - `"public"` (default) — the anonymous `getSharedProfile` RPC. An
19
+ * org-audience share answers NOT_FOUND here by design.
20
+ * - `"org"` — the authenticated `getSharedProfileForMember` RPC, which
21
+ * resolves org-audience shares for signed-in members of the owning
22
+ * organization (and public shares too). Requires a `StigmerProvider`
23
+ * whose client carries the member's token.
24
+ */
25
+ readonly audience?: SharingAudience;
26
+
27
+ /**
28
+ * Share-link token from the URL's `?k=` parameter (public path only).
29
+ *
30
+ * Required when the share link has been locked with a rotatable token;
31
+ * harmless on plain links. On a locked link a missing or rotated-away
32
+ * token resolves to `profile === null`, indistinguishable from an
33
+ * agent that does not exist.
34
+ */
35
+ readonly linkToken?: string;
36
+ }
37
+
38
+ /** Return value of {@link useSharedAgentProfile}. */
39
+ export interface UseSharedAgentProfileReturn {
40
+ /**
41
+ * The shared agent's public profile, or `null` while loading, on
42
+ * error, or when the agent is not shared.
43
+ */
44
+ readonly profile: SharedAgentProfile | null;
45
+ /** `true` while the initial fetch or a refetch is in flight. */
46
+ readonly isLoading: boolean;
47
+ /** `true` while a background refetch is in flight and stale data is shown. */
48
+ readonly isRefetching: boolean;
49
+ /** Error from the last failed request, or `null` when healthy. */
50
+ readonly error: Error | null;
51
+ /** Discard cached data and re-fetch the profile from the server. */
52
+ readonly refetch: () => void;
53
+ }
54
+
55
+ /**
56
+ * Data hook that fetches a shared agent's public profile by
57
+ * organization and agent slug.
58
+ *
59
+ * By default this hook calls the `getSharedProfile` endpoint, which is
60
+ * **public** — it requires no authentication. The server returns a
61
+ * trimmed projection safe for anonymous visitors: name, description,
62
+ * icon, and the default instance id needed to start a session
63
+ * (an identifier, not a capability — session creation still requires
64
+ * an authorized token, e.g. a guest token from `createGuestAuth`).
65
+ * With `{ audience: "org" }` it calls the authenticated
66
+ * `getSharedProfileForMember` endpoint instead, which resolves
67
+ * org-members-only shares for a signed-in member.
68
+ *
69
+ * **Not-shared handling:** the server answers NOT_FOUND for "this
70
+ * agent does not exist", "this agent is not shared", and (on the
71
+ * member path) "you are not a member of this organization" — all
72
+ * indistinguishable by design so a share URL leaks nothing. The hook
73
+ * maps that to `profile === null` without raising an error, matching
74
+ * `useAgent`'s not-found convention:
75
+ * `profile === null && !isLoading && !error` means unavailable.
76
+ *
77
+ * Pass `null` for either `org` or `slug` to skip fetching (stable
78
+ * no-op) — useful while route params are still resolving.
79
+ *
80
+ * **Note**: Although authentication is not required by the server,
81
+ * the hook still requires a `StigmerProvider` ancestor because it
82
+ * needs the transport configuration (API base URL) to make the
83
+ * request.
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * const { profile, isLoading, error } = useSharedAgentProfile(org, slug);
88
+ *
89
+ * if (isLoading) return <Spinner />;
90
+ * if (error) return <ErrorCard error={error} />;
91
+ * if (!profile) return <AgentUnavailable />;
92
+ *
93
+ * return <h1>{profile.name}</h1>;
94
+ * ```
95
+ */
96
+ export function useSharedAgentProfile(
97
+ org: string | null,
98
+ slug: string | null,
99
+ options?: UseSharedAgentProfileOptions,
100
+ ): UseSharedAgentProfileReturn {
101
+ const stigmer = useStigmer();
102
+ const audience = options?.audience ?? "public";
103
+ const linkToken = options?.linkToken ?? "";
104
+
105
+ const fetchFn =
106
+ org && slug
107
+ ? async () => {
108
+ try {
109
+ // The member path is tokenless by contract: org-audience access
110
+ // is gated by live membership, and token-locked public shares
111
+ // deliberately refuse there (see getSharedProfileForMember).
112
+ return audience === "org"
113
+ ? await stigmer.agent.getSharedProfileForMember({ org, slug })
114
+ : await stigmer.agent.getSharedProfile(
115
+ create(GetSharedProfileRequestSchema, { org, slug, linkToken }),
116
+ );
117
+ } catch (err) {
118
+ if (isNotFound(err)) return null;
119
+ throw err;
120
+ }
121
+ }
122
+ : null;
123
+
124
+ const { data: profile, isLoading, isRefetching, error, refetch } = useFetch(
125
+ fetchFn,
126
+ [org, slug, audience, linkToken, stigmer],
127
+ null as SharedAgentProfile | null,
128
+ );
129
+
130
+ return { profile, isLoading, isRefetching, error, refetch };
131
+ }
@@ -0,0 +1,151 @@
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 { UpdateAgentSharingInputSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/io_pb";
7
+ import {
8
+ AgentSharingAudience,
9
+ AgentSharingSchema,
10
+ AgentSharingMessagesSchema,
11
+ } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
12
+ import { useStigmer } from "../hooks.js";
13
+ import { toError } from "../internal/toError.js";
14
+
15
+ /**
16
+ * Who can chat with a shared agent over its hosted link.
17
+ *
18
+ * - `"public"` — anyone with the link; visitors are anonymous guests.
19
+ * - `"org"` — signed-in members of the owning organization only.
20
+ * Membership is checked on every conversation turn, so access ends the
21
+ * moment a member leaves the org.
22
+ */
23
+ export type SharingAudience = "public" | "org";
24
+
25
+ /**
26
+ * A complete sharing configuration to persist.
27
+ *
28
+ * Every field is required by design: the `updateSharing` RPC **replaces**
29
+ * the agent's whole `spec.sharing` block, so a caller must always supply
30
+ * the full configuration. A partial type here would let a toggle change
31
+ * silently wipe `allowedOrigins`, `messages`, or the `audience` — the
32
+ * required shape makes that mistake unrepresentable.
33
+ */
34
+ export interface AgentSharingDraft {
35
+ /** Whether hosted-chat access for the configured audience is enabled. */
36
+ readonly enabled: boolean;
37
+ /** Who can chat: anyone with the link, or org members only. */
38
+ readonly audience: SharingAudience;
39
+ /**
40
+ * Exact web origins allowed to embed the shared agent
41
+ * (e.g. `https://example.com`). Stored now; enforced by the embed
42
+ * widget's Origin check. Embedding is public-audience only.
43
+ */
44
+ readonly allowedOrigins: readonly string[];
45
+ /** Owner-customized visitor refusal copy. Empty strings mean platform defaults. */
46
+ readonly messages: {
47
+ /** Shown when a visitor exceeds the message rate limit. */
48
+ readonly rateLimited: string;
49
+ /** Shown when the org's credits are exhausted (sharing fails closed). */
50
+ readonly unavailable: string;
51
+ /** Shown when a conversation hits its turn limit or inactivity timeout. */
52
+ readonly conversationEnded: string;
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Maps a proto {@link AgentSharingAudience} to the SDK's string union.
58
+ * Unspecified means public by contract (pre-audience shares keep their
59
+ * anyone-with-link behavior).
60
+ */
61
+ export function sharingAudienceFromProto(
62
+ audience: AgentSharingAudience | undefined,
63
+ ): SharingAudience {
64
+ return audience === AgentSharingAudience.org ? "org" : "public";
65
+ }
66
+
67
+ function sharingAudienceToProto(audience: SharingAudience): AgentSharingAudience {
68
+ return audience === "org"
69
+ ? AgentSharingAudience.org
70
+ : AgentSharingAudience.public;
71
+ }
72
+
73
+ /** Return value of {@link useUpdateAgentSharing}. */
74
+ export interface UseUpdateAgentSharingReturn {
75
+ /**
76
+ * Persist the complete sharing configuration via the `updateSharing`
77
+ * RPC. Resolves with the updated agent.
78
+ */
79
+ readonly updateSharing: (draft: AgentSharingDraft) => Promise<Agent | undefined>;
80
+ /** `true` while the RPC is in flight. */
81
+ readonly isPending: boolean;
82
+ /** The last error from the RPC, or `null`. */
83
+ readonly error: Error | null;
84
+ }
85
+
86
+ /**
87
+ * Behavior hook that updates an agent's sharing configuration.
88
+ *
89
+ * Wraps the targeted `stigmer.agent.updateSharing()` RPC (authorized by
90
+ * `can_edit`) with loading and error state management. The hook is
91
+ * stateless with respect to the agent — the caller refreshes the resource
92
+ * after a successful update (e.g. via `refetch` from `useAgent`), matching
93
+ * the {@link useUpdateVisibility} convention.
94
+ *
95
+ * Pass `null` for `agentId` to produce a stable no-op (useful while the
96
+ * agent is still loading).
97
+ *
98
+ * @example
99
+ * ```tsx
100
+ * const { updateSharing, isPending } = useUpdateAgentSharing(agent.metadata.id);
101
+ *
102
+ * await updateSharing({
103
+ * enabled: true,
104
+ * audience: "public",
105
+ * allowedOrigins: ["https://example.com"],
106
+ * messages: { rateLimited: "", unavailable: "", conversationEnded: "" },
107
+ * });
108
+ * refetch();
109
+ * ```
110
+ */
111
+ export function useUpdateAgentSharing(
112
+ agentId: string | null,
113
+ ): UseUpdateAgentSharingReturn {
114
+ const stigmer = useStigmer();
115
+ const [isPending, setIsPending] = useState(false);
116
+ const [error, setError] = useState<Error | null>(null);
117
+
118
+ const updateSharing = useCallback(
119
+ async (draft: AgentSharingDraft): Promise<Agent | undefined> => {
120
+ if (!agentId) return undefined;
121
+
122
+ setIsPending(true);
123
+ setError(null);
124
+
125
+ try {
126
+ const input = create(UpdateAgentSharingInputSchema, {
127
+ resourceId: agentId,
128
+ sharing: create(AgentSharingSchema, {
129
+ enabled: draft.enabled,
130
+ audience: sharingAudienceToProto(draft.audience),
131
+ allowedOrigins: [...draft.allowedOrigins],
132
+ messages: create(AgentSharingMessagesSchema, {
133
+ rateLimited: draft.messages.rateLimited,
134
+ unavailable: draft.messages.unavailable,
135
+ conversationEnded: draft.messages.conversationEnded,
136
+ }),
137
+ }),
138
+ });
139
+ return await stigmer.agent.updateSharing(input);
140
+ } catch (err) {
141
+ setError(toError(err));
142
+ throw err;
143
+ } finally {
144
+ setIsPending(false);
145
+ }
146
+ },
147
+ [agentId, stigmer],
148
+ );
149
+
150
+ return { updateSharing, isPending, error };
151
+ }
@@ -0,0 +1,92 @@
1
+ "use client";
2
+
3
+ import { cn } from "@stigmer/theme";
4
+
5
+ /** Props for {@link Switch}. */
6
+ export interface SwitchProps {
7
+ /** Whether the switch is on. */
8
+ readonly checked: boolean;
9
+ /** Called with the next value when the user toggles the switch. */
10
+ readonly onCheckedChange: (checked: boolean) => void;
11
+ /** When `true`, the switch is shown but non-interactive. */
12
+ readonly disabled?: boolean;
13
+ /**
14
+ * Accessible name. Required unless `aria-labelledby` points at a
15
+ * visible label element.
16
+ */
17
+ readonly "aria-label"?: string;
18
+ /** ID of the element that labels this switch. */
19
+ readonly "aria-labelledby"?: string;
20
+ /** Element ID, for association with an external `<label htmlFor>`. */
21
+ readonly id?: string;
22
+ /** Additional CSS classes for the root button. */
23
+ readonly className?: string;
24
+ }
25
+
26
+ /**
27
+ * Accessible on/off toggle following the WAI-ARIA Switch pattern.
28
+ *
29
+ * Built on a native `<button role="switch">` so Space/Enter activation,
30
+ * focus, and disabled semantics come from the platform. `aria-checked`
31
+ * reflects the state for assistive technology.
32
+ *
33
+ * Use a switch only for instant binary state changes that take effect
34
+ * immediately (e.g. enabling sharing) — not for choices that need a
35
+ * separate save step (use radios or a select for those).
36
+ *
37
+ * The visible track is compact; an invisible expanded hit area keeps the
38
+ * touch target at ~44px without disturbing layout. All visual properties
39
+ * flow through `--stgm-*` design tokens, and the thumb transition is
40
+ * suppressed under `prefers-reduced-motion`.
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * <Switch
45
+ * checked={sharing.enabled}
46
+ * onCheckedChange={(next) => save({ ...sharing, enabled: next })}
47
+ * aria-label="Enable sharing"
48
+ * />
49
+ * ```
50
+ */
51
+ export function Switch({
52
+ checked,
53
+ onCheckedChange,
54
+ disabled,
55
+ "aria-label": ariaLabel,
56
+ "aria-labelledby": ariaLabelledby,
57
+ id,
58
+ className,
59
+ }: SwitchProps) {
60
+ return (
61
+ <button
62
+ type="button"
63
+ role="switch"
64
+ id={id}
65
+ aria-checked={checked}
66
+ aria-label={ariaLabel}
67
+ aria-labelledby={ariaLabelledby}
68
+ disabled={disabled}
69
+ onClick={() => onCheckedChange(!checked)}
70
+ className={cn(
71
+ // Track. `relative` anchors both the thumb and the expanded hit area.
72
+ "relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors",
73
+ "motion-reduce:transition-none",
74
+ checked ? "bg-primary" : "bg-muted",
75
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
76
+ "disabled:pointer-events-none disabled:opacity-50",
77
+ // Expanded invisible hit area (~44px tall) for touch targets.
78
+ "after:absolute after:-inset-3 after:content-['']",
79
+ className,
80
+ )}
81
+ >
82
+ <span
83
+ aria-hidden="true"
84
+ className={cn(
85
+ "pointer-events-none block size-4 rounded-full bg-background shadow-sm transition-transform",
86
+ "motion-reduce:transition-none",
87
+ checked ? "translate-x-[18px]" : "translate-x-0.5",
88
+ )}
89
+ />
90
+ </button>
91
+ );
92
+ }
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { render, screen, cleanup } from "@testing-library/react";
3
+ import { Switch } from "../Switch";
4
+
5
+ afterEach(cleanup);
6
+
7
+ describe("Switch", () => {
8
+ it("renders as role=switch with aria-checked reflecting state", () => {
9
+ const { rerender } = render(
10
+ <Switch checked={false} onCheckedChange={() => {}} aria-label="Enable sharing" />,
11
+ );
12
+
13
+ const el = screen.getByRole("switch", { name: "Enable sharing" });
14
+ expect(el.getAttribute("aria-checked")).toBe("false");
15
+
16
+ rerender(
17
+ <Switch checked onCheckedChange={() => {}} aria-label="Enable sharing" />,
18
+ );
19
+ expect(el.getAttribute("aria-checked")).toBe("true");
20
+ });
21
+
22
+ it("calls onCheckedChange with the next value on click", () => {
23
+ const onCheckedChange = vi.fn();
24
+ render(
25
+ <Switch checked={false} onCheckedChange={onCheckedChange} aria-label="Enable sharing" />,
26
+ );
27
+
28
+ screen.getByRole("switch").click();
29
+ expect(onCheckedChange).toHaveBeenCalledTimes(1);
30
+ expect(onCheckedChange).toHaveBeenCalledWith(true);
31
+ });
32
+
33
+ it("calls onCheckedChange(false) when toggling off", () => {
34
+ const onCheckedChange = vi.fn();
35
+ render(
36
+ <Switch checked onCheckedChange={onCheckedChange} aria-label="Enable sharing" />,
37
+ );
38
+
39
+ screen.getByRole("switch").click();
40
+ expect(onCheckedChange).toHaveBeenCalledWith(false);
41
+ });
42
+
43
+ it("is a native button, so Space/Enter activation comes from the platform", () => {
44
+ render(
45
+ <Switch checked={false} onCheckedChange={() => {}} aria-label="Enable sharing" />,
46
+ );
47
+
48
+ const el = screen.getByRole("switch");
49
+ // happy-dom does not synthesize click from keydown; assert the native
50
+ // button element carries the behavior instead of re-implementing it.
51
+ expect(el.tagName).toBe("BUTTON");
52
+ expect(el.getAttribute("type")).toBe("button");
53
+ });
54
+
55
+ it("does not fire when disabled", () => {
56
+ const onCheckedChange = vi.fn();
57
+ render(
58
+ <Switch
59
+ checked={false}
60
+ onCheckedChange={onCheckedChange}
61
+ disabled
62
+ aria-label="Enable sharing"
63
+ />,
64
+ );
65
+
66
+ const el = screen.getByRole("switch") as HTMLButtonElement;
67
+ expect(el.disabled).toBe(true);
68
+ el.click();
69
+ expect(onCheckedChange).not.toHaveBeenCalled();
70
+ });
71
+
72
+ it("supports aria-labelledby for external labels", () => {
73
+ render(
74
+ <>
75
+ <span id="share-label">Enable sharing</span>
76
+ <Switch
77
+ checked={false}
78
+ onCheckedChange={() => {}}
79
+ aria-labelledby="share-label"
80
+ />
81
+ </>,
82
+ );
83
+
84
+ expect(
85
+ screen.getByRole("switch", { name: "Enable sharing" }),
86
+ ).toBeTruthy();
87
+ });
88
+ });
@@ -0,0 +1,2 @@
1
+ export { Switch } from "./Switch.js";
2
+ export type { SwitchProps } from "./Switch.js";