@stigmer/react 3.1.13 → 3.1.15
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 -9
- package/agent/AgentDetailView.d.ts.map +1 -1
- package/agent/AgentDetailView.js +23 -31
- package/agent/AgentDetailView.js.map +1 -1
- package/agent-instance/AgentInstanceEmptyState.d.ts +4 -1
- package/agent-instance/AgentInstanceEmptyState.d.ts.map +1 -1
- package/agent-instance/AgentInstanceEmptyState.js +12 -5
- package/agent-instance/AgentInstanceEmptyState.js.map +1 -1
- package/agent-instance/AgentInstanceList.d.ts +13 -2
- package/agent-instance/AgentInstanceList.d.ts.map +1 -1
- package/agent-instance/AgentInstanceList.js +8 -4
- package/agent-instance/AgentInstanceList.js.map +1 -1
- package/agent-instance/useAgentInstances.d.ts +10 -3
- package/agent-instance/useAgentInstances.d.ts.map +1 -1
- package/agent-instance/useAgentInstances.js +12 -4
- package/agent-instance/useAgentInstances.js.map +1 -1
- package/button/Button.d.ts +54 -0
- package/button/Button.d.ts.map +1 -0
- package/button/Button.js +42 -0
- package/button/Button.js.map +1 -0
- package/button/index.d.ts +3 -0
- package/button/index.d.ts.map +1 -0
- package/button/index.js +2 -0
- package/button/index.js.map +1 -0
- package/empty-state/EmptyState.d.ts.map +1 -1
- package/empty-state/EmptyState.js +8 -3
- package/empty-state/EmptyState.js.map +1 -1
- package/empty-state/types.d.ts +2 -0
- package/empty-state/types.d.ts.map +1 -1
- package/execution/index.d.ts +1 -1
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js.map +1 -1
- package/execution/useCreateAgentExecution.d.ts +81 -9
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +42 -4
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/index.d.ts +5 -3
- package/index.d.ts.map +1 -1
- package/index.js +4 -2
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/session/useNewSessionFlow.d.ts +6 -3
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +34 -35
- package/session/useNewSessionFlow.js.map +1 -1
- package/sharing/AgentShareList.d.ts +46 -0
- package/sharing/AgentShareList.d.ts.map +1 -0
- package/sharing/AgentShareList.js +159 -0
- package/sharing/AgentShareList.js.map +1 -0
- package/sharing/ShareAgentDialog.d.ts +38 -25
- package/sharing/ShareAgentDialog.d.ts.map +1 -1
- package/sharing/ShareAgentDialog.js +131 -62
- package/sharing/ShareAgentDialog.js.map +1 -1
- package/sharing/index.d.ts +10 -8
- package/sharing/index.d.ts.map +1 -1
- package/sharing/index.js +5 -4
- package/sharing/index.js.map +1 -1
- package/sharing/useAgentShares.d.ts +48 -0
- package/sharing/useAgentShares.d.ts.map +1 -0
- package/sharing/useAgentShares.js +44 -0
- package/sharing/useAgentShares.js.map +1 -0
- package/sharing/useCanCreateAgentShare.d.ts +43 -0
- package/sharing/useCanCreateAgentShare.d.ts.map +1 -0
- package/sharing/useCanCreateAgentShare.js +44 -0
- package/sharing/useCanCreateAgentShare.js.map +1 -0
- package/sharing/useDeleteAgentShare.d.ts +42 -0
- package/sharing/useDeleteAgentShare.d.ts.map +1 -0
- package/sharing/useDeleteAgentShare.js +45 -0
- package/sharing/useDeleteAgentShare.js.map +1 -0
- package/sharing/useSaveAgentShare.d.ts +43 -18
- package/sharing/useSaveAgentShare.d.ts.map +1 -1
- package/sharing/useSaveAgentShare.js +55 -22
- package/sharing/useSaveAgentShare.js.map +1 -1
- package/src/agent/AgentDetailView.tsx +37 -43
- package/src/agent-instance/AgentInstanceEmptyState.tsx +24 -43
- package/src/agent-instance/AgentInstanceList.tsx +29 -16
- package/src/agent-instance/__tests__/agent-instance-hooks.test.tsx +19 -0
- package/src/agent-instance/useAgentInstances.ts +12 -3
- package/src/button/Button.tsx +104 -0
- package/src/button/__tests__/Button.test.tsx +82 -0
- package/src/button/index.ts +2 -0
- package/src/empty-state/EmptyState.tsx +8 -10
- package/src/empty-state/types.ts +2 -0
- package/src/execution/__tests__/useCreateAgentExecution.test.tsx +92 -0
- package/src/execution/index.ts +2 -0
- package/src/execution/useCreateAgentExecution.ts +116 -10
- package/src/index.ts +17 -9
- package/src/session/__tests__/useNewSessionFlow.test.tsx +121 -52
- package/src/session/useNewSessionFlow.ts +38 -37
- package/src/sharing/AgentShareList.tsx +502 -0
- package/src/sharing/ShareAgentDialog.tsx +310 -112
- package/src/sharing/__tests__/AgentShareList.test.tsx +388 -0
- package/src/sharing/__tests__/ShareAgentDialog.test.tsx +311 -312
- package/src/sharing/__tests__/useAgentShares.test.tsx +186 -0
- package/src/sharing/__tests__/useCanCreateAgentShare.test.tsx +190 -0
- package/src/sharing/index.ts +10 -12
- package/src/sharing/useAgentShares.ts +74 -0
- package/src/sharing/useCanCreateAgentShare.ts +74 -0
- package/src/sharing/useDeleteAgentShare.ts +73 -0
- package/src/sharing/useSaveAgentShare.ts +73 -23
- package/src/workflow/WorkflowDetailView.tsx +9 -0
- package/src/workflow/instance/WorkflowInstanceEmptyState.tsx +24 -43
- package/src/workflow/instance/WorkflowInstanceList.tsx +29 -16
- package/src/workflow/useWorkflowInstances.ts +10 -3
- package/styles.css +1 -1
- package/workflow/WorkflowDetailView.d.ts +8 -1
- package/workflow/WorkflowDetailView.d.ts.map +1 -1
- package/workflow/WorkflowDetailView.js +2 -2
- package/workflow/WorkflowDetailView.js.map +1 -1
- package/workflow/instance/WorkflowInstanceEmptyState.d.ts +4 -1
- package/workflow/instance/WorkflowInstanceEmptyState.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceEmptyState.js +12 -5
- package/workflow/instance/WorkflowInstanceEmptyState.js.map +1 -1
- package/workflow/instance/WorkflowInstanceList.d.ts +13 -2
- package/workflow/instance/WorkflowInstanceList.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceList.js +8 -4
- package/workflow/instance/WorkflowInstanceList.js.map +1 -1
- package/workflow/useWorkflowInstances.d.ts +8 -3
- package/workflow/useWorkflowInstances.d.ts.map +1 -1
- package/workflow/useWorkflowInstances.js +10 -4
- package/workflow/useWorkflowInstances.js.map +1 -1
- package/sharing/useAgentShare.d.ts +0 -49
- package/sharing/useAgentShare.d.ts.map +0 -1
- package/sharing/useAgentShare.js +0 -64
- package/sharing/useAgentShare.js.map +0 -1
- package/sharing/useCreateExternalShareLink.d.ts +0 -90
- package/sharing/useCreateExternalShareLink.d.ts.map +0 -1
- package/sharing/useCreateExternalShareLink.js +0 -65
- package/sharing/useCreateExternalShareLink.js.map +0 -1
- package/sharing/useShareAgent.d.ts +0 -69
- package/sharing/useShareAgent.d.ts.map +0 -1
- package/sharing/useShareAgent.js +0 -42
- package/sharing/useShareAgent.js.map +0 -1
- package/src/sharing/__tests__/useAgentShare.test.tsx +0 -210
- package/src/sharing/__tests__/useCreateExternalShareLink.test.tsx +0 -194
- package/src/sharing/__tests__/useShareAgent.test.tsx +0 -131
- package/src/sharing/useAgentShare.ts +0 -105
- package/src/sharing/useCreateExternalShareLink.tsx +0 -141
- package/src/sharing/useShareAgent.tsx +0 -107
|
@@ -17,36 +17,65 @@ function sharingAudienceToProto(audience) {
|
|
|
17
17
|
: AgentShareAudience.public;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* Projects a share into the editable {@link AgentShareDraft} — the
|
|
21
|
+
* single constructor for the full-spec drafts every save requires.
|
|
22
|
+
* `null` (no share yet) seeds the same defaults the server would apply
|
|
23
|
+
* on first create.
|
|
24
|
+
*
|
|
25
|
+
* Shared by the Share dialog (draft seeding) and the share list's
|
|
26
|
+
* pause/resume toggle, so a toggle rebuilds the complete spec with only
|
|
27
|
+
* `enabled` flipped and can never wipe origins, messages, or credential
|
|
28
|
+
* bindings — the fails-closed posture {@link AgentShareDraft} exists to
|
|
29
|
+
* enforce.
|
|
30
|
+
*/
|
|
31
|
+
export function draftFromShare(share) {
|
|
32
|
+
const spec = share?.spec;
|
|
33
|
+
return {
|
|
34
|
+
enabled: spec?.enabled ?? false,
|
|
35
|
+
audience: sharingAudienceFromProto(spec?.audience),
|
|
36
|
+
allowedOrigins: spec?.allowedOrigins ?? [],
|
|
37
|
+
messages: {
|
|
38
|
+
rateLimited: spec?.messages?.rateLimited ?? "",
|
|
39
|
+
unavailable: spec?.messages?.unavailable ?? "",
|
|
40
|
+
conversationEnded: spec?.messages?.conversationEnded ?? "",
|
|
41
|
+
},
|
|
42
|
+
environmentRefs: (spec?.environmentRefs ?? []).map((ref) => ({
|
|
43
|
+
org: ref.org,
|
|
44
|
+
slug: ref.slug,
|
|
45
|
+
})),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Behavior hook that persists an agent share's configuration.
|
|
21
50
|
*
|
|
22
51
|
* Commits via `stigmer.agentShare.apply()` — an idempotent upsert keyed
|
|
23
52
|
* on the share's `(org, slug)` identity — so one code path serves both
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
53
|
+
* creation (the server authorizes on the referenced agent's `can_edit`)
|
|
54
|
+
* and every later edit. There is deliberately no create/update
|
|
55
|
+
* branching: the share slug is unique per org, which makes
|
|
56
|
+
* apply-by-identity exact, and the generated `AgentShareInput` carries
|
|
57
|
+
* no `metadata.id` for an update to route by anyway. A create that
|
|
58
|
+
* collides on `(org, slug)` rejects with an already-exists error rather
|
|
59
|
+
* than touching the existing share.
|
|
30
60
|
*
|
|
31
61
|
* Disabling is a save with `enabled: false` — a config-preserving pause
|
|
32
62
|
* (decision 011 D1). Deleting the share is a separate, destructive
|
|
33
|
-
* operation
|
|
63
|
+
* operation ({@link useDeleteAgentShare}).
|
|
34
64
|
*
|
|
35
|
-
* `shareOrg` selects which org
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
65
|
+
* `shareOrg` selects which org owns a created share and defaults to the
|
|
66
|
+
* agent's own org. Pass the viewer's org to create a **cross-org
|
|
67
|
+
* share** (decision 013) — the server then authorizes on the public
|
|
68
|
+
* agent's `can_execute` plus `can_create_agent_share` in the sharing
|
|
69
|
+
* org, and bills that org.
|
|
40
70
|
*
|
|
41
71
|
* Pass `null` for `agent` to produce a stable no-op (useful while the
|
|
42
72
|
* agent is still loading).
|
|
43
73
|
*
|
|
44
74
|
* @example
|
|
45
75
|
* ```tsx
|
|
46
|
-
* const { share } = useAgentShare(agent);
|
|
47
76
|
* const { save, isPending } = useSaveAgentShare(agent);
|
|
48
77
|
*
|
|
49
|
-
* const persisted = await save({ ...
|
|
78
|
+
* const persisted = await save({ ...draftFromShare(share), enabled: true }, share);
|
|
50
79
|
* // persisted.status.shareLinkToken is the live link token
|
|
51
80
|
* ```
|
|
52
81
|
*/
|
|
@@ -58,7 +87,7 @@ export function useSaveAgentShare(agent, shareOrg) {
|
|
|
58
87
|
const agentSlug = agent?.metadata?.slug ?? "";
|
|
59
88
|
const agentName = agent?.metadata?.name ?? "";
|
|
60
89
|
const resolvedShareOrg = shareOrg || agentOrg;
|
|
61
|
-
const save = useCallback(async (draft, current) => {
|
|
90
|
+
const save = useCallback(async (draft, current, createIdentity) => {
|
|
62
91
|
if (!agentOrg || !agentSlug)
|
|
63
92
|
return undefined;
|
|
64
93
|
setIsPending(true);
|
|
@@ -66,13 +95,17 @@ export function useSaveAgentShare(agent, shareOrg) {
|
|
|
66
95
|
try {
|
|
67
96
|
return await stigmer.agentShare.apply({
|
|
68
97
|
// Identity: the existing share's org/slug when editing (a
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// sharing org + the
|
|
72
|
-
// own D2 default, made
|
|
98
|
+
// share may carry a non-default slug — apply with the agent's
|
|
99
|
+
// slug would create a SECOND share); when creating, the
|
|
100
|
+
// sharing org + the caller-chosen identity, falling back to
|
|
101
|
+
// the agent's own slug/name (the server's D2 default, made
|
|
102
|
+
// explicit).
|
|
73
103
|
org: current?.metadata?.org || resolvedShareOrg,
|
|
74
|
-
slug: current?.metadata?.slug || agentSlug,
|
|
75
|
-
name: current?.metadata?.name ||
|
|
104
|
+
slug: current?.metadata?.slug || createIdentity?.slug || agentSlug,
|
|
105
|
+
name: current?.metadata?.name ||
|
|
106
|
+
createIdentity?.name ||
|
|
107
|
+
agentName ||
|
|
108
|
+
agentSlug,
|
|
76
109
|
agentRef: { org: agentOrg, slug: agentSlug },
|
|
77
110
|
enabled: draft.enabled,
|
|
78
111
|
// Written as the explicit enum value, never left unspecified —
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSaveAgentShare.js","sourceRoot":"","sources":["../../src/sharing/useSaveAgentShare.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAG9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,0DAA0D,CAAC;AAE9F,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAqDjD;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CACtC,QAAwC;IAExC,OAAO,QAAQ,KAAK,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC;AAChE,CAAC;AAED,SAAS,sBAAsB,CAAC,QAAyB;IACvD,OAAO,QAAQ,KAAK,KAAK;QACvB,CAAC,CAAC,kBAAkB,CAAC,GAAG;QACxB,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC;AAChC,CAAC;
|
|
1
|
+
{"version":3,"file":"useSaveAgentShare.js","sourceRoot":"","sources":["../../src/sharing/useSaveAgentShare.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,8 +17,7 @@ import { agentToInput } from "./internal/agentToInput.js";
|
|
|
17
17
|
import { ErrorMessage } from "../error/ErrorMessage.js";
|
|
18
18
|
import { VisibilityBadge } from "../library/VisibilitySelector.js";
|
|
19
19
|
import { useManageAccess } from "../access/useManageAccess.js";
|
|
20
|
-
import {
|
|
21
|
-
import { useCreateExternalShareLink } from "../sharing/useCreateExternalShareLink.js";
|
|
20
|
+
import { AgentShareList } from "../sharing/AgentShareList.js";
|
|
22
21
|
import { ResourceDetailShell } from "../resource-detail/ResourceDetailShell.js";
|
|
23
22
|
import { Section } from "../resource-detail/Section.js";
|
|
24
23
|
import { useDetailTabs } from "../resource-detail/useDetailTabs.js";
|
|
@@ -38,6 +37,7 @@ const INSTRUCTIONS_COLLAPSED_HEIGHT = "12rem";
|
|
|
38
37
|
|
|
39
38
|
const OVERVIEW_TAB: TabItem = { id: "overview", label: "Overview" };
|
|
40
39
|
const INSTANCES_TAB: TabItem = { id: "instances", label: "Instances" };
|
|
40
|
+
const SHARES_TAB: TabItem = { id: "shares", label: "Shares" };
|
|
41
41
|
const DEPENDENCIES_TAB: TabItem = { id: "dependencies", label: "Dependencies" };
|
|
42
42
|
|
|
43
43
|
/** Props for {@link AgentDetailView}. */
|
|
@@ -130,21 +130,21 @@ export interface AgentDetailViewProps {
|
|
|
130
130
|
*/
|
|
131
131
|
readonly onCreateInstanceClick?: () => void;
|
|
132
132
|
/**
|
|
133
|
-
* Builds the absolute public chat URL
|
|
133
|
+
* Builds the absolute public chat URL for shares in the Shares tab.
|
|
134
134
|
* The host application owns URL construction — its configured public
|
|
135
135
|
* origin may differ from the rendering origin (e.g. the desktop app).
|
|
136
|
-
* When omitted,
|
|
136
|
+
* When omitted, share links fall back to the relative
|
|
137
137
|
* `/chat/<org>/<slug>` path.
|
|
138
138
|
*/
|
|
139
139
|
readonly buildShareUrl?: (org: string, slug: string) => string;
|
|
140
140
|
/**
|
|
141
|
-
* The viewer's active organization slug
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
141
|
+
* The viewer's active organization slug, feeding the Shares and
|
|
142
|
+
* Instances tabs. It scopes both lists to this org's rows (a member
|
|
143
|
+
* of several orgs sees the current org context only), and a share
|
|
144
|
+
* created in the Shares tab lands in this org — its URL, billing,
|
|
145
|
+
* and credentials — which for another org's marketplace-public agent
|
|
146
|
+
* is a **cross-org share** (decision 013). Omit to default to the
|
|
147
|
+
* agent's own org.
|
|
148
148
|
*/
|
|
149
149
|
readonly viewerOrg?: string;
|
|
150
150
|
/**
|
|
@@ -265,8 +265,8 @@ export function AgentDetailView({
|
|
|
265
265
|
const builtInTabs = useMemo<readonly TabItem[]>(
|
|
266
266
|
() =>
|
|
267
267
|
noDeps
|
|
268
|
-
? [OVERVIEW_TAB, INSTANCES_TAB]
|
|
269
|
-
: [OVERVIEW_TAB, INSTANCES_TAB, DEPENDENCIES_TAB],
|
|
268
|
+
? [OVERVIEW_TAB, INSTANCES_TAB, SHARES_TAB]
|
|
269
|
+
: [OVERVIEW_TAB, INSTANCES_TAB, SHARES_TAB, DEPENDENCIES_TAB],
|
|
270
270
|
[noDeps],
|
|
271
271
|
);
|
|
272
272
|
|
|
@@ -328,27 +328,6 @@ export function AgentDetailView({
|
|
|
328
328
|
: undefined,
|
|
329
329
|
});
|
|
330
330
|
|
|
331
|
-
// Share — the sibling consent to Manage access: visibility governs who
|
|
332
|
-
// can read the blueprint; sharing governs who can chat with the running
|
|
333
|
-
// agent (billed to the owning org). Same null-until-ready contract.
|
|
334
|
-
const share = useShareAgent({
|
|
335
|
-
agent,
|
|
336
|
-
buildShareUrl,
|
|
337
|
-
onSharingChanged: refetch,
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// Cross-org "Create share link" (decision 013) — the marketplace entry
|
|
341
|
-
// for another org's public agent. Mutually exclusive with the Share
|
|
342
|
-
// action above by construction (same-org gates on can_edit; this one
|
|
343
|
-
// requires viewerOrg != agent org), so both fold into the actions
|
|
344
|
-
// array unconditionally.
|
|
345
|
-
const externalShare = useCreateExternalShareLink({
|
|
346
|
-
agent,
|
|
347
|
-
viewerOrg: viewerOrg ?? "",
|
|
348
|
-
buildShareUrl,
|
|
349
|
-
onSharingChanged: refetch,
|
|
350
|
-
});
|
|
351
|
-
|
|
352
331
|
if (isLoading) return <LoadingSkeleton className={className} />;
|
|
353
332
|
if (error)
|
|
354
333
|
return <ErrorMessage error={error} retry={refetch} className={className} />;
|
|
@@ -387,15 +366,23 @@ export function AgentDetailView({
|
|
|
387
366
|
<VisibilityBadge visibility={meta.visibility} />
|
|
388
367
|
) : undefined;
|
|
389
368
|
|
|
390
|
-
// Share
|
|
391
|
-
//
|
|
392
|
-
|
|
369
|
+
// Share — the sibling consent to Manage access: visibility governs who
|
|
370
|
+
// can read the blueprint; sharing governs who can chat with the running
|
|
371
|
+
// agent (billed to the org that owns each share). Pure navigation to
|
|
372
|
+
// the Shares tab, so it renders unconditionally (like the tab itself);
|
|
373
|
+
// all capability gating lives in the tab, next to its affordances.
|
|
374
|
+
const shareAction: DetailAction = {
|
|
375
|
+
id: "share",
|
|
376
|
+
label: "Share",
|
|
377
|
+
group: "sharing",
|
|
378
|
+
onAction: () => effectiveOnTabChange(SHARES_TAB.id),
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Share precedes Manage access within the "sharing" group.
|
|
382
|
+
const injectedActions = [shareAction, access.action].filter(
|
|
393
383
|
(a): a is NonNullable<typeof a> => a != null,
|
|
394
384
|
);
|
|
395
|
-
const mergedActions =
|
|
396
|
-
injectedActions.length > 0
|
|
397
|
-
? [...(actions ?? []), ...injectedActions]
|
|
398
|
-
: actions;
|
|
385
|
+
const mergedActions = [...(actions ?? []), ...injectedActions];
|
|
399
386
|
|
|
400
387
|
let tabContent: React.ReactNode;
|
|
401
388
|
if (activeAdditionalTab) {
|
|
@@ -406,6 +393,7 @@ export function AgentDetailView({
|
|
|
406
393
|
agentId={meta?.id ?? ""}
|
|
407
394
|
defaultInstanceId={agent.status?.defaultInstanceId}
|
|
408
395
|
org={org}
|
|
396
|
+
viewerOrg={viewerOrg}
|
|
409
397
|
onCreateClick={onCreateInstanceClick}
|
|
410
398
|
onInstanceClick={onInstanceClick}
|
|
411
399
|
onStartSessionClick={onInstanceStartSessionClick}
|
|
@@ -413,6 +401,14 @@ export function AgentDetailView({
|
|
|
413
401
|
refreshKey={instancesRefreshKey}
|
|
414
402
|
/>
|
|
415
403
|
);
|
|
404
|
+
} else if (effectiveActiveTab === "shares") {
|
|
405
|
+
tabContent = (
|
|
406
|
+
<AgentShareList
|
|
407
|
+
agent={agent}
|
|
408
|
+
viewerOrg={viewerOrg}
|
|
409
|
+
buildShareUrl={buildShareUrl}
|
|
410
|
+
/>
|
|
411
|
+
);
|
|
416
412
|
} else if (effectiveActiveTab === "dependencies" && tree) {
|
|
417
413
|
tabContent = (
|
|
418
414
|
<DependencyGraph
|
|
@@ -451,8 +447,6 @@ export function AgentDetailView({
|
|
|
451
447
|
{tabContent}
|
|
452
448
|
</ResourceDetailShell>
|
|
453
449
|
{access.dialog}
|
|
454
|
-
{share.dialog}
|
|
455
|
-
{externalShare.dialog}
|
|
456
450
|
</>
|
|
457
451
|
);
|
|
458
452
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { EmptyState } from "../empty-state/EmptyState.js";
|
|
4
4
|
|
|
5
5
|
/** Props for {@link AgentInstanceEmptyState}. */
|
|
6
6
|
export interface AgentInstanceEmptyStateProps {
|
|
7
|
-
/** Called when the user clicks "Create
|
|
7
|
+
/** Called when the user clicks "Create instance". */
|
|
8
8
|
readonly onCreateClick?: () => void;
|
|
9
9
|
/** Additional CSS class names. */
|
|
10
10
|
readonly className?: string;
|
|
@@ -17,62 +17,43 @@ export interface AgentInstanceEmptyStateProps {
|
|
|
17
17
|
* the platform-managed default instance (an implementation detail). For
|
|
18
18
|
* agents, an instance is a reusable, environment-bound deployment you can
|
|
19
19
|
* start sessions against — and share with your team.
|
|
20
|
+
*
|
|
21
|
+
* A thin composition over the shared {@link EmptyState} primitive: only the
|
|
22
|
+
* icon and copy are specific to agent instances.
|
|
20
23
|
*/
|
|
21
24
|
export function AgentInstanceEmptyState({
|
|
22
25
|
onCreateClick,
|
|
23
26
|
className,
|
|
24
27
|
}: AgentInstanceEmptyStateProps) {
|
|
25
28
|
return (
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
credentials and settings. Each instance binds environments (secrets,
|
|
43
|
-
configuration) and can be shared with your team independently.
|
|
44
|
-
</p>
|
|
45
|
-
|
|
46
|
-
{onCreateClick && (
|
|
47
|
-
<button
|
|
48
|
-
type="button"
|
|
49
|
-
onClick={onCreateClick}
|
|
50
|
-
className={cn(
|
|
51
|
-
"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5",
|
|
52
|
-
"text-sm font-medium",
|
|
53
|
-
"bg-primary text-primary-foreground",
|
|
54
|
-
"hover:bg-primary/90",
|
|
55
|
-
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
56
|
-
)}
|
|
57
|
-
>
|
|
58
|
-
<PlusIcon />
|
|
59
|
-
Create Instance
|
|
60
|
-
</button>
|
|
61
|
-
)}
|
|
62
|
-
</div>
|
|
29
|
+
<EmptyState
|
|
30
|
+
variant="first-use"
|
|
31
|
+
icon={<LayersIcon />}
|
|
32
|
+
title="No instances yet"
|
|
33
|
+
description={
|
|
34
|
+
"Create instances to start sessions with this agent using different " +
|
|
35
|
+
"credentials and settings. Each instance binds environments (secrets, " +
|
|
36
|
+
"configuration) and can be shared with your team independently."
|
|
37
|
+
}
|
|
38
|
+
action={
|
|
39
|
+
onCreateClick
|
|
40
|
+
? { label: "Create instance", onClick: onCreateClick, icon: <PlusIcon /> }
|
|
41
|
+
: undefined
|
|
42
|
+
}
|
|
43
|
+
className={className}
|
|
44
|
+
/>
|
|
63
45
|
);
|
|
64
46
|
}
|
|
65
47
|
|
|
66
48
|
function LayersIcon() {
|
|
67
49
|
return (
|
|
68
|
-
<svg width="
|
|
50
|
+
<svg width="24" height="24" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
69
51
|
<path
|
|
70
52
|
d="M10 2L2 6l8 4 8-4-8-4zM2 10l8 4 8-4M2 14l8 4 8-4"
|
|
71
53
|
stroke="currentColor"
|
|
72
54
|
strokeWidth="1.5"
|
|
73
55
|
strokeLinecap="round"
|
|
74
56
|
strokeLinejoin="round"
|
|
75
|
-
className="text-muted-foreground"
|
|
76
57
|
/>
|
|
77
58
|
</svg>
|
|
78
59
|
);
|
|
@@ -80,8 +61,8 @@ function LayersIcon() {
|
|
|
80
61
|
|
|
81
62
|
function PlusIcon() {
|
|
82
63
|
return (
|
|
83
|
-
<svg width="
|
|
84
|
-
<path d="
|
|
64
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
65
|
+
<path d="M6 2v8M2 6h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
85
66
|
</svg>
|
|
86
67
|
);
|
|
87
68
|
}
|
|
@@ -5,6 +5,7 @@ import { cn } from "@stigmer/theme";
|
|
|
5
5
|
import type { AgentInstance } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/api_pb";
|
|
6
6
|
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
7
7
|
import { useAgentInstances } from "./useAgentInstances.js";
|
|
8
|
+
import { Button } from "../button/Button.js";
|
|
8
9
|
import { useEnvironmentList } from "../environment/useEnvironmentList.js";
|
|
9
10
|
import { ResourceVisibilityControl } from "../library/ResourceVisibilityControl.js";
|
|
10
11
|
import { PermissionGate } from "../iam-policy/PermissionGate.js";
|
|
@@ -19,8 +20,19 @@ export interface AgentInstanceListProps {
|
|
|
19
20
|
readonly agentId: string;
|
|
20
21
|
/** The default instance ID (from agent.status.defaultInstanceId) — filtered out of the list. */
|
|
21
22
|
readonly defaultInstanceId?: string;
|
|
22
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* The AGENT's organization slug (needed for environment resolution
|
|
25
|
+
* and the create flow). Not the list scope — that is `viewerOrg`,
|
|
26
|
+
* which differs from `org` when viewing another org's agent.
|
|
27
|
+
*/
|
|
23
28
|
readonly org: string;
|
|
29
|
+
/**
|
|
30
|
+
* The viewer's active organization slug. Scopes the list to this
|
|
31
|
+
* org's instances of the agent, so a member of several orgs sees
|
|
32
|
+
* exactly the current org context's instances. Omit to default to
|
|
33
|
+
* the agent's own org.
|
|
34
|
+
*/
|
|
35
|
+
readonly viewerOrg?: string;
|
|
24
36
|
/** Called when the user wants to create a new instance. */
|
|
25
37
|
readonly onCreateClick?: () => void;
|
|
26
38
|
/** Called when the user clicks an instance row (opens detail). */
|
|
@@ -53,6 +65,7 @@ export function AgentInstanceList({
|
|
|
53
65
|
agentId,
|
|
54
66
|
defaultInstanceId,
|
|
55
67
|
org,
|
|
68
|
+
viewerOrg,
|
|
56
69
|
onCreateClick,
|
|
57
70
|
onInstanceClick,
|
|
58
71
|
onStartSessionClick,
|
|
@@ -60,7 +73,13 @@ export function AgentInstanceList({
|
|
|
60
73
|
refreshKey,
|
|
61
74
|
className,
|
|
62
75
|
}: AgentInstanceListProps) {
|
|
63
|
-
|
|
76
|
+
// Scope the list to the org whose context the viewer is in; when the
|
|
77
|
+
// host passes no viewerOrg the scope falls back to the agent's own org
|
|
78
|
+
// (the same-org owner flow).
|
|
79
|
+
const { instances, isLoading, error, refetch } = useAgentInstances(
|
|
80
|
+
agentId,
|
|
81
|
+
viewerOrg || org,
|
|
82
|
+
);
|
|
64
83
|
const { environments } = useEnvironmentList(org);
|
|
65
84
|
|
|
66
85
|
// Refetch when refreshKey changes (signals external mutation like create/delete)
|
|
@@ -108,27 +127,21 @@ export function AgentInstanceList({
|
|
|
108
127
|
{userInstances.length} {userInstances.length === 1 ? "instance" : "instances"}
|
|
109
128
|
</h3>
|
|
110
129
|
{onCreateClick && (
|
|
111
|
-
<
|
|
112
|
-
|
|
130
|
+
<Button
|
|
131
|
+
variant="outline"
|
|
132
|
+
size="xs"
|
|
133
|
+
icon={<PlusIcon />}
|
|
113
134
|
onClick={onCreateClick}
|
|
114
|
-
className={cn(
|
|
115
|
-
"inline-flex items-center gap-1 rounded-md px-2.5 py-1",
|
|
116
|
-
"text-xs font-medium",
|
|
117
|
-
"border border-border text-foreground",
|
|
118
|
-
"hover:bg-accent-hover",
|
|
119
|
-
"focus:outline-none focus:ring-2 focus:ring-ring",
|
|
120
|
-
)}
|
|
121
135
|
>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
</button>
|
|
136
|
+
Create instance
|
|
137
|
+
</Button>
|
|
125
138
|
)}
|
|
126
139
|
</div>
|
|
127
140
|
|
|
128
141
|
<div className="overflow-hidden rounded-lg border border-border">
|
|
129
142
|
<table className="w-full text-sm">
|
|
130
143
|
<thead>
|
|
131
|
-
<tr className="border-b border-border bg-muted
|
|
144
|
+
<tr className="border-b border-border bg-muted-subtle">
|
|
132
145
|
<th className="px-4 py-2 text-left font-medium text-muted-foreground">Name</th>
|
|
133
146
|
<th className="px-4 py-2 text-left font-medium text-muted-foreground">Environments</th>
|
|
134
147
|
<th className="px-4 py-2 text-left font-medium text-muted-foreground">Visibility</th>
|
|
@@ -289,7 +302,7 @@ function LoadingSkeleton() {
|
|
|
289
302
|
return (
|
|
290
303
|
<div className="space-y-2 py-4">
|
|
291
304
|
{[1, 2, 3].map((i) => (
|
|
292
|
-
<div key={i} className="h-12 rounded-md bg-muted
|
|
305
|
+
<div key={i} className="h-12 animate-pulse rounded-md bg-muted-faint" />
|
|
293
306
|
))}
|
|
294
307
|
</div>
|
|
295
308
|
);
|
|
@@ -200,6 +200,25 @@ describe("useAgentInstances", () => {
|
|
|
200
200
|
expect(result.current.instances).toHaveLength(2);
|
|
201
201
|
expect(result.current.instances[0]?.metadata?.id).toBe("ain-1");
|
|
202
202
|
expect(mockGetByAgent).toHaveBeenCalledTimes(1);
|
|
203
|
+
expect(mockGetByAgent).toHaveBeenCalledWith(
|
|
204
|
+
expect.objectContaining({ agentId: "agent-001", org: "" }),
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("threads the org scope into the request", async () => {
|
|
209
|
+
// The org scope belongs in the RPC, never client-side filtering: the
|
|
210
|
+
// server narrows a multi-org member's view to one org's instances.
|
|
211
|
+
mockGetByAgent.mockResolvedValueOnce({ items: [], totalCount: 0 });
|
|
212
|
+
|
|
213
|
+
const { result } = renderHook(
|
|
214
|
+
() => useAgentInstances("agent-001", "acme"),
|
|
215
|
+
{ wrapper: createWrapper(makeMockClient()) },
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
219
|
+
expect(mockGetByAgent).toHaveBeenCalledWith(
|
|
220
|
+
expect.objectContaining({ agentId: "agent-001", org: "acme" }),
|
|
221
|
+
);
|
|
203
222
|
});
|
|
204
223
|
|
|
205
224
|
it("does not fetch when agentId is null", async () => {
|
|
@@ -21,12 +21,19 @@ export interface UseAgentInstancesReturn {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Data hook that fetches
|
|
24
|
+
* Data hook that fetches instances for a specific agent.
|
|
25
25
|
*
|
|
26
26
|
* Wraps `stigmer.agentInstance.getByAgent()` with loading and error
|
|
27
27
|
* state management. Used on the Agent detail page "Instances" tab to
|
|
28
28
|
* show environment-bound deployments of the agent blueprint.
|
|
29
29
|
*
|
|
30
|
+
* Pass `org` to scope the list to one organization's instances — the
|
|
31
|
+
* org-context view a console tab needs. Permissions alone cannot provide
|
|
32
|
+
* this scope: a member of several orgs can view all of their orgs'
|
|
33
|
+
* instances of the agent, so the unscoped list merges them. The server
|
|
34
|
+
* applies the scope (it only ever narrows the permission-bounded view);
|
|
35
|
+
* omit `org` for the full permission-bounded list.
|
|
36
|
+
*
|
|
30
37
|
* Pass `null` as `agentId` to skip fetching (stable no-op).
|
|
31
38
|
*
|
|
32
39
|
* Note: the `getByAgent` RPC returns an `AgentInstanceList` whose
|
|
@@ -35,11 +42,12 @@ export interface UseAgentInstancesReturn {
|
|
|
35
42
|
*
|
|
36
43
|
* @example
|
|
37
44
|
* ```tsx
|
|
38
|
-
* const { instances, isLoading } = useAgentInstances(agent.metadata?.id);
|
|
45
|
+
* const { instances, isLoading } = useAgentInstances(agent.metadata?.id, viewerOrg);
|
|
39
46
|
* ```
|
|
40
47
|
*/
|
|
41
48
|
export function useAgentInstances(
|
|
42
49
|
agentId: string | null | undefined,
|
|
50
|
+
org?: string,
|
|
43
51
|
): UseAgentInstancesReturn {
|
|
44
52
|
const stigmer = useStigmer();
|
|
45
53
|
|
|
@@ -48,6 +56,7 @@ export function useAgentInstances(
|
|
|
48
56
|
const resp = await stigmer.agentInstance.getByAgent(
|
|
49
57
|
create(GetAgentInstancesByAgentRequestSchema, {
|
|
50
58
|
agentId,
|
|
59
|
+
org: org ?? "",
|
|
51
60
|
}),
|
|
52
61
|
);
|
|
53
62
|
return resp.items ? [...resp.items] : [];
|
|
@@ -60,7 +69,7 @@ export function useAgentInstances(
|
|
|
60
69
|
isRefetching,
|
|
61
70
|
error,
|
|
62
71
|
refetch,
|
|
63
|
-
} = useFetch<readonly AgentInstance[]>(fetchFn, [agentId, stigmer], []);
|
|
72
|
+
} = useFetch<readonly AgentInstance[]>(fetchFn, [agentId, org, stigmer], []);
|
|
64
73
|
|
|
65
74
|
return { instances, isLoading, isRefetching, error, refetch };
|
|
66
75
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Visual weight of a {@link Button}, from most to least prominent.
|
|
8
|
+
*
|
|
9
|
+
* - `primary` — filled call to action (one per view: empty-state CTAs,
|
|
10
|
+
* dialog confirms).
|
|
11
|
+
* - `outline` — bordered pill for secondary actions that share a row
|
|
12
|
+
* with content (list-header actions like "Create share").
|
|
13
|
+
* - `ghost` — borderless, hover-surfaced; for repeated inline actions
|
|
14
|
+
* (table row actions) where borders would add noise.
|
|
15
|
+
* - `destructive` — filled danger action (dialog confirms for deletes).
|
|
16
|
+
* For inline destructive row actions prefer `ghost` with destructive
|
|
17
|
+
* text styling via `className`.
|
|
18
|
+
*/
|
|
19
|
+
export type ButtonVariant = "primary" | "outline" | "ghost" | "destructive";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Size of a {@link Button}.
|
|
23
|
+
*
|
|
24
|
+
* - `xs` — compact pill (list headers, table rows).
|
|
25
|
+
* - `sm` — standard (empty-state CTAs, dialog actions).
|
|
26
|
+
*/
|
|
27
|
+
export type ButtonSize = "xs" | "sm";
|
|
28
|
+
|
|
29
|
+
/** Props for {@link Button}. Extends the native button attributes. */
|
|
30
|
+
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
31
|
+
/** Visual weight. @default "primary" */
|
|
32
|
+
readonly variant?: ButtonVariant;
|
|
33
|
+
/** Size. @default "sm" */
|
|
34
|
+
readonly size?: ButtonSize;
|
|
35
|
+
/** Optional leading icon, rendered before the label. */
|
|
36
|
+
readonly icon?: ReactNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// One source of truth for each tier's classes: every mapping below is the
|
|
40
|
+
// established idiom already used across the SDK (list-header pills,
|
|
41
|
+
// EmptyState CTAs, ConfirmDialog actions) — collected here so surfaces
|
|
42
|
+
// can stop hand-rolling drifting copies of the same button.
|
|
43
|
+
const VARIANT_CLASSES: Record<ButtonVariant, string> = {
|
|
44
|
+
primary: "bg-primary text-primary-foreground hover:bg-primary-hover",
|
|
45
|
+
outline: "border border-border text-foreground hover:bg-accent-hover",
|
|
46
|
+
ghost: "text-foreground hover:bg-accent-hover",
|
|
47
|
+
destructive:
|
|
48
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive-hover",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const SIZE_CLASSES: Record<ButtonSize, string> = {
|
|
52
|
+
xs: "px-2.5 py-1 text-xs",
|
|
53
|
+
sm: "px-3 py-1.5 text-xs",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The SDK's shared button primitive.
|
|
58
|
+
*
|
|
59
|
+
* A thin, token-compliant wrapper over the native `<button>`: variants
|
|
60
|
+
* and sizes cover the console's established action tiers, everything
|
|
61
|
+
* else (handlers, ARIA, `disabled`, `form`) passes through natively.
|
|
62
|
+
* `type` defaults to `"button"` so a button inside a form never submits
|
|
63
|
+
* it accidentally.
|
|
64
|
+
*
|
|
65
|
+
* All visual properties flow through `--stgm-*` design tokens (DD-005);
|
|
66
|
+
* hover states use dedicated hover tokens, never opacity modifiers.
|
|
67
|
+
*
|
|
68
|
+
* This is an SDK component (DD-001) — embeddable by platform builders.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```tsx
|
|
72
|
+
* <Button variant="outline" size="xs" icon={<PlusIcon />} onClick={onCreate}>
|
|
73
|
+
* Create share
|
|
74
|
+
* </Button>
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function Button({
|
|
78
|
+
variant = "primary",
|
|
79
|
+
size = "sm",
|
|
80
|
+
icon,
|
|
81
|
+
type = "button",
|
|
82
|
+
className,
|
|
83
|
+
children,
|
|
84
|
+
...rest
|
|
85
|
+
}: ButtonProps) {
|
|
86
|
+
return (
|
|
87
|
+
<button
|
|
88
|
+
type={type}
|
|
89
|
+
className={cn(
|
|
90
|
+
"inline-flex items-center gap-1 rounded-md font-medium transition-colors",
|
|
91
|
+
"motion-reduce:transition-none",
|
|
92
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
93
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
94
|
+
SIZE_CLASSES[size],
|
|
95
|
+
VARIANT_CLASSES[variant],
|
|
96
|
+
className,
|
|
97
|
+
)}
|
|
98
|
+
{...rest}
|
|
99
|
+
>
|
|
100
|
+
{icon}
|
|
101
|
+
{children}
|
|
102
|
+
</button>
|
|
103
|
+
);
|
|
104
|
+
}
|