@opengeni/react 0.52.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -1
- package/dist/artifacts.d.ts +1 -1
- package/dist/artifacts.js +43 -14
- package/dist/artifacts.js.map +1 -1
- package/dist/{chunk-ROIRTM37.js → chunk-5UQPNLIO.js} +6 -6
- package/dist/{chunk-JR3QFY6B.js → chunk-6KLX2RH5.js} +49 -2
- package/dist/chunk-6KLX2RH5.js.map +1 -0
- package/dist/{chunk-FWS3KC3Z.js → chunk-B6WPZSTT.js} +104 -104
- package/dist/{chunk-FWS3KC3Z.js.map → chunk-B6WPZSTT.js.map} +1 -1
- package/dist/{chunk-HDBG7XGO.js → chunk-C5ZPSSS5.js} +50 -1
- package/dist/chunk-C5ZPSSS5.js.map +1 -0
- package/dist/{chunk-X4S56Y2H.js → chunk-HA46CYQK.js} +135 -11
- package/dist/chunk-HA46CYQK.js.map +1 -0
- package/dist/{chunk-724BW4TW.js → chunk-IGC6A2ZI.js} +2 -2
- package/dist/chunk-LLOLUHVG.js +5937 -0
- package/dist/chunk-LLOLUHVG.js.map +1 -0
- package/dist/{chunk-WYA65Y3F.js → chunk-MKPIAGVL.js} +13 -3
- package/dist/chunk-MKPIAGVL.js.map +1 -0
- package/dist/chunk-R3AZOAB5.js +933 -0
- package/dist/chunk-R3AZOAB5.js.map +1 -0
- package/dist/{chunk-23EJ676W.js → chunk-XBZWOM2K.js} +9 -4
- package/dist/chunk-XBZWOM2K.js.map +1 -0
- package/dist/{chunk-JAGDUCZQ.js → chunk-Y7OEDUJ5.js} +17 -13
- package/dist/chunk-Y7OEDUJ5.js.map +1 -0
- package/dist/client.d.ts +6 -4
- package/dist/components/artifacts/index.d.ts +1 -0
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
- package/dist/components/browser-input.d.ts +7 -0
- package/dist/components/browser-viewer.d.ts +5 -4
- package/dist/components/computer-viewer.d.ts +2 -1
- package/dist/components/desktop-viewer.d.ts +26 -2
- package/dist/components/interaction-intervention-banner.d.ts +11 -0
- package/dist/components/sandbox-terminal.d.ts +4 -1
- package/dist/components/sandbox-workspace.d.ts +3 -0
- package/dist/composer.js +6 -6
- package/dist/hooks/use-auth-runs.d.ts +32 -0
- package/dist/hooks/use-browser-downloads.d.ts +25 -0
- package/dist/hooks/use-browser-identities.d.ts +5 -1
- package/dist/hooks/use-browser-session.d.ts +2 -1
- package/dist/hooks/use-browser-sessions.d.ts +1 -1
- package/dist/hooks/use-channels.d.ts +26 -0
- package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
- package/dist/hooks/use-computer-session.d.ts +2 -1
- package/dist/hooks/use-computer-sessions.d.ts +1 -1
- package/dist/hooks/use-desktop-stream.d.ts +4 -0
- package/dist/hooks/use-interaction-interventions.d.ts +27 -0
- package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
- package/dist/hooks/use-network-routes.d.ts +25 -0
- package/dist/hooks/use-site-auth-connections.d.ts +25 -0
- package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
- package/dist/hooks/use-terminal-stream.d.ts +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1220 -1545
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +13 -3
- package/dist/interaction.js +520 -6
- package/dist/interaction.js.map +1 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
- package/dist/lib/host-platform.d.ts +5 -0
- package/dist/lib/relay-wire.d.ts +20 -1
- package/dist/machines.js +3 -3
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/provider.d.ts +3 -2
- package/dist/realtime/realtime-control.d.ts +6 -2
- package/dist/realtime.js +138 -24
- package/dist/realtime.js.map +1 -1
- package/dist/session-context.d.ts +10 -2
- package/dist/session-ui.js +3 -3
- package/dist/session.js +11 -11
- package/dist/timeline/types.d.ts +5 -0
- package/package.json +2 -2
- package/src/artifacts.ts +3 -0
- package/src/client.ts +73 -3
- package/src/components/artifacts/index.ts +5 -0
- package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
- package/src/components/browser-input.ts +65 -0
- package/src/components/browser-viewer.tsx +1287 -177
- package/src/components/computer-viewer.tsx +611 -114
- package/src/components/desktop-viewer.tsx +114 -1
- package/src/components/interaction-intervention-banner.tsx +99 -0
- package/src/components/machines/machine-detail.tsx +1 -1
- package/src/components/model-policy-picker.tsx +15 -0
- package/src/components/sandbox-terminal.tsx +24 -7
- package/src/components/sandbox-workspace.tsx +42 -4
- package/src/components/workspace-dock.tsx +59 -5
- package/src/hooks/use-attached-browsers.ts +13 -11
- package/src/hooks/use-auth-runs.ts +278 -0
- package/src/hooks/use-browser-downloads.ts +218 -0
- package/src/hooks/use-browser-frame-stream.ts +168 -59
- package/src/hooks/use-browser-identities.ts +47 -6
- package/src/hooks/use-browser-session.ts +94 -27
- package/src/hooks/use-browser-sessions.ts +19 -14
- package/src/hooks/use-channels.ts +99 -0
- package/src/hooks/use-computer-frame-stream.ts +184 -61
- package/src/hooks/use-computer-session.ts +22 -2
- package/src/hooks/use-computer-sessions.ts +19 -13
- package/src/hooks/use-desktop-stream.ts +39 -7
- package/src/hooks/use-interaction-interventions.ts +242 -0
- package/src/hooks/use-interaction-invalidation.ts +66 -0
- package/src/hooks/use-machine-chip.ts +13 -3
- package/src/hooks/use-network-routes.ts +214 -0
- package/src/hooks/use-site-auth-connections.ts +226 -0
- package/src/hooks/use-supergrok-accounts.ts +81 -0
- package/src/hooks/use-terminal-stream.ts +288 -55
- package/src/index.ts +8 -0
- package/src/interaction.ts +30 -0
- package/src/lib/host-platform.ts +10 -0
- package/src/lib/relay-wire.ts +43 -4
- package/src/lib/terminal-capability.ts +1 -1
- package/src/model-policy.ts +19 -4
- package/src/provider.tsx +133 -1
- package/src/realtime/realtime-control.tsx +181 -26
- package/src/session-context.ts +66 -1
- package/src/timeline/activity-rail.tsx +6 -0
- package/src/timeline/projection.ts +58 -0
- package/src/timeline/types.ts +2 -0
- package/styles/compiled.css +64 -6
- package/dist/chunk-23EJ676W.js.map +0 -1
- package/dist/chunk-6BBWWOOU.js +0 -82
- package/dist/chunk-6BBWWOOU.js.map +0 -1
- package/dist/chunk-7I2KB3OK.js +0 -3895
- package/dist/chunk-7I2KB3OK.js.map +0 -1
- package/dist/chunk-HDBG7XGO.js.map +0 -1
- package/dist/chunk-JAGDUCZQ.js.map +0 -1
- package/dist/chunk-JR3QFY6B.js.map +0 -1
- package/dist/chunk-WYA65Y3F.js.map +0 -1
- package/dist/chunk-X4S56Y2H.js.map +0 -1
- package/dist/interactive-workbench-viewers-KHNOHWQE.js +0 -13
- /package/dist/{chunk-ROIRTM37.js.map → chunk-5UQPNLIO.js.map} +0 -0
- /package/dist/{chunk-724BW4TW.js.map → chunk-IGC6A2ZI.js.map} +0 -0
- /package/dist/{interactive-workbench-viewers-KHNOHWQE.js.map → interactive-workbench-viewers-TOHBXJEE.js.map} +0 -0
|
@@ -7,7 +7,16 @@ import {
|
|
|
7
7
|
ttydResizeFrame,
|
|
8
8
|
TtydServerCommand,
|
|
9
9
|
} from "@opengeni/sdk";
|
|
10
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
10
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
11
|
+
import {
|
|
12
|
+
decodeStreamFrame,
|
|
13
|
+
decodeStreamOpenAck,
|
|
14
|
+
encodeStreamFrame,
|
|
15
|
+
encodeStreamOpen,
|
|
16
|
+
type RelayChannel,
|
|
17
|
+
STREAM_KIND_PTY,
|
|
18
|
+
STREAM_ROLE_CLIENT,
|
|
19
|
+
} from "../lib/relay-wire";
|
|
11
20
|
|
|
12
21
|
/** The ttyd connection lifecycle as surfaced to the component. */
|
|
13
22
|
export type TerminalStreamStatus = "connecting" | "open" | "closed" | "error";
|
|
@@ -22,6 +31,9 @@ export type UseTerminalStreamOptions = {
|
|
|
22
31
|
onOutput?: ((data: string) => void) | undefined;
|
|
23
32
|
/** Called when ttyd sends a SET_WINDOW_TITLE frame. */
|
|
24
33
|
onTitle?: ((title: string) => void) | undefined;
|
|
34
|
+
/** Requests a fresh negotiated terminal grant after an unexpected transport
|
|
35
|
+
* failure. Intentional disconnects and effect cleanup never call it. */
|
|
36
|
+
onReconnectNeeded?: (() => void) | undefined;
|
|
25
37
|
/** Initial PTY size to seed the ttyd auth frame + first resize. */
|
|
26
38
|
initialCols?: number | undefined;
|
|
27
39
|
initialRows?: number | undefined;
|
|
@@ -47,6 +59,58 @@ export type UseTerminalStreamResult = {
|
|
|
47
59
|
// input rather than ever executing a truncated shell command.
|
|
48
60
|
export const MAX_PENDING_TERMINAL_INPUT_CODE_UNITS = 64 * 1024;
|
|
49
61
|
const TERMINAL_CONNECT_EXPIRY_SKEW_MS = 5_000;
|
|
62
|
+
const TERMINAL_RECONNECT_INITIAL_MS = 100;
|
|
63
|
+
const TERMINAL_RECONNECT_MAX_MS = 3_000;
|
|
64
|
+
const RELAY_TAG_OPEN = 1;
|
|
65
|
+
const RELAY_TAG_OPEN_ACK = 2;
|
|
66
|
+
const RELAY_TAG_FRAME = 3;
|
|
67
|
+
|
|
68
|
+
function relayDatagram(tag: number, body: Uint8Array): ArrayBuffer {
|
|
69
|
+
const bytes = new Uint8Array(body.length + 1);
|
|
70
|
+
bytes[0] = tag;
|
|
71
|
+
bytes.set(body, 1);
|
|
72
|
+
return bytes.buffer;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function relayChannelFromUrl(url: string): RelayChannel {
|
|
76
|
+
const endpoint = new URL(url);
|
|
77
|
+
const channel: RelayChannel = {
|
|
78
|
+
channelId: endpoint.searchParams.get("channel") ?? "",
|
|
79
|
+
workspaceId: endpoint.searchParams.get("ws") ?? "",
|
|
80
|
+
agentId: endpoint.searchParams.get("agent") ?? "",
|
|
81
|
+
kind: STREAM_KIND_PTY,
|
|
82
|
+
port: Number(endpoint.searchParams.get("port") ?? "0"),
|
|
83
|
+
};
|
|
84
|
+
if (
|
|
85
|
+
!channel.channelId ||
|
|
86
|
+
!channel.workspaceId ||
|
|
87
|
+
!channel.agentId ||
|
|
88
|
+
!Number.isSafeInteger(channel.port) ||
|
|
89
|
+
channel.port <= 0
|
|
90
|
+
) {
|
|
91
|
+
throw new Error("terminal relay endpoint is incomplete");
|
|
92
|
+
}
|
|
93
|
+
return channel;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sendRelayInput(
|
|
97
|
+
socket: WebSocket,
|
|
98
|
+
channel: RelayChannel,
|
|
99
|
+
sequence: bigint,
|
|
100
|
+
data: string,
|
|
101
|
+
): void {
|
|
102
|
+
socket.send(
|
|
103
|
+
relayDatagram(
|
|
104
|
+
RELAY_TAG_FRAME,
|
|
105
|
+
encodeStreamFrame({
|
|
106
|
+
channelId: channel.channelId,
|
|
107
|
+
seq: sequence.toString(),
|
|
108
|
+
data: new TextEncoder().encode(data),
|
|
109
|
+
producedAtMs: String(Date.now()),
|
|
110
|
+
}),
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
50
114
|
|
|
51
115
|
/** A bearer must remain valid long enough to finish a new websocket handshake.
|
|
52
116
|
* Existing open sockets are unaffected: this is evaluated only when the
|
|
@@ -55,7 +119,12 @@ export function terminalStreamCredentialUsable(
|
|
|
55
119
|
capability: Pick<TerminalCapability, "transport" | "url" | "expiresAt"> | null | undefined,
|
|
56
120
|
nowMs = Date.now(),
|
|
57
121
|
): boolean {
|
|
58
|
-
if (
|
|
122
|
+
if (
|
|
123
|
+
(capability?.transport !== "pty-ws" && capability?.transport !== "relay-pty") ||
|
|
124
|
+
!capability.url
|
|
125
|
+
) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
59
128
|
if (!capability.expiresAt) return true;
|
|
60
129
|
const expiresAt = Date.parse(capability.expiresAt);
|
|
61
130
|
return Number.isFinite(expiresAt) && expiresAt > nowMs + TERMINAL_CONNECT_EXPIRY_SKEW_MS;
|
|
@@ -100,12 +169,21 @@ function closeSocket(socket: WebSocket | null): void {
|
|
|
100
169
|
* SSR-safe: the socket open lives in `useEffect`, so a server render is a no-op.
|
|
101
170
|
*/
|
|
102
171
|
export function useTerminalStream(options: UseTerminalStreamOptions): UseTerminalStreamResult {
|
|
103
|
-
const { capability, onOutput, onTitle, initialCols, initialRows } = options;
|
|
172
|
+
const { capability, onOutput, onTitle, onReconnectNeeded, initialCols, initialRows } = options;
|
|
104
173
|
const [status, setStatus] = useState<TerminalStreamStatus>("closed");
|
|
174
|
+
const [reconnectGeneration, setReconnectGeneration] = useState(0);
|
|
105
175
|
const wsRef = useRef<WebSocket | null>(null);
|
|
106
176
|
const pendingInputRef = useRef("");
|
|
107
177
|
const socketFailedRef = useRef(false);
|
|
108
178
|
const socketAuthenticatedRef = useRef(false);
|
|
179
|
+
const relayChannelRef = useRef<RelayChannel | null>(null);
|
|
180
|
+
const relayIdentityRef = useRef<string | null>(null);
|
|
181
|
+
const relayInputSequenceRef = useRef(0n);
|
|
182
|
+
const relayOutputSequenceRef = useRef(0n);
|
|
183
|
+
const reconnectAttemptRef = useRef(0);
|
|
184
|
+
const credentialIdentityRef = useRef<string | null>(null);
|
|
185
|
+
const refreshRequestedRef = useRef(false);
|
|
186
|
+
const intentionalCloseRef = useRef<WeakSet<WebSocket>>(new WeakSet());
|
|
109
187
|
// Latest size, so a resize() before the socket opens is replayed on open, and a
|
|
110
188
|
// reconnect seeds the right geometry.
|
|
111
189
|
const sizeRef = useRef<{ cols: number; rows: number }>({
|
|
@@ -116,26 +194,43 @@ export function useTerminalStream(options: UseTerminalStreamOptions): UseTermina
|
|
|
116
194
|
// render (the parent passes fresh closures each time).
|
|
117
195
|
const onOutputRef = useRef(onOutput);
|
|
118
196
|
const onTitleRef = useRef(onTitle);
|
|
197
|
+
const onReconnectNeededRef = useRef(onReconnectNeeded);
|
|
119
198
|
onOutputRef.current = onOutput;
|
|
120
199
|
onTitleRef.current = onTitle;
|
|
200
|
+
onReconnectNeededRef.current = onReconnectNeeded;
|
|
121
201
|
|
|
122
202
|
const transport = capability?.transport ?? null;
|
|
123
203
|
const url = capability?.url ?? null;
|
|
124
204
|
const token = capability?.token ?? null;
|
|
125
205
|
const expiresAt = capability?.expiresAt ?? null;
|
|
206
|
+
const transportRef = useRef(transport);
|
|
207
|
+
transportRef.current = transport;
|
|
126
208
|
|
|
127
209
|
useEffect(() => {
|
|
128
210
|
// SSR / no WebSocket / not a live pty-ws cell: stay closed; the caller falls
|
|
129
211
|
// back to the Channel-A read-only firehose.
|
|
130
212
|
if (typeof window === "undefined" || typeof WebSocket === "undefined") return;
|
|
131
|
-
|
|
213
|
+
const relay = transport === "relay-pty";
|
|
214
|
+
if (transport !== "pty-ws" && !relay) {
|
|
132
215
|
// A transport downgrade is a real semantic boundary. Do not replay input
|
|
133
216
|
// captured for a PTY if this surface later changes back from firehose mode.
|
|
134
217
|
pendingInputRef.current = "";
|
|
135
218
|
socketAuthenticatedRef.current = false;
|
|
219
|
+
relayIdentityRef.current = null;
|
|
220
|
+
relayInputSequenceRef.current = 0n;
|
|
221
|
+
relayOutputSequenceRef.current = 0n;
|
|
222
|
+
reconnectAttemptRef.current = 0;
|
|
223
|
+
credentialIdentityRef.current = null;
|
|
224
|
+
refreshRequestedRef.current = false;
|
|
136
225
|
setStatus("closed");
|
|
137
226
|
return;
|
|
138
227
|
}
|
|
228
|
+
const credentialIdentity = `${transport}\u0000${url ?? ""}\u0000${token ?? ""}\u0000${expiresAt ?? ""}`;
|
|
229
|
+
if (credentialIdentityRef.current !== credentialIdentity) {
|
|
230
|
+
credentialIdentityRef.current = credentialIdentity;
|
|
231
|
+
reconnectAttemptRef.current = 0;
|
|
232
|
+
refreshRequestedRef.current = false;
|
|
233
|
+
}
|
|
139
234
|
// A changed/cleared credential is a new connection attempt boundary. It may
|
|
140
235
|
// receive input while its exact grant is pending even if the prior socket
|
|
141
236
|
// failed; the unchanged failed identity remains fenced because this effect
|
|
@@ -150,18 +245,42 @@ export function useTerminalStream(options: UseTerminalStreamOptions): UseTermina
|
|
|
150
245
|
}
|
|
151
246
|
if (!terminalStreamCredentialUsable({ transport, url, expiresAt })) {
|
|
152
247
|
// A descriptor from an older rolling server may still carry a bearer that
|
|
153
|
-
// aged out before this surface mounted.
|
|
248
|
+
// aged out before this surface mounted. Ask once for the exact fresh grant.
|
|
154
249
|
setStatus("closed");
|
|
250
|
+
if (!refreshRequestedRef.current) {
|
|
251
|
+
refreshRequestedRef.current = true;
|
|
252
|
+
onReconnectNeededRef.current?.();
|
|
253
|
+
}
|
|
155
254
|
return;
|
|
156
255
|
}
|
|
157
256
|
|
|
158
257
|
let disposed = false;
|
|
258
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
159
259
|
let socket: WebSocket;
|
|
260
|
+
let relayChannel: RelayChannel | null = null;
|
|
261
|
+
const relayOutputDecoder = new TextDecoder();
|
|
160
262
|
setStatus("connecting");
|
|
161
263
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
264
|
+
if (relay) {
|
|
265
|
+
relayChannel = relayChannelFromUrl(url);
|
|
266
|
+
const relayIdentity = [
|
|
267
|
+
relayChannel.workspaceId,
|
|
268
|
+
relayChannel.agentId,
|
|
269
|
+
relayChannel.port,
|
|
270
|
+
relayChannel.channelId,
|
|
271
|
+
].join("\u0000");
|
|
272
|
+
if (relayIdentityRef.current !== relayIdentity) {
|
|
273
|
+
relayIdentityRef.current = relayIdentity;
|
|
274
|
+
relayInputSequenceRef.current = 0n;
|
|
275
|
+
relayOutputSequenceRef.current = 0n;
|
|
276
|
+
}
|
|
277
|
+
relayChannelRef.current = relayChannel;
|
|
278
|
+
socket = new WebSocket(url);
|
|
279
|
+
} else {
|
|
280
|
+
// The ttyd "tty" subprotocol is REQUIRED — ttyd rejects a handshake without
|
|
281
|
+
// it. The scoped token is already in the tunnel `url`.
|
|
282
|
+
socket = new WebSocket(terminalSocketUrl({ url }), TTYD_SUBPROTOCOL);
|
|
283
|
+
}
|
|
165
284
|
} catch {
|
|
166
285
|
socketFailedRef.current = true;
|
|
167
286
|
setStatus("error");
|
|
@@ -170,8 +289,54 @@ export function useTerminalStream(options: UseTerminalStreamOptions): UseTermina
|
|
|
170
289
|
socket.binaryType = "arraybuffer";
|
|
171
290
|
wsRef.current = socket;
|
|
172
291
|
|
|
292
|
+
const requestFreshGrant = () => {
|
|
293
|
+
if (disposed || refreshRequestedRef.current || intentionalCloseRef.current.has(socket))
|
|
294
|
+
return;
|
|
295
|
+
refreshRequestedRef.current = true;
|
|
296
|
+
onReconnectNeededRef.current?.();
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const scheduleTransportReconnect = () => {
|
|
300
|
+
if (disposed || reconnectTimer !== null || intentionalCloseRef.current.has(socket)) return;
|
|
301
|
+
if (refreshRequestedRef.current) return;
|
|
302
|
+
if (!terminalStreamCredentialUsable({ transport, url, expiresAt })) {
|
|
303
|
+
requestFreshGrant();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const attempt = reconnectAttemptRef.current++;
|
|
307
|
+
const delay = Math.min(
|
|
308
|
+
TERMINAL_RECONNECT_INITIAL_MS * 2 ** Math.min(attempt, 8),
|
|
309
|
+
TERMINAL_RECONNECT_MAX_MS,
|
|
310
|
+
);
|
|
311
|
+
setStatus("connecting");
|
|
312
|
+
reconnectTimer = setTimeout(() => {
|
|
313
|
+
reconnectTimer = null;
|
|
314
|
+
if (!disposed) setReconnectGeneration((generation) => generation + 1);
|
|
315
|
+
}, delay);
|
|
316
|
+
};
|
|
317
|
+
|
|
173
318
|
socket.onopen = () => {
|
|
174
319
|
if (disposed) return;
|
|
320
|
+
if (relay && relayChannel) {
|
|
321
|
+
try {
|
|
322
|
+
socket.send(
|
|
323
|
+
relayDatagram(
|
|
324
|
+
RELAY_TAG_OPEN,
|
|
325
|
+
encodeStreamOpen({
|
|
326
|
+
channel: relayChannel,
|
|
327
|
+
token: token ?? "",
|
|
328
|
+
role: STREAM_ROLE_CLIENT,
|
|
329
|
+
resumeFromSeq: relayOutputSequenceRef.current.toString(),
|
|
330
|
+
}),
|
|
331
|
+
),
|
|
332
|
+
);
|
|
333
|
+
} catch {
|
|
334
|
+
socketFailedRef.current = false;
|
|
335
|
+
setStatus("connecting");
|
|
336
|
+
closeSocket(socket);
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
175
340
|
// ttyd's required first frame: the auth message (empty token — the gate is
|
|
176
341
|
// the tunnel url + scoped stream token, not a ttyd -c credential), seeded
|
|
177
342
|
// with the current PTY geometry. Then an explicit resize to be safe.
|
|
@@ -186,16 +351,62 @@ export function useTerminalStream(options: UseTerminalStreamOptions): UseTermina
|
|
|
186
351
|
}
|
|
187
352
|
socketAuthenticatedRef.current = true;
|
|
188
353
|
} catch {
|
|
189
|
-
socketFailedRef.current =
|
|
190
|
-
setStatus("
|
|
354
|
+
socketFailedRef.current = false;
|
|
355
|
+
setStatus("connecting");
|
|
191
356
|
closeSocket(socket);
|
|
192
357
|
return;
|
|
193
358
|
}
|
|
194
359
|
setStatus("open");
|
|
360
|
+
reconnectAttemptRef.current = 0;
|
|
361
|
+
refreshRequestedRef.current = false;
|
|
195
362
|
};
|
|
196
363
|
|
|
197
364
|
socket.onmessage = (ev: MessageEvent) => {
|
|
198
365
|
if (disposed) return;
|
|
366
|
+
if (relay) {
|
|
367
|
+
if (!(ev.data instanceof ArrayBuffer) || !relayChannel) return;
|
|
368
|
+
const bytes = new Uint8Array(ev.data);
|
|
369
|
+
if (bytes.length < 1) return;
|
|
370
|
+
const tag = bytes[0];
|
|
371
|
+
const body = bytes.subarray(1);
|
|
372
|
+
if (tag === RELAY_TAG_OPEN_ACK) {
|
|
373
|
+
try {
|
|
374
|
+
const ack = decodeStreamOpenAck(body);
|
|
375
|
+
if (!ack.accepted) throw new Error(ack.error?.message ?? "terminal relay rejected");
|
|
376
|
+
reconnectAttemptRef.current = 0;
|
|
377
|
+
refreshRequestedRef.current = false;
|
|
378
|
+
socketAuthenticatedRef.current = true;
|
|
379
|
+
if (pendingInputRef.current.length > 0) {
|
|
380
|
+
sendRelayInput(
|
|
381
|
+
socket,
|
|
382
|
+
relayChannel,
|
|
383
|
+
relayInputSequenceRef.current++,
|
|
384
|
+
pendingInputRef.current,
|
|
385
|
+
);
|
|
386
|
+
pendingInputRef.current = "";
|
|
387
|
+
}
|
|
388
|
+
setStatus("open");
|
|
389
|
+
} catch {
|
|
390
|
+
socketFailedRef.current = true;
|
|
391
|
+
setStatus("error");
|
|
392
|
+
requestFreshGrant();
|
|
393
|
+
closeSocket(socket);
|
|
394
|
+
}
|
|
395
|
+
} else if (tag === RELAY_TAG_FRAME && socketAuthenticatedRef.current) {
|
|
396
|
+
try {
|
|
397
|
+
const frame = decodeStreamFrame(body);
|
|
398
|
+
const sequence = BigInt(frame.seq);
|
|
399
|
+
if (sequence < relayOutputSequenceRef.current) return;
|
|
400
|
+
relayOutputSequenceRef.current = sequence + 1n;
|
|
401
|
+
if (frame.data.length > 0) {
|
|
402
|
+
onOutputRef.current?.(relayOutputDecoder.decode(frame.data, { stream: true }));
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
// Ignore one malformed frame; the live PTY remains usable.
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
199
410
|
const { command, payload } = decodeFrame(ev.data as string | ArrayBuffer);
|
|
200
411
|
switch (command) {
|
|
201
412
|
case TtydServerCommand.OUTPUT:
|
|
@@ -212,20 +423,28 @@ export function useTerminalStream(options: UseTerminalStreamOptions): UseTermina
|
|
|
212
423
|
|
|
213
424
|
socket.onerror = () => {
|
|
214
425
|
if (!disposed) {
|
|
215
|
-
|
|
216
|
-
|
|
426
|
+
// A network/relay outage is retryable with the same credential and PTY.
|
|
427
|
+
// Keep accepting bounded input while the transport comes back.
|
|
428
|
+
socketFailedRef.current = false;
|
|
429
|
+
setStatus("connecting");
|
|
430
|
+
closeSocket(socket);
|
|
217
431
|
}
|
|
218
432
|
};
|
|
219
433
|
socket.onclose = () => {
|
|
220
434
|
if (wsRef.current === socket) wsRef.current = null;
|
|
221
435
|
socketAuthenticatedRef.current = false;
|
|
222
|
-
if (!disposed)
|
|
436
|
+
if (!disposed) {
|
|
437
|
+
if (socketFailedRef.current) setStatus("error");
|
|
438
|
+
else scheduleTransportReconnect();
|
|
439
|
+
}
|
|
223
440
|
};
|
|
224
441
|
|
|
225
442
|
return () => {
|
|
226
443
|
disposed = true;
|
|
227
444
|
wsRef.current = null;
|
|
228
445
|
socketAuthenticatedRef.current = false;
|
|
446
|
+
relayChannelRef.current = null;
|
|
447
|
+
if (reconnectTimer !== null) clearTimeout(reconnectTimer);
|
|
229
448
|
// Drop handlers so an in-flight close/error doesn't mutate state post-unmount.
|
|
230
449
|
socket.onopen = null;
|
|
231
450
|
socket.onmessage = null;
|
|
@@ -235,52 +454,66 @@ export function useTerminalStream(options: UseTerminalStreamOptions): UseTermina
|
|
|
235
454
|
};
|
|
236
455
|
// A url/token change (rotation) re-runs this effect → close old, open new.
|
|
237
456
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
238
|
-
}, [transport, url, token, expiresAt]);
|
|
457
|
+
}, [transport, url, token, expiresAt, reconnectGeneration]);
|
|
239
458
|
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
459
|
+
const write = useCallback((data: string) => {
|
|
460
|
+
const currentTransport = transportRef.current;
|
|
461
|
+
const ws = wsRef.current;
|
|
462
|
+
if (ws && ws.readyState === WebSocket.OPEN && socketAuthenticatedRef.current) {
|
|
463
|
+
try {
|
|
464
|
+
const relayChannel = relayChannelRef.current;
|
|
465
|
+
if (currentTransport === "relay-pty" && relayChannel) {
|
|
466
|
+
sendRelayInput(ws, relayChannel, relayInputSequenceRef.current++, data);
|
|
467
|
+
} else {
|
|
245
468
|
ws.send(ttydInputFrame(data));
|
|
246
|
-
} catch {
|
|
247
|
-
socketFailedRef.current = true;
|
|
248
|
-
setStatus("error");
|
|
249
|
-
closeSocket(ws);
|
|
250
|
-
}
|
|
251
|
-
} else if (transport === "pty-ws" && !socketFailedRef.current) {
|
|
252
|
-
const nextSize = pendingInputRef.current.length + data.length;
|
|
253
|
-
if (nextSize > MAX_PENDING_TERMINAL_INPUT_CODE_UNITS) {
|
|
254
|
-
pendingInputRef.current = "";
|
|
255
|
-
socketFailedRef.current = true;
|
|
256
|
-
setStatus("error");
|
|
257
|
-
closeSocket(ws);
|
|
258
|
-
return;
|
|
259
469
|
}
|
|
260
|
-
|
|
470
|
+
} catch {
|
|
471
|
+
socketFailedRef.current = false;
|
|
472
|
+
setStatus("connecting");
|
|
473
|
+
closeSocket(ws);
|
|
261
474
|
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
475
|
+
} else if (
|
|
476
|
+
(currentTransport === "pty-ws" || currentTransport === "relay-pty") &&
|
|
477
|
+
!socketFailedRef.current
|
|
478
|
+
) {
|
|
479
|
+
const nextSize = pendingInputRef.current.length + data.length;
|
|
480
|
+
if (nextSize > MAX_PENDING_TERMINAL_INPUT_CODE_UNITS) {
|
|
481
|
+
pendingInputRef.current = "";
|
|
482
|
+
socketFailedRef.current = true;
|
|
483
|
+
setStatus("error");
|
|
484
|
+
closeSocket(ws);
|
|
485
|
+
return;
|
|
273
486
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
487
|
+
pendingInputRef.current += data;
|
|
488
|
+
}
|
|
489
|
+
}, []);
|
|
490
|
+
const resize = useCallback((cols: number, rows: number) => {
|
|
491
|
+
if (cols <= 0 || rows <= 0) return;
|
|
492
|
+
sizeRef.current = { cols, rows };
|
|
493
|
+
const ws = wsRef.current;
|
|
494
|
+
if (ws && ws.readyState === WebSocket.OPEN && socketAuthenticatedRef.current) {
|
|
495
|
+
try {
|
|
496
|
+
// Self-hosted relay PTYs carry raw input/output frames. Resize remains
|
|
497
|
+
// an out-of-band control operation; do not send ttyd framing into them.
|
|
498
|
+
if (transportRef.current === "pty-ws") ws.send(ttydResizeFrame(cols, rows));
|
|
499
|
+
} catch {
|
|
500
|
+
// socket raced closed — geometry is replayed on the next open.
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}, []);
|
|
504
|
+
const disconnect = useCallback(() => {
|
|
505
|
+
const ws = wsRef.current;
|
|
506
|
+
wsRef.current = null;
|
|
507
|
+
pendingInputRef.current = "";
|
|
508
|
+
socketFailedRef.current = false;
|
|
509
|
+
socketAuthenticatedRef.current = false;
|
|
510
|
+
relayChannelRef.current = null;
|
|
511
|
+
setStatus("closed");
|
|
512
|
+
if (ws) intentionalCloseRef.current.add(ws);
|
|
513
|
+
closeSocket(ws);
|
|
514
|
+
}, []);
|
|
515
|
+
|
|
516
|
+
return useMemo<UseTerminalStreamResult>(() => {
|
|
284
517
|
return { connected: status === "open", status, write, resize, disconnect };
|
|
285
|
-
}, [status,
|
|
518
|
+
}, [status, write, resize, disconnect]);
|
|
286
519
|
}
|
package/src/index.ts
CHANGED
|
@@ -175,6 +175,8 @@ export type {
|
|
|
175
175
|
UseEnvironmentsOptions,
|
|
176
176
|
UseEnvironmentsResult,
|
|
177
177
|
} from "./hooks/use-environments";
|
|
178
|
+
export { useChannels } from "./hooks/use-channels";
|
|
179
|
+
export type { UseChannelsOptions, UseChannelsResult } from "./hooks/use-channels";
|
|
178
180
|
export { useRigs, useRig, useRigVersions, useRigChanges } from "./hooks/use-rigs";
|
|
179
181
|
export type {
|
|
180
182
|
UseRigsOptions,
|
|
@@ -543,6 +545,12 @@ export type {
|
|
|
543
545
|
UseCodexAccountsOptions,
|
|
544
546
|
UseCodexAccountsResult,
|
|
545
547
|
} from "./hooks/use-codex-accounts";
|
|
548
|
+
export { useSuperGrokAccounts } from "./hooks/use-supergrok-accounts";
|
|
549
|
+
export type {
|
|
550
|
+
SuperGrokAccountsClientLike,
|
|
551
|
+
UseSuperGrokAccountsOptions,
|
|
552
|
+
UseSuperGrokAccountsResult,
|
|
553
|
+
} from "./hooks/use-supergrok-accounts";
|
|
546
554
|
|
|
547
555
|
// Sandbox helpers
|
|
548
556
|
export { gitFileDiffToPatch } from "./lib/git-patch";
|
package/src/interaction.ts
CHANGED
|
@@ -4,17 +4,40 @@ export type {
|
|
|
4
4
|
EmbeddedBrowserInteractionClientLike,
|
|
5
5
|
EmbeddedComputerInteractionClientLike,
|
|
6
6
|
EmbeddedInteractionClientLike,
|
|
7
|
+
EmbeddedInterventionClientLike,
|
|
7
8
|
} from "./client";
|
|
8
9
|
export {
|
|
9
10
|
useEmbeddedBrowserInteraction,
|
|
10
11
|
useEmbeddedComputerInteraction,
|
|
11
12
|
useEmbeddedInteraction,
|
|
13
|
+
useEmbeddedInterventions,
|
|
12
14
|
} from "./session-context";
|
|
13
15
|
export type {
|
|
14
16
|
EmbeddedBrowserInteractionClientOverride,
|
|
15
17
|
EmbeddedComputerInteractionClientOverride,
|
|
16
18
|
EmbeddedInteractionClientOverride,
|
|
19
|
+
EmbeddedInterventionClientOverride,
|
|
17
20
|
} from "./session-context";
|
|
21
|
+
export { useNetworkRoutes } from "./hooks/use-network-routes";
|
|
22
|
+
export type {
|
|
23
|
+
NetworkRoutesClient,
|
|
24
|
+
UseNetworkRoutesOptions,
|
|
25
|
+
UseNetworkRoutesResult,
|
|
26
|
+
} from "./hooks/use-network-routes";
|
|
27
|
+
export { useSiteAuthConnections } from "./hooks/use-site-auth-connections";
|
|
28
|
+
export type {
|
|
29
|
+
SiteAuthConnectionsClient,
|
|
30
|
+
UseSiteAuthConnectionsOptions,
|
|
31
|
+
UseSiteAuthConnectionsResult,
|
|
32
|
+
} from "./hooks/use-site-auth-connections";
|
|
33
|
+
export { useAuthRuns } from "./hooks/use-auth-runs";
|
|
34
|
+
export type { AuthRunsClient, UseAuthRunsOptions, UseAuthRunsResult } from "./hooks/use-auth-runs";
|
|
35
|
+
export { useInteractionInterventions } from "./hooks/use-interaction-interventions";
|
|
36
|
+
export type {
|
|
37
|
+
InteractionInterventionsClient,
|
|
38
|
+
UseInteractionInterventionsOptions,
|
|
39
|
+
UseInteractionInterventionsResult,
|
|
40
|
+
} from "./hooks/use-interaction-interventions";
|
|
18
41
|
export { useAttachedBrowsers } from "./hooks/use-attached-browsers";
|
|
19
42
|
export type {
|
|
20
43
|
AttachedBrowsersClient,
|
|
@@ -38,6 +61,13 @@ export type {
|
|
|
38
61
|
UseBrowserSessionOptions,
|
|
39
62
|
UseBrowserSessionResult,
|
|
40
63
|
} from "./hooks/use-browser-session";
|
|
64
|
+
export { useBrowserDownloads } from "./hooks/use-browser-downloads";
|
|
65
|
+
export type {
|
|
66
|
+
BrowserDownloadsClient,
|
|
67
|
+
BrowserDownloadSaveOptions,
|
|
68
|
+
UseBrowserDownloadsOptions,
|
|
69
|
+
UseBrowserDownloadsResult,
|
|
70
|
+
} from "./hooks/use-browser-downloads";
|
|
41
71
|
export { useBrowserFrameStream } from "./hooks/use-browser-frame-stream";
|
|
42
72
|
export type {
|
|
43
73
|
BrowserFrameConnectionState,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type InteractionHostPlatform = "mac" | "other";
|
|
2
|
+
|
|
3
|
+
/** Physical client platform. Remote input adapters use this only to identify
|
|
4
|
+
* the user's primary shortcut modifier; the target platform is resolved
|
|
5
|
+
* separately. */
|
|
6
|
+
export function interactionHostPlatform(): InteractionHostPlatform {
|
|
7
|
+
if (typeof navigator === "undefined") return "other";
|
|
8
|
+
const platform = navigator.platform || navigator.userAgent;
|
|
9
|
+
return /Mac|iPhone|iPad|iPod/u.test(platform) ? "mac" : "other";
|
|
10
|
+
}
|
package/src/lib/relay-wire.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Minimal HAND-MIRROR of the relay stream wire messages the desktop frame client
|
|
2
|
-
// needs (`StreamOpen` encode; `StreamOpenAck` + `StreamFrame` decode). It exists
|
|
2
|
+
// needs (`StreamOpen`/`StreamClose` encode; `StreamOpenAck` + `StreamFrame` decode). It exists
|
|
3
3
|
// because the publish-closure guard (scripts/publish-closure-guard.ts) forbids
|
|
4
4
|
// `@opengeni/react` from depending on `@opengeni/agent-proto` — the published SDK
|
|
5
5
|
// closure may only reach `@opengeni/sdk` among `@opengeni/*` packages. So, exactly
|
|
@@ -13,16 +13,21 @@
|
|
|
13
13
|
// StreamOpenAck: accepted=1(bool), error=2(AgentError msg), resumeFromSeq=3
|
|
14
14
|
// AgentError: code=1, message=2, retryable=3, detail=4 (we read only message)
|
|
15
15
|
// StreamFrame: channelId=1, seq=2, data=3(bytes), producedAtMs=4 (we read only data)
|
|
16
|
+
// StreamClose: channelId=1, reason=2(int32), message=3
|
|
16
17
|
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
|
17
18
|
|
|
18
19
|
/** `StreamKind.STREAM_KIND_DESKTOP` (agent-proto enum value). */
|
|
19
20
|
export const STREAM_KIND_DESKTOP = 2;
|
|
21
|
+
/** `StreamKind.STREAM_KIND_PTY` (agent-proto enum value). */
|
|
22
|
+
export const STREAM_KIND_PTY = 1;
|
|
20
23
|
/** `StreamKind.STREAM_KIND_BROWSER` (agent-proto enum value). */
|
|
21
24
|
export const STREAM_KIND_BROWSER = 3;
|
|
22
25
|
/** `StreamKind.STREAM_KIND_COMPUTER` (agent-proto enum value). */
|
|
23
26
|
export const STREAM_KIND_COMPUTER = 4;
|
|
24
27
|
/** `StreamRole.STREAM_ROLE_CLIENT` (agent-proto enum value). */
|
|
25
28
|
export const STREAM_ROLE_CLIENT = 2;
|
|
29
|
+
/** `StreamCloseReason.STREAM_CLOSE_REASON_NORMAL`. */
|
|
30
|
+
export const STREAM_CLOSE_REASON_NORMAL = 1;
|
|
26
31
|
|
|
27
32
|
export interface RelayChannel {
|
|
28
33
|
channelId: string;
|
|
@@ -58,6 +63,35 @@ export function encodeStreamOpen(message: RelayStreamOpen): Uint8Array {
|
|
|
58
63
|
return writer.finish();
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
/** Encode an intentional viewer detach. Transport loss omits this message so
|
|
67
|
+
* reconnect/resume remains available. */
|
|
68
|
+
export function encodeStreamClose(message: {
|
|
69
|
+
channelId: string;
|
|
70
|
+
reason: number;
|
|
71
|
+
message: string;
|
|
72
|
+
}): Uint8Array {
|
|
73
|
+
const writer = new BinaryWriter();
|
|
74
|
+
if (message.channelId !== "") writer.uint32(10).string(message.channelId);
|
|
75
|
+
if (message.reason !== 0) writer.uint32(16).int32(message.reason);
|
|
76
|
+
if (message.message !== "") writer.uint32(26).string(message.message);
|
|
77
|
+
return writer.finish();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Encode a viewer→agent PTY input `StreamFrame`. */
|
|
81
|
+
export function encodeStreamFrame(message: {
|
|
82
|
+
channelId: string;
|
|
83
|
+
seq: string;
|
|
84
|
+
data: Uint8Array;
|
|
85
|
+
producedAtMs: string;
|
|
86
|
+
}): Uint8Array {
|
|
87
|
+
const writer = new BinaryWriter();
|
|
88
|
+
if (message.channelId !== "") writer.uint32(10).string(message.channelId);
|
|
89
|
+
if (message.seq !== "0") writer.uint32(16).uint64(message.seq);
|
|
90
|
+
if (message.data.length > 0) writer.uint32(26).bytes(message.data);
|
|
91
|
+
if (message.producedAtMs !== "0") writer.uint32(32).uint64(message.producedAtMs);
|
|
92
|
+
return writer.finish();
|
|
93
|
+
}
|
|
94
|
+
|
|
61
95
|
/** The only `AgentError` field we surface from a rejected ack. */
|
|
62
96
|
function decodeAgentErrorMessage(reader: BinaryReader, length: number): { message?: string } {
|
|
63
97
|
const end = reader.pos + length;
|
|
@@ -99,13 +133,18 @@ export function decodeStreamOpenAck(bytes: Uint8Array): {
|
|
|
99
133
|
return error !== undefined ? { accepted, error } : { accepted };
|
|
100
134
|
}
|
|
101
135
|
|
|
102
|
-
/** Decode
|
|
103
|
-
export function decodeStreamFrame(bytes: Uint8Array): { data: Uint8Array } {
|
|
136
|
+
/** Decode the sequence plus `data` from a `StreamFrame`. */
|
|
137
|
+
export function decodeStreamFrame(bytes: Uint8Array): { seq: string; data: Uint8Array } {
|
|
104
138
|
const reader = new BinaryReader(bytes);
|
|
105
139
|
const end = reader.len;
|
|
140
|
+
let seq = "0";
|
|
106
141
|
let data = new Uint8Array(0);
|
|
107
142
|
while (reader.pos < end) {
|
|
108
143
|
const tag = reader.uint32();
|
|
144
|
+
if (tag >>> 3 === 2 && tag === 16) {
|
|
145
|
+
seq = reader.uint64().toString();
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
109
148
|
if (tag >>> 3 === 3 && tag === 26) {
|
|
110
149
|
// Copy into a fresh ArrayBuffer-backed view: BinaryReader.bytes() returns
|
|
111
150
|
// `Uint8Array<ArrayBufferLike>` (a view into the message buffer), which both
|
|
@@ -116,5 +155,5 @@ export function decodeStreamFrame(bytes: Uint8Array): { data: Uint8Array } {
|
|
|
116
155
|
if ((tag & 7) === 4 || tag === 0) break;
|
|
117
156
|
reader.skip(tag & 7);
|
|
118
157
|
}
|
|
119
|
-
return { data };
|
|
158
|
+
return { seq, data };
|
|
120
159
|
}
|
|
@@ -5,6 +5,6 @@ import type { TerminalCapability } from "@opengeni/sdk";
|
|
|
5
5
|
* real-PTY backend reports sse-events until the just-in-time grant warms it. */
|
|
6
6
|
export function terminalCanAcquirePty(cell: TerminalCapability | null | undefined): boolean {
|
|
7
7
|
if (!cell?.ptyCapable) return false;
|
|
8
|
-
if (cell.transport === "pty-ws") return true;
|
|
8
|
+
if (cell.transport === "pty-ws" || cell.transport === "relay-pty") return true;
|
|
9
9
|
return cell.reason === "lease_cold" || cell.reason === "not_provisioned";
|
|
10
10
|
}
|