@tangle-network/sandbox-ui 0.113.7 → 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.
Files changed (106) hide show
  1. package/dist/WorkflowGraph-B-NsdYz_.js +1961 -0
  2. package/dist/assets.d.ts +214 -206
  3. package/dist/assets.js +1057 -908
  4. package/dist/auth-CWe17Hly.js +37 -0
  5. package/dist/auth.d.ts +2 -30
  6. package/dist/auth.js +2 -15
  7. package/dist/chat-BOvFAe6p.js +903 -0
  8. package/dist/chat.d.ts +2 -230
  9. package/dist/chat.js +2 -34
  10. package/dist/connectors.d.ts +52 -49
  11. package/dist/connectors.js +595 -643
  12. package/dist/dashboard-CZ3VgNPR.js +4969 -0
  13. package/dist/dashboard.d.ts +4 -1297
  14. package/dist/dashboard.js +4 -152
  15. package/dist/editor.d.ts +2 -1
  16. package/dist/editor.js +2 -31
  17. package/dist/files.d.ts +2 -6
  18. package/dist/files.js +2 -33
  19. package/dist/globals.css +63 -3
  20. package/dist/hooks.d.ts +146 -148
  21. package/dist/hooks.js +222 -55
  22. package/dist/index-CKRaj1pM.d.ts +776 -0
  23. package/dist/index-CXX5IpH2.d.ts +2 -0
  24. package/dist/index-DPePq7c8.d.ts +1340 -0
  25. package/dist/index-DiyLLZuJ.d.ts +230 -0
  26. package/dist/index-DvmZLMT1.d.ts +30 -0
  27. package/dist/index-I8ZLVtHd.d.ts +330 -0
  28. package/dist/index-PRM636Ex.d.ts +86 -0
  29. package/dist/index.d.ts +21 -33
  30. package/dist/index.js +21 -701
  31. package/dist/info-panel-C9cqDzOL.js +594 -0
  32. package/dist/integrations.d.ts +133 -141
  33. package/dist/integrations.js +491 -673
  34. package/dist/markdown.d.ts +2 -1
  35. package/dist/markdown.js +2 -13
  36. package/dist/model-I-NIxeM-.js +1250 -0
  37. package/dist/model-brand-B9A4H9pJ.d.ts +98 -0
  38. package/dist/model-brand-hMyDTUqB.js +624 -0
  39. package/dist/motion-DkyBRJb9.js +37 -0
  40. package/dist/openui.d.ts +2 -1
  41. package/dist/openui.js +2 -7
  42. package/dist/pages.d.ts +307 -300
  43. package/dist/pages.js +4404 -4181
  44. package/dist/primitives-Dh1vG6J3.js +113 -0
  45. package/dist/primitives.d.ts +2 -74
  46. package/dist/primitives.js +2 -204
  47. package/dist/provider-logo-DjqmConF.js +311 -0
  48. package/dist/run.d.ts +2 -1
  49. package/dist/run.js +2 -23
  50. package/dist/sdk-hooks.d.ts +3 -4
  51. package/dist/sdk-hooks.js +3 -31
  52. package/dist/sidecar-DgH_vQ6m.d.ts +9 -0
  53. package/dist/stores.d.ts +14 -15
  54. package/dist/stores.js +13 -81
  55. package/dist/styles.css +63 -3
  56. package/dist/template-card-L-4gmjx2.d.ts +90 -0
  57. package/dist/terminal-CN9ZgHRt.js +197 -0
  58. package/dist/terminal.d.ts +60 -60
  59. package/dist/terminal.js +2 -10
  60. package/dist/tokens.css +51 -0
  61. package/dist/types.d.ts +4 -11
  62. package/dist/types.js +0 -1
  63. package/dist/use-pty-session-C8cE0adT.js +669 -0
  64. package/dist/use-sidecar-auth-CCTJcrq7.d.ts +145 -0
  65. package/dist/use-sidecar-auth-DkdcufOz.js +742 -0
  66. package/dist/utils-DzWbktWG.js +2 -0
  67. package/dist/utils.d.ts +2 -1
  68. package/dist/utils.js +2 -37
  69. package/dist/workbench-BCaCKl-J.js +8347 -0
  70. package/dist/workbench.d.ts +2 -310
  71. package/dist/workbench.js +2 -44
  72. package/dist/workflows.d.ts +341 -339
  73. package/dist/workflows.js +74 -109
  74. package/dist/workspace-B6AtI6ld.js +2510 -0
  75. package/dist/workspace.d.ts +2 -761
  76. package/dist/workspace.js +2 -51
  77. package/package.json +46 -40
  78. package/dist/WorkflowGraph-2QOORYIR.js +0 -1722
  79. package/dist/chunk-2BUPSB7O.js +0 -0
  80. package/dist/chunk-3KBNPQQF.js +0 -2697
  81. package/dist/chunk-3LIXVEEE.js +0 -279
  82. package/dist/chunk-72UF2H5H.js +0 -707
  83. package/dist/chunk-AI25G5MX.js +0 -944
  84. package/dist/chunk-ANSC5AFC.js +0 -314
  85. package/dist/chunk-AZ3AWMTM.js +0 -8
  86. package/dist/chunk-BGVEHENC.js +0 -10
  87. package/dist/chunk-CZ7CSFO4.js +0 -39
  88. package/dist/chunk-DYXXYGS4.js +0 -4700
  89. package/dist/chunk-EI44GEQ5.js +0 -6
  90. package/dist/chunk-IACRDXER.js +0 -219
  91. package/dist/chunk-IOB2PW5Z.js +0 -55
  92. package/dist/chunk-M4K7ZGR2.js +0 -24
  93. package/dist/chunk-MYZAW4RX.js +0 -665
  94. package/dist/chunk-OMUIPJQZ.js +0 -555
  95. package/dist/chunk-RQZRXKMR.js +0 -398
  96. package/dist/chunk-SOKKTB7W.js +0 -230
  97. package/dist/chunk-TADGS4Q7.js +0 -1010
  98. package/dist/chunk-UCQNOUN3.js +0 -7964
  99. package/dist/chunk-ULPLRBMF.js +0 -34
  100. package/dist/chunk-WID73FPH.js +0 -89
  101. package/dist/chunk-X3YXP7FG.js +0 -38
  102. package/dist/logo-DiAjq759.d.ts +0 -15
  103. package/dist/model-brand-BNnaUKKZ.d.ts +0 -99
  104. package/dist/ports-list-CDIvTNnX.d.ts +0 -17
  105. package/dist/sdk-hooks-Djkjq4cp.d.ts +0 -145
  106. package/dist/template-card-Bn-Bw5xC.d.ts +0 -87
@@ -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
- };