@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
@@ -0,0 +1,669 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ //#region src/hooks/use-pty-session.ts
3
+ function createEmptyBatch() {
4
+ return {
5
+ data: "",
6
+ waiters: []
7
+ };
8
+ }
9
+ /**
10
+ * Time we wait for the terminal WebSocket to reach server-confirmed READY
11
+ * before falling back to the HTTP+SSE transport. The cold terminal-WS
12
+ * upgrade can traverse several proxy hops (browser → app worker → sandbox
13
+ * edge → orchestrator dial → host-agent → sidecar) and take a few seconds
14
+ * to establish; the budget must cover the full OPEN → init → ready exchange
15
+ * so the direct WS path wins instead of dropping through to the fallback
16
+ * dial on a cold connect.
17
+ */
18
+ const WS_READY_TIMEOUT_MS = 1e4;
19
+ /** Convert an http(s) base URL into the matching ws(s) URL. */
20
+ function toWsUrl(apiUrl, sessionId) {
21
+ try {
22
+ const base = typeof window !== "undefined" ? window.location.href : void 0;
23
+ const url = new URL(`${apiUrl}/terminals/${sessionId}/ws`, base);
24
+ if (url.protocol === "https:") url.protocol = "wss:";
25
+ else if (url.protocol === "http:") url.protocol = "ws:";
26
+ else return null;
27
+ return url.toString();
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+ /**
33
+ * Encode the bearer token as a WebSocket subprotocol identifier.
34
+ *
35
+ * Browsers cannot set the `Authorization` header on a WebSocket
36
+ * upgrade. The naive workaround — putting the token in a query
37
+ * parameter — is a real security risk: query strings are routinely
38
+ * captured in edge-proxy access logs, browser DevTools network
39
+ * panels, referrer headers on internal links, and log-aggregation
40
+ * systems where they may be retained for years. Move the token to
41
+ * `Sec-WebSocket-Protocol` instead, which is a request *header* and
42
+ * is treated like other auth headers by the same systems (i.e. not
43
+ * surfaced in URL-shaped logs).
44
+ *
45
+ * Per RFC 7230 a subprotocol identifier is a `token`, which excludes
46
+ * `+`, `/`, and `=`. Bearer tokens minted by the sandbox API are
47
+ * not guaranteed to satisfy that grammar, so we base64url-encode the
48
+ * value first. The matching server reverses the encoding before
49
+ * validating.
50
+ *
51
+ * The credential is offered alongside {@link TERMINAL_WS_ECHO_SUBPROTOCOL}.
52
+ * A server must never echo `bearer.<…>` — that would put the live credential
53
+ * in a response header — so it selects the marker instead. The marker is
54
+ * required, not decorative: Chrome fails a handshake with "Sent non-empty
55
+ * 'Sec-WebSocket-Protocol' header but no response was received" when the
56
+ * client offered subprotocols and the server selected none, so a
57
+ * credential-only offer can never complete against a correct server.
58
+ */
59
+ const BEARER_SUBPROTOCOL_PREFIX = "bearer.";
60
+ /**
61
+ * Non-credential subprotocol the server echoes into the 101 to satisfy the
62
+ * client's subprotocol negotiation. Must match the value the backends select
63
+ * (`TERMINAL_WS_ECHO_SUBPROTOCOL` in sandbox-api / the sidecar's
64
+ * `ws-subprotocol-auth`).
65
+ */
66
+ const TERMINAL_WS_ECHO_SUBPROTOCOL = "tangle.terminal.v1";
67
+ function toBearerSubprotocol(token) {
68
+ if (typeof btoa === "undefined") return null;
69
+ let encoded;
70
+ try {
71
+ encoded = btoa(token);
72
+ } catch {
73
+ return null;
74
+ }
75
+ return `${BEARER_SUBPROTOCOL_PREFIX}${encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
76
+ }
77
+ const DEFAULT_TERMINAL_COLS = 80;
78
+ const DEFAULT_TERMINAL_ROWS = 24;
79
+ function createTerminalConnectionId() {
80
+ const cryptoApi = globalThis.crypto;
81
+ if (cryptoApi && typeof cryptoApi.randomUUID === "function") return `terminal-${cryptoApi.randomUUID()}`;
82
+ return `terminal-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
83
+ }
84
+ function interactiveIdentityKey(incarnationId, control) {
85
+ if (incarnationId === void 0 && control === void 0) return "plain";
86
+ if (incarnationId === void 0 || control === void 0) return `incomplete:${incarnationId ?? ""}`;
87
+ return [
88
+ incarnationId,
89
+ control.refDigest,
90
+ String(control.generation),
91
+ control.leaseId,
92
+ control.holderId,
93
+ control.expiresAt
94
+ ].join("\0");
95
+ }
96
+ /**
97
+ * Manages a PTY session against the sidecar terminal API.
98
+ *
99
+ * Transport:
100
+ * 1. Try the current sidecar WebSocket contract directly:
101
+ * GET /terminals/:id/ws, then send `{"type":"init",...}`.
102
+ * - Server → client: BINARY frames carrying PTY output; TEXT frames
103
+ * carrying lifecycle/control messages.
104
+ * - Client → server: TEXT frames carrying JSON input/resize messages.
105
+ * 2. If direct WS does not reach server `ready` within
106
+ * WS_READY_TIMEOUT_MS, or it errors before ready, fall back to the older
107
+ * terminal contract:
108
+ * - POST /terminals creates the session.
109
+ * - GET /terminals/:id/ws tries the older WS transport.
110
+ * - GET /terminals/:id/stream (SSE for output)
111
+ * - POST /terminals/:id/input (one batched POST at a time)
112
+ * - PATCH /terminals/:id (resize)
113
+ * 3. DELETE /terminals/:id closes sessions created by the older REST API.
114
+ *
115
+ * The WS path eliminates the per-keystroke HTTP round-trip that
116
+ * dominates typing latency through edge proxies; the HTTP+SSE path is
117
+ * preserved as a fallback so the hook keeps working against
118
+ * deployments that have not yet shipped the WS endpoint.
119
+ */
120
+ function usePtySession({ apiUrl, token, onData, connectionId: providedConnectionId, incarnationId, control }) {
121
+ const [isConnected, setIsConnected] = useState(false);
122
+ const [error, setError] = useState(null);
123
+ const sessionIdRef = useRef(null);
124
+ const connectGenRef = useRef(0);
125
+ const abortRef = useRef(null);
126
+ const retryTimerRef = useRef(void 0);
127
+ const retryCountRef = useRef(0);
128
+ const mountedRef = useRef(true);
129
+ const onDataRef = useRef(onData);
130
+ const tokenRef = useRef(token);
131
+ const incarnationIdRef = useRef(incarnationId);
132
+ const controlRef = useRef(control);
133
+ const exactAttachRequiredRef = useRef(incarnationId !== void 0 || control !== void 0);
134
+ const connectStreamRef = useRef(null);
135
+ const reconnectRef = useRef(null);
136
+ const shouldDeleteSessionRef = useRef(false);
137
+ const transportReadyRef = useRef(false);
138
+ const protocolFailureRef = useRef(false);
139
+ const colsRef = useRef(DEFAULT_TERMINAL_COLS);
140
+ const rowsRef = useRef(DEFAULT_TERMINAL_ROWS);
141
+ const wsRef = useRef(null);
142
+ const pendingWsRef = useRef(null);
143
+ const geometryPendingRef = useRef(false);
144
+ const pendingBatchRef = useRef(createEmptyBatch());
145
+ const drainPromiseRef = useRef(null);
146
+ const inputAbortRef = useRef(null);
147
+ const ensureDrainRunningRef = useRef(null);
148
+ const rejectPendingInput = useCallback((reason) => {
149
+ const batch = pendingBatchRef.current;
150
+ if (batch.waiters.length === 0) {
151
+ pendingBatchRef.current = createEmptyBatch();
152
+ return;
153
+ }
154
+ pendingBatchRef.current = createEmptyBatch();
155
+ const err = new Error(reason);
156
+ for (const w of batch.waiters) w.reject(err);
157
+ }, []);
158
+ const abortStream = useCallback(() => {
159
+ if (retryTimerRef.current) {
160
+ clearTimeout(retryTimerRef.current);
161
+ retryTimerRef.current = void 0;
162
+ }
163
+ if (abortRef.current) {
164
+ abortRef.current.abort();
165
+ abortRef.current = null;
166
+ }
167
+ }, []);
168
+ const closeWs = useCallback(() => {
169
+ const active = wsRef.current;
170
+ if (active) {
171
+ wsRef.current = null;
172
+ try {
173
+ active.close();
174
+ } catch {}
175
+ }
176
+ const pending = pendingWsRef.current;
177
+ if (pending) {
178
+ pendingWsRef.current = null;
179
+ try {
180
+ pending.close();
181
+ } catch {}
182
+ }
183
+ }, []);
184
+ const cleanup = useCallback(() => {
185
+ abortStream();
186
+ closeWs();
187
+ if (inputAbortRef.current) {
188
+ inputAbortRef.current.abort();
189
+ inputAbortRef.current = null;
190
+ }
191
+ transportReadyRef.current = false;
192
+ geometryPendingRef.current = false;
193
+ if (sessionIdRef.current) {
194
+ const sid = sessionIdRef.current;
195
+ const shouldDeleteSession = shouldDeleteSessionRef.current;
196
+ sessionIdRef.current = null;
197
+ shouldDeleteSessionRef.current = false;
198
+ if (shouldDeleteSession) fetch(`${apiUrl}/terminals/${sid}`, {
199
+ method: "DELETE",
200
+ headers: { Authorization: `Bearer ${tokenRef.current}` },
201
+ credentials: "include"
202
+ }).catch(() => {});
203
+ }
204
+ rejectPendingInput("Terminal session is not connected");
205
+ setIsConnected(false);
206
+ }, [
207
+ apiUrl,
208
+ abortStream,
209
+ closeWs,
210
+ rejectPendingInput
211
+ ]);
212
+ const connectWs = useCallback((sessionId, options = {}) => {
213
+ return new Promise((resolve) => {
214
+ if (typeof WebSocket === "undefined") {
215
+ resolve(false);
216
+ return;
217
+ }
218
+ const wsUrl = toWsUrl(apiUrl, sessionId);
219
+ if (!wsUrl) {
220
+ resolve(false);
221
+ return;
222
+ }
223
+ const subprotocol = toBearerSubprotocol(tokenRef.current);
224
+ if (!subprotocol) {
225
+ resolve(false);
226
+ return;
227
+ }
228
+ let ws;
229
+ try {
230
+ ws = new WebSocket(wsUrl, [TERMINAL_WS_ECHO_SUBPROTOCOL, subprotocol]);
231
+ } catch {
232
+ resolve(false);
233
+ return;
234
+ }
235
+ ws.binaryType = "arraybuffer";
236
+ pendingWsRef.current = ws;
237
+ const isCurrentSocket = () => wsRef.current === ws || pendingWsRef.current === ws;
238
+ let opened = false;
239
+ let settled = false;
240
+ const settle = (ok) => {
241
+ if (settled) return;
242
+ settled = true;
243
+ clearTimeout(handshakeTimer);
244
+ resolve(ok);
245
+ };
246
+ const handshakeTimer = setTimeout(() => {
247
+ try {
248
+ ws.close();
249
+ } catch {}
250
+ settle(false);
251
+ }, WS_READY_TIMEOUT_MS);
252
+ const failProtocol = (message) => {
253
+ protocolFailureRef.current = true;
254
+ transportReadyRef.current = false;
255
+ if (wsRef.current === ws) wsRef.current = null;
256
+ setIsConnected(false);
257
+ setError(message);
258
+ rejectPendingInput(message);
259
+ try {
260
+ ws.close();
261
+ } catch {}
262
+ settle(false);
263
+ };
264
+ ws.onopen = () => {
265
+ opened = true;
266
+ if (pendingWsRef.current !== ws || !mountedRef.current) {
267
+ try {
268
+ ws.close();
269
+ } catch {}
270
+ settle(false);
271
+ return;
272
+ }
273
+ pendingWsRef.current = null;
274
+ if (options.initOnOpen) try {
275
+ const initIncarnationId = incarnationIdRef.current;
276
+ const initControl = controlRef.current;
277
+ if (exactAttachRequiredRef.current && (!initIncarnationId || !initControl)) throw new Error("Interactive terminal identity is incomplete");
278
+ ws.send(JSON.stringify({
279
+ type: "init",
280
+ ...initIncarnationId !== void 0 ? { incarnationId: initIncarnationId } : {},
281
+ ...initControl !== void 0 ? { control: initControl } : {},
282
+ cols: colsRef.current,
283
+ rows: rowsRef.current
284
+ }));
285
+ geometryPendingRef.current = false;
286
+ } catch {
287
+ try {
288
+ ws.close();
289
+ } catch {}
290
+ settle(false);
291
+ return;
292
+ }
293
+ if (protocolFailureRef.current) return;
294
+ wsRef.current = ws;
295
+ };
296
+ ws.onmessage = (ev) => {
297
+ if (!mountedRef.current || !isCurrentSocket()) return;
298
+ const data = ev.data;
299
+ let text;
300
+ if (typeof data === "string") text = data;
301
+ else if (data instanceof ArrayBuffer) text = new TextDecoder().decode(data);
302
+ else if (ArrayBuffer.isView(data)) text = new TextDecoder().decode(data);
303
+ else {
304
+ data.text().then((t) => {
305
+ if (mountedRef.current && isCurrentSocket()) onDataRef.current(t);
306
+ }).catch(() => {});
307
+ return;
308
+ }
309
+ if (typeof data === "string") try {
310
+ const event = JSON.parse(text);
311
+ if (event !== null && typeof event === "object" && !Array.isArray(event)) {
312
+ const controlFrame = event;
313
+ switch (controlFrame.type) {
314
+ case "ready":
315
+ transportReadyRef.current = true;
316
+ setIsConnected(true);
317
+ setError(null);
318
+ retryCountRef.current = 0;
319
+ settle(true);
320
+ return;
321
+ case "error": {
322
+ const message = typeof controlFrame.message === "string" ? controlFrame.message : "Terminal WebSocket error";
323
+ if (!transportReadyRef.current) failProtocol(message);
324
+ else setError(message);
325
+ return;
326
+ }
327
+ case "exit":
328
+ transportReadyRef.current = false;
329
+ setIsConnected(false);
330
+ return;
331
+ case "rec":
332
+ case "rec.done": return;
333
+ default: {
334
+ const frameType = typeof controlFrame.type === "string" ? controlFrame.type : "<missing type>";
335
+ failProtocol(`Unknown terminal control frame: ${frameType}`);
336
+ return;
337
+ }
338
+ }
339
+ }
340
+ } catch {}
341
+ if (text) onDataRef.current(text);
342
+ };
343
+ ws.onerror = () => {};
344
+ ws.onclose = () => {
345
+ if (pendingWsRef.current === ws) pendingWsRef.current = null;
346
+ const wasActive = wsRef.current === ws;
347
+ const wasReady = wasActive && transportReadyRef.current;
348
+ if (wasActive) {
349
+ wsRef.current = null;
350
+ transportReadyRef.current = false;
351
+ }
352
+ if (!opened) {
353
+ settle(false);
354
+ return;
355
+ }
356
+ if (!wasReady) {
357
+ settle(false);
358
+ return;
359
+ }
360
+ if (protocolFailureRef.current || !wasActive || !mountedRef.current) return;
361
+ setIsConnected(false);
362
+ if (sessionIdRef.current) retryTimerRef.current = setTimeout(() => {
363
+ if (mountedRef.current && sessionIdRef.current) {
364
+ if (shouldDeleteSessionRef.current) connectStreamRef.current?.(sessionIdRef.current);
365
+ else reconnectRef.current?.();
366
+ }
367
+ }, 1e3);
368
+ };
369
+ });
370
+ }, [apiUrl, rejectPendingInput]);
371
+ const patchGeometry = useCallback(async (sessionId, cols, rows) => {
372
+ try {
373
+ const res = await fetch(`${apiUrl}/terminals/${sessionId}`, {
374
+ method: "PATCH",
375
+ headers: {
376
+ Authorization: `Bearer ${tokenRef.current}`,
377
+ "Content-Type": "application/json"
378
+ },
379
+ credentials: "include",
380
+ body: JSON.stringify({
381
+ cols,
382
+ rows
383
+ })
384
+ });
385
+ if (!res.ok) console.error("Failed to resize terminal:", res.status);
386
+ } catch (err) {
387
+ console.error("Failed to resize terminal", err);
388
+ }
389
+ }, [apiUrl]);
390
+ const connectStream = useCallback(async (sessionId) => {
391
+ if (exactAttachRequiredRef.current) throw new Error("Interactive terminal requires an exact WebSocket attachment");
392
+ abortStream();
393
+ setError(null);
394
+ try {
395
+ const controller = new AbortController();
396
+ abortRef.current = controller;
397
+ const streamRes = await fetch(`${apiUrl}/terminals/${sessionId}/stream`, {
398
+ headers: { Authorization: `Bearer ${tokenRef.current}` },
399
+ credentials: "include",
400
+ signal: controller.signal
401
+ });
402
+ if (!streamRes.ok || !streamRes.body) {
403
+ const err = /* @__PURE__ */ new Error(`SSE stream failed: ${streamRes.status}`);
404
+ err.httpStatus = streamRes.status;
405
+ throw err;
406
+ }
407
+ if (mountedRef.current) {
408
+ transportReadyRef.current = true;
409
+ setIsConnected(true);
410
+ setError(null);
411
+ retryCountRef.current = 0;
412
+ ensureDrainRunningRef.current?.();
413
+ if (geometryPendingRef.current) {
414
+ geometryPendingRef.current = false;
415
+ patchGeometry(sessionId, colsRef.current, rowsRef.current);
416
+ }
417
+ }
418
+ const reader = streamRes.body.getReader();
419
+ const decoder = new TextDecoder();
420
+ let buffer = "";
421
+ while (true) {
422
+ const { done, value } = await reader.read();
423
+ if (done) break;
424
+ buffer += decoder.decode(value, { stream: true });
425
+ const frames = buffer.split("\n\n");
426
+ buffer = frames.pop() ?? "";
427
+ for (const frame of frames) {
428
+ if (!frame.trim()) continue;
429
+ for (const line of frame.split("\n")) if (line.startsWith("data:")) {
430
+ const raw = line.slice(5).trim();
431
+ if (!raw) continue;
432
+ try {
433
+ const event = JSON.parse(raw);
434
+ if (event.type === "data.stdout" || event.type === "data.stderr") {
435
+ const text = event.properties?.text ?? "";
436
+ if (text && mountedRef.current) onDataRef.current(text);
437
+ }
438
+ } catch {
439
+ if (mountedRef.current) onDataRef.current(raw);
440
+ }
441
+ }
442
+ }
443
+ }
444
+ if (mountedRef.current) {
445
+ transportReadyRef.current = false;
446
+ setIsConnected(false);
447
+ retryTimerRef.current = setTimeout(() => {
448
+ if (mountedRef.current && sessionIdRef.current) connectStreamRef.current?.(sessionIdRef.current);
449
+ }, 1e3);
450
+ }
451
+ } catch (err) {
452
+ if (err.name === "AbortError") return;
453
+ if (mountedRef.current) {
454
+ const message = err instanceof Error ? err.message : "Stream connection failed";
455
+ setError(message);
456
+ transportReadyRef.current = false;
457
+ setIsConnected(false);
458
+ const httpStatus = err.httpStatus;
459
+ if (!(httpStatus !== void 0 && httpStatus >= 400 && httpStatus < 500) && retryCountRef.current < 8) {
460
+ const delay = Math.min(3e3 * Math.pow(2, retryCountRef.current), 3e4);
461
+ retryCountRef.current++;
462
+ retryTimerRef.current = setTimeout(() => {
463
+ if (mountedRef.current && sessionIdRef.current) connectStreamRef.current?.(sessionIdRef.current);
464
+ }, delay);
465
+ }
466
+ }
467
+ }
468
+ }, [
469
+ apiUrl,
470
+ abortStream,
471
+ patchGeometry
472
+ ]);
473
+ onDataRef.current = onData;
474
+ tokenRef.current = token;
475
+ incarnationIdRef.current = incarnationId;
476
+ controlRef.current = control;
477
+ exactAttachRequiredRef.current = incarnationId !== void 0 || control !== void 0;
478
+ connectStreamRef.current = connectStream;
479
+ const exactIdentityKey = interactiveIdentityKey(incarnationId, control);
480
+ const connect = useCallback(async () => {
481
+ cleanup();
482
+ const myGen = ++connectGenRef.current;
483
+ protocolFailureRef.current = false;
484
+ retryCountRef.current = 0;
485
+ setError(null);
486
+ if (exactAttachRequiredRef.current && (!incarnationIdRef.current || !controlRef.current)) {
487
+ setError("Interactive terminal identity is incomplete");
488
+ return;
489
+ }
490
+ try {
491
+ const connectionId = providedConnectionId ?? createTerminalConnectionId();
492
+ if (!mountedRef.current) return;
493
+ sessionIdRef.current = connectionId;
494
+ shouldDeleteSessionRef.current = false;
495
+ transportReadyRef.current = false;
496
+ inputAbortRef.current = new AbortController();
497
+ const directWsOk = await connectWs(connectionId, { initOnOpen: true });
498
+ if (!mountedRef.current || connectGenRef.current !== myGen) return;
499
+ if (protocolFailureRef.current) return;
500
+ ensureDrainRunningRef.current?.();
501
+ if (directWsOk) return;
502
+ if (exactAttachRequiredRef.current) throw new Error("Interactive terminal WebSocket attach failed; exact identity was not accepted");
503
+ sessionIdRef.current = null;
504
+ transportReadyRef.current = false;
505
+ if (inputAbortRef.current) {
506
+ inputAbortRef.current.abort();
507
+ inputAbortRef.current = null;
508
+ }
509
+ const res = await fetch(`${apiUrl}/terminals`, {
510
+ method: "POST",
511
+ headers: {
512
+ Authorization: `Bearer ${tokenRef.current}`,
513
+ "Content-Type": "application/json"
514
+ },
515
+ credentials: "include",
516
+ body: JSON.stringify({
517
+ cols: colsRef.current,
518
+ rows: rowsRef.current
519
+ })
520
+ });
521
+ geometryPendingRef.current = false;
522
+ if (!res.ok) throw new Error(`Failed to create terminal: ${res.status}`);
523
+ const body = await res.json();
524
+ const sessionId = body.data?.sessionId ?? body.sessionId;
525
+ if (!sessionId) throw new Error("No sessionId in response");
526
+ if (!mountedRef.current) return;
527
+ sessionIdRef.current = sessionId;
528
+ shouldDeleteSessionRef.current = true;
529
+ transportReadyRef.current = false;
530
+ inputAbortRef.current = new AbortController();
531
+ const wsOk = await connectWs(sessionId, { initOnOpen: true });
532
+ if (!mountedRef.current || connectGenRef.current !== myGen) return;
533
+ if (protocolFailureRef.current) return;
534
+ ensureDrainRunningRef.current?.();
535
+ if (wsOk) return;
536
+ await connectStream(sessionId);
537
+ } catch (err) {
538
+ if (err.name === "AbortError") return;
539
+ if (mountedRef.current) {
540
+ const message = err instanceof Error ? err.message : "Terminal connection failed";
541
+ setError(message);
542
+ transportReadyRef.current = false;
543
+ setIsConnected(false);
544
+ }
545
+ }
546
+ }, [
547
+ apiUrl,
548
+ providedConnectionId,
549
+ cleanup,
550
+ connectWs,
551
+ connectStream,
552
+ exactIdentityKey
553
+ ]);
554
+ reconnectRef.current = connect;
555
+ const resizeTerminal = useCallback(async (cols, rows) => {
556
+ if (cols > 0) colsRef.current = cols;
557
+ if (rows > 0) rowsRef.current = rows;
558
+ const sid = sessionIdRef.current;
559
+ if (!sid || cols <= 0 || rows <= 0) return;
560
+ const ws = wsRef.current;
561
+ if (ws && ws.readyState === WebSocket.OPEN) try {
562
+ ws.send(JSON.stringify({
563
+ type: "resize",
564
+ cols,
565
+ rows
566
+ }));
567
+ return;
568
+ } catch (err) {
569
+ console.warn("Terminal resize over WS failed; falling back to HTTP", err);
570
+ }
571
+ if (pendingWsRef.current) {
572
+ geometryPendingRef.current = true;
573
+ return;
574
+ }
575
+ if (exactAttachRequiredRef.current) throw new Error("Interactive terminal is not attached with its exact identity");
576
+ await patchGeometry(sid, cols, rows);
577
+ }, [apiUrl, patchGeometry]);
578
+ const drainInputQueue = useCallback(async () => {
579
+ while (pendingBatchRef.current.data.length > 0) {
580
+ const sid = sessionIdRef.current;
581
+ if (!sid || !transportReadyRef.current) return;
582
+ const batch = pendingBatchRef.current;
583
+ pendingBatchRef.current = createEmptyBatch();
584
+ const ws = wsRef.current;
585
+ if (ws && ws.readyState === WebSocket.OPEN) {
586
+ try {
587
+ ws.send(JSON.stringify({
588
+ type: "input",
589
+ data: batch.data
590
+ }));
591
+ for (const w of batch.waiters) w.resolve();
592
+ } catch (err) {
593
+ for (const w of batch.waiters) w.reject(err);
594
+ }
595
+ continue;
596
+ }
597
+ if (exactAttachRequiredRef.current) {
598
+ for (const w of batch.waiters) w.reject(/* @__PURE__ */ new Error("Interactive terminal is not attached with its exact identity"));
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 ? /* @__PURE__ */ 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) drainPromiseRef.current = run();
629
+ else drainPromiseRef.current = null;
630
+ });
631
+ drainPromiseRef.current = run();
632
+ }, [drainInputQueue]);
633
+ ensureDrainRunningRef.current = ensureDrainRunning;
634
+ const sendCommand = useCallback((command) => {
635
+ return new Promise((resolve, reject) => {
636
+ if (command.length === 0) {
637
+ resolve();
638
+ return;
639
+ }
640
+ if (protocolFailureRef.current) {
641
+ reject(/* @__PURE__ */ new Error("Terminal protocol failure"));
642
+ return;
643
+ }
644
+ pendingBatchRef.current.data += command;
645
+ pendingBatchRef.current.waiters.push({
646
+ resolve,
647
+ reject
648
+ });
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 {
661
+ isConnected,
662
+ error,
663
+ sendCommand,
664
+ resizeTerminal,
665
+ reconnect: connect
666
+ };
667
+ }
668
+ //#endregion
669
+ export { usePtySession as t };