@stigmer/react 3.0.5 → 3.0.7-dev.20260611143057

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 (137) hide show
  1. package/agent/AgentDetailView.d.ts.map +1 -1
  2. package/agent/AgentDetailView.js +1 -1
  3. package/agent/AgentDetailView.js.map +1 -1
  4. package/agent-instance/AgentInstanceDetailPanel.d.ts.map +1 -1
  5. package/agent-instance/AgentInstanceDetailPanel.js +2 -13
  6. package/agent-instance/AgentInstanceDetailPanel.js.map +1 -1
  7. package/agent-instance/AgentInstanceList.d.ts.map +1 -1
  8. package/agent-instance/AgentInstanceList.js +2 -13
  9. package/agent-instance/AgentInstanceList.js.map +1 -1
  10. package/agent-instance/CreateAgentInstanceDialog.d.ts.map +1 -1
  11. package/agent-instance/CreateAgentInstanceDialog.js +1 -1
  12. package/agent-instance/CreateAgentInstanceDialog.js.map +1 -1
  13. package/composer/SessionComposer.d.ts +14 -0
  14. package/composer/SessionComposer.d.ts.map +1 -1
  15. package/composer/SessionComposer.js +15 -9
  16. package/composer/SessionComposer.js.map +1 -1
  17. package/index.d.ts +3 -3
  18. package/index.d.ts.map +1 -1
  19. package/index.js +1 -1
  20. package/index.js.map +1 -1
  21. package/library/InstanceVisibilitySelector.d.ts +30 -23
  22. package/library/InstanceVisibilitySelector.d.ts.map +1 -1
  23. package/library/InstanceVisibilitySelector.js +22 -145
  24. package/library/InstanceVisibilitySelector.js.map +1 -1
  25. package/library/ResourceVisibilityControl.d.ts +23 -6
  26. package/library/ResourceVisibilityControl.d.ts.map +1 -1
  27. package/library/ResourceVisibilityControl.js +38 -9
  28. package/library/ResourceVisibilityControl.js.map +1 -1
  29. package/library/ScopeToggle.d.ts +1 -1
  30. package/library/ScopeToggle.js +1 -1
  31. package/library/VisibilityOptionRow.d.ts +52 -0
  32. package/library/VisibilityOptionRow.d.ts.map +1 -0
  33. package/library/VisibilityOptionRow.js +92 -0
  34. package/library/VisibilityOptionRow.js.map +1 -0
  35. package/library/VisibilitySelector.d.ts +98 -0
  36. package/library/VisibilitySelector.d.ts.map +1 -0
  37. package/library/VisibilitySelector.js +193 -0
  38. package/library/VisibilitySelector.js.map +1 -0
  39. package/library/index.d.ts +4 -2
  40. package/library/index.d.ts.map +1 -1
  41. package/library/index.js +2 -1
  42. package/library/index.js.map +1 -1
  43. package/library/useUpdateVisibility.d.ts +5 -4
  44. package/library/useUpdateVisibility.d.ts.map +1 -1
  45. package/library/useUpdateVisibility.js +5 -4
  46. package/library/useUpdateVisibility.js.map +1 -1
  47. package/library/visibilityLevels.d.ts +96 -0
  48. package/library/visibilityLevels.d.ts.map +1 -0
  49. package/library/visibilityLevels.js +97 -0
  50. package/library/visibilityLevels.js.map +1 -0
  51. package/mcp-server/McpServerDetailView.d.ts +1 -11
  52. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  53. package/mcp-server/McpServerDetailView.js +3 -6
  54. package/mcp-server/McpServerDetailView.js.map +1 -1
  55. package/package.json +4 -4
  56. package/resource-detail/types.d.ts +1 -1
  57. package/session/NewSessionViewer.d.ts +32 -1
  58. package/session/NewSessionViewer.d.ts.map +1 -1
  59. package/session/NewSessionViewer.js +20 -9
  60. package/session/NewSessionViewer.js.map +1 -1
  61. package/session/SessionViewer.d.ts +24 -1
  62. package/session/SessionViewer.d.ts.map +1 -1
  63. package/session/SessionViewer.js +18 -12
  64. package/session/SessionViewer.js.map +1 -1
  65. package/session/audience.d.ts +21 -0
  66. package/session/audience.d.ts.map +1 -0
  67. package/session/audience.js +2 -0
  68. package/session/audience.js.map +1 -0
  69. package/session/index.d.ts +2 -0
  70. package/session/index.d.ts.map +1 -1
  71. package/session/index.js.map +1 -1
  72. package/session/runtime-env.d.ts +47 -0
  73. package/session/runtime-env.d.ts.map +1 -0
  74. package/session/runtime-env.js +20 -0
  75. package/session/runtime-env.js.map +1 -0
  76. package/session/useNewSessionFlow.d.ts +25 -0
  77. package/session/useNewSessionFlow.d.ts.map +1 -1
  78. package/session/useNewSessionFlow.js +20 -8
  79. package/session/useNewSessionFlow.js.map +1 -1
  80. package/session/useSessionPageFlow.d.ts +27 -2
  81. package/session/useSessionPageFlow.d.ts.map +1 -1
  82. package/session/useSessionPageFlow.js +34 -13
  83. package/session/useSessionPageFlow.js.map +1 -1
  84. package/skill/SkillDetailView.d.ts.map +1 -1
  85. package/skill/SkillDetailView.js +1 -1
  86. package/skill/SkillDetailView.js.map +1 -1
  87. package/src/agent/AgentDetailView.tsx +1 -0
  88. package/src/agent-instance/AgentInstanceDetailPanel.tsx +7 -32
  89. package/src/agent-instance/AgentInstanceList.tsx +7 -32
  90. package/src/agent-instance/CreateAgentInstanceDialog.tsx +1 -0
  91. package/src/composer/SessionComposer.tsx +30 -8
  92. package/src/composer/__tests__/SessionComposer-lockAgent.test.tsx +150 -0
  93. package/src/index.ts +10 -2
  94. package/src/library/InstanceVisibilitySelector.tsx +44 -283
  95. package/src/library/ResourceVisibilityControl.tsx +54 -8
  96. package/src/library/ScopeToggle.tsx +1 -1
  97. package/src/library/VisibilityOptionRow.tsx +244 -0
  98. package/src/library/VisibilitySelector.tsx +436 -0
  99. package/src/library/__tests__/VisibilitySelector.test.tsx +256 -0
  100. package/src/library/index.ts +13 -2
  101. package/src/library/useUpdateVisibility.ts +5 -4
  102. package/src/library/visibilityLevels.ts +174 -0
  103. package/src/mcp-server/McpServerDetailView.tsx +10 -35
  104. package/src/resource-detail/types.ts +1 -1
  105. package/src/session/NewSessionViewer.tsx +61 -12
  106. package/src/session/SessionViewer.tsx +51 -15
  107. package/src/session/__tests__/audienceWiring.test.tsx +274 -0
  108. package/src/session/__tests__/useNewSessionFlow.test.tsx +122 -0
  109. package/src/session/__tests__/useSessionPageFlow.runtimeEnv.test.tsx +170 -0
  110. package/src/session/audience.ts +20 -0
  111. package/src/session/index.ts +3 -0
  112. package/src/session/runtime-env.ts +57 -0
  113. package/src/session/useNewSessionFlow.ts +44 -9
  114. package/src/session/useSessionPageFlow.ts +65 -17
  115. package/src/skill/SkillDetailView.tsx +1 -0
  116. package/src/workflow/WorkflowDetailView.tsx +1 -0
  117. package/src/workflow/instance/CreateWorkflowInstanceDialog.tsx +1 -0
  118. package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +7 -32
  119. package/src/workflow/instance/WorkflowInstanceList.tsx +7 -32
  120. package/styles.css +1 -1
  121. package/workflow/WorkflowDetailView.d.ts.map +1 -1
  122. package/workflow/WorkflowDetailView.js +1 -1
  123. package/workflow/WorkflowDetailView.js.map +1 -1
  124. package/workflow/instance/CreateWorkflowInstanceDialog.d.ts.map +1 -1
  125. package/workflow/instance/CreateWorkflowInstanceDialog.js +1 -1
  126. package/workflow/instance/CreateWorkflowInstanceDialog.js.map +1 -1
  127. package/workflow/instance/WorkflowInstanceDetailPanel.d.ts.map +1 -1
  128. package/workflow/instance/WorkflowInstanceDetailPanel.js +2 -13
  129. package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
  130. package/workflow/instance/WorkflowInstanceList.d.ts.map +1 -1
  131. package/workflow/instance/WorkflowInstanceList.js +2 -13
  132. package/workflow/instance/WorkflowInstanceList.js.map +1 -1
  133. package/library/VisibilityToggle.d.ts +0 -53
  134. package/library/VisibilityToggle.d.ts.map +0 -1
  135. package/library/VisibilityToggle.js +0 -100
  136. package/library/VisibilityToggle.js.map +0 -1
  137. package/src/library/VisibilityToggle.tsx +0 -280
@@ -0,0 +1,256 @@
1
+ import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
2
+ import {
3
+ render,
4
+ screen,
5
+ within,
6
+ fireEvent,
7
+ waitFor,
8
+ cleanup,
9
+ } from "@testing-library/react";
10
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
11
+ import {
12
+ VisibilitySelector,
13
+ VisibilityBadge,
14
+ } from "../VisibilitySelector";
15
+ import {
16
+ INSTANCE_VISIBILITY_LEVELS,
17
+ blueprintVisibilityLevels,
18
+ } from "../visibilityLevels";
19
+
20
+ // Without a StigmerProvider the portal container is null, and Base UI's
21
+ // Portal renders nothing — pin it to document.body so the popover mounts.
22
+ vi.mock("../../portal-container", () => ({
23
+ useStigmerPortalContainer: () => document.body,
24
+ }));
25
+
26
+ // Base UI's Popover positioner observes its anchor; happy-dom lacks
27
+ // ResizeObserver, so provide a no-op shim.
28
+ beforeAll(() => {
29
+ if (!("ResizeObserver" in globalThis)) {
30
+ (globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver =
31
+ class {
32
+ observe() {}
33
+ unobserve() {}
34
+ disconnect() {}
35
+ };
36
+ }
37
+ });
38
+
39
+ afterEach(cleanup);
40
+
41
+ const BLUEPRINT_LEVELS = blueprintVisibilityLevels({
42
+ deploymentMode: "cloud",
43
+ hasIdentityProvider: true,
44
+ });
45
+
46
+ // Option accessible names are "<label> <description>"; anchor on the label so
47
+ // e.g. /^Organization/ does not also match Platform's "All organizations …".
48
+ const optionByLabel = (label: string) =>
49
+ screen.getByRole("option", { name: new RegExp(`^${label}`, "i") });
50
+
51
+ /** Opens the manage-mode popover, resolving once its option rows are mounted. */
52
+ async function openPopover() {
53
+ fireEvent.click(screen.getByRole("button", { name: /Resource visibility:/i }));
54
+ await screen.findByRole("option", { name: /^Private/i });
55
+ }
56
+
57
+ describe("VisibilitySelector — create mode (inline list)", () => {
58
+ it("renders every offered level with its label and description", () => {
59
+ render(
60
+ <VisibilitySelector
61
+ mode="create"
62
+ visibility={ApiResourceVisibility.visibility_private}
63
+ options={INSTANCE_VISIBILITY_LEVELS}
64
+ onVisibilityChange={() => {}}
65
+ />,
66
+ );
67
+
68
+ const group = screen.getByRole("radiogroup", { name: "Resource visibility" });
69
+ const radios = within(group).getAllByRole("radio");
70
+ expect(radios).toHaveLength(3);
71
+ expect(within(group).getByText("Private")).toBeTruthy();
72
+ expect(within(group).getByText("Organization")).toBeTruthy();
73
+ expect(within(group).getByText("Public")).toBeTruthy();
74
+ expect(within(group).getByText("Only you can access")).toBeTruthy();
75
+ });
76
+
77
+ it("applies any selection immediately, with no confirmation", () => {
78
+ const onChange = vi.fn();
79
+ render(
80
+ <VisibilitySelector
81
+ mode="create"
82
+ visibility={ApiResourceVisibility.visibility_private}
83
+ options={INSTANCE_VISIBILITY_LEVELS}
84
+ onVisibilityChange={onChange}
85
+ />,
86
+ );
87
+
88
+ // Public is an escalation, but in create mode there is nothing to escalate.
89
+ fireEvent.click(screen.getByRole("radio", { name: /Public/i }));
90
+ expect(onChange).toHaveBeenCalledWith(ApiResourceVisibility.visibility_public);
91
+ expect(screen.queryByRole("alert")).toBeNull();
92
+ });
93
+
94
+ it("renders the current level even when it is not offerable", () => {
95
+ render(
96
+ <VisibilitySelector
97
+ mode="create"
98
+ visibility={ApiResourceVisibility.visibility_platform}
99
+ options={INSTANCE_VISIBILITY_LEVELS}
100
+ onVisibilityChange={() => {}}
101
+ />,
102
+ );
103
+
104
+ const group = screen.getByRole("radiogroup", { name: "Resource visibility" });
105
+ const platformRow = within(group).getByText("Platform").closest("button");
106
+ expect(platformRow).not.toBeNull();
107
+ expect(platformRow?.getAttribute("aria-checked")).toBe("true");
108
+ });
109
+
110
+ it("disables interaction when disabled", () => {
111
+ const onChange = vi.fn();
112
+ render(
113
+ <VisibilitySelector
114
+ mode="create"
115
+ disabled
116
+ visibility={ApiResourceVisibility.visibility_private}
117
+ options={INSTANCE_VISIBILITY_LEVELS}
118
+ onVisibilityChange={onChange}
119
+ />,
120
+ );
121
+ for (const radio of screen.getAllByRole("radio")) {
122
+ expect((radio as HTMLButtonElement).disabled).toBe(true);
123
+ }
124
+ });
125
+ });
126
+
127
+ describe("VisibilitySelector — manage mode (popover + confirmation)", () => {
128
+ it("shows the current level on the trigger and lists levels on open", async () => {
129
+ render(
130
+ <VisibilitySelector
131
+ visibility={ApiResourceVisibility.visibility_org}
132
+ options={BLUEPRINT_LEVELS}
133
+ onVisibilityChange={() => {}}
134
+ />,
135
+ );
136
+
137
+ expect(
138
+ screen.getByRole("button", { name: "Resource visibility: Organization" }),
139
+ ).toBeTruthy();
140
+
141
+ await openPopover();
142
+ expect(screen.getAllByRole("option")).toHaveLength(4);
143
+ });
144
+
145
+ it("applies a de-escalation immediately, without confirmation", async () => {
146
+ const onChange = vi.fn();
147
+ render(
148
+ <VisibilitySelector
149
+ visibility={ApiResourceVisibility.visibility_public}
150
+ options={BLUEPRINT_LEVELS}
151
+ onVisibilityChange={onChange}
152
+ />,
153
+ );
154
+
155
+ await openPopover();
156
+ fireEvent.click(optionByLabel("Private"));
157
+ expect(onChange).toHaveBeenCalledWith(ApiResourceVisibility.visibility_private);
158
+ expect(screen.queryByRole("alert")).toBeNull();
159
+ });
160
+
161
+ it("requires an inline confirm before escalating to Organization", async () => {
162
+ const onChange = vi.fn();
163
+ render(
164
+ <VisibilitySelector
165
+ visibility={ApiResourceVisibility.visibility_private}
166
+ options={BLUEPRINT_LEVELS}
167
+ onVisibilityChange={onChange}
168
+ />,
169
+ );
170
+
171
+ await openPopover();
172
+ fireEvent.click(optionByLabel("Organization"));
173
+
174
+ // Not applied yet — an inline prompt appears first.
175
+ expect(onChange).not.toHaveBeenCalled();
176
+ const alert = await screen.findByRole("alert");
177
+ expect(alert).toBeTruthy();
178
+
179
+ fireEvent.click(within(alert).getByRole("button", { name: "Confirm" }));
180
+ expect(onChange).toHaveBeenCalledWith(ApiResourceVisibility.visibility_org);
181
+ });
182
+
183
+ it("requires the confirm dialog before escalating to Public", async () => {
184
+ const onChange = vi.fn();
185
+ render(
186
+ <VisibilitySelector
187
+ visibility={ApiResourceVisibility.visibility_org}
188
+ options={BLUEPRINT_LEVELS}
189
+ onVisibilityChange={onChange}
190
+ />,
191
+ );
192
+
193
+ await openPopover();
194
+ fireEvent.click(optionByLabel("Public"));
195
+
196
+ // Not applied until the modal is confirmed.
197
+ expect(onChange).not.toHaveBeenCalled();
198
+ expect(await screen.findByText("Make this public?")).toBeTruthy();
199
+
200
+ fireEvent.click(screen.getByRole("button", { name: "Make Public" }));
201
+ // The confirm resolves on a microtask, so the apply is asynchronous.
202
+ await waitFor(() =>
203
+ expect(onChange).toHaveBeenCalledWith(ApiResourceVisibility.visibility_public),
204
+ );
205
+ });
206
+
207
+ it("moves focus between options with the arrow keys", async () => {
208
+ render(
209
+ <VisibilitySelector
210
+ visibility={ApiResourceVisibility.visibility_org}
211
+ options={BLUEPRINT_LEVELS}
212
+ onVisibilityChange={() => {}}
213
+ />,
214
+ );
215
+
216
+ await openPopover();
217
+ // Opening focuses the current level.
218
+ await waitFor(() =>
219
+ expect(document.activeElement).toBe(optionByLabel("Organization")),
220
+ );
221
+
222
+ fireEvent.keyDown(optionByLabel("Organization"), { key: "ArrowDown" });
223
+ expect(document.activeElement).toBe(optionByLabel("Platform"));
224
+
225
+ fireEvent.keyDown(optionByLabel("Platform"), { key: "ArrowUp" });
226
+ expect(document.activeElement).toBe(optionByLabel("Organization"));
227
+ });
228
+
229
+ it("does not apply when the confirm dialog is cancelled", async () => {
230
+ const onChange = vi.fn();
231
+ render(
232
+ <VisibilitySelector
233
+ visibility={ApiResourceVisibility.visibility_org}
234
+ options={BLUEPRINT_LEVELS}
235
+ onVisibilityChange={onChange}
236
+ />,
237
+ );
238
+
239
+ await openPopover();
240
+ fireEvent.click(optionByLabel("Public"));
241
+ await screen.findByText("Make this public?");
242
+
243
+ fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
244
+ await waitFor(() =>
245
+ expect(screen.queryByText("Make this public?")).toBeNull(),
246
+ );
247
+ expect(onChange).not.toHaveBeenCalled();
248
+ });
249
+ });
250
+
251
+ describe("VisibilityBadge", () => {
252
+ it("renders the human label for a visibility value", () => {
253
+ render(<VisibilityBadge visibility={ApiResourceVisibility.visibility_platform} />);
254
+ expect(screen.getByText("Platform")).toBeTruthy();
255
+ });
256
+ });
@@ -68,8 +68,19 @@ export type {
68
68
  export { ImportResourceDialog } from "./ImportResourceDialog";
69
69
  export type { ImportResourceDialogProps } from "./ImportResourceDialog";
70
70
 
71
- export { VisibilityToggle, VisibilityBadge } from "./VisibilityToggle";
72
- export type { VisibilityToggleProps } from "./VisibilityToggle";
71
+ export { VisibilitySelector, VisibilityBadge } from "./VisibilitySelector";
72
+ export type { VisibilitySelectorProps } from "./VisibilitySelector";
73
+
74
+ export {
75
+ blueprintVisibilityLevels,
76
+ INSTANCE_VISIBILITY_LEVELS,
77
+ visibilityLabel,
78
+ visibilityOption,
79
+ } from "./visibilityLevels";
80
+ export type {
81
+ BlueprintVisibilityLevelsContext,
82
+ VisibilityLevelOption,
83
+ } from "./visibilityLevels";
73
84
 
74
85
  export { ResourceVisibilityControl } from "./ResourceVisibilityControl";
75
86
  export type { ResourceVisibilityControlProps } from "./ResourceVisibilityControl";
@@ -34,9 +34,9 @@ export interface UseUpdateVisibilityReturn {
34
34
  /**
35
35
  * Behavior hook that updates the visibility of a resource.
36
36
  *
37
- * Supports blueprints (Agent, Workflow, Skill, MCP Server) with
38
- * private/public visibility, and instances (AgentInstance,
39
- * WorkflowInstance) with the full private/org/public spectrum.
37
+ * Supports blueprints (Agent, Workflow, Skill, MCP Server) with the
38
+ * full private/org/public/platform spectrum, and instances
39
+ * (AgentInstance, WorkflowInstance) with private/org/public.
40
40
  *
41
41
  * Wraps the generated `stigmer.{kind}.updateVisibility()` SDK method
42
42
  * with loading and error state management. The hook is stateless with
@@ -51,8 +51,9 @@ export interface UseUpdateVisibilityReturn {
51
51
  * ```tsx
52
52
  * const { updateVisibility, isPending } = useUpdateVisibility("workflow", workflow.metadata.id);
53
53
  *
54
- * <VisibilityToggle
54
+ * <VisibilitySelector
55
55
  * visibility={workflow.metadata.visibility}
56
+ * options={options}
56
57
  * onVisibilityChange={updateVisibility}
57
58
  * isPending={isPending}
58
59
  * />
@@ -0,0 +1,174 @@
1
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
2
+
3
+ /**
4
+ * A selectable visibility level: label, explanation, and escalation copy.
5
+ *
6
+ * Options are always declared in escalation order (least to most exposed:
7
+ * private < org < platform < public); {@link VisibilitySelector} derives
8
+ * "is this an escalation?" from array position, and shows
9
+ * {@link confirmPrompt} before applying one.
10
+ */
11
+ export interface VisibilityLevelOption {
12
+ readonly value: ApiResourceVisibility;
13
+ readonly label: string;
14
+ /** One-line explanation shown under the selector for the current level. */
15
+ readonly description: string;
16
+ /**
17
+ * Light inline confirmation question shown inside the selector when the
18
+ * user escalates TO this level (e.g. private → org). Omitted for the
19
+ * least-exposed level (de-escalation never confirms — revoking access is
20
+ * always safe).
21
+ *
22
+ * Levels that expand access far beyond the owning org carry a
23
+ * {@link confirmDialog} instead; see the severity ladder on
24
+ * {@link VisibilityLevelOption}.
25
+ */
26
+ readonly confirmPrompt?: string;
27
+ /**
28
+ * Heavy confirmation shown as a modal {@link ConfirmDialog} when the user
29
+ * escalates TO this level. Reserved for levels that expose the resource
30
+ * beyond the owning organization (platform, public), where a blocking,
31
+ * audience-naming confirmation is warranted.
32
+ *
33
+ * The selector derives escalation severity purely from this data:
34
+ * `confirmDialog` present → modal; else `confirmPrompt` present → inline;
35
+ * else apply immediately. There is no per-level branching in the
36
+ * component.
37
+ */
38
+ readonly confirmDialog?: {
39
+ /** Modal title, phrased as a question (e.g. "Make this public?"). */
40
+ readonly title: string;
41
+ /** Body copy that names the exact audience and the consequence. */
42
+ readonly description: string;
43
+ };
44
+ /** Color treatment for the selected segment and the confirmation prompt. */
45
+ readonly tone: "private" | "org" | "platform" | "public";
46
+ }
47
+
48
+ const PRIVATE_OPTION: VisibilityLevelOption = {
49
+ value: ApiResourceVisibility.visibility_private,
50
+ label: "Private",
51
+ description: "Only you can access",
52
+ tone: "private",
53
+ };
54
+
55
+ const ORG_OPTION: VisibilityLevelOption = {
56
+ value: ApiResourceVisibility.visibility_org,
57
+ label: "Organization",
58
+ description: "All members of your organization",
59
+ confirmPrompt: "Make visible to all org members?",
60
+ tone: "org",
61
+ };
62
+
63
+ const PLATFORM_OPTION: VisibilityLevelOption = {
64
+ value: ApiResourceVisibility.visibility_platform,
65
+ label: "Platform",
66
+ description: "All organizations managed by your platform",
67
+ confirmDialog: {
68
+ title: "Share with your whole platform?",
69
+ description:
70
+ "Every organization managed by your platform will be able to view and use this resource. You can return it to a narrower visibility at any time.",
71
+ },
72
+ tone: "platform",
73
+ };
74
+
75
+ const PUBLIC_OPTION: VisibilityLevelOption = {
76
+ value: ApiResourceVisibility.visibility_public,
77
+ label: "Public",
78
+ description: "Anyone on Stigmer",
79
+ confirmDialog: {
80
+ title: "Make this public?",
81
+ description:
82
+ "Anyone signed in to Stigmer will be able to view and use this resource. You can return it to a narrower visibility at any time.",
83
+ },
84
+ tone: "public",
85
+ };
86
+
87
+ /**
88
+ * Inputs that gate which levels a blueprint selector offers.
89
+ *
90
+ * Mirrors the backend's per-kind `VisibilityConfig` plus runtime context the
91
+ * proto cannot know:
92
+ *
93
+ * - `deploymentMode`: the OSS Go backend (`local`) is single-user and
94
+ * performs no org/platform visibility gating, so only Private/Public are
95
+ * meaningful there.
96
+ * - `hasIdentityProvider`: `visibility_platform` requires the owning org to
97
+ * operate an IdentityProvider — the backend rejects it otherwise
98
+ * (`ValidateVisibilityStep`), so the option only renders when the signal
99
+ * is present (use `useSsoProvider`, the permission-free lookup).
100
+ */
101
+ export interface BlueprintVisibilityLevelsContext {
102
+ readonly deploymentMode: "cloud" | "local";
103
+ readonly hasIdentityProvider: boolean;
104
+ }
105
+
106
+ /**
107
+ * The levels a blueprint (agent, skill, workflow, mcp_server) selector
108
+ * offers, in escalation order.
109
+ *
110
+ * Cloud: Private / Organization [/ Platform] / Public — Organization is the
111
+ * creation default (blueprints are shared org assets; Private is an explicit
112
+ * opt-in). Local: Private / Public.
113
+ */
114
+ export function blueprintVisibilityLevels(
115
+ context: BlueprintVisibilityLevelsContext,
116
+ ): readonly VisibilityLevelOption[] {
117
+ if (context.deploymentMode === "local") {
118
+ return [PRIVATE_OPTION, PUBLIC_OPTION];
119
+ }
120
+ return context.hasIdentityProvider
121
+ ? [PRIVATE_OPTION, ORG_OPTION, PLATFORM_OPTION, PUBLIC_OPTION]
122
+ : [PRIVATE_OPTION, ORG_OPTION, PUBLIC_OPTION];
123
+ }
124
+
125
+ /**
126
+ * The levels an instance (agent_instance, workflow_instance) selector
127
+ * offers, in escalation order: Private / Organization / Public.
128
+ *
129
+ * Platform is deliberately absent — instances are tenant-isolated by
130
+ * design (each managed org instantiates shared blueprints inside its own
131
+ * boundary). Descriptions are execution-oriented because org visibility on
132
+ * an instance is about who can run it and see its executions.
133
+ */
134
+ export const INSTANCE_VISIBILITY_LEVELS: readonly VisibilityLevelOption[] = [
135
+ PRIVATE_OPTION,
136
+ {
137
+ ...ORG_OPTION,
138
+ description: "All org members can view executions",
139
+ },
140
+ {
141
+ ...PUBLIC_OPTION,
142
+ description: "All authenticated users can view",
143
+ },
144
+ ];
145
+
146
+ /**
147
+ * Canonical option for a visibility value, independent of any kind's offered
148
+ * list. Used to render the current level even when it is not offerable in
149
+ * the current context (e.g. a platform-shared blueprint whose org no longer
150
+ * operates an IdentityProvider) — the state must stay legible.
151
+ */
152
+ export function visibilityOption(
153
+ visibility: ApiResourceVisibility,
154
+ ): VisibilityLevelOption {
155
+ switch (visibility) {
156
+ case ApiResourceVisibility.visibility_org:
157
+ return ORG_OPTION;
158
+ case ApiResourceVisibility.visibility_platform:
159
+ return PLATFORM_OPTION;
160
+ case ApiResourceVisibility.visibility_public:
161
+ return PUBLIC_OPTION;
162
+ default:
163
+ return PRIVATE_OPTION;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Human label for a visibility value — the one place list rows, badges, and
169
+ * detail panels resolve enum-to-text, so no surface ever falls through to
170
+ * "Private" (or "unknown") for org/platform.
171
+ */
172
+ export function visibilityLabel(visibility: ApiResourceVisibility): string {
173
+ return visibilityOption(visibility).label;
174
+ }
@@ -13,7 +13,6 @@ import type {
13
13
  import type { ToolApprovalPolicy, McpServerSpec } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
14
14
  import { ValidationState } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
15
15
  import type { EnvVarDeclaration } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
16
- import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
17
16
  import { useMcpServer } from "./useMcpServer";
18
17
  import { useUpdateMcpServer } from "./useUpdateMcpServer";
19
18
  import { mcpServerToInput } from "./internal/mcpServerToInput";
@@ -27,8 +26,7 @@ import { OAuthAppForm } from "./OAuthAppForm";
27
26
  import { ErrorMessage } from "../error/ErrorMessage";
28
27
  import { EnvVarForm } from "../environment/EnvVarForm";
29
28
  import type { EnvVarFormVariable } from "../environment/EnvVarForm";
30
- import { VisibilityToggle } from "../library/VisibilityToggle";
31
- import { PermissionGate } from "../iam-policy/PermissionGate";
29
+ import { ResourceVisibilityControl } from "../library/ResourceVisibilityControl";
32
30
  import { Tabs, type TabItem } from "../tabs/Tabs";
33
31
  import { ResourceDetailShell } from "../resource-detail/ResourceDetailShell";
34
32
  import { Section } from "../resource-detail/Section";
@@ -58,15 +56,6 @@ export interface McpServerDetailViewProps {
58
56
  * Not called on error or not-found states.
59
57
  */
60
58
  readonly onResourceLoad?: (meta: { name: string; id: string }) => void;
61
- /**
62
- * Called when the user toggles visibility via the inline control.
63
- * When provided, the header renders an interactive
64
- * {@link VisibilityToggle} instead of a read-only badge.
65
- * When omitted, visibility is displayed as a static "Public" pill.
66
- */
67
- readonly onVisibilityChange?: (v: ApiResourceVisibility) => void;
68
- /** `true` while a visibility update RPC is in flight. */
69
- readonly isVisibilityPending?: boolean;
70
59
  /**
71
60
  * Initial active capability tab. Defaults to `"tools"`.
72
61
  * Useful for deep-linking or demo scenarios that need to start on
@@ -147,8 +136,6 @@ export function McpServerDetailView({
147
136
  org,
148
137
  slug,
149
138
  onResourceLoad,
150
- onVisibilityChange,
151
- isVisibilityPending,
152
139
  defaultCapabilityTab = "tools",
153
140
  defaultShowCredentialForm = false,
154
141
  credentialPoolValues,
@@ -393,27 +380,15 @@ export function McpServerDetailView({
393
380
  updatedAt: specAudit?.updatedAt ? timestampDate(specAudit.updatedAt) : null,
394
381
  };
395
382
 
396
- const visibilityBadge =
397
- meta?.visibility === ApiResourceVisibility.visibility_public ? (
398
- <span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
399
- Public
400
- </span>
401
- ) : undefined;
402
-
403
- const visibilityControl =
404
- onVisibilityChange && meta ? (
405
- <PermissionGate
406
- resource={{ kind: "mcp_server", id: meta.id }}
407
- relation="can_edit"
408
- fallback={visibilityBadge}
409
- >
410
- <VisibilityToggle
411
- visibility={meta.visibility}
412
- onVisibilityChange={onVisibilityChange}
413
- isPending={isVisibilityPending}
414
- />
415
- </PermissionGate>
416
- ) : visibilityBadge;
383
+ const visibilityControl = meta ? (
384
+ <ResourceVisibilityControl
385
+ kind="mcpServer"
386
+ resourceId={meta.id}
387
+ visibility={meta.visibility}
388
+ org={meta.org || org}
389
+ onChanged={refetch}
390
+ />
391
+ ) : undefined;
417
392
 
418
393
  const headerMetaExtra = (
419
394
  <>
@@ -151,7 +151,7 @@ export interface ResourceDetailShellProps {
151
151
 
152
152
  /**
153
153
  * Visibility control rendered in the header.
154
- * Typically a `<VisibilityToggle />` from the library module.
154
+ * Typically a `<ResourceVisibilityControl />` from the library module.
155
155
  * Rendered inline after the resource name.
156
156
  */
157
157
  readonly visibilityControl?: ReactNode;
@@ -7,10 +7,13 @@ import type { UseGitHubConnectionReturn } from "../github/useGitHubConnection";
7
7
  import type { WorkspaceFileLister } from "../workspace/WorkspaceFileLister";
8
8
  import type { InteractionModeOption } from "../composer";
9
9
  import { SessionComposer } from "../composer";
10
+ import type { HarnessOption } from "../models/harness";
10
11
  import { ResizableSplit } from "../internal/ResizableSplit";
11
12
  import { SessionInspector } from "./inspector/SessionInspector";
12
13
  import type { SetupTabProps } from "./inspector/SetupTab";
13
14
  import { useNewSessionFlow } from "./useNewSessionFlow";
15
+ import type { RuntimeEnvProvider } from "./runtime-env";
16
+ import type { SessionAudience } from "./audience";
14
17
 
15
18
  /** Props for {@link NewSessionViewer}. */
16
19
  export interface NewSessionViewerProps {
@@ -44,6 +47,37 @@ export interface NewSessionViewerProps {
44
47
  */
45
48
  readonly workspaceFileLister?: WorkspaceFileLister;
46
49
 
50
+ /**
51
+ * Supplies host-app environment variables for the session's first
52
+ * execution (e.g. short-lived credentials for MCP tools, minted as
53
+ * the signed-in user). Evaluated at submit time, before the session
54
+ * is created; host values win over composer-collected env on key
55
+ * collisions. If the provider throws, the submission fails with an
56
+ * error surfaced via `onError` — see {@link RuntimeEnvProvider}.
57
+ */
58
+ readonly getRuntimeEnv?: RuntimeEnvProvider;
59
+
60
+ /**
61
+ * Presentation audience for the launcher. `"endUser"` locks the
62
+ * pinned agent (when `initialAgentRef` is set) and hides the MCP
63
+ * server, skill, and session-variable pickers — for product-embedded
64
+ * chat where the agent is configured upstream by the platform. The
65
+ * model selector, interaction mode, harness selector, attachments,
66
+ * and workspace picker remain. See {@link SessionAudience}.
67
+ *
68
+ * @default "integrator"
69
+ */
70
+ readonly audience?: SessionAudience;
71
+
72
+ /**
73
+ * Harness pre-selected for new sessions when the user has not made
74
+ * an explicit choice yet. The user can still switch before starting
75
+ * the session, and their explicit choice wins on subsequent visits.
76
+ *
77
+ * @default "native"
78
+ */
79
+ readonly defaultHarness?: HarnessOption;
80
+
47
81
  /** Agent to auto-select on mount (used for draft flows). */
48
82
  readonly initialAgentRef?: ResourceRef;
49
83
  /**
@@ -122,6 +156,9 @@ export function NewSessionViewer({
122
156
  enableLocal = false,
123
157
  onBrowseLocalFolder,
124
158
  workspaceFileLister,
159
+ getRuntimeEnv,
160
+ audience = "integrator",
161
+ defaultHarness,
125
162
  initialAgentRef,
126
163
  initialInstanceId,
127
164
  initialAttachments,
@@ -132,8 +169,15 @@ export function NewSessionViewer({
132
169
  footerContent,
133
170
  className,
134
171
  }: NewSessionViewerProps) {
135
- const flow = useNewSessionFlow({ org, onSessionCreated, onError });
172
+ const flow = useNewSessionFlow({
173
+ org,
174
+ onSessionCreated,
175
+ onError,
176
+ getRuntimeEnv,
177
+ defaultHarness,
178
+ });
136
179
  const [interactionMode, setInteractionMode] = useState<InteractionModeOption>("agent");
180
+ const isEndUser = audience === "endUser";
137
181
 
138
182
  const hasContext =
139
183
  flow.workspace.hasEntries ||
@@ -179,16 +223,20 @@ export function NewSessionViewer({
179
223
  harness: flow.harness,
180
224
  executionTarget: undefined,
181
225
  modelId: flow.modelId,
182
- mutations: {
183
- onRemoveAgent: flow.agentRef ? handleRemoveAgent : undefined,
184
- onRemoveMcp: handleRemoveMcp,
185
- onRemoveSkill: handleRemoveSkill,
186
- },
226
+ // End users see the configuration but cannot strip it — the Setup
227
+ // tab renders read-only without mutation callbacks (DD-011).
228
+ mutations: isEndUser
229
+ ? undefined
230
+ : {
231
+ onRemoveAgent: flow.agentRef ? handleRemoveAgent : undefined,
232
+ onRemoveMcp: handleRemoveMcp,
233
+ onRemoveSkill: handleRemoveSkill,
234
+ },
187
235
  }),
188
236
  [
189
237
  flow.agentRef, flow.mcpServerUsages, flow.skillRefs,
190
238
  flow.sessionVariables, flow.harness, flow.modelId,
191
- handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
239
+ isEndUser, handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
192
240
  ],
193
241
  );
194
242
 
@@ -231,11 +279,12 @@ export function NewSessionViewer({
231
279
  initialAgentRef={initialAgentRef}
232
280
  initialInstanceId={initialInstanceId}
233
281
  initialAttachments={initialAttachments}
234
- mcpServerUsages={flow.mcpServerUsages}
235
- onMcpServerUsagesChange={flow.setMcpServerUsages}
236
- skillRefs={flow.skillRefs}
237
- onSkillRefsChange={flow.setSkillRefs}
238
- sessionVariables={flow.sessionVariables}
282
+ lockAgent={isEndUser && initialAgentRef != null}
283
+ mcpServerUsages={isEndUser ? undefined : flow.mcpServerUsages}
284
+ onMcpServerUsagesChange={isEndUser ? undefined : flow.setMcpServerUsages}
285
+ skillRefs={isEndUser ? undefined : flow.skillRefs}
286
+ onSkillRefsChange={isEndUser ? undefined : flow.setSkillRefs}
287
+ sessionVariables={isEndUser ? undefined : flow.sessionVariables}
239
288
  showHarnessSelector
240
289
  harness={flow.harness}
241
290
  onHarnessChange={flow.setHarness}