@stigmer/react 3.0.7 → 3.0.8-dev.20260612073024

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/access/ManageAccessButton.d.ts +47 -0
  2. package/access/ManageAccessButton.d.ts.map +1 -0
  3. package/access/ManageAccessButton.js +45 -0
  4. package/access/ManageAccessButton.js.map +1 -0
  5. package/access/ManageAccessDialog.d.ts +60 -0
  6. package/access/ManageAccessDialog.d.ts.map +1 -0
  7. package/access/ManageAccessDialog.js +83 -0
  8. package/access/ManageAccessDialog.js.map +1 -0
  9. package/access/index.d.ts +5 -0
  10. package/access/index.d.ts.map +1 -0
  11. package/access/index.js +7 -0
  12. package/access/index.js.map +1 -0
  13. package/access/types.d.ts +56 -0
  14. package/access/types.d.ts.map +1 -0
  15. package/access/types.js +2 -0
  16. package/access/types.js.map +1 -0
  17. package/access/useManageAccess.d.ts +60 -0
  18. package/access/useManageAccess.d.ts.map +1 -0
  19. package/access/useManageAccess.js +41 -0
  20. package/access/useManageAccess.js.map +1 -0
  21. package/agent/AgentDetailView.d.ts.map +1 -1
  22. package/agent/AgentDetailView.js +34 -4
  23. package/agent/AgentDetailView.js.map +1 -1
  24. package/agent-instance/AgentInstanceDetailPanel.d.ts.map +1 -1
  25. package/agent-instance/AgentInstanceDetailPanel.js +14 -4
  26. package/agent-instance/AgentInstanceDetailPanel.js.map +1 -1
  27. package/iam-policy/GrantAccessForm.d.ts +15 -5
  28. package/iam-policy/GrantAccessForm.d.ts.map +1 -1
  29. package/iam-policy/GrantAccessForm.js +15 -12
  30. package/iam-policy/GrantAccessForm.js.map +1 -1
  31. package/iam-policy/OrgMembersPanel.d.ts.map +1 -1
  32. package/iam-policy/OrgMembersPanel.js +2 -1
  33. package/iam-policy/OrgMembersPanel.js.map +1 -1
  34. package/iam-policy/PeopleWithAccess.d.ts +34 -0
  35. package/iam-policy/PeopleWithAccess.d.ts.map +1 -0
  36. package/iam-policy/PeopleWithAccess.js +55 -0
  37. package/iam-policy/PeopleWithAccess.js.map +1 -0
  38. package/iam-policy/PrincipalPicker.d.ts +48 -0
  39. package/iam-policy/PrincipalPicker.d.ts.map +1 -0
  40. package/iam-policy/PrincipalPicker.js +139 -0
  41. package/iam-policy/PrincipalPicker.js.map +1 -0
  42. package/iam-policy/ProviderBadge.d.ts +28 -0
  43. package/iam-policy/ProviderBadge.d.ts.map +1 -0
  44. package/iam-policy/ProviderBadge.js +31 -0
  45. package/iam-policy/ProviderBadge.js.map +1 -0
  46. package/iam-policy/SharePanel.d.ts +13 -5
  47. package/iam-policy/SharePanel.d.ts.map +1 -1
  48. package/iam-policy/SharePanel.js +9 -34
  49. package/iam-policy/SharePanel.js.map +1 -1
  50. package/iam-policy/index.d.ts +3 -0
  51. package/iam-policy/index.d.ts.map +1 -1
  52. package/iam-policy/index.js +3 -0
  53. package/iam-policy/index.js.map +1 -1
  54. package/index.d.ts +8 -6
  55. package/index.d.ts.map +1 -1
  56. package/index.js +6 -3
  57. package/index.js.map +1 -1
  58. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  59. package/mcp-server/McpServerDetailView.js +41 -12
  60. package/mcp-server/McpServerDetailView.js.map +1 -1
  61. package/organization/OrgProvider.d.ts +9 -0
  62. package/organization/OrgProvider.d.ts.map +1 -1
  63. package/organization/OrgProvider.js +12 -0
  64. package/organization/OrgProvider.js.map +1 -1
  65. package/organization/index.d.ts +1 -1
  66. package/organization/index.d.ts.map +1 -1
  67. package/organization/index.js +1 -1
  68. package/organization/index.js.map +1 -1
  69. package/package.json +4 -4
  70. package/skill/SkillDetailView.d.ts.map +1 -1
  71. package/skill/SkillDetailView.js +31 -3
  72. package/skill/SkillDetailView.js.map +1 -1
  73. package/src/access/ManageAccessButton.tsx +115 -0
  74. package/src/access/ManageAccessDialog.tsx +239 -0
  75. package/src/access/__tests__/ManageAccessButton.test.tsx +62 -0
  76. package/src/access/__tests__/ManageAccessDialog.test.tsx +146 -0
  77. package/src/access/index.ts +21 -0
  78. package/src/access/types.ts +58 -0
  79. package/src/access/useManageAccess.tsx +101 -0
  80. package/src/agent/AgentDetailView.tsx +50 -21
  81. package/src/agent-instance/AgentInstanceDetailPanel.tsx +24 -42
  82. package/src/iam-policy/GrantAccessForm.tsx +30 -35
  83. package/src/iam-policy/OrgMembersPanel.tsx +2 -0
  84. package/src/iam-policy/PeopleWithAccess.tsx +220 -0
  85. package/src/iam-policy/PrincipalPicker.tsx +347 -0
  86. package/src/iam-policy/ProviderBadge.tsx +53 -0
  87. package/src/iam-policy/SharePanel.tsx +20 -165
  88. package/src/iam-policy/index.ts +17 -0
  89. package/src/index.ts +31 -0
  90. package/src/mcp-server/McpServerDetailView.tsx +37 -9
  91. package/src/organization/OrgProvider.tsx +13 -0
  92. package/src/organization/index.ts +1 -1
  93. package/src/session/__tests__/execution-target.test.ts +18 -0
  94. package/src/skill/SkillDetailView.tsx +34 -9
  95. package/src/workflow/WorkflowDetailView.tsx +49 -22
  96. package/src/workflow/WorkflowExecutionHeader.tsx +12 -1
  97. package/src/workflow/WorkflowExecutionViewer.tsx +8 -1
  98. package/src/workflow/index.ts +4 -0
  99. package/src/workflow/instance/RunVisibilityControl.tsx +116 -0
  100. package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +55 -42
  101. package/src/workflow/instance/index.ts +5 -0
  102. package/src/workflow/instance/useUpdateWorkflowInstanceExecutionVisibility.ts +74 -0
  103. package/styles.css +1 -1
  104. package/workflow/WorkflowDetailView.d.ts.map +1 -1
  105. package/workflow/WorkflowDetailView.js +31 -3
  106. package/workflow/WorkflowDetailView.js.map +1 -1
  107. package/workflow/WorkflowExecutionHeader.d.ts +7 -0
  108. package/workflow/WorkflowExecutionHeader.d.ts.map +1 -1
  109. package/workflow/WorkflowExecutionHeader.js +2 -2
  110. package/workflow/WorkflowExecutionHeader.js.map +1 -1
  111. package/workflow/WorkflowExecutionViewer.d.ts +6 -1
  112. package/workflow/WorkflowExecutionViewer.d.ts.map +1 -1
  113. package/workflow/WorkflowExecutionViewer.js +2 -2
  114. package/workflow/WorkflowExecutionViewer.js.map +1 -1
  115. package/workflow/index.d.ts +1 -1
  116. package/workflow/index.d.ts.map +1 -1
  117. package/workflow/index.js +1 -1
  118. package/workflow/index.js.map +1 -1
  119. package/workflow/instance/RunVisibilityControl.d.ts +25 -0
  120. package/workflow/instance/RunVisibilityControl.d.ts.map +1 -0
  121. package/workflow/instance/RunVisibilityControl.js +56 -0
  122. package/workflow/instance/RunVisibilityControl.js.map +1 -0
  123. package/workflow/instance/WorkflowInstanceDetailPanel.d.ts.map +1 -1
  124. package/workflow/instance/WorkflowInstanceDetailPanel.js +30 -4
  125. package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
  126. package/workflow/instance/index.d.ts +2 -0
  127. package/workflow/instance/index.d.ts.map +1 -1
  128. package/workflow/instance/index.js +2 -0
  129. package/workflow/instance/index.js.map +1 -1
  130. package/workflow/instance/useUpdateWorkflowInstanceExecutionVisibility.d.ts +30 -0
  131. package/workflow/instance/useUpdateWorkflowInstanceExecutionVisibility.d.ts.map +1 -0
  132. package/workflow/instance/useUpdateWorkflowInstanceExecutionVisibility.js +39 -0
  133. package/workflow/instance/useUpdateWorkflowInstanceExecutionVisibility.js.map +1 -0
@@ -0,0 +1,146 @@
1
+ import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
2
+ import { render, screen, cleanup } from "@testing-library/react";
3
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
4
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
5
+ import { ManageAccessDialog } from "../ManageAccessDialog";
6
+
7
+ // The dialog's job is composition + conditional section rendering — not the
8
+ // behavior of the lower-level controls, which own their own tests. Stub the
9
+ // two heavy children to assert *which* sections mount, and pin the
10
+ // proto-generated capability so the People gate is deterministic.
11
+ const hasGrantableRolesMock = vi.fn<(kind: ApiResourceKind) => boolean>(() => true);
12
+ vi.mock("@stigmer/sdk", async (importActual) => {
13
+ const actual = await importActual<typeof import("@stigmer/sdk")>();
14
+ return { ...actual, hasGrantableRoles: (kind: ApiResourceKind) => hasGrantableRolesMock(kind) };
15
+ });
16
+
17
+ vi.mock("../../iam-policy/PeopleWithAccess", () => ({
18
+ PeopleWithAccess: () => <div data-testid="people-with-access" />,
19
+ }));
20
+
21
+ vi.mock("../../library/ResourceVisibilityControl", () => ({
22
+ ResourceVisibilityControl: () => <div data-testid="visibility-control" />,
23
+ }));
24
+
25
+ // happy-dom does not implement the native dialog show/close methods.
26
+ beforeAll(() => {
27
+ HTMLDialogElement.prototype.showModal = function showModal() {
28
+ this.open = true;
29
+ };
30
+ HTMLDialogElement.prototype.close = function close() {
31
+ this.open = false;
32
+ };
33
+ });
34
+
35
+ afterEach(() => {
36
+ cleanup();
37
+ hasGrantableRolesMock.mockReset();
38
+ hasGrantableRolesMock.mockReturnValue(true);
39
+ });
40
+
41
+ const RESOURCE = {
42
+ kind: ApiResourceKind.agent,
43
+ kindString: "agent",
44
+ id: "agt_123",
45
+ org: "acme",
46
+ name: "Release Bot",
47
+ } as const;
48
+
49
+ describe("ManageAccessDialog", () => {
50
+ it("mounts no body while closed (the access-list fetch stays lazy)", () => {
51
+ render(
52
+ <ManageAccessDialog
53
+ open={false}
54
+ onOpenChange={() => {}}
55
+ resource={RESOURCE}
56
+ />,
57
+ );
58
+
59
+ expect(screen.queryByText("Manage access")).toBeNull();
60
+ expect(screen.queryByTestId("people-with-access")).toBeNull();
61
+ });
62
+
63
+ it("renders the header with the resource name when open", () => {
64
+ render(
65
+ <ManageAccessDialog open onOpenChange={() => {}} resource={RESOURCE} />,
66
+ );
67
+
68
+ expect(screen.getByText("Manage access")).toBeTruthy();
69
+ expect(screen.getByText("Release Bot")).toBeTruthy();
70
+ });
71
+
72
+ it("renders General access only when a visibility descriptor is provided", () => {
73
+ const { rerender } = render(
74
+ <ManageAccessDialog open onOpenChange={() => {}} resource={RESOURCE} />,
75
+ );
76
+ expect(screen.queryByTestId("visibility-control")).toBeNull();
77
+
78
+ rerender(
79
+ <ManageAccessDialog
80
+ open
81
+ onOpenChange={() => {}}
82
+ resource={RESOURCE}
83
+ visibility={{
84
+ kind: "agent",
85
+ current: ApiResourceVisibility.visibility_private,
86
+ org: "acme",
87
+ }}
88
+ />,
89
+ );
90
+ expect(screen.getByTestId("visibility-control")).toBeTruthy();
91
+ expect(screen.getByText("General access")).toBeTruthy();
92
+ });
93
+
94
+ it("renders People only when the kind has grantable roles", () => {
95
+ hasGrantableRolesMock.mockReturnValue(false);
96
+ const { rerender } = render(
97
+ <ManageAccessDialog open onOpenChange={() => {}} resource={RESOURCE} />,
98
+ );
99
+ expect(screen.queryByTestId("people-with-access")).toBeNull();
100
+
101
+ hasGrantableRolesMock.mockReturnValue(true);
102
+ rerender(
103
+ <ManageAccessDialog
104
+ open
105
+ onOpenChange={() => {}}
106
+ resource={{ ...RESOURCE, id: "agt_456" }}
107
+ />,
108
+ );
109
+ expect(screen.getByTestId("people-with-access")).toBeTruthy();
110
+ expect(screen.getByText("People with access")).toBeTruthy();
111
+ });
112
+
113
+ it("renders the extra section only when provided", () => {
114
+ render(
115
+ <ManageAccessDialog
116
+ open
117
+ onOpenChange={() => {}}
118
+ resource={RESOURCE}
119
+ extraSection={{
120
+ title: "Run visibility",
121
+ description: "Who can observe runs.",
122
+ content: <div data-testid="run-visibility" />,
123
+ }}
124
+ />,
125
+ );
126
+
127
+ expect(screen.getByText("Run visibility")).toBeTruthy();
128
+ expect(screen.getByTestId("run-visibility")).toBeTruthy();
129
+ });
130
+
131
+ it("requests close via Done and the close affordance", () => {
132
+ const onOpenChange = vi.fn();
133
+ render(
134
+ <ManageAccessDialog open onOpenChange={onOpenChange} resource={RESOURCE} />,
135
+ );
136
+
137
+ // Query by text/label rather than role: a native <dialog> that has not yet
138
+ // run showModal() hides its descendants from the accessibility tree.
139
+ screen.getByText("Done").click();
140
+ expect(onOpenChange).toHaveBeenCalledWith(false);
141
+
142
+ onOpenChange.mockClear();
143
+ screen.getByLabelText("Close").click();
144
+ expect(onOpenChange).toHaveBeenCalledWith(false);
145
+ });
146
+ });
@@ -0,0 +1,21 @@
1
+ // Access — the unified "Manage access" experience composing visibility
2
+ // (library) and explicit grants (iam-policy) into one dialog, with a kebab
3
+ // hook and a visible-button trigger for the surfaces that mount it.
4
+ export {
5
+ ManageAccessDialog,
6
+ type ManageAccessDialogProps,
7
+ } from "./ManageAccessDialog";
8
+ export {
9
+ ManageAccessButton,
10
+ type ManageAccessButtonProps,
11
+ } from "./ManageAccessButton";
12
+ export {
13
+ useManageAccess,
14
+ type UseManageAccessArgs,
15
+ type UseManageAccessReturn,
16
+ } from "./useManageAccess";
17
+ export type {
18
+ AccessResource,
19
+ AccessVisibility,
20
+ AccessExtraSection,
21
+ } from "./types";
@@ -0,0 +1,58 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
3
+ import type { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
4
+ import type { VisibilityResourceKind } from "../library/useUpdateVisibility";
5
+
6
+ /**
7
+ * The resource whose access is being managed. Carries everything the People
8
+ * section needs: the enum {@link ApiResourceKind} (for grantable-role lookup),
9
+ * the FGA/API kind string (for IAM refs and permission checks), the id, and
10
+ * the owning org (drives the org-member typeahead).
11
+ */
12
+ export interface AccessResource {
13
+ /** ApiResourceKind enum — drives grantable-role lookup and capability. */
14
+ readonly kind: ApiResourceKind;
15
+ /** FGA/API kind string (e.g. "mcp_server", "session", "workflow_execution"). */
16
+ readonly kindString: string;
17
+ /** Resource id. */
18
+ readonly id: string;
19
+ /** Slug of the owning organization (`metadata.org`). */
20
+ readonly org: string;
21
+ /** Optional display name, shown as the dialog subtitle for context. */
22
+ readonly name?: string;
23
+ }
24
+
25
+ /**
26
+ * Describes the "General access" (visibility) axis for the Manage access
27
+ * dialog. Optional because not every resource has visibility (e.g. sessions
28
+ * and workflow executions do not). When present, the dialog renders the
29
+ * shared `ResourceVisibilityControl`, which owns level selection and
30
+ * the `can_edit` gate.
31
+ */
32
+ export interface AccessVisibility {
33
+ /** Resource kind, selecting both the updateVisibility RPC and FGA type. */
34
+ readonly kind: VisibilityResourceKind;
35
+ /** Current visibility of the resource. */
36
+ readonly current: ApiResourceVisibility;
37
+ /**
38
+ * Slug of the owning org (`metadata.org`); gates the Platform option for
39
+ * blueprints. Omit for instances and where Platform should not be offered.
40
+ */
41
+ readonly org?: string;
42
+ /** Called after a successful visibility change so the host can refetch. */
43
+ readonly onChanged?: () => void;
44
+ }
45
+
46
+ /**
47
+ * A generic, resource-specific access section appended below People — the
48
+ * escape hatch for the rare per-kind axis (today: workflow-instance run
49
+ * observability) without baking that knowledge into a generic dialog.
50
+ */
51
+ export interface AccessExtraSection {
52
+ /** Section heading. */
53
+ readonly title: string;
54
+ /** Optional one-line explanation under the heading. */
55
+ readonly description?: string;
56
+ /** The section body (e.g. a `<RunVisibilityControl />`). */
57
+ readonly content: ReactNode;
58
+ }
@@ -0,0 +1,101 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState, type ReactNode } from "react";
4
+ import { useCheckPermission } from "../iam-policy/useCheckPermission";
5
+ import type { DetailAction } from "../resource-detail/types";
6
+ import { ManageAccessDialog } from "./ManageAccessDialog";
7
+ import type {
8
+ AccessResource,
9
+ AccessVisibility,
10
+ AccessExtraSection,
11
+ } from "./types";
12
+
13
+ /** Arguments for {@link useManageAccess}. */
14
+ export interface UseManageAccessArgs {
15
+ /**
16
+ * The resource whose access is managed, or `null` while it is still
17
+ * loading. When `null`, {@link UseManageAccessReturn.action} is `null` and
18
+ * the dialog renders nothing — safe to call before the resource is ready.
19
+ */
20
+ readonly resource: AccessResource | null;
21
+ /** General access (visibility) axis; omit for resources without visibility. */
22
+ readonly visibility?: AccessVisibility;
23
+ /** Optional resource-specific section (e.g. run observability). */
24
+ readonly extraSection?: AccessExtraSection;
25
+ /** Menu-item label. @default "Manage access" */
26
+ readonly label?: string;
27
+ }
28
+
29
+ /** Return value of {@link useManageAccess}. */
30
+ export interface UseManageAccessReturn {
31
+ /**
32
+ * A ready-to-spread {@link DetailAction} for a kebab/overflow menu, or
33
+ * `null` when the resource is unavailable or the user lacks
34
+ * `can_view_access`. Lives in the `"sharing"` group.
35
+ */
36
+ readonly action: DetailAction | null;
37
+ /** The {@link ManageAccessDialog} node — render it once in the host tree. */
38
+ readonly dialog: ReactNode;
39
+ /** Imperatively open the dialog. */
40
+ readonly open: () => void;
41
+ /** Whether the dialog is currently open. */
42
+ readonly isOpen: boolean;
43
+ }
44
+
45
+ /**
46
+ * Wires the unified Manage access dialog to a kebab/overflow menu — the
47
+ * trigger shape used by static resource detail views (agent, skill,
48
+ * mcp_server, workflow), whose actions live in {@link ResourceActionBar}'s
49
+ * menu rather than as a standalone button.
50
+ *
51
+ * Owns the open-state and the `can_view_access` gate (a viewer may open the
52
+ * dialog to read general access and the people list; the dialog's own sections
53
+ * gate editing). Returns a `null` action when the resource is still loading or
54
+ * the user cannot view access — so the host can unconditionally fold
55
+ * `action` into its actions array.
56
+ *
57
+ * Surfaces that want a *visible* trigger instead use {@link ManageAccessButton}.
58
+ *
59
+ * @example
60
+ * ```tsx
61
+ * const access = useManageAccess({
62
+ * resource: meta ? { kind: ApiResourceKind.agent, kindString: "agent", id: meta.id, org: meta.org } : null,
63
+ * visibility: meta ? { kind: "agent", current: meta.visibility, org: meta.org, onChanged: refetch } : undefined,
64
+ * });
65
+ * // ...
66
+ * <ResourceDetailShell actions={access.action ? [...actions, access.action] : actions} ... />
67
+ * {access.dialog}
68
+ * ```
69
+ */
70
+ export function useManageAccess({
71
+ resource,
72
+ visibility,
73
+ extraSection,
74
+ label = "Manage access",
75
+ }: UseManageAccessArgs): UseManageAccessReturn {
76
+ const [isOpen, setIsOpen] = useState(false);
77
+
78
+ const { allowed: canView } = useCheckPermission(
79
+ resource ? { kind: resource.kindString, id: resource.id } : null,
80
+ "can_view_access",
81
+ );
82
+
83
+ const open = useCallback(() => setIsOpen(true), []);
84
+
85
+ const action: DetailAction | null =
86
+ resource && canView
87
+ ? { id: "manage-access", label, group: "sharing", onAction: open }
88
+ : null;
89
+
90
+ const dialog = resource ? (
91
+ <ManageAccessDialog
92
+ open={isOpen}
93
+ onOpenChange={setIsOpen}
94
+ resource={resource}
95
+ visibility={visibility}
96
+ extraSection={extraSection}
97
+ />
98
+ ) : null;
99
+
100
+ return { action, dialog, open, isOpen };
101
+ }
@@ -10,11 +10,13 @@ import type {
10
10
  import type { ApiResourceReference } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
11
11
  import type { EnvVarDeclaration } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
12
12
  import type { AgentInstance } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/api_pb";
13
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
13
14
  import { useAgent } from "./useAgent";
14
15
  import { useUpdateAgent } from "./useUpdateAgent";
15
16
  import { agentToInput } from "./internal/agentToInput";
16
17
  import { ErrorMessage } from "../error/ErrorMessage";
17
- import { ResourceVisibilityControl } from "../library/ResourceVisibilityControl";
18
+ import { VisibilityBadge } from "../library/VisibilitySelector";
19
+ import { useManageAccess } from "../access/useManageAccess";
18
20
  import { ResourceDetailShell } from "../resource-detail/ResourceDetailShell";
19
21
  import { Section } from "../resource-detail/Section";
20
22
  import { useDetailTabs } from "../resource-detail/useDetailTabs";
@@ -280,6 +282,30 @@ export function AgentDetailView({
280
282
  }
281
283
  }, [agent]);
282
284
 
285
+ // Unified Manage access — visibility (General access) over explicit grants
286
+ // (People), opened from the kebab. Derived from the loaded metadata, so the
287
+ // action stays null until the resource is ready (and folds harmlessly into
288
+ // the actions array meanwhile).
289
+ const access = useManageAccess({
290
+ resource: agent?.metadata
291
+ ? {
292
+ kind: ApiResourceKind.agent,
293
+ kindString: "agent",
294
+ id: agent.metadata.id,
295
+ org: agent.metadata.org,
296
+ name: agent.metadata.name,
297
+ }
298
+ : null,
299
+ visibility: agent?.metadata
300
+ ? {
301
+ kind: "agent",
302
+ current: agent.metadata.visibility,
303
+ org: agent.metadata.org,
304
+ onChanged: refetch,
305
+ }
306
+ : undefined,
307
+ });
308
+
283
309
  if (isLoading) return <LoadingSkeleton className={className} />;
284
310
  if (error)
285
311
  return <ErrorMessage error={error} retry={refetch} className={className} />;
@@ -312,16 +338,16 @@ export function AgentDetailView({
312
338
  updatedAt: specAudit?.updatedAt ? timestampDate(specAudit.updatedAt) : null,
313
339
  };
314
340
 
341
+ // Inline visibility is read-only (at-a-glance); editing lives in the
342
+ // Manage access dialog, the single writer for both access axes.
315
343
  const visibilityControl = meta ? (
316
- <ResourceVisibilityControl
317
- kind="agent"
318
- resourceId={meta.id}
319
- visibility={meta.visibility}
320
- org={meta.org || org}
321
- onChanged={refetch}
322
- />
344
+ <VisibilityBadge visibility={meta.visibility} />
323
345
  ) : undefined;
324
346
 
347
+ const mergedActions = access.action
348
+ ? [...(actions ?? []), access.action]
349
+ : actions;
350
+
325
351
  let tabContent: React.ReactNode;
326
352
  if (activeAdditionalTab) {
327
353
  tabContent = activeAdditionalTab.content;
@@ -361,19 +387,22 @@ export function AgentDetailView({
361
387
  }
362
388
 
363
389
  return (
364
- <ResourceDetailShell
365
- header={headerMeta}
366
- visibilityControl={visibilityControl}
367
- primaryAction={primaryAction}
368
- actions={actions}
369
- tabs={effectiveTabs}
370
- activeTab={effectiveTabs ? effectiveActiveTab : undefined}
371
- onTabChange={effectiveTabs ? effectiveOnTabChange : undefined}
372
- tabsAriaLabel="Agent detail sections"
373
- className={className}
374
- >
375
- {tabContent}
376
- </ResourceDetailShell>
390
+ <>
391
+ <ResourceDetailShell
392
+ header={headerMeta}
393
+ visibilityControl={visibilityControl}
394
+ primaryAction={primaryAction}
395
+ actions={mergedActions}
396
+ tabs={effectiveTabs}
397
+ activeTab={effectiveTabs ? effectiveActiveTab : undefined}
398
+ onTabChange={effectiveTabs ? effectiveOnTabChange : undefined}
399
+ tabsAriaLabel="Agent detail sections"
400
+ className={className}
401
+ >
402
+ {tabContent}
403
+ </ResourceDetailShell>
404
+ {access.dialog}
405
+ </>
377
406
  );
378
407
  }
379
408
 
@@ -10,9 +10,9 @@ import type { ResourceRef } from "@stigmer/sdk";
10
10
  import { getUserMessage } from "@stigmer/sdk";
11
11
  import { useUpdateAgentInstance } from "./useUpdateAgentInstance";
12
12
  import { useDeleteAgentInstance } from "./useDeleteAgentInstance";
13
- import { ResourceVisibilityControl } from "../library/ResourceVisibilityControl";
13
+ import { VisibilityBadge } from "../library/VisibilitySelector";
14
14
  import { PermissionGate } from "../iam-policy/PermissionGate";
15
- import { SharePanel } from "../iam-policy/SharePanel";
15
+ import { ManageAccessButton } from "../access/ManageAccessButton";
16
16
  import { EnvironmentPicker } from "../environment/EnvironmentPicker";
17
17
  import { useEnvironmentList } from "../environment/useEnvironmentList";
18
18
 
@@ -58,10 +58,11 @@ export function AgentInstanceDetailPanel({
58
58
 
59
59
  const [isEditingEnvs, setIsEditingEnvs] = useState(false);
60
60
  const [editEnvRefs, setEditEnvRefs] = useState<ResourceRef[]>([]);
61
- const [showSharePanel, setShowSharePanel] = useState(false);
62
61
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
63
62
  const [deleteError, setDeleteError] = useState<Error | null>(null);
64
63
 
64
+ const visibility = meta?.visibility ?? ApiResourceVisibility.visibility_private;
65
+
65
66
  const handleStartEditEnvs = useCallback(() => {
66
67
  const currentRefs: ResourceRef[] = (spec?.environmentRefs ?? []).map((ref) => ({
67
68
  org: ref.org || org,
@@ -115,9 +116,12 @@ export function AgentInstanceDetailPanel({
115
116
  {/* Header */}
116
117
  <div className="flex items-center justify-between border-b border-border px-4 py-3">
117
118
  <div>
118
- <h3 className="text-sm font-semibold text-foreground">
119
- {meta?.name || meta?.slug || "Instance"}
120
- </h3>
119
+ <div className="flex items-center gap-2">
120
+ <h3 className="text-sm font-semibold text-foreground">
121
+ {meta?.name || meta?.slug || "Instance"}
122
+ </h3>
123
+ <VisibilityBadge visibility={visibility} />
124
+ </div>
121
125
  <p className="text-[0.65rem] text-muted-foreground">
122
126
  {createdAt && `Created ${createdAt.toLocaleDateString()}`}
123
127
  {updatedAt && ` \u00B7 Updated ${updatedAt.toLocaleDateString()}`}
@@ -137,19 +141,20 @@ export function AgentInstanceDetailPanel({
137
141
  Start session
138
142
  </button>
139
143
  )}
140
- <PermissionGate resource={{ kind: "agent_instance", id }} relation="can_grant_access">
141
- <button
142
- type="button"
143
- onClick={() => setShowSharePanel((v) => !v)}
144
- className={cn(
145
- "rounded-md px-2.5 py-1 text-xs font-medium",
146
- "border border-border text-foreground hover:bg-accent-hover",
147
- "focus:outline-none focus:ring-2 focus:ring-ring",
148
- )}
149
- >
150
- Share
151
- </button>
152
- </PermissionGate>
144
+ <ManageAccessButton
145
+ resource={{
146
+ kind: ApiResourceKind.agent_instance,
147
+ kindString: "agent_instance",
148
+ id,
149
+ org: meta?.org ?? "",
150
+ name: meta?.name,
151
+ }}
152
+ visibility={{
153
+ kind: "agentInstance",
154
+ current: visibility,
155
+ onChanged: onUpdated,
156
+ }}
157
+ />
153
158
  <button
154
159
  type="button"
155
160
  onClick={onClose}
@@ -241,29 +246,6 @@ export function AgentInstanceDetailPanel({
241
246
  )}
242
247
  </div>
243
248
 
244
- {/* Visibility */}
245
- <div className="px-4 py-3">
246
- <h4 className="text-xs font-medium text-muted-foreground mb-2">Visibility</h4>
247
- <ResourceVisibilityControl
248
- kind="agentInstance"
249
- resourceId={id}
250
- visibility={meta?.visibility ?? ApiResourceVisibility.visibility_private}
251
- onChanged={onUpdated}
252
- />
253
- </div>
254
-
255
- {/* Share Panel */}
256
- {showSharePanel && (
257
- <div className="px-4 py-3">
258
- <SharePanel
259
- resource={{ kind: "agent_instance", id, resourceKind: ApiResourceKind.agent_instance }}
260
- resourceKindString="agent_instance"
261
- resourceKind={ApiResourceKind.agent_instance}
262
- onClose={() => setShowSharePanel(false)}
263
- />
264
- </div>
265
- )}
266
-
267
249
  {/* Delete */}
268
250
  <PermissionGate resource={{ kind: "agent_instance", id }} relation="can_delete">
269
251
  <div className="px-4 py-3">
@@ -11,6 +11,7 @@ import { cn } from "@stigmer/theme";
11
11
  import { getUserMessage, iamRoleToString } from "@stigmer/sdk";
12
12
  import { useCreateIamPolicy } from "./useCreateIamPolicy";
13
13
  import { RoleSelector } from "./RoleSelector";
14
+ import { PrincipalPicker, type SelectedPrincipal } from "./PrincipalPicker";
14
15
 
15
16
  /** Props for {@link GrantAccessForm}. */
16
17
  export interface GrantAccessFormProps {
@@ -20,6 +21,13 @@ export interface GrantAccessFormProps {
20
21
  readonly resourceKindString: string;
21
22
  /** ID of the resource being granted access to. */
22
23
  readonly resourceId: string;
24
+ /**
25
+ * Organization whose members can be granted access. Drives the
26
+ * {@link PrincipalPicker} typeahead.
27
+ */
28
+ readonly orgId: string;
29
+ /** Principal IDs that already have access (shown disabled in the picker). */
30
+ readonly excludePrincipalIds?: readonly string[];
23
31
  /** Fired after a policy is successfully created. */
24
32
  readonly onGranted?: (policy: IamPolicy) => void;
25
33
  /** Fired when the user cancels. */
@@ -29,11 +37,13 @@ export interface GrantAccessFormProps {
29
37
  }
30
38
 
31
39
  /**
32
- * Form for granting a principal access to a resource.
40
+ * Form for granting an organization member access to a resource.
33
41
  *
34
- * Collects a **principal ID** (identity account), lets the user pick
35
- * a **role** from the resource's grantable roles, and creates the IAM
36
- * policy binding.
42
+ * Lets the user pick a **person** from the org's member list (by name or
43
+ * email, disambiguating identity sources) via {@link PrincipalPicker}, choose
44
+ * a **role** from the resource's grantable roles, and creates the IAM policy
45
+ * binding. The resolved `identity_account` ID is carried internally — the user
46
+ * never types or sees raw account IDs.
37
47
  *
38
48
  * All visual properties flow through `--stgm-*` design tokens.
39
49
  *
@@ -43,6 +53,7 @@ export interface GrantAccessFormProps {
43
53
  * resourceKind={ApiResourceKind.organization}
44
54
  * resourceKindString="organization"
45
55
  * resourceId="org-abc123"
56
+ * orgId="org-abc123"
46
57
  * onGranted={(policy) => refetchAccessList()}
47
58
  * onCancel={() => setShowForm(false)}
48
59
  * />
@@ -52,6 +63,8 @@ export function GrantAccessForm({
52
63
  resourceKind,
53
64
  resourceKindString,
54
65
  resourceId,
66
+ orgId,
67
+ excludePrincipalIds,
55
68
  onGranted,
56
69
  onCancel,
57
70
  className,
@@ -59,24 +72,23 @@ export function GrantAccessForm({
59
72
  const { create: createPolicy, isCreating, error, clearError } =
60
73
  useCreateIamPolicy();
61
74
 
62
- const [principalId, setPrincipalId] = useState("");
75
+ const [principal, setPrincipal] = useState<SelectedPrincipal | null>(null);
63
76
  const [selectedRole, setSelectedRole] = useState<IamRole | null>(null);
64
77
 
65
- const trimmedPrincipalId = principalId.trim();
66
78
  const canSubmit =
67
- trimmedPrincipalId !== "" && selectedRole !== null && !isCreating;
79
+ principal !== null && selectedRole !== null && !isCreating;
68
80
 
69
81
  const handleSubmit = useCallback(
70
82
  async (e: FormEvent) => {
71
83
  e.preventDefault();
72
- if (!canSubmit || selectedRole === null) return;
84
+ if (!canSubmit || selectedRole === null || principal === null) return;
73
85
 
74
86
  clearError();
75
87
  try {
76
88
  const spec = create(IamPolicySpecSchema, {
77
89
  principal: create(ApiResourceRefSchema, {
78
90
  kind: "identity_account",
79
- id: trimmedPrincipalId,
91
+ id: principal.id,
80
92
  }),
81
93
  resource: create(ApiResourceRefSchema, {
82
94
  kind: resourceKindString,
@@ -93,7 +105,7 @@ export function GrantAccessForm({
93
105
  [
94
106
  canSubmit,
95
107
  selectedRole,
96
- trimmedPrincipalId,
108
+ principal,
97
109
  resourceKindString,
98
110
  resourceId,
99
111
  createPolicy,
@@ -105,31 +117,14 @@ export function GrantAccessForm({
105
117
  return (
106
118
  <form onSubmit={handleSubmit} className={cn("space-y-3", className)}>
107
119
  <div className="space-y-3">
108
- {/* Principal */}
109
- <div className="space-y-1">
110
- <label
111
- htmlFor="stgm-grant-principal"
112
- className="text-xs font-medium text-foreground"
113
- >
114
- Account ID
115
- </label>
116
- <input
117
- id="stgm-grant-principal"
118
- type="text"
119
- value={principalId}
120
- onChange={(e) => setPrincipalId(e.target.value)}
121
- placeholder="e.g. ia-01HQUSER123"
122
- disabled={isCreating}
123
- autoFocus
124
- required
125
- className={cn(
126
- "w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
127
- "placeholder:text-muted-foreground",
128
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
129
- "disabled:pointer-events-none disabled:opacity-50",
130
- )}
131
- />
132
- </div>
120
+ {/* Principal picker (org-member typeahead) */}
121
+ <PrincipalPicker
122
+ orgId={orgId}
123
+ value={principal}
124
+ onChange={setPrincipal}
125
+ excludePrincipalIds={excludePrincipalIds}
126
+ disabled={isCreating}
127
+ />
133
128
 
134
129
  {/* Role selector */}
135
130
  <RoleSelector