@opengeni/react 0.48.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/README.md +138 -2
  2. package/dist/artifacts-document.d.ts +3 -0
  3. package/dist/artifacts-document.js +17 -0
  4. package/dist/artifacts-document.js.map +1 -0
  5. package/dist/artifacts-presentation.d.ts +3 -0
  6. package/dist/artifacts-presentation.js +17 -0
  7. package/dist/artifacts-presentation.js.map +1 -0
  8. package/dist/artifacts-spreadsheet.d.ts +3 -0
  9. package/dist/artifacts-spreadsheet.js +17 -0
  10. package/dist/artifacts-spreadsheet.js.map +1 -0
  11. package/dist/artifacts.d.ts +2 -0
  12. package/dist/artifacts.js +199 -0
  13. package/dist/artifacts.js.map +1 -0
  14. package/dist/{chunk-NJ7M2B6Z.js → chunk-5UQPNLIO.js} +16 -8
  15. package/dist/{chunk-NJ7M2B6Z.js.map → chunk-5UQPNLIO.js.map} +1 -1
  16. package/dist/{chunk-LHLYWG5R.js → chunk-6KLX2RH5.js} +461 -38
  17. package/dist/chunk-6KLX2RH5.js.map +1 -0
  18. package/dist/chunk-7WCLAEFV.js +2828 -0
  19. package/dist/chunk-7WCLAEFV.js.map +1 -0
  20. package/dist/{chunk-I4MPFDRP.js → chunk-B6WPZSTT.js} +104 -104
  21. package/dist/{chunk-I4MPFDRP.js.map → chunk-B6WPZSTT.js.map} +1 -1
  22. package/dist/{chunk-EKZH6IDG.js → chunk-C5ZPSSS5.js} +111 -1
  23. package/dist/chunk-C5ZPSSS5.js.map +1 -0
  24. package/dist/{chunk-OL6QVXGF.js → chunk-HA46CYQK.js} +152 -8
  25. package/dist/chunk-HA46CYQK.js.map +1 -0
  26. package/dist/chunk-HUIKIDM5.js +2148 -0
  27. package/dist/chunk-HUIKIDM5.js.map +1 -0
  28. package/dist/{chunk-FWXGSNXT.js → chunk-IGC6A2ZI.js} +585 -34
  29. package/dist/chunk-IGC6A2ZI.js.map +1 -0
  30. package/dist/chunk-JVYQRRC4.js +53 -0
  31. package/dist/chunk-JVYQRRC4.js.map +1 -0
  32. package/dist/chunk-KCGXPP5V.js +157 -0
  33. package/dist/chunk-KCGXPP5V.js.map +1 -0
  34. package/dist/chunk-LLOLUHVG.js +5937 -0
  35. package/dist/chunk-LLOLUHVG.js.map +1 -0
  36. package/dist/{chunk-WYA65Y3F.js → chunk-MKPIAGVL.js} +13 -3
  37. package/dist/chunk-MKPIAGVL.js.map +1 -0
  38. package/dist/chunk-PEI6RANI.js +3234 -0
  39. package/dist/chunk-PEI6RANI.js.map +1 -0
  40. package/dist/chunk-R3AZOAB5.js +933 -0
  41. package/dist/chunk-R3AZOAB5.js.map +1 -0
  42. package/dist/{chunk-O4VXOBGF.js → chunk-WSK7G5LE.js} +70 -2
  43. package/dist/chunk-WSK7G5LE.js.map +1 -0
  44. package/dist/{chunk-23EJ676W.js → chunk-XBZWOM2K.js} +9 -4
  45. package/dist/chunk-XBZWOM2K.js.map +1 -0
  46. package/dist/{chunk-XCNTXWBB.js → chunk-Y7OEDUJ5.js} +856 -338
  47. package/dist/chunk-Y7OEDUJ5.js.map +1 -0
  48. package/dist/client.d.ts +10 -2
  49. package/dist/clipboard.d.ts +1 -0
  50. package/dist/clipboard.js +7 -0
  51. package/dist/clipboard.js.map +1 -0
  52. package/dist/commands/index.d.ts +3 -3
  53. package/dist/commands/registry.d.ts +1 -1
  54. package/dist/commands/types.d.ts +1 -1
  55. package/dist/components/approval-surface.d.ts +2 -2
  56. package/dist/components/artifacts/artifact-surface.d.ts +18 -0
  57. package/dist/components/artifacts/document-editor.d.ts +172 -0
  58. package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
  59. package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
  60. package/dist/components/artifacts/editable-document.d.ts +24 -0
  61. package/dist/components/artifacts/editable-presentation.d.ts +24 -0
  62. package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
  63. package/dist/components/artifacts/index.d.ts +9 -0
  64. package/dist/components/artifacts/presentation-editor.d.ts +297 -0
  65. package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
  66. package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
  67. package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
  68. package/dist/components/browser-input.d.ts +7 -0
  69. package/dist/components/browser-viewer.d.ts +30 -0
  70. package/dist/components/chat-composer.d.ts +8 -8
  71. package/dist/components/code-editor.d.ts +25 -3
  72. package/dist/components/command-palette.d.ts +2 -2
  73. package/dist/components/composer-transcription-control.d.ts +2 -2
  74. package/dist/components/composer.d.ts +17 -17
  75. package/dist/components/computer-viewer.d.ts +24 -0
  76. package/dist/components/copy-button.d.ts +2 -2
  77. package/dist/components/desktop-viewer.d.ts +50 -22
  78. package/dist/components/diff-view.d.ts +1 -1
  79. package/dist/components/enrollment-consent.d.ts +1 -1
  80. package/dist/components/enrollment-device-flow.d.ts +1 -1
  81. package/dist/components/file-browser.d.ts +2 -2
  82. package/dist/components/fleet-tile.d.ts +1 -1
  83. package/dist/components/generated-video-player.d.ts +10 -0
  84. package/dist/components/human-input-form.d.ts +1 -1
  85. package/dist/components/human-input-surface.d.ts +2 -2
  86. package/dist/components/interaction-intervention-banner.d.ts +11 -0
  87. package/dist/components/interactive-workbench-viewers.d.ts +2 -0
  88. package/dist/components/machine-card.d.ts +2 -2
  89. package/dist/components/machine-dock-bar.d.ts +3 -3
  90. package/dist/components/machine-health-pill.d.ts +2 -2
  91. package/dist/components/machine-input-display.d.ts +1 -1
  92. package/dist/components/machine-metrics.d.ts +2 -2
  93. package/dist/components/machine-status-pill.d.ts +4 -4
  94. package/dist/components/machines/health.d.ts +1 -1
  95. package/dist/components/machines/machine-detail.d.ts +3 -3
  96. package/dist/components/machines/metric-history-chart.d.ts +1 -1
  97. package/dist/components/machines/metric-sparkline.d.ts +2 -2
  98. package/dist/components/machines/series.d.ts +2 -2
  99. package/dist/components/machines-dashboard.d.ts +2 -2
  100. package/dist/components/markdown.d.ts +1 -1
  101. package/dist/components/message-timeline.d.ts +12 -5
  102. package/dist/components/model-picker.d.ts +2 -2
  103. package/dist/components/model-policy-picker.d.ts +7 -7
  104. package/dist/components/pierre-diff.d.ts +1 -1
  105. package/dist/components/pierre-file.d.ts +1 -1
  106. package/dist/components/queue-draft-policy.d.ts +1 -1
  107. package/dist/components/queue-surface-implementation.d.ts +3 -3
  108. package/dist/components/queue-surface-state.d.ts +4 -4
  109. package/dist/components/queue-surface.d.ts +5 -5
  110. package/dist/components/sandbox-files.d.ts +3 -3
  111. package/dist/components/sandbox-terminal.d.ts +24 -4
  112. package/dist/components/sandbox-workspace.d.ts +28 -9
  113. package/dist/components/session-chrome.d.ts +4 -4
  114. package/dist/components/session-status.d.ts +2 -2
  115. package/dist/components/timeline-annotation-selection.d.ts +9 -0
  116. package/dist/components/timeline-annotations-dialog.d.ts +16 -0
  117. package/dist/components/timeline-annotations.d.ts +13 -0
  118. package/dist/components/tooltip.d.ts +4 -4
  119. package/dist/components/user-message-body.d.ts +2 -2
  120. package/dist/components/workbench-changes.d.ts +2 -2
  121. package/dist/components/workspace-dock.d.ts +1 -1
  122. package/dist/composer.d.ts +11 -11
  123. package/dist/composer.js +7 -7
  124. package/dist/hooks/internal.d.ts +1 -1
  125. package/dist/hooks/use-attached-browsers.d.ts +21 -0
  126. package/dist/hooks/use-auth-runs.d.ts +32 -0
  127. package/dist/hooks/use-available-models.d.ts +2 -2
  128. package/dist/hooks/use-billing-usage.d.ts +1 -1
  129. package/dist/hooks/use-browser-downloads.d.ts +25 -0
  130. package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
  131. package/dist/hooks/use-browser-identities.d.ts +30 -0
  132. package/dist/hooks/use-browser-session.d.ts +29 -0
  133. package/dist/hooks/use-browser-sessions.d.ts +32 -0
  134. package/dist/hooks/use-channels.d.ts +26 -0
  135. package/dist/hooks/use-codex-accounts.d.ts +2 -2
  136. package/dist/hooks/use-composer.d.ts +36 -9
  137. package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
  138. package/dist/hooks/use-computer-session.d.ts +29 -0
  139. package/dist/hooks/use-computer-sessions.d.ts +28 -0
  140. package/dist/hooks/use-desktop-stream.d.ts +5 -1
  141. package/dist/hooks/use-environments.d.ts +1 -1
  142. package/dist/hooks/use-file-attachments.d.ts +1 -1
  143. package/dist/hooks/use-goal.d.ts +2 -2
  144. package/dist/hooks/use-human-input.d.ts +2 -2
  145. package/dist/hooks/use-interaction-interventions.d.ts +27 -0
  146. package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
  147. package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
  148. package/dist/hooks/use-machine-chip.d.ts +2 -2
  149. package/dist/hooks/use-machines.d.ts +4 -4
  150. package/dist/hooks/use-network-routes.d.ts +25 -0
  151. package/dist/hooks/use-packs.d.ts +7 -2
  152. package/dist/hooks/use-rigs.d.ts +1 -1
  153. package/dist/hooks/use-sandbox-files.d.ts +6 -4
  154. package/dist/hooks/use-sandbox-git.d.ts +6 -5
  155. package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
  156. package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
  157. package/dist/hooks/use-session-capabilities.d.ts +5 -4
  158. package/dist/hooks/use-session-control.d.ts +1 -1
  159. package/dist/hooks/use-session-events.d.ts +2 -2
  160. package/dist/hooks/use-session-lineage.d.ts +1 -1
  161. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  162. package/dist/hooks/use-session.d.ts +2 -2
  163. package/dist/hooks/use-site-auth-connections.d.ts +25 -0
  164. package/dist/hooks/use-slash-commands.d.ts +1 -1
  165. package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
  166. package/dist/hooks/use-terminal-stream.d.ts +11 -2
  167. package/dist/hooks/use-turn-queue.d.ts +2 -2
  168. package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
  169. package/dist/hooks/use-voice-input.d.ts +1 -1
  170. package/dist/hooks/use-workspace-capture.d.ts +1 -1
  171. package/dist/hooks/use-workspace-edit.d.ts +1 -1
  172. package/dist/hooks/use-workspace-sessions.d.ts +1 -1
  173. package/dist/hooks/use-workspaces.d.ts +1 -1
  174. package/dist/index.d.ts +162 -155
  175. package/dist/index.js +1822 -1703
  176. package/dist/index.js.map +1 -1
  177. package/dist/interaction.d.ts +33 -0
  178. package/dist/interaction.js +550 -0
  179. package/dist/interaction.js.map +1 -0
  180. package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
  181. package/dist/interactive-workbench-viewers-TOHBXJEE.js.map +1 -0
  182. package/dist/lib/host-platform.d.ts +5 -0
  183. package/dist/lib/relay-wire.d.ts +24 -1
  184. package/dist/lib/sandbox-liveness.d.ts +9 -0
  185. package/dist/lib/terminal-capability.d.ts +5 -0
  186. package/dist/lib/xterm-theme.d.ts +1 -1
  187. package/dist/machines.d.ts +30 -30
  188. package/dist/machines.js +2 -2
  189. package/dist/model-policy.d.ts +1 -1
  190. package/dist/model-policy.js +1 -1
  191. package/dist/provider.d.ts +6 -5
  192. package/dist/{queue-surface-implementation-U5JFG7HT.js → queue-surface-implementation-2KVJTYXH.js} +100 -48
  193. package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
  194. package/dist/realtime/dropdown-menu.d.ts +8 -8
  195. package/dist/realtime/realtime-control.d.ts +8 -8
  196. package/dist/realtime.d.ts +4 -4
  197. package/dist/realtime.js +38 -11
  198. package/dist/realtime.js.map +1 -1
  199. package/dist/session-context.d.ts +28 -2
  200. package/dist/session-ui.d.ts +14 -14
  201. package/dist/session-ui.js +5 -4
  202. package/dist/session.d.ts +28 -28
  203. package/dist/session.js +10 -10
  204. package/dist/timeline/activity-rail.d.ts +5 -3
  205. package/dist/timeline/disclosure-context.d.ts +1 -1
  206. package/dist/timeline/fleet-decision-projection.d.ts +1 -1
  207. package/dist/timeline/fleet-decision-row.d.ts +3 -3
  208. package/dist/timeline/index.d.ts +19 -19
  209. package/dist/timeline/parsers.d.ts +4 -1
  210. package/dist/timeline/projection.d.ts +3 -3
  211. package/dist/timeline/registry.d.ts +8 -2
  212. package/dist/timeline/screenshot-lightbox.d.ts +2 -2
  213. package/dist/timeline/shared.d.ts +21 -10
  214. package/dist/timeline/tool-diff.d.ts +2 -2
  215. package/dist/timeline/tool-renderers.d.ts +1 -1
  216. package/dist/timeline/turn-summary.d.ts +3 -3
  217. package/dist/timeline/types.d.ts +44 -3
  218. package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
  219. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
  220. package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
  221. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
  222. package/package.json +36 -7
  223. package/src/artifacts-document.ts +31 -0
  224. package/src/artifacts-presentation.ts +36 -0
  225. package/src/artifacts-spreadsheet.ts +21 -0
  226. package/src/artifacts.ts +89 -0
  227. package/src/client.ts +157 -1
  228. package/src/clipboard.ts +2 -0
  229. package/src/components/artifacts/artifact-surface.tsx +93 -0
  230. package/src/components/artifacts/document-editor.tsx +2255 -0
  231. package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
  232. package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
  233. package/src/components/artifacts/editable-document.tsx +657 -0
  234. package/src/components/artifacts/editable-presentation.tsx +1020 -0
  235. package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
  236. package/src/components/artifacts/index.ts +104 -0
  237. package/src/components/artifacts/presentation-editor.tsx +3148 -0
  238. package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
  239. package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
  240. package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
  241. package/src/components/browser-input.ts +65 -0
  242. package/src/components/browser-viewer.tsx +2702 -0
  243. package/src/components/chat-composer.tsx +13 -0
  244. package/src/components/code-editor.tsx +89 -30
  245. package/src/components/composer-transcription-control.tsx +10 -6
  246. package/src/components/computer-viewer.tsx +1636 -0
  247. package/src/components/desktop-viewer.tsx +215 -74
  248. package/src/components/generated-video-player.tsx +89 -0
  249. package/src/components/interaction-intervention-banner.tsx +99 -0
  250. package/src/components/interactive-workbench-viewers.ts +2 -0
  251. package/src/components/machine-input-display.ts +3 -0
  252. package/src/components/machines/machine-detail.tsx +1 -1
  253. package/src/components/message-timeline.tsx +289 -32
  254. package/src/components/model-policy-picker.tsx +15 -0
  255. package/src/components/queue-surface-implementation.tsx +116 -60
  256. package/src/components/sandbox-files.tsx +2 -2
  257. package/src/components/sandbox-terminal.tsx +128 -46
  258. package/src/components/sandbox-workspace.tsx +256 -90
  259. package/src/components/session-chrome.tsx +53 -13
  260. package/src/components/timeline-annotation-selection.tsx +221 -0
  261. package/src/components/timeline-annotations-dialog.tsx +213 -0
  262. package/src/components/timeline-annotations.tsx +84 -0
  263. package/src/components/workbench-changes.tsx +188 -31
  264. package/src/components/workspace-dock.tsx +59 -5
  265. package/src/hooks/use-attached-browsers.ts +154 -0
  266. package/src/hooks/use-auth-runs.ts +278 -0
  267. package/src/hooks/use-browser-downloads.ts +218 -0
  268. package/src/hooks/use-browser-frame-stream.ts +425 -0
  269. package/src/hooks/use-browser-identities.ts +266 -0
  270. package/src/hooks/use-browser-session.ts +543 -0
  271. package/src/hooks/use-browser-sessions.ts +286 -0
  272. package/src/hooks/use-channels.ts +99 -0
  273. package/src/hooks/use-composer.ts +769 -24
  274. package/src/hooks/use-computer-frame-stream.ts +434 -0
  275. package/src/hooks/use-computer-session.ts +462 -0
  276. package/src/hooks/use-computer-sessions.ts +251 -0
  277. package/src/hooks/use-desktop-stream.ts +39 -7
  278. package/src/hooks/use-interaction-interventions.ts +242 -0
  279. package/src/hooks/use-interaction-invalidation.ts +66 -0
  280. package/src/hooks/use-machine-chip.ts +15 -3
  281. package/src/hooks/use-machines.ts +33 -3
  282. package/src/hooks/use-network-routes.ts +214 -0
  283. package/src/hooks/use-packs.ts +51 -0
  284. package/src/hooks/use-sandbox-files.ts +98 -55
  285. package/src/hooks/use-sandbox-git.ts +93 -38
  286. package/src/hooks/use-sandbox-terminal.ts +3 -2
  287. package/src/hooks/use-session-capabilities.ts +59 -46
  288. package/src/hooks/use-site-auth-connections.ts +226 -0
  289. package/src/hooks/use-supergrok-accounts.ts +81 -0
  290. package/src/hooks/use-terminal-stream.ts +359 -47
  291. package/src/hooks/use-video-artifact-playback.ts +28 -0
  292. package/src/hooks/use-workspace-edit.ts +2 -1
  293. package/src/index.ts +18 -0
  294. package/src/interaction.ts +101 -0
  295. package/src/lib/host-platform.ts +10 -0
  296. package/src/lib/relay-wire.ts +47 -4
  297. package/src/lib/sandbox-liveness.ts +11 -0
  298. package/src/lib/terminal-capability.ts +10 -0
  299. package/src/model-policy.ts +19 -4
  300. package/src/provider.tsx +133 -1
  301. package/src/realtime/realtime-control.tsx +34 -6
  302. package/src/session-context.ts +156 -1
  303. package/src/timeline/activity-rail.tsx +25 -3
  304. package/src/timeline/index.ts +3 -0
  305. package/src/timeline/parsers.ts +14 -1
  306. package/src/timeline/projection.ts +269 -14
  307. package/src/timeline/registry.ts +13 -1
  308. package/src/timeline/screenshot-lightbox.tsx +19 -9
  309. package/src/timeline/shared.tsx +56 -8
  310. package/src/timeline/tool-renderers.tsx +303 -50
  311. package/src/timeline/types.ts +57 -3
  312. package/styles/compiled.css +312 -21
  313. package/styles/index.css +15 -0
  314. package/styles/responsive.css +13 -1
  315. package/dist/chunk-23EJ676W.js.map +0 -1
  316. package/dist/chunk-EKZH6IDG.js.map +0 -1
  317. package/dist/chunk-FWXGSNXT.js.map +0 -1
  318. package/dist/chunk-LHLYWG5R.js.map +0 -1
  319. package/dist/chunk-O4VXOBGF.js.map +0 -1
  320. package/dist/chunk-OL6QVXGF.js.map +0 -1
  321. package/dist/chunk-WYA65Y3F.js.map +0 -1
  322. package/dist/chunk-XCNTXWBB.js.map +0 -1
  323. package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
  324. package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
  325. package/dist/queue-surface-implementation-U5JFG7HT.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import type { GitFileDiff } from "@opengeni/sdk";
2
2
  import { ArrowUpRightIcon, FileWarningIcon, HistoryIcon } from "lucide-react";
3
3
  import {
4
+ Component,
4
5
  type ReactNode,
5
6
  useCallback,
6
7
  useEffect,
@@ -10,7 +11,7 @@ import {
10
11
  useRef,
11
12
  useState,
12
13
  } from "react";
13
- import { VList } from "virtua";
14
+ import { VList, type VListHandle } from "virtua";
14
15
  import { cn } from "../lib/cn";
15
16
  import { useUnicodeFallbackFonts } from "../lib/use-unicode-fonts";
16
17
  import { useThemeType } from "../lib/use-theme-type";
@@ -96,6 +97,153 @@ type RailRow =
96
97
  | { kind: "group"; label: string; count: number }
97
98
  | { kind: "file"; file: SandboxGitFileDiff; index: number };
98
99
 
100
+ type ChangesRailVListProps = {
101
+ children: ReactNode;
102
+ className: string;
103
+ coarsePointer: boolean;
104
+ files: readonly SandboxGitFileDiff[];
105
+ rowCount: number;
106
+ };
107
+
108
+ type ChangesRailVListState = {
109
+ renderedRowCount: number;
110
+ resetVersion: number;
111
+ };
112
+
113
+ type ChangesRailSnapshot = {
114
+ focusedPath: string | null;
115
+ scrollOffset: number;
116
+ };
117
+
118
+ /** Virtua must reset its cached range when the rail shrinks. Keep that remount
119
+ * scoped to shrink transitions and preserve the reader's focus/scroll context. */
120
+ class ChangesRailVList extends Component<
121
+ ChangesRailVListProps,
122
+ ChangesRailVListState,
123
+ ChangesRailSnapshot | null
124
+ > {
125
+ state: ChangesRailVListState = {
126
+ renderedRowCount: this.props.rowCount,
127
+ resetVersion: 0,
128
+ };
129
+
130
+ private host: HTMLDivElement | null = null;
131
+ private listHandle: VListHandle | null = null;
132
+ private pendingFocusedPath: string | null = null;
133
+ private restoreFrame: number | null = null;
134
+
135
+ static getDerivedStateFromProps(
136
+ props: ChangesRailVListProps,
137
+ state: ChangesRailVListState,
138
+ ): ChangesRailVListState | null {
139
+ if (props.rowCount === state.renderedRowCount) return null;
140
+ return {
141
+ renderedRowCount: props.rowCount,
142
+ resetVersion:
143
+ props.rowCount < state.renderedRowCount ? state.resetVersion + 1 : state.resetVersion,
144
+ };
145
+ }
146
+
147
+ getSnapshotBeforeUpdate(
148
+ _previousProps: ChangesRailVListProps,
149
+ previousState: ChangesRailVListState,
150
+ ): ChangesRailSnapshot | null {
151
+ if (previousState.resetVersion === this.state.resetVersion) return null;
152
+ const scroller = this.host?.firstElementChild;
153
+ if (!(scroller instanceof HTMLElement)) return null;
154
+ const active = document.activeElement;
155
+ const focusedRow =
156
+ active instanceof HTMLElement && scroller.contains(active)
157
+ ? active.closest<HTMLElement>("[data-rail-file-path]")
158
+ : null;
159
+ return {
160
+ focusedPath: focusedRow?.dataset.railFilePath ?? this.pendingFocusedPath,
161
+ scrollOffset: scroller.scrollTop,
162
+ };
163
+ }
164
+
165
+ componentDidUpdate(
166
+ _previousProps: ChangesRailVListProps,
167
+ _previousState: ChangesRailVListState,
168
+ snapshot: ChangesRailSnapshot | null,
169
+ ): void {
170
+ if (!snapshot || !this.listHandle) return;
171
+ this.cancelRestoreFrame();
172
+ const maxOffset = Math.max(0, this.listHandle.scrollSize - this.listHandle.viewportSize);
173
+ this.listHandle.scrollTo(Math.min(Math.max(snapshot.scrollOffset, 0), maxOffset));
174
+ const focusedPath = snapshot.focusedPath;
175
+ if (!focusedPath) return;
176
+ this.pendingFocusedPath = focusedPath;
177
+ this.continueFocusRestore();
178
+ }
179
+
180
+ componentWillUnmount(): void {
181
+ this.cancelRestoreFrame();
182
+ this.pendingFocusedPath = null;
183
+ }
184
+
185
+ private cancelRestoreFrame(): void {
186
+ if (this.restoreFrame === null) return;
187
+ if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(this.restoreFrame);
188
+ this.restoreFrame = null;
189
+ }
190
+
191
+ private continueFocusRestore = (): void => {
192
+ const path = this.pendingFocusedPath;
193
+ if (!path) return;
194
+ if (this.restoreFocusedPath(path)) {
195
+ this.pendingFocusedPath = null;
196
+ return;
197
+ }
198
+ if (this.restoreFrame === null && typeof requestAnimationFrame === "function") {
199
+ this.restoreFrame = requestAnimationFrame(() => {
200
+ this.restoreFrame = null;
201
+ this.continueFocusRestore();
202
+ });
203
+ }
204
+ };
205
+
206
+ private restoreFocusedPath(path: string): boolean {
207
+ const host = this.host;
208
+ if (!host) return true;
209
+ const active = document.activeElement;
210
+ if (active instanceof HTMLElement && active.isConnected && active !== document.body) {
211
+ return true;
212
+ }
213
+ if (!this.props.files.some((file) => file.path === path)) return true;
214
+ const row = Array.from(host.querySelectorAll<HTMLButtonElement>("[data-rail-file-path]")).find(
215
+ (candidate) => candidate.dataset.railFilePath === path,
216
+ );
217
+ if (!row) return false;
218
+ row.focus({ preventScroll: true });
219
+ return true;
220
+ }
221
+
222
+ private setHost = (host: HTMLDivElement | null): void => {
223
+ this.host = host;
224
+ };
225
+
226
+ private setListHandle = (handle: VListHandle | null): void => {
227
+ this.listHandle = handle;
228
+ };
229
+
230
+ render() {
231
+ return (
232
+ <div ref={this.setHost} data-opengeni-changes-rail className={this.props.className}>
233
+ <VList
234
+ key={this.state.resetVersion}
235
+ ref={this.setListHandle}
236
+ className="h-full"
237
+ itemSize={this.props.coarsePointer ? 44 : 28}
238
+ ssrCount={Math.min(30, this.props.rowCount)}
239
+ >
240
+ {this.props.children}
241
+ </VList>
242
+ </div>
243
+ );
244
+ }
245
+ }
246
+
99
247
  /** Order the files and build the rail rows (grouped past the threshold). The
100
248
  * returned `orderedFiles` drives BOTH the rail and the diff pane so a rail row's
101
249
  * `index` addresses the matching pane section. Exported for tests. */
@@ -330,7 +478,13 @@ export function WorkbenchChanges({
330
478
  ))}
331
479
  </select>
332
480
  </div>
333
- <div className="min-h-0 min-w-0 flex-1 overflow-auto" data-opengeni-changes-pane>
481
+ <div
482
+ aria-label="Changed file diff"
483
+ className="min-h-0 min-w-0 flex-1 overflow-auto outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent"
484
+ data-opengeni-changes-pane
485
+ role="region"
486
+ tabIndex={0}
487
+ >
334
488
  {activeFile ? (
335
489
  <DiffSection
336
490
  file={activeFile}
@@ -344,42 +498,44 @@ export function WorkbenchChanges({
344
498
  ) : (
345
499
  <div className="flex min-h-0 flex-1">
346
500
  {/* File rail — virtualized (virtua): a dense change set is fine. */}
347
- <div className="w-[clamp(12rem,28%,15rem)] shrink-0 border-r border-og-border bg-og-surface-1/35">
348
- <VList
349
- className="h-full"
350
- itemSize={coarsePointer ? 44 : 28}
351
- ssrCount={Math.min(30, rows.length)}
352
- >
353
- {rows.map((row) =>
354
- row.kind === "group" ? (
355
- <div
356
- key={`g:${row.label}`}
357
- data-rail-group
358
- className="flex items-center gap-1.5 px-2 pb-0.5 pt-2 text-og-xs font-medium uppercase tracking-wide text-og-fg-subtle"
359
- >
360
- <span className="min-w-0 truncate">{row.label}</span>
361
- <span className="shrink-0">{row.count}</span>
362
- </div>
363
- ) : (
364
- <RailFileRow
365
- key={row.file.path}
366
- file={row.file}
367
- grouped={grouped}
368
- active={row.index === activeIndex}
369
- onClick={() => jumpTo(row.index)}
370
- />
371
- ),
372
- )}
373
- </VList>
374
- </div>
501
+ <ChangesRailVList
502
+ rowCount={rows.length}
503
+ coarsePointer={coarsePointer}
504
+ files={orderedFiles}
505
+ className="w-[clamp(12rem,28%,15rem)] shrink-0 border-r border-og-border bg-og-surface-1/35"
506
+ >
507
+ {rows.map((row) =>
508
+ row.kind === "group" ? (
509
+ <div
510
+ key={`g:${row.label}`}
511
+ data-rail-group
512
+ className="flex items-center gap-1.5 px-2 pb-0.5 pt-2 text-og-xs font-medium uppercase tracking-wide text-og-fg-subtle"
513
+ >
514
+ <span className="min-w-0 truncate">{row.label}</span>
515
+ <span className="shrink-0">{row.count}</span>
516
+ </div>
517
+ ) : (
518
+ <RailFileRow
519
+ key={row.file.path}
520
+ file={row.file}
521
+ grouped={grouped}
522
+ active={row.index === activeIndex}
523
+ onClick={() => jumpTo(row.index)}
524
+ />
525
+ ),
526
+ )}
527
+ </ChangesRailVList>
375
528
 
376
529
  {/* Diff pane — windowed file sections. Only the sections inside the
377
530
  visible window ± overscan are in the DOM; the container reserves the
378
531
  full scroll height so scrolling + the rail-jump stay accurate. */}
379
532
  <div
380
533
  ref={windowed.scrollRef}
381
- className="min-h-0 min-w-0 flex-1 overflow-auto"
534
+ aria-label="Changed files diff"
535
+ className="min-h-0 min-w-0 flex-1 overflow-auto outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent"
382
536
  data-opengeni-changes-pane
537
+ role="region"
538
+ tabIndex={0}
383
539
  >
384
540
  <div style={{ position: "relative", height: windowed.totalHeight }}>
385
541
  {orderedFiles.map((file, index) => {
@@ -479,6 +635,7 @@ function RailFileRow({
479
635
  onClick={onClick}
480
636
  title={file.path}
481
637
  data-rail-file
638
+ data-rail-file-path={file.path}
482
639
  className={cn(
483
640
  "relative flex min-h-7 w-full items-center gap-1.5 truncate px-2 py-0.5 text-left text-og-sm transition-colors hover:bg-og-surface-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent pointer-coarse:min-h-11",
484
641
  grouped && "pl-3",
@@ -536,6 +536,8 @@ function DockChrome({
536
536
  /** Right-aligned chrome controls (maximize / collapse, or the overlay close). */
537
537
  controls: ReactNode;
538
538
  }) {
539
+ const chromeRef = useRef<HTMLDivElement | null>(null);
540
+ const [compact, setCompact] = useState(false);
539
541
  const active = tabs.find((t) => t.id === current) ?? tabs[0];
540
542
  const tabsetId = useId();
541
543
  const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
@@ -544,6 +546,31 @@ function DockChrome({
544
546
  const [tabOverflow, setTabOverflow] = useState({ start: false, end: false });
545
547
  const activeId = active?.id ?? "";
546
548
  const activeTabIndex = tabs.findIndex((tab) => tab.id === activeId);
549
+
550
+ // A desktop dock can be much narrower than the viewport after the user drags
551
+ // its separator. Viewport breakpoints therefore cannot protect the tab strip:
552
+ // the status + controls used to squeeze it to zero width, leaving visible but
553
+ // unclickable ARIA tabs. At narrow *dock* widths, give status/controls their
554
+ // own row and the horizontally scrollable tabs the complete second row.
555
+ useDockLayoutEffect(() => {
556
+ const node = chromeRef.current;
557
+ if (!node) return;
558
+ const update = () => {
559
+ const desktop =
560
+ typeof window.matchMedia !== "function" || window.matchMedia("(min-width: 1024px)").matches;
561
+ const next = desktop && node.getBoundingClientRect().width < 420;
562
+ setCompact((previous) => (previous === next ? previous : next));
563
+ };
564
+ update();
565
+ if (typeof ResizeObserver === "undefined") {
566
+ window.addEventListener("resize", update);
567
+ return () => window.removeEventListener("resize", update);
568
+ }
569
+ const observer = new ResizeObserver(update);
570
+ observer.observe(node);
571
+ return () => observer.disconnect();
572
+ }, []);
573
+
547
574
  const keepActiveTabVisible = useCallback(() => {
548
575
  const selected = activeTabIndex >= 0 ? tabRefs.current[activeTabIndex] : null;
549
576
  selected?.scrollIntoView({ block: "nearest", inline: "nearest" });
@@ -631,8 +658,22 @@ function DockChrome({
631
658
 
632
659
  return (
633
660
  <>
634
- <div className="grid shrink-0 grid-cols-[auto_minmax(0,1fr)_auto_auto] items-center gap-x-2 border-b border-og-border px-1.5 py-1 max-[1023px]:grid-cols-[auto_minmax(0,1fr)_auto] max-[1023px]:gap-y-0 max-[1023px]:px-2 max-[1023px]:pb-1 max-[1023px]:pt-0 min-[640px]:max-[1023px]:grid-cols-[auto_minmax(0,1fr)_auto_auto]">
635
- <div className="flex min-w-0 shrink-0 items-center max-[1023px]:min-h-11 min-[640px]:max-[1023px]:col-start-1 min-[640px]:max-[1023px]:row-start-1">
661
+ <div
662
+ ref={chromeRef}
663
+ data-dock-chrome
664
+ data-compact={compact ? "true" : undefined}
665
+ className={cn(
666
+ "grid shrink-0 grid-cols-[auto_minmax(0,1fr)_auto_auto] items-center gap-x-2 border-b border-og-border px-1.5 py-1 max-[1023px]:grid-cols-[auto_minmax(0,1fr)_auto] max-[1023px]:gap-y-0 max-[1023px]:px-2 max-[1023px]:pb-1 max-[1023px]:pt-0 min-[640px]:max-[1023px]:grid-cols-[auto_minmax(0,1fr)_auto_auto]",
667
+ compact && "gap-y-1",
668
+ )}
669
+ style={compact ? { gridTemplateColumns: "minmax(0, 1fr) auto" } : undefined}
670
+ >
671
+ <div
672
+ className={cn(
673
+ "flex min-w-0 shrink-0 items-center max-[1023px]:min-h-11 min-[640px]:max-[1023px]:col-start-1 min-[640px]:max-[1023px]:row-start-1",
674
+ compact && "hidden",
675
+ )}
676
+ >
636
677
  {leading ?? (
637
678
  <span className="hidden truncate px-1 text-og-sm font-semibold text-og-fg max-[1023px]:inline">
638
679
  Workspace
@@ -648,7 +689,10 @@ function DockChrome({
648
689
  absorbing any unusually long host-injected labels. */}
649
690
  <div
650
691
  ref={tabListRef}
651
- className="flex min-w-0 items-center gap-0.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden max-[1023px]:col-span-3 max-[1023px]:row-start-2 max-[1023px]:w-full min-[640px]:max-[1023px]:col-span-1 min-[640px]:max-[1023px]:col-start-2 min-[640px]:max-[1023px]:row-start-1"
692
+ className={cn(
693
+ "flex min-w-0 items-center gap-0.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden max-[1023px]:col-span-3 max-[1023px]:row-start-2 max-[1023px]:w-full min-[640px]:max-[1023px]:col-span-1 min-[640px]:max-[1023px]:col-start-2 min-[640px]:max-[1023px]:row-start-1",
694
+ compact && "col-span-2 col-start-1 row-start-2 w-full",
695
+ )}
652
696
  role="tablist"
653
697
  aria-orientation="horizontal"
654
698
  style={{
@@ -683,10 +727,20 @@ function DockChrome({
683
727
  </button>
684
728
  ))}
685
729
  </div>
686
- <div className="flex min-w-0 shrink-0 items-center justify-self-end min-[640px]:max-[1023px]:col-start-3 min-[640px]:max-[1023px]:row-start-1">
730
+ <div
731
+ className={cn(
732
+ "flex min-w-0 shrink-0 items-center justify-self-end min-[640px]:max-[1023px]:col-start-3 min-[640px]:max-[1023px]:row-start-1",
733
+ compact && "col-start-1 row-start-1 justify-self-start",
734
+ )}
735
+ >
687
736
  {accessory}
688
737
  </div>
689
- <div className="flex shrink-0 items-center gap-0.5 text-og-fg-subtle min-[640px]:max-[1023px]:col-start-4 min-[640px]:max-[1023px]:row-start-1">
738
+ <div
739
+ className={cn(
740
+ "flex shrink-0 items-center gap-0.5 text-og-fg-subtle min-[640px]:max-[1023px]:col-start-4 min-[640px]:max-[1023px]:row-start-1",
741
+ compact && "col-start-2 row-start-1",
742
+ )}
743
+ >
690
744
  {controls}
691
745
  </div>
692
746
  </div>
@@ -0,0 +1,154 @@
1
+ import type { AttachedBrowserDevice } from "@opengeni/sdk/interaction";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import type { EmbeddedBrowserInteractionClientLike } from "../client";
4
+ import {
5
+ type EmbeddedBrowserInteractionClientOverride,
6
+ useEmbeddedBrowserInteraction,
7
+ } from "../session-context";
8
+ import { useInteractionInvalidation } from "./use-interaction-invalidation";
9
+
10
+ export type UseAttachedBrowsersOptions = EmbeddedBrowserInteractionClientOverride & {
11
+ enabled?: boolean | undefined;
12
+ includeDisconnected?: boolean | undefined;
13
+ pollIntervalMs?: number | undefined;
14
+ };
15
+
16
+ export type UseAttachedBrowsersResult = {
17
+ revision: number;
18
+ devices: AttachedBrowserDevice[];
19
+ loading: boolean;
20
+ refreshing: boolean;
21
+ error: Error | null;
22
+ refresh: () => Promise<void>;
23
+ };
24
+
25
+ /** Workspace-wide inventory of Chrome profiles currently connected through an
26
+ * enrolled OpenGeni machine. BrowserSessions remain the execution authority. */
27
+ export function useAttachedBrowsers(
28
+ options: UseAttachedBrowsersOptions = {},
29
+ ): UseAttachedBrowsersResult {
30
+ const { client, workspaceId, workspaceInteractionEvent, workspaceInteractionConnectionState } =
31
+ useEmbeddedBrowserInteraction(options);
32
+ const enabled = options.enabled ?? true;
33
+ const includeDisconnected = options.includeDisconnected ?? false;
34
+ const [state, setState] = useState(() => emptyState(workspaceId, enabled));
35
+ const requestRef = useRef<{ id: number; controller: AbortController | null }>({
36
+ id: 0,
37
+ controller: null,
38
+ });
39
+ const mountedRef = useRef(true);
40
+ const visible = state.workspaceId === workspaceId ? state : emptyState(workspaceId, enabled);
41
+
42
+ const cancelLoad = useCallback(() => {
43
+ const id = requestRef.current.id + 1;
44
+ requestRef.current.controller?.abort();
45
+ requestRef.current = { id, controller: null };
46
+ }, []);
47
+
48
+ const load = useCallback(
49
+ async (foreground: boolean): Promise<void> => {
50
+ if (!enabled) return;
51
+ const id = requestRef.current.id + 1;
52
+ requestRef.current.controller?.abort();
53
+ const controller = new AbortController();
54
+ requestRef.current = { id, controller };
55
+ setState((current) => ({
56
+ ...(current.workspaceId === workspaceId ? current : emptyState(workspaceId, true)),
57
+ loading: foreground,
58
+ refreshing: !foreground,
59
+ error: foreground || current.workspaceId !== workspaceId ? null : current.error,
60
+ }));
61
+ try {
62
+ const response = await client.listAttachedBrowsers(workspaceId, {
63
+ includeDisconnected,
64
+ signal: controller.signal,
65
+ });
66
+ if (!mountedRef.current || requestRef.current.id !== id) return;
67
+ setState({
68
+ workspaceId,
69
+ revision: response.revision,
70
+ devices: sortDevices(response.devices),
71
+ loading: false,
72
+ refreshing: false,
73
+ error: null,
74
+ });
75
+ } catch (cause) {
76
+ if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
77
+ return;
78
+ setState((current) => ({
79
+ ...(current.workspaceId === workspaceId ? current : emptyState(workspaceId, true)),
80
+ loading: false,
81
+ refreshing: false,
82
+ error: cause instanceof Error ? cause : new Error(String(cause)),
83
+ }));
84
+ } finally {
85
+ if (requestRef.current.id === id) requestRef.current = { id, controller: null };
86
+ }
87
+ },
88
+ [client, enabled, includeDisconnected, workspaceId],
89
+ );
90
+
91
+ const refresh = useCallback(async () => await load(false), [load]);
92
+
93
+ useEffect(() => {
94
+ mountedRef.current = true;
95
+ if (!enabled) {
96
+ cancelLoad();
97
+ setState(emptyState(workspaceId, false));
98
+ return;
99
+ }
100
+ void load(true);
101
+ return () => {
102
+ mountedRef.current = false;
103
+ cancelLoad();
104
+ };
105
+ }, [cancelLoad, enabled, load, workspaceId]);
106
+
107
+ useInteractionInvalidation({
108
+ workspaceId,
109
+ key: `${workspaceId}:${includeDisconnected}`,
110
+ enabled,
111
+ event: workspaceInteractionEvent,
112
+ connectionState: workspaceInteractionConnectionState,
113
+ knownRevision: visible.revision,
114
+ refresh,
115
+ fallbackPollIntervalMs: options.pollIntervalMs,
116
+ });
117
+
118
+ return {
119
+ revision: visible.revision,
120
+ devices: visible.devices,
121
+ loading: visible.loading,
122
+ refreshing: visible.refreshing,
123
+ error: visible.error,
124
+ refresh,
125
+ };
126
+ }
127
+
128
+ function emptyState(workspaceId: string, loading: boolean) {
129
+ return {
130
+ workspaceId,
131
+ revision: 0,
132
+ devices: [] as AttachedBrowserDevice[],
133
+ loading,
134
+ refreshing: false,
135
+ error: null as Error | null,
136
+ };
137
+ }
138
+
139
+ function sortDevices(devices: readonly AttachedBrowserDevice[]): AttachedBrowserDevice[] {
140
+ return [...devices].sort(
141
+ (left, right) =>
142
+ Number(right.state === "connected") - Number(left.state === "connected") ||
143
+ timestamp(right.lastSeenAt) - timestamp(left.lastSeenAt) ||
144
+ left.name.localeCompare(right.name),
145
+ );
146
+ }
147
+
148
+ function timestamp(value: string): number {
149
+ const parsed = Date.parse(value);
150
+ return Number.isFinite(parsed) ? parsed : 0;
151
+ }
152
+
153
+ /** Exported for narrow proxy implementations and tests. */
154
+ export type AttachedBrowsersClient = EmbeddedBrowserInteractionClientLike;