@tangle-network/sandbox-ui 0.113.6 → 0.114.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/dist/WorkflowGraph-B-NsdYz_.js +1961 -0
- package/dist/assets.d.ts +214 -206
- package/dist/assets.js +1057 -908
- package/dist/auth-CWe17Hly.js +37 -0
- package/dist/auth.d.ts +2 -30
- package/dist/auth.js +2 -15
- package/dist/chat-BOvFAe6p.js +903 -0
- package/dist/chat.d.ts +2 -230
- package/dist/chat.js +2 -34
- package/dist/connectors.d.ts +52 -49
- package/dist/connectors.js +595 -643
- package/dist/dashboard-CZ3VgNPR.js +4969 -0
- package/dist/dashboard.d.ts +4 -1297
- package/dist/dashboard.js +4 -152
- package/dist/editor.d.ts +2 -1
- package/dist/editor.js +2 -31
- package/dist/files.d.ts +2 -6
- package/dist/files.js +2 -33
- package/dist/globals.css +63 -3
- package/dist/hooks.d.ts +146 -148
- package/dist/hooks.js +222 -55
- package/dist/index-CKRaj1pM.d.ts +776 -0
- package/dist/index-CXX5IpH2.d.ts +2 -0
- package/dist/index-DPePq7c8.d.ts +1340 -0
- package/dist/index-DiyLLZuJ.d.ts +230 -0
- package/dist/index-DvmZLMT1.d.ts +30 -0
- package/dist/index-I8ZLVtHd.d.ts +330 -0
- package/dist/index-PRM636Ex.d.ts +86 -0
- package/dist/index.d.ts +21 -33
- package/dist/index.js +21 -701
- package/dist/info-panel-C9cqDzOL.js +594 -0
- package/dist/integrations.d.ts +133 -139
- package/dist/integrations.js +491 -673
- package/dist/markdown.d.ts +2 -1
- package/dist/markdown.js +2 -13
- package/dist/model-I-NIxeM-.js +1250 -0
- package/dist/model-brand-B9A4H9pJ.d.ts +98 -0
- package/dist/model-brand-hMyDTUqB.js +624 -0
- package/dist/motion-DkyBRJb9.js +37 -0
- package/dist/openui.d.ts +2 -1
- package/dist/openui.js +2 -7
- package/dist/pages.d.ts +307 -300
- package/dist/pages.js +4404 -4181
- package/dist/primitives-Dh1vG6J3.js +113 -0
- package/dist/primitives.d.ts +2 -74
- package/dist/primitives.js +2 -204
- package/dist/provider-logo-DjqmConF.js +311 -0
- package/dist/run.d.ts +2 -1
- package/dist/run.js +2 -23
- package/dist/sdk-hooks.d.ts +3 -4
- package/dist/sdk-hooks.js +3 -31
- package/dist/sidecar-DgH_vQ6m.d.ts +9 -0
- package/dist/stores.d.ts +14 -15
- package/dist/stores.js +13 -81
- package/dist/styles.css +63 -3
- package/dist/template-card-L-4gmjx2.d.ts +90 -0
- package/dist/terminal-CN9ZgHRt.js +197 -0
- package/dist/terminal.d.ts +60 -60
- package/dist/terminal.js +2 -10
- package/dist/tokens.css +51 -0
- package/dist/types.d.ts +4 -11
- package/dist/types.js +0 -1
- package/dist/use-pty-session-C8cE0adT.js +669 -0
- package/dist/use-sidecar-auth-CCTJcrq7.d.ts +145 -0
- package/dist/use-sidecar-auth-DkdcufOz.js +742 -0
- package/dist/utils-DzWbktWG.js +2 -0
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +2 -37
- package/dist/workbench-BCaCKl-J.js +8347 -0
- package/dist/workbench.d.ts +2 -310
- package/dist/workbench.js +2 -44
- package/dist/workflows.d.ts +341 -339
- package/dist/workflows.js +74 -109
- package/dist/workspace-B6AtI6ld.js +2510 -0
- package/dist/workspace.d.ts +2 -761
- package/dist/workspace.js +2 -51
- package/package.json +46 -40
- package/dist/WorkflowGraph-2QOORYIR.js +0 -1722
- package/dist/chunk-2BUPSB7O.js +0 -0
- package/dist/chunk-3KBNPQQF.js +0 -2697
- package/dist/chunk-3LIXVEEE.js +0 -279
- package/dist/chunk-72UF2H5H.js +0 -707
- package/dist/chunk-AI25G5MX.js +0 -944
- package/dist/chunk-ANSC5AFC.js +0 -314
- package/dist/chunk-AZ3AWMTM.js +0 -8
- package/dist/chunk-BGVEHENC.js +0 -10
- package/dist/chunk-CZ7CSFO4.js +0 -39
- package/dist/chunk-DYXXYGS4.js +0 -4700
- package/dist/chunk-EI44GEQ5.js +0 -6
- package/dist/chunk-IACRDXER.js +0 -219
- package/dist/chunk-IOB2PW5Z.js +0 -55
- package/dist/chunk-M4K7ZGR2.js +0 -24
- package/dist/chunk-MYZAW4RX.js +0 -665
- package/dist/chunk-OMUIPJQZ.js +0 -555
- package/dist/chunk-RQZRXKMR.js +0 -398
- package/dist/chunk-SOKKTB7W.js +0 -230
- package/dist/chunk-TADGS4Q7.js +0 -1010
- package/dist/chunk-UCQNOUN3.js +0 -7964
- package/dist/chunk-ULPLRBMF.js +0 -34
- package/dist/chunk-WID73FPH.js +0 -89
- package/dist/chunk-X3YXP7FG.js +0 -38
- package/dist/logo-DiAjq759.d.ts +0 -15
- package/dist/model-brand-BNnaUKKZ.d.ts +0 -99
- package/dist/ports-list-CDIvTNnX.d.ts +0 -17
- package/dist/sdk-hooks-Djkjq4cp.d.ts +0 -145
- package/dist/template-card-Bn-Bw5xC.d.ts +0 -87
package/dist/chunk-MYZAW4RX.js
DELETED
|
@@ -1,665 +0,0 @@
|
|
|
1
|
-
// src/hooks/use-pty-session.ts
|
|
2
|
-
import { useState, useEffect, useRef, useCallback } from "react";
|
|
3
|
-
function createEmptyBatch() {
|
|
4
|
-
return { data: "", waiters: [] };
|
|
5
|
-
}
|
|
6
|
-
var WS_READY_TIMEOUT_MS = 1e4;
|
|
7
|
-
function toWsUrl(apiUrl, sessionId) {
|
|
8
|
-
try {
|
|
9
|
-
const base = typeof window !== "undefined" ? window.location.href : void 0;
|
|
10
|
-
const url = new URL(`${apiUrl}/terminals/${sessionId}/ws`, base);
|
|
11
|
-
if (url.protocol === "https:") url.protocol = "wss:";
|
|
12
|
-
else if (url.protocol === "http:") url.protocol = "ws:";
|
|
13
|
-
else return null;
|
|
14
|
-
return url.toString();
|
|
15
|
-
} catch {
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
var BEARER_SUBPROTOCOL_PREFIX = "bearer.";
|
|
20
|
-
var TERMINAL_WS_ECHO_SUBPROTOCOL = "tangle.terminal.v1";
|
|
21
|
-
function toBearerSubprotocol(token) {
|
|
22
|
-
if (typeof btoa === "undefined") return null;
|
|
23
|
-
let encoded;
|
|
24
|
-
try {
|
|
25
|
-
encoded = btoa(token);
|
|
26
|
-
} catch {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
return `${BEARER_SUBPROTOCOL_PREFIX}${encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
|
|
30
|
-
}
|
|
31
|
-
var DEFAULT_TERMINAL_COLS = 80;
|
|
32
|
-
var DEFAULT_TERMINAL_ROWS = 24;
|
|
33
|
-
function createTerminalConnectionId() {
|
|
34
|
-
const cryptoApi = globalThis.crypto;
|
|
35
|
-
if (cryptoApi && typeof cryptoApi.randomUUID === "function") {
|
|
36
|
-
return `terminal-${cryptoApi.randomUUID()}`;
|
|
37
|
-
}
|
|
38
|
-
return `terminal-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
39
|
-
}
|
|
40
|
-
function interactiveIdentityKey(incarnationId, control) {
|
|
41
|
-
if (incarnationId === void 0 && control === void 0) return "plain";
|
|
42
|
-
if (incarnationId === void 0 || control === void 0) {
|
|
43
|
-
return `incomplete:${incarnationId ?? ""}`;
|
|
44
|
-
}
|
|
45
|
-
return [
|
|
46
|
-
incarnationId,
|
|
47
|
-
control.refDigest,
|
|
48
|
-
String(control.generation),
|
|
49
|
-
control.leaseId,
|
|
50
|
-
control.holderId,
|
|
51
|
-
control.expiresAt
|
|
52
|
-
].join("\0");
|
|
53
|
-
}
|
|
54
|
-
function usePtySession({
|
|
55
|
-
apiUrl,
|
|
56
|
-
token,
|
|
57
|
-
onData,
|
|
58
|
-
connectionId: providedConnectionId,
|
|
59
|
-
incarnationId,
|
|
60
|
-
control
|
|
61
|
-
}) {
|
|
62
|
-
const [isConnected, setIsConnected] = useState(false);
|
|
63
|
-
const [error, setError] = useState(null);
|
|
64
|
-
const sessionIdRef = useRef(null);
|
|
65
|
-
const connectGenRef = useRef(0);
|
|
66
|
-
const abortRef = useRef(null);
|
|
67
|
-
const retryTimerRef = useRef(void 0);
|
|
68
|
-
const retryCountRef = useRef(0);
|
|
69
|
-
const mountedRef = useRef(true);
|
|
70
|
-
const onDataRef = useRef(onData);
|
|
71
|
-
const tokenRef = useRef(token);
|
|
72
|
-
const incarnationIdRef = useRef(incarnationId);
|
|
73
|
-
const controlRef = useRef(control);
|
|
74
|
-
const exactAttachRequiredRef = useRef(
|
|
75
|
-
incarnationId !== void 0 || control !== void 0
|
|
76
|
-
);
|
|
77
|
-
const connectStreamRef = useRef(null);
|
|
78
|
-
const reconnectRef = useRef(null);
|
|
79
|
-
const shouldDeleteSessionRef = useRef(false);
|
|
80
|
-
const transportReadyRef = useRef(false);
|
|
81
|
-
const protocolFailureRef = useRef(false);
|
|
82
|
-
const colsRef = useRef(DEFAULT_TERMINAL_COLS);
|
|
83
|
-
const rowsRef = useRef(DEFAULT_TERMINAL_ROWS);
|
|
84
|
-
const wsRef = useRef(null);
|
|
85
|
-
const pendingWsRef = useRef(null);
|
|
86
|
-
const geometryPendingRef = useRef(false);
|
|
87
|
-
const pendingBatchRef = useRef(createEmptyBatch());
|
|
88
|
-
const drainPromiseRef = useRef(null);
|
|
89
|
-
const inputAbortRef = useRef(null);
|
|
90
|
-
const ensureDrainRunningRef = useRef(null);
|
|
91
|
-
const rejectPendingInput = useCallback((reason) => {
|
|
92
|
-
const batch = pendingBatchRef.current;
|
|
93
|
-
if (batch.waiters.length === 0) {
|
|
94
|
-
pendingBatchRef.current = createEmptyBatch();
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
pendingBatchRef.current = createEmptyBatch();
|
|
98
|
-
const err = new Error(reason);
|
|
99
|
-
for (const w of batch.waiters) w.reject(err);
|
|
100
|
-
}, []);
|
|
101
|
-
const abortStream = useCallback(() => {
|
|
102
|
-
if (retryTimerRef.current) {
|
|
103
|
-
clearTimeout(retryTimerRef.current);
|
|
104
|
-
retryTimerRef.current = void 0;
|
|
105
|
-
}
|
|
106
|
-
if (abortRef.current) {
|
|
107
|
-
abortRef.current.abort();
|
|
108
|
-
abortRef.current = null;
|
|
109
|
-
}
|
|
110
|
-
}, []);
|
|
111
|
-
const closeWs = useCallback(() => {
|
|
112
|
-
const active = wsRef.current;
|
|
113
|
-
if (active) {
|
|
114
|
-
wsRef.current = null;
|
|
115
|
-
try {
|
|
116
|
-
active.close();
|
|
117
|
-
} catch {
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
const pending = pendingWsRef.current;
|
|
121
|
-
if (pending) {
|
|
122
|
-
pendingWsRef.current = null;
|
|
123
|
-
try {
|
|
124
|
-
pending.close();
|
|
125
|
-
} catch {
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}, []);
|
|
129
|
-
const cleanup = useCallback(() => {
|
|
130
|
-
abortStream();
|
|
131
|
-
closeWs();
|
|
132
|
-
if (inputAbortRef.current) {
|
|
133
|
-
inputAbortRef.current.abort();
|
|
134
|
-
inputAbortRef.current = null;
|
|
135
|
-
}
|
|
136
|
-
transportReadyRef.current = false;
|
|
137
|
-
geometryPendingRef.current = false;
|
|
138
|
-
if (sessionIdRef.current) {
|
|
139
|
-
const sid = sessionIdRef.current;
|
|
140
|
-
const shouldDeleteSession = shouldDeleteSessionRef.current;
|
|
141
|
-
sessionIdRef.current = null;
|
|
142
|
-
shouldDeleteSessionRef.current = false;
|
|
143
|
-
if (shouldDeleteSession) {
|
|
144
|
-
fetch(`${apiUrl}/terminals/${sid}`, {
|
|
145
|
-
method: "DELETE",
|
|
146
|
-
headers: { Authorization: `Bearer ${tokenRef.current}` },
|
|
147
|
-
credentials: "include"
|
|
148
|
-
}).catch(() => {
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
rejectPendingInput("Terminal session is not connected");
|
|
153
|
-
setIsConnected(false);
|
|
154
|
-
}, [apiUrl, abortStream, closeWs, rejectPendingInput]);
|
|
155
|
-
const connectWs = useCallback((sessionId, options = {}) => {
|
|
156
|
-
return new Promise((resolve) => {
|
|
157
|
-
if (typeof WebSocket === "undefined") {
|
|
158
|
-
resolve(false);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
const wsUrl = toWsUrl(apiUrl, sessionId);
|
|
162
|
-
if (!wsUrl) {
|
|
163
|
-
resolve(false);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
const subprotocol = toBearerSubprotocol(tokenRef.current);
|
|
167
|
-
if (!subprotocol) {
|
|
168
|
-
resolve(false);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
let ws;
|
|
172
|
-
try {
|
|
173
|
-
ws = new WebSocket(wsUrl, [TERMINAL_WS_ECHO_SUBPROTOCOL, subprotocol]);
|
|
174
|
-
} catch {
|
|
175
|
-
resolve(false);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
ws.binaryType = "arraybuffer";
|
|
179
|
-
pendingWsRef.current = ws;
|
|
180
|
-
const isCurrentSocket = () => wsRef.current === ws || pendingWsRef.current === ws;
|
|
181
|
-
let opened = false;
|
|
182
|
-
let settled = false;
|
|
183
|
-
const settle = (ok) => {
|
|
184
|
-
if (settled) return;
|
|
185
|
-
settled = true;
|
|
186
|
-
clearTimeout(handshakeTimer);
|
|
187
|
-
resolve(ok);
|
|
188
|
-
};
|
|
189
|
-
const handshakeTimer = setTimeout(() => {
|
|
190
|
-
try {
|
|
191
|
-
ws.close();
|
|
192
|
-
} catch {
|
|
193
|
-
}
|
|
194
|
-
settle(false);
|
|
195
|
-
}, WS_READY_TIMEOUT_MS);
|
|
196
|
-
const failProtocol = (message) => {
|
|
197
|
-
protocolFailureRef.current = true;
|
|
198
|
-
transportReadyRef.current = false;
|
|
199
|
-
if (wsRef.current === ws) wsRef.current = null;
|
|
200
|
-
setIsConnected(false);
|
|
201
|
-
setError(message);
|
|
202
|
-
rejectPendingInput(message);
|
|
203
|
-
try {
|
|
204
|
-
ws.close();
|
|
205
|
-
} catch {
|
|
206
|
-
}
|
|
207
|
-
settle(false);
|
|
208
|
-
};
|
|
209
|
-
ws.onopen = () => {
|
|
210
|
-
opened = true;
|
|
211
|
-
if (pendingWsRef.current !== ws || !mountedRef.current) {
|
|
212
|
-
try {
|
|
213
|
-
ws.close();
|
|
214
|
-
} catch {
|
|
215
|
-
}
|
|
216
|
-
settle(false);
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
pendingWsRef.current = null;
|
|
220
|
-
if (options.initOnOpen) {
|
|
221
|
-
try {
|
|
222
|
-
const initIncarnationId = incarnationIdRef.current;
|
|
223
|
-
const initControl = controlRef.current;
|
|
224
|
-
if (exactAttachRequiredRef.current && (!initIncarnationId || !initControl)) {
|
|
225
|
-
throw new Error(
|
|
226
|
-
"Interactive terminal identity is incomplete"
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
ws.send(JSON.stringify({
|
|
230
|
-
type: "init",
|
|
231
|
-
...initIncarnationId !== void 0 ? { incarnationId: initIncarnationId } : {},
|
|
232
|
-
...initControl !== void 0 ? { control: initControl } : {},
|
|
233
|
-
cols: colsRef.current,
|
|
234
|
-
rows: rowsRef.current
|
|
235
|
-
}));
|
|
236
|
-
geometryPendingRef.current = false;
|
|
237
|
-
} catch {
|
|
238
|
-
try {
|
|
239
|
-
ws.close();
|
|
240
|
-
} catch {
|
|
241
|
-
}
|
|
242
|
-
settle(false);
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
if (protocolFailureRef.current) return;
|
|
247
|
-
wsRef.current = ws;
|
|
248
|
-
};
|
|
249
|
-
ws.onmessage = (ev) => {
|
|
250
|
-
if (!mountedRef.current || !isCurrentSocket()) return;
|
|
251
|
-
const data = ev.data;
|
|
252
|
-
let text;
|
|
253
|
-
if (typeof data === "string") {
|
|
254
|
-
text = data;
|
|
255
|
-
} else if (data instanceof ArrayBuffer) {
|
|
256
|
-
text = new TextDecoder().decode(data);
|
|
257
|
-
} else if (ArrayBuffer.isView(data)) {
|
|
258
|
-
text = new TextDecoder().decode(data);
|
|
259
|
-
} else {
|
|
260
|
-
data.text().then((t) => {
|
|
261
|
-
if (mountedRef.current && isCurrentSocket()) onDataRef.current(t);
|
|
262
|
-
}).catch(() => {
|
|
263
|
-
});
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
if (typeof data === "string") {
|
|
267
|
-
try {
|
|
268
|
-
const event = JSON.parse(text);
|
|
269
|
-
if (event !== null && typeof event === "object" && !Array.isArray(event)) {
|
|
270
|
-
const controlFrame = event;
|
|
271
|
-
switch (controlFrame.type) {
|
|
272
|
-
case "ready":
|
|
273
|
-
transportReadyRef.current = true;
|
|
274
|
-
setIsConnected(true);
|
|
275
|
-
setError(null);
|
|
276
|
-
retryCountRef.current = 0;
|
|
277
|
-
settle(true);
|
|
278
|
-
return;
|
|
279
|
-
case "error": {
|
|
280
|
-
const message = typeof controlFrame.message === "string" ? controlFrame.message : "Terminal WebSocket error";
|
|
281
|
-
if (!transportReadyRef.current) {
|
|
282
|
-
failProtocol(message);
|
|
283
|
-
} else {
|
|
284
|
-
setError(message);
|
|
285
|
-
}
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
case "exit":
|
|
289
|
-
transportReadyRef.current = false;
|
|
290
|
-
setIsConnected(false);
|
|
291
|
-
return;
|
|
292
|
-
case "rec":
|
|
293
|
-
case "rec.done":
|
|
294
|
-
return;
|
|
295
|
-
default: {
|
|
296
|
-
const frameType = typeof controlFrame.type === "string" ? controlFrame.type : "<missing type>";
|
|
297
|
-
failProtocol(`Unknown terminal control frame: ${frameType}`);
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
} catch {
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
if (text) onDataRef.current(text);
|
|
306
|
-
};
|
|
307
|
-
ws.onerror = () => {
|
|
308
|
-
};
|
|
309
|
-
ws.onclose = () => {
|
|
310
|
-
if (pendingWsRef.current === ws) {
|
|
311
|
-
pendingWsRef.current = null;
|
|
312
|
-
}
|
|
313
|
-
const wasActive = wsRef.current === ws;
|
|
314
|
-
const wasReady = wasActive && transportReadyRef.current;
|
|
315
|
-
if (wasActive) {
|
|
316
|
-
wsRef.current = null;
|
|
317
|
-
transportReadyRef.current = false;
|
|
318
|
-
}
|
|
319
|
-
if (!opened) {
|
|
320
|
-
settle(false);
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
if (!wasReady) {
|
|
324
|
-
settle(false);
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
if (protocolFailureRef.current || !wasActive || !mountedRef.current) return;
|
|
328
|
-
setIsConnected(false);
|
|
329
|
-
if (sessionIdRef.current) {
|
|
330
|
-
retryTimerRef.current = setTimeout(() => {
|
|
331
|
-
if (mountedRef.current && sessionIdRef.current) {
|
|
332
|
-
if (shouldDeleteSessionRef.current) {
|
|
333
|
-
connectStreamRef.current?.(sessionIdRef.current);
|
|
334
|
-
} else {
|
|
335
|
-
reconnectRef.current?.();
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}, 1e3);
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
});
|
|
342
|
-
}, [apiUrl, rejectPendingInput]);
|
|
343
|
-
const patchGeometry = useCallback(async (sessionId, cols, rows) => {
|
|
344
|
-
try {
|
|
345
|
-
const res = await fetch(`${apiUrl}/terminals/${sessionId}`, {
|
|
346
|
-
method: "PATCH",
|
|
347
|
-
headers: {
|
|
348
|
-
Authorization: `Bearer ${tokenRef.current}`,
|
|
349
|
-
"Content-Type": "application/json"
|
|
350
|
-
},
|
|
351
|
-
credentials: "include",
|
|
352
|
-
body: JSON.stringify({ cols, rows })
|
|
353
|
-
});
|
|
354
|
-
if (!res.ok) {
|
|
355
|
-
console.error("Failed to resize terminal:", res.status);
|
|
356
|
-
}
|
|
357
|
-
} catch (err) {
|
|
358
|
-
console.error("Failed to resize terminal", err);
|
|
359
|
-
}
|
|
360
|
-
}, [apiUrl]);
|
|
361
|
-
const connectStream = useCallback(async (sessionId) => {
|
|
362
|
-
if (exactAttachRequiredRef.current) {
|
|
363
|
-
throw new Error(
|
|
364
|
-
"Interactive terminal requires an exact WebSocket attachment"
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
abortStream();
|
|
368
|
-
setError(null);
|
|
369
|
-
try {
|
|
370
|
-
const controller = new AbortController();
|
|
371
|
-
abortRef.current = controller;
|
|
372
|
-
const streamRes = await fetch(`${apiUrl}/terminals/${sessionId}/stream`, {
|
|
373
|
-
headers: { Authorization: `Bearer ${tokenRef.current}` },
|
|
374
|
-
credentials: "include",
|
|
375
|
-
signal: controller.signal
|
|
376
|
-
});
|
|
377
|
-
if (!streamRes.ok || !streamRes.body) {
|
|
378
|
-
const err = new Error(`SSE stream failed: ${streamRes.status}`);
|
|
379
|
-
err.httpStatus = streamRes.status;
|
|
380
|
-
throw err;
|
|
381
|
-
}
|
|
382
|
-
if (mountedRef.current) {
|
|
383
|
-
transportReadyRef.current = true;
|
|
384
|
-
setIsConnected(true);
|
|
385
|
-
setError(null);
|
|
386
|
-
retryCountRef.current = 0;
|
|
387
|
-
ensureDrainRunningRef.current?.();
|
|
388
|
-
if (geometryPendingRef.current) {
|
|
389
|
-
geometryPendingRef.current = false;
|
|
390
|
-
void patchGeometry(sessionId, colsRef.current, rowsRef.current);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
const reader = streamRes.body.getReader();
|
|
394
|
-
const decoder = new TextDecoder();
|
|
395
|
-
let buffer = "";
|
|
396
|
-
while (true) {
|
|
397
|
-
const { done, value } = await reader.read();
|
|
398
|
-
if (done) break;
|
|
399
|
-
buffer += decoder.decode(value, { stream: true });
|
|
400
|
-
const frames = buffer.split("\n\n");
|
|
401
|
-
buffer = frames.pop() ?? "";
|
|
402
|
-
for (const frame of frames) {
|
|
403
|
-
if (!frame.trim()) continue;
|
|
404
|
-
for (const line of frame.split("\n")) {
|
|
405
|
-
if (line.startsWith("data:")) {
|
|
406
|
-
const raw = line.slice(5).trim();
|
|
407
|
-
if (!raw) continue;
|
|
408
|
-
try {
|
|
409
|
-
const event = JSON.parse(raw);
|
|
410
|
-
if (event.type === "data.stdout" || event.type === "data.stderr") {
|
|
411
|
-
const text = event.properties?.text ?? "";
|
|
412
|
-
if (text && mountedRef.current) {
|
|
413
|
-
onDataRef.current(text);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
} catch {
|
|
417
|
-
if (mountedRef.current) {
|
|
418
|
-
onDataRef.current(raw);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
if (mountedRef.current) {
|
|
426
|
-
transportReadyRef.current = false;
|
|
427
|
-
setIsConnected(false);
|
|
428
|
-
retryTimerRef.current = setTimeout(() => {
|
|
429
|
-
if (mountedRef.current && sessionIdRef.current) {
|
|
430
|
-
connectStreamRef.current?.(sessionIdRef.current);
|
|
431
|
-
}
|
|
432
|
-
}, 1e3);
|
|
433
|
-
}
|
|
434
|
-
} catch (err) {
|
|
435
|
-
if (err.name === "AbortError") return;
|
|
436
|
-
if (mountedRef.current) {
|
|
437
|
-
const message = err instanceof Error ? err.message : "Stream connection failed";
|
|
438
|
-
setError(message);
|
|
439
|
-
transportReadyRef.current = false;
|
|
440
|
-
setIsConnected(false);
|
|
441
|
-
const httpStatus = err.httpStatus;
|
|
442
|
-
const is4xx = httpStatus !== void 0 && httpStatus >= 400 && httpStatus < 500;
|
|
443
|
-
const MAX_RETRIES = 8;
|
|
444
|
-
if (!is4xx && retryCountRef.current < MAX_RETRIES) {
|
|
445
|
-
const delay = Math.min(3e3 * Math.pow(2, retryCountRef.current), 3e4);
|
|
446
|
-
retryCountRef.current++;
|
|
447
|
-
retryTimerRef.current = setTimeout(() => {
|
|
448
|
-
if (mountedRef.current && sessionIdRef.current) {
|
|
449
|
-
connectStreamRef.current?.(sessionIdRef.current);
|
|
450
|
-
}
|
|
451
|
-
}, delay);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}, [apiUrl, abortStream, patchGeometry]);
|
|
456
|
-
onDataRef.current = onData;
|
|
457
|
-
tokenRef.current = token;
|
|
458
|
-
incarnationIdRef.current = incarnationId;
|
|
459
|
-
controlRef.current = control;
|
|
460
|
-
exactAttachRequiredRef.current = incarnationId !== void 0 || control !== void 0;
|
|
461
|
-
connectStreamRef.current = connectStream;
|
|
462
|
-
const exactIdentityKey = interactiveIdentityKey(incarnationId, control);
|
|
463
|
-
const connect = useCallback(async () => {
|
|
464
|
-
cleanup();
|
|
465
|
-
const myGen = ++connectGenRef.current;
|
|
466
|
-
protocolFailureRef.current = false;
|
|
467
|
-
retryCountRef.current = 0;
|
|
468
|
-
setError(null);
|
|
469
|
-
if (exactAttachRequiredRef.current && (!incarnationIdRef.current || !controlRef.current)) {
|
|
470
|
-
setError("Interactive terminal identity is incomplete");
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
try {
|
|
474
|
-
const connectionId = providedConnectionId ?? createTerminalConnectionId();
|
|
475
|
-
if (!mountedRef.current) return;
|
|
476
|
-
sessionIdRef.current = connectionId;
|
|
477
|
-
shouldDeleteSessionRef.current = false;
|
|
478
|
-
transportReadyRef.current = false;
|
|
479
|
-
inputAbortRef.current = new AbortController();
|
|
480
|
-
const directWsOk = await connectWs(connectionId, { initOnOpen: true });
|
|
481
|
-
if (!mountedRef.current || connectGenRef.current !== myGen) return;
|
|
482
|
-
if (protocolFailureRef.current) return;
|
|
483
|
-
ensureDrainRunningRef.current?.();
|
|
484
|
-
if (directWsOk) {
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
if (exactAttachRequiredRef.current) {
|
|
488
|
-
throw new Error(
|
|
489
|
-
"Interactive terminal WebSocket attach failed; exact identity was not accepted"
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
sessionIdRef.current = null;
|
|
493
|
-
transportReadyRef.current = false;
|
|
494
|
-
if (inputAbortRef.current) {
|
|
495
|
-
inputAbortRef.current.abort();
|
|
496
|
-
inputAbortRef.current = null;
|
|
497
|
-
}
|
|
498
|
-
const res = await fetch(`${apiUrl}/terminals`, {
|
|
499
|
-
method: "POST",
|
|
500
|
-
headers: {
|
|
501
|
-
Authorization: `Bearer ${tokenRef.current}`,
|
|
502
|
-
"Content-Type": "application/json"
|
|
503
|
-
},
|
|
504
|
-
credentials: "include",
|
|
505
|
-
body: JSON.stringify({
|
|
506
|
-
cols: colsRef.current,
|
|
507
|
-
rows: rowsRef.current
|
|
508
|
-
})
|
|
509
|
-
});
|
|
510
|
-
geometryPendingRef.current = false;
|
|
511
|
-
if (!res.ok) {
|
|
512
|
-
throw new Error(`Failed to create terminal: ${res.status}`);
|
|
513
|
-
}
|
|
514
|
-
const body = await res.json();
|
|
515
|
-
const sessionId = body.data?.sessionId ?? body.sessionId;
|
|
516
|
-
if (!sessionId) throw new Error("No sessionId in response");
|
|
517
|
-
if (!mountedRef.current) return;
|
|
518
|
-
sessionIdRef.current = sessionId;
|
|
519
|
-
shouldDeleteSessionRef.current = true;
|
|
520
|
-
transportReadyRef.current = false;
|
|
521
|
-
inputAbortRef.current = new AbortController();
|
|
522
|
-
const wsOk = await connectWs(sessionId, { initOnOpen: true });
|
|
523
|
-
if (!mountedRef.current || connectGenRef.current !== myGen) return;
|
|
524
|
-
if (protocolFailureRef.current) return;
|
|
525
|
-
ensureDrainRunningRef.current?.();
|
|
526
|
-
if (wsOk) {
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
await connectStream(sessionId);
|
|
530
|
-
} catch (err) {
|
|
531
|
-
if (err.name === "AbortError") return;
|
|
532
|
-
if (mountedRef.current) {
|
|
533
|
-
const message = err instanceof Error ? err.message : "Terminal connection failed";
|
|
534
|
-
setError(message);
|
|
535
|
-
transportReadyRef.current = false;
|
|
536
|
-
setIsConnected(false);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}, [
|
|
540
|
-
apiUrl,
|
|
541
|
-
providedConnectionId,
|
|
542
|
-
cleanup,
|
|
543
|
-
connectWs,
|
|
544
|
-
connectStream,
|
|
545
|
-
exactIdentityKey
|
|
546
|
-
]);
|
|
547
|
-
reconnectRef.current = connect;
|
|
548
|
-
const resizeTerminal = useCallback(async (cols, rows) => {
|
|
549
|
-
if (cols > 0) colsRef.current = cols;
|
|
550
|
-
if (rows > 0) rowsRef.current = rows;
|
|
551
|
-
const sid = sessionIdRef.current;
|
|
552
|
-
if (!sid || cols <= 0 || rows <= 0) return;
|
|
553
|
-
const ws = wsRef.current;
|
|
554
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
555
|
-
try {
|
|
556
|
-
ws.send(JSON.stringify({ type: "resize", cols, rows }));
|
|
557
|
-
return;
|
|
558
|
-
} catch (err) {
|
|
559
|
-
console.warn("Terminal resize over WS failed; falling back to HTTP", err);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
if (pendingWsRef.current) {
|
|
563
|
-
geometryPendingRef.current = true;
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
if (exactAttachRequiredRef.current) {
|
|
567
|
-
throw new Error(
|
|
568
|
-
"Interactive terminal is not attached with its exact identity"
|
|
569
|
-
);
|
|
570
|
-
}
|
|
571
|
-
await patchGeometry(sid, cols, rows);
|
|
572
|
-
}, [apiUrl, patchGeometry]);
|
|
573
|
-
const drainInputQueue = useCallback(async () => {
|
|
574
|
-
while (pendingBatchRef.current.data.length > 0) {
|
|
575
|
-
const sid = sessionIdRef.current;
|
|
576
|
-
if (!sid || !transportReadyRef.current) {
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
const batch = pendingBatchRef.current;
|
|
580
|
-
pendingBatchRef.current = createEmptyBatch();
|
|
581
|
-
const ws = wsRef.current;
|
|
582
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
583
|
-
try {
|
|
584
|
-
ws.send(JSON.stringify({ type: "input", data: batch.data }));
|
|
585
|
-
for (const w of batch.waiters) w.resolve();
|
|
586
|
-
} catch (err) {
|
|
587
|
-
for (const w of batch.waiters) w.reject(err);
|
|
588
|
-
}
|
|
589
|
-
continue;
|
|
590
|
-
}
|
|
591
|
-
if (exactAttachRequiredRef.current) {
|
|
592
|
-
for (const w of batch.waiters) {
|
|
593
|
-
w.reject(
|
|
594
|
-
new Error(
|
|
595
|
-
"Interactive terminal is not attached with its exact identity"
|
|
596
|
-
)
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
continue;
|
|
600
|
-
}
|
|
601
|
-
try {
|
|
602
|
-
const res = await fetch(`${apiUrl}/terminals/${sid}/input`, {
|
|
603
|
-
method: "POST",
|
|
604
|
-
headers: {
|
|
605
|
-
Authorization: `Bearer ${tokenRef.current}`,
|
|
606
|
-
"Content-Type": "application/json"
|
|
607
|
-
},
|
|
608
|
-
credentials: "include",
|
|
609
|
-
body: JSON.stringify({ data: batch.data }),
|
|
610
|
-
signal: inputAbortRef.current?.signal
|
|
611
|
-
});
|
|
612
|
-
if (!res.ok) {
|
|
613
|
-
const text = await res.text().catch(() => "");
|
|
614
|
-
throw new Error(text || `Input failed: ${res.status}`);
|
|
615
|
-
}
|
|
616
|
-
for (const w of batch.waiters) w.resolve();
|
|
617
|
-
} catch (err) {
|
|
618
|
-
const isAbort = err?.name === "AbortError";
|
|
619
|
-
const rejection = isAbort ? new Error("Terminal session is not connected") : err;
|
|
620
|
-
if (!isAbort) console.error("Failed to send command", err);
|
|
621
|
-
for (const w of batch.waiters) w.reject(rejection);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
}, [apiUrl]);
|
|
625
|
-
const ensureDrainRunning = useCallback(() => {
|
|
626
|
-
if (drainPromiseRef.current) return;
|
|
627
|
-
const run = () => drainInputQueue().finally(() => {
|
|
628
|
-
if (pendingBatchRef.current.data.length > 0 && sessionIdRef.current && transportReadyRef.current) {
|
|
629
|
-
drainPromiseRef.current = run();
|
|
630
|
-
} else {
|
|
631
|
-
drainPromiseRef.current = null;
|
|
632
|
-
}
|
|
633
|
-
});
|
|
634
|
-
drainPromiseRef.current = run();
|
|
635
|
-
}, [drainInputQueue]);
|
|
636
|
-
ensureDrainRunningRef.current = ensureDrainRunning;
|
|
637
|
-
const sendCommand = useCallback((command) => {
|
|
638
|
-
return new Promise((resolve, reject) => {
|
|
639
|
-
if (command.length === 0) {
|
|
640
|
-
resolve();
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
if (protocolFailureRef.current) {
|
|
644
|
-
reject(new Error("Terminal protocol failure"));
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
pendingBatchRef.current.data += command;
|
|
648
|
-
pendingBatchRef.current.waiters.push({ resolve, reject });
|
|
649
|
-
ensureDrainRunning();
|
|
650
|
-
});
|
|
651
|
-
}, [ensureDrainRunning]);
|
|
652
|
-
useEffect(() => {
|
|
653
|
-
mountedRef.current = true;
|
|
654
|
-
connect();
|
|
655
|
-
return () => {
|
|
656
|
-
mountedRef.current = false;
|
|
657
|
-
cleanup();
|
|
658
|
-
};
|
|
659
|
-
}, [connect, cleanup]);
|
|
660
|
-
return { isConnected, error, sendCommand, resizeTerminal, reconnect: connect };
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
export {
|
|
664
|
-
usePtySession
|
|
665
|
-
};
|