@opengeni/react 1.4.2 → 1.4.4
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.
- package/dist/{browser-viewer-WGJ43YGI.js → browser-viewer-5EEAS3IU.js} +2 -2
- package/dist/{chunk-VCSERYXH.js → chunk-2OQA3CPB.js} +291 -158
- package/dist/{chunk-VCSERYXH.js.map → chunk-2OQA3CPB.js.map} +1 -1
- package/dist/{chunk-CREMJMVN.js → chunk-EAOSEIXB.js} +2 -2
- package/dist/{chunk-CREMJMVN.js.map → chunk-EAOSEIXB.js.map} +1 -1
- package/dist/{chunk-MDIXFKA3.js → chunk-I6OY3MWC.js} +302 -108
- package/dist/chunk-I6OY3MWC.js.map +1 -0
- package/dist/{computer-viewer-46UIN7F3.js → computer-viewer-ELNLZGHI.js} +2 -2
- package/dist/hooks/use-computer-session.d.ts +2 -0
- package/dist/index.js +3 -3
- package/dist/interaction.js +2 -2
- package/dist/session-ui.js +1 -1
- package/package.json +3 -3
- package/src/components/computer-viewer.tsx +139 -30
- package/src/components/session-list.tsx +1 -1
- package/src/hooks/use-browser-session.ts +445 -168
- package/src/hooks/use-computer-session.ts +334 -98
- package/styles/compiled.css +5 -0
- package/dist/chunk-MDIXFKA3.js.map +0 -1
- /package/dist/{browser-viewer-WGJ43YGI.js.map → browser-viewer-5EEAS3IU.js.map} +0 -0
- /package/dist/{computer-viewer-46UIN7F3.js.map → computer-viewer-ELNLZGHI.js.map} +0 -0
|
@@ -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
|
-
|
|
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.
|
|
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 (!
|
|
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 (!
|
|
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 || !
|
|
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(
|
|
557
|
+
setState(emptyState(source, false));
|
|
531
558
|
return;
|
|
532
559
|
}
|
|
533
560
|
setState(
|
|
534
|
-
(current) => current.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
635
|
-
(
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
695
|
-
|
|
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
|
-
[
|
|
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
|
-
|
|
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(
|
|
871
|
+
function emptyState(source, loading) {
|
|
743
872
|
return {
|
|
744
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
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
|
-
|
|
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)
|
|
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-
|
|
2928
|
+
//# sourceMappingURL=chunk-I6OY3MWC.js.map
|