@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.
- 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/EnvironmentPicker.d.ts +17 -1
- package/environment/EnvironmentPicker.d.ts.map +1 -1
- package/environment/EnvironmentPicker.js +3 -2
- package/environment/EnvironmentPicker.js.map +1 -1
- package/environment/EnvironmentVariableEditor.d.ts.map +1 -1
- package/environment/EnvironmentVariableEditor.js +9 -3
- package/environment/EnvironmentVariableEditor.js.map +1 -1
- package/environment/index.d.ts +1 -0
- package/environment/index.d.ts.map +1 -1
- package/environment/index.js +1 -0
- package/environment/index.js.map +1 -1
- package/environment/shareRestriction.d.ts +13 -0
- package/environment/shareRestriction.d.ts.map +1 -0
- package/environment/shareRestriction.js +15 -0
- package/environment/shareRestriction.js.map +1 -0
- package/execution/ToolCallItem.d.ts.map +1 -1
- package/execution/ToolCallItem.js +12 -0
- package/execution/ToolCallItem.js.map +1 -1
- package/execution/ToolRunGroup.js +4 -0
- package/execution/ToolRunGroup.js.map +1 -1
- package/execution/WriteBackCard.d.ts +14 -5
- package/execution/WriteBackCard.d.ts.map +1 -1
- package/execution/WriteBackCard.js +103 -32
- package/execution/WriteBackCard.js.map +1 -1
- package/execution/WriteBacksWidget.d.ts +1 -1
- package/execution/WriteBacksWidget.js +2 -2
- package/execution/WriteBacksWidget.js.map +1 -1
- package/execution/index.d.ts +2 -0
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +1 -0
- package/execution/index.js.map +1 -1
- package/execution/write-back-utils.d.ts +57 -0
- package/execution/write-back-utils.d.ts.map +1 -0
- package/execution/write-back-utils.js +81 -0
- package/execution/write-back-utils.js.map +1 -0
- package/index.d.ts +6 -2
- package/index.d.ts.map +1 -1
- package/index.js +6 -1
- package/index.js.map +1 -1
- package/internal/dev/dom-counter.d.ts.map +1 -1
- package/internal/dev/dom-counter.js +2 -2
- package/internal/dev/dom-counter.js.map +1 -1
- package/internal/dev/enabled.d.ts +7 -0
- package/internal/dev/enabled.d.ts.map +1 -0
- package/internal/dev/enabled.js +38 -0
- package/internal/dev/enabled.js.map +1 -0
- package/internal/dev/profiler-wrapper.d.ts +3 -3
- package/internal/dev/profiler-wrapper.d.ts.map +1 -1
- package/internal/dev/profiler-wrapper.js +5 -5
- package/internal/dev/profiler-wrapper.js.map +1 -1
- package/internal/dev/use-key-stability.d.ts.map +1 -1
- package/internal/dev/use-key-stability.js +2 -2
- package/internal/dev/use-key-stability.js.map +1 -1
- package/internal/dev/use-render-tracer.d.ts +2 -2
- package/internal/dev/use-render-tracer.d.ts.map +1 -1
- package/internal/dev/use-render-tracer.js +4 -4
- package/internal/dev/use-render-tracer.js.map +1 -1
- package/internal/dev/use-stream-rate.d.ts +2 -1
- package/internal/dev/use-stream-rate.d.ts.map +1 -1
- package/internal/dev/use-stream-rate.js +4 -3
- package/internal/dev/use-stream-rate.js.map +1 -1
- package/library/ResourceVisibilityControl.d.ts +3 -0
- package/library/ResourceVisibilityControl.d.ts.map +1 -1
- package/library/ResourceVisibilityControl.js +22 -8
- package/library/ResourceVisibilityControl.js.map +1 -1
- package/library/useUpdateVisibility.d.ts +5 -3
- package/library/useUpdateVisibility.d.ts.map +1 -1
- package/library/useUpdateVisibility.js +7 -2
- package/library/useUpdateVisibility.js.map +1 -1
- package/library/visibilityLevels.d.ts +17 -0
- package/library/visibilityLevels.d.ts.map +1 -1
- package/library/visibilityLevels.js +32 -0
- package/library/visibilityLevels.js.map +1 -1
- package/package.json +4 -4
- package/session/NewSessionViewer.d.ts +11 -1
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +28 -8
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +9 -2
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +26 -12
- package/session/SessionViewer.js.map +1 -1
- package/session/audience.d.ts +12 -4
- package/session/audience.d.ts.map +1 -1
- package/session/facets/ChangesTab.d.ts.map +1 -1
- package/session/facets/ChangesTab.js +3 -1
- package/session/facets/ChangesTab.js.map +1 -1
- package/session/useNewSessionFlow.d.ts +12 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +13 -1
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +13 -0
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +7 -2
- package/session/useSessionPageFlow.js.map +1 -1
- package/sharing/ShareAgentDialog.d.ts +57 -0
- package/sharing/ShareAgentDialog.d.ts.map +1 -0
- package/sharing/ShareAgentDialog.js +478 -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 +18 -0
- package/sharing/index.d.ts.map +1 -0
- package/sharing/index.js +12 -0
- package/sharing/index.js.map +1 -0
- package/sharing/useAgentShare.d.ts +43 -0
- package/sharing/useAgentShare.d.ts.map +1 -0
- package/sharing/useAgentShare.js +54 -0
- package/sharing/useAgentShare.js.map +1 -0
- package/sharing/useRotateShareLink.d.ts +45 -0
- package/sharing/useRotateShareLink.d.ts.map +1 -0
- package/sharing/useRotateShareLink.js +58 -0
- package/sharing/useRotateShareLink.js.map +1 -0
- package/sharing/useSaveAgentShare.d.ts +105 -0
- package/sharing/useSaveAgentShare.d.ts.map +1 -0
- package/sharing/useSaveAgentShare.js +97 -0
- package/sharing/useSaveAgentShare.js.map +1 -0
- package/sharing/useShareAgent.d.ts +69 -0
- package/sharing/useShareAgent.d.ts.map +1 -0
- package/sharing/useShareAgent.js +42 -0
- package/sharing/useShareAgent.js.map +1 -0
- package/sharing/useShareToolReadiness.d.ts +52 -0
- package/sharing/useShareToolReadiness.d.ts.map +1 -0
- package/sharing/useShareToolReadiness.js +89 -0
- package/sharing/useShareToolReadiness.js.map +1 -0
- package/sharing/useSharedAgentProfile.d.ts +85 -0
- package/sharing/useSharedAgentProfile.d.ts.map +1 -0
- package/sharing/useSharedAgentProfile.js +73 -0
- package/sharing/useSharedAgentProfile.js.map +1 -0
- package/src/agent/AgentDetailView.tsx +28 -3
- package/src/environment/EnvironmentListPanel.tsx +47 -18
- package/src/environment/EnvironmentPicker.tsx +23 -2
- 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 +38 -0
- package/src/internal/dev/__tests__/use-key-stability.test.ts +4 -0
- package/src/internal/dev/__tests__/use-render-tracer.test.ts +4 -0
- package/src/internal/dev/dom-counter.ts +2 -3
- package/src/internal/dev/enabled.ts +39 -0
- package/src/internal/dev/profiler-wrapper.tsx +5 -6
- package/src/internal/dev/use-key-stability.ts +2 -3
- package/src/internal/dev/use-render-tracer.ts +4 -5
- package/src/internal/dev/use-stream-rate.ts +4 -4
- package/src/library/ResourceVisibilityControl.tsx +23 -7
- package/src/library/__tests__/visibilityLevels.test.ts +33 -0
- package/src/library/useUpdateVisibility.ts +9 -3
- package/src/library/visibilityLevels.ts +36 -0
- package/src/session/NewSessionViewer.tsx +67 -31
- package/src/session/SessionViewer.tsx +67 -36
- package/src/session/__tests__/audienceWiring.test.tsx +99 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +75 -1
- package/src/session/__tests__/useSessionPageFlow.guest.test.tsx +112 -0
- package/src/session/audience.ts +12 -4
- package/src/session/facets/ChangesTab.tsx +6 -4
- package/src/session/useNewSessionFlow.ts +26 -1
- package/src/session/useSessionPageFlow.ts +24 -2
- package/src/sharing/ShareAgentDialog.tsx +1489 -0
- package/src/sharing/SharedAgentChat.tsx +283 -0
- package/src/sharing/__tests__/ShareAgentDialog.test.tsx +926 -0
- package/src/sharing/__tests__/SharedAgentChat.test.tsx +222 -0
- package/src/sharing/__tests__/useAgentShare.test.tsx +149 -0
- package/src/sharing/__tests__/useRotateShareLink.test.tsx +96 -0
- package/src/sharing/__tests__/useSaveAgentShare.test.tsx +241 -0
- package/src/sharing/__tests__/useShareAgent.test.tsx +131 -0
- package/src/sharing/__tests__/useShareToolReadiness.test.tsx +219 -0
- package/src/sharing/__tests__/useSharedAgentProfile.test.tsx +201 -0
- package/src/sharing/index.ts +32 -0
- package/src/sharing/useAgentShare.ts +91 -0
- package/src/sharing/useRotateShareLink.ts +81 -0
- package/src/sharing/useSaveAgentShare.ts +186 -0
- package/src/sharing/useShareAgent.tsx +107 -0
- package/src/sharing/useShareToolReadiness.ts +131 -0
- package/src/sharing/useSharedAgentProfile.ts +132 -0
- package/src/switch/Switch.tsx +97 -0
- package/src/switch/__tests__/Switch.test.tsx +107 -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 +41 -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,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
|
+
});
|