@opengeni/react 0.48.0 → 0.57.0
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/README.md +138 -2
- package/dist/artifacts-document.d.ts +3 -0
- package/dist/artifacts-document.js +17 -0
- package/dist/artifacts-document.js.map +1 -0
- package/dist/artifacts-presentation.d.ts +3 -0
- package/dist/artifacts-presentation.js +17 -0
- package/dist/artifacts-presentation.js.map +1 -0
- package/dist/artifacts-spreadsheet.d.ts +3 -0
- package/dist/artifacts-spreadsheet.js +17 -0
- package/dist/artifacts-spreadsheet.js.map +1 -0
- package/dist/artifacts.d.ts +2 -0
- package/dist/artifacts.js +199 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/{chunk-NJ7M2B6Z.js → chunk-5UQPNLIO.js} +16 -8
- package/dist/{chunk-NJ7M2B6Z.js.map → chunk-5UQPNLIO.js.map} +1 -1
- package/dist/{chunk-LHLYWG5R.js → chunk-6KLX2RH5.js} +461 -38
- package/dist/chunk-6KLX2RH5.js.map +1 -0
- package/dist/chunk-7WCLAEFV.js +2828 -0
- package/dist/chunk-7WCLAEFV.js.map +1 -0
- package/dist/{chunk-I4MPFDRP.js → chunk-B6WPZSTT.js} +104 -104
- package/dist/{chunk-I4MPFDRP.js.map → chunk-B6WPZSTT.js.map} +1 -1
- package/dist/{chunk-EKZH6IDG.js → chunk-C5ZPSSS5.js} +111 -1
- package/dist/chunk-C5ZPSSS5.js.map +1 -0
- package/dist/{chunk-OL6QVXGF.js → chunk-HA46CYQK.js} +152 -8
- package/dist/chunk-HA46CYQK.js.map +1 -0
- package/dist/chunk-HUIKIDM5.js +2148 -0
- package/dist/chunk-HUIKIDM5.js.map +1 -0
- package/dist/{chunk-FWXGSNXT.js → chunk-IGC6A2ZI.js} +585 -34
- package/dist/chunk-IGC6A2ZI.js.map +1 -0
- package/dist/chunk-JVYQRRC4.js +53 -0
- package/dist/chunk-JVYQRRC4.js.map +1 -0
- package/dist/chunk-KCGXPP5V.js +157 -0
- package/dist/chunk-KCGXPP5V.js.map +1 -0
- package/dist/chunk-LLOLUHVG.js +5937 -0
- package/dist/chunk-LLOLUHVG.js.map +1 -0
- package/dist/{chunk-WYA65Y3F.js → chunk-MKPIAGVL.js} +13 -3
- package/dist/chunk-MKPIAGVL.js.map +1 -0
- package/dist/chunk-PEI6RANI.js +3234 -0
- package/dist/chunk-PEI6RANI.js.map +1 -0
- package/dist/chunk-R3AZOAB5.js +933 -0
- package/dist/chunk-R3AZOAB5.js.map +1 -0
- package/dist/{chunk-O4VXOBGF.js → chunk-WSK7G5LE.js} +70 -2
- package/dist/chunk-WSK7G5LE.js.map +1 -0
- package/dist/{chunk-23EJ676W.js → chunk-XBZWOM2K.js} +9 -4
- package/dist/chunk-XBZWOM2K.js.map +1 -0
- package/dist/{chunk-XCNTXWBB.js → chunk-Y7OEDUJ5.js} +856 -338
- package/dist/chunk-Y7OEDUJ5.js.map +1 -0
- package/dist/client.d.ts +10 -2
- package/dist/clipboard.d.ts +1 -0
- package/dist/clipboard.js +7 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/commands/index.d.ts +3 -3
- package/dist/commands/registry.d.ts +1 -1
- package/dist/commands/types.d.ts +1 -1
- package/dist/components/approval-surface.d.ts +2 -2
- package/dist/components/artifacts/artifact-surface.d.ts +18 -0
- package/dist/components/artifacts/document-editor.d.ts +172 -0
- package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
- package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
- package/dist/components/artifacts/editable-document.d.ts +24 -0
- package/dist/components/artifacts/editable-presentation.d.ts +24 -0
- package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
- package/dist/components/artifacts/index.d.ts +9 -0
- package/dist/components/artifacts/presentation-editor.d.ts +297 -0
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
- package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
- package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
- package/dist/components/browser-input.d.ts +7 -0
- package/dist/components/browser-viewer.d.ts +30 -0
- package/dist/components/chat-composer.d.ts +8 -8
- package/dist/components/code-editor.d.ts +25 -3
- package/dist/components/command-palette.d.ts +2 -2
- package/dist/components/composer-transcription-control.d.ts +2 -2
- package/dist/components/composer.d.ts +17 -17
- package/dist/components/computer-viewer.d.ts +24 -0
- package/dist/components/copy-button.d.ts +2 -2
- package/dist/components/desktop-viewer.d.ts +50 -22
- package/dist/components/diff-view.d.ts +1 -1
- package/dist/components/enrollment-consent.d.ts +1 -1
- package/dist/components/enrollment-device-flow.d.ts +1 -1
- package/dist/components/file-browser.d.ts +2 -2
- package/dist/components/fleet-tile.d.ts +1 -1
- package/dist/components/generated-video-player.d.ts +10 -0
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +2 -2
- package/dist/components/interaction-intervention-banner.d.ts +11 -0
- package/dist/components/interactive-workbench-viewers.d.ts +2 -0
- package/dist/components/machine-card.d.ts +2 -2
- package/dist/components/machine-dock-bar.d.ts +3 -3
- package/dist/components/machine-health-pill.d.ts +2 -2
- package/dist/components/machine-input-display.d.ts +1 -1
- package/dist/components/machine-metrics.d.ts +2 -2
- package/dist/components/machine-status-pill.d.ts +4 -4
- package/dist/components/machines/health.d.ts +1 -1
- package/dist/components/machines/machine-detail.d.ts +3 -3
- package/dist/components/machines/metric-history-chart.d.ts +1 -1
- package/dist/components/machines/metric-sparkline.d.ts +2 -2
- package/dist/components/machines/series.d.ts +2 -2
- package/dist/components/machines-dashboard.d.ts +2 -2
- package/dist/components/markdown.d.ts +1 -1
- package/dist/components/message-timeline.d.ts +12 -5
- package/dist/components/model-picker.d.ts +2 -2
- package/dist/components/model-policy-picker.d.ts +7 -7
- package/dist/components/pierre-diff.d.ts +1 -1
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/queue-draft-policy.d.ts +1 -1
- package/dist/components/queue-surface-implementation.d.ts +3 -3
- package/dist/components/queue-surface-state.d.ts +4 -4
- package/dist/components/queue-surface.d.ts +5 -5
- package/dist/components/sandbox-files.d.ts +3 -3
- package/dist/components/sandbox-terminal.d.ts +24 -4
- package/dist/components/sandbox-workspace.d.ts +28 -9
- package/dist/components/session-chrome.d.ts +4 -4
- package/dist/components/session-status.d.ts +2 -2
- package/dist/components/timeline-annotation-selection.d.ts +9 -0
- package/dist/components/timeline-annotations-dialog.d.ts +16 -0
- package/dist/components/timeline-annotations.d.ts +13 -0
- package/dist/components/tooltip.d.ts +4 -4
- package/dist/components/user-message-body.d.ts +2 -2
- package/dist/components/workbench-changes.d.ts +2 -2
- package/dist/components/workspace-dock.d.ts +1 -1
- package/dist/composer.d.ts +11 -11
- package/dist/composer.js +7 -7
- package/dist/hooks/internal.d.ts +1 -1
- package/dist/hooks/use-attached-browsers.d.ts +21 -0
- package/dist/hooks/use-auth-runs.d.ts +32 -0
- package/dist/hooks/use-available-models.d.ts +2 -2
- package/dist/hooks/use-billing-usage.d.ts +1 -1
- package/dist/hooks/use-browser-downloads.d.ts +25 -0
- package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
- package/dist/hooks/use-browser-identities.d.ts +30 -0
- package/dist/hooks/use-browser-session.d.ts +29 -0
- package/dist/hooks/use-browser-sessions.d.ts +32 -0
- package/dist/hooks/use-channels.d.ts +26 -0
- package/dist/hooks/use-codex-accounts.d.ts +2 -2
- package/dist/hooks/use-composer.d.ts +36 -9
- package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
- package/dist/hooks/use-computer-session.d.ts +29 -0
- package/dist/hooks/use-computer-sessions.d.ts +28 -0
- package/dist/hooks/use-desktop-stream.d.ts +5 -1
- package/dist/hooks/use-environments.d.ts +1 -1
- package/dist/hooks/use-file-attachments.d.ts +1 -1
- package/dist/hooks/use-goal.d.ts +2 -2
- package/dist/hooks/use-human-input.d.ts +2 -2
- package/dist/hooks/use-interaction-interventions.d.ts +27 -0
- package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
- package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
- package/dist/hooks/use-machine-chip.d.ts +2 -2
- package/dist/hooks/use-machines.d.ts +4 -4
- package/dist/hooks/use-network-routes.d.ts +25 -0
- package/dist/hooks/use-packs.d.ts +7 -2
- package/dist/hooks/use-rigs.d.ts +1 -1
- package/dist/hooks/use-sandbox-files.d.ts +6 -4
- package/dist/hooks/use-sandbox-git.d.ts +6 -5
- package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
- package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
- package/dist/hooks/use-session-capabilities.d.ts +5 -4
- package/dist/hooks/use-session-control.d.ts +1 -1
- package/dist/hooks/use-session-events.d.ts +2 -2
- package/dist/hooks/use-session-lineage.d.ts +1 -1
- package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
- package/dist/hooks/use-session.d.ts +2 -2
- package/dist/hooks/use-site-auth-connections.d.ts +25 -0
- package/dist/hooks/use-slash-commands.d.ts +1 -1
- package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
- package/dist/hooks/use-terminal-stream.d.ts +11 -2
- package/dist/hooks/use-turn-queue.d.ts +2 -2
- package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
- package/dist/hooks/use-voice-input.d.ts +1 -1
- package/dist/hooks/use-workspace-capture.d.ts +1 -1
- package/dist/hooks/use-workspace-edit.d.ts +1 -1
- package/dist/hooks/use-workspace-sessions.d.ts +1 -1
- package/dist/hooks/use-workspaces.d.ts +1 -1
- package/dist/index.d.ts +162 -155
- package/dist/index.js +1822 -1703
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +33 -0
- package/dist/interaction.js +550 -0
- package/dist/interaction.js.map +1 -0
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
- package/dist/interactive-workbench-viewers-TOHBXJEE.js.map +1 -0
- package/dist/lib/host-platform.d.ts +5 -0
- package/dist/lib/relay-wire.d.ts +24 -1
- package/dist/lib/sandbox-liveness.d.ts +9 -0
- package/dist/lib/terminal-capability.d.ts +5 -0
- package/dist/lib/xterm-theme.d.ts +1 -1
- package/dist/machines.d.ts +30 -30
- package/dist/machines.js +2 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/provider.d.ts +6 -5
- package/dist/{queue-surface-implementation-U5JFG7HT.js → queue-surface-implementation-2KVJTYXH.js} +100 -48
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
- package/dist/realtime/dropdown-menu.d.ts +8 -8
- package/dist/realtime/realtime-control.d.ts +8 -8
- package/dist/realtime.d.ts +4 -4
- package/dist/realtime.js +38 -11
- package/dist/realtime.js.map +1 -1
- package/dist/session-context.d.ts +28 -2
- package/dist/session-ui.d.ts +14 -14
- package/dist/session-ui.js +5 -4
- package/dist/session.d.ts +28 -28
- package/dist/session.js +10 -10
- package/dist/timeline/activity-rail.d.ts +5 -3
- package/dist/timeline/disclosure-context.d.ts +1 -1
- package/dist/timeline/fleet-decision-projection.d.ts +1 -1
- package/dist/timeline/fleet-decision-row.d.ts +3 -3
- package/dist/timeline/index.d.ts +19 -19
- package/dist/timeline/parsers.d.ts +4 -1
- package/dist/timeline/projection.d.ts +3 -3
- package/dist/timeline/registry.d.ts +8 -2
- package/dist/timeline/screenshot-lightbox.d.ts +2 -2
- package/dist/timeline/shared.d.ts +21 -10
- package/dist/timeline/tool-diff.d.ts +2 -2
- package/dist/timeline/tool-renderers.d.ts +1 -1
- package/dist/timeline/turn-summary.d.ts +3 -3
- package/dist/timeline/types.d.ts +44 -3
- package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
- package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
- package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
- package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
- package/package.json +36 -7
- package/src/artifacts-document.ts +31 -0
- package/src/artifacts-presentation.ts +36 -0
- package/src/artifacts-spreadsheet.ts +21 -0
- package/src/artifacts.ts +89 -0
- package/src/client.ts +157 -1
- package/src/clipboard.ts +2 -0
- package/src/components/artifacts/artifact-surface.tsx +93 -0
- package/src/components/artifacts/document-editor.tsx +2255 -0
- package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
- package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
- package/src/components/artifacts/editable-document.tsx +657 -0
- package/src/components/artifacts/editable-presentation.tsx +1020 -0
- package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
- package/src/components/artifacts/index.ts +104 -0
- package/src/components/artifacts/presentation-editor.tsx +3148 -0
- package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
- package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
- package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
- package/src/components/browser-input.ts +65 -0
- package/src/components/browser-viewer.tsx +2702 -0
- package/src/components/chat-composer.tsx +13 -0
- package/src/components/code-editor.tsx +89 -30
- package/src/components/composer-transcription-control.tsx +10 -6
- package/src/components/computer-viewer.tsx +1636 -0
- package/src/components/desktop-viewer.tsx +215 -74
- package/src/components/generated-video-player.tsx +89 -0
- package/src/components/interaction-intervention-banner.tsx +99 -0
- package/src/components/interactive-workbench-viewers.ts +2 -0
- package/src/components/machine-input-display.ts +3 -0
- package/src/components/machines/machine-detail.tsx +1 -1
- package/src/components/message-timeline.tsx +289 -32
- package/src/components/model-policy-picker.tsx +15 -0
- package/src/components/queue-surface-implementation.tsx +116 -60
- package/src/components/sandbox-files.tsx +2 -2
- package/src/components/sandbox-terminal.tsx +128 -46
- package/src/components/sandbox-workspace.tsx +256 -90
- package/src/components/session-chrome.tsx +53 -13
- package/src/components/timeline-annotation-selection.tsx +221 -0
- package/src/components/timeline-annotations-dialog.tsx +213 -0
- package/src/components/timeline-annotations.tsx +84 -0
- package/src/components/workbench-changes.tsx +188 -31
- package/src/components/workspace-dock.tsx +59 -5
- package/src/hooks/use-attached-browsers.ts +154 -0
- package/src/hooks/use-auth-runs.ts +278 -0
- package/src/hooks/use-browser-downloads.ts +218 -0
- package/src/hooks/use-browser-frame-stream.ts +425 -0
- package/src/hooks/use-browser-identities.ts +266 -0
- package/src/hooks/use-browser-session.ts +543 -0
- package/src/hooks/use-browser-sessions.ts +286 -0
- package/src/hooks/use-channels.ts +99 -0
- package/src/hooks/use-composer.ts +769 -24
- package/src/hooks/use-computer-frame-stream.ts +434 -0
- package/src/hooks/use-computer-session.ts +462 -0
- package/src/hooks/use-computer-sessions.ts +251 -0
- package/src/hooks/use-desktop-stream.ts +39 -7
- package/src/hooks/use-interaction-interventions.ts +242 -0
- package/src/hooks/use-interaction-invalidation.ts +66 -0
- package/src/hooks/use-machine-chip.ts +15 -3
- package/src/hooks/use-machines.ts +33 -3
- package/src/hooks/use-network-routes.ts +214 -0
- package/src/hooks/use-packs.ts +51 -0
- package/src/hooks/use-sandbox-files.ts +98 -55
- package/src/hooks/use-sandbox-git.ts +93 -38
- package/src/hooks/use-sandbox-terminal.ts +3 -2
- package/src/hooks/use-session-capabilities.ts +59 -46
- package/src/hooks/use-site-auth-connections.ts +226 -0
- package/src/hooks/use-supergrok-accounts.ts +81 -0
- package/src/hooks/use-terminal-stream.ts +359 -47
- package/src/hooks/use-video-artifact-playback.ts +28 -0
- package/src/hooks/use-workspace-edit.ts +2 -1
- package/src/index.ts +18 -0
- package/src/interaction.ts +101 -0
- package/src/lib/host-platform.ts +10 -0
- package/src/lib/relay-wire.ts +47 -4
- package/src/lib/sandbox-liveness.ts +11 -0
- package/src/lib/terminal-capability.ts +10 -0
- package/src/model-policy.ts +19 -4
- package/src/provider.tsx +133 -1
- package/src/realtime/realtime-control.tsx +34 -6
- package/src/session-context.ts +156 -1
- package/src/timeline/activity-rail.tsx +25 -3
- package/src/timeline/index.ts +3 -0
- package/src/timeline/parsers.ts +14 -1
- package/src/timeline/projection.ts +269 -14
- package/src/timeline/registry.ts +13 -1
- package/src/timeline/screenshot-lightbox.tsx +19 -9
- package/src/timeline/shared.tsx +56 -8
- package/src/timeline/tool-renderers.tsx +303 -50
- package/src/timeline/types.ts +57 -3
- package/styles/compiled.css +312 -21
- package/styles/index.css +15 -0
- package/styles/responsive.css +13 -1
- package/dist/chunk-23EJ676W.js.map +0 -1
- package/dist/chunk-EKZH6IDG.js.map +0 -1
- package/dist/chunk-FWXGSNXT.js.map +0 -1
- package/dist/chunk-LHLYWG5R.js.map +0 -1
- package/dist/chunk-O4VXOBGF.js.map +0 -1
- package/dist/chunk-OL6QVXGF.js.map +0 -1
- package/dist/chunk-WYA65Y3F.js.map +0 -1
- package/dist/chunk-XCNTXWBB.js.map +0 -1
- package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
- package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
- package/dist/queue-surface-implementation-U5JFG7HT.js.map +0 -1
|
@@ -4,8 +4,16 @@ import type {
|
|
|
4
4
|
DesktopStreamCapability,
|
|
5
5
|
} from "@opengeni/sdk";
|
|
6
6
|
import { LoaderCircleIcon, MonitorIcon, MousePointerClickIcon, WifiOffIcon } from "lucide-react";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type ClipboardEvent,
|
|
9
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
useEffect,
|
|
12
|
+
useRef,
|
|
13
|
+
useState,
|
|
14
|
+
} from "react";
|
|
8
15
|
import { cn } from "../lib/cn";
|
|
16
|
+
import { interactionHostPlatform, type InteractionHostPlatform } from "../lib/host-platform";
|
|
9
17
|
import { useDesktopStream } from "../hooks/use-desktop-stream";
|
|
10
18
|
import type { DesktopWebSocketFactory } from "../hooks/use-relay-frame-stream";
|
|
11
19
|
|
|
@@ -24,26 +32,43 @@ export type DesktopViewerProps = {
|
|
|
24
32
|
scaleViewport?: boolean | undefined;
|
|
25
33
|
/** Custom RFB factory (tests / a WebRTC swap). Defaults to lazy @novnc/novnc. */
|
|
26
34
|
rfbFactory?: DesktopRfbFactory | undefined;
|
|
35
|
+
/** Authenticated controller protocols for proxied RFB transports. */
|
|
36
|
+
webSocketProtocols?: string[] | undefined;
|
|
27
37
|
/** Custom socket factory for the self-hosted `relay-frames` transport (tests).
|
|
28
38
|
* Defaults to `new WebSocket(url)`. Mirrors `rfbFactory`. */
|
|
29
39
|
webSocketFactory?: DesktopWebSocketFactory | undefined;
|
|
30
40
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
41
|
+
* Optional host-owned paste path. ComputerSession hosts use their native,
|
|
42
|
+
* target-scoped clipboard action instead of relying on an RFB server to sync
|
|
43
|
+
* ClientCutText into the graphical seat. Return true once the host accepted
|
|
44
|
+
* the text. Without a host authority the viewer leaves paste untouched rather
|
|
45
|
+
* than claiming success for an unreliable display-server clipboard heuristic.
|
|
46
|
+
*/
|
|
47
|
+
onPasteText?: ((text: string) => boolean) | undefined;
|
|
48
|
+
/** OS behind the RFB seat. Used only to translate the host's primary shortcut
|
|
49
|
+
* (Cmd on macOS, Ctrl elsewhere) to the target's primary modifier. */
|
|
50
|
+
targetPlatform?: "linux" | "macos" | "windows" | null | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* @deprecated Desktop acknowledgment is automatic. Retained as an ignored
|
|
53
|
+
* compatibility prop so existing embedders keep compiling.
|
|
36
54
|
*/
|
|
37
55
|
renderConsentGate?: ((onAccept: () => void, shared: boolean) => ReactNode) | undefined;
|
|
38
|
-
/** Called when the default consent gate's accept button is pressed. */
|
|
39
|
-
onAcknowledge?: (() => void) | undefined;
|
|
40
56
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*
|
|
57
|
+
* Called once when the viewer mounts. Hosts use this to engage the desktop
|
|
58
|
+
* holder immediately when the Desktop tab opens.
|
|
59
|
+
*/
|
|
60
|
+
onActivate?: (() => void) | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Called automatically when the server requires acknowledgment of the
|
|
63
|
+
* un-redacted (and possibly shared) pixel plane. The viewer shows its ordinary
|
|
64
|
+
* opening state while this completes; no click-through gate is rendered.
|
|
65
|
+
*/
|
|
66
|
+
onAcknowledge?: (() => void | Promise<void>) | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Whether the host has the viewer attach engaged. Drives the cold-state
|
|
69
|
+
* behaviour: when watching, a cold-but-warmable lease AUTO-WARMS (and re-warms
|
|
70
|
+
* when the box drains) instead of dead-ending. Mounting the viewer counts as
|
|
71
|
+
* watching when this is omitted.
|
|
47
72
|
*/
|
|
48
73
|
watching?: boolean | undefined;
|
|
49
74
|
/**
|
|
@@ -51,8 +76,7 @@ export type DesktopViewerProps = {
|
|
|
51
76
|
* already been recorded — only the box drained). The host wires this to
|
|
52
77
|
* "engage the viewer attach + re-negotiate". Called automatically when a
|
|
53
78
|
* watched desktop is found cold-but-warmable, and behind the manual retry on
|
|
54
|
-
* the warming notice. Distinct from
|
|
55
|
-
* consent-bearing warm.
|
|
79
|
+
* the warming notice. Distinct from the automatic acknowledgment request.
|
|
56
80
|
*/
|
|
57
81
|
onWarm?: (() => void) | undefined;
|
|
58
82
|
/** Shown when transport is null (headless backend / degraded / disabled). */
|
|
@@ -75,13 +99,14 @@ export type DesktopViewerProps = {
|
|
|
75
99
|
/**
|
|
76
100
|
* The derived desktop surface state. A single source of truth so the overlay,
|
|
77
101
|
* the scrim, the auto-warm effect, and the watchdog all agree. Priority order
|
|
78
|
-
* (highest first): viewer-cap → unavailable →
|
|
79
|
-
* → error → connecting → connected.
|
|
102
|
+
* (highest first): viewer-cap → unavailable → acknowledgment-error → warming →
|
|
103
|
+
* connect-failed → error → opening/connecting → connected.
|
|
80
104
|
*/
|
|
81
105
|
type DesktopUiState =
|
|
82
106
|
| "viewer_cap" // 429 — the per-session live-viewer limit is reached.
|
|
83
107
|
| "unavailable" // genuinely unsupported (headless/policy/os/backend/not-provisioned).
|
|
84
|
-
| "
|
|
108
|
+
| "acknowledgment_error" // automatic un-redacted/shared acknowledgment failed.
|
|
109
|
+
| "opening" // acknowledgment or viewer attach is in flight.
|
|
85
110
|
| "warming" // cold-but-warmable: box not running yet (we auto-warm + spin).
|
|
86
111
|
| "connecting" // url is live, RFB negotiating/handshaking (we spin, with a watchdog).
|
|
87
112
|
| "connect_failed" // watchdog fired: url present but never connected.
|
|
@@ -122,10 +147,11 @@ function isHardUnavailable(reason: CapabilityUnavailableReason | null): boolean
|
|
|
122
147
|
* reason).
|
|
123
148
|
*
|
|
124
149
|
* Warming: a cold-but-warmable lease (`reason: "lease_cold"`) is NOT a dead end.
|
|
125
|
-
* When the
|
|
126
|
-
*
|
|
127
|
-
* cold it re-warms.
|
|
128
|
-
* keep a clear,
|
|
150
|
+
* When the viewer is mounted, it asks the host to engage the desktop and records
|
|
151
|
+
* any required acknowledgment automatically. A cold-but-warmable box then
|
|
152
|
+
* (re)warms through `onWarm`; if it later drains to cold it re-warms.
|
|
153
|
+
* Genuinely-unavailable surfaces (headless/policy/os/backend) keep a clear,
|
|
154
|
+
* static unavailable notice.
|
|
129
155
|
*/
|
|
130
156
|
export function DesktopViewer({
|
|
131
157
|
capability,
|
|
@@ -133,8 +159,11 @@ export function DesktopViewer({
|
|
|
133
159
|
showControlToggle = true,
|
|
134
160
|
scaleViewport,
|
|
135
161
|
rfbFactory,
|
|
162
|
+
webSocketProtocols,
|
|
136
163
|
webSocketFactory,
|
|
137
|
-
|
|
164
|
+
onPasteText,
|
|
165
|
+
targetPlatform,
|
|
166
|
+
onActivate,
|
|
138
167
|
onAcknowledge,
|
|
139
168
|
watching,
|
|
140
169
|
onWarm,
|
|
@@ -146,7 +175,13 @@ export function DesktopViewer({
|
|
|
146
175
|
className,
|
|
147
176
|
}: DesktopViewerProps) {
|
|
148
177
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
149
|
-
const
|
|
178
|
+
const onActivateRef = useRef(onActivate);
|
|
179
|
+
const onAcknowledgeRef = useRef(onAcknowledge);
|
|
180
|
+
onActivateRef.current = onActivate;
|
|
181
|
+
onAcknowledgeRef.current = onAcknowledge;
|
|
182
|
+
const acknowledgmentAttemptedRef = useRef(false);
|
|
183
|
+
const [acknowledgmentError, setAcknowledgmentError] = useState<Error | null>(null);
|
|
184
|
+
const [acknowledgmentRetryNonce, setAcknowledgmentRetryNonce] = useState(0);
|
|
150
185
|
// Local control state when not externally controlled. The server gate
|
|
151
186
|
// (`capability.mode`) is the hard ceiling — a read-only deployment can never
|
|
152
187
|
// be flipped to interactive regardless of this toggle.
|
|
@@ -156,16 +191,16 @@ export function DesktopViewer({
|
|
|
156
191
|
const wantControl = externallyControlled ? interactive : takeControl;
|
|
157
192
|
const inControl = Boolean(wantControl) && serverAllowsControl;
|
|
158
193
|
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
const isWatching = watching ??
|
|
194
|
+
// Mounting this component means the Desktop surface is being viewed. A host
|
|
195
|
+
// may still pass its holder state explicitly while the activation callback
|
|
196
|
+
// flips that state on the first mount.
|
|
197
|
+
const isWatching = watching ?? true;
|
|
163
198
|
|
|
164
199
|
// ── Decide the rendered state (before touching the stream hook) ─────────────
|
|
165
200
|
const transportNull = !capability || capability.transport === null;
|
|
166
201
|
const reason = capability?.reason ?? null;
|
|
167
|
-
const
|
|
168
|
-
capability?.requiresAcknowledgment === true && capability.acknowledged !== true
|
|
202
|
+
const needsAcknowledgment =
|
|
203
|
+
capability?.requiresAcknowledgment === true && capability.acknowledged !== true;
|
|
169
204
|
// No live address yet on an otherwise-live transport (post-ack, mid-warm).
|
|
170
205
|
const noLiveAddress = Boolean(capability) && !transportNull && !capability!.url;
|
|
171
206
|
|
|
@@ -176,9 +211,45 @@ export function DesktopViewer({
|
|
|
176
211
|
// Genuinely-unavailable: transport null for a HARD reason (not lease_cold).
|
|
177
212
|
const hardUnavailable = transportNull && isHardUnavailable(reason);
|
|
178
213
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
214
|
+
// Selecting Desktop mounts the viewer, which is itself the user's intent to
|
|
215
|
+
// open it. Engage the holder immediately — no separate "watch" click.
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
onActivateRef.current?.();
|
|
218
|
+
}, []);
|
|
219
|
+
|
|
220
|
+
// Preserve the server-side acknowledgment/audit contract without making the
|
|
221
|
+
// user click through a disclosure card. One automatic attempt runs for each
|
|
222
|
+
// unacknowledged episode; a genuine request failure gets an explicit retry.
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (!needsAcknowledgment) {
|
|
225
|
+
acknowledgmentAttemptedRef.current = false;
|
|
226
|
+
setAcknowledgmentError(null);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (acknowledgmentAttemptedRef.current) return;
|
|
230
|
+
acknowledgmentAttemptedRef.current = true;
|
|
231
|
+
setAcknowledgmentError(null);
|
|
232
|
+
const acknowledge = onAcknowledgeRef.current;
|
|
233
|
+
if (!acknowledge) {
|
|
234
|
+
setAcknowledgmentError(new Error("Desktop acknowledgment is not configured."));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
let active = true;
|
|
238
|
+
void Promise.resolve()
|
|
239
|
+
.then(() => acknowledge())
|
|
240
|
+
.catch((cause) => {
|
|
241
|
+
if (!active) return;
|
|
242
|
+
setAcknowledgmentError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
243
|
+
});
|
|
244
|
+
return () => {
|
|
245
|
+
active = false;
|
|
246
|
+
};
|
|
247
|
+
}, [needsAcknowledgment, acknowledgmentRetryNonce]);
|
|
248
|
+
|
|
249
|
+
const retryAcknowledgment = () => {
|
|
250
|
+
acknowledgmentAttemptedRef.current = false;
|
|
251
|
+
setAcknowledgmentError(null);
|
|
252
|
+
setAcknowledgmentRetryNonce((nonce) => nonce + 1);
|
|
182
253
|
};
|
|
183
254
|
|
|
184
255
|
// Release control WITHOUT ever swallowing a key the desktop needs. Esc, and
|
|
@@ -218,13 +289,15 @@ export function DesktopViewer({
|
|
|
218
289
|
// Do NOT open a socket while the viewer-cap (429) notice is showing — the slot
|
|
219
290
|
// is already exhausted, so connecting would only burn a doomed attempt (and in
|
|
220
291
|
// tests leak an unhandled ws error from the never-resolving tunnel URL).
|
|
221
|
-
const connectCapability =
|
|
292
|
+
const connectCapability =
|
|
293
|
+
!transportNull && !needsAcknowledgment && !viewerCapReached ? capability : null;
|
|
222
294
|
const stream = useDesktopStream({
|
|
223
295
|
capability: connectCapability,
|
|
224
296
|
containerRef,
|
|
225
297
|
interactive: inControl,
|
|
226
298
|
...(scaleViewport !== undefined ? { scaleViewport } : {}),
|
|
227
299
|
...(rfbFactory ? { rfbFactory } : {}),
|
|
300
|
+
...(webSocketProtocols ? { webSocketProtocols } : {}),
|
|
228
301
|
...(webSocketFactory ? { webSocketFactory } : {}),
|
|
229
302
|
});
|
|
230
303
|
|
|
@@ -238,21 +311,19 @@ export function DesktopViewer({
|
|
|
238
311
|
|
|
239
312
|
// ── AUTO-WARM ───────────────────────────────────────────────────────────────
|
|
240
313
|
// When the user is watching and the desktop is cold-but-warmable, ask the host
|
|
241
|
-
// to (re)warm the box. This covers BOTH (a) the
|
|
314
|
+
// to (re)warm the box. This covers BOTH (a) the desktop was just opened and
|
|
242
315
|
// (b) a previously-warm box that drained back to cold under a live viewer. We
|
|
243
316
|
// fire once per distinct cold episode (keyed on the lease epoch + url) so we
|
|
244
317
|
// don't spam the attach while a single warm is in flight. `onWarm` is the
|
|
245
|
-
// no-re-ack path;
|
|
246
|
-
// first warm still rides consent and subsequent drains fall back to it.
|
|
318
|
+
// no-re-ack path; acknowledgment remains a separate automatic request.
|
|
247
319
|
const warmKeyRef = useRef<string | null>(null);
|
|
248
320
|
useEffect(() => {
|
|
249
|
-
if (!isWatching || !coldWarmable ||
|
|
321
|
+
if (!isWatching || !coldWarmable || needsAcknowledgment || viewerCapReached) {
|
|
250
322
|
// Reset the de-dupe once we leave the cold episode so a future drain warms.
|
|
251
323
|
if (!coldWarmable) warmKeyRef.current = null;
|
|
252
324
|
return;
|
|
253
325
|
}
|
|
254
|
-
|
|
255
|
-
if (!warm) return;
|
|
326
|
+
if (!onWarm) return;
|
|
256
327
|
// Key the de-dupe on the cell fields available to the viewer: while cold,
|
|
257
328
|
// `reason`/`url`/`expiresAt` are stable, so we warm exactly once; once the box
|
|
258
329
|
// warms the cell changes (url+expiresAt minted) and `coldWarmable` flips false,
|
|
@@ -260,10 +331,10 @@ export function DesktopViewer({
|
|
|
260
331
|
const key = `${capability?.reason ?? ""}:${capability?.url ?? ""}:${capability?.expiresAt ?? ""}`;
|
|
261
332
|
if (warmKeyRef.current === key) return; // already kicked this episode.
|
|
262
333
|
warmKeyRef.current = key;
|
|
263
|
-
|
|
334
|
+
onWarm();
|
|
264
335
|
// capability identity is the trigger; leaseEpoch/expiresAt key the episode.
|
|
265
336
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
266
|
-
}, [isWatching, coldWarmable,
|
|
337
|
+
}, [isWatching, coldWarmable, needsAcknowledgment, viewerCapReached, capability, onWarm]);
|
|
267
338
|
|
|
268
339
|
// ── TAB RE-ATTACH ───────────────────────────────────────────────────────────
|
|
269
340
|
// The "stuck Watching, never connects" bug: the RFB socket never (re)fires when
|
|
@@ -327,11 +398,51 @@ export function DesktopViewer({
|
|
|
327
398
|
stream.reconnect();
|
|
328
399
|
};
|
|
329
400
|
|
|
401
|
+
const paste = (event: ClipboardEvent<HTMLDivElement>) => {
|
|
402
|
+
if (!inControl) return;
|
|
403
|
+
const text = event.clipboardData.getData("text/plain");
|
|
404
|
+
if (!text && !event.clipboardData.types.includes("text/plain")) return;
|
|
405
|
+
if (!onPasteText?.(text)) return;
|
|
406
|
+
event.preventDefault();
|
|
407
|
+
event.stopPropagation();
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const primaryShortcut = (event: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
411
|
+
if (!inControl || !targetPlatform) return;
|
|
412
|
+
const hostPlatform = interactionHostPlatform();
|
|
413
|
+
// noVNC maps a Mac Command key to remote Alt before the actual chord key
|
|
414
|
+
// arrives. Suppress that standalone physical modifier; the complete target
|
|
415
|
+
// chord below owns both its down and up events.
|
|
416
|
+
if (isHostPrimaryModifier(event, hostPlatform)) {
|
|
417
|
+
event.stopPropagation();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const chord = desktopPrimaryShortcut(event, hostPlatform, targetPlatform);
|
|
421
|
+
if (!chord) return;
|
|
422
|
+
// Copy/paste must keep the browser's default clipboard dispatch alive, but
|
|
423
|
+
// noVNC must not also send the host modifier into the remote seat.
|
|
424
|
+
if (chord === "clipboard") {
|
|
425
|
+
event.stopPropagation();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const sent = chord.every(({ keysym, code, down }) => stream.sendKey?.(keysym, code, down));
|
|
429
|
+
if (!sent) return;
|
|
430
|
+
event.preventDefault();
|
|
431
|
+
event.stopPropagation();
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const primaryModifierUp = (event: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
435
|
+
if (inControl && isHostPrimaryModifier(event, interactionHostPlatform())) {
|
|
436
|
+
event.stopPropagation();
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
330
440
|
// ── Resolve the single UI state (priority order) ────────────────────────────
|
|
331
441
|
let uiState: DesktopUiState;
|
|
332
442
|
if (viewerCapReached) uiState = "viewer_cap";
|
|
333
443
|
else if (hardUnavailable) uiState = "unavailable";
|
|
334
|
-
else if (
|
|
444
|
+
else if (acknowledgmentError) uiState = "acknowledgment_error";
|
|
445
|
+
else if (needsAcknowledgment) uiState = "opening";
|
|
335
446
|
else if (coldWarmable) uiState = "warming";
|
|
336
447
|
else if (connected) uiState = "connected";
|
|
337
448
|
else if (stream.error) uiState = "error";
|
|
@@ -340,7 +451,7 @@ export function DesktopViewer({
|
|
|
340
451
|
|
|
341
452
|
// An overlay blocks the surface for every state except connected; the toggle
|
|
342
453
|
// and the live scrim are suppressed under an overlay.
|
|
343
|
-
const overlayShown = uiState !== "connected" && uiState !== "connecting";
|
|
454
|
+
const overlayShown = uiState !== "connected" && uiState !== "connecting" && uiState !== "opening";
|
|
344
455
|
const showToggle = showControlToggle && !overlayShown;
|
|
345
456
|
|
|
346
457
|
let overlay: ReactNode = null;
|
|
@@ -358,11 +469,11 @@ export function DesktopViewer({
|
|
|
358
469
|
renderUnavailable?.(reason) ??
|
|
359
470
|
defaultNotice("Desktop unavailable", unavailableCopy(reason));
|
|
360
471
|
break;
|
|
361
|
-
case "
|
|
362
|
-
overlay =
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
472
|
+
case "acknowledgment_error":
|
|
473
|
+
overlay = defaultNotice(
|
|
474
|
+
"Couldn’t open desktop",
|
|
475
|
+
acknowledgmentError?.message ?? "The desktop acknowledgment failed.",
|
|
476
|
+
retryAcknowledgment,
|
|
366
477
|
);
|
|
367
478
|
break;
|
|
368
479
|
case "warming":
|
|
@@ -382,6 +493,7 @@ export function DesktopViewer({
|
|
|
382
493
|
reconnect,
|
|
383
494
|
);
|
|
384
495
|
break;
|
|
496
|
+
case "opening":
|
|
385
497
|
case "connecting":
|
|
386
498
|
case "connected":
|
|
387
499
|
overlay = null;
|
|
@@ -400,6 +512,10 @@ export function DesktopViewer({
|
|
|
400
512
|
data-state={stream.state}
|
|
401
513
|
data-ui-state={uiState}
|
|
402
514
|
data-in-control={inControl ? "true" : undefined}
|
|
515
|
+
data-target-platform={targetPlatform ?? undefined}
|
|
516
|
+
onKeyDownCapture={primaryShortcut}
|
|
517
|
+
onKeyUpCapture={primaryModifierUp}
|
|
518
|
+
onPasteCapture={paste}
|
|
403
519
|
>
|
|
404
520
|
{/* noVNC mount target. It appends a `width:100%;height:100%` `_screen` div
|
|
405
521
|
and AUTOSCALES the 1280x800 framebuffer to fit THIS box. We pin it to
|
|
@@ -420,7 +536,7 @@ export function DesktopViewer({
|
|
|
420
536
|
rectangle before the first framebuffer paints. Only shown in the
|
|
421
537
|
`connecting` UI-state (every other non-connected state has an explicit
|
|
422
538
|
overlay). A spinner + transitional copy makes it feel live. */}
|
|
423
|
-
{uiState === "connecting" && (
|
|
539
|
+
{(uiState === "opening" || uiState === "connecting") && (
|
|
424
540
|
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-3 text-og-fg-subtle">
|
|
425
541
|
<span className="relative flex items-center justify-center">
|
|
426
542
|
<MonitorIcon className="size-8 opacity-30" strokeWidth={1.5} />
|
|
@@ -429,7 +545,9 @@ export function DesktopViewer({
|
|
|
429
545
|
strokeWidth={1.25}
|
|
430
546
|
/>
|
|
431
547
|
</span>
|
|
432
|
-
<span className="text-og-sm">
|
|
548
|
+
<span className="text-og-sm">
|
|
549
|
+
{uiState === "opening" ? "Opening the desktop…" : "Connecting to the desktop…"}
|
|
550
|
+
</span>
|
|
433
551
|
</div>
|
|
434
552
|
)}
|
|
435
553
|
|
|
@@ -474,6 +592,51 @@ export function DesktopViewer({
|
|
|
474
592
|
);
|
|
475
593
|
}
|
|
476
594
|
|
|
595
|
+
type RfbKeyEvent = { keysym: number; code: string; down: boolean };
|
|
596
|
+
|
|
597
|
+
function isHostPrimaryModifier(
|
|
598
|
+
event: Pick<ReactKeyboardEvent<HTMLDivElement>, "key">,
|
|
599
|
+
hostPlatform: InteractionHostPlatform,
|
|
600
|
+
): boolean {
|
|
601
|
+
return event.key === (hostPlatform === "mac" ? "Meta" : "Control");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** Translate one host-primary chord into deterministic RFB events for the
|
|
605
|
+
* target OS. noVNC otherwise treats a Mac Command key as remote Alt, which
|
|
606
|
+
* makes ordinary Cmd+A/Z shortcuts silently wrong on Linux sandboxes. */
|
|
607
|
+
export function desktopPrimaryShortcut(
|
|
608
|
+
event: Pick<
|
|
609
|
+
ReactKeyboardEvent<HTMLDivElement>,
|
|
610
|
+
"altKey" | "ctrlKey" | "key" | "metaKey" | "shiftKey" | "code"
|
|
611
|
+
>,
|
|
612
|
+
hostPlatform: InteractionHostPlatform,
|
|
613
|
+
targetPlatform: "linux" | "macos" | "windows",
|
|
614
|
+
): RfbKeyEvent[] | "clipboard" | null {
|
|
615
|
+
const primaryHeld = hostPlatform === "mac" ? event.metaKey : event.ctrlKey;
|
|
616
|
+
if (!primaryHeld || [...event.key].length !== 1) return null;
|
|
617
|
+
const key = event.key.toLowerCase();
|
|
618
|
+
if (!event.altKey && (key === "c" || key === "v")) return "clipboard";
|
|
619
|
+
const primary =
|
|
620
|
+
targetPlatform === "macos"
|
|
621
|
+
? { keysym: 0xffeb, code: "MetaLeft" }
|
|
622
|
+
: { keysym: 0xffe3, code: "ControlLeft" };
|
|
623
|
+
const modifiers = [
|
|
624
|
+
primary,
|
|
625
|
+
...(event.altKey ? [{ keysym: 0xffe9, code: "AltLeft" }] : []),
|
|
626
|
+
...(event.shiftKey ? [{ keysym: 0xffe1, code: "ShiftLeft" }] : []),
|
|
627
|
+
];
|
|
628
|
+
const keyEvent = {
|
|
629
|
+
keysym: key.codePointAt(0)!,
|
|
630
|
+
code: event.code || (/^[a-z]$/u.test(key) ? `Key${key.toUpperCase()}` : key),
|
|
631
|
+
};
|
|
632
|
+
return [
|
|
633
|
+
...modifiers.map((modifier) => ({ ...modifier, down: true })),
|
|
634
|
+
{ ...keyEvent, down: true },
|
|
635
|
+
{ ...keyEvent, down: false },
|
|
636
|
+
...modifiers.reverse().map((modifier) => ({ ...modifier, down: false })),
|
|
637
|
+
];
|
|
638
|
+
}
|
|
639
|
+
|
|
477
640
|
/**
|
|
478
641
|
* The primary WATCHING-state call-to-action: a large, centered pill button
|
|
479
642
|
* overlaid on the desktop inviting the viewer to drive the mouse & keyboard.
|
|
@@ -630,28 +793,6 @@ function WarmingNotice({ onRetry }: { onRetry?: (() => void) | undefined }) {
|
|
|
630
793
|
);
|
|
631
794
|
}
|
|
632
795
|
|
|
633
|
-
function DefaultConsentGate({ shared, onAccept }: { shared: boolean; onAccept: () => void }) {
|
|
634
|
-
return (
|
|
635
|
-
<div className="max-w-sm rounded-og-lg border border-og-border bg-og-bg p-4 text-center text-og-base text-og-fg">
|
|
636
|
-
<div className="mb-1 font-medium">Watch the live desktop?</div>
|
|
637
|
-
<p className="mb-3 text-og-sm text-og-fg-subtle">
|
|
638
|
-
The desktop pixel stream is <strong>un-redacted</strong> — it can show secrets the agent
|
|
639
|
-
prints on screen.
|
|
640
|
-
{shared
|
|
641
|
-
? " This sandbox is shared: you'll also see other sessions' agents on the same screen."
|
|
642
|
-
: ""}
|
|
643
|
-
</p>
|
|
644
|
-
<button
|
|
645
|
-
type="button"
|
|
646
|
-
onClick={onAccept}
|
|
647
|
-
className="rounded-og-sm bg-og-accent px-3 py-1.5 text-og-sm font-medium text-og-accent-fg pointer-coarse:min-h-11"
|
|
648
|
-
>
|
|
649
|
-
I understand — show the desktop
|
|
650
|
-
</button>
|
|
651
|
-
</div>
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
796
|
function defaultNotice(title: string, body: string, onRetry?: () => void): ReactNode {
|
|
656
797
|
return (
|
|
657
798
|
<div className="max-w-sm rounded-og-lg border border-og-border bg-og-bg p-4 text-center text-og-base text-og-fg">
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { GeneratedVideoReceipt, VideoArtifactPlaybackSource } from "@opengeni/sdk";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import type { VideoArtifactPlaybackLoader } from "../timeline";
|
|
5
|
+
|
|
6
|
+
export type GeneratedVideoPlayerProps = {
|
|
7
|
+
receipt: GeneratedVideoReceipt;
|
|
8
|
+
loadPlaybackSource: VideoArtifactPlaybackLoader;
|
|
9
|
+
className?: string | undefined;
|
|
10
|
+
label?: string | undefined;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type SourceState =
|
|
14
|
+
| { kind: "loading" }
|
|
15
|
+
| { kind: "ready"; source: VideoArtifactPlaybackSource }
|
|
16
|
+
| { kind: "error"; message: string };
|
|
17
|
+
|
|
18
|
+
/** Native, Range-based playback. Video bytes never pass through React or the SDK heap. */
|
|
19
|
+
export function GeneratedVideoPlayer({
|
|
20
|
+
receipt,
|
|
21
|
+
loadPlaybackSource,
|
|
22
|
+
className,
|
|
23
|
+
label = "Generated video",
|
|
24
|
+
}: GeneratedVideoPlayerProps) {
|
|
25
|
+
const [retry, setRetry] = useState(0);
|
|
26
|
+
const [state, setState] = useState<SourceState>({ kind: "loading" });
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
setState({ kind: "loading" });
|
|
31
|
+
void loadPlaybackSource(receipt.artifact.artifactId, controller.signal).then(
|
|
32
|
+
(source) => {
|
|
33
|
+
if (!controller.signal.aborted) setState({ kind: "ready", source });
|
|
34
|
+
},
|
|
35
|
+
(error: unknown) => {
|
|
36
|
+
if (controller.signal.aborted) return;
|
|
37
|
+
setState({
|
|
38
|
+
kind: "error",
|
|
39
|
+
message: error instanceof Error ? error.message : "Playback is unavailable.",
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
return () => controller.abort();
|
|
44
|
+
}, [loadPlaybackSource, receipt.artifact.artifactId, retry]);
|
|
45
|
+
|
|
46
|
+
if (state.kind === "loading") {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
aria-label={`Loading ${label.toLowerCase()}`}
|
|
50
|
+
className={cn(
|
|
51
|
+
"aspect-video w-full animate-pulse rounded-og-md bg-og-surface-2 motion-reduce:animate-none",
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (state.kind === "error") {
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
role="status"
|
|
61
|
+
className={cn(
|
|
62
|
+
"rounded-og-md border border-og-status-failed/30 bg-og-status-failed/5 px-3 py-2 text-og-sm text-og-status-failed",
|
|
63
|
+
className,
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
{state.message}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<video
|
|
73
|
+
key={state.source.url}
|
|
74
|
+
aria-label={label}
|
|
75
|
+
className={cn(
|
|
76
|
+
"max-h-[32rem] w-full rounded-og-md bg-black object-contain shadow-sm",
|
|
77
|
+
className,
|
|
78
|
+
)}
|
|
79
|
+
controls
|
|
80
|
+
playsInline
|
|
81
|
+
preload="metadata"
|
|
82
|
+
src={state.source.url}
|
|
83
|
+
onError={() => {
|
|
84
|
+
if (retry === 0) setRetry(1);
|
|
85
|
+
else setState({ kind: "error", message: "Video playback failed." });
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { InteractionIntervention } from "@opengeni/sdk/interaction";
|
|
2
|
+
import { CheckIcon, CircleAlertIcon, LoaderCircleIcon, XIcon } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
|
|
5
|
+
export type InteractionInterventionBannerProps = {
|
|
6
|
+
interventions: InteractionIntervention[];
|
|
7
|
+
activeTargetId?: string | null | undefined;
|
|
8
|
+
mutating?: boolean | undefined;
|
|
9
|
+
className?: string | undefined;
|
|
10
|
+
onOpen?: ((intervention: InteractionIntervention) => void) | undefined;
|
|
11
|
+
onResolve: (intervention: InteractionIntervention, outcome: "completed" | "dismissed") => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Compact durable human-action prompt shared by Browser and Computer viewers. */
|
|
15
|
+
export function InteractionInterventionBanner({
|
|
16
|
+
interventions,
|
|
17
|
+
activeTargetId,
|
|
18
|
+
mutating = false,
|
|
19
|
+
className,
|
|
20
|
+
onOpen,
|
|
21
|
+
onResolve,
|
|
22
|
+
}: InteractionInterventionBannerProps) {
|
|
23
|
+
const intervention = interventions[0];
|
|
24
|
+
if (!intervention) return null;
|
|
25
|
+
const targetOpen = activeTargetId === undefined || activeTargetId === intervention.targetId;
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={cn(
|
|
29
|
+
"flex shrink-0 items-center gap-2 border-b border-og-status-waiting/25 bg-og-status-waiting/[0.07] px-3 py-2",
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
role="status"
|
|
33
|
+
>
|
|
34
|
+
<span className="grid size-7 shrink-0 place-items-center rounded-og-sm bg-og-status-waiting/10 text-og-status-waiting">
|
|
35
|
+
<CircleAlertIcon className="size-3.5" />
|
|
36
|
+
</span>
|
|
37
|
+
<span className="min-w-0 flex-1">
|
|
38
|
+
<span className="flex items-center gap-1.5 text-og-control font-medium text-og-fg">
|
|
39
|
+
{interventionTitle(intervention.kind)}
|
|
40
|
+
{interventions.length > 1 ? (
|
|
41
|
+
<span className="rounded-full bg-og-surface-2 px-1.5 py-0.5 text-[10px] font-normal text-og-muted">
|
|
42
|
+
+{interventions.length - 1}
|
|
43
|
+
</span>
|
|
44
|
+
) : null}
|
|
45
|
+
</span>
|
|
46
|
+
<span className="block truncate text-og-xs text-og-muted">{intervention.reason}</span>
|
|
47
|
+
</span>
|
|
48
|
+
{!targetOpen && onOpen ? (
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
onClick={() => onOpen(intervention)}
|
|
52
|
+
disabled={mutating}
|
|
53
|
+
className="h-7 shrink-0 rounded-og-sm border border-og-border bg-og-surface-1 px-2.5 text-og-control font-medium text-og-fg transition hover:bg-og-surface-2 disabled:opacity-50"
|
|
54
|
+
>
|
|
55
|
+
Open
|
|
56
|
+
</button>
|
|
57
|
+
) : (
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
onClick={() => onResolve(intervention, "completed")}
|
|
61
|
+
disabled={mutating}
|
|
62
|
+
className="inline-flex h-7 shrink-0 items-center gap-1 rounded-og-sm bg-og-accent px-2.5 text-og-control font-medium text-og-accent-fg transition hover:brightness-105 disabled:opacity-50"
|
|
63
|
+
>
|
|
64
|
+
{mutating ? (
|
|
65
|
+
<LoaderCircleIcon className="size-3 animate-spin" />
|
|
66
|
+
) : (
|
|
67
|
+
<CheckIcon className="size-3" />
|
|
68
|
+
)}
|
|
69
|
+
Done
|
|
70
|
+
</button>
|
|
71
|
+
)}
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
onClick={() => onResolve(intervention, "dismissed")}
|
|
75
|
+
disabled={mutating}
|
|
76
|
+
className="grid size-7 shrink-0 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-50"
|
|
77
|
+
aria-label="Cancel request"
|
|
78
|
+
title="Cancel request"
|
|
79
|
+
>
|
|
80
|
+
<XIcon className="size-3.5" />
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function interventionTitle(kind: InteractionIntervention["kind"]): string {
|
|
87
|
+
switch (kind) {
|
|
88
|
+
case "manual_login":
|
|
89
|
+
return "Sign in needed";
|
|
90
|
+
case "mfa":
|
|
91
|
+
return "Verification needed";
|
|
92
|
+
case "external_action":
|
|
93
|
+
return "Action needed";
|
|
94
|
+
case "confirmation":
|
|
95
|
+
return "Confirmation needed";
|
|
96
|
+
case "other":
|
|
97
|
+
return "Agent needs your help";
|
|
98
|
+
}
|
|
99
|
+
}
|