@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
@@ -0,0 +1,442 @@
1
+ import type {
2
+ ComputerAction,
3
+ ComputerActionReceipt,
4
+ ComputerFrame,
5
+ ComputerObservation,
6
+ ComputerSession,
7
+ ComputerTarget,
8
+ } from "@opengeni/sdk/interaction";
9
+ import { useCallback, useEffect, useRef, useState } from "react";
10
+ import {
11
+ type EmbeddedComputerInteractionClientOverride,
12
+ useEmbeddedComputerInteraction,
13
+ } from "../session-context";
14
+ import { usePageLiveActivity } from "./internal";
15
+
16
+ export type UseComputerSessionOptions = EmbeddedComputerInteractionClientOverride & {
17
+ computerSessionId: string | null;
18
+ enabled?: boolean | undefined;
19
+ pollIntervalMs?: number | undefined;
20
+ };
21
+
22
+ export type UseComputerSessionResult = {
23
+ session: ComputerSession | null;
24
+ targets: ComputerTarget[];
25
+ selectedTarget: ComputerTarget | null;
26
+ observation: ComputerObservation | null;
27
+ loading: boolean;
28
+ mutating: boolean;
29
+ error: Error | null;
30
+ refresh: () => Promise<void>;
31
+ /** Changes only this viewer's target cursor; it never takes ownership or
32
+ * foregrounds an application. */
33
+ selectTarget: (targetId: string) => Promise<ComputerTarget>;
34
+ act: (action: ComputerAction, operationId?: string) => Promise<ComputerActionReceipt>;
35
+ /** Dispatch pointer input against the exact displayed frame. */
36
+ actFromFrame: (
37
+ action: Extract<ComputerAction, { type: "pointer" }>,
38
+ frame: ComputerFrame,
39
+ operationId?: string,
40
+ ) => Promise<ComputerActionReceipt>;
41
+ };
42
+
43
+ /** Selected ComputerSession state. Semantic and pixel input share the same
44
+ * generation-fenced controller operation stream used by agents. */
45
+ export function useComputerSession(options: UseComputerSessionOptions): UseComputerSessionResult {
46
+ const { client, workspaceId } = useEmbeddedComputerInteraction(options);
47
+ const computerSessionId = options.computerSessionId;
48
+ const enabled = (options.enabled ?? true) && computerSessionId !== null;
49
+ const pageLive = usePageLiveActivity();
50
+ const pollIntervalMs = Math.max(750, options.pollIntervalMs ?? 2_000);
51
+ const [state, setState] = useState<ComputerControlState>(() =>
52
+ emptyState(computerSessionId, enabled),
53
+ );
54
+ const visible =
55
+ state.computerSessionId === computerSessionId ? state : emptyState(computerSessionId, enabled);
56
+ const selectedTargetIdRef = useRef<string | null>(visible.selectedTargetId);
57
+ selectedTargetIdRef.current = visible.selectedTargetId;
58
+ const targetsRef = useRef<{
59
+ computerSessionId: string | null;
60
+ targets: ComputerTarget[];
61
+ }>({
62
+ computerSessionId,
63
+ targets: visible.targets,
64
+ });
65
+ const observationRef = useRef<{
66
+ computerSessionId: string | null;
67
+ observation: ComputerObservation | null;
68
+ }>({ computerSessionId, observation: visible.observation });
69
+ if (targetsRef.current.computerSessionId !== computerSessionId) {
70
+ targetsRef.current = { computerSessionId, targets: visible.targets };
71
+ observationRef.current = {
72
+ computerSessionId,
73
+ observation: visible.observation,
74
+ };
75
+ } else {
76
+ targetsRef.current.targets = visible.targets;
77
+ observationRef.current.observation = visible.observation;
78
+ }
79
+ const requestRef = useRef<{ id: number; controller: AbortController | null }>({
80
+ id: 0,
81
+ controller: null,
82
+ });
83
+ const mutationRef = useRef<{
84
+ computerSessionId: string | null;
85
+ count: number;
86
+ }>({
87
+ computerSessionId,
88
+ count: 0,
89
+ });
90
+ const mountedRef = useRef(true);
91
+
92
+ const invalidateRefresh = useCallback(() => {
93
+ const id = requestRef.current.id + 1;
94
+ requestRef.current.controller?.abort();
95
+ requestRef.current = { id, controller: null };
96
+ }, []);
97
+
98
+ const refresh = useCallback(async (): Promise<void> => {
99
+ if (!enabled || !computerSessionId) return;
100
+ const id = requestRef.current.id + 1;
101
+ requestRef.current.controller?.abort();
102
+ const controller = new AbortController();
103
+ requestRef.current = { id, controller };
104
+ try {
105
+ const [session, targetResponse] = await Promise.all([
106
+ client.getComputerSession(workspaceId, computerSessionId, {
107
+ signal: controller.signal,
108
+ }),
109
+ client.listComputerTargets(workspaceId, computerSessionId, {
110
+ signal: controller.signal,
111
+ }),
112
+ ]);
113
+ if (!mountedRef.current || requestRef.current.id !== id) return;
114
+ const targets = sortComputerTargets(targetResponse.targets);
115
+ const selected = chooseTarget(targets, selectedTargetIdRef.current);
116
+ const observation = selected
117
+ ? await client.observeComputerTarget(workspaceId, computerSessionId, selected.id, {
118
+ signal: controller.signal,
119
+ })
120
+ : null;
121
+ if (!mountedRef.current || requestRef.current.id !== id) return;
122
+ selectedTargetIdRef.current = selected?.id ?? null;
123
+ targetsRef.current = { computerSessionId, targets };
124
+ observationRef.current = { computerSessionId, observation };
125
+ setState((current) =>
126
+ current.computerSessionId === computerSessionId
127
+ ? {
128
+ ...current,
129
+ session,
130
+ targets,
131
+ selectedTargetId: selected?.id ?? null,
132
+ observation,
133
+ loading: false,
134
+ error: null,
135
+ }
136
+ : current,
137
+ );
138
+ } catch (cause) {
139
+ if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id) return;
140
+ setState((current) =>
141
+ current.computerSessionId === computerSessionId
142
+ ? {
143
+ ...current,
144
+ loading: false,
145
+ error: cause instanceof Error ? cause : new Error(String(cause)),
146
+ }
147
+ : current,
148
+ );
149
+ } finally {
150
+ if (requestRef.current.id === id) requestRef.current = { id, controller: null };
151
+ }
152
+ }, [client, computerSessionId, enabled, workspaceId]);
153
+
154
+ useEffect(() => {
155
+ mountedRef.current = true;
156
+ if (!enabled) {
157
+ invalidateRefresh();
158
+ setState(emptyState(computerSessionId, false));
159
+ return;
160
+ }
161
+ setState((current) =>
162
+ current.computerSessionId === computerSessionId
163
+ ? { ...current, loading: true }
164
+ : emptyState(computerSessionId, true),
165
+ );
166
+ void refresh();
167
+ return () => {
168
+ mountedRef.current = false;
169
+ invalidateRefresh();
170
+ };
171
+ }, [computerSessionId, enabled, invalidateRefresh, refresh]);
172
+
173
+ useEffect(() => {
174
+ if (!enabled || !pageLive || visible.mutating) return;
175
+ const timer = setInterval(() => {
176
+ if (!requestRef.current.controller) void refresh();
177
+ }, pollIntervalMs);
178
+ return () => clearInterval(timer);
179
+ }, [enabled, pageLive, pollIntervalMs, refresh, visible.mutating]);
180
+
181
+ useEffect(() => {
182
+ if (!enabled || !computerSessionId || !pageLive) return;
183
+ let disposed = false;
184
+ const timer = setInterval(() => {
185
+ void client.heartbeatComputerSession(workspaceId, computerSessionId).catch(() => {
186
+ if (!disposed) void refresh();
187
+ });
188
+ }, 30_000);
189
+ return () => {
190
+ disposed = true;
191
+ clearInterval(timer);
192
+ };
193
+ }, [client, computerSessionId, enabled, pageLive, refresh, workspaceId]);
194
+
195
+ const runMutation = useCallback(
196
+ async <T>(scopeComputerSessionId: string, operation: () => Promise<T>): Promise<T> => {
197
+ invalidateRefresh();
198
+ if (mutationRef.current.computerSessionId !== scopeComputerSessionId) {
199
+ mutationRef.current = {
200
+ computerSessionId: scopeComputerSessionId,
201
+ count: 0,
202
+ };
203
+ }
204
+ mutationRef.current.count += 1;
205
+ setState((current) =>
206
+ current.computerSessionId === scopeComputerSessionId
207
+ ? { ...current, mutating: true, error: null }
208
+ : current,
209
+ );
210
+ try {
211
+ return await operation();
212
+ } catch (cause) {
213
+ const error = cause instanceof Error ? cause : new Error(String(cause));
214
+ setState((current) =>
215
+ current.computerSessionId === scopeComputerSessionId ? { ...current, error } : current,
216
+ );
217
+ throw error;
218
+ } finally {
219
+ if (mutationRef.current.computerSessionId === scopeComputerSessionId) {
220
+ mutationRef.current.count = Math.max(0, mutationRef.current.count - 1);
221
+ const mutating = mutationRef.current.count > 0;
222
+ setState((current) =>
223
+ current.computerSessionId === scopeComputerSessionId
224
+ ? { ...current, mutating }
225
+ : current,
226
+ );
227
+ }
228
+ }
229
+ },
230
+ [invalidateRefresh],
231
+ );
232
+
233
+ const selectTarget = useCallback(
234
+ async (targetId: string): Promise<ComputerTarget> => {
235
+ if (!computerSessionId) throw new Error("No ComputerSession is selected.");
236
+ const target = targetsRef.current.targets.find((candidate) => candidate.id === targetId);
237
+ if (!target) throw new Error("The selected computer target is no longer available.");
238
+ invalidateRefresh();
239
+ selectedTargetIdRef.current = target.id;
240
+ observationRef.current = { computerSessionId, observation: null };
241
+ setState((current) =>
242
+ current.computerSessionId === computerSessionId
243
+ ? {
244
+ ...current,
245
+ selectedTargetId: target.id,
246
+ observation: null,
247
+ loading: true,
248
+ error: null,
249
+ }
250
+ : current,
251
+ );
252
+ try {
253
+ const observation = await client.observeComputerTarget(
254
+ workspaceId,
255
+ computerSessionId,
256
+ target.id,
257
+ );
258
+ if (selectedTargetIdRef.current !== target.id) return target;
259
+ observationRef.current = { computerSessionId, observation };
260
+ setState((current) =>
261
+ current.computerSessionId === computerSessionId && current.selectedTargetId === target.id
262
+ ? { ...current, observation, loading: false, error: null }
263
+ : current,
264
+ );
265
+ return observation.target;
266
+ } catch (cause) {
267
+ const error = cause instanceof Error ? cause : new Error(String(cause));
268
+ setState((current) =>
269
+ current.computerSessionId === computerSessionId && current.selectedTargetId === target.id
270
+ ? { ...current, loading: false, error }
271
+ : current,
272
+ );
273
+ throw error;
274
+ }
275
+ },
276
+ [client, computerSessionId, invalidateRefresh, workspaceId],
277
+ );
278
+
279
+ const dispatchAction = useCallback(
280
+ async (
281
+ action: ComputerAction,
282
+ operationId: string,
283
+ frame: ComputerFrame | null,
284
+ ): Promise<ComputerActionReceipt> => {
285
+ if (!computerSessionId) throw new Error("No ComputerSession is selected.");
286
+ if (frame && frame.computerSessionId !== computerSessionId) {
287
+ throw new Error("The displayed computer frame belongs to another ComputerSession.");
288
+ }
289
+ const currentObservation =
290
+ observationRef.current.computerSessionId === computerSessionId
291
+ ? observationRef.current.observation
292
+ : null;
293
+ const focusTarget =
294
+ action.type === "focus"
295
+ ? (targetsRef.current.targets.find((candidate) => candidate.id === action.targetId) ??
296
+ null)
297
+ : null;
298
+ const frameTarget = frame
299
+ ? (targetsRef.current.targets.find((candidate) => candidate.id === frame.targetId) ??
300
+ (currentObservation?.target.id === frame.targetId ? currentObservation.target : null))
301
+ : null;
302
+ const target = frameTarget ?? focusTarget ?? currentObservation?.target ?? null;
303
+ if (!target) throw new Error("The computer target is not ready for input.");
304
+ if (frame && frame.targetId !== selectedTargetIdRef.current) {
305
+ throw new Error("The displayed computer frame is no longer selected.");
306
+ }
307
+ if (action.type === "pointer") {
308
+ const expectedFrameId = frame?.frameId ?? currentObservation?.frameId ?? null;
309
+ if (!expectedFrameId || action.frameId !== expectedFrameId) {
310
+ throw new Error("Pointer input must reference the exact displayed computer frame.");
311
+ }
312
+ }
313
+ if (action.type === "semantic" && !currentObservation) {
314
+ throw new Error("The computer accessibility tree is not ready for input.");
315
+ }
316
+
317
+ return await runMutation(computerSessionId, async () => {
318
+ const receipt = await client.actInComputer(workspaceId, computerSessionId, {
319
+ operationId,
320
+ targetId: target.id,
321
+ expectedTargetGeneration: frame?.targetGeneration ?? target.targetGeneration,
322
+ expectedObservationId:
323
+ action.type === "pointer" ? null : (currentObservation?.observationId ?? null),
324
+ expectedFrameId: action.type === "pointer" ? action.frameId : null,
325
+ action,
326
+ });
327
+ if (receipt.observation) {
328
+ const observation = receipt.observation;
329
+ observationRef.current = { computerSessionId, observation };
330
+ selectedTargetIdRef.current = observation.target.id;
331
+ setState((current) =>
332
+ current.computerSessionId === computerSessionId
333
+ ? {
334
+ ...current,
335
+ observation,
336
+ selectedTargetId: observation.target.id,
337
+ targets: replaceTarget(current.targets, observation.target),
338
+ }
339
+ : current,
340
+ );
341
+ } else {
342
+ void refresh();
343
+ }
344
+ return receipt;
345
+ });
346
+ },
347
+ [client, computerSessionId, refresh, runMutation, workspaceId],
348
+ );
349
+
350
+ const act = useCallback(
351
+ async (
352
+ action: ComputerAction,
353
+ operationId: string = crypto.randomUUID(),
354
+ ): Promise<ComputerActionReceipt> => await dispatchAction(action, operationId, null),
355
+ [dispatchAction],
356
+ );
357
+
358
+ const actFromFrame = useCallback(
359
+ async (
360
+ action: Extract<ComputerAction, { type: "pointer" }>,
361
+ frame: ComputerFrame,
362
+ operationId: string = crypto.randomUUID(),
363
+ ): Promise<ComputerActionReceipt> => await dispatchAction(action, operationId, frame),
364
+ [dispatchAction],
365
+ );
366
+
367
+ return {
368
+ session: visible.session,
369
+ targets: visible.targets,
370
+ selectedTarget:
371
+ visible.targets.find((target) => target.id === visible.selectedTargetId) ?? null,
372
+ observation: visible.observation,
373
+ loading: visible.loading,
374
+ mutating: visible.mutating,
375
+ error: visible.error,
376
+ refresh,
377
+ selectTarget,
378
+ act,
379
+ actFromFrame,
380
+ };
381
+ }
382
+
383
+ type ComputerControlState = {
384
+ computerSessionId: string | null;
385
+ session: ComputerSession | null;
386
+ targets: ComputerTarget[];
387
+ selectedTargetId: string | null;
388
+ observation: ComputerObservation | null;
389
+ loading: boolean;
390
+ mutating: boolean;
391
+ error: Error | null;
392
+ };
393
+
394
+ function emptyState(computerSessionId: string | null, loading: boolean): ComputerControlState {
395
+ return {
396
+ computerSessionId,
397
+ session: null,
398
+ targets: [],
399
+ selectedTargetId: null,
400
+ observation: null,
401
+ loading,
402
+ mutating: false,
403
+ error: null,
404
+ };
405
+ }
406
+
407
+ function chooseTarget(
408
+ targets: readonly ComputerTarget[],
409
+ preferredId: string | null,
410
+ ): ComputerTarget | null {
411
+ return (
412
+ targets.find((target) => target.id === preferredId) ??
413
+ targets.find((target) => target.focused && target.kind === "window") ??
414
+ targets.find((target) => target.kind === "window") ??
415
+ targets.find((target) => target.focused && target.kind === "app") ??
416
+ targets.find((target) => target.kind === "app") ??
417
+ targets.find((target) => target.kind === "screen") ??
418
+ targets[0] ??
419
+ null
420
+ );
421
+ }
422
+
423
+ function sortComputerTargets(targets: readonly ComputerTarget[]): ComputerTarget[] {
424
+ const rank = { window: 0, app: 1, screen: 2 } as const;
425
+ return [...targets].sort(
426
+ (left, right) =>
427
+ Number(right.focused) - Number(left.focused) ||
428
+ rank[left.kind] - rank[right.kind] ||
429
+ left.title.localeCompare(right.title) ||
430
+ left.id.localeCompare(right.id),
431
+ );
432
+ }
433
+
434
+ function replaceTarget(
435
+ targets: readonly ComputerTarget[],
436
+ target: ComputerTarget,
437
+ ): ComputerTarget[] {
438
+ return sortComputerTargets([
439
+ ...targets.filter((candidate) => candidate.id !== target.id),
440
+ target,
441
+ ]);
442
+ }
@@ -0,0 +1,245 @@
1
+ import type {
2
+ ComputerSession,
3
+ ComputerSessionMutationResponse,
4
+ CreateComputerSessionRequest,
5
+ } from "@opengeni/sdk/interaction";
6
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
+ import type { EmbeddedComputerInteractionClientLike } from "../client";
8
+ import {
9
+ type EmbeddedComputerInteractionClientOverride,
10
+ useEmbeddedComputerInteraction,
11
+ } from "../session-context";
12
+ import { usePageLiveActivity } from "./internal";
13
+
14
+ export type UseComputerSessionsOptions = EmbeddedComputerInteractionClientOverride & {
15
+ /** Current agent/session relevance. Workspace peers remain in `sessions`. */
16
+ sessionId?: string | undefined;
17
+ enabled?: boolean | undefined;
18
+ /** Temporary invalidation path until the interaction event stream lands. */
19
+ pollIntervalMs?: number | undefined;
20
+ };
21
+
22
+ export type UseComputerSessionsResult = {
23
+ revision: number;
24
+ sessions: ComputerSession[];
25
+ relevantSessions: ComputerSession[];
26
+ loading: boolean;
27
+ refreshing: boolean;
28
+ error: Error | null;
29
+ refresh: () => Promise<void>;
30
+ create: (
31
+ request: Omit<CreateComputerSessionRequest, "operationId"> & {
32
+ operationId?: string;
33
+ },
34
+ ) => Promise<ComputerSessionMutationResponse>;
35
+ end: (
36
+ computerSessionId: string,
37
+ operationId?: string,
38
+ ) => Promise<ComputerSessionMutationResponse>;
39
+ };
40
+
41
+ /** Workspace-wide ComputerSession registry. Associations rank relevance but
42
+ * never hide peer resources or create an ownership boundary. */
43
+ export function useComputerSessions(
44
+ options: UseComputerSessionsOptions = {},
45
+ ): UseComputerSessionsResult {
46
+ const { client, workspaceId, registerSessionReconciler } =
47
+ useEmbeddedComputerInteraction(options);
48
+ const enabled = options.enabled ?? true;
49
+ const pageLive = usePageLiveActivity();
50
+ const pollIntervalMs = Math.max(1_000, options.pollIntervalMs ?? 3_000);
51
+ const [state, setState] = useState<ComputerRegistryState>(() => emptyState(workspaceId, enabled));
52
+ const requestRef = useRef<{ id: number; controller: AbortController | null }>({
53
+ id: 0,
54
+ controller: null,
55
+ });
56
+ const mountedRef = useRef(true);
57
+ const visible = state.workspaceId === workspaceId ? state : emptyState(workspaceId, enabled);
58
+
59
+ const cancelLoad = useCallback(() => {
60
+ const id = requestRef.current.id + 1;
61
+ requestRef.current.controller?.abort();
62
+ requestRef.current = { id, controller: null };
63
+ }, []);
64
+
65
+ const load = useCallback(
66
+ async (foreground: boolean): Promise<void> => {
67
+ if (!enabled) return;
68
+ const id = requestRef.current.id + 1;
69
+ requestRef.current.controller?.abort();
70
+ const controller = new AbortController();
71
+ requestRef.current = { id, controller };
72
+ setState((current) => ({
73
+ ...(current.workspaceId === workspaceId ? current : emptyState(workspaceId, true)),
74
+ loading: foreground,
75
+ refreshing: !foreground,
76
+ error: foreground || current.workspaceId !== workspaceId ? null : current.error,
77
+ }));
78
+ try {
79
+ const response = await client.listComputerSessions(workspaceId, {
80
+ signal: controller.signal,
81
+ });
82
+ if (!mountedRef.current || requestRef.current.id !== id) return;
83
+ setState({
84
+ workspaceId,
85
+ revision: response.revision,
86
+ sessions: sortComputerSessions(response.sessions),
87
+ loading: false,
88
+ refreshing: false,
89
+ error: null,
90
+ });
91
+ } catch (cause) {
92
+ if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
93
+ return;
94
+ setState((current) => ({
95
+ ...(current.workspaceId === workspaceId ? current : emptyState(workspaceId, true)),
96
+ loading: false,
97
+ refreshing: false,
98
+ error: cause instanceof Error ? cause : new Error(String(cause)),
99
+ }));
100
+ } finally {
101
+ if (requestRef.current.id === id) requestRef.current = { id, controller: null };
102
+ }
103
+ },
104
+ [client, enabled, workspaceId],
105
+ );
106
+
107
+ const refresh = useCallback(async () => await load(false), [load]);
108
+
109
+ useEffect(() => {
110
+ mountedRef.current = true;
111
+ if (!enabled) {
112
+ cancelLoad();
113
+ setState(emptyState(workspaceId, false));
114
+ return;
115
+ }
116
+ void load(true);
117
+ return () => {
118
+ mountedRef.current = false;
119
+ cancelLoad();
120
+ };
121
+ }, [cancelLoad, enabled, load, workspaceId]);
122
+
123
+ useEffect(() => {
124
+ if (!enabled || !pageLive) return;
125
+ const timer = setInterval(() => {
126
+ if (!requestRef.current.controller) void load(false);
127
+ }, pollIntervalMs);
128
+ return () => clearInterval(timer);
129
+ }, [enabled, load, pageLive, pollIntervalMs]);
130
+
131
+ useEffect(() => {
132
+ if (!enabled || !options.sessionId) return;
133
+ return registerSessionReconciler(
134
+ options.sessionId,
135
+ `computer-sessions:${workspaceId}`,
136
+ refresh,
137
+ );
138
+ }, [enabled, options.sessionId, refresh, registerSessionReconciler, workspaceId]);
139
+
140
+ const create = useCallback(
141
+ async (
142
+ request: Omit<CreateComputerSessionRequest, "operationId"> & {
143
+ operationId?: string;
144
+ },
145
+ ): Promise<ComputerSessionMutationResponse> => {
146
+ const response = await client.createComputerSession(workspaceId, {
147
+ ...request,
148
+ operationId: request.operationId ?? crypto.randomUUID(),
149
+ });
150
+ setState((current) => mergeSession(current, workspaceId, response.session));
151
+ return response;
152
+ },
153
+ [client, workspaceId],
154
+ );
155
+
156
+ const end = useCallback(
157
+ async (
158
+ computerSessionId: string,
159
+ operationId: string = crypto.randomUUID(),
160
+ ): Promise<ComputerSessionMutationResponse> => {
161
+ const response = await client.endComputerSession(workspaceId, computerSessionId, {
162
+ operationId,
163
+ });
164
+ setState((current) => mergeSession(current, workspaceId, response.session));
165
+ return response;
166
+ },
167
+ [client, workspaceId],
168
+ );
169
+
170
+ const relevantSessions = useMemo(
171
+ () =>
172
+ options.sessionId
173
+ ? visible.sessions.filter((session) =>
174
+ session.associations.some((association) => association.sessionId === options.sessionId),
175
+ )
176
+ : [],
177
+ [options.sessionId, visible.sessions],
178
+ );
179
+
180
+ return {
181
+ revision: visible.revision,
182
+ sessions: visible.sessions,
183
+ relevantSessions,
184
+ loading: visible.loading,
185
+ refreshing: visible.refreshing,
186
+ error: visible.error,
187
+ refresh,
188
+ create,
189
+ end,
190
+ };
191
+ }
192
+
193
+ type ComputerRegistryState = {
194
+ workspaceId: string;
195
+ revision: number;
196
+ sessions: ComputerSession[];
197
+ loading: boolean;
198
+ refreshing: boolean;
199
+ error: Error | null;
200
+ };
201
+
202
+ function emptyState(workspaceId: string, loading: boolean): ComputerRegistryState {
203
+ return {
204
+ workspaceId,
205
+ revision: 0,
206
+ sessions: [],
207
+ loading,
208
+ refreshing: false,
209
+ error: null,
210
+ };
211
+ }
212
+
213
+ function sortComputerSessions(sessions: readonly ComputerSession[]): ComputerSession[] {
214
+ return [...sessions].sort((left, right) => {
215
+ const live = Number(isLiveComputer(right)) - Number(isLiveComputer(left));
216
+ return (
217
+ live ||
218
+ timestamp(right.lastUsedAt) - timestamp(left.lastUsedAt) ||
219
+ left.id.localeCompare(right.id)
220
+ );
221
+ });
222
+ }
223
+
224
+ function isLiveComputer(session: ComputerSession): boolean {
225
+ return !["ending", "ended", "failed", "lost"].includes(session.lifecycle);
226
+ }
227
+
228
+ function timestamp(value: string): number {
229
+ const parsed = Date.parse(value);
230
+ return Number.isFinite(parsed) ? parsed : 0;
231
+ }
232
+
233
+ function mergeSession(
234
+ current: ComputerRegistryState,
235
+ workspaceId: string,
236
+ session: ComputerSession,
237
+ ): ComputerRegistryState {
238
+ if (current.workspaceId !== workspaceId) return current;
239
+ const sessions = current.sessions.filter((candidate) => candidate.id !== session.id);
240
+ sessions.push(session);
241
+ return { ...current, sessions: sortComputerSessions(sessions), error: null };
242
+ }
243
+
244
+ /** Exported for narrow proxy implementations and tests. */
245
+ export type ComputerSessionsClient = EmbeddedComputerInteractionClientLike;