@stigmer/react 3.0.6 → 3.0.7

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 (94) hide show
  1. package/agent-instance/AgentInstanceDetailPanel.d.ts.map +1 -1
  2. package/agent-instance/AgentInstanceDetailPanel.js +2 -9
  3. package/agent-instance/AgentInstanceDetailPanel.js.map +1 -1
  4. package/agent-instance/AgentInstanceList.d.ts.map +1 -1
  5. package/agent-instance/AgentInstanceList.js +2 -9
  6. package/agent-instance/AgentInstanceList.js.map +1 -1
  7. package/agent-instance/CreateAgentInstanceDialog.d.ts.map +1 -1
  8. package/agent-instance/CreateAgentInstanceDialog.js +1 -1
  9. package/agent-instance/CreateAgentInstanceDialog.js.map +1 -1
  10. package/composer/SessionComposer.d.ts +14 -0
  11. package/composer/SessionComposer.d.ts.map +1 -1
  12. package/composer/SessionComposer.js +15 -9
  13. package/composer/SessionComposer.js.map +1 -1
  14. package/index.d.ts +1 -1
  15. package/index.d.ts.map +1 -1
  16. package/index.js.map +1 -1
  17. package/library/InstanceVisibilitySelector.d.ts +23 -9
  18. package/library/InstanceVisibilitySelector.d.ts.map +1 -1
  19. package/library/InstanceVisibilitySelector.js +14 -9
  20. package/library/InstanceVisibilitySelector.js.map +1 -1
  21. package/library/VisibilityOptionRow.d.ts +52 -0
  22. package/library/VisibilityOptionRow.d.ts.map +1 -0
  23. package/library/VisibilityOptionRow.js +92 -0
  24. package/library/VisibilityOptionRow.js.map +1 -0
  25. package/library/VisibilitySelector.d.ts +47 -24
  26. package/library/VisibilitySelector.d.ts.map +1 -1
  27. package/library/VisibilitySelector.js +137 -115
  28. package/library/VisibilitySelector.js.map +1 -1
  29. package/library/visibilityLevels.d.ts +25 -3
  30. package/library/visibilityLevels.d.ts.map +1 -1
  31. package/library/visibilityLevels.js +8 -2
  32. package/library/visibilityLevels.js.map +1 -1
  33. package/package.json +4 -4
  34. package/session/NewSessionViewer.d.ts +32 -1
  35. package/session/NewSessionViewer.d.ts.map +1 -1
  36. package/session/NewSessionViewer.js +20 -9
  37. package/session/NewSessionViewer.js.map +1 -1
  38. package/session/SessionViewer.d.ts +24 -1
  39. package/session/SessionViewer.d.ts.map +1 -1
  40. package/session/SessionViewer.js +18 -12
  41. package/session/SessionViewer.js.map +1 -1
  42. package/session/audience.d.ts +21 -0
  43. package/session/audience.d.ts.map +1 -0
  44. package/session/audience.js +2 -0
  45. package/session/audience.js.map +1 -0
  46. package/session/index.d.ts +2 -0
  47. package/session/index.d.ts.map +1 -1
  48. package/session/index.js.map +1 -1
  49. package/session/runtime-env.d.ts +47 -0
  50. package/session/runtime-env.d.ts.map +1 -0
  51. package/session/runtime-env.js +20 -0
  52. package/session/runtime-env.js.map +1 -0
  53. package/session/useNewSessionFlow.d.ts +25 -0
  54. package/session/useNewSessionFlow.d.ts.map +1 -1
  55. package/session/useNewSessionFlow.js +20 -8
  56. package/session/useNewSessionFlow.js.map +1 -1
  57. package/session/useSessionPageFlow.d.ts +27 -2
  58. package/session/useSessionPageFlow.d.ts.map +1 -1
  59. package/session/useSessionPageFlow.js +34 -13
  60. package/session/useSessionPageFlow.js.map +1 -1
  61. package/src/agent-instance/AgentInstanceDetailPanel.tsx +7 -27
  62. package/src/agent-instance/AgentInstanceList.tsx +7 -27
  63. package/src/agent-instance/CreateAgentInstanceDialog.tsx +1 -0
  64. package/src/composer/SessionComposer.tsx +30 -8
  65. package/src/composer/__tests__/SessionComposer-lockAgent.test.tsx +150 -0
  66. package/src/index.ts +2 -0
  67. package/src/library/InstanceVisibilitySelector.tsx +27 -9
  68. package/src/library/VisibilityOptionRow.tsx +244 -0
  69. package/src/library/VisibilitySelector.tsx +303 -260
  70. package/src/library/__tests__/VisibilitySelector.test.tsx +256 -0
  71. package/src/library/visibilityLevels.ts +35 -5
  72. package/src/session/NewSessionViewer.tsx +61 -12
  73. package/src/session/SessionViewer.tsx +51 -15
  74. package/src/session/__tests__/audienceWiring.test.tsx +274 -0
  75. package/src/session/__tests__/useNewSessionFlow.test.tsx +122 -0
  76. package/src/session/__tests__/useSessionPageFlow.runtimeEnv.test.tsx +170 -0
  77. package/src/session/audience.ts +20 -0
  78. package/src/session/index.ts +3 -0
  79. package/src/session/runtime-env.ts +57 -0
  80. package/src/session/useNewSessionFlow.ts +44 -9
  81. package/src/session/useSessionPageFlow.ts +65 -17
  82. package/src/workflow/instance/CreateWorkflowInstanceDialog.tsx +1 -0
  83. package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +7 -27
  84. package/src/workflow/instance/WorkflowInstanceList.tsx +7 -27
  85. package/styles.css +1 -1
  86. package/workflow/instance/CreateWorkflowInstanceDialog.d.ts.map +1 -1
  87. package/workflow/instance/CreateWorkflowInstanceDialog.js +1 -1
  88. package/workflow/instance/CreateWorkflowInstanceDialog.js.map +1 -1
  89. package/workflow/instance/WorkflowInstanceDetailPanel.d.ts.map +1 -1
  90. package/workflow/instance/WorkflowInstanceDetailPanel.js +2 -9
  91. package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
  92. package/workflow/instance/WorkflowInstanceList.d.ts.map +1 -1
  93. package/workflow/instance/WorkflowInstanceList.js +2 -9
  94. package/workflow/instance/WorkflowInstanceList.js.map +1 -1
@@ -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
+ });
@@ -14,11 +14,33 @@ export interface VisibilityLevelOption {
14
14
  /** One-line explanation shown under the selector for the current level. */
15
15
  readonly description: string;
16
16
  /**
17
- * Inline confirmation question shown when the user escalates TO this
18
- * level. Omitted for the least-exposed level (de-escalation never
19
- * confirms — revoking access is always safe).
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}.
20
25
  */
21
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
+ };
22
44
  /** Color treatment for the selected segment and the confirmation prompt. */
23
45
  readonly tone: "private" | "org" | "platform" | "public";
24
46
  }
@@ -42,7 +64,11 @@ const PLATFORM_OPTION: VisibilityLevelOption = {
42
64
  value: ApiResourceVisibility.visibility_platform,
43
65
  label: "Platform",
44
66
  description: "All organizations managed by your platform",
45
- confirmPrompt: "Share with every organization 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
+ },
46
72
  tone: "platform",
47
73
  };
48
74
 
@@ -50,7 +76,11 @@ const PUBLIC_OPTION: VisibilityLevelOption = {
50
76
  value: ApiResourceVisibility.visibility_public,
51
77
  label: "Public",
52
78
  description: "Anyone on Stigmer",
53
- confirmPrompt: "Make visible to all authenticated users?",
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
+ },
54
84
  tone: "public",
55
85
  };
56
86
 
@@ -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}
@@ -17,6 +17,8 @@ import { SessionComposer } from "../composer";
17
17
  import { SecretFlowErrorGuide, isSecretFlowError } from "../error";
18
18
  import { useSessionPageFlow } from "./useSessionPageFlow";
19
19
  import { SessionInspector } from "./inspector/SessionInspector";
20
+ import type { RuntimeEnvProvider } from "./runtime-env";
21
+ import type { SessionAudience } from "./audience";
20
22
 
21
23
  /**
22
24
  * Message submitted when the user implements a plan. References the published
@@ -67,6 +69,27 @@ export interface SessionViewerProps {
67
69
  * expandable file tree. (DD-004 capability injection, DD-011 opt-in.)
68
70
  */
69
71
  readonly workspaceFileLister?: WorkspaceFileLister;
72
+ /**
73
+ * Supplies host-app environment variables for every follow-up
74
+ * execution (e.g. short-lived credentials for MCP tools, minted as
75
+ * the signed-in user). Evaluated fresh at each send; host values win
76
+ * over composer-collected env on key collisions. If the provider
77
+ * throws, the send is blocked and an error banner is shown — see
78
+ * {@link RuntimeEnvProvider}.
79
+ */
80
+ readonly getRuntimeEnv?: RuntimeEnvProvider;
81
+ /**
82
+ * Presentation audience for the viewer. `"endUser"` locks the
83
+ * session's agent and hides the MCP server, skill, and
84
+ * session-variable configuration in both the composer and the
85
+ * inspector's Setup tab — for product-embedded chat where the agent
86
+ * is configured upstream by the platform. The model selector,
87
+ * interaction mode, attachments, and workspace picker remain. See
88
+ * {@link SessionAudience}.
89
+ *
90
+ * @default "integrator"
91
+ */
92
+ readonly audience?: SessionAudience;
70
93
  /**
71
94
  * Slot for host-injected header actions (e.g., Share button with
72
95
  * PermissionGate). Rendered in the top-right corner of the viewer.
@@ -126,12 +149,15 @@ export function SessionViewer({
126
149
  enableLocal = false,
127
150
  onBrowseLocalFolder,
128
151
  workspaceFileLister,
152
+ getRuntimeEnv,
153
+ audience = "integrator",
129
154
  headerActions,
130
155
  onApplied,
131
156
  className,
132
157
  }: SessionViewerProps) {
133
- const flow = useSessionPageFlow({ sessionId, org });
158
+ const flow = useSessionPageFlow({ sessionId, org, getRuntimeEnv });
134
159
  const { conv } = flow;
160
+ const isEndUser = audience === "endUser";
135
161
 
136
162
  const [modelId, setModelId] = flow.model;
137
163
  const [interactionMode, setInteractionMode] = flow.interactionMode;
@@ -202,6 +228,7 @@ export function SessionViewer({
202
228
  enableLocal={enableLocal}
203
229
  onBrowseLocalFolder={onBrowseLocalFolder}
204
230
  onBuildFromPlan={handleBuildFromPlan}
231
+ isEndUser={isEndUser}
205
232
  />
206
233
  }
207
234
  secondary={
@@ -216,6 +243,7 @@ export function SessionViewer({
216
243
  gitHubConnection={gitHubConnection}
217
244
  onBrowseLocalFolder={onBrowseLocalFolder}
218
245
  workspaceFileLister={workspaceFileLister}
246
+ isEndUser={isEndUser}
219
247
  />
220
248
  }
221
249
  />
@@ -241,6 +269,7 @@ interface ConversationColumnProps {
241
269
  readonly enableLocal: boolean;
242
270
  readonly onBrowseLocalFolder?: () => Promise<string | null>;
243
271
  readonly onBuildFromPlan: () => void;
272
+ readonly isEndUser: boolean;
244
273
  }
245
274
 
246
275
  function ConversationColumn({
@@ -256,8 +285,10 @@ function ConversationColumn({
256
285
  enableLocal,
257
286
  onBrowseLocalFolder,
258
287
  onBuildFromPlan,
288
+ isEndUser,
259
289
  }: ConversationColumnProps) {
260
290
  const { conv } = flow;
291
+ const sendError = flow.submitError ?? conv.sendError ?? conv.approvalError;
261
292
 
262
293
  return (
263
294
  <div className="flex h-full min-w-0 flex-col">
@@ -282,9 +313,7 @@ function ConversationColumn({
282
313
  onReconnect={conv.reconnectStream}
283
314
  />
284
315
  )}
285
- {(conv.sendError || conv.approvalError) && (
286
- <SendErrorBanner error={(conv.sendError ?? conv.approvalError)!} />
287
- )}
316
+ {sendError && <SendErrorBanner error={sendError} />}
288
317
  {flow.autoApproveAll && (
289
318
  <AutoApproveIndicator onTurnOff={() => flow.setAutoApproveAll(false)} />
290
319
  )}
@@ -309,11 +338,12 @@ function ConversationColumn({
309
338
  onAgentRefChange={flow.setAgentRef}
310
339
  onAgentResolutionChange={flow.setResolution}
311
340
  isDefaultAgent={flow.isDefaultAgent}
312
- mcpServerUsages={flow.mcpServerUsages}
313
- onMcpServerUsagesChange={flow.setMcpServerUsages}
314
- skillRefs={flow.skillRefs}
315
- onSkillRefsChange={flow.setSkillRefs}
316
- sessionVariables={flow.sessionVariables}
341
+ lockAgent={isEndUser}
342
+ mcpServerUsages={isEndUser ? undefined : flow.mcpServerUsages}
343
+ onMcpServerUsagesChange={isEndUser ? undefined : flow.setMcpServerUsages}
344
+ skillRefs={isEndUser ? undefined : flow.skillRefs}
345
+ onSkillRefsChange={isEndUser ? undefined : flow.setSkillRefs}
346
+ sessionVariables={isEndUser ? undefined : flow.sessionVariables}
317
347
  className="px-4 py-3"
318
348
  />
319
349
  </div>
@@ -337,6 +367,7 @@ interface InspectorPanelProps {
337
367
  readonly gitHubConnection?: UseGitHubConnectionReturn;
338
368
  readonly onBrowseLocalFolder?: () => Promise<string | null>;
339
369
  readonly workspaceFileLister?: WorkspaceFileLister;
370
+ readonly isEndUser: boolean;
340
371
  }
341
372
 
342
373
  function InspectorPanel({
@@ -350,6 +381,7 @@ function InspectorPanel({
350
381
  gitHubConnection,
351
382
  onBrowseLocalFolder,
352
383
  workspaceFileLister,
384
+ isEndUser,
353
385
  }: InspectorPanelProps) {
354
386
  const selectedItem = useSelectedThreadItem();
355
387
 
@@ -390,16 +422,20 @@ function InspectorPanel({
390
422
  harness: flow.harness,
391
423
  executionTarget: flow.executionTarget,
392
424
  modelId: flow.model[0],
393
- mutations: {
394
- onRemoveAgent: flow.isDefaultAgent ? undefined : handleRemoveAgent,
395
- onRemoveMcp: handleRemoveMcp,
396
- onRemoveSkill: handleRemoveSkill,
397
- },
425
+ // End users see the configuration but cannot strip it — the Setup
426
+ // tab renders read-only without mutation callbacks (DD-011).
427
+ mutations: isEndUser
428
+ ? undefined
429
+ : {
430
+ onRemoveAgent: flow.isDefaultAgent ? undefined : handleRemoveAgent,
431
+ onRemoveMcp: handleRemoveMcp,
432
+ onRemoveSkill: handleRemoveSkill,
433
+ },
398
434
  }),
399
435
  [
400
436
  flow.agentRef, flow.isDefaultAgent, flow.mcpServerUsages, flow.skillRefs,
401
437
  flow.sessionVariables, flow.harness, flow.executionTarget, flow.model,
402
- handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
438
+ isEndUser, handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
403
439
  ],
404
440
  );
405
441