@opengeni/react 0.48.0 → 0.57.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 (325) hide show
  1. package/README.md +138 -2
  2. package/dist/artifacts-document.d.ts +3 -0
  3. package/dist/artifacts-document.js +17 -0
  4. package/dist/artifacts-document.js.map +1 -0
  5. package/dist/artifacts-presentation.d.ts +3 -0
  6. package/dist/artifacts-presentation.js +17 -0
  7. package/dist/artifacts-presentation.js.map +1 -0
  8. package/dist/artifacts-spreadsheet.d.ts +3 -0
  9. package/dist/artifacts-spreadsheet.js +17 -0
  10. package/dist/artifacts-spreadsheet.js.map +1 -0
  11. package/dist/artifacts.d.ts +2 -0
  12. package/dist/artifacts.js +199 -0
  13. package/dist/artifacts.js.map +1 -0
  14. package/dist/{chunk-NJ7M2B6Z.js → chunk-5UQPNLIO.js} +16 -8
  15. package/dist/{chunk-NJ7M2B6Z.js.map → chunk-5UQPNLIO.js.map} +1 -1
  16. package/dist/{chunk-LHLYWG5R.js → chunk-6KLX2RH5.js} +461 -38
  17. package/dist/chunk-6KLX2RH5.js.map +1 -0
  18. package/dist/chunk-7WCLAEFV.js +2828 -0
  19. package/dist/chunk-7WCLAEFV.js.map +1 -0
  20. package/dist/{chunk-I4MPFDRP.js → chunk-B6WPZSTT.js} +104 -104
  21. package/dist/{chunk-I4MPFDRP.js.map → chunk-B6WPZSTT.js.map} +1 -1
  22. package/dist/{chunk-EKZH6IDG.js → chunk-C5ZPSSS5.js} +111 -1
  23. package/dist/chunk-C5ZPSSS5.js.map +1 -0
  24. package/dist/{chunk-OL6QVXGF.js → chunk-HA46CYQK.js} +152 -8
  25. package/dist/chunk-HA46CYQK.js.map +1 -0
  26. package/dist/chunk-HUIKIDM5.js +2148 -0
  27. package/dist/chunk-HUIKIDM5.js.map +1 -0
  28. package/dist/{chunk-FWXGSNXT.js → chunk-IGC6A2ZI.js} +585 -34
  29. package/dist/chunk-IGC6A2ZI.js.map +1 -0
  30. package/dist/chunk-JVYQRRC4.js +53 -0
  31. package/dist/chunk-JVYQRRC4.js.map +1 -0
  32. package/dist/chunk-KCGXPP5V.js +157 -0
  33. package/dist/chunk-KCGXPP5V.js.map +1 -0
  34. package/dist/chunk-LLOLUHVG.js +5937 -0
  35. package/dist/chunk-LLOLUHVG.js.map +1 -0
  36. package/dist/{chunk-WYA65Y3F.js → chunk-MKPIAGVL.js} +13 -3
  37. package/dist/chunk-MKPIAGVL.js.map +1 -0
  38. package/dist/chunk-PEI6RANI.js +3234 -0
  39. package/dist/chunk-PEI6RANI.js.map +1 -0
  40. package/dist/chunk-R3AZOAB5.js +933 -0
  41. package/dist/chunk-R3AZOAB5.js.map +1 -0
  42. package/dist/{chunk-O4VXOBGF.js → chunk-WSK7G5LE.js} +70 -2
  43. package/dist/chunk-WSK7G5LE.js.map +1 -0
  44. package/dist/{chunk-23EJ676W.js → chunk-XBZWOM2K.js} +9 -4
  45. package/dist/chunk-XBZWOM2K.js.map +1 -0
  46. package/dist/{chunk-XCNTXWBB.js → chunk-Y7OEDUJ5.js} +856 -338
  47. package/dist/chunk-Y7OEDUJ5.js.map +1 -0
  48. package/dist/client.d.ts +10 -2
  49. package/dist/clipboard.d.ts +1 -0
  50. package/dist/clipboard.js +7 -0
  51. package/dist/clipboard.js.map +1 -0
  52. package/dist/commands/index.d.ts +3 -3
  53. package/dist/commands/registry.d.ts +1 -1
  54. package/dist/commands/types.d.ts +1 -1
  55. package/dist/components/approval-surface.d.ts +2 -2
  56. package/dist/components/artifacts/artifact-surface.d.ts +18 -0
  57. package/dist/components/artifacts/document-editor.d.ts +172 -0
  58. package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
  59. package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
  60. package/dist/components/artifacts/editable-document.d.ts +24 -0
  61. package/dist/components/artifacts/editable-presentation.d.ts +24 -0
  62. package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
  63. package/dist/components/artifacts/index.d.ts +9 -0
  64. package/dist/components/artifacts/presentation-editor.d.ts +297 -0
  65. package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
  66. package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
  67. package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
  68. package/dist/components/browser-input.d.ts +7 -0
  69. package/dist/components/browser-viewer.d.ts +30 -0
  70. package/dist/components/chat-composer.d.ts +8 -8
  71. package/dist/components/code-editor.d.ts +25 -3
  72. package/dist/components/command-palette.d.ts +2 -2
  73. package/dist/components/composer-transcription-control.d.ts +2 -2
  74. package/dist/components/composer.d.ts +17 -17
  75. package/dist/components/computer-viewer.d.ts +24 -0
  76. package/dist/components/copy-button.d.ts +2 -2
  77. package/dist/components/desktop-viewer.d.ts +50 -22
  78. package/dist/components/diff-view.d.ts +1 -1
  79. package/dist/components/enrollment-consent.d.ts +1 -1
  80. package/dist/components/enrollment-device-flow.d.ts +1 -1
  81. package/dist/components/file-browser.d.ts +2 -2
  82. package/dist/components/fleet-tile.d.ts +1 -1
  83. package/dist/components/generated-video-player.d.ts +10 -0
  84. package/dist/components/human-input-form.d.ts +1 -1
  85. package/dist/components/human-input-surface.d.ts +2 -2
  86. package/dist/components/interaction-intervention-banner.d.ts +11 -0
  87. package/dist/components/interactive-workbench-viewers.d.ts +2 -0
  88. package/dist/components/machine-card.d.ts +2 -2
  89. package/dist/components/machine-dock-bar.d.ts +3 -3
  90. package/dist/components/machine-health-pill.d.ts +2 -2
  91. package/dist/components/machine-input-display.d.ts +1 -1
  92. package/dist/components/machine-metrics.d.ts +2 -2
  93. package/dist/components/machine-status-pill.d.ts +4 -4
  94. package/dist/components/machines/health.d.ts +1 -1
  95. package/dist/components/machines/machine-detail.d.ts +3 -3
  96. package/dist/components/machines/metric-history-chart.d.ts +1 -1
  97. package/dist/components/machines/metric-sparkline.d.ts +2 -2
  98. package/dist/components/machines/series.d.ts +2 -2
  99. package/dist/components/machines-dashboard.d.ts +2 -2
  100. package/dist/components/markdown.d.ts +1 -1
  101. package/dist/components/message-timeline.d.ts +12 -5
  102. package/dist/components/model-picker.d.ts +2 -2
  103. package/dist/components/model-policy-picker.d.ts +7 -7
  104. package/dist/components/pierre-diff.d.ts +1 -1
  105. package/dist/components/pierre-file.d.ts +1 -1
  106. package/dist/components/queue-draft-policy.d.ts +1 -1
  107. package/dist/components/queue-surface-implementation.d.ts +3 -3
  108. package/dist/components/queue-surface-state.d.ts +4 -4
  109. package/dist/components/queue-surface.d.ts +5 -5
  110. package/dist/components/sandbox-files.d.ts +3 -3
  111. package/dist/components/sandbox-terminal.d.ts +24 -4
  112. package/dist/components/sandbox-workspace.d.ts +28 -9
  113. package/dist/components/session-chrome.d.ts +4 -4
  114. package/dist/components/session-status.d.ts +2 -2
  115. package/dist/components/timeline-annotation-selection.d.ts +9 -0
  116. package/dist/components/timeline-annotations-dialog.d.ts +16 -0
  117. package/dist/components/timeline-annotations.d.ts +13 -0
  118. package/dist/components/tooltip.d.ts +4 -4
  119. package/dist/components/user-message-body.d.ts +2 -2
  120. package/dist/components/workbench-changes.d.ts +2 -2
  121. package/dist/components/workspace-dock.d.ts +1 -1
  122. package/dist/composer.d.ts +11 -11
  123. package/dist/composer.js +7 -7
  124. package/dist/hooks/internal.d.ts +1 -1
  125. package/dist/hooks/use-attached-browsers.d.ts +21 -0
  126. package/dist/hooks/use-auth-runs.d.ts +32 -0
  127. package/dist/hooks/use-available-models.d.ts +2 -2
  128. package/dist/hooks/use-billing-usage.d.ts +1 -1
  129. package/dist/hooks/use-browser-downloads.d.ts +25 -0
  130. package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
  131. package/dist/hooks/use-browser-identities.d.ts +30 -0
  132. package/dist/hooks/use-browser-session.d.ts +29 -0
  133. package/dist/hooks/use-browser-sessions.d.ts +32 -0
  134. package/dist/hooks/use-channels.d.ts +26 -0
  135. package/dist/hooks/use-codex-accounts.d.ts +2 -2
  136. package/dist/hooks/use-composer.d.ts +36 -9
  137. package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
  138. package/dist/hooks/use-computer-session.d.ts +29 -0
  139. package/dist/hooks/use-computer-sessions.d.ts +28 -0
  140. package/dist/hooks/use-desktop-stream.d.ts +5 -1
  141. package/dist/hooks/use-environments.d.ts +1 -1
  142. package/dist/hooks/use-file-attachments.d.ts +1 -1
  143. package/dist/hooks/use-goal.d.ts +2 -2
  144. package/dist/hooks/use-human-input.d.ts +2 -2
  145. package/dist/hooks/use-interaction-interventions.d.ts +27 -0
  146. package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
  147. package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
  148. package/dist/hooks/use-machine-chip.d.ts +2 -2
  149. package/dist/hooks/use-machines.d.ts +4 -4
  150. package/dist/hooks/use-network-routes.d.ts +25 -0
  151. package/dist/hooks/use-packs.d.ts +7 -2
  152. package/dist/hooks/use-rigs.d.ts +1 -1
  153. package/dist/hooks/use-sandbox-files.d.ts +6 -4
  154. package/dist/hooks/use-sandbox-git.d.ts +6 -5
  155. package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
  156. package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
  157. package/dist/hooks/use-session-capabilities.d.ts +5 -4
  158. package/dist/hooks/use-session-control.d.ts +1 -1
  159. package/dist/hooks/use-session-events.d.ts +2 -2
  160. package/dist/hooks/use-session-lineage.d.ts +1 -1
  161. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  162. package/dist/hooks/use-session.d.ts +2 -2
  163. package/dist/hooks/use-site-auth-connections.d.ts +25 -0
  164. package/dist/hooks/use-slash-commands.d.ts +1 -1
  165. package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
  166. package/dist/hooks/use-terminal-stream.d.ts +11 -2
  167. package/dist/hooks/use-turn-queue.d.ts +2 -2
  168. package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
  169. package/dist/hooks/use-voice-input.d.ts +1 -1
  170. package/dist/hooks/use-workspace-capture.d.ts +1 -1
  171. package/dist/hooks/use-workspace-edit.d.ts +1 -1
  172. package/dist/hooks/use-workspace-sessions.d.ts +1 -1
  173. package/dist/hooks/use-workspaces.d.ts +1 -1
  174. package/dist/index.d.ts +162 -155
  175. package/dist/index.js +1822 -1703
  176. package/dist/index.js.map +1 -1
  177. package/dist/interaction.d.ts +33 -0
  178. package/dist/interaction.js +550 -0
  179. package/dist/interaction.js.map +1 -0
  180. package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
  181. package/dist/interactive-workbench-viewers-TOHBXJEE.js.map +1 -0
  182. package/dist/lib/host-platform.d.ts +5 -0
  183. package/dist/lib/relay-wire.d.ts +24 -1
  184. package/dist/lib/sandbox-liveness.d.ts +9 -0
  185. package/dist/lib/terminal-capability.d.ts +5 -0
  186. package/dist/lib/xterm-theme.d.ts +1 -1
  187. package/dist/machines.d.ts +30 -30
  188. package/dist/machines.js +2 -2
  189. package/dist/model-policy.d.ts +1 -1
  190. package/dist/model-policy.js +1 -1
  191. package/dist/provider.d.ts +6 -5
  192. package/dist/{queue-surface-implementation-U5JFG7HT.js → queue-surface-implementation-2KVJTYXH.js} +100 -48
  193. package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
  194. package/dist/realtime/dropdown-menu.d.ts +8 -8
  195. package/dist/realtime/realtime-control.d.ts +8 -8
  196. package/dist/realtime.d.ts +4 -4
  197. package/dist/realtime.js +38 -11
  198. package/dist/realtime.js.map +1 -1
  199. package/dist/session-context.d.ts +28 -2
  200. package/dist/session-ui.d.ts +14 -14
  201. package/dist/session-ui.js +5 -4
  202. package/dist/session.d.ts +28 -28
  203. package/dist/session.js +10 -10
  204. package/dist/timeline/activity-rail.d.ts +5 -3
  205. package/dist/timeline/disclosure-context.d.ts +1 -1
  206. package/dist/timeline/fleet-decision-projection.d.ts +1 -1
  207. package/dist/timeline/fleet-decision-row.d.ts +3 -3
  208. package/dist/timeline/index.d.ts +19 -19
  209. package/dist/timeline/parsers.d.ts +4 -1
  210. package/dist/timeline/projection.d.ts +3 -3
  211. package/dist/timeline/registry.d.ts +8 -2
  212. package/dist/timeline/screenshot-lightbox.d.ts +2 -2
  213. package/dist/timeline/shared.d.ts +21 -10
  214. package/dist/timeline/tool-diff.d.ts +2 -2
  215. package/dist/timeline/tool-renderers.d.ts +1 -1
  216. package/dist/timeline/turn-summary.d.ts +3 -3
  217. package/dist/timeline/types.d.ts +44 -3
  218. package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
  219. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
  220. package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
  221. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
  222. package/package.json +36 -7
  223. package/src/artifacts-document.ts +31 -0
  224. package/src/artifacts-presentation.ts +36 -0
  225. package/src/artifacts-spreadsheet.ts +21 -0
  226. package/src/artifacts.ts +89 -0
  227. package/src/client.ts +157 -1
  228. package/src/clipboard.ts +2 -0
  229. package/src/components/artifacts/artifact-surface.tsx +93 -0
  230. package/src/components/artifacts/document-editor.tsx +2255 -0
  231. package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
  232. package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
  233. package/src/components/artifacts/editable-document.tsx +657 -0
  234. package/src/components/artifacts/editable-presentation.tsx +1020 -0
  235. package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
  236. package/src/components/artifacts/index.ts +104 -0
  237. package/src/components/artifacts/presentation-editor.tsx +3148 -0
  238. package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
  239. package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
  240. package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
  241. package/src/components/browser-input.ts +65 -0
  242. package/src/components/browser-viewer.tsx +2702 -0
  243. package/src/components/chat-composer.tsx +13 -0
  244. package/src/components/code-editor.tsx +89 -30
  245. package/src/components/composer-transcription-control.tsx +10 -6
  246. package/src/components/computer-viewer.tsx +1636 -0
  247. package/src/components/desktop-viewer.tsx +215 -74
  248. package/src/components/generated-video-player.tsx +89 -0
  249. package/src/components/interaction-intervention-banner.tsx +99 -0
  250. package/src/components/interactive-workbench-viewers.ts +2 -0
  251. package/src/components/machine-input-display.ts +3 -0
  252. package/src/components/machines/machine-detail.tsx +1 -1
  253. package/src/components/message-timeline.tsx +289 -32
  254. package/src/components/model-policy-picker.tsx +15 -0
  255. package/src/components/queue-surface-implementation.tsx +116 -60
  256. package/src/components/sandbox-files.tsx +2 -2
  257. package/src/components/sandbox-terminal.tsx +128 -46
  258. package/src/components/sandbox-workspace.tsx +256 -90
  259. package/src/components/session-chrome.tsx +53 -13
  260. package/src/components/timeline-annotation-selection.tsx +221 -0
  261. package/src/components/timeline-annotations-dialog.tsx +213 -0
  262. package/src/components/timeline-annotations.tsx +84 -0
  263. package/src/components/workbench-changes.tsx +188 -31
  264. package/src/components/workspace-dock.tsx +59 -5
  265. package/src/hooks/use-attached-browsers.ts +154 -0
  266. package/src/hooks/use-auth-runs.ts +278 -0
  267. package/src/hooks/use-browser-downloads.ts +218 -0
  268. package/src/hooks/use-browser-frame-stream.ts +425 -0
  269. package/src/hooks/use-browser-identities.ts +266 -0
  270. package/src/hooks/use-browser-session.ts +543 -0
  271. package/src/hooks/use-browser-sessions.ts +286 -0
  272. package/src/hooks/use-channels.ts +99 -0
  273. package/src/hooks/use-composer.ts +769 -24
  274. package/src/hooks/use-computer-frame-stream.ts +434 -0
  275. package/src/hooks/use-computer-session.ts +462 -0
  276. package/src/hooks/use-computer-sessions.ts +251 -0
  277. package/src/hooks/use-desktop-stream.ts +39 -7
  278. package/src/hooks/use-interaction-interventions.ts +242 -0
  279. package/src/hooks/use-interaction-invalidation.ts +66 -0
  280. package/src/hooks/use-machine-chip.ts +15 -3
  281. package/src/hooks/use-machines.ts +33 -3
  282. package/src/hooks/use-network-routes.ts +214 -0
  283. package/src/hooks/use-packs.ts +51 -0
  284. package/src/hooks/use-sandbox-files.ts +98 -55
  285. package/src/hooks/use-sandbox-git.ts +93 -38
  286. package/src/hooks/use-sandbox-terminal.ts +3 -2
  287. package/src/hooks/use-session-capabilities.ts +59 -46
  288. package/src/hooks/use-site-auth-connections.ts +226 -0
  289. package/src/hooks/use-supergrok-accounts.ts +81 -0
  290. package/src/hooks/use-terminal-stream.ts +359 -47
  291. package/src/hooks/use-video-artifact-playback.ts +28 -0
  292. package/src/hooks/use-workspace-edit.ts +2 -1
  293. package/src/index.ts +18 -0
  294. package/src/interaction.ts +101 -0
  295. package/src/lib/host-platform.ts +10 -0
  296. package/src/lib/relay-wire.ts +47 -4
  297. package/src/lib/sandbox-liveness.ts +11 -0
  298. package/src/lib/terminal-capability.ts +10 -0
  299. package/src/model-policy.ts +19 -4
  300. package/src/provider.tsx +133 -1
  301. package/src/realtime/realtime-control.tsx +34 -6
  302. package/src/session-context.ts +156 -1
  303. package/src/timeline/activity-rail.tsx +25 -3
  304. package/src/timeline/index.ts +3 -0
  305. package/src/timeline/parsers.ts +14 -1
  306. package/src/timeline/projection.ts +269 -14
  307. package/src/timeline/registry.ts +13 -1
  308. package/src/timeline/screenshot-lightbox.tsx +19 -9
  309. package/src/timeline/shared.tsx +56 -8
  310. package/src/timeline/tool-renderers.tsx +303 -50
  311. package/src/timeline/types.ts +57 -3
  312. package/styles/compiled.css +312 -21
  313. package/styles/index.css +15 -0
  314. package/styles/responsive.css +13 -1
  315. package/dist/chunk-23EJ676W.js.map +0 -1
  316. package/dist/chunk-EKZH6IDG.js.map +0 -1
  317. package/dist/chunk-FWXGSNXT.js.map +0 -1
  318. package/dist/chunk-LHLYWG5R.js.map +0 -1
  319. package/dist/chunk-O4VXOBGF.js.map +0 -1
  320. package/dist/chunk-OL6QVXGF.js.map +0 -1
  321. package/dist/chunk-WYA65Y3F.js.map +0 -1
  322. package/dist/chunk-XCNTXWBB.js.map +0 -1
  323. package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
  324. package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
  325. package/dist/queue-surface-implementation-U5JFG7HT.js.map +0 -1
@@ -1,11 +1,13 @@
1
1
  import type {
2
2
  GitFileDiff,
3
+ SessionCapabilities,
3
4
  SessionEvent,
4
5
  WorkspaceCaptureManifest,
5
6
  WorkspaceCaptureRepo,
6
7
  } from "@opengeni/sdk";
7
8
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
8
9
  import { type ClientOverride, useOpenGeni } from "../provider";
10
+ import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
9
11
 
10
12
  export type UseSandboxGitOptions = ClientOverride & {
11
13
  /** Live event log (usually `useSessionEvents().events`) — drives auto-refresh
@@ -26,9 +28,10 @@ export type UseSandboxGitOptions = ClientOverride & {
26
28
  enabled?: boolean | undefined;
27
29
  /** Pause live reads/invalidation while preserving the last rendered result. */
28
30
  active?: boolean | undefined;
29
- /** Lease liveness ("warm" | "draining" | "cold"). When NOT warm, the diff is
30
- * served from the capture (cold/offline) instead of a live `gitDiff` RPC. */
31
- liveness?: string | undefined;
31
+ /** Lease liveness. `null` means capability negotiation is still pending and
32
+ * blocks live I/O. When not warm, the diff is served from the capture instead
33
+ * of a live `gitDiff` RPC. Omission preserves legacy direct-hook behavior. */
34
+ liveness?: SessionCapabilities["liveness"] | null | undefined;
32
35
  /** The latest turn-end capture (from `useWorkspaceCapture`). Seeds the diff
33
36
  * immediately; a warm box reconciles live and leaves this durable review
34
37
  * surface in place if the live request is temporarily unavailable. */
@@ -93,6 +96,11 @@ function uniqueRoots(roots: readonly string[]): string[] {
93
96
  return [...new Set(roots.map(normalizeRoot))].sort((a, b) => a.localeCompare(b));
94
97
  }
95
98
 
99
+ // GitReadBatchRequest is contract-bounded to 32 items. Keep workspace-wide
100
+ // reads bounded without changing the wire limit; sequential chunks preserve
101
+ // deterministic root/result mapping and cap Channel-A lease concurrency at one.
102
+ const GIT_READ_BATCH_REQUEST_LIMIT = 32;
103
+
96
104
  /** Find the capture repo matching `repoPath` (default workspace root). */
97
105
  function repoForPath(
98
106
  manifest: WorkspaceCaptureManifest,
@@ -201,7 +209,13 @@ export function useSandboxGit(
201
209
  // remains live-only; legacy captures simply lack branchDiff and fall back via
202
210
  // the workspace controller without inventing an empty branch comparison.
203
211
  const capture = comparison === "staged" ? null : (options.capture ?? null);
204
- const isLive = options.liveness === "warm" || options.liveness === "draining";
212
+ const isLive = sandboxAcceptsLiveIo(options.liveness);
213
+ // `liveness` is optional for compatibility with direct hook consumers. Explicit
214
+ // `null` (negotiating) and lifecycle values require authoritative warm.
215
+ const acceptsEventReads = options.liveness === undefined || isLive;
216
+ const acceptsEventReadsRef = useRef(acceptsEventReads);
217
+ acceptsEventReadsRef.current = acceptsEventReads;
218
+ const hasLiveIoFence = options.liveness !== undefined;
205
219
  const identityKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${repoPathsKey}\u0000${workspaceWide}\u0000${comparison}`;
206
220
 
207
221
  const [diff, setDiff] = useState<SandboxGitFileDiff[]>([]);
@@ -251,35 +265,54 @@ export function useSandboxGit(
251
265
  // take several seconds on a remote sandbox. The legacy root probe remains
252
266
  // serial so a non-repository workspace never receives a failing git diff.
253
267
  const prefetched = hasAdvertisedRepoPaths
254
- ? await client
255
- .gitReadBatch(
256
- workspaceId,
257
- sessionId,
258
- {
259
- requests: repoPaths.map((root) => ({
260
- status: { path: root },
261
- diff: {
262
- path: root,
263
- ...(comparison === "branch"
264
- ? { fromRef: "origin/HEAD", includeUntracked: true }
265
- : comparison === "working"
266
- ? { fromRef: "HEAD", includeUntracked: true }
267
- : { staged: true, includeUntracked: false }),
268
- },
269
- })),
270
- },
271
- { signal: refreshAbort.signal },
272
- )
273
- .then((batch) =>
274
- batch.results.map((item, index) => ({
275
- root: repoPaths[index] ?? "",
276
- status: item.status,
277
- result: item.diff ?? {
278
- files: [],
279
- revision: item.status.revision,
268
+ ? await (async () => {
269
+ const prefetchedRepos: Array<{
270
+ root: string;
271
+ status: Awaited<ReturnType<typeof client.gitStatus>>;
272
+ result: Awaited<ReturnType<typeof client.gitDiff>>;
273
+ }> = [];
274
+ for (
275
+ let offset = 0;
276
+ offset < repoPaths.length;
277
+ offset += GIT_READ_BATCH_REQUEST_LIMIT
278
+ ) {
279
+ const roots = repoPaths.slice(offset, offset + GIT_READ_BATCH_REQUEST_LIMIT);
280
+ const batch = await client.gitReadBatch(
281
+ workspaceId,
282
+ sessionId,
283
+ {
284
+ requests: roots.map((root) => ({
285
+ status: { path: root },
286
+ diff: {
287
+ path: root,
288
+ ...(comparison === "branch"
289
+ ? { fromRef: "origin/HEAD", includeUntracked: true }
290
+ : comparison === "working"
291
+ ? { fromRef: "HEAD", includeUntracked: true }
292
+ : { staged: true, includeUntracked: false }),
293
+ },
294
+ })),
280
295
  },
281
- })),
282
- )
296
+ { signal: refreshAbort.signal },
297
+ );
298
+ if (batch.results.length !== roots.length) {
299
+ throw new Error(
300
+ `Workspace Git batch returned ${batch.results.length} results for ${roots.length} repositories.`,
301
+ );
302
+ }
303
+ batch.results.forEach((item, index) => {
304
+ prefetchedRepos.push({
305
+ root: roots[index]!,
306
+ status: item.status,
307
+ result: item.diff ?? {
308
+ files: [],
309
+ revision: item.status.revision,
310
+ },
311
+ });
312
+ });
313
+ }
314
+ return prefetchedRepos;
315
+ })()
283
316
  : null;
284
317
  const statuses =
285
318
  prefetched ??
@@ -427,7 +460,7 @@ export function useSandboxGit(
427
460
  if (!active) setLoading(false);
428
461
  return;
429
462
  }
430
- if (isLive || !currentCapture) {
463
+ if (isLive || (!hasLiveIoFence && !currentCapture)) {
431
464
  void refresh();
432
465
  }
433
466
  return () => {
@@ -435,14 +468,36 @@ export function useSandboxGit(
435
468
  refreshAbortRef.current = null;
436
469
  refreshGenerationRef.current += 1;
437
470
  };
438
- }, [enabled, active, isLive, captureRevision, identityKey, refresh, seedFromCapture]);
471
+ }, [
472
+ enabled,
473
+ active,
474
+ isLive,
475
+ hasLiveIoFence,
476
+ captureRevision,
477
+ identityKey,
478
+ refresh,
479
+ seedFromCapture,
480
+ ]);
439
481
 
440
482
  // git.changed → re-fetch the LIVE diff. Agent tool completion is also an
441
483
  // invalidation point because shell/apply-patch writes can bypass Channel-A and
442
484
  // therefore emit no git.changed event. This is event-driven, never polling.
443
485
  const events = options.events;
444
486
  useEffect(() => {
445
- if (!enabled || !active || !events) {
487
+ if (!enabled || !events) {
488
+ if (commandRefreshTimerRef.current) {
489
+ clearTimeout(commandRefreshTimerRef.current);
490
+ commandRefreshTimerRef.current = null;
491
+ }
492
+ return;
493
+ }
494
+ // Historical events describe the capture we already loaded. Inactive tabs
495
+ // and holderless draining/cold leases advance the cursor without issuing
496
+ // provider I/O; becoming warm performs one authoritative reconcile above.
497
+ if (!active || !acceptsEventReads) {
498
+ for (const event of events) {
499
+ if (event.sequence > lastChangeRef.current) lastChangeRef.current = event.sequence;
500
+ }
446
501
  if (commandRefreshTimerRef.current) {
447
502
  clearTimeout(commandRefreshTimerRef.current);
448
503
  commandRefreshTimerRef.current = null;
@@ -467,15 +522,15 @@ export function useSandboxGit(
467
522
  if (immediate) {
468
523
  if (commandRefreshTimerRef.current) clearTimeout(commandRefreshTimerRef.current);
469
524
  commandRefreshTimerRef.current = null;
470
- void refresh();
525
+ if (acceptsEventReadsRef.current) void refresh();
471
526
  } else if (commandDelta && !commandRefreshTimerRef.current) {
472
527
  commandRefreshTimerRef.current = setTimeout(() => {
473
528
  commandRefreshTimerRef.current = null;
474
- void refresh();
529
+ if (acceptsEventReadsRef.current) void refresh();
475
530
  }, 1_000);
476
531
  }
477
532
  }
478
- }, [enabled, active, events, refresh]);
533
+ }, [enabled, active, acceptsEventReads, events, refresh]);
479
534
 
480
535
  useEffect(
481
536
  () => () => {
@@ -7,6 +7,7 @@ import type {
7
7
  import { OpenGeniApiError } from "@opengeni/sdk";
8
8
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
9
9
  import { useOpenGeni, type ClientOverride } from "../provider";
10
+ import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
10
11
 
11
12
  export type TerminalChunk = {
12
13
  /** Stable key (the source event id) so the xterm writer tracks a written-cursor. */
@@ -107,9 +108,9 @@ export function useSandboxTerminal(
107
108
  // projection is the single source of truth for what's written). The open is
108
109
  // gated on a WARM box: opening on a cold/warming lease races the box (the PTY
109
110
  // exec-session is created on a box that the next op may not resume), which is
110
- // exactly the "session not found" terminal failure; we wait for warm/draining.
111
+ // exactly the "session not found" terminal failure; we wait for warm.
111
112
  const openInFlight = useRef(false);
112
- const boxWarm = liveness === undefined || liveness === "warm" || liveness === "draining";
113
+ const boxWarm = liveness === undefined || sandboxAcceptsLiveIo(liveness);
113
114
  useEffect(() => {
114
115
  if (!interactive || !sessionId || ptyFilter || !boxWarm) {
115
116
  setOpenError(null);
@@ -7,10 +7,13 @@ import {
7
7
  } from "@opengeni/sdk";
8
8
  import { useCallback, useEffect, useRef, useState } from "react";
9
9
  import { useOpenGeni, type ClientOverride } from "../provider";
10
+ import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
11
+ import { terminalCanAcquirePty } from "../lib/terminal-capability";
10
12
  import { usePageLiveActivity } from "./internal";
11
13
 
12
- // "on-demand" is the boxless-benign resting state: the lease is cold and NOTHING
13
- // asked to warm a box (no viewer/consent action, no files-tab warm). Under lazy
14
+ // "on-demand" is the boxless-benign resting state: the lease is not currently
15
+ // holder-backed and NOTHING asked to warm a box (no viewer/consent action, no
16
+ // files-tab warm). Under lazy
14
17
  // provisioning a chat-only turn never creates a box, so this is the correct calm
15
18
  // terminal state — NOT an error. It is distinct from "cold", which now means "a
16
19
  // warm-up is genuinely in flight" (a viewer attach was requested) and therefore
@@ -84,7 +87,8 @@ export type UseSessionCapabilitiesResult = {
84
87
  error: Error | null;
85
88
  /**
86
89
  * 409 from the desktop attach: the un-redacted (or shared) plane needs explicit
87
- * acknowledgment before a viewer holder is granted. Drives the consent prompt.
90
+ * acknowledgment before a viewer holder is granted. The workbench uses this
91
+ * to complete acknowledgment automatically while Desktop is opening.
88
92
  */
89
93
  acknowledgmentRequired: "unredacted" | "shared" | null;
90
94
  /** 429 from the desktop attach: the per-session viewer cap is reached. */
@@ -112,24 +116,10 @@ function desktopAttachable(cell: SessionCapabilities["DesktopStream"]): boolean
112
116
 
113
117
  /**
114
118
  * Whether warming the box for the interactive terminal (pty-ws) is worth it.
115
- * The Terminal cell is ALWAYS feasible when the backend advertises a transport at
116
- * all (`sse-events` on a cold box, `pty-ws` once warm) only a genuinely
117
- * terminal-less backend reports transport:null with a hard reason. The attach is
118
- * what flips `sse-events` → `pty-ws` by warming the box and minting the ttyd
119
- * tunnel URL, so we attach whenever the terminal is not hard-unavailable.
119
+ * A real-PTY descriptor is attachable when already routed as `pty-ws`, or while
120
+ * transiently cold. A firehose-only backend and policy/OS/backend degradation
121
+ * cannot become interactive, so attaching there would only waste a holder.
120
122
  */
121
- function terminalAttachable(cell: SessionCapabilities["Terminal"]): boolean {
122
- if (cell.transport === null) {
123
- // No terminal at all unless the only blocker is a transient cold state.
124
- return (
125
- cell.reason === "lease_cold" || cell.reason === "not_provisioned" || cell.reason === null
126
- );
127
- }
128
- // Already pty-ws (warm) is fine to (re)attach; sse-events is the cold state the
129
- // attach upgrades. Either way it's attachable.
130
- return true;
131
- }
132
-
133
123
  /** Read `stream.url.rotated` payloads off the live event log, newest last. */
134
124
  function rotationsFrom(events: SessionEvent[]): StreamUrlRotatedPayload[] {
135
125
  const out: StreamUrlRotatedPayload[] = [];
@@ -149,8 +139,8 @@ function rotationsFrom(events: SessionEvent[]): StreamUrlRotatedPayload[] {
149
139
  *
150
140
  * Degradation is a value, never a crash: an unsupported surface comes back
151
141
  * `available:false`/`transport:null` + a `reason`; the components render the
152
- * reason-aware empty state. 409 (consent) and 429 (viewer cap) are surfaced as
153
- * typed signals, not thrown.
142
+ * reason-aware empty state. 409 (acknowledgment) and 429 (viewer cap) are
143
+ * surfaced as typed signals, not thrown.
154
144
  */
155
145
  export function useSessionCapabilities(
156
146
  sessionId: string | null | undefined,
@@ -207,8 +197,15 @@ export function useSessionCapabilities(
207
197
  if (!lifecycleEnabled || !sessionId) {
208
198
  setState("idle");
209
199
  setCapabilities(null);
200
+ viewerIdRef.current = null;
201
+ setViewerId(null);
210
202
  return;
211
203
  }
204
+ // The preceding effect cleanup released any old holder before this fresh
205
+ // lifecycle starts. Never project that stale id while re-negotiating or if
206
+ // the replacement grant degrades.
207
+ viewerIdRef.current = null;
208
+ setViewerId(null);
212
209
  let cancelled = false;
213
210
  let pollTimer: ReturnType<typeof setTimeout> | null = null;
214
211
  let heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
@@ -273,7 +270,7 @@ export function useSessionCapabilities(
273
270
  .then((caps) => {
274
271
  if (cancelled) return;
275
272
  settle(caps);
276
- if (caps.liveness === "warm" || caps.liveness === "draining") {
273
+ if (sandboxAcceptsLiveIo(caps.liveness)) {
277
274
  setState("ready");
278
275
  return;
279
276
  }
@@ -308,8 +305,8 @@ export function useSessionCapabilities(
308
305
  // attach serves BOTH live planes — the desktop pixel stream AND the
309
306
  // interactive terminal (ttyd pty-ws) ride the same warm box and the same
310
307
  // holder; the response folds the minted address for each requested plane.
311
- // The consent gate (409) and the viewer cap (429) surface as typed signals
312
- // rather than throwing — the tabs degrade gracefully.
308
+ // The acknowledgment requirement (409) and viewer cap (429) surface as
309
+ // typed signals rather than throwing — the tabs degrade gracefully.
313
310
  //
314
311
  // KEY: the handshake (getStreamCapabilities) NEVER spins up a cold box, so
315
312
  // on a cold/warming lease the desktop cell comes back transport:null and
@@ -321,7 +318,7 @@ export function useSessionCapabilities(
321
318
  // already live OR cold-but-feasible — and let POST /viewers warm the box and
322
319
  // mint the URLs. Only a genuinely-unsupported reason suppresses the attach.
323
320
  const wantDesktopAttach = attachDesktop && desktopAttachable(caps.DesktopStream);
324
- const wantTerminalAttach = attachTerminal && terminalAttachable(caps.Terminal);
321
+ const wantTerminalAttach = attachTerminal && terminalCanAcquirePty(caps.Terminal);
325
322
  // The Files attach warms the box for a file WRITE (wake-on-edit intent). It
326
323
  // folds no live URL (files are stateless HTTP) — the attach is purely a
327
324
  // liveness refcount. Unlike the previous "keep the box warm while the Files
@@ -339,6 +336,8 @@ export function useSessionCapabilities(
339
336
  // pty-ws terminal cell WITHOUT tripping the desktop consent 409.
340
337
  const holder = await client.attachViewer(workspaceId, sessionId, {
341
338
  desktop: wantDesktopAttach,
339
+ terminal: wantTerminalAttach,
340
+ files: wantFilesAttach,
342
341
  });
343
342
  if (cancelled) {
344
343
  // The attach crossed an identity/unmount boundary after the server
@@ -348,6 +347,18 @@ export function useSessionCapabilities(
348
347
  void client.detachViewer(workspaceId, sessionId, holder.viewerId).catch(() => {});
349
348
  return;
350
349
  }
350
+ if (
351
+ (wantDesktopAttach &&
352
+ (!holder.dataPlaneUrl || !holder.transport || !holder.client)) ||
353
+ (wantTerminalAttach && (!holder.terminalUrl || !holder.terminalTransport))
354
+ ) {
355
+ // Nullable plane cells are valid graceful-degradation values at
356
+ // the raw API boundary, but an explicit UI grant must not become
357
+ // a successful holder that waits forever. Release it and surface
358
+ // a retryable error instead of retaining stale credentials.
359
+ await client.detachViewer(workspaceId, sessionId, holder.viewerId).catch(() => {});
360
+ throw new Error("live plane unavailable");
361
+ }
351
362
  localViewerId = holder.viewerId;
352
363
  viewerIdRef.current = holder.viewerId;
353
364
  setViewerId(holder.viewerId);
@@ -367,24 +378,24 @@ export function useSessionCapabilities(
367
378
  ...prev.DesktopStream,
368
379
  transport: holder.transport ?? prev.DesktopStream.transport,
369
380
  client: holder.client ?? prev.DesktopStream.client,
370
- url: holder.dataPlaneUrl ?? prev.DesktopStream.url,
371
- token: holder.streamToken ?? prev.DesktopStream.token,
372
- expiresAt: holder.streamExpiresAt ?? prev.DesktopStream.expiresAt,
381
+ url: holder.dataPlaneUrl,
382
+ token: holder.streamToken,
383
+ expiresAt: holder.streamExpiresAt,
373
384
  resolution: holder.resolution ?? prev.DesktopStream.resolution,
374
385
  }
375
386
  : prev.DesktopStream,
376
- Terminal:
377
- holder.terminalUrl && holder.terminalTransport
378
- ? {
379
- ...prev.Terminal,
380
- transport: holder.terminalTransport,
381
- url: holder.terminalUrl,
382
- token: holder.terminalToken ?? prev.Terminal.token,
383
- // A live pty-ws means the box is pty-capable for real.
384
- ptyCapable: true,
385
- reason: null,
386
- }
387
- : prev.Terminal,
387
+ Terminal: wantTerminalAttach
388
+ ? {
389
+ ...prev.Terminal,
390
+ transport: holder.terminalTransport ?? prev.Terminal.transport,
391
+ url: holder.terminalUrl,
392
+ token: holder.terminalToken,
393
+ expiresAt: holder.terminalExpiresAt,
394
+ // A live pty-ws means the box is pty-capable for real.
395
+ ptyCapable: holder.terminalTransport ? true : prev.Terminal.ptyCapable,
396
+ reason: holder.terminalTransport ? null : prev.Terminal.reason,
397
+ }
398
+ : prev.Terminal,
388
399
  }
389
400
  : prev,
390
401
  );
@@ -393,8 +404,8 @@ export function useSessionCapabilities(
393
404
  if (cause instanceof OpenGeniApiError) {
394
405
  if (cause.status === 409) {
395
406
  // The un-redacted/shared consent gate is a DESKTOP requirement. A
396
- // terminal-only warm attach needs no consent, so a 409 there is not
397
- // a consent prompt — the terminal just stays on the read-only
407
+ // terminal-only warm attach needs no acknowledgment, so a 409 there
408
+ // is not a desktop-open signal — the terminal stays on the read-only
398
409
  // firehose. Only raise the consent requirement when the desktop was
399
410
  // the (or a) reason we attached.
400
411
  if (wantDesktopAttach) {
@@ -408,6 +419,8 @@ export function useSessionCapabilities(
408
419
  setState("error");
409
420
  setError(cause);
410
421
  return;
422
+ } else {
423
+ throw cause;
411
424
  }
412
425
  // 409/429 are recoverable: structured surfaces still negotiated;
413
426
  // keep going to set ready/cold below.
@@ -418,16 +431,16 @@ export function useSessionCapabilities(
418
431
  }
419
432
 
420
433
  // Whether ANYTHING asked to warm a box this negotiation. Only then is a
421
- // cold lease "warming in flight" — worth polling, and worth surfacing a
434
+ // non-warm lease "warming in flight" — worth polling, and worth surfacing a
422
435
  // stall as an error. With no warm-up requested a cold lease is the benign
423
436
  // boxless resting state (below), never an error.
424
437
  const warmUpRequested = wantDesktopAttach || wantTerminalAttach || wantFilesAttach;
425
- if (caps.liveness === "warm" || caps.liveness === "draining" || localViewerId) {
438
+ if (sandboxAcceptsLiveIo(caps.liveness) || localViewerId) {
426
439
  setState("ready");
427
440
  startHeartbeat(caps);
428
441
  } else if (warmUpRequested) {
429
442
  // A warm-up is genuinely in flight (a viewer attach was requested) but the
430
- // box isn't warm yet — poll until it warms, with the patience deadline so a
443
+ // box isn't holder-confirmed warm yet — poll until it is, with the deadline so a
431
444
  // real stall surfaces as an error.
432
445
  setState("cold");
433
446
  pollUntilWarm();
@@ -0,0 +1,226 @@
1
+ import type {
2
+ CreateSiteAuthConnectionRequest,
3
+ SiteAuthConnection,
4
+ SiteAuthConnectionMutationResponse,
5
+ UpdateSiteAuthConnectionRequest,
6
+ } from "@opengeni/sdk/interaction";
7
+ import { useCallback, useEffect, useRef, useState } from "react";
8
+ import type { EmbeddedBrowserInteractionClientLike } from "../client";
9
+ import {
10
+ type EmbeddedBrowserInteractionClientOverride,
11
+ useEmbeddedBrowserInteraction,
12
+ } from "../session-context";
13
+ import { useInteractionInvalidation } from "./use-interaction-invalidation";
14
+
15
+ export type UseSiteAuthConnectionsOptions = EmbeddedBrowserInteractionClientOverride & {
16
+ enabled?: boolean | undefined;
17
+ includeArchived?: boolean | undefined;
18
+ };
19
+
20
+ export type UseSiteAuthConnectionsResult = {
21
+ revision: number;
22
+ connections: SiteAuthConnection[];
23
+ loading: boolean;
24
+ refreshing: boolean;
25
+ mutating: boolean;
26
+ error: Error | null;
27
+ refresh: () => Promise<void>;
28
+ create: (
29
+ request: Omit<CreateSiteAuthConnectionRequest, "operationId"> & { operationId?: string },
30
+ ) => Promise<SiteAuthConnectionMutationResponse>;
31
+ update: (
32
+ connectionId: string,
33
+ request: Omit<UpdateSiteAuthConnectionRequest, "operationId"> & {
34
+ operationId?: string;
35
+ },
36
+ ) => Promise<SiteAuthConnectionMutationResponse>;
37
+ };
38
+
39
+ /** Secret-free workspace login recipes and their verified health. */
40
+ export function useSiteAuthConnections(
41
+ options: UseSiteAuthConnectionsOptions = {},
42
+ ): UseSiteAuthConnectionsResult {
43
+ const { client, workspaceId, workspaceInteractionEvent, workspaceInteractionConnectionState } =
44
+ useEmbeddedBrowserInteraction(options);
45
+ const enabled = options.enabled ?? true;
46
+ const includeArchived = options.includeArchived ?? false;
47
+ const key = `${workspaceId}:${includeArchived ? "all" : "active"}`;
48
+ const [state, setState] = useState(() => emptyState(key, enabled));
49
+ const [mutationCount, setMutationCount] = useState(0);
50
+ const requestRef = useRef<{ id: number; controller: AbortController | null }>({
51
+ id: 0,
52
+ controller: null,
53
+ });
54
+ const mountedRef = useRef(true);
55
+ const visible = state.key === key ? state : emptyState(key, enabled);
56
+
57
+ const cancelLoad = useCallback(() => {
58
+ requestRef.current.controller?.abort();
59
+ requestRef.current = { id: requestRef.current.id + 1, controller: null };
60
+ }, []);
61
+
62
+ const load = useCallback(
63
+ async (foreground: boolean): Promise<void> => {
64
+ if (!enabled) return;
65
+ const id = requestRef.current.id + 1;
66
+ requestRef.current.controller?.abort();
67
+ const controller = new AbortController();
68
+ requestRef.current = { id, controller };
69
+ setState((current) => ({
70
+ ...(current.key === key ? current : emptyState(key, true)),
71
+ loading: foreground,
72
+ refreshing: !foreground,
73
+ error: foreground || current.key !== key ? null : current.error,
74
+ }));
75
+ try {
76
+ const response = await client.listSiteAuthConnections(workspaceId, {
77
+ includeArchived,
78
+ signal: controller.signal,
79
+ });
80
+ if (!mountedRef.current || requestRef.current.id !== id) return;
81
+ setState({
82
+ key,
83
+ revision: response.revision,
84
+ connections: sortConnections(response.connections),
85
+ loading: false,
86
+ refreshing: false,
87
+ error: null,
88
+ });
89
+ } catch (cause) {
90
+ if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
91
+ return;
92
+ setState((current) => ({
93
+ ...(current.key === key ? current : emptyState(key, true)),
94
+ loading: false,
95
+ refreshing: false,
96
+ error: asError(cause),
97
+ }));
98
+ } finally {
99
+ if (requestRef.current.id === id) requestRef.current = { id, controller: null };
100
+ }
101
+ },
102
+ [client, enabled, includeArchived, key, workspaceId],
103
+ );
104
+
105
+ const refresh = useCallback(async () => await load(false), [load]);
106
+
107
+ useEffect(() => {
108
+ mountedRef.current = true;
109
+ if (!enabled) {
110
+ cancelLoad();
111
+ setState(emptyState(key, false));
112
+ return;
113
+ }
114
+ void load(true);
115
+ return () => {
116
+ mountedRef.current = false;
117
+ cancelLoad();
118
+ };
119
+ }, [cancelLoad, enabled, key, load]);
120
+
121
+ useInteractionInvalidation({
122
+ workspaceId,
123
+ key,
124
+ enabled,
125
+ event: workspaceInteractionEvent,
126
+ connectionState: workspaceInteractionConnectionState,
127
+ knownRevision: visible.revision,
128
+ refresh,
129
+ });
130
+
131
+ const mutate = useCallback(async <T>(operation: () => Promise<T>): Promise<T> => {
132
+ setMutationCount((count) => count + 1);
133
+ try {
134
+ return await operation();
135
+ } finally {
136
+ if (mountedRef.current) setMutationCount((count) => Math.max(0, count - 1));
137
+ }
138
+ }, []);
139
+
140
+ const merge = useCallback(
141
+ (connection: SiteAuthConnection) => {
142
+ setState((current) => {
143
+ if (current.key !== key) return current;
144
+ const connections = current.connections.filter(
145
+ (candidate) => candidate.id !== connection.id,
146
+ );
147
+ if (includeArchived || connection.status !== "archived") connections.push(connection);
148
+ return { ...current, connections: sortConnections(connections), error: null };
149
+ });
150
+ },
151
+ [includeArchived, key],
152
+ );
153
+
154
+ const create = useCallback(
155
+ async (
156
+ request: Omit<CreateSiteAuthConnectionRequest, "operationId"> & {
157
+ operationId?: string;
158
+ },
159
+ ): Promise<SiteAuthConnectionMutationResponse> =>
160
+ await mutate(async () => {
161
+ const response = await client.createSiteAuthConnection(workspaceId, {
162
+ ...request,
163
+ operationId: request.operationId ?? crypto.randomUUID(),
164
+ });
165
+ merge(response.connection);
166
+ return response;
167
+ }),
168
+ [client, merge, mutate, workspaceId],
169
+ );
170
+
171
+ const update = useCallback(
172
+ async (
173
+ connectionId: string,
174
+ request: Omit<UpdateSiteAuthConnectionRequest, "operationId"> & {
175
+ operationId?: string;
176
+ },
177
+ ): Promise<SiteAuthConnectionMutationResponse> =>
178
+ await mutate(async () => {
179
+ const response = await client.updateSiteAuthConnection(workspaceId, connectionId, {
180
+ ...request,
181
+ operationId: request.operationId ?? crypto.randomUUID(),
182
+ });
183
+ merge(response.connection);
184
+ return response;
185
+ }),
186
+ [client, merge, mutate, workspaceId],
187
+ );
188
+
189
+ return {
190
+ revision: visible.revision,
191
+ connections: visible.connections,
192
+ loading: visible.loading,
193
+ refreshing: visible.refreshing,
194
+ mutating: mutationCount > 0,
195
+ error: visible.error,
196
+ refresh,
197
+ create,
198
+ update,
199
+ };
200
+ }
201
+
202
+ function emptyState(key: string, loading: boolean) {
203
+ return {
204
+ key,
205
+ revision: 0,
206
+ connections: [] as SiteAuthConnection[],
207
+ loading,
208
+ refreshing: false,
209
+ error: null as Error | null,
210
+ };
211
+ }
212
+
213
+ function sortConnections(connections: readonly SiteAuthConnection[]): SiteAuthConnection[] {
214
+ return [...connections].sort(
215
+ (left, right) =>
216
+ Number(left.status === "archived") - Number(right.status === "archived") ||
217
+ left.name.localeCompare(right.name) ||
218
+ left.accountLabel.localeCompare(right.accountLabel),
219
+ );
220
+ }
221
+
222
+ function asError(cause: unknown): Error {
223
+ return cause instanceof Error ? cause : new Error(String(cause));
224
+ }
225
+
226
+ export type SiteAuthConnectionsClient = EmbeddedBrowserInteractionClientLike;