@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
@@ -16,6 +16,6 @@ import { useActiveOrgSlug } from "../organization/OrgProvider.js";
16
16
  export function MemorySection() {
17
17
  const headingId = useId();
18
18
  const org = useActiveOrgSlug();
19
- return (_jsxs("section", { "aria-labelledby": headingId, children: [_jsx("h2", { id: headingId, className: "stg:text-foreground stg:mb-1 stg:text-sm stg:font-semibold", children: "Memory" }), _jsx("p", { className: "stg:text-muted-foreground stg:mb-6 stg:text-xs", children: "Facts agents proposed to remember about you in this organization. Confirmed memories are shared with agents in your future sessions and appear in those executions' records. Only you can see them here, and you can edit or delete any of them at any time." }), !org ? (_jsx("p", { className: "stg:text-muted-foreground stg:py-4 stg:text-center stg:text-xs", children: "Select an organization to view its memories." })) : (_jsx(MemoryListPanel, { org: org }))] }));
19
+ return (_jsxs("section", { "aria-labelledby": headingId, children: [_jsx("h2", { id: headingId, className: "stg:text-foreground stg:mb-1 stg:text-sm stg:font-semibold", children: "Memory" }), _jsx("p", { className: "stg:text-muted-foreground stg:mb-6 stg:text-xs", children: "Facts agents proposed to remember about you in this organization. Confirmed memories are shared with agents in your future sessions and appear in those executions' records; once you have many, each conversation recalls the most relevant ones, shown on the execution. Only you can see them here, and you can edit or delete any of them at any time." }), !org ? (_jsx("p", { className: "stg:text-muted-foreground stg:py-4 stg:text-center stg:text-xs", children: "Select an organization to view its memories." })) : (_jsx(MemoryListPanel, { org: org }))] }));
20
20
  }
21
21
  //# sourceMappingURL=MemorySection.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"MemorySection.js","sourceRoot":"","sources":["../../src/settings/MemorySection.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAElE;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,SAAS,GAAG,KAAK,EAAE,CAAC;IAC1B,MAAM,GAAG,GAAG,gBAAgB,EAAE,CAAC;IAE/B,OAAO,CACL,sCAA0B,SAAS,aACjC,aACE,EAAE,EAAE,SAAS,EACb,SAAS,EAAC,4DAA4D,uBAGnE,EACL,YAAG,SAAS,EAAC,gDAAgD,6QAKzD,EAEH,CAAC,GAAG,CAAC,CAAC,CAAC,CACN,YAAG,SAAS,EAAC,gEAAgE,6DAEzE,CACL,CAAC,CAAC,CAAC,CACF,KAAC,eAAe,IAAC,GAAG,EAAE,GAAG,GAAI,CAC9B,IACO,CACX,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"MemorySection.js","sourceRoot":"","sources":["../../src/settings/MemorySection.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAElE;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,SAAS,GAAG,KAAK,EAAE,CAAC;IAC1B,MAAM,GAAG,GAAG,gBAAgB,EAAE,CAAC;IAE/B,OAAO,CACL,sCAA0B,SAAS,aACjC,aACE,EAAE,EAAE,SAAS,EACb,SAAS,EAAC,4DAA4D,uBAGnE,EACL,YAAG,SAAS,EAAC,gDAAgD,2WAOzD,EAEH,CAAC,GAAG,CAAC,CAAC,CAAC,CACN,YAAG,SAAS,EAAC,gEAAgE,6DAEzE,CACL,CAAC,CAAC,CAAC,CACF,KAAC,eAAe,IAAC,GAAG,EAAE,GAAG,GAAI,CAC9B,IACO,CACX,CAAC;AACJ,CAAC"}
@@ -1,6 +1,10 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
2
  import { prepareImageForVision } from "../prepare-image.js";
3
3
 
4
+ afterEach(() => {
5
+ vi.unstubAllGlobals();
6
+ });
7
+
4
8
  /**
5
9
  * happy-dom half of the prepare-image coverage: the guard rails that make
6
10
  * the function safe to call from any environment. The actual pixel work
@@ -17,11 +21,13 @@ describe("prepareImageForVision — environment guard rails", () => {
17
21
  });
18
22
 
19
23
  it("returns an image untouched when the canvas pipeline is unavailable", async () => {
20
- // happy-dom has no createImageBitmap the exact situation of an old
21
- // browser or a privacy extension that blocks canvas. The paste must
22
- // still work end to end with the original bytes; it merely degrades
23
- // at the runner with the standard disclosure.
24
- expect(typeof createImageBitmap).toBe("undefined");
24
+ // The situation of an old browser or a privacy extension that blocks
25
+ // canvas. Stubbed explicitly rather than relying on the DOM shim
26
+ // lacking createImageBitmap newer shims ship one, and this guard
27
+ // must hold regardless of the test environment's capabilities. The
28
+ // paste must still work end to end with the original bytes; it merely
29
+ // degrades at the runner with the standard disclosure.
30
+ vi.stubGlobal("createImageBitmap", undefined);
25
31
 
26
32
  const png = new File([new Uint8Array([0x89, 0x50, 0x4e, 0x47])], "shot.png", {
27
33
  type: "image/png",
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
 
3
- import { useId } from "react";
4
3
  import { cn } from "@stigmer/theme";
5
4
  import {
6
5
  Tooltip,
@@ -8,7 +7,6 @@ import {
8
7
  TooltipProvider,
9
8
  TooltipTrigger,
10
9
  } from "../internal/tooltip.js";
11
- import { Switch } from "../switch/Switch.js";
12
10
  import { ContextPopover } from "./ContextPopover.js";
13
11
  import { ConfigureMenu, type ConfigureMenuItem } from "./ConfigureMenu.js";
14
12
  import { ModelSelector } from "../models/ModelSelector.js";
@@ -25,18 +23,6 @@ import {
25
23
  } from "./icons.js";
26
24
  import { SpinnerIcon } from "../internal/SpinnerIcon.js";
27
25
 
28
- /**
29
- * Wiring for the always-available "auto-approve tool calls" toggle
30
- * (stigmer/stigmer#816). Presence renders the control; omission keeps the
31
- * toolbar byte-identical for consumers that don't wire it (DD-011).
32
- */
33
- export interface ComposerAutoApproveProps {
34
- /** Whether auto-approve is currently armed for this conversation. */
35
- readonly armed: boolean;
36
- /** Called with the next value when the user flips the toggle. */
37
- readonly onChange: (armed: boolean) => void;
38
- }
39
-
40
26
  export interface ComposerToolbarProps {
41
27
  readonly disabled: boolean;
42
28
  readonly isSubmitting: boolean;
@@ -61,14 +47,6 @@ export interface ComposerToolbarProps {
61
47
  readonly interactionMode?: InteractionModeOption;
62
48
  readonly onInteractionModeChange: (mode: InteractionModeOption) => void;
63
49
 
64
- /**
65
- * Always-available auto-approve toggle (#816). Deliberately NOT bound to
66
- * {@link disabled}: the walk-away user flips it exactly while a turn
67
- * streams, so it stays interactive under the composer lock — the same
68
- * exemption the Stop button carries.
69
- */
70
- readonly autoApprove?: ComposerAutoApproveProps;
71
-
72
50
  readonly showModelSelector: boolean;
73
51
  readonly modelId?: string;
74
52
  readonly onModelChange: (id: string) => void;
@@ -150,7 +128,6 @@ export function ComposerToolbar({
150
128
  showInteractionModePicker,
151
129
  interactionMode,
152
130
  onInteractionModeChange,
153
- autoApprove,
154
131
  showModelSelector,
155
132
  modelId,
156
133
  onModelChange,
@@ -160,7 +137,6 @@ export function ComposerToolbar({
160
137
  onThinkingModeChange,
161
138
  }: ComposerToolbarProps) {
162
139
  const showHarnessSeparate = showHarnessSelector && !showModelSelector;
163
- const autoApproveLabelId = useId();
164
140
 
165
141
  return (
166
142
  <div className="stg:flex stg:items-center stg:justify-between stg:gap-2 stg:border-t stg:border-border-muted stg:px-3 stg:py-2">
@@ -198,39 +174,6 @@ export function ComposerToolbar({
198
174
  />
199
175
  )}
200
176
 
201
- {/* Auto-approve (#816): primary state like Mode and Model — armed
202
- vs gated is something the user glances at before walking away.
203
- Never bound to `disabled` (see ComposerAutoApproveProps): the
204
- whole point is flipping it while a turn streams. Scoped provider:
205
- this tooltip does not share the icon cluster's hover-delay group. */}
206
- {autoApprove && (
207
- <TooltipProvider>
208
- <Tooltip>
209
- <TooltipTrigger render={<span className="stg:inline-flex" />}>
210
- <span className="stg:flex stg:shrink-0 stg:items-center stg:gap-1.5 stg:px-1">
211
- <label
212
- id={autoApproveLabelId}
213
- htmlFor={`${autoApproveLabelId}-switch`}
214
- className="stg:cursor-pointer stg:select-none stg:text-xs stg:text-muted-foreground"
215
- >
216
- Auto-approve
217
- </label>
218
- <Switch
219
- id={`${autoApproveLabelId}-switch`}
220
- checked={autoApprove.armed}
221
- onCheckedChange={autoApprove.onChange}
222
- aria-labelledby={autoApproveLabelId}
223
- />
224
- </span>
225
- </TooltipTrigger>
226
- <TooltipContent side="top">
227
- {autoApprove.armed
228
- ? "Tool calls run without asking — this conversation only. Turn off to be asked again."
229
- : "Run this conversation's tool calls without asking for approval, including the run in progress."}
230
- </TooltipContent>
231
- </Tooltip>
232
- </TooltipProvider>
233
- )}
234
177
  </div>
235
178
 
236
179
  {/* ---- Right group: Secondary actions (icon-only) + Send ----
@@ -4,7 +4,7 @@ import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo,
4
4
  import { cn } from "@stigmer/theme";
5
5
  import { getUserMessage, type AttachmentInput, type EnvVarInput, type McpServerUsageInput, type ResourceRef } from "@stigmer/sdk";
6
6
  import { useComposer } from "./useComposer.js";
7
- import { ComposerToolbar, type ComposerAutoApproveProps } from "./ComposerToolbar.js";
7
+ import { ComposerToolbar } from "./ComposerToolbar.js";
8
8
  import { type ConfigureMenuItem } from "./ConfigureMenu.js";
9
9
  import type { HarnessOption } from "../models/harness.js";
10
10
  import { FAST_SERVICE_TIER, type ServiceTierOption } from "../models/service-tier.js";
@@ -300,16 +300,6 @@ export interface SessionComposerProps {
300
300
  /** Show the model selector. @default true */
301
301
  readonly showModelSelector?: boolean;
302
302
 
303
- /**
304
- * Always-visible "auto-approve tool calls" toggle in the toolbar
305
- * (stigmer/stigmer#816). Presence renders it; omission keeps the composer
306
- * byte-identical (DD-011). Stays interactive while the composer is
307
- * `disabled` — the walk-away user flips it exactly while a turn streams —
308
- * so consumers must only wire it on surfaces whose viewer may submit
309
- * approvals (never guest/observer). See {@link ComposerAutoApproveProps}.
310
- */
311
- readonly autoApprove?: ComposerAutoApproveProps;
312
-
313
303
  /**
314
304
  * Workspace state managed by {@link useWorkspaceEntries}.
315
305
  * When provided, renders a workspace trigger in the toolbar
@@ -627,7 +617,6 @@ const SessionComposerInner = forwardRef<SessionComposerHandle, SessionComposerPr
627
617
  defaultModelId,
628
618
  onModelChange,
629
619
  showModelSelector = true,
630
- autoApprove,
631
620
  workspace,
632
621
  gitHubConnection,
633
622
  enableGitHub = true,
@@ -1931,7 +1920,6 @@ const SessionComposerInner = forwardRef<SessionComposerHandle, SessionComposerPr
1931
1920
  interactionMode={interactionMode}
1932
1921
  onInteractionModeChange={handleInteractionModeChange}
1933
1922
  showModelSelector={showModelSelector}
1934
- autoApprove={autoApprove}
1935
1923
  // The pill renders the effective selection — the same value the
1936
1924
  // submit payload carries (#663), never a fallback of its own.
1937
1925
  modelId={effective.modelId}
@@ -7,7 +7,6 @@ export type {
7
7
  SessionComposerProps,
8
8
  SessionComposerSubmitContext,
9
9
  } from "./SessionComposer.js";
10
- export type { ComposerAutoApproveProps } from "./ComposerToolbar.js";
11
10
 
12
11
  export { InteractionModePicker } from "./InteractionModePicker.js";
13
12
  export type {
@@ -17,7 +17,7 @@ import {
17
17
  TooltipProvider,
18
18
  TooltipTrigger,
19
19
  } from "../internal/tooltip.js";
20
- import { useAutoScroll } from "../internal/useAutoScroll.js";
20
+ import { useAutoScroll, usePinToLatestOnSignal } from "../internal/useAutoScroll.js";
21
21
  import { ConversationMediaAttachment } from "./ConversationMediaAttachment.js";
22
22
  import {
23
23
  attemptExplanationOf,
@@ -69,6 +69,20 @@ export interface ConversationTimelineViewProps {
69
69
  readonly provider: ChannelProviderId | null;
70
70
  /** Frozen instant for deterministic hosts (tests, documentation tours). */
71
71
  readonly now?: Date;
72
+ /**
73
+ * Scroll-on-send signal (stigmer-cloud#267): increment this counter when
74
+ * the reader dispatches a reply and the view pins to the latest content,
75
+ * re-engaging follow mode so the item stays in view when the timeline
76
+ * refetch delivers it — even for a reader who had deliberately scrolled
77
+ * up. The view stays presentational: it has no composer and deliberately
78
+ * renders no optimistic item (the SDK never fabricates what the provider
79
+ * might not have done), so the send moment must arrive from the host —
80
+ * `ConversationsWorkbench` wires its own composer's send here; headless
81
+ * hosts composing their own composer get the same seam. Omit to keep
82
+ * today's leave-the-reader-alone behavior. Incoming items never move a
83
+ * scrolled-up reader either way.
84
+ */
85
+ readonly pinToLatestSignal?: number;
72
86
  /** Additional classes for the view container. */
73
87
  readonly className?: string;
74
88
  }
@@ -100,10 +114,12 @@ export function ConversationTimelineView({
100
114
  isLoadingOlder,
101
115
  provider,
102
116
  now,
117
+ pinToLatestSignal,
103
118
  className,
104
119
  }: ConversationTimelineViewProps) {
105
120
  const { scrollRef, sentinelRef, contentRef, isFollowing, jumpToLatest } =
106
121
  useAutoScroll();
122
+ usePinToLatestOnSignal(pinToLatestSignal, jumpToLatest);
107
123
 
108
124
  const days = useMemo(() => groupByDay(items, now), [items, now]);
109
125
  const descriptor = channelProviderOf(provider ?? undefined);
@@ -82,6 +82,19 @@ export interface ConversationsWorkbenchProps {
82
82
  readonly channelHref?: (channel: AgentChannel) => string | null;
83
83
  /** Frozen instant for deterministic hosts (tests, documentation tours). */
84
84
  readonly now?: Date;
85
+ /**
86
+ * Scroll to the reader's own reply when they send one from a scrolled-up
87
+ * position (stigmer-cloud#267): an accepted send pins the timeline to the
88
+ * latest content and re-engages follow mode, so the reply stays in view
89
+ * when the refetch delivers its real ledger item. Refused sends never pin
90
+ * — the composer restores the draft and there is nothing to show. Default
91
+ * `true` on all three SDK thread surfaces at once (the ratified DD-011
92
+ * divergence — cross-surface consistency is the point); set `false` to
93
+ * keep today's leave-the-reader-alone behavior.
94
+ *
95
+ * @default true
96
+ */
97
+ readonly scrollOnSend?: boolean;
85
98
  /** Additional classes for the workbench container. */
86
99
  readonly className?: string;
87
100
  }
@@ -103,6 +116,7 @@ export function ConversationsWorkbench({
103
116
  headerAccessory,
104
117
  channelHref,
105
118
  now,
119
+ scrollOnSend = true,
106
120
  className,
107
121
  }: ConversationsWorkbenchProps) {
108
122
  const [channelFilter, setChannelFilter] = useState("");
@@ -233,6 +247,12 @@ export function ConversationsWorkbench({
233
247
  }
234
248
  }, [timeline.items, settlingItemId]);
235
249
 
250
+ // Scroll-on-send (stigmer-cloud#267): a monotonic counter the timeline
251
+ // view pins on. Lives outside the conversation-keyed column, which is
252
+ // safe by the signal contract: the view initializes to the current value
253
+ // on (re)mount and pins only on increments it observes.
254
+ const [sendPinSignal, setSendPinSignal] = useState(0);
255
+
236
256
  const handleSend = useCallback(
237
257
  async (payload: ConversationReplyPayload) => {
238
258
  const output = await participation.reply(payload);
@@ -252,6 +272,10 @@ export function ConversationsWorkbench({
252
272
  selectionKey,
253
273
  itemId: outboundItemIdOf(output.outboundMessageId),
254
274
  });
275
+ // The reply produced a ledger item to show — pin the timeline so
276
+ // it lands in view (a refusal pins nothing: the draft came back
277
+ // and the reader's position should not move for it).
278
+ setSendPinSignal((n) => n + 1);
255
279
  }
256
280
  timeline.refetch();
257
281
  return output;
@@ -379,6 +403,7 @@ export function ConversationsWorkbench({
379
403
  isLoadingOlder={timeline.isLoadingOlder}
380
404
  provider={descriptor?.id ?? null}
381
405
  now={now}
406
+ pinToLatestSignal={scrollOnSend ? sendPinSignal : undefined}
382
407
  />
383
408
 
384
409
  <ConversationComposer
@@ -35,7 +35,13 @@ function items(count: number): ConversationTimelineItem[] {
35
35
  ? ConversationItemAuthor.author_customer
36
36
  : ConversationItemAuthor.author_agent,
37
37
  text: `message ${i}`,
38
- at: timestampFromDate(new Date(NOW.getTime() - (count - i) * 60_000)),
38
+ // Anchored by INDEX, never by count: growth must only APPEND rows,
39
+ // as production items keep their timestamps. A count-relative time
40
+ // rewrote every existing row on growth — the resulting full-thread
41
+ // reflow could clamp scrollTop mid-pin and trip the hook's
42
+ // reader-took-control guard (measured flaky ~1-in-5 on the
43
+ // scroll-on-send case).
44
+ at: timestampFromDate(new Date(NOW.getTime() - (1_000 - i) * 60_000)),
39
45
  }),
40
46
  );
41
47
  }
@@ -43,6 +49,7 @@ function items(count: number): ConversationTimelineItem[] {
43
49
  function view(props: {
44
50
  readonly items: readonly ConversationTimelineItem[];
45
51
  readonly isLoading: boolean;
52
+ readonly pinToLatestSignal?: number;
46
53
  }): ReactElement {
47
54
  return (
48
55
  <ConversationTimelineView
@@ -54,6 +61,7 @@ function view(props: {
54
61
  isLoadingOlder={false}
55
62
  provider="whatsapp"
56
63
  now={NOW}
64
+ pinToLatestSignal={props.pinToLatestSignal}
57
65
  />
58
66
  );
59
67
  }
@@ -157,4 +165,32 @@ describe("ConversationTimelineView scroll behavior (F-09)", () => {
157
165
  expect(jumpButton().getAttribute("aria-hidden")).toBe("true");
158
166
  });
159
167
  });
168
+
169
+ it("pins a scrolled-up reader on their OWN send via pinToLatestSignal, and keeps following as the reply's real item lands (stigmer-cloud#267)", async () => {
170
+ const { rerender } = renderInPane(
171
+ view({ items: [], isLoading: true, pinToLatestSignal: 0 }),
172
+ );
173
+ rerender(view({ items: items(40), isLoading: false, pinToLatestSignal: 0 }));
174
+ await settled(() => expect(isPinnedToBottom(scroller())).toBe(true));
175
+
176
+ // The reader scrolls up to read history.
177
+ scroller().scrollTop = 0;
178
+ await settled(() =>
179
+ expect(jumpButton().getAttribute("aria-hidden")).toBe("false"),
180
+ );
181
+
182
+ // They send a reply: the workbench increments the signal on the
183
+ // accepted send. The view pins and re-engages follow — the timeline
184
+ // has no optimistic item, so re-engaged follow is what carries the
185
+ // reply's REAL ledger item into view when the refetch delivers it.
186
+ rerender(view({ items: items(40), isLoading: false, pinToLatestSignal: 1 }));
187
+ await settled(() => {
188
+ expect(isPinnedToBottom(scroller())).toBe(true);
189
+ expect(jumpButton().getAttribute("aria-hidden")).toBe("true");
190
+ });
191
+
192
+ // The refetch delivers the item — still pinned, the reply visible.
193
+ rerender(view({ items: items(41), isLoading: false, pinToLatestSignal: 1 }));
194
+ await settled(() => expect(isPinnedToBottom(scroller())).toBe(true));
195
+ });
160
196
  });
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
 
3
- import { lazy, memo, Suspense, useCallback, useMemo, type ComponentType } from "react";
3
+ import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState, type ComponentType } from "react";
4
4
  import { create } from "@bufbuild/protobuf";
5
- import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
5
+ import type { AgentExecution, RecalledMemoriesReport } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
6
+ import type { RecalledMemoryFact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
6
7
  import type { AgentMessage, ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
7
8
  import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
8
9
  import type { SubAgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
@@ -29,6 +30,7 @@ import { ToolCallGroup } from "./ToolCallGroup.js";
29
30
  import { SubAgentSection } from "./SubAgentSection.js";
30
31
  import { ExecutionPhaseBadge } from "./ExecutionPhaseBadge.js";
31
32
  import { SetupProgress, type SetupProgressProps } from "./SetupProgress.js";
33
+ import { RecalledMemoriesCard, type RecalledMemoriesCardProps } from "./RecalledMemoriesCard.js";
32
34
  import {
33
35
  LivenessStatusLine,
34
36
  type LivenessStatusLineProps,
@@ -56,7 +58,7 @@ import {
56
58
  unwrapEnclosingMarkdownFence,
57
59
  } from "../internal/markdown-components.js";
58
60
  import { useRenderTracer, useKeyStability, useDomNodeCount, DevProfiler } from "../internal/dev/index.js";
59
- import { useAutoScroll } from "../internal/useAutoScroll.js";
61
+ import { useAutoScroll, usePinToLatestOnSignal } from "../internal/useAutoScroll.js";
60
62
  import { JumpToLatestButton } from "../internal/JumpToLatestButton.js";
61
63
  import { ApprovalPeekBar } from "../internal/ApprovalPeekBar.js";
62
64
  import { ThreadItemWrapper } from "../internal/ThreadItemWrapper.js";
@@ -113,6 +115,11 @@ export interface MessageThreadSlots {
113
115
  readonly PlanStreamingCard?: ComponentType<PlanStreamingCardProps>;
114
116
  /** Pre-first-token setup / "Thinking…" indicator. */
115
117
  readonly SetupProgress?: ComponentType<SetupProgressProps>;
118
+ /**
119
+ * The retriever transparency card at a selection-active execution's
120
+ * segment start — "Recalled N of M memories" (DD-008 D5).
121
+ */
122
+ readonly RecalledMemoriesCard?: ComponentType<RecalledMemoriesCardProps>;
116
123
  /**
117
124
  * The terminal execution-failure notice. Receives the raw server-reported
118
125
  * reason and the retry wiring, so a host can turn the failure into its own
@@ -170,6 +177,25 @@ export interface MessageThreadProps {
170
177
  * "Retry" control when {@link pendingMessageFailed} is `true`.
171
178
  */
172
179
  readonly onRetrySend?: () => void;
180
+ /**
181
+ * Scroll to the reader's own message when they send one from a
182
+ * scrolled-up position (stigmer-cloud#267 — the WhatsApp convention:
183
+ * showing the result of the reader's own action is Nielsen #1
184
+ * system-status feedback). The send moment is the
185
+ * {@link pendingUserMessage} transition from empty to present; the pin
186
+ * re-engages follow mode, so the optimistic bubble and the streamed
187
+ * reply stay in view. Incoming content is unaffected — it still never
188
+ * moves a scrolled-up reader.
189
+ *
190
+ * Default `true` on all three SDK thread surfaces at once — a deliberate,
191
+ * ratified divergence from DD-011's opt-in default: the issue's whole
192
+ * point is cross-surface consistency, and a per-surface opt-in would
193
+ * re-create the inconsistency it fixes. Set `false` to keep today's
194
+ * leave-the-reader-alone behavior.
195
+ *
196
+ * @default true
197
+ */
198
+ readonly scrollOnSend?: boolean;
173
199
  /**
174
200
  * When provided, the in-flight human turn (the active execution's prompt)
175
201
  * shows a hover "Edit" affordance. Clicking it invokes this callback with
@@ -379,6 +405,7 @@ export type ThreadItem =
379
405
  | { readonly kind: "approval-request"; readonly pendingApproval: PendingApproval; readonly key: string }
380
406
  | { readonly kind: "file-review-record"; readonly fileChangeSet: FileChangeSet; readonly key: string }
381
407
  | { readonly kind: "setup-progress"; readonly workspaceEntries: readonly WorkspaceEntry[]; readonly serverPhase?: string; readonly isAwaitingResponse?: boolean; readonly key: string }
408
+ | { readonly kind: "recalled-memories"; readonly report: RecalledMemoriesReport; readonly facts: readonly RecalledMemoryFact[]; readonly key: string }
382
409
  | { readonly kind: "context-compacted"; readonly event: SummarizationEventView; readonly key: string }
383
410
  | {
384
411
  readonly kind: "todos";
@@ -805,6 +832,24 @@ export function buildThreadItems(
805
832
  });
806
833
  }
807
834
 
835
+ // The retriever transparency card (stigmer#293 Phase 3a, DD-008 D5):
836
+ // this execution ran semantic selection over its memory snapshot, so
837
+ // its segment discloses the subset right after the user's turn —
838
+ // spec.message is the query the retriever embedded. Absent report or
839
+ // selection_active=false means wholesale (unchanged Phase 2 behavior)
840
+ // and renders nothing. Gated on the REPORT, not the prompt bubble:
841
+ // syntheticUserPrompt deliberately skips some user turns (empty
842
+ // prompt, build-from-plan) that can still be selection-active.
843
+ const recalledReport = exec.status?.recalledMemoriesReport;
844
+ if (recalledReport?.selectionActive) {
845
+ items.push({
846
+ kind: "recalled-memories",
847
+ report: recalledReport,
848
+ facts: exec.spec?.recalledMemories?.facts ?? [],
849
+ key: `${execId}-recalled-memories`,
850
+ });
851
+ }
852
+
808
853
  for (let mi = 0; mi < messages.length; mi++) {
809
854
  const msg = messages[mi];
810
855
 
@@ -1163,6 +1208,7 @@ export function MessageThread({
1163
1208
  pendingAttachments,
1164
1209
  pendingMessageFailed = false,
1165
1210
  onRetrySend,
1211
+ scrollOnSend = true,
1166
1212
  onRetryExecution,
1167
1213
  onEditMessage,
1168
1214
  className,
@@ -1199,6 +1245,22 @@ export function MessageThread({
1199
1245
 
1200
1246
  useKeyStability(items);
1201
1247
 
1248
+ // Scroll-on-send (stigmer-cloud#267): the send moment is the optimistic
1249
+ // message's empty→present transition — the one signal both render paths
1250
+ // share. A monotonic counter (not the message text) carries it down, so
1251
+ // repeated sends of identical text still pin and a retry of a FAILED send
1252
+ // (pending stays present throughout) deliberately does not re-pin.
1253
+ const [sendSignal, setSendSignal] = useState(0);
1254
+ const wasPendingRef = useRef(false);
1255
+ useEffect(() => {
1256
+ const isPending = !!pendingUserMessage;
1257
+ if (isPending && !wasPendingRef.current) {
1258
+ setSendSignal((n) => n + 1);
1259
+ }
1260
+ wasPendingRef.current = isPending;
1261
+ }, [pendingUserMessage]);
1262
+ const pinToLatestSignal = scrollOnSend ? sendSignal : undefined;
1263
+
1202
1264
  const filePathCtx = useMemo<FilePathContextValue>(
1203
1265
  () => ({
1204
1266
  workspaceEntries: workspaceEntries ?? [],
@@ -1288,6 +1350,7 @@ export function MessageThread({
1288
1350
  onRetryExecution={onRetryExecution}
1289
1351
  onEditMessage={onEditMessage}
1290
1352
  slots={slots}
1353
+ pinToLatestSignal={pinToLatestSignal}
1291
1354
  />
1292
1355
  </Suspense>
1293
1356
  </div>
@@ -1317,6 +1380,7 @@ export function MessageThread({
1317
1380
  onRetryExecution={onRetryExecution}
1318
1381
  onEditMessage={onEditMessage}
1319
1382
  slots={slots}
1383
+ pinToLatestSignal={pinToLatestSignal}
1320
1384
  />
1321
1385
  );
1322
1386
  }
@@ -1352,6 +1416,8 @@ interface NonVirtualizedThreadProps {
1352
1416
  readonly onRetryExecution?: (message: string) => void;
1353
1417
  readonly onEditMessage?: (text: string) => void;
1354
1418
  readonly slots?: MessageThreadSlots;
1419
+ /** Scroll-on-send counter from the parent (see `usePinToLatestOnSignal`). */
1420
+ readonly pinToLatestSignal?: number;
1355
1421
  }
1356
1422
 
1357
1423
  function NonVirtualizedThread({
@@ -1376,9 +1442,11 @@ function NonVirtualizedThread({
1376
1442
  onRetryExecution,
1377
1443
  onEditMessage,
1378
1444
  slots,
1445
+ pinToLatestSignal,
1379
1446
  }: NonVirtualizedThreadProps) {
1380
1447
  const { scrollRef, sentinelRef, contentRef, isFollowing, jumpToLatest } =
1381
1448
  useAutoScroll();
1449
+ usePinToLatestOnSignal(pinToLatestSignal, jumpToLatest);
1382
1450
 
1383
1451
  useDomNodeCount(scrollRef, "MessageThread");
1384
1452
 
@@ -1593,6 +1661,10 @@ export function ThreadItemRenderer({
1593
1661
  />
1594
1662
  );
1595
1663
  }
1664
+ case "recalled-memories": {
1665
+ const Recalled = slots?.RecalledMemoriesCard ?? RecalledMemoriesCard;
1666
+ return <Recalled report={item.report} facts={item.facts} />;
1667
+ }
1596
1668
  case "context-compacted":
1597
1669
  return <SummarizationCard event={item.event} />;
1598
1670
  case "liveness": {