@stigmer/react 3.12.7 → 3.12.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 (196) hide show
  1. package/README.md +1 -3
  2. package/composer/ComposerToolbar.d.ts +1 -19
  3. package/composer/ComposerToolbar.d.ts.map +1 -1
  4. package/composer/ComposerToolbar.js +2 -7
  5. package/composer/ComposerToolbar.js.map +1 -1
  6. package/composer/SessionComposer.d.ts +0 -10
  7. package/composer/SessionComposer.d.ts.map +1 -1
  8. package/composer/SessionComposer.js +2 -2
  9. package/composer/SessionComposer.js.map +1 -1
  10. package/composer/index.d.ts +0 -1
  11. package/composer/index.d.ts.map +1 -1
  12. package/composer/index.js.map +1 -1
  13. package/conversation/ConversationTimelineView.d.ts +15 -1
  14. package/conversation/ConversationTimelineView.d.ts.map +1 -1
  15. package/conversation/ConversationTimelineView.js +3 -2
  16. package/conversation/ConversationTimelineView.js.map +1 -1
  17. package/conversation/ConversationsWorkbench.d.ts +14 -1
  18. package/conversation/ConversationsWorkbench.d.ts.map +1 -1
  19. package/conversation/ConversationsWorkbench.js +11 -2
  20. package/conversation/ConversationsWorkbench.js.map +1 -1
  21. package/execution/MessageThread.d.ts +33 -2
  22. package/execution/MessageThread.d.ts.map +1 -1
  23. package/execution/MessageThread.js +44 -6
  24. package/execution/MessageThread.js.map +1 -1
  25. package/execution/RecalledMemoriesCard.d.ts +47 -0
  26. package/execution/RecalledMemoriesCard.d.ts.map +1 -0
  27. package/execution/RecalledMemoriesCard.js +58 -0
  28. package/execution/RecalledMemoriesCard.js.map +1 -0
  29. package/execution/ResultView.d.ts.map +1 -1
  30. package/execution/ResultView.js +6 -0
  31. package/execution/ResultView.js.map +1 -1
  32. package/execution/ToolCallItem.d.ts.map +1 -1
  33. package/execution/ToolCallItem.js +13 -2
  34. package/execution/ToolCallItem.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/tool-categories.d.ts +1 -1
  40. package/execution/tool-categories.d.ts.map +1 -1
  41. package/execution/tool-categories.js +6 -0
  42. package/execution/tool-categories.js.map +1 -1
  43. package/identity-account/AccountPreferencesPanel.d.ts +2 -1
  44. package/identity-account/AccountPreferencesPanel.d.ts.map +1 -1
  45. package/identity-account/AccountPreferencesPanel.js +41 -3
  46. package/identity-account/AccountPreferencesPanel.js.map +1 -1
  47. package/identity-account/useAccountExecutionDefaults.d.ts +7 -0
  48. package/identity-account/useAccountExecutionDefaults.d.ts.map +1 -1
  49. package/identity-account/useAccountExecutionDefaults.js +5 -2
  50. package/identity-account/useAccountExecutionDefaults.js.map +1 -1
  51. package/index.d.ts +6 -6
  52. package/index.d.ts.map +1 -1
  53. package/index.js +2 -2
  54. package/index.js.map +1 -1
  55. package/internal/MemoryEnabledRow.d.ts +37 -0
  56. package/internal/MemoryEnabledRow.d.ts.map +1 -0
  57. package/internal/MemoryEnabledRow.js +25 -0
  58. package/internal/MemoryEnabledRow.js.map +1 -0
  59. package/internal/VirtualizedThread.d.ts +3 -1
  60. package/internal/VirtualizedThread.d.ts.map +1 -1
  61. package/internal/VirtualizedThread.js +3 -1
  62. package/internal/VirtualizedThread.js.map +1 -1
  63. package/internal/useAutoScroll.d.ts +18 -0
  64. package/internal/useAutoScroll.d.ts.map +1 -1
  65. package/internal/useAutoScroll.js +50 -0
  66. package/internal/useAutoScroll.js.map +1 -1
  67. package/memory/MemoryProposalCard.d.ts +35 -0
  68. package/memory/MemoryProposalCard.d.ts.map +1 -0
  69. package/memory/MemoryProposalCard.js +73 -0
  70. package/memory/MemoryProposalCard.js.map +1 -0
  71. package/memory/index.d.ts +2 -0
  72. package/memory/index.d.ts.map +1 -1
  73. package/memory/index.js +2 -0
  74. package/memory/index.js.map +1 -1
  75. package/memory/useMemory.d.ts +39 -0
  76. package/memory/useMemory.d.ts.map +1 -0
  77. package/memory/useMemory.js +43 -0
  78. package/memory/useMemory.js.map +1 -0
  79. package/organization/OrgPreferencesPanel.d.ts.map +1 -1
  80. package/organization/OrgPreferencesPanel.js +36 -4
  81. package/organization/OrgPreferencesPanel.js.map +1 -1
  82. package/package.json +4 -4
  83. package/session/AutoApproveIndicator.d.ts +18 -0
  84. package/session/AutoApproveIndicator.d.ts.map +1 -0
  85. package/session/AutoApproveIndicator.js +23 -0
  86. package/session/AutoApproveIndicator.js.map +1 -0
  87. package/session/NewSessionViewer.d.ts.map +1 -1
  88. package/session/NewSessionViewer.js +10 -9
  89. package/session/NewSessionViewer.js.map +1 -1
  90. package/session/SessionViewer.d.ts +21 -9
  91. package/session/SessionViewer.d.ts.map +1 -1
  92. package/session/SessionViewer.js +31 -21
  93. package/session/SessionViewer.js.map +1 -1
  94. package/session/facets/SetupTab.d.ts +43 -6
  95. package/session/facets/SetupTab.d.ts.map +1 -1
  96. package/session/facets/SetupTab.js +32 -36
  97. package/session/facets/SetupTab.js.map +1 -1
  98. package/session/facets/UsageTab.d.ts +2 -1
  99. package/session/facets/UsageTab.d.ts.map +1 -1
  100. package/session/facets/UsageTab.js +11 -5
  101. package/session/facets/UsageTab.js.map +1 -1
  102. package/session/facets/index.d.ts +1 -1
  103. package/session/facets/index.d.ts.map +1 -1
  104. package/session/facets/primitives.d.ts +58 -0
  105. package/session/facets/primitives.d.ts.map +1 -0
  106. package/session/facets/primitives.js +59 -0
  107. package/session/facets/primitives.js.map +1 -0
  108. package/session/index.d.ts +1 -1
  109. package/session/index.d.ts.map +1 -1
  110. package/session/index.js.map +1 -1
  111. package/session/useNewSessionFlow.d.ts +9 -4
  112. package/session/useNewSessionFlow.d.ts.map +1 -1
  113. package/session/useNewSessionFlow.js +20 -9
  114. package/session/useNewSessionFlow.js.map +1 -1
  115. package/session/useSessionPageFlow.d.ts +28 -10
  116. package/session/useSessionPageFlow.d.ts.map +1 -1
  117. package/session/useSessionPageFlow.js +16 -10
  118. package/session/useSessionPageFlow.js.map +1 -1
  119. package/settings/MemorySection.d.ts.map +1 -1
  120. package/settings/MemorySection.js +1 -1
  121. package/settings/MemorySection.js.map +1 -1
  122. package/src/attachment/__tests__/prepare-image.test.ts +12 -6
  123. package/src/composer/ComposerToolbar.tsx +0 -57
  124. package/src/composer/SessionComposer.tsx +1 -13
  125. package/src/composer/index.ts +0 -1
  126. package/src/conversation/ConversationTimelineView.tsx +17 -1
  127. package/src/conversation/ConversationsWorkbench.tsx +25 -0
  128. package/src/conversation/__tests__/ConversationTimelineView.layout.test.tsx +37 -1
  129. package/src/execution/MessageThread.tsx +75 -3
  130. package/src/execution/RecalledMemoriesCard.tsx +164 -0
  131. package/src/execution/ResultView.tsx +6 -0
  132. package/src/execution/ToolCallItem.tsx +18 -1
  133. package/src/execution/__tests__/RecalledMemoriesCard.test.tsx +123 -0
  134. package/src/execution/__tests__/message-thread-recalled-memories.test.tsx +216 -0
  135. package/src/execution/__tests__/message-thread-scroll-on-send.test.tsx +141 -0
  136. package/src/execution/__tests__/message-thread-slots.test.tsx +36 -0
  137. package/src/execution/index.ts +2 -0
  138. package/src/execution/tool-categories.ts +7 -0
  139. package/src/identity-account/AccountPreferencesPanel.tsx +89 -2
  140. package/src/identity-account/__tests__/AccountPreferencesPanel.test.tsx +112 -0
  141. package/src/identity-account/__tests__/useAccountExecutionDefaults.test.tsx +34 -1
  142. package/src/identity-account/useAccountExecutionDefaults.ts +12 -2
  143. package/src/index.ts +8 -1
  144. package/src/internal/MemoryEnabledRow.tsx +92 -0
  145. package/src/internal/VirtualizedThread.tsx +5 -0
  146. package/src/internal/__tests__/useAutoScroll.layout.test.tsx +105 -4
  147. package/src/internal/__tests__/useAutoScroll.test.tsx +41 -1
  148. package/src/internal/useAutoScroll.ts +54 -0
  149. package/src/memory/MemoryProposalCard.tsx +181 -0
  150. package/src/memory/__tests__/MemoryProposalCard.test.tsx +236 -0
  151. package/src/memory/__tests__/memoryHooks.test.tsx +63 -0
  152. package/src/memory/index.ts +5 -0
  153. package/src/memory/useMemory.ts +80 -0
  154. package/src/organization/OrgPreferencesPanel.tsx +52 -3
  155. package/src/organization/__tests__/OrgPreferencesPanel.test.tsx +65 -0
  156. package/src/session/AutoApproveIndicator.tsx +45 -0
  157. package/src/session/NewSessionViewer.tsx +18 -15
  158. package/src/session/SessionViewer.tsx +70 -32
  159. package/src/session/__tests__/NewSessionViewer.test.tsx +32 -0
  160. package/src/session/__tests__/SessionViewer-autoApprove.test.tsx +244 -0
  161. package/src/session/__tests__/SessionViewer-transcriptExport.test.tsx +60 -22
  162. package/src/session/__tests__/useNewSessionFlow.test.tsx +56 -0
  163. package/src/session/__tests__/useSessionPageFlow.autoApprove.test.tsx +87 -0
  164. package/src/session/facets/SetupTab.tsx +193 -157
  165. package/src/session/facets/UsageTab.tsx +38 -38
  166. package/src/session/facets/index.ts +1 -0
  167. package/src/session/facets/primitives.tsx +169 -0
  168. package/src/session/index.ts +1 -0
  169. package/src/session/useNewSessionFlow.ts +30 -15
  170. package/src/session/useSessionPageFlow.ts +44 -20
  171. package/src/settings/MemorySection.tsx +4 -2
  172. package/src/switch/Switch.tsx +4 -0
  173. package/src/workflow/__tests__/WorkflowExecutionViewer.approvals.test.tsx +26 -7
  174. package/src/workflow/__tests__/task-presentation.test.ts +49 -12
  175. package/src/workflow/__tests__/useWorkflowCanvas.dirty.test.tsx +7 -2
  176. package/src/workflow/__tests__/useWorkflowCanvas.drag.test.tsx +7 -2
  177. package/src/workflow/__tests__/workflow-thread-scroll-on-send.test.tsx +145 -0
  178. package/src/workflow/thread/WorkflowTaskThread.tsx +59 -32
  179. package/src/workflow/thread/task-presentation.ts +52 -7
  180. package/src/workspace/WorkspaceSurface.tsx +26 -3
  181. package/styles.css +1 -1
  182. package/switch/Switch.d.ts +3 -1
  183. package/switch/Switch.d.ts.map +1 -1
  184. package/switch/Switch.js +2 -2
  185. package/switch/Switch.js.map +1 -1
  186. package/workflow/thread/WorkflowTaskThread.d.ts +13 -0
  187. package/workflow/thread/WorkflowTaskThread.d.ts.map +1 -1
  188. package/workflow/thread/WorkflowTaskThread.js +33 -25
  189. package/workflow/thread/WorkflowTaskThread.js.map +1 -1
  190. package/workflow/thread/task-presentation.d.ts.map +1 -1
  191. package/workflow/thread/task-presentation.js +50 -7
  192. package/workflow/thread/task-presentation.js.map +1 -1
  193. package/workspace/WorkspaceSurface.js +11 -2
  194. package/workspace/WorkspaceSurface.js.map +1 -1
  195. package/src/composer/__tests__/ComposerToolbar-autoApprove.test.tsx +0 -80
  196. package/src/session/__tests__/SessionViewer-autoApproveToggle.test.tsx +0 -178
@@ -6,7 +6,7 @@ import type { UseGitHubConnectionReturn } from "../github/useGitHubConnection.js
6
6
  import type { WorkspaceFileLister } from "../workspace/WorkspaceFileLister.js";
7
7
  import type { WorkspaceFileReader } from "../workspace/WorkspaceFileReader.js";
8
8
  import type { WorkspaceContentSearcher } from "../workspace/WorkspaceContentSearcher.js";
9
- import type { ComposerAutoApproveProps, InteractionModeOption } from "../composer/index.js";
9
+ import type { InteractionModeOption } from "../composer/index.js";
10
10
  import { SessionComposer } from "../composer/index.js";
11
11
  import type { HarnessOption } from "../models/harness.js";
12
12
  import type { AccountExecutionDefaults } from "../identity-account/useAccountExecutionDefaults.js";
@@ -18,6 +18,7 @@ import { useNewSessionFlow } from "./useNewSessionFlow.js";
18
18
  import { useSessionPanel } from "./useSessionPanel.js";
19
19
  import { useSessionRailViews } from "./useSessionRailViews.js";
20
20
  import { SessionPanelChip } from "./SessionPanelChip.js";
21
+ import { AutoApproveIndicator } from "./AutoApproveIndicator.js";
21
22
  import type { RuntimeEnvProvider } from "./runtime-env.js";
22
23
  import type { SessionAudience, SessionPanelMode } from "./audience.js";
23
24
  import type { SessionRunConfig } from "./run-config.js";
@@ -299,19 +300,6 @@ export function NewSessionViewer({
299
300
  // SessionViewer derives (DD-016), gating the chip and the region together.
300
301
  const panelEnabled = panelMode !== "none" && !isGuest;
301
302
 
302
- // Always-visible auto-approve toggle (#816), present before the first
303
- // message is ever typed — the walk-away user arms it here and the create
304
- // carries spec.auto_approve_all server-side. Guests never get it (the
305
- // operator-consent reasoning as the #302 host default). Memoized per
306
- // DD-010 — the composer is memo'd and an inline object would defeat it.
307
- const autoApprove = useMemo<ComposerAutoApproveProps | undefined>(
308
- () =>
309
- isGuest
310
- ? undefined
311
- : { armed: flow.autoApproveAll, onChange: flow.setAutoApproveAll },
312
- [isGuest, flow.autoApproveAll, flow.setAutoApproveAll],
313
- );
314
-
315
303
  // Guest agent binding is host configuration, not a picker interaction:
316
304
  // the composer's agent machinery (picker, env-collection, personal
317
305
  // environments — all org reads a guest token cannot make) stays fully
@@ -387,6 +375,13 @@ export function NewSessionViewer({
387
375
  harness: flow.harness,
388
376
  executionTarget: undefined,
389
377
  modelId: flow.modelId,
378
+ // Pre-session arming (#816): the switch covers the session this
379
+ // surface will create — the bootstrap execution carries
380
+ // spec.auto_approve_all. Guests never get it (they never render the
381
+ // panel either — belt and braces for the operator-consent withhold).
382
+ autoApprove: isGuest
383
+ ? undefined
384
+ : { armed: flow.autoApproveAll, onChange: flow.setAutoApproveAll },
390
385
  // Curated audiences see the configuration but cannot strip it — the
391
386
  // Setup tab renders read-only without mutation callbacks (DD-011).
392
387
  mutations: isCurated
@@ -400,6 +395,7 @@ export function NewSessionViewer({
400
395
  [
401
396
  flow.agentRef, flow.mcpServerUsages, flow.skillRefs,
402
397
  flow.sessionVariables, flow.harness, flow.modelId,
398
+ flow.autoApproveAll, flow.setAutoApproveAll, isGuest,
403
399
  isCurated, handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
404
400
  ],
405
401
  );
@@ -465,7 +461,6 @@ export function NewSessionViewer({
465
461
  onInteractionModeChange={setInteractionMode}
466
462
  showInteractionModePicker={!isGuest}
467
463
  showModelSelector={modelSelectorVisible}
468
- autoApprove={autoApprove}
469
464
  enableAttachments={!isGuest}
470
465
  defaultModelId={flow.modelId}
471
466
  onModelChange={flow.setModelId}
@@ -475,6 +470,14 @@ export function NewSessionViewer({
475
470
  ariaLabel="Start a new session"
476
471
  />
477
472
 
473
+ {/* Armed-only disclosure (#816 rework): a persisted account preference
474
+ or the host's approvalDefaults can pre-arm the session this surface
475
+ will create — the user must see that before the first send, with
476
+ the one-click way off. Guests never inherit either seed. */}
477
+ {!isGuest && flow.autoApproveAll && (
478
+ <AutoApproveIndicator onTurnOff={() => flow.setAutoApproveAll(false)} />
479
+ )}
480
+
478
481
  {footerContent}
479
482
 
480
483
  <p className="stg:text-center stg:text-[0.65rem] stg:text-muted-foreground">
@@ -17,7 +17,8 @@ import {
17
17
  WorkspaceSurface,
18
18
  type SurfaceVirtualDocument,
19
19
  } from "../workspace/WorkspaceSurface.js";
20
- import type { ComposerAutoApproveProps, InteractionModeOption, SessionComposerHandle, SessionComposerSubmitContext } from "../composer/index.js";
20
+ import type { InteractionModeOption, SessionComposerHandle, SessionComposerSubmitContext } from "../composer/index.js";
21
+ import type { AccountExecutionDefaults } from "../identity-account/useAccountExecutionDefaults.js";
21
22
  import type { ApplyResourceResult } from "../library/useApplyResource.js";
22
23
  import { SessionViewerLayout } from "./SessionViewerLayout.js";
23
24
  import { useWorkspaceEditors, isVirtualEntryId } from "../internal/store/index.js";
@@ -52,6 +53,7 @@ import { useSessionRailViews } from "./useSessionRailViews.js";
52
53
  import { useSessionPanel, type SessionPanelController } from "./useSessionPanel.js";
53
54
  import { SessionPanelChip } from "./SessionPanelChip.js";
54
55
  import { TranscriptExportMenu } from "./TranscriptExportMenu.js";
56
+ import { AutoApproveIndicator } from "./AutoApproveIndicator.js";
55
57
  import { useSessionWriteBacks } from "./useSessionWriteBacks.js";
56
58
  import { useWorkspaceReadRefs } from "./useWorkspaceReadRefs.js";
57
59
  import {
@@ -240,6 +242,17 @@ export interface SessionViewerProps {
240
242
  * server-side share policy owns. See {@link SessionRunConfig}.
241
243
  */
242
244
  readonly runConfig?: SessionRunConfig;
245
+ /**
246
+ * The user's account-level execution defaults, typically from
247
+ * `useAccountExecutionDefaults()` — same seam as
248
+ * `NewSessionViewer.accountDefaults` (DD-016). On the session page only
249
+ * the `autoApprove` default applies: it seeds the session-scoped
250
+ * auto-approve state so a declared `default_auto_approve` preference
251
+ * covers follow-ups on existing sessions too. An explicit flip of the
252
+ * Config facet's switch wins for this session. Ignored for the
253
+ * `"guest"` audience.
254
+ */
255
+ readonly accountDefaults?: AccountExecutionDefaults;
243
256
  /**
244
257
  * Whether the follow-up composer offers the model picker. `false`
245
258
  * hides the picker while keeping the SDK's model sourcing (persisted
@@ -295,14 +308,14 @@ export interface SessionViewerProps {
295
308
  */
296
309
  readonly headerActions?: ReactNode;
297
310
  /**
298
- * Whether the viewer renders its built-in whole-conversation export
299
- * control ({@link TranscriptExportMenu}) in the header corner
300
- * (stigmer/stigmer#814). Default ON a deliberate DD-011 divergence,
301
- * owner-ratified: wherever a conversation can be viewed its transcript
302
- * should be one click away, the control is an additive header button (no
303
- * rendering-strategy or DOM-structure change to existing content), and
304
- * the export carries only what the current caller can already read.
305
- * Hosts that own their export surface (or inert demo scenes) opt out.
311
+ * Whether the viewer offers its built-in whole-conversation export
312
+ * (stigmer/stigmer#814). Placement is panel-aware: viewers with the
313
+ * session panel export from the Config facet's Transcript section, and
314
+ * panel-less viewers (guests, `panel="none"`) render the header
315
+ * {@link TranscriptExportMenu} instead so wherever a conversation can
316
+ * be viewed, its transcript stays reachable. The export carries only
317
+ * what the current caller can already read (`can_view`-scoped). Hosts
318
+ * that own their export surface (or inert demo scenes) opt out.
306
319
  *
307
320
  * @default true
308
321
  */
@@ -382,6 +395,7 @@ export function SessionViewer({
382
395
  getRuntimeEnv,
383
396
  audience = "integrator",
384
397
  runConfig,
398
+ accountDefaults,
385
399
  showModelSelector = true,
386
400
  panel: panelMode = "auto",
387
401
  defaultPanelOpen,
@@ -400,6 +414,7 @@ export function SessionViewer({
400
414
  getRuntimeEnv,
401
415
  audience,
402
416
  runConfig,
417
+ accountDefaults,
403
418
  });
404
419
  const { conv } = flow;
405
420
  const isGuest = audience === "guest";
@@ -661,15 +676,18 @@ export function SessionViewer({
661
676
  return (
662
677
  <SessionViewerLayout
663
678
  className={className}
664
- // Top-right controls: host actions + the built-in transcript export +
665
- // the panel chip. The chip is the panel's always-mounted toggle; while
666
- // collapsed it carries only the pending-item count. Execution status
667
- // is never surfaced as header chrome — the thread itself communicates
668
- // run state. A viewer without a panel (guests — session configuration
669
- // is not a visitor's business — or a host's panel="none") has no
670
- // toggle: it is simply absent.
679
+ // Top-right controls: host actions + the panel chip. The chip is the
680
+ // panel's always-mounted toggle; while collapsed it carries only the
681
+ // pending-item count. Execution status is never surfaced as header
682
+ // chrome — the thread itself communicates run state. A viewer without
683
+ // a panel (guests — session configuration is not a visitor's business —
684
+ // or a host's panel="none") has no toggle: it is simply absent.
685
+ //
686
+ // Transcript export lives in the panel's Config facet; the header menu
687
+ // renders ONLY for panel-less viewers, so no audience loses export
688
+ // (guests and panel="none" hosts have no facet to reach it through).
671
689
  headerActions={
672
- transcriptExport ? (
690
+ transcriptExport && !panelEnabled ? (
673
691
  <>
674
692
  {headerActions}
675
693
  <TranscriptExportMenu sessionId={sessionId} />
@@ -745,6 +763,10 @@ export function SessionViewer({
745
763
  workspaceFileReader={workspaceFileReader}
746
764
  workspaceContentSearcher={workspaceContentSearcher}
747
765
  isCurated={isCurated}
766
+ isObserver={isObserver}
767
+ // Drives the Config facet's Transcript section only — a host
768
+ // that opted out of export gets no facet section either.
769
+ exportSessionId={transcriptExport ? sessionId : null}
748
770
  />
749
771
  ) : null
750
772
  }
@@ -849,20 +871,6 @@ const ConversationColumn = memo(function ConversationColumn({
849
871
  void conv.stop();
850
872
  }, [conv.stop]);
851
873
 
852
- // Always-visible auto-approve toggle (#816), replacing the old armed-only
853
- // indicator banner: the walk-away user needs a way ON before any gate
854
- // exists, not just a way off after one. Guests never get it (they don't
855
- // inherit the host default and this is the operator's consent surface);
856
- // observers have no composer at all. Memoized per DD-010 — the composer
857
- // is memo'd and an inline object would defeat it.
858
- const autoApprove = useMemo<ComposerAutoApproveProps | undefined>(
859
- () =>
860
- isGuest
861
- ? undefined
862
- : { armed: flow.autoApproveAll, onChange: flow.setAutoApproveAll },
863
- [isGuest, flow.autoApproveAll, flow.setAutoApproveAll],
864
- );
865
-
866
874
  // Edit-and-resubmit: stop the in-flight turn, pre-fill the composer with
867
875
  // the original text, and remember which execution is being edited. The
868
876
  // append-only execution log is never rewritten — submitting while editing
@@ -987,6 +995,13 @@ const ConversationColumn = memo(function ConversationColumn({
987
995
  />
988
996
  )}
989
997
  {!isObserver && sendError && <SendErrorBanner error={sendError} />}
998
+ {/* Armed-only disclosure (#816 rework): auto-approve's way ON lives in
999
+ the Config facet and the account preference — this strip is the
1000
+ always-visible proof it is on, plus the one-click way off. Never
1001
+ shown to observers (they cannot arm or disarm anything). */}
1002
+ {!isObserver && flow.autoApproveAll && (
1003
+ <AutoApproveIndicator onTurnOff={() => flow.setAutoApproveAll(false)} />
1004
+ )}
990
1005
  {/* Pending file reviews dock here — pinned above the composer so the
991
1006
  decision the agent is blocked on can never scroll out of view. The
992
1007
  thread renders only observational rows (badges) and read-only
@@ -1028,7 +1043,6 @@ const ConversationColumn = memo(function ConversationColumn({
1028
1043
  onInteractionModeChange={setInteractionMode}
1029
1044
  showInteractionModePicker={!isGuest}
1030
1045
  showModelSelector={modelSelectorVisible}
1031
- autoApprove={autoApprove}
1032
1046
  enableAttachments={!isGuest}
1033
1047
  workspace={isGuest ? undefined : flow.workspace}
1034
1048
  gitHubConnection={isGuest ? undefined : gitHubConnection}
@@ -1105,6 +1119,17 @@ interface SessionPanelRegionProps {
1105
1119
  readonly workspaceContentSearcher?: WorkspaceContentSearcher;
1106
1120
  /** Curated audience (endUser or guest): the Setup facet renders read-only. */
1107
1121
  readonly isCurated: boolean;
1122
+ /**
1123
+ * Observer audience (incl. channel-origin sessions): read-only viewers who
1124
+ * must never receive the Config facet's auto-approve switch — the same
1125
+ * withhold as approval submission. (Guests never render the panel at all.)
1126
+ */
1127
+ readonly isObserver: boolean;
1128
+ /**
1129
+ * Session id for the Config facet's Transcript export section, or `null`
1130
+ * when the host opted out of export (`transcriptExport={false}`).
1131
+ */
1132
+ readonly exportSessionId: string | null;
1108
1133
  }
1109
1134
 
1110
1135
  function SessionPanelRegion({
@@ -1128,6 +1153,8 @@ function SessionPanelRegion({
1128
1153
  workspaceFileReader,
1129
1154
  workspaceContentSearcher,
1130
1155
  isCurated,
1156
+ isObserver,
1157
+ exportSessionId,
1131
1158
  }: SessionPanelRegionProps) {
1132
1159
  const { editors, activeKey, activeFile, reveal } = useWorkspaceEditors(
1133
1160
  panel.editorsStore,
@@ -1295,6 +1322,16 @@ function SessionPanelRegion({
1295
1322
  harness: flow.harness,
1296
1323
  executionTarget: flow.executionTarget,
1297
1324
  modelId: flow.model[0],
1325
+ // Arming auto-approve is an operator decision — withheld from
1326
+ // observers with the same gate as approval submission (guests never
1327
+ // render the panel). The switch stays interactive mid-run: flipping
1328
+ // it ON releases the in-flight run's gates via the flow's standing
1329
+ // responder (#816).
1330
+ autoApprove: isObserver
1331
+ ? undefined
1332
+ : { armed: flow.autoApproveAll, onChange: flow.setAutoApproveAll },
1333
+ // Transcript export is view-scoped, so observers keep it (#814).
1334
+ sessionId: exportSessionId,
1298
1335
  // Curated audiences see the configuration but cannot strip it — the
1299
1336
  // Config facet renders read-only without mutation callbacks (DD-011).
1300
1337
  mutations: isCurated
@@ -1309,6 +1346,7 @@ function SessionPanelRegion({
1309
1346
  [
1310
1347
  flow.agentRef, flow.isDefaultAgent, flow.mcpServerUsages, flow.skillRefs,
1311
1348
  flow.sessionVariables, flow.harness, flow.executionTarget, flow.model,
1349
+ flow.autoApproveAll, flow.setAutoApproveAll, isObserver, exportSessionId,
1312
1350
  isCurated, handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
1313
1351
  accessSlot,
1314
1352
  ],
@@ -33,6 +33,7 @@ const emptyWorkspace = {
33
33
  clear: vi.fn(),
34
34
  };
35
35
 
36
+ const setAutoApproveAll = vi.fn();
36
37
  const stubEmptyFlow = {
37
38
  harness: "native" as const,
38
39
  setHarness: vi.fn(),
@@ -48,6 +49,8 @@ const stubEmptyFlow = {
48
49
  setSkillRefs: vi.fn(),
49
50
  workspace: emptyWorkspace,
50
51
  sessionVariables: { entries: [], isEmpty: true, clear: vi.fn() },
52
+ autoApproveAll: false,
53
+ setAutoApproveAll,
51
54
  isSubmitting: false,
52
55
  submitError: null,
53
56
  submit: vi.fn(),
@@ -125,6 +128,35 @@ describe("NewSessionViewer — persistent panel chip", () => {
125
128
  });
126
129
  });
127
130
 
131
+ describe("NewSessionViewer — auto-approve placement (#816 rework)", () => {
132
+ const INDICATOR_TEXT = "Auto-approving tool calls for this session";
133
+
134
+ it("shows no indicator while auto-approve is off — the launcher stays clean", () => {
135
+ render(<NewSessionViewer org="acme" onSessionCreated={vi.fn()} />);
136
+ expect(screen.queryByText(INDICATOR_TEXT)).toBeNull();
137
+ });
138
+
139
+ it("shows the armed-only indicator when a seed pre-arms the session, with Turn off", () => {
140
+ mockFlow = { ...stubEmptyFlow, autoApproveAll: true };
141
+ render(<NewSessionViewer org="acme" onSessionCreated={vi.fn()} />);
142
+
143
+ expect(screen.getByText(INDICATOR_TEXT)).toBeTruthy();
144
+ fireEvent.click(screen.getByRole("button", { name: "Turn off" }));
145
+ expect(setAutoApproveAll).toHaveBeenCalledExactlyOnceWith(false);
146
+ });
147
+
148
+ it("offers the auto-approve switch in the Config facet, wired to the flow", () => {
149
+ render(<NewSessionViewer org="acme" onSessionCreated={vi.fn()} />);
150
+
151
+ fireEvent.click(screen.getByRole("button", { name: "Show panel" }));
152
+ const toggle = screen.getByRole("switch", { name: "Auto-approve" });
153
+ expect(toggle.getAttribute("aria-checked")).toBe("false");
154
+
155
+ fireEvent.click(toggle);
156
+ expect(setAutoApproveAll).toHaveBeenCalledExactlyOnceWith(true);
157
+ });
158
+ });
159
+
128
160
  describe("NewSessionViewer — composer stays centered", () => {
129
161
  // The composer's wrapper is the probe's parent (wrapper > h1 + composer +
130
162
  // footer). `my-auto` is unconditional safe-centering; the old `my-6`
@@ -0,0 +1,244 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { render, screen, cleanup, fireEvent } from "@testing-library/react";
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Wiring-contract tests for auto-approve placement (the #816 rework):
6
+ //
7
+ // - The composer carries NO auto-approve control — the session-level switch
8
+ // lives in the Config facet (SetupTab receives the `autoApprove` group for
9
+ // every audience that may submit approvals, never observers; guests never
10
+ // render the panel at all).
11
+ // - The armed-only AutoApproveIndicator above the composer is the
12
+ // always-visible disclosure: present exactly while the state is armed,
13
+ // with a one-click "Turn off"; never rendered for observers.
14
+ // ---------------------------------------------------------------------------
15
+
16
+ type CapturedProps = Record<string, unknown>;
17
+
18
+ const composerProps: CapturedProps[] = [];
19
+ vi.mock("../../composer", async (importOriginal) => {
20
+ const actual = await importOriginal<typeof import("../../composer")>();
21
+ return {
22
+ ...actual,
23
+ SessionComposer: (props: CapturedProps) => {
24
+ composerProps.push(props);
25
+ return <div data-testid="composer-probe" />;
26
+ },
27
+ };
28
+ });
29
+
30
+ vi.mock("../../execution/MessageThread", () => ({
31
+ MessageThread: () => <div data-testid="thread-probe" />,
32
+ }));
33
+
34
+ vi.mock("../../execution/FileReviewDock", () => ({
35
+ FileReviewDock: () => <div data-testid="file-review-dock-probe" />,
36
+ }));
37
+
38
+ // SetupTab renders for real: the facet switch is asserted through its
39
+ // actual DOM (a switch labeled "Auto-approve" in the Run Config section),
40
+ // reached by opening the panel and selecting the Config rail view.
41
+
42
+ const stubWorkspace = {
43
+ entries: [],
44
+ hasEntries: false,
45
+ toInput: vi.fn().mockReturnValue([]),
46
+ addGitRepo: vi.fn(),
47
+ addLocalPath: vi.fn(),
48
+ removeEntry: vi.fn(),
49
+ clear: vi.fn(),
50
+ };
51
+
52
+ const stubConv = {
53
+ session: { spec: {} } as Record<string, unknown>,
54
+ isLoading: false,
55
+ loadError: null,
56
+ completedExecutions: [],
57
+ activeStreamExecution: null,
58
+ activePhase: null,
59
+ isStreaming: false,
60
+ isConnecting: false,
61
+ sendFollowUp: vi.fn(),
62
+ canSendFollowUp: true,
63
+ isSending: false,
64
+ sendError: null,
65
+ clearSendError: vi.fn(),
66
+ pendingUserMessage: null,
67
+ workspaceEntries: [],
68
+ mcpServerUsages: [],
69
+ skillRefs: [],
70
+ pendingApprovals: [],
71
+ submitApproval: vi.fn(),
72
+ submittingApprovalIds: new Set<string>(),
73
+ fileChangeSets: [],
74
+ submitFileDecision: vi.fn(),
75
+ submittingFileDecisionKeys: new Set<string>(),
76
+ fileDecisionErrors: new Map<string, Error>(),
77
+ streamError: null,
78
+ reconnectStream: vi.fn(),
79
+ approvalError: null,
80
+ retryLastSend: vi.fn(),
81
+ };
82
+
83
+ const setAutoApproveAll = vi.fn();
84
+ const stubSessionPageFlow = {
85
+ conv: stubConv,
86
+ harness: "native" as const,
87
+ executionTarget: undefined,
88
+ model: [undefined, vi.fn()] as const,
89
+ interactionMode: ["agent" as const, vi.fn()] as const,
90
+ agentRef: { org: "acme", slug: "support-bot" },
91
+ setAgentRef: vi.fn(),
92
+ resolution: null,
93
+ setResolution: vi.fn(),
94
+ isDefaultAgent: false,
95
+ mcpServerUsages: [],
96
+ setMcpServerUsages: vi.fn(),
97
+ skillRefs: [],
98
+ setSkillRefs: vi.fn(),
99
+ workspace: stubWorkspace,
100
+ sessionVariables: { entries: [], isEmpty: true, clear: vi.fn() },
101
+ autoApproveAll: false,
102
+ setAutoApproveAll,
103
+ submitApproval: vi.fn(),
104
+ handleSubmit: vi.fn(),
105
+ submitError: null as Error | null,
106
+ displayExecution: null,
107
+ allExecutions: [],
108
+ sandboxWorkspaceRoot: undefined,
109
+ };
110
+ vi.mock("../useSessionPageFlow", () => ({
111
+ useSessionPageFlow: () => stubSessionPageFlow,
112
+ }));
113
+
114
+ vi.mock("../../hooks", () => ({
115
+ useStigmer: () => ({
116
+ agentExecution: {
117
+ uploadAttachment: vi.fn(),
118
+ getArtifactContent: vi.fn(),
119
+ },
120
+ }),
121
+ }));
122
+
123
+ import { SessionViewer } from "../SessionViewer";
124
+
125
+ const INDICATOR_TEXT = "Auto-approving tool calls for this session";
126
+
127
+ function lastComposerProps(): CapturedProps {
128
+ expect(composerProps.length).toBeGreaterThan(0);
129
+ return composerProps.at(-1)!;
130
+ }
131
+
132
+ /** Selects the panel's Config facet (the rail's radio). */
133
+ function openConfigFacet() {
134
+ fireEvent.click(screen.getByRole("radio", { name: "Config" }));
135
+ }
136
+
137
+ function facetSwitch(): HTMLElement | null {
138
+ return screen.queryByRole("switch", { name: "Auto-approve" });
139
+ }
140
+
141
+ beforeEach(() => {
142
+ composerProps.length = 0;
143
+ stubConv.session = { spec: {} };
144
+ stubSessionPageFlow.autoApproveAll = false;
145
+ });
146
+
147
+ afterEach(() => {
148
+ cleanup();
149
+ vi.clearAllMocks();
150
+ });
151
+
152
+ describe("SessionViewer — composer stays clean (#816 rework)", () => {
153
+ it("hands the composer no auto-approve control at all", () => {
154
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
155
+
156
+ expect("autoApprove" in lastComposerProps()).toBe(false);
157
+ });
158
+ });
159
+
160
+ describe("SessionViewer — Config facet auto-approve switch (#816 rework)", () => {
161
+ it("renders the switch reflecting the flow state, and flipping calls the flow (integrator)", () => {
162
+ render(<SessionViewer sessionId="ses_1" org="acme" defaultPanelOpen />);
163
+ openConfigFacet();
164
+
165
+ const toggle = facetSwitch();
166
+ expect(toggle).not.toBeNull();
167
+ expect(toggle!.getAttribute("aria-checked")).toBe("false");
168
+ fireEvent.click(toggle!);
169
+ expect(setAutoApproveAll).toHaveBeenCalledExactlyOnceWith(true);
170
+ });
171
+
172
+ it("mirrors an armed flow state into the switch", () => {
173
+ stubSessionPageFlow.autoApproveAll = true;
174
+ render(<SessionViewer sessionId="ses_1" org="acme" defaultPanelOpen />);
175
+ openConfigFacet();
176
+
177
+ expect(facetSwitch()!.getAttribute("aria-checked")).toBe("true");
178
+ });
179
+
180
+ it("endUser gets the switch — the walk-away persona is an end user", () => {
181
+ render(
182
+ <SessionViewer
183
+ sessionId="ses_1"
184
+ org="acme"
185
+ audience="endUser"
186
+ defaultPanelOpen
187
+ />,
188
+ );
189
+ openConfigFacet();
190
+
191
+ expect(facetSwitch()).not.toBeNull();
192
+ });
193
+
194
+ it("observers never get the switch — the approval-submission withhold", () => {
195
+ render(
196
+ <SessionViewer
197
+ sessionId="ses_1"
198
+ org="acme"
199
+ audience="observer"
200
+ defaultPanelOpen
201
+ />,
202
+ );
203
+ openConfigFacet();
204
+
205
+ expect(facetSwitch()).toBeNull();
206
+ });
207
+
208
+ it("channel-origin sessions never get the switch (observer by construction)", () => {
209
+ stubConv.session = {
210
+ spec: {},
211
+ metadata: { labels: { "stigmer.ai/channel-id": "chn_1" } },
212
+ };
213
+ render(<SessionViewer sessionId="ses_1" org="acme" defaultPanelOpen />);
214
+ openConfigFacet();
215
+
216
+ expect(facetSwitch()).toBeNull();
217
+ });
218
+ });
219
+
220
+ describe("SessionViewer — armed-only indicator (#816 rework)", () => {
221
+ it("absent while auto-approve is off — the composer stays clean", () => {
222
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
223
+
224
+ expect(screen.queryByText(INDICATOR_TEXT)).toBeNull();
225
+ });
226
+
227
+ it("renders while armed, and Turn off flips the flow state", () => {
228
+ stubSessionPageFlow.autoApproveAll = true;
229
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
230
+
231
+ expect(screen.getByText(INDICATOR_TEXT)).toBeTruthy();
232
+ fireEvent.click(screen.getByRole("button", { name: "Turn off" }));
233
+ expect(setAutoApproveAll).toHaveBeenCalledExactlyOnceWith(false);
234
+ });
235
+
236
+ it("never renders for observers — they cannot arm or disarm anything", () => {
237
+ stubSessionPageFlow.autoApproveAll = true;
238
+ render(
239
+ <SessionViewer sessionId="ses_1" org="acme" audience="observer" />,
240
+ );
241
+
242
+ expect(screen.queryByText(INDICATOR_TEXT)).toBeNull();
243
+ });
244
+ });