@stigmer/react 3.12.5 → 3.12.7

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 (329) hide show
  1. package/agent/AgentDetailView.js +2 -2
  2. package/agent/AgentDetailView.js.map +1 -1
  3. package/agent/index.d.ts +0 -1
  4. package/agent/index.d.ts.map +1 -1
  5. package/agent/index.js +0 -1
  6. package/agent/index.js.map +1 -1
  7. package/agent/useUpdateAgent.d.ts +3 -3
  8. package/agent/useUpdateAgent.js +3 -3
  9. package/agent-instance/AgentInstanceDetailPanel.d.ts.map +1 -1
  10. package/agent-instance/AgentInstanceDetailPanel.js +5 -7
  11. package/agent-instance/AgentInstanceDetailPanel.js.map +1 -1
  12. package/agent-instance/useUpdateAgentInstance.d.ts +1 -1
  13. package/agent-instance/useUpdateAgentInstance.js +1 -1
  14. package/channel/AgentChannelsPanel.d.ts.map +1 -1
  15. package/channel/AgentChannelsPanel.js +10 -5
  16. package/channel/AgentChannelsPanel.js.map +1 -1
  17. package/channel/ChannelRunConfigDialog.d.ts +51 -0
  18. package/channel/ChannelRunConfigDialog.d.ts.map +1 -0
  19. package/channel/ChannelRunConfigDialog.js +121 -0
  20. package/channel/ChannelRunConfigDialog.js.map +1 -0
  21. package/channel/index.d.ts +2 -0
  22. package/channel/index.d.ts.map +1 -1
  23. package/channel/index.js +1 -0
  24. package/channel/index.js.map +1 -1
  25. package/channel/useSaveAgentChannel.d.ts.map +1 -1
  26. package/channel/useSaveAgentChannel.js +6 -3
  27. package/channel/useSaveAgentChannel.js.map +1 -1
  28. package/channel-app/ChannelAppDetailPanel.js +9 -9
  29. package/channel-app/ChannelAppDetailPanel.js.map +1 -1
  30. package/composer/ComposerToolbar.d.ts +24 -1
  31. package/composer/ComposerToolbar.d.ts.map +1 -1
  32. package/composer/ComposerToolbar.js +7 -2
  33. package/composer/ComposerToolbar.js.map +1 -1
  34. package/composer/SessionComposer.d.ts +19 -0
  35. package/composer/SessionComposer.d.ts.map +1 -1
  36. package/composer/SessionComposer.js +24 -5
  37. package/composer/SessionComposer.js.map +1 -1
  38. package/composer/index.d.ts +1 -0
  39. package/composer/index.d.ts.map +1 -1
  40. package/composer/index.js.map +1 -1
  41. package/cursor-accounts/CursorAccountsConsole.d.ts.map +1 -1
  42. package/cursor-accounts/CursorAccountsConsole.js +12 -3
  43. package/cursor-accounts/CursorAccountsConsole.js.map +1 -1
  44. package/cursor-accounts/cursor-account-coverage.d.ts +30 -1
  45. package/cursor-accounts/cursor-account-coverage.d.ts.map +1 -1
  46. package/cursor-accounts/cursor-account-coverage.js +21 -0
  47. package/cursor-accounts/cursor-account-coverage.js.map +1 -1
  48. package/execution/MessageThread.d.ts.map +1 -1
  49. package/execution/MessageThread.js +8 -11
  50. package/execution/MessageThread.js.map +1 -1
  51. package/execution/useCreateAgentExecution.d.ts +15 -0
  52. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  53. package/execution/useCreateAgentExecution.js +5 -0
  54. package/execution/useCreateAgentExecution.js.map +1 -1
  55. package/execution/useFileChangeContent.d.ts +2 -6
  56. package/execution/useFileChangeContent.d.ts.map +1 -1
  57. package/execution/useFileChangeContent.js +4 -12
  58. package/execution/useFileChangeContent.js.map +1 -1
  59. package/identity-account/AccountPreferencesPanel.d.ts +2 -1
  60. package/identity-account/AccountPreferencesPanel.d.ts.map +1 -1
  61. package/identity-account/AccountPreferencesPanel.js +33 -18
  62. package/identity-account/AccountPreferencesPanel.js.map +1 -1
  63. package/identity-provider/IdentityProviderDetailPanel.d.ts.map +1 -1
  64. package/identity-provider/IdentityProviderDetailPanel.js +16 -15
  65. package/identity-provider/IdentityProviderDetailPanel.js.map +1 -1
  66. package/index.d.ts +10 -7
  67. package/index.d.ts.map +1 -1
  68. package/index.js +8 -4
  69. package/index.js.map +1 -1
  70. package/internal/download.d.ts +8 -0
  71. package/internal/download.d.ts.map +1 -0
  72. package/internal/download.js +25 -0
  73. package/internal/download.js.map +1 -0
  74. package/library/useExportResource.d.ts.map +1 -1
  75. package/library/useExportResource.js +3 -17
  76. package/library/useExportResource.js.map +1 -1
  77. package/mcp-server/McpServerDetailView.js +2 -2
  78. package/mcp-server/McpServerDetailView.js.map +1 -1
  79. package/mcp-server/index.d.ts +0 -1
  80. package/mcp-server/index.d.ts.map +1 -1
  81. package/mcp-server/index.js +0 -1
  82. package/mcp-server/index.js.map +1 -1
  83. package/mcp-server/internal/definitionDrift.d.ts +14 -12
  84. package/mcp-server/internal/definitionDrift.d.ts.map +1 -1
  85. package/mcp-server/internal/definitionDrift.js +22 -19
  86. package/mcp-server/internal/definitionDrift.js.map +1 -1
  87. package/mcp-server/useUpdateMcpServer.d.ts +3 -3
  88. package/mcp-server/useUpdateMcpServer.js +3 -3
  89. package/memory/MemoryListPanel.d.ts +35 -0
  90. package/memory/MemoryListPanel.d.ts.map +1 -0
  91. package/memory/MemoryListPanel.js +141 -0
  92. package/memory/MemoryListPanel.js.map +1 -0
  93. package/memory/index.d.ts +8 -0
  94. package/memory/index.d.ts.map +1 -0
  95. package/memory/index.js +12 -0
  96. package/memory/index.js.map +1 -0
  97. package/memory/memoryGroups.d.ts +31 -0
  98. package/memory/memoryGroups.d.ts.map +1 -0
  99. package/memory/memoryGroups.js +52 -0
  100. package/memory/memoryGroups.js.map +1 -0
  101. package/memory/useConfirmMemory.d.ts +31 -0
  102. package/memory/useConfirmMemory.d.ts.map +1 -0
  103. package/memory/useConfirmMemory.js +43 -0
  104. package/memory/useConfirmMemory.js.map +1 -0
  105. package/memory/useDeleteMemory.d.ts +30 -0
  106. package/memory/useDeleteMemory.d.ts.map +1 -0
  107. package/memory/useDeleteMemory.js +42 -0
  108. package/memory/useDeleteMemory.js.map +1 -0
  109. package/memory/useMemories.d.ts +33 -0
  110. package/memory/useMemories.d.ts.map +1 -0
  111. package/memory/useMemories.js +42 -0
  112. package/memory/useMemories.js.map +1 -0
  113. package/memory/useRejectMemory.d.ts +34 -0
  114. package/memory/useRejectMemory.d.ts.map +1 -0
  115. package/memory/useRejectMemory.js +46 -0
  116. package/memory/useRejectMemory.js.map +1 -0
  117. package/memory/useUpdateMemoryContent.d.ts +32 -0
  118. package/memory/useUpdateMemoryContent.d.ts.map +1 -0
  119. package/memory/useUpdateMemoryContent.js +48 -0
  120. package/memory/useUpdateMemoryContent.js.map +1 -0
  121. package/models/ModelSelector.d.ts +33 -1
  122. package/models/ModelSelector.d.ts.map +1 -1
  123. package/models/ModelSelector.js +37 -4
  124. package/models/ModelSelector.js.map +1 -1
  125. package/models/index.d.ts +2 -0
  126. package/models/index.d.ts.map +1 -1
  127. package/models/index.js +1 -0
  128. package/models/index.js.map +1 -1
  129. package/models/registry.d.ts +16 -0
  130. package/models/registry.d.ts.map +1 -1
  131. package/models/registry.js +13 -0
  132. package/models/registry.js.map +1 -1
  133. package/models/thinking-mode.d.ts +27 -0
  134. package/models/thinking-mode.d.ts.map +1 -0
  135. package/models/thinking-mode.js +32 -0
  136. package/models/thinking-mode.js.map +1 -0
  137. package/oauth-app/OAuthAppDetailPanel.d.ts.map +1 -1
  138. package/oauth-app/OAuthAppDetailPanel.js +21 -20
  139. package/oauth-app/OAuthAppDetailPanel.js.map +1 -1
  140. package/package.json +4 -4
  141. package/platform-client/PlatformClientDetailPanel.d.ts.map +1 -1
  142. package/platform-client/PlatformClientDetailPanel.js +12 -13
  143. package/platform-client/PlatformClientDetailPanel.js.map +1 -1
  144. package/schedule/ScheduleDetailView.d.ts.map +1 -1
  145. package/schedule/ScheduleDetailView.js +21 -10
  146. package/schedule/ScheduleDetailView.js.map +1 -1
  147. package/schedule/ScheduleForm.d.ts.map +1 -1
  148. package/schedule/ScheduleForm.js +13 -3
  149. package/schedule/ScheduleForm.js.map +1 -1
  150. package/session/NewSessionViewer.d.ts.map +1 -1
  151. package/session/NewSessionViewer.js +9 -1
  152. package/session/NewSessionViewer.js.map +1 -1
  153. package/session/SessionViewer.d.ts +14 -1
  154. package/session/SessionViewer.d.ts.map +1 -1
  155. package/session/SessionViewer.js +21 -25
  156. package/session/SessionViewer.js.map +1 -1
  157. package/session/TranscriptExportMenu.d.ts +24 -0
  158. package/session/TranscriptExportMenu.d.ts.map +1 -0
  159. package/session/TranscriptExportMenu.js +26 -0
  160. package/session/TranscriptExportMenu.js.map +1 -0
  161. package/session/index.d.ts +4 -0
  162. package/session/index.d.ts.map +1 -1
  163. package/session/index.js +2 -0
  164. package/session/index.js.map +1 -1
  165. package/session/run-config.d.ts +9 -0
  166. package/session/run-config.d.ts.map +1 -1
  167. package/session/run-config.js +5 -0
  168. package/session/run-config.js.map +1 -1
  169. package/session/useExportTranscript.d.ts +44 -0
  170. package/session/useExportTranscript.d.ts.map +1 -0
  171. package/session/useExportTranscript.js +109 -0
  172. package/session/useExportTranscript.js.map +1 -0
  173. package/session/useNewSessionFlow.d.ts +11 -0
  174. package/session/useNewSessionFlow.d.ts.map +1 -1
  175. package/session/useNewSessionFlow.js +20 -6
  176. package/session/useNewSessionFlow.js.map +1 -1
  177. package/session/useSessionConversation.d.ts +12 -1
  178. package/session/useSessionConversation.d.ts.map +1 -1
  179. package/session/useSessionConversation.js +21 -42
  180. package/session/useSessionConversation.js.map +1 -1
  181. package/session/useSessionExecutions.d.ts +2 -14
  182. package/session/useSessionExecutions.d.ts.map +1 -1
  183. package/session/useSessionExecutions.js +4 -26
  184. package/session/useSessionExecutions.js.map +1 -1
  185. package/session/useSessionPageFlow.d.ts +23 -10
  186. package/session/useSessionPageFlow.d.ts.map +1 -1
  187. package/session/useSessionPageFlow.js +75 -19
  188. package/session/useSessionPageFlow.js.map +1 -1
  189. package/session/useUpdateSession.d.ts +18 -24
  190. package/session/useUpdateSession.d.ts.map +1 -1
  191. package/session/useUpdateSession.js +18 -24
  192. package/session/useUpdateSession.js.map +1 -1
  193. package/settings/MemorySection.d.ts +12 -0
  194. package/settings/MemorySection.d.ts.map +1 -0
  195. package/settings/MemorySection.js +21 -0
  196. package/settings/MemorySection.js.map +1 -0
  197. package/settings/index.d.ts +1 -0
  198. package/settings/index.d.ts.map +1 -1
  199. package/settings/index.js +1 -0
  200. package/settings/index.js.map +1 -1
  201. package/settings/settings-nav.d.ts.map +1 -1
  202. package/settings/settings-nav.js +2 -1
  203. package/settings/settings-nav.js.map +1 -1
  204. package/src/__tests__/public-api.test.ts +21 -0
  205. package/src/agent/AgentDetailView.tsx +2 -2
  206. package/src/agent/index.ts +0 -2
  207. package/src/agent/useUpdateAgent.ts +3 -3
  208. package/src/agent-instance/AgentInstanceDetailPanel.tsx +5 -7
  209. package/src/agent-instance/__tests__/AgentInstanceDetailPanel.test.tsx +75 -0
  210. package/src/agent-instance/useUpdateAgentInstance.ts +1 -1
  211. package/src/channel/AgentChannelsPanel.tsx +26 -1
  212. package/src/channel/ChannelRunConfigDialog.tsx +351 -0
  213. package/src/channel/__tests__/ChannelRunConfigDialog.test.tsx +210 -0
  214. package/src/channel/index.ts +2 -0
  215. package/src/channel/useSaveAgentChannel.ts +6 -3
  216. package/src/channel-app/ChannelAppDetailPanel.tsx +9 -9
  217. package/src/channel-app/__tests__/channelAppWhatsApp.test.tsx +2 -0
  218. package/src/channel-app/__tests__/channelAppWipeSafety.test.tsx +87 -0
  219. package/src/composer/ComposerToolbar.tsx +67 -0
  220. package/src/composer/SessionComposer.tsx +49 -3
  221. package/src/composer/__tests__/ComposerToolbar-autoApprove.test.tsx +80 -0
  222. package/src/composer/__tests__/SessionComposer-thinkingMode.test.tsx +176 -0
  223. package/src/composer/index.ts +1 -0
  224. package/src/cursor-accounts/CursorAccountsConsole.tsx +40 -5
  225. package/src/cursor-accounts/__tests__/CursorAccountsConsole.test.tsx +32 -0
  226. package/src/cursor-accounts/__tests__/cursor-account-coverage.test.ts +58 -1
  227. package/src/cursor-accounts/cursor-account-coverage.ts +55 -0
  228. package/src/execution/MessageThread.tsx +8 -11
  229. package/src/execution/useCreateAgentExecution.ts +19 -0
  230. package/src/execution/useFileChangeContent.ts +5 -12
  231. package/src/identity-account/AccountPreferencesPanel.tsx +154 -68
  232. package/src/identity-account/__tests__/AccountPreferencesPanel.test.tsx +36 -2
  233. package/src/identity-provider/IdentityProviderDetailPanel.tsx +18 -15
  234. package/src/identity-provider/__tests__/IdentityProviderDetailPanel.test.tsx +80 -0
  235. package/src/index.ts +36 -2
  236. package/src/internal/download.ts +29 -0
  237. package/src/library/useExportResource.ts +3 -19
  238. package/src/mcp-server/McpServerDetailView.tsx +2 -2
  239. package/src/mcp-server/index.ts +0 -2
  240. package/src/mcp-server/internal/definitionDrift.ts +22 -20
  241. package/src/mcp-server/useUpdateMcpServer.ts +3 -3
  242. package/src/memory/MemoryListPanel.tsx +532 -0
  243. package/src/memory/__tests__/MemoryListPanel.test.tsx +259 -0
  244. package/src/memory/__tests__/memoryHooks.test.tsx +220 -0
  245. package/src/memory/index.ts +19 -0
  246. package/src/memory/memoryGroups.ts +64 -0
  247. package/src/memory/useConfirmMemory.ts +62 -0
  248. package/src/memory/useDeleteMemory.ts +61 -0
  249. package/src/memory/useMemories.ts +66 -0
  250. package/src/memory/useRejectMemory.ts +65 -0
  251. package/src/memory/useUpdateMemoryContent.ts +67 -0
  252. package/src/models/ModelSelector.tsx +94 -1
  253. package/src/models/index.ts +2 -0
  254. package/src/models/registry.ts +29 -0
  255. package/src/models/thinking-mode.ts +48 -0
  256. package/src/oauth-app/OAuthAppDetailPanel.tsx +23 -21
  257. package/src/oauth-app/__tests__/OAuthAppDetailPanel.test.tsx +94 -0
  258. package/src/platform-client/PlatformClientDetailPanel.tsx +14 -13
  259. package/src/platform-client/__tests__/PlatformClientDetailPanel.test.tsx +82 -0
  260. package/src/schedule/ScheduleDetailView.tsx +29 -4
  261. package/src/schedule/ScheduleForm.tsx +14 -1
  262. package/src/session/NewSessionViewer.tsx +15 -1
  263. package/src/session/SessionViewer.tsx +48 -49
  264. package/src/session/TranscriptExportMenu.tsx +78 -0
  265. package/src/session/__tests__/SessionViewer-autoApproveToggle.test.tsx +178 -0
  266. package/src/session/__tests__/SessionViewer-transcriptExport.test.tsx +151 -0
  267. package/src/session/__tests__/TranscriptExportMenu.test.tsx +97 -0
  268. package/src/session/__tests__/useExportTranscript.test.tsx +187 -0
  269. package/src/session/__tests__/useNewSessionFlow.test.tsx +33 -0
  270. package/src/session/__tests__/useSessionPageFlow.autoApprove.test.tsx +176 -2
  271. package/src/session/index.ts +9 -0
  272. package/src/session/run-config.ts +16 -0
  273. package/src/session/useExportTranscript.ts +171 -0
  274. package/src/session/useNewSessionFlow.ts +34 -7
  275. package/src/session/useSessionConversation.ts +42 -56
  276. package/src/session/useSessionExecutions.ts +5 -26
  277. package/src/session/useSessionPageFlow.ts +98 -32
  278. package/src/session/useUpdateSession.ts +18 -24
  279. package/src/settings/MemorySection.tsx +45 -0
  280. package/src/settings/index.ts +1 -0
  281. package/src/settings/settings-nav.ts +2 -0
  282. package/src/usage/useExportCSV.ts +2 -14
  283. package/src/workflow/ApprovalFormBuilder.tsx +20 -5
  284. package/src/workflow/WorkflowDetailView.tsx +2 -2
  285. package/src/workflow/__tests__/WorkflowInstanceDetailPanel.test.tsx +83 -0
  286. package/src/workflow/index.ts +0 -2
  287. package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +6 -7
  288. package/src/workflow/useUpdateWorkflow.ts +3 -3
  289. package/styles.css +1 -1
  290. package/usage/useExportCSV.d.ts.map +1 -1
  291. package/usage/useExportCSV.js +2 -13
  292. package/usage/useExportCSV.js.map +1 -1
  293. package/workflow/ApprovalFormBuilder.d.ts.map +1 -1
  294. package/workflow/ApprovalFormBuilder.js +10 -8
  295. package/workflow/ApprovalFormBuilder.js.map +1 -1
  296. package/workflow/WorkflowDetailView.js +2 -2
  297. package/workflow/WorkflowDetailView.js.map +1 -1
  298. package/workflow/index.d.ts +0 -1
  299. package/workflow/index.d.ts.map +1 -1
  300. package/workflow/index.js +0 -1
  301. package/workflow/index.js.map +1 -1
  302. package/workflow/instance/WorkflowInstanceDetailPanel.d.ts.map +1 -1
  303. package/workflow/instance/WorkflowInstanceDetailPanel.js +6 -7
  304. package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
  305. package/workflow/useUpdateWorkflow.d.ts +3 -3
  306. package/workflow/useUpdateWorkflow.js +3 -3
  307. package/agent/internal/agentToInput.d.ts +0 -15
  308. package/agent/internal/agentToInput.d.ts.map +0 -1
  309. package/agent/internal/agentToInput.js +0 -84
  310. package/agent/internal/agentToInput.js.map +0 -1
  311. package/mcp-server/internal/mcpServerToInput.d.ts +0 -12
  312. package/mcp-server/internal/mcpServerToInput.d.ts.map +0 -1
  313. package/mcp-server/internal/mcpServerToInput.js +0 -84
  314. package/mcp-server/internal/mcpServerToInput.js.map +0 -1
  315. package/session/session-spec-converters.d.ts +0 -24
  316. package/session/session-spec-converters.d.ts.map +0 -1
  317. package/session/session-spec-converters.js +0 -72
  318. package/session/session-spec-converters.js.map +0 -1
  319. package/src/agent/internal/__tests__/agentToInput.test.ts +0 -145
  320. package/src/agent/internal/agentToInput.ts +0 -102
  321. package/src/mcp-server/__tests__/mcpServerToInput.test.ts +0 -201
  322. package/src/mcp-server/internal/mcpServerToInput.ts +0 -98
  323. package/src/session/__tests__/session-spec-converters.test.ts +0 -183
  324. package/src/session/session-spec-converters.ts +0 -86
  325. package/src/workflow/internal/workflowToInput.ts +0 -96
  326. package/workflow/internal/workflowToInput.d.ts +0 -12
  327. package/workflow/internal/workflowToInput.d.ts.map +0 -1
  328. package/workflow/internal/workflowToInput.js +0 -73
  329. package/workflow/internal/workflowToInput.js.map +0 -1
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ import { create } from "@bufbuild/protobuf";
4
+ import type { Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
5
+ import { ListMemoriesRequestSchema } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/io_pb";
6
+ import { useStigmer } from "../hooks.js";
7
+ import { useFetch } from "../internal/useFetch.js";
8
+
9
+ /**
10
+ * The server-enforced per-subject-per-org record ceiling (all lifecycle
11
+ * states). Requesting one page of exactly this size fetches EVERY record
12
+ * the caller can see, by construction — the cap is why this list needs
13
+ * no pagination UI.
14
+ */
15
+ const MEMORY_CAP = 100;
16
+
17
+ /** Return value of {@link useMemories}. */
18
+ export interface UseMemoriesReturn {
19
+ /** Every memory the caller can view in the org, newest first. */
20
+ readonly memories: readonly Memory[];
21
+ /** `true` while the initial fetch is in flight. */
22
+ readonly isLoading: boolean;
23
+ /** `true` while a background refetch is in flight. */
24
+ readonly isRefetching: boolean;
25
+ /** Error from the last failed request, or `null` when healthy. */
26
+ readonly error: Error | null;
27
+ /** Discard cached data and re-fetch the list from the server. */
28
+ readonly refetch: () => void;
29
+ }
30
+
31
+ /**
32
+ * Data hook that fetches the caller's memories in an organization.
33
+ *
34
+ * Memory content is subject-only: the server returns exactly the records
35
+ * that are about the caller (FGA-filtered in cloud; single-user in local
36
+ * mode), so this list is always "what the platform remembers about ME
37
+ * here". Ordering is newest first; group by lifecycle state for display
38
+ * with {@link groupMemoriesByLifecycle}.
39
+ *
40
+ * Pass `null` as `org` to skip fetching (stable no-op).
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * const { memories, isLoading, refetch } = useMemories(org);
45
+ * const groups = groupMemoriesByLifecycle(memories);
46
+ * ```
47
+ */
48
+ export function useMemories(org: string | null): UseMemoriesReturn {
49
+ const stigmer = useStigmer();
50
+
51
+ const { data: memories, isLoading, isRefetching, error, refetch } = useFetch(
52
+ org
53
+ ? () =>
54
+ stigmer.memory
55
+ .list(create(ListMemoriesRequestSchema, {
56
+ org,
57
+ pageInfo: { num: 1, size: MEMORY_CAP },
58
+ }))
59
+ .then((r) => [...r.items])
60
+ : null,
61
+ [stigmer, org],
62
+ [] as Memory[],
63
+ );
64
+
65
+ return { memories, isLoading, isRefetching, error, refetch };
66
+ }
@@ -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
+ }
@@ -9,6 +9,7 @@ import { useModelRegistry } from "./useModelRegistry.js";
9
9
  import type { ModelInfo, CostTier, SpeedTier } from "./registry.js";
10
10
  import { HARNESS_META, HARNESS_OPTIONS, type HarnessOption } from "./harness.js";
11
11
  import { FAST_SERVICE_TIER, type ServiceTierOption } from "./service-tier.js";
12
+ import type { ThinkingModeOption } from "./thinking-mode.js";
12
13
 
13
14
  const COST_TIER_LABEL: Record<CostTier, string> = {
14
15
  economy: "$",
@@ -23,6 +24,17 @@ const SPEED_TIER_LABEL: Record<SpeedTier, string> = {
23
24
  slow: "Powerful",
24
25
  };
25
26
 
27
+ /**
28
+ * Whether the thinking switch may act on this model (stigmer/stigmer#772):
29
+ * the registry declares the capability AND the model is cursor-harness —
30
+ * the only harness with a thinking translation in v1. Native models
31
+ * truthfully declare the capability too, but selecting it there would be
32
+ * refused at create time, so the control never renders for them.
33
+ */
34
+ function supportsThinking(model: ModelInfo): boolean {
35
+ return model.harness === "cursor" && model.thinkingCapable === true;
36
+ }
37
+
26
38
  /** Props for {@link ModelSelector}. */
27
39
  export interface ModelSelectorProps {
28
40
  /** Currently selected model ID. */
@@ -95,6 +107,30 @@ export interface ModelSelectorProps {
95
107
  */
96
108
  readonly onServiceTierChange?: (tier: ServiceTierOption) => void;
97
109
 
110
+ /**
111
+ * Current thinking mode for the selected model (stigmer/stigmer#772).
112
+ * Defaults to "disabled". Only meaningful together with
113
+ * {@link onThinkingModeChange}.
114
+ */
115
+ readonly thinkingMode?: ThinkingModeOption;
116
+ /**
117
+ * Called when the user toggles extended reasoning. Providing this enables
118
+ * the "Thinking" switch as a sibling of the fast-tier switch in the
119
+ * popover's options area, which renders ONLY while the selected model is
120
+ * a cursor-harness model declaring the thinking capability
121
+ * ({@link ModelInfo.thinkingCapable}) — no dead controls, and v1 honors
122
+ * the selection on the cursor harness only.
123
+ *
124
+ * The same persistence rule as the fast tier: an active thinking mode
125
+ * survives switches between thinking-capable models (trigger badge +
126
+ * options switch keep it visible) and resets to "disabled" only when the
127
+ * new model lacks the capability — an unsupported combination can never
128
+ * be submitted (#772, refused at create time). Unlike fast, thinking
129
+ * bills at base per-token rates; enabled turns consume more output
130
+ * (reasoning) tokens.
131
+ */
132
+ readonly onThinkingModeChange?: (mode: ThinkingModeOption) => void;
133
+
98
134
  /**
99
135
  * @deprecated Use {@link onHarnessChange} instead.
100
136
  */
@@ -125,6 +161,14 @@ export interface ModelSelectorProps {
125
161
  * switch keep the state honest — and resets only for models with no fast
126
162
  * variant, where it would be refused at create time.
127
163
  *
164
+ * **Thinking mode (#772).** The same options-area pattern for the second
165
+ * variant dimension: opting in via
166
+ * {@link ModelSelectorProps.onThinkingModeChange} renders a "Thinking"
167
+ * switch for cursor-harness models declaring the thinking capability.
168
+ * Capability-gated rather than priced-variant-gated — thinking bills at
169
+ * base per-token rates and costs more only through extra reasoning
170
+ * (output) tokens. Persistence and reset rules mirror the fast tier's.
171
+ *
128
172
  * @example
129
173
  * ```tsx
130
174
  * <ModelSelector
@@ -153,6 +197,8 @@ export function ModelSelector({
153
197
  placeholderLabel,
154
198
  serviceTier = "standard",
155
199
  onServiceTierChange,
200
+ thinkingMode = "disabled",
201
+ onThinkingModeChange,
156
202
  }: ModelSelectorProps) {
157
203
  const portalContainer = useStigmerPortalContainer();
158
204
 
@@ -277,9 +323,19 @@ export function ModelSelector({
277
323
  ) {
278
324
  onServiceTierChange("standard");
279
325
  }
326
+ // The identical persistence rule for the thinking mode (#772):
327
+ // survives between thinking-capable cursor models, resets only
328
+ // where the selection would be refused at create time.
329
+ if (
330
+ thinkingMode === "enabled"
331
+ && onThinkingModeChange
332
+ && !supportsThinking(model)
333
+ ) {
334
+ onThinkingModeChange("disabled");
335
+ }
280
336
  setOpen(false);
281
337
  },
282
- [onValueChange, serviceTier, onServiceTierChange],
338
+ [onValueChange, serviceTier, onServiceTierChange, thinkingMode, onThinkingModeChange],
283
339
  );
284
340
 
285
341
  const scrollHighlightIntoView = useCallback((idx: number) => {
@@ -338,6 +394,15 @@ export function ModelSelector({
338
394
  const fastActive =
339
395
  serviceTier === "fast"
340
396
  && (selectedModel?.serviceTiers.includes(FAST_SERVICE_TIER) ?? false);
397
+ // The thinking switch renders under the same no-dead-controls rule,
398
+ // gated on the capability instead of a priced variant (#772).
399
+ const showThinkingToggle =
400
+ onThinkingModeChange !== undefined
401
+ && selectedModel !== undefined
402
+ && supportsThinking(selectedModel);
403
+ const thinkingActive =
404
+ thinkingMode === "enabled"
405
+ && (selectedModel !== undefined && supportsThinking(selectedModel));
341
406
 
342
407
  const usingPlaceholder = !value && placeholderLabel !== undefined;
343
408
  const triggerLabel = usingPlaceholder
@@ -379,6 +444,16 @@ export function ModelSelector({
379
444
  Fast
380
445
  </span>
381
446
  )}
447
+ {thinkingActive && (
448
+ <span
449
+ className={cn(
450
+ "stg:inline-flex stg:shrink-0 stg:items-center stg:gap-0.5 stg:rounded-full stg:border stg:border-primary stg:px-1.5 stg:py-px",
451
+ "stg:text-[0.6rem] stg:font-medium stg:uppercase stg:tracking-wider stg:text-primary",
452
+ )}
453
+ >
454
+ Thinking
455
+ </span>
456
+ )}
382
457
  <ChevronIcon />
383
458
  </Popover.Trigger>
384
459
 
@@ -483,6 +558,24 @@ export function ModelSelector({
483
558
  </div>
484
559
  )}
485
560
 
561
+ {showThinkingToggle && (
562
+ <div className="stg:flex stg:items-center stg:justify-between stg:border-b stg:border-border stg:px-3 stg:py-2">
563
+ <div className="stg:flex stg:flex-col">
564
+ <span className="stg:text-xs stg:text-foreground">Thinking</span>
565
+ <span className="stg:text-[0.65rem] stg:text-muted-foreground">
566
+ Extended reasoning; uses more output tokens
567
+ </span>
568
+ </div>
569
+ <Switch
570
+ checked={thinkingActive}
571
+ onCheckedChange={(next) =>
572
+ onThinkingModeChange?.(next ? "enabled" : "disabled")
573
+ }
574
+ aria-label="Thinking"
575
+ />
576
+ </div>
577
+ )}
578
+
486
579
  {/* Search input */}
487
580
  <div className="stg:border-b stg:border-border stg:px-3 stg:py-1.5">
488
581
  <input
@@ -13,3 +13,5 @@ export { DEFAULT_HARNESS, HARNESS_LABELS, HARNESS_META, HARNESS_OPTIONS, toProto
13
13
  export type { HarnessOption, HarnessDisplayInfo } from "./harness.js";
14
14
  export { FAST_SERVICE_TIER, toProtoServiceTier, fromProtoServiceTier } from "./service-tier.js";
15
15
  export type { ServiceTierOption } from "./service-tier.js";
16
+ export { toProtoThinkingMode, fromProtoThinkingMode } from "./thinking-mode.js";
17
+ export type { ThinkingModeOption } from "./thinking-mode.js";
@@ -114,6 +114,22 @@ export interface ModelInfo {
114
114
  * parse the same document and must agree on what absence means.
115
115
  */
116
116
  readonly visionCapability?: boolean;
117
+ /**
118
+ * Tri-state thinking capability from the registry's `capabilities` block
119
+ * (stigmer/stigmer#772), the same absence convention as
120
+ * {@link visionCapability}:
121
+ *
122
+ * - `true` — the model declares an extended-reasoning (thinking) variant;
123
+ * the thinking switch in the model popover's options area may render
124
+ * - `false` — explicitly assessed as having none
125
+ * - `undefined` — never assessed; **stay silent, never treat as false**
126
+ *
127
+ * Capability-gated, not pricing-gated (unlike {@link serviceTiers}):
128
+ * thinking bills at base per-token rates, so no priced variant exists to
129
+ * key on. v1 honors the selection on the cursor harness only — gate any
130
+ * control on `harness === "cursor"` as well as this flag.
131
+ */
132
+ readonly thinkingCapable?: boolean;
117
133
  }
118
134
 
119
135
  /**
@@ -229,6 +245,17 @@ function parseVisionCapability(capabilities: unknown): boolean | undefined {
229
245
  return typeof vision === "boolean" ? vision : undefined;
230
246
  }
231
247
 
248
+ /**
249
+ * Extract `capabilities.thinking` with the same tri-state convention as
250
+ * {@link parseVisionCapability} (stigmer/stigmer#772): a missing or
251
+ * malformed `capabilities` block stays `undefined`, never coerced to false.
252
+ */
253
+ function parseThinkingCapability(capabilities: unknown): boolean | undefined {
254
+ if (!capabilities || typeof capabilities !== "object") return undefined;
255
+ const thinking = (capabilities as Record<string, unknown>).thinking;
256
+ return typeof thinking === "boolean" ? thinking : undefined;
257
+ }
258
+
232
259
  /**
233
260
  * Parse the document-level `limits.vision` block. Returns `undefined` for
234
261
  * documents that predate the block or carry a malformed one — absence
@@ -284,6 +311,7 @@ export function parseRegistryDocument(data: unknown): ModelRegistryDocument {
284
311
  .filter(isModelEntry)
285
312
  .map((m) => {
286
313
  const visionCapability = parseVisionCapability(m.capabilities);
314
+ const thinkingCapable = parseThinkingCapability(m.capabilities);
287
315
  return {
288
316
  modelId: m.id,
289
317
  provider: m.provider as Provider,
@@ -300,6 +328,7 @@ export function parseRegistryDocument(data: unknown): ModelRegistryDocument {
300
328
  // Conditional spread keeps unassessed models free of the key
301
329
  // (tri-state absence, not an explicit undefined).
302
330
  ...(visionCapability !== undefined ? { visionCapability } : {}),
331
+ ...(thinkingCapable !== undefined ? { thinkingCapable } : {}),
303
332
  };
304
333
  });
305
334
 
@@ -0,0 +1,48 @@
1
+ import { ThinkingMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
2
+
3
+ /**
4
+ * String form of the thinking mode for component props and hook inputs
5
+ * (stigmer/stigmer#772). Mirrors {@link ServiceTierOption}: components
6
+ * speak strings, execution creation speaks proto.
7
+ *
8
+ * "disabled" is the model's base variant; "enabled" is the model's
9
+ * extended-reasoning variant — selectable only for models whose
10
+ * {@link ModelInfo.thinkingCapable} is true. Unlike the fast tier,
11
+ * thinking bills at base per-token rates; enabled turns simply consume
12
+ * more output (reasoning) tokens.
13
+ */
14
+ export type ThinkingModeOption = "disabled" | "enabled";
15
+
16
+ /**
17
+ * Convert a {@link ThinkingModeOption} string to the proto enum.
18
+ */
19
+ export function toProtoThinkingMode(mode: ThinkingModeOption): ThinkingMode {
20
+ switch (mode) {
21
+ case "enabled":
22
+ return ThinkingMode.ENABLED;
23
+ case "disabled":
24
+ default:
25
+ return ThinkingMode.DISABLED;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Convert a proto {@link ThinkingMode} enum to its string form.
31
+ *
32
+ * `UNSPECIFIED` (and any unknown value) maps to `undefined` so callers can
33
+ * decide their own default — matching the proto contract (`UNSPECIFIED`
34
+ * resolves to `THINKING_MODE_DISABLED` in the runner, never the provider
35
+ * account default).
36
+ */
37
+ export function fromProtoThinkingMode(
38
+ mode: ThinkingMode | undefined,
39
+ ): ThinkingModeOption | undefined {
40
+ switch (mode) {
41
+ case ThinkingMode.DISABLED:
42
+ return "disabled";
43
+ case ThinkingMode.ENABLED:
44
+ return "enabled";
45
+ default:
46
+ return undefined;
47
+ }
48
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { type FormEvent, useCallback, useId, useState } from "react";
4
4
  import { cn } from "@stigmer/theme";
5
- import { getUserMessage } from "@stigmer/sdk";
5
+ import { getUserMessage, toOAuthAppUpdateInput } from "@stigmer/sdk";
6
6
  import type { OAuthApp } from "@stigmer/protos/ai/stigmer/iam/oauthapp/v1/api_pb";
7
7
  import {
8
8
  TokenEndpointAuthMethod,
@@ -132,30 +132,32 @@ export function OAuthAppDetailPanel({
132
132
  .filter(Boolean);
133
133
 
134
134
  try {
135
+ // Full-spec-replace safety: spread the complete mapped input and
136
+ // override every field this form owns, so a future spec field the
137
+ // form does not know about survives the save. An empty secret
138
+ // input keeps the fetched value — the redaction marker — which
139
+ // the server's update pipeline treats as "keep the stored
140
+ // secret"; a non-empty input replaces it.
141
+ const mapped = toOAuthAppUpdateInput(oauthApp);
135
142
  const updated = await update({
136
- name: meta?.name ?? "",
137
- slug: meta?.slug,
138
- org: meta?.org ?? "",
143
+ ...mapped,
139
144
  provider: provider.trim(),
140
145
  clientId: clientId.trim(),
141
- ...(clientSecret.trim() && { clientSecret: clientSecret.trim() }),
146
+ clientSecret: clientSecret.trim() || mapped.clientSecret,
142
147
  authorizationUrl: authorizationUrl.trim(),
143
148
  tokenUrl: tokenUrl.trim(),
144
- ...(parsedScopes.length > 0 && { scopes: parsedScopes }),
145
- ...(userinfoUrl.trim() && { userinfoUrl: userinfoUrl.trim() }),
146
- ...(scopeParameterName.trim() && {
147
- scopeParameterName: scopeParameterName.trim(),
148
- }),
149
- ...(tokenEndpointAuthMethod !== "unspecified" && {
150
- tokenEndpointAuthMethod:
151
- TOKEN_AUTH_METHOD_MAP[tokenEndpointAuthMethod],
152
- }),
153
- ...(vendorApprovalStatus !== "unspecified" && {
154
- vendorApprovalStatus: APPROVAL_STATUS_MAP[vendorApprovalStatus],
155
- }),
156
- ...(vendorApprovalDocsUrl.trim() && {
157
- vendorApprovalDocsUrl: vendorApprovalDocsUrl.trim(),
158
- }),
149
+ scopes: parsedScopes.length > 0 ? parsedScopes : undefined,
150
+ userinfoUrl: userinfoUrl.trim() || undefined,
151
+ scopeParameterName: scopeParameterName.trim() || undefined,
152
+ tokenEndpointAuthMethod:
153
+ tokenEndpointAuthMethod !== "unspecified"
154
+ ? TOKEN_AUTH_METHOD_MAP[tokenEndpointAuthMethod]
155
+ : undefined,
156
+ vendorApprovalStatus:
157
+ vendorApprovalStatus !== "unspecified"
158
+ ? APPROVAL_STATUS_MAP[vendorApprovalStatus]
159
+ : undefined,
160
+ vendorApprovalDocsUrl: vendorApprovalDocsUrl.trim() || undefined,
159
161
  });
160
162
  setMode("view");
161
163
  onUpdated?.(updated);
@@ -164,7 +166,7 @@ export function OAuthAppDetailPanel({
164
166
  }
165
167
  },
166
168
  [
167
- meta, provider, clientId, clientSecret, authorizationUrl, tokenUrl,
169
+ oauthApp, provider, clientId, clientSecret, authorizationUrl, tokenUrl,
168
170
  scopes, userinfoUrl, scopeParameterName, tokenEndpointAuthMethod,
169
171
  vendorApprovalStatus, vendorApprovalDocsUrl, update, clearUpdateError,
170
172
  onUpdated,
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
3
+ import { create } from "@bufbuild/protobuf";
4
+ import {
5
+ OAuthAppSchema,
6
+ type OAuthApp,
7
+ } from "@stigmer/protos/ai/stigmer/iam/oauthapp/v1/api_pb";
8
+ import { TokenEndpointAuthMethod } from "@stigmer/protos/ai/stigmer/iam/oauthapp/v1/spec_pb";
9
+ import type { OAuthAppInput } from "@stigmer/sdk";
10
+ import { StigmerContext } from "../../context";
11
+ import { OAuthAppDetailPanel } from "../OAuthAppDetailPanel";
12
+
13
+ /**
14
+ * Regression suite for the full-spec-replace wipe bug: the panel must
15
+ * spread `toOAuthAppUpdateInput` and override only what it edits, so a
16
+ * spec field the form does not know about survives the save. The secret
17
+ * has its own contract: an empty secret input sends the fetched value —
18
+ * the server's redaction marker — which the update pipeline treats as
19
+ * "keep the stored secret" (oss#395 pins the marker/ciphertext boundary).
20
+ */
21
+
22
+ const REDACTED = "***REDACTED***";
23
+
24
+ const APP: OAuthApp = create(OAuthAppSchema, {
25
+ metadata: {
26
+ id: "oa-1",
27
+ name: "GitHub OAuth",
28
+ slug: "github-oauth",
29
+ org: "acme",
30
+ },
31
+ spec: {
32
+ provider: "github",
33
+ clientId: "gh-client-1",
34
+ clientSecret: REDACTED,
35
+ authorizationUrl: "https://github.com/login/oauth/authorize",
36
+ tokenUrl: "https://github.com/login/oauth/access_token",
37
+ scopes: ["repo"],
38
+ tokenEndpointAuthMethod: TokenEndpointAuthMethod.CLIENT_SECRET_POST,
39
+ },
40
+ });
41
+
42
+ function renderPanel(update: ReturnType<typeof vi.fn>) {
43
+ const client = {
44
+ oauthapp: { update },
45
+ } as never;
46
+ return render(
47
+ <StigmerContext.Provider value={client}>
48
+ <OAuthAppDetailPanel oauthApp={APP} />
49
+ </StigmerContext.Provider>,
50
+ );
51
+ }
52
+
53
+ afterEach(cleanup);
54
+
55
+ describe("OAuthAppDetailPanel save payload", () => {
56
+ it("keeps the stored secret (redaction marker) when the secret input is left empty", async () => {
57
+ const update = vi.fn(async (_input: OAuthAppInput) => APP);
58
+ renderPanel(update);
59
+
60
+ fireEvent.click(screen.getByRole("button", { name: "Edit" }));
61
+ const provider = await screen.findByLabelText("Provider");
62
+ fireEvent.change(provider, { target: { value: "github-enterprise" } });
63
+ fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
64
+
65
+ await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
66
+ const input = update.mock.calls[0]![0];
67
+
68
+ // Empty secret input → the redaction marker rides along → the server
69
+ // preserves the stored secret.
70
+ expect(input.clientSecret).toBe(REDACTED);
71
+ // The edited field.
72
+ expect(input.provider).toBe("github-enterprise");
73
+ // Unedited fields round-trip.
74
+ expect(input.tokenEndpointAuthMethod).toBe(
75
+ TokenEndpointAuthMethod.CLIENT_SECRET_POST,
76
+ );
77
+ expect(input.scopes).toEqual(["repo"]);
78
+ expect(input.org).toBe("acme");
79
+ expect(input.slug).toBe("github-oauth");
80
+ });
81
+
82
+ it("sends a newly entered secret instead of the marker", async () => {
83
+ const update = vi.fn(async (_input: OAuthAppInput) => APP);
84
+ renderPanel(update);
85
+
86
+ fireEvent.click(screen.getByRole("button", { name: "Edit" }));
87
+ const secret = await screen.findByLabelText("Client secret");
88
+ fireEvent.change(secret, { target: { value: "new-secret" } });
89
+ fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
90
+
91
+ await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
92
+ expect(update.mock.calls[0]![0].clientSecret).toBe("new-secret");
93
+ });
94
+ });