@stigmer/react 3.1.7 → 3.1.9

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 (206) hide show
  1. package/agent/AgentDetailView.d.ts +9 -1
  2. package/agent/AgentDetailView.d.ts.map +1 -1
  3. package/agent/AgentDetailView.js +15 -4
  4. package/agent/AgentDetailView.js.map +1 -1
  5. package/environment/EnvironmentListPanel.d.ts.map +1 -1
  6. package/environment/EnvironmentListPanel.js +11 -3
  7. package/environment/EnvironmentListPanel.js.map +1 -1
  8. package/environment/EnvironmentPicker.d.ts +17 -1
  9. package/environment/EnvironmentPicker.d.ts.map +1 -1
  10. package/environment/EnvironmentPicker.js +3 -2
  11. package/environment/EnvironmentPicker.js.map +1 -1
  12. package/environment/EnvironmentVariableEditor.d.ts.map +1 -1
  13. package/environment/EnvironmentVariableEditor.js +9 -3
  14. package/environment/EnvironmentVariableEditor.js.map +1 -1
  15. package/environment/index.d.ts +1 -0
  16. package/environment/index.d.ts.map +1 -1
  17. package/environment/index.js +1 -0
  18. package/environment/index.js.map +1 -1
  19. package/environment/shareRestriction.d.ts +13 -0
  20. package/environment/shareRestriction.d.ts.map +1 -0
  21. package/environment/shareRestriction.js +15 -0
  22. package/environment/shareRestriction.js.map +1 -0
  23. package/execution/ToolCallItem.d.ts.map +1 -1
  24. package/execution/ToolCallItem.js +12 -0
  25. package/execution/ToolCallItem.js.map +1 -1
  26. package/execution/ToolRunGroup.js +4 -0
  27. package/execution/ToolRunGroup.js.map +1 -1
  28. package/execution/WriteBackCard.d.ts +14 -5
  29. package/execution/WriteBackCard.d.ts.map +1 -1
  30. package/execution/WriteBackCard.js +103 -32
  31. package/execution/WriteBackCard.js.map +1 -1
  32. package/execution/WriteBacksWidget.d.ts +1 -1
  33. package/execution/WriteBacksWidget.js +2 -2
  34. package/execution/WriteBacksWidget.js.map +1 -1
  35. package/execution/index.d.ts +2 -0
  36. package/execution/index.d.ts.map +1 -1
  37. package/execution/index.js +1 -0
  38. package/execution/index.js.map +1 -1
  39. package/execution/write-back-utils.d.ts +57 -0
  40. package/execution/write-back-utils.d.ts.map +1 -0
  41. package/execution/write-back-utils.js +81 -0
  42. package/execution/write-back-utils.js.map +1 -0
  43. package/index.d.ts +6 -2
  44. package/index.d.ts.map +1 -1
  45. package/index.js +6 -1
  46. package/index.js.map +1 -1
  47. package/internal/dev/dom-counter.d.ts.map +1 -1
  48. package/internal/dev/dom-counter.js +2 -2
  49. package/internal/dev/dom-counter.js.map +1 -1
  50. package/internal/dev/enabled.d.ts +7 -0
  51. package/internal/dev/enabled.d.ts.map +1 -0
  52. package/internal/dev/enabled.js +38 -0
  53. package/internal/dev/enabled.js.map +1 -0
  54. package/internal/dev/profiler-wrapper.d.ts +3 -3
  55. package/internal/dev/profiler-wrapper.d.ts.map +1 -1
  56. package/internal/dev/profiler-wrapper.js +5 -5
  57. package/internal/dev/profiler-wrapper.js.map +1 -1
  58. package/internal/dev/use-key-stability.d.ts.map +1 -1
  59. package/internal/dev/use-key-stability.js +2 -2
  60. package/internal/dev/use-key-stability.js.map +1 -1
  61. package/internal/dev/use-render-tracer.d.ts +2 -2
  62. package/internal/dev/use-render-tracer.d.ts.map +1 -1
  63. package/internal/dev/use-render-tracer.js +4 -4
  64. package/internal/dev/use-render-tracer.js.map +1 -1
  65. package/internal/dev/use-stream-rate.d.ts +2 -1
  66. package/internal/dev/use-stream-rate.d.ts.map +1 -1
  67. package/internal/dev/use-stream-rate.js +4 -3
  68. package/internal/dev/use-stream-rate.js.map +1 -1
  69. package/library/ResourceVisibilityControl.d.ts +3 -0
  70. package/library/ResourceVisibilityControl.d.ts.map +1 -1
  71. package/library/ResourceVisibilityControl.js +22 -8
  72. package/library/ResourceVisibilityControl.js.map +1 -1
  73. package/library/useUpdateVisibility.d.ts +5 -3
  74. package/library/useUpdateVisibility.d.ts.map +1 -1
  75. package/library/useUpdateVisibility.js +7 -2
  76. package/library/useUpdateVisibility.js.map +1 -1
  77. package/library/visibilityLevels.d.ts +17 -0
  78. package/library/visibilityLevels.d.ts.map +1 -1
  79. package/library/visibilityLevels.js +32 -0
  80. package/library/visibilityLevels.js.map +1 -1
  81. package/package.json +4 -4
  82. package/session/NewSessionViewer.d.ts +11 -1
  83. package/session/NewSessionViewer.d.ts.map +1 -1
  84. package/session/NewSessionViewer.js +28 -8
  85. package/session/NewSessionViewer.js.map +1 -1
  86. package/session/SessionViewer.d.ts +9 -2
  87. package/session/SessionViewer.d.ts.map +1 -1
  88. package/session/SessionViewer.js +26 -12
  89. package/session/SessionViewer.js.map +1 -1
  90. package/session/audience.d.ts +12 -4
  91. package/session/audience.d.ts.map +1 -1
  92. package/session/facets/ChangesTab.d.ts.map +1 -1
  93. package/session/facets/ChangesTab.js +3 -1
  94. package/session/facets/ChangesTab.js.map +1 -1
  95. package/session/useNewSessionFlow.d.ts +12 -0
  96. package/session/useNewSessionFlow.d.ts.map +1 -1
  97. package/session/useNewSessionFlow.js +13 -1
  98. package/session/useNewSessionFlow.js.map +1 -1
  99. package/session/useSessionPageFlow.d.ts +13 -0
  100. package/session/useSessionPageFlow.d.ts.map +1 -1
  101. package/session/useSessionPageFlow.js +7 -2
  102. package/session/useSessionPageFlow.js.map +1 -1
  103. package/sharing/ShareAgentDialog.d.ts +57 -0
  104. package/sharing/ShareAgentDialog.d.ts.map +1 -0
  105. package/sharing/ShareAgentDialog.js +478 -0
  106. package/sharing/ShareAgentDialog.js.map +1 -0
  107. package/sharing/SharedAgentChat.d.ts +84 -0
  108. package/sharing/SharedAgentChat.d.ts.map +1 -0
  109. package/sharing/SharedAgentChat.js +80 -0
  110. package/sharing/SharedAgentChat.js.map +1 -0
  111. package/sharing/index.d.ts +18 -0
  112. package/sharing/index.d.ts.map +1 -0
  113. package/sharing/index.js +12 -0
  114. package/sharing/index.js.map +1 -0
  115. package/sharing/useAgentShare.d.ts +43 -0
  116. package/sharing/useAgentShare.d.ts.map +1 -0
  117. package/sharing/useAgentShare.js +54 -0
  118. package/sharing/useAgentShare.js.map +1 -0
  119. package/sharing/useRotateShareLink.d.ts +45 -0
  120. package/sharing/useRotateShareLink.d.ts.map +1 -0
  121. package/sharing/useRotateShareLink.js +58 -0
  122. package/sharing/useRotateShareLink.js.map +1 -0
  123. package/sharing/useSaveAgentShare.d.ts +105 -0
  124. package/sharing/useSaveAgentShare.d.ts.map +1 -0
  125. package/sharing/useSaveAgentShare.js +97 -0
  126. package/sharing/useSaveAgentShare.js.map +1 -0
  127. package/sharing/useShareAgent.d.ts +69 -0
  128. package/sharing/useShareAgent.d.ts.map +1 -0
  129. package/sharing/useShareAgent.js +42 -0
  130. package/sharing/useShareAgent.js.map +1 -0
  131. package/sharing/useShareToolReadiness.d.ts +52 -0
  132. package/sharing/useShareToolReadiness.d.ts.map +1 -0
  133. package/sharing/useShareToolReadiness.js +89 -0
  134. package/sharing/useShareToolReadiness.js.map +1 -0
  135. package/sharing/useSharedAgentProfile.d.ts +85 -0
  136. package/sharing/useSharedAgentProfile.d.ts.map +1 -0
  137. package/sharing/useSharedAgentProfile.js +73 -0
  138. package/sharing/useSharedAgentProfile.js.map +1 -0
  139. package/src/agent/AgentDetailView.tsx +28 -3
  140. package/src/environment/EnvironmentListPanel.tsx +47 -18
  141. package/src/environment/EnvironmentPicker.tsx +23 -2
  142. package/src/environment/EnvironmentVariableEditor.tsx +27 -17
  143. package/src/environment/__tests__/shareRestriction.test.ts +35 -0
  144. package/src/environment/index.ts +5 -0
  145. package/src/environment/shareRestriction.ts +22 -0
  146. package/src/execution/ToolCallItem.tsx +19 -1
  147. package/src/execution/ToolRunGroup.tsx +4 -0
  148. package/src/execution/WriteBackCard.tsx +285 -88
  149. package/src/execution/WriteBacksWidget.tsx +5 -5
  150. package/src/execution/__tests__/ToolCallItem.test.tsx +51 -0
  151. package/src/execution/__tests__/ToolRunGroup.test.tsx +22 -0
  152. package/src/execution/__tests__/WriteBackCard.test.tsx +64 -10
  153. package/src/execution/__tests__/write-back-utils.test.ts +135 -0
  154. package/src/execution/index.ts +7 -0
  155. package/src/execution/write-back-utils.ts +107 -0
  156. package/src/index.ts +38 -0
  157. package/src/internal/dev/__tests__/use-key-stability.test.ts +4 -0
  158. package/src/internal/dev/__tests__/use-render-tracer.test.ts +4 -0
  159. package/src/internal/dev/dom-counter.ts +2 -3
  160. package/src/internal/dev/enabled.ts +39 -0
  161. package/src/internal/dev/profiler-wrapper.tsx +5 -6
  162. package/src/internal/dev/use-key-stability.ts +2 -3
  163. package/src/internal/dev/use-render-tracer.ts +4 -5
  164. package/src/internal/dev/use-stream-rate.ts +4 -4
  165. package/src/library/ResourceVisibilityControl.tsx +23 -7
  166. package/src/library/__tests__/visibilityLevels.test.ts +33 -0
  167. package/src/library/useUpdateVisibility.ts +9 -3
  168. package/src/library/visibilityLevels.ts +36 -0
  169. package/src/session/NewSessionViewer.tsx +67 -31
  170. package/src/session/SessionViewer.tsx +67 -36
  171. package/src/session/__tests__/audienceWiring.test.tsx +99 -0
  172. package/src/session/__tests__/useNewSessionFlow.test.tsx +75 -1
  173. package/src/session/__tests__/useSessionPageFlow.guest.test.tsx +112 -0
  174. package/src/session/audience.ts +12 -4
  175. package/src/session/facets/ChangesTab.tsx +6 -4
  176. package/src/session/useNewSessionFlow.ts +26 -1
  177. package/src/session/useSessionPageFlow.ts +24 -2
  178. package/src/sharing/ShareAgentDialog.tsx +1489 -0
  179. package/src/sharing/SharedAgentChat.tsx +283 -0
  180. package/src/sharing/__tests__/ShareAgentDialog.test.tsx +926 -0
  181. package/src/sharing/__tests__/SharedAgentChat.test.tsx +222 -0
  182. package/src/sharing/__tests__/useAgentShare.test.tsx +149 -0
  183. package/src/sharing/__tests__/useRotateShareLink.test.tsx +96 -0
  184. package/src/sharing/__tests__/useSaveAgentShare.test.tsx +241 -0
  185. package/src/sharing/__tests__/useShareAgent.test.tsx +131 -0
  186. package/src/sharing/__tests__/useShareToolReadiness.test.tsx +219 -0
  187. package/src/sharing/__tests__/useSharedAgentProfile.test.tsx +201 -0
  188. package/src/sharing/index.ts +32 -0
  189. package/src/sharing/useAgentShare.ts +91 -0
  190. package/src/sharing/useRotateShareLink.ts +81 -0
  191. package/src/sharing/useSaveAgentShare.ts +186 -0
  192. package/src/sharing/useShareAgent.tsx +107 -0
  193. package/src/sharing/useShareToolReadiness.ts +131 -0
  194. package/src/sharing/useSharedAgentProfile.ts +132 -0
  195. package/src/switch/Switch.tsx +97 -0
  196. package/src/switch/__tests__/Switch.test.tsx +107 -0
  197. package/src/switch/index.ts +2 -0
  198. package/styles.css +1 -1
  199. package/switch/Switch.d.ts +47 -0
  200. package/switch/Switch.d.ts.map +1 -0
  201. package/switch/Switch.js +41 -0
  202. package/switch/Switch.js.map +1 -0
  203. package/switch/index.d.ts +3 -0
  204. package/switch/index.d.ts.map +1 -0
  205. package/switch/index.js +2 -0
  206. package/switch/index.js.map +1 -0
@@ -143,6 +143,42 @@ export const INSTANCE_VISIBILITY_LEVELS: readonly VisibilityLevelOption[] = [
143
143
  },
144
144
  ];
145
145
 
146
+ /**
147
+ * The levels an environment selector offers, in escalation order:
148
+ * Private / Organization. Broader levels are structurally absent —
149
+ * secret values never leave the org boundary (the backend rejects
150
+ * public/platform via the kind's VisibilityConfig).
151
+ *
152
+ * Org sharing on an environment carries credential semantics, so the
153
+ * copy names both effects: members get redacted view, and executions
154
+ * in the org (teammate-run agents AND shared-agent visitors) can use
155
+ * the values at runtime. Secret reveal stays creator-only at every
156
+ * level.
157
+ *
158
+ * In `local` mode (OSS Go backend, single-user) sharing has no
159
+ * enforcement meaning, so no interactive levels are offered —
160
+ * {@link ResourceVisibilityControl} degrades to a read-only badge.
161
+ */
162
+ export function environmentVisibilityLevels(
163
+ deploymentMode: "cloud" | "local",
164
+ ): readonly VisibilityLevelOption[] {
165
+ if (deploymentMode === "local") {
166
+ return [PRIVATE_OPTION];
167
+ }
168
+ return [
169
+ {
170
+ ...PRIVATE_OPTION,
171
+ description: "Only you can view and use these credentials",
172
+ },
173
+ {
174
+ ...ORG_OPTION,
175
+ description:
176
+ "Agents run in your org — including shared-agent visitors — can use these credentials. Members see names only; secret values stay hidden.",
177
+ confirmPrompt: "Let agents run in your org use these credentials?",
178
+ },
179
+ ];
180
+ }
181
+
146
182
  /**
147
183
  * Canonical option for a visibility value, independent of any kind's offered
148
184
  * list. Used to render the current level even when it is not offerable in
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useMemo, useState, type ReactNode } from "react";
3
+ import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
4
4
  import { cn } from "@stigmer/theme";
5
5
  import type { ResourceRef } from "@stigmer/sdk";
6
6
  import type { UseGitHubConnectionReturn } from "../github/useGitHubConnection.js";
@@ -83,7 +83,17 @@ export interface NewSessionViewerProps {
83
83
  * server, skill, and session-variable pickers — for product-embedded
84
84
  * chat where the agent is configured upstream by the platform. The
85
85
  * model selector, interaction mode, harness selector, attachments,
86
- * and workspace picker remain. See {@link SessionAudience}.
86
+ * and workspace picker remain.
87
+ *
88
+ * `"guest"` (anonymous visitor with a guest token) is pure chat: it
89
+ * additionally hides the model/harness/mode pickers, attachments,
90
+ * the workspace picker, and the session panel, binds the session to
91
+ * `initialAgentRef` + `initialInstanceId` without any picker
92
+ * machinery, and skips the org-level reads a guest principal cannot
93
+ * make. Requires both `initialAgentRef` and `initialInstanceId` —
94
+ * a guest launcher never falls back to the org default agent.
95
+ *
96
+ * See {@link SessionAudience}.
87
97
  *
88
98
  * @default "integrator"
89
99
  */
@@ -198,9 +208,26 @@ export function NewSessionViewer({
198
208
  onError,
199
209
  getRuntimeEnv,
200
210
  defaultHarness,
211
+ audience,
201
212
  });
202
213
  const [interactionMode, setInteractionMode] = useState<InteractionModeOption>("agent");
203
- const isEndUser = audience === "endUser";
214
+ const isGuest = audience === "guest";
215
+ // Both curated audiences (endUser, guest) lock the agent and hide the
216
+ // integrator pickers; guest adds its own restrictions below.
217
+ const isCurated = audience !== "integrator";
218
+
219
+ // Guest agent binding is host configuration, not a picker interaction:
220
+ // the composer's agent machinery (picker, env-collection, personal
221
+ // environments — all org reads a guest token cannot make) stays fully
222
+ // unwired, and the launcher pins the flow to the shared agent's
223
+ // instance directly. Without the pin, submission fails closed in
224
+ // useNewSessionFlow rather than falling back to the org default agent.
225
+ const { setAgentRef, setResolution } = flow;
226
+ useEffect(() => {
227
+ if (!isGuest || !initialAgentRef || !initialInstanceId) return;
228
+ setAgentRef(initialAgentRef);
229
+ setResolution({ mode: "saved", instanceId: initialInstanceId });
230
+ }, [isGuest, initialAgentRef, initialInstanceId, setAgentRef, setResolution]);
204
231
 
205
232
  // The unified-panel controller (shared with SessionViewer, DD-016). The
206
233
  // launcher has no execution yet, so the FSM inputs are static. It has no
@@ -259,9 +286,9 @@ export function NewSessionViewer({
259
286
  harness: flow.harness,
260
287
  executionTarget: undefined,
261
288
  modelId: flow.modelId,
262
- // End users see the configuration but cannot strip it — the Setup
263
- // tab renders read-only without mutation callbacks (DD-011).
264
- mutations: isEndUser
289
+ // Curated audiences see the configuration but cannot strip it — the
290
+ // Setup tab renders read-only without mutation callbacks (DD-011).
291
+ mutations: isCurated
265
292
  ? undefined
266
293
  : {
267
294
  onRemoveAgent: flow.agentRef ? handleRemoveAgent : undefined,
@@ -272,7 +299,7 @@ export function NewSessionViewer({
272
299
  [
273
300
  flow.agentRef, flow.mcpServerUsages, flow.skillRefs,
274
301
  flow.sessionVariables, flow.harness, flow.modelId,
275
- isEndUser, handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
302
+ isCurated, handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
276
303
  ],
277
304
  );
278
305
 
@@ -312,29 +339,33 @@ export function NewSessionViewer({
312
339
  onSubmit={flow.submit}
313
340
  isSubmitting={flow.isSubmitting}
314
341
  org={org}
315
- workspace={flow.workspace}
316
- gitHubConnection={enableGitHub ? gitHubConnection : undefined}
317
- enableGitHub={enableGitHub}
318
- enableLocal={enableLocal}
342
+ workspace={isGuest ? undefined : flow.workspace}
343
+ gitHubConnection={enableGitHub && !isGuest ? gitHubConnection : undefined}
344
+ enableGitHub={enableGitHub && !isGuest}
345
+ enableLocal={enableLocal && !isGuest}
319
346
  onBrowseLocalFolder={onBrowseLocalFolder}
320
347
  agentRef={flow.agentRef}
321
- onAgentRefChange={flow.setAgentRef}
322
- onAgentResolutionChange={flow.setResolution}
323
- initialAgentRef={initialAgentRef}
324
- initialInstanceId={initialInstanceId}
325
- initialAttachments={initialAttachments}
326
- lockAgent={isEndUser && initialAgentRef != null}
327
- mcpServerUsages={isEndUser ? undefined : flow.mcpServerUsages}
328
- onMcpServerUsagesChange={isEndUser ? undefined : flow.setMcpServerUsages}
329
- skillRefs={isEndUser ? undefined : flow.skillRefs}
330
- onSkillRefsChange={isEndUser ? undefined : flow.setSkillRefs}
331
- sessionVariables={isEndUser ? undefined : flow.sessionVariables}
332
- showHarnessSelector
348
+ // Guests get no agent machinery at all — the binding is applied
349
+ // by the launcher's pin effect above, not by picker resolution.
350
+ onAgentRefChange={isGuest ? undefined : flow.setAgentRef}
351
+ onAgentResolutionChange={isGuest ? undefined : flow.setResolution}
352
+ initialAgentRef={isGuest ? undefined : initialAgentRef}
353
+ initialInstanceId={isGuest ? undefined : initialInstanceId}
354
+ initialAttachments={isGuest ? undefined : initialAttachments}
355
+ lockAgent={isCurated && initialAgentRef != null}
356
+ mcpServerUsages={isCurated ? undefined : flow.mcpServerUsages}
357
+ onMcpServerUsagesChange={isCurated ? undefined : flow.setMcpServerUsages}
358
+ skillRefs={isCurated ? undefined : flow.skillRefs}
359
+ onSkillRefsChange={isCurated ? undefined : flow.setSkillRefs}
360
+ sessionVariables={isCurated ? undefined : flow.sessionVariables}
361
+ showHarnessSelector={!isGuest}
333
362
  harness={flow.harness}
334
363
  onHarnessChange={flow.setHarness}
335
364
  interactionMode={interactionMode}
336
365
  onInteractionModeChange={setInteractionMode}
337
- showInteractionModePicker
366
+ showInteractionModePicker={!isGuest}
367
+ showModelSelector={!isGuest}
368
+ enableAttachments={!isGuest}
338
369
  defaultModelId={flow.modelId}
339
370
  onModelChange={flow.setModelId}
340
371
  placeholder={placeholder}
@@ -360,13 +391,18 @@ export function NewSessionViewer({
360
391
  reads as instability and, worse, unmounts the open panel's only
361
392
  collapse control when the last context item is removed. Always-on is
362
393
  the predictable, discoverable shape; opening homes on the Config
363
- facet, which carries useful defaults (harness/model) pre-session. */}
364
- <div className="absolute top-2 right-6 z-10">
365
- <SessionPanelChip
366
- isOpen={panel.isOpen}
367
- onToggle={panel.isOpen ? panel.closePanel : panel.openPanel}
368
- />
369
- </div>
394
+ facet, which carries useful defaults (harness/model) pre-session.
395
+ Guests are the exception: the panel exposes configuration a visitor
396
+ has no business with, so the chip (the panel's only toggle) is
397
+ simply absent and the panel can never open. */}
398
+ {!isGuest && (
399
+ <div className="absolute top-2 right-6 z-10">
400
+ <SessionPanelChip
401
+ isOpen={panel.isOpen}
402
+ onToggle={panel.isOpen ? panel.closePanel : panel.openPanel}
403
+ />
404
+ </div>
405
+ )}
370
406
 
371
407
  {/* Same unified-panel layout as SessionViewer (DD-016): collapsed by
372
408
  default (composer fills the row); opening makes the composer the
@@ -209,8 +209,15 @@ export interface SessionViewerProps {
209
209
  * session-variable configuration in both the composer and the
210
210
  * inspector's Setup tab — for product-embedded chat where the agent
211
211
  * is configured upstream by the platform. The model selector,
212
- * interaction mode, attachments, and workspace picker remain. See
213
- * {@link SessionAudience}.
212
+ * interaction mode, attachments, and workspace picker remain.
213
+ *
214
+ * `"guest"` (anonymous visitor with a guest token) is pure chat: it
215
+ * additionally hides the model/mode pickers, attachments, the
216
+ * workspace picker, and the session panel, and skips the org-level
217
+ * reads a guest principal cannot make — follow-ups simply continue
218
+ * on the session's bound agent instance.
219
+ *
220
+ * See {@link SessionAudience}.
214
221
  *
215
222
  * @default "integrator"
216
223
  */
@@ -303,9 +310,12 @@ export function SessionViewer({
303
310
  threadSlots,
304
311
  className,
305
312
  }: SessionViewerProps) {
306
- const flow = useSessionPageFlow({ sessionId, org, getRuntimeEnv });
313
+ const flow = useSessionPageFlow({ sessionId, org, getRuntimeEnv, audience });
307
314
  const { conv } = flow;
308
- const isEndUser = audience === "endUser";
315
+ const isGuest = audience === "guest";
316
+ // Both curated audiences (endUser, guest) lock the agent and hide the
317
+ // integrator configuration; guest adds its own restrictions below.
318
+ const isCurated = audience !== "integrator";
309
319
 
310
320
  const [modelId, setModelId] = flow.model;
311
321
  const [interactionMode, setInteractionMode] = flow.interactionMode;
@@ -391,7 +401,9 @@ export function SessionViewer({
391
401
  const panel = useSessionPanel({
392
402
  phase: hasPhase ? phase : null,
393
403
  hasChanges: hasWriteBacks,
394
- planKey: panelPlanKey,
404
+ // A streaming plan auto-opens the panel via its key — but guests have
405
+ // no panel, so the trigger is suppressed along with the chip.
406
+ planKey: isGuest ? null : panelPlanKey,
395
407
  });
396
408
 
397
409
  const handleBuildFromPlan = useCallback(() => {
@@ -504,6 +516,10 @@ export function SessionViewer({
504
516
  // (which subscribes), never this streaming column.
505
517
  const handleTranscriptFilePathClick = useCallback(
506
518
  (path: string): boolean => {
519
+ // Guests have no panel (and no file viewer), so paths keep their
520
+ // default copy behavior instead of opening a surface that cannot
521
+ // render.
522
+ if (isGuest) return false;
507
523
  const selection = resolveWorkspaceFileSelection(
508
524
  path,
509
525
  flow.workspace.entries,
@@ -513,7 +529,7 @@ export function SessionViewer({
513
529
  panel.openFile(selection.entryId, selection.path);
514
530
  return true;
515
531
  },
516
- [flow.workspace.entries, flow.sandboxWorkspaceRoot, panel.openFile],
532
+ [isGuest, flow.workspace.entries, flow.sandboxWorkspaceRoot, panel.openFile],
517
533
  );
518
534
 
519
535
  if (conv.isLoading) {
@@ -537,14 +553,18 @@ export function SessionViewer({
537
553
  {/* Top-right controls: host actions + the panel chip. The chip is the
538
554
  panel's always-mounted toggle; while collapsed it carries only the
539
555
  pending-item count. Execution status is never surfaced as header
540
- chrome — the thread itself communicates run state. */}
556
+ chrome — the thread itself communicates run state. Guests get no
557
+ chip: the panel exposes session configuration a visitor has no
558
+ business with, so its only toggle is simply absent. */}
541
559
  <div className="absolute top-2 right-6 z-10 flex items-center gap-2">
542
560
  {headerActions}
543
- <SessionPanelChip
544
- isOpen={panel.isOpen}
545
- onToggle={panel.isOpen ? panel.closePanel : panel.openPanel}
546
- badgeCount={writeBackCount + artifactCount}
547
- />
561
+ {!isGuest && (
562
+ <SessionPanelChip
563
+ isOpen={panel.isOpen}
564
+ onToggle={panel.isOpen ? panel.closePanel : panel.openPanel}
565
+ badgeCount={writeBackCount + artifactCount}
566
+ />
567
+ )}
548
568
  </div>
549
569
 
550
570
  <ThreadSelectionContext.Provider value={selectionStore}>
@@ -583,12 +603,13 @@ export function SessionViewer({
583
603
  planAttachFailed={planAttachFailed}
584
604
  onDismissPlanAttachFailed={() => setPlanAttachFailed(false)}
585
605
  onFilePathClick={handleTranscriptFilePathClick}
586
- isEndUser={isEndUser}
606
+ isCurated={isCurated}
607
+ isGuest={isGuest}
587
608
  threadSlots={threadSlots}
588
609
  />
589
610
  }
590
611
  secondary={
591
- panel.isOpen ? (
612
+ panel.isOpen && !isGuest ? (
592
613
  <SessionPanelRegion
593
614
  flow={flow}
594
615
  org={org}
@@ -609,7 +630,7 @@ export function SessionViewer({
609
630
  workspaceFileLister={workspaceFileLister}
610
631
  workspaceFileReader={workspaceFileReader}
611
632
  workspaceContentSearcher={workspaceContentSearcher}
612
- isEndUser={isEndUser}
633
+ isCurated={isCurated}
613
634
  />
614
635
  ) : null
615
636
  }
@@ -649,7 +670,10 @@ interface ConversationColumnProps {
649
670
  * let the path keep its copy / GitHub-link behavior.
650
671
  */
651
672
  readonly onFilePathClick: (path: string) => boolean;
652
- readonly isEndUser: boolean;
673
+ /** Curated audience (endUser or guest): locked agent, no integrator pickers. */
674
+ readonly isCurated: boolean;
675
+ /** Guest audience: pure chat — additionally no model picker, mode picker, attachments, or workspace. */
676
+ readonly isGuest: boolean;
653
677
  readonly threadSlots?: MessageThreadSlots;
654
678
  }
655
679
 
@@ -671,7 +695,8 @@ const ConversationColumn = memo(function ConversationColumn({
671
695
  planAttachFailed,
672
696
  onDismissPlanAttachFailed,
673
697
  onFilePathClick,
674
- isEndUser,
698
+ isCurated,
699
+ isGuest,
675
700
  threadSlots,
676
701
  }: ConversationColumnProps) {
677
702
  const { conv } = flow;
@@ -839,22 +864,27 @@ const ConversationColumn = memo(function ConversationColumn({
839
864
  onModelChange={setModelId}
840
865
  interactionMode={interactionMode}
841
866
  onInteractionModeChange={setInteractionMode}
842
- showInteractionModePicker
843
- workspace={flow.workspace}
844
- gitHubConnection={gitHubConnection}
845
- enableGitHub={enableGitHub}
846
- enableLocal={enableLocal}
867
+ showInteractionModePicker={!isGuest}
868
+ showModelSelector={!isGuest}
869
+ enableAttachments={!isGuest}
870
+ workspace={isGuest ? undefined : flow.workspace}
871
+ gitHubConnection={isGuest ? undefined : gitHubConnection}
872
+ enableGitHub={enableGitHub && !isGuest}
873
+ enableLocal={enableLocal && !isGuest}
847
874
  onBrowseLocalFolder={onBrowseLocalFolder}
848
875
  agentRef={flow.agentRef}
849
- onAgentRefChange={flow.setAgentRef}
850
- onAgentResolutionChange={flow.setResolution}
876
+ // Guests get no agent machinery: the session's agent is already
877
+ // bound server-side, and the picker's resolution path performs
878
+ // org reads a guest token cannot make.
879
+ onAgentRefChange={isGuest ? undefined : flow.setAgentRef}
880
+ onAgentResolutionChange={isGuest ? undefined : flow.setResolution}
851
881
  isDefaultAgent={flow.isDefaultAgent}
852
- lockAgent={isEndUser}
853
- mcpServerUsages={isEndUser ? undefined : flow.mcpServerUsages}
854
- onMcpServerUsagesChange={isEndUser ? undefined : flow.setMcpServerUsages}
855
- skillRefs={isEndUser ? undefined : flow.skillRefs}
856
- onSkillRefsChange={isEndUser ? undefined : flow.setSkillRefs}
857
- sessionVariables={isEndUser ? undefined : flow.sessionVariables}
882
+ lockAgent={isCurated}
883
+ mcpServerUsages={isCurated ? undefined : flow.mcpServerUsages}
884
+ onMcpServerUsagesChange={isCurated ? undefined : flow.setMcpServerUsages}
885
+ skillRefs={isCurated ? undefined : flow.skillRefs}
886
+ onSkillRefsChange={isCurated ? undefined : flow.setSkillRefs}
887
+ sessionVariables={isCurated ? undefined : flow.sessionVariables}
858
888
  className="px-4 py-3"
859
889
  />
860
890
  </div>
@@ -909,7 +939,8 @@ interface SessionPanelRegionProps {
909
939
  readonly workspaceFileLister?: WorkspaceFileLister;
910
940
  readonly workspaceFileReader?: WorkspaceFileReader;
911
941
  readonly workspaceContentSearcher?: WorkspaceContentSearcher;
912
- readonly isEndUser: boolean;
942
+ /** Curated audience (endUser or guest): the Setup facet renders read-only. */
943
+ readonly isCurated: boolean;
913
944
  }
914
945
 
915
946
  function SessionPanelRegion({
@@ -932,7 +963,7 @@ function SessionPanelRegion({
932
963
  workspaceFileLister,
933
964
  workspaceFileReader,
934
965
  workspaceContentSearcher,
935
- isEndUser,
966
+ isCurated,
936
967
  }: SessionPanelRegionProps) {
937
968
  const selectedItem = useSelectedThreadItem();
938
969
  const { editors, activeKey, activeFile, reveal } = useWorkspaceEditors(
@@ -1108,9 +1139,9 @@ function SessionPanelRegion({
1108
1139
  harness: flow.harness,
1109
1140
  executionTarget: flow.executionTarget,
1110
1141
  modelId: flow.model[0],
1111
- // End users see the configuration but cannot strip it — the Config
1112
- // facet renders read-only without mutation callbacks (DD-011).
1113
- mutations: isEndUser
1142
+ // Curated audiences see the configuration but cannot strip it — the
1143
+ // Config facet renders read-only without mutation callbacks (DD-011).
1144
+ mutations: isCurated
1114
1145
  ? undefined
1115
1146
  : {
1116
1147
  onRemoveAgent: flow.isDefaultAgent ? undefined : handleRemoveAgent,
@@ -1122,7 +1153,7 @@ function SessionPanelRegion({
1122
1153
  [
1123
1154
  flow.agentRef, flow.isDefaultAgent, flow.mcpServerUsages, flow.skillRefs,
1124
1155
  flow.sessionVariables, flow.harness, flow.executionTarget, flow.model,
1125
- isEndUser, handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
1156
+ isCurated, handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
1126
1157
  accessSlot,
1127
1158
  ],
1128
1159
  );
@@ -249,6 +249,75 @@ describe("NewSessionViewer — audience wiring", () => {
249
249
  expect.objectContaining({ getRuntimeEnv, defaultHarness: "cursor" }),
250
250
  );
251
251
  });
252
+
253
+ it("guest: pure chat — every picker hidden, attachments and workspace off", () => {
254
+ render(
255
+ <NewSessionViewer
256
+ org="acme"
257
+ onSessionCreated={vi.fn()}
258
+ audience="guest"
259
+ initialAgentRef={AGENT_REF}
260
+ initialInstanceId="inst_1"
261
+ />,
262
+ );
263
+
264
+ const props = lastComposerProps();
265
+ expect(props.lockAgent).toBe(true);
266
+ expect(props.onMcpServerUsagesChange).toBeUndefined();
267
+ expect(props.onSkillRefsChange).toBeUndefined();
268
+ expect(props.sessionVariables).toBeUndefined();
269
+ // Guest-only restrictions on top of the endUser curation.
270
+ expect(props.showHarnessSelector).toBe(false);
271
+ expect(props.showInteractionModePicker).toBe(false);
272
+ expect(props.showModelSelector).toBe(false);
273
+ expect(props.enableAttachments).toBe(false);
274
+ expect(props.enableGitHub).toBe(false);
275
+ expect(props.workspace).toBeUndefined();
276
+ // The session panel's only toggle is absent for guests.
277
+ expect(screen.queryByRole("button", { name: "Show panel" })).toBeNull();
278
+ });
279
+
280
+ it("guest: agent binding bypasses the composer's picker machinery", () => {
281
+ render(
282
+ <NewSessionViewer
283
+ org="acme"
284
+ onSessionCreated={vi.fn()}
285
+ audience="guest"
286
+ initialAgentRef={AGENT_REF}
287
+ initialInstanceId="inst_1"
288
+ />,
289
+ );
290
+
291
+ // The composer's agent machinery is fully unwired (no picker, no
292
+ // resolution flow — those perform org reads a guest token cannot make)…
293
+ const props = lastComposerProps();
294
+ expect(props.onAgentRefChange).toBeUndefined();
295
+ expect(props.onAgentResolutionChange).toBeUndefined();
296
+ expect(props.initialAgentRef).toBeUndefined();
297
+ expect(props.initialInstanceId).toBeUndefined();
298
+ // …and the launcher pins the flow to the shared instance directly.
299
+ expect(stubNewSessionFlow.setAgentRef).toHaveBeenCalledWith(AGENT_REF);
300
+ expect(stubNewSessionFlow.setResolution).toHaveBeenCalledWith({
301
+ mode: "saved",
302
+ instanceId: "inst_1",
303
+ });
304
+ });
305
+
306
+ it("guest: forwards the audience to the flow", () => {
307
+ render(
308
+ <NewSessionViewer
309
+ org="acme"
310
+ onSessionCreated={vi.fn()}
311
+ audience="guest"
312
+ initialAgentRef={AGENT_REF}
313
+ initialInstanceId="inst_1"
314
+ />,
315
+ );
316
+
317
+ expect(mockUseNewSessionFlow).toHaveBeenCalledWith(
318
+ expect.objectContaining({ audience: "guest" }),
319
+ );
320
+ });
252
321
  });
253
322
 
254
323
  describe("SessionViewer — audience wiring", () => {
@@ -276,6 +345,36 @@ describe("SessionViewer — audience wiring", () => {
276
345
  expect(openedConfigFacet().mutations).toBeUndefined();
277
346
  });
278
347
 
348
+ it("guest: pure chat — every picker hidden, attachments and workspace off", () => {
349
+ render(<SessionViewer sessionId="ses_1" org="acme" audience="guest" />);
350
+
351
+ const props = lastComposerProps();
352
+ expect(props.lockAgent).toBe(true);
353
+ expect(props.onMcpServerUsagesChange).toBeUndefined();
354
+ expect(props.onSkillRefsChange).toBeUndefined();
355
+ expect(props.sessionVariables).toBeUndefined();
356
+ // Guest-only restrictions on top of the endUser curation.
357
+ expect(props.showInteractionModePicker).toBe(false);
358
+ expect(props.showModelSelector).toBe(false);
359
+ expect(props.enableAttachments).toBe(false);
360
+ expect(props.enableGitHub).toBe(false);
361
+ expect(props.workspace).toBeUndefined();
362
+ // Agent machinery is fully unwired — the session's agent is bound
363
+ // server-side and its resolution reads are FGA-denied for guests.
364
+ expect(props.onAgentRefChange).toBeUndefined();
365
+ expect(props.onAgentResolutionChange).toBeUndefined();
366
+ // The session panel's only toggle is absent for guests.
367
+ expect(screen.queryByRole("button", { name: "Show panel" })).toBeNull();
368
+ });
369
+
370
+ it("guest: forwards the audience to the flow", () => {
371
+ render(<SessionViewer sessionId="ses_1" org="acme" audience="guest" />);
372
+
373
+ expect(mockUseSessionPageFlow).toHaveBeenCalledWith(
374
+ expect.objectContaining({ audience: "guest" }),
375
+ );
376
+ });
377
+
279
378
  it("forwards getRuntimeEnv to the flow", () => {
280
379
  const getRuntimeEnv = vi.fn();
281
380
  render(
@@ -36,8 +36,9 @@ const mockDefaultAgent = {
36
36
  refetch: vi.fn(),
37
37
  waitForResolution: vi.fn<() => Promise<unknown>>(),
38
38
  };
39
+ const useDefaultAgentSpy = vi.fn((_org: string | null) => mockDefaultAgent);
39
40
  vi.mock("../../agent", () => ({
40
- useDefaultAgent: () => mockDefaultAgent,
41
+ useDefaultAgent: (org: string | null) => useDefaultAgentSpy(org),
41
42
  }));
42
43
 
43
44
  const mockWorkspace = {
@@ -683,4 +684,77 @@ describe("useNewSessionFlow", () => {
683
684
  expect(mockDefaultAgent.waitForResolution).not.toHaveBeenCalled();
684
685
  });
685
686
  });
687
+
688
+ describe("guest audience", () => {
689
+ it("disables the default-agent lookup (a read guests cannot make)", () => {
690
+ const opts = { ...defaultOptions(), audience: "guest" as const };
691
+ renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
692
+
693
+ expect(useDefaultAgentSpy).toHaveBeenCalledWith(null);
694
+ });
695
+
696
+ it("keeps the default-agent lookup for other audiences", () => {
697
+ renderHook(() => useNewSessionFlow(defaultOptions()), { wrapper: createWrapper() });
698
+
699
+ expect(useDefaultAgentSpy).toHaveBeenCalledWith("acme");
700
+ });
701
+
702
+ it("creates the session against the pinned resolution", async () => {
703
+ const opts = { ...defaultOptions(), audience: "guest" as const };
704
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
705
+
706
+ act(() => {
707
+ result.current.setAgentRef({ org: "acme", slug: "support-bot" });
708
+ result.current.setResolution({ mode: "saved", instanceId: "shared-inst" });
709
+ });
710
+ await act(async () => {
711
+ await result.current.submit("Hello");
712
+ });
713
+
714
+ expect(mockCreateSession).toHaveBeenCalledOnce();
715
+ expect(mockCreateSession.mock.calls[0][0].agentInstanceId).toBe("shared-inst");
716
+ expect(opts.onSessionCreated).toHaveBeenCalledWith("sess-new");
717
+ });
718
+
719
+ it("fails closed on submit without a resolution — never the default agent", async () => {
720
+ // A default agent exists (the beforeEach seeds one); a guest must
721
+ // still never fall back to it.
722
+ const opts = { ...defaultOptions(), audience: "guest" as const };
723
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
724
+
725
+ await act(async () => {
726
+ await result.current.submit("Hello");
727
+ });
728
+
729
+ expect(mockCreateSession).not.toHaveBeenCalled();
730
+ expect(mockCreateExecution).not.toHaveBeenCalled();
731
+ expect(result.current.submitError).toContain("still loading");
732
+ expect(opts.onError).toHaveBeenCalled();
733
+ });
734
+
735
+ it("surfaces launch-gate refusal copy verbatim from the status description", async () => {
736
+ // The backend resolves owner-customizable refusal copy server-side and
737
+ // carries it in the gRPC status description — the flow must hand it to
738
+ // onError untouched (no client-side mapping exists by design).
739
+ const ownerCopy = "This agent is currently unavailable. Please check back later.";
740
+ const { ConnectError, Code } = await import("@connectrpc/connect");
741
+ mockCreateExecution.mockRejectedValueOnce(
742
+ new ConnectError(ownerCopy, Code.FailedPrecondition),
743
+ );
744
+
745
+ const opts = { ...defaultOptions(), audience: "guest" as const };
746
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
747
+
748
+ act(() => {
749
+ result.current.setAgentRef({ org: "acme", slug: "support-bot" });
750
+ result.current.setResolution({ mode: "saved", instanceId: "shared-inst" });
751
+ });
752
+ await act(async () => {
753
+ await result.current.submit("Hello");
754
+ });
755
+
756
+ expect(result.current.submitError).toBe(ownerCopy);
757
+ expect(opts.onError).toHaveBeenCalledWith(ownerCopy);
758
+ });
759
+ });
686
760
  });