@opengeni/react 0.46.1 → 0.52.1
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 +158 -14
- 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 +170 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/chunk-6BBWWOOU.js +82 -0
- package/dist/chunk-6BBWWOOU.js.map +1 -0
- package/dist/{chunk-HFO4ERGQ.js → chunk-724BW4TW.js} +586 -35
- package/dist/chunk-724BW4TW.js.map +1 -0
- package/dist/chunk-7I2KB3OK.js +3895 -0
- package/dist/chunk-7I2KB3OK.js.map +1 -0
- package/dist/chunk-7WCLAEFV.js +2828 -0
- package/dist/chunk-7WCLAEFV.js.map +1 -0
- package/dist/{chunk-U3CSCMWA.js → chunk-FK6VG4LL.js} +9 -13
- package/dist/chunk-FK6VG4LL.js.map +1 -0
- package/dist/{chunk-SRFUT2ZU.js → chunk-FWS3KC3Z.js} +120 -64
- package/dist/chunk-FWS3KC3Z.js.map +1 -0
- package/dist/{chunk-EKZH6IDG.js → chunk-HDBG7XGO.js} +62 -1
- package/dist/chunk-HDBG7XGO.js.map +1 -0
- package/dist/chunk-HUIKIDM5.js +2148 -0
- package/dist/chunk-HUIKIDM5.js.map +1 -0
- package/dist/{chunk-LGVMUIRV.js → chunk-JAGDUCZQ.js} +1053 -390
- package/dist/chunk-JAGDUCZQ.js.map +1 -0
- package/dist/{chunk-LWR4MXSS.js → chunk-JR3QFY6B.js} +420 -35
- package/dist/chunk-JR3QFY6B.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-EB67J347.js → chunk-KZ73RL76.js} +111 -31
- package/dist/chunk-KZ73RL76.js.map +1 -0
- package/dist/chunk-PEI6RANI.js +3234 -0
- package/dist/chunk-PEI6RANI.js.map +1 -0
- package/dist/{chunk-3PB7MIT6.js → chunk-ROIRTM37.js} +220 -72
- package/dist/chunk-ROIRTM37.js.map +1 -0
- package/dist/chunk-VZTQ73XT.js +86 -0
- package/dist/chunk-VZTQ73XT.js.map +1 -0
- package/dist/{chunk-VCBFUSUZ.js → chunk-WSK7G5LE.js} +72 -4
- package/dist/chunk-WSK7G5LE.js.map +1 -0
- package/dist/{chunk-FGZUCXZF.js → chunk-WYA65Y3F.js} +16 -16
- package/dist/chunk-WYA65Y3F.js.map +1 -0
- package/dist/{chunk-6EDV6YFE.js → chunk-X4S56Y2H.js} +147 -35
- package/dist/chunk-X4S56Y2H.js.map +1 -0
- package/dist/client.d.ts +7 -1
- 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 +8 -0
- package/dist/components/artifacts/presentation-editor.d.ts +297 -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-viewer.d.ts +29 -0
- package/dist/components/chat-composer.d.ts +13 -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 +4 -2
- package/dist/components/composer.d.ts +29 -15
- package/dist/components/computer-viewer.d.ts +23 -0
- package/dist/components/copy-button.d.ts +2 -2
- package/dist/components/desktop-viewer.d.ts +25 -21
- 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/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 +5 -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 +5 -5
- package/dist/components/markdown.d.ts +1 -1
- package/dist/components/message-timeline.d.ts +22 -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 +5 -5
- package/dist/components/sandbox-terminal.d.ts +50 -4
- package/dist/components/sandbox-workspace.d.ts +30 -10
- 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 +3 -1
- package/dist/composer.d.ts +11 -11
- package/dist/composer.js +7 -7
- package/dist/hooks/internal.d.ts +7 -1
- package/dist/hooks/use-attached-browsers.d.ts +21 -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-frame-stream.d.ts +28 -0
- package/dist/hooks/use-browser-identities.d.ts +26 -0
- package/dist/hooks/use-browser-session.d.ts +28 -0
- package/dist/hooks/use-browser-sessions.d.ts +32 -0
- package/dist/hooks/use-codex-accounts.d.ts +3 -3
- 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 +28 -0
- package/dist/hooks/use-computer-sessions.d.ts +28 -0
- package/dist/hooks/use-desktop-stream.d.ts +1 -1
- package/dist/hooks/use-environments.d.ts +11 -5
- 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-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 +10 -3
- 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 +25 -4
- package/dist/hooks/use-sandbox-git.d.ts +13 -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-slash-commands.d.ts +1 -1
- package/dist/hooks/use-terminal-stream.d.ts +8 -2
- package/dist/hooks/use-transcription.d.ts +2 -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 +6 -4
- 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 +158 -154
- package/dist/index.js +1718 -908
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +23 -0
- package/dist/interaction.js +36 -0
- package/dist/interaction.js.map +1 -0
- package/dist/interactive-workbench-viewers-KHNOHWQE.js +13 -0
- package/dist/interactive-workbench-viewers-KHNOHWQE.js.map +1 -0
- package/dist/lib/composer-responsive-context.d.ts +9 -0
- package/dist/lib/relay-wire.d.ts +4 -0
- package/dist/lib/sandbox-liveness.d.ts +9 -0
- package/dist/lib/terminal-capability.d.ts +5 -0
- package/dist/lib/use-portal-token-style.d.ts +14 -3
- package/dist/lib/xterm-theme.d.ts +1 -1
- package/dist/machines.d.ts +30 -30
- package/dist/machines.js +3 -3
- package/dist/provider.d.ts +4 -4
- package/dist/{queue-surface-implementation-J4XJFKRW.js → queue-surface-implementation-2KVJTYXH.js} +117 -64
- 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 +15 -6
- package/dist/realtime.d.ts +4 -4
- package/dist/realtime.js +30 -33
- package/dist/realtime.js.map +1 -1
- package/dist/session-context.d.ts +19 -1
- package/dist/session-ui.d.ts +14 -14
- package/dist/session-ui.js +6 -5
- package/dist/session.d.ts +28 -28
- package/dist/session.js +5 -5
- package/dist/timeline/activity-rail.d.ts +6 -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 +6 -3
- package/dist/timeline/projection.d.ts +3 -3
- package/dist/timeline/registry.d.ts +10 -1
- 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 +42 -2
- 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/dist/voice-recording-store.d.ts +8 -2
- package/package.json +52 -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 +86 -0
- package/src/client.ts +90 -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 +99 -0
- package/src/components/artifacts/presentation-editor.tsx +3148 -0
- package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
- package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
- package/src/components/browser-viewer.tsx +1592 -0
- package/src/components/chat-composer.tsx +25 -2
- package/src/components/code-editor.tsx +90 -31
- package/src/components/command-palette.tsx +1 -1
- package/src/components/composer-transcription-control.tsx +51 -22
- package/src/components/composer.tsx +49 -23
- package/src/components/computer-viewer.tsx +1139 -0
- package/src/components/copy-button.tsx +1 -1
- package/src/components/desktop-viewer.tsx +103 -75
- package/src/components/file-browser.tsx +39 -17
- package/src/components/generated-video-player.tsx +89 -0
- package/src/components/human-input-form.tsx +2 -2
- package/src/components/interactive-workbench-viewers.ts +2 -0
- package/src/components/machine-card.tsx +1 -1
- package/src/components/machine-input-display.ts +3 -0
- package/src/components/machines/machine-detail.tsx +39 -15
- package/src/components/machines-dashboard.tsx +7 -6
- package/src/components/message-timeline.tsx +327 -37
- package/src/components/model-picker.tsx +2 -2
- package/src/components/model-policy-picker.tsx +16 -15
- package/src/components/queue-surface-implementation.tsx +135 -75
- package/src/components/queue-surface-state.tsx +2 -2
- package/src/components/sandbox-files.tsx +142 -62
- package/src/components/sandbox-terminal.tsx +119 -35
- package/src/components/sandbox-workspace.tsx +394 -228
- package/src/components/session-chrome.tsx +67 -27
- 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/tooltip.tsx +13 -16
- package/src/components/user-message-body.tsx +1 -1
- package/src/components/workbench-changes.tsx +192 -35
- package/src/components/workspace-dock.tsx +13 -5
- package/src/composer.ts +3 -0
- package/src/hooks/internal.ts +132 -38
- package/src/hooks/use-attached-browsers.ts +152 -0
- package/src/hooks/use-browser-frame-stream.ts +316 -0
- package/src/hooks/use-browser-identities.ts +225 -0
- package/src/hooks/use-browser-session.ts +476 -0
- package/src/hooks/use-browser-sessions.ts +281 -0
- package/src/hooks/use-codex-accounts.ts +4 -4
- package/src/hooks/use-composer.ts +769 -24
- package/src/hooks/use-computer-frame-stream.ts +311 -0
- package/src/hooks/use-computer-session.ts +442 -0
- package/src/hooks/use-computer-sessions.ts +245 -0
- package/src/hooks/use-environments.ts +24 -5
- package/src/hooks/use-file-attachments.ts +70 -32
- package/src/hooks/use-goal.ts +17 -5
- package/src/hooks/use-machine-chip.ts +5 -3
- package/src/hooks/use-machines.ts +79 -3
- package/src/hooks/use-packs.ts +51 -0
- package/src/hooks/use-sandbox-files.ts +382 -88
- package/src/hooks/use-sandbox-git.ts +208 -46
- package/src/hooks/use-sandbox-terminal.ts +3 -2
- package/src/hooks/use-session-capabilities.ts +94 -70
- package/src/hooks/use-session-events.ts +18 -4
- package/src/hooks/use-terminal-stream.ts +102 -23
- package/src/hooks/use-transcription.ts +4 -14
- package/src/hooks/use-turn-queue.ts +17 -5
- package/src/hooks/use-video-artifact-playback.ts +28 -0
- package/src/hooks/use-voice-input.ts +163 -19
- package/src/hooks/use-windowed-sections.ts +9 -7
- package/src/hooks/use-workspace-edit.ts +2 -1
- package/src/index.ts +21 -6
- package/src/interaction.ts +71 -0
- package/src/lib/composer-responsive-context.ts +15 -0
- package/src/lib/relay-wire.ts +4 -0
- package/src/lib/sandbox-liveness.ts +11 -0
- package/src/lib/terminal-capability.ts +10 -0
- package/src/lib/use-portal-token-style.ts +60 -15
- package/src/provider.tsx +52 -7
- package/src/realtime/dropdown-menu.tsx +15 -19
- package/src/realtime/realtime-control.tsx +16 -5
- package/src/session-context.ts +90 -0
- package/src/timeline/activity-rail.tsx +31 -6
- package/src/timeline/index.ts +4 -1
- package/src/timeline/parsers.ts +46 -20
- package/src/timeline/projection.ts +233 -13
- package/src/timeline/registry.ts +19 -0
- package/src/timeline/screenshot-lightbox.tsx +30 -9
- package/src/timeline/shared.tsx +59 -11
- package/src/timeline/tool-renderers.tsx +421 -24
- package/src/timeline/turn-summary.tsx +2 -2
- package/src/timeline/types.ts +58 -2
- package/src/voice-recording-store.ts +15 -0
- package/styles/compiled.css +5298 -0
- package/styles/compiled.d.ts +2 -0
- package/styles/effective-tokens.css +27 -0
- package/styles/index.css +18 -0
- package/styles/responsive.css +208 -0
- package/styles/responsive.d.ts +2 -0
- package/styles/tokens.css +50 -17
- package/dist/chunk-3PB7MIT6.js.map +0 -1
- package/dist/chunk-6EDV6YFE.js.map +0 -1
- package/dist/chunk-EB67J347.js.map +0 -1
- package/dist/chunk-EKZH6IDG.js.map +0 -1
- package/dist/chunk-FGZUCXZF.js.map +0 -1
- package/dist/chunk-HFO4ERGQ.js.map +0 -1
- package/dist/chunk-JB5725BI.js +0 -48
- package/dist/chunk-JB5725BI.js.map +0 -1
- package/dist/chunk-LGVMUIRV.js.map +0 -1
- package/dist/chunk-LWR4MXSS.js.map +0 -1
- package/dist/chunk-SRFUT2ZU.js.map +0 -1
- package/dist/chunk-U3CSCMWA.js.map +0 -1
- package/dist/chunk-VCBFUSUZ.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-J4XJFKRW.js.map +0 -1
|
@@ -0,0 +1,1139 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComputerAction,
|
|
3
|
+
ComputerFrame,
|
|
4
|
+
ComputerObservation,
|
|
5
|
+
ComputerSession,
|
|
6
|
+
ComputerTarget,
|
|
7
|
+
InteractionSemanticNode,
|
|
8
|
+
} from "@opengeni/sdk/interaction";
|
|
9
|
+
import {
|
|
10
|
+
ChevronDownIcon,
|
|
11
|
+
CircleAlertIcon,
|
|
12
|
+
KeyboardIcon,
|
|
13
|
+
LoaderCircleIcon,
|
|
14
|
+
LockKeyholeIcon,
|
|
15
|
+
MonitorIcon,
|
|
16
|
+
MousePointer2Icon,
|
|
17
|
+
PanelsTopLeftIcon,
|
|
18
|
+
PlusIcon,
|
|
19
|
+
RefreshCwIcon,
|
|
20
|
+
RotateCcwIcon,
|
|
21
|
+
} from "lucide-react";
|
|
22
|
+
import {
|
|
23
|
+
type KeyboardEvent,
|
|
24
|
+
type MouseEvent,
|
|
25
|
+
type PointerEvent,
|
|
26
|
+
type ReactNode,
|
|
27
|
+
type WheelEvent,
|
|
28
|
+
useCallback,
|
|
29
|
+
useEffect,
|
|
30
|
+
useMemo,
|
|
31
|
+
useRef,
|
|
32
|
+
useState,
|
|
33
|
+
} from "react";
|
|
34
|
+
import {
|
|
35
|
+
type ComputerFrameWebSocketFactory,
|
|
36
|
+
useComputerFrameStream,
|
|
37
|
+
} from "../hooks/use-computer-frame-stream";
|
|
38
|
+
import { useComputerSession } from "../hooks/use-computer-session";
|
|
39
|
+
import { useComputerSessions } from "../hooks/use-computer-sessions";
|
|
40
|
+
import { cn } from "../lib/cn";
|
|
41
|
+
import type { EmbeddedComputerInteractionClientOverride } from "../session-context";
|
|
42
|
+
|
|
43
|
+
export type ComputerViewerNotification = { kind: "error" | "info"; message: string };
|
|
44
|
+
|
|
45
|
+
export type ComputerViewerProps = EmbeddedComputerInteractionClientOverride & {
|
|
46
|
+
/** Selected OpenGeni agent/session. Peer ComputerSessions remain visible. */
|
|
47
|
+
sessionId: string;
|
|
48
|
+
enabled?: boolean | undefined;
|
|
49
|
+
className?: string | undefined;
|
|
50
|
+
onNotify?: ((notification: ComputerViewerNotification) => void) | undefined;
|
|
51
|
+
/** Tests/demos only. Production uses the browser's native WebSocket. */
|
|
52
|
+
webSocketFactory?: ComputerFrameWebSocketFactory | undefined;
|
|
53
|
+
renderEmpty?: ((create: () => void, creating: boolean) => ReactNode) | undefined;
|
|
54
|
+
/** Host navigation request for one exact ComputerSession. */
|
|
55
|
+
requestedComputerSessionId?: string | null | undefined;
|
|
56
|
+
requestedComputerRequestId?: string | number | null | undefined;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type ComputerSelection = { sessionId: string; pinned: boolean } | null;
|
|
60
|
+
type PointerStart = { x: number; y: number; pointerId: number; frame: ComputerFrame };
|
|
61
|
+
|
|
62
|
+
/** Full ComputerSession surface: workspace discovery, peer switching, app/window/
|
|
63
|
+
* screen navigation, semantic controls, exact-frame input, and live media. */
|
|
64
|
+
export function ComputerViewer({
|
|
65
|
+
sessionId,
|
|
66
|
+
enabled = true,
|
|
67
|
+
className,
|
|
68
|
+
onNotify,
|
|
69
|
+
webSocketFactory,
|
|
70
|
+
renderEmpty,
|
|
71
|
+
requestedComputerSessionId,
|
|
72
|
+
requestedComputerRequestId,
|
|
73
|
+
...override
|
|
74
|
+
}: ComputerViewerProps) {
|
|
75
|
+
const registry = useComputerSessions({ ...override, sessionId, enabled });
|
|
76
|
+
const createSession = registry.create;
|
|
77
|
+
const refreshRegistry = registry.refresh;
|
|
78
|
+
const liveSessions = useMemo(
|
|
79
|
+
() => registry.sessions.filter((session) => isLiveComputer(session)),
|
|
80
|
+
[registry.sessions],
|
|
81
|
+
);
|
|
82
|
+
const relevant = useMemo(
|
|
83
|
+
() => registry.relevantSessions.filter((session) => isLiveComputer(session)),
|
|
84
|
+
[registry.relevantSessions],
|
|
85
|
+
);
|
|
86
|
+
const [selection, setSelection] = useState<ComputerSelection>(null);
|
|
87
|
+
const [creating, setCreating] = useState(false);
|
|
88
|
+
const [showControls, setShowControls] = useState(false);
|
|
89
|
+
const previousSessionIdRef = useRef(sessionId);
|
|
90
|
+
const handledRequestRef = useRef<string | null>(null);
|
|
91
|
+
|
|
92
|
+
const notifyError = useCallback(
|
|
93
|
+
(cause: unknown, fallback: string) => {
|
|
94
|
+
onNotify?.({
|
|
95
|
+
kind: "error",
|
|
96
|
+
message: cause instanceof Error ? cause.message : fallback,
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
[onNotify],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (previousSessionIdRef.current === sessionId) return;
|
|
104
|
+
previousSessionIdRef.current = sessionId;
|
|
105
|
+
handledRequestRef.current = null;
|
|
106
|
+
setSelection(null);
|
|
107
|
+
}, [sessionId]);
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const requested = requestedComputerSessionId
|
|
111
|
+
? (liveSessions.find((session) => session.id === requestedComputerSessionId) ?? null)
|
|
112
|
+
: null;
|
|
113
|
+
const requestKey = requested
|
|
114
|
+
? `${requestedComputerRequestId ?? "default"}:${requested.id}`
|
|
115
|
+
: null;
|
|
116
|
+
if (requested && requestKey && handledRequestRef.current !== requestKey) {
|
|
117
|
+
handledRequestRef.current = requestKey;
|
|
118
|
+
setSelection({ sessionId: requested.id, pinned: true });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const selectedStillLive = liveSessions.some((session) => session.id === selection?.sessionId);
|
|
123
|
+
const preferred = relevant[0] ?? liveSessions[0] ?? null;
|
|
124
|
+
if (!preferred) {
|
|
125
|
+
if (selection) setSelection(null);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!selectedStillLive || !selection) {
|
|
129
|
+
setSelection({ sessionId: preferred.id, pinned: false });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!selection.pinned && relevant[0] && selection.sessionId !== relevant[0].id) {
|
|
133
|
+
setSelection({ sessionId: relevant[0].id, pinned: false });
|
|
134
|
+
}
|
|
135
|
+
}, [liveSessions, relevant, requestedComputerRequestId, requestedComputerSessionId, selection]);
|
|
136
|
+
|
|
137
|
+
const selectedRegistrySession = useMemo(
|
|
138
|
+
() => liveSessions.find((session) => session.id === selection?.sessionId) ?? null,
|
|
139
|
+
[liveSessions, selection?.sessionId],
|
|
140
|
+
);
|
|
141
|
+
const controllerReady = selectedRegistrySession?.lifecycle === "active";
|
|
142
|
+
const computer = useComputerSession({
|
|
143
|
+
...override,
|
|
144
|
+
computerSessionId: selection?.sessionId ?? null,
|
|
145
|
+
enabled: enabled && selection !== null && controllerReady,
|
|
146
|
+
});
|
|
147
|
+
const act = computer.act;
|
|
148
|
+
const actFromFrame = computer.actFromFrame;
|
|
149
|
+
const refreshComputer = computer.refresh;
|
|
150
|
+
const frames = useComputerFrameStream({
|
|
151
|
+
...override,
|
|
152
|
+
computerSessionId: selection?.sessionId ?? null,
|
|
153
|
+
targetId: computer.selectedTarget?.id ?? null,
|
|
154
|
+
enabled: enabled && selection !== null && controllerReady && computer.selectedTarget !== null,
|
|
155
|
+
stream: { format: "jpeg", quality: 82, maxWidth: 1_920, maxHeight: 1_200 },
|
|
156
|
+
...(webSocketFactory ? { webSocketFactory } : {}),
|
|
157
|
+
});
|
|
158
|
+
const displayedFrame =
|
|
159
|
+
frames.frame &&
|
|
160
|
+
frames.frame.computerSessionId === selection?.sessionId &&
|
|
161
|
+
frames.frame.targetId === computer.selectedTarget?.id
|
|
162
|
+
? frames.frame
|
|
163
|
+
: null;
|
|
164
|
+
const machineLocked = selectedRegistrySession?.failureCode === "machine_locked";
|
|
165
|
+
const currentIds = useMemo(() => new Set(relevant.map((session) => session.id)), [relevant]);
|
|
166
|
+
|
|
167
|
+
const createComputer = useCallback(() => {
|
|
168
|
+
if (creating) return;
|
|
169
|
+
setCreating(true);
|
|
170
|
+
void createSession({ sessionId, name: "Computer" })
|
|
171
|
+
.then((response) => {
|
|
172
|
+
setSelection({ sessionId: response.session.id, pinned: false });
|
|
173
|
+
})
|
|
174
|
+
.catch((cause) => notifyError(cause, "Could not open a computer."))
|
|
175
|
+
.finally(() => setCreating(false));
|
|
176
|
+
}, [createSession, creating, notifyError, sessionId]);
|
|
177
|
+
|
|
178
|
+
const perform = useCallback(
|
|
179
|
+
async (action: ComputerAction, frame: ComputerFrame | null): Promise<void> => {
|
|
180
|
+
if (action.type === "pointer") {
|
|
181
|
+
if (!frame) throw new Error("Computer view is not ready for pointer input.");
|
|
182
|
+
await actFromFrame(action, frame);
|
|
183
|
+
} else {
|
|
184
|
+
await act(action);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
[act, actFromFrame],
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
if (!enabled) return null;
|
|
191
|
+
if (registry.loading && liveSessions.length === 0) {
|
|
192
|
+
return (
|
|
193
|
+
<ComputerNotice
|
|
194
|
+
icon={<LoaderCircleIcon className="size-4 animate-spin" />}
|
|
195
|
+
text="Finding workspace computers…"
|
|
196
|
+
{...(className ? { className } : {})}
|
|
197
|
+
/>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (liveSessions.length === 0) {
|
|
201
|
+
if (renderEmpty) return renderEmpty(createComputer, creating);
|
|
202
|
+
return (
|
|
203
|
+
<div className={cn("grid h-full place-items-center bg-og-bg p-6", className)}>
|
|
204
|
+
<div className="max-w-sm text-center">
|
|
205
|
+
<span className="mx-auto grid size-10 place-items-center rounded-og-lg border border-og-border bg-og-surface-1 text-og-muted">
|
|
206
|
+
<MonitorIcon className="size-5" />
|
|
207
|
+
</span>
|
|
208
|
+
<p className="mt-3 text-og-menu font-medium text-og-fg">No computer open</p>
|
|
209
|
+
<p className="mt-1 text-og-control leading-5 text-og-muted">
|
|
210
|
+
Open this agent's desktop, or switch to any computer another workspace agent uses.
|
|
211
|
+
</p>
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
onClick={createComputer}
|
|
215
|
+
disabled={creating}
|
|
216
|
+
className="mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm bg-og-accent-deep px-3 text-og-control font-medium text-og-accent-fg transition hover:brightness-110 disabled:opacity-50"
|
|
217
|
+
>
|
|
218
|
+
{creating ? (
|
|
219
|
+
<LoaderCircleIcon className="size-3.5 animate-spin" />
|
|
220
|
+
) : (
|
|
221
|
+
<PlusIcon className="size-3.5" />
|
|
222
|
+
)}
|
|
223
|
+
Open computer
|
|
224
|
+
</button>
|
|
225
|
+
{registry.error ? (
|
|
226
|
+
<p className="mt-3 text-og-control text-og-status-error">{registry.error.message}</p>
|
|
227
|
+
) : null}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<div className={cn("flex h-full min-h-0 flex-col overflow-hidden bg-og-bg", className)}>
|
|
235
|
+
<ComputerToolbar
|
|
236
|
+
sessions={liveSessions}
|
|
237
|
+
relevantSessionIds={currentIds}
|
|
238
|
+
selectedSessionId={selection?.sessionId ?? null}
|
|
239
|
+
creating={creating}
|
|
240
|
+
refreshing={registry.refreshing}
|
|
241
|
+
onSelect={(computerSessionId) =>
|
|
242
|
+
setSelection({ sessionId: computerSessionId, pinned: true })
|
|
243
|
+
}
|
|
244
|
+
onFollow={() => {
|
|
245
|
+
if (relevant[0]) setSelection({ sessionId: relevant[0].id, pinned: false });
|
|
246
|
+
}}
|
|
247
|
+
onCreate={createComputer}
|
|
248
|
+
onRefresh={() => void Promise.all([refreshRegistry(), refreshComputer()])}
|
|
249
|
+
/>
|
|
250
|
+
{selectedRegistrySession && !controllerReady ? (
|
|
251
|
+
<ComputerLifecyclePanel session={selectedRegistrySession} onRefresh={registry.refresh} />
|
|
252
|
+
) : (
|
|
253
|
+
<>
|
|
254
|
+
<ComputerTargetRail
|
|
255
|
+
targets={computer.targets}
|
|
256
|
+
selectedTargetId={computer.selectedTarget?.id ?? null}
|
|
257
|
+
loading={computer.loading}
|
|
258
|
+
onSelect={(targetId) =>
|
|
259
|
+
void computer
|
|
260
|
+
.selectTarget(targetId)
|
|
261
|
+
.catch((cause) => notifyError(cause, "Could not switch computer views."))
|
|
262
|
+
}
|
|
263
|
+
/>
|
|
264
|
+
<div className="flex min-h-0 flex-1">
|
|
265
|
+
<ComputerViewport
|
|
266
|
+
frame={machineLocked ? null : displayedFrame}
|
|
267
|
+
observation={computer.observation}
|
|
268
|
+
target={computer.selectedTarget}
|
|
269
|
+
machineLocked={machineLocked}
|
|
270
|
+
connectionState={frames.state}
|
|
271
|
+
connectionError={frames.error ?? computer.error}
|
|
272
|
+
mutating={computer.mutating}
|
|
273
|
+
onAction={perform}
|
|
274
|
+
onReconnect={frames.reconnect}
|
|
275
|
+
onError={(cause) => notifyError(cause, "Computer input failed.")}
|
|
276
|
+
/>
|
|
277
|
+
{showControls ? (
|
|
278
|
+
<ComputerSemanticPanel
|
|
279
|
+
observation={computer.observation}
|
|
280
|
+
mutating={computer.mutating}
|
|
281
|
+
onAction={(action) =>
|
|
282
|
+
void perform(action, null).catch((cause) =>
|
|
283
|
+
notifyError(cause, "Computer action failed."),
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
/>
|
|
287
|
+
) : null}
|
|
288
|
+
</div>
|
|
289
|
+
<ComputerStatusBar
|
|
290
|
+
session={computer.session}
|
|
291
|
+
target={computer.selectedTarget}
|
|
292
|
+
connectionState={frames.state}
|
|
293
|
+
refreshing={registry.refreshing}
|
|
294
|
+
showControls={showControls}
|
|
295
|
+
controlCount={semanticNodes(computer.observation).length}
|
|
296
|
+
onToggleControls={() => setShowControls((current) => !current)}
|
|
297
|
+
/>
|
|
298
|
+
</>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function ComputerToolbar(props: {
|
|
305
|
+
sessions: ComputerSession[];
|
|
306
|
+
relevantSessionIds: Set<string>;
|
|
307
|
+
selectedSessionId: string | null;
|
|
308
|
+
creating: boolean;
|
|
309
|
+
refreshing: boolean;
|
|
310
|
+
onSelect: (id: string) => void;
|
|
311
|
+
onFollow: () => void;
|
|
312
|
+
onCreate: () => void;
|
|
313
|
+
onRefresh: () => void;
|
|
314
|
+
}) {
|
|
315
|
+
const detailsRef = useRef<HTMLDetailsElement | null>(null);
|
|
316
|
+
const selected = props.sessions.find((session) => session.id === props.selectedSessionId);
|
|
317
|
+
const current = props.sessions.filter((session) => props.relevantSessionIds.has(session.id));
|
|
318
|
+
const others = props.sessions.filter((session) => !props.relevantSessionIds.has(session.id));
|
|
319
|
+
const choose = (id: string) => {
|
|
320
|
+
props.onSelect(id);
|
|
321
|
+
detailsRef.current?.removeAttribute("open");
|
|
322
|
+
};
|
|
323
|
+
return (
|
|
324
|
+
<div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-surface-0 px-2">
|
|
325
|
+
<details ref={detailsRef} className="relative min-w-0">
|
|
326
|
+
<summary className="flex h-7 max-w-52 cursor-pointer list-none items-center gap-2 rounded-og-sm px-2 text-og-control text-og-fg transition hover:bg-og-surface-2 [&::-webkit-details-marker]:hidden">
|
|
327
|
+
<MonitorIcon className="size-3.5 shrink-0 text-og-muted" />
|
|
328
|
+
<span className="truncate font-medium">{selected?.name ?? "Computer"}</span>
|
|
329
|
+
<ChevronDownIcon className="size-3 shrink-0 text-og-subtle" />
|
|
330
|
+
</summary>
|
|
331
|
+
<div className="absolute left-0 top-8 z-30 w-72 overflow-hidden rounded-og-md border border-og-border bg-og-surface-1 p-1 shadow-xl">
|
|
332
|
+
<ComputerSessionGroup
|
|
333
|
+
label="Current agent"
|
|
334
|
+
sessions={current}
|
|
335
|
+
selectedId={props.selectedSessionId}
|
|
336
|
+
onSelect={choose}
|
|
337
|
+
/>
|
|
338
|
+
<ComputerSessionGroup
|
|
339
|
+
label="Other agents"
|
|
340
|
+
sessions={others}
|
|
341
|
+
selectedId={props.selectedSessionId}
|
|
342
|
+
onSelect={choose}
|
|
343
|
+
/>
|
|
344
|
+
<div className="mt-1 flex gap-1 border-t border-og-border pt-1">
|
|
345
|
+
{current.length > 0 ? (
|
|
346
|
+
<MenuButton onClick={props.onFollow}>Follow agent</MenuButton>
|
|
347
|
+
) : null}
|
|
348
|
+
<MenuButton onClick={props.onCreate} disabled={props.creating}>
|
|
349
|
+
<PlusIcon className="size-3.5" /> New computer
|
|
350
|
+
</MenuButton>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
</details>
|
|
354
|
+
<span className="min-w-0 flex-1 truncate text-og-xs text-og-subtle">
|
|
355
|
+
{selected ? `${platformLabel(selected)} · ${placementLabel(selected)}` : ""}
|
|
356
|
+
</span>
|
|
357
|
+
<button
|
|
358
|
+
type="button"
|
|
359
|
+
onClick={props.onRefresh}
|
|
360
|
+
className="grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg"
|
|
361
|
+
aria-label="Refresh computers"
|
|
362
|
+
>
|
|
363
|
+
<RefreshCwIcon className={cn("size-3.5", props.refreshing && "animate-spin")} />
|
|
364
|
+
</button>
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
367
|
+
onClick={props.onCreate}
|
|
368
|
+
disabled={props.creating}
|
|
369
|
+
className="grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-40"
|
|
370
|
+
aria-label="Open a new computer"
|
|
371
|
+
>
|
|
372
|
+
{props.creating ? (
|
|
373
|
+
<LoaderCircleIcon className="size-3.5 animate-spin" />
|
|
374
|
+
) : (
|
|
375
|
+
<PlusIcon className="size-3.5" />
|
|
376
|
+
)}
|
|
377
|
+
</button>
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function ComputerSessionGroup(props: {
|
|
383
|
+
label: string;
|
|
384
|
+
sessions: ComputerSession[];
|
|
385
|
+
selectedId: string | null;
|
|
386
|
+
onSelect: (id: string) => void;
|
|
387
|
+
}) {
|
|
388
|
+
if (props.sessions.length === 0) return null;
|
|
389
|
+
return (
|
|
390
|
+
<div className="py-1">
|
|
391
|
+
<p className="px-2 pb-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle">
|
|
392
|
+
{props.label}
|
|
393
|
+
</p>
|
|
394
|
+
{props.sessions.map((session) => (
|
|
395
|
+
<button
|
|
396
|
+
key={session.id}
|
|
397
|
+
type="button"
|
|
398
|
+
onClick={() => props.onSelect(session.id)}
|
|
399
|
+
className={cn(
|
|
400
|
+
"flex w-full items-center gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2",
|
|
401
|
+
session.id === props.selectedId && "bg-og-surface-2",
|
|
402
|
+
)}
|
|
403
|
+
>
|
|
404
|
+
<span
|
|
405
|
+
className={cn(
|
|
406
|
+
"size-1.5 rounded-full",
|
|
407
|
+
session.lifecycle === "active" ? "bg-og-status-running" : "bg-og-muted",
|
|
408
|
+
)}
|
|
409
|
+
/>
|
|
410
|
+
<span className="min-w-0 flex-1">
|
|
411
|
+
<span className="block truncate text-og-control text-og-fg">{session.name}</span>
|
|
412
|
+
<span className="block truncate text-og-xs text-og-subtle">
|
|
413
|
+
{platformLabel(session)} · {placementLabel(session)}
|
|
414
|
+
</span>
|
|
415
|
+
</span>
|
|
416
|
+
</button>
|
|
417
|
+
))}
|
|
418
|
+
</div>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function MenuButton(props: { children: ReactNode; onClick: () => void; disabled?: boolean }) {
|
|
423
|
+
return (
|
|
424
|
+
<button
|
|
425
|
+
type="button"
|
|
426
|
+
onClick={props.onClick}
|
|
427
|
+
disabled={props.disabled}
|
|
428
|
+
className="flex h-7 flex-1 items-center justify-center gap-1 rounded-og-sm px-2 text-og-control text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-50"
|
|
429
|
+
>
|
|
430
|
+
{props.children}
|
|
431
|
+
</button>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function ComputerTargetRail(props: {
|
|
436
|
+
targets: ComputerTarget[];
|
|
437
|
+
selectedTargetId: string | null;
|
|
438
|
+
loading: boolean;
|
|
439
|
+
onSelect: (id: string) => void;
|
|
440
|
+
}) {
|
|
441
|
+
return (
|
|
442
|
+
<div
|
|
443
|
+
className="flex h-10 shrink-0 items-center gap-1 overflow-x-auto border-b border-og-border bg-og-surface-0 px-2"
|
|
444
|
+
aria-label="Computer views"
|
|
445
|
+
>
|
|
446
|
+
{props.targets.map((target) => (
|
|
447
|
+
<button
|
|
448
|
+
key={target.id}
|
|
449
|
+
type="button"
|
|
450
|
+
onClick={() => props.onSelect(target.id)}
|
|
451
|
+
className={cn(
|
|
452
|
+
"flex h-7 max-w-56 shrink-0 items-center gap-1.5 rounded-og-sm border px-2 text-og-control transition",
|
|
453
|
+
target.id === props.selectedTargetId
|
|
454
|
+
? "border-og-border-strong bg-og-surface-2 text-og-fg"
|
|
455
|
+
: "border-transparent text-og-muted hover:bg-og-surface-1 hover:text-og-fg",
|
|
456
|
+
)}
|
|
457
|
+
aria-pressed={target.id === props.selectedTargetId}
|
|
458
|
+
>
|
|
459
|
+
{target.kind === "screen" ? (
|
|
460
|
+
<MonitorIcon className="size-3.5 shrink-0" />
|
|
461
|
+
) : (
|
|
462
|
+
<PanelsTopLeftIcon className="size-3.5 shrink-0" />
|
|
463
|
+
)}
|
|
464
|
+
<span className="truncate">{target.title || target.applicationId || target.kind}</span>
|
|
465
|
+
{target.focused ? (
|
|
466
|
+
<span
|
|
467
|
+
className="size-1.5 shrink-0 rounded-full bg-og-status-running"
|
|
468
|
+
aria-label="Focused"
|
|
469
|
+
/>
|
|
470
|
+
) : null}
|
|
471
|
+
</button>
|
|
472
|
+
))}
|
|
473
|
+
{props.loading ? (
|
|
474
|
+
<LoaderCircleIcon className="ml-1 size-3.5 animate-spin text-og-muted" />
|
|
475
|
+
) : null}
|
|
476
|
+
{!props.loading && props.targets.length === 0 ? (
|
|
477
|
+
<span className="text-og-xs text-og-subtle">Waiting for apps and screens…</span>
|
|
478
|
+
) : null}
|
|
479
|
+
</div>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function ComputerLifecyclePanel(props: {
|
|
484
|
+
session: ComputerSession;
|
|
485
|
+
onRefresh: () => Promise<void>;
|
|
486
|
+
}) {
|
|
487
|
+
const failed = ["failed", "lost", "repair_required"].includes(props.session.lifecycle);
|
|
488
|
+
return (
|
|
489
|
+
<div className="grid min-h-0 flex-1 place-items-center bg-og-bg p-6">
|
|
490
|
+
<div className="max-w-sm text-center">
|
|
491
|
+
{failed ? (
|
|
492
|
+
<CircleAlertIcon className="mx-auto size-5 text-og-status-error" />
|
|
493
|
+
) : (
|
|
494
|
+
<LoaderCircleIcon className="mx-auto size-5 animate-spin text-og-muted" />
|
|
495
|
+
)}
|
|
496
|
+
<p className="mt-3 text-og-menu font-medium text-og-fg">
|
|
497
|
+
{failed ? "Computer needs attention" : lifecycleLabel(props.session.lifecycle)}
|
|
498
|
+
</p>
|
|
499
|
+
<p className="mt-1 text-og-control leading-5 text-og-muted">
|
|
500
|
+
{props.session.failureCode ??
|
|
501
|
+
"The computer is being prepared on its placement. It will appear here when ready."}
|
|
502
|
+
</p>
|
|
503
|
+
{failed ? (
|
|
504
|
+
<button
|
|
505
|
+
type="button"
|
|
506
|
+
onClick={() => void props.onRefresh()}
|
|
507
|
+
className="mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm border border-og-border bg-og-surface-1 px-3 text-og-control font-medium text-og-fg transition hover:bg-og-surface-2"
|
|
508
|
+
>
|
|
509
|
+
<RotateCcwIcon className="size-3.5" /> Check again
|
|
510
|
+
</button>
|
|
511
|
+
) : null}
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function ComputerViewport(props: {
|
|
518
|
+
frame: ComputerFrame | null;
|
|
519
|
+
observation: ComputerObservation | null;
|
|
520
|
+
target: ComputerTarget | null;
|
|
521
|
+
machineLocked: boolean;
|
|
522
|
+
connectionState: string;
|
|
523
|
+
connectionError: Error | null;
|
|
524
|
+
mutating: boolean;
|
|
525
|
+
onAction: (action: ComputerAction, frame: ComputerFrame | null) => Promise<void>;
|
|
526
|
+
onReconnect: () => void;
|
|
527
|
+
onError: (cause: unknown) => void;
|
|
528
|
+
}) {
|
|
529
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
530
|
+
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
|
531
|
+
const pointerStartRef = useRef<PointerStart | null>(null);
|
|
532
|
+
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
533
|
+
const lastClickRef = useRef<{
|
|
534
|
+
at: number;
|
|
535
|
+
x: number;
|
|
536
|
+
y: number;
|
|
537
|
+
frame: ComputerFrame;
|
|
538
|
+
} | null>(null);
|
|
539
|
+
const wheelRef = useRef<{
|
|
540
|
+
x: number;
|
|
541
|
+
y: number;
|
|
542
|
+
deltaX: number;
|
|
543
|
+
deltaY: number;
|
|
544
|
+
frame: ComputerFrame;
|
|
545
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
546
|
+
} | null>(null);
|
|
547
|
+
const actionRef = useRef(props.onAction);
|
|
548
|
+
const errorRef = useRef(props.onError);
|
|
549
|
+
const actionTailRef = useRef<Promise<void>>(Promise.resolve());
|
|
550
|
+
actionRef.current = props.onAction;
|
|
551
|
+
errorRef.current = props.onError;
|
|
552
|
+
|
|
553
|
+
useEffect(() => {
|
|
554
|
+
const frame = props.frame;
|
|
555
|
+
const canvas = canvasRef.current;
|
|
556
|
+
if (!frame || !canvas) return;
|
|
557
|
+
let cancelled = false;
|
|
558
|
+
let objectUrl: string | null = null;
|
|
559
|
+
void (async () => {
|
|
560
|
+
try {
|
|
561
|
+
const blob = new Blob([frame.data.slice().buffer], { type: frame.mediaType });
|
|
562
|
+
if (typeof createImageBitmap === "function") {
|
|
563
|
+
const bitmap = await createImageBitmap(blob);
|
|
564
|
+
if (cancelled) {
|
|
565
|
+
bitmap.close();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
paintCanvas(canvas, bitmap, frame.width, frame.height);
|
|
569
|
+
bitmap.close();
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
objectUrl = URL.createObjectURL(blob);
|
|
573
|
+
const image = await loadImage(objectUrl);
|
|
574
|
+
if (!cancelled) paintCanvas(canvas, image, frame.width, frame.height);
|
|
575
|
+
} catch (cause) {
|
|
576
|
+
if (!cancelled) errorRef.current(cause);
|
|
577
|
+
}
|
|
578
|
+
})();
|
|
579
|
+
return () => {
|
|
580
|
+
cancelled = true;
|
|
581
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
582
|
+
};
|
|
583
|
+
}, [props.frame]);
|
|
584
|
+
|
|
585
|
+
useEffect(
|
|
586
|
+
() => () => {
|
|
587
|
+
if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
|
|
588
|
+
if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
|
|
589
|
+
},
|
|
590
|
+
[],
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
const enqueue = useCallback((action: ComputerAction, frame: ComputerFrame | null) => {
|
|
594
|
+
actionTailRef.current = actionTailRef.current
|
|
595
|
+
.catch(() => undefined)
|
|
596
|
+
.then(async () => await actionRef.current(action, frame))
|
|
597
|
+
.catch((cause) => errorRef.current(cause));
|
|
598
|
+
}, []);
|
|
599
|
+
|
|
600
|
+
const point = useCallback(
|
|
601
|
+
(frame: ComputerFrame, clientX: number, clientY: number) =>
|
|
602
|
+
computerPoint(canvasRef.current, frame, clientX, clientY),
|
|
603
|
+
[],
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
const pointerDown = (event: PointerEvent<HTMLCanvasElement>) => {
|
|
607
|
+
if (!props.frame || props.mutating) return;
|
|
608
|
+
pointerStartRef.current = {
|
|
609
|
+
x: event.clientX,
|
|
610
|
+
y: event.clientY,
|
|
611
|
+
pointerId: event.pointerId,
|
|
612
|
+
frame: props.frame,
|
|
613
|
+
};
|
|
614
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
615
|
+
inputRef.current?.focus({ preventScroll: true });
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const pointerUp = (event: PointerEvent<HTMLCanvasElement>) => {
|
|
619
|
+
const start = pointerStartRef.current;
|
|
620
|
+
pointerStartRef.current = null;
|
|
621
|
+
if (!start || start.pointerId !== event.pointerId) return;
|
|
622
|
+
const from = point(start.frame, start.x, start.y);
|
|
623
|
+
const to = point(start.frame, event.clientX, event.clientY);
|
|
624
|
+
if (!from || !to) return;
|
|
625
|
+
if (Math.hypot(event.clientX - start.x, event.clientY - start.y) > 5) {
|
|
626
|
+
enqueue(
|
|
627
|
+
{
|
|
628
|
+
type: "pointer",
|
|
629
|
+
frameId: start.frame.frameId,
|
|
630
|
+
action: "drag",
|
|
631
|
+
x: from.x,
|
|
632
|
+
y: from.y,
|
|
633
|
+
endX: to.x,
|
|
634
|
+
endY: to.y,
|
|
635
|
+
},
|
|
636
|
+
start.frame,
|
|
637
|
+
);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const now = Date.now();
|
|
641
|
+
const previous = lastClickRef.current;
|
|
642
|
+
if (
|
|
643
|
+
previous &&
|
|
644
|
+
now - previous.at < 280 &&
|
|
645
|
+
Math.hypot(previous.x - to.x, previous.y - to.y) < 6 &&
|
|
646
|
+
sameFrameFence(previous.frame, start.frame)
|
|
647
|
+
) {
|
|
648
|
+
if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
|
|
649
|
+
clickTimerRef.current = null;
|
|
650
|
+
lastClickRef.current = null;
|
|
651
|
+
enqueue(
|
|
652
|
+
{
|
|
653
|
+
type: "pointer",
|
|
654
|
+
frameId: start.frame.frameId,
|
|
655
|
+
action: "double_click",
|
|
656
|
+
x: to.x,
|
|
657
|
+
y: to.y,
|
|
658
|
+
},
|
|
659
|
+
start.frame,
|
|
660
|
+
);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
lastClickRef.current = { at: now, x: to.x, y: to.y, frame: start.frame };
|
|
664
|
+
clickTimerRef.current = setTimeout(() => {
|
|
665
|
+
clickTimerRef.current = null;
|
|
666
|
+
lastClickRef.current = null;
|
|
667
|
+
enqueue(
|
|
668
|
+
{
|
|
669
|
+
type: "pointer",
|
|
670
|
+
frameId: start.frame.frameId,
|
|
671
|
+
action: "click",
|
|
672
|
+
x: to.x,
|
|
673
|
+
y: to.y,
|
|
674
|
+
},
|
|
675
|
+
start.frame,
|
|
676
|
+
);
|
|
677
|
+
}, 280);
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
const contextMenu = (event: MouseEvent<HTMLCanvasElement>) => {
|
|
681
|
+
event.preventDefault();
|
|
682
|
+
const frame = props.frame;
|
|
683
|
+
if (!frame) return;
|
|
684
|
+
const at = point(frame, event.clientX, event.clientY);
|
|
685
|
+
if (at) {
|
|
686
|
+
enqueue(
|
|
687
|
+
{
|
|
688
|
+
type: "pointer",
|
|
689
|
+
frameId: frame.frameId,
|
|
690
|
+
action: "click",
|
|
691
|
+
x: at.x,
|
|
692
|
+
y: at.y,
|
|
693
|
+
button: "right",
|
|
694
|
+
},
|
|
695
|
+
frame,
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const wheel = (event: WheelEvent<HTMLCanvasElement>) => {
|
|
701
|
+
const frame = props.frame;
|
|
702
|
+
if (!frame) return;
|
|
703
|
+
const at = point(frame, event.clientX, event.clientY);
|
|
704
|
+
if (!at) return;
|
|
705
|
+
event.preventDefault();
|
|
706
|
+
const pending = wheelRef.current;
|
|
707
|
+
if (pending?.timer) clearTimeout(pending.timer);
|
|
708
|
+
wheelRef.current = {
|
|
709
|
+
x: at.x,
|
|
710
|
+
y: at.y,
|
|
711
|
+
deltaX: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaX : 0) + event.deltaX,
|
|
712
|
+
deltaY: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaY : 0) + event.deltaY,
|
|
713
|
+
frame,
|
|
714
|
+
timer: setTimeout(() => {
|
|
715
|
+
const batch = wheelRef.current;
|
|
716
|
+
wheelRef.current = null;
|
|
717
|
+
if (batch) {
|
|
718
|
+
enqueue(
|
|
719
|
+
{
|
|
720
|
+
type: "pointer",
|
|
721
|
+
frameId: batch.frame.frameId,
|
|
722
|
+
action: "scroll",
|
|
723
|
+
x: batch.x,
|
|
724
|
+
y: batch.y,
|
|
725
|
+
deltaX: batch.deltaX,
|
|
726
|
+
deltaY: batch.deltaY,
|
|
727
|
+
},
|
|
728
|
+
batch.frame,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
}, 45),
|
|
732
|
+
};
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
const keyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
736
|
+
const key = computerKey(event);
|
|
737
|
+
if (!key) return;
|
|
738
|
+
event.preventDefault();
|
|
739
|
+
enqueue({ type: "keyboard", action: "press", value: key }, null);
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
const input = (value: string) => {
|
|
743
|
+
if (!value) return;
|
|
744
|
+
enqueue({ type: "keyboard", action: "type", value }, null);
|
|
745
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
const showCanvas = props.frame !== null && !props.machineLocked;
|
|
749
|
+
return (
|
|
750
|
+
<div className="relative min-h-0 flex-1 overflow-hidden bg-black">
|
|
751
|
+
<canvas
|
|
752
|
+
ref={canvasRef}
|
|
753
|
+
className={cn(
|
|
754
|
+
"absolute inset-0 m-auto max-h-full max-w-full touch-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent",
|
|
755
|
+
!showCanvas && "invisible",
|
|
756
|
+
)}
|
|
757
|
+
onPointerDown={pointerDown}
|
|
758
|
+
onPointerUp={pointerUp}
|
|
759
|
+
onPointerCancel={() => {
|
|
760
|
+
pointerStartRef.current = null;
|
|
761
|
+
}}
|
|
762
|
+
onContextMenu={contextMenu}
|
|
763
|
+
onWheel={wheel}
|
|
764
|
+
aria-label={`Interactive ${props.target?.kind ?? "computer"} view`}
|
|
765
|
+
/>
|
|
766
|
+
<textarea
|
|
767
|
+
ref={inputRef}
|
|
768
|
+
defaultValue=""
|
|
769
|
+
onInput={(event) => input(event.currentTarget.value)}
|
|
770
|
+
onKeyDown={keyDown}
|
|
771
|
+
className="pointer-events-none absolute left-1/2 top-1/2 size-px resize-none overflow-hidden opacity-0"
|
|
772
|
+
aria-label="Computer keyboard input"
|
|
773
|
+
autoCapitalize="off"
|
|
774
|
+
autoCorrect="off"
|
|
775
|
+
spellCheck={false}
|
|
776
|
+
/>
|
|
777
|
+
{!showCanvas ? (
|
|
778
|
+
<ComputerViewportFallback
|
|
779
|
+
observation={props.observation}
|
|
780
|
+
machineLocked={props.machineLocked}
|
|
781
|
+
connectionState={props.connectionState}
|
|
782
|
+
error={props.connectionError}
|
|
783
|
+
onAction={(action) => enqueue(action, null)}
|
|
784
|
+
onReconnect={props.onReconnect}
|
|
785
|
+
/>
|
|
786
|
+
) : null}
|
|
787
|
+
{props.mutating ? (
|
|
788
|
+
<div className="pointer-events-none absolute right-3 top-3 flex items-center gap-1.5 rounded-full border border-white/10 bg-black/65 px-2.5 py-1 text-[11px] text-white/80 backdrop-blur">
|
|
789
|
+
<LoaderCircleIcon className="size-3 animate-spin" /> Acting
|
|
790
|
+
</div>
|
|
791
|
+
) : null}
|
|
792
|
+
</div>
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function ComputerViewportFallback(props: {
|
|
797
|
+
observation: ComputerObservation | null;
|
|
798
|
+
machineLocked: boolean;
|
|
799
|
+
connectionState: string;
|
|
800
|
+
error: Error | null;
|
|
801
|
+
onAction: (action: ComputerAction) => void;
|
|
802
|
+
onReconnect: () => void;
|
|
803
|
+
}) {
|
|
804
|
+
if (props.machineLocked) {
|
|
805
|
+
return (
|
|
806
|
+
<div className="absolute inset-0 grid place-items-center bg-og-bg p-6">
|
|
807
|
+
<div className="max-w-sm text-center">
|
|
808
|
+
<LockKeyholeIcon className="mx-auto size-6 text-og-muted" />
|
|
809
|
+
<p className="mt-3 text-og-menu font-medium text-og-fg">Machine locked</p>
|
|
810
|
+
<p className="mt-1 text-og-control leading-5 text-og-muted">
|
|
811
|
+
Live frames and input stay private until someone unlocks the machine.
|
|
812
|
+
</p>
|
|
813
|
+
</div>
|
|
814
|
+
</div>
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
const interactive = semanticNodes(props.observation)
|
|
818
|
+
.filter((node) => semanticAction(node) !== null)
|
|
819
|
+
.slice(0, 10);
|
|
820
|
+
return (
|
|
821
|
+
<div className="absolute inset-0 grid place-items-center bg-og-bg p-6">
|
|
822
|
+
<div className="w-full max-w-md rounded-og-lg border border-og-border bg-og-surface-0 p-4 shadow-lg">
|
|
823
|
+
<div className="flex items-center gap-2">
|
|
824
|
+
{props.error ? (
|
|
825
|
+
<CircleAlertIcon className="size-4 text-og-status-error" />
|
|
826
|
+
) : (
|
|
827
|
+
<LoaderCircleIcon className="size-4 animate-spin text-og-muted" />
|
|
828
|
+
)}
|
|
829
|
+
<p className="text-og-menu font-medium text-og-fg">
|
|
830
|
+
{props.error
|
|
831
|
+
? "Live view disconnected"
|
|
832
|
+
: computerConnectionLabel(props.connectionState)}
|
|
833
|
+
</p>
|
|
834
|
+
</div>
|
|
835
|
+
{interactive.length > 0 ? (
|
|
836
|
+
<div className="mt-3 border-t border-og-border pt-3">
|
|
837
|
+
<p className="mb-2 text-og-xs text-og-subtle">Native controls remain available</p>
|
|
838
|
+
<div className="flex flex-wrap gap-1.5">
|
|
839
|
+
{interactive.map((node) => (
|
|
840
|
+
<button
|
|
841
|
+
key={node.ref}
|
|
842
|
+
type="button"
|
|
843
|
+
onClick={() => {
|
|
844
|
+
const action = semanticAction(node);
|
|
845
|
+
if (action) props.onAction(action);
|
|
846
|
+
}}
|
|
847
|
+
className="rounded-og-sm border border-og-border bg-og-surface-1 px-2 py-1 text-og-control text-og-fg transition hover:bg-og-surface-2"
|
|
848
|
+
>
|
|
849
|
+
{node.name || node.role}
|
|
850
|
+
</button>
|
|
851
|
+
))}
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
) : null}
|
|
855
|
+
{props.error ? (
|
|
856
|
+
<button
|
|
857
|
+
type="button"
|
|
858
|
+
onClick={props.onReconnect}
|
|
859
|
+
className="mt-3 text-og-control font-medium text-og-accent hover:underline"
|
|
860
|
+
>
|
|
861
|
+
Reconnect
|
|
862
|
+
</button>
|
|
863
|
+
) : null}
|
|
864
|
+
</div>
|
|
865
|
+
</div>
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function ComputerSemanticPanel(props: {
|
|
870
|
+
observation: ComputerObservation | null;
|
|
871
|
+
mutating: boolean;
|
|
872
|
+
onAction: (action: ComputerAction) => void;
|
|
873
|
+
}) {
|
|
874
|
+
const nodes = semanticNodes(props.observation).slice(0, 100);
|
|
875
|
+
return (
|
|
876
|
+
<aside className="w-64 shrink-0 overflow-y-auto border-l border-og-border bg-og-surface-0 p-2">
|
|
877
|
+
<div className="mb-2 flex items-center gap-1.5 px-1 text-og-xs font-medium uppercase tracking-[0.1em] text-og-subtle">
|
|
878
|
+
<KeyboardIcon className="size-3" /> Native controls
|
|
879
|
+
</div>
|
|
880
|
+
{nodes.length === 0 ? (
|
|
881
|
+
<p className="px-1 py-2 text-og-control leading-5 text-og-muted">
|
|
882
|
+
This view has no accessibility controls. Use the live image instead.
|
|
883
|
+
</p>
|
|
884
|
+
) : (
|
|
885
|
+
<div className="space-y-0.5">
|
|
886
|
+
{nodes.map((node) => {
|
|
887
|
+
const action = semanticAction(node);
|
|
888
|
+
return (
|
|
889
|
+
<button
|
|
890
|
+
key={node.ref}
|
|
891
|
+
type="button"
|
|
892
|
+
disabled={!action || props.mutating}
|
|
893
|
+
onClick={() => {
|
|
894
|
+
if (action) props.onAction(action);
|
|
895
|
+
}}
|
|
896
|
+
className="flex w-full items-start gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2 disabled:cursor-default disabled:hover:bg-transparent"
|
|
897
|
+
>
|
|
898
|
+
<span className="mt-0.5 text-[10px] uppercase text-og-subtle">{node.role}</span>
|
|
899
|
+
<span className="min-w-0 flex-1 truncate text-og-control text-og-fg">
|
|
900
|
+
{node.name || semanticValue(node) || node.identifier || "Unnamed control"}
|
|
901
|
+
</span>
|
|
902
|
+
</button>
|
|
903
|
+
);
|
|
904
|
+
})}
|
|
905
|
+
</div>
|
|
906
|
+
)}
|
|
907
|
+
</aside>
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function ComputerStatusBar(props: {
|
|
912
|
+
session: ComputerSession | null;
|
|
913
|
+
target: ComputerTarget | null;
|
|
914
|
+
connectionState: string;
|
|
915
|
+
refreshing: boolean;
|
|
916
|
+
showControls: boolean;
|
|
917
|
+
controlCount: number;
|
|
918
|
+
onToggleControls: () => void;
|
|
919
|
+
}) {
|
|
920
|
+
const screen = props.target?.kind === "screen";
|
|
921
|
+
return (
|
|
922
|
+
<div className="flex h-8 shrink-0 items-center gap-2 border-t border-og-border bg-og-surface-0 px-2 text-og-xs text-og-subtle">
|
|
923
|
+
<span
|
|
924
|
+
className={cn(
|
|
925
|
+
"size-1.5 rounded-full",
|
|
926
|
+
props.connectionState === "live" ? "bg-og-status-running" : "bg-og-muted",
|
|
927
|
+
)}
|
|
928
|
+
/>
|
|
929
|
+
<span>
|
|
930
|
+
{props.connectionState === "live" ? "Live" : computerConnectionLabel(props.connectionState)}
|
|
931
|
+
</span>
|
|
932
|
+
<span className="min-w-0 flex-1 truncate">
|
|
933
|
+
{screen
|
|
934
|
+
? "Full screen · input may move pointer and focus"
|
|
935
|
+
: props.session?.capabilities?.backgroundActions
|
|
936
|
+
? "Window · native actions can stay in the background"
|
|
937
|
+
: (props.target?.kind ?? "Computer")}
|
|
938
|
+
</span>
|
|
939
|
+
{screen ? <MousePointer2Icon className="size-3" aria-hidden /> : null}
|
|
940
|
+
{props.refreshing ? <LoaderCircleIcon className="size-3 animate-spin" /> : null}
|
|
941
|
+
<button
|
|
942
|
+
type="button"
|
|
943
|
+
onClick={props.onToggleControls}
|
|
944
|
+
aria-pressed={props.showControls}
|
|
945
|
+
className={cn(
|
|
946
|
+
"rounded px-1.5 py-0.5 transition hover:bg-og-surface-2 hover:text-og-fg",
|
|
947
|
+
props.showControls && "bg-og-surface-2 text-og-fg",
|
|
948
|
+
)}
|
|
949
|
+
>
|
|
950
|
+
Controls {props.controlCount}
|
|
951
|
+
</button>
|
|
952
|
+
</div>
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function ComputerNotice(props: { icon: ReactNode; text: string; className?: string }) {
|
|
957
|
+
return (
|
|
958
|
+
<div
|
|
959
|
+
className={cn(
|
|
960
|
+
"grid h-full place-items-center bg-og-bg text-og-control text-og-muted",
|
|
961
|
+
props.className,
|
|
962
|
+
)}
|
|
963
|
+
>
|
|
964
|
+
<div className="flex items-center gap-2">
|
|
965
|
+
{props.icon}
|
|
966
|
+
{props.text}
|
|
967
|
+
</div>
|
|
968
|
+
</div>
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function semanticNodes(observation: ComputerObservation | null): InteractionSemanticNode[] {
|
|
973
|
+
if (observation?.semantic?.kind !== "snapshot") return [];
|
|
974
|
+
const result: InteractionSemanticNode[] = [];
|
|
975
|
+
const visit = (node: InteractionSemanticNode) => {
|
|
976
|
+
result.push(node);
|
|
977
|
+
node.children?.forEach(visit);
|
|
978
|
+
};
|
|
979
|
+
observation.semantic.roots.forEach(visit);
|
|
980
|
+
return result;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function semanticAction(node: InteractionSemanticNode): ComputerAction | null {
|
|
984
|
+
const invoke = ["invoke", "press", "click"].some((action) => node.actions.includes(action));
|
|
985
|
+
if (invoke) {
|
|
986
|
+
return {
|
|
987
|
+
type: "semantic",
|
|
988
|
+
locator: { kind: "ref", ref: node.ref },
|
|
989
|
+
action: "invoke",
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
if (node.actions.includes("focus")) {
|
|
993
|
+
return {
|
|
994
|
+
type: "semantic",
|
|
995
|
+
locator: { kind: "ref", ref: node.ref },
|
|
996
|
+
action: "focus",
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function semanticValue(node: InteractionSemanticNode): string | null {
|
|
1003
|
+
return typeof node.value === "string" ? node.value : null;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function paintCanvas(
|
|
1007
|
+
canvas: HTMLCanvasElement,
|
|
1008
|
+
image: CanvasImageSource,
|
|
1009
|
+
width: number,
|
|
1010
|
+
height: number,
|
|
1011
|
+
): void {
|
|
1012
|
+
canvas.width = width;
|
|
1013
|
+
canvas.height = height;
|
|
1014
|
+
const context = canvas.getContext("2d", { alpha: false });
|
|
1015
|
+
if (!context) throw new Error("Computer canvas is unavailable.");
|
|
1016
|
+
context.drawImage(image, 0, 0, width, height);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function loadImage(url: string): Promise<HTMLImageElement> {
|
|
1020
|
+
return new Promise((resolve, reject) => {
|
|
1021
|
+
const image = new Image();
|
|
1022
|
+
image.onload = () => resolve(image);
|
|
1023
|
+
image.onerror = () => reject(new Error("Computer frame image could not be decoded."));
|
|
1024
|
+
image.src = url;
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function computerPoint(
|
|
1029
|
+
canvas: HTMLCanvasElement | null,
|
|
1030
|
+
frame: ComputerFrame,
|
|
1031
|
+
clientX: number,
|
|
1032
|
+
clientY: number,
|
|
1033
|
+
): { x: number; y: number } | null {
|
|
1034
|
+
if (!canvas) return null;
|
|
1035
|
+
const rect = canvas.getBoundingClientRect();
|
|
1036
|
+
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
1037
|
+
return {
|
|
1038
|
+
x: Math.max(0, Math.min(frame.width - 1, ((clientX - rect.left) / rect.width) * frame.width)),
|
|
1039
|
+
y: Math.max(0, Math.min(frame.height - 1, ((clientY - rect.top) / rect.height) * frame.height)),
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function computerKey(event: KeyboardEvent<HTMLTextAreaElement>): string | null {
|
|
1044
|
+
const special = new Set([
|
|
1045
|
+
"Enter",
|
|
1046
|
+
"Tab",
|
|
1047
|
+
"Escape",
|
|
1048
|
+
"Backspace",
|
|
1049
|
+
"Delete",
|
|
1050
|
+
"ArrowUp",
|
|
1051
|
+
"ArrowDown",
|
|
1052
|
+
"ArrowLeft",
|
|
1053
|
+
"ArrowRight",
|
|
1054
|
+
"Home",
|
|
1055
|
+
"End",
|
|
1056
|
+
"PageUp",
|
|
1057
|
+
"PageDown",
|
|
1058
|
+
]);
|
|
1059
|
+
const modified = event.altKey || event.ctrlKey || event.metaKey;
|
|
1060
|
+
if (!modified && !special.has(event.key)) return null;
|
|
1061
|
+
const parts: string[] = [];
|
|
1062
|
+
if (event.ctrlKey) parts.push("Control");
|
|
1063
|
+
if (event.altKey) parts.push("Alt");
|
|
1064
|
+
if (event.metaKey) parts.push("Meta");
|
|
1065
|
+
if (event.shiftKey) parts.push("Shift");
|
|
1066
|
+
parts.push(event.key === " " ? "Space" : event.key);
|
|
1067
|
+
return parts.join("+");
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function sameFrameFence(left: ComputerFrame, right: ComputerFrame): boolean {
|
|
1071
|
+
return (
|
|
1072
|
+
left.frameId === right.frameId &&
|
|
1073
|
+
left.computerSessionId === right.computerSessionId &&
|
|
1074
|
+
left.targetId === right.targetId &&
|
|
1075
|
+
left.targetGeneration === right.targetGeneration
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function isLiveComputer(session: ComputerSession): boolean {
|
|
1080
|
+
return !["ending", "ended", "failed", "lost"].includes(session.lifecycle);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function platformLabel(session: ComputerSession): string {
|
|
1084
|
+
switch (session.platform) {
|
|
1085
|
+
case "macos":
|
|
1086
|
+
return "Mac";
|
|
1087
|
+
case "linux":
|
|
1088
|
+
return "Linux";
|
|
1089
|
+
case "windows":
|
|
1090
|
+
return "Windows";
|
|
1091
|
+
default:
|
|
1092
|
+
return "Computer";
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function placementLabel(session: ComputerSession): string {
|
|
1097
|
+
switch (session.placement.kind) {
|
|
1098
|
+
case "sandbox_group":
|
|
1099
|
+
return "Agent sandbox";
|
|
1100
|
+
case "connected_machine":
|
|
1101
|
+
return "Connected machine";
|
|
1102
|
+
case "attached_device":
|
|
1103
|
+
return "Your machine";
|
|
1104
|
+
case "external_provider":
|
|
1105
|
+
return "Cloud computer";
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function lifecycleLabel(lifecycle: ComputerSession["lifecycle"]): string {
|
|
1110
|
+
switch (lifecycle) {
|
|
1111
|
+
case "starting":
|
|
1112
|
+
return "Opening computer…";
|
|
1113
|
+
case "suspending":
|
|
1114
|
+
return "Pausing computer…";
|
|
1115
|
+
case "suspended":
|
|
1116
|
+
return "Computer paused";
|
|
1117
|
+
case "restoring":
|
|
1118
|
+
return "Restoring computer…";
|
|
1119
|
+
case "ending":
|
|
1120
|
+
return "Closing computer…";
|
|
1121
|
+
default:
|
|
1122
|
+
return "Connecting to computer…";
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function computerConnectionLabel(state: string): string {
|
|
1127
|
+
switch (state) {
|
|
1128
|
+
case "attaching":
|
|
1129
|
+
return "Opening computer view…";
|
|
1130
|
+
case "connecting":
|
|
1131
|
+
return "Connecting…";
|
|
1132
|
+
case "reconnecting":
|
|
1133
|
+
return "Reconnecting…";
|
|
1134
|
+
case "error":
|
|
1135
|
+
return "Disconnected";
|
|
1136
|
+
default:
|
|
1137
|
+
return "Waiting for computer…";
|
|
1138
|
+
}
|
|
1139
|
+
}
|