@stigmer/react 3.1.6 → 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/mcp-server/McpServerConnectDialog.d.ts.map +1 -1
- package/mcp-server/McpServerConnectDialog.js +7 -3
- package/mcp-server/McpServerConnectDialog.js.map +1 -1
- package/mcp-server/McpServerDetailView.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +6 -3
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/mcp-server/McpServerPicker.d.ts.map +1 -1
- package/mcp-server/McpServerPicker.js +3 -1
- package/mcp-server/McpServerPicker.js.map +1 -1
- package/mcp-server/OAuthRequiredNotice.d.ts +29 -0
- package/mcp-server/OAuthRequiredNotice.d.ts.map +1 -0
- package/mcp-server/OAuthRequiredNotice.js +29 -0
- package/mcp-server/OAuthRequiredNotice.js.map +1 -0
- package/mcp-server/StdioSandboxNotice.d.ts +43 -0
- package/mcp-server/StdioSandboxNotice.d.ts.map +1 -0
- package/mcp-server/StdioSandboxNotice.js +45 -0
- package/mcp-server/StdioSandboxNotice.js.map +1 -0
- package/mcp-server/useMcpServerCredentials.d.ts +12 -0
- package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
- package/mcp-server/useMcpServerCredentials.js +12 -4
- package/mcp-server/useMcpServerCredentials.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/mcp-server/McpServerConnectDialog.tsx +23 -11
- package/src/mcp-server/McpServerDetailView.tsx +22 -9
- package/src/mcp-server/McpServerPicker.tsx +12 -9
- package/src/mcp-server/OAuthRequiredNotice.tsx +77 -0
- package/src/mcp-server/StdioSandboxNotice.tsx +98 -0
- package/src/mcp-server/__tests__/OAuthRequiredNotice.test.tsx +25 -0
- package/src/mcp-server/__tests__/StdioSandboxNotice.test.tsx +71 -0
- package/src/mcp-server/useMcpServerCredentials.ts +25 -4
- 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,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
|
+
});
|