@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
@@ -0,0 +1,81 @@
1
+ import type { SuperGrokAccount, SuperGrokAccountsResponse } from "@opengeni/sdk";
2
+ import { useCallback } from "react";
3
+
4
+ import { useOpenGeni, type ClientOverride } from "../provider";
5
+ import { useMutationRunner, usePolledValue } from "./internal";
6
+
7
+ export type SuperGrokAccountsClientLike = {
8
+ listSuperGrokAccounts: (workspaceId: string) => Promise<SuperGrokAccountsResponse>;
9
+ activateSuperGrokAccount?: (
10
+ workspaceId: string,
11
+ accountId: string,
12
+ ) => Promise<{ activated: boolean; accountId: string }>;
13
+ };
14
+
15
+ export type UseSuperGrokAccountsOptions = ClientOverride & {
16
+ enabled?: boolean | undefined;
17
+ pollIntervalMs?: number | undefined;
18
+ supergrokClient?: SuperGrokAccountsClientLike | undefined;
19
+ };
20
+
21
+ export type UseSuperGrokAccountsResult = {
22
+ accounts: SuperGrokAccount[];
23
+ activeAccountId: string | null;
24
+ settings: SuperGrokAccountsResponse["settings"];
25
+ loading: boolean;
26
+ refresh: () => Promise<void>;
27
+ activate: (accountId: string) => Promise<boolean>;
28
+ activating: boolean;
29
+ mutationError: Error | null;
30
+ };
31
+
32
+ const EMPTY: SuperGrokAccountsResponse = {
33
+ accounts: [],
34
+ activeAccountId: null,
35
+ settings: {
36
+ rotationEnabled: false,
37
+ rotationStrategy: "sharded",
38
+ activeCredentialId: null,
39
+ },
40
+ };
41
+
42
+ /** Workspace-visible xAI accounts. Workspace scope is the connection default. */
43
+ export function useSuperGrokAccounts(
44
+ options: UseSuperGrokAccountsOptions = {},
45
+ ): UseSuperGrokAccountsResult {
46
+ const { client, workspaceId } = useOpenGeni(options);
47
+ const supergrokClient =
48
+ options.supergrokClient ?? (client as unknown as SuperGrokAccountsClientLike);
49
+ const load = useCallback(
50
+ async () => await supergrokClient.listSuperGrokAccounts(workspaceId),
51
+ [supergrokClient, workspaceId],
52
+ );
53
+ const { data, loading, refresh } = usePolledValue(load, {
54
+ enabled: options.enabled,
55
+ pollIntervalMs: options.pollIntervalMs,
56
+ });
57
+ const { run, mutating: activating, mutationError } = useMutationRunner();
58
+ const activate = useCallback(
59
+ async (accountId: string): Promise<boolean> => {
60
+ if (!supergrokClient.activateSuperGrokAccount) return false;
61
+ const result = await run(async () => {
62
+ await supergrokClient.activateSuperGrokAccount!(workspaceId, accountId);
63
+ return true;
64
+ });
65
+ if (result) await refresh();
66
+ return result === true;
67
+ },
68
+ [refresh, run, supergrokClient, workspaceId],
69
+ );
70
+ const state = data ?? EMPTY;
71
+ return {
72
+ accounts: state.accounts,
73
+ activeAccountId: state.activeAccountId,
74
+ settings: state.settings,
75
+ loading,
76
+ refresh,
77
+ activate,
78
+ activating,
79
+ mutationError,
80
+ };
81
+ }
@@ -7,7 +7,16 @@ import {
7
7
  ttydResizeFrame,
8
8
  TtydServerCommand,
9
9
  } from "@opengeni/sdk";
10
- import { useEffect, useMemo, useRef, useState } from "react";
10
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
11
+ import {
12
+ decodeStreamFrame,
13
+ decodeStreamOpenAck,
14
+ encodeStreamFrame,
15
+ encodeStreamOpen,
16
+ type RelayChannel,
17
+ STREAM_KIND_PTY,
18
+ STREAM_ROLE_CLIENT,
19
+ } from "../lib/relay-wire";
11
20
 
12
21
  /** The ttyd connection lifecycle as surfaced to the component. */
13
22
  export type TerminalStreamStatus = "connecting" | "open" | "closed" | "error";
@@ -17,11 +26,14 @@ export type UseTerminalStreamOptions = {
17
26
  * The stream connects ONLY when `transport === "pty-ws"` and `url` is set; on a
18
27
  * cold box (`transport === "sse-events"` / no url) it stays idle and the caller
19
28
  * falls back to the Channel-A read-only firehose. */
20
- capability: Pick<TerminalCapability, "transport" | "url" | "token"> | null;
29
+ capability: Pick<TerminalCapability, "transport" | "url" | "token" | "expiresAt"> | null;
21
30
  /** Called for each OUTPUT payload from ttyd (write verbatim into xterm). */
22
31
  onOutput?: ((data: string) => void) | undefined;
23
32
  /** Called when ttyd sends a SET_WINDOW_TITLE frame. */
24
33
  onTitle?: ((title: string) => void) | undefined;
34
+ /** Requests a fresh negotiated terminal grant after an unexpected transport
35
+ * failure. Intentional disconnects and effect cleanup never call it. */
36
+ onReconnectNeeded?: (() => void) | undefined;
25
37
  /** Initial PTY size to seed the ttyd auth frame + first resize. */
26
38
  initialCols?: number | undefined;
27
39
  initialRows?: number | undefined;
@@ -31,7 +43,8 @@ export type UseTerminalStreamResult = {
31
43
  /** True once the ttyd socket is open (and the auth frame has been sent). */
32
44
  connected: boolean;
33
45
  status: TerminalStreamStatus;
34
- /** Pipe a keystroke/paste to the PTY stdin. No-op until the socket is open. */
46
+ /** Pipe a keystroke/paste to PTY stdin. Input during CONNECTING is bounded and
47
+ * replayed only after ttyd authentication succeeds. */
35
48
  write: (data: string) => void;
36
49
  /** Tell ttyd the PTY window changed size (on xterm fit/resize). */
37
50
  resize: (cols: number, rows: number) => void;
@@ -39,6 +52,84 @@ export type UseTerminalStreamResult = {
39
52
  disconnect: () => void;
40
53
  };
41
54
 
55
+ // Keystrokes entered during the websocket handshake are held until ttyd has
56
+ // accepted its auth/resize frames. Bound the queue by UTF-16 code units (a hard
57
+ // <=128 KiB string payload in current JS engines) so a paste cannot turn a slow
58
+ // handshake into unbounded renderer memory. Overflow rejects the whole pending
59
+ // input rather than ever executing a truncated shell command.
60
+ export const MAX_PENDING_TERMINAL_INPUT_CODE_UNITS = 64 * 1024;
61
+ const TERMINAL_CONNECT_EXPIRY_SKEW_MS = 5_000;
62
+ const TERMINAL_RECONNECT_INITIAL_MS = 100;
63
+ const TERMINAL_RECONNECT_MAX_MS = 3_000;
64
+ const RELAY_TAG_OPEN = 1;
65
+ const RELAY_TAG_OPEN_ACK = 2;
66
+ const RELAY_TAG_FRAME = 3;
67
+
68
+ function relayDatagram(tag: number, body: Uint8Array): ArrayBuffer {
69
+ const bytes = new Uint8Array(body.length + 1);
70
+ bytes[0] = tag;
71
+ bytes.set(body, 1);
72
+ return bytes.buffer;
73
+ }
74
+
75
+ function relayChannelFromUrl(url: string): RelayChannel {
76
+ const endpoint = new URL(url);
77
+ const channel: RelayChannel = {
78
+ channelId: endpoint.searchParams.get("channel") ?? "",
79
+ workspaceId: endpoint.searchParams.get("ws") ?? "",
80
+ agentId: endpoint.searchParams.get("agent") ?? "",
81
+ kind: STREAM_KIND_PTY,
82
+ port: Number(endpoint.searchParams.get("port") ?? "0"),
83
+ };
84
+ if (
85
+ !channel.channelId ||
86
+ !channel.workspaceId ||
87
+ !channel.agentId ||
88
+ !Number.isSafeInteger(channel.port) ||
89
+ channel.port <= 0
90
+ ) {
91
+ throw new Error("terminal relay endpoint is incomplete");
92
+ }
93
+ return channel;
94
+ }
95
+
96
+ function sendRelayInput(
97
+ socket: WebSocket,
98
+ channel: RelayChannel,
99
+ sequence: bigint,
100
+ data: string,
101
+ ): void {
102
+ socket.send(
103
+ relayDatagram(
104
+ RELAY_TAG_FRAME,
105
+ encodeStreamFrame({
106
+ channelId: channel.channelId,
107
+ seq: sequence.toString(),
108
+ data: new TextEncoder().encode(data),
109
+ producedAtMs: String(Date.now()),
110
+ }),
111
+ ),
112
+ );
113
+ }
114
+
115
+ /** A bearer must remain valid long enough to finish a new websocket handshake.
116
+ * Existing open sockets are unaffected: this is evaluated only when the
117
+ * credential identity changes and the connection effect attempts a new socket. */
118
+ export function terminalStreamCredentialUsable(
119
+ capability: Pick<TerminalCapability, "transport" | "url" | "expiresAt"> | null | undefined,
120
+ nowMs = Date.now(),
121
+ ): boolean {
122
+ if (
123
+ (capability?.transport !== "pty-ws" && capability?.transport !== "relay-pty") ||
124
+ !capability.url
125
+ ) {
126
+ return false;
127
+ }
128
+ if (!capability.expiresAt) return true;
129
+ const expiresAt = Date.parse(capability.expiresAt);
130
+ return Number.isFinite(expiresAt) && expiresAt > nowMs + TERMINAL_CONNECT_EXPIRY_SKEW_MS;
131
+ }
132
+
42
133
  /** Decode an inbound ttyd frame's payload (everything after the 1-char command).
43
134
  * ttyd may send either a text frame (string) or a binary frame (ArrayBuffer);
44
135
  * for binary we slice off the first byte (the command) and utf-8 decode the rest. */
@@ -52,6 +143,14 @@ function decodeFrame(data: string | ArrayBuffer): { command: string; payload: st
52
143
  return { command, payload };
53
144
  }
54
145
 
146
+ function closeSocket(socket: WebSocket | null): void {
147
+ try {
148
+ socket?.close();
149
+ } catch {
150
+ // Already closed or rejected by the host WebSocket implementation.
151
+ }
152
+ }
153
+
55
154
  /**
56
155
  * Drive a ttyd PTY-over-websocket connection from a `pty-ws` Terminal capability,
57
156
  * symmetric with `use-desktop-stream` (the noVNC-over-tunnel hook). The scoped
@@ -70,9 +169,21 @@ function decodeFrame(data: string | ArrayBuffer): { command: string; payload: st
70
169
  * SSR-safe: the socket open lives in `useEffect`, so a server render is a no-op.
71
170
  */
72
171
  export function useTerminalStream(options: UseTerminalStreamOptions): UseTerminalStreamResult {
73
- const { capability, onOutput, onTitle, initialCols, initialRows } = options;
172
+ const { capability, onOutput, onTitle, onReconnectNeeded, initialCols, initialRows } = options;
74
173
  const [status, setStatus] = useState<TerminalStreamStatus>("closed");
174
+ const [reconnectGeneration, setReconnectGeneration] = useState(0);
75
175
  const wsRef = useRef<WebSocket | null>(null);
176
+ const pendingInputRef = useRef("");
177
+ const socketFailedRef = useRef(false);
178
+ const socketAuthenticatedRef = useRef(false);
179
+ const relayChannelRef = useRef<RelayChannel | null>(null);
180
+ const relayIdentityRef = useRef<string | null>(null);
181
+ const relayInputSequenceRef = useRef(0n);
182
+ const relayOutputSequenceRef = useRef(0n);
183
+ const reconnectAttemptRef = useRef(0);
184
+ const credentialIdentityRef = useRef<string | null>(null);
185
+ const refreshRequestedRef = useRef(false);
186
+ const intentionalCloseRef = useRef<WeakSet<WebSocket>>(new WeakSet());
76
187
  // Latest size, so a resize() before the socket opens is replayed on open, and a
77
188
  // reconnect seeds the right geometry.
78
189
  const sizeRef = useRef<{ cols: number; rows: number }>({
@@ -83,52 +194,219 @@ export function useTerminalStream(options: UseTerminalStreamOptions): UseTermina
83
194
  // render (the parent passes fresh closures each time).
84
195
  const onOutputRef = useRef(onOutput);
85
196
  const onTitleRef = useRef(onTitle);
197
+ const onReconnectNeededRef = useRef(onReconnectNeeded);
86
198
  onOutputRef.current = onOutput;
87
199
  onTitleRef.current = onTitle;
200
+ onReconnectNeededRef.current = onReconnectNeeded;
88
201
 
89
202
  const transport = capability?.transport ?? null;
90
203
  const url = capability?.url ?? null;
91
204
  const token = capability?.token ?? null;
205
+ const expiresAt = capability?.expiresAt ?? null;
206
+ const transportRef = useRef(transport);
207
+ transportRef.current = transport;
92
208
 
93
209
  useEffect(() => {
94
210
  // SSR / no WebSocket / not a live pty-ws cell: stay closed; the caller falls
95
211
  // back to the Channel-A read-only firehose.
96
212
  if (typeof window === "undefined" || typeof WebSocket === "undefined") return;
97
- if (transport !== "pty-ws" || !url) {
213
+ const relay = transport === "relay-pty";
214
+ if (transport !== "pty-ws" && !relay) {
215
+ // A transport downgrade is a real semantic boundary. Do not replay input
216
+ // captured for a PTY if this surface later changes back from firehose mode.
217
+ pendingInputRef.current = "";
218
+ socketAuthenticatedRef.current = false;
219
+ relayIdentityRef.current = null;
220
+ relayInputSequenceRef.current = 0n;
221
+ relayOutputSequenceRef.current = 0n;
222
+ reconnectAttemptRef.current = 0;
223
+ credentialIdentityRef.current = null;
224
+ refreshRequestedRef.current = false;
225
+ setStatus("closed");
226
+ return;
227
+ }
228
+ const credentialIdentity = `${transport}\u0000${url ?? ""}\u0000${token ?? ""}\u0000${expiresAt ?? ""}`;
229
+ if (credentialIdentityRef.current !== credentialIdentity) {
230
+ credentialIdentityRef.current = credentialIdentity;
231
+ reconnectAttemptRef.current = 0;
232
+ refreshRequestedRef.current = false;
233
+ }
234
+ // A changed/cleared credential is a new connection attempt boundary. It may
235
+ // receive input while its exact grant is pending even if the prior socket
236
+ // failed; the unchanged failed identity remains fenced because this effect
237
+ // does not re-run for a status-only render.
238
+ socketFailedRef.current = false;
239
+ socketAuthenticatedRef.current = false;
240
+ if (!url) {
241
+ // Exact grants arrive asynchronously. Preserve the bounded first input
242
+ // while the descriptor remains pty-ws, but never open without a credential.
243
+ setStatus("closed");
244
+ return;
245
+ }
246
+ if (!terminalStreamCredentialUsable({ transport, url, expiresAt })) {
247
+ // A descriptor from an older rolling server may still carry a bearer that
248
+ // aged out before this surface mounted. Ask once for the exact fresh grant.
98
249
  setStatus("closed");
250
+ if (!refreshRequestedRef.current) {
251
+ refreshRequestedRef.current = true;
252
+ onReconnectNeededRef.current?.();
253
+ }
99
254
  return;
100
255
  }
101
256
 
102
257
  let disposed = false;
258
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
103
259
  let socket: WebSocket;
260
+ let relayChannel: RelayChannel | null = null;
261
+ const relayOutputDecoder = new TextDecoder();
104
262
  setStatus("connecting");
105
263
  try {
106
- // The ttyd "tty" subprotocol is REQUIRED — ttyd rejects a handshake without
107
- // it. The scoped token is already in the tunnel `url`.
108
- socket = new WebSocket(terminalSocketUrl({ url }), TTYD_SUBPROTOCOL);
264
+ if (relay) {
265
+ relayChannel = relayChannelFromUrl(url);
266
+ const relayIdentity = [
267
+ relayChannel.workspaceId,
268
+ relayChannel.agentId,
269
+ relayChannel.port,
270
+ relayChannel.channelId,
271
+ ].join("\u0000");
272
+ if (relayIdentityRef.current !== relayIdentity) {
273
+ relayIdentityRef.current = relayIdentity;
274
+ relayInputSequenceRef.current = 0n;
275
+ relayOutputSequenceRef.current = 0n;
276
+ }
277
+ relayChannelRef.current = relayChannel;
278
+ socket = new WebSocket(url);
279
+ } else {
280
+ // The ttyd "tty" subprotocol is REQUIRED — ttyd rejects a handshake without
281
+ // it. The scoped token is already in the tunnel `url`.
282
+ socket = new WebSocket(terminalSocketUrl({ url }), TTYD_SUBPROTOCOL);
283
+ }
109
284
  } catch {
285
+ socketFailedRef.current = true;
110
286
  setStatus("error");
111
287
  return;
112
288
  }
113
289
  socket.binaryType = "arraybuffer";
114
290
  wsRef.current = socket;
115
291
 
292
+ const requestFreshGrant = () => {
293
+ if (disposed || refreshRequestedRef.current || intentionalCloseRef.current.has(socket))
294
+ return;
295
+ refreshRequestedRef.current = true;
296
+ onReconnectNeededRef.current?.();
297
+ };
298
+
299
+ const scheduleTransportReconnect = () => {
300
+ if (disposed || reconnectTimer !== null || intentionalCloseRef.current.has(socket)) return;
301
+ if (refreshRequestedRef.current) return;
302
+ if (!terminalStreamCredentialUsable({ transport, url, expiresAt })) {
303
+ requestFreshGrant();
304
+ return;
305
+ }
306
+ const attempt = reconnectAttemptRef.current++;
307
+ const delay = Math.min(
308
+ TERMINAL_RECONNECT_INITIAL_MS * 2 ** Math.min(attempt, 8),
309
+ TERMINAL_RECONNECT_MAX_MS,
310
+ );
311
+ setStatus("connecting");
312
+ reconnectTimer = setTimeout(() => {
313
+ reconnectTimer = null;
314
+ if (!disposed) setReconnectGeneration((generation) => generation + 1);
315
+ }, delay);
316
+ };
317
+
116
318
  socket.onopen = () => {
117
319
  if (disposed) return;
320
+ if (relay && relayChannel) {
321
+ try {
322
+ socket.send(
323
+ relayDatagram(
324
+ RELAY_TAG_OPEN,
325
+ encodeStreamOpen({
326
+ channel: relayChannel,
327
+ token: token ?? "",
328
+ role: STREAM_ROLE_CLIENT,
329
+ resumeFromSeq: relayOutputSequenceRef.current.toString(),
330
+ }),
331
+ ),
332
+ );
333
+ } catch {
334
+ socketFailedRef.current = false;
335
+ setStatus("connecting");
336
+ closeSocket(socket);
337
+ }
338
+ return;
339
+ }
118
340
  // ttyd's required first frame: the auth message (empty token — the gate is
119
341
  // the tunnel url + scoped stream token, not a ttyd -c credential), seeded
120
342
  // with the current PTY geometry. Then an explicit resize to be safe.
121
343
  try {
122
344
  socket.send(ttydAuthFrame({ columns: sizeRef.current.cols, rows: sizeRef.current.rows }));
123
345
  socket.send(ttydResizeFrame(sizeRef.current.cols, sizeRef.current.rows));
346
+ if (pendingInputRef.current.length > 0) {
347
+ // One websocket frame makes the pre-open queue all-or-nothing from this
348
+ // client's perspective; never execute a prefix of a buffered command.
349
+ socket.send(ttydInputFrame(pendingInputRef.current));
350
+ pendingInputRef.current = "";
351
+ }
352
+ socketAuthenticatedRef.current = true;
124
353
  } catch {
125
- // a closed socket between open and send — onclose handles state.
354
+ socketFailedRef.current = false;
355
+ setStatus("connecting");
356
+ closeSocket(socket);
357
+ return;
126
358
  }
127
359
  setStatus("open");
360
+ reconnectAttemptRef.current = 0;
361
+ refreshRequestedRef.current = false;
128
362
  };
129
363
 
130
364
  socket.onmessage = (ev: MessageEvent) => {
131
365
  if (disposed) return;
366
+ if (relay) {
367
+ if (!(ev.data instanceof ArrayBuffer) || !relayChannel) return;
368
+ const bytes = new Uint8Array(ev.data);
369
+ if (bytes.length < 1) return;
370
+ const tag = bytes[0];
371
+ const body = bytes.subarray(1);
372
+ if (tag === RELAY_TAG_OPEN_ACK) {
373
+ try {
374
+ const ack = decodeStreamOpenAck(body);
375
+ if (!ack.accepted) throw new Error(ack.error?.message ?? "terminal relay rejected");
376
+ reconnectAttemptRef.current = 0;
377
+ refreshRequestedRef.current = false;
378
+ socketAuthenticatedRef.current = true;
379
+ if (pendingInputRef.current.length > 0) {
380
+ sendRelayInput(
381
+ socket,
382
+ relayChannel,
383
+ relayInputSequenceRef.current++,
384
+ pendingInputRef.current,
385
+ );
386
+ pendingInputRef.current = "";
387
+ }
388
+ setStatus("open");
389
+ } catch {
390
+ socketFailedRef.current = true;
391
+ setStatus("error");
392
+ requestFreshGrant();
393
+ closeSocket(socket);
394
+ }
395
+ } else if (tag === RELAY_TAG_FRAME && socketAuthenticatedRef.current) {
396
+ try {
397
+ const frame = decodeStreamFrame(body);
398
+ const sequence = BigInt(frame.seq);
399
+ if (sequence < relayOutputSequenceRef.current) return;
400
+ relayOutputSequenceRef.current = sequence + 1n;
401
+ if (frame.data.length > 0) {
402
+ onOutputRef.current?.(relayOutputDecoder.decode(frame.data, { stream: true }));
403
+ }
404
+ } catch {
405
+ // Ignore one malformed frame; the live PTY remains usable.
406
+ }
407
+ }
408
+ return;
409
+ }
132
410
  const { command, payload } = decodeFrame(ev.data as string | ArrayBuffer);
133
411
  switch (command) {
134
412
  case TtydServerCommand.OUTPUT:
@@ -144,64 +422,98 @@ export function useTerminalStream(options: UseTerminalStreamOptions): UseTermina
144
422
  };
145
423
 
146
424
  socket.onerror = () => {
147
- if (!disposed) setStatus("error");
425
+ if (!disposed) {
426
+ // A network/relay outage is retryable with the same credential and PTY.
427
+ // Keep accepting bounded input while the transport comes back.
428
+ socketFailedRef.current = false;
429
+ setStatus("connecting");
430
+ closeSocket(socket);
431
+ }
148
432
  };
149
433
  socket.onclose = () => {
150
- if (!disposed) setStatus("closed");
434
+ if (wsRef.current === socket) wsRef.current = null;
435
+ socketAuthenticatedRef.current = false;
436
+ if (!disposed) {
437
+ if (socketFailedRef.current) setStatus("error");
438
+ else scheduleTransportReconnect();
439
+ }
151
440
  };
152
441
 
153
442
  return () => {
154
443
  disposed = true;
155
444
  wsRef.current = null;
445
+ socketAuthenticatedRef.current = false;
446
+ relayChannelRef.current = null;
447
+ if (reconnectTimer !== null) clearTimeout(reconnectTimer);
156
448
  // Drop handlers so an in-flight close/error doesn't mutate state post-unmount.
157
449
  socket.onopen = null;
158
450
  socket.onmessage = null;
159
451
  socket.onerror = null;
160
452
  socket.onclose = null;
161
- try {
162
- socket.close();
163
- } catch {
164
- // ignore teardown errors
165
- }
453
+ closeSocket(socket);
166
454
  };
167
455
  // A url/token change (rotation) re-runs this effect → close old, open new.
168
456
  // eslint-disable-next-line react-hooks/exhaustive-deps
169
- }, [transport, url, token]);
457
+ }, [transport, url, token, expiresAt, reconnectGeneration]);
170
458
 
171
- return useMemo<UseTerminalStreamResult>(() => {
172
- const write = (data: string) => {
173
- const ws = wsRef.current;
174
- if (ws && ws.readyState === WebSocket.OPEN) {
175
- try {
459
+ const write = useCallback((data: string) => {
460
+ const currentTransport = transportRef.current;
461
+ const ws = wsRef.current;
462
+ if (ws && ws.readyState === WebSocket.OPEN && socketAuthenticatedRef.current) {
463
+ try {
464
+ const relayChannel = relayChannelRef.current;
465
+ if (currentTransport === "relay-pty" && relayChannel) {
466
+ sendRelayInput(ws, relayChannel, relayInputSequenceRef.current++, data);
467
+ } else {
176
468
  ws.send(ttydInputFrame(data));
177
- } catch {
178
- // socket raced closed — the reconnect effect will re-establish.
179
469
  }
470
+ } catch {
471
+ socketFailedRef.current = false;
472
+ setStatus("connecting");
473
+ closeSocket(ws);
180
474
  }
181
- };
182
- const resize = (cols: number, rows: number) => {
183
- if (cols <= 0 || rows <= 0) return;
184
- sizeRef.current = { cols, rows };
185
- const ws = wsRef.current;
186
- if (ws && ws.readyState === WebSocket.OPEN) {
187
- try {
188
- ws.send(ttydResizeFrame(cols, rows));
189
- } catch {
190
- // socket raced closed — geometry is replayed on the next open.
191
- }
475
+ } else if (
476
+ (currentTransport === "pty-ws" || currentTransport === "relay-pty") &&
477
+ !socketFailedRef.current
478
+ ) {
479
+ const nextSize = pendingInputRef.current.length + data.length;
480
+ if (nextSize > MAX_PENDING_TERMINAL_INPUT_CODE_UNITS) {
481
+ pendingInputRef.current = "";
482
+ socketFailedRef.current = true;
483
+ setStatus("error");
484
+ closeSocket(ws);
485
+ return;
192
486
  }
193
- };
194
- const disconnect = () => {
195
- const ws = wsRef.current;
196
- wsRef.current = null;
197
- if (ws) {
198
- try {
199
- ws.close();
200
- } catch {
201
- // ignore
202
- }
487
+ pendingInputRef.current += data;
488
+ }
489
+ }, []);
490
+ const resize = useCallback((cols: number, rows: number) => {
491
+ if (cols <= 0 || rows <= 0) return;
492
+ sizeRef.current = { cols, rows };
493
+ const ws = wsRef.current;
494
+ if (ws && ws.readyState === WebSocket.OPEN && socketAuthenticatedRef.current) {
495
+ try {
496
+ // Self-hosted relay PTYs carry raw input/output frames. Resize remains
497
+ // an out-of-band control operation; do not send ttyd framing into them.
498
+ if (transportRef.current === "pty-ws") ws.send(ttydResizeFrame(cols, rows));
499
+ } catch {
500
+ // socket raced closed — geometry is replayed on the next open.
203
501
  }
204
- };
502
+ }
503
+ }, []);
504
+ const disconnect = useCallback(() => {
505
+ const ws = wsRef.current;
506
+ wsRef.current = null;
507
+ pendingInputRef.current = "";
508
+ socketFailedRef.current = false;
509
+ socketAuthenticatedRef.current = false;
510
+ relayChannelRef.current = null;
511
+ setStatus("closed");
512
+ if (ws) intentionalCloseRef.current.add(ws);
513
+ closeSocket(ws);
514
+ }, []);
515
+
516
+ return useMemo<UseTerminalStreamResult>(() => {
205
517
  return { connected: status === "open", status, write, resize, disconnect };
206
- }, [status]);
518
+ }, [status, write, resize, disconnect]);
207
519
  }
@@ -0,0 +1,28 @@
1
+ import { useMemo } from "react";
2
+ import type { VideoArtifactPlaybackLoader } from "../timeline";
3
+ import { useOpenGeni, type ClientOverride } from "../session-context";
4
+
5
+ /**
6
+ * Bind the current public SDK client/workspace to MessageTimeline's renewable
7
+ * video-source boundary. Proxy hosts may omit the method until they support
8
+ * retained video; the failure remains local to the player instead of loading
9
+ * any video bytes through React.
10
+ */
11
+ export function useVideoArtifactPlaybackLoader(
12
+ options: ClientOverride = {},
13
+ ): VideoArtifactPlaybackLoader {
14
+ const { client, workspaceId } = useOpenGeni(options);
15
+ return useMemo(
16
+ () => async (artifactId: string, signal?: AbortSignal) => {
17
+ if (typeof client.createVideoArtifactPlaybackSource !== "function") {
18
+ throw new Error("This OpenGeni host does not expose retained-video playback.");
19
+ }
20
+ return await client.createVideoArtifactPlaybackSource(
21
+ workspaceId,
22
+ artifactId,
23
+ signal ? { signal } : {},
24
+ );
25
+ },
26
+ [client, workspaceId],
27
+ );
28
+ }
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { useOpenGeni, type ClientOverride } from "../provider";
3
+ import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
3
4
 
4
5
  /**
5
6
  * The wake-on-edit state machine. This hook owns the logic and the UI renders
@@ -73,7 +74,7 @@ export type UseWorkspaceEditResult = {
73
74
  };
74
75
 
75
76
  function isLive(liveness: string | undefined): boolean {
76
- return liveness === "warm" || liveness === "draining";
77
+ return sandboxAcceptsLiveIo(liveness);
77
78
  }
78
79
 
79
80
  /**