@opengeni/react 0.57.0 → 2.2.0-canary.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 (174) hide show
  1. package/README.md +36 -11
  2. package/dist/artifacts-spreadsheet.js +1 -1
  3. package/dist/artifacts.js +1 -1
  4. package/dist/browser-viewer-GXUICIAG.js +14 -0
  5. package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
  6. package/dist/chunk-4MDLEHXL.js.map +1 -0
  7. package/dist/{chunk-Y7OEDUJ5.js → chunk-5RZN3DD5.js} +1014 -420
  8. package/dist/chunk-5RZN3DD5.js.map +1 -0
  9. package/dist/chunk-5YK2ZDIG.js +116 -0
  10. package/dist/chunk-5YK2ZDIG.js.map +1 -0
  11. package/dist/chunk-H5YGYNXS.js +329 -0
  12. package/dist/chunk-H5YGYNXS.js.map +1 -0
  13. package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
  14. package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
  15. package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
  16. package/dist/chunk-JOAVA2TW.js.map +1 -0
  17. package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
  18. package/dist/chunk-LTCHJ4MS.js.map +1 -0
  19. package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
  20. package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
  21. package/dist/chunk-QGBJ6GHG.js +2300 -0
  22. package/dist/chunk-QGBJ6GHG.js.map +1 -0
  23. package/dist/{chunk-B6WPZSTT.js → chunk-UOLJWT5Y.js} +78 -16
  24. package/dist/chunk-UOLJWT5Y.js.map +1 -0
  25. package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
  26. package/dist/chunk-W6NFJXSA.js.map +1 -0
  27. package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
  28. package/dist/chunk-X4A66EWZ.js.map +1 -0
  29. package/dist/chunk-X4JAFD5R.js +3907 -0
  30. package/dist/chunk-X4JAFD5R.js.map +1 -0
  31. package/dist/{chunk-6KLX2RH5.js → chunk-XSA2QAKP.js} +334 -19
  32. package/dist/chunk-XSA2QAKP.js.map +1 -0
  33. package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
  34. package/dist/chunk-XXWH2JLC.js.map +1 -0
  35. package/dist/client.d.ts +2 -2
  36. package/dist/components/browser-viewer.d.ts +7 -1
  37. package/dist/components/computer-viewer.d.ts +4 -1
  38. package/dist/components/enrollment-consent.d.ts +6 -3
  39. package/dist/components/file-browser.d.ts +7 -1
  40. package/dist/components/human-input-form.d.ts +1 -1
  41. package/dist/components/human-input-surface.d.ts +1 -1
  42. package/dist/components/machine-card.d.ts +4 -1
  43. package/dist/components/machines/machine-detail.d.ts +4 -1
  44. package/dist/components/machines/machine-operation-policy.d.ts +11 -0
  45. package/dist/components/machines-dashboard.d.ts +3 -1
  46. package/dist/components/markdown.d.ts +20 -1
  47. package/dist/components/message-timeline.d.ts +1 -1
  48. package/dist/components/model-policy-picker.d.ts +2 -0
  49. package/dist/components/pierre-file.d.ts +1 -1
  50. package/dist/components/sandbox-files.d.ts +8 -2
  51. package/dist/components/sandbox-workspace.d.ts +33 -3
  52. package/dist/components/session-chrome.d.ts +15 -3
  53. package/dist/components/workspace-dock.d.ts +6 -2
  54. package/dist/composer.js +5 -5
  55. package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
  56. package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
  57. package/dist/hooks/channel-cache.d.ts +23 -0
  58. package/dist/hooks/shared-machines-list.d.ts +14 -0
  59. package/dist/hooks/use-attached-browsers.d.ts +2 -1
  60. package/dist/hooks/use-channels.d.ts +1 -0
  61. package/dist/hooks/use-composer.d.ts +37 -9
  62. package/dist/hooks/use-machine-chip.d.ts +3 -3
  63. package/dist/hooks/use-machines.d.ts +19 -1
  64. package/dist/hooks/use-sandbox-files.d.ts +1 -0
  65. package/dist/hooks/use-session-background-commands.d.ts +14 -0
  66. package/dist/hooks/use-turn-queue.d.ts +12 -0
  67. package/dist/hooks/use-workspace-sessions.d.ts +1 -0
  68. package/dist/index.d.ts +7 -5
  69. package/dist/index.js +1096 -623
  70. package/dist/index.js.map +1 -1
  71. package/dist/interaction.js +29 -181
  72. package/dist/interaction.js.map +1 -1
  73. package/dist/lib/interaction-errors.d.ts +3 -0
  74. package/dist/machines.d.ts +4 -2
  75. package/dist/machines.js +10 -2
  76. package/dist/model-policy.d.ts +1 -1
  77. package/dist/model-policy.js +1 -1
  78. package/dist/platform-activity-row-BJHUHXAT.js +169 -0
  79. package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
  80. package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
  81. package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
  82. package/dist/realtime/realtime-control.d.ts +10 -3
  83. package/dist/realtime.js +138 -29
  84. package/dist/realtime.js.map +1 -1
  85. package/dist/session-ui.d.ts +1 -1
  86. package/dist/session-ui.js +6 -2
  87. package/dist/session.d.ts +2 -2
  88. package/dist/session.js +4 -4
  89. package/dist/timeline/index.d.ts +1 -1
  90. package/dist/timeline/platform-activity-row.d.ts +15 -0
  91. package/dist/timeline/screenshot-lightbox.d.ts +1 -1
  92. package/dist/timeline/shared.d.ts +4 -2
  93. package/dist/timeline/types.d.ts +48 -5
  94. package/dist/types/machines.d.ts +2 -2
  95. package/package.json +3 -3
  96. package/src/client.ts +12 -1
  97. package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
  98. package/src/components/browser-viewer.tsx +361 -58
  99. package/src/components/chat-composer.tsx +1 -1
  100. package/src/components/composer.tsx +6 -6
  101. package/src/components/computer-viewer.tsx +368 -115
  102. package/src/components/enrollment-consent.tsx +64 -6
  103. package/src/components/file-browser.tsx +117 -2
  104. package/src/components/human-input-form.tsx +91 -55
  105. package/src/components/human-input-surface.tsx +1 -1
  106. package/src/components/machine-card.tsx +111 -0
  107. package/src/components/machine-input-display.ts +15 -0
  108. package/src/components/machines/machine-detail.tsx +47 -0
  109. package/src/components/machines/machine-operation-policy.tsx +179 -0
  110. package/src/components/machines-dashboard.tsx +6 -0
  111. package/src/components/markdown.tsx +174 -20
  112. package/src/components/message-timeline.tsx +162 -17
  113. package/src/components/model-policy-picker.tsx +9 -17
  114. package/src/components/pierre-file.tsx +1 -1
  115. package/src/components/queue-surface-implementation.tsx +82 -16
  116. package/src/components/sandbox-files.tsx +135 -19
  117. package/src/components/sandbox-workspace.tsx +204 -92
  118. package/src/components/session-chrome.tsx +415 -208
  119. package/src/components/session-status.tsx +1 -1
  120. package/src/components/user-message-body.tsx +168 -48
  121. package/src/components/workspace-dock.tsx +141 -175
  122. package/src/hooks/channel-cache.ts +104 -0
  123. package/src/hooks/shared-machines-list.ts +239 -0
  124. package/src/hooks/use-attached-browsers.ts +5 -1
  125. package/src/hooks/use-browser-frame-stream.ts +6 -1
  126. package/src/hooks/use-browser-session.ts +6 -4
  127. package/src/hooks/use-browser-sessions.ts +7 -1
  128. package/src/hooks/use-channels.ts +32 -10
  129. package/src/hooks/use-composer.ts +760 -291
  130. package/src/hooks/use-computer-frame-stream.ts +14 -9
  131. package/src/hooks/use-computer-session.ts +16 -14
  132. package/src/hooks/use-computer-sessions.ts +8 -1
  133. package/src/hooks/use-last-started-turn-policy.ts +1 -1
  134. package/src/hooks/use-machine-chip.ts +23 -6
  135. package/src/hooks/use-machines.ts +104 -6
  136. package/src/hooks/use-sandbox-files.ts +195 -140
  137. package/src/hooks/use-sandbox-git.ts +58 -29
  138. package/src/hooks/use-session-background-commands.ts +56 -0
  139. package/src/hooks/use-session-capabilities.ts +18 -0
  140. package/src/hooks/use-turn-queue.ts +111 -12
  141. package/src/hooks/use-workspace-sessions.ts +5 -1
  142. package/src/index.ts +26 -4
  143. package/src/lib/interaction-errors.ts +18 -0
  144. package/src/machines.ts +9 -1
  145. package/src/model-policy.ts +10 -2
  146. package/src/realtime/realtime-control.tsx +200 -37
  147. package/src/session-context.ts +1 -0
  148. package/src/session-ui.ts +6 -1
  149. package/src/session.ts +9 -1
  150. package/src/timeline/activity-rail.tsx +20 -75
  151. package/src/timeline/index.ts +4 -0
  152. package/src/timeline/platform-activity-row.tsx +201 -0
  153. package/src/timeline/projection.ts +476 -23
  154. package/src/timeline/screenshot-lightbox.tsx +48 -14
  155. package/src/timeline/shared.tsx +8 -2
  156. package/src/timeline/tool-renderers.tsx +104 -6
  157. package/src/timeline/turn-summary.tsx +6 -1
  158. package/src/timeline/types.ts +75 -2
  159. package/src/types/machines.ts +10 -0
  160. package/styles/compiled.css +115 -104
  161. package/dist/chunk-5UQPNLIO.js.map +0 -1
  162. package/dist/chunk-6KLX2RH5.js.map +0 -1
  163. package/dist/chunk-B6WPZSTT.js.map +0 -1
  164. package/dist/chunk-HA46CYQK.js.map +0 -1
  165. package/dist/chunk-IGC6A2ZI.js.map +0 -1
  166. package/dist/chunk-LLOLUHVG.js +0 -5937
  167. package/dist/chunk-LLOLUHVG.js.map +0 -1
  168. package/dist/chunk-MKPIAGVL.js.map +0 -1
  169. package/dist/chunk-R3AZOAB5.js.map +0 -1
  170. package/dist/chunk-XBZWOM2K.js.map +0 -1
  171. package/dist/chunk-Y7OEDUJ5.js.map +0 -1
  172. package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
  173. package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
  174. /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
@@ -55,7 +55,7 @@ import {
55
55
  import { useSandboxTerminal } from "../hooks/use-sandbox-terminal";
56
56
  import { useWorkspaceCapture } from "../hooks/use-workspace-capture";
57
57
  import { useMachineChip, type MachineChip } from "../hooks/use-machine-chip";
58
- import { useMachines } from "../hooks/use-machines";
58
+ import { MACHINES_SESSION_POLL_MS, useMachines } from "../hooks/use-machines";
59
59
  import type { MachineView } from "../types/machines";
60
60
  import { SandboxFiles } from "./sandbox-files";
61
61
  import { WorkbenchChanges } from "./workbench-changes";
@@ -63,13 +63,13 @@ import { SandboxTerminal, type XtermTheme } from "./sandbox-terminal";
63
63
  import { WorkspaceDock, type WorkspaceDockProps, type WorkspaceTab } from "./workspace-dock";
64
64
 
65
65
  const LazyBrowserViewer = lazy(async () => {
66
- const viewers = await import("./interactive-workbench-viewers");
67
- return { default: viewers.BrowserViewer };
66
+ const { BrowserViewer } = await import("./browser-viewer");
67
+ return { default: BrowserViewer };
68
68
  });
69
69
 
70
70
  const LazyComputerViewer = lazy(async () => {
71
- const viewers = await import("./interactive-workbench-viewers");
72
- return { default: viewers.ComputerViewer };
71
+ const { ComputerViewer } = await import("./computer-viewer");
72
+ return { default: ComputerViewer };
73
73
  });
74
74
 
75
75
  /** A host-routed notification (replaces the app-only `sonner` toast coupling). */
@@ -125,18 +125,7 @@ function captureDegradedMessage(reason: string): string {
125
125
  }
126
126
  }
127
127
 
128
- function WorkbenchTabLabel({ icon, children }: { icon: ReactNode; children: ReactNode }) {
129
- return (
130
- <span className="inline-flex items-center gap-1.5">
131
- <span className="[&>svg]:size-3.5" aria-hidden>
132
- {icon}
133
- </span>
134
- <span>{children}</span>
135
- </span>
136
- );
137
- }
138
-
139
- function WorkbenchSurfaceLoading({ name }: { name: "Browser" | "Computer" }) {
128
+ function WorkbenchSurfaceLoading({ name }: { name: "Browser" | "Desktop" }) {
140
129
  return (
141
130
  <CenteredState
142
131
  icon={
@@ -218,10 +207,51 @@ function sandboxProvisionInFlight(events: SessionEvent[]): boolean {
218
207
  return inFlight;
219
208
  }
220
209
 
210
+ /**
211
+ * Whether this session currently owns an admitted agent turn. Automatic live
212
+ * Files/Git reads pause across the whole turn: command boundaries are not a safe
213
+ * gap because the next tool can dispatch before a remote provider read releases
214
+ * its sandbox holder. Existing/captured content remains usable, and the terminal
215
+ * turn event resumes one authoritative refresh.
216
+ */
217
+ function workspaceTurnInFlight(events: SessionEvent[]): boolean {
218
+ const activeTurnIds = new Set<string>();
219
+ let anonymousTurnInFlight = false;
220
+ let sessionOwnsWorkspace = false;
221
+ for (const event of events) {
222
+ if (event.type === "session.status.changed") {
223
+ const status =
224
+ event.payload && typeof event.payload === "object" && !Array.isArray(event.payload)
225
+ ? (event.payload as Record<string, unknown>).status
226
+ : null;
227
+ sessionOwnsWorkspace = status === "running" || status === "recovering";
228
+ continue;
229
+ }
230
+ if (event.type === "turn.started") {
231
+ if (event.turnId) activeTurnIds.add(event.turnId);
232
+ else anonymousTurnInFlight = true;
233
+ continue;
234
+ }
235
+ if (
236
+ event.type !== "turn.completed" &&
237
+ event.type !== "turn.failed" &&
238
+ event.type !== "turn.cancelled" &&
239
+ event.type !== "turn.superseded"
240
+ ) {
241
+ continue;
242
+ }
243
+ if (event.turnId) activeTurnIds.delete(event.turnId);
244
+ else anonymousTurnInFlight = false;
245
+ }
246
+ // The retained event window preserves the latest status even after a very
247
+ // long turn has evicted its original turn.started event.
248
+ return sessionOwnsWorkspace || anonymousTurnInFlight || activeTurnIds.size > 0;
249
+ }
250
+
221
251
  export type WorkspaceMachine = {
222
252
  /** Whether at least one built-in workbench surface is enabled. */
223
253
  enabled: boolean;
224
- /** The derived live/waking/offline chip model. */
254
+ /** The derived live/waking/sleeping-or-offline chip model. */
225
255
  chip: MachineChip;
226
256
  /** The machine these surfaces are bound to (the Modal group box or a
227
257
  * self-hosted machine), or null while the fleet is still resolving. */
@@ -256,14 +286,25 @@ export type UseSandboxWorkspaceTabsOptions = ClientOverride & {
256
286
  browserWebSocketFactory?: BrowserFrameWebSocketFactory | undefined;
257
287
  /** Alternate Computer frame transport for non-browser runtimes and deterministic tests. */
258
288
  computerWebSocketFactory?: ComputerFrameWebSocketFactory | undefined;
289
+ /** Host-owned setup page or store listing for attaching an existing Chrome profile. */
290
+ browserExtensionSetupUrl?: string | undefined;
259
291
  /** File requested by a Changes guard. The Files surface defers the read until
260
292
  * the sandbox is live, then reveals this path. */
261
293
  requestedFilePath?: string | null | undefined;
262
294
  /** Unique identity for `requestedFilePath`, including repeated requests for the
263
295
  * same path. */
264
296
  requestedFileRequestId?: string | number | null | undefined;
297
+ /** 1-based line to reveal after `requestedFilePath` opens. */
298
+ requestedFileLine?: number | null | undefined;
265
299
  /** Route a guarded diff into the host's Files tab. */
266
300
  onOpenFile?: ((path: string) => void) | undefined;
301
+ /** Restore and report navigation inside the built-in workbench surfaces. */
302
+ initialFilePath?: string | null | undefined;
303
+ onFilePathChange?: ((path: string | null) => void) | undefined;
304
+ initialBrowserSessionId?: string | null | undefined;
305
+ onBrowserSessionIdChange?: ((browserSessionId: string | null) => void) | undefined;
306
+ initialComputerSessionId?: string | null | undefined;
307
+ onComputerSessionIdChange?: ((computerSessionId: string | null) => void) | undefined;
267
308
  /** Navigate the host dock to one exact linked ComputerSession. */
268
309
  onOpenComputerSession?: ((computerSessionId: string) => void) | undefined;
269
310
  requestedComputerSessionId?: string | null | undefined;
@@ -274,7 +315,8 @@ export type UseSandboxWorkspaceTabsResult = {
274
315
  /** Changes | Files | Terminal | Browser | Desktop (capability-gated where noted). */
275
316
  tabs: WorkspaceTab[];
276
317
  /** The source-driven default tab: Changes when the first authoritative capture
277
- * or live Git result has changes, else Files (a host `initialTab` overrides).
318
+ * has changes, else Files (a host `initialTab` overrides). Choosing a tab
319
+ * never performs hidden live workspace I/O.
278
320
  * null while that source resolves, or permanently when neither Changes nor
279
321
  * Files is enabled; the choice latches once. */
280
322
  defaultTab: string | null;
@@ -312,9 +354,17 @@ export function useSandboxWorkspaceTabs(
312
354
  onNotify,
313
355
  browserWebSocketFactory,
314
356
  computerWebSocketFactory,
357
+ browserExtensionSetupUrl,
315
358
  requestedFilePath,
316
359
  requestedFileRequestId,
360
+ requestedFileLine,
317
361
  onOpenFile,
362
+ initialFilePath,
363
+ onFilePathChange,
364
+ initialBrowserSessionId,
365
+ onBrowserSessionIdChange,
366
+ initialComputerSessionId,
367
+ onComputerSessionIdChange,
318
368
  onOpenComputerSession,
319
369
  requestedComputerSessionId,
320
370
  requestedComputerRequestId,
@@ -338,7 +388,7 @@ export function useSandboxWorkspaceTabs(
338
388
  ...(placement ? { placement } : {}),
339
389
  });
340
390
  if (response.operation.state !== "completed" || response.session.lifecycle !== "active") {
341
- throw new Error(response.operation.error?.message ?? "The computer could not be opened.");
391
+ throw new Error(response.operation.error?.message ?? "The desktop could not be opened.");
342
392
  }
343
393
  return {
344
394
  id: response.session.id,
@@ -356,6 +406,7 @@ export function useSandboxWorkspaceTabs(
356
406
  const resolvedActiveTab = activeTab ?? initialTab;
357
407
  const changesActive = resolvedActiveTab === WORKBENCH_TAB_CHANGES;
358
408
  const filesActive = resolvedActiveTab === WORKBENCH_TAB_FILES;
409
+ const turnInFlight = workspaceTurnInFlight(events);
359
410
  const surfaceIdentity = WORKBENCH_SURFACES.filter((surface) => surfaceSet.has(surface)).join(",");
360
411
 
361
412
  // The two box-warming INTENTS, each off by default and each
@@ -378,13 +429,16 @@ export function useSandboxWorkspaceTabs(
378
429
  },
379
430
  [sessionId],
380
431
  );
432
+ useEffect(() => {
433
+ if (requestedFilePath) requestWarmIntent("warmFiles");
434
+ }, [requestWarmIntent, requestedFilePath, requestedFileRequestId]);
381
435
 
382
436
  // The session's machine fleet + the active-sandbox pointer. Drives the header
383
- // chip (which machine + its connection state). Polls slowly ambient context.
437
+ // chip (which machine + its connection state). Shares the session list poll.
384
438
  const machines = useMachines({
385
439
  workspaceId,
386
440
  sessionId,
387
- pollIntervalMs: 8000,
441
+ pollIntervalMs: MACHINES_SESSION_POLL_MS,
388
442
  enabled: machineSurfaceEnabled,
389
443
  });
390
444
  const activeMachine: MachineView | null =
@@ -509,7 +563,16 @@ export function useSandboxWorkspaceTabs(
509
563
  // No passive Channel-A reads while cold, even after a conclusive capture
510
564
  // miss. A missing capture gets an explicit "Open live workspace" gate.
511
565
  enabled: filesEnabled && (captureAvailable || (fileSystemOn && liveWorkspaceExpected)),
512
- active: filesActive,
566
+ // A cold sandbox can become warm while the first agent command still owns
567
+ // startup. Do not race that command with an automatic Files read. Keeping
568
+ // this fence at turn scope also avoids unsafe millisecond gaps between
569
+ // sequential tool calls; rendered state and explicit user actions remain.
570
+ active: filesActive && !turnInFlight,
571
+ // A deliberate canonical absolute-path open browses in the selected target's
572
+ // advertised namespace, so the authoritative tree and link share exact paths.
573
+ ...(requestedFilePath?.startsWith("/") && capabilities?.FileSystem.root
574
+ ? { rootPath: capabilities.FileSystem.root }
575
+ : {}),
513
576
  repoPaths,
514
577
  liveness: liveIoLiveness,
515
578
  capture: captureState.capture,
@@ -550,10 +613,11 @@ export function useSandboxWorkspaceTabs(
550
613
  : changesComparison === "branch"
551
614
  ? workspaceDataEnabled && (captureSupportsBranch || (gitOn && liveWorkspaceExpected))
552
615
  : workspaceDataEnabled && gitOn && liveWorkspaceExpected,
553
- // Before the dock chooses its source-driven default, Git is the sole probe
554
- // that may need to resolve that choice on a live workspace. Files remains
555
- // dormant, avoiding the old duplicate full-tree read.
556
- active: changesActive || resolvedActiveTab === null,
616
+ // Live Git belongs to the visible Changes surface. In particular, do not
617
+ // turn an unresolved/default tab into a hidden provider read: that races the
618
+ // agent's own sandbox commands and used to refresh after every tool output
619
+ // even while Files was visibly selected.
620
+ active: changesActive && !turnInFlight,
557
621
  repoPaths,
558
622
  liveness: liveIoLiveness,
559
623
  comparison: changesComparison,
@@ -602,14 +666,12 @@ export function useSandboxWorkspaceTabs(
602
666
  });
603
667
  const workspaceWaking = chip.state === "waking";
604
668
 
605
- // The pre-paint default tab is decided from the first AUTHORITATIVE workspace
606
- // source. A capture wins immediately on the cold/offline fast path; a warm box
607
- // waits for live Git so a prior snapshot can never outrank the current tree.
608
- // When the GET says no capture exists, `fileCount: 0` does NOT itself mean the
609
- // working tree is clean. A captured-revision announce can
610
- // resolve Changes earlier, but a pure embedder with no event preload still gets
611
- // the correct live default. The choice latches once so later edits never steal
612
- // the user's current tab.
669
+ // The pre-paint default comes only from the durable capture. Default selection
670
+ // must never dispatch live provider work: a live Git probe can overlap the
671
+ // agent's first command, and an uncontrolled dock used to keep that probe
672
+ // active forever after it visibly selected Files. A capture with reviewable
673
+ // changes opens Changes; no capture (or an empty one) opens Files. The choice
674
+ // latches once so later edits never steal the user's current tab.
613
675
  const defaultIdentity = `${sessionId}\u0000${surfaceIdentity}`;
614
676
  const defaultTabRef = useRef<{ identity: string; value: string | null }>({
615
677
  identity: defaultIdentity,
@@ -618,62 +680,25 @@ export function useSandboxWorkspaceTabs(
618
680
  if (defaultTabRef.current.identity !== defaultIdentity) {
619
681
  defaultTabRef.current = { identity: defaultIdentity, value: null };
620
682
  }
621
- const captureIsAuthoritative =
622
- !liveWorkspaceExpected && (liveness !== undefined || caps.error !== null);
623
683
  const captureHasChanges =
624
684
  (captureState.fileCount ?? 0) > 0 ||
625
685
  (captureState.capture?.repos.some((repo) => (repo.branchDiff?.length ?? 0) > 0) ?? false);
626
686
  const captureUnavailable =
627
687
  (captureState.fileCount === 0 && !captureHasChanges) || captureState.error !== null;
628
- const implicitBranchFallbackPending =
629
- changesComparison === "branch" &&
630
- git.error !== null &&
631
- storedChangesComparison?.sessionId !== sessionId;
632
688
  if (defaultTabRef.current.value === null) {
633
689
  if (initialTab && (!isWorkbenchSurface(initialTab) || surfaceSet.has(initialTab))) {
634
690
  defaultTabRef.current.value = initialTab;
635
- } else if (git.source === "live") {
636
- defaultTabRef.current.value = sourceDrivenDefaultTab(
637
- git.diff.length > 0,
638
- changesEnabled,
639
- filesEnabled,
640
- );
641
- } else if (
642
- captureIsAuthoritative &&
643
- captureState.fileCount !== null &&
644
- captureState.available
645
- ) {
691
+ } else if (captureState.fileCount !== null && captureState.available) {
646
692
  defaultTabRef.current.value = sourceDrivenDefaultTab(
647
693
  captureHasChanges,
648
694
  changesEnabled,
649
695
  filesEnabled,
650
696
  );
651
- } else if (
652
- captureIsAuthoritative &&
653
- captureUnavailable &&
654
- initialWorkspaceTab(events) === WORKBENCH_TAB_CHANGES
655
- ) {
697
+ } else if (captureUnavailable && initialWorkspaceTab(events) === WORKBENCH_TAB_CHANGES) {
656
698
  defaultTabRef.current.value = sourceDrivenDefaultTab(true, changesEnabled, filesEnabled);
657
- } else if (captureUnavailable && caps.error) {
658
- // The Changes surface owns the truthful sandbox-unavailable state + retry.
659
- // Files would misleadingly describe this as a missing filesystem.
660
- defaultTabRef.current.value = sourceDrivenDefaultTab(true, changesEnabled, filesEnabled);
661
- } else if (captureIsAuthoritative && captureState.fileCount !== null && captureUnavailable) {
662
- // No durable review surface exists and the machine is resting. Files owns
663
- // the explicit wake gate; do not issue a live Git probe merely to choose a
664
- // tab.
665
- defaultTabRef.current.value = sourceDrivenDefaultTab(false, changesEnabled, filesEnabled);
666
- } else if (captureState.available && git.error && captureState.fileCount !== null) {
667
- // A warm live read failed but the durable capture is intact: retain an
668
- // immediate, deterministic review surface instead of hanging unresolved.
669
- defaultTabRef.current.value = sourceDrivenDefaultTab(
670
- captureHasChanges,
671
- changesEnabled,
672
- filesEnabled,
673
- );
674
- } else if (captureUnavailable && git.error && !implicitBranchFallbackPending) {
675
- // No capture and live Git failed: Files is the least surprising fallback
676
- // after the implicit branch-to-working fallback has also had its chance.
699
+ } else if (captureState.fileCount !== null || captureState.error !== null) {
700
+ // No durable review surface exists. Files owns the explicit live-workspace
701
+ // gate; a user can open Changes deliberately if they need a fresh Git read.
677
702
  defaultTabRef.current.value = sourceDrivenDefaultTab(false, changesEnabled, filesEnabled);
678
703
  }
679
704
  }
@@ -687,7 +712,8 @@ export function useSandboxWorkspaceTabs(
687
712
  if (changesEnabled)
688
713
  list.push({
689
714
  id: WORKBENCH_TAB_CHANGES,
690
- label: <WorkbenchTabLabel icon={<GitCompareArrowsIcon />}>Changes</WorkbenchTabLabel>,
715
+ label: "Changes",
716
+ icon: <GitCompareArrowsIcon />,
691
717
  badge: dirtyCount > 0 ? <DirtyBadge count={dirtyCount} /> : undefined,
692
718
  content: (
693
719
  <ChangesTabBody
@@ -719,7 +745,8 @@ export function useSandboxWorkspaceTabs(
719
745
  if (filesEnabled)
720
746
  list.push({
721
747
  id: WORKBENCH_TAB_FILES,
722
- label: <WorkbenchTabLabel icon={<FileCode2Icon />}>Files</WorkbenchTabLabel>,
748
+ label: "Files",
749
+ icon: <FileCode2Icon />,
723
750
  content: (
724
751
  <SandboxFiles
725
752
  key={sessionId}
@@ -746,11 +773,16 @@ export function useSandboxWorkspaceTabs(
746
773
  ? { requestedPathRequestId: requestedFileRequestId }
747
774
  : {}),
748
775
  requestedPathReady: sandboxAcceptsLiveIo(liveness),
776
+ ...(requestedFileLine != null && requestedFileLine > 0
777
+ ? { requestedLine: requestedFileLine }
778
+ : {}),
749
779
  }
750
780
  : {})}
751
781
  // Ordinary capture browsing does not warm a box. The first edit — or an
752
782
  // explicit guarded-file open from Changes — is deliberate live-file intent.
753
783
  onEditIntent={() => requestWarmIntent("warmFiles")}
784
+ initialSelectedPath={initialFilePath}
785
+ onSelectedPathChange={onFilePathChange}
754
786
  className="h-full"
755
787
  />
756
788
  ),
@@ -760,7 +792,8 @@ export function useSandboxWorkspaceTabs(
760
792
  if (terminalOn) {
761
793
  list.push({
762
794
  id: WORKBENCH_TAB_TERMINAL,
763
- label: <WorkbenchTabLabel icon={<SquareTerminalIcon />}>Terminal</WorkbenchTabLabel>,
795
+ label: "Terminal",
796
+ icon: <SquareTerminalIcon />,
764
797
  content: (
765
798
  <div className="h-full bg-og-bg p-1">
766
799
  <SandboxTerminal
@@ -785,7 +818,8 @@ export function useSandboxWorkspaceTabs(
785
818
  if (browserEnabled) {
786
819
  list.push({
787
820
  id: WORKBENCH_TAB_BROWSER,
788
- label: <WorkbenchTabLabel icon={<Globe2Icon />}>Browser</WorkbenchTabLabel>,
821
+ label: "Browser",
822
+ icon: <Globe2Icon />,
789
823
  content: (
790
824
  <Suspense fallback={<WorkbenchSurfaceLoading name="Browser" />}>
791
825
  <LazyBrowserViewer
@@ -803,9 +837,12 @@ export function useSandboxWorkspaceTabs(
803
837
  // user/agent first visits it.
804
838
  enabled={workspaceVisible}
805
839
  onNotify={onNotify}
840
+ initialBrowserSessionId={initialBrowserSessionId}
841
+ onBrowserSessionIdChange={onBrowserSessionIdChange}
806
842
  {...(desktopEnabled ? { createLinkedComputer } : {})}
807
843
  {...(onOpenComputerSession ? { onOpenComputer: onOpenComputerSession } : {})}
808
844
  {...(browserWebSocketFactory ? { webSocketFactory: browserWebSocketFactory } : {})}
845
+ {...(browserExtensionSetupUrl ? { browserExtensionSetupUrl } : {})}
809
846
  className="h-full"
810
847
  />
811
848
  </Suspense>
@@ -818,9 +855,10 @@ export function useSandboxWorkspaceTabs(
818
855
  if (desktopEnabled) {
819
856
  list.push({
820
857
  id: WORKBENCH_TAB_DESKTOP,
821
- label: <WorkbenchTabLabel icon={<MonitorIcon />}>Computer</WorkbenchTabLabel>,
858
+ label: "Desktop",
859
+ icon: <MonitorIcon />,
822
860
  content: (
823
- <Suspense fallback={<WorkbenchSurfaceLoading name="Computer" />}>
861
+ <Suspense fallback={<WorkbenchSurfaceLoading name="Desktop" />}>
824
862
  <LazyComputerViewer
825
863
  key={sessionId}
826
864
  client={client}
@@ -832,6 +870,8 @@ export function useSandboxWorkspaceTabs(
832
870
  // second and later Computer opens local-machine fast.
833
871
  enabled={workspaceVisible}
834
872
  onNotify={onNotify}
873
+ initialComputerSessionId={initialComputerSessionId}
874
+ onComputerSessionIdChange={onComputerSessionIdChange}
835
875
  requestedComputerSessionId={requestedComputerSessionId}
836
876
  requestedComputerRequestId={requestedComputerRequestId}
837
877
  {...(computerWebSocketFactory ? { webSocketFactory: computerWebSocketFactory } : {})}
@@ -856,7 +896,14 @@ export function useSandboxWorkspaceTabs(
856
896
  warmTerminal,
857
897
  requestedFilePath,
858
898
  requestedFileRequestId,
899
+ requestedFileLine,
859
900
  onOpenFile,
901
+ initialFilePath,
902
+ onFilePathChange,
903
+ initialBrowserSessionId,
904
+ onBrowserSessionIdChange,
905
+ initialComputerSessionId,
906
+ onComputerSessionIdChange,
860
907
  onOpenComputerSession,
861
908
  requestedComputerSessionId,
862
909
  requestedComputerRequestId,
@@ -913,14 +960,35 @@ export type SandboxWorkspaceProps = ClientOverride & {
913
960
  /** Host tabs injected AFTER the workbench tabs (e.g. a "Debug" tab). */
914
961
  trailingTabs?: WorkspaceTab[] | undefined;
915
962
  /** Override the pre-paint default tab (e.g. a host landing tab id). When
916
- * omitted the workbench decides from capture, then live Git when no capture exists. */
963
+ * omitted the workbench decides from the durable workspace capture. */
917
964
  initialTab?: string | undefined;
965
+ onActiveTabChange?: ((activeTab: string) => void) | undefined;
966
+ initialFilePath?: string | null | undefined;
967
+ onFilePathChange?: ((path: string | null) => void) | undefined;
968
+ /**
969
+ * Host request to open a workspace file (and optional line) in Files. A new
970
+ * `requestId` re-opens even the same path.
971
+ */
972
+ openFileRequest?:
973
+ | {
974
+ path: string;
975
+ line?: number | null;
976
+ requestId: number;
977
+ }
978
+ | null
979
+ | undefined;
980
+ initialBrowserSessionId?: string | null | undefined;
981
+ onBrowserSessionIdChange?: ((browserSessionId: string | null) => void) | undefined;
982
+ initialComputerSessionId?: string | null | undefined;
983
+ onComputerSessionIdChange?: ((computerSessionId: string | null) => void) | undefined;
918
984
  /** Host-routed notifications (no toast dependency in the package). */
919
985
  onNotify?: ((notification: WorkspaceNotification) => void) | undefined;
920
986
  /** Alternate Browser frame transport for non-browser runtimes and deterministic tests. */
921
987
  browserWebSocketFactory?: BrowserFrameWebSocketFactory | undefined;
922
988
  /** Alternate Computer frame transport for non-browser runtimes and deterministic tests. */
923
989
  computerWebSocketFactory?: ComputerFrameWebSocketFactory | undefined;
990
+ /** Host-owned setup page or store listing for attaching an existing Chrome profile. */
991
+ browserExtensionSetupUrl?: string | undefined;
924
992
  /** Controlled collapsed state for hosts with their own dock toggle. */
925
993
  collapsed?: boolean | undefined;
926
994
  onCollapsedChange?: ((collapsed: boolean) => void) | undefined;
@@ -951,9 +1019,18 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
951
1019
  leadingTabs,
952
1020
  trailingTabs,
953
1021
  initialTab,
1022
+ onActiveTabChange,
1023
+ initialFilePath,
1024
+ onFilePathChange,
1025
+ openFileRequest,
1026
+ initialBrowserSessionId,
1027
+ onBrowserSessionIdChange,
1028
+ initialComputerSessionId,
1029
+ onComputerSessionIdChange,
954
1030
  onNotify,
955
1031
  browserWebSocketFactory,
956
1032
  computerWebSocketFactory,
1033
+ browserExtensionSetupUrl,
957
1034
  collapsed,
958
1035
  onCollapsedChange,
959
1036
  showCollapseControl,
@@ -972,6 +1049,7 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
972
1049
  const [requestedFile, setRequestedFile] = useState<{
973
1050
  sessionId: string;
974
1051
  path: string;
1052
+ line: number | null;
975
1053
  requestId: number;
976
1054
  } | null>(null);
977
1055
  const [requestedComputer, setRequestedComputer] = useState<{
@@ -984,13 +1062,27 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
984
1062
  const selectedTab = storedSelection?.sessionId === sessionId ? storedSelection.tab : null;
985
1063
  const activeTabHint = selectedTab ?? initialTab ?? leadingTabs?.[0]?.id ?? null;
986
1064
  const openFile = useCallback(
987
- (path: string) => {
988
- nextFileRequestId.current += 1;
989
- setRequestedFile({ sessionId, path, requestId: nextFileRequestId.current });
1065
+ (path: string, line?: number | null) => {
1066
+ setRequestedFile({
1067
+ sessionId,
1068
+ path,
1069
+ line: line != null && line > 0 ? line : null,
1070
+ requestId: ++nextFileRequestId.current,
1071
+ });
990
1072
  setStoredSelection({ sessionId, tab: WORKBENCH_TAB_FILES });
1073
+ onActiveTabChange?.(WORKBENCH_TAB_FILES);
1074
+ onFilePathChange?.(path);
1075
+ onCollapsedChange?.(false);
991
1076
  },
992
- [sessionId],
1077
+ [onActiveTabChange, onCollapsedChange, onFilePathChange, sessionId],
993
1078
  );
1079
+ const handledOpenFileRequestId = useRef<number | null>(null);
1080
+ useEffect(() => {
1081
+ if (!openFileRequest) return;
1082
+ if (handledOpenFileRequestId.current === openFileRequest.requestId) return;
1083
+ handledOpenFileRequestId.current = openFileRequest.requestId;
1084
+ openFile(openFileRequest.path, openFileRequest.line);
1085
+ }, [openFile, openFileRequest]);
994
1086
  const openComputer = useCallback(
995
1087
  (computerSessionId: string) => {
996
1088
  nextComputerRequestId.current += 1;
@@ -1000,8 +1092,10 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
1000
1092
  requestId: nextComputerRequestId.current,
1001
1093
  });
1002
1094
  setStoredSelection({ sessionId, tab: WORKBENCH_TAB_DESKTOP });
1095
+ onActiveTabChange?.(WORKBENCH_TAB_DESKTOP);
1096
+ onComputerSessionIdChange?.(computerSessionId);
1003
1097
  },
1004
- [sessionId],
1098
+ [onActiveTabChange, onComputerSessionIdChange, sessionId],
1005
1099
  );
1006
1100
 
1007
1101
  const {
@@ -1020,8 +1114,16 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
1020
1114
  ...(onNotify ? { onNotify } : {}),
1021
1115
  ...(browserWebSocketFactory ? { browserWebSocketFactory } : {}),
1022
1116
  ...(computerWebSocketFactory ? { computerWebSocketFactory } : {}),
1117
+ ...(browserExtensionSetupUrl ? { browserExtensionSetupUrl } : {}),
1118
+ initialFilePath,
1119
+ onFilePathChange,
1120
+ initialBrowserSessionId,
1121
+ onBrowserSessionIdChange,
1122
+ initialComputerSessionId,
1123
+ onComputerSessionIdChange,
1023
1124
  requestedFilePath: requestedFile?.sessionId === sessionId ? requestedFile.path : null,
1024
1125
  requestedFileRequestId: requestedFile?.sessionId === sessionId ? requestedFile.requestId : null,
1126
+ requestedFileLine: requestedFile?.sessionId === sessionId ? requestedFile.line : null,
1025
1127
  onOpenFile: openFile,
1026
1128
  onOpenComputerSession: openComputer,
1027
1129
  requestedComputerSessionId:
@@ -1040,9 +1142,19 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
1040
1142
  const activeTab =
1041
1143
  preferredTab && tabs.some((tab) => tab.id === preferredTab) ? preferredTab : tabs[0]?.id;
1042
1144
  const selectTab = useCallback(
1043
- (tab: string) => setStoredSelection({ sessionId, tab }),
1044
- [sessionId],
1145
+ (tab: string) => {
1146
+ setStoredSelection({ sessionId, tab });
1147
+ onActiveTabChange?.(tab);
1148
+ },
1149
+ [onActiveTabChange, sessionId],
1045
1150
  );
1151
+ useEffect(() => {
1152
+ if (selectedTab !== null || defaultTab === null) return;
1153
+ // Once the source-driven choice resolves, make it the real controlled
1154
+ // selection. Without this handoff the dock could visibly show Files while
1155
+ // the data hooks continued treating the selection as unresolved.
1156
+ setStoredSelection({ sessionId, tab: defaultTab });
1157
+ }, [defaultTab, selectedTab, sessionId]);
1046
1158
 
1047
1159
  return (
1048
1160
  <WorkspaceDock
@@ -1086,7 +1198,7 @@ function chipDotClass(state: MachineChip["state"]): string {
1086
1198
  }
1087
1199
 
1088
1200
  /**
1089
- * The dock-header machine status: one quiet, truthful live/waking/offline
1201
+ * The dock-header machine status: one quiet, truthful live/waking/resting
1090
1202
  * indicator. It is intentionally not interactive; detailed machine controls do
1091
1203
  * not belong in a transient popover above the workspace tabs.
1092
1204
  */