@opengeni/react 7.6.1 → 7.7.0-canary.37111190284001

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 (145) hide show
  1. package/README.md +47 -2
  2. package/dist/{browser-viewer-5X47I6T2.js → browser-viewer-63TXMINB.js} +6 -5
  3. package/dist/{chunk-5EXGU7UF.js → chunk-55DDDVKU.js} +3 -3
  4. package/dist/{chunk-UINDK6C3.js → chunk-7CYYXWHU.js} +5 -8
  5. package/dist/chunk-7CYYXWHU.js.map +1 -0
  6. package/dist/{chunk-LZ3KHA55.js → chunk-A2L5N3EI.js} +332 -339
  7. package/dist/chunk-A2L5N3EI.js.map +1 -0
  8. package/dist/{chunk-KGENSZKS.js → chunk-AAG4R3QN.js} +4 -3
  9. package/dist/chunk-AAG4R3QN.js.map +1 -0
  10. package/dist/{chunk-Z3DISEQN.js → chunk-CWWKTNKK.js} +35 -18
  11. package/dist/chunk-CWWKTNKK.js.map +1 -0
  12. package/dist/{chunk-WZ55DVGK.js → chunk-ELCE5SXV.js} +8 -4
  13. package/dist/chunk-ELCE5SXV.js.map +1 -0
  14. package/dist/{chunk-YSMMDOOE.js → chunk-GISEVGS6.js} +10 -2
  15. package/dist/chunk-GISEVGS6.js.map +1 -0
  16. package/dist/{chunk-BI57CQUV.js → chunk-I7IWRQLN.js} +2 -2
  17. package/dist/{chunk-XA6R7DJQ.js → chunk-IQUCRAT7.js} +3 -3
  18. package/dist/{chunk-CHPKSLPN.js → chunk-NBZ64YY7.js} +4 -2
  19. package/dist/chunk-NBZ64YY7.js.map +1 -0
  20. package/dist/{chunk-DV2DBNPQ.js → chunk-Q5QBEZIQ.js} +48 -29
  21. package/dist/chunk-Q5QBEZIQ.js.map +1 -0
  22. package/dist/{chunk-FGBCYQOT.js → chunk-RDKBXMWX.js} +120 -74
  23. package/dist/chunk-RDKBXMWX.js.map +1 -0
  24. package/dist/{chunk-45GE35O7.js → chunk-TKB4SB44.js} +10 -2
  25. package/dist/chunk-TKB4SB44.js.map +1 -0
  26. package/dist/{chunk-YCGBREUV.js → chunk-TOAH2BKA.js} +6 -2
  27. package/dist/{chunk-YCGBREUV.js.map → chunk-TOAH2BKA.js.map} +1 -1
  28. package/dist/chunk-UGLEYEMP.js +33 -0
  29. package/dist/chunk-UGLEYEMP.js.map +1 -0
  30. package/dist/{chunk-UJRYEWZI.js → chunk-UTRTG6LH.js} +48 -8
  31. package/dist/chunk-UTRTG6LH.js.map +1 -0
  32. package/dist/{chunk-YRYF2GLG.js → chunk-WYVLAAJA.js} +39 -31
  33. package/dist/chunk-WYVLAAJA.js.map +1 -0
  34. package/dist/{chunk-VGIKFIO2.js → chunk-XOAMRDDK.js} +6 -3
  35. package/dist/chunk-XOAMRDDK.js.map +1 -0
  36. package/dist/{chunk-6NEZPF53.js → chunk-YN7I3STW.js} +153 -32
  37. package/dist/chunk-YN7I3STW.js.map +1 -0
  38. package/dist/{chunk-KZ73RL76.js → chunk-YQEAA6LT.js} +7 -3
  39. package/dist/chunk-YQEAA6LT.js.map +1 -0
  40. package/dist/client.d.ts +4 -1
  41. package/dist/commands/types.d.ts +2 -0
  42. package/dist/components/approval-surface.d.ts +15 -0
  43. package/dist/components/session-conversation.d.ts +3 -0
  44. package/dist/composer.js +8 -7
  45. package/dist/{computer-viewer-7GF35SNY.js → computer-viewer-X2MBRZY4.js} +5 -4
  46. package/dist/connect.js +3 -3
  47. package/dist/hooks/use-browser-session.d.ts +4 -0
  48. package/dist/hooks/use-composer.d.ts +2 -0
  49. package/dist/hooks/use-computer-session.d.ts +2 -0
  50. package/dist/hooks/use-file-attachments.d.ts +2 -0
  51. package/dist/hooks/use-session-events.d.ts +1 -1
  52. package/dist/hooks/use-workspace-sessions.d.ts +15 -5
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.js +28 -21
  55. package/dist/index.js.map +1 -1
  56. package/dist/interaction.d.ts +1 -1
  57. package/dist/interaction.js +6 -5
  58. package/dist/interaction.js.map +1 -1
  59. package/dist/{latest-question-3BJGKQQ6.js → latest-question-CNEZ5KAN.js} +3 -3
  60. package/dist/lib/error-message.d.ts +7 -0
  61. package/dist/lib/format.d.ts +27 -3
  62. package/dist/lib/interaction-errors.d.ts +3 -0
  63. package/dist/machines.js +5 -4
  64. package/dist/{model-policy-picker-menu-PUTA5JOB.js → model-policy-picker-menu-GN2LTB7Z.js} +4 -4
  65. package/dist/model-policy.js +1 -1
  66. package/dist/provider.d.ts +5 -1
  67. package/dist/{queue-surface-implementation-2CAQAIHM.js → queue-surface-implementation-HENWYFWA.js} +3 -2
  68. package/dist/{queue-surface-implementation-2CAQAIHM.js.map → queue-surface-implementation-HENWYFWA.js.map} +1 -1
  69. package/dist/realtime.js +2 -2
  70. package/dist/{sandbox-files-AEJTWGT4.js → sandbox-files-BBC47SLD.js} +4 -3
  71. package/dist/session-ui.d.ts +1 -1
  72. package/dist/session-ui.js +13 -12
  73. package/dist/session-ui.js.map +1 -1
  74. package/dist/session.js +7 -6
  75. package/dist/usage.js +2 -1
  76. package/dist/usage.js.map +1 -1
  77. package/package.json +3 -3
  78. package/src/client.ts +5 -1
  79. package/src/commands/registry.ts +18 -15
  80. package/src/commands/types.ts +2 -0
  81. package/src/components/approval-surface.tsx +166 -42
  82. package/src/components/browser-viewer.tsx +38 -9
  83. package/src/components/composer.tsx +9 -2
  84. package/src/components/computer-viewer.tsx +160 -30
  85. package/src/components/generated-video-player.tsx +4 -2
  86. package/src/components/human-input-form.tsx +18 -5
  87. package/src/components/message-timeline.tsx +120 -168
  88. package/src/components/model-policy-picker.tsx +2 -15
  89. package/src/components/open-geni-chat.tsx +11 -4
  90. package/src/components/queue-surface-state.tsx +3 -1
  91. package/src/components/session-chrome.tsx +5 -2
  92. package/src/components/session-commands-panel.tsx +3 -1
  93. package/src/components/session-conversation.tsx +10 -3
  94. package/src/components/session-list.tsx +7 -5
  95. package/src/components/session-mcp-capability-card.tsx +11 -1
  96. package/src/hooks/internal.ts +3 -2
  97. package/src/hooks/use-browser-session.ts +115 -79
  98. package/src/hooks/use-composer.ts +70 -31
  99. package/src/hooks/use-computer-session.ts +57 -6
  100. package/src/hooks/use-file-attachments.ts +16 -3
  101. package/src/hooks/use-goal.ts +2 -1
  102. package/src/hooks/use-session-capabilities.ts +3 -2
  103. package/src/hooks/use-session-events.ts +15 -8
  104. package/src/hooks/use-slash-commands.ts +6 -11
  105. package/src/hooks/use-turn-queue.ts +2 -1
  106. package/src/hooks/use-workspace-sessions.ts +40 -7
  107. package/src/index.ts +7 -2
  108. package/src/interaction.ts +1 -0
  109. package/src/lib/error-message.ts +37 -0
  110. package/src/lib/format.ts +73 -9
  111. package/src/lib/interaction-errors.ts +15 -0
  112. package/src/model-policy.ts +6 -1
  113. package/src/provider.tsx +12 -4
  114. package/src/session-ui.ts +1 -1
  115. package/src/timeline/projection.ts +12 -1
  116. package/styles/compiled.css +14 -30
  117. package/dist/chunk-45GE35O7.js.map +0 -1
  118. package/dist/chunk-6NEZPF53.js.map +0 -1
  119. package/dist/chunk-CHPKSLPN.js.map +0 -1
  120. package/dist/chunk-DV2DBNPQ.js.map +0 -1
  121. package/dist/chunk-FGBCYQOT.js.map +0 -1
  122. package/dist/chunk-KGENSZKS.js.map +0 -1
  123. package/dist/chunk-KZ73RL76.js.map +0 -1
  124. package/dist/chunk-LZ3KHA55.js.map +0 -1
  125. package/dist/chunk-UINDK6C3.js.map +0 -1
  126. package/dist/chunk-UJRYEWZI.js.map +0 -1
  127. package/dist/chunk-VGIKFIO2.js.map +0 -1
  128. package/dist/chunk-WZ55DVGK.js.map +0 -1
  129. package/dist/chunk-YRYF2GLG.js.map +0 -1
  130. package/dist/chunk-YSMMDOOE.js.map +0 -1
  131. package/dist/chunk-Z3DISEQN.js.map +0 -1
  132. package/dist/components/timeline-pill-geometry.d.ts +0 -8
  133. package/dist/components/timeline-pill-placement.d.ts +0 -24
  134. package/dist/timeline-pill-geometry-Z23V243M.js +0 -110
  135. package/dist/timeline-pill-geometry-Z23V243M.js.map +0 -1
  136. package/src/components/timeline-pill-geometry.ts +0 -169
  137. package/src/components/timeline-pill-placement.ts +0 -99
  138. /package/dist/{browser-viewer-5X47I6T2.js.map → browser-viewer-63TXMINB.js.map} +0 -0
  139. /package/dist/{chunk-5EXGU7UF.js.map → chunk-55DDDVKU.js.map} +0 -0
  140. /package/dist/{chunk-BI57CQUV.js.map → chunk-I7IWRQLN.js.map} +0 -0
  141. /package/dist/{chunk-XA6R7DJQ.js.map → chunk-IQUCRAT7.js.map} +0 -0
  142. /package/dist/{computer-viewer-7GF35SNY.js.map → computer-viewer-X2MBRZY4.js.map} +0 -0
  143. /package/dist/{latest-question-3BJGKQQ6.js.map → latest-question-CNEZ5KAN.js.map} +0 -0
  144. /package/dist/{model-policy-picker-menu-PUTA5JOB.js.map → model-policy-picker-menu-GN2LTB7Z.js.map} +0 -0
  145. /package/dist/{sandbox-files-AEJTWGT4.js.map → sandbox-files-BBC47SLD.js.map} +0 -0
@@ -25,6 +25,7 @@ import {
25
25
  } from "lucide-react";
26
26
  import {
27
27
  type ClipboardEvent,
28
+ type CompositionEvent,
28
29
  type KeyboardEvent,
29
30
  type MouseEvent,
30
31
  type PointerEvent,
@@ -290,9 +291,12 @@ export function ComputerViewer({
290
291
  const rfbStream =
291
292
  frames.attachment?.stream.kind === "direct_rfb" ? frames.attachment.stream : null;
292
293
  const machineLocked = selectedRegistrySession?.failureCode === "machine_locked";
294
+ const controlUnavailable = computer.controlError !== null;
295
+ const connectionState = controlUnavailable ? "error" : frames.state;
293
296
  // RFB has one combined input switch; partial native input stays view-only.
294
297
  const rfbInputEnabled =
295
298
  !machineLocked &&
299
+ !controlUnavailable &&
296
300
  computer.session?.capabilities?.pointerInput === true &&
297
301
  computer.session?.capabilities?.keyboardInput === true;
298
302
  const rfbCapability = useMemo<DesktopStreamCapability | null>(() => {
@@ -441,6 +445,7 @@ export function ComputerViewer({
441
445
 
442
446
  const perform = useCallback(
443
447
  async (action: ComputerAction, frame: ComputerFrame | null): Promise<void> => {
448
+ if (computer.controlError) throw computer.controlError;
444
449
  let receipt;
445
450
  if (action.type === "pointer") {
446
451
  if (!frame) throw new Error("Desktop view is not ready for pointer input.");
@@ -452,9 +457,14 @@ export function ComputerViewer({
452
457
  throw new Error(receipt.error?.message ?? "Desktop input did not complete.");
453
458
  }
454
459
  },
455
- [act, actFromFrame],
460
+ [act, actFromFrame, computer.controlError],
456
461
  );
457
462
 
463
+ const reconnect = () => {
464
+ void refreshComputer();
465
+ frames.reconnect();
466
+ };
467
+
458
468
  const copyFromRfb = useCallback(
459
469
  (event: ClipboardEvent<HTMLDivElement>) => {
460
470
  if (!rfbInputEnabled || !rfbStream || computer.session?.capabilities?.clipboard !== true)
@@ -586,7 +596,7 @@ export function ComputerViewer({
586
596
  }}
587
597
  onCreate={hideGenericCreate ? undefined : createComputer}
588
598
  onRefresh={() => {
589
- if (frames.state === "error" && !generationLossSelected && !generationLossFrames) {
599
+ if (connectionState === "error" && !generationLossSelected && !generationLossFrames) {
590
600
  frames.reconnect();
591
601
  }
592
602
  void Promise.all([refreshRegistry(), refreshComputer()]);
@@ -644,7 +654,20 @@ export function ComputerViewer({
644
654
  />
645
655
  {!rfbInputEnabled ? (
646
656
  <div className="pointer-events-none absolute bottom-3 left-1/2 -translate-x-1/2 rounded-full border border-white/10 bg-black/70 px-3 py-1 text-[11px] text-white/80 backdrop-blur">
647
- View only · direct input unavailable
657
+ {controlUnavailable ? (
658
+ <>
659
+ Desktop controls unavailable{" "}
660
+ <button
661
+ type="button"
662
+ onClick={reconnect}
663
+ className="pointer-events-auto ml-2 font-medium underline"
664
+ >
665
+ Reconnect
666
+ </button>
667
+ </>
668
+ ) : (
669
+ "View only · direct input unavailable"
670
+ )}
648
671
  </div>
649
672
  ) : null}
650
673
  </div>
@@ -660,19 +683,19 @@ export function ComputerViewer({
660
683
  observation={computer.observation}
661
684
  target={computer.selectedTarget}
662
685
  machineLocked={machineLocked}
663
- connectionState={frames.state}
664
- connectionError={frames.error ?? computer.error}
686
+ controlUnavailable={controlUnavailable}
687
+ connectionState={connectionState}
688
+ connectionError={computer.controlError ?? frames.error ?? computer.error}
665
689
  mutating={computer.mutating}
666
690
  backgroundActions={computer.session?.capabilities?.backgroundActions === true}
691
+ backgroundInput={computer.session?.capabilities?.backgroundInput === true}
667
692
  clipboardEnabled={computer.session?.capabilities?.clipboard === true}
668
693
  pointerInput={computer.session?.capabilities?.pointerInput === true}
669
694
  keyboardInput={computer.session?.capabilities?.keyboardInput === true}
670
695
  onAction={perform}
671
696
  onReadClipboard={computer.readClipboard}
672
697
  onReconnect={
673
- generationLossSelected || generationLossFrames
674
- ? () => undefined
675
- : frames.reconnect
698
+ generationLossSelected || generationLossFrames ? () => undefined : reconnect
676
699
  }
677
700
  onError={(cause) => notifyError(cause, "Desktop input failed.")}
678
701
  />
@@ -680,7 +703,8 @@ export function ComputerViewer({
680
703
  {showControls ? (
681
704
  <ComputerSemanticPanel
682
705
  observation={computer.observation}
683
- mutating={computer.mutating}
706
+ mutating={computer.mutating || controlUnavailable}
707
+ controlUnavailable={controlUnavailable}
684
708
  onAction={(action) =>
685
709
  void perform(action, null).catch((cause) =>
686
710
  notifyError(cause, "Desktop action failed."),
@@ -692,10 +716,11 @@ export function ComputerViewer({
692
716
  <ComputerStatusBar
693
717
  session={computer.session}
694
718
  target={computer.selectedTarget}
695
- connectionState={frames.state}
719
+ connectionState={connectionState}
720
+ controlUnavailable={controlUnavailable}
696
721
  refreshing={registry.refreshing}
697
722
  showControls={showControls}
698
- controlCount={semanticNodes(computer.observation).length}
723
+ controlCount={semanticControls(computer.observation).length}
699
724
  onToggleControls={() => setShowControls((current) => !current)}
700
725
  />
701
726
  </>
@@ -1025,10 +1050,12 @@ function ComputerViewport(props: {
1025
1050
  observation: ComputerObservation | null;
1026
1051
  target: ComputerTarget | null;
1027
1052
  machineLocked: boolean;
1053
+ controlUnavailable: boolean;
1028
1054
  connectionState: string;
1029
1055
  connectionError: Error | null;
1030
1056
  mutating: boolean;
1031
1057
  backgroundActions: boolean;
1058
+ backgroundInput: boolean;
1032
1059
  clipboardEnabled: boolean;
1033
1060
  pointerInput: boolean;
1034
1061
  keyboardInput: boolean;
@@ -1039,6 +1066,7 @@ function ComputerViewport(props: {
1039
1066
  }) {
1040
1067
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
1041
1068
  const inputRef = useRef<HTMLTextAreaElement | null>(null);
1069
+ const composingRef = useRef(false);
1042
1070
  const pointerStartRef = useRef<PointerStart | null>(null);
1043
1071
  const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
1044
1072
  const lastClickRef = useRef<{
@@ -1076,8 +1104,12 @@ function ComputerViewport(props: {
1076
1104
  const streamFailed = props.connectionState === "error";
1077
1105
  const rawInputEnabled =
1078
1106
  !streamFailed &&
1107
+ !props.controlUnavailable &&
1079
1108
  !props.machineLocked &&
1080
- (!props.backgroundActions || props.target?.kind === "screen" || props.target?.focused === true);
1109
+ (props.backgroundInput ||
1110
+ !props.backgroundActions ||
1111
+ props.target?.kind === "screen" ||
1112
+ props.target?.focused === true);
1081
1113
 
1082
1114
  const clearBufferedInput = useCallback(() => {
1083
1115
  if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
@@ -1088,12 +1120,29 @@ function ComputerViewport(props: {
1088
1120
  pendingTextRef.current = null;
1089
1121
  pointerStartRef.current = null;
1090
1122
  lastClickRef.current = null;
1123
+ composingRef.current = false;
1091
1124
  if (inputRef.current) inputRef.current.value = "";
1092
1125
  }, []);
1093
1126
 
1094
1127
  const pointerInputEnabled = rawInputEnabled && props.pointerInput;
1095
1128
  const keyboardInputEnabled = rawInputEnabled && props.keyboardInput;
1096
1129
 
1130
+ useLayoutEffect(() => {
1131
+ const canvas = canvasRef.current;
1132
+ const viewport = canvas?.parentElement;
1133
+ if (!canvas || !viewport) return;
1134
+ const fit = () => fitComputerCanvas(canvas);
1135
+ fit();
1136
+ if (typeof ResizeObserver === "undefined") {
1137
+ window.addEventListener("resize", fit);
1138
+ return () => window.removeEventListener("resize", fit);
1139
+ }
1140
+ // The canvas is absolute, so fitting it cannot resize the observed dock.
1141
+ const observer = new ResizeObserver(fit);
1142
+ observer.observe(viewport);
1143
+ return () => observer.disconnect();
1144
+ }, []);
1145
+
1097
1146
  const paintQueuedFrames = useCallback(() => {
1098
1147
  if (decodingFrameRef.current) return;
1099
1148
  decodingFrameRef.current = true;
@@ -1403,6 +1452,13 @@ function ComputerViewport(props: {
1403
1452
 
1404
1453
  const keyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
1405
1454
  if (!keyboardInputEnabled) return;
1455
+ if (
1456
+ composingRef.current ||
1457
+ event.nativeEvent.isComposing ||
1458
+ event.nativeEvent.keyCode === 229
1459
+ ) {
1460
+ return;
1461
+ }
1406
1462
  flushPendingWheel();
1407
1463
  const command = event.metaKey || event.ctrlKey;
1408
1464
  if (
@@ -1456,7 +1512,8 @@ function ComputerViewport(props: {
1456
1512
  });
1457
1513
  };
1458
1514
 
1459
- const input = (value: string) => {
1515
+ const input = (value: string, nativeComposing = false) => {
1516
+ if (composingRef.current || nativeComposing) return;
1460
1517
  if (!value || !keyboardInputEnabled) return;
1461
1518
  flushPendingWheel();
1462
1519
  flushPendingClick();
@@ -1474,6 +1531,11 @@ function ComputerViewport(props: {
1474
1531
  if (inputRef.current) inputRef.current.value = "";
1475
1532
  };
1476
1533
 
1534
+ const compositionEnd = (event: CompositionEvent<HTMLTextAreaElement>) => {
1535
+ composingRef.current = false;
1536
+ input(event.currentTarget.value || event.data);
1537
+ };
1538
+
1477
1539
  const showCanvas =
1478
1540
  props.frame !== null &&
1479
1541
  paintedFrame !== null &&
@@ -1501,7 +1563,13 @@ function ComputerViewport(props: {
1501
1563
  <textarea
1502
1564
  ref={inputRef}
1503
1565
  defaultValue=""
1504
- onInput={(event) => input(event.currentTarget.value)}
1566
+ onInput={(event) =>
1567
+ input(event.currentTarget.value, (event.nativeEvent as InputEvent).isComposing)
1568
+ }
1569
+ onCompositionStart={() => {
1570
+ composingRef.current = true;
1571
+ }}
1572
+ onCompositionEnd={compositionEnd}
1505
1573
  onKeyDown={keyDown}
1506
1574
  onCopy={copy}
1507
1575
  onPaste={paste}
@@ -1516,6 +1584,7 @@ function ComputerViewport(props: {
1516
1584
  <ComputerViewportFallback
1517
1585
  observation={props.observation}
1518
1586
  machineLocked={props.machineLocked}
1587
+ controlUnavailable={props.controlUnavailable}
1519
1588
  connectionState={props.connectionState}
1520
1589
  error={props.connectionError}
1521
1590
  onAction={(action) => enqueue(action, null)}
@@ -1563,6 +1632,7 @@ function assertExactComputerClipboard(clipboard: ComputerClipboard, expected: st
1563
1632
  function ComputerViewportFallback(props: {
1564
1633
  observation: ComputerObservation | null;
1565
1634
  machineLocked: boolean;
1635
+ controlUnavailable: boolean;
1566
1636
  connectionState: string;
1567
1637
  error: Error | null;
1568
1638
  onAction: (action: ComputerAction) => void;
@@ -1582,7 +1652,7 @@ function ComputerViewportFallback(props: {
1582
1652
  );
1583
1653
  }
1584
1654
  const controlFailure = interactionControlFailureFromError(props.error);
1585
- const interactive = semanticNodes(props.observation)
1655
+ const interactive = semanticControls(props.observation)
1586
1656
  .filter((node) => semanticAction(node) !== null)
1587
1657
  .slice(0, 10);
1588
1658
  return (
@@ -1597,9 +1667,11 @@ function ComputerViewportFallback(props: {
1597
1667
  <p className="text-og-menu font-medium text-og-fg">
1598
1668
  {isAttachedChromeGenerationLossError(props.error)
1599
1669
  ? "Chrome reconnected—open a fresh browser/desktop."
1600
- : props.error
1601
- ? "Live view disconnected"
1602
- : computerConnectionLabel(props.connectionState)}
1670
+ : props.controlUnavailable
1671
+ ? "Desktop controls unavailable"
1672
+ : props.error
1673
+ ? "Live view disconnected"
1674
+ : computerConnectionLabel(props.connectionState)}
1603
1675
  </p>
1604
1676
  </div>
1605
1677
  {props.error ? (
@@ -1609,7 +1681,7 @@ function ComputerViewportFallback(props: {
1609
1681
  : (controlFailure?.message ?? props.error.message)}
1610
1682
  </p>
1611
1683
  ) : null}
1612
- {interactive.length > 0 ? (
1684
+ {interactive.length > 0 && !props.controlUnavailable ? (
1613
1685
  <div className="mt-3 border-t border-og-border pt-3">
1614
1686
  <p className="mb-2 text-og-xs text-og-fg-subtle">App controls remain available</p>
1615
1687
  <div className="flex flex-wrap gap-1.5">
@@ -1646,18 +1718,26 @@ function ComputerViewportFallback(props: {
1646
1718
  function ComputerSemanticPanel(props: {
1647
1719
  observation: ComputerObservation | null;
1648
1720
  mutating: boolean;
1721
+ controlUnavailable: boolean;
1649
1722
  onAction: (action: ComputerAction) => void;
1650
1723
  }) {
1651
- const nodes = semanticNodes(props.observation).slice(0, 100);
1724
+ const nodes = semanticControls(props.observation).slice(0, 100);
1652
1725
  return (
1653
1726
  <aside className="box-border max-h-[40%] w-full shrink-0 overflow-y-auto border-t border-og-border bg-og-surface-1 p-2 @xl/computer-viewer:max-h-none @xl/computer-viewer:w-64 @xl/computer-viewer:border-t-0 @xl/computer-viewer:border-l">
1654
1727
  <div className="mb-2 flex items-center gap-1.5 px-1 text-og-xs font-medium uppercase tracking-[0.1em] text-og-fg-subtle">
1655
1728
  <KeyboardIcon className="size-3" /> App controls
1656
1729
  </div>
1657
- {nodes.length === 0 ? (
1730
+ {props.controlUnavailable ? (
1658
1731
  <p className="px-1 py-2 text-og-control leading-5 text-og-fg-muted">
1659
- This view has no accessibility controls. Use the live image instead.
1732
+ Reconnect to use app controls.
1660
1733
  </p>
1734
+ ) : null}
1735
+ {nodes.length === 0 ? (
1736
+ props.controlUnavailable ? null : (
1737
+ <p className="px-1 py-2 text-og-control leading-5 text-og-fg-muted">
1738
+ This view has no accessibility controls. Use the live image instead.
1739
+ </p>
1740
+ )
1661
1741
  ) : (
1662
1742
  <div className="space-y-0.5">
1663
1743
  {nodes.map((node) => (
@@ -1747,6 +1827,7 @@ function ComputerStatusBar(props: {
1747
1827
  session: ComputerSession | null;
1748
1828
  target: ComputerTarget | null;
1749
1829
  connectionState: string;
1830
+ controlUnavailable: boolean;
1750
1831
  refreshing: boolean;
1751
1832
  showControls: boolean;
1752
1833
  controlCount: number;
@@ -1762,14 +1843,22 @@ function ComputerStatusBar(props: {
1762
1843
  )}
1763
1844
  />
1764
1845
  <span>
1765
- {props.connectionState === "live" ? "Live" : computerConnectionLabel(props.connectionState)}
1846
+ {props.controlUnavailable
1847
+ ? "Controls unavailable"
1848
+ : props.connectionState === "live"
1849
+ ? "Live"
1850
+ : computerConnectionLabel(props.connectionState)}
1766
1851
  </span>
1767
1852
  <span className="min-w-0 flex-1 truncate">
1768
- {screen
1769
- ? "Full screen · input may move pointer and focus"
1770
- : props.session?.capabilities?.backgroundActions
1771
- ? "Window · app controls work in the background"
1772
- : (props.target?.kind ?? "Desktop")}
1853
+ {props.controlUnavailable
1854
+ ? "Reconnect to use desktop input"
1855
+ : screen
1856
+ ? "Full screen · input may move pointer and focus"
1857
+ : props.session?.capabilities?.backgroundInput
1858
+ ? "Window · clicks and typing stay in the background"
1859
+ : props.session?.capabilities?.backgroundActions
1860
+ ? "Window · app controls work in the background"
1861
+ : (props.target?.kind ?? "Desktop")}
1773
1862
  </span>
1774
1863
  {screen ? <MousePointer2Icon className="size-3" aria-hidden /> : null}
1775
1864
  {props.refreshing ? <LoaderCircleIcon className="size-3 animate-spin" /> : null}
@@ -1804,11 +1893,37 @@ function ComputerNotice(props: { icon: ReactNode; text: string; className?: stri
1804
1893
  );
1805
1894
  }
1806
1895
 
1807
- function semanticNodes(observation: ComputerObservation | null): InteractionSemanticNode[] {
1896
+ const structuralRoles = new Set([
1897
+ "frame",
1898
+ "panel",
1899
+ "section",
1900
+ "document",
1901
+ "document web",
1902
+ "static",
1903
+ "static text",
1904
+ "heading",
1905
+ "paragraph",
1906
+ "notification",
1907
+ "separator",
1908
+ "tool bar",
1909
+ "page tab list",
1910
+ ]);
1911
+
1912
+ function semanticControls(observation: ComputerObservation | null): InteractionSemanticNode[] {
1808
1913
  if (observation?.semantic?.kind !== "snapshot") return [];
1809
1914
  const result: InteractionSemanticNode[] = [];
1810
1915
  const visit = (node: InteractionSemanticNode) => {
1811
- result.push(node);
1916
+ const role = node.role.trim().toLowerCase();
1917
+ const label = node.name?.trim() || node.identifier?.trim();
1918
+ if (
1919
+ node.actions.includes("set_value") ||
1920
+ (label &&
1921
+ label.toLowerCase() !== role &&
1922
+ !structuralRoles.has(role) &&
1923
+ semanticAction(node) !== null)
1924
+ ) {
1925
+ result.push(node);
1926
+ }
1812
1927
  node.children?.forEach(visit);
1813
1928
  };
1814
1929
  observation.semantic.roots.forEach(visit);
@@ -1840,11 +1955,26 @@ function paintCanvas(
1840
1955
  width: number,
1841
1956
  height: number,
1842
1957
  ): void {
1958
+ const dimensionsChanged = canvas.width !== width || canvas.height !== height;
1843
1959
  canvas.width = width;
1844
1960
  canvas.height = height;
1845
1961
  const context = canvas.getContext("2d", { alpha: false });
1846
1962
  if (!context) throw new Error("Desktop canvas is unavailable.");
1847
1963
  context.drawImage(image, 0, 0, width, height);
1964
+ if (dimensionsChanged) fitComputerCanvas(canvas);
1965
+ }
1966
+
1967
+ function fitComputerCanvas(canvas: HTMLCanvasElement): void {
1968
+ const viewport = canvas.parentElement;
1969
+ if (!viewport || canvas.width <= 0 || canvas.height <= 0) return;
1970
+ const scale = Math.min(
1971
+ viewport.clientWidth / canvas.width,
1972
+ viewport.clientHeight / canvas.height,
1973
+ );
1974
+ // Hidden docks retain their previous fit until their content box is visible.
1975
+ if (!Number.isFinite(scale) || scale <= 0) return;
1976
+ canvas.style.width = `${canvas.width * scale}px`;
1977
+ canvas.style.height = `${canvas.height * scale}px`;
1848
1978
  }
1849
1979
 
1850
1980
  function loadImage(url: string): Promise<HTMLImageElement> {
@@ -1,6 +1,7 @@
1
1
  import type { GeneratedVideoReceipt, VideoArtifactPlaybackSource } from "@opengeni/sdk";
2
2
  import { useEffect, useState } from "react";
3
3
  import { cn } from "../lib/cn";
4
+ import { useErrorMessage } from "../lib/error-message";
4
5
  import type { VideoArtifactPlaybackLoader } from "../timeline";
5
6
 
6
7
  export type GeneratedVideoPlayerProps = {
@@ -22,6 +23,7 @@ export function GeneratedVideoPlayer({
22
23
  className,
23
24
  label = "Generated video",
24
25
  }: GeneratedVideoPlayerProps) {
26
+ const formatError = useErrorMessage();
25
27
  const [retry, setRetry] = useState(0);
26
28
  const [state, setState] = useState<SourceState>({ kind: "loading" });
27
29
 
@@ -36,12 +38,12 @@ export function GeneratedVideoPlayer({
36
38
  if (controller.signal.aborted) return;
37
39
  setState({
38
40
  kind: "error",
39
- message: error instanceof Error ? error.message : "Playback is unavailable.",
41
+ message: formatError(error),
40
42
  });
41
43
  },
42
44
  );
43
45
  return () => controller.abort();
44
- }, [loadPlaybackSource, receipt.artifact.artifactId, retry]);
46
+ }, [loadPlaybackSource, receipt.artifact.artifactId, retry, formatError]);
45
47
 
46
48
  if (state.kind === "loading") {
47
49
  return (
@@ -8,6 +8,7 @@ import type {
8
8
  import { ChevronDownIcon, ChevronUpIcon, MessageCircleQuestionIcon } from "lucide-react";
9
9
  import { useEffect, useId, useRef, useState, type FormEvent, type ReactNode } from "react";
10
10
  import { cn } from "../lib/cn";
11
+ import { useErrorMessage } from "../lib/error-message";
11
12
 
12
13
  export type HumanInputAnswerDraft = {
13
14
  values: string[];
@@ -15,6 +16,8 @@ export type HumanInputAnswerDraft = {
15
16
  otherSelected: boolean;
16
17
  };
17
18
 
19
+ class SkillReviewVerificationError extends Error {}
20
+
18
21
  export type HumanInputFormMessages = {
19
22
  title: string;
20
23
  description: string;
@@ -116,6 +119,7 @@ function HumanInputRequestForm({
116
119
  defaultCollapsed = false,
117
120
  className,
118
121
  }: HumanInputFormProps) {
122
+ const formatError = useErrorMessage();
119
123
  const messages = { ...defaultHumanInputFormMessages, ...messageOverrides };
120
124
  const singleQuestion = request.questions.length === 1 ? request.questions[0]! : null;
121
125
  const resolvedTitle =
@@ -184,7 +188,7 @@ function HumanInputRequestForm({
184
188
  questions.map(async (question) => {
185
189
  if (!loadSkillReview)
186
190
  throw new Error(
187
- "This client cannot preview Skill files. Open this request in Opengeni to review it.",
191
+ "This client cannot preview Skill files. Ask your administrator for a review-capable client.",
188
192
  );
189
193
  const reference = question.reference;
190
194
  const record = await loadSkillReview(reference);
@@ -194,7 +198,9 @@ function HumanInputRequestForm({
194
198
  (record.removalOperationId ?? undefined) !== reference.removalOperationId ||
195
199
  !record.files.some((file) => file.path === "SKILL.md")
196
200
  ) {
197
- throw new Error("The requested Skill revision could not be verified.");
201
+ throw new SkillReviewVerificationError(
202
+ "The requested Skill revision could not be verified.",
203
+ );
198
204
  }
199
205
  return [question.id, record] as const;
200
206
  }),
@@ -214,13 +220,20 @@ function HumanInputRequestForm({
214
220
  loader: loadSkillReview,
215
221
  identity: reviewIdentity,
216
222
  records: {},
217
- error: cause instanceof Error ? cause.message : "Could not load the Skill files.",
223
+ error: formatError(
224
+ cause,
225
+ cause instanceof SkillReviewVerificationError
226
+ ? cause.message
227
+ : !loadSkillReview
228
+ ? "This client cannot preview Skill files. Ask your administrator for a review-capable client."
229
+ : undefined,
230
+ ),
218
231
  });
219
232
  });
220
233
  return () => {
221
234
  current = false;
222
235
  };
223
- }, [loadSkillReview, reviewIdentity, reviewReload]);
236
+ }, [loadSkillReview, reviewIdentity, reviewReload, formatError]);
224
237
  const visibleReviews =
225
238
  reviews?.loader === loadSkillReview && reviews?.identity === reviewIdentity ? reviews : null;
226
239
  const preview = (question: HumanInputQuestion) => {
@@ -319,7 +332,7 @@ function HumanInputRequestForm({
319
332
  await onSubmit(response);
320
333
  } catch (cause) {
321
334
  if (generation === submissionGeneration.current) {
322
- setSubmissionError(cause instanceof Error ? cause.message : String(cause));
335
+ setSubmissionError(formatError(cause));
323
336
  }
324
337
  } finally {
325
338
  if (generation === submissionGeneration.current) {