@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
@@ -0,0 +1,141 @@
1
+ // Scroll-on-send wiring pins for MessageThread (stigmer-cloud#267): the
2
+ // send moment is the optimistic message's empty→present transition, and it
3
+ // must pin the thread exactly once — default-ON, opt-out via
4
+ // `scrollOnSend={false}` (the ratified DD-011 divergence). The REAL scroll
5
+ // mechanics (pin + follow re-engagement under real layout) are pinned in
6
+ // `internal/__tests__/useAutoScroll.layout.test.tsx`; this file pins the
7
+ // surface's signal derivation through a spied `jumpToLatest`, with the real
8
+ // `usePinToLatestOnSignal` connecting them.
9
+
10
+ import { describe, it, expect, vi, afterEach } from "vitest";
11
+ import { render, cleanup } from "@testing-library/react";
12
+ import { create } from "@bufbuild/protobuf";
13
+ import {
14
+ AgentExecutionSchema,
15
+ type AgentExecution,
16
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
17
+ import { AgentExecutionSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
18
+ import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
19
+
20
+ const jumpToLatestSpy = vi.fn();
21
+
22
+ // The fake replaces only the scroll machine (browser observers happy-dom
23
+ // lacks); `usePinToLatestOnSignal` stays REAL, so these tests exercise the
24
+ // actual signal contract between MessageThread and its render paths.
25
+ vi.mock("../../internal/useAutoScroll", async (importOriginal) => {
26
+ const original =
27
+ await importOriginal<typeof import("../../internal/useAutoScroll")>();
28
+ return {
29
+ ...original,
30
+ useAutoScroll: () => ({
31
+ scrollRef: { current: null },
32
+ sentinelRef: { current: null },
33
+ contentRef: () => {},
34
+ isFollowing: true,
35
+ jumpToLatest: jumpToLatestSpy,
36
+ }),
37
+ };
38
+ });
39
+
40
+ import { MessageThread } from "../MessageThread";
41
+
42
+ afterEach(() => {
43
+ cleanup();
44
+ jumpToLatestSpy.mockClear();
45
+ });
46
+
47
+ function makeExecution(id: string, message: string): AgentExecution {
48
+ const exec = create(AgentExecutionSchema);
49
+ const meta = create(ApiResourceMetadataSchema);
50
+ meta.id = id;
51
+ exec.metadata = meta;
52
+ const spec = create(AgentExecutionSpecSchema);
53
+ spec.message = message;
54
+ exec.spec = spec;
55
+ return exec;
56
+ }
57
+
58
+ describe("MessageThread — scroll-on-send (stigmer-cloud#267)", () => {
59
+ it("pins the thread exactly once when the optimistic message appears (default-on)", () => {
60
+ const executions = [makeExecution("aex_1", "earlier turn")];
61
+ const { rerender } = render(
62
+ <MessageThread executions={executions} pendingUserMessage={null} />,
63
+ );
64
+ expect(jumpToLatestSpy).not.toHaveBeenCalled();
65
+
66
+ // The send: the composer sets the optimistic message.
67
+ rerender(
68
+ <MessageThread executions={executions} pendingUserMessage="do the thing" />,
69
+ );
70
+ expect(jumpToLatestSpy).toHaveBeenCalledTimes(1);
71
+
72
+ // Re-renders while the message stays pending must not re-pin — the
73
+ // reader may have scrolled up again to read history mid-turn.
74
+ rerender(
75
+ <MessageThread executions={executions} pendingUserMessage="do the thing" />,
76
+ );
77
+ expect(jumpToLatestSpy).toHaveBeenCalledTimes(1);
78
+ });
79
+
80
+ it("pins again on the NEXT send after the pending message clears", () => {
81
+ const executions = [makeExecution("aex_1", "earlier turn")];
82
+ const { rerender } = render(
83
+ <MessageThread executions={executions} pendingUserMessage={null} />,
84
+ );
85
+
86
+ rerender(
87
+ <MessageThread executions={executions} pendingUserMessage="first send" />,
88
+ );
89
+ rerender(<MessageThread executions={executions} pendingUserMessage={null} />);
90
+ rerender(
91
+ <MessageThread executions={executions} pendingUserMessage="second send" />,
92
+ );
93
+
94
+ expect(jumpToLatestSpy).toHaveBeenCalledTimes(2);
95
+ });
96
+
97
+ it("does not re-pin on a failed send's retry — pending stays present throughout, and the reader was already brought to it", () => {
98
+ const executions = [makeExecution("aex_1", "earlier turn")];
99
+ const { rerender } = render(
100
+ <MessageThread executions={executions} pendingUserMessage={null} />,
101
+ );
102
+
103
+ rerender(
104
+ <MessageThread executions={executions} pendingUserMessage="flaky send" />,
105
+ );
106
+ rerender(
107
+ <MessageThread
108
+ executions={executions}
109
+ pendingUserMessage="flaky send"
110
+ pendingMessageFailed
111
+ onRetrySend={() => {}}
112
+ />,
113
+ );
114
+ rerender(
115
+ <MessageThread executions={executions} pendingUserMessage="flaky send" />,
116
+ );
117
+
118
+ expect(jumpToLatestSpy).toHaveBeenCalledTimes(1);
119
+ });
120
+
121
+ it("never pins with scrollOnSend={false} — the opt-out preserves today's leave-the-reader-alone behavior", () => {
122
+ const executions = [makeExecution("aex_1", "earlier turn")];
123
+ const { rerender } = render(
124
+ <MessageThread
125
+ executions={executions}
126
+ pendingUserMessage={null}
127
+ scrollOnSend={false}
128
+ />,
129
+ );
130
+
131
+ rerender(
132
+ <MessageThread
133
+ executions={executions}
134
+ pendingUserMessage="do the thing"
135
+ scrollOnSend={false}
136
+ />,
137
+ );
138
+
139
+ expect(jumpToLatestSpy).not.toHaveBeenCalled();
140
+ });
141
+ });
@@ -5,11 +5,13 @@ import { create } from "@bufbuild/protobuf";
5
5
  import {
6
6
  AgentExecutionSchema,
7
7
  AgentExecutionStatusSchema,
8
+ RecalledMemoriesReportSchema,
8
9
  type AgentExecution,
9
10
  } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
10
11
  import {
11
12
  AgentExecutionSpecSchema,
12
13
  ExecutionConfigSchema,
14
+ RecalledMemoriesSchema,
13
15
  } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
14
16
  import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
15
17
  import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
@@ -32,6 +34,7 @@ import type { ExecutionErrorNoticeProps } from "../ExecutionErrorNotice";
32
34
  import type { TodoCardProps } from "../TodoCard";
33
35
  import type { TodoRowProps } from "../TodoList";
34
36
  import type { SetupProgressProps } from "../SetupProgress";
37
+ import type { RecalledMemoriesCardProps } from "../RecalledMemoriesCard";
35
38
  import type { LivenessStatusLineProps } from "../LivenessStatusLine";
36
39
  import type { PlanCompletionCardProps } from "../PlanCompletionCard";
37
40
  import type { PlanArtifactCardProps } from "../PlanArtifactCard";
@@ -318,6 +321,39 @@ describe("MessageThread slots", () => {
318
321
  expect(screen.getByTestId("custom-setup").textContent).toBe("waiting");
319
322
  });
320
323
 
324
+ it("renders the RecalledMemoriesCard slot for a selection-active execution", () => {
325
+ const CustomRecalled = ({ report, facts }: RecalledMemoriesCardProps) => (
326
+ <div data-testid="custom-recalled">
327
+ {report.injectedMemoryIds.length} of {facts.length}
328
+ </div>
329
+ );
330
+
331
+ const exec = makeExecution({ id: "e1" });
332
+ exec.spec!.recalledMemories = create(RecalledMemoriesSchema, {
333
+ enabled: true,
334
+ facts: [
335
+ { memoryId: "mem_a", content: "Prefers concise answers." },
336
+ { memoryId: "mem_b", content: "Deploys with Bazel." },
337
+ ],
338
+ });
339
+ exec.status!.recalledMemoriesReport = create(RecalledMemoriesReportSchema, {
340
+ selectionActive: true,
341
+ injectedMemoryIds: ["mem_a"],
342
+ embeddingModel: "text-embedding-3-small",
343
+ });
344
+
345
+ render(
346
+ <MessageThread
347
+ executions={[exec]}
348
+ slots={{ RecalledMemoriesCard: CustomRecalled }}
349
+ />,
350
+ );
351
+
352
+ expect(screen.getByTestId("custom-recalled").textContent).toBe("1 of 2");
353
+ // The built-in card must not render alongside the override.
354
+ expect(screen.queryByRole("status", { name: /Recalled/ })).toBeNull();
355
+ });
356
+
321
357
  it("renders the LivenessStatusLine slot while the execution is live between events", () => {
322
358
  const CustomLiveness = ({ label }: LivenessStatusLineProps) => (
323
359
  <div data-testid="custom-liveness">{label ?? "custom-live"}</div>
@@ -42,6 +42,8 @@ export type { SummarizationBadgeProps } from "./SummarizationBadge.js";
42
42
 
43
43
  export { SummarizationCard } from "./SummarizationCard.js";
44
44
  export type { SummarizationCardProps } from "./SummarizationCard.js";
45
+ export { RecalledMemoriesCard, resolveInjectedFacts } from "./RecalledMemoriesCard.js";
46
+ export type { RecalledMemoriesCardProps } from "./RecalledMemoriesCard.js";
45
47
 
46
48
  export { PlanCompletionCard } from "./PlanCompletionCard.js";
47
49
  export type { PlanCompletionCardProps } from "./PlanCompletionCard.js";
@@ -32,6 +32,7 @@ export type ToolCategory =
32
32
  | "sub-agent"
33
33
  | "internal"
34
34
  | "mcp"
35
+ | "memory"
35
36
  | "unknown";
36
37
 
37
38
  /**
@@ -122,6 +123,11 @@ const PREVIEW_CATEGORIES: ReadonlySet<ToolCategory> = new Set<ToolCategory>([
122
123
  "mcp",
123
124
  "web-search",
124
125
  "fetch",
126
+ // A memory proposal is a decision surface: the verbatim fact and the
127
+ // Confirm/Reject actions ARE the content — never fold it behind a chevron
128
+ // (DD-005 D4: a proposal the user scrolled past is caught by the memory
129
+ // page, but the chip must not hide itself while in view).
130
+ "memory",
125
131
  "unknown",
126
132
  ]);
127
133
 
@@ -232,6 +238,7 @@ const KIND_DISPLAY: Partial<Record<ToolKind, KindDisplayEntry>> = {
232
238
  [ToolKind.THINK]: { category: "think", label: "Thinking", primaryField: "thought" },
233
239
  [ToolKind.SUBAGENT]: { category: "sub-agent", label: "Sub-agent", primaryField: "description", fallbackFields: ["prompt"] },
234
240
  [ToolKind.TODO]: { category: "internal", label: "Todos", primaryField: "todos" },
241
+ [ToolKind.MEMORY]: { category: "memory", label: "Remember", primaryField: "fact" },
235
242
  };
236
243
 
237
244
  /**
@@ -18,8 +18,10 @@ import { useResourceAvailable, ApiResourceKind } from "../deployment-mode.js";
18
18
  import { useModelRegistry } from "../models/index.js";
19
19
  import { HARNESS_META, type HarnessOption } from "../models/harness.js";
20
20
  import { CloudFeatureNotice } from "../internal/CloudFeatureNotice.js";
21
+ import { MemoryEnabledRow } from "../internal/MemoryEnabledRow.js";
21
22
  import { StandingContextField } from "../internal/StandingContextField.js";
22
23
  import { SpinnerIcon } from "../internal/SpinnerIcon.js";
24
+ import { Switch } from "../switch/Switch.js";
23
25
 
24
26
  /** Props for {@link AccountPreferencesPanel}. */
25
27
  export interface AccountPreferencesPanelProps {
@@ -32,7 +34,8 @@ export interface AccountPreferencesPanelProps {
32
34
  /**
33
35
  * Self-contained editor for the current user's declared preferences
34
36
  * (`IdentityAccountSpec.preferences`): standing context plus the
35
- * execution defaults (default harness and per-harness default models).
37
+ * execution defaults (default harness, per-harness default models, and
38
+ * the auto-approve default).
36
39
  *
37
40
  * The declared text is snapshotted into the user's own eligible agent
38
41
  * executions and delivered to the agent as background context.
@@ -108,16 +111,26 @@ function AccountPreferencesForm({
108
111
  clearError,
109
112
  } = useUpdateIdentityAccount();
110
113
 
114
+ // The memory toggle saves instantly (its own hook instance, so a flip's
115
+ // in-flight/error state never bleeds into the form's Save button).
116
+ const {
117
+ update: updateMemoryFlag,
118
+ isUpdating: isSavingMemoryFlag,
119
+ error: memoryFlagError,
120
+ } = useUpdateIdentityAccount();
121
+
111
122
  const [standingContext, setStandingContext] = useState("");
112
123
  const [defaultHarness, setDefaultHarness] = useState("");
113
124
  const [defaultNativeModel, setDefaultNativeModel] = useState("");
114
125
  const [defaultCursorModel, setDefaultCursorModel] = useState("");
126
+ const [defaultAutoApprove, setDefaultAutoApprove] = useState(false);
115
127
 
116
128
  const serverPreferences = account?.spec?.preferences;
117
129
  const serverStandingContext = serverPreferences?.standingContext ?? "";
118
130
  const serverDefaultHarness = serverPreferences?.defaultHarness ?? "";
119
131
  const serverDefaultNativeModel = serverPreferences?.defaultNativeModel ?? "";
120
132
  const serverDefaultCursorModel = serverPreferences?.defaultCursorModel ?? "";
133
+ const serverDefaultAutoApprove = serverPreferences?.defaultAutoApprove ?? false;
121
134
 
122
135
  // Sync the form fields when server data changes.
123
136
  useEffect(() => {
@@ -127,6 +140,7 @@ function AccountPreferencesForm({
127
140
  setDefaultHarness(prefs?.defaultHarness ?? "");
128
141
  setDefaultNativeModel(prefs?.defaultNativeModel ?? "");
129
142
  setDefaultCursorModel(prefs?.defaultCursorModel ?? "");
143
+ setDefaultAutoApprove(prefs?.defaultAutoApprove ?? false);
130
144
  }, [account]);
131
145
 
132
146
  const hasChanges = useMemo(
@@ -134,16 +148,19 @@ function AccountPreferencesForm({
134
148
  standingContext.trim() !== serverStandingContext ||
135
149
  defaultHarness !== serverDefaultHarness ||
136
150
  defaultNativeModel !== serverDefaultNativeModel ||
137
- defaultCursorModel !== serverDefaultCursorModel,
151
+ defaultCursorModel !== serverDefaultCursorModel ||
152
+ defaultAutoApprove !== serverDefaultAutoApprove,
138
153
  [
139
154
  standingContext,
140
155
  defaultHarness,
141
156
  defaultNativeModel,
142
157
  defaultCursorModel,
158
+ defaultAutoApprove,
143
159
  serverStandingContext,
144
160
  serverDefaultHarness,
145
161
  serverDefaultNativeModel,
146
162
  serverDefaultCursorModel,
163
+ serverDefaultAutoApprove,
147
164
  ],
148
165
  );
149
166
 
@@ -154,12 +171,14 @@ function AccountPreferencesForm({
154
171
  setDefaultHarness(serverDefaultHarness);
155
172
  setDefaultNativeModel(serverDefaultNativeModel);
156
173
  setDefaultCursorModel(serverDefaultCursorModel);
174
+ setDefaultAutoApprove(serverDefaultAutoApprove);
157
175
  clearError();
158
176
  }, [
159
177
  serverStandingContext,
160
178
  serverDefaultHarness,
161
179
  serverDefaultNativeModel,
162
180
  serverDefaultCursorModel,
181
+ serverDefaultAutoApprove,
163
182
  clearError,
164
183
  ]);
165
184
 
@@ -183,6 +202,7 @@ function AccountPreferencesForm({
183
202
  defaultHarness: defaultHarness || undefined,
184
203
  defaultNativeModel: defaultNativeModel || undefined,
185
204
  defaultCursorModel: defaultCursorModel || undefined,
205
+ defaultAutoApprove: defaultAutoApprove || undefined,
186
206
  },
187
207
  });
188
208
  refetch();
@@ -198,6 +218,7 @@ function AccountPreferencesForm({
198
218
  defaultHarness,
199
219
  defaultNativeModel,
200
220
  defaultCursorModel,
221
+ defaultAutoApprove,
201
222
  update,
202
223
  clearError,
203
224
  refetch,
@@ -205,6 +226,30 @@ function AccountPreferencesForm({
205
226
  ],
206
227
  );
207
228
 
229
+ // The memory consent flag applies instantly (the UX-checkpoint decision):
230
+ // a consent bit flipped-but-unsaved that silently reverts on navigation is
231
+ // the failure consent UX must not have. Same wipe-safe double spread.
232
+ const handleMemoryToggle = useCallback(
233
+ async (next: boolean) => {
234
+ if (!account) return;
235
+ try {
236
+ const mapped = toIdentityAccountUpdateInput(account);
237
+ const updated = await updateMemoryFlag({
238
+ ...mapped,
239
+ preferences: {
240
+ ...mapped.preferences,
241
+ memoryEnabled: next || undefined,
242
+ },
243
+ });
244
+ refetch();
245
+ onUpdated?.(updated);
246
+ } catch {
247
+ // error state is managed by the toggle's own hook instance
248
+ }
249
+ },
250
+ [account, updateMemoryFlag, refetch, onUpdated],
251
+ );
252
+
208
253
  // -----------------------------------------------------------------------
209
254
  // Loading state
210
255
  // -----------------------------------------------------------------------
@@ -330,8 +375,50 @@ function AccountPreferencesForm({
330
375
  on that harness — even when it isn&apos;t your default.
331
376
  </p>
332
377
  </fieldset>
378
+
379
+ {/* Auto-approve default (stigmer/stigmer#816): a client-read seed
380
+ like the harness/model defaults above — the session panel's
381
+ Config switch beats it per conversation, and armed sessions
382
+ always show the "Auto-approving tool calls" strip by the
383
+ composer. Never applied to guest or observer surfaces. */}
384
+ <div
385
+ className={cn(
386
+ "stg:flex stg:items-start stg:justify-between stg:gap-3 stg:rounded-md stg:border stg:border-input stg:px-3 stg:py-2.5",
387
+ isUpdating && "stg:opacity-50",
388
+ )}
389
+ >
390
+ <div className="stg:min-w-0">
391
+ <span
392
+ id={`${baseId}-default-auto-approve-title`}
393
+ className="stg:block stg:text-xs stg:font-medium stg:text-foreground"
394
+ >
395
+ Auto-approve tool calls
396
+ </span>
397
+ <p className="stg:text-[0.65rem] stg:text-muted-foreground">
398
+ Start your sessions with tool calls running unattended — no
399
+ approval prompts. Each conversation can turn this off (or on)
400
+ in its session panel.
401
+ </p>
402
+ </div>
403
+ <Switch
404
+ checked={defaultAutoApprove}
405
+ onCheckedChange={setDefaultAutoApprove}
406
+ disabled={isUpdating}
407
+ aria-labelledby={`${baseId}-default-auto-approve-title`}
408
+ className="stg:mt-0.5"
409
+ />
410
+ </div>
333
411
  </div>
334
412
 
413
+ <MemoryEnabledRow
414
+ id={`${baseId}-memory-enabled`}
415
+ checked={account.spec?.preferences?.memoryEnabled ?? false}
416
+ onToggle={(next) => void handleMemoryToggle(next)}
417
+ saving={isSavingMemoryFlag}
418
+ error={memoryFlagError}
419
+ helperText="When on, agents may propose facts to remember about you; only facts you confirm are stored. Confirmed memories are shared with your future sessions and appear on those executions' records; once you have many, each conversation recalls the most relevant ones, shown on the execution. Requires your organization to have memory enabled. Changes apply immediately."
420
+ />
421
+
335
422
  {updateError && (
336
423
  <p className="stg:text-destructive stg:text-[0.65rem]" role="alert">
337
424
  {getUserMessage(updateError)}
@@ -115,6 +115,52 @@ describe("AccountPreferencesPanel", () => {
115
115
  expect(input.slug).toBe("ada");
116
116
  });
117
117
 
118
+ it("saves the auto-approve default when the toggle is flipped on", async () => {
119
+ const update = vi.fn(async (_input: IdentityAccountInput) => ACCOUNT);
120
+ renderPanel(createMockStigmer({ update }));
121
+
122
+ await findSyncedField("Keep answers terse.");
123
+ const toggle = screen.getByRole("switch", { name: "Auto-approve tool calls" });
124
+ expect(toggle.getAttribute("aria-checked")).toBe("false");
125
+
126
+ fireEvent.click(toggle);
127
+ fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
128
+
129
+ await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
130
+ const input = update.mock.calls[0]![0];
131
+ expect(input.preferences?.defaultAutoApprove).toBe(true);
132
+ });
133
+
134
+ it("reflects a declared auto-approve default and omits it when turned off", async () => {
135
+ const armedAccount = create(IdentityAccountSchema, {
136
+ metadata: { id: "ia-1", name: "Ada Lovelace", slug: "ada", org: "acme" },
137
+ spec: {
138
+ idpId: "auth0|abc",
139
+ email: "ada@acme.example",
140
+ preferences: { defaultAutoApprove: true },
141
+ },
142
+ });
143
+ const update = vi.fn(async (_input: IdentityAccountInput) => armedAccount);
144
+ renderPanel(
145
+ createMockStigmer({ whoAmI: vi.fn(async () => armedAccount), update }),
146
+ );
147
+
148
+ const toggle = await screen.findByRole("switch", {
149
+ name: "Auto-approve tool calls",
150
+ });
151
+ await waitFor(() =>
152
+ expect(toggle.getAttribute("aria-checked")).toBe("true"),
153
+ );
154
+
155
+ fireEvent.click(toggle);
156
+ fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
157
+
158
+ await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
159
+ // Only a declared true travels — off is the absent zero value.
160
+ const input = update.mock.calls[0]![0];
161
+ expect(input.preferences?.defaultAutoApprove).toBeUndefined();
162
+ });
163
+
118
164
  it("refetches the account after a successful save", async () => {
119
165
  const whoAmI = vi.fn(async () => ACCOUNT);
120
166
  renderPanel(createMockStigmer({ whoAmI }));
@@ -137,6 +183,72 @@ describe("AccountPreferencesPanel", () => {
137
183
  expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy();
138
184
  });
139
185
 
186
+ describe("memory consent (oss#293 Phase 2 Stage 3)", () => {
187
+ it("memory toggle applies instantly with the full mapped input (wipe-safe)", async () => {
188
+ const update = vi.fn(async (_input: IdentityAccountInput) => ACCOUNT);
189
+ renderPanel(createMockStigmer({ update }));
190
+ await findSyncedField("Keep answers terse.");
191
+
192
+ // No Save click: consent applies the moment it is flipped (the
193
+ // UX-checkpoint decision).
194
+ fireEvent.click(screen.getByRole("switch", { name: "Memory" }));
195
+
196
+ await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
197
+ const input = update.mock.calls[0]![0];
198
+ expect(input.preferences?.memoryEnabled).toBe(true);
199
+ // The double spread: the flip preserves every sibling preference…
200
+ expect(input.preferences?.standingContext).toBe("Keep answers terse.");
201
+ // …and the identity fields the row never renders.
202
+ expect(input.id).toBe("ia-1");
203
+ expect(input.email).toBe("ada@acme.example");
204
+ });
205
+
206
+ it("saving the form preserves memory_enabled (the wipe-hazard regression)", async () => {
207
+ const accountWithMemoryOn = create(IdentityAccountSchema, {
208
+ metadata: { id: "ia-1", name: "Ada Lovelace", slug: "ada", org: "acme" },
209
+ spec: {
210
+ idpId: "auth0|abc",
211
+ email: "ada@acme.example",
212
+ preferences: {
213
+ standingContext: "Keep answers terse.",
214
+ memoryEnabled: true,
215
+ },
216
+ },
217
+ });
218
+ const update = vi.fn(async (_input: IdentityAccountInput) => accountWithMemoryOn);
219
+ renderPanel(
220
+ createMockStigmer({ whoAmI: vi.fn(async () => accountWithMemoryOn), update }),
221
+ );
222
+
223
+ const field = await findSyncedField("Keep answers terse.");
224
+ fireEvent.change(field, { target: { value: "Terser still." } });
225
+ fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
226
+
227
+ await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
228
+ // Update is a full-spec replace: without the nested preferences
229
+ // spread, this save would silently revoke the memory consent.
230
+ expect(update.mock.calls[0]![0].preferences?.memoryEnabled).toBe(true);
231
+ });
232
+
233
+ it("reflects the stored flag as the switch state", async () => {
234
+ const accountWithMemoryOn = create(IdentityAccountSchema, {
235
+ metadata: { id: "ia-1", name: "Ada", slug: "ada", org: "acme" },
236
+ spec: {
237
+ idpId: "auth0|abc",
238
+ preferences: { memoryEnabled: true },
239
+ },
240
+ });
241
+ renderPanel(createMockStigmer({ whoAmI: vi.fn(async () => accountWithMemoryOn) }));
242
+ await findSyncedField("");
243
+
244
+ await waitFor(() =>
245
+ expect(
246
+ screen.getByRole("switch", { name: "Memory" }).getAttribute("aria-checked"),
247
+ ).toBe("true"),
248
+ );
249
+ });
250
+ });
251
+
140
252
  describe("execution defaults (oss#293 Phase 1.5)", () => {
141
253
  const ACCOUNT_WITH_DEFAULTS: IdentityAccount = create(IdentityAccountSchema, {
142
254
  metadata: { id: "ia-1", name: "Ada Lovelace", slug: "ada", org: "acme" },
@@ -12,7 +12,7 @@ import { DeploymentModeContext } from "../../deployment-mode";
12
12
  import { useAccountExecutionDefaults } from "../useAccountExecutionDefaults";
13
13
 
14
14
  function accountWithPreferences(
15
- preferences: Record<string, string>,
15
+ preferences: Record<string, string | boolean>,
16
16
  ): IdentityAccount {
17
17
  return create(IdentityAccountSchema, {
18
18
  metadata: { id: "ia-1", name: "Ada", slug: "ada", org: "acme" },
@@ -102,6 +102,39 @@ describe("useAccountExecutionDefaults", () => {
102
102
  );
103
103
  });
104
104
 
105
+ it("a declared auto-approve default alone counts as a declared default", async () => {
106
+ const whoAmI = vi.fn(async () =>
107
+ accountWithPreferences({ defaultAutoApprove: true }),
108
+ );
109
+ const { result } = renderHook(() => useAccountExecutionDefaults(), {
110
+ wrapper: createWrapper(whoAmI),
111
+ });
112
+
113
+ await waitFor(() =>
114
+ expect(result.current).toEqual({
115
+ harness: undefined,
116
+ nativeModel: undefined,
117
+ cursorModel: undefined,
118
+ autoApprove: true,
119
+ }),
120
+ );
121
+ });
122
+
123
+ it("an unset auto-approve bool is no preference, not an explicit false", async () => {
124
+ const whoAmI = vi.fn(async () =>
125
+ accountWithPreferences({
126
+ defaultHarness: "native",
127
+ defaultAutoApprove: false,
128
+ }),
129
+ );
130
+ const { result } = renderHook(() => useAccountExecutionDefaults(), {
131
+ wrapper: createWrapper(whoAmI),
132
+ });
133
+
134
+ await waitFor(() => expect(result.current?.harness).toBe("native"));
135
+ expect(result.current?.autoApprove).toBeUndefined();
136
+ });
137
+
105
138
  it("returns undefined on a whoAmI failure — degrades to platform defaults", async () => {
106
139
  const whoAmI = vi.fn(async () => {
107
140
  throw new Error("boom");
@@ -16,6 +16,13 @@ export interface AccountExecutionDefaults {
16
16
  readonly nativeModel?: string;
17
17
  /** Preferred model for cursor-harness sessions, when declared. */
18
18
  readonly cursorModel?: string;
19
+ /**
20
+ * Start sessions with "auto-approve tool calls" armed
21
+ * (`default_auto_approve`, stigmer/stigmer#816). A seed like every other
22
+ * field here: an explicit in-session flip beats it for that conversation,
23
+ * and it is never applied to guest/observer surfaces.
24
+ */
25
+ readonly autoApprove?: boolean;
19
26
  }
20
27
 
21
28
  /**
@@ -56,8 +63,11 @@ export function useAccountExecutionDefaults(): AccountExecutionDefaults | undefi
56
63
  : undefined;
57
64
  const nativeModel = prefs.defaultNativeModel || undefined;
58
65
  const cursorModel = prefs.defaultCursorModel || undefined;
66
+ // Only a declared `true` travels — an unset bool is "no preference",
67
+ // matching the empty-string convention of the fields above.
68
+ const autoApprove = prefs.defaultAutoApprove || undefined;
59
69
 
60
- if (!harness && !nativeModel && !cursorModel) return undefined;
61
- return { harness, nativeModel, cursorModel };
70
+ if (!harness && !nativeModel && !cursorModel && !autoApprove) return undefined;
71
+ return { harness, nativeModel, cursorModel, autoApprove };
62
72
  }, [account]);
63
73
  }