@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
@@ -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
 
@@ -0,0 +1,274 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { render, screen, cleanup } from "@testing-library/react";
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Wiring-contract tests for the `audience` preset on the session organisms.
6
+ //
7
+ // The composer and inspector are replaced with prop-capturing probes so the
8
+ // tests assert exactly what `audience="endUser"` maps to: a locked agent,
9
+ // unwired MCP/skill/session-variable pickers, and a read-only Setup tab.
10
+ // The molecules' own behavior is covered by their co-located tests.
11
+ // ---------------------------------------------------------------------------
12
+
13
+ type CapturedProps = Record<string, unknown>;
14
+
15
+ const composerProps: CapturedProps[] = [];
16
+ vi.mock("../../composer", async (importOriginal) => {
17
+ const actual = await importOriginal<typeof import("../../composer")>();
18
+ return {
19
+ ...actual,
20
+ SessionComposer: (props: CapturedProps) => {
21
+ composerProps.push(props);
22
+ return <div data-testid="composer-probe" />;
23
+ },
24
+ };
25
+ });
26
+
27
+ const inspectorProps: CapturedProps[] = [];
28
+ vi.mock("../inspector/SessionInspector", () => ({
29
+ SessionInspector: (props: CapturedProps) => {
30
+ inspectorProps.push(props);
31
+ return <div data-testid="inspector-probe" />;
32
+ },
33
+ }));
34
+
35
+ vi.mock("../../execution/MessageThread", () => ({
36
+ MessageThread: () => <div data-testid="thread-probe" />,
37
+ }));
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Flow stubs — the organisms own these hooks; stub them to a loaded state.
41
+ // ---------------------------------------------------------------------------
42
+
43
+ const stubWorkspace = {
44
+ entries: [],
45
+ hasEntries: false,
46
+ toInput: vi.fn().mockReturnValue([]),
47
+ addGitRepo: vi.fn(),
48
+ addLocalPath: vi.fn(),
49
+ removeEntry: vi.fn(),
50
+ clear: vi.fn(),
51
+ };
52
+
53
+ const stubSessionVariables = { entries: [], isEmpty: true, clear: vi.fn() };
54
+
55
+ const stubNewSessionFlow = {
56
+ harness: "native" as const,
57
+ setHarness: vi.fn(),
58
+ modelId: undefined,
59
+ setModelId: vi.fn(),
60
+ agentRef: { org: "acme", slug: "support-bot" },
61
+ setAgentRef: vi.fn(),
62
+ resolution: null,
63
+ setResolution: vi.fn(),
64
+ mcpServerUsages: [],
65
+ setMcpServerUsages: vi.fn(),
66
+ skillRefs: [],
67
+ setSkillRefs: vi.fn(),
68
+ workspace: stubWorkspace,
69
+ sessionVariables: stubSessionVariables,
70
+ isSubmitting: false,
71
+ submitError: null,
72
+ submit: vi.fn(),
73
+ };
74
+ const mockUseNewSessionFlow = vi.fn((_options: unknown) => stubNewSessionFlow);
75
+ vi.mock("../useNewSessionFlow", () => ({
76
+ useNewSessionFlow: (options: unknown) => mockUseNewSessionFlow(options),
77
+ }));
78
+
79
+ const stubConv = {
80
+ session: { spec: {} },
81
+ isLoading: false,
82
+ loadError: null,
83
+ completedExecutions: [],
84
+ activeStreamExecution: null,
85
+ activePhase: null,
86
+ isStreaming: false,
87
+ isConnecting: false,
88
+ sendFollowUp: vi.fn(),
89
+ canSendFollowUp: true,
90
+ isSending: false,
91
+ sendError: null,
92
+ clearSendError: vi.fn(),
93
+ pendingUserMessage: null,
94
+ workspaceEntries: [],
95
+ mcpServerUsages: [],
96
+ skillRefs: [],
97
+ pendingApprovals: [],
98
+ submitApproval: vi.fn(),
99
+ submittingApprovalIds: new Set<string>(),
100
+ streamError: null,
101
+ reconnectStream: vi.fn(),
102
+ approvalError: null,
103
+ };
104
+
105
+ const stubSessionPageFlow = {
106
+ conv: stubConv,
107
+ harness: "native" as const,
108
+ executionTarget: undefined,
109
+ model: [undefined, vi.fn()] as const,
110
+ interactionMode: ["agent" as const, vi.fn()] as const,
111
+ agentRef: { org: "acme", slug: "support-bot" },
112
+ setAgentRef: vi.fn(),
113
+ resolution: null,
114
+ setResolution: vi.fn(),
115
+ isDefaultAgent: false,
116
+ mcpServerUsages: [],
117
+ setMcpServerUsages: vi.fn(),
118
+ skillRefs: [],
119
+ setSkillRefs: vi.fn(),
120
+ workspace: stubWorkspace,
121
+ sessionVariables: stubSessionVariables,
122
+ autoApproveAll: false,
123
+ setAutoApproveAll: vi.fn(),
124
+ submitApproval: vi.fn(),
125
+ handleSubmit: vi.fn(),
126
+ submitError: null as Error | null,
127
+ displayExecution: null,
128
+ allExecutions: [],
129
+ sandboxWorkspaceRoot: undefined,
130
+ };
131
+ const mockUseSessionPageFlow = vi.fn((_options: unknown) => stubSessionPageFlow);
132
+ vi.mock("../useSessionPageFlow", () => ({
133
+ useSessionPageFlow: (options: unknown) => mockUseSessionPageFlow(options),
134
+ }));
135
+
136
+ import { NewSessionViewer } from "../NewSessionViewer";
137
+ import { SessionViewer } from "../SessionViewer";
138
+
139
+ const AGENT_REF = { org: "acme", slug: "support-bot" };
140
+
141
+ function lastComposerProps(): CapturedProps {
142
+ expect(composerProps.length).toBeGreaterThan(0);
143
+ return composerProps.at(-1)!;
144
+ }
145
+
146
+ function lastSessionConfig(): { mutations?: unknown } {
147
+ expect(inspectorProps.length).toBeGreaterThan(0);
148
+ return inspectorProps.at(-1)!.sessionConfig as { mutations?: unknown };
149
+ }
150
+
151
+ beforeEach(() => {
152
+ composerProps.length = 0;
153
+ inspectorProps.length = 0;
154
+ stubSessionPageFlow.submitError = null;
155
+ });
156
+
157
+ afterEach(() => {
158
+ cleanup();
159
+ vi.clearAllMocks();
160
+ });
161
+
162
+ describe("NewSessionViewer — audience wiring", () => {
163
+ it("integrator (default): full configuration surface, agent unlocked", () => {
164
+ render(
165
+ <NewSessionViewer
166
+ org="acme"
167
+ onSessionCreated={vi.fn()}
168
+ initialAgentRef={AGENT_REF}
169
+ />,
170
+ );
171
+
172
+ const props = lastComposerProps();
173
+ expect(props.lockAgent).toBe(false);
174
+ expect(props.onMcpServerUsagesChange).toBeDefined();
175
+ expect(props.onSkillRefsChange).toBeDefined();
176
+ expect(props.sessionVariables).toBeDefined();
177
+ expect(lastSessionConfig().mutations).toBeDefined();
178
+ });
179
+
180
+ it("endUser with a pinned agent: locked agent, integrator pickers unwired", () => {
181
+ render(
182
+ <NewSessionViewer
183
+ org="acme"
184
+ onSessionCreated={vi.fn()}
185
+ audience="endUser"
186
+ initialAgentRef={AGENT_REF}
187
+ />,
188
+ );
189
+
190
+ const props = lastComposerProps();
191
+ expect(props.lockAgent).toBe(true);
192
+ expect(props.onMcpServerUsagesChange).toBeUndefined();
193
+ expect(props.onSkillRefsChange).toBeUndefined();
194
+ expect(props.sessionVariables).toBeUndefined();
195
+ // End-user controls survive the curation.
196
+ expect(props.showHarnessSelector).toBe(true);
197
+ expect(props.showInteractionModePicker).toBe(true);
198
+ // Setup tab is read-only: no remove affordances for pinned config.
199
+ expect(lastSessionConfig().mutations).toBeUndefined();
200
+ });
201
+
202
+ it("endUser without a pinned agent: pickers hidden but agent not locked", () => {
203
+ render(
204
+ <NewSessionViewer
205
+ org="acme"
206
+ onSessionCreated={vi.fn()}
207
+ audience="endUser"
208
+ />,
209
+ );
210
+
211
+ expect(lastComposerProps().lockAgent).toBe(false);
212
+ });
213
+
214
+ it("forwards getRuntimeEnv and defaultHarness to the flow", () => {
215
+ const getRuntimeEnv = vi.fn();
216
+ render(
217
+ <NewSessionViewer
218
+ org="acme"
219
+ onSessionCreated={vi.fn()}
220
+ getRuntimeEnv={getRuntimeEnv}
221
+ defaultHarness="cursor"
222
+ />,
223
+ );
224
+
225
+ expect(mockUseNewSessionFlow).toHaveBeenCalledWith(
226
+ expect.objectContaining({ getRuntimeEnv, defaultHarness: "cursor" }),
227
+ );
228
+ });
229
+ });
230
+
231
+ describe("SessionViewer — audience wiring", () => {
232
+ it("integrator (default): full configuration surface, agent unlocked", () => {
233
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
234
+
235
+ const props = lastComposerProps();
236
+ expect(props.lockAgent).toBe(false);
237
+ expect(props.onMcpServerUsagesChange).toBeDefined();
238
+ expect(props.onSkillRefsChange).toBeDefined();
239
+ expect(props.sessionVariables).toBeDefined();
240
+ expect(lastSessionConfig().mutations).toBeDefined();
241
+ });
242
+
243
+ it("endUser: locked agent, integrator pickers unwired, read-only Setup tab", () => {
244
+ render(<SessionViewer sessionId="ses_1" org="acme" audience="endUser" />);
245
+
246
+ const props = lastComposerProps();
247
+ expect(props.lockAgent).toBe(true);
248
+ expect(props.onMcpServerUsagesChange).toBeUndefined();
249
+ expect(props.onSkillRefsChange).toBeUndefined();
250
+ expect(props.sessionVariables).toBeUndefined();
251
+ // End-user controls survive the curation.
252
+ expect(props.showInteractionModePicker).toBe(true);
253
+ expect(lastSessionConfig().mutations).toBeUndefined();
254
+ });
255
+
256
+ it("forwards getRuntimeEnv to the flow", () => {
257
+ const getRuntimeEnv = vi.fn();
258
+ render(
259
+ <SessionViewer sessionId="ses_1" org="acme" getRuntimeEnv={getRuntimeEnv} />,
260
+ );
261
+
262
+ expect(mockUseSessionPageFlow).toHaveBeenCalledWith(
263
+ expect.objectContaining({ getRuntimeEnv }),
264
+ );
265
+ });
266
+
267
+ it("renders the flow's submitError through the send-error banner", () => {
268
+ stubSessionPageFlow.submitError = new Error("token mint failed");
269
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
270
+
271
+ const alert = screen.getByRole("alert");
272
+ expect(alert.textContent).toContain("token mint failed");
273
+ });
274
+ });
@@ -468,6 +468,128 @@ describe("useNewSessionFlow", () => {
468
468
  });
469
469
  });
470
470
 
471
+ describe("host runtime env (getRuntimeEnv)", () => {
472
+ it("merges host env into the first execution, host wins on collisions", async () => {
473
+ const getRuntimeEnv = vi.fn().mockResolvedValue({
474
+ PLATFORM_TOKEN: { value: "fresh-token", isSecret: true },
475
+ });
476
+ const opts = { ...defaultOptions(), getRuntimeEnv };
477
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
478
+
479
+ await act(async () => {
480
+ await result.current.submit("Hello", undefined, {
481
+ runtimeEnv: {
482
+ PLATFORM_TOKEN: { value: "stale-token", isSecret: true },
483
+ USER_VAR: { value: "kept" },
484
+ },
485
+ });
486
+ });
487
+
488
+ expect(getRuntimeEnv).toHaveBeenCalledTimes(1);
489
+ const execInput = mockCreateExecution.mock.calls[0][0];
490
+ expect(execInput.runtimeEnv).toEqual({
491
+ PLATFORM_TOKEN: { value: "fresh-token", isSecret: true },
492
+ USER_VAR: { value: "kept" },
493
+ });
494
+ });
495
+
496
+ it("evaluates the provider fresh on every submission", async () => {
497
+ let mint = 0;
498
+ const getRuntimeEnv = vi.fn(() => ({
499
+ TOKEN: { value: `token-${++mint}` },
500
+ }));
501
+ const opts = { ...defaultOptions(), getRuntimeEnv };
502
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
503
+
504
+ await act(async () => {
505
+ await result.current.submit("First");
506
+ });
507
+ await act(async () => {
508
+ await result.current.submit("Second");
509
+ });
510
+
511
+ expect(getRuntimeEnv).toHaveBeenCalledTimes(2);
512
+ expect(mockCreateExecution.mock.calls[0][0].runtimeEnv).toEqual({
513
+ TOKEN: { value: "token-1" },
514
+ });
515
+ expect(mockCreateExecution.mock.calls[1][0].runtimeEnv).toEqual({
516
+ TOKEN: { value: "token-2" },
517
+ });
518
+ });
519
+
520
+ it("aborts before session creation when the provider throws", async () => {
521
+ const getRuntimeEnv = vi.fn().mockRejectedValue(new Error("token mint failed"));
522
+ const opts = { ...defaultOptions(), getRuntimeEnv };
523
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
524
+
525
+ await act(async () => {
526
+ await result.current.submit("Hello");
527
+ });
528
+
529
+ // No orphan session, no execution — the failure is fully pre-flight.
530
+ expect(mockCreateSession).not.toHaveBeenCalled();
531
+ expect(mockCreateExecution).not.toHaveBeenCalled();
532
+ expect(result.current.submitError).toContain("token mint failed");
533
+ expect(opts.onError).toHaveBeenCalled();
534
+ expect(result.current.isSubmitting).toBe(false);
535
+ });
536
+
537
+ it("passes composer env through untouched when no provider is configured", async () => {
538
+ const opts = defaultOptions();
539
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
540
+
541
+ await act(async () => {
542
+ await result.current.submit("Hello", undefined, {
543
+ runtimeEnv: { USER_VAR: { value: "composer-only" } },
544
+ });
545
+ });
546
+
547
+ expect(mockCreateExecution.mock.calls[0][0].runtimeEnv).toEqual({
548
+ USER_VAR: { value: "composer-only" },
549
+ });
550
+ });
551
+ });
552
+
553
+ describe("defaultHarness", () => {
554
+ it("seeds the embedder default when no harness is stored", () => {
555
+ const opts = { ...defaultOptions(), defaultHarness: "cursor" as const };
556
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
557
+
558
+ expect(result.current.harness).toBe("cursor");
559
+ });
560
+
561
+ it("stored user choice outranks the embedder default", () => {
562
+ localStorage.setItem(STORAGE_KEY_HARNESS, "native");
563
+ const opts = { ...defaultOptions(), defaultHarness: "cursor" as const };
564
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
565
+
566
+ expect(result.current.harness).toBe("native");
567
+ });
568
+
569
+ it("does not persist the seeded default — only explicit choices", () => {
570
+ const opts = { ...defaultOptions(), defaultHarness: "cursor" as const };
571
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
572
+
573
+ // Seeding must not masquerade as a user choice, otherwise the
574
+ // embedder default would stop applying after the first visit.
575
+ expect(localStorage.getItem(STORAGE_KEY_HARNESS)).toBeNull();
576
+
577
+ act(() => result.current.setHarness("native"));
578
+ expect(localStorage.getItem(STORAGE_KEY_HARNESS)).toBe("native");
579
+ });
580
+
581
+ it("submits sessions with the seeded default harness", async () => {
582
+ const opts = { ...defaultOptions(), defaultHarness: "cursor" as const };
583
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
584
+
585
+ await act(async () => {
586
+ await result.current.submit("Hello");
587
+ });
588
+
589
+ expect(mockCreateSession.mock.calls[0][0].harness).toBe("cursor");
590
+ });
591
+ });
592
+
471
593
  describe("submit while default agent is loading", () => {
472
594
  it("awaits default agent and creates session when fetch resolves", async () => {
473
595
  const resolvedAgent = { status: { defaultInstanceId: "awaited-inst" } };