@stigmer/react 3.1.7 → 3.1.9

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 (206) 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/EnvironmentPicker.d.ts +17 -1
  9. package/environment/EnvironmentPicker.d.ts.map +1 -1
  10. package/environment/EnvironmentPicker.js +3 -2
  11. package/environment/EnvironmentPicker.js.map +1 -1
  12. package/environment/EnvironmentVariableEditor.d.ts.map +1 -1
  13. package/environment/EnvironmentVariableEditor.js +9 -3
  14. package/environment/EnvironmentVariableEditor.js.map +1 -1
  15. package/environment/index.d.ts +1 -0
  16. package/environment/index.d.ts.map +1 -1
  17. package/environment/index.js +1 -0
  18. package/environment/index.js.map +1 -1
  19. package/environment/shareRestriction.d.ts +13 -0
  20. package/environment/shareRestriction.d.ts.map +1 -0
  21. package/environment/shareRestriction.js +15 -0
  22. package/environment/shareRestriction.js.map +1 -0
  23. package/execution/ToolCallItem.d.ts.map +1 -1
  24. package/execution/ToolCallItem.js +12 -0
  25. package/execution/ToolCallItem.js.map +1 -1
  26. package/execution/ToolRunGroup.js +4 -0
  27. package/execution/ToolRunGroup.js.map +1 -1
  28. package/execution/WriteBackCard.d.ts +14 -5
  29. package/execution/WriteBackCard.d.ts.map +1 -1
  30. package/execution/WriteBackCard.js +103 -32
  31. package/execution/WriteBackCard.js.map +1 -1
  32. package/execution/WriteBacksWidget.d.ts +1 -1
  33. package/execution/WriteBacksWidget.js +2 -2
  34. package/execution/WriteBacksWidget.js.map +1 -1
  35. package/execution/index.d.ts +2 -0
  36. package/execution/index.d.ts.map +1 -1
  37. package/execution/index.js +1 -0
  38. package/execution/index.js.map +1 -1
  39. package/execution/write-back-utils.d.ts +57 -0
  40. package/execution/write-back-utils.d.ts.map +1 -0
  41. package/execution/write-back-utils.js +81 -0
  42. package/execution/write-back-utils.js.map +1 -0
  43. package/index.d.ts +6 -2
  44. package/index.d.ts.map +1 -1
  45. package/index.js +6 -1
  46. package/index.js.map +1 -1
  47. package/internal/dev/dom-counter.d.ts.map +1 -1
  48. package/internal/dev/dom-counter.js +2 -2
  49. package/internal/dev/dom-counter.js.map +1 -1
  50. package/internal/dev/enabled.d.ts +7 -0
  51. package/internal/dev/enabled.d.ts.map +1 -0
  52. package/internal/dev/enabled.js +38 -0
  53. package/internal/dev/enabled.js.map +1 -0
  54. package/internal/dev/profiler-wrapper.d.ts +3 -3
  55. package/internal/dev/profiler-wrapper.d.ts.map +1 -1
  56. package/internal/dev/profiler-wrapper.js +5 -5
  57. package/internal/dev/profiler-wrapper.js.map +1 -1
  58. package/internal/dev/use-key-stability.d.ts.map +1 -1
  59. package/internal/dev/use-key-stability.js +2 -2
  60. package/internal/dev/use-key-stability.js.map +1 -1
  61. package/internal/dev/use-render-tracer.d.ts +2 -2
  62. package/internal/dev/use-render-tracer.d.ts.map +1 -1
  63. package/internal/dev/use-render-tracer.js +4 -4
  64. package/internal/dev/use-render-tracer.js.map +1 -1
  65. package/internal/dev/use-stream-rate.d.ts +2 -1
  66. package/internal/dev/use-stream-rate.d.ts.map +1 -1
  67. package/internal/dev/use-stream-rate.js +4 -3
  68. package/internal/dev/use-stream-rate.js.map +1 -1
  69. package/library/ResourceVisibilityControl.d.ts +3 -0
  70. package/library/ResourceVisibilityControl.d.ts.map +1 -1
  71. package/library/ResourceVisibilityControl.js +22 -8
  72. package/library/ResourceVisibilityControl.js.map +1 -1
  73. package/library/useUpdateVisibility.d.ts +5 -3
  74. package/library/useUpdateVisibility.d.ts.map +1 -1
  75. package/library/useUpdateVisibility.js +7 -2
  76. package/library/useUpdateVisibility.js.map +1 -1
  77. package/library/visibilityLevels.d.ts +17 -0
  78. package/library/visibilityLevels.d.ts.map +1 -1
  79. package/library/visibilityLevels.js +32 -0
  80. package/library/visibilityLevels.js.map +1 -1
  81. package/package.json +4 -4
  82. package/session/NewSessionViewer.d.ts +11 -1
  83. package/session/NewSessionViewer.d.ts.map +1 -1
  84. package/session/NewSessionViewer.js +28 -8
  85. package/session/NewSessionViewer.js.map +1 -1
  86. package/session/SessionViewer.d.ts +9 -2
  87. package/session/SessionViewer.d.ts.map +1 -1
  88. package/session/SessionViewer.js +26 -12
  89. package/session/SessionViewer.js.map +1 -1
  90. package/session/audience.d.ts +12 -4
  91. package/session/audience.d.ts.map +1 -1
  92. package/session/facets/ChangesTab.d.ts.map +1 -1
  93. package/session/facets/ChangesTab.js +3 -1
  94. package/session/facets/ChangesTab.js.map +1 -1
  95. package/session/useNewSessionFlow.d.ts +12 -0
  96. package/session/useNewSessionFlow.d.ts.map +1 -1
  97. package/session/useNewSessionFlow.js +13 -1
  98. package/session/useNewSessionFlow.js.map +1 -1
  99. package/session/useSessionPageFlow.d.ts +13 -0
  100. package/session/useSessionPageFlow.d.ts.map +1 -1
  101. package/session/useSessionPageFlow.js +7 -2
  102. package/session/useSessionPageFlow.js.map +1 -1
  103. package/sharing/ShareAgentDialog.d.ts +57 -0
  104. package/sharing/ShareAgentDialog.d.ts.map +1 -0
  105. package/sharing/ShareAgentDialog.js +478 -0
  106. package/sharing/ShareAgentDialog.js.map +1 -0
  107. package/sharing/SharedAgentChat.d.ts +84 -0
  108. package/sharing/SharedAgentChat.d.ts.map +1 -0
  109. package/sharing/SharedAgentChat.js +80 -0
  110. package/sharing/SharedAgentChat.js.map +1 -0
  111. package/sharing/index.d.ts +18 -0
  112. package/sharing/index.d.ts.map +1 -0
  113. package/sharing/index.js +12 -0
  114. package/sharing/index.js.map +1 -0
  115. package/sharing/useAgentShare.d.ts +43 -0
  116. package/sharing/useAgentShare.d.ts.map +1 -0
  117. package/sharing/useAgentShare.js +54 -0
  118. package/sharing/useAgentShare.js.map +1 -0
  119. package/sharing/useRotateShareLink.d.ts +45 -0
  120. package/sharing/useRotateShareLink.d.ts.map +1 -0
  121. package/sharing/useRotateShareLink.js +58 -0
  122. package/sharing/useRotateShareLink.js.map +1 -0
  123. package/sharing/useSaveAgentShare.d.ts +105 -0
  124. package/sharing/useSaveAgentShare.d.ts.map +1 -0
  125. package/sharing/useSaveAgentShare.js +97 -0
  126. package/sharing/useSaveAgentShare.js.map +1 -0
  127. package/sharing/useShareAgent.d.ts +69 -0
  128. package/sharing/useShareAgent.d.ts.map +1 -0
  129. package/sharing/useShareAgent.js +42 -0
  130. package/sharing/useShareAgent.js.map +1 -0
  131. package/sharing/useShareToolReadiness.d.ts +52 -0
  132. package/sharing/useShareToolReadiness.d.ts.map +1 -0
  133. package/sharing/useShareToolReadiness.js +89 -0
  134. package/sharing/useShareToolReadiness.js.map +1 -0
  135. package/sharing/useSharedAgentProfile.d.ts +85 -0
  136. package/sharing/useSharedAgentProfile.d.ts.map +1 -0
  137. package/sharing/useSharedAgentProfile.js +73 -0
  138. package/sharing/useSharedAgentProfile.js.map +1 -0
  139. package/src/agent/AgentDetailView.tsx +28 -3
  140. package/src/environment/EnvironmentListPanel.tsx +47 -18
  141. package/src/environment/EnvironmentPicker.tsx +23 -2
  142. package/src/environment/EnvironmentVariableEditor.tsx +27 -17
  143. package/src/environment/__tests__/shareRestriction.test.ts +35 -0
  144. package/src/environment/index.ts +5 -0
  145. package/src/environment/shareRestriction.ts +22 -0
  146. package/src/execution/ToolCallItem.tsx +19 -1
  147. package/src/execution/ToolRunGroup.tsx +4 -0
  148. package/src/execution/WriteBackCard.tsx +285 -88
  149. package/src/execution/WriteBacksWidget.tsx +5 -5
  150. package/src/execution/__tests__/ToolCallItem.test.tsx +51 -0
  151. package/src/execution/__tests__/ToolRunGroup.test.tsx +22 -0
  152. package/src/execution/__tests__/WriteBackCard.test.tsx +64 -10
  153. package/src/execution/__tests__/write-back-utils.test.ts +135 -0
  154. package/src/execution/index.ts +7 -0
  155. package/src/execution/write-back-utils.ts +107 -0
  156. package/src/index.ts +38 -0
  157. package/src/internal/dev/__tests__/use-key-stability.test.ts +4 -0
  158. package/src/internal/dev/__tests__/use-render-tracer.test.ts +4 -0
  159. package/src/internal/dev/dom-counter.ts +2 -3
  160. package/src/internal/dev/enabled.ts +39 -0
  161. package/src/internal/dev/profiler-wrapper.tsx +5 -6
  162. package/src/internal/dev/use-key-stability.ts +2 -3
  163. package/src/internal/dev/use-render-tracer.ts +4 -5
  164. package/src/internal/dev/use-stream-rate.ts +4 -4
  165. package/src/library/ResourceVisibilityControl.tsx +23 -7
  166. package/src/library/__tests__/visibilityLevels.test.ts +33 -0
  167. package/src/library/useUpdateVisibility.ts +9 -3
  168. package/src/library/visibilityLevels.ts +36 -0
  169. package/src/session/NewSessionViewer.tsx +67 -31
  170. package/src/session/SessionViewer.tsx +67 -36
  171. package/src/session/__tests__/audienceWiring.test.tsx +99 -0
  172. package/src/session/__tests__/useNewSessionFlow.test.tsx +75 -1
  173. package/src/session/__tests__/useSessionPageFlow.guest.test.tsx +112 -0
  174. package/src/session/audience.ts +12 -4
  175. package/src/session/facets/ChangesTab.tsx +6 -4
  176. package/src/session/useNewSessionFlow.ts +26 -1
  177. package/src/session/useSessionPageFlow.ts +24 -2
  178. package/src/sharing/ShareAgentDialog.tsx +1489 -0
  179. package/src/sharing/SharedAgentChat.tsx +283 -0
  180. package/src/sharing/__tests__/ShareAgentDialog.test.tsx +926 -0
  181. package/src/sharing/__tests__/SharedAgentChat.test.tsx +222 -0
  182. package/src/sharing/__tests__/useAgentShare.test.tsx +149 -0
  183. package/src/sharing/__tests__/useRotateShareLink.test.tsx +96 -0
  184. package/src/sharing/__tests__/useSaveAgentShare.test.tsx +241 -0
  185. package/src/sharing/__tests__/useShareAgent.test.tsx +131 -0
  186. package/src/sharing/__tests__/useShareToolReadiness.test.tsx +219 -0
  187. package/src/sharing/__tests__/useSharedAgentProfile.test.tsx +201 -0
  188. package/src/sharing/index.ts +32 -0
  189. package/src/sharing/useAgentShare.ts +91 -0
  190. package/src/sharing/useRotateShareLink.ts +81 -0
  191. package/src/sharing/useSaveAgentShare.ts +186 -0
  192. package/src/sharing/useShareAgent.tsx +107 -0
  193. package/src/sharing/useShareToolReadiness.ts +131 -0
  194. package/src/sharing/useSharedAgentProfile.ts +132 -0
  195. package/src/switch/Switch.tsx +97 -0
  196. package/src/switch/__tests__/Switch.test.tsx +107 -0
  197. package/src/switch/index.ts +2 -0
  198. package/styles.css +1 -1
  199. package/switch/Switch.d.ts +47 -0
  200. package/switch/Switch.d.ts.map +1 -0
  201. package/switch/Switch.js +41 -0
  202. package/switch/Switch.js.map +1 -0
  203. package/switch/index.d.ts +3 -0
  204. package/switch/index.d.ts.map +1 -0
  205. package/switch/index.js +2 -0
  206. package/switch/index.js.map +1 -0
@@ -0,0 +1,107 @@
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
+ * AgentShare create/apply handlers enforce on the referenced agent, so
58
+ * the action never appears to a user whose changes would be rejected.
59
+ * Returns a `null` action while the agent is loading or the user cannot
60
+ * edit, so the host can unconditionally fold `action` into its actions
61
+ * array.
62
+ *
63
+ * @example
64
+ * ```tsx
65
+ * const share = useShareAgent({
66
+ * agent,
67
+ * buildShareUrl: (org, slug) => `${appOrigin}/chat/${org}/${slug}`,
68
+ * onSharingChanged: refetch,
69
+ * });
70
+ * // ...
71
+ * <ResourceDetailShell actions={share.action ? [...actions, share.action] : actions} ... />
72
+ * {share.dialog}
73
+ * ```
74
+ */
75
+ export function useShareAgent({
76
+ agent,
77
+ buildShareUrl,
78
+ onSharingChanged,
79
+ label = "Share",
80
+ }: UseShareAgentArgs): UseShareAgentReturn {
81
+ const [isOpen, setIsOpen] = useState(false);
82
+
83
+ const agentId = agent?.metadata?.id ?? null;
84
+ const { allowed: canEdit } = useCheckPermission(
85
+ agentId ? { kind: "agent", id: agentId } : null,
86
+ "can_edit",
87
+ );
88
+
89
+ const open = useCallback(() => setIsOpen(true), []);
90
+
91
+ const action: DetailAction | null =
92
+ agent && canEdit
93
+ ? { id: "share", label, group: "sharing", onAction: open }
94
+ : null;
95
+
96
+ const dialog = agent ? (
97
+ <ShareAgentDialog
98
+ open={isOpen}
99
+ onOpenChange={setIsOpen}
100
+ agent={agent}
101
+ buildShareUrl={buildShareUrl}
102
+ onSharingChanged={onSharingChanged}
103
+ />
104
+ ) : null;
105
+
106
+ return { action, dialog, open, isOpen };
107
+ }
@@ -0,0 +1,131 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, 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
+ import type { AgentShareDraft } from "./useSaveAgentShare.js";
9
+
10
+ /**
11
+ * Readiness of a share's tool credentials for visitor executions.
12
+ *
13
+ * - `na` — nothing to check: the agent uses no MCP tools, sharing is
14
+ * off, the audience is org (bindings are public-audience only), or
15
+ * the deployment cannot serve guest chat (local mode).
16
+ * - `needs-credentials` — the agent uses MCP tools but the share has no
17
+ * environments bound: visitors' tool calls will fail until the owner
18
+ * binds credentials.
19
+ * - `checking` — lookups in flight.
20
+ * - `ready` — every environment bound to the share is shared with the
21
+ * organization.
22
+ * - `blocked` — one or more bound environments are private; their
23
+ * `org/slug` references are listed for the hint copy.
24
+ */
25
+ export type ShareToolReadiness =
26
+ | { readonly status: "na" }
27
+ | { readonly status: "needs-credentials" }
28
+ | { readonly status: "checking" }
29
+ | { readonly status: "ready" }
30
+ | { readonly status: "blocked"; readonly privateEnvironments: readonly string[] };
31
+
32
+ const NA: ShareToolReadiness = { status: "na" };
33
+ const NEEDS_CREDENTIALS: ShareToolReadiness = { status: "needs-credentials" };
34
+
35
+ /**
36
+ * Data hook that checks whether a tool-using shared agent's credentials
37
+ * will work for visitors — i.e. whether the share binds environments
38
+ * (`environment_refs`) and each one is shared with the organization
39
+ * (`visibility_org`).
40
+ *
41
+ * Guest executions receive credentials exclusively from the share's own
42
+ * `environment_refs`, resolved through the org-shared environment seam
43
+ * (decision 011 — the agent's default instance stays pristine and is
44
+ * never consulted). So a tool-using agent with an empty binding list is
45
+ * *guaranteed* broken for visitors, and this hook says so explicitly
46
+ * (`needs-credentials`) instead of staying silent — the gap that made
47
+ * session 12's misconfiguration invisible until a visitor's first
48
+ * message failed.
49
+ *
50
+ * The check runs only when the share is enabled with a public audience
51
+ * (org-audience shares reject bindings at the proto boundary), the
52
+ * deployment is cloud (local mode has no guest runtime or secret
53
+ * gating), and the agent declares MCP server usages. All lookups run as
54
+ * the owner viewing the dialog, who can read the bound environments.
55
+ */
56
+ export function useShareToolReadiness(
57
+ agent: Agent,
58
+ draft: AgentShareDraft,
59
+ ): ShareToolReadiness {
60
+ const stigmer = useStigmer();
61
+ const deploymentMode = useDeploymentMode();
62
+ const [checked, setChecked] = useState<ShareToolReadiness>(NA);
63
+
64
+ const hasMcpTools = (agent.spec?.mcpServerUsages?.length ?? 0) > 0;
65
+ const applicable =
66
+ draft.enabled &&
67
+ draft.audience === "public" &&
68
+ deploymentMode === "cloud" &&
69
+ hasMcpTools;
70
+
71
+ // Stable key for the effect: the visibility lookups depend only on
72
+ // which environments are bound, not on the array's identity.
73
+ const refsKey = useMemo(
74
+ () => draft.environmentRefs.map((ref) => `${ref.org}/${ref.slug}`).join(","),
75
+ [draft.environmentRefs],
76
+ );
77
+
78
+ const shouldFetch = applicable && refsKey !== "";
79
+
80
+ useEffect(() => {
81
+ if (!shouldFetch) {
82
+ setChecked(NA);
83
+ return;
84
+ }
85
+
86
+ let cancelled = false;
87
+ setChecked({ status: "checking" });
88
+
89
+ (async (): Promise<ShareToolReadiness> => {
90
+ const privateRefs: string[] = [];
91
+ for (const ref of refsKey.split(",")) {
92
+ const [org, slug] = ref.split("/");
93
+ try {
94
+ const env = await stigmer.environment.getByReference({ org, slug });
95
+ if (
96
+ env.metadata?.visibility !== ApiResourceVisibility.visibility_org
97
+ ) {
98
+ privateRefs.push(ref);
99
+ }
100
+ } catch {
101
+ // Unreadable ref (deleted, foreign): the runtime's merge will
102
+ // skip it for visitors too — surface it as blocking.
103
+ privateRefs.push(ref);
104
+ }
105
+ }
106
+
107
+ return privateRefs.length === 0
108
+ ? { status: "ready" }
109
+ : { status: "blocked", privateEnvironments: privateRefs };
110
+ })().then(
111
+ (result) => {
112
+ if (!cancelled) setChecked(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) setChecked(NA);
118
+ },
119
+ );
120
+
121
+ return () => {
122
+ cancelled = true;
123
+ };
124
+ }, [shouldFetch, refsKey, stigmer]);
125
+
126
+ if (!applicable) return NA;
127
+ // Zero bindings needs no lookup: a tool-using agent without bound
128
+ // credentials is broken for visitors by construction.
129
+ if (refsKey === "") return NEEDS_CREDENTIALS;
130
+ return checked;
131
+ }
@@ -0,0 +1,132 @@
1
+ "use client";
2
+
3
+ import { create } from "@bufbuild/protobuf";
4
+ import {
5
+ GetSharedProfileRequestSchema,
6
+ type SharedAgentProfile,
7
+ } from "@stigmer/protos/ai/stigmer/agentic/agentshare/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 "./useSaveAgentShare.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 the org and
57
+ * slug from the share URL (the share's slug — it defaults to the
58
+ * agent's, so existing links resolve unchanged).
59
+ *
60
+ * By default this hook calls the `getSharedProfile` endpoint, which is
61
+ * **public** — it requires no authentication. The server returns a
62
+ * trimmed projection safe for anonymous visitors: name, description,
63
+ * icon, and the default instance id needed to start a session
64
+ * (an identifier, not a capability — session creation still requires
65
+ * an authorized token, e.g. a guest token from `createGuestAuth`).
66
+ * With `{ audience: "org" }` it calls the authenticated
67
+ * `getSharedProfileForMember` endpoint instead, which resolves
68
+ * org-members-only shares for a signed-in member.
69
+ *
70
+ * **Not-shared handling:** the server answers NOT_FOUND for "this
71
+ * agent does not exist", "this agent is not shared", and (on the
72
+ * member path) "you are not a member of this organization" — all
73
+ * indistinguishable by design so a share URL leaks nothing. The hook
74
+ * maps that to `profile === null` without raising an error, matching
75
+ * `useAgent`'s not-found convention:
76
+ * `profile === null && !isLoading && !error` means unavailable.
77
+ *
78
+ * Pass `null` for either `org` or `slug` to skip fetching (stable
79
+ * no-op) — useful while route params are still resolving.
80
+ *
81
+ * **Note**: Although authentication is not required by the server,
82
+ * the hook still requires a `StigmerProvider` ancestor because it
83
+ * needs the transport configuration (API base URL) to make the
84
+ * request.
85
+ *
86
+ * @example
87
+ * ```tsx
88
+ * const { profile, isLoading, error } = useSharedAgentProfile(org, slug);
89
+ *
90
+ * if (isLoading) return <Spinner />;
91
+ * if (error) return <ErrorCard error={error} />;
92
+ * if (!profile) return <AgentUnavailable />;
93
+ *
94
+ * return <h1>{profile.name}</h1>;
95
+ * ```
96
+ */
97
+ export function useSharedAgentProfile(
98
+ org: string | null,
99
+ slug: string | null,
100
+ options?: UseSharedAgentProfileOptions,
101
+ ): UseSharedAgentProfileReturn {
102
+ const stigmer = useStigmer();
103
+ const audience = options?.audience ?? "public";
104
+ const linkToken = options?.linkToken ?? "";
105
+
106
+ const fetchFn =
107
+ org && slug
108
+ ? async () => {
109
+ try {
110
+ // The member path is tokenless by contract: org-audience access
111
+ // is gated by live membership, and token-locked public shares
112
+ // deliberately refuse there (see getSharedProfileForMember).
113
+ return audience === "org"
114
+ ? await stigmer.agentShare.getSharedProfileForMember({ org, slug })
115
+ : await stigmer.agentShare.getSharedProfile(
116
+ create(GetSharedProfileRequestSchema, { org, slug, linkToken }),
117
+ );
118
+ } catch (err) {
119
+ if (isNotFound(err)) return null;
120
+ throw err;
121
+ }
122
+ }
123
+ : null;
124
+
125
+ const { data: profile, isLoading, isRefetching, error, refetch } = useFetch(
126
+ fetchFn,
127
+ [org, slug, audience, linkToken, stigmer],
128
+ null as SharedAgentProfile | null,
129
+ );
130
+
131
+ return { profile, isLoading, isRefetching, error, refetch };
132
+ }
@@ -0,0 +1,97 @@
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
+ // Off state uses the form-control token (`--stgm-input`), NOT
73
+ // `bg-muted`: muted matches the popover surface in dark presets, which
74
+ // made the track invisible inside dialogs. Both track fills are
75
+ // registered as surface pairs in the theme contrast contract
76
+ // (@stigmer/theme contract/pairs.ts), so every preset is audited.
77
+ "relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors",
78
+ "motion-reduce:transition-none",
79
+ checked ? "bg-primary" : "bg-input",
80
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
81
+ "disabled:pointer-events-none disabled:opacity-50",
82
+ // Expanded invisible hit area (~44px tall) for touch targets.
83
+ "after:absolute after:-inset-3 after:content-['']",
84
+ className,
85
+ )}
86
+ >
87
+ <span
88
+ aria-hidden="true"
89
+ className={cn(
90
+ "pointer-events-none block size-4 rounded-full bg-background shadow-sm transition-transform",
91
+ "motion-reduce:transition-none",
92
+ checked ? "translate-x-[18px]" : "translate-x-0.5",
93
+ )}
94
+ />
95
+ </button>
96
+ );
97
+ }
@@ -0,0 +1,107 @@
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("fills the track with the contract-audited surface tokens", () => {
73
+ // bg-input (off) / bg-primary (on) are registered as surface pairs in
74
+ // the @stigmer/theme contrast contract — bg-muted is NOT valid here (it
75
+ // matches the popover surface in dark presets, making the track
76
+ // invisible inside dialogs).
77
+ const { rerender } = render(
78
+ <Switch checked={false} onCheckedChange={() => {}} aria-label="Enable sharing" />,
79
+ );
80
+
81
+ const el = screen.getByRole("switch");
82
+ expect(el.className).toContain("bg-input");
83
+ expect(el.className).not.toContain("bg-muted");
84
+
85
+ rerender(
86
+ <Switch checked onCheckedChange={() => {}} aria-label="Enable sharing" />,
87
+ );
88
+ expect(el.className).toContain("bg-primary");
89
+ });
90
+
91
+ it("supports aria-labelledby for external labels", () => {
92
+ render(
93
+ <>
94
+ <span id="share-label">Enable sharing</span>
95
+ <Switch
96
+ checked={false}
97
+ onCheckedChange={() => {}}
98
+ aria-labelledby="share-label"
99
+ />
100
+ </>,
101
+ );
102
+
103
+ expect(
104
+ screen.getByRole("switch", { name: "Enable sharing" }),
105
+ ).toBeTruthy();
106
+ });
107
+ });
@@ -0,0 +1,2 @@
1
+ export { Switch } from "./Switch.js";
2
+ export type { SwitchProps } from "./Switch.js";