@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
@@ -10,7 +10,7 @@ import {
10
10
  } from "./chunk-BNKV5ZLF.js";
11
11
  import {
12
12
  copyTextToClipboard
13
- } from "./chunk-CVYCLFMH.js";
13
+ } from "./chunk-PEHTX3V7.js";
14
14
  import {
15
15
  DesktopViewer
16
16
  } from "./chunk-IMSUVYQB.js";
@@ -408,27 +408,49 @@ function isPlacementGenerationLossError(cause) {
408
408
 
409
409
  // src/hooks/use-computer-session.ts
410
410
  import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
411
+ var actionResultFences = /* @__PURE__ */ new WeakMap();
412
+ function isStaleComputerActionResult(value) {
413
+ return typeof value === "object" && value !== null && actionResultFences.get(value)?.() === false;
414
+ }
415
+ function computerActionReceiptError(receipt) {
416
+ const error = new Error(receipt.error?.message ?? "Desktop input did not complete.");
417
+ const fence = actionResultFences.get(receipt);
418
+ if (fence) actionResultFences.set(error, fence);
419
+ return error;
420
+ }
411
421
  function useComputerSession(options) {
412
422
  const { client, workspaceId } = useEmbeddedComputerInteraction(options);
413
423
  const computerSessionId = options.computerSessionId;
414
424
  const enabled = (options.enabled ?? true) && computerSessionId !== null;
425
+ const sourceRef = useRef2({
426
+ client,
427
+ workspaceId,
428
+ computerSessionId,
429
+ enabled
430
+ });
431
+ if (sourceRef.current.client !== client || sourceRef.current.workspaceId !== workspaceId || sourceRef.current.computerSessionId !== computerSessionId || sourceRef.current.enabled !== enabled) {
432
+ sourceRef.current = { client, workspaceId, computerSessionId, enabled };
433
+ }
434
+ const source = sourceRef.current;
415
435
  const pageLive = usePageLiveActivity();
416
436
  const pollIntervalMs = Math.max(750, options.pollIntervalMs ?? 2e3);
417
- const [state, setState] = useState2(
418
- () => emptyState(computerSessionId, enabled)
419
- );
420
- const visible = state.computerSessionId === computerSessionId ? state : emptyState(computerSessionId, enabled);
437
+ const [state, setState] = useState2(() => emptyState(source, enabled));
438
+ const visible = state.source === source ? state : emptyState(source, enabled);
421
439
  const refreshBlocked = isNonRetryableInteractionError(visible.error);
422
440
  const selectedTargetIdRef = useRef2(visible.selectedTargetId);
423
441
  selectedTargetIdRef.current = visible.selectedTargetId;
442
+ const selectionRevisionRef = useRef2(0);
443
+ const actionResultOrderRef = useRef2({ next: 0, settled: 0 });
424
444
  const targetsRef = useRef2({
445
+ source,
425
446
  computerSessionId,
426
447
  targets: visible.targets
427
448
  });
428
- const observationRef = useRef2({ computerSessionId, observation: visible.observation });
429
- if (targetsRef.current.computerSessionId !== computerSessionId) {
430
- targetsRef.current = { computerSessionId, targets: visible.targets };
449
+ const observationRef = useRef2({ source, computerSessionId, observation: visible.observation });
450
+ if (targetsRef.current.source !== source) {
451
+ targetsRef.current = { source, computerSessionId, targets: visible.targets };
431
452
  observationRef.current = {
453
+ source,
432
454
  computerSessionId,
433
455
  observation: visible.observation
434
456
  };
@@ -442,16 +464,21 @@ function useComputerSession(options) {
442
464
  });
443
465
  const mutationRef = useRef2({
444
466
  computerSessionId,
467
+ source,
445
468
  count: 0
446
469
  });
447
470
  const mountedRef = useRef2(true);
471
+ const isCurrentSource = useCallback2(
472
+ () => mountedRef.current && sourceRef.current === source,
473
+ [source]
474
+ );
448
475
  const invalidateRefresh = useCallback2(() => {
449
476
  const id = requestRef.current.id + 1;
450
477
  requestRef.current.controller?.abort();
451
478
  requestRef.current = { id, controller: null };
452
479
  }, []);
453
480
  const refresh = useCallback2(async () => {
454
- if (!enabled || !computerSessionId) return;
481
+ if (!enabled || !computerSessionId || !isCurrentSource()) return;
455
482
  const id = requestRef.current.id + 1;
456
483
  requestRef.current.controller?.abort();
457
484
  const controller = new AbortController();
@@ -467,16 +494,16 @@ function useComputerSession(options) {
467
494
  })
468
495
  ]);
469
496
  discoveryCompleted = true;
470
- if (!mountedRef.current || requestRef.current.id !== id) return;
497
+ if (!isCurrentSource() || requestRef.current.id !== id) return;
471
498
  const targets = sortComputerTargets(targetResponse.targets);
472
499
  const selected = chooseTarget(targets, selectedTargetIdRef.current);
473
- const previous = observationRef.current.observation;
500
+ const previous = observationRef.current.source === source ? observationRef.current.observation : null;
474
501
  const retainedObservation = selected && previous?.target.id === selected.id && previous.target.controllerGeneration === selected.controllerGeneration && previous.target.targetGeneration === selected.targetGeneration ? previous : null;
475
502
  selectedTargetIdRef.current = selected?.id ?? null;
476
- targetsRef.current = { computerSessionId, targets };
477
- observationRef.current = { computerSessionId, observation: retainedObservation };
503
+ targetsRef.current = { source, computerSessionId, targets };
504
+ observationRef.current = { source, computerSessionId, observation: retainedObservation };
478
505
  setState(
479
- (current) => current.computerSessionId === computerSessionId ? {
506
+ (current) => isCurrentSource() && current.source === source && current.computerSessionId === computerSessionId ? {
480
507
  ...current,
481
508
  session,
482
509
  targets,
@@ -490,12 +517,12 @@ function useComputerSession(options) {
490
517
  const observation = selected ? await client.observeComputerTarget(workspaceId, computerSessionId, selected.id, {
491
518
  signal: controller.signal
492
519
  }) : null;
493
- if (!mountedRef.current || requestRef.current.id !== id) return;
520
+ if (!isCurrentSource() || requestRef.current.id !== id) return;
494
521
  selectedTargetIdRef.current = selected?.id ?? null;
495
- targetsRef.current = { computerSessionId, targets };
496
- observationRef.current = { computerSessionId, observation };
522
+ targetsRef.current = { source, computerSessionId, targets };
523
+ observationRef.current = { source, computerSessionId, observation };
497
524
  setState(
498
- (current) => current.computerSessionId === computerSessionId ? {
525
+ (current) => isCurrentSource() && current.source === source && current.computerSessionId === computerSessionId ? {
499
526
  ...current,
500
527
  session,
501
528
  targets,
@@ -506,10 +533,10 @@ function useComputerSession(options) {
506
533
  } : current
507
534
  );
508
535
  } catch (cause) {
509
- if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id) return;
536
+ if (controller.signal.aborted || !isCurrentSource() || requestRef.current.id !== id) return;
510
537
  const error = cause instanceof Error ? cause : new Error(String(cause));
511
538
  setState(
512
- (current) => current.computerSessionId === computerSessionId ? {
539
+ (current) => isCurrentSource() && current.source === source && current.computerSessionId === computerSessionId ? {
513
540
  ...current,
514
541
  loading: false,
515
542
  error,
@@ -522,23 +549,27 @@ function useComputerSession(options) {
522
549
  } finally {
523
550
  if (requestRef.current.id === id) requestRef.current = { id, controller: null };
524
551
  }
525
- }, [client, computerSessionId, enabled, workspaceId]);
552
+ }, [client, computerSessionId, enabled, isCurrentSource, source, workspaceId]);
526
553
  useEffect2(() => {
527
554
  mountedRef.current = true;
528
555
  if (!enabled) {
529
556
  invalidateRefresh();
530
- setState(emptyState(computerSessionId, false));
557
+ setState(emptyState(source, false));
531
558
  return;
532
559
  }
533
560
  setState(
534
- (current) => current.computerSessionId === computerSessionId ? { ...current, loading: true } : emptyState(computerSessionId, true)
561
+ (current) => current.source === source ? {
562
+ ...current,
563
+ loading: true,
564
+ mutating: mutationRef.current.source === source && mutationRef.current.count > 0
565
+ } : emptyState(source, true)
535
566
  );
536
567
  void refresh();
537
568
  return () => {
538
569
  mountedRef.current = false;
539
570
  invalidateRefresh();
540
571
  };
541
- }, [computerSessionId, enabled, invalidateRefresh, refresh]);
572
+ }, [computerSessionId, enabled, invalidateRefresh, refresh, source]);
542
573
  useEffect2(() => {
543
574
  if (!enabled || !pageLive || visible.mutating || refreshBlocked) return;
544
575
  const timer = setInterval(() => {
@@ -561,55 +592,48 @@ function useComputerSession(options) {
561
592
  }, [client, computerSessionId, enabled, pageLive, refresh, refreshBlocked, workspaceId]);
562
593
  const runMutation = useCallback2(
563
594
  async (scopeComputerSessionId, operation) => {
595
+ if (!isCurrentSource()) throw new Error("The desktop source is no longer selected.");
564
596
  invalidateRefresh();
565
- if (mutationRef.current.computerSessionId !== scopeComputerSessionId) {
597
+ if (mutationRef.current.computerSessionId !== scopeComputerSessionId || mutationRef.current.source !== source) {
566
598
  mutationRef.current = {
567
599
  computerSessionId: scopeComputerSessionId,
600
+ source,
568
601
  count: 0
569
602
  };
570
603
  }
571
604
  mutationRef.current.count += 1;
572
605
  setState(
573
- (current) => current.computerSessionId === scopeComputerSessionId ? { ...current, mutating: true, error: null } : current
606
+ (current) => isCurrentSource() && current.source === source && current.computerSessionId === scopeComputerSessionId ? { ...current, mutating: true, error: null } : current
574
607
  );
575
608
  try {
576
- const result = await operation();
577
- setState(
578
- (current) => current.computerSessionId === scopeComputerSessionId ? { ...current, controlError: null } : current
579
- );
580
- return result;
581
- } catch (cause) {
582
- const error = cause instanceof Error ? cause : new Error(String(cause));
583
- setState(
584
- (current) => current.computerSessionId === scopeComputerSessionId ? {
585
- ...current,
586
- error,
587
- controlError: isInteractionControlUnavailable(error) ? error : current.controlError
588
- } : current
589
- );
590
- throw error;
609
+ return await operation();
591
610
  } finally {
592
- if (mutationRef.current.computerSessionId === scopeComputerSessionId) {
611
+ if (mutationRef.current.computerSessionId === scopeComputerSessionId && mutationRef.current.source === source) {
593
612
  mutationRef.current.count = Math.max(0, mutationRef.current.count - 1);
594
613
  const mutating = mutationRef.current.count > 0;
595
614
  setState(
596
- (current) => current.computerSessionId === scopeComputerSessionId ? { ...current, mutating } : current
615
+ (current) => isCurrentSource() && current.source === source && current.computerSessionId === scopeComputerSessionId ? { ...current, mutating } : current
597
616
  );
598
617
  }
599
618
  }
600
619
  },
601
- [invalidateRefresh]
620
+ [invalidateRefresh, isCurrentSource, source]
602
621
  );
603
622
  const selectTarget = useCallback2(
604
623
  async (targetId) => {
605
624
  if (!computerSessionId) throw new Error("No desktop is selected.");
606
- const target = targetsRef.current.targets.find((candidate) => candidate.id === targetId);
625
+ if (!isCurrentSource()) throw new Error("The desktop source is no longer selected.");
626
+ const target = targetsRef.current.source === source ? targetsRef.current.targets.find((candidate) => candidate.id === targetId) : null;
607
627
  if (!target) throw new Error("The selected desktop target is no longer available.");
608
628
  invalidateRefresh();
629
+ const readRequestId = requestRef.current.id;
630
+ const selectionRevision = ++selectionRevisionRef.current;
631
+ const isCurrentSelection = () => isCurrentSource() && selectionRevisionRef.current === selectionRevision;
632
+ const isCurrentSelectionRead = () => isCurrentSelection() && requestRef.current.id === readRequestId;
609
633
  selectedTargetIdRef.current = target.id;
610
- observationRef.current = { computerSessionId, observation: null };
634
+ observationRef.current = { source, computerSessionId, observation: null };
611
635
  setState(
612
- (current) => current.computerSessionId === computerSessionId ? {
636
+ (current) => isCurrentSelection() && current.source === source && current.computerSessionId === computerSessionId ? {
613
637
  ...current,
614
638
  selectedTargetId: target.id,
615
639
  observation: null,
@@ -623,37 +647,40 @@ function useComputerSession(options) {
623
647
  computerSessionId,
624
648
  target.id
625
649
  );
626
- if (selectedTargetIdRef.current !== target.id) return target;
627
- observationRef.current = { computerSessionId, observation };
650
+ if (!isCurrentSelectionRead() || selectedTargetIdRef.current !== target.id) return target;
651
+ observationRef.current = { source, computerSessionId, observation };
628
652
  setState(
629
- (current) => current.computerSessionId === computerSessionId && current.selectedTargetId === target.id ? { ...current, observation, loading: false, error: null } : current
653
+ (current) => isCurrentSelection() && current.source === source && current.computerSessionId === computerSessionId && current.selectedTargetId === target.id ? { ...current, observation, loading: false, error: null } : current
630
654
  );
631
655
  return observation.target;
632
656
  } catch (cause) {
633
657
  const error = cause instanceof Error ? cause : new Error(String(cause));
634
- setState(
635
- (current) => current.computerSessionId === computerSessionId && current.selectedTargetId === target.id ? {
636
- ...current,
637
- loading: false,
638
- error,
639
- controlError: isInteractionControlUnavailable(error, "observation") ? error : current.controlError
640
- } : current
641
- );
658
+ if (isCurrentSelectionRead()) {
659
+ setState(
660
+ (current) => isCurrentSelection() && current.source === source && current.computerSessionId === computerSessionId && current.selectedTargetId === target.id ? {
661
+ ...current,
662
+ loading: false,
663
+ error,
664
+ controlError: isInteractionControlUnavailable(error, "observation") ? error : current.controlError
665
+ } : current
666
+ );
667
+ }
642
668
  throw error;
643
669
  }
644
670
  },
645
- [client, computerSessionId, invalidateRefresh, workspaceId]
671
+ [client, computerSessionId, invalidateRefresh, isCurrentSource, source, workspaceId]
646
672
  );
647
673
  const dispatchAction = useCallback2(
648
674
  async (action, operationId, frame) => {
649
675
  if (!computerSessionId) throw new Error("No desktop is selected.");
676
+ if (!isCurrentSource()) throw new Error("The desktop source is no longer selected.");
650
677
  if (frame && frame.computerSessionId !== computerSessionId) {
651
678
  throw new Error("The displayed desktop frame belongs to another desktop session.");
652
679
  }
653
- const currentObservation = observationRef.current.computerSessionId === computerSessionId ? observationRef.current.observation : null;
654
- const focusTarget = action.type === "focus" ? targetsRef.current.targets.find((candidate) => candidate.id === action.targetId) ?? null : null;
655
- const frameTarget = frame ? targetsRef.current.targets.find((candidate) => candidate.id === frame.targetId) ?? (currentObservation?.target.id === frame.targetId ? currentObservation.target : null) : null;
656
- const inputTarget = (action.type === "keyboard" || action.type === "clipboard") && targetsRef.current.computerSessionId === computerSessionId ? targetsRef.current.targets.find(
680
+ const currentObservation = observationRef.current.source === source && observationRef.current.computerSessionId === computerSessionId ? observationRef.current.observation : null;
681
+ const focusTarget = action.type === "focus" && targetsRef.current.source === source ? targetsRef.current.targets.find((candidate) => candidate.id === action.targetId) ?? null : null;
682
+ const frameTarget = frame && targetsRef.current.source === source ? currentObservation?.target.id === frame.targetId ? currentObservation.target : targetsRef.current.targets.find((candidate) => candidate.id === frame.targetId) ?? null : null;
683
+ const inputTarget = (action.type === "keyboard" || action.type === "clipboard") && targetsRef.current.source === source && targetsRef.current.computerSessionId === computerSessionId ? targetsRef.current.targets.find(
657
684
  (candidate) => candidate.id === selectedTargetIdRef.current
658
685
  ) : null;
659
686
  const target = frameTarget ?? focusTarget ?? currentObservation?.target ?? inputTarget ?? null;
@@ -661,6 +688,9 @@ function useComputerSession(options) {
661
688
  if (frame && frame.targetId !== selectedTargetIdRef.current) {
662
689
  throw new Error("The displayed desktop frame is no longer selected.");
663
690
  }
691
+ if (frame && (frame.controllerGeneration !== target.controllerGeneration || frame.targetGeneration !== target.targetGeneration)) {
692
+ throw new Error("The displayed desktop frame belongs to an earlier target generation.");
693
+ }
664
694
  if (action.type === "pointer") {
665
695
  const expectedFrameId = frame?.frameId ?? currentObservation?.frameId ?? null;
666
696
  if (!expectedFrameId || action.frameId !== expectedFrameId) {
@@ -670,34 +700,114 @@ function useComputerSession(options) {
670
700
  if (action.type === "semantic" && !currentObservation) {
671
701
  throw new Error("The desktop accessibility tree is not ready for input.");
672
702
  }
703
+ const selectedTargetId = selectedTargetIdRef.current;
704
+ const selectedTarget = targetsRef.current.targets.find(
705
+ (candidate) => candidate.id === selectedTargetId
706
+ );
707
+ const selectedFence = selectedTarget && {
708
+ id: selectedTarget.id,
709
+ computerSessionId: selectedTarget.computerSessionId,
710
+ controllerGeneration: selectedTarget.controllerGeneration,
711
+ targetGeneration: selectedTarget.targetGeneration
712
+ };
713
+ const selectionRevision = selectionRevisionRef.current;
714
+ const isCurrentView = () => isCurrentSource() && selectionRevisionRef.current === selectionRevision && selectedTargetIdRef.current === selectedTargetId && targetsRef.current.source === source && targetsRef.current.computerSessionId === computerSessionId && sameTargetFence(
715
+ selectedFence,
716
+ targetsRef.current.targets.find((candidate) => candidate.id === selectedTargetId)
717
+ );
718
+ const resultOrder = ++actionResultOrderRef.current.next;
719
+ let admitted = false;
720
+ let resultTargetId = selectedTargetId;
721
+ let resultFence = selectedFence;
722
+ const isCurrentResultView = () => isCurrentSource() && selectionRevisionRef.current === selectionRevision && selectedTargetIdRef.current === resultTargetId && targetsRef.current.source === source && sameTargetFence(
723
+ resultFence,
724
+ targetsRef.current.targets.find((candidate) => candidate.id === resultTargetId)
725
+ );
726
+ const canProjectResult = () => {
727
+ if (!admitted) {
728
+ if (!isCurrentView() || resultOrder < actionResultOrderRef.current.settled) return false;
729
+ actionResultOrderRef.current.settled = resultOrder;
730
+ admitted = true;
731
+ invalidateRefresh();
732
+ }
733
+ return isCurrentResultView() && resultOrder === actionResultOrderRef.current.settled;
734
+ };
735
+ const projectState = (update) => {
736
+ setState(
737
+ (current) => admitted && isCurrentResultView() && current.source === source && current.computerSessionId === computerSessionId && current.actionResultOrder <= resultOrder ? { ...update(current), actionResultOrder: resultOrder } : current
738
+ );
739
+ };
673
740
  return await runMutation(computerSessionId, async () => {
674
- const receipt = await client.actInComputer(workspaceId, computerSessionId, {
675
- operationId,
676
- targetId: target.id,
677
- expectedTargetGeneration: frame?.targetGeneration ?? target.targetGeneration,
678
- expectedObservationId: action.type === "pointer" ? null : currentObservation?.observationId ?? null,
679
- expectedFrameId: action.type === "pointer" ? action.frameId : null,
680
- action
681
- });
682
- if (receipt.observation) {
683
- const observation = receipt.observation;
684
- observationRef.current = { computerSessionId, observation };
685
- selectedTargetIdRef.current = observation.target.id;
686
- setState(
687
- (current) => current.computerSessionId === computerSessionId ? {
741
+ try {
742
+ const receipt = await client.actInComputer(workspaceId, computerSessionId, {
743
+ operationId,
744
+ targetId: target.id,
745
+ expectedTargetGeneration: frame?.targetGeneration ?? target.targetGeneration,
746
+ expectedObservationId: action.type === "pointer" ? null : currentObservation?.observationId ?? null,
747
+ expectedFrameId: action.type === "pointer" ? action.frameId : null,
748
+ action
749
+ });
750
+ const receiptSettled = receipt.state === "completed" || receipt.state === "failed" || receipt.state === "outcome_unknown";
751
+ actionResultFences.set(
752
+ receipt,
753
+ () => admitted ? isCurrentResultView() && resultOrder === actionResultOrderRef.current.settled : isCurrentView() && resultOrder >= actionResultOrderRef.current.settled
754
+ );
755
+ if (!receiptSettled || !canProjectResult()) return receipt;
756
+ if (receipt.observation) {
757
+ const observation = receipt.observation;
758
+ observationRef.current = { source, computerSessionId, observation };
759
+ selectedTargetIdRef.current = observation.target.id;
760
+ targetsRef.current.targets = replaceTarget(
761
+ targetsRef.current.targets,
762
+ observation.target
763
+ );
764
+ resultTargetId = observation.target.id;
765
+ resultFence = {
766
+ id: observation.target.id,
767
+ computerSessionId: observation.target.computerSessionId,
768
+ controllerGeneration: observation.target.controllerGeneration,
769
+ targetGeneration: observation.target.targetGeneration
770
+ };
771
+ projectState((current) => ({
688
772
  ...current,
773
+ error: null,
774
+ controlError: null,
689
775
  observation,
690
776
  selectedTargetId: observation.target.id,
691
777
  targets: replaceTarget(current.targets, observation.target)
692
- } : current
778
+ }));
779
+ } else {
780
+ projectState((current) => ({ ...current, error: null, controlError: null }));
781
+ void refresh();
782
+ }
783
+ return receipt;
784
+ } catch (cause) {
785
+ const error = cause instanceof Error ? cause : new Error(String(cause));
786
+ actionResultFences.set(
787
+ error,
788
+ () => admitted && isCurrentResultView() && resultOrder === actionResultOrderRef.current.settled
693
789
  );
694
- } else {
695
- void refresh();
790
+ if (canProjectResult()) {
791
+ projectState((current) => ({
792
+ ...current,
793
+ error,
794
+ controlError: isInteractionControlUnavailable(error) ? error : current.controlError
795
+ }));
796
+ }
797
+ throw error;
696
798
  }
697
- return receipt;
698
799
  });
699
800
  },
700
- [client, computerSessionId, refresh, runMutation, workspaceId]
801
+ [
802
+ client,
803
+ computerSessionId,
804
+ invalidateRefresh,
805
+ isCurrentSource,
806
+ refresh,
807
+ runMutation,
808
+ source,
809
+ workspaceId
810
+ ]
701
811
  );
702
812
  const act = useCallback2(
703
813
  async (action, operationId = crypto.randomUUID()) => await dispatchAction(action, operationId, null),
@@ -709,12 +819,31 @@ function useComputerSession(options) {
709
819
  );
710
820
  const readClipboard = useCallback2(async () => {
711
821
  if (!computerSessionId) throw new Error("No desktop is selected.");
822
+ const selectionRevision = selectionRevisionRef.current;
823
+ const settledOrder = actionResultOrderRef.current.settled;
824
+ const selectedTargetId = selectedTargetIdRef.current;
825
+ const selectedTarget = targetsRef.current.targets.find(
826
+ (candidate) => candidate.id === selectedTargetId
827
+ );
828
+ const selectedFence = selectedTarget && {
829
+ id: selectedTarget.id,
830
+ computerSessionId: selectedTarget.computerSessionId,
831
+ controllerGeneration: selectedTarget.controllerGeneration,
832
+ targetGeneration: selectedTarget.targetGeneration
833
+ };
834
+ const isCurrentRead = () => isCurrentSource() && selectionRevisionRef.current === selectionRevision && actionResultOrderRef.current.settled === settledOrder && selectedTargetIdRef.current === selectedTargetId && targetsRef.current.source === source && sameTargetFence(
835
+ selectedFence,
836
+ targetsRef.current.targets.find((candidate) => candidate.id === selectedTargetId)
837
+ );
712
838
  try {
713
- return await client.readComputerClipboard(workspaceId, computerSessionId);
839
+ const clipboard = await client.readComputerClipboard(workspaceId, computerSessionId);
840
+ actionResultFences.set(clipboard, isCurrentRead);
841
+ return clipboard;
714
842
  } catch (cause) {
715
843
  const error = cause instanceof Error ? cause : new Error(String(cause));
844
+ actionResultFences.set(error, isCurrentRead);
716
845
  setState(
717
- (current) => current.computerSessionId === computerSessionId ? {
846
+ (current) => isCurrentRead() && current.source === source && current.computerSessionId === computerSessionId ? {
718
847
  ...current,
719
848
  error,
720
849
  controlError: isInteractionControlUnavailable(error) ? error : current.controlError
@@ -722,7 +851,7 @@ function useComputerSession(options) {
722
851
  );
723
852
  throw error;
724
853
  }
725
- }, [client, computerSessionId, workspaceId]);
854
+ }, [client, computerSessionId, isCurrentSource, source, workspaceId]);
726
855
  return {
727
856
  session: visible.session,
728
857
  targets: visible.targets,
@@ -739,9 +868,10 @@ function useComputerSession(options) {
739
868
  actFromFrame
740
869
  };
741
870
  }
742
- function emptyState(computerSessionId, loading) {
871
+ function emptyState(source, loading) {
743
872
  return {
744
- computerSessionId,
873
+ source,
874
+ computerSessionId: source.computerSessionId,
745
875
  session: null,
746
876
  targets: [],
747
877
  selectedTargetId: null,
@@ -749,7 +879,8 @@ function emptyState(computerSessionId, loading) {
749
879
  loading,
750
880
  mutating: false,
751
881
  error: null,
752
- controlError: null
882
+ controlError: null,
883
+ actionResultOrder: 0
753
884
  };
754
885
  }
755
886
  function chooseTarget(targets, preferredId) {
@@ -769,6 +900,9 @@ function replaceTarget(targets, target) {
769
900
  target
770
901
  ]);
771
902
  }
903
+ function sameTargetFence(left, right) {
904
+ return left?.id === right?.id && left?.computerSessionId === right?.computerSessionId && left?.controllerGeneration === right?.controllerGeneration && left?.targetGeneration === right?.targetGeneration;
905
+ }
772
906
 
773
907
  // src/hooks/use-computer-input-posture.ts
774
908
  import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
@@ -1089,6 +1223,7 @@ function ComputerViewer({
1089
1223
  }, [enabled, endStaleComputer, registry.sessions]);
1090
1224
  const notifyError = useCallback5(
1091
1225
  (cause, fallback) => {
1226
+ if (isStaleComputerActionResult(cause)) return;
1092
1227
  onNotify?.({
1093
1228
  kind: "error",
1094
1229
  message: cause instanceof Error ? cause.message : fallback
@@ -1323,19 +1458,20 @@ function ComputerViewer({
1323
1458
  suppressGenericCreate
1324
1459
  ]);
1325
1460
  const perform = useCallback5(
1326
- async (action, frame) => {
1461
+ async (action, frame, operationId) => {
1327
1462
  if (computer.controlError) throw computer.controlError;
1328
1463
  if (inputDenied) throw new Error("Desktop is view only. Refresh to check desktop controls.");
1329
1464
  let receipt;
1330
1465
  if (action.type === "pointer") {
1331
1466
  if (!frame) throw new Error("Desktop view is not ready for pointer input.");
1332
- receipt = await actFromFrame(action, frame);
1467
+ receipt = await actFromFrame(action, frame, operationId);
1333
1468
  } else {
1334
- receipt = await act(action);
1469
+ receipt = await act(action, operationId);
1335
1470
  }
1336
- if (receipt.state !== "completed") {
1337
- throw new Error(receipt.error?.message ?? "Desktop input did not complete.");
1471
+ if (!isStaleComputerActionResult(receipt) && receipt.state !== "completed") {
1472
+ throw computerActionReceiptError(receipt);
1338
1473
  }
1474
+ return receipt;
1339
1475
  },
1340
1476
  [act, actFromFrame, computer.controlError, inputDenied]
1341
1477
  );
@@ -1350,11 +1486,15 @@ function ComputerViewer({
1350
1486
  return;
1351
1487
  event.preventDefault();
1352
1488
  event.stopPropagation();
1353
- void perform({ type: "clipboard", operation: "copy" }, null).then(() => computer.readClipboard()).then(async (clipboard) => {
1489
+ void (async () => {
1490
+ const receipt = await perform({ type: "clipboard", operation: "copy" }, null);
1491
+ if (isStaleComputerActionResult(receipt)) return;
1492
+ const clipboard = await computer.readClipboard();
1493
+ if (isStaleComputerActionResult(receipt) || isStaleComputerActionResult(clipboard)) return;
1354
1494
  if (clipboard.text && !await copyTextToClipboard(clipboard.text)) {
1355
1495
  throw new Error("Desktop text could not be copied to the local clipboard");
1356
1496
  }
1357
- }).catch((cause) => notifyError(cause, "Could not copy from the desktop."));
1497
+ })().catch((cause) => notifyError(cause, "Could not copy from the desktop."));
1358
1498
  },
1359
1499
  [computer, notifyError, perform, rfbInputEnabled, rfbStream]
1360
1500
  );
@@ -1363,8 +1503,11 @@ function ComputerViewer({
1363
1503
  if (!rfbInputEnabled || !rfbStream || computer.session?.capabilities?.clipboard !== true)
1364
1504
  return false;
1365
1505
  void (async () => {
1366
- await perform({ type: "clipboard", operation: "write", text }, null);
1367
- assertExactComputerClipboard(await computer.readClipboard(), text);
1506
+ const receipt = await perform({ type: "clipboard", operation: "write", text }, null);
1507
+ if (isStaleComputerActionResult(receipt)) return;
1508
+ const clipboard = await computer.readClipboard();
1509
+ if (isStaleComputerActionResult(receipt) || isStaleComputerActionResult(clipboard)) return;
1510
+ assertExactComputerClipboard(clipboard, text);
1368
1511
  await perform({ type: "clipboard", operation: "paste" }, null);
1369
1512
  })().catch((cause) => notifyError(cause, "Could not paste into the desktop."));
1370
1513
  return true;
@@ -1523,6 +1666,7 @@ function ComputerViewer({
1523
1666
  backgroundInput: computer.session?.capabilities?.backgroundInput === true,
1524
1667
  clipboardEnabled: attachmentInputAllowed && computer.session?.capabilities?.clipboard === true,
1525
1668
  pointerInput: attachmentInputAllowed && computer.session?.capabilities?.pointerInput === true,
1669
+ pointerClickContinuation: attachmentInputAllowed && computer.session?.capabilities?.pointerClickContinuation === true,
1526
1670
  keyboardInput: attachmentInputAllowed && computer.session?.capabilities?.keyboardInput === true,
1527
1671
  onAction: perform,
1528
1672
  onReadClipboard: computer.readClipboard,
@@ -1997,21 +2141,38 @@ function ComputerViewport(props) {
1997
2141
  }
1998
2142
  }, [clearBufferedInput, props.keyboardInput, props.pointerInput, rawInputEnabled]);
1999
2143
  const enqueue = useCallback5(
2000
- (action, frame, after) => {
2144
+ (action, frame, after, continuation) => {
2001
2145
  const epoch = actionQueueEpochRef.current;
2002
2146
  const dispatch = actionRef.current;
2003
2147
  const isCurrent = () => mountedRef.current && epoch === actionQueueEpochRef.current;
2004
- actionTailRef.current = actionTailRef.current.catch(() => void 0).then(async () => {
2005
- if (!isCurrent()) return;
2006
- await dispatch(action, frame);
2007
- if (!isCurrent()) return;
2008
- await after?.(isCurrent);
2148
+ const operationId = crypto.randomUUID();
2149
+ let markStarted;
2150
+ const started = new Promise((resolve) => {
2151
+ markStarted = resolve;
2152
+ });
2153
+ const previous = actionTailRef.current;
2154
+ const ready = continuation && previous === continuation.completion ? continuation.started : previous.then(() => true);
2155
+ const delivery = ready.then(async (firstStarted) => {
2156
+ if (!firstStarted || !isCurrent()) {
2157
+ markStarted(false);
2158
+ return;
2159
+ }
2160
+ const pending = dispatch(action, frame, operationId);
2161
+ markStarted(true);
2162
+ const receipt = await pending;
2163
+ const isCurrentResult = () => isCurrent() && !isStaleComputerActionResult(receipt);
2164
+ if (!isCurrentResult()) return;
2165
+ await after?.(isCurrentResult);
2009
2166
  }).catch((cause) => {
2010
- if (!isCurrent()) return;
2167
+ markStarted(false);
2168
+ if (!isCurrent() || isStaleComputerActionResult(cause)) return;
2011
2169
  actionQueueEpochRef.current += 1;
2012
2170
  clearBufferedInput();
2013
2171
  errorRef.current(cause);
2014
2172
  });
2173
+ const completion = continuation ? Promise.all([previous, delivery]).then(() => void 0) : delivery;
2174
+ actionTailRef.current = completion;
2175
+ return { operationId, started, completion };
2015
2176
  },
2016
2177
  [clearBufferedInput]
2017
2178
  );
@@ -2032,6 +2193,7 @@ function ComputerViewport(props) {
2032
2193
  if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
2033
2194
  clickTimerRef.current = null;
2034
2195
  lastClickRef.current = null;
2196
+ if (!pending.pending) return;
2035
2197
  enqueue(
2036
2198
  {
2037
2199
  type: "pointer",
@@ -2063,7 +2225,8 @@ function ComputerViewport(props) {
2063
2225
  }, [enqueue]);
2064
2226
  const pointerDown = (event) => {
2065
2227
  const frame = paintedFrameRef.current;
2066
- if (!frame || props.mutating || !rawInputEnabled || event.button !== 0) return;
2228
+ if (!frame || !props.pointerClickContinuation && props.mutating || !rawInputEnabled || event.button !== 0)
2229
+ return;
2067
2230
  event.preventDefault();
2068
2231
  if (keyboardInputEnabled) inputRef.current?.focus({ preventScroll: true });
2069
2232
  if (!pointerInputEnabled) return;
@@ -2082,6 +2245,7 @@ function ComputerViewport(props) {
2082
2245
  const start = pointerStartRef.current;
2083
2246
  pointerStartRef.current = null;
2084
2247
  if (!start || start.pointerId !== event.pointerId) return;
2248
+ const canvasRect = event.currentTarget.getBoundingClientRect();
2085
2249
  const from = point(start.frame, start.x, start.y);
2086
2250
  const to = point(start.frame, event.clientX, event.clientY);
2087
2251
  if (!from || !to) return;
@@ -2103,7 +2267,7 @@ function ComputerViewport(props) {
2103
2267
  }
2104
2268
  const now = Date.now();
2105
2269
  const previous = lastClickRef.current;
2106
- if (previous && now - previous.at < 280 && Math.hypot(previous.x - to.x, previous.y - to.y) < 6 && sameFrameFence(previous.frame, start.frame)) {
2270
+ if (previous && now - previous.at < 280 && previous.pending === !props.pointerClickContinuation && (props.pointerClickContinuation ? Math.hypot(previous.clientX - event.clientX, previous.clientY - event.clientY) < 6 && previous.canvasRect.left === canvasRect.left && previous.canvasRect.top === canvasRect.top && previous.canvasRect.width === canvasRect.width && previous.canvasRect.height === canvasRect.height && sameComputerTarget(previous.frame, start.frame) && previous.frame.width === start.frame.width && previous.frame.height === start.frame.height : Math.hypot(previous.x - to.x, previous.y - to.y) < 6 && sameFrameFence(previous.frame, start.frame))) {
2107
2271
  if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
2108
2272
  clickTimerRef.current = null;
2109
2273
  lastClickRef.current = null;
@@ -2111,15 +2275,45 @@ function ComputerViewport(props) {
2111
2275
  {
2112
2276
  type: "pointer",
2113
2277
  frameId: start.frame.frameId,
2114
- action: "double_click",
2278
+ action: props.pointerClickContinuation ? "click" : "double_click",
2279
+ ...props.pointerClickContinuation ? { clickCount: 2 } : {},
2280
+ ...previous.delivery ? { continuationOfOperationId: previous.delivery.operationId } : {},
2281
+ x: to.x,
2282
+ y: to.y
2283
+ },
2284
+ start.frame,
2285
+ void 0,
2286
+ previous.delivery ?? void 0
2287
+ );
2288
+ return;
2289
+ }
2290
+ flushPendingClick();
2291
+ lastClickRef.current = {
2292
+ at: now,
2293
+ x: to.x,
2294
+ y: to.y,
2295
+ frame: start.frame,
2296
+ clientX: event.clientX,
2297
+ clientY: event.clientY,
2298
+ canvasRect,
2299
+ pending: !props.pointerClickContinuation,
2300
+ delivery: null
2301
+ };
2302
+ if (props.pointerClickContinuation) {
2303
+ const delivery = enqueue(
2304
+ {
2305
+ type: "pointer",
2306
+ frameId: start.frame.frameId,
2307
+ action: "click",
2308
+ clickCount: 1,
2115
2309
  x: to.x,
2116
2310
  y: to.y
2117
2311
  },
2118
2312
  start.frame
2119
2313
  );
2314
+ if (lastClickRef.current) lastClickRef.current.delivery = delivery;
2120
2315
  return;
2121
2316
  }
2122
- lastClickRef.current = { at: now, x: to.x, y: to.y, frame: start.frame };
2123
2317
  clickTimerRef.current = setTimeout(() => {
2124
2318
  clickTimerRef.current = null;
2125
2319
  lastClickRef.current = null;
@@ -2731,4 +2925,4 @@ export {
2731
2925
  ComputerViewer,
2732
2926
  computerKey
2733
2927
  };
2734
- //# sourceMappingURL=chunk-MDIXFKA3.js.map
2928
+ //# sourceMappingURL=chunk-6FOTSVWU.js.map