@opengeni/react 1.4.2 → 1.4.5-canary.37458084490001

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 (240) hide show
  1. package/README.md +17 -6
  2. package/dist/artifacts.d.ts +1 -0
  3. package/dist/artifacts.js +22 -15
  4. package/dist/artifacts.js.map +1 -1
  5. package/dist/{browser-viewer-WGJ43YGI.js → browser-viewer-CV23VIHB.js} +8 -6
  6. package/dist/chunk-3ZC6QPOM.js +174 -0
  7. package/dist/chunk-3ZC6QPOM.js.map +1 -0
  8. package/dist/{chunk-YVFRBXST.js → chunk-4BCVCJSU.js} +2 -2
  9. package/dist/{chunk-JHTIMBOL.js → chunk-5JSUNNQN.js} +26 -4
  10. package/dist/{chunk-JHTIMBOL.js.map → chunk-5JSUNNQN.js.map} +1 -1
  11. package/dist/{chunk-MDIXFKA3.js → chunk-6FOTSVWU.js} +303 -109
  12. package/dist/chunk-6FOTSVWU.js.map +1 -0
  13. package/dist/{chunk-IUHHPMAX.js → chunk-7RRBX5EM.js} +14 -85
  14. package/dist/chunk-7RRBX5EM.js.map +1 -0
  15. package/dist/{chunk-L2J3PU7A.js → chunk-7ZB7I3KP.js} +24 -98
  16. package/dist/chunk-7ZB7I3KP.js.map +1 -0
  17. package/dist/{chunk-LVS3MH5T.js → chunk-ATIRUHBZ.js} +128 -10
  18. package/dist/chunk-ATIRUHBZ.js.map +1 -0
  19. package/dist/chunk-BOFEQC6K.js +162 -0
  20. package/dist/chunk-BOFEQC6K.js.map +1 -0
  21. package/dist/{chunk-VCSERYXH.js → chunk-CESBPQP5.js} +309 -162
  22. package/dist/chunk-CESBPQP5.js.map +1 -0
  23. package/dist/{chunk-IYQCWGU5.js → chunk-GHJU7XG7.js} +35 -62
  24. package/dist/chunk-GHJU7XG7.js.map +1 -0
  25. package/dist/{chunk-G6YRCIYJ.js → chunk-HNVWMDNN.js} +55 -60
  26. package/dist/chunk-HNVWMDNN.js.map +1 -0
  27. package/dist/{chunk-PHBFJCEU.js → chunk-I5EGRXXG.js} +110 -7
  28. package/dist/chunk-I5EGRXXG.js.map +1 -0
  29. package/dist/{chunk-PBCZL4JP.js → chunk-INIZSNXT.js} +2 -2
  30. package/dist/{chunk-XS67YG5R.js → chunk-IX3K4THH.js} +24 -3
  31. package/dist/chunk-IX3K4THH.js.map +1 -0
  32. package/dist/chunk-IZV5FTJC.js +55 -0
  33. package/dist/chunk-IZV5FTJC.js.map +1 -0
  34. package/dist/chunk-JDJJMBB3.js +1883 -0
  35. package/dist/chunk-JDJJMBB3.js.map +1 -0
  36. package/dist/{chunk-C5BF4L26.js → chunk-KHF2VRXF.js} +7 -7
  37. package/dist/{chunk-QMMCJGFI.js → chunk-L5OKM2VX.js} +2 -2
  38. package/dist/chunk-LHF6BVM4.js +83 -0
  39. package/dist/chunk-LHF6BVM4.js.map +1 -0
  40. package/dist/chunk-N7S72EXK.js +25 -0
  41. package/dist/chunk-N7S72EXK.js.map +1 -0
  42. package/dist/{chunk-CVYCLFMH.js → chunk-PEHTX3V7.js} +19 -1
  43. package/dist/chunk-PEHTX3V7.js.map +1 -0
  44. package/dist/chunk-R5R5RQQD.js +51 -0
  45. package/dist/chunk-R5R5RQQD.js.map +1 -0
  46. package/dist/chunk-RI42H5IU.js +326 -0
  47. package/dist/chunk-RI42H5IU.js.map +1 -0
  48. package/dist/{chunk-JH7V627O.js → chunk-SHLZMAJN.js} +2 -11
  49. package/dist/chunk-SHLZMAJN.js.map +1 -0
  50. package/dist/{chunk-CREMJMVN.js → chunk-TZQSHCZN.js} +692 -1996
  51. package/dist/chunk-TZQSHCZN.js.map +1 -0
  52. package/dist/chunk-ZIB4H5LN.js +101 -0
  53. package/dist/chunk-ZIB4H5LN.js.map +1 -0
  54. package/dist/client.d.ts +9 -3
  55. package/dist/clipboard.js +1 -1
  56. package/dist/components/browser-viewer.d.ts +6 -0
  57. package/dist/components/composer.d.ts +3 -69
  58. package/dist/components/human-input-form.d.ts +3 -32
  59. package/dist/components/markdown.d.ts +8 -0
  60. package/dist/components/model-policy-picker.d.ts +4 -37
  61. package/dist/components/opper-mark.d.ts +7 -0
  62. package/dist/components/provider-recovery-notice.d.ts +15 -0
  63. package/dist/components/remark-soft-line-breaks.d.ts +38 -0
  64. package/dist/components/sandbox-workspace.d.ts +13 -0
  65. package/dist/components/session-chrome.d.ts +5 -3
  66. package/dist/components/session-conversation.d.ts +15 -3
  67. package/dist/composer-messages.d.ts +74 -0
  68. package/dist/composer-messages.js +9 -0
  69. package/dist/composer.js +15 -10
  70. package/dist/{computer-viewer-46UIN7F3.js → computer-viewer-K4CKYRX6.js} +4 -4
  71. package/dist/connect-setup.d.ts +2 -0
  72. package/dist/connect.d.ts +1 -1
  73. package/dist/connect.js +21 -6
  74. package/dist/connect.js.map +1 -1
  75. package/dist/{embedded-voice-NIL4RDX3.js → embedded-voice-3ZFWWJ6O.js} +6 -4
  76. package/dist/{embedded-voice-NIL4RDX3.js.map → embedded-voice-3ZFWWJ6O.js.map} +1 -1
  77. package/dist/hooks/shared-machines-list.d.ts +4 -0
  78. package/dist/hooks/use-client-config-flags.d.ts +6 -0
  79. package/dist/hooks/use-computer-session.d.ts +2 -0
  80. package/dist/hooks/use-goal.d.ts +2 -1
  81. package/dist/human-input-model.d.ts +40 -0
  82. package/dist/index.d.ts +4 -1
  83. package/dist/index.js +116 -73
  84. package/dist/index.js.map +1 -1
  85. package/dist/interaction.js +5 -5
  86. package/dist/{latest-question-2POFPYNB.js → latest-question-SWC6QEVN.js} +3 -3
  87. package/dist/lib/clipboard.d.ts +7 -0
  88. package/dist/lib/provider-recovery.d.ts +49 -0
  89. package/dist/machines.js +2 -2
  90. package/dist/model-mark-paths.d.ts +34 -0
  91. package/dist/model-mark-paths.js +16 -0
  92. package/dist/model-picker-model.d.ts +111 -0
  93. package/dist/model-picker-model.js +25 -0
  94. package/dist/model-picker-model.js.map +1 -0
  95. package/dist/{model-policy-picker-menu-WRNSRFQC.js → model-policy-picker-menu-NM4RPTY6.js} +6 -4
  96. package/dist/model-policy-picker-menu-NM4RPTY6.js.map +1 -0
  97. package/dist/model-policy.d.ts +10 -0
  98. package/dist/model-policy.js +5 -1
  99. package/dist/organization-model.d.ts +32 -0
  100. package/dist/organization-model.js +65 -0
  101. package/dist/organization-model.js.map +1 -0
  102. package/dist/{platform-activity-row-PJJVATTZ.js → platform-activity-row-NJ47YBXW.js} +7 -84
  103. package/dist/platform-activity-row-NJ47YBXW.js.map +1 -0
  104. package/dist/queue-presentation-model.d.ts +72 -0
  105. package/dist/queue-presentation-model.js +38 -0
  106. package/dist/queue-presentation-model.js.map +1 -0
  107. package/dist/{queue-surface-implementation-67FS7FUI.js → queue-surface-implementation-YPJFQ3O6.js} +24 -252
  108. package/dist/queue-surface-implementation-YPJFQ3O6.js.map +1 -0
  109. package/dist/realtime.js +5 -3
  110. package/dist/{sandbox-files-34CJJNOY.js → sandbox-files-RCVCMXG7.js} +5 -5
  111. package/dist/sandbox-files-RCVCMXG7.js.map +1 -0
  112. package/dist/sandbox-label-model.d.ts +23 -0
  113. package/dist/sandbox-label-model.js +28 -0
  114. package/dist/sandbox-label-model.js.map +1 -0
  115. package/dist/session-attention-model.d.ts +8 -0
  116. package/dist/session-attention-model.js +46 -0
  117. package/dist/session-attention-model.js.map +1 -0
  118. package/dist/session-feedback-model.d.ts +20 -0
  119. package/dist/session-feedback-model.js +56 -0
  120. package/dist/session-feedback-model.js.map +1 -0
  121. package/dist/session-list-model.d.ts +96 -0
  122. package/dist/session-list-model.js +219 -0
  123. package/dist/session-list-model.js.map +1 -0
  124. package/dist/session-status-model.d.ts +19 -0
  125. package/dist/session-ui.js +26 -17
  126. package/dist/session-ui.js.map +1 -1
  127. package/dist/session.js +8 -6
  128. package/dist/testing/session-scenarios.d.ts +10 -0
  129. package/dist/testing/session-scenarios.js +303 -0
  130. package/dist/testing/session-scenarios.js.map +1 -0
  131. package/dist/timeline/genie-copy.d.ts +3 -0
  132. package/dist/timeline/genie-loading.d.ts +11 -0
  133. package/dist/timeline/index.d.ts +1 -1
  134. package/dist/timeline/notice-presentation.d.ts +7 -0
  135. package/dist/timeline/patch-apply-command.d.ts +18 -0
  136. package/dist/timeline/platform-activity-presentation.d.ts +7 -0
  137. package/dist/timeline/presented-image.d.ts +6 -1
  138. package/dist/timeline/question-nav-model.d.ts +23 -0
  139. package/dist/timeline/registry.d.ts +10 -1
  140. package/dist/timeline/retained-loaders.d.ts +1 -1
  141. package/dist/timeline/tool-presentation.d.ts +172 -0
  142. package/dist/timeline/turn-summary-model.d.ts +90 -0
  143. package/dist/timeline/turn-summary.d.ts +4 -74
  144. package/dist/timeline/work-presentation.d.ts +42 -0
  145. package/dist/timeline-model.d.ts +24 -0
  146. package/dist/timeline-model.js +152 -0
  147. package/dist/timeline-model.js.map +1 -0
  148. package/package.json +47 -3
  149. package/src/artifacts.ts +1 -0
  150. package/src/client.ts +9 -3
  151. package/src/components/browser-viewer.tsx +23 -2
  152. package/src/components/claude-mark.tsx +2 -1
  153. package/src/components/composer.tsx +4 -144
  154. package/src/components/computer-viewer.tsx +139 -30
  155. package/src/components/grok-mark.tsx +2 -1
  156. package/src/components/human-input-form.tsx +13 -124
  157. package/src/components/markdown.tsx +19 -1
  158. package/src/components/message-timeline.tsx +45 -99
  159. package/src/components/model-mark.tsx +2 -1
  160. package/src/components/model-policy-picker-menu.tsx +30 -85
  161. package/src/components/model-policy-picker.tsx +15 -148
  162. package/src/components/opper-mark.tsx +18 -0
  163. package/src/components/provider-recovery-notice.tsx +41 -0
  164. package/src/components/queue-surface-implementation.tsx +20 -349
  165. package/src/components/remark-soft-line-breaks.ts +166 -0
  166. package/src/components/sandbox-workspace.tsx +17 -1
  167. package/src/components/session-chrome.tsx +114 -99
  168. package/src/components/session-conversation.tsx +55 -6
  169. package/src/components/session-list.tsx +1 -1
  170. package/src/components/session-status.tsx +17 -16
  171. package/src/composer-messages.ts +150 -0
  172. package/src/connect-setup.tsx +45 -11
  173. package/src/connect.ts +1 -1
  174. package/src/hooks/shared-machines-list.ts +34 -1
  175. package/src/hooks/use-browser-session.ts +445 -168
  176. package/src/hooks/use-client-config-flags.ts +8 -0
  177. package/src/hooks/use-computer-session.ts +334 -98
  178. package/src/hooks/use-goal.ts +66 -10
  179. package/src/human-input-model.ts +147 -0
  180. package/src/index.ts +16 -0
  181. package/src/lib/clipboard.ts +29 -0
  182. package/src/lib/format.ts +7 -0
  183. package/src/lib/provider-recovery.ts +202 -0
  184. package/src/model-mark-paths.ts +53 -0
  185. package/src/model-picker-model.ts +330 -0
  186. package/src/model-policy.ts +43 -2
  187. package/src/organization-model.ts +118 -0
  188. package/src/queue-presentation-model.ts +464 -0
  189. package/src/sandbox-label-model.ts +41 -0
  190. package/src/session-attention-model.ts +59 -0
  191. package/src/session-feedback-model.ts +95 -0
  192. package/src/session-list-model.ts +327 -0
  193. package/src/session-status-model.ts +62 -0
  194. package/src/testing/session-scenarios.ts +326 -0
  195. package/src/timeline/activity-rail.tsx +5 -17
  196. package/src/timeline/genie-copy.ts +19 -0
  197. package/src/timeline/genie-loading.tsx +13 -18
  198. package/src/timeline/index.ts +1 -0
  199. package/src/timeline/notice-presentation.ts +22 -0
  200. package/src/timeline/patch-apply-command.tsx +135 -0
  201. package/src/timeline/platform-activity-presentation.ts +122 -0
  202. package/src/timeline/platform-activity-row.tsx +6 -96
  203. package/src/timeline/presented-image.ts +16 -2
  204. package/src/timeline/question-nav-model.ts +28 -0
  205. package/src/timeline/registry.ts +11 -1
  206. package/src/timeline/retained-loaders.ts +9 -4
  207. package/src/timeline/rolling-activity.tsx +7 -19
  208. package/src/timeline/tool-presentation.ts +1316 -0
  209. package/src/timeline/tool-renderers.tsx +226 -1025
  210. package/src/timeline/turn-summary-model.ts +338 -0
  211. package/src/timeline/turn-summary.tsx +21 -326
  212. package/src/timeline/work-presentation.ts +139 -0
  213. package/src/timeline-model.ts +108 -0
  214. package/styles/compiled.css +8 -0
  215. package/styles/connect.css +4 -0
  216. package/dist/chunk-CREMJMVN.js.map +0 -1
  217. package/dist/chunk-CVYCLFMH.js.map +0 -1
  218. package/dist/chunk-G6YRCIYJ.js.map +0 -1
  219. package/dist/chunk-HXYOSSQO.js +0 -117
  220. package/dist/chunk-HXYOSSQO.js.map +0 -1
  221. package/dist/chunk-IUHHPMAX.js.map +0 -1
  222. package/dist/chunk-IYQCWGU5.js.map +0 -1
  223. package/dist/chunk-JH7V627O.js.map +0 -1
  224. package/dist/chunk-L2J3PU7A.js.map +0 -1
  225. package/dist/chunk-LVS3MH5T.js.map +0 -1
  226. package/dist/chunk-MDIXFKA3.js.map +0 -1
  227. package/dist/chunk-PHBFJCEU.js.map +0 -1
  228. package/dist/chunk-VCSERYXH.js.map +0 -1
  229. package/dist/chunk-XS67YG5R.js.map +0 -1
  230. package/dist/platform-activity-row-PJJVATTZ.js.map +0 -1
  231. package/dist/queue-surface-implementation-67FS7FUI.js.map +0 -1
  232. /package/dist/{browser-viewer-WGJ43YGI.js.map → browser-viewer-CV23VIHB.js.map} +0 -0
  233. /package/dist/{chunk-YVFRBXST.js.map → chunk-4BCVCJSU.js.map} +0 -0
  234. /package/dist/{chunk-PBCZL4JP.js.map → chunk-INIZSNXT.js.map} +0 -0
  235. /package/dist/{chunk-C5BF4L26.js.map → chunk-KHF2VRXF.js.map} +0 -0
  236. /package/dist/{chunk-QMMCJGFI.js.map → chunk-L5OKM2VX.js.map} +0 -0
  237. /package/dist/{computer-viewer-46UIN7F3.js.map → composer-messages.js.map} +0 -0
  238. /package/dist/{model-policy-picker-menu-WRNSRFQC.js.map → computer-viewer-K4CKYRX6.js.map} +0 -0
  239. /package/dist/{latest-question-2POFPYNB.js.map → latest-question-SWC6QEVN.js.map} +0 -0
  240. /package/dist/{sandbox-files-34CJJNOY.js.map → model-mark-paths.js.map} +0 -0
@@ -18,6 +18,19 @@ import {
18
18
  } from "../session-context";
19
19
  import { usePageLiveActivity } from "./internal";
20
20
 
21
+ // Keep caller promises intact while fencing their later viewer side effects.
22
+ // These local fences never become part of an operation receipt on the wire.
23
+ const actionResultFences = new WeakMap<object, () => boolean>();
24
+ export function isStaleComputerActionResult(value: unknown): boolean {
25
+ return typeof value === "object" && value !== null && actionResultFences.get(value)?.() === false;
26
+ }
27
+ export function computerActionReceiptError(receipt: ComputerActionReceipt): Error {
28
+ const error = new Error(receipt.error?.message ?? "Desktop input did not complete.");
29
+ const fence = actionResultFences.get(receipt);
30
+ if (fence) actionResultFences.set(error, fence);
31
+ return error;
32
+ }
33
+
21
34
  export type UseComputerSessionOptions = EmbeddedComputerInteractionClientOverride & {
22
35
  computerSessionId: string | null;
23
36
  enabled?: boolean | undefined;
@@ -54,30 +67,48 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
54
67
  const { client, workspaceId } = useEmbeddedComputerInteraction(options);
55
68
  const computerSessionId = options.computerSessionId;
56
69
  const enabled = (options.enabled ?? true) && computerSessionId !== null;
70
+ const sourceRef = useRef<ComputerControlSource>({
71
+ client,
72
+ workspaceId,
73
+ computerSessionId,
74
+ enabled,
75
+ });
76
+ if (
77
+ sourceRef.current.client !== client ||
78
+ sourceRef.current.workspaceId !== workspaceId ||
79
+ sourceRef.current.computerSessionId !== computerSessionId ||
80
+ sourceRef.current.enabled !== enabled
81
+ ) {
82
+ sourceRef.current = { client, workspaceId, computerSessionId, enabled };
83
+ }
84
+ const source = sourceRef.current;
57
85
  const pageLive = usePageLiveActivity();
58
86
  const pollIntervalMs = Math.max(750, options.pollIntervalMs ?? 2_000);
59
- const [state, setState] = useState<ComputerControlState>(() =>
60
- emptyState(computerSessionId, enabled),
61
- );
62
- const visible =
63
- state.computerSessionId === computerSessionId ? state : emptyState(computerSessionId, enabled);
87
+ const [state, setState] = useState<ComputerControlState>(() => emptyState(source, enabled));
88
+ const visible = state.source === source ? state : emptyState(source, enabled);
64
89
  const refreshBlocked = isNonRetryableInteractionError(visible.error);
65
90
  const selectedTargetIdRef = useRef<string | null>(visible.selectedTargetId);
66
91
  selectedTargetIdRef.current = visible.selectedTargetId;
92
+ const selectionRevisionRef = useRef(0);
93
+ const actionResultOrderRef = useRef({ next: 0, settled: 0 });
67
94
  const targetsRef = useRef<{
95
+ source: ComputerControlSource;
68
96
  computerSessionId: string | null;
69
97
  targets: ComputerTarget[];
70
98
  }>({
99
+ source,
71
100
  computerSessionId,
72
101
  targets: visible.targets,
73
102
  });
74
103
  const observationRef = useRef<{
104
+ source: ComputerControlSource;
75
105
  computerSessionId: string | null;
76
106
  observation: ComputerObservation | null;
77
- }>({ computerSessionId, observation: visible.observation });
78
- if (targetsRef.current.computerSessionId !== computerSessionId) {
79
- targetsRef.current = { computerSessionId, targets: visible.targets };
107
+ }>({ source, computerSessionId, observation: visible.observation });
108
+ if (targetsRef.current.source !== source) {
109
+ targetsRef.current = { source, computerSessionId, targets: visible.targets };
80
110
  observationRef.current = {
111
+ source,
81
112
  computerSessionId,
82
113
  observation: visible.observation,
83
114
  };
@@ -91,12 +122,18 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
91
122
  });
92
123
  const mutationRef = useRef<{
93
124
  computerSessionId: string | null;
125
+ source: typeof source;
94
126
  count: number;
95
127
  }>({
96
128
  computerSessionId,
129
+ source,
97
130
  count: 0,
98
131
  });
99
132
  const mountedRef = useRef(true);
133
+ const isCurrentSource = useCallback(
134
+ () => mountedRef.current && sourceRef.current === source,
135
+ [source],
136
+ );
100
137
 
101
138
  const invalidateRefresh = useCallback(() => {
102
139
  const id = requestRef.current.id + 1;
@@ -105,7 +142,7 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
105
142
  }, []);
106
143
 
107
144
  const refresh = useCallback(async (): Promise<void> => {
108
- if (!enabled || !computerSessionId) return;
145
+ if (!enabled || !computerSessionId || !isCurrentSource()) return;
109
146
  const id = requestRef.current.id + 1;
110
147
  requestRef.current.controller?.abort();
111
148
  const controller = new AbortController();
@@ -121,13 +158,14 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
121
158
  }),
122
159
  ]);
123
160
  discoveryCompleted = true;
124
- if (!mountedRef.current || requestRef.current.id !== id) return;
161
+ if (!isCurrentSource() || requestRef.current.id !== id) return;
125
162
  const targets = sortComputerTargets(targetResponse.targets);
126
163
  const selected = chooseTarget(targets, selectedTargetIdRef.current);
127
164
  // Discovery is useful independently of semantic observation. Publish it
128
165
  // now so a slow/unresponsive application cannot block the frame stream
129
166
  // or prevent the person from choosing a different window or screen.
130
- const previous = observationRef.current.observation;
167
+ const previous =
168
+ observationRef.current.source === source ? observationRef.current.observation : null;
131
169
  const retainedObservation =
132
170
  selected &&
133
171
  previous?.target.id === selected.id &&
@@ -136,9 +174,11 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
136
174
  ? previous
137
175
  : null;
138
176
  selectedTargetIdRef.current = selected?.id ?? null;
139
- targetsRef.current = { computerSessionId, targets };
140
- observationRef.current = { computerSessionId, observation: retainedObservation };
177
+ targetsRef.current = { source, computerSessionId, targets };
178
+ observationRef.current = { source, computerSessionId, observation: retainedObservation };
141
179
  setState((current) =>
180
+ isCurrentSource() &&
181
+ current.source === source &&
142
182
  current.computerSessionId === computerSessionId
143
183
  ? {
144
184
  ...current,
@@ -157,11 +197,13 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
157
197
  signal: controller.signal,
158
198
  })
159
199
  : null;
160
- if (!mountedRef.current || requestRef.current.id !== id) return;
200
+ if (!isCurrentSource() || requestRef.current.id !== id) return;
161
201
  selectedTargetIdRef.current = selected?.id ?? null;
162
- targetsRef.current = { computerSessionId, targets };
163
- observationRef.current = { computerSessionId, observation };
202
+ targetsRef.current = { source, computerSessionId, targets };
203
+ observationRef.current = { source, computerSessionId, observation };
164
204
  setState((current) =>
205
+ isCurrentSource() &&
206
+ current.source === source &&
165
207
  current.computerSessionId === computerSessionId
166
208
  ? {
167
209
  ...current,
@@ -175,9 +217,11 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
175
217
  : current,
176
218
  );
177
219
  } catch (cause) {
178
- if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id) return;
220
+ if (controller.signal.aborted || !isCurrentSource() || requestRef.current.id !== id) return;
179
221
  const error = cause instanceof Error ? cause : new Error(String(cause));
180
222
  setState((current) =>
223
+ isCurrentSource() &&
224
+ current.source === source &&
181
225
  current.computerSessionId === computerSessionId
182
226
  ? {
183
227
  ...current,
@@ -195,26 +239,30 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
195
239
  } finally {
196
240
  if (requestRef.current.id === id) requestRef.current = { id, controller: null };
197
241
  }
198
- }, [client, computerSessionId, enabled, workspaceId]);
242
+ }, [client, computerSessionId, enabled, isCurrentSource, source, workspaceId]);
199
243
 
200
244
  useEffect(() => {
201
245
  mountedRef.current = true;
202
246
  if (!enabled) {
203
247
  invalidateRefresh();
204
- setState(emptyState(computerSessionId, false));
248
+ setState(emptyState(source, false));
205
249
  return;
206
250
  }
207
251
  setState((current) =>
208
- current.computerSessionId === computerSessionId
209
- ? { ...current, loading: true }
210
- : emptyState(computerSessionId, true),
252
+ current.source === source
253
+ ? {
254
+ ...current,
255
+ loading: true,
256
+ mutating: mutationRef.current.source === source && mutationRef.current.count > 0,
257
+ }
258
+ : emptyState(source, true),
211
259
  );
212
260
  void refresh();
213
261
  return () => {
214
262
  mountedRef.current = false;
215
263
  invalidateRefresh();
216
264
  };
217
- }, [computerSessionId, enabled, invalidateRefresh, refresh]);
265
+ }, [computerSessionId, enabled, invalidateRefresh, refresh, source]);
218
266
 
219
267
  useEffect(() => {
220
268
  if (!enabled || !pageLive || visible.mutating || refreshBlocked) return;
@@ -240,44 +288,38 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
240
288
 
241
289
  const runMutation = useCallback(
242
290
  async <T>(scopeComputerSessionId: string, operation: () => Promise<T>): Promise<T> => {
291
+ if (!isCurrentSource()) throw new Error("The desktop source is no longer selected.");
243
292
  invalidateRefresh();
244
- if (mutationRef.current.computerSessionId !== scopeComputerSessionId) {
293
+ if (
294
+ mutationRef.current.computerSessionId !== scopeComputerSessionId ||
295
+ mutationRef.current.source !== source
296
+ ) {
245
297
  mutationRef.current = {
246
298
  computerSessionId: scopeComputerSessionId,
299
+ source,
247
300
  count: 0,
248
301
  };
249
302
  }
250
303
  mutationRef.current.count += 1;
251
304
  setState((current) =>
305
+ isCurrentSource() &&
306
+ current.source === source &&
252
307
  current.computerSessionId === scopeComputerSessionId
253
308
  ? { ...current, mutating: true, error: null }
254
309
  : current,
255
310
  );
256
311
  try {
257
- const result = await operation();
258
- setState((current) =>
259
- current.computerSessionId === scopeComputerSessionId
260
- ? { ...current, controlError: null }
261
- : current,
262
- );
263
- return result;
264
- } catch (cause) {
265
- const error = cause instanceof Error ? cause : new Error(String(cause));
266
- setState((current) =>
267
- current.computerSessionId === scopeComputerSessionId
268
- ? {
269
- ...current,
270
- error,
271
- controlError: isInteractionControlUnavailable(error) ? error : current.controlError,
272
- }
273
- : current,
274
- );
275
- throw error;
312
+ return await operation();
276
313
  } finally {
277
- if (mutationRef.current.computerSessionId === scopeComputerSessionId) {
314
+ if (
315
+ mutationRef.current.computerSessionId === scopeComputerSessionId &&
316
+ mutationRef.current.source === source
317
+ ) {
278
318
  mutationRef.current.count = Math.max(0, mutationRef.current.count - 1);
279
319
  const mutating = mutationRef.current.count > 0;
280
320
  setState((current) =>
321
+ isCurrentSource() &&
322
+ current.source === source &&
281
323
  current.computerSessionId === scopeComputerSessionId
282
324
  ? { ...current, mutating }
283
325
  : current,
@@ -285,18 +327,31 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
285
327
  }
286
328
  }
287
329
  },
288
- [invalidateRefresh],
330
+ [invalidateRefresh, isCurrentSource, source],
289
331
  );
290
332
 
291
333
  const selectTarget = useCallback(
292
334
  async (targetId: string): Promise<ComputerTarget> => {
293
335
  if (!computerSessionId) throw new Error("No desktop is selected.");
294
- const target = targetsRef.current.targets.find((candidate) => candidate.id === targetId);
336
+ if (!isCurrentSource()) throw new Error("The desktop source is no longer selected.");
337
+ const target =
338
+ targetsRef.current.source === source
339
+ ? targetsRef.current.targets.find((candidate) => candidate.id === targetId)
340
+ : null;
295
341
  if (!target) throw new Error("The selected desktop target is no longer available.");
296
342
  invalidateRefresh();
343
+ const readRequestId = requestRef.current.id;
344
+ const selectionRevision = ++selectionRevisionRef.current;
345
+ const isCurrentSelection = () =>
346
+ isCurrentSource() && selectionRevisionRef.current === selectionRevision;
347
+ // Once admitted, a queued observation survives immediate input.
348
+ const isCurrentSelectionRead = () =>
349
+ isCurrentSelection() && requestRef.current.id === readRequestId;
297
350
  selectedTargetIdRef.current = target.id;
298
- observationRef.current = { computerSessionId, observation: null };
351
+ observationRef.current = { source, computerSessionId, observation: null };
299
352
  setState((current) =>
353
+ isCurrentSelection() &&
354
+ current.source === source &&
300
355
  current.computerSessionId === computerSessionId
301
356
  ? {
302
357
  ...current,
@@ -313,32 +368,40 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
313
368
  computerSessionId,
314
369
  target.id,
315
370
  );
316
- if (selectedTargetIdRef.current !== target.id) return target;
317
- observationRef.current = { computerSessionId, observation };
371
+ if (!isCurrentSelectionRead() || selectedTargetIdRef.current !== target.id) return target;
372
+ observationRef.current = { source, computerSessionId, observation };
318
373
  setState((current) =>
319
- current.computerSessionId === computerSessionId && current.selectedTargetId === target.id
374
+ isCurrentSelection() &&
375
+ current.source === source &&
376
+ current.computerSessionId === computerSessionId &&
377
+ current.selectedTargetId === target.id
320
378
  ? { ...current, observation, loading: false, error: null }
321
379
  : current,
322
380
  );
323
381
  return observation.target;
324
382
  } catch (cause) {
325
383
  const error = cause instanceof Error ? cause : new Error(String(cause));
326
- setState((current) =>
327
- current.computerSessionId === computerSessionId && current.selectedTargetId === target.id
328
- ? {
329
- ...current,
330
- loading: false,
331
- error,
332
- controlError: isInteractionControlUnavailable(error, "observation")
333
- ? error
334
- : current.controlError,
335
- }
336
- : current,
337
- );
384
+ if (isCurrentSelectionRead()) {
385
+ setState((current) =>
386
+ isCurrentSelection() &&
387
+ current.source === source &&
388
+ current.computerSessionId === computerSessionId &&
389
+ current.selectedTargetId === target.id
390
+ ? {
391
+ ...current,
392
+ loading: false,
393
+ error,
394
+ controlError: isInteractionControlUnavailable(error, "observation")
395
+ ? error
396
+ : current.controlError,
397
+ }
398
+ : current,
399
+ );
400
+ }
338
401
  throw error;
339
402
  }
340
403
  },
341
- [client, computerSessionId, invalidateRefresh, workspaceId],
404
+ [client, computerSessionId, invalidateRefresh, isCurrentSource, source, workspaceId],
342
405
  );
343
406
 
344
407
  const dispatchAction = useCallback(
@@ -348,24 +411,30 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
348
411
  frame: ComputerFrame | null,
349
412
  ): Promise<ComputerActionReceipt> => {
350
413
  if (!computerSessionId) throw new Error("No desktop is selected.");
414
+ if (!isCurrentSource()) throw new Error("The desktop source is no longer selected.");
351
415
  if (frame && frame.computerSessionId !== computerSessionId) {
352
416
  throw new Error("The displayed desktop frame belongs to another desktop session.");
353
417
  }
354
418
  const currentObservation =
419
+ observationRef.current.source === source &&
355
420
  observationRef.current.computerSessionId === computerSessionId
356
421
  ? observationRef.current.observation
357
422
  : null;
358
423
  const focusTarget =
359
- action.type === "focus"
424
+ action.type === "focus" && targetsRef.current.source === source
360
425
  ? (targetsRef.current.targets.find((candidate) => candidate.id === action.targetId) ??
361
426
  null)
362
427
  : null;
363
- const frameTarget = frame
364
- ? (targetsRef.current.targets.find((candidate) => candidate.id === frame.targetId) ??
365
- (currentObservation?.target.id === frame.targetId ? currentObservation.target : null))
366
- : null;
428
+ const frameTarget =
429
+ frame && targetsRef.current.source === source
430
+ ? currentObservation?.target.id === frame.targetId
431
+ ? currentObservation.target
432
+ : (targetsRef.current.targets.find((candidate) => candidate.id === frame.targetId) ??
433
+ null)
434
+ : null;
367
435
  const inputTarget =
368
436
  (action.type === "keyboard" || action.type === "clipboard") &&
437
+ targetsRef.current.source === source &&
369
438
  targetsRef.current.computerSessionId === computerSessionId
370
439
  ? targetsRef.current.targets.find(
371
440
  (candidate) => candidate.id === selectedTargetIdRef.current,
@@ -377,6 +446,13 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
377
446
  if (frame && frame.targetId !== selectedTargetIdRef.current) {
378
447
  throw new Error("The displayed desktop frame is no longer selected.");
379
448
  }
449
+ if (
450
+ frame &&
451
+ (frame.controllerGeneration !== target.controllerGeneration ||
452
+ frame.targetGeneration !== target.targetGeneration)
453
+ ) {
454
+ throw new Error("The displayed desktop frame belongs to an earlier target generation.");
455
+ }
380
456
  if (action.type === "pointer") {
381
457
  const expectedFrameId = frame?.frameId ?? currentObservation?.frameId ?? null;
382
458
  if (!expectedFrameId || action.frameId !== expectedFrameId) {
@@ -386,38 +462,142 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
386
462
  if (action.type === "semantic" && !currentObservation) {
387
463
  throw new Error("The desktop accessibility tree is not ready for input.");
388
464
  }
465
+ const selectedTargetId = selectedTargetIdRef.current;
466
+ const selectedTarget = targetsRef.current.targets.find(
467
+ (candidate) => candidate.id === selectedTargetId,
468
+ );
469
+ const selectedFence = selectedTarget && {
470
+ id: selectedTarget.id,
471
+ computerSessionId: selectedTarget.computerSessionId,
472
+ controllerGeneration: selectedTarget.controllerGeneration,
473
+ targetGeneration: selectedTarget.targetGeneration,
474
+ };
475
+ const selectionRevision = selectionRevisionRef.current;
476
+ const isCurrentView = () =>
477
+ isCurrentSource() &&
478
+ selectionRevisionRef.current === selectionRevision &&
479
+ selectedTargetIdRef.current === selectedTargetId &&
480
+ targetsRef.current.source === source &&
481
+ targetsRef.current.computerSessionId === computerSessionId &&
482
+ sameTargetFence(
483
+ selectedFence,
484
+ targetsRef.current.targets.find((candidate) => candidate.id === selectedTargetId),
485
+ );
486
+ const resultOrder = ++actionResultOrderRef.current.next;
487
+ let admitted = false;
488
+ let resultTargetId = selectedTargetId;
489
+ let resultFence = selectedFence;
490
+ const isCurrentResultView = () =>
491
+ isCurrentSource() &&
492
+ selectionRevisionRef.current === selectionRevision &&
493
+ selectedTargetIdRef.current === resultTargetId &&
494
+ targetsRef.current.source === source &&
495
+ sameTargetFence(
496
+ resultFence,
497
+ targetsRef.current.targets.find((candidate) => candidate.id === resultTargetId),
498
+ );
499
+ const canProjectResult = () => {
500
+ if (!admitted) {
501
+ if (!isCurrentView() || resultOrder < actionResultOrderRef.current.settled) return false;
502
+ actionResultOrderRef.current.settled = resultOrder;
503
+ admitted = true;
504
+ invalidateRefresh();
505
+ }
506
+ return isCurrentResultView() && resultOrder === actionResultOrderRef.current.settled;
507
+ };
508
+ const projectState = (update: (current: ComputerControlState) => ComputerControlState) => {
509
+ setState((current) =>
510
+ admitted &&
511
+ isCurrentResultView() &&
512
+ current.source === source &&
513
+ current.computerSessionId === computerSessionId &&
514
+ current.actionResultOrder <= resultOrder
515
+ ? { ...update(current), actionResultOrder: resultOrder }
516
+ : current,
517
+ );
518
+ };
389
519
 
390
520
  return await runMutation(computerSessionId, async () => {
391
- const receipt = await client.actInComputer(workspaceId, computerSessionId, {
392
- operationId,
393
- targetId: target.id,
394
- expectedTargetGeneration: frame?.targetGeneration ?? target.targetGeneration,
395
- expectedObservationId:
396
- action.type === "pointer" ? null : (currentObservation?.observationId ?? null),
397
- expectedFrameId: action.type === "pointer" ? action.frameId : null,
398
- action,
399
- });
400
- if (receipt.observation) {
401
- const observation = receipt.observation;
402
- observationRef.current = { computerSessionId, observation };
403
- selectedTargetIdRef.current = observation.target.id;
404
- setState((current) =>
405
- current.computerSessionId === computerSessionId
406
- ? {
407
- ...current,
408
- observation,
409
- selectedTargetId: observation.target.id,
410
- targets: replaceTarget(current.targets, observation.target),
411
- }
412
- : current,
521
+ try {
522
+ const receipt = await client.actInComputer(workspaceId, computerSessionId, {
523
+ operationId,
524
+ targetId: target.id,
525
+ expectedTargetGeneration: frame?.targetGeneration ?? target.targetGeneration,
526
+ expectedObservationId:
527
+ action.type === "pointer" ? null : (currentObservation?.observationId ?? null),
528
+ expectedFrameId: action.type === "pointer" ? action.frameId : null,
529
+ action,
530
+ });
531
+ const receiptSettled =
532
+ receipt.state === "completed" ||
533
+ receipt.state === "failed" ||
534
+ receipt.state === "outcome_unknown";
535
+ actionResultFences.set(receipt, () =>
536
+ admitted
537
+ ? isCurrentResultView() && resultOrder === actionResultOrderRef.current.settled
538
+ : isCurrentView() && resultOrder >= actionResultOrderRef.current.settled,
539
+ );
540
+ // A settled result may project only until a later dispatched action
541
+ // settles for this view. Pending later input does not hide this result.
542
+ if (!receiptSettled || !canProjectResult()) return receipt;
543
+ if (receipt.observation) {
544
+ const observation = receipt.observation;
545
+ observationRef.current = { source, computerSessionId, observation };
546
+ selectedTargetIdRef.current = observation.target.id;
547
+ targetsRef.current.targets = replaceTarget(
548
+ targetsRef.current.targets,
549
+ observation.target,
550
+ );
551
+ resultTargetId = observation.target.id;
552
+ resultFence = {
553
+ id: observation.target.id,
554
+ computerSessionId: observation.target.computerSessionId,
555
+ controllerGeneration: observation.target.controllerGeneration,
556
+ targetGeneration: observation.target.targetGeneration,
557
+ };
558
+ projectState((current) => ({
559
+ ...current,
560
+ error: null,
561
+ controlError: null,
562
+ observation,
563
+ selectedTargetId: observation.target.id,
564
+ targets: replaceTarget(current.targets, observation.target),
565
+ }));
566
+ } else {
567
+ projectState((current) => ({ ...current, error: null, controlError: null }));
568
+ void refresh();
569
+ }
570
+ return receipt;
571
+ } catch (cause) {
572
+ const error = cause instanceof Error ? cause : new Error(String(cause));
573
+ actionResultFences.set(
574
+ error,
575
+ () =>
576
+ admitted &&
577
+ isCurrentResultView() &&
578
+ resultOrder === actionResultOrderRef.current.settled,
413
579
  );
414
- } else {
415
- void refresh();
580
+ if (canProjectResult()) {
581
+ projectState((current) => ({
582
+ ...current,
583
+ error,
584
+ controlError: isInteractionControlUnavailable(error) ? error : current.controlError,
585
+ }));
586
+ }
587
+ throw error;
416
588
  }
417
- return receipt;
418
589
  });
419
590
  },
420
- [client, computerSessionId, refresh, runMutation, workspaceId],
591
+ [
592
+ client,
593
+ computerSessionId,
594
+ invalidateRefresh,
595
+ isCurrentSource,
596
+ refresh,
597
+ runMutation,
598
+ source,
599
+ workspaceId,
600
+ ],
421
601
  );
422
602
 
423
603
  const act = useCallback(
@@ -439,11 +619,38 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
439
619
 
440
620
  const readClipboard = useCallback(async (): Promise<ComputerClipboard> => {
441
621
  if (!computerSessionId) throw new Error("No desktop is selected.");
622
+ const selectionRevision = selectionRevisionRef.current;
623
+ const settledOrder = actionResultOrderRef.current.settled;
624
+ const selectedTargetId = selectedTargetIdRef.current;
625
+ const selectedTarget = targetsRef.current.targets.find(
626
+ (candidate) => candidate.id === selectedTargetId,
627
+ );
628
+ const selectedFence = selectedTarget && {
629
+ id: selectedTarget.id,
630
+ computerSessionId: selectedTarget.computerSessionId,
631
+ controllerGeneration: selectedTarget.controllerGeneration,
632
+ targetGeneration: selectedTarget.targetGeneration,
633
+ };
634
+ const isCurrentRead = () =>
635
+ isCurrentSource() &&
636
+ selectionRevisionRef.current === selectionRevision &&
637
+ actionResultOrderRef.current.settled === settledOrder &&
638
+ selectedTargetIdRef.current === selectedTargetId &&
639
+ targetsRef.current.source === source &&
640
+ sameTargetFence(
641
+ selectedFence,
642
+ targetsRef.current.targets.find((candidate) => candidate.id === selectedTargetId),
643
+ );
442
644
  try {
443
- return await client.readComputerClipboard(workspaceId, computerSessionId);
645
+ const clipboard = await client.readComputerClipboard(workspaceId, computerSessionId);
646
+ actionResultFences.set(clipboard, isCurrentRead);
647
+ return clipboard;
444
648
  } catch (cause) {
445
649
  const error = cause instanceof Error ? cause : new Error(String(cause));
650
+ actionResultFences.set(error, isCurrentRead);
446
651
  setState((current) =>
652
+ isCurrentRead() &&
653
+ current.source === source &&
447
654
  current.computerSessionId === computerSessionId
448
655
  ? {
449
656
  ...current,
@@ -454,7 +661,7 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
454
661
  );
455
662
  throw error;
456
663
  }
457
- }, [client, computerSessionId, workspaceId]);
664
+ }, [client, computerSessionId, isCurrentSource, source, workspaceId]);
458
665
 
459
666
  return {
460
667
  session: visible.session,
@@ -474,7 +681,16 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
474
681
  };
475
682
  }
476
683
 
684
+ type ComputerControlSource = Pick<
685
+ ReturnType<typeof useEmbeddedComputerInteraction>,
686
+ "client" | "workspaceId"
687
+ > & {
688
+ computerSessionId: string | null;
689
+ enabled: boolean;
690
+ };
691
+
477
692
  type ComputerControlState = {
693
+ source: ComputerControlSource;
478
694
  computerSessionId: string | null;
479
695
  session: ComputerSession | null;
480
696
  targets: ComputerTarget[];
@@ -484,11 +700,13 @@ type ComputerControlState = {
484
700
  mutating: boolean;
485
701
  error: Error | null;
486
702
  controlError: Error | null;
703
+ actionResultOrder: number;
487
704
  };
488
705
 
489
- function emptyState(computerSessionId: string | null, loading: boolean): ComputerControlState {
706
+ function emptyState(source: ComputerControlSource, loading: boolean): ComputerControlState {
490
707
  return {
491
- computerSessionId,
708
+ source,
709
+ computerSessionId: source.computerSessionId,
492
710
  session: null,
493
711
  targets: [],
494
712
  selectedTargetId: null,
@@ -497,6 +715,7 @@ function emptyState(computerSessionId: string | null, loading: boolean): Compute
497
715
  mutating: false,
498
716
  error: null,
499
717
  controlError: null,
718
+ actionResultOrder: 0,
500
719
  };
501
720
  }
502
721
 
@@ -538,3 +757,20 @@ function replaceTarget(
538
757
  target,
539
758
  ]);
540
759
  }
760
+
761
+ type ComputerTargetFence = Pick<
762
+ ComputerTarget,
763
+ "id" | "computerSessionId" | "controllerGeneration" | "targetGeneration"
764
+ >;
765
+
766
+ function sameTargetFence(
767
+ left: ComputerTargetFence | undefined,
768
+ right: ComputerTargetFence | undefined,
769
+ ): boolean {
770
+ return (
771
+ left?.id === right?.id &&
772
+ left?.computerSessionId === right?.computerSessionId &&
773
+ left?.controllerGeneration === right?.controllerGeneration &&
774
+ left?.targetGeneration === right?.targetGeneration
775
+ );
776
+ }