@stigmer/react 3.12.6 → 3.12.8

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 (219) hide show
  1. package/composer/ComposerToolbar.d.ts.map +1 -1
  2. package/composer/ComposerToolbar.js.map +1 -1
  3. package/execution/MessageThread.d.ts.map +1 -1
  4. package/execution/MessageThread.js +8 -11
  5. package/execution/MessageThread.js.map +1 -1
  6. package/execution/ResultView.d.ts.map +1 -1
  7. package/execution/ResultView.js +6 -0
  8. package/execution/ResultView.js.map +1 -1
  9. package/execution/ToolCallItem.d.ts.map +1 -1
  10. package/execution/ToolCallItem.js +13 -2
  11. package/execution/ToolCallItem.js.map +1 -1
  12. package/execution/tool-categories.d.ts +1 -1
  13. package/execution/tool-categories.d.ts.map +1 -1
  14. package/execution/tool-categories.js +6 -0
  15. package/execution/tool-categories.js.map +1 -1
  16. package/execution/useFileChangeContent.d.ts +2 -6
  17. package/execution/useFileChangeContent.d.ts.map +1 -1
  18. package/execution/useFileChangeContent.js +4 -12
  19. package/execution/useFileChangeContent.js.map +1 -1
  20. package/identity-account/AccountPreferencesPanel.d.ts +2 -1
  21. package/identity-account/AccountPreferencesPanel.d.ts.map +1 -1
  22. package/identity-account/AccountPreferencesPanel.js +41 -3
  23. package/identity-account/AccountPreferencesPanel.js.map +1 -1
  24. package/identity-account/useAccountExecutionDefaults.d.ts +7 -0
  25. package/identity-account/useAccountExecutionDefaults.d.ts.map +1 -1
  26. package/identity-account/useAccountExecutionDefaults.js +5 -2
  27. package/identity-account/useAccountExecutionDefaults.js.map +1 -1
  28. package/index.d.ts +5 -2
  29. package/index.d.ts.map +1 -1
  30. package/index.js +5 -1
  31. package/index.js.map +1 -1
  32. package/internal/MemoryEnabledRow.d.ts +37 -0
  33. package/internal/MemoryEnabledRow.d.ts.map +1 -0
  34. package/internal/MemoryEnabledRow.js +25 -0
  35. package/internal/MemoryEnabledRow.js.map +1 -0
  36. package/internal/download.d.ts +8 -0
  37. package/internal/download.d.ts.map +1 -0
  38. package/internal/download.js +25 -0
  39. package/internal/download.js.map +1 -0
  40. package/library/useExportResource.d.ts.map +1 -1
  41. package/library/useExportResource.js +3 -17
  42. package/library/useExportResource.js.map +1 -1
  43. package/memory/MemoryListPanel.d.ts +35 -0
  44. package/memory/MemoryListPanel.d.ts.map +1 -0
  45. package/memory/MemoryListPanel.js +141 -0
  46. package/memory/MemoryListPanel.js.map +1 -0
  47. package/memory/MemoryProposalCard.d.ts +35 -0
  48. package/memory/MemoryProposalCard.d.ts.map +1 -0
  49. package/memory/MemoryProposalCard.js +73 -0
  50. package/memory/MemoryProposalCard.js.map +1 -0
  51. package/memory/index.d.ts +10 -0
  52. package/memory/index.d.ts.map +1 -0
  53. package/memory/index.js +14 -0
  54. package/memory/index.js.map +1 -0
  55. package/memory/memoryGroups.d.ts +31 -0
  56. package/memory/memoryGroups.d.ts.map +1 -0
  57. package/memory/memoryGroups.js +52 -0
  58. package/memory/memoryGroups.js.map +1 -0
  59. package/memory/useConfirmMemory.d.ts +31 -0
  60. package/memory/useConfirmMemory.d.ts.map +1 -0
  61. package/memory/useConfirmMemory.js +43 -0
  62. package/memory/useConfirmMemory.js.map +1 -0
  63. package/memory/useDeleteMemory.d.ts +30 -0
  64. package/memory/useDeleteMemory.d.ts.map +1 -0
  65. package/memory/useDeleteMemory.js +42 -0
  66. package/memory/useDeleteMemory.js.map +1 -0
  67. package/memory/useMemories.d.ts +33 -0
  68. package/memory/useMemories.d.ts.map +1 -0
  69. package/memory/useMemories.js +42 -0
  70. package/memory/useMemories.js.map +1 -0
  71. package/memory/useMemory.d.ts +39 -0
  72. package/memory/useMemory.d.ts.map +1 -0
  73. package/memory/useMemory.js +43 -0
  74. package/memory/useMemory.js.map +1 -0
  75. package/memory/useRejectMemory.d.ts +34 -0
  76. package/memory/useRejectMemory.d.ts.map +1 -0
  77. package/memory/useRejectMemory.js +46 -0
  78. package/memory/useRejectMemory.js.map +1 -0
  79. package/memory/useUpdateMemoryContent.d.ts +32 -0
  80. package/memory/useUpdateMemoryContent.d.ts.map +1 -0
  81. package/memory/useUpdateMemoryContent.js +48 -0
  82. package/memory/useUpdateMemoryContent.js.map +1 -0
  83. package/organization/OrgPreferencesPanel.d.ts.map +1 -1
  84. package/organization/OrgPreferencesPanel.js +36 -4
  85. package/organization/OrgPreferencesPanel.js.map +1 -1
  86. package/package.json +4 -4
  87. package/session/AutoApproveIndicator.d.ts +18 -0
  88. package/session/AutoApproveIndicator.d.ts.map +1 -0
  89. package/session/AutoApproveIndicator.js +23 -0
  90. package/session/AutoApproveIndicator.js.map +1 -0
  91. package/session/NewSessionViewer.d.ts.map +1 -1
  92. package/session/NewSessionViewer.js +10 -1
  93. package/session/NewSessionViewer.js.map +1 -1
  94. package/session/SessionViewer.d.ts +26 -1
  95. package/session/SessionViewer.d.ts.map +1 -1
  96. package/session/SessionViewer.js +28 -22
  97. package/session/SessionViewer.js.map +1 -1
  98. package/session/TranscriptExportMenu.d.ts +24 -0
  99. package/session/TranscriptExportMenu.d.ts.map +1 -0
  100. package/session/TranscriptExportMenu.js +26 -0
  101. package/session/TranscriptExportMenu.js.map +1 -0
  102. package/session/facets/SetupTab.d.ts +43 -6
  103. package/session/facets/SetupTab.d.ts.map +1 -1
  104. package/session/facets/SetupTab.js +32 -36
  105. package/session/facets/SetupTab.js.map +1 -1
  106. package/session/facets/UsageTab.d.ts +2 -1
  107. package/session/facets/UsageTab.d.ts.map +1 -1
  108. package/session/facets/UsageTab.js +11 -5
  109. package/session/facets/UsageTab.js.map +1 -1
  110. package/session/facets/index.d.ts +1 -1
  111. package/session/facets/index.d.ts.map +1 -1
  112. package/session/facets/primitives.d.ts +58 -0
  113. package/session/facets/primitives.d.ts.map +1 -0
  114. package/session/facets/primitives.js +59 -0
  115. package/session/facets/primitives.js.map +1 -0
  116. package/session/index.d.ts +5 -1
  117. package/session/index.d.ts.map +1 -1
  118. package/session/index.js +2 -0
  119. package/session/index.js.map +1 -1
  120. package/session/useExportTranscript.d.ts +44 -0
  121. package/session/useExportTranscript.d.ts.map +1 -0
  122. package/session/useExportTranscript.js +109 -0
  123. package/session/useExportTranscript.js.map +1 -0
  124. package/session/useNewSessionFlow.d.ts +16 -0
  125. package/session/useNewSessionFlow.d.ts.map +1 -1
  126. package/session/useNewSessionFlow.js +25 -6
  127. package/session/useNewSessionFlow.js.map +1 -1
  128. package/session/useSessionConversation.d.ts.map +1 -1
  129. package/session/useSessionConversation.js +7 -19
  130. package/session/useSessionConversation.js.map +1 -1
  131. package/session/useSessionExecutions.d.ts +2 -14
  132. package/session/useSessionExecutions.d.ts.map +1 -1
  133. package/session/useSessionExecutions.js +4 -26
  134. package/session/useSessionExecutions.js.map +1 -1
  135. package/session/useSessionPageFlow.d.ts +41 -10
  136. package/session/useSessionPageFlow.d.ts.map +1 -1
  137. package/session/useSessionPageFlow.js +67 -9
  138. package/session/useSessionPageFlow.js.map +1 -1
  139. package/settings/MemorySection.d.ts +12 -0
  140. package/settings/MemorySection.d.ts.map +1 -0
  141. package/settings/MemorySection.js +21 -0
  142. package/settings/MemorySection.js.map +1 -0
  143. package/settings/index.d.ts +1 -0
  144. package/settings/index.d.ts.map +1 -1
  145. package/settings/index.js +1 -0
  146. package/settings/index.js.map +1 -1
  147. package/settings/settings-nav.d.ts.map +1 -1
  148. package/settings/settings-nav.js +2 -1
  149. package/settings/settings-nav.js.map +1 -1
  150. package/src/__tests__/public-api.test.ts +21 -0
  151. package/src/attachment/__tests__/prepare-image.test.ts +12 -6
  152. package/src/composer/ComposerToolbar.tsx +1 -0
  153. package/src/execution/MessageThread.tsx +8 -11
  154. package/src/execution/ResultView.tsx +6 -0
  155. package/src/execution/ToolCallItem.tsx +18 -1
  156. package/src/execution/tool-categories.ts +7 -0
  157. package/src/execution/useFileChangeContent.ts +5 -12
  158. package/src/identity-account/AccountPreferencesPanel.tsx +89 -2
  159. package/src/identity-account/__tests__/AccountPreferencesPanel.test.tsx +112 -0
  160. package/src/identity-account/__tests__/useAccountExecutionDefaults.test.tsx +34 -1
  161. package/src/identity-account/useAccountExecutionDefaults.ts +12 -2
  162. package/src/index.ts +38 -0
  163. package/src/internal/MemoryEnabledRow.tsx +92 -0
  164. package/src/internal/download.ts +29 -0
  165. package/src/library/useExportResource.ts +3 -19
  166. package/src/memory/MemoryListPanel.tsx +532 -0
  167. package/src/memory/MemoryProposalCard.tsx +181 -0
  168. package/src/memory/__tests__/MemoryListPanel.test.tsx +259 -0
  169. package/src/memory/__tests__/MemoryProposalCard.test.tsx +236 -0
  170. package/src/memory/__tests__/memoryHooks.test.tsx +283 -0
  171. package/src/memory/index.ts +24 -0
  172. package/src/memory/memoryGroups.ts +64 -0
  173. package/src/memory/useConfirmMemory.ts +62 -0
  174. package/src/memory/useDeleteMemory.ts +61 -0
  175. package/src/memory/useMemories.ts +66 -0
  176. package/src/memory/useMemory.ts +80 -0
  177. package/src/memory/useRejectMemory.ts +65 -0
  178. package/src/memory/useUpdateMemoryContent.ts +67 -0
  179. package/src/organization/OrgPreferencesPanel.tsx +52 -3
  180. package/src/organization/__tests__/OrgPreferencesPanel.test.tsx +65 -0
  181. package/src/session/AutoApproveIndicator.tsx +45 -0
  182. package/src/session/NewSessionViewer.tsx +17 -0
  183. package/src/session/SessionViewer.tsx +78 -41
  184. package/src/session/TranscriptExportMenu.tsx +78 -0
  185. package/src/session/__tests__/NewSessionViewer.test.tsx +32 -0
  186. package/src/session/__tests__/SessionViewer-autoApprove.test.tsx +244 -0
  187. package/src/session/__tests__/SessionViewer-transcriptExport.test.tsx +189 -0
  188. package/src/session/__tests__/TranscriptExportMenu.test.tsx +97 -0
  189. package/src/session/__tests__/useExportTranscript.test.tsx +187 -0
  190. package/src/session/__tests__/useNewSessionFlow.test.tsx +89 -0
  191. package/src/session/__tests__/useSessionPageFlow.autoApprove.test.tsx +263 -2
  192. package/src/session/facets/SetupTab.tsx +193 -157
  193. package/src/session/facets/UsageTab.tsx +38 -38
  194. package/src/session/facets/index.ts +1 -0
  195. package/src/session/facets/primitives.tsx +169 -0
  196. package/src/session/index.ts +10 -0
  197. package/src/session/useExportTranscript.ts +171 -0
  198. package/src/session/useNewSessionFlow.ts +42 -6
  199. package/src/session/useSessionConversation.ts +10 -16
  200. package/src/session/useSessionExecutions.ts +5 -26
  201. package/src/session/useSessionPageFlow.ts +108 -21
  202. package/src/settings/MemorySection.tsx +45 -0
  203. package/src/settings/index.ts +1 -0
  204. package/src/settings/settings-nav.ts +2 -0
  205. package/src/switch/Switch.tsx +4 -0
  206. package/src/usage/useExportCSV.ts +2 -14
  207. package/src/workflow/__tests__/useWorkflowCanvas.dirty.test.tsx +7 -2
  208. package/src/workflow/__tests__/useWorkflowCanvas.drag.test.tsx +7 -2
  209. package/src/workspace/WorkspaceSurface.tsx +26 -3
  210. package/styles.css +1 -1
  211. package/switch/Switch.d.ts +3 -1
  212. package/switch/Switch.d.ts.map +1 -1
  213. package/switch/Switch.js +2 -2
  214. package/switch/Switch.js.map +1 -1
  215. package/usage/useExportCSV.d.ts.map +1 -1
  216. package/usage/useExportCSV.js +2 -13
  217. package/usage/useExportCSV.js.map +1 -1
  218. package/workspace/WorkspaceSurface.js +11 -2
  219. package/workspace/WorkspaceSurface.js.map +1 -1
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import type { Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
5
+ import { useStigmer } from "../hooks.js";
6
+ import { toError } from "../internal/toError.js";
7
+
8
+ /** Return value of {@link useRejectMemory}. */
9
+ export interface UseRejectMemoryReturn {
10
+ /** Reject a proposed memory by its resource ID. Resolves with the rejected record. */
11
+ readonly rejectMemory: (id: string) => Promise<Memory>;
12
+ /** `true` while the reject request is in flight. */
13
+ readonly isRejecting: boolean;
14
+ /** Error from the last failed reject, or `null` when healthy. */
15
+ readonly error: Error | null;
16
+ /** Reset `error` to `null`. */
17
+ readonly clearError: () => void;
18
+ }
19
+
20
+ /**
21
+ * Behavior hook that wraps `memory.reject()`. A rejected memory is never
22
+ * recalled; the record is kept as an audit trail until deleted.
23
+ *
24
+ * Rejecting an already-rejected memory succeeds and changes nothing.
25
+ * Rejecting a confirmed memory fails with FAILED_PRECONDITION —
26
+ * deleting a confirmed memory is how it is revoked.
27
+ *
28
+ * Rejection is deliberately one click on every surface, with no
29
+ * confirmation dialog (DD-005 D4): expensive review teaches users to
30
+ * ignore the proposal queue.
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * const { rejectMemory, isRejecting } = useRejectMemory();
35
+ *
36
+ * await rejectMemory(memory.metadata?.id ?? "");
37
+ * refetch(); // refresh the list after the decision
38
+ * ```
39
+ */
40
+ export function useRejectMemory(): UseRejectMemoryReturn {
41
+ const stigmer = useStigmer();
42
+ const [isRejecting, setIsRejecting] = useState(false);
43
+ const [error, setError] = useState<Error | null>(null);
44
+
45
+ const clearError = useCallback(() => setError(null), []);
46
+
47
+ const rejectMemory = useCallback(
48
+ async (id: string): Promise<Memory> => {
49
+ setIsRejecting(true);
50
+ setError(null);
51
+
52
+ try {
53
+ return await stigmer.memory.reject(id);
54
+ } catch (err) {
55
+ setError(toError(err));
56
+ throw err;
57
+ } finally {
58
+ setIsRejecting(false);
59
+ }
60
+ },
61
+ [stigmer],
62
+ );
63
+
64
+ return { rejectMemory, isRejecting, error, clearError };
65
+ }
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import type { Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
5
+ import { toMemoryUpdateInput } from "@stigmer/sdk";
6
+ import { useStigmer } from "../hooks.js";
7
+ import { toError } from "../internal/toError.js";
8
+
9
+ /** Return value of {@link useUpdateMemoryContent}. */
10
+ export interface UseUpdateMemoryContentReturn {
11
+ /** Replace a memory's fact text. Resolves with the updated record. */
12
+ readonly updateContent: (memory: Memory, content: string) => Promise<Memory>;
13
+ /** `true` while the update request is in flight. */
14
+ readonly isUpdating: boolean;
15
+ /** Error from the last failed update, or `null` when healthy. */
16
+ readonly error: Error | null;
17
+ /** Reset `error` to `null`. */
18
+ readonly clearError: () => void;
19
+ }
20
+
21
+ /**
22
+ * Behavior hook that edits a memory's fact text — the one field the
23
+ * subject owns after capture (DD-004).
24
+ *
25
+ * The update RPC replaces the entire spec, so the loaded record is
26
+ * mapped through the generated `toMemoryUpdateInput` and only `content`
27
+ * is overridden — the wipe-safe editing rule. Subject and provenance
28
+ * ride along verbatim (they are update-immutable server-side), and the
29
+ * consent lifecycle is never touched by updates.
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * const { updateContent, isUpdating } = useUpdateMemoryContent();
34
+ *
35
+ * await updateContent(memory, editedText);
36
+ * refetch(); // refresh the list after the edit
37
+ * ```
38
+ */
39
+ export function useUpdateMemoryContent(): UseUpdateMemoryContentReturn {
40
+ const stigmer = useStigmer();
41
+ const [isUpdating, setIsUpdating] = useState(false);
42
+ const [error, setError] = useState<Error | null>(null);
43
+
44
+ const clearError = useCallback(() => setError(null), []);
45
+
46
+ const updateContent = useCallback(
47
+ async (memory: Memory, content: string): Promise<Memory> => {
48
+ setIsUpdating(true);
49
+ setError(null);
50
+
51
+ try {
52
+ return await stigmer.memory.update({
53
+ ...toMemoryUpdateInput(memory),
54
+ content,
55
+ });
56
+ } catch (err) {
57
+ setError(toError(err));
58
+ throw err;
59
+ } finally {
60
+ setIsUpdating(false);
61
+ }
62
+ },
63
+ [stigmer],
64
+ );
65
+
66
+ return { updateContent, isUpdating, error, clearError };
67
+ }
@@ -7,6 +7,7 @@ import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organizati
7
7
  import { useOrganization } from "./useOrganization.js";
8
8
  import { useUpdateOrganization } from "./useUpdateOrganization.js";
9
9
  import { useCheckPermission } from "../iam-policy/useCheckPermission.js";
10
+ import { MemoryEnabledRow } from "../internal/MemoryEnabledRow.js";
10
11
  import { StandingContextField } from "../internal/StandingContextField.js";
11
12
  import { SpinnerIcon } from "../internal/SpinnerIcon.js";
12
13
 
@@ -60,6 +61,14 @@ export function OrgPreferencesPanel({
60
61
  clearError,
61
62
  } = useUpdateOrganization();
62
63
 
64
+ // The memory toggle saves instantly (its own hook instance, so a flip's
65
+ // in-flight/error state never bleeds into the form's Save button).
66
+ const {
67
+ update: updateMemoryFlag,
68
+ isUpdating: isSavingMemoryFlag,
69
+ error: memoryFlagError,
70
+ } = useUpdateOrganization();
71
+
63
72
  // Fail-open: OSS local mode has no IAM service, so editing stays
64
73
  // available there; cloud gets a genuine server verdict.
65
74
  const { allowed: canEdit } = useCheckPermission(
@@ -100,10 +109,16 @@ export function OrgPreferencesPanel({
100
109
  clearError();
101
110
  try {
102
111
  // update() is a full-spec replace: spread the complete mapped input
103
- // so unedited spec fields survive, and override only preferences.
112
+ // so unedited spec fields survive. `preferences` is a nested message,
113
+ // so the override spreads the mapper's COMPLETE preferences too —
114
+ // fields this form does not own (memory_enabled) survive the save.
115
+ const mapped = toOrganizationUpdateInput(organization);
104
116
  const updated = await update({
105
- ...toOrganizationUpdateInput(organization),
106
- preferences: { standingContext: standingContext.trim() || undefined },
117
+ ...mapped,
118
+ preferences: {
119
+ ...mapped.preferences,
120
+ standingContext: standingContext.trim() || undefined,
121
+ },
107
122
  });
108
123
  refetch();
109
124
  onUpdated?.(updated);
@@ -114,6 +129,30 @@ export function OrgPreferencesPanel({
114
129
  [canSubmit, organization, standingContext, update, clearError, refetch, onUpdated],
115
130
  );
116
131
 
132
+ // The memory consent flag applies instantly (the UX-checkpoint decision):
133
+ // a consent bit flipped-but-unsaved that silently reverts on navigation is
134
+ // the failure consent UX must not have. Same wipe-safe double spread.
135
+ const handleMemoryToggle = useCallback(
136
+ async (next: boolean) => {
137
+ if (!organization) return;
138
+ try {
139
+ const mapped = toOrganizationUpdateInput(organization);
140
+ const updated = await updateMemoryFlag({
141
+ ...mapped,
142
+ preferences: {
143
+ ...mapped.preferences,
144
+ memoryEnabled: next || undefined,
145
+ },
146
+ });
147
+ refetch();
148
+ onUpdated?.(updated);
149
+ } catch {
150
+ // error state is managed by the toggle's own hook instance
151
+ }
152
+ },
153
+ [organization, updateMemoryFlag, refetch, onUpdated],
154
+ );
155
+
117
156
  // -----------------------------------------------------------------------
118
157
  // Loading state
119
158
  // -----------------------------------------------------------------------
@@ -175,6 +214,16 @@ export function OrgPreferencesPanel({
175
214
  helperText="Shared with agents as background context — not instructions. Applies to executions started by signed-in members of this organization and is visible on their execution records."
176
215
  />
177
216
 
217
+ <MemoryEnabledRow
218
+ id={`${baseId}-memory-enabled`}
219
+ checked={organization.spec?.preferences?.memoryEnabled ?? false}
220
+ onToggle={(next) => void handleMemoryToggle(next)}
221
+ saving={isSavingMemoryFlag}
222
+ readOnly={!canEdit}
223
+ error={memoryFlagError}
224
+ helperText="Allow agents to remember confirmed facts about members of this organization. Each member must also turn memory on in their own account preferences. Changes apply immediately."
225
+ />
226
+
178
227
  {!canEdit && (
179
228
  <p className="stg:text-[0.65rem] stg:text-muted-foreground">
180
229
  Only organization admins can edit these preferences.
@@ -125,6 +125,71 @@ describe("OrgPreferencesPanel", () => {
125
125
  expect(screen.queryByRole("button", { name: "Save changes" })).toBeNull();
126
126
  });
127
127
 
128
+ it("memory toggle applies instantly with the full mapped input (wipe-safe)", async () => {
129
+ const update = vi.fn(async (_input: OrganizationInput) => ORG);
130
+ renderPanel(createMockStigmer({ update }));
131
+ await findSyncedField("We deploy to us-east-1.");
132
+
133
+ // No Save click: consent applies the moment it is flipped (the
134
+ // UX-checkpoint decision — a flipped-but-unsaved consent bit that
135
+ // silently reverts on navigation is the failure this prevents).
136
+ fireEvent.click(screen.getByRole("switch", { name: "Memory" }));
137
+
138
+ await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
139
+ const input = update.mock.calls[0]![0];
140
+ expect(input.preferences?.memoryEnabled).toBe(true);
141
+ // The double spread: the flip must not wipe the sibling preference…
142
+ expect(input.preferences?.standingContext).toBe("We deploy to us-east-1.");
143
+ // …nor any profile field this row never renders.
144
+ expect(input.name).toBe("Acme Corp");
145
+ expect(input.description).toBe("We make everything.");
146
+ });
147
+
148
+ it("saving standing context preserves memory_enabled (the wipe-hazard regression)", async () => {
149
+ const orgWithMemoryOn = create(OrganizationSchema, {
150
+ metadata: { id: "acme", name: "Acme Corp", slug: "acme", org: "acme" },
151
+ spec: {
152
+ description: "We make everything.",
153
+ logoUrl: "https://acme.example/logo.png",
154
+ preferences: {
155
+ standingContext: "We deploy to us-east-1.",
156
+ memoryEnabled: true,
157
+ },
158
+ },
159
+ });
160
+ const update = vi.fn(async (_input: OrganizationInput) => orgWithMemoryOn);
161
+ const client = {
162
+ organization: {
163
+ get: vi.fn(async () => orgWithMemoryOn),
164
+ update,
165
+ },
166
+ iamPolicy: {
167
+ checkMyPermission: vi.fn(async () => ({ isAuthorized: true })),
168
+ },
169
+ } as never;
170
+ renderPanel(client);
171
+
172
+ const field = await findSyncedField("We deploy to us-east-1.");
173
+ fireEvent.change(field, { target: { value: "Prefer terse answers." } });
174
+ fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
175
+
176
+ await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
177
+ // Update is a full-spec replace: without the nested preferences
178
+ // spread, this save would silently switch the org's memory OFF.
179
+ expect(update.mock.calls[0]![0].preferences?.memoryEnabled).toBe(true);
180
+ });
181
+
182
+ it("memory toggle is read-only without can_edit", async () => {
183
+ renderPanel(createMockStigmer({ canEdit: false }));
184
+ await screen.findByLabelText("Standing context");
185
+
186
+ await waitFor(() =>
187
+ expect(
188
+ screen.getByRole("switch", { name: "Memory" }).hasAttribute("disabled"),
189
+ ).toBe(true),
190
+ );
191
+ });
192
+
128
193
  it("shows the fetch error with a retry action when loading fails", async () => {
129
194
  const client = {
130
195
  organization: {
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Armed-only indicator for the session-scoped auto-approve state, docked
5
+ * above the composer. Renders nothing about approvals until auto-approve is
6
+ * actually ON — the composer stays clean by default — and while ON it is the
7
+ * always-visible safety surface: the user can never be silently
8
+ * auto-approving without seeing this strip and its one-click "Turn off".
9
+ *
10
+ * The way ON lives elsewhere (the Config facet's Run Config switch, the
11
+ * gate-time "Approve & don't ask again", the account's `default_auto_approve`
12
+ * preference, the host's `approvalDefaults`); this strip is deliberately only
13
+ * the way OFF plus the disclosure. Shared by `SessionViewer` and
14
+ * `NewSessionViewer` (DD-016) — a persisted account preference can arm the
15
+ * launcher before the first message, so both surfaces need the disclosure.
16
+ */
17
+ export function AutoApproveIndicator({ onTurnOff }: { readonly onTurnOff: () => void }) {
18
+ return (
19
+ <div
20
+ role="status"
21
+ className="stg:flex stg:items-center stg:gap-2 stg:border-t stg:border-border-muted stg:px-4 stg:py-1.5 stg:text-xs stg:text-muted-foreground"
22
+ >
23
+ <ShieldCheckIcon />
24
+ <span className="stg:min-w-0 stg:flex-1 stg:truncate">
25
+ Auto-approving tool calls for this session
26
+ </span>
27
+ <button
28
+ type="button"
29
+ onClick={onTurnOff}
30
+ className="stg:shrink-0 stg:rounded stg:font-medium stg:text-foreground stg:underline-offset-2 stg:hover:underline stg:focus-visible:outline-none stg:focus-visible:ring-2 stg:focus-visible:ring-ring"
31
+ >
32
+ Turn off
33
+ </button>
34
+ </div>
35
+ );
36
+ }
37
+
38
+ function ShieldCheckIcon() {
39
+ return (
40
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="stg:shrink-0 stg:text-success" aria-hidden="true">
41
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
42
+ <path d="m9 12 2 2 4-4" />
43
+ </svg>
44
+ );
45
+ }
@@ -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";
@@ -374,6 +375,13 @@ export function NewSessionViewer({
374
375
  harness: flow.harness,
375
376
  executionTarget: undefined,
376
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 },
377
385
  // Curated audiences see the configuration but cannot strip it — the
378
386
  // Setup tab renders read-only without mutation callbacks (DD-011).
379
387
  mutations: isCurated
@@ -387,6 +395,7 @@ export function NewSessionViewer({
387
395
  [
388
396
  flow.agentRef, flow.mcpServerUsages, flow.skillRefs,
389
397
  flow.sessionVariables, flow.harness, flow.modelId,
398
+ flow.autoApproveAll, flow.setAutoApproveAll, isGuest,
390
399
  isCurated, handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
391
400
  ],
392
401
  );
@@ -461,6 +470,14 @@ export function NewSessionViewer({
461
470
  ariaLabel="Start a new session"
462
471
  />
463
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
+
464
481
  {footerContent}
465
482
 
466
483
  <p className="stg:text-center stg:text-[0.65rem] stg:text-muted-foreground">
@@ -18,6 +18,7 @@ import {
18
18
  type SurfaceVirtualDocument,
19
19
  } from "../workspace/WorkspaceSurface.js";
20
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";
@@ -51,6 +52,8 @@ import { ArtifactDocument } from "../execution/ArtifactDocument.js";
51
52
  import { useSessionRailViews } from "./useSessionRailViews.js";
52
53
  import { useSessionPanel, type SessionPanelController } from "./useSessionPanel.js";
53
54
  import { SessionPanelChip } from "./SessionPanelChip.js";
55
+ import { TranscriptExportMenu } from "./TranscriptExportMenu.js";
56
+ import { AutoApproveIndicator } from "./AutoApproveIndicator.js";
54
57
  import { useSessionWriteBacks } from "./useSessionWriteBacks.js";
55
58
  import { useWorkspaceReadRefs } from "./useWorkspaceReadRefs.js";
56
59
  import {
@@ -239,6 +242,17 @@ export interface SessionViewerProps {
239
242
  * server-side share policy owns. See {@link SessionRunConfig}.
240
243
  */
241
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;
242
256
  /**
243
257
  * Whether the follow-up composer offers the model picker. `false`
244
258
  * hides the picker while keeping the SDK's model sourcing (persisted
@@ -293,6 +307,19 @@ export interface SessionViewerProps {
293
307
  * unopinionated about Console auth (DD-004).
294
308
  */
295
309
  readonly headerActions?: ReactNode;
310
+ /**
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.
319
+ *
320
+ * @default true
321
+ */
322
+ readonly transcriptExport?: boolean;
296
323
  /**
297
324
  * Host-injected access management control (e.g. the Console's
298
325
  * `ManageAccessButton` with its own permission gating). Rendered inside
@@ -368,12 +395,14 @@ export function SessionViewer({
368
395
  getRuntimeEnv,
369
396
  audience = "integrator",
370
397
  runConfig,
398
+ accountDefaults,
371
399
  showModelSelector = true,
372
400
  panel: panelMode = "auto",
373
401
  defaultPanelOpen,
374
402
  panelOpen,
375
403
  onPanelOpenChange,
376
404
  headerActions,
405
+ transcriptExport = true,
377
406
  accessSlot,
378
407
  onApplied,
379
408
  threadSlots,
@@ -385,6 +414,7 @@ export function SessionViewer({
385
414
  getRuntimeEnv,
386
415
  audience,
387
416
  runConfig,
417
+ accountDefaults,
388
418
  });
389
419
  const { conv } = flow;
390
420
  const isGuest = audience === "guest";
@@ -650,9 +680,22 @@ export function SessionViewer({
650
680
  // panel's always-mounted toggle; while collapsed it carries only the
651
681
  // pending-item count. Execution status is never surfaced as header
652
682
  // chrome — the thread itself communicates run state. A viewer without
653
- // a panel (guests — session configuration is not a visitor's business
654
- // or a host's panel="none") has no toggle: it is simply absent.
655
- headerActions={headerActions}
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).
689
+ headerActions={
690
+ transcriptExport && !panelEnabled ? (
691
+ <>
692
+ {headerActions}
693
+ <TranscriptExportMenu sessionId={sessionId} />
694
+ </>
695
+ ) : (
696
+ headerActions
697
+ )
698
+ }
656
699
  chip={
657
700
  panelEnabled ? (
658
701
  <SessionPanelChip
@@ -720,6 +763,10 @@ export function SessionViewer({
720
763
  workspaceFileReader={workspaceFileReader}
721
764
  workspaceContentSearcher={workspaceContentSearcher}
722
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}
723
770
  />
724
771
  ) : null
725
772
  }
@@ -948,6 +995,10 @@ const ConversationColumn = memo(function ConversationColumn({
948
995
  />
949
996
  )}
950
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). */}
951
1002
  {!isObserver && flow.autoApproveAll && (
952
1003
  <AutoApproveIndicator onTurnOff={() => flow.setAutoApproveAll(false)} />
953
1004
  )}
@@ -1068,6 +1119,17 @@ interface SessionPanelRegionProps {
1068
1119
  readonly workspaceContentSearcher?: WorkspaceContentSearcher;
1069
1120
  /** Curated audience (endUser or guest): the Setup facet renders read-only. */
1070
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;
1071
1133
  }
1072
1134
 
1073
1135
  function SessionPanelRegion({
@@ -1091,6 +1153,8 @@ function SessionPanelRegion({
1091
1153
  workspaceFileReader,
1092
1154
  workspaceContentSearcher,
1093
1155
  isCurated,
1156
+ isObserver,
1157
+ exportSessionId,
1094
1158
  }: SessionPanelRegionProps) {
1095
1159
  const { editors, activeKey, activeFile, reveal } = useWorkspaceEditors(
1096
1160
  panel.editorsStore,
@@ -1258,6 +1322,16 @@ function SessionPanelRegion({
1258
1322
  harness: flow.harness,
1259
1323
  executionTarget: flow.executionTarget,
1260
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,
1261
1335
  // Curated audiences see the configuration but cannot strip it — the
1262
1336
  // Config facet renders read-only without mutation callbacks (DD-011).
1263
1337
  mutations: isCurated
@@ -1272,6 +1346,7 @@ function SessionPanelRegion({
1272
1346
  [
1273
1347
  flow.agentRef, flow.isDefaultAgent, flow.mcpServerUsages, flow.skillRefs,
1274
1348
  flow.sessionVariables, flow.harness, flow.executionTarget, flow.model,
1349
+ flow.autoApproveAll, flow.setAutoApproveAll, isObserver, exportSessionId,
1275
1350
  isCurated, handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
1276
1351
  accessSlot,
1277
1352
  ],
@@ -1386,36 +1461,6 @@ function SessionStarting() {
1386
1461
  );
1387
1462
  }
1388
1463
 
1389
- /**
1390
- * Low-weight, always-visible indicator shown while the session-scoped
1391
- * auto-approve preference is active. The "Turn off" control reverts the
1392
- * preference in one click — the safety affordance for "Approve & don't ask
1393
- * again". Visible from the first render when the host pre-armed the
1394
- * preference via `StigmerProvider`'s `approvalDefaults` (#302); otherwise
1395
- * nothing about approvals appears until the user opts in at a gate. Either
1396
- * way the user keeps the last word.
1397
- */
1398
- function AutoApproveIndicator({ onTurnOff }: { onTurnOff: () => void }) {
1399
- return (
1400
- <div
1401
- role="status"
1402
- className="stg:flex stg:items-center stg:gap-2 stg:border-t stg:border-border-muted stg:px-4 stg:py-1.5 stg:text-xs stg:text-muted-foreground"
1403
- >
1404
- <ShieldCheckIcon />
1405
- <span className="stg:min-w-0 stg:flex-1 stg:truncate">
1406
- Auto-approving tool calls for this session
1407
- </span>
1408
- <button
1409
- type="button"
1410
- onClick={onTurnOff}
1411
- className="stg:shrink-0 stg:rounded stg:font-medium stg:text-foreground stg:underline-offset-2 stg:hover:underline stg:focus-visible:outline-none stg:focus-visible:ring-2 stg:focus-visible:ring-ring"
1412
- >
1413
- Turn off
1414
- </button>
1415
- </div>
1416
- );
1417
- }
1418
-
1419
1464
  /**
1420
1465
  * The plan document tab's empty state — reachable only when a streaming plan
1421
1466
  * auto-opened the tab and its turn then ended without publishing (stopped or
@@ -1616,11 +1661,3 @@ function LoaderIcon() {
1616
1661
  );
1617
1662
  }
1618
1663
 
1619
- function ShieldCheckIcon() {
1620
- return (
1621
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="stg:shrink-0 stg:text-success" aria-hidden="true">
1622
- <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
1623
- <path d="m9 12 2 2 4-4" />
1624
- </svg>
1625
- );
1626
- }