@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
@@ -4,7 +4,8 @@
4
4
  // Changes — review-first git: turn-end capture (cold) or live diff (warm)
5
5
  // Files — tree + inline editor (capture-backed cold, live warm)
6
6
  // Terminal — interactive xterm wired to the box PTY (Channel-A projection)
7
- // DesktopnoVNC (Channel-B), watch by default + a server-gated take-control
7
+ // Browserworkspace-wide BrowserSessions, tabs, live frames, and input
8
+ // Computer — workspace ComputerSessions: native targets, frames, and input
8
9
  // plus a machine-state chip in the dock header (the one truthful live/waking/
9
10
  // offline indicator) and any host-injected extra tabs (Run/Debug in apps/web).
10
11
  //
@@ -13,13 +14,24 @@
13
14
  // (no `sonner` import), and every surface renders with package primitives + og
14
15
  // tokens only. `apps/web` consumes this through the exact public surface an
15
16
  // external embedder uses — that is criterion F1.
16
- import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
17
+ import {
18
+ lazy,
19
+ type ReactNode,
20
+ Suspense,
21
+ useCallback,
22
+ useEffect,
23
+ useMemo,
24
+ useRef,
25
+ useState,
26
+ } from "react";
17
27
  import type { SessionEvent } from "@opengeni/sdk";
28
+ import type { InteractionPlacement } from "@opengeni/sdk/interaction";
18
29
  import {
19
30
  CircleCheckIcon,
20
31
  CpuIcon,
21
32
  FileCode2Icon,
22
33
  GitCompareArrowsIcon,
34
+ Globe2Icon,
23
35
  LoaderCircleIcon,
24
36
  MonitorIcon,
25
37
  RefreshCwIcon,
@@ -30,6 +42,9 @@ import {
30
42
  import { type ClientOverride, useOpenGeni } from "../provider";
31
43
  import { cn } from "../lib/cn";
32
44
  import { xtermThemeFromTokens } from "../lib/xterm-theme";
45
+ import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
46
+ import type { BrowserFrameWebSocketFactory } from "../hooks/use-browser-frame-stream";
47
+ import type { ComputerFrameWebSocketFactory } from "../hooks/use-computer-frame-stream";
33
48
  import { useSessionCapabilities } from "../hooks/use-session-capabilities";
34
49
  import { useSandboxFiles } from "../hooks/use-sandbox-files";
35
50
  import {
@@ -45,9 +60,18 @@ import type { MachineView } from "../types/machines";
45
60
  import { SandboxFiles } from "./sandbox-files";
46
61
  import { WorkbenchChanges } from "./workbench-changes";
47
62
  import { SandboxTerminal, type XtermTheme } from "./sandbox-terminal";
48
- import { DesktopViewer } from "./desktop-viewer";
49
63
  import { WorkspaceDock, type WorkspaceDockProps, type WorkspaceTab } from "./workspace-dock";
50
64
 
65
+ const LazyBrowserViewer = lazy(async () => {
66
+ const viewers = await import("./interactive-workbench-viewers");
67
+ return { default: viewers.BrowserViewer };
68
+ });
69
+
70
+ const LazyComputerViewer = lazy(async () => {
71
+ const viewers = await import("./interactive-workbench-viewers");
72
+ return { default: viewers.ComputerViewer };
73
+ });
74
+
51
75
  /** A host-routed notification (replaces the app-only `sonner` toast coupling). */
52
76
  export type WorkspaceNotification = { kind: "error" | "info"; message: string };
53
77
 
@@ -55,6 +79,7 @@ export type WorkspaceNotification = { kind: "error" | "info"; message: string };
55
79
  export const WORKBENCH_TAB_CHANGES = "changes";
56
80
  export const WORKBENCH_TAB_FILES = "files";
57
81
  export const WORKBENCH_TAB_TERMINAL = "terminal";
82
+ export const WORKBENCH_TAB_BROWSER = "browser";
58
83
  export const WORKBENCH_TAB_DESKTOP = "desktop";
59
84
 
60
85
  /**
@@ -66,6 +91,7 @@ export const WORKBENCH_SURFACES = [
66
91
  WORKBENCH_TAB_CHANGES,
67
92
  WORKBENCH_TAB_FILES,
68
93
  WORKBENCH_TAB_TERMINAL,
94
+ WORKBENCH_TAB_BROWSER,
69
95
  WORKBENCH_TAB_DESKTOP,
70
96
  ] as const;
71
97
 
@@ -110,6 +136,18 @@ function WorkbenchTabLabel({ icon, children }: { icon: ReactNode; children: Reac
110
136
  );
111
137
  }
112
138
 
139
+ function WorkbenchSurfaceLoading({ name }: { name: "Browser" | "Computer" }) {
140
+ return (
141
+ <CenteredState
142
+ icon={
143
+ <LoaderCircleIcon className="size-5 animate-spin motion-reduce:animate-none" aria-hidden />
144
+ }
145
+ >
146
+ <p className="text-og-sm font-medium text-og-fg">Opening {name}</p>
147
+ </CenteredState>
148
+ );
149
+ }
150
+
113
151
  /**
114
152
  * Decide a workspace tab from an already-local event log: the newest
115
153
  * `workspace.revision.captured` announce carries the change surface stats, so
@@ -134,7 +172,19 @@ export function initialWorkspaceTab(
134
172
  bestSeq = event.sequence;
135
173
  bestFileCount = typeof stats?.fileCount === "number" ? stats.fileCount : 0;
136
174
  }
137
- return bestFileCount > 0 ? WORKBENCH_TAB_CHANGES : WORKBENCH_TAB_FILES;
175
+ if (bestFileCount > 0) return WORKBENCH_TAB_CHANGES;
176
+
177
+ let latestGitSeq = -1;
178
+ let committedOnly = false;
179
+ for (const event of events ?? []) {
180
+ if (event.type !== "git.changed" || event.sequence > bestSeq) continue;
181
+ if (event.sequence <= latestGitSeq) continue;
182
+ const payload = event.payload as { ahead?: number; reason?: string } | null;
183
+ latestGitSeq = event.sequence;
184
+ committedOnly =
185
+ (typeof payload?.ahead === "number" && payload.ahead > 0) || payload?.reason === "commit";
186
+ }
187
+ return committedOnly ? WORKBENCH_TAB_CHANGES : WORKBENCH_TAB_FILES;
138
188
  }
139
189
 
140
190
  /**
@@ -186,7 +236,7 @@ export type UseSandboxWorkspaceTabsOptions = ClientOverride & {
186
236
  /** Live event log (usually `useSessionEvents().events`). */
187
237
  events: SessionEvent[];
188
238
  /**
189
- * Built-in surfaces this host wants to expose. Omit for all four. A disabled
239
+ * Built-in surfaces this host wants to expose. Omit for all five. A disabled
190
240
  * surface is behaviorally dormant: its data hooks, warm intents, stream
191
241
  * attachment, and navigation callbacks are not activated.
192
242
  */
@@ -196,9 +246,16 @@ export type UseSandboxWorkspaceTabsOptions = ClientOverride & {
196
246
  initialTab?: string | null | undefined;
197
247
  /** Actually selected dock tab. Inactive data surfaces retain cache but issue no live reads. */
198
248
  activeTab?: string | null | undefined;
249
+ /** Whether the dock is visible. Hidden docks retain UI state but must not keep
250
+ * expensive browser/computer media streams alive. */
251
+ workspaceVisible?: boolean | undefined;
199
252
  /** Host-routed notifications (mutation errors, desktop-consent failures). The
200
253
  * package never imports a toast library — the host decides how to surface. */
201
254
  onNotify?: ((notification: WorkspaceNotification) => void) | undefined;
255
+ /** Alternate frame transport for non-browser runtimes and deterministic tests. */
256
+ browserWebSocketFactory?: BrowserFrameWebSocketFactory | undefined;
257
+ /** Alternate Computer frame transport for non-browser runtimes and deterministic tests. */
258
+ computerWebSocketFactory?: ComputerFrameWebSocketFactory | undefined;
202
259
  /** File requested by a Changes guard. The Files surface defers the read until
203
260
  * the sandbox is live, then reveals this path. */
204
261
  requestedFilePath?: string | null | undefined;
@@ -207,10 +264,14 @@ export type UseSandboxWorkspaceTabsOptions = ClientOverride & {
207
264
  requestedFileRequestId?: string | number | null | undefined;
208
265
  /** Route a guarded diff into the host's Files tab. */
209
266
  onOpenFile?: ((path: string) => void) | undefined;
267
+ /** Navigate the host dock to one exact linked ComputerSession. */
268
+ onOpenComputerSession?: ((computerSessionId: string) => void) | undefined;
269
+ requestedComputerSessionId?: string | null | undefined;
270
+ requestedComputerRequestId?: string | number | null | undefined;
210
271
  };
211
272
 
212
273
  export type UseSandboxWorkspaceTabsResult = {
213
- /** Changes | Files | Terminal | Desktop (capability-gated where noted). */
274
+ /** Changes | Files | Terminal | Browser | Desktop (capability-gated where noted). */
214
275
  tabs: WorkspaceTab[];
215
276
  /** The source-driven default tab: Changes when the first authoritative capture
216
277
  * or live Git result has changes, else Files (a host `initialTab` overrides).
@@ -223,7 +284,6 @@ export type UseSandboxWorkspaceTabsResult = {
223
284
 
224
285
  type SessionWarmIntents = {
225
286
  sessionId: string;
226
- watchDesktop: boolean;
227
287
  warmTerminal: boolean;
228
288
  warmFiles: boolean;
229
289
  };
@@ -231,7 +291,6 @@ type SessionWarmIntents = {
231
291
  function emptyWarmIntents(sessionId: string): SessionWarmIntents {
232
292
  return {
233
293
  sessionId,
234
- watchDesktop: false,
235
294
  warmTerminal: false,
236
295
  warmFiles: false,
237
296
  };
@@ -240,24 +299,56 @@ function emptyWarmIntents(sessionId: string): SessionWarmIntents {
240
299
  /**
241
300
  * Build the workbench tabs + the machine-chip model for one session. This is the
242
301
  * dock "brain": capability negotiation, capture-backed cold reads, prewarm
243
- * flags, desktop consent, and the xterm theme observer — all package-local.
302
+ * flags, automatic desktop acknowledgment, and the xterm theme observer — all
303
+ * package-local.
244
304
  */
245
305
  export function useSandboxWorkspaceTabs(
246
306
  options: UseSandboxWorkspaceTabsOptions,
247
307
  ): UseSandboxWorkspaceTabsResult {
248
- const { client, workspaceId } = useOpenGeni(options);
249
- const { sessionId, events, onNotify, requestedFilePath, requestedFileRequestId, onOpenFile } =
250
- options;
308
+ const { client, workspaceId, workspaceInteractionEvent } = useOpenGeni(options);
309
+ const {
310
+ sessionId,
311
+ events,
312
+ onNotify,
313
+ browserWebSocketFactory,
314
+ computerWebSocketFactory,
315
+ requestedFilePath,
316
+ requestedFileRequestId,
317
+ onOpenFile,
318
+ onOpenComputerSession,
319
+ requestedComputerSessionId,
320
+ requestedComputerRequestId,
321
+ } = options;
251
322
  const initialTab = options.initialTab ?? null;
252
323
  const activeTab = options.activeTab ?? initialTab;
324
+ const workspaceVisible = options.workspaceVisible ?? true;
253
325
  const requestedSurfaces = options.surfaces ?? WORKBENCH_SURFACES;
254
326
  const surfaceSet = new Set<SandboxWorkspaceSurface>(requestedSurfaces);
255
327
  const changesEnabled = surfaceSet.has(WORKBENCH_TAB_CHANGES);
256
328
  const filesEnabled = surfaceSet.has(WORKBENCH_TAB_FILES);
257
329
  const terminalEnabled = surfaceSet.has(WORKBENCH_TAB_TERMINAL);
330
+ const browserEnabled = surfaceSet.has(WORKBENCH_TAB_BROWSER);
258
331
  const desktopEnabled = surfaceSet.has(WORKBENCH_TAB_DESKTOP);
332
+ const createLinkedComputer = useCallback(
333
+ async (name: string, placement?: InteractionPlacement) => {
334
+ const response = await client.createComputerSession(workspaceId, {
335
+ operationId: crypto.randomUUID(),
336
+ sessionId,
337
+ name,
338
+ ...(placement ? { placement } : {}),
339
+ });
340
+ if (response.operation.state !== "completed" || response.session.lifecycle !== "active") {
341
+ throw new Error(response.operation.error?.message ?? "The computer could not be opened.");
342
+ }
343
+ return {
344
+ id: response.session.id,
345
+ placement: response.session.placement,
346
+ };
347
+ },
348
+ [client, sessionId, workspaceId],
349
+ );
259
350
  const workspaceDataEnabled = changesEnabled || filesEnabled;
260
- const anySurfaceEnabled = workspaceDataEnabled || terminalEnabled || desktopEnabled;
351
+ const machineSurfaceEnabled = workspaceDataEnabled || terminalEnabled || desktopEnabled;
261
352
  // Do not speculatively activate both remote workspace surfaces while the dock
262
353
  // is still resolving its selected tab. Modal calls cannot be cancelled after
263
354
  // dispatch, so that transient state otherwise creates an aborted duplicate
@@ -267,17 +358,17 @@ export function useSandboxWorkspaceTabs(
267
358
  const filesActive = resolvedActiveTab === WORKBENCH_TAB_FILES;
268
359
  const surfaceIdentity = WORKBENCH_SURFACES.filter((surface) => surfaceSet.has(surface)).join(",");
269
360
 
270
- // The three box-warming INTENTS, each off by default and each
361
+ // The two box-warming INTENTS, each off by default and each
271
362
  // flipped true by a genuine user action (never on mount, never on a passive
272
- // capture glance): desktop watch consent, terminal engagement (`onActivate`), and
273
- // a deliberate live-file open/edit in Files. Browsing capture-served
363
+ // capture glance): terminal engagement (`onActivate`) and a deliberate
364
+ // live-file open/edit in Files. Browsing capture-served
274
365
  // Changes/Files warms nothing — that is the whole point of Refinement 1.
275
366
  const [storedWarmIntents, setStoredWarmIntents] = useState<SessionWarmIntents>(() =>
276
367
  emptyWarmIntents(sessionId),
277
368
  );
278
369
  const warmIntents =
279
370
  storedWarmIntents.sessionId === sessionId ? storedWarmIntents : emptyWarmIntents(sessionId);
280
- const { watchDesktop, warmTerminal, warmFiles } = warmIntents;
371
+ const { warmTerminal, warmFiles } = warmIntents;
281
372
  const requestWarmIntent = useCallback(
282
373
  (intent: Exclude<keyof SessionWarmIntents, "sessionId">) => {
283
374
  setStoredWarmIntents((previous) => {
@@ -294,7 +385,7 @@ export function useSandboxWorkspaceTabs(
294
385
  workspaceId,
295
386
  sessionId,
296
387
  pollIntervalMs: 8000,
297
- enabled: anySurfaceEnabled,
388
+ enabled: machineSurfaceEnabled,
298
389
  });
299
390
  const activeMachine: MachineView | null =
300
391
  machines.machines.find((m) => m.sandboxId === machines.activeSandboxId) ??
@@ -303,8 +394,8 @@ export function useSandboxWorkspaceTabs(
303
394
 
304
395
  const caps = useSessionCapabilities(sessionId, {
305
396
  events,
306
- enabled: anySurfaceEnabled,
307
- attachDesktop: desktopEnabled && watchDesktop,
397
+ enabled: machineSurfaceEnabled,
398
+ attachDesktop: false,
308
399
  attachTerminal: terminalEnabled && warmTerminal,
309
400
  // Explicit live-file intent only — NOT "the Files tab is open". A cold edit
310
401
  // or guarded-file open wakes the box; a glance at the tree/diff does not.
@@ -312,6 +403,11 @@ export function useSandboxWorkspaceTabs(
312
403
  });
313
404
  const capabilities = caps.capabilities;
314
405
  const liveness = capabilities?.liveness;
406
+ // A missing capability document is not evidence that the sandbox is live.
407
+ // Pass explicit null to live-I/O hooks so fast capture/event hydration cannot
408
+ // exploit their legacy "liveness omitted" compatibility path while negotiation
409
+ // is still pending.
410
+ const liveIoLiveness = liveness ?? null;
315
411
  const fileSystemOn = capabilities?.FileSystem.available ?? false;
316
412
  // The FS is writable only when it's live AND not read-only. A self-hosted box
317
413
  // that's offline (or any read-only advertisement) or a capture-served cold tree
@@ -323,26 +419,23 @@ export function useSandboxWorkspaceTabs(
323
419
  // Mutations are live-only. A capture is a review artifact, never a writable
324
420
  // surrogate for a sleeping machine; the user must deliberately open the live
325
421
  // workspace before create/rename/delete/edit controls appear.
326
- const filesEditable =
327
- fileSystemOn && !fsReadOnly && (liveness === "warm" || liveness === "draining");
422
+ const filesEditable = fileSystemOn && !fsReadOnly && sandboxAcceptsLiveIo(liveness);
328
423
  const gitOn = capabilities?.Git.available ?? false;
329
424
  const terminalOn = terminalEnabled && (capabilities?.Terminal.transport ?? null) !== null;
330
- // The REAL interactive terminal is the ttyd pty-ws stream. When it's live the
331
- // legacy HTTP PTY must NOT run; `useSandboxTerminal` opens its HTTP PTY only as
332
- // the firehose-mode fallback (a backend without ttyd).
333
- const ptyWsLive =
334
- capabilities?.Terminal.transport === "pty-ws" && Boolean(capabilities?.Terminal.url);
335
- const ptyCapable = (capabilities?.Terminal.ptyCapable ?? false) && !ptyWsLive;
425
+ // A descriptor-only pty-ws cell intentionally has no short-lived URL until
426
+ // terminal intent acquires one. Do not mistake that grant boundary for a
427
+ // legacy HTTP PTY and eagerly open a second terminal. The fallback exists only
428
+ // for a genuinely firehose-transport backend with no typed degradation.
429
+ const ptyCapable =
430
+ warmTerminal &&
431
+ (capabilities?.Terminal.ptyCapable ?? false) &&
432
+ capabilities?.Terminal.transport === "sse-events" &&
433
+ capabilities?.Terminal.reason === null;
336
434
  const terminal = useSandboxTerminal(sessionId, {
337
435
  events: terminalEnabled ? events : [],
338
436
  interactive: terminalEnabled && ptyCapable,
339
437
  liveness,
340
438
  });
341
- const desktopAdvertised =
342
- desktopEnabled &&
343
- ((capabilities?.DesktopStream.transport ?? null) !== null ||
344
- capabilities?.DesktopStream.reason === "lease_cold");
345
-
346
439
  // Lazy provisioning (#315) creates the box mid-turn on the first sandbox tool
347
440
  // call, emitting sandbox.provision started→completed/failed on the live stream.
348
441
  // The on-demand resting hook rests without polling, so when the box warms the
@@ -360,6 +453,17 @@ export function useSandboxWorkspaceTabs(
360
453
  provisioningRef.current = { sessionId, provisioning };
361
454
  }, [sessionId, provisioning, renegotiate]);
362
455
 
456
+ // BrowserSession and ComputerSession lifecycles use the same placement lease
457
+ // as files/terminal/desktop, but are workspace resources rather than session
458
+ // events. Re-read the capability document on their canonical workspace
459
+ // invalidation so the one machine chip cannot remain "Offline" beside a live
460
+ // browser/computer (or remain "Live" after the final interaction holder ends).
461
+ // This is a read-only negotiation: it never acquires a holder or wakes a box.
462
+ useEffect(() => {
463
+ if (workspaceInteractionEvent?.workspaceId !== workspaceId) return;
464
+ renegotiate();
465
+ }, [workspaceId, workspaceInteractionEvent, renegotiate]);
466
+
363
467
  // The cold-paint data source: the latest turn-end capture, fetched with a single
364
468
  // api round-trip on mount (no machine). Feeds the Files tree + the Changes/Git
365
469
  // diff immediately; a warm box reconciles live without discarding the capture
@@ -369,7 +473,7 @@ export function useSandboxWorkspaceTabs(
369
473
  enabled: workspaceDataEnabled,
370
474
  });
371
475
  const captureAvailable = captureState.available;
372
- const liveWorkspaceExpected = liveness === "warm" || liveness === "draining";
476
+ const liveWorkspaceExpected = sandboxAcceptsLiveIo(liveness);
373
477
  // Capability negotiation and capture resolution are independent requests. A
374
478
  // cold capability document can win that race by a few milliseconds; enabling
375
479
  // the leaf hooks in that window would make them interpret `capture: null` as a
@@ -407,7 +511,7 @@ export function useSandboxWorkspaceTabs(
407
511
  enabled: filesEnabled && (captureAvailable || (fileSystemOn && liveWorkspaceExpected)),
408
512
  active: filesActive,
409
513
  repoPaths,
410
- liveness,
514
+ liveness: liveIoLiveness,
411
515
  capture: captureState.capture,
412
516
  // A reverted optimistic mutation (e.g. a 409 rename collision) surfaces as a
413
517
  // host notification — the tree silently rolls back, the user sees why.
@@ -451,7 +555,7 @@ export function useSandboxWorkspaceTabs(
451
555
  // dormant, avoiding the old duplicate full-tree read.
452
556
  active: changesActive || resolvedActiveTab === null,
453
557
  repoPaths,
454
- liveness,
558
+ liveness: liveIoLiveness,
455
559
  comparison: changesComparison,
456
560
  capture: changesComparison === "staged" ? null : captureState.capture,
457
561
  });
@@ -484,30 +588,6 @@ export function useSandboxWorkspaceTabs(
484
588
  return () => observer.disconnect();
485
589
  }, [terminalEnabled]);
486
590
 
487
- async function acknowledgeAndWatch() {
488
- try {
489
- const shared = capabilities?.DesktopStream.shared ?? false;
490
- await client.acknowledgeStream(workspaceId, sessionId, {
491
- acknowledgeUnredacted: true,
492
- acknowledgeShared: shared,
493
- });
494
- requestWarmIntent("watchDesktop");
495
- caps.renegotiate();
496
- } catch (error) {
497
- onNotify?.({
498
- kind: "error",
499
- message: `Could not start the desktop stream: ${error instanceof Error ? error.message : String(error)}`,
500
- });
501
- }
502
- }
503
-
504
- // Re-warm WITHOUT re-acknowledging: the consent was already recorded, only the
505
- // box drained back to cold. Idempotent — the viewer auto-warm de-dupes.
506
- function rewarmDesktop() {
507
- if (!watchDesktop) requestWarmIntent("watchDesktop");
508
- caps.renegotiate();
509
- }
510
-
511
591
  const dirtyCount = git.diff.length;
512
592
 
513
593
  // The one truthful machine indicator, derived from the live capability/liveness
@@ -517,10 +597,7 @@ export function useSandboxWorkspaceTabs(
517
597
  capabilitiesState: caps.state,
518
598
  activeMachineState: activeMachine?.state ?? null,
519
599
  activeIsSelfhosted: activeMachine?.kind === "selfhosted",
520
- wantsWarm:
521
- (terminalEnabled && warmTerminal) ||
522
- (desktopEnabled && watchDesktop) ||
523
- (workspaceDataEnabled && warmFiles),
600
+ wantsWarm: (terminalEnabled && warmTerminal) || (workspaceDataEnabled && warmFiles),
524
601
  capturedAt: captureState.capturedAt,
525
602
  });
526
603
  const workspaceWaking = chip.state === "waking";
@@ -543,7 +620,11 @@ export function useSandboxWorkspaceTabs(
543
620
  }
544
621
  const captureIsAuthoritative =
545
622
  !liveWorkspaceExpected && (liveness !== undefined || caps.error !== null);
546
- const captureUnavailable = captureState.fileCount === 0 || captureState.error !== null;
623
+ const captureHasChanges =
624
+ (captureState.fileCount ?? 0) > 0 ||
625
+ (captureState.capture?.repos.some((repo) => (repo.branchDiff?.length ?? 0) > 0) ?? false);
626
+ const captureUnavailable =
627
+ (captureState.fileCount === 0 && !captureHasChanges) || captureState.error !== null;
547
628
  const implicitBranchFallbackPending =
548
629
  changesComparison === "branch" &&
549
630
  git.error !== null &&
@@ -563,7 +644,7 @@ export function useSandboxWorkspaceTabs(
563
644
  captureState.available
564
645
  ) {
565
646
  defaultTabRef.current.value = sourceDrivenDefaultTab(
566
- captureState.fileCount > 0,
647
+ captureHasChanges,
567
648
  changesEnabled,
568
649
  filesEnabled,
569
650
  );
@@ -586,7 +667,7 @@ export function useSandboxWorkspaceTabs(
586
667
  // A warm live read failed but the durable capture is intact: retain an
587
668
  // immediate, deterministic review surface instead of hanging unresolved.
588
669
  defaultTabRef.current.value = sourceDrivenDefaultTab(
589
- captureState.fileCount > 0,
670
+ captureHasChanges,
590
671
  changesEnabled,
591
672
  filesEnabled,
592
673
  );
@@ -664,7 +745,7 @@ export function useSandboxWorkspaceTabs(
664
745
  ...(requestedFileRequestId !== null && requestedFileRequestId !== undefined
665
746
  ? { requestedPathRequestId: requestedFileRequestId }
666
747
  : {}),
667
- requestedPathReady: liveness === "warm" || liveness === "draining",
748
+ requestedPathReady: sandboxAcceptsLiveIo(liveness),
668
749
  }
669
750
  : {})}
670
751
  // Ordinary capture browsing does not warm a box. The first edit — or an
@@ -683,9 +764,11 @@ export function useSandboxWorkspaceTabs(
683
764
  content: (
684
765
  <div className="h-full bg-og-bg p-1">
685
766
  <SandboxTerminal
767
+ key={sessionId}
686
768
  result={terminal}
687
769
  terminalCapability={capabilities?.Terminal ?? null}
688
770
  onActivate={() => requestWarmIntent("warmTerminal")}
771
+ onReconnectNeeded={caps.renegotiate}
689
772
  showHeader
690
773
  shell={capabilities?.Terminal.shell ?? undefined}
691
774
  liveness={liveness}
@@ -696,25 +779,65 @@ export function useSandboxWorkspaceTabs(
696
779
  });
697
780
  }
698
781
 
699
- // Desktop — capability-gated + consent-gated.
700
- if (desktopAdvertised) {
782
+ // Browserindependent from sandbox capability negotiation. BrowserSessions
783
+ // are workspace resources and may live in this agent's sandbox, a peer's
784
+ // sandbox, a connected machine, or an external browser placement.
785
+ if (browserEnabled) {
786
+ list.push({
787
+ id: WORKBENCH_TAB_BROWSER,
788
+ label: <WorkbenchTabLabel icon={<Globe2Icon />}>Browser</WorkbenchTabLabel>,
789
+ content: (
790
+ <Suspense fallback={<WorkbenchSurfaceLoading name="Browser" />}>
791
+ <LazyBrowserViewer
792
+ key={sessionId}
793
+ client={client}
794
+ workspaceId={workspaceId}
795
+ sessionId={sessionId}
796
+ // WorkspaceDock mounts a heavy surface only after its first visit,
797
+ // then keeps it mounted behind `hidden` when another tab is
798
+ // selected. Keep that already-visited surface enabled while the
799
+ // dock itself remains open: tearing down its controller + media
800
+ // socket on every Browser↔Computer tab switch turned an ordinary
801
+ // click into a multi-second cold reconnect. This does not eagerly
802
+ // start BrowserViewer—the lazy panel is still unmounted until the
803
+ // user/agent first visits it.
804
+ enabled={workspaceVisible}
805
+ onNotify={onNotify}
806
+ {...(desktopEnabled ? { createLinkedComputer } : {})}
807
+ {...(onOpenComputerSession ? { onOpenComputer: onOpenComputerSession } : {})}
808
+ {...(browserWebSocketFactory ? { webSocketFactory: browserWebSocketFactory } : {})}
809
+ className="h-full"
810
+ />
811
+ </Suspense>
812
+ ),
813
+ });
814
+ }
815
+
816
+ // Computer — stable legacy tab id, new workspace resource body. Like Browser,
817
+ // it is independent from the selected agent's sandbox capability document.
818
+ if (desktopEnabled) {
701
819
  list.push({
702
820
  id: WORKBENCH_TAB_DESKTOP,
703
- label: <WorkbenchTabLabel icon={<MonitorIcon />}>Desktop</WorkbenchTabLabel>,
704
- badge: watchDesktop ? (
705
- <span className="rounded-og-xs bg-og-status-running/20 px-1 text-og-xs text-og-status-running">
706
- Live
707
- </span>
708
- ) : undefined,
821
+ label: <WorkbenchTabLabel icon={<MonitorIcon />}>Computer</WorkbenchTabLabel>,
709
822
  content: (
710
- <DesktopViewer
711
- capability={capabilities?.DesktopStream ?? null}
712
- viewerCapReached={caps.viewerCapReached}
713
- watching={watchDesktop}
714
- onAcknowledge={() => void acknowledgeAndWatch()}
715
- onWarm={rewarmDesktop}
716
- className="h-full"
717
- />
823
+ <Suspense fallback={<WorkbenchSurfaceLoading name="Computer" />}>
824
+ <LazyComputerViewer
825
+ key={sessionId}
826
+ client={client}
827
+ workspaceId={workspaceId}
828
+ sessionId={sessionId}
829
+ // Same visited-surface lifetime as Browser above. A live noVNC
830
+ // connection survives tab switches and ResizeObserver re-fits its
831
+ // canvas when this hidden panel becomes visible again, making the
832
+ // second and later Computer opens local-machine fast.
833
+ enabled={workspaceVisible}
834
+ onNotify={onNotify}
835
+ requestedComputerSessionId={requestedComputerSessionId}
836
+ requestedComputerRequestId={requestedComputerRequestId}
837
+ {...(computerWebSocketFactory ? { webSocketFactory: computerWebSocketFactory } : {})}
838
+ className="h-full"
839
+ />
840
+ </Suspense>
718
841
  ),
719
842
  });
720
843
  }
@@ -726,14 +849,24 @@ export function useSandboxWorkspaceTabs(
726
849
  changesEnabled,
727
850
  filesEnabled,
728
851
  terminalOn,
729
- desktopAdvertised,
852
+ browserEnabled,
853
+ desktopEnabled,
730
854
  captureAvailable,
731
855
  dirtyCount,
732
- watchDesktop,
733
856
  warmTerminal,
734
857
  requestedFilePath,
735
858
  requestedFileRequestId,
736
859
  onOpenFile,
860
+ onOpenComputerSession,
861
+ requestedComputerSessionId,
862
+ requestedComputerRequestId,
863
+ onNotify,
864
+ browserWebSocketFactory,
865
+ computerWebSocketFactory,
866
+ workspaceVisible,
867
+ resolvedActiveTab,
868
+ client,
869
+ workspaceId,
737
870
  liveness,
738
871
  sessionId,
739
872
  files,
@@ -747,13 +880,14 @@ export function useSandboxWorkspaceTabs(
747
880
  caps.error,
748
881
  caps.viewerCapReached,
749
882
  requestWarmIntent,
883
+ createLinkedComputer,
750
884
  ]);
751
885
 
752
886
  return {
753
887
  tabs,
754
888
  defaultTab,
755
889
  machine: {
756
- enabled: anySurfaceEnabled,
890
+ enabled: machineSurfaceEnabled,
757
891
  chip,
758
892
  activeMachine,
759
893
  loading: machines.loading,
@@ -771,7 +905,7 @@ export type SandboxWorkspaceProps = ClientOverride & {
771
905
  primary: ReactNode;
772
906
  /**
773
907
  * Built-in surfaces this host wants to expose. Omit for Changes, Files,
774
- * Terminal, and Desktop. Disabled surfaces remain behaviorally dormant.
908
+ * Terminal, Browser, and Desktop. Disabled surfaces remain behaviorally dormant.
775
909
  */
776
910
  surfaces?: readonly SandboxWorkspaceSurface[] | undefined;
777
911
  /** Host tabs injected BEFORE the workbench tabs (e.g. a "Run" landing tab). */
@@ -783,6 +917,10 @@ export type SandboxWorkspaceProps = ClientOverride & {
783
917
  initialTab?: string | undefined;
784
918
  /** Host-routed notifications (no toast dependency in the package). */
785
919
  onNotify?: ((notification: WorkspaceNotification) => void) | undefined;
920
+ /** Alternate Browser frame transport for non-browser runtimes and deterministic tests. */
921
+ browserWebSocketFactory?: BrowserFrameWebSocketFactory | undefined;
922
+ /** Alternate Computer frame transport for non-browser runtimes and deterministic tests. */
923
+ computerWebSocketFactory?: ComputerFrameWebSocketFactory | undefined;
786
924
  /** Controlled collapsed state for hosts with their own dock toggle. */
787
925
  collapsed?: boolean | undefined;
788
926
  onCollapsedChange?: ((collapsed: boolean) => void) | undefined;
@@ -814,6 +952,8 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
814
952
  trailingTabs,
815
953
  initialTab,
816
954
  onNotify,
955
+ browserWebSocketFactory,
956
+ computerWebSocketFactory,
817
957
  collapsed,
818
958
  onCollapsedChange,
819
959
  showCollapseControl,
@@ -834,7 +974,13 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
834
974
  path: string;
835
975
  requestId: number;
836
976
  } | null>(null);
977
+ const [requestedComputer, setRequestedComputer] = useState<{
978
+ sessionId: string;
979
+ computerSessionId: string;
980
+ requestId: number;
981
+ } | null>(null);
837
982
  const nextFileRequestId = useRef(0);
983
+ const nextComputerRequestId = useRef(0);
838
984
  const selectedTab = storedSelection?.sessionId === sessionId ? storedSelection.tab : null;
839
985
  const activeTabHint = selectedTab ?? initialTab ?? leadingTabs?.[0]?.id ?? null;
840
986
  const openFile = useCallback(
@@ -845,6 +991,18 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
845
991
  },
846
992
  [sessionId],
847
993
  );
994
+ const openComputer = useCallback(
995
+ (computerSessionId: string) => {
996
+ nextComputerRequestId.current += 1;
997
+ setRequestedComputer({
998
+ sessionId,
999
+ computerSessionId,
1000
+ requestId: nextComputerRequestId.current,
1001
+ });
1002
+ setStoredSelection({ sessionId, tab: WORKBENCH_TAB_DESKTOP });
1003
+ },
1004
+ [sessionId],
1005
+ );
848
1006
 
849
1007
  const {
850
1008
  tabs: workbenchTabs,
@@ -858,10 +1016,18 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
858
1016
  ...(surfaces ? { surfaces } : {}),
859
1017
  ...(initialTab ? { initialTab } : {}),
860
1018
  activeTab: activeTabHint,
1019
+ workspaceVisible: collapsed !== true,
861
1020
  ...(onNotify ? { onNotify } : {}),
1021
+ ...(browserWebSocketFactory ? { browserWebSocketFactory } : {}),
1022
+ ...(computerWebSocketFactory ? { computerWebSocketFactory } : {}),
862
1023
  requestedFilePath: requestedFile?.sessionId === sessionId ? requestedFile.path : null,
863
1024
  requestedFileRequestId: requestedFile?.sessionId === sessionId ? requestedFile.requestId : null,
864
1025
  onOpenFile: openFile,
1026
+ onOpenComputerSession: openComputer,
1027
+ requestedComputerSessionId:
1028
+ requestedComputer?.sessionId === sessionId ? requestedComputer.computerSessionId : null,
1029
+ requestedComputerRequestId:
1030
+ requestedComputer?.sessionId === sessionId ? requestedComputer.requestId : null,
865
1031
  });
866
1032
 
867
1033
  // A user's tab click wins forever; before that we follow the source-driven
@@ -929,13 +1095,13 @@ function MachineStateChip({ chip }: { chip: MachineChip }) {
929
1095
  <div
930
1096
  role="status"
931
1097
  aria-label={`Machine: ${chip.label}`}
932
- className="inline-flex min-h-7 items-center gap-1.5 px-2 py-1 text-og-xs font-medium text-og-fg-muted max-[1023px]:min-h-11 pointer-coarse:min-h-11"
1098
+ className="inline-flex min-h-7 min-w-0 items-center gap-1.5 px-2 py-1 text-og-xs font-medium text-og-fg-muted max-[1023px]:min-h-11 pointer-coarse:min-h-11"
933
1099
  >
934
1100
  <span
935
1101
  className={cn("size-1.5 shrink-0 rounded-full", chipDotClass(chip.state))}
936
1102
  aria-hidden
937
1103
  />
938
- <span className="max-w-[11rem] truncate">{chip.label}</span>
1104
+ <span className="min-w-0 max-w-[11rem] truncate">{chip.label}</span>
939
1105
  </div>
940
1106
  );
941
1107
  }