@opengeni/react 0.48.0 → 0.52.1

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 (284) hide show
  1. package/README.md +90 -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 +170 -0
  13. package/dist/artifacts.js.map +1 -0
  14. package/dist/chunk-6BBWWOOU.js +82 -0
  15. package/dist/chunk-6BBWWOOU.js.map +1 -0
  16. package/dist/{chunk-FWXGSNXT.js → chunk-724BW4TW.js} +585 -34
  17. package/dist/chunk-724BW4TW.js.map +1 -0
  18. package/dist/chunk-7I2KB3OK.js +3895 -0
  19. package/dist/chunk-7I2KB3OK.js.map +1 -0
  20. package/dist/chunk-7WCLAEFV.js +2828 -0
  21. package/dist/chunk-7WCLAEFV.js.map +1 -0
  22. package/dist/{chunk-I4MPFDRP.js → chunk-FWS3KC3Z.js} +3 -3
  23. package/dist/{chunk-EKZH6IDG.js → chunk-HDBG7XGO.js} +62 -1
  24. package/dist/chunk-HDBG7XGO.js.map +1 -0
  25. package/dist/chunk-HUIKIDM5.js +2148 -0
  26. package/dist/chunk-HUIKIDM5.js.map +1 -0
  27. package/dist/{chunk-XCNTXWBB.js → chunk-JAGDUCZQ.js} +854 -340
  28. package/dist/chunk-JAGDUCZQ.js.map +1 -0
  29. package/dist/{chunk-LHLYWG5R.js → chunk-JR3QFY6B.js} +413 -37
  30. package/dist/chunk-JR3QFY6B.js.map +1 -0
  31. package/dist/chunk-JVYQRRC4.js +53 -0
  32. package/dist/chunk-JVYQRRC4.js.map +1 -0
  33. package/dist/chunk-KCGXPP5V.js +157 -0
  34. package/dist/chunk-KCGXPP5V.js.map +1 -0
  35. package/dist/chunk-PEI6RANI.js +3234 -0
  36. package/dist/chunk-PEI6RANI.js.map +1 -0
  37. package/dist/{chunk-NJ7M2B6Z.js → chunk-ROIRTM37.js} +18 -10
  38. package/dist/{chunk-NJ7M2B6Z.js.map → chunk-ROIRTM37.js.map} +1 -1
  39. package/dist/{chunk-O4VXOBGF.js → chunk-WSK7G5LE.js} +70 -2
  40. package/dist/chunk-WSK7G5LE.js.map +1 -0
  41. package/dist/{chunk-OL6QVXGF.js → chunk-X4S56Y2H.js} +27 -7
  42. package/dist/{chunk-OL6QVXGF.js.map → chunk-X4S56Y2H.js.map} +1 -1
  43. package/dist/client.d.ts +7 -1
  44. package/dist/clipboard.d.ts +1 -0
  45. package/dist/clipboard.js +7 -0
  46. package/dist/clipboard.js.map +1 -0
  47. package/dist/commands/index.d.ts +3 -3
  48. package/dist/commands/registry.d.ts +1 -1
  49. package/dist/commands/types.d.ts +1 -1
  50. package/dist/components/approval-surface.d.ts +2 -2
  51. package/dist/components/artifacts/artifact-surface.d.ts +18 -0
  52. package/dist/components/artifacts/document-editor.d.ts +172 -0
  53. package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
  54. package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
  55. package/dist/components/artifacts/editable-document.d.ts +24 -0
  56. package/dist/components/artifacts/editable-presentation.d.ts +24 -0
  57. package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
  58. package/dist/components/artifacts/index.d.ts +8 -0
  59. package/dist/components/artifacts/presentation-editor.d.ts +297 -0
  60. package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
  61. package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
  62. package/dist/components/browser-viewer.d.ts +29 -0
  63. package/dist/components/chat-composer.d.ts +8 -8
  64. package/dist/components/code-editor.d.ts +25 -3
  65. package/dist/components/command-palette.d.ts +2 -2
  66. package/dist/components/composer-transcription-control.d.ts +2 -2
  67. package/dist/components/composer.d.ts +17 -17
  68. package/dist/components/computer-viewer.d.ts +23 -0
  69. package/dist/components/copy-button.d.ts +2 -2
  70. package/dist/components/desktop-viewer.d.ts +25 -21
  71. package/dist/components/diff-view.d.ts +1 -1
  72. package/dist/components/enrollment-consent.d.ts +1 -1
  73. package/dist/components/enrollment-device-flow.d.ts +1 -1
  74. package/dist/components/file-browser.d.ts +2 -2
  75. package/dist/components/fleet-tile.d.ts +1 -1
  76. package/dist/components/generated-video-player.d.ts +10 -0
  77. package/dist/components/human-input-form.d.ts +1 -1
  78. package/dist/components/human-input-surface.d.ts +2 -2
  79. package/dist/components/interactive-workbench-viewers.d.ts +2 -0
  80. package/dist/components/machine-card.d.ts +2 -2
  81. package/dist/components/machine-dock-bar.d.ts +3 -3
  82. package/dist/components/machine-health-pill.d.ts +2 -2
  83. package/dist/components/machine-input-display.d.ts +1 -1
  84. package/dist/components/machine-metrics.d.ts +2 -2
  85. package/dist/components/machine-status-pill.d.ts +4 -4
  86. package/dist/components/machines/health.d.ts +1 -1
  87. package/dist/components/machines/machine-detail.d.ts +3 -3
  88. package/dist/components/machines/metric-history-chart.d.ts +1 -1
  89. package/dist/components/machines/metric-sparkline.d.ts +2 -2
  90. package/dist/components/machines/series.d.ts +2 -2
  91. package/dist/components/machines-dashboard.d.ts +2 -2
  92. package/dist/components/markdown.d.ts +1 -1
  93. package/dist/components/message-timeline.d.ts +12 -5
  94. package/dist/components/model-picker.d.ts +2 -2
  95. package/dist/components/model-policy-picker.d.ts +7 -7
  96. package/dist/components/pierre-diff.d.ts +1 -1
  97. package/dist/components/pierre-file.d.ts +1 -1
  98. package/dist/components/queue-draft-policy.d.ts +1 -1
  99. package/dist/components/queue-surface-implementation.d.ts +3 -3
  100. package/dist/components/queue-surface-state.d.ts +4 -4
  101. package/dist/components/queue-surface.d.ts +5 -5
  102. package/dist/components/sandbox-files.d.ts +3 -3
  103. package/dist/components/sandbox-terminal.d.ts +21 -4
  104. package/dist/components/sandbox-workspace.d.ts +25 -9
  105. package/dist/components/session-chrome.d.ts +4 -4
  106. package/dist/components/session-status.d.ts +2 -2
  107. package/dist/components/timeline-annotation-selection.d.ts +9 -0
  108. package/dist/components/timeline-annotations-dialog.d.ts +16 -0
  109. package/dist/components/timeline-annotations.d.ts +13 -0
  110. package/dist/components/tooltip.d.ts +4 -4
  111. package/dist/components/user-message-body.d.ts +2 -2
  112. package/dist/components/workbench-changes.d.ts +2 -2
  113. package/dist/components/workspace-dock.d.ts +1 -1
  114. package/dist/composer.d.ts +11 -11
  115. package/dist/composer.js +5 -5
  116. package/dist/hooks/internal.d.ts +1 -1
  117. package/dist/hooks/use-attached-browsers.d.ts +21 -0
  118. package/dist/hooks/use-available-models.d.ts +2 -2
  119. package/dist/hooks/use-billing-usage.d.ts +1 -1
  120. package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
  121. package/dist/hooks/use-browser-identities.d.ts +26 -0
  122. package/dist/hooks/use-browser-session.d.ts +28 -0
  123. package/dist/hooks/use-browser-sessions.d.ts +32 -0
  124. package/dist/hooks/use-codex-accounts.d.ts +2 -2
  125. package/dist/hooks/use-composer.d.ts +36 -9
  126. package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
  127. package/dist/hooks/use-computer-session.d.ts +28 -0
  128. package/dist/hooks/use-computer-sessions.d.ts +28 -0
  129. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  130. package/dist/hooks/use-environments.d.ts +1 -1
  131. package/dist/hooks/use-file-attachments.d.ts +1 -1
  132. package/dist/hooks/use-goal.d.ts +2 -2
  133. package/dist/hooks/use-human-input.d.ts +2 -2
  134. package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
  135. package/dist/hooks/use-machine-chip.d.ts +2 -2
  136. package/dist/hooks/use-machines.d.ts +4 -4
  137. package/dist/hooks/use-packs.d.ts +7 -2
  138. package/dist/hooks/use-rigs.d.ts +1 -1
  139. package/dist/hooks/use-sandbox-files.d.ts +6 -4
  140. package/dist/hooks/use-sandbox-git.d.ts +6 -5
  141. package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
  142. package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
  143. package/dist/hooks/use-session-capabilities.d.ts +5 -4
  144. package/dist/hooks/use-session-control.d.ts +1 -1
  145. package/dist/hooks/use-session-events.d.ts +2 -2
  146. package/dist/hooks/use-session-lineage.d.ts +1 -1
  147. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  148. package/dist/hooks/use-session.d.ts +2 -2
  149. package/dist/hooks/use-slash-commands.d.ts +1 -1
  150. package/dist/hooks/use-terminal-stream.d.ts +8 -2
  151. package/dist/hooks/use-turn-queue.d.ts +2 -2
  152. package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
  153. package/dist/hooks/use-voice-input.d.ts +1 -1
  154. package/dist/hooks/use-workspace-capture.d.ts +1 -1
  155. package/dist/hooks/use-workspace-edit.d.ts +1 -1
  156. package/dist/hooks/use-workspace-sessions.d.ts +1 -1
  157. package/dist/hooks/use-workspaces.d.ts +1 -1
  158. package/dist/index.d.ts +158 -155
  159. package/dist/index.js +906 -462
  160. package/dist/index.js.map +1 -1
  161. package/dist/interaction.d.ts +23 -0
  162. package/dist/interaction.js +36 -0
  163. package/dist/interaction.js.map +1 -0
  164. package/dist/interactive-workbench-viewers-KHNOHWQE.js +13 -0
  165. package/dist/interactive-workbench-viewers-KHNOHWQE.js.map +1 -0
  166. package/dist/lib/relay-wire.d.ts +4 -0
  167. package/dist/lib/sandbox-liveness.d.ts +9 -0
  168. package/dist/lib/terminal-capability.d.ts +5 -0
  169. package/dist/lib/xterm-theme.d.ts +1 -1
  170. package/dist/machines.d.ts +30 -30
  171. package/dist/machines.js +3 -3
  172. package/dist/provider.d.ts +4 -4
  173. package/dist/{queue-surface-implementation-U5JFG7HT.js → queue-surface-implementation-2KVJTYXH.js} +100 -48
  174. package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
  175. package/dist/realtime/dropdown-menu.d.ts +8 -8
  176. package/dist/realtime/realtime-control.d.ts +6 -6
  177. package/dist/realtime.d.ts +4 -4
  178. package/dist/realtime.js +3 -3
  179. package/dist/session-context.d.ts +19 -1
  180. package/dist/session-ui.d.ts +14 -14
  181. package/dist/session-ui.js +5 -4
  182. package/dist/session.d.ts +28 -28
  183. package/dist/session.js +5 -5
  184. package/dist/timeline/activity-rail.d.ts +5 -3
  185. package/dist/timeline/disclosure-context.d.ts +1 -1
  186. package/dist/timeline/fleet-decision-projection.d.ts +1 -1
  187. package/dist/timeline/fleet-decision-row.d.ts +3 -3
  188. package/dist/timeline/index.d.ts +19 -19
  189. package/dist/timeline/parsers.d.ts +4 -1
  190. package/dist/timeline/projection.d.ts +3 -3
  191. package/dist/timeline/registry.d.ts +8 -2
  192. package/dist/timeline/screenshot-lightbox.d.ts +2 -2
  193. package/dist/timeline/shared.d.ts +21 -10
  194. package/dist/timeline/tool-diff.d.ts +2 -2
  195. package/dist/timeline/tool-renderers.d.ts +1 -1
  196. package/dist/timeline/turn-summary.d.ts +3 -3
  197. package/dist/timeline/types.d.ts +39 -3
  198. package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
  199. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
  200. package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
  201. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
  202. package/package.json +36 -7
  203. package/src/artifacts-document.ts +31 -0
  204. package/src/artifacts-presentation.ts +36 -0
  205. package/src/artifacts-spreadsheet.ts +21 -0
  206. package/src/artifacts.ts +86 -0
  207. package/src/client.ts +87 -1
  208. package/src/clipboard.ts +2 -0
  209. package/src/components/artifacts/artifact-surface.tsx +93 -0
  210. package/src/components/artifacts/document-editor.tsx +2255 -0
  211. package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
  212. package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
  213. package/src/components/artifacts/editable-document.tsx +657 -0
  214. package/src/components/artifacts/editable-presentation.tsx +1020 -0
  215. package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
  216. package/src/components/artifacts/index.ts +99 -0
  217. package/src/components/artifacts/presentation-editor.tsx +3148 -0
  218. package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
  219. package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
  220. package/src/components/browser-viewer.tsx +1592 -0
  221. package/src/components/chat-composer.tsx +13 -0
  222. package/src/components/code-editor.tsx +89 -30
  223. package/src/components/composer-transcription-control.tsx +10 -6
  224. package/src/components/computer-viewer.tsx +1139 -0
  225. package/src/components/desktop-viewer.tsx +101 -73
  226. package/src/components/generated-video-player.tsx +89 -0
  227. package/src/components/interactive-workbench-viewers.ts +2 -0
  228. package/src/components/machine-input-display.ts +3 -0
  229. package/src/components/message-timeline.tsx +289 -32
  230. package/src/components/queue-surface-implementation.tsx +116 -60
  231. package/src/components/sandbox-files.tsx +2 -2
  232. package/src/components/sandbox-terminal.tsx +106 -41
  233. package/src/components/sandbox-workspace.tsx +215 -87
  234. package/src/components/session-chrome.tsx +53 -13
  235. package/src/components/timeline-annotation-selection.tsx +221 -0
  236. package/src/components/timeline-annotations-dialog.tsx +213 -0
  237. package/src/components/timeline-annotations.tsx +84 -0
  238. package/src/components/workbench-changes.tsx +188 -31
  239. package/src/hooks/use-attached-browsers.ts +152 -0
  240. package/src/hooks/use-browser-frame-stream.ts +316 -0
  241. package/src/hooks/use-browser-identities.ts +225 -0
  242. package/src/hooks/use-browser-session.ts +476 -0
  243. package/src/hooks/use-browser-sessions.ts +281 -0
  244. package/src/hooks/use-composer.ts +769 -24
  245. package/src/hooks/use-computer-frame-stream.ts +311 -0
  246. package/src/hooks/use-computer-session.ts +442 -0
  247. package/src/hooks/use-computer-sessions.ts +245 -0
  248. package/src/hooks/use-machine-chip.ts +5 -3
  249. package/src/hooks/use-machines.ts +33 -3
  250. package/src/hooks/use-packs.ts +51 -0
  251. package/src/hooks/use-sandbox-files.ts +98 -55
  252. package/src/hooks/use-sandbox-git.ts +93 -38
  253. package/src/hooks/use-sandbox-terminal.ts +3 -2
  254. package/src/hooks/use-session-capabilities.ts +59 -46
  255. package/src/hooks/use-terminal-stream.ts +102 -23
  256. package/src/hooks/use-video-artifact-playback.ts +28 -0
  257. package/src/hooks/use-workspace-edit.ts +2 -1
  258. package/src/index.ts +10 -0
  259. package/src/interaction.ts +71 -0
  260. package/src/lib/relay-wire.ts +4 -0
  261. package/src/lib/sandbox-liveness.ts +11 -0
  262. package/src/lib/terminal-capability.ts +10 -0
  263. package/src/session-context.ts +90 -0
  264. package/src/timeline/activity-rail.tsx +19 -3
  265. package/src/timeline/index.ts +3 -0
  266. package/src/timeline/parsers.ts +14 -1
  267. package/src/timeline/projection.ts +211 -14
  268. package/src/timeline/registry.ts +13 -1
  269. package/src/timeline/screenshot-lightbox.tsx +19 -9
  270. package/src/timeline/shared.tsx +56 -8
  271. package/src/timeline/tool-renderers.tsx +303 -50
  272. package/src/timeline/types.ts +55 -3
  273. package/styles/compiled.css +248 -15
  274. package/styles/index.css +15 -0
  275. package/styles/responsive.css +13 -1
  276. package/dist/chunk-EKZH6IDG.js.map +0 -1
  277. package/dist/chunk-FWXGSNXT.js.map +0 -1
  278. package/dist/chunk-LHLYWG5R.js.map +0 -1
  279. package/dist/chunk-O4VXOBGF.js.map +0 -1
  280. package/dist/chunk-XCNTXWBB.js.map +0 -1
  281. package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
  282. package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
  283. package/dist/queue-surface-implementation-U5JFG7HT.js.map +0 -1
  284. /package/dist/{chunk-I4MPFDRP.js.map → chunk-FWS3KC3Z.js.map} +0 -0
@@ -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,23 @@
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";
18
28
  import {
19
29
  CircleCheckIcon,
20
30
  CpuIcon,
21
31
  FileCode2Icon,
22
32
  GitCompareArrowsIcon,
33
+ Globe2Icon,
23
34
  LoaderCircleIcon,
24
35
  MonitorIcon,
25
36
  RefreshCwIcon,
@@ -30,6 +41,9 @@ import {
30
41
  import { type ClientOverride, useOpenGeni } from "../provider";
31
42
  import { cn } from "../lib/cn";
32
43
  import { xtermThemeFromTokens } from "../lib/xterm-theme";
44
+ import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
45
+ import type { BrowserFrameWebSocketFactory } from "../hooks/use-browser-frame-stream";
46
+ import type { ComputerFrameWebSocketFactory } from "../hooks/use-computer-frame-stream";
33
47
  import { useSessionCapabilities } from "../hooks/use-session-capabilities";
34
48
  import { useSandboxFiles } from "../hooks/use-sandbox-files";
35
49
  import {
@@ -45,9 +59,18 @@ import type { MachineView } from "../types/machines";
45
59
  import { SandboxFiles } from "./sandbox-files";
46
60
  import { WorkbenchChanges } from "./workbench-changes";
47
61
  import { SandboxTerminal, type XtermTheme } from "./sandbox-terminal";
48
- import { DesktopViewer } from "./desktop-viewer";
49
62
  import { WorkspaceDock, type WorkspaceDockProps, type WorkspaceTab } from "./workspace-dock";
50
63
 
64
+ const LazyBrowserViewer = lazy(async () => {
65
+ const viewers = await import("./interactive-workbench-viewers");
66
+ return { default: viewers.BrowserViewer };
67
+ });
68
+
69
+ const LazyComputerViewer = lazy(async () => {
70
+ const viewers = await import("./interactive-workbench-viewers");
71
+ return { default: viewers.ComputerViewer };
72
+ });
73
+
51
74
  /** A host-routed notification (replaces the app-only `sonner` toast coupling). */
52
75
  export type WorkspaceNotification = { kind: "error" | "info"; message: string };
53
76
 
@@ -55,6 +78,7 @@ export type WorkspaceNotification = { kind: "error" | "info"; message: string };
55
78
  export const WORKBENCH_TAB_CHANGES = "changes";
56
79
  export const WORKBENCH_TAB_FILES = "files";
57
80
  export const WORKBENCH_TAB_TERMINAL = "terminal";
81
+ export const WORKBENCH_TAB_BROWSER = "browser";
58
82
  export const WORKBENCH_TAB_DESKTOP = "desktop";
59
83
 
60
84
  /**
@@ -66,6 +90,7 @@ export const WORKBENCH_SURFACES = [
66
90
  WORKBENCH_TAB_CHANGES,
67
91
  WORKBENCH_TAB_FILES,
68
92
  WORKBENCH_TAB_TERMINAL,
93
+ WORKBENCH_TAB_BROWSER,
69
94
  WORKBENCH_TAB_DESKTOP,
70
95
  ] as const;
71
96
 
@@ -110,6 +135,18 @@ function WorkbenchTabLabel({ icon, children }: { icon: ReactNode; children: Reac
110
135
  );
111
136
  }
112
137
 
138
+ function WorkbenchSurfaceLoading({ name }: { name: "Browser" | "Computer" }) {
139
+ return (
140
+ <CenteredState
141
+ icon={
142
+ <LoaderCircleIcon className="size-5 animate-spin motion-reduce:animate-none" aria-hidden />
143
+ }
144
+ >
145
+ <p className="text-og-sm font-medium text-og-fg">Opening {name}</p>
146
+ </CenteredState>
147
+ );
148
+ }
149
+
113
150
  /**
114
151
  * Decide a workspace tab from an already-local event log: the newest
115
152
  * `workspace.revision.captured` announce carries the change surface stats, so
@@ -134,7 +171,19 @@ export function initialWorkspaceTab(
134
171
  bestSeq = event.sequence;
135
172
  bestFileCount = typeof stats?.fileCount === "number" ? stats.fileCount : 0;
136
173
  }
137
- return bestFileCount > 0 ? WORKBENCH_TAB_CHANGES : WORKBENCH_TAB_FILES;
174
+ if (bestFileCount > 0) return WORKBENCH_TAB_CHANGES;
175
+
176
+ let latestGitSeq = -1;
177
+ let committedOnly = false;
178
+ for (const event of events ?? []) {
179
+ if (event.type !== "git.changed" || event.sequence > bestSeq) continue;
180
+ if (event.sequence <= latestGitSeq) continue;
181
+ const payload = event.payload as { ahead?: number; reason?: string } | null;
182
+ latestGitSeq = event.sequence;
183
+ committedOnly =
184
+ (typeof payload?.ahead === "number" && payload.ahead > 0) || payload?.reason === "commit";
185
+ }
186
+ return committedOnly ? WORKBENCH_TAB_CHANGES : WORKBENCH_TAB_FILES;
138
187
  }
139
188
 
140
189
  /**
@@ -186,7 +235,7 @@ export type UseSandboxWorkspaceTabsOptions = ClientOverride & {
186
235
  /** Live event log (usually `useSessionEvents().events`). */
187
236
  events: SessionEvent[];
188
237
  /**
189
- * Built-in surfaces this host wants to expose. Omit for all four. A disabled
238
+ * Built-in surfaces this host wants to expose. Omit for all five. A disabled
190
239
  * surface is behaviorally dormant: its data hooks, warm intents, stream
191
240
  * attachment, and navigation callbacks are not activated.
192
241
  */
@@ -199,6 +248,10 @@ export type UseSandboxWorkspaceTabsOptions = ClientOverride & {
199
248
  /** Host-routed notifications (mutation errors, desktop-consent failures). The
200
249
  * package never imports a toast library — the host decides how to surface. */
201
250
  onNotify?: ((notification: WorkspaceNotification) => void) | undefined;
251
+ /** Alternate frame transport for non-browser runtimes and deterministic tests. */
252
+ browserWebSocketFactory?: BrowserFrameWebSocketFactory | undefined;
253
+ /** Alternate Computer frame transport for non-browser runtimes and deterministic tests. */
254
+ computerWebSocketFactory?: ComputerFrameWebSocketFactory | undefined;
202
255
  /** File requested by a Changes guard. The Files surface defers the read until
203
256
  * the sandbox is live, then reveals this path. */
204
257
  requestedFilePath?: string | null | undefined;
@@ -207,10 +260,14 @@ export type UseSandboxWorkspaceTabsOptions = ClientOverride & {
207
260
  requestedFileRequestId?: string | number | null | undefined;
208
261
  /** Route a guarded diff into the host's Files tab. */
209
262
  onOpenFile?: ((path: string) => void) | undefined;
263
+ /** Navigate the host dock to one exact linked ComputerSession. */
264
+ onOpenComputerSession?: ((computerSessionId: string) => void) | undefined;
265
+ requestedComputerSessionId?: string | null | undefined;
266
+ requestedComputerRequestId?: string | number | null | undefined;
210
267
  };
211
268
 
212
269
  export type UseSandboxWorkspaceTabsResult = {
213
- /** Changes | Files | Terminal | Desktop (capability-gated where noted). */
270
+ /** Changes | Files | Terminal | Browser | Desktop (capability-gated where noted). */
214
271
  tabs: WorkspaceTab[];
215
272
  /** The source-driven default tab: Changes when the first authoritative capture
216
273
  * or live Git result has changes, else Files (a host `initialTab` overrides).
@@ -223,7 +280,6 @@ export type UseSandboxWorkspaceTabsResult = {
223
280
 
224
281
  type SessionWarmIntents = {
225
282
  sessionId: string;
226
- watchDesktop: boolean;
227
283
  warmTerminal: boolean;
228
284
  warmFiles: boolean;
229
285
  };
@@ -231,7 +287,6 @@ type SessionWarmIntents = {
231
287
  function emptyWarmIntents(sessionId: string): SessionWarmIntents {
232
288
  return {
233
289
  sessionId,
234
- watchDesktop: false,
235
290
  warmTerminal: false,
236
291
  warmFiles: false,
237
292
  };
@@ -240,14 +295,26 @@ function emptyWarmIntents(sessionId: string): SessionWarmIntents {
240
295
  /**
241
296
  * Build the workbench tabs + the machine-chip model for one session. This is the
242
297
  * dock "brain": capability negotiation, capture-backed cold reads, prewarm
243
- * flags, desktop consent, and the xterm theme observer — all package-local.
298
+ * flags, automatic desktop acknowledgment, and the xterm theme observer — all
299
+ * package-local.
244
300
  */
245
301
  export function useSandboxWorkspaceTabs(
246
302
  options: UseSandboxWorkspaceTabsOptions,
247
303
  ): UseSandboxWorkspaceTabsResult {
248
304
  const { client, workspaceId } = useOpenGeni(options);
249
- const { sessionId, events, onNotify, requestedFilePath, requestedFileRequestId, onOpenFile } =
250
- options;
305
+ const {
306
+ sessionId,
307
+ events,
308
+ onNotify,
309
+ browserWebSocketFactory,
310
+ computerWebSocketFactory,
311
+ requestedFilePath,
312
+ requestedFileRequestId,
313
+ onOpenFile,
314
+ onOpenComputerSession,
315
+ requestedComputerSessionId,
316
+ requestedComputerRequestId,
317
+ } = options;
251
318
  const initialTab = options.initialTab ?? null;
252
319
  const activeTab = options.activeTab ?? initialTab;
253
320
  const requestedSurfaces = options.surfaces ?? WORKBENCH_SURFACES;
@@ -255,9 +322,24 @@ export function useSandboxWorkspaceTabs(
255
322
  const changesEnabled = surfaceSet.has(WORKBENCH_TAB_CHANGES);
256
323
  const filesEnabled = surfaceSet.has(WORKBENCH_TAB_FILES);
257
324
  const terminalEnabled = surfaceSet.has(WORKBENCH_TAB_TERMINAL);
325
+ const browserEnabled = surfaceSet.has(WORKBENCH_TAB_BROWSER);
258
326
  const desktopEnabled = surfaceSet.has(WORKBENCH_TAB_DESKTOP);
327
+ const createLinkedComputer = useCallback(
328
+ async (name: string) => {
329
+ const response = await client.createComputerSession(workspaceId, {
330
+ operationId: crypto.randomUUID(),
331
+ sessionId,
332
+ name,
333
+ });
334
+ return {
335
+ id: response.session.id,
336
+ placement: response.session.placement,
337
+ };
338
+ },
339
+ [client, sessionId, workspaceId],
340
+ );
259
341
  const workspaceDataEnabled = changesEnabled || filesEnabled;
260
- const anySurfaceEnabled = workspaceDataEnabled || terminalEnabled || desktopEnabled;
342
+ const machineSurfaceEnabled = workspaceDataEnabled || terminalEnabled || desktopEnabled;
261
343
  // Do not speculatively activate both remote workspace surfaces while the dock
262
344
  // is still resolving its selected tab. Modal calls cannot be cancelled after
263
345
  // dispatch, so that transient state otherwise creates an aborted duplicate
@@ -267,17 +349,17 @@ export function useSandboxWorkspaceTabs(
267
349
  const filesActive = resolvedActiveTab === WORKBENCH_TAB_FILES;
268
350
  const surfaceIdentity = WORKBENCH_SURFACES.filter((surface) => surfaceSet.has(surface)).join(",");
269
351
 
270
- // The three box-warming INTENTS, each off by default and each
352
+ // The two box-warming INTENTS, each off by default and each
271
353
  // 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
354
+ // capture glance): terminal engagement (`onActivate`) and a deliberate
355
+ // live-file open/edit in Files. Browsing capture-served
274
356
  // Changes/Files warms nothing — that is the whole point of Refinement 1.
275
357
  const [storedWarmIntents, setStoredWarmIntents] = useState<SessionWarmIntents>(() =>
276
358
  emptyWarmIntents(sessionId),
277
359
  );
278
360
  const warmIntents =
279
361
  storedWarmIntents.sessionId === sessionId ? storedWarmIntents : emptyWarmIntents(sessionId);
280
- const { watchDesktop, warmTerminal, warmFiles } = warmIntents;
362
+ const { warmTerminal, warmFiles } = warmIntents;
281
363
  const requestWarmIntent = useCallback(
282
364
  (intent: Exclude<keyof SessionWarmIntents, "sessionId">) => {
283
365
  setStoredWarmIntents((previous) => {
@@ -294,7 +376,7 @@ export function useSandboxWorkspaceTabs(
294
376
  workspaceId,
295
377
  sessionId,
296
378
  pollIntervalMs: 8000,
297
- enabled: anySurfaceEnabled,
379
+ enabled: machineSurfaceEnabled,
298
380
  });
299
381
  const activeMachine: MachineView | null =
300
382
  machines.machines.find((m) => m.sandboxId === machines.activeSandboxId) ??
@@ -303,8 +385,8 @@ export function useSandboxWorkspaceTabs(
303
385
 
304
386
  const caps = useSessionCapabilities(sessionId, {
305
387
  events,
306
- enabled: anySurfaceEnabled,
307
- attachDesktop: desktopEnabled && watchDesktop,
388
+ enabled: machineSurfaceEnabled,
389
+ attachDesktop: false,
308
390
  attachTerminal: terminalEnabled && warmTerminal,
309
391
  // Explicit live-file intent only — NOT "the Files tab is open". A cold edit
310
392
  // or guarded-file open wakes the box; a glance at the tree/diff does not.
@@ -312,6 +394,11 @@ export function useSandboxWorkspaceTabs(
312
394
  });
313
395
  const capabilities = caps.capabilities;
314
396
  const liveness = capabilities?.liveness;
397
+ // A missing capability document is not evidence that the sandbox is live.
398
+ // Pass explicit null to live-I/O hooks so fast capture/event hydration cannot
399
+ // exploit their legacy "liveness omitted" compatibility path while negotiation
400
+ // is still pending.
401
+ const liveIoLiveness = liveness ?? null;
315
402
  const fileSystemOn = capabilities?.FileSystem.available ?? false;
316
403
  // The FS is writable only when it's live AND not read-only. A self-hosted box
317
404
  // that's offline (or any read-only advertisement) or a capture-served cold tree
@@ -323,26 +410,23 @@ export function useSandboxWorkspaceTabs(
323
410
  // Mutations are live-only. A capture is a review artifact, never a writable
324
411
  // surrogate for a sleeping machine; the user must deliberately open the live
325
412
  // workspace before create/rename/delete/edit controls appear.
326
- const filesEditable =
327
- fileSystemOn && !fsReadOnly && (liveness === "warm" || liveness === "draining");
413
+ const filesEditable = fileSystemOn && !fsReadOnly && sandboxAcceptsLiveIo(liveness);
328
414
  const gitOn = capabilities?.Git.available ?? false;
329
415
  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;
416
+ // A descriptor-only pty-ws cell intentionally has no short-lived URL until
417
+ // terminal intent acquires one. Do not mistake that grant boundary for a
418
+ // legacy HTTP PTY and eagerly open a second terminal. The fallback exists only
419
+ // for a genuinely firehose-transport backend with no typed degradation.
420
+ const ptyCapable =
421
+ warmTerminal &&
422
+ (capabilities?.Terminal.ptyCapable ?? false) &&
423
+ capabilities?.Terminal.transport === "sse-events" &&
424
+ capabilities?.Terminal.reason === null;
336
425
  const terminal = useSandboxTerminal(sessionId, {
337
426
  events: terminalEnabled ? events : [],
338
427
  interactive: terminalEnabled && ptyCapable,
339
428
  liveness,
340
429
  });
341
- const desktopAdvertised =
342
- desktopEnabled &&
343
- ((capabilities?.DesktopStream.transport ?? null) !== null ||
344
- capabilities?.DesktopStream.reason === "lease_cold");
345
-
346
430
  // Lazy provisioning (#315) creates the box mid-turn on the first sandbox tool
347
431
  // call, emitting sandbox.provision started→completed/failed on the live stream.
348
432
  // The on-demand resting hook rests without polling, so when the box warms the
@@ -369,7 +453,7 @@ export function useSandboxWorkspaceTabs(
369
453
  enabled: workspaceDataEnabled,
370
454
  });
371
455
  const captureAvailable = captureState.available;
372
- const liveWorkspaceExpected = liveness === "warm" || liveness === "draining";
456
+ const liveWorkspaceExpected = sandboxAcceptsLiveIo(liveness);
373
457
  // Capability negotiation and capture resolution are independent requests. A
374
458
  // cold capability document can win that race by a few milliseconds; enabling
375
459
  // the leaf hooks in that window would make them interpret `capture: null` as a
@@ -407,7 +491,7 @@ export function useSandboxWorkspaceTabs(
407
491
  enabled: filesEnabled && (captureAvailable || (fileSystemOn && liveWorkspaceExpected)),
408
492
  active: filesActive,
409
493
  repoPaths,
410
- liveness,
494
+ liveness: liveIoLiveness,
411
495
  capture: captureState.capture,
412
496
  // A reverted optimistic mutation (e.g. a 409 rename collision) surfaces as a
413
497
  // host notification — the tree silently rolls back, the user sees why.
@@ -451,7 +535,7 @@ export function useSandboxWorkspaceTabs(
451
535
  // dormant, avoiding the old duplicate full-tree read.
452
536
  active: changesActive || resolvedActiveTab === null,
453
537
  repoPaths,
454
- liveness,
538
+ liveness: liveIoLiveness,
455
539
  comparison: changesComparison,
456
540
  capture: changesComparison === "staged" ? null : captureState.capture,
457
541
  });
@@ -484,30 +568,6 @@ export function useSandboxWorkspaceTabs(
484
568
  return () => observer.disconnect();
485
569
  }, [terminalEnabled]);
486
570
 
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
571
  const dirtyCount = git.diff.length;
512
572
 
513
573
  // The one truthful machine indicator, derived from the live capability/liveness
@@ -517,10 +577,7 @@ export function useSandboxWorkspaceTabs(
517
577
  capabilitiesState: caps.state,
518
578
  activeMachineState: activeMachine?.state ?? null,
519
579
  activeIsSelfhosted: activeMachine?.kind === "selfhosted",
520
- wantsWarm:
521
- (terminalEnabled && warmTerminal) ||
522
- (desktopEnabled && watchDesktop) ||
523
- (workspaceDataEnabled && warmFiles),
580
+ wantsWarm: (terminalEnabled && warmTerminal) || (workspaceDataEnabled && warmFiles),
524
581
  capturedAt: captureState.capturedAt,
525
582
  });
526
583
  const workspaceWaking = chip.state === "waking";
@@ -543,7 +600,11 @@ export function useSandboxWorkspaceTabs(
543
600
  }
544
601
  const captureIsAuthoritative =
545
602
  !liveWorkspaceExpected && (liveness !== undefined || caps.error !== null);
546
- const captureUnavailable = captureState.fileCount === 0 || captureState.error !== null;
603
+ const captureHasChanges =
604
+ (captureState.fileCount ?? 0) > 0 ||
605
+ (captureState.capture?.repos.some((repo) => (repo.branchDiff?.length ?? 0) > 0) ?? false);
606
+ const captureUnavailable =
607
+ (captureState.fileCount === 0 && !captureHasChanges) || captureState.error !== null;
547
608
  const implicitBranchFallbackPending =
548
609
  changesComparison === "branch" &&
549
610
  git.error !== null &&
@@ -563,7 +624,7 @@ export function useSandboxWorkspaceTabs(
563
624
  captureState.available
564
625
  ) {
565
626
  defaultTabRef.current.value = sourceDrivenDefaultTab(
566
- captureState.fileCount > 0,
627
+ captureHasChanges,
567
628
  changesEnabled,
568
629
  filesEnabled,
569
630
  );
@@ -586,7 +647,7 @@ export function useSandboxWorkspaceTabs(
586
647
  // A warm live read failed but the durable capture is intact: retain an
587
648
  // immediate, deterministic review surface instead of hanging unresolved.
588
649
  defaultTabRef.current.value = sourceDrivenDefaultTab(
589
- captureState.fileCount > 0,
650
+ captureHasChanges,
590
651
  changesEnabled,
591
652
  filesEnabled,
592
653
  );
@@ -664,7 +725,7 @@ export function useSandboxWorkspaceTabs(
664
725
  ...(requestedFileRequestId !== null && requestedFileRequestId !== undefined
665
726
  ? { requestedPathRequestId: requestedFileRequestId }
666
727
  : {}),
667
- requestedPathReady: liveness === "warm" || liveness === "draining",
728
+ requestedPathReady: sandboxAcceptsLiveIo(liveness),
668
729
  }
669
730
  : {})}
670
731
  // Ordinary capture browsing does not warm a box. The first edit — or an
@@ -683,6 +744,7 @@ export function useSandboxWorkspaceTabs(
683
744
  content: (
684
745
  <div className="h-full bg-og-bg p-1">
685
746
  <SandboxTerminal
747
+ key={sessionId}
686
748
  result={terminal}
687
749
  terminalCapability={capabilities?.Terminal ?? null}
688
750
  onActivate={() => requestWarmIntent("warmTerminal")}
@@ -696,25 +758,51 @@ export function useSandboxWorkspaceTabs(
696
758
  });
697
759
  }
698
760
 
699
- // Desktop — capability-gated + consent-gated.
700
- if (desktopAdvertised) {
761
+ // Browserindependent from sandbox capability negotiation. BrowserSessions
762
+ // are workspace resources and may live in this agent's sandbox, a peer's
763
+ // sandbox, a connected machine, or an external browser placement.
764
+ if (browserEnabled) {
765
+ list.push({
766
+ id: WORKBENCH_TAB_BROWSER,
767
+ label: <WorkbenchTabLabel icon={<Globe2Icon />}>Browser</WorkbenchTabLabel>,
768
+ content: (
769
+ <Suspense fallback={<WorkbenchSurfaceLoading name="Browser" />}>
770
+ <LazyBrowserViewer
771
+ key={sessionId}
772
+ client={client}
773
+ workspaceId={workspaceId}
774
+ sessionId={sessionId}
775
+ onNotify={onNotify}
776
+ {...(desktopEnabled ? { createLinkedComputer } : {})}
777
+ {...(onOpenComputerSession ? { onOpenComputer: onOpenComputerSession } : {})}
778
+ {...(browserWebSocketFactory ? { webSocketFactory: browserWebSocketFactory } : {})}
779
+ className="h-full"
780
+ />
781
+ </Suspense>
782
+ ),
783
+ });
784
+ }
785
+
786
+ // Computer — stable legacy tab id, new workspace resource body. Like Browser,
787
+ // it is independent from the selected agent's sandbox capability document.
788
+ if (desktopEnabled) {
701
789
  list.push({
702
790
  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,
791
+ label: <WorkbenchTabLabel icon={<MonitorIcon />}>Computer</WorkbenchTabLabel>,
709
792
  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
- />
793
+ <Suspense fallback={<WorkbenchSurfaceLoading name="Computer" />}>
794
+ <LazyComputerViewer
795
+ key={sessionId}
796
+ client={client}
797
+ workspaceId={workspaceId}
798
+ sessionId={sessionId}
799
+ onNotify={onNotify}
800
+ requestedComputerSessionId={requestedComputerSessionId}
801
+ requestedComputerRequestId={requestedComputerRequestId}
802
+ {...(computerWebSocketFactory ? { webSocketFactory: computerWebSocketFactory } : {})}
803
+ className="h-full"
804
+ />
805
+ </Suspense>
718
806
  ),
719
807
  });
720
808
  }
@@ -726,14 +814,22 @@ export function useSandboxWorkspaceTabs(
726
814
  changesEnabled,
727
815
  filesEnabled,
728
816
  terminalOn,
729
- desktopAdvertised,
817
+ browserEnabled,
818
+ desktopEnabled,
730
819
  captureAvailable,
731
820
  dirtyCount,
732
- watchDesktop,
733
821
  warmTerminal,
734
822
  requestedFilePath,
735
823
  requestedFileRequestId,
736
824
  onOpenFile,
825
+ onOpenComputerSession,
826
+ requestedComputerSessionId,
827
+ requestedComputerRequestId,
828
+ onNotify,
829
+ browserWebSocketFactory,
830
+ computerWebSocketFactory,
831
+ client,
832
+ workspaceId,
737
833
  liveness,
738
834
  sessionId,
739
835
  files,
@@ -747,13 +843,14 @@ export function useSandboxWorkspaceTabs(
747
843
  caps.error,
748
844
  caps.viewerCapReached,
749
845
  requestWarmIntent,
846
+ createLinkedComputer,
750
847
  ]);
751
848
 
752
849
  return {
753
850
  tabs,
754
851
  defaultTab,
755
852
  machine: {
756
- enabled: anySurfaceEnabled,
853
+ enabled: machineSurfaceEnabled,
757
854
  chip,
758
855
  activeMachine,
759
856
  loading: machines.loading,
@@ -771,7 +868,7 @@ export type SandboxWorkspaceProps = ClientOverride & {
771
868
  primary: ReactNode;
772
869
  /**
773
870
  * Built-in surfaces this host wants to expose. Omit for Changes, Files,
774
- * Terminal, and Desktop. Disabled surfaces remain behaviorally dormant.
871
+ * Terminal, Browser, and Desktop. Disabled surfaces remain behaviorally dormant.
775
872
  */
776
873
  surfaces?: readonly SandboxWorkspaceSurface[] | undefined;
777
874
  /** Host tabs injected BEFORE the workbench tabs (e.g. a "Run" landing tab). */
@@ -783,6 +880,10 @@ export type SandboxWorkspaceProps = ClientOverride & {
783
880
  initialTab?: string | undefined;
784
881
  /** Host-routed notifications (no toast dependency in the package). */
785
882
  onNotify?: ((notification: WorkspaceNotification) => void) | undefined;
883
+ /** Alternate Browser frame transport for non-browser runtimes and deterministic tests. */
884
+ browserWebSocketFactory?: BrowserFrameWebSocketFactory | undefined;
885
+ /** Alternate Computer frame transport for non-browser runtimes and deterministic tests. */
886
+ computerWebSocketFactory?: ComputerFrameWebSocketFactory | undefined;
786
887
  /** Controlled collapsed state for hosts with their own dock toggle. */
787
888
  collapsed?: boolean | undefined;
788
889
  onCollapsedChange?: ((collapsed: boolean) => void) | undefined;
@@ -814,6 +915,8 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
814
915
  trailingTabs,
815
916
  initialTab,
816
917
  onNotify,
918
+ browserWebSocketFactory,
919
+ computerWebSocketFactory,
817
920
  collapsed,
818
921
  onCollapsedChange,
819
922
  showCollapseControl,
@@ -834,7 +937,13 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
834
937
  path: string;
835
938
  requestId: number;
836
939
  } | null>(null);
940
+ const [requestedComputer, setRequestedComputer] = useState<{
941
+ sessionId: string;
942
+ computerSessionId: string;
943
+ requestId: number;
944
+ } | null>(null);
837
945
  const nextFileRequestId = useRef(0);
946
+ const nextComputerRequestId = useRef(0);
838
947
  const selectedTab = storedSelection?.sessionId === sessionId ? storedSelection.tab : null;
839
948
  const activeTabHint = selectedTab ?? initialTab ?? leadingTabs?.[0]?.id ?? null;
840
949
  const openFile = useCallback(
@@ -845,6 +954,18 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
845
954
  },
846
955
  [sessionId],
847
956
  );
957
+ const openComputer = useCallback(
958
+ (computerSessionId: string) => {
959
+ nextComputerRequestId.current += 1;
960
+ setRequestedComputer({
961
+ sessionId,
962
+ computerSessionId,
963
+ requestId: nextComputerRequestId.current,
964
+ });
965
+ setStoredSelection({ sessionId, tab: WORKBENCH_TAB_DESKTOP });
966
+ },
967
+ [sessionId],
968
+ );
848
969
 
849
970
  const {
850
971
  tabs: workbenchTabs,
@@ -859,9 +980,16 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
859
980
  ...(initialTab ? { initialTab } : {}),
860
981
  activeTab: activeTabHint,
861
982
  ...(onNotify ? { onNotify } : {}),
983
+ ...(browserWebSocketFactory ? { browserWebSocketFactory } : {}),
984
+ ...(computerWebSocketFactory ? { computerWebSocketFactory } : {}),
862
985
  requestedFilePath: requestedFile?.sessionId === sessionId ? requestedFile.path : null,
863
986
  requestedFileRequestId: requestedFile?.sessionId === sessionId ? requestedFile.requestId : null,
864
987
  onOpenFile: openFile,
988
+ onOpenComputerSession: openComputer,
989
+ requestedComputerSessionId:
990
+ requestedComputer?.sessionId === sessionId ? requestedComputer.computerSessionId : null,
991
+ requestedComputerRequestId:
992
+ requestedComputer?.sessionId === sessionId ? requestedComputer.requestId : null,
865
993
  });
866
994
 
867
995
  // A user's tab click wins forever; before that we follow the source-driven