@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
@@ -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",
@@ -0,0 +1,152 @@
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 { usePageLiveActivity } from "./internal";
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 } = useEmbeddedBrowserInteraction(options);
31
+ const enabled = options.enabled ?? true;
32
+ const includeDisconnected = options.includeDisconnected ?? false;
33
+ const pageLive = usePageLiveActivity();
34
+ const pollIntervalMs = Math.max(1_000, options.pollIntervalMs ?? 3_000);
35
+ const [state, setState] = useState(() => emptyState(workspaceId, enabled));
36
+ const requestRef = useRef<{ id: number; controller: AbortController | null }>({
37
+ id: 0,
38
+ controller: null,
39
+ });
40
+ const mountedRef = useRef(true);
41
+ const visible = state.workspaceId === workspaceId ? state : emptyState(workspaceId, enabled);
42
+
43
+ const cancelLoad = useCallback(() => {
44
+ const id = requestRef.current.id + 1;
45
+ requestRef.current.controller?.abort();
46
+ requestRef.current = { id, controller: null };
47
+ }, []);
48
+
49
+ const load = useCallback(
50
+ async (foreground: boolean): Promise<void> => {
51
+ if (!enabled) return;
52
+ const id = requestRef.current.id + 1;
53
+ requestRef.current.controller?.abort();
54
+ const controller = new AbortController();
55
+ requestRef.current = { id, controller };
56
+ setState((current) => ({
57
+ ...(current.workspaceId === workspaceId ? current : emptyState(workspaceId, true)),
58
+ loading: foreground,
59
+ refreshing: !foreground,
60
+ error: foreground || current.workspaceId !== workspaceId ? null : current.error,
61
+ }));
62
+ try {
63
+ const response = await client.listAttachedBrowsers(workspaceId, {
64
+ includeDisconnected,
65
+ signal: controller.signal,
66
+ });
67
+ if (!mountedRef.current || requestRef.current.id !== id) return;
68
+ setState({
69
+ workspaceId,
70
+ revision: response.revision,
71
+ devices: sortDevices(response.devices),
72
+ loading: false,
73
+ refreshing: false,
74
+ error: null,
75
+ });
76
+ } catch (cause) {
77
+ if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
78
+ return;
79
+ setState((current) => ({
80
+ ...(current.workspaceId === workspaceId ? current : emptyState(workspaceId, true)),
81
+ loading: false,
82
+ refreshing: false,
83
+ error: cause instanceof Error ? cause : new Error(String(cause)),
84
+ }));
85
+ } finally {
86
+ if (requestRef.current.id === id) requestRef.current = { id, controller: null };
87
+ }
88
+ },
89
+ [client, enabled, includeDisconnected, workspaceId],
90
+ );
91
+
92
+ const refresh = useCallback(async () => await load(false), [load]);
93
+
94
+ useEffect(() => {
95
+ mountedRef.current = true;
96
+ if (!enabled) {
97
+ cancelLoad();
98
+ setState(emptyState(workspaceId, false));
99
+ return;
100
+ }
101
+ void load(true);
102
+ return () => {
103
+ mountedRef.current = false;
104
+ cancelLoad();
105
+ };
106
+ }, [cancelLoad, enabled, load, workspaceId]);
107
+
108
+ useEffect(() => {
109
+ if (!enabled || !pageLive) return;
110
+ const timer = setInterval(() => {
111
+ if (!requestRef.current.controller) void load(false);
112
+ }, pollIntervalMs);
113
+ return () => clearInterval(timer);
114
+ }, [enabled, load, pageLive, pollIntervalMs]);
115
+
116
+ return {
117
+ revision: visible.revision,
118
+ devices: visible.devices,
119
+ loading: visible.loading,
120
+ refreshing: visible.refreshing,
121
+ error: visible.error,
122
+ refresh,
123
+ };
124
+ }
125
+
126
+ function emptyState(workspaceId: string, loading: boolean) {
127
+ return {
128
+ workspaceId,
129
+ revision: 0,
130
+ devices: [] as AttachedBrowserDevice[],
131
+ loading,
132
+ refreshing: false,
133
+ error: null as Error | null,
134
+ };
135
+ }
136
+
137
+ function sortDevices(devices: readonly AttachedBrowserDevice[]): AttachedBrowserDevice[] {
138
+ return [...devices].sort(
139
+ (left, right) =>
140
+ Number(right.state === "connected") - Number(left.state === "connected") ||
141
+ timestamp(right.lastSeenAt) - timestamp(left.lastSeenAt) ||
142
+ left.name.localeCompare(right.name),
143
+ );
144
+ }
145
+
146
+ function timestamp(value: string): number {
147
+ const parsed = Date.parse(value);
148
+ return Number.isFinite(parsed) ? parsed : 0;
149
+ }
150
+
151
+ /** Exported for narrow proxy implementations and tests. */
152
+ export type AttachedBrowsersClient = EmbeddedBrowserInteractionClientLike;