@tangle-network/agent-app 0.43.2 → 0.43.3

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.
@@ -0,0 +1,166 @@
1
+ // src/web-react/sandbox-terminal.ts
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ var DEFAULT_PROVISION_POLL_INTERVAL_MS = 2e3;
4
+ var DEFAULT_PROVISION_POLL_TIMEOUT_MS = 9e4;
5
+ var DEFAULT_TOKEN_REFRESH_SKEW_MS = 12e4;
6
+ var EMPTY_CONNECTION = {
7
+ runtimeUrl: null,
8
+ sidecarUrl: null,
9
+ token: null,
10
+ expiresAt: null,
11
+ status: "idle",
12
+ error: null,
13
+ loading: false
14
+ };
15
+ function useSandboxTerminalConnection(opts) {
16
+ const [conn, setConn] = useState(EMPTY_CONNECTION);
17
+ const mountedRef = useRef(false);
18
+ const generationRef = useRef(0);
19
+ const fetcher = opts.fetcher ?? fetch;
20
+ const pollIntervalMs = opts.provisionPollIntervalMs ?? DEFAULT_PROVISION_POLL_INTERVAL_MS;
21
+ const pollTimeoutMs = opts.provisionPollTimeoutMs ?? DEFAULT_PROVISION_POLL_TIMEOUT_MS;
22
+ const refreshSkewMs = opts.tokenRefreshSkewMs ?? DEFAULT_TOKEN_REFRESH_SKEW_MS;
23
+ const connectionUrl = useCallback(() => {
24
+ if (typeof opts.connectionUrl === "function") return opts.connectionUrl(opts.workspaceId);
25
+ return opts.connectionUrl ?? `/api/workspaces/${encodeURIComponent(opts.workspaceId)}/sandbox/connection`;
26
+ }, [opts.connectionUrl, opts.workspaceId]);
27
+ const connect = useCallback(async () => {
28
+ const generation = generationRef.current + 1;
29
+ generationRef.current = generation;
30
+ const isCurrent = () => mountedRef.current && generationRef.current === generation;
31
+ const setCurrentConn = (value) => {
32
+ if (!isCurrent()) return;
33
+ setConn(value);
34
+ };
35
+ setCurrentConn((current) => ({ ...current, loading: true, error: null }));
36
+ const deadline = Date.now() + pollTimeoutMs;
37
+ while (isCurrent()) {
38
+ try {
39
+ const res = await fetcher(connectionUrl());
40
+ const data = await res.json();
41
+ if (!isCurrent()) return;
42
+ const runtimeUrl = data.runtimeUrl ?? data.sidecarUrl;
43
+ if (res.ok && runtimeUrl && data.token && data.expiresAt) {
44
+ setCurrentConn({
45
+ runtimeUrl,
46
+ sidecarUrl: data.sidecarUrl ?? runtimeUrl,
47
+ token: data.token,
48
+ expiresAt: data.expiresAt,
49
+ status: data.status ?? "running",
50
+ error: null,
51
+ loading: false,
52
+ ...data.sandboxId ? { sandboxId: data.sandboxId } : {}
53
+ });
54
+ return;
55
+ }
56
+ if (res.ok) {
57
+ setCurrentConn({
58
+ runtimeUrl: null,
59
+ sidecarUrl: null,
60
+ token: null,
61
+ expiresAt: null,
62
+ loading: false,
63
+ error: "Sandbox connection response is missing required fields",
64
+ status: data.status ?? "error",
65
+ ...data.sandboxId ? { sandboxId: data.sandboxId } : {}
66
+ });
67
+ return;
68
+ }
69
+ if (res.status === 503 && Date.now() < deadline) {
70
+ setCurrentConn((current) => ({
71
+ ...current,
72
+ loading: true,
73
+ status: data.status ?? "provisioning",
74
+ error: null
75
+ }));
76
+ await sleep(pollIntervalMs);
77
+ continue;
78
+ }
79
+ setCurrentConn((current) => ({
80
+ ...current,
81
+ runtimeUrl: null,
82
+ sidecarUrl: null,
83
+ token: null,
84
+ expiresAt: null,
85
+ loading: false,
86
+ error: data.error ?? "Sandbox not available",
87
+ status: data.status ?? "error"
88
+ }));
89
+ return;
90
+ } catch (err) {
91
+ if (!isCurrent()) return;
92
+ if (Date.now() < deadline) {
93
+ await sleep(pollIntervalMs);
94
+ continue;
95
+ }
96
+ setCurrentConn((current) => ({
97
+ ...current,
98
+ runtimeUrl: null,
99
+ sidecarUrl: null,
100
+ token: null,
101
+ expiresAt: null,
102
+ loading: false,
103
+ error: err instanceof Error ? err.message : "Connection failed"
104
+ }));
105
+ return;
106
+ }
107
+ }
108
+ }, [connectionUrl, fetcher, pollIntervalMs, pollTimeoutMs]);
109
+ useEffect(() => {
110
+ mountedRef.current = true;
111
+ return () => {
112
+ mountedRef.current = false;
113
+ generationRef.current += 1;
114
+ };
115
+ }, []);
116
+ useEffect(() => {
117
+ void connect();
118
+ }, [connect]);
119
+ useEffect(() => {
120
+ if (!conn.runtimeUrl || !conn.token || !conn.expiresAt) return;
121
+ const refreshAt = Date.parse(conn.expiresAt) - Date.now() - refreshSkewMs;
122
+ if (!Number.isFinite(refreshAt)) {
123
+ setConn((current) => ({
124
+ ...current,
125
+ runtimeUrl: null,
126
+ sidecarUrl: null,
127
+ token: null,
128
+ expiresAt: null,
129
+ status: "error",
130
+ error: "Sandbox token expiry is invalid"
131
+ }));
132
+ return;
133
+ }
134
+ const timer = window.setTimeout(() => {
135
+ void connect();
136
+ }, Math.max(1e3, refreshAt));
137
+ return () => window.clearTimeout(timer);
138
+ }, [conn.runtimeUrl, conn.token, conn.expiresAt, connect, refreshSkewMs]);
139
+ return { ...conn, connect };
140
+ }
141
+ function sleep(ms) {
142
+ return new Promise((resolve) => window.setTimeout(resolve, ms));
143
+ }
144
+ var DEFAULT_TERMINAL_CID_KEY = "agent-app:terminal-connection-id";
145
+ function newConnectionId() {
146
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
147
+ return `cid-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`;
148
+ }
149
+ function tabTerminalConnectionId(storageKey = DEFAULT_TERMINAL_CID_KEY) {
150
+ try {
151
+ const store = globalThis.sessionStorage;
152
+ const existing = store?.getItem(storageKey);
153
+ if (existing) return existing;
154
+ const id = newConnectionId();
155
+ store?.setItem(storageKey, id);
156
+ return id;
157
+ } catch {
158
+ return newConnectionId();
159
+ }
160
+ }
161
+
162
+ export {
163
+ useSandboxTerminalConnection,
164
+ tabTerminalConnectionId
165
+ };
166
+ //# sourceMappingURL=chunk-65P3HJY3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/web-react/sandbox-terminal.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\n\nexport interface SandboxTerminalConnection {\n runtimeUrl: string | null\n sidecarUrl: string | null\n token: string | null\n expiresAt: string | null\n status: string\n error: string | null\n loading: boolean\n sandboxId?: string\n}\n\nexport interface SandboxTerminalConnectionResponse {\n runtimeUrl?: string\n sidecarUrl?: string\n token?: string\n expiresAt?: string\n status?: string\n error?: string\n sandboxId?: string\n}\n\nexport interface UseSandboxTerminalConnectionOptions {\n workspaceId: string\n connectionUrl?: string | ((workspaceId: string) => string)\n fetcher?: typeof fetch\n provisionPollIntervalMs?: number\n provisionPollTimeoutMs?: number\n tokenRefreshSkewMs?: number\n}\n\nexport interface UseSandboxTerminalConnectionResult extends SandboxTerminalConnection {\n connect: () => Promise<void>\n}\n\nconst DEFAULT_PROVISION_POLL_INTERVAL_MS = 2_000\nconst DEFAULT_PROVISION_POLL_TIMEOUT_MS = 90_000\nconst DEFAULT_TOKEN_REFRESH_SKEW_MS = 120_000\n\nconst EMPTY_CONNECTION: SandboxTerminalConnection = {\n runtimeUrl: null,\n sidecarUrl: null,\n token: null,\n expiresAt: null,\n status: 'idle',\n error: null,\n loading: false,\n}\n\nexport function useSandboxTerminalConnection(opts: UseSandboxTerminalConnectionOptions): UseSandboxTerminalConnectionResult {\n const [conn, setConn] = useState<SandboxTerminalConnection>(EMPTY_CONNECTION)\n const mountedRef = useRef(false)\n const generationRef = useRef(0)\n const fetcher = opts.fetcher ?? fetch\n const pollIntervalMs = opts.provisionPollIntervalMs ?? DEFAULT_PROVISION_POLL_INTERVAL_MS\n const pollTimeoutMs = opts.provisionPollTimeoutMs ?? DEFAULT_PROVISION_POLL_TIMEOUT_MS\n const refreshSkewMs = opts.tokenRefreshSkewMs ?? DEFAULT_TOKEN_REFRESH_SKEW_MS\n\n const connectionUrl = useCallback(() => {\n if (typeof opts.connectionUrl === 'function') return opts.connectionUrl(opts.workspaceId)\n return opts.connectionUrl ?? `/api/workspaces/${encodeURIComponent(opts.workspaceId)}/sandbox/connection`\n }, [opts.connectionUrl, opts.workspaceId])\n\n const connect = useCallback(async () => {\n const generation = generationRef.current + 1\n generationRef.current = generation\n const isCurrent = () => mountedRef.current && generationRef.current === generation\n const setCurrentConn: typeof setConn = (value) => {\n if (!isCurrent()) return\n setConn(value)\n }\n\n setCurrentConn((current) => ({ ...current, loading: true, error: null }))\n const deadline = Date.now() + pollTimeoutMs\n while (isCurrent()) {\n try {\n const res = await fetcher(connectionUrl())\n const data = (await res.json()) as SandboxTerminalConnectionResponse\n if (!isCurrent()) return\n const runtimeUrl = data.runtimeUrl ?? data.sidecarUrl\n if (res.ok && runtimeUrl && data.token && data.expiresAt) {\n setCurrentConn({\n runtimeUrl,\n sidecarUrl: data.sidecarUrl ?? runtimeUrl,\n token: data.token,\n expiresAt: data.expiresAt,\n status: data.status ?? 'running',\n error: null,\n loading: false,\n ...(data.sandboxId ? { sandboxId: data.sandboxId } : {}),\n })\n return\n }\n if (res.ok) {\n setCurrentConn({\n runtimeUrl: null,\n sidecarUrl: null,\n token: null,\n expiresAt: null,\n loading: false,\n error: 'Sandbox connection response is missing required fields',\n status: data.status ?? 'error',\n ...(data.sandboxId ? { sandboxId: data.sandboxId } : {}),\n })\n return\n }\n if (res.status === 503 && Date.now() < deadline) {\n setCurrentConn((current) => ({\n ...current,\n loading: true,\n status: data.status ?? 'provisioning',\n error: null,\n }))\n await sleep(pollIntervalMs)\n continue\n }\n setCurrentConn((current) => ({\n ...current,\n runtimeUrl: null,\n sidecarUrl: null,\n token: null,\n expiresAt: null,\n loading: false,\n error: data.error ?? 'Sandbox not available',\n status: data.status ?? 'error',\n }))\n return\n } catch (err) {\n if (!isCurrent()) return\n if (Date.now() < deadline) {\n await sleep(pollIntervalMs)\n continue\n }\n setCurrentConn((current) => ({\n ...current,\n runtimeUrl: null,\n sidecarUrl: null,\n token: null,\n expiresAt: null,\n loading: false,\n error: err instanceof Error ? err.message : 'Connection failed',\n }))\n return\n }\n }\n }, [connectionUrl, fetcher, pollIntervalMs, pollTimeoutMs])\n\n useEffect(() => {\n mountedRef.current = true\n return () => {\n mountedRef.current = false\n generationRef.current += 1\n }\n }, [])\n\n useEffect(() => {\n void connect()\n }, [connect])\n\n useEffect(() => {\n if (!conn.runtimeUrl || !conn.token || !conn.expiresAt) return\n const refreshAt = Date.parse(conn.expiresAt) - Date.now() - refreshSkewMs\n if (!Number.isFinite(refreshAt)) {\n setConn((current) => ({\n ...current,\n runtimeUrl: null,\n sidecarUrl: null,\n token: null,\n expiresAt: null,\n status: 'error',\n error: 'Sandbox token expiry is invalid',\n }))\n return\n }\n const timer = window.setTimeout(() => {\n void connect()\n }, Math.max(1_000, refreshAt))\n return () => window.clearTimeout(timer)\n }, [conn.runtimeUrl, conn.token, conn.expiresAt, connect, refreshSkewMs])\n\n return { ...conn, connect }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => window.setTimeout(resolve, ms))\n}\n\nconst DEFAULT_TERMINAL_CID_KEY = 'agent-app:terminal-connection-id'\n\nfunction newConnectionId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') return crypto.randomUUID()\n return `cid-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`\n}\n\n/**\n * Stable-per-tab, unique-per-client terminal connection id.\n *\n * Persists in `sessionStorage` so a reload in the same tab reuses the id (the\n * sidecar restores the same PTY session via `TerminalView.connectionId`), while\n * separate tabs/windows each get a distinct id. Pass the result as\n * `TerminalView`'s `connectionId`. Without it (e.g. gtm-agent today) every tab\n * shares one connection id and their reconnects evict each other.\n *\n * Falls back to an ephemeral id when `sessionStorage` is unavailable (SSR,\n * privacy mode) — still unique per call, just not reload-stable.\n */\nexport function tabTerminalConnectionId(storageKey: string = DEFAULT_TERMINAL_CID_KEY): string {\n try {\n const store = globalThis.sessionStorage\n const existing = store?.getItem(storageKey)\n if (existing) return existing\n const id = newConnectionId()\n store?.setItem(storageKey, id)\n return id\n } catch {\n return newConnectionId()\n }\n}\n"],"mappings":"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","names":[]}