@opengeni/react 1.0.1 → 2.3.0-canary.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 +33 -10
- package/dist/artifacts-spreadsheet.js +1 -1
- package/dist/artifacts.js +1 -1
- package/dist/browser-viewer-GXUICIAG.js +14 -0
- package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
- package/dist/chunk-4MDLEHXL.js.map +1 -0
- package/dist/chunk-5YK2ZDIG.js +116 -0
- package/dist/chunk-5YK2ZDIG.js.map +1 -0
- package/dist/chunk-H5YGYNXS.js +329 -0
- package/dist/chunk-H5YGYNXS.js.map +1 -0
- package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
- package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
- package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
- package/dist/chunk-JOAVA2TW.js.map +1 -0
- package/dist/{chunk-Y7OEDUJ5.js → chunk-LS4POUTY.js} +1057 -426
- package/dist/chunk-LS4POUTY.js.map +1 -0
- package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
- package/dist/chunk-LTCHJ4MS.js.map +1 -0
- package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
- package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
- package/dist/chunk-QGBJ6GHG.js +2300 -0
- package/dist/chunk-QGBJ6GHG.js.map +1 -0
- package/dist/{chunk-6KLX2RH5.js → chunk-RSXJN73N.js} +365 -21
- package/dist/chunk-RSXJN73N.js.map +1 -0
- package/dist/{chunk-B6WPZSTT.js → chunk-SE4W4WQ3.js} +78 -16
- package/dist/chunk-SE4W4WQ3.js.map +1 -0
- package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
- package/dist/chunk-W6NFJXSA.js.map +1 -0
- package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
- package/dist/chunk-X4A66EWZ.js.map +1 -0
- package/dist/chunk-X4JAFD5R.js +3907 -0
- package/dist/chunk-X4JAFD5R.js.map +1 -0
- package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
- package/dist/chunk-XXWH2JLC.js.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/components/browser-viewer.d.ts +7 -1
- package/dist/components/computer-viewer.d.ts +4 -1
- package/dist/components/enrollment-consent.d.ts +6 -3
- package/dist/components/file-browser.d.ts +7 -1
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +1 -1
- package/dist/components/machine-card.d.ts +4 -1
- package/dist/components/machines/machine-detail.d.ts +4 -1
- package/dist/components/machines/machine-operation-policy.d.ts +11 -0
- package/dist/components/machines-dashboard.d.ts +3 -1
- package/dist/components/markdown.d.ts +20 -1
- package/dist/components/message-timeline.d.ts +1 -1
- package/dist/components/model-policy-picker.d.ts +2 -0
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/sandbox-files.d.ts +8 -2
- package/dist/components/sandbox-workspace.d.ts +33 -3
- package/dist/components/session-chrome.d.ts +15 -3
- package/dist/components/workspace-dock.d.ts +6 -2
- package/dist/composer.js +5 -5
- package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
- package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
- package/dist/hooks/channel-cache.d.ts +23 -0
- package/dist/hooks/shared-machines-list.d.ts +14 -0
- package/dist/hooks/use-attached-browsers.d.ts +2 -1
- package/dist/hooks/use-channels.d.ts +1 -0
- package/dist/hooks/use-composer.d.ts +37 -9
- package/dist/hooks/use-machine-chip.d.ts +3 -3
- package/dist/hooks/use-machines.d.ts +19 -1
- package/dist/hooks/use-sandbox-files.d.ts +1 -0
- package/dist/hooks/use-session-background-commands.d.ts +14 -0
- package/dist/hooks/use-turn-queue.d.ts +12 -0
- package/dist/hooks/use-workspace-sessions.d.ts +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1096 -623
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +29 -181
- package/dist/interaction.js.map +1 -1
- package/dist/lib/interaction-errors.d.ts +3 -0
- package/dist/machines.d.ts +4 -2
- package/dist/machines.js +10 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/platform-activity-row-BJHUHXAT.js +169 -0
- package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
- package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
- package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +6 -3
- package/dist/realtime.js +36 -14
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.d.ts +1 -1
- package/dist/session-ui.js +6 -2
- package/dist/session.d.ts +2 -2
- package/dist/session.js +4 -4
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/platform-activity-row.d.ts +15 -0
- package/dist/timeline/screenshot-lightbox.d.ts +1 -1
- package/dist/timeline/shared.d.ts +4 -2
- package/dist/timeline/types.d.ts +50 -5
- package/dist/types/machines.d.ts +2 -2
- package/package.json +3 -3
- package/src/client.ts +12 -1
- package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
- package/src/components/browser-viewer.tsx +361 -58
- package/src/components/chat-composer.tsx +1 -1
- package/src/components/composer.tsx +6 -6
- package/src/components/computer-viewer.tsx +368 -115
- package/src/components/enrollment-consent.tsx +64 -6
- package/src/components/file-browser.tsx +117 -2
- package/src/components/human-input-form.tsx +103 -59
- package/src/components/human-input-surface.tsx +1 -1
- package/src/components/machine-card.tsx +111 -0
- package/src/components/machine-input-display.ts +15 -0
- package/src/components/machines/machine-detail.tsx +47 -0
- package/src/components/machines/machine-operation-policy.tsx +179 -0
- package/src/components/machines-dashboard.tsx +6 -0
- package/src/components/markdown.tsx +174 -20
- package/src/components/message-timeline.tsx +215 -18
- package/src/components/model-policy-picker.tsx +9 -17
- package/src/components/pierre-file.tsx +1 -1
- package/src/components/queue-surface-implementation.tsx +82 -16
- package/src/components/sandbox-files.tsx +135 -19
- package/src/components/sandbox-workspace.tsx +204 -92
- package/src/components/session-chrome.tsx +415 -208
- package/src/components/session-status.tsx +1 -1
- package/src/components/user-message-body.tsx +168 -48
- package/src/components/workspace-dock.tsx +141 -175
- package/src/hooks/channel-cache.ts +104 -0
- package/src/hooks/shared-machines-list.ts +239 -0
- package/src/hooks/use-attached-browsers.ts +5 -1
- package/src/hooks/use-browser-frame-stream.ts +6 -1
- package/src/hooks/use-browser-session.ts +6 -4
- package/src/hooks/use-browser-sessions.ts +7 -1
- package/src/hooks/use-channels.ts +32 -10
- package/src/hooks/use-composer.ts +760 -291
- package/src/hooks/use-computer-frame-stream.ts +14 -9
- package/src/hooks/use-computer-session.ts +16 -14
- package/src/hooks/use-computer-sessions.ts +8 -1
- package/src/hooks/use-last-started-turn-policy.ts +1 -1
- package/src/hooks/use-machine-chip.ts +23 -6
- package/src/hooks/use-machines.ts +104 -6
- package/src/hooks/use-sandbox-files.ts +195 -140
- package/src/hooks/use-sandbox-git.ts +58 -29
- package/src/hooks/use-session-background-commands.ts +56 -0
- package/src/hooks/use-session-capabilities.ts +18 -0
- package/src/hooks/use-turn-queue.ts +111 -12
- package/src/hooks/use-workspace-sessions.ts +5 -1
- package/src/index.ts +26 -4
- package/src/lib/interaction-errors.ts +18 -0
- package/src/machines.ts +9 -1
- package/src/model-policy.ts +10 -2
- package/src/realtime/realtime-control.tsx +53 -17
- package/src/session-context.ts +1 -0
- package/src/session-ui.ts +6 -1
- package/src/session.ts +9 -1
- package/src/timeline/activity-rail.tsx +20 -75
- package/src/timeline/index.ts +4 -0
- package/src/timeline/platform-activity-row.tsx +201 -0
- package/src/timeline/projection.ts +529 -25
- package/src/timeline/screenshot-lightbox.tsx +48 -14
- package/src/timeline/shared.tsx +8 -2
- package/src/timeline/tool-renderers.tsx +104 -6
- package/src/timeline/turn-summary.tsx +6 -1
- package/src/timeline/types.ts +77 -2
- package/src/types/machines.ts +10 -0
- package/styles/compiled.css +115 -104
- package/dist/chunk-5UQPNLIO.js.map +0 -1
- package/dist/chunk-6KLX2RH5.js.map +0 -1
- package/dist/chunk-B6WPZSTT.js.map +0 -1
- package/dist/chunk-HA46CYQK.js.map +0 -1
- package/dist/chunk-IGC6A2ZI.js.map +0 -1
- package/dist/chunk-LLOLUHVG.js +0 -5937
- package/dist/chunk-LLOLUHVG.js.map +0 -1
- package/dist/chunk-MKPIAGVL.js.map +0 -1
- package/dist/chunk-R3AZOAB5.js.map +0 -1
- package/dist/chunk-XBZWOM2K.js.map +0 -1
- package/dist/chunk-Y7OEDUJ5.js.map +0 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
- /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
package/README.md
CHANGED
|
@@ -514,12 +514,18 @@ state remains application-owned; durable draft and session state remain in
|
|
|
514
514
|
previous resume semantics.
|
|
515
515
|
- `useComposer(sessionId, { sendExtras, effectiveControl })` — revisioned private
|
|
516
516
|
draft, Send, Steer, and workstream Pause/Resume state. `send()` appends in
|
|
517
|
-
visible queue order; `steer()`
|
|
517
|
+
visible queue order (including while paused); `steer()` puts the new direction
|
|
518
|
+
directly in chat and supersedes the current direction. Resume is always an
|
|
519
|
+
explicit control action and never an implicit side effect of Send. Drafts
|
|
518
520
|
autosave with optimistic concurrency, survive failed sends, and reuse one
|
|
519
|
-
`clientEventId` across retries so the server dedupes.
|
|
520
|
-
`
|
|
521
|
-
|
|
522
|
-
|
|
521
|
+
`clientEventId` across retries so the server dedupes. `composer.policy` and
|
|
522
|
+
`setModel` / `setReasoningEffort` / `setLatencyMode` expose the exact policy
|
|
523
|
+
owned by that actor/session draft; policy is `null` until hydration completes.
|
|
524
|
+
`sendExtras` (object or function evaluated at send time) is only for
|
|
525
|
+
non-policy per-message fields such as live attachment resources and connection
|
|
526
|
+
authority. Disabling durable draft persistence requires an explicit
|
|
527
|
+
`initialPolicy`; no session or workspace fallback is invented. All human input
|
|
528
|
+
is plain chat text by design; approvals flow as control events
|
|
523
529
|
(`useSessionControl`), not bespoke widgets.
|
|
524
530
|
- `useTurnQueue(sessionId, { events })` — the one server-authoritative human
|
|
525
531
|
prompt queue with `moveTurn`, crash-safe `editTurn`, identity-preserving
|
|
@@ -591,8 +597,9 @@ intentional changes should regenerate those snapshots and review the diff.
|
|
|
591
597
|
|
|
592
598
|
## Components
|
|
593
599
|
|
|
594
|
-
- `ChatComposer` — auto-growing textarea, Enter-to-send (IME-safe),
|
|
595
|
-
|
|
600
|
+
- `ChatComposer` — auto-growing textarea, Enter-to-send (IME-safe), direct
|
|
601
|
+
Cmd/Ctrl+Enter steering, pause/resume controls, and inline finite error
|
|
602
|
+
recovery. Paused sends visibly join the queue without resuming. Slots for app chrome:
|
|
596
603
|
`controlsStart` (footer controls like model pickers / attach buttons),
|
|
597
604
|
`header` (e.g. attachment chips above the field), and `onPaste`
|
|
598
605
|
(paste-image-to-attach). Its advanced controller and compound primitives are
|
|
@@ -616,8 +623,19 @@ intentional changes should regenerate those snapshots and review the diff.
|
|
|
616
623
|
runnable latency modes such as Fast. It accepts either `ClientModel[]` or
|
|
617
624
|
catalog-backed `PickerModelRow[]`, and supports host-supplied labels.
|
|
618
625
|
- `Markdown` — the timeline's markdown renderer (GFM), also usable standalone.
|
|
626
|
+
With `onSandboxFile`, a valid `sandbox:<path>[:line]` application link becomes
|
|
627
|
+
an in-session Open action. The callback receives the decoded path unchanged;
|
|
628
|
+
the optional line is positive and 1-based. Invalid sandbox references render
|
|
629
|
+
as disabled text, never as empty or browser-navigation links.
|
|
619
630
|
- `CommandPalette` — the slash-command palette UI over `useSlashCommands`.
|
|
620
631
|
|
|
632
|
+
`SandboxWorkspace.openFileRequest` is the host seam for that callback. Each new
|
|
633
|
+
`requestId` selects Files, opens the exact path on the already selected session
|
|
634
|
+
target, reveals its loaded/lazy ancestors in the file tree, selects and scrolls
|
|
635
|
+
to the file, and optionally focuses the requested line. A request without a line
|
|
636
|
+
opens the top of the file; file publication/download remains a separate explicit
|
|
637
|
+
artifact action.
|
|
638
|
+
|
|
621
639
|
The timeline is extensible: `createToolRegistry` / `defaultToolRegistry` plug
|
|
622
640
|
per-tool renderers, and the rendering primitives (`ActivityDisclosure`,
|
|
623
641
|
`ScreenshotFigure`, `TermBlock`, `LightboxProvider`, …) compose custom rows with
|
|
@@ -674,7 +692,8 @@ capability document so every surface degrades to a reason instead of crashing.
|
|
|
674
692
|
firehose and the real interactive PTY over the minted `pty-ws` cell.
|
|
675
693
|
- `useDesktopStream` — the noVNC socket, hot-swapped on box rollover via
|
|
676
694
|
`stream.url.rotated`.
|
|
677
|
-
- Components: `WorkspaceDock` (the resizable/collapsible right-hand dock
|
|
695
|
+
- Components: `WorkspaceDock` (the resizable/collapsible right-hand dock with a
|
|
696
|
+
vertical activity rail, persistent visited panels, and a panel-local Hide action),
|
|
678
697
|
`FileBrowser` / `SandboxFiles`, `DiffView` / `PierreDiff` / `PierreFile`,
|
|
679
698
|
`CodeEditor`, `SandboxTerminal`, and `DesktopViewer`.
|
|
680
699
|
|
|
@@ -702,11 +721,15 @@ never pull it in.
|
|
|
702
721
|
plus the `MachineView` / `MachineState` / `MetricSample` view-model types.
|
|
703
722
|
|
|
704
723
|
```tsx
|
|
705
|
-
import {
|
|
724
|
+
import {
|
|
725
|
+
MACHINES_SESSION_POLL_MS,
|
|
726
|
+
MachinesDashboard,
|
|
727
|
+
useMachines,
|
|
728
|
+
} from "@opengeni/react/machines";
|
|
706
729
|
|
|
707
730
|
function Fleet({ sessionId }: { sessionId: string }) {
|
|
708
731
|
const { machines, activeSandboxId, attach, attachingSandboxId, refresh } =
|
|
709
|
-
useMachines({ sessionId, pollIntervalMs:
|
|
732
|
+
useMachines({ sessionId, pollIntervalMs: MACHINES_SESSION_POLL_MS });
|
|
710
733
|
return (
|
|
711
734
|
<MachinesDashboard
|
|
712
735
|
machines={machines}
|
package/dist/artifacts.js
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BrowserViewer
|
|
3
|
+
} from "./chunk-X4JAFD5R.js";
|
|
4
|
+
import "./chunk-H5YGYNXS.js";
|
|
5
|
+
import "./chunk-5YK2ZDIG.js";
|
|
6
|
+
import "./chunk-KZ73RL76.js";
|
|
7
|
+
import "./chunk-NEXLLTP4.js";
|
|
8
|
+
import "./chunk-LAKLF4PA.js";
|
|
9
|
+
import "./chunk-22KP6LR5.js";
|
|
10
|
+
import "./chunk-JVYQRRC4.js";
|
|
11
|
+
export {
|
|
12
|
+
BrowserViewer
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=browser-viewer-GXUICIAG.js.map
|
|
@@ -1,101 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
STREAM_KIND_DESKTOP,
|
|
3
|
+
STREAM_ROLE_CLIENT,
|
|
4
|
+
decodeStreamFrame,
|
|
5
|
+
decodeStreamOpenAck,
|
|
6
|
+
encodeStreamOpen,
|
|
7
|
+
interactionHostPlatform
|
|
8
|
+
} from "./chunk-5YK2ZDIG.js";
|
|
1
9
|
import {
|
|
2
10
|
cn
|
|
3
11
|
} from "./chunk-22KP6LR5.js";
|
|
4
12
|
|
|
5
|
-
// src/lib/relay-wire.ts
|
|
6
|
-
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
|
7
|
-
var STREAM_KIND_DESKTOP = 2;
|
|
8
|
-
var STREAM_KIND_PTY = 1;
|
|
9
|
-
var STREAM_KIND_BROWSER = 3;
|
|
10
|
-
var STREAM_KIND_COMPUTER = 4;
|
|
11
|
-
var STREAM_ROLE_CLIENT = 2;
|
|
12
|
-
var STREAM_CLOSE_REASON_NORMAL = 1;
|
|
13
|
-
function encodeChannel(message, writer) {
|
|
14
|
-
if (message.channelId !== "") writer.uint32(10).string(message.channelId);
|
|
15
|
-
if (message.workspaceId !== "") writer.uint32(18).string(message.workspaceId);
|
|
16
|
-
if (message.agentId !== "") writer.uint32(26).string(message.agentId);
|
|
17
|
-
if (message.kind !== 0) writer.uint32(32).int32(message.kind);
|
|
18
|
-
if (message.port !== 0) writer.uint32(40).uint32(message.port);
|
|
19
|
-
return writer;
|
|
20
|
-
}
|
|
21
|
-
function encodeStreamOpen(message) {
|
|
22
|
-
const writer = new BinaryWriter();
|
|
23
|
-
encodeChannel(message.channel, writer.uint32(10).fork()).join();
|
|
24
|
-
if (message.token !== "") writer.uint32(18).string(message.token);
|
|
25
|
-
if (message.role !== 0) writer.uint32(24).int32(message.role);
|
|
26
|
-
if (message.resumeFromSeq !== "0") writer.uint32(32).uint64(message.resumeFromSeq);
|
|
27
|
-
return writer.finish();
|
|
28
|
-
}
|
|
29
|
-
function encodeStreamClose(message) {
|
|
30
|
-
const writer = new BinaryWriter();
|
|
31
|
-
if (message.channelId !== "") writer.uint32(10).string(message.channelId);
|
|
32
|
-
if (message.reason !== 0) writer.uint32(16).int32(message.reason);
|
|
33
|
-
if (message.message !== "") writer.uint32(26).string(message.message);
|
|
34
|
-
return writer.finish();
|
|
35
|
-
}
|
|
36
|
-
function encodeStreamFrame(message) {
|
|
37
|
-
const writer = new BinaryWriter();
|
|
38
|
-
if (message.channelId !== "") writer.uint32(10).string(message.channelId);
|
|
39
|
-
if (message.seq !== "0") writer.uint32(16).uint64(message.seq);
|
|
40
|
-
if (message.data.length > 0) writer.uint32(26).bytes(message.data);
|
|
41
|
-
if (message.producedAtMs !== "0") writer.uint32(32).uint64(message.producedAtMs);
|
|
42
|
-
return writer.finish();
|
|
43
|
-
}
|
|
44
|
-
function decodeAgentErrorMessage(reader, length) {
|
|
45
|
-
const end = reader.pos + length;
|
|
46
|
-
let message;
|
|
47
|
-
while (reader.pos < end) {
|
|
48
|
-
const tag = reader.uint32();
|
|
49
|
-
if (tag >>> 3 === 2 && tag === 18) {
|
|
50
|
-
message = reader.string();
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if ((tag & 7) === 4 || tag === 0) break;
|
|
54
|
-
reader.skip(tag & 7);
|
|
55
|
-
}
|
|
56
|
-
return message !== void 0 ? { message } : {};
|
|
57
|
-
}
|
|
58
|
-
function decodeStreamOpenAck(bytes) {
|
|
59
|
-
const reader = new BinaryReader(bytes);
|
|
60
|
-
const end = reader.len;
|
|
61
|
-
let accepted = false;
|
|
62
|
-
let error;
|
|
63
|
-
while (reader.pos < end) {
|
|
64
|
-
const tag = reader.uint32();
|
|
65
|
-
if (tag >>> 3 === 1 && tag === 8) {
|
|
66
|
-
accepted = reader.bool();
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
if (tag >>> 3 === 2 && tag === 18) {
|
|
70
|
-
error = decodeAgentErrorMessage(reader, reader.uint32());
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if ((tag & 7) === 4 || tag === 0) break;
|
|
74
|
-
reader.skip(tag & 7);
|
|
75
|
-
}
|
|
76
|
-
return error !== void 0 ? { accepted, error } : { accepted };
|
|
77
|
-
}
|
|
78
|
-
function decodeStreamFrame(bytes) {
|
|
79
|
-
const reader = new BinaryReader(bytes);
|
|
80
|
-
const end = reader.len;
|
|
81
|
-
let seq = "0";
|
|
82
|
-
let data = new Uint8Array(0);
|
|
83
|
-
while (reader.pos < end) {
|
|
84
|
-
const tag = reader.uint32();
|
|
85
|
-
if (tag >>> 3 === 2 && tag === 16) {
|
|
86
|
-
seq = reader.uint64().toString();
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
if (tag >>> 3 === 3 && tag === 26) {
|
|
90
|
-
data = new Uint8Array(reader.bytes());
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
if ((tag & 7) === 4 || tag === 0) break;
|
|
94
|
-
reader.skip(tag & 7);
|
|
95
|
-
}
|
|
96
|
-
return { seq, data };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
13
|
// src/hooks/use-relay-frame-stream.ts
|
|
100
14
|
import { useEffect, useRef, useState } from "react";
|
|
101
15
|
var TAG_OPEN = 1;
|
|
@@ -447,15 +361,6 @@ import {
|
|
|
447
361
|
useRef as useRef3,
|
|
448
362
|
useState as useState3
|
|
449
363
|
} from "react";
|
|
450
|
-
|
|
451
|
-
// src/lib/host-platform.ts
|
|
452
|
-
function interactionHostPlatform() {
|
|
453
|
-
if (typeof navigator === "undefined") return "other";
|
|
454
|
-
const platform = navigator.platform || navigator.userAgent;
|
|
455
|
-
return /Mac|iPhone|iPad|iPod/u.test(platform) ? "mac" : "other";
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// src/components/desktop-viewer.tsx
|
|
459
364
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
460
365
|
var DESKTOP_MEDIA_BACKGROUND = "#000";
|
|
461
366
|
function isHardUnavailable(reason) {
|
|
@@ -915,19 +820,8 @@ function defaultNotice(title, body, onRetry) {
|
|
|
915
820
|
}
|
|
916
821
|
|
|
917
822
|
export {
|
|
918
|
-
STREAM_KIND_PTY,
|
|
919
|
-
STREAM_KIND_BROWSER,
|
|
920
|
-
STREAM_KIND_COMPUTER,
|
|
921
|
-
STREAM_ROLE_CLIENT,
|
|
922
|
-
STREAM_CLOSE_REASON_NORMAL,
|
|
923
|
-
encodeStreamOpen,
|
|
924
|
-
encodeStreamClose,
|
|
925
|
-
encodeStreamFrame,
|
|
926
|
-
decodeStreamOpenAck,
|
|
927
|
-
decodeStreamFrame,
|
|
928
823
|
useRelayFrameStream,
|
|
929
824
|
useDesktopStream,
|
|
930
|
-
interactionHostPlatform,
|
|
931
825
|
DesktopViewer
|
|
932
826
|
};
|
|
933
|
-
//# sourceMappingURL=chunk-
|
|
827
|
+
//# sourceMappingURL=chunk-4MDLEHXL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/use-relay-frame-stream.ts","../src/hooks/use-desktop-stream.ts","../src/components/desktop-viewer.tsx"],"sourcesContent":["import {\n decodeStreamFrame,\n decodeStreamOpenAck,\n encodeStreamOpen,\n STREAM_KIND_DESKTOP,\n STREAM_ROLE_CLIENT,\n} from \"../lib/relay-wire\";\nimport type { DesktopConnectionState, DesktopStreamCapability } from \"@opengeni/sdk\";\nimport { type RefObject, useEffect, useRef, useState } from \"react\";\n\n/**\n * The minimal WebSocket surface the frame renderer drives. Lets tests (and a\n * future transport swap) inject a fake without a live socket — the exact\n * analogue of `DesktopRfbFactory` for the noVNC path. Structurally a subset of\n * the browser `WebSocket`.\n */\nexport interface DesktopWebSocketLike {\n binaryType: string;\n send(data: ArrayBuffer): void;\n close(): void;\n addEventListener(\n type: \"open\" | \"message\" | \"error\" | \"close\",\n listener: (ev: { data?: unknown } & Record<string, unknown>) => void,\n ): void;\n removeEventListener(\n type: \"open\" | \"message\" | \"error\" | \"close\",\n listener: (ev: { data?: unknown } & Record<string, unknown>) => void,\n ): void;\n}\n\nexport type DesktopWebSocketFactory = (url: string) => DesktopWebSocketLike;\n\nexport type UseRelayFrameStreamOptions = {\n /** The desktop cell of the negotiated capabilities (`capabilities.DesktopStream`). */\n capability: DesktopStreamCapability | null;\n /** The mount target. A `<canvas>` is appended here on connect. */\n containerRef: RefObject<HTMLDivElement | null>;\n /** Custom socket factory (tests / a transport swap). Defaults to `new WebSocket(url)`. */\n webSocketFactory?: DesktopWebSocketFactory | undefined;\n};\n\nexport type UseRelayFrameStreamResult = {\n state: DesktopConnectionState;\n error: Error | null;\n /** Tear down + reopen the socket (e.g. after a drop, once a fresh url arrives). */\n reconnect: () => void;\n};\n\n// Relay datagram tags: byte[0] of every binary message. Mirrors the proven wire\n// protocol in `apps/api/scripts/diagnose-mac-desktop-stream.ts`.\nconst TAG_OPEN = 1;\nconst TAG_OPENACK = 2;\nconst TAG_FRAME = 3;\n\n/** Build a relay datagram: a fresh Uint8Array of `body.length + 1` with the tag\n * at [0] and the protobuf body copied at offset 1 (so `.buffer` is exact). */\nfunction datagram(tag: number, body: Uint8Array): Uint8Array {\n const out = new Uint8Array(body.length + 1);\n out[0] = tag;\n out.set(body, 1);\n return out;\n}\n\nfunction defaultWebSocketFactory(url: string): DesktopWebSocketLike {\n // The browser `WebSocket` is structurally a superset of `DesktopWebSocketLike`\n // (its event maps are stricter); cast so the seam stays loosely typed for fakes.\n return new WebSocket(url) as unknown as DesktopWebSocketLike;\n}\n\n/**\n * VIEW-ONLY PNG-frame renderer for a SELF-HOSTED desktop stream — the relay\n * transport (`transport: \"relay-frames\"`, `client: \"frames\"`). The self-hosted\n * agent produces one PNG per frame as a protobuf datagram over the relay; there\n * is no RFB/noVNC here. This hook opens the relay DESKTOP channel as a CLIENT\n * (mirroring the proven diagnostic wire protocol), decodes each PNG onto a\n * `<canvas>` mounted into `containerRef`, and drives the connection state\n * machine: idle → connecting (ws opening) → connected (first painted frame) →\n * error (ws error/close/ack rejection).\n *\n * It is DORMANT for any other transport (or no url): it stays idle and touches\n * nothing, so the noVNC path owns the surface. SSR-safe: the socket + DOM attach\n * live inside `useEffect`.\n *\n * Backpressure (critical — frames are ~1.8MB at ~10fps): at most one frame is\n * decoded at a time; a frame arriving mid-decode replaces the pending one\n * (latest-wins), so a slow decode can never build an unbounded queue.\n */\nexport function useRelayFrameStream(\n options: UseRelayFrameStreamOptions,\n): UseRelayFrameStreamResult {\n const { capability, containerRef, webSocketFactory } = options;\n const [state, setState] = useState<DesktopConnectionState>(\"idle\");\n const [error, setError] = useState<Error | null>(null);\n const [nonce, setNonce] = useState(0);\n const stateRef = useRef<DesktopConnectionState>(\"idle\");\n const setBoth = (next: DesktopConnectionState) => {\n stateRef.current = next;\n setState(next);\n };\n\n const reconnect = () => setNonce((n) => n + 1);\n\n const transport = capability?.transport ?? null;\n const url = capability?.url ?? null;\n const token = capability?.token ?? null;\n\n // The factory is read via a ref so swapping it never re-opens the socket (it is\n // not a connect-effect dependency), symmetric with the noVNC `rfbFactory`.\n const factoryRef = useRef(webSocketFactory);\n factoryRef.current = webSocketFactory;\n\n useEffect(() => {\n // SSR / no DOM: stay idle and show the placeholder.\n if (typeof window === \"undefined\") return;\n // This hook OWNS the surface ONLY for the frame transport; anything else (or\n // no live url) → dormant idle so the noVNC path can drive.\n if (transport !== \"relay-frames\" || !url) {\n setBoth(\"idle\");\n return;\n }\n const container = containerRef.current;\n if (!container) {\n // Mount target not in the DOM yet (tab hidden) — stay idle so a re-attach\n // nudge can re-run this effect once the container exists.\n setBoth(\"idle\");\n return;\n }\n\n // Parse the 4 relay query params the channel is keyed on.\n let channel: {\n channelId: string;\n workspaceId: string;\n agentId: string;\n kind: number;\n port: number;\n };\n try {\n const u = new URL(url);\n channel = {\n channelId: u.searchParams.get(\"channel\") ?? \"\",\n workspaceId: u.searchParams.get(\"ws\") ?? \"\",\n agentId: u.searchParams.get(\"agent\") ?? \"\",\n kind: STREAM_KIND_DESKTOP,\n port: Number(u.searchParams.get(\"port\") ?? \"0\"),\n };\n } catch (cause) {\n setError(cause instanceof Error ? cause : new Error(String(cause)));\n setBoth(\"error\");\n return;\n }\n\n setError(null);\n setBoth(\"connecting\");\n\n // Canvas mount — mirror the noVNC surface conventions: absolute-fill the\n // panel and CONTAIN the framebuffer (aspect-preserved, centered, letterboxed)\n // — never distorted, never overflowing. `max-*: 100%` + `margin: auto` +\n // intrinsic size gives fit-to-panel without a resize observer.\n const canvas = document.createElement(\"canvas\");\n canvas.setAttribute(\"data-opengeni-desktop-frames\", \"\");\n const style = canvas.style;\n style.position = \"absolute\";\n style.top = \"0\";\n style.left = \"0\";\n style.right = \"0\";\n style.bottom = \"0\";\n style.margin = \"auto\";\n style.maxWidth = \"100%\";\n style.maxHeight = \"100%\";\n style.width = \"auto\";\n style.height = \"auto\";\n style.display = \"block\";\n style.imageRendering = \"auto\";\n container.appendChild(canvas);\n const ctx = canvas.getContext(\"2d\");\n\n let disposed = false;\n let acked = false;\n // Backpressure: decode at most ONE frame at a time; a frame arriving mid-\n // decode replaces `pending` (keep only the LATEST) — never a queue.\n let decoding = false;\n let pending: Uint8Array | null = null;\n\n const drainLatest = async () => {\n if (decoding) return;\n decoding = true;\n try {\n while (pending) {\n if (disposed) break;\n const data = pending;\n pending = null;\n let bmp: ImageBitmap;\n try {\n // Copy the exact frame bytes into a fresh ArrayBuffer-backed view:\n // ts-proto's `bytes()` hands back a subarray VIEW into the larger\n // message buffer, so slicing isolates just this PNG (and satisfies the\n // `BlobPart` typing, which rejects the generic `ArrayBufferLike`).\n bmp = await createImageBitmap(new Blob([new Uint8Array(data)], { type: \"image/png\" }));\n } catch {\n // A corrupt/partial frame is skipped; latest-wins keeps us live.\n continue;\n }\n if (disposed) {\n bmp.close();\n break;\n }\n // Size the backing store to the frame's natural WxH on the first frame\n // (or when the resolution changes); CSS then scales the element to fit.\n if (canvas.width !== bmp.width || canvas.height !== bmp.height) {\n canvas.width = bmp.width;\n canvas.height = bmp.height;\n }\n ctx?.drawImage(bmp, 0, 0, canvas.width, canvas.height);\n bmp.close();\n // First painted frame → connected.\n if (!disposed && stateRef.current !== \"connected\") setBoth(\"connected\");\n }\n } finally {\n decoding = false;\n }\n };\n\n let ws: DesktopWebSocketLike;\n try {\n ws = (factoryRef.current ?? defaultWebSocketFactory)(url);\n } catch (cause) {\n setError(cause instanceof Error ? cause : new Error(String(cause)));\n setBoth(\"error\");\n canvas.remove();\n return;\n }\n ws.binaryType = \"arraybuffer\";\n\n const onOpen = () => {\n if (disposed) return;\n const body = encodeStreamOpen({\n channel,\n token: token ?? \"\",\n role: STREAM_ROLE_CLIENT,\n resumeFromSeq: \"0\",\n });\n try {\n ws.send(datagram(TAG_OPEN, body).buffer as ArrayBuffer);\n } catch {\n // Socket already closing; the close/error handler surfaces it.\n }\n };\n\n const onMessage = (ev: { data?: unknown }) => {\n if (disposed) return;\n const data = ev.data;\n if (!(data instanceof ArrayBuffer)) return;\n const buf = new Uint8Array(data);\n if (buf.length === 0) return;\n const tag = buf[0];\n const rest = buf.subarray(1);\n if (tag === TAG_OPENACK) {\n let ack: ReturnType<typeof decodeStreamOpenAck>;\n try {\n ack = decodeStreamOpenAck(rest);\n } catch {\n return;\n }\n if (!ack.accepted) {\n setError(\n new Error(\n ack.error?.message\n ? `desktop stream rejected: ${ack.error.message}`\n : \"desktop stream rejected by the relay\",\n ),\n );\n setBoth(\"error\");\n try {\n ws.close();\n } catch {\n // already closed\n }\n return;\n }\n // Accepted — stay \"connecting\" until the first frame paints.\n acked = true;\n } else if (tag === TAG_FRAME) {\n let fr: ReturnType<typeof decodeStreamFrame>;\n try {\n fr = decodeStreamFrame(rest);\n } catch {\n return;\n }\n if (fr.data && fr.data.length > 0) {\n pending = fr.data; // latest-wins backpressure\n void drainLatest();\n }\n }\n };\n\n const onError = () => {\n if (disposed) return;\n setError((prev) => prev ?? new Error(\"desktop stream connection error\"));\n setBoth(\"error\");\n };\n\n const onClose = () => {\n if (disposed) return;\n setError(\n (prev) =>\n prev ??\n new Error(acked ? \"desktop stream closed\" : \"desktop stream closed before it opened\"),\n );\n setBoth(\"error\");\n };\n\n ws.addEventListener(\"open\", onOpen);\n ws.addEventListener(\"message\", onMessage);\n ws.addEventListener(\"error\", onError);\n ws.addEventListener(\"close\", onClose);\n\n return () => {\n disposed = true;\n pending = null;\n ws.removeEventListener(\"open\", onOpen);\n ws.removeEventListener(\"message\", onMessage);\n ws.removeEventListener(\"error\", onError);\n ws.removeEventListener(\"close\", onClose);\n try {\n ws.close();\n } catch {\n // ignore teardown errors\n }\n canvas.remove();\n };\n // ONLY a real transport change reopens: a fresh url (rotation), a new token,\n // the transport flipping, or a manual reconnect (`nonce`). The factory is read\n // via a ref and must never re-open the socket.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [url, token, transport, nonce]);\n\n return { state, error, reconnect };\n}\n","import {\n desktopSocketUrl,\n nextDesktopState,\n type DesktopConnectionState,\n type DesktopRfbFactory,\n type DesktopRfbLike,\n type DesktopStreamCapability,\n} from \"@opengeni/sdk\";\nimport { type RefObject, useCallback, useEffect, useRef, useState } from \"react\";\nimport { type DesktopWebSocketFactory, useRelayFrameStream } from \"./use-relay-frame-stream\";\n\nexport type UseDesktopStreamOptions = {\n /** The desktop cell of the negotiated capabilities (`capabilities.DesktopStream`). */\n capability: DesktopStreamCapability | null;\n /** The mount target. RFB (or the frame canvas) attaches here on connect. */\n containerRef: RefObject<HTMLDivElement | null>;\n /** Read-only by default (v1 ruling H). interactive only when cap.mode allows. */\n interactive?: boolean | undefined;\n scaleViewport?: boolean | undefined;\n /** Custom RFB factory (tests / a WebRTC swap). Defaults to a lazy @novnc/novnc. */\n rfbFactory?: DesktopRfbFactory | undefined;\n /** Additional WebSocket protocols for an authenticated placement proxy. */\n webSocketProtocols?: string[] | undefined;\n /** Custom socket factory for the `relay-frames` transport (tests). Defaults to\n * `new WebSocket(url)`. Mirrors `rfbFactory` for the frame renderer. */\n webSocketFactory?: DesktopWebSocketFactory | undefined;\n};\n\nexport type UseDesktopStreamResult = {\n state: DesktopConnectionState;\n error: Error | null;\n /** Manual reconnect (e.g. after a securityfailure once a fresh URL arrives). */\n reconnect: () => void;\n /** Synchronous input on the already-open RFB authority. */\n sendKey?: ((keysym: number, code: string, down?: boolean) => boolean) | undefined;\n};\n\n/** Lazy-load @novnc/novnc's RFB as the default factory. Imported inside the\n * connect effect so SSR / non-desktop bundles never pull the DOM-only lib.\n * @novnc/novnc ships no types and a single `export default class RFB`. The\n * specifier is STATIC (`import(\"@novnc/novnc\")`) so Vite can pre-bundle and\n * resolve it — a runtime-string indirection with `@vite-ignore` (the previous\n * approach) hands the browser a bare specifier and throws\n * \"Failed to resolve module specifier '@novnc/novnc'\". The dynamic form keeps\n * it out of the SSR / non-desktop critical path while staying resolvable. */\nasync function defaultRfbFactory(): Promise<DesktopRfbFactory> {\n const mod = (await import(\"@novnc/novnc\")) as unknown as {\n default: new (\n t: HTMLElement,\n u: string,\n o: {\n credentials?: { password?: string | undefined } | undefined;\n wsProtocols?: string[] | undefined;\n },\n ) => DesktopRfbLike;\n };\n const RFB = mod.default;\n return (target, url, opts) => new RFB(target, url, opts);\n}\n\n/**\n * Drive the noVNC RFB lifecycle from a `DesktopStreamCapability`, using the\n * SDK's `desktop.ts` reducer + `desktopSocketUrl`. SSR-safe: the RFB import and\n * the DOM attach happen inside `useEffect`, so a server render is a no-op and\n * the component shows its placeholder until hydration.\n *\n * Read-only is enforced at three layers: `capability.mode` (server) →\n * `interactive` prop → `RFB.viewOnly`. v1 always resolves to read-only. On a\n * capability `url` change (a rotation), the old RFB disconnects and a fresh one\n * connects to the new URL — a brief \"desktop blink\", acceptable on rollover.\n *\n * Transport dispatch: a Modal box negotiates `transport: \"vnc-ws\"` and drives the\n * noVNC RFB below. A SELF-HOSTED machine negotiates `transport: \"relay-frames\"`\n * (PNG-per-frame over the relay, view-only) — that path is delegated to\n * `useRelayFrameStream`, which paints a `<canvas>`. Both hooks are ALWAYS called\n * (rules of hooks); each is dormant for the other's transport, and we return the\n * result of whichever owns the surface. The public interface is identical either\n * way, so `DesktopViewer` is transport-agnostic.\n */\nexport function useDesktopStream(options: UseDesktopStreamOptions): UseDesktopStreamResult {\n const {\n capability,\n containerRef,\n interactive,\n scaleViewport,\n rfbFactory,\n webSocketProtocols,\n webSocketFactory,\n } = options;\n\n // The self-hosted PNG-frame path. Always invoked (rules of hooks); dormant\n // unless `transport === \"relay-frames\"`, in which case it owns the surface.\n const frameStream = useRelayFrameStream({\n capability,\n containerRef,\n ...(webSocketFactory ? { webSocketFactory } : {}),\n });\n const [state, setState] = useState<DesktopConnectionState>(\"idle\");\n const [error, setError] = useState<Error | null>(null);\n const [nonce, setNonce] = useState(0);\n const stateRef = useRef<DesktopConnectionState>(\"idle\");\n const setBoth = (next: DesktopConnectionState) => {\n stateRef.current = next;\n setState(next);\n };\n\n const reconnect = () => setNonce((n) => n + 1);\n const sendKey = useCallback((keysym: number, code: string, down?: boolean): boolean => {\n const rfb = rfbRef.current;\n if (!rfb?.sendKey || rfb.viewOnly || stateRef.current !== \"connected\") return false;\n rfb.sendKey(keysym, code, down);\n return true;\n }, []);\n\n const url = capability?.url ?? null;\n // The credential is keyed into the connect effect so a token rotation (new\n // password) reconnects, but NOT folded into the whole-`capability` identity —\n // a benign re-negotiation that re-mints the same cell must not churn the socket.\n const token = capability?.token ?? null;\n const transport = capability?.transport ?? null;\n const mode = capability?.mode ?? \"read-only\";\n // For direct sandbox RFB, authorization is carried in WebSocket\n // subprotocols rather than `capability.token`. The view grant rotates before\n // expiry while the URL stays stable, so the protocol credential is a real\n // connection boundary: keeping the old socket leaves the viewer black once\n // browserd expires it. Key the effect on the primitive value (not the caller's\n // array identity) so credential rotation reconnects exactly once without\n // churning on ordinary renders.\n const webSocketProtocolsKey = webSocketProtocols?.join(\"\\u0000\") ?? \"\";\n\n // The live RFB handle. Holding it in a ref lets us flip view-only (take\n // control / return control) and re-scale on the OPEN connection instead of\n // tearing the socket down — the old behaviour reconnected on every\n // `interactive` change, which read as a constant \"refresh\" and (combined with\n // the canvas churn) bounced control back to watch.\n const rfbRef = useRef<DesktopRfbLike | null>(null);\n // Latest input/scale/factory the connect effect reads via refs so they are NOT\n // connect-effect dependencies: changing them must update the live RFB in place,\n // never reconnect it.\n const interactiveRef = useRef(interactive);\n const scaleViewportRef = useRef(scaleViewport);\n const modeRef = useRef(mode);\n const rfbFactoryRef = useRef(rfbFactory);\n const webSocketProtocolsRef = useRef(webSocketProtocols);\n interactiveRef.current = interactive;\n scaleViewportRef.current = scaleViewport;\n modeRef.current = mode;\n rfbFactoryRef.current = rfbFactory;\n webSocketProtocolsRef.current = webSocketProtocols;\n\n // read-only is forced when the server says so OR the caller didn't opt in.\n const viewOnlyFor = (m: string, want: boolean | undefined) => m === \"read-only\" || !want;\n\n useEffect(() => {\n // SSR / no DOM / no usable transport: stay idle and show the placeholder.\n if (typeof window === \"undefined\") return;\n if (transport !== \"vnc-ws\" || !url) {\n setBoth(\"idle\");\n return;\n }\n const container = containerRef.current;\n if (!container) {\n // The mount target isn't in the DOM yet (e.g. the tab is still hidden).\n // Stay idle (not a stale negotiating/connecting) so a re-attach nudge —\n // fired by the viewer on becoming visible / on mount — can re-run this\n // effect once the container exists. Without this the surface can stick\n // forever on the idle scrim and never open a socket.\n setBoth(\"idle\");\n return;\n }\n\n let rfb: DesktopRfbLike | null = null;\n let disposed = false;\n setError(null);\n setBoth(\"negotiating\");\n\n const onConnect = () => {\n if (!disposed) setBoth(nextDesktopState(stateRef.current, { type: \"connected\" }));\n };\n const onDisconnect = () => {\n if (!disposed) setBoth(nextDesktopState(stateRef.current, { type: \"disconnected\" }));\n };\n const onSecurityFailure = () => {\n if (disposed) return;\n setError(new Error(\"desktop authentication failed (token expired or revoked)\"));\n setBoth(nextDesktopState(stateRef.current, { type: \"fail\" }));\n };\n\n void (async () => {\n try {\n const factory = rfbFactoryRef.current ?? (await defaultRfbFactory());\n if (disposed) return;\n const socketUrl = desktopSocketUrl({ url });\n setBoth(nextDesktopState(stateRef.current, { type: \"negotiated\" }));\n rfb = factory(container, socketUrl, {\n credentials: token ? { password: token } : undefined,\n ...(webSocketProtocolsRef.current ? { wsProtocols: webSocketProtocolsRef.current } : {}),\n });\n rfb.viewOnly = viewOnlyFor(modeRef.current, interactiveRef.current);\n // Fit-to-panel: SCALE the 1280x800 framebuffer down to the container\n // (aspect-preserved) and never 1:1-clip. `clipViewport=false` is pinned\n // explicitly — noVNC forces it off while scaling, but a fresh RFB on a\n // url rotation could otherwise start from a stale clip and read as\n // \"zoomed in\". Order matters: clip off, then scale on.\n rfb.clipViewport = false;\n rfb.scaleViewport = scaleViewportRef.current ?? true;\n rfb.addEventListener(\"connect\", onConnect);\n rfb.addEventListener(\"disconnect\", onDisconnect);\n rfb.addEventListener(\"securityfailure\", onSecurityFailure);\n rfbRef.current = rfb;\n } catch (cause) {\n if (!disposed) {\n setError(cause instanceof Error ? cause : new Error(String(cause)));\n setBoth(\"error\");\n }\n }\n })();\n\n return () => {\n disposed = true;\n if (rfbRef.current === rfb) rfbRef.current = null;\n if (rfb) {\n rfb.removeEventListener?.(\"connect\", onConnect);\n rfb.removeEventListener?.(\"disconnect\", onDisconnect);\n rfb.removeEventListener?.(\"securityfailure\", onSecurityFailure);\n try {\n rfb.disconnect();\n } catch {\n // ignore teardown errors\n }\n }\n };\n // ONLY a real transport change reconnects: a fresh url (rotation), a new\n // credential (RFB password OR bearer subprotocol), the transport flipping,\n // or an explicit manual reconnect\n // (`nonce`). `interactive`/`scaleViewport`/`mode`/`rfbFactory` are read via\n // refs and applied live below — they must never re-open the socket.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [url, token, transport, webSocketProtocolsKey, nonce]);\n\n // Apply take-control / return-control to the OPEN connection in place. noVNC\n // honours `viewOnly` live, so flipping it neither blinks the surface nor drops\n // the framebuffer — the cure for the take-control \"refresh + auto-release\" loop.\n useEffect(() => {\n const rfb = rfbRef.current;\n if (rfb) rfb.viewOnly = viewOnlyFor(mode, interactive);\n // viewOnlyFor is pure; re-run only when the inputs change.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [interactive, mode, state]);\n\n // Re-scale the live RFB in place (no reconnect) when the scale preference flips\n // OR the connection state advances (e.g. the first framebuffer paints after a\n // take-control flip / a re-attach). Re-asserting clip-off + scale-on here is the\n // belt-and-suspenders against a \"zoomed\" surface: it re-fits the canvas to the\n // panel on every meaningful transition, so a transient mis-measure at connect\n // time can never stick.\n useEffect(() => {\n const rfb = rfbRef.current;\n if (rfb) {\n rfb.clipViewport = false;\n rfb.scaleViewport = scaleViewport ?? true;\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [scaleViewport, state]);\n\n // Delegate to whichever transport owns the surface. For `relay-frames` the\n // noVNC effect above is dormant (it bailed to idle) and the frame renderer\n // drives; for `vnc-ws` (and everything else) the noVNC path drives while the\n // frame renderer stays idle. Same public shape either way.\n if (transport === \"relay-frames\") return frameStream;\n return { state, error, reconnect, sendKey };\n}\n","import type {\n CapabilityUnavailableReason,\n DesktopRfbFactory,\n DesktopStreamCapability,\n} from \"@opengeni/sdk\";\nimport { LoaderCircleIcon, MonitorIcon, MousePointerClickIcon, WifiOffIcon } from \"lucide-react\";\nimport {\n type ClipboardEvent,\n type KeyboardEvent as ReactKeyboardEvent,\n type ReactNode,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { interactionHostPlatform, type InteractionHostPlatform } from \"../lib/host-platform\";\nimport { useDesktopStream } from \"../hooks/use-desktop-stream\";\nimport type { DesktopWebSocketFactory } from \"../hooks/use-relay-frame-stream\";\n\nexport type DesktopViewerProps = {\n /** The desktop cell of the negotiated capabilities (`capabilities.DesktopStream`). */\n capability: DesktopStreamCapability | null;\n /**\n * Initial control mode. Default false (watch). When the user flips\n * \"Take control\" the viewer drives input — but only if `capability.mode`\n * permits it (server-gated; a read-only deployment disables the toggle).\n * Pass a value to control it externally; omit to let the viewer own the state.\n */\n interactive?: boolean | undefined;\n /** Render the built-in Watching ⇄ Take control toggle (default true). */\n showControlToggle?: boolean | undefined;\n scaleViewport?: boolean | undefined;\n /** Custom RFB factory (tests / a WebRTC swap). Defaults to lazy @novnc/novnc. */\n rfbFactory?: DesktopRfbFactory | undefined;\n /** Authenticated controller protocols for proxied RFB transports. */\n webSocketProtocols?: string[] | undefined;\n /** Custom socket factory for the self-hosted `relay-frames` transport (tests).\n * Defaults to `new WebSocket(url)`. Mirrors `rfbFactory`. */\n webSocketFactory?: DesktopWebSocketFactory | undefined;\n /**\n * Optional host-owned paste path. ComputerSession hosts use their native,\n * target-scoped clipboard action instead of relying on an RFB server to sync\n * ClientCutText into the graphical seat. Return true once the host accepted\n * the text. Without a host authority the viewer leaves paste untouched rather\n * than claiming success for an unreliable display-server clipboard heuristic.\n */\n onPasteText?: ((text: string) => boolean) | undefined;\n /** OS behind the RFB seat. Used only to translate the host's primary shortcut\n * (Cmd on macOS, Ctrl elsewhere) to the target's primary modifier. */\n targetPlatform?: \"linux\" | \"macos\" | \"windows\" | null | undefined;\n /**\n * @deprecated Desktop acknowledgment is automatic. Retained as an ignored\n * compatibility prop so existing embedders keep compiling.\n */\n renderConsentGate?: ((onAccept: () => void, shared: boolean) => ReactNode) | undefined;\n /**\n * Called once when the viewer mounts. Hosts use this to engage the desktop\n * holder immediately when the Desktop tab opens.\n */\n onActivate?: (() => void) | undefined;\n /**\n * Called automatically when the server requires acknowledgment of the\n * un-redacted (and possibly shared) pixel plane. The viewer shows its ordinary\n * opening state while this completes; no click-through gate is rendered.\n */\n onAcknowledge?: (() => void | Promise<void>) | undefined;\n /**\n * Whether the host has the viewer attach engaged. Drives the cold-state\n * behaviour: when watching, a cold-but-warmable lease AUTO-WARMS (and re-warms\n * when the box drains) instead of dead-ending. Mounting the viewer counts as\n * watching when this is omitted.\n */\n watching?: boolean | undefined;\n /**\n * Request a (re)warm of the sandbox WITHOUT re-acknowledging (the consent has\n * already been recorded — only the box drained). The host wires this to\n * \"engage the viewer attach + re-negotiate\". Called automatically when a\n * watched desktop is found cold-but-warmable, and behind the manual retry on\n * the warming notice. Distinct from the automatic acknowledgment request.\n */\n onWarm?: (() => void) | undefined;\n /** Shown when transport is null (headless backend / degraded / disabled). */\n renderUnavailable?: ((reason: CapabilityUnavailableReason | null) => ReactNode) | undefined;\n /** Shown while the box is cold/warming (no live address yet). */\n renderWarming?: (() => ReactNode) | undefined;\n /** Shown when the per-session viewer cap (429) was hit. */\n renderViewerCap?: (() => ReactNode) | undefined;\n /** Surface the 429 cap state from `useSessionCapabilities().viewerCapReached`. */\n viewerCapReached?: boolean | undefined;\n /**\n * Connect watchdog (ms): if a live url is present but the RFB hasn't connected\n * within this window, surface a \"Couldn't connect\" + Reconnect instead of an\n * eternal idle scrim. Default 13000. 0 disables the watchdog.\n */\n connectTimeoutMs?: number | undefined;\n className?: string | undefined;\n};\n\n/**\n * The derived desktop surface state. A single source of truth so the overlay,\n * the scrim, the auto-warm effect, and the watchdog all agree. Priority order\n * (highest first): viewer-cap → unavailable → acknowledgment-error → warming →\n * connect-failed → error → opening/connecting → connected.\n */\ntype DesktopUiState =\n | \"viewer_cap\" // 429 — the per-session live-viewer limit is reached.\n | \"unavailable\" // genuinely unsupported (headless/policy/os/backend/not-provisioned).\n | \"acknowledgment_error\" // automatic un-redacted/shared acknowledgment failed.\n | \"opening\" // acknowledgment or viewer attach is in flight.\n | \"warming\" // cold-but-warmable: box not running yet (we auto-warm + spin).\n | \"connecting\" // url is live, RFB negotiating/handshaking (we spin, with a watchdog).\n | \"connect_failed\" // watchdog fired: url present but never connected.\n | \"error\" // RFB error / securityfailure after a connect.\n | \"connected\"; // live framebuffer painting.\n\n// Media-inspection exception: the framebuffer sits on true black so screenshots\n// and desktop pixels are not tinted by the product chrome palette.\nconst DESKTOP_MEDIA_BACKGROUND = \"#000\";\n\n/** Reasons that are genuinely-unavailable (never warmable from the viewer). A\n * `lease_cold` is deliberately EXCLUDED — that's the warmable cold state. */\nfunction isHardUnavailable(reason: CapabilityUnavailableReason | null): boolean {\n switch (reason) {\n case \"backend_unsupported\":\n case \"os_unsupported\":\n case \"not_provisioned\":\n case \"disabled_by_policy\":\n case \"tier_headless\":\n return true;\n default:\n // null or \"lease_cold\" → not a hard reason (cold-but-warmable / live).\n return false;\n }\n}\n\n/**\n * The desktop surface: a noVNC client connecting to the Channel-B scoped tunnel\n * URL from the capability doc. Owns the mount `<div ref>`, drives\n * `useDesktopStream` (SSR-safe lazy RFB), and renders a real\n * cold → warming → connecting → connected → error state machine with live\n * feedback (spinners, transitions) — never a dead black box with stale text.\n *\n * The read-only vs interactive decision is enforced server-first\n * (`capability.mode`): when the deployment advertises mode \"interactive\" the\n * viewer can TAKE CONTROL and drive the mouse & keyboard into the box's :0; a\n * \"read-only\" deployment disables the take-control affordance (graceful, with a\n * reason).\n *\n * Warming: a cold-but-warmable lease (`reason: \"lease_cold\"`) is NOT a dead end.\n * When the viewer is mounted, it asks the host to engage the desktop and records\n * any required acknowledgment automatically. A cold-but-warmable box then\n * (re)warms through `onWarm`; if it later drains to cold it re-warms.\n * Genuinely-unavailable surfaces (headless/policy/os/backend) keep a clear,\n * static unavailable notice.\n */\nexport function DesktopViewer({\n capability,\n interactive,\n showControlToggle = true,\n scaleViewport,\n rfbFactory,\n webSocketProtocols,\n webSocketFactory,\n onPasteText,\n targetPlatform,\n onActivate,\n onAcknowledge,\n watching,\n onWarm,\n renderUnavailable,\n renderWarming,\n renderViewerCap,\n viewerCapReached,\n connectTimeoutMs = 13_000,\n className,\n}: DesktopViewerProps) {\n const containerRef = useRef<HTMLDivElement | null>(null);\n const onActivateRef = useRef(onActivate);\n const onAcknowledgeRef = useRef(onAcknowledge);\n onActivateRef.current = onActivate;\n onAcknowledgeRef.current = onAcknowledge;\n const acknowledgmentAttemptedRef = useRef(false);\n const [acknowledgmentError, setAcknowledgmentError] = useState<Error | null>(null);\n const [acknowledgmentRetryNonce, setAcknowledgmentRetryNonce] = useState(0);\n // Local control state when not externally controlled. The server gate\n // (`capability.mode`) is the hard ceiling — a read-only deployment can never\n // be flipped to interactive regardless of this toggle.\n const [takeControl, setTakeControl] = useState(interactive ?? false);\n const externallyControlled = interactive !== undefined;\n const serverAllowsControl = capability?.mode !== \"read-only\";\n const wantControl = externallyControlled ? interactive : takeControl;\n const inControl = Boolean(wantControl) && serverAllowsControl;\n\n // Mounting this component means the Desktop surface is being viewed. A host\n // may still pass its holder state explicitly while the activation callback\n // flips that state on the first mount.\n const isWatching = watching ?? true;\n\n // ── Decide the rendered state (before touching the stream hook) ─────────────\n const transportNull = !capability || capability.transport === null;\n const reason = capability?.reason ?? null;\n const needsAcknowledgment =\n capability?.requiresAcknowledgment === true && capability.acknowledged !== true;\n // No live address yet on an otherwise-live transport (post-ack, mid-warm).\n const noLiveAddress = Boolean(capability) && !transportNull && !capability!.url;\n\n // Cold-but-warmable: the lease is cold (`lease_cold`) OR the transport is up\n // but no url has been minted yet — either way warming, not unavailable.\n const coldWarmable =\n (transportNull && reason === \"lease_cold\") || (!transportNull && noLiveAddress);\n // Genuinely-unavailable: transport null for a HARD reason (not lease_cold).\n const hardUnavailable = transportNull && isHardUnavailable(reason);\n\n // Selecting Desktop mounts the viewer, which is itself the user's intent to\n // open it. Engage the holder immediately — no separate \"watch\" click.\n useEffect(() => {\n onActivateRef.current?.();\n }, []);\n\n // Preserve the server-side acknowledgment/audit contract without making the\n // user click through a disclosure card. One automatic attempt runs for each\n // unacknowledged episode; a genuine request failure gets an explicit retry.\n useEffect(() => {\n if (!needsAcknowledgment) {\n acknowledgmentAttemptedRef.current = false;\n setAcknowledgmentError(null);\n return;\n }\n if (acknowledgmentAttemptedRef.current) return;\n acknowledgmentAttemptedRef.current = true;\n setAcknowledgmentError(null);\n const acknowledge = onAcknowledgeRef.current;\n if (!acknowledge) {\n setAcknowledgmentError(new Error(\"Desktop acknowledgment is not configured.\"));\n return;\n }\n let active = true;\n void Promise.resolve()\n .then(() => acknowledge())\n .catch((cause) => {\n if (!active) return;\n setAcknowledgmentError(cause instanceof Error ? cause : new Error(String(cause)));\n });\n return () => {\n active = false;\n };\n }, [needsAcknowledgment, acknowledgmentRetryNonce]);\n\n const retryAcknowledgment = () => {\n acknowledgmentAttemptedRef.current = false;\n setAcknowledgmentError(null);\n setAcknowledgmentRetryNonce((nonce) => nonce + 1);\n };\n\n // Release control WITHOUT ever swallowing a key the desktop needs. Esc, and\n // every other key, pass straight through to noVNC/:0 — vital for vim, menus,\n // and dialogs inside the box. Exactly ONE non-trapping keyboard exit:\n // • A single non-conflicting chord — Ctrl+Alt+Shift pressed on its own (no\n // other key) — which no app binds, so it never eats a real keystroke.\n // (Plus the always-visible \"Return control\" button in the in-control bar.)\n //\n // We deliberately DO NOT release on pointer-leave (or window blur): those fire\n // as a SIDE EFFECT of connecting — the noVNC canvas re-laying-out, the surface\n // grabbing focus, the connecting scrim swapping in — which bounced control\n // straight back to \"watch\" the instant the user took it. Control is given up\n // only on an explicit, intentional gesture (the button or the chord).\n useEffect(() => {\n if (!inControl || externallyControlled) return;\n const onKey = (event: KeyboardEvent) => {\n // Only the bare modifier chord releases; if any non-modifier key is also\n // down (event.key is a real key like \"a\"/\"Escape\"), let it pass through.\n if (\n event.ctrlKey &&\n event.altKey &&\n event.shiftKey &&\n (event.key === \"Control\" || event.key === \"Alt\" || event.key === \"Shift\")\n ) {\n event.preventDefault();\n setTakeControl(false);\n }\n };\n window.addEventListener(\"keydown\", onKey);\n return () => {\n window.removeEventListener(\"keydown\", onKey);\n };\n }, [inControl, externallyControlled]);\n\n // The hook is always called (rules of hooks); it stays idle until `url` is set.\n // Do NOT open a socket while the viewer-cap (429) notice is showing — the slot\n // is already exhausted, so connecting would only burn a doomed attempt (and in\n // tests leak an unhandled ws error from the never-resolving tunnel URL).\n const connectCapability =\n !transportNull && !needsAcknowledgment && !viewerCapReached ? capability : null;\n const stream = useDesktopStream({\n capability: connectCapability,\n containerRef,\n interactive: inControl,\n ...(scaleViewport !== undefined ? { scaleViewport } : {}),\n ...(rfbFactory ? { rfbFactory } : {}),\n ...(webSocketProtocols ? { webSocketProtocols } : {}),\n ...(webSocketFactory ? { webSocketFactory } : {}),\n });\n\n const connected = stream.state === \"connected\";\n const hasLiveUrl = Boolean(connectCapability?.url);\n // The latest stream state, read without making it an effect dependency — so the\n // re-attach below can consult it on a visibility change WITHOUT re-subscribing\n // (and re-firing) every time the state walks idle→negotiating→connected.\n const streamStateRef = useRef(stream.state);\n streamStateRef.current = stream.state;\n\n // ── AUTO-WARM ───────────────────────────────────────────────────────────────\n // When the user is watching and the desktop is cold-but-warmable, ask the host\n // to (re)warm the box. This covers BOTH (a) the desktop was just opened and\n // (b) a previously-warm box that drained back to cold under a live viewer. We\n // fire once per distinct cold episode (keyed on the lease epoch + url) so we\n // don't spam the attach while a single warm is in flight. `onWarm` is the\n // no-re-ack path; acknowledgment remains a separate automatic request.\n const warmKeyRef = useRef<string | null>(null);\n useEffect(() => {\n if (!isWatching || !coldWarmable || needsAcknowledgment || viewerCapReached) {\n // Reset the de-dupe once we leave the cold episode so a future drain warms.\n if (!coldWarmable) warmKeyRef.current = null;\n return;\n }\n if (!onWarm) return;\n // Key the de-dupe on the cell fields available to the viewer: while cold,\n // `reason`/`url`/`expiresAt` are stable, so we warm exactly once; once the box\n // warms the cell changes (url+expiresAt minted) and `coldWarmable` flips false,\n // resetting the ref so a later drain re-warms.\n const key = `${capability?.reason ?? \"\"}:${capability?.url ?? \"\"}:${capability?.expiresAt ?? \"\"}`;\n if (warmKeyRef.current === key) return; // already kicked this episode.\n warmKeyRef.current = key;\n onWarm();\n // capability identity is the trigger; leaseEpoch/expiresAt key the episode.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isWatching, coldWarmable, needsAcknowledgment, viewerCapReached, capability, onWarm]);\n\n // ── TAB RE-ATTACH ───────────────────────────────────────────────────────────\n // The \"stuck Watching, never connects\" bug: the RFB socket never (re)fires when\n // the Desktop tab is shown without a refresh. When a live url exists but we are\n // not connected/connecting, nudge the stream hook to (re)attach — on mount, on\n // the document becoming visible, and on a fresh capability. Re-attach is cheap\n // (disconnect-old/connect-new) and idempotent: the hook ignores it once live.\n useEffect(() => {\n if (!hasLiveUrl || typeof document === \"undefined\") return;\n const maybeReattach = () => {\n if (document.visibilityState !== \"visible\") return;\n // Only kick a socket that never OPENED (idle — e.g. the tab was hidden when\n // the url arrived, so the connect effect bailed on a missing container).\n // Deliberately NOT on \"error\": that surfaces an overlay with an explicit\n // Reconnect, and auto-retrying here would hammer (reconnect → error →\n // reconnect…). We read the live state via a ref so this effect does NOT\n // depend on `stream.state` — depending on it re-ran the effect on every\n // transition and re-fired the kick, which is the reconnect loop.\n if (streamStateRef.current === \"idle\") stream.reconnect();\n };\n // The connect effect already opens the socket on mount / a fresh url. The\n // ONLY gap it can't self-heal is a tab that was hidden when the url arrived\n // (container not in the DOM → connect effect bailed to idle): the socket then\n // never (re)fires on its own. So we revive it on the tab becoming visible —\n // NOT on mount (that double-opens the socket the connect effect just opened).\n document.addEventListener(\"visibilitychange\", maybeReattach);\n return () => document.removeEventListener(\"visibilitychange\", maybeReattach);\n // Re-run ONLY on a fresh live url, never on a state transition.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hasLiveUrl, connectCapability?.url]);\n\n // ── CONNECT WATCHDOG ─────────────────────────────────────────────────────────\n // If a live url is present but the RFB hasn't reached \"connected\" within the\n // window, stop spinning forever and surface \"Couldn't connect\" + Reconnect.\n // Cleared whenever we connect, the url rotates, or the user reconnects.\n const [connectTimedOut, setConnectTimedOut] = useState(false);\n // Bumped on every manual reconnect so the watchdog re-arms its window (the\n // stream hook's url/error don't change on a reconnect, so we need our own key).\n const [reconnectNonce, setReconnectNonce] = useState(0);\n useEffect(() => {\n // Re-arm on a fresh url / a reconnect and clear the moment we connect or hit a\n // real RFB error. We deliberately do NOT key on `stream.state`, so the benign\n // idle→connecting walk doesn't keep resetting the window — the full timeout\n // runs from the url becoming live.\n setConnectTimedOut(false);\n if (!hasLiveUrl || connected || stream.error || connectTimeoutMs <= 0) return;\n const timer = setTimeout(() => setConnectTimedOut(true), connectTimeoutMs);\n return () => clearTimeout(timer);\n }, [\n hasLiveUrl,\n connected,\n connectTimeoutMs,\n connectCapability?.url,\n stream.error,\n reconnectNonce,\n ]);\n\n const reconnect = () => {\n setConnectTimedOut(false);\n setReconnectNonce((n) => n + 1);\n stream.reconnect();\n };\n\n const paste = (event: ClipboardEvent<HTMLDivElement>) => {\n if (!inControl) return;\n const text = event.clipboardData.getData(\"text/plain\");\n if (!text && !event.clipboardData.types.includes(\"text/plain\")) return;\n if (!onPasteText?.(text)) return;\n event.preventDefault();\n event.stopPropagation();\n };\n\n const primaryShortcut = (event: ReactKeyboardEvent<HTMLDivElement>) => {\n if (!inControl || !targetPlatform) return;\n const hostPlatform = interactionHostPlatform();\n // noVNC maps a Mac Command key to remote Alt before the actual chord key\n // arrives. Suppress that standalone physical modifier; the complete target\n // chord below owns both its down and up events.\n if (isHostPrimaryModifier(event, hostPlatform)) {\n event.stopPropagation();\n return;\n }\n const chord = desktopPrimaryShortcut(event, hostPlatform, targetPlatform);\n if (!chord) return;\n // Copy/paste must keep the browser's default clipboard dispatch alive, but\n // noVNC must not also send the host modifier into the remote seat.\n if (chord === \"clipboard\") {\n event.stopPropagation();\n return;\n }\n const sent = chord.every(({ keysym, code, down }) => stream.sendKey?.(keysym, code, down));\n if (!sent) return;\n event.preventDefault();\n event.stopPropagation();\n };\n\n const primaryModifierUp = (event: ReactKeyboardEvent<HTMLDivElement>) => {\n if (inControl && isHostPrimaryModifier(event, interactionHostPlatform())) {\n event.stopPropagation();\n }\n };\n\n // ── Resolve the single UI state (priority order) ────────────────────────────\n let uiState: DesktopUiState;\n if (viewerCapReached) uiState = \"viewer_cap\";\n else if (hardUnavailable) uiState = \"unavailable\";\n else if (acknowledgmentError) uiState = \"acknowledgment_error\";\n else if (needsAcknowledgment) uiState = \"opening\";\n else if (coldWarmable) uiState = \"warming\";\n else if (connected) uiState = \"connected\";\n else if (stream.error) uiState = \"error\";\n else if (connectTimedOut) uiState = \"connect_failed\";\n else uiState = \"connecting\"; // hasLiveUrl && not yet connected (idle/negotiating/connecting).\n\n // An overlay blocks the surface for every state except connected; the toggle\n // and the live scrim are suppressed under an overlay.\n const overlayShown = uiState !== \"connected\" && uiState !== \"connecting\" && uiState !== \"opening\";\n const showToggle = showControlToggle && !overlayShown;\n\n let overlay: ReactNode = null;\n switch (uiState) {\n case \"viewer_cap\":\n overlay =\n renderViewerCap?.() ??\n defaultNotice(\n \"Too many viewers\",\n \"This session has reached its live-viewer limit. Try again shortly.\",\n );\n break;\n case \"unavailable\":\n overlay =\n renderUnavailable?.(reason) ??\n defaultNotice(\"Desktop unavailable\", unavailableCopy(reason));\n break;\n case \"acknowledgment_error\":\n overlay = defaultNotice(\n \"Couldn’t open desktop\",\n acknowledgmentError?.message ?? \"The desktop acknowledgment failed.\",\n retryAcknowledgment,\n );\n break;\n case \"warming\":\n overlay = renderWarming?.() ?? <WarmingNotice />;\n break;\n case \"connect_failed\":\n overlay = defaultNotice(\n \"Couldn’t connect\",\n \"The desktop is warm but the live stream didn’t come up. This usually clears on a retry.\",\n reconnect,\n );\n break;\n case \"error\":\n overlay = defaultNotice(\n \"Desktop disconnected\",\n stream.error?.message ?? \"The stream dropped.\",\n reconnect,\n );\n break;\n case \"opening\":\n case \"connecting\":\n case \"connected\":\n overlay = null;\n break;\n }\n\n return (\n <div\n className={cn(\n \"relative h-full w-full overflow-hidden\",\n inControl && \"ring-2 ring-inset ring-og-accent\",\n className,\n )}\n style={{ backgroundColor: DESKTOP_MEDIA_BACKGROUND }}\n data-opengeni-desktop\n data-state={stream.state}\n data-ui-state={uiState}\n data-in-control={inControl ? \"true\" : undefined}\n data-target-platform={targetPlatform ?? undefined}\n onKeyDownCapture={primaryShortcut}\n onKeyUpCapture={primaryModifierUp}\n onPasteCapture={paste}\n >\n {/* noVNC mount target. It appends a `width:100%;height:100%` `_screen` div\n and AUTOSCALES the 1280x800 framebuffer to fit THIS box. We pin it to\n the bounded `relative` wrapper with `absolute inset-0` (not just\n `h-full w-full`) so its measured size is ALWAYS the panel — never the\n canvas content. A content-sized mount is exactly what makes noVNC\n measure a huge screen and paint the desktop \"zoomed in\". `overflow-hidden`\n keeps the centered (margin:auto) canvas from ever spilling the panel. */}\n <div\n ref={containerRef}\n className=\"absolute inset-0 overflow-hidden\"\n data-opengeni-desktop-canvas\n data-state={stream.state}\n />\n\n {/* Idle / connecting scrim: a quiet, ALIVE \"connecting to the desktop\"\n state behind the canvas so the surface never reads as a dead black\n rectangle before the first framebuffer paints. Only shown in the\n `connecting` UI-state (every other non-connected state has an explicit\n overlay). A spinner + transitional copy makes it feel live. */}\n {(uiState === \"opening\" || uiState === \"connecting\") && (\n <div className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-3 text-og-fg-subtle\">\n <span className=\"relative flex items-center justify-center\">\n <MonitorIcon className=\"size-8 opacity-30\" strokeWidth={1.5} />\n <LoaderCircleIcon\n className=\"absolute size-12 animate-og-spin text-og-accent opacity-70\"\n strokeWidth={1.25}\n />\n </span>\n <span className=\"text-og-sm\">\n {uiState === \"opening\" ? \"Opening the desktop…\" : \"Connecting to the desktop…\"}\n </span>\n </div>\n )}\n\n {/* Take-control affordance. Two distinct states:\n - WATCHING → a prominent, centered call-to-action button overlaid on\n the desktop (the primary CTA; tasteful so the screen stays visible).\n - IN CONTROL → a small top bar (\"You're in control · click Return\n control (or Ctrl+Alt+Shift)\") with a clearly-visible Return-control\n button; the desktop stays fully usable and EVERY key — Esc included —\n passes through to the box. Server-gated: when the deployment is\n read-only the CTA renders disabled with a reason so it degrades\n gracefully. */}\n {showToggle && !externallyControlled && inControl && (\n <InControlBar\n shared={capability?.shared ?? false}\n onRelease={() => setTakeControl(false)}\n />\n )}\n {/* The big CTA only appears once the framebuffer is live + the viewer is\n watching: before connect the scrim already communicates state, so we\n don't double up. A read-only deployment (serverAllowsControl=false) still\n surfaces the CTA disabled-with-reason once connected, so it degrades\n gracefully and stays discoverable. */}\n {showToggle && !externallyControlled && !inControl && connected && (\n <TakeControlCallToAction\n disabled={!serverAllowsControl}\n disabledReason={\n !serverAllowsControl\n ? capability?.client === \"frames\"\n ? \"View-only — live control isn't available for this machine yet.\"\n : \"This deployment streams the desktop read-only.\"\n : undefined\n }\n onTakeControl={() => setTakeControl(true)}\n />\n )}\n\n {overlay && (\n <div className=\"absolute inset-0 flex items-center justify-center p-4\">{overlay}</div>\n )}\n </div>\n );\n}\n\ntype RfbKeyEvent = { keysym: number; code: string; down: boolean };\n\nfunction isHostPrimaryModifier(\n event: Pick<ReactKeyboardEvent<HTMLDivElement>, \"key\">,\n hostPlatform: InteractionHostPlatform,\n): boolean {\n return event.key === (hostPlatform === \"mac\" ? \"Meta\" : \"Control\");\n}\n\n/** Translate one host-primary chord into deterministic RFB events for the\n * target OS. noVNC otherwise treats a Mac Command key as remote Alt, which\n * makes ordinary Cmd+A/Z shortcuts silently wrong on Linux sandboxes. */\nexport function desktopPrimaryShortcut(\n event: Pick<\n ReactKeyboardEvent<HTMLDivElement>,\n \"altKey\" | \"ctrlKey\" | \"key\" | \"metaKey\" | \"shiftKey\" | \"code\"\n >,\n hostPlatform: InteractionHostPlatform,\n targetPlatform: \"linux\" | \"macos\" | \"windows\",\n): RfbKeyEvent[] | \"clipboard\" | null {\n const primaryHeld = hostPlatform === \"mac\" ? event.metaKey : event.ctrlKey;\n if (!primaryHeld || [...event.key].length !== 1) return null;\n const key = event.key.toLowerCase();\n if (!event.altKey && (key === \"c\" || key === \"v\")) return \"clipboard\";\n const primary =\n targetPlatform === \"macos\"\n ? { keysym: 0xffeb, code: \"MetaLeft\" }\n : { keysym: 0xffe3, code: \"ControlLeft\" };\n const modifiers = [\n primary,\n ...(event.altKey ? [{ keysym: 0xffe9, code: \"AltLeft\" }] : []),\n ...(event.shiftKey ? [{ keysym: 0xffe1, code: \"ShiftLeft\" }] : []),\n ];\n const keyEvent = {\n keysym: key.codePointAt(0)!,\n code: event.code || (/^[a-z]$/u.test(key) ? `Key${key.toUpperCase()}` : key),\n };\n return [\n ...modifiers.map((modifier) => ({ ...modifier, down: true })),\n { ...keyEvent, down: true },\n { ...keyEvent, down: false },\n ...modifiers.reverse().map((modifier) => ({ ...modifier, down: false })),\n ];\n}\n\n/**\n * The primary WATCHING-state call-to-action: a large, centered pill button\n * overlaid on the desktop inviting the viewer to drive the mouse & keyboard.\n * Tasteful by default — it sits in the LOWER-center with a soft scrim only behind\n * the button (not the whole screen) and lifts on hover, so a watcher can still see\n * the agent work. Server-gated: when the deployment is read-only (or the desktop\n * hasn't connected yet) it renders disabled with a reason. Accessible: a real\n * <button> with a title, focus ring, and Enter/Space activation.\n */\nfunction TakeControlCallToAction({\n disabled,\n disabledReason,\n onTakeControl,\n}: {\n disabled: boolean;\n disabledReason?: string | undefined;\n onTakeControl: () => void;\n}) {\n return (\n // The wrapper spans the surface but is click-through (pointer-events-none); only\n // the button itself is interactive, so watchers can still see the desktop.\n <div className=\"pointer-events-none absolute inset-0 flex items-end justify-center pb-[8%]\">\n <button\n type=\"button\"\n disabled={disabled}\n aria-label=\"Take control of the desktop\"\n title={disabled ? disabledReason : \"Take control of the desktop\"}\n onClick={onTakeControl}\n className={cn(\n \"group pointer-events-auto flex items-center gap-3 rounded-og-lg border px-5 py-3\",\n \"border-og-border\",\n \"bg-og-bg/85 backdrop-blur-md\",\n \"shadow-og-lg\",\n \"outline-hidden transition-all duration-150 ease-out\",\n \"focus-visible:ring-2 focus-visible:ring-og-accent focus-visible:ring-offset-2 focus-visible:ring-offset-og-bg\",\n disabled\n ? \"cursor-not-allowed opacity-60\"\n : cn(\n \"cursor-pointer opacity-90 hover:-translate-y-0.5 hover:opacity-100\",\n \"hover:border-og-accent\",\n \"hover:bg-og-accent/10\",\n ),\n )}\n >\n <span\n className={cn(\n \"flex size-9 shrink-0 items-center justify-center rounded-full transition-colors\",\n \"bg-og-accent\",\n \"text-og-accent-fg\",\n disabled ? \"\" : \"group-hover:scale-105\",\n )}\n >\n <MousePointerClickIcon className=\"size-5\" strokeWidth={2} />\n </span>\n <span className=\"flex flex-col items-start leading-tight\">\n <span className=\"text-og-base font-semibold text-og-fg\">Take control</span>\n <span className=\"text-og-xs text-og-fg-subtle\">\n {disabled && disabledReason ? disabledReason : \"Drive the mouse & keyboard\"}\n </span>\n </span>\n </button>\n </div>\n );\n}\n\n/**\n * The IN-CONTROL state: a small top bar so the desktop stays fully usable while\n * driving (the accent ring around the viewport carries the primary \"you're\n * driving\" signal). Every keystroke — including Esc — passes through to the box,\n * so release lives only on explicit, non-trapping affordances: a clearly-visible\n * \"Return control\" button and the Ctrl+Alt+Shift chord. The button is solid\n * (not a faint ghost) so it's always discoverable as the way out.\n */\nfunction InControlBar({ shared, onRelease }: { shared: boolean; onRelease: () => void }) {\n return (\n <div className=\"pointer-events-none absolute inset-x-0 top-0 flex items-center justify-between gap-2 p-2\">\n <span className=\"pointer-events-auto inline-flex items-center gap-2 rounded-og-sm bg-og-accent px-2.5 py-1 text-og-xs font-medium text-og-accent-fg shadow-og-md\">\n <span className=\"size-1.5 animate-pulse rounded-full bg-current\" aria-hidden />\n You're in control\n <span className=\"opacity-75\">· click Return control (or Ctrl+Alt+Shift)</span>\n </span>\n <div className=\"pointer-events-auto flex items-center gap-1.5\">\n {shared && (\n <span className=\"rounded-og-sm bg-og-status-failed/85 px-2 py-0.5 text-og-xs text-og-accent-fg\">\n Shared sandbox — others are watching\n </span>\n )}\n <button\n type=\"button\"\n onClick={onRelease}\n title=\"Return control (or press Ctrl+Alt+Shift)\"\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-og-sm border px-2.5 py-1 text-og-xs font-semibold shadow-og-md transition-colors\",\n \"border-og-accent bg-og-bg/90 text-og-fg backdrop-blur-sm\",\n \"outline-hidden hover:bg-og-accent hover:text-og-accent-fg focus-visible:ring-2 focus-visible:ring-og-accent\",\n )}\n >\n <MonitorIcon className=\"size-3.5\" strokeWidth={2} aria-hidden />\n Return control\n </button>\n </div>\n </div>\n );\n}\n\nfunction unavailableCopy(reason: CapabilityUnavailableReason | null): string {\n switch (reason) {\n case \"backend_unsupported\":\n return \"This sandbox backend cannot stream a desktop.\";\n case \"tier_headless\":\n return \"This deployment is headless — terminal, files, and diff only.\";\n case \"os_unsupported\":\n return \"The sandbox OS does not support a desktop stream.\";\n case \"not_provisioned\":\n return \"This sandbox doesn't have a desktop yet.\";\n case \"disabled_by_policy\":\n return \"Desktop streaming is disabled on this deployment.\";\n case \"lease_cold\":\n // Should not reach the unavailable notice (lease_cold warms), but keep\n // friendly copy as a fallback rather than the old dead-end wording.\n return \"Waiting for the sandbox to start…\";\n default:\n return \"The desktop isn’t available for this sandbox.\";\n }\n}\n\n/**\n * The WARMING state: a cold-but-warmable box that is being spun up. A spinner +\n * clear, accurate copy — and (when the host wired a manual warm) an explicit\n * retry so a slow warm never feels stuck. This is the cure for the old\n * \"Desktop unavailable — Start a turn or attach to warm it\" dead-end: the box IS\n * being warmed automatically; we just tell the user it's happening.\n */\nfunction WarmingNotice({ onRetry }: { onRetry?: (() => void) | undefined }) {\n return (\n <div className=\"flex max-w-sm flex-col items-center gap-3 rounded-og-lg border border-og-border bg-og-bg/90 p-5 text-center text-og-base text-og-fg backdrop-blur-sm\">\n <LoaderCircleIcon className=\"size-7 animate-og-spin text-og-accent\" strokeWidth={1.5} />\n <div className=\"space-y-1\">\n <div className=\"font-medium\">Warming the sandbox…</div>\n <p className=\"text-og-sm text-og-fg-subtle\">\n Spinning up the desktop — this takes a few seconds.\n </p>\n </div>\n {onRetry && (\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"rounded-og-sm border border-og-border px-3 py-1.5 text-og-sm text-og-fg-muted transition-colors hover:text-og-fg pointer-coarse:min-h-11\"\n >\n Taking too long? Retry\n </button>\n )}\n </div>\n );\n}\n\nfunction defaultNotice(title: string, body: string, onRetry?: () => void): ReactNode {\n return (\n <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\">\n <div className=\"mb-1 flex items-center justify-center gap-1.5 font-medium\">\n {onRetry && <WifiOffIcon className=\"size-4 opacity-70\" strokeWidth={1.75} />}\n {title}\n </div>\n <p className=\"text-og-sm text-og-fg-subtle\">{body}</p>\n {onRetry && (\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"mt-3 rounded-og-sm border border-og-border px-3 py-1.5 text-og-sm transition-colors hover:border-og-accent pointer-coarse:min-h-11\"\n >\n Reconnect\n </button>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["useEffect","useRef","useState","useState","useRef","useEffect","useEffect","useRef","useState","useRef","useState","useEffect"]}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// src/lib/relay-wire.ts
|
|
2
|
+
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
|
3
|
+
var STREAM_KIND_DESKTOP = 2;
|
|
4
|
+
var STREAM_KIND_PTY = 1;
|
|
5
|
+
var STREAM_KIND_BROWSER = 3;
|
|
6
|
+
var STREAM_KIND_COMPUTER = 4;
|
|
7
|
+
var STREAM_ROLE_CLIENT = 2;
|
|
8
|
+
var STREAM_CLOSE_REASON_NORMAL = 1;
|
|
9
|
+
function encodeChannel(message, writer) {
|
|
10
|
+
if (message.channelId !== "") writer.uint32(10).string(message.channelId);
|
|
11
|
+
if (message.workspaceId !== "") writer.uint32(18).string(message.workspaceId);
|
|
12
|
+
if (message.agentId !== "") writer.uint32(26).string(message.agentId);
|
|
13
|
+
if (message.kind !== 0) writer.uint32(32).int32(message.kind);
|
|
14
|
+
if (message.port !== 0) writer.uint32(40).uint32(message.port);
|
|
15
|
+
return writer;
|
|
16
|
+
}
|
|
17
|
+
function encodeStreamOpen(message) {
|
|
18
|
+
const writer = new BinaryWriter();
|
|
19
|
+
encodeChannel(message.channel, writer.uint32(10).fork()).join();
|
|
20
|
+
if (message.token !== "") writer.uint32(18).string(message.token);
|
|
21
|
+
if (message.role !== 0) writer.uint32(24).int32(message.role);
|
|
22
|
+
if (message.resumeFromSeq !== "0") writer.uint32(32).uint64(message.resumeFromSeq);
|
|
23
|
+
return writer.finish();
|
|
24
|
+
}
|
|
25
|
+
function encodeStreamClose(message) {
|
|
26
|
+
const writer = new BinaryWriter();
|
|
27
|
+
if (message.channelId !== "") writer.uint32(10).string(message.channelId);
|
|
28
|
+
if (message.reason !== 0) writer.uint32(16).int32(message.reason);
|
|
29
|
+
if (message.message !== "") writer.uint32(26).string(message.message);
|
|
30
|
+
return writer.finish();
|
|
31
|
+
}
|
|
32
|
+
function encodeStreamFrame(message) {
|
|
33
|
+
const writer = new BinaryWriter();
|
|
34
|
+
if (message.channelId !== "") writer.uint32(10).string(message.channelId);
|
|
35
|
+
if (message.seq !== "0") writer.uint32(16).uint64(message.seq);
|
|
36
|
+
if (message.data.length > 0) writer.uint32(26).bytes(message.data);
|
|
37
|
+
if (message.producedAtMs !== "0") writer.uint32(32).uint64(message.producedAtMs);
|
|
38
|
+
return writer.finish();
|
|
39
|
+
}
|
|
40
|
+
function decodeAgentErrorMessage(reader, length) {
|
|
41
|
+
const end = reader.pos + length;
|
|
42
|
+
let message;
|
|
43
|
+
while (reader.pos < end) {
|
|
44
|
+
const tag = reader.uint32();
|
|
45
|
+
if (tag >>> 3 === 2 && tag === 18) {
|
|
46
|
+
message = reader.string();
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if ((tag & 7) === 4 || tag === 0) break;
|
|
50
|
+
reader.skip(tag & 7);
|
|
51
|
+
}
|
|
52
|
+
return message !== void 0 ? { message } : {};
|
|
53
|
+
}
|
|
54
|
+
function decodeStreamOpenAck(bytes) {
|
|
55
|
+
const reader = new BinaryReader(bytes);
|
|
56
|
+
const end = reader.len;
|
|
57
|
+
let accepted = false;
|
|
58
|
+
let error;
|
|
59
|
+
while (reader.pos < end) {
|
|
60
|
+
const tag = reader.uint32();
|
|
61
|
+
if (tag >>> 3 === 1 && tag === 8) {
|
|
62
|
+
accepted = reader.bool();
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (tag >>> 3 === 2 && tag === 18) {
|
|
66
|
+
error = decodeAgentErrorMessage(reader, reader.uint32());
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if ((tag & 7) === 4 || tag === 0) break;
|
|
70
|
+
reader.skip(tag & 7);
|
|
71
|
+
}
|
|
72
|
+
return error !== void 0 ? { accepted, error } : { accepted };
|
|
73
|
+
}
|
|
74
|
+
function decodeStreamFrame(bytes) {
|
|
75
|
+
const reader = new BinaryReader(bytes);
|
|
76
|
+
const end = reader.len;
|
|
77
|
+
let seq = "0";
|
|
78
|
+
let data = new Uint8Array(0);
|
|
79
|
+
while (reader.pos < end) {
|
|
80
|
+
const tag = reader.uint32();
|
|
81
|
+
if (tag >>> 3 === 2 && tag === 16) {
|
|
82
|
+
seq = reader.uint64().toString();
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (tag >>> 3 === 3 && tag === 26) {
|
|
86
|
+
data = new Uint8Array(reader.bytes());
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if ((tag & 7) === 4 || tag === 0) break;
|
|
90
|
+
reader.skip(tag & 7);
|
|
91
|
+
}
|
|
92
|
+
return { seq, data };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/lib/host-platform.ts
|
|
96
|
+
function interactionHostPlatform() {
|
|
97
|
+
if (typeof navigator === "undefined") return "other";
|
|
98
|
+
const platform = navigator.platform || navigator.userAgent;
|
|
99
|
+
return /Mac|iPhone|iPad|iPod/u.test(platform) ? "mac" : "other";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export {
|
|
103
|
+
STREAM_KIND_DESKTOP,
|
|
104
|
+
STREAM_KIND_PTY,
|
|
105
|
+
STREAM_KIND_BROWSER,
|
|
106
|
+
STREAM_KIND_COMPUTER,
|
|
107
|
+
STREAM_ROLE_CLIENT,
|
|
108
|
+
STREAM_CLOSE_REASON_NORMAL,
|
|
109
|
+
encodeStreamOpen,
|
|
110
|
+
encodeStreamClose,
|
|
111
|
+
encodeStreamFrame,
|
|
112
|
+
decodeStreamOpenAck,
|
|
113
|
+
decodeStreamFrame,
|
|
114
|
+
interactionHostPlatform
|
|
115
|
+
};
|
|
116
|
+
//# sourceMappingURL=chunk-5YK2ZDIG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/relay-wire.ts","../src/lib/host-platform.ts"],"sourcesContent":["// Minimal HAND-MIRROR of the relay stream wire messages the desktop frame client\n// needs (`StreamOpen`/`StreamClose` encode; `StreamOpenAck` + `StreamFrame` decode). It exists\n// because the publish-closure guard (scripts/publish-closure-guard.ts) forbids\n// `@opengeni/react` from depending on `@opengeni/agent-proto` — the published SDK\n// closure may only reach `@opengeni/sdk` among `@opengeni/*` packages. So, exactly\n// as the guard prescribes (\"hand-mirror it\"), we re-encode just these three\n// messages against the third-party `@bufbuild/protobuf/wire` runtime (the same\n// runtime `@opengeni/agent-proto` generates against), byte-compatible with the\n// generated code. Field numbers copied verbatim from\n// `packages/agent-proto/src/gen/opengeni_agent.ts`:\n// StreamChannel: channelId=1, workspaceId=2, agentId=3, kind=4(int32), port=5(uint32)\n// StreamOpen: channel=1(msg), token=2, role=3(int32), resumeFromSeq=4(uint64, omitted when \"0\")\n// StreamOpenAck: accepted=1(bool), error=2(AgentError msg), resumeFromSeq=3\n// AgentError: code=1, message=2, retryable=3, detail=4 (we read only message)\n// StreamFrame: channelId=1, seq=2, data=3(bytes), producedAtMs=4 (we read only data)\n// StreamClose: channelId=1, reason=2(int32), message=3\nimport { BinaryReader, BinaryWriter } from \"@bufbuild/protobuf/wire\";\n\n/** `StreamKind.STREAM_KIND_DESKTOP` (agent-proto enum value). */\nexport const STREAM_KIND_DESKTOP = 2;\n/** `StreamKind.STREAM_KIND_PTY` (agent-proto enum value). */\nexport const STREAM_KIND_PTY = 1;\n/** `StreamKind.STREAM_KIND_BROWSER` (agent-proto enum value). */\nexport const STREAM_KIND_BROWSER = 3;\n/** `StreamKind.STREAM_KIND_COMPUTER` (agent-proto enum value). */\nexport const STREAM_KIND_COMPUTER = 4;\n/** `StreamRole.STREAM_ROLE_CLIENT` (agent-proto enum value). */\nexport const STREAM_ROLE_CLIENT = 2;\n/** `StreamCloseReason.STREAM_CLOSE_REASON_NORMAL`. */\nexport const STREAM_CLOSE_REASON_NORMAL = 1;\n\nexport interface RelayChannel {\n channelId: string;\n workspaceId: string;\n agentId: string;\n kind: number;\n port: number;\n}\n\nexport interface RelayStreamOpen {\n channel: RelayChannel;\n token: string;\n role: number;\n resumeFromSeq: string;\n}\n\nfunction encodeChannel(message: RelayChannel, writer: BinaryWriter): BinaryWriter {\n if (message.channelId !== \"\") writer.uint32(10).string(message.channelId);\n if (message.workspaceId !== \"\") writer.uint32(18).string(message.workspaceId);\n if (message.agentId !== \"\") writer.uint32(26).string(message.agentId);\n if (message.kind !== 0) writer.uint32(32).int32(message.kind);\n if (message.port !== 0) writer.uint32(40).uint32(message.port);\n return writer;\n}\n\n/** Encode a `StreamOpen`, byte-compatible with `StreamOpen.encode(...).finish()`. */\nexport function encodeStreamOpen(message: RelayStreamOpen): Uint8Array {\n const writer = new BinaryWriter();\n encodeChannel(message.channel, writer.uint32(10).fork()).join();\n if (message.token !== \"\") writer.uint32(18).string(message.token);\n if (message.role !== 0) writer.uint32(24).int32(message.role);\n if (message.resumeFromSeq !== \"0\") writer.uint32(32).uint64(message.resumeFromSeq);\n return writer.finish();\n}\n\n/** Encode an intentional viewer detach. Transport loss omits this message so\n * reconnect/resume remains available. */\nexport function encodeStreamClose(message: {\n channelId: string;\n reason: number;\n message: string;\n}): Uint8Array {\n const writer = new BinaryWriter();\n if (message.channelId !== \"\") writer.uint32(10).string(message.channelId);\n if (message.reason !== 0) writer.uint32(16).int32(message.reason);\n if (message.message !== \"\") writer.uint32(26).string(message.message);\n return writer.finish();\n}\n\n/** Encode a viewer→agent PTY input `StreamFrame`. */\nexport function encodeStreamFrame(message: {\n channelId: string;\n seq: string;\n data: Uint8Array;\n producedAtMs: string;\n}): Uint8Array {\n const writer = new BinaryWriter();\n if (message.channelId !== \"\") writer.uint32(10).string(message.channelId);\n if (message.seq !== \"0\") writer.uint32(16).uint64(message.seq);\n if (message.data.length > 0) writer.uint32(26).bytes(message.data);\n if (message.producedAtMs !== \"0\") writer.uint32(32).uint64(message.producedAtMs);\n return writer.finish();\n}\n\n/** The only `AgentError` field we surface from a rejected ack. */\nfunction decodeAgentErrorMessage(reader: BinaryReader, length: number): { message?: string } {\n const end = reader.pos + length;\n let message: string | undefined;\n while (reader.pos < end) {\n const tag = reader.uint32();\n if (tag >>> 3 === 2 && tag === 18) {\n message = reader.string();\n continue;\n }\n if ((tag & 7) === 4 || tag === 0) break;\n reader.skip(tag & 7);\n }\n return message !== undefined ? { message } : {};\n}\n\n/** Decode a `StreamOpenAck` (we need `accepted` + the error message). */\nexport function decodeStreamOpenAck(bytes: Uint8Array): {\n accepted: boolean;\n error?: { message?: string };\n} {\n const reader = new BinaryReader(bytes);\n const end = reader.len;\n let accepted = false;\n let error: { message?: string } | undefined;\n while (reader.pos < end) {\n const tag = reader.uint32();\n if (tag >>> 3 === 1 && tag === 8) {\n accepted = reader.bool();\n continue;\n }\n if (tag >>> 3 === 2 && tag === 18) {\n error = decodeAgentErrorMessage(reader, reader.uint32());\n continue;\n }\n if ((tag & 7) === 4 || tag === 0) break;\n reader.skip(tag & 7);\n }\n return error !== undefined ? { accepted, error } : { accepted };\n}\n\n/** Decode the sequence plus `data` from a `StreamFrame`. */\nexport function decodeStreamFrame(bytes: Uint8Array): { seq: string; data: Uint8Array } {\n const reader = new BinaryReader(bytes);\n const end = reader.len;\n let seq = \"0\";\n let data = new Uint8Array(0);\n while (reader.pos < end) {\n const tag = reader.uint32();\n if (tag >>> 3 === 2 && tag === 16) {\n seq = reader.uint64().toString();\n continue;\n }\n if (tag >>> 3 === 3 && tag === 26) {\n // Copy into a fresh ArrayBuffer-backed view: BinaryReader.bytes() returns\n // `Uint8Array<ArrayBufferLike>` (a view into the message buffer), which both\n // narrows the type to `Uint8Array<ArrayBuffer>` and isolates just this frame.\n data = new Uint8Array(reader.bytes());\n continue;\n }\n if ((tag & 7) === 4 || tag === 0) break;\n reader.skip(tag & 7);\n }\n return { seq, data };\n}\n","export type InteractionHostPlatform = \"mac\" | \"other\";\n\n/** Physical client platform. Remote input adapters use this only to identify\n * the user's primary shortcut modifier; the target platform is resolved\n * separately. */\nexport function interactionHostPlatform(): InteractionHostPlatform {\n if (typeof navigator === \"undefined\") return \"other\";\n const platform = navigator.platform || navigator.userAgent;\n return /Mac|iPhone|iPad|iPod/u.test(platform) ? \"mac\" : \"other\";\n}\n"],"mappings":";AAgBA,SAAS,cAAc,oBAAoB;AAGpC,IAAM,sBAAsB;AAE5B,IAAM,kBAAkB;AAExB,IAAM,sBAAsB;AAE5B,IAAM,uBAAuB;AAE7B,IAAM,qBAAqB;AAE3B,IAAM,6BAA6B;AAiB1C,SAAS,cAAc,SAAuB,QAAoC;AAChF,MAAI,QAAQ,cAAc,GAAI,QAAO,OAAO,EAAE,EAAE,OAAO,QAAQ,SAAS;AACxE,MAAI,QAAQ,gBAAgB,GAAI,QAAO,OAAO,EAAE,EAAE,OAAO,QAAQ,WAAW;AAC5E,MAAI,QAAQ,YAAY,GAAI,QAAO,OAAO,EAAE,EAAE,OAAO,QAAQ,OAAO;AACpE,MAAI,QAAQ,SAAS,EAAG,QAAO,OAAO,EAAE,EAAE,MAAM,QAAQ,IAAI;AAC5D,MAAI,QAAQ,SAAS,EAAG,QAAO,OAAO,EAAE,EAAE,OAAO,QAAQ,IAAI;AAC7D,SAAO;AACT;AAGO,SAAS,iBAAiB,SAAsC;AACrE,QAAM,SAAS,IAAI,aAAa;AAChC,gBAAc,QAAQ,SAAS,OAAO,OAAO,EAAE,EAAE,KAAK,CAAC,EAAE,KAAK;AAC9D,MAAI,QAAQ,UAAU,GAAI,QAAO,OAAO,EAAE,EAAE,OAAO,QAAQ,KAAK;AAChE,MAAI,QAAQ,SAAS,EAAG,QAAO,OAAO,EAAE,EAAE,MAAM,QAAQ,IAAI;AAC5D,MAAI,QAAQ,kBAAkB,IAAK,QAAO,OAAO,EAAE,EAAE,OAAO,QAAQ,aAAa;AACjF,SAAO,OAAO,OAAO;AACvB;AAIO,SAAS,kBAAkB,SAInB;AACb,QAAM,SAAS,IAAI,aAAa;AAChC,MAAI,QAAQ,cAAc,GAAI,QAAO,OAAO,EAAE,EAAE,OAAO,QAAQ,SAAS;AACxE,MAAI,QAAQ,WAAW,EAAG,QAAO,OAAO,EAAE,EAAE,MAAM,QAAQ,MAAM;AAChE,MAAI,QAAQ,YAAY,GAAI,QAAO,OAAO,EAAE,EAAE,OAAO,QAAQ,OAAO;AACpE,SAAO,OAAO,OAAO;AACvB;AAGO,SAAS,kBAAkB,SAKnB;AACb,QAAM,SAAS,IAAI,aAAa;AAChC,MAAI,QAAQ,cAAc,GAAI,QAAO,OAAO,EAAE,EAAE,OAAO,QAAQ,SAAS;AACxE,MAAI,QAAQ,QAAQ,IAAK,QAAO,OAAO,EAAE,EAAE,OAAO,QAAQ,GAAG;AAC7D,MAAI,QAAQ,KAAK,SAAS,EAAG,QAAO,OAAO,EAAE,EAAE,MAAM,QAAQ,IAAI;AACjE,MAAI,QAAQ,iBAAiB,IAAK,QAAO,OAAO,EAAE,EAAE,OAAO,QAAQ,YAAY;AAC/E,SAAO,OAAO,OAAO;AACvB;AAGA,SAAS,wBAAwB,QAAsB,QAAsC;AAC3F,QAAM,MAAM,OAAO,MAAM;AACzB,MAAI;AACJ,SAAO,OAAO,MAAM,KAAK;AACvB,UAAM,MAAM,OAAO,OAAO;AAC1B,QAAI,QAAQ,MAAM,KAAK,QAAQ,IAAI;AACjC,gBAAU,OAAO,OAAO;AACxB;AAAA,IACF;AACA,SAAK,MAAM,OAAO,KAAK,QAAQ,EAAG;AAClC,WAAO,KAAK,MAAM,CAAC;AAAA,EACrB;AACA,SAAO,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC;AAChD;AAGO,SAAS,oBAAoB,OAGlC;AACA,QAAM,SAAS,IAAI,aAAa,KAAK;AACrC,QAAM,MAAM,OAAO;AACnB,MAAI,WAAW;AACf,MAAI;AACJ,SAAO,OAAO,MAAM,KAAK;AACvB,UAAM,MAAM,OAAO,OAAO;AAC1B,QAAI,QAAQ,MAAM,KAAK,QAAQ,GAAG;AAChC,iBAAW,OAAO,KAAK;AACvB;AAAA,IACF;AACA,QAAI,QAAQ,MAAM,KAAK,QAAQ,IAAI;AACjC,cAAQ,wBAAwB,QAAQ,OAAO,OAAO,CAAC;AACvD;AAAA,IACF;AACA,SAAK,MAAM,OAAO,KAAK,QAAQ,EAAG;AAClC,WAAO,KAAK,MAAM,CAAC;AAAA,EACrB;AACA,SAAO,UAAU,SAAY,EAAE,UAAU,MAAM,IAAI,EAAE,SAAS;AAChE;AAGO,SAAS,kBAAkB,OAAsD;AACtF,QAAM,SAAS,IAAI,aAAa,KAAK;AACrC,QAAM,MAAM,OAAO;AACnB,MAAI,MAAM;AACV,MAAI,OAAO,IAAI,WAAW,CAAC;AAC3B,SAAO,OAAO,MAAM,KAAK;AACvB,UAAM,MAAM,OAAO,OAAO;AAC1B,QAAI,QAAQ,MAAM,KAAK,QAAQ,IAAI;AACjC,YAAM,OAAO,OAAO,EAAE,SAAS;AAC/B;AAAA,IACF;AACA,QAAI,QAAQ,MAAM,KAAK,QAAQ,IAAI;AAIjC,aAAO,IAAI,WAAW,OAAO,MAAM,CAAC;AACpC;AAAA,IACF;AACA,SAAK,MAAM,OAAO,KAAK,QAAQ,EAAG;AAClC,WAAO,KAAK,MAAM,CAAC;AAAA,EACrB;AACA,SAAO,EAAE,KAAK,KAAK;AACrB;;;ACzJO,SAAS,0BAAmD;AACjE,MAAI,OAAO,cAAc,YAAa,QAAO;AAC7C,QAAM,WAAW,UAAU,YAAY,UAAU;AACjD,SAAO,wBAAwB,KAAK,QAAQ,IAAI,QAAQ;AAC1D;","names":[]}
|