@stigmer/react 3.40.1 → 3.41.0

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 (271) hide show
  1. package/README.md +1 -0
  2. package/access/ManageAccessDialog.d.ts +1 -1
  3. package/access/ManageAccessDialog.js +1 -1
  4. package/access/types.d.ts +1 -1
  5. package/billing/ChangePlanDialog.d.ts.map +1 -1
  6. package/billing/ChangePlanDialog.js +13 -1
  7. package/billing/ChangePlanDialog.js.map +1 -1
  8. package/billing/ProviderKeysSection.d.ts +33 -0
  9. package/billing/ProviderKeysSection.d.ts.map +1 -0
  10. package/billing/ProviderKeysSection.js +143 -0
  11. package/billing/ProviderKeysSection.js.map +1 -0
  12. package/billing/UpgradeNotice.d.ts.map +1 -1
  13. package/billing/UpgradeNotice.js +1 -0
  14. package/billing/UpgradeNotice.js.map +1 -1
  15. package/billing/index.d.ts +4 -0
  16. package/billing/index.d.ts.map +1 -1
  17. package/billing/index.js +2 -0
  18. package/billing/index.js.map +1 -1
  19. package/billing/plan-features.d.ts +3 -2
  20. package/billing/plan-features.d.ts.map +1 -1
  21. package/billing/plan-features.js +4 -3
  22. package/billing/plan-features.js.map +1 -1
  23. package/billing/useProviderKeys.d.ts +67 -0
  24. package/billing/useProviderKeys.d.ts.map +1 -0
  25. package/billing/useProviderKeys.js +61 -0
  26. package/billing/useProviderKeys.js.map +1 -0
  27. package/channel/AgentChannelsPanel.d.ts.map +1 -1
  28. package/channel/AgentChannelsPanel.js +11 -9
  29. package/channel/AgentChannelsPanel.js.map +1 -1
  30. package/channel/connect/ServingAppSection.js +1 -1
  31. package/channel/connect/ServingAppSection.js.map +1 -1
  32. package/channel-app/ChannelAppDetailPanel.d.ts +2 -2
  33. package/channel-app/ChannelAppDetailPanel.js +17 -10
  34. package/channel-app/ChannelAppDetailPanel.js.map +1 -1
  35. package/channel-app/ChannelAppListPanel.d.ts +2 -2
  36. package/channel-app/CreateChannelAppForm.js +1 -1
  37. package/channel-app/CreateChannelAppForm.js.map +1 -1
  38. package/channel-app/copy.d.ts +3 -3
  39. package/channel-app/copy.js +3 -3
  40. package/channel-app/internal.d.ts +9 -0
  41. package/channel-app/internal.d.ts.map +1 -1
  42. package/channel-app/internal.js +9 -0
  43. package/channel-app/internal.js.map +1 -1
  44. package/channel-app/slackAppSetup.d.ts +3 -1
  45. package/channel-app/slackAppSetup.d.ts.map +1 -1
  46. package/channel-app/slackAppSetup.js +3 -1
  47. package/channel-app/slackAppSetup.js.map +1 -1
  48. package/channel-app/whatsappAppSetup.d.ts +3 -1
  49. package/channel-app/whatsappAppSetup.d.ts.map +1 -1
  50. package/channel-app/whatsappAppSetup.js +3 -1
  51. package/channel-app/whatsappAppSetup.js.map +1 -1
  52. package/composer/SessionComposer.d.ts.map +1 -1
  53. package/composer/SessionComposer.js +5 -9
  54. package/composer/SessionComposer.js.map +1 -1
  55. package/environment/EnvironmentVariableEditor.js +1 -1
  56. package/environment/EnvironmentVariableEditor.js.map +1 -1
  57. package/environment/index.d.ts +1 -1
  58. package/environment/index.d.ts.map +1 -1
  59. package/environment/index.js +1 -1
  60. package/environment/index.js.map +1 -1
  61. package/environment/systemEnvVars.d.ts +4 -63
  62. package/environment/systemEnvVars.d.ts.map +1 -1
  63. package/environment/systemEnvVars.js +21 -113
  64. package/environment/systemEnvVars.js.map +1 -1
  65. package/execution/RecalledMemoriesCard.js +1 -1
  66. package/iam-policy/OrgMembersPanel.js +1 -1
  67. package/iam-policy/OrgMembersPanel.js.map +1 -1
  68. package/iam-policy/useCheckPermission.d.ts +4 -1
  69. package/iam-policy/useCheckPermission.d.ts.map +1 -1
  70. package/iam-policy/useCheckPermission.js +4 -1
  71. package/iam-policy/useCheckPermission.js.map +1 -1
  72. package/iam-policy/useGrantableRoles.d.ts +1 -1
  73. package/iam-policy/useGrantableRoles.js +1 -1
  74. package/iam-policy/useRevokeOrgAccess.d.ts +10 -8
  75. package/iam-policy/useRevokeOrgAccess.d.ts.map +1 -1
  76. package/iam-policy/useRevokeOrgAccess.js +4 -4
  77. package/iam-policy/useRevokeOrgAccess.js.map +1 -1
  78. package/identity-account/AccountPreferencesPanel.d.ts +6 -0
  79. package/identity-account/AccountPreferencesPanel.d.ts.map +1 -1
  80. package/identity-account/AccountPreferencesPanel.js +33 -2
  81. package/identity-account/AccountPreferencesPanel.js.map +1 -1
  82. package/identity-account/useAccountExecutionDefaults.d.ts +1 -1
  83. package/identity-account/useAccountExecutionDefaults.d.ts.map +1 -1
  84. package/identity-account/useAccountExecutionDefaults.js +2 -3
  85. package/identity-account/useAccountExecutionDefaults.js.map +1 -1
  86. package/index.d.ts +3 -3
  87. package/index.d.ts.map +1 -1
  88. package/index.js +3 -3
  89. package/index.js.map +1 -1
  90. package/internal/UnknownPublicAddressNotice.d.ts +20 -0
  91. package/internal/UnknownPublicAddressNotice.d.ts.map +1 -0
  92. package/internal/UnknownPublicAddressNotice.js +19 -0
  93. package/internal/UnknownPublicAddressNotice.js.map +1 -0
  94. package/internal/features.js +2 -2
  95. package/internal/features.js.map +1 -1
  96. package/library/InstanceVisibilitySelector.d.ts +1 -1
  97. package/library/InstanceVisibilitySelector.d.ts.map +1 -1
  98. package/library/ResourceVisibilityControl.d.ts +6 -3
  99. package/library/ResourceVisibilityControl.d.ts.map +1 -1
  100. package/library/ResourceVisibilityControl.js +7 -4
  101. package/library/ResourceVisibilityControl.js.map +1 -1
  102. package/library/VisibilitySelector.d.ts +2 -2
  103. package/library/VisibilitySelector.d.ts.map +1 -1
  104. package/library/VisibilitySelector.js +1 -1
  105. package/licenses/LicenseIssueForm.js +1 -1
  106. package/licenses/LicenseIssueForm.js.map +1 -1
  107. package/licenses/license-format.d.ts +7 -5
  108. package/licenses/license-format.d.ts.map +1 -1
  109. package/licenses/license-format.js +1 -3
  110. package/licenses/license-format.js.map +1 -1
  111. package/mcp-server/McpServerConnectDialog.js +4 -5
  112. package/mcp-server/McpServerConnectDialog.js.map +1 -1
  113. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  114. package/mcp-server/McpServerDetailView.js +3 -6
  115. package/mcp-server/McpServerDetailView.js.map +1 -1
  116. package/mcp-server/McpServerPicker.d.ts.map +1 -1
  117. package/mcp-server/McpServerPicker.js +2 -6
  118. package/mcp-server/McpServerPicker.js.map +1 -1
  119. package/mcp-server/McpToolSelector.js +1 -1
  120. package/mcp-server/McpToolSelector.js.map +1 -1
  121. package/mcp-server/useMcpServerConnect.d.ts +14 -25
  122. package/mcp-server/useMcpServerConnect.d.ts.map +1 -1
  123. package/mcp-server/useMcpServerConnect.js +7 -19
  124. package/mcp-server/useMcpServerConnect.js.map +1 -1
  125. package/mcp-server/useMcpServerOAuthConnect.d.ts +1 -3
  126. package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -1
  127. package/mcp-server/useMcpServerOAuthConnect.js +6 -19
  128. package/mcp-server/useMcpServerOAuthConnect.js.map +1 -1
  129. package/models/HarnessSelector.d.ts.map +1 -1
  130. package/models/HarnessSelector.js +1 -2
  131. package/models/HarnessSelector.js.map +1 -1
  132. package/models/ModelSelector.d.ts +1 -1
  133. package/models/ModelSelector.d.ts.map +1 -1
  134. package/models/ModelSelector.js +1 -7
  135. package/models/ModelSelector.js.map +1 -1
  136. package/models/harness.d.ts +10 -6
  137. package/models/harness.d.ts.map +1 -1
  138. package/models/harness.js +35 -28
  139. package/models/harness.js.map +1 -1
  140. package/models/index.d.ts +1 -1
  141. package/models/index.d.ts.map +1 -1
  142. package/models/index.js +1 -1
  143. package/models/index.js.map +1 -1
  144. package/organization/OrgPreferencesPanel.d.ts.map +1 -1
  145. package/organization/OrgPreferencesPanel.js +13 -1
  146. package/organization/OrgPreferencesPanel.js.map +1 -1
  147. package/package.json +5 -5
  148. package/plugin/useMcpServerReadiness.js +2 -2
  149. package/plugin/useMcpServerReadiness.js.map +1 -1
  150. package/provider-standing/ProviderStandingConsole.js +1 -1
  151. package/provider.d.ts +27 -1
  152. package/provider.d.ts.map +1 -1
  153. package/provider.js +3 -2
  154. package/provider.js.map +1 -1
  155. package/public-base-url-context.d.ts +35 -0
  156. package/public-base-url-context.d.ts.map +1 -0
  157. package/public-base-url-context.js +62 -0
  158. package/public-base-url-context.js.map +1 -0
  159. package/resource-workbench/components/SelectionCheckbox.js +1 -1
  160. package/resource-workbench/components/SelectionCheckbox.js.map +1 -1
  161. package/session/useLocalSessionWorker.d.ts +19 -3
  162. package/session/useLocalSessionWorker.d.ts.map +1 -1
  163. package/session/useLocalSessionWorker.js +37 -4
  164. package/session/useLocalSessionWorker.js.map +1 -1
  165. package/settings/ChannelAppsSection.d.ts +4 -4
  166. package/settings/ChannelAppsSection.js +4 -4
  167. package/settings/ProviderKeysSection.d.ts +3 -0
  168. package/settings/ProviderKeysSection.d.ts.map +1 -0
  169. package/settings/ProviderKeysSection.js +3 -0
  170. package/settings/ProviderKeysSection.js.map +1 -0
  171. package/settings/index.d.ts +2 -0
  172. package/settings/index.d.ts.map +1 -1
  173. package/settings/index.js +1 -0
  174. package/settings/index.js.map +1 -1
  175. package/settings/settings-nav.d.ts.map +1 -1
  176. package/settings/settings-nav.js +2 -1
  177. package/settings/settings-nav.js.map +1 -1
  178. package/sharing/ShareAgentDialog.d.ts.map +1 -1
  179. package/sharing/ShareAgentDialog.js +12 -8
  180. package/sharing/ShareAgentDialog.js.map +1 -1
  181. package/sharing/useCanCreateAgentShare.d.ts +3 -1
  182. package/sharing/useCanCreateAgentShare.d.ts.map +1 -1
  183. package/sharing/useCanCreateAgentShare.js +5 -3
  184. package/sharing/useCanCreateAgentShare.js.map +1 -1
  185. package/sharing/useSaveAgentShare.d.ts +1 -1
  186. package/sharing/useSaveAgentShare.js +1 -1
  187. package/src/access/ManageAccessDialog.tsx +1 -1
  188. package/src/access/types.ts +1 -1
  189. package/src/billing/ChangePlanDialog.tsx +14 -2
  190. package/src/billing/ProviderKeysSection.tsx +324 -0
  191. package/src/billing/UpgradeNotice.tsx +1 -0
  192. package/src/billing/index.ts +9 -0
  193. package/src/billing/plan-features.ts +4 -3
  194. package/src/billing/useProviderKeys.ts +129 -0
  195. package/src/channel/AgentChannelsPanel.tsx +11 -9
  196. package/src/channel/connect/ServingAppSection.tsx +1 -1
  197. package/src/channel-app/ChannelAppDetailPanel.tsx +37 -20
  198. package/src/channel-app/ChannelAppListPanel.tsx +2 -2
  199. package/src/channel-app/CreateChannelAppForm.tsx +0 -1
  200. package/src/channel-app/copy.ts +3 -3
  201. package/src/channel-app/internal.tsx +21 -0
  202. package/src/channel-app/slackAppSetup.ts +3 -1
  203. package/src/channel-app/whatsappAppSetup.ts +3 -1
  204. package/src/composer/SessionComposer.tsx +5 -14
  205. package/src/environment/EnvironmentVariableEditor.tsx +1 -1
  206. package/src/environment/index.ts +1 -7
  207. package/src/environment/systemEnvVars.ts +21 -133
  208. package/src/execution/RecalledMemoriesCard.tsx +1 -1
  209. package/src/iam-policy/OrgMembersPanel.tsx +3 -1
  210. package/src/iam-policy/useCheckPermission.ts +4 -1
  211. package/src/iam-policy/useGrantableRoles.ts +1 -1
  212. package/src/iam-policy/useRevokeOrgAccess.ts +10 -8
  213. package/src/identity-account/AccountPreferencesPanel.tsx +56 -9
  214. package/src/identity-account/useAccountExecutionDefaults.ts +2 -5
  215. package/src/index.ts +9 -5
  216. package/src/internal/UnknownPublicAddressNotice.tsx +31 -0
  217. package/src/internal/features.ts +2 -2
  218. package/src/library/InstanceVisibilitySelector.tsx +1 -1
  219. package/src/library/ResourceVisibilityControl.tsx +7 -4
  220. package/src/library/VisibilitySelector.tsx +2 -2
  221. package/src/licenses/LicenseIssueForm.tsx +1 -1
  222. package/src/licenses/license-format.ts +11 -7
  223. package/src/mcp-server/McpServerConnectDialog.tsx +4 -6
  224. package/src/mcp-server/McpServerDetailView.tsx +3 -6
  225. package/src/mcp-server/McpServerPicker.tsx +2 -12
  226. package/src/mcp-server/McpToolSelector.tsx +1 -1
  227. package/src/mcp-server/useMcpServerConnect.ts +14 -33
  228. package/src/mcp-server/useMcpServerOAuthConnect.ts +7 -23
  229. package/src/models/HarnessSelector.tsx +1 -3
  230. package/src/models/ModelSelector.tsx +2 -7
  231. package/src/models/harness.ts +41 -39
  232. package/src/models/index.ts +1 -1
  233. package/src/organization/OrgPreferencesPanel.tsx +19 -1
  234. package/src/plugin/useMcpServerReadiness.ts +2 -2
  235. package/src/provider-standing/ProviderStandingConsole.tsx +1 -1
  236. package/src/provider.tsx +56 -26
  237. package/src/public-base-url-context.ts +70 -0
  238. package/src/resource-workbench/components/SelectionCheckbox.tsx +1 -1
  239. package/src/session/useLocalSessionWorker.ts +35 -4
  240. package/src/settings/ChannelAppsSection.tsx +4 -4
  241. package/src/settings/ProviderKeysSection.tsx +4 -0
  242. package/src/settings/index.ts +2 -0
  243. package/src/settings/settings-nav.ts +2 -0
  244. package/src/sharing/ShareAgentDialog.tsx +30 -14
  245. package/src/sharing/useCanCreateAgentShare.ts +6 -4
  246. package/src/sharing/useSaveAgentShare.ts +1 -1
  247. package/src/styles.css +14 -4
  248. package/src/workflow/WorkflowTaskApprovalSummary.tsx +14 -7
  249. package/src/workflow/inspector/tabs/BranchesTab.tsx +2 -2
  250. package/src/workflow/inspector/tabs/CatchTab.tsx +1 -1
  251. package/src/workflow/inspector/tabs/IterationTab.tsx +1 -1
  252. package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +7 -6
  253. package/src/workflow/task-detail/task-approval.ts +20 -13
  254. package/styles.css +1 -1
  255. package/workflow/WorkflowTaskApprovalSummary.d.ts +7 -4
  256. package/workflow/WorkflowTaskApprovalSummary.d.ts.map +1 -1
  257. package/workflow/WorkflowTaskApprovalSummary.js +8 -3
  258. package/workflow/WorkflowTaskApprovalSummary.js.map +1 -1
  259. package/workflow/inspector/tabs/BranchesTab.js +1 -1
  260. package/workflow/inspector/tabs/BranchesTab.js.map +1 -1
  261. package/workflow/inspector/tabs/CatchTab.js +1 -1
  262. package/workflow/inspector/tabs/CatchTab.js.map +1 -1
  263. package/workflow/inspector/tabs/IterationTab.js +1 -1
  264. package/workflow/inspector/tabs/IterationTab.js.map +1 -1
  265. package/workflow/instance/WorkflowInstanceDetailPanel.d.ts.map +1 -1
  266. package/workflow/instance/WorkflowInstanceDetailPanel.js +6 -5
  267. package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
  268. package/workflow/task-detail/task-approval.d.ts +13 -11
  269. package/workflow/task-detail/task-approval.d.ts.map +1 -1
  270. package/workflow/task-detail/task-approval.js +8 -4
  271. package/workflow/task-detail/task-approval.js.map +1 -1
package/src/provider.tsx CHANGED
@@ -15,6 +15,7 @@ import { StigmerContext } from "./context.js";
15
15
  import { DeploymentModeContext } from "./deployment-mode.js";
16
16
  import { ExecutionTargetContext } from "./execution-target-context.js";
17
17
  import { ApprovalDefaultsContext } from "./approval-defaults-context.js";
18
+ import { PublicBaseUrlContext } from "./public-base-url-context.js";
18
19
  import type { ApprovalDefaults } from "./approval-defaults-context.js";
19
20
  import type { ExecutionTargetOption } from "./session/execution-target.js";
20
21
  import type { RunnerAdapter } from "./runner-adapter.js";
@@ -121,6 +122,32 @@ export interface StigmerProviderProps {
121
122
  * ```
122
123
  */
123
124
  readonly approvalDefaults?: ApprovalDefaults;
125
+ /**
126
+ * The address the Stigmer API is reached at from outside this page, for
127
+ * the URLs the SDK shows a user to copy (the Slack and WhatsApp webhook
128
+ * URLs a channel app registers, and the platform client snippet in the
129
+ * Share dialog). A session's MCP servers get their
130
+ * `STIGMER_SERVER_ADDRESS` from the platform, never from this value.
131
+ *
132
+ * Omit it when the client's `baseUrl` is absolute: that address is used,
133
+ * exactly as before. Pass it when the client talks to the server through
134
+ * a relative `baseUrl` (a same-origin proxy), because the page's own
135
+ * origin need not be where Slack, Meta, a backend or a remote MCP server
136
+ * can reach the API. Without it, the copy surfaces say they do not know
137
+ * the server's public address and offer nothing to copy. Only an
138
+ * absolute `http(s)` URL is used.
139
+ *
140
+ * @example
141
+ * ```tsx
142
+ * // The browser reaches the API through the page's own origin
143
+ * const client = new Stigmer({ baseUrl: "/", getAccessToken });
144
+ *
145
+ * <StigmerProvider client={client} publicBaseUrl="https://api.example.com">
146
+ * <App />
147
+ * </StigmerProvider>
148
+ * ```
149
+ */
150
+ readonly publicBaseUrl?: string;
124
151
  /**
125
152
  * Built-in theme preset to apply.
126
153
  *
@@ -288,6 +315,7 @@ export function StigmerProvider({
288
315
  executionTarget,
289
316
  runnerAdapter,
290
317
  approvalDefaults,
318
+ publicBaseUrl,
291
319
  preset,
292
320
  className,
293
321
  colorMode = "light",
@@ -310,32 +338,34 @@ export function StigmerProvider({
310
338
  <ExecutionTargetContext.Provider value={executionTarget}>
311
339
  <ApprovalDefaultsContext.Provider value={approvalDefaults}>
312
340
  <RunnerAdapterContext.Provider value={runnerAdapter ?? null}>
313
- <ColorModeContext.Provider value={resolvedMode}>
314
- <ModelRegistryContext.Provider value={registryState}>
315
- <TaskKindRegistryContext.Provider value={taskKindRegistryState}>
316
- <ReviewRendererContext.Provider value={reviewRenderers ?? EMPTY_REVIEW_RENDERERS}>
317
- <PortalContainerContext.Provider value={portalContainer}>
318
- {/*
319
- The in-tree scoping container. `data-stgm-root` is a
320
- stable public selector — the documented seam for hosts
321
- to style THIS element only (e.g. the fixed-height
322
- embedding recipe, #260). It must never be mirrored
323
- onto the portal container, whose own `data-stgm-portal`
324
- marker keeps host in-tree-only CSS away from the
325
- off-tree element.
326
- */}
327
- <div
328
- className={scope.className}
329
- data-stgm-root=""
330
- data-stgm-color-mode={scope.colorMode}
331
- >
332
- {children}
333
- </div>
334
- </PortalContainerContext.Provider>
335
- </ReviewRendererContext.Provider>
336
- </TaskKindRegistryContext.Provider>
337
- </ModelRegistryContext.Provider>
338
- </ColorModeContext.Provider>
341
+ <PublicBaseUrlContext.Provider value={publicBaseUrl}>
342
+ <ColorModeContext.Provider value={resolvedMode}>
343
+ <ModelRegistryContext.Provider value={registryState}>
344
+ <TaskKindRegistryContext.Provider value={taskKindRegistryState}>
345
+ <ReviewRendererContext.Provider value={reviewRenderers ?? EMPTY_REVIEW_RENDERERS}>
346
+ <PortalContainerContext.Provider value={portalContainer}>
347
+ {/*
348
+ The in-tree scoping container. `data-stgm-root` is a
349
+ stable public selector — the documented seam for hosts
350
+ to style THIS element only (e.g. the fixed-height
351
+ embedding recipe, #260). It must never be mirrored
352
+ onto the portal container, whose own `data-stgm-portal`
353
+ marker keeps host in-tree-only CSS away from the
354
+ off-tree element.
355
+ */}
356
+ <div
357
+ className={scope.className}
358
+ data-stgm-root=""
359
+ data-stgm-color-mode={scope.colorMode}
360
+ >
361
+ {children}
362
+ </div>
363
+ </PortalContainerContext.Provider>
364
+ </ReviewRendererContext.Provider>
365
+ </TaskKindRegistryContext.Provider>
366
+ </ModelRegistryContext.Provider>
367
+ </ColorModeContext.Provider>
368
+ </PublicBaseUrlContext.Provider>
339
369
  </RunnerAdapterContext.Provider>
340
370
  </ApprovalDefaultsContext.Provider>
341
371
  </ExecutionTargetContext.Provider>
@@ -0,0 +1,70 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+ import { useStigmer } from "./hooks.js";
5
+
6
+ /**
7
+ * React context for the server's public base URL: the address the Stigmer
8
+ * API is reached at from outside the page, supplied by the host through
9
+ * `StigmerProvider`'s `publicBaseUrl`.
10
+ *
11
+ * Separated from the provider to mirror the `ExecutionTargetContext`
12
+ * pattern and avoid circular imports.
13
+ *
14
+ * `undefined` means the host named no public address; the client's own
15
+ * `baseUrl` then stands in when it is absolute.
16
+ */
17
+ export const PublicBaseUrlContext = createContext<string | undefined>(
18
+ undefined,
19
+ );
20
+
21
+ /**
22
+ * The server's public base URL for URLs the SDK shows a user to copy (a
23
+ * webhook URL pasted into Slack or Meta, a platform client snippet run by
24
+ * a backend), or `null` when it cannot be known.
25
+ *
26
+ * The host owns public addresses, and the SDK never guesses one (the
27
+ * `buildShareUrl` precedent in `ShareAgentDialog`): the provider's
28
+ * `publicBaseUrl` wins, the client's `baseUrl` stands in for it, and only
29
+ * an absolute `http(s)` URL counts. A relative `baseUrl` (a same-origin
30
+ * proxy, the documentation tours) with no `publicBaseUrl` answers `null`,
31
+ * because the page's own origin need not be where the API is reached from
32
+ * outside; the surface then says the address is unknown instead of offering
33
+ * one nobody can call. A `publicBaseUrl` that is not absolute is a host
34
+ * mistake and counts for nothing. Trailing slashes are trimmed.
35
+ */
36
+ export function usePublicBaseUrl(): string | null {
37
+ const hostPublicBaseUrl = useContext(PublicBaseUrlContext);
38
+ const { baseUrl } = useStigmer();
39
+ return resolvePublicBaseUrl(hostPublicBaseUrl, baseUrl);
40
+ }
41
+
42
+ /**
43
+ * The pure rule behind {@link usePublicBaseUrl}: the first of the host's
44
+ * public base URL and the client's base URL that is an absolute `http(s)` URL,
45
+ * trailing slashes trimmed, or `null` when neither is.
46
+ */
47
+ export function resolvePublicBaseUrl(
48
+ hostPublicBaseUrl: string | undefined,
49
+ clientBaseUrl: string,
50
+ ): string | null {
51
+ for (const candidate of [hostPublicBaseUrl, clientBaseUrl]) {
52
+ if (candidate !== undefined && isAbsoluteUrl(candidate)) {
53
+ return candidate.replace(/\/+$/, "");
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+
59
+ /**
60
+ * Whether `value` is an absolute `http(s)` URL. The protocol check matters:
61
+ * `new URL("localhost:7234")` parses, with `localhost:` as its scheme.
62
+ */
63
+ function isAbsoluteUrl(value: string): boolean {
64
+ try {
65
+ const { protocol } = new URL(value);
66
+ return protocol === "http:" || protocol === "https:";
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
@@ -43,7 +43,7 @@ export function SelectionCheckbox({
43
43
  onChange={(e) => onChange(e.target.checked)}
44
44
  aria-label={ariaLabel}
45
45
  className={cn(
46
- "stg:size-3.5 stg:shrink-0 stg:cursor-pointer stg:rounded stg:border stg:border-input stg:accent-primary",
46
+ "stg:size-3.5 stg:shrink-0 stg:cursor-pointer stg:rounded stg:border stg:border-input",
47
47
  "stg:focus-visible:outline-none stg:focus-visible:ring-2 stg:focus-visible:ring-ring stg:focus-visible:ring-offset-1",
48
48
  className,
49
49
  )}
@@ -3,6 +3,7 @@
3
3
  import { useEffect, useMemo } from "react";
4
4
  import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
5
5
  import { ExecutionTarget } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
6
+ import { useWhoAmI } from "../iam-policy/useWhoAmI.js";
6
7
  import { useRunnerAdapter } from "../runner-adapter.js";
7
8
  import { useExecutionTarget } from "../execution-target-context.js";
8
9
  import { fromProtoExecutionTarget } from "./execution-target.js";
@@ -25,12 +26,28 @@ import { fromProtoExecutionTarget } from "./execution-target.js";
25
26
  * free. It is a no-op unless a `runnerAdapter` is configured **and** the
26
27
  * session resolves to local execution (cloud sessions are server-provisioned).
27
28
  *
28
- * The effect depends only on stable primitives (`adapter`, `sessionId`, and
29
- * the derived `effectiveTarget` string) and **never on the `session` object**.
29
+ * **Only the session's own person serves it.** Anyone a session is shared
30
+ * with can open it, and every desktop that attached would poll the same
31
+ * queue, each turn going to whichever worker asks first. A server with
32
+ * sign-in credentials a local session's runner for the person who created
33
+ * the session, so another viewer's desktop would take turns it cannot run
34
+ * (or, where it could, run someone else's turn on its own machine). The
35
+ * worker is therefore kept off exactly when the signed-in account is known
36
+ * and is not the session's creator — the creator stamp read the two ways
37
+ * the server reads it, as the account id and as the account's identity
38
+ * provider subject. Every other case attaches as before: while the account
39
+ * is loading the attach waits (as it waits for the session), and when there
40
+ * is no account (a single-user server answers `whoAmI` NOT_FOUND, and every
41
+ * session there is its operator's) the desktop serves the session.
42
+ *
43
+ * The effect depends only on stable primitives (`adapter`, `sessionId`, the
44
+ * derived `effectiveTarget` string and the derived `servesHere` flag) and
45
+ * **never on the `session` object**.
30
46
  * `useSessionConversation` refetches the session frequently; keying the effect
31
47
  * on the object would tear down and restart the worker on every refetch
32
48
  * (DD-010 / reference-stability). `executionTarget` is immutable after the
33
- * first execution, so the derived string stays stable across refetches.
49
+ * first execution, and a session's creator never changes, so both derived
50
+ * values stay stable across refetches.
34
51
  *
35
52
  * @param sessionId - The session being viewed, or `null` to skip.
36
53
  * @param session - The loaded session, or `null` while loading. The worker is
@@ -43,6 +60,7 @@ export function useLocalSessionWorker(
43
60
  ): void {
44
61
  const adapter = useRunnerAdapter();
45
62
  const contextTarget = useExecutionTarget();
63
+ const { account, isLoading: accountLoading } = useWhoAmI();
46
64
 
47
65
  // Resolve the session's effective execution target as a stable primitive.
48
66
  // `undefined` until the session loads — so we never attach a local worker
@@ -59,8 +77,21 @@ export function useLocalSessionWorker(
59
77
  );
60
78
  }, [isLoaded, specTarget, contextTarget]);
61
79
 
80
+ // Whether this viewer's desktop may serve the session, as a stable
81
+ // primitive: `undefined` while the account is loading, `true` when there
82
+ // is no account to compare or the account is the session's creator.
83
+ const creatorStamp = session?.status?.audit?.specAudit?.createdBy?.id ?? "";
84
+ const accountId = account?.metadata?.id ?? "";
85
+ const accountSubject = account?.spec?.idpId ?? "";
86
+ const servesHere = useMemo(() => {
87
+ if (accountLoading) return undefined;
88
+ if (accountId === "" || creatorStamp === "") return true;
89
+ return creatorStamp === accountId || creatorStamp === accountSubject;
90
+ }, [accountLoading, accountId, accountSubject, creatorStamp]);
91
+
62
92
  useEffect(() => {
63
93
  if (!adapter || !sessionId || effectiveTarget !== "local") return;
94
+ if (servesHere !== true) return;
64
95
 
65
96
  // Fire-and-forget: a runner-start failure must not crash the session view.
66
97
  // The host adapter's add/remove are idempotent, so React's cleanup-driven
@@ -69,5 +100,5 @@ export function useLocalSessionWorker(
69
100
  return () => {
70
101
  adapter.onSessionClosed(sessionId).catch(() => {});
71
102
  };
72
- }, [adapter, sessionId, effectiveTarget]);
103
+ }, [adapter, sessionId, effectiveTarget, servesHere]);
73
104
  }
@@ -41,10 +41,10 @@ type FlowState =
41
41
  *
42
42
  * "New channel app" is offered once the server confirms the caller holds
43
43
  * `can_create_channel_app` on the organization. The list shows only the
44
- * apps the caller may view (their creator, anyone granted viewer on one,
45
- * and the organization's admins), so a caller who may not create channel
46
- * apps is told that the organization's admins manage them rather than that
47
- * none is registered.
44
+ * apps the caller may view (the organization's admins, and anyone granted
45
+ * viewer on an app), so a caller who may not create channel apps is told
46
+ * that the organization's admins manage them rather than that none is
47
+ * registered.
48
48
  */
49
49
  export function ChannelAppsSection() {
50
50
  const headingId = useId();
@@ -0,0 +1,4 @@
1
+ "use client";
2
+
3
+ export { ProviderKeysSection } from "../billing/ProviderKeysSection.js";
4
+ export type { ProviderKeysSectionProps } from "../billing/ProviderKeysSection.js";
@@ -22,3 +22,5 @@ export { TeamsSection } from "./TeamsSection.js";
22
22
  export { OAuthAppsSection } from "./OAuthAppsSection.js";
23
23
  export { ChannelAppsSection } from "./ChannelAppsSection.js";
24
24
  export { UsageSection } from "./UsageSection.js";
25
+ export { ProviderKeysSection } from "./ProviderKeysSection.js";
26
+ export type { ProviderKeysSectionProps } from "./ProviderKeysSection.js";
@@ -10,6 +10,7 @@ import {
10
10
  FileBadge,
11
11
  Layers,
12
12
  KeyRound,
13
+ KeySquare,
13
14
  Link,
14
15
  MessageSquare,
15
16
  MousePointerClick,
@@ -102,6 +103,7 @@ export const SETTINGS_NAV_GROUPS: readonly SettingsNavGroup[] = [
102
103
  items: [
103
104
  { href: "/settings/billing", label: "Billing", icon: CreditCard },
104
105
  { href: "/settings/usage", label: "Usage", icon: BarChart3 },
106
+ { href: "/settings/provider-keys", label: "Provider keys", icon: KeySquare },
105
107
  ],
106
108
  },
107
109
  {
@@ -23,7 +23,8 @@ import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/ap
23
23
  import { Switch } from "../switch/Switch.js";
24
24
  import { Tabs, type TabItem } from "../tabs/Tabs.js";
25
25
  import { toast } from "../feedback/toast.js";
26
- import { useStigmer } from "../hooks.js";
26
+ import { usePublicBaseUrl } from "../public-base-url-context.js";
27
+ import { UnknownPublicAddressNotice } from "../internal/UnknownPublicAddressNotice.js";
27
28
  import { useCopyResource } from "../resource-detail/useCopyResource.js";
28
29
  import { useDeploymentMode } from "../deployment-mode.js";
29
30
  import { useBillingAccount } from "../billing/useBillingAccount.js";
@@ -1510,10 +1511,10 @@ function MessageField({
1510
1511
 
1511
1512
  /**
1512
1513
  * The platform client snippet, aimed at the server the console is connected
1513
- * to (`stigmer.baseUrl`) — a self-hosted admin copies code for their own
1514
- * server, not Stigmer Cloud. The base URL is normalised as the channel-app
1515
- * webhook URLs normalise it, and JSON-quoted so the line is always a valid
1516
- * string literal.
1514
+ * to — a self-hosted admin copies code for their own server, not Stigmer
1515
+ * Cloud. `apiBaseUrl` is the server's public base URL (`usePublicBaseUrl`),
1516
+ * the address the channel-app webhook URLs are built from too, JSON-quoted
1517
+ * so the line is always a valid string literal.
1517
1518
  */
1518
1519
  function buildDeveloperSnippet(
1519
1520
  apiBaseUrl: string,
@@ -1544,10 +1545,14 @@ function DeveloperTab({
1544
1545
  readonly org: string;
1545
1546
  readonly slug: string;
1546
1547
  }) {
1547
- const stigmer = useStigmer();
1548
+ const publicBaseUrl = usePublicBaseUrl();
1549
+ // `null` while the server's public address is unknown: the field says so.
1548
1550
  const snippet = useMemo(
1549
- () => buildDeveloperSnippet(stigmer.baseUrl, org, slug),
1550
- [stigmer.baseUrl, org, slug],
1551
+ () =>
1552
+ publicBaseUrl === null
1553
+ ? null
1554
+ : buildDeveloperSnippet(publicBaseUrl, org, slug),
1555
+ [publicBaseUrl, org, slug],
1551
1556
  );
1552
1557
 
1553
1558
  return (
@@ -1557,12 +1562,23 @@ function DeveloperTab({
1557
1562
  integrate this agent with the Stigmer SDK — the same runtime, full
1558
1563
  control.
1559
1564
  </p>
1560
- <CopyField
1561
- label="Platform client integration"
1562
- value={snippet}
1563
- copyLabel="Code"
1564
- multiline
1565
- />
1565
+ {snippet !== null ? (
1566
+ <CopyField
1567
+ label="Platform client integration"
1568
+ value={snippet}
1569
+ copyLabel="Code"
1570
+ multiline
1571
+ />
1572
+ ) : (
1573
+ <section>
1574
+ <h3 className="stg:text-xs stg:font-medium stg:text-muted-foreground">
1575
+ Platform client integration
1576
+ </h3>
1577
+ <div className="stg:mt-1.5">
1578
+ <UnknownPublicAddressNotice subject="the integration code" />
1579
+ </div>
1580
+ </section>
1581
+ )}
1566
1582
  <a
1567
1583
  href={PLATFORM_CLIENT_DOCS_URL}
1568
1584
  target="_blank"
@@ -18,7 +18,9 @@ export interface UseCanCreateAgentShareReturn {
18
18
  * installed copy), and creating one asks two questions of that
19
19
  * organization, both of which the create handler enforces:
20
20
  *
21
- * - `can_edit` on the agent — the share is a channel to this agent.
21
+ * - `can_manage_audience` on the agent — a share puts the agent in front of a
22
+ * wider audience, so it is the owner's decision; an editor, who may change
23
+ * the definition, may not publish it.
22
24
  * - `can_create_agent_share` on the organization (admin by default) — a
23
25
  * share spends the organization's credits with anyone on the internet,
24
26
  * so it is an admin-level decision. The org check uses the org slug as
@@ -46,14 +48,14 @@ export function useCanCreateAgentShare(
46
48
  const agentId = agent?.metadata?.id ?? "";
47
49
  const agentOrg = agent?.metadata?.org ?? "";
48
50
 
49
- const { allowed: canEditAgent } = useCheckPermission(
51
+ const { allowed: canPublishAgent } = useCheckPermission(
50
52
  agentId ? { kind: "agent", id: agentId } : null,
51
- "can_edit",
53
+ "can_manage_audience",
52
54
  );
53
55
  const { allowed: canCreateInOrg } = useCheckPermission(
54
56
  agentOrg ? { kind: "organization", id: agentOrg } : null,
55
57
  "can_create_agent_share",
56
58
  );
57
59
 
58
- return { allowed: !!agent && canEditAgent && canCreateInOrg };
60
+ return { allowed: !!agent && canPublishAgent && canCreateInOrg };
59
61
  }
@@ -162,7 +162,7 @@ export interface UseSaveAgentShareReturn {
162
162
  *
163
163
  * Commits via `stigmer.agentShare.apply()` — an idempotent upsert keyed
164
164
  * on the share's `(org, slug)` identity — so one code path serves both
165
- * creation (the server authorizes on the referenced agent's `can_edit`
165
+ * creation (the server authorizes on the referenced agent's `can_manage_audience`
166
166
  * and the organization's `can_create_agent_share`) and every later
167
167
  * edit. There is deliberately no create/update
168
168
  * branching: the share slug is unique per org, which makes
package/src/styles.css CHANGED
@@ -28,10 +28,11 @@
28
28
  self-referential `--stgm-font-sans: var(--stgm-font-sans)` cycle through
29
29
  the `@theme inline` block below).
30
30
 
31
- The `stg:` prefix contract is enforced by `scripts/check-prefixed-classnames`
32
- (unprefixed utilities in source fail the suite) and by the dist isolation
33
- invariant test (unscoped selectors in the built stylesheet fail the suite).
34
- The one-shot migration lives in `scripts/prefix-classnames.ts`. */
31
+ The `stg:` prefix contract is enforced by `scripts/prefix-classnames.ts
32
+ --check` (`npm run lint:prefix`: an unprefixed utility, or a prefix placed
33
+ after a variant, fails the lint) and by the dist isolation invariant test
34
+ (unscoped selectors in the built stylesheet fail the suite). The same
35
+ script, without `--check`, is the one-shot migration. */
35
36
  @import "tailwindcss/theme.css" layer(stgm) prefix(stg);
36
37
  @import "tailwindcss/utilities.css" layer(stgm) prefix(stg);
37
38
  /* Enter/exit animation utilities (`stg:animate-in`, `stg:fade-in`,
@@ -294,6 +295,15 @@
294
295
  color-scheme: dark;
295
296
  }
296
297
 
298
+ /* The same holds for a checkbox's or radio's checked state: the browser
299
+ paints it from `accent-color` (its own blue by default), never from our
300
+ tokens (#1228). One inherited rule on both containers gives every native
301
+ control inside the scope the theme's primary, in both modes and every
302
+ preset, so no control needs its own accent class. */
303
+ .stgm {
304
+ accent-color: var(--stgm-primary);
305
+ }
306
+
297
307
  /* The provider's in-tree container ([data-stgm-root]) ships with NO
298
308
  layout styling — deliberately (#260, DD-019). A default
299
309
  `height: 100%` looks benign ("resolves to auto under auto-height
@@ -31,10 +31,13 @@ export interface WorkflowTaskApprovalSummaryProps {
31
31
  */
32
32
  readonly outcomes: readonly TaskOutcome[];
33
33
  /**
34
- * The recorded decision, sourced from the canonical task output. When
35
- * `null` — or when its `outcome` is still empty — the gate has been
36
- * resolved but the decision record has not yet materialized in the
37
- * status snapshot; a "finalizing" affordance is shown instead.
34
+ * The recorded decision, sourced from the canonical task output with the
35
+ * `approval_resolved` event standing in until the status snapshot
36
+ * catches up. When `null` — or when its `outcome` is still empty — the
37
+ * gate has been resolved but neither source carries the decision yet; a
38
+ * "finalizing" affordance is shown instead. The one exception is an
39
+ * auto-resolved decision with an empty outcome: the gate timed out under
40
+ * the fail policy and ended without a decision, which is final.
38
41
  */
39
42
  readonly decision: TaskDetailApprovalDecision | null;
40
43
  /** Additional CSS class names for the root container. */
@@ -73,13 +76,17 @@ export const WorkflowTaskApprovalSummary = memo(function WorkflowTaskApprovalSum
73
76
  decision,
74
77
  className,
75
78
  }: WorkflowTaskApprovalSummaryProps) {
76
- const isFinalizing = !decision || decision.outcome === "";
79
+ // A fail-policy timeout ends the gate with no outcome; that is the record,
80
+ // not a decision still on its way.
81
+ const timedOutUndecided = decision !== null && decision.autoResolved && decision.outcome === "";
82
+ const isFinalizing = !decision || (decision.outcome === "" && !timedOutUndecided);
77
83
 
78
84
  const outcomeLabel = useMemo(() => {
85
+ if (timedOutUndecided) return "Timed out — no decision";
79
86
  if (!decision || !decision.outcome) return "";
80
87
  const match = outcomes.find((o) => o.name === decision.outcome);
81
88
  return match?.label || capitalize(decision.outcome.replace(/_/g, " "));
82
- }, [decision, outcomes]);
89
+ }, [decision, outcomes, timedOutUndecided]);
83
90
 
84
91
  const tone: DecisionTone = useMemo(
85
92
  () => (decision && !isFinalizing ? resolveTone(decision.outcome) : "neutral"),
@@ -116,7 +123,7 @@ export const WorkflowTaskApprovalSummary = memo(function WorkflowTaskApprovalSum
116
123
  <ToneIcon tone={tone} />
117
124
  {outcomeLabel}
118
125
  </span>
119
- {decision!.autoResolved && (
126
+ {decision!.autoResolved && !timedOutUndecided && (
120
127
  <span className="stg:rounded stg:bg-muted stg:px-1.5 stg:py-0.5 stg:text-[10px] stg:font-medium stg:text-muted-foreground">
121
128
  auto-resolved
122
129
  </span>
@@ -295,7 +295,7 @@ function ForkBranches({
295
295
  name={`join-policy-${node.id}`}
296
296
  checked={!compete}
297
297
  onChange={() => handleSetCompete(false)}
298
- className="stg:h-3 stg:w-3 stg:accent-[var(--stgm-primary,#6366f1)]"
298
+ className="stg:h-3 stg:w-3"
299
299
  />
300
300
  Wait for all branches
301
301
  </label>
@@ -305,7 +305,7 @@ function ForkBranches({
305
305
  name={`join-policy-${node.id}`}
306
306
  checked={compete}
307
307
  onChange={() => handleSetCompete(true)}
308
- className="stg:h-3 stg:w-3 stg:accent-[var(--stgm-primary,#6366f1)]"
308
+ className="stg:h-3 stg:w-3"
309
309
  />
310
310
  Race mode (first branch wins)
311
311
  </label>
@@ -135,7 +135,7 @@ export const CatchTab = memo(function CatchTab({ node, mutations }: CatchTabProp
135
135
  type="checkbox"
136
136
  checked={catchBlock.compensate}
137
137
  onChange={handleCompensateToggle}
138
- className="stg:h-3 stg:w-3 stg:accent-[var(--stgm-primary,#6366f1)]"
138
+ className="stg:h-3 stg:w-3"
139
139
  />
140
140
  Run compensation before catch block
141
141
  </label>
@@ -194,7 +194,7 @@ export const IterationTab = memo(function IterationTab({
194
194
  value={opt.value}
195
195
  checked={onError === opt.value}
196
196
  onChange={() => handleErrorPolicyChange(opt.value)}
197
- className="stg:mt-0.5 stg:h-3 stg:w-3 stg:accent-[var(--stgm-primary,#6366f1)]"
197
+ className="stg:mt-0.5 stg:h-3 stg:w-3"
198
198
  />
199
199
  <span>
200
200
  <span className="stg:font-medium">{opt.label}</span>
@@ -68,15 +68,16 @@ export function WorkflowInstanceDetailPanel({
68
68
  const visibility = meta?.visibility ?? ApiResourceVisibility.visibility_private;
69
69
  // Run observability is a separate axis from instance visibility, and only
70
70
  // meaningful while the instance is private (ORG/PUBLIC already expose runs
71
- // by inheritance). It is offered only to those who can grant access, so it
72
- // rides into the dialog as the resource-specific section — present only when
73
- // both conditions hold.
74
- const { allowed: canGrantAccess } = useCheckPermission(
71
+ // by inheritance). It is offered only to those who may change who reaches
72
+ // the instance (can_manage_audience, the server's bar on
73
+ // updateExecutionVisibility), so it rides into the dialog as the
74
+ // resource-specific section — present only when both conditions hold.
75
+ const { allowed: canManageAudience } = useCheckPermission(
75
76
  id ? { kind: "workflow_instance", id } : null,
76
- "can_grant_access",
77
+ "can_manage_audience",
77
78
  );
78
79
  const runVisibilitySection =
79
- visibility === ApiResourceVisibility.visibility_private && canGrantAccess
80
+ visibility === ApiResourceVisibility.visibility_private && canManageAudience
80
81
  ? {
81
82
  title: "Run visibility",
82
83
  description:
@@ -50,15 +50,6 @@ export interface TaskApprovalRequestView {
50
50
  readonly payloadArtifactId: string | null;
51
51
  }
52
52
 
53
- /**
54
- * A resolved human_input decision, sourced from the canonical task-output
55
- * record (the runner stores the reviewer's full response as the task
56
- * output) and enriched with timing from the `approval_resolved` event.
57
- *
58
- * `outcome` is the empty string during the brief window after a decision
59
- * is signalled but before the status snapshot reflects the task output —
60
- * consumers render a "finalizing" affordance in that state.
61
- */
62
53
  /**
63
54
  * Display identity of the reviewer, snapshotted server-side at decision
64
55
  * time (see `reviewer_actor` in the human_input task-output contract).
@@ -95,6 +86,17 @@ export interface TaskReviewerView {
95
86
  readonly isRawId: boolean;
96
87
  }
97
88
 
89
+ /**
90
+ * A resolved human_input decision, sourced from the canonical task-output
91
+ * record (the runner stores the reviewer's full response as the task
92
+ * output), with the `approval_resolved` event standing in for any field the
93
+ * status snapshot has not captured yet and supplying the wait duration.
94
+ *
95
+ * `outcome` is the empty string only when neither source carries it: in
96
+ * the brief window before the status snapshot reflects the task output,
97
+ * for an event recorded before the payload had an `outcome` field.
98
+ * Consumers render a "finalizing" affordance in that state.
99
+ */
98
100
  export interface TaskDetailApprovalDecision {
99
101
  /** Chosen outcome identifier (e.g. "approve", "pause_campaigns"). */
100
102
  readonly outcome: string;
@@ -166,7 +168,7 @@ export function deriveTaskApprovalDecision(
166
168
  if (resolution === null && outputOutcome === "") return null;
167
169
 
168
170
  return {
169
- outcome: outputOutcome,
171
+ outcome: outputOutcome || (resolution?.outcome ?? ""),
170
172
  reviewer:
171
173
  readSnapshotString(taskOutput, "reviewer") || (resolution?.resolvedBy ?? ""),
172
174
  reviewerActor: deriveReviewerActor(resolution, taskOutput),
@@ -175,15 +177,20 @@ export function deriveTaskApprovalDecision(
175
177
  readSnapshotString(taskOutput, "comment") || (resolution?.comment ?? ""),
176
178
  formData: readSnapshotObject(taskOutput, "form_data"),
177
179
  waitDurationMs: resolution ? Number(resolution.waitDurationMs) : 0,
178
- autoResolved: taskOutput?.["auto_resolved"] === true,
180
+ // Output first, like every field above: once the snapshot carries the
181
+ // decision it is the record, and the event only fills the window before.
182
+ autoResolved:
183
+ outputOutcome !== ""
184
+ ? taskOutput?.["auto_resolved"] === true
185
+ : resolution?.autoResolved === true,
179
186
  };
180
187
  }
181
188
 
182
189
  /**
183
190
  * Collapses a decision's attribution into the render-ready reviewer view,
184
191
  * or `null` when the decision carries no attribution at all (timeout
185
- * auto-resolution, OSS single-user edition) — consumers omit the "by …"
186
- * segment entirely in that case.
192
+ * auto-resolution) — consumers omit the "by …" segment entirely in that
193
+ * case.
187
194
  */
188
195
  export function deriveTaskReviewer(
189
196
  decision: TaskDetailApprovalDecision,