@tangle-network/sandbox-ui 0.10.7 → 0.10.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,6 +35,52 @@ Import styles in your app root:
35
35
  import "@tangle-network/sandbox-ui/styles";
36
36
  ```
37
37
 
38
+ ### Fonts
39
+
40
+ sandbox-ui references the following font families in its design tokens but does **not** bundle them — consumer apps must load the fonts themselves. This is deliberate: a URL `@import` inside a library CSS bundle breaks when downstream apps chain-import the stylesheet (see CHANGELOG 0.10.9 for the full reasoning).
41
+
42
+ | Family | Role | Used as CSS variable |
43
+ | ------------ | ----------------------------------- | -------------------- |
44
+ | Geist | UI body text | `--font-sans` |
45
+ | Geist Mono | Code, terminal | `--font-mono` |
46
+ | Outfit | Display / headings (default theme) | `--font-display` |
47
+ | Manrope | Display / headings (vault theme) | `--font-display` |
48
+ | Inter | UI body (vault theme) | `--font-sans` |
49
+
50
+ Pick one loading strategy that fits your app:
51
+
52
+ **1. Self-hosted via `@fontsource/*`** (recommended — no external network request):
53
+
54
+ ```bash
55
+ npm install @fontsource/geist-sans @fontsource/geist-mono @fontsource/outfit @fontsource/manrope @fontsource/inter
56
+ ```
57
+
58
+ ```tsx
59
+ // app entry
60
+ import "@fontsource/geist-sans/400.css";
61
+ import "@fontsource/geist-sans/500.css";
62
+ import "@fontsource/geist-sans/600.css";
63
+ import "@fontsource/geist-sans/700.css";
64
+ import "@fontsource/geist-mono/400.css";
65
+ import "@fontsource/geist-mono/500.css";
66
+ import "@fontsource/outfit/500.css";
67
+ import "@fontsource/outfit/700.css";
68
+ import "@fontsource/manrope/500.css";
69
+ import "@fontsource/manrope/700.css";
70
+ import "@fontsource/inter/400.css";
71
+ import "@fontsource/inter/600.css";
72
+ ```
73
+
74
+ **2. Google Fonts via HTML `<link>`:**
75
+
76
+ ```html
77
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
78
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
79
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500;600&family=Outfit:wght@400;500;600;700&family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&display=swap" />
80
+ ```
81
+
82
+ Any family you omit falls back per the `--font-*` token chain (e.g. `--font-sans` falls back to `"DM Sans", ui-sans-serif, system-ui, sans-serif`).
83
+
38
84
  If you are building on the sandbox SDK directly, use `useSdkSession` to turn raw SDK/session-gateway events into the `messages + partMap` model that `ChatContainer` and `SandboxWorkbench` expect:
39
85
 
40
86
  ```tsx
package/dist/chat.js CHANGED
@@ -8,10 +8,10 @@ import {
8
8
  MessageList,
9
9
  ThinkingIndicator,
10
10
  UserMessage
11
- } from "./chunk-WKSGQVLI.js";
11
+ } from "./chunk-XLG757B6.js";
12
12
  import "./chunk-54SQQMMM.js";
13
- import "./chunk-QOL4ZB24.js";
14
- import "./chunk-HRMUF35V.js";
13
+ import "./chunk-MQXABZTB.js";
14
+ import "./chunk-4CLN43XT.js";
15
15
  import "./chunk-MT5FJ3ZT.js";
16
16
  import "./chunk-BX6AQMUS.js";
17
17
  import "./chunk-ZNCEM5CD.js";
@@ -0,0 +1,45 @@
1
+ // src/utils/format.ts
2
+ function formatDuration(ms) {
3
+ if (ms < 1e3) return "<1s";
4
+ const seconds = Math.floor(ms / 1e3);
5
+ if (seconds < 60) return `${seconds}s`;
6
+ const minutes = Math.floor(seconds / 60);
7
+ const remaining = seconds % 60;
8
+ return remaining > 0 ? `${minutes}m ${remaining}s` : `${minutes}m`;
9
+ }
10
+ function truncateText(text, max) {
11
+ const cleaned = text.replace(/\s+/g, " ").trim();
12
+ if (cleaned.length <= max) return cleaned;
13
+ return cleaned.slice(0, max).trim() + "...";
14
+ }
15
+ function formatUptime(ms) {
16
+ if (!Number.isFinite(ms) || ms < 0) return "\u2014";
17
+ const totalSeconds = Math.floor(ms / 1e3);
18
+ if (totalSeconds < 60) return `${totalSeconds}s`;
19
+ const minutes = Math.floor(totalSeconds / 60);
20
+ const seconds = totalSeconds % 60;
21
+ if (minutes < 60) return `${minutes}m ${seconds}s`;
22
+ const hours = Math.floor(minutes / 60);
23
+ const remMinutes = minutes % 60;
24
+ if (hours < 24) return `${hours}h ${remMinutes}m`;
25
+ const days = Math.floor(hours / 24);
26
+ const remHours = hours % 24;
27
+ return `${days}d ${remHours}h`;
28
+ }
29
+ function formatBytes(bytes) {
30
+ if (!Number.isFinite(bytes) || bytes < 0) return "\u2014";
31
+ if (bytes < 1024) return `${Math.round(bytes)} B`;
32
+ const kb = bytes / 1024;
33
+ if (kb < 1024) return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} KB`;
34
+ const mb = kb / 1024;
35
+ if (mb < 1024) return `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;
36
+ const gb = mb / 1024;
37
+ return `${gb < 10 ? gb.toFixed(2) : gb.toFixed(1)} GB`;
38
+ }
39
+
40
+ export {
41
+ formatDuration,
42
+ truncateText,
43
+ formatUptime,
44
+ formatBytes
45
+ };
@@ -10,7 +10,7 @@ import {
10
10
  } from "./chunk-MA7YKRUP.js";
11
11
  import {
12
12
  ChatContainer
13
- } from "./chunk-WKSGQVLI.js";
13
+ } from "./chunk-XLG757B6.js";
14
14
  import {
15
15
  OpenUIArtifactRenderer
16
16
  } from "./chunk-ZNCEM5CD.js";
@@ -0,0 +1,220 @@
1
+ // src/hooks/use-auth.ts
2
+ import * as React from "react";
3
+ function useAuth({
4
+ apiBaseUrl,
5
+ revalidateOnFocus = false,
6
+ shouldRetryOnError = false
7
+ }) {
8
+ const [user, setUser] = React.useState(null);
9
+ const [isLoading, setIsLoading] = React.useState(true);
10
+ const [error, setError] = React.useState(null);
11
+ const retryTimerRef = React.useRef(null);
12
+ const abortRef = React.useRef(null);
13
+ const fetchSession = React.useCallback(async () => {
14
+ abortRef.current?.abort();
15
+ const controller = new AbortController();
16
+ abortRef.current = controller;
17
+ setIsLoading(true);
18
+ setError(null);
19
+ try {
20
+ const res = await fetch(`${apiBaseUrl}/auth/session`, {
21
+ credentials: "include",
22
+ signal: controller.signal
23
+ });
24
+ if (!res.ok) {
25
+ throw new Error("Not authenticated");
26
+ }
27
+ const data = await res.json();
28
+ if (data.success && data.data) {
29
+ setUser(data.data);
30
+ } else {
31
+ setUser(null);
32
+ }
33
+ } catch (err) {
34
+ if (err.name === "AbortError") return;
35
+ setError(err instanceof Error ? err : new Error("Unknown error"));
36
+ setUser(null);
37
+ if (shouldRetryOnError) {
38
+ if (retryTimerRef.current) clearTimeout(retryTimerRef.current);
39
+ retryTimerRef.current = setTimeout(fetchSession, 5e3);
40
+ }
41
+ } finally {
42
+ if (!controller.signal.aborted) {
43
+ setIsLoading(false);
44
+ }
45
+ }
46
+ }, [apiBaseUrl, shouldRetryOnError]);
47
+ React.useEffect(() => {
48
+ fetchSession();
49
+ return () => {
50
+ abortRef.current?.abort();
51
+ if (retryTimerRef.current) clearTimeout(retryTimerRef.current);
52
+ };
53
+ }, [fetchSession]);
54
+ React.useEffect(() => {
55
+ if (!revalidateOnFocus) return;
56
+ const handleFocus = () => {
57
+ fetchSession();
58
+ };
59
+ window.addEventListener("focus", handleFocus);
60
+ return () => window.removeEventListener("focus", handleFocus);
61
+ }, [revalidateOnFocus, fetchSession]);
62
+ return {
63
+ user,
64
+ isLoading,
65
+ isError: !!error,
66
+ error,
67
+ mutate: fetchSession
68
+ };
69
+ }
70
+ function createAuthFetcher(_apiBaseUrl) {
71
+ return async function authFetcher(url, options) {
72
+ const res = await fetch(url, {
73
+ ...options,
74
+ credentials: "include",
75
+ headers: {
76
+ ...options?.headers
77
+ }
78
+ });
79
+ if (!res.ok) {
80
+ throw new Error(`Request failed with status ${res.status}`);
81
+ }
82
+ return res.json();
83
+ };
84
+ }
85
+ function useApiKey() {
86
+ const [apiKey, setApiKey] = React.useState(null);
87
+ React.useEffect(() => {
88
+ if (typeof window !== "undefined") {
89
+ setApiKey(localStorage.getItem("apiKey"));
90
+ }
91
+ }, []);
92
+ return apiKey;
93
+ }
94
+
95
+ // src/hooks/use-live-time.ts
96
+ import * as React2 from "react";
97
+ function useLiveTime(intervalMs = 1e3) {
98
+ const [now, setNow] = React2.useState(() => Date.now());
99
+ React2.useEffect(() => {
100
+ const requested = Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : 1e3;
101
+ const delay = Math.max(requested, 100);
102
+ const id = window.setInterval(() => {
103
+ setNow(Date.now());
104
+ }, delay);
105
+ return () => {
106
+ window.clearInterval(id);
107
+ };
108
+ }, [intervalMs]);
109
+ return now;
110
+ }
111
+
112
+ // src/hooks/use-sandbox-metrics.ts
113
+ import * as React3 from "react";
114
+ function useSandboxMetrics({
115
+ apiBaseUrl,
116
+ sandboxId,
117
+ token,
118
+ enabled = true,
119
+ intervalMs = 3e3
120
+ }) {
121
+ const [metrics, setMetrics] = React3.useState(null);
122
+ const [loading, setLoading] = React3.useState(false);
123
+ const [error, setError] = React3.useState(null);
124
+ const [lastUpdatedAt, setLastUpdatedAt] = React3.useState(null);
125
+ const sampleRef = React3.useRef(null);
126
+ const hasLoadedRef = React3.useRef(false);
127
+ const prevSandboxIdRef = React3.useRef(null);
128
+ React3.useEffect(() => {
129
+ const sandboxCleared = !sandboxId || !apiBaseUrl;
130
+ const sandboxChanged = prevSandboxIdRef.current !== null && prevSandboxIdRef.current !== sandboxId;
131
+ if (sandboxCleared && prevSandboxIdRef.current !== null || sandboxChanged) {
132
+ sampleRef.current = null;
133
+ hasLoadedRef.current = false;
134
+ setMetrics(null);
135
+ setLastUpdatedAt(null);
136
+ setError(null);
137
+ if (sandboxCleared) setLoading(false);
138
+ }
139
+ prevSandboxIdRef.current = sandboxId ?? null;
140
+ if (!enabled || sandboxCleared) {
141
+ return;
142
+ }
143
+ const controller = new AbortController();
144
+ let cancelled = false;
145
+ let timeoutId = null;
146
+ const delay = Math.max(intervalMs, 500);
147
+ const fetchOnce = async () => {
148
+ if (!hasLoadedRef.current) setLoading(true);
149
+ try {
150
+ const headers = {};
151
+ if (token) headers.Authorization = `Bearer ${token}`;
152
+ const res = await fetch(
153
+ `${apiBaseUrl}/v1/sidecar-proxy/${encodeURIComponent(sandboxId)}/metrics/json`,
154
+ {
155
+ method: "GET",
156
+ credentials: "include",
157
+ headers,
158
+ signal: controller.signal
159
+ }
160
+ );
161
+ if (!res.ok) {
162
+ throw new Error(`Metrics request failed (HTTP ${res.status})`);
163
+ }
164
+ const data = await res.json();
165
+ const user = data?.process?.cpuSeconds?.user ?? 0;
166
+ const system = data?.process?.cpuSeconds?.system ?? 0;
167
+ const cpuSeconds = user + system;
168
+ const wallMs = Date.now();
169
+ if (cancelled) return;
170
+ let cpuPercent = null;
171
+ const prev = sampleRef.current;
172
+ if (prev && prev.sandboxId === sandboxId) {
173
+ const dCpu = cpuSeconds - prev.cpuSeconds;
174
+ const dWallSec = (wallMs - prev.wallMs) / 1e3;
175
+ if (dWallSec > 0 && dCpu >= 0) {
176
+ cpuPercent = dCpu / dWallSec * 100;
177
+ }
178
+ }
179
+ sampleRef.current = { cpuSeconds, wallMs, sandboxId };
180
+ setMetrics({
181
+ cpuPercent,
182
+ rssBytes: data?.process?.memoryBytes?.rss ?? 0,
183
+ heapUsedBytes: data?.process?.memoryBytes?.heapUsed ?? 0,
184
+ heapTotalBytes: data?.process?.memoryBytes?.heapTotal ?? 0
185
+ });
186
+ setLastUpdatedAt(wallMs);
187
+ setError(null);
188
+ hasLoadedRef.current = true;
189
+ setLoading(false);
190
+ } catch (err) {
191
+ if (cancelled || err instanceof DOMException && err.name === "AbortError") {
192
+ return;
193
+ }
194
+ setError(err instanceof Error ? err : new Error(String(err)));
195
+ if (!hasLoadedRef.current) setLoading(false);
196
+ }
197
+ };
198
+ const runLoop = async () => {
199
+ if (cancelled) return;
200
+ await fetchOnce();
201
+ if (cancelled) return;
202
+ timeoutId = window.setTimeout(runLoop, delay);
203
+ };
204
+ runLoop();
205
+ return () => {
206
+ cancelled = true;
207
+ controller.abort();
208
+ if (timeoutId !== null) window.clearTimeout(timeoutId);
209
+ };
210
+ }, [apiBaseUrl, sandboxId, token, enabled, intervalMs]);
211
+ return { metrics, loading, error, lastUpdatedAt };
212
+ }
213
+
214
+ export {
215
+ useAuth,
216
+ createAuthFetcher,
217
+ useApiKey,
218
+ useLiveTime,
219
+ useSandboxMetrics
220
+ };
@@ -1,5 +1,8 @@
1
1
  // src/hooks/use-pty-session.ts
2
2
  import { useState, useEffect, useRef, useCallback } from "react";
3
+ function createEmptyBatch() {
4
+ return { data: "", waiters: [] };
5
+ }
3
6
  function usePtySession({ apiUrl, token, onData }) {
4
7
  const [isConnected, setIsConnected] = useState(false);
5
8
  const [error, setError] = useState(null);
@@ -10,6 +13,20 @@ function usePtySession({ apiUrl, token, onData }) {
10
13
  const mountedRef = useRef(true);
11
14
  const onDataRef = useRef(onData);
12
15
  const connectStreamRef = useRef(null);
16
+ const pendingBatchRef = useRef(createEmptyBatch());
17
+ const drainPromiseRef = useRef(null);
18
+ const inputAbortRef = useRef(null);
19
+ const ensureDrainRunningRef = useRef(null);
20
+ const rejectPendingInput = useCallback((reason) => {
21
+ const batch = pendingBatchRef.current;
22
+ if (batch.waiters.length === 0) {
23
+ pendingBatchRef.current = createEmptyBatch();
24
+ return;
25
+ }
26
+ pendingBatchRef.current = createEmptyBatch();
27
+ const err = new Error(reason);
28
+ for (const w of batch.waiters) w.reject(err);
29
+ }, []);
13
30
  const abortStream = useCallback(() => {
14
31
  if (retryTimerRef.current) {
15
32
  clearTimeout(retryTimerRef.current);
@@ -22,6 +39,10 @@ function usePtySession({ apiUrl, token, onData }) {
22
39
  }, []);
23
40
  const cleanup = useCallback(() => {
24
41
  abortStream();
42
+ if (inputAbortRef.current) {
43
+ inputAbortRef.current.abort();
44
+ inputAbortRef.current = null;
45
+ }
25
46
  if (sessionIdRef.current) {
26
47
  const sid = sessionIdRef.current;
27
48
  sessionIdRef.current = null;
@@ -32,8 +53,9 @@ function usePtySession({ apiUrl, token, onData }) {
32
53
  }).catch(() => {
33
54
  });
34
55
  }
56
+ rejectPendingInput("Terminal session is not connected");
35
57
  setIsConnected(false);
36
- }, [apiUrl, token, abortStream]);
58
+ }, [apiUrl, token, abortStream, rejectPendingInput]);
37
59
  const connectStream = useCallback(async (sessionId) => {
38
60
  abortStream();
39
61
  setError(null);
@@ -139,6 +161,8 @@ function usePtySession({ apiUrl, token, onData }) {
139
161
  if (!sessionId) throw new Error("No sessionId in response");
140
162
  if (!mountedRef.current) return;
141
163
  sessionIdRef.current = sessionId;
164
+ inputAbortRef.current = new AbortController();
165
+ ensureDrainRunningRef.current?.();
142
166
  await connectStream(sessionId);
143
167
  } catch (err) {
144
168
  if (err.name === "AbortError") return;
@@ -169,28 +193,61 @@ function usePtySession({ apiUrl, token, onData }) {
169
193
  console.error("Failed to resize terminal", err);
170
194
  }
171
195
  }, [apiUrl, token]);
172
- const sendCommand = useCallback(async (command) => {
173
- const sid = sessionIdRef.current;
174
- if (!sid) return;
175
- try {
176
- const res = await fetch(`${apiUrl}/terminals/${sid}/input`, {
177
- method: "POST",
178
- headers: {
179
- Authorization: `Bearer ${token}`,
180
- "Content-Type": "application/json"
181
- },
182
- credentials: "include",
183
- body: JSON.stringify({ data: command })
184
- });
185
- if (!res.ok) {
186
- const text = await res.text();
187
- throw new Error(text || `Input failed: ${res.status}`);
196
+ const drainInputQueue = useCallback(async () => {
197
+ while (pendingBatchRef.current.data.length > 0) {
198
+ const sid = sessionIdRef.current;
199
+ if (!sid) {
200
+ return;
201
+ }
202
+ const batch = pendingBatchRef.current;
203
+ pendingBatchRef.current = createEmptyBatch();
204
+ try {
205
+ const res = await fetch(`${apiUrl}/terminals/${sid}/input`, {
206
+ method: "POST",
207
+ headers: {
208
+ Authorization: `Bearer ${token}`,
209
+ "Content-Type": "application/json"
210
+ },
211
+ credentials: "include",
212
+ body: JSON.stringify({ data: batch.data }),
213
+ signal: inputAbortRef.current?.signal
214
+ });
215
+ if (!res.ok) {
216
+ const text = await res.text().catch(() => "");
217
+ throw new Error(text || `Input failed: ${res.status}`);
218
+ }
219
+ for (const w of batch.waiters) w.resolve();
220
+ } catch (err) {
221
+ const isAbort = err?.name === "AbortError";
222
+ const rejection = isAbort ? new Error("Terminal session is not connected") : err;
223
+ if (!isAbort) console.error("Failed to send command", err);
224
+ for (const w of batch.waiters) w.reject(rejection);
188
225
  }
189
- } catch (err) {
190
- console.error("Failed to send command", err);
191
- throw err;
192
226
  }
193
227
  }, [apiUrl, token]);
228
+ const ensureDrainRunning = useCallback(() => {
229
+ if (drainPromiseRef.current) return;
230
+ const run = () => drainInputQueue().finally(() => {
231
+ if (pendingBatchRef.current.data.length > 0 && sessionIdRef.current) {
232
+ drainPromiseRef.current = run();
233
+ } else {
234
+ drainPromiseRef.current = null;
235
+ }
236
+ });
237
+ drainPromiseRef.current = run();
238
+ }, [drainInputQueue]);
239
+ ensureDrainRunningRef.current = ensureDrainRunning;
240
+ const sendCommand = useCallback((command) => {
241
+ return new Promise((resolve, reject) => {
242
+ if (command.length === 0) {
243
+ resolve();
244
+ return;
245
+ }
246
+ pendingBatchRef.current.data += command;
247
+ pendingBatchRef.current.waiters.push({ resolve, reject });
248
+ ensureDrainRunning();
249
+ });
250
+ }, [ensureDrainRunning]);
194
251
  useEffect(() => {
195
252
  mountedRef.current = true;
196
253
  connect();
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  formatDuration,
3
3
  truncateText
4
- } from "./chunk-HRMUF35V.js";
4
+ } from "./chunk-4CLN43XT.js";
5
5
  import {
6
6
  getToolCategory,
7
7
  getToolDisplayMetadata,
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  InlineThinkingItem,
8
8
  RunGroup
9
- } from "./chunk-QOL4ZB24.js";
9
+ } from "./chunk-MQXABZTB.js";
10
10
  import {
11
11
  ToolCallGroup,
12
12
  ToolCallStep
package/dist/globals.css CHANGED
@@ -1,5 +1,4 @@
1
1
  /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */
2
- @import url("https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500;600&family=Outfit:wght@400;500;600;700&family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&display=swap");
3
2
  @layer properties;
4
3
  :root, [data-sandbox-ui] {
5
4
  --md3-surface: #0a0a14;
@@ -661,6 +660,9 @@
661
660
  .collapse {
662
661
  visibility: collapse;
663
662
  }
663
+ .invisible {
664
+ visibility: hidden;
665
+ }
664
666
  .visible {
665
667
  visibility: visible;
666
668
  }
@@ -1464,6 +1466,9 @@
1464
1466
  .shrink-0 {
1465
1467
  flex-shrink: 0;
1466
1468
  }
1469
+ .grow {
1470
+ flex-grow: 1;
1471
+ }
1467
1472
  .caption-bottom {
1468
1473
  caption-side: bottom;
1469
1474
  }
package/dist/hooks.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as AuthUser, U as UseAuthOptions, a as UseAuthResult, b as UsePtySessionOptions, c as UsePtySessionReturn, d as createAuthFetcher, u as useApiKey, e as useAuth, f as usePtySession } from './use-pty-session-0AOuwXgq.js';
1
+ export { A as AuthUser, S as SandboxMetrics, a as SidecarMetricsPayload, U as UseAuthOptions, b as UseAuthResult, c as UsePtySessionOptions, d as UsePtySessionReturn, e as UseSandboxMetricsOptions, f as UseSandboxMetricsResult, g as createAuthFetcher, u as useApiKey, h as useAuth, i as useLiveTime, j as usePtySession, k as useSandboxMetrics } from './use-sandbox-metrics-B64diPqN.js';
2
2
  export { AgentStreamEvent, AppendUserMessageOptions, ApplySdkEventOptions, AutomationStreamEvent, BeginAssistantMessageOptions, BotStreamEvent, CompleteAssistantMessageOptions, ConnectionState, RealtimeSessionOptions, RealtimeSessionRegistry, RealtimeSessionRegistryProps, RealtimeSessionState, RealtimeSessionTarget, SSEEvent, SdkSessionAttachment, SdkSessionEvent, SdkSessionSeed, SessionInfo, SidecarAuth, TaskStreamEvent, TerminalStreamEvent, UseRunGroupsOptions, UseSSEStreamOptions, UseSSEStreamResult, UseSdkSessionOptions, UseSdkSessionReturn, UseSessionStreamOptions, UseSessionStreamResult, UseSidecarAuthOptions, UseToolCallStreamReturn, useAutoScroll, useDropdownMenu, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useSessionStream, useSidecarAuth, useToolCallStream } from './sdk-hooks.js';
3
3
  import * as _tanstack_react_query from '@tanstack/react-query';
4
4
  import { S as Session } from './sidecar-CFU2W9j1.js';
package/dist/hooks.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  createAuthFetcher,
3
3
  useApiKey,
4
- useAuth
5
- } from "./chunk-S7OXQTST.js";
4
+ useAuth,
5
+ useLiveTime,
6
+ useSandboxMetrics
7
+ } from "./chunk-DPGIXDAI.js";
6
8
  import {
7
9
  usePtySession
8
- } from "./chunk-5UM2XMEJ.js";
10
+ } from "./chunk-ESVYBDGA.js";
9
11
  import {
10
12
  RealtimeSessionRegistry,
11
13
  useDropdownMenu,
@@ -103,12 +105,14 @@ export {
103
105
  useCreateSession,
104
106
  useDeleteSession,
105
107
  useDropdownMenu,
108
+ useLiveTime,
106
109
  usePtySession,
107
110
  useRealtimeSession,
108
111
  useRenameSession,
109
112
  useRunCollapseState,
110
113
  useRunGroups,
111
114
  useSSEStream,
115
+ useSandboxMetrics,
112
116
  useSdkSession,
113
117
  useSessionStream,
114
118
  useSessions,
package/dist/index.d.ts CHANGED
@@ -20,13 +20,13 @@ export { c as BillingDashboard, d as BillingDashboardProps, e as PricingCards, f
20
20
  export { AuthHeader, GitHubLoginButton, LoginLayout, LoginLayoutProps, UserMenu } from './auth.js';
21
21
  export { CodeBlock, CodeBlock as CodeBlockDisplay, CopyButton, Markdown, MarkdownProps } from './markdown.js';
22
22
  export { AppendUserMessageOptions, ApplySdkEventOptions, BeginAssistantMessageOptions, CompleteAssistantMessageOptions, RealtimeSessionOptions, RealtimeSessionRegistry, RealtimeSessionRegistryProps, RealtimeSessionState, RealtimeSessionTarget, SdkSessionAttachment, SdkSessionEvent, SdkSessionSeed, UseSdkSessionOptions, UseSdkSessionReturn, UseToolCallStreamReturn, useAutoScroll, useDropdownMenu, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useSessionStream, useSidecarAuth, useToolCallStream } from './sdk-hooks.js';
23
- export { d as createAuthFetcher, u as useApiKey, e as useAuth, f as usePtySession } from './use-pty-session-0AOuwXgq.js';
23
+ export { S as SandboxMetrics, a as SidecarMetricsPayload, e as UseSandboxMetricsOptions, f as UseSandboxMetricsResult, g as createAuthFetcher, u as useApiKey, h as useAuth, i as useLiveTime, j as usePtySession, k as useSandboxMetrics } from './use-sandbox-metrics-B64diPqN.js';
24
24
  export { G as GroupedMessage, R as Run, b as RunStats, T as ToolCategory } from './run-CtFZ6s-D.js';
25
25
  export { S as Session } from './sidecar-CFU2W9j1.js';
26
26
  export { C as CustomToolRenderer, D as DisplayVariant, T as ToolDisplayMetadata } from './tool-display-Ct9nFAzJ.js';
27
27
  export { A as AgentBranding } from './branding-DCi5VEik.js';
28
28
  export { A as ActiveProjectActivity, a as ActiveSessionActivityOptions, b as ActiveSessionConnectionOptions, c as ActiveSessionConnectionState, d as ActiveSessionReconnectState, e as ActiveSessionRecord, f as ActiveSessionStatus, g as ActiveSessionTransportMode, h as ActiveSessionsState, R as RegisterActiveSessionOptions, S as SessionProjectKey } from './active-sessions-store-CeOmXgv5.js';
29
- export { TOOL_CATEGORY_ICONS, cn, copyText, formatDuration, getToolCategory, getToolDisplayMetadata, getToolErrorText, timeAgo, truncateText } from './utils.js';
29
+ export { TOOL_CATEGORY_ICONS, cn, copyText, formatBytes, formatDuration, formatUptime, getToolCategory, getToolDisplayMetadata, getToolErrorText, timeAgo, truncateText } from './utils.js';
30
30
  import 'class-variance-authority/types';
31
31
  import '@radix-ui/react-dialog';
32
32
  import 'class-variance-authority';
package/dist/index.js CHANGED
@@ -25,11 +25,13 @@ import {
25
25
  import {
26
26
  createAuthFetcher,
27
27
  useApiKey,
28
- useAuth
29
- } from "./chunk-S7OXQTST.js";
28
+ useAuth,
29
+ useLiveTime,
30
+ useSandboxMetrics
31
+ } from "./chunk-DPGIXDAI.js";
30
32
  import {
31
33
  usePtySession
32
- } from "./chunk-5UM2XMEJ.js";
34
+ } from "./chunk-ESVYBDGA.js";
33
35
  import {
34
36
  RealtimeSessionRegistry,
35
37
  useDropdownMenu,
@@ -104,7 +106,7 @@ import {
104
106
  TaskBoard,
105
107
  TerminalPanel,
106
108
  WorkspaceLayout
107
- } from "./chunk-HEXQVHXJ.js";
109
+ } from "./chunk-5OQ27N57.js";
108
110
  import "./chunk-OEX7NZE3.js";
109
111
  import {
110
112
  EmptyState,
@@ -118,7 +120,7 @@ import {
118
120
  MessageList,
119
121
  ThinkingIndicator,
120
122
  UserMessage
121
- } from "./chunk-WKSGQVLI.js";
123
+ } from "./chunk-XLG757B6.js";
122
124
  import {
123
125
  useAutoScroll,
124
126
  useRunCollapseState,
@@ -136,11 +138,13 @@ import {
136
138
  RunGroup,
137
139
  WebSearchPreview,
138
140
  WriteFilePreview
139
- } from "./chunk-QOL4ZB24.js";
141
+ } from "./chunk-MQXABZTB.js";
140
142
  import {
143
+ formatBytes,
141
144
  formatDuration,
145
+ formatUptime,
142
146
  truncateText
143
- } from "./chunk-HRMUF35V.js";
147
+ } from "./chunk-4CLN43XT.js";
144
148
  import {
145
149
  ToolCallGroup,
146
150
  ToolCallStep
@@ -530,7 +534,9 @@ export {
530
534
  copyText,
531
535
  createAuthFetcher,
532
536
  filterFileTree,
537
+ formatBytes,
533
538
  formatDuration,
539
+ formatUptime,
534
540
  getToolCategory,
535
541
  getToolDisplayMetadata,
536
542
  getToolErrorText,
@@ -547,11 +553,13 @@ export {
547
553
  useDropdownMenu,
548
554
  useEditorConnection,
549
555
  useEditorContext,
556
+ useLiveTime,
550
557
  usePtySession,
551
558
  useRealtimeSession,
552
559
  useRunCollapseState,
553
560
  useRunGroups,
554
561
  useSSEStream,
562
+ useSandboxMetrics,
555
563
  useSdkSession,
556
564
  useSessionStream,
557
565
  useSidebar,
package/dist/pages.js CHANGED
@@ -502,21 +502,32 @@ function ProvisioningWizard({
502
502
  const ramStep = alignSliderStep(RAM_MIN, ramMax, RAM_STEP);
503
503
  const storageStep = alignSliderStep(STORAGE_MIN, storageMax, STORAGE_STEP);
504
504
  const dc = defaultConfig;
505
- const [envList, setEnvList] = React2.useState(
506
- environmentsProp ?? defaultEnvironments
505
+ const [envList, setEnvList] = React2.useState(() => {
506
+ if (environmentsProp) return environmentsProp;
507
+ if (onLoadEnvironments) return [];
508
+ return defaultEnvironments;
509
+ });
510
+ const [isLoadingEnvironments, setIsLoadingEnvironments] = React2.useState(
511
+ () => !environmentsProp && !!onLoadEnvironments
507
512
  );
508
513
  const onLoadEnvironmentsRef = React2.useRef(onLoadEnvironments);
509
514
  onLoadEnvironmentsRef.current = onLoadEnvironments;
510
515
  React2.useEffect(() => {
511
516
  let cancelled = false;
512
517
  if (onLoadEnvironmentsRef.current) {
518
+ setIsLoadingEnvironments(true);
513
519
  onLoadEnvironmentsRef.current().then((entries) => {
514
- if (!cancelled) setEnvList(entries.map(resolveEnvironment));
520
+ if (!cancelled) {
521
+ setEnvList(entries.map(resolveEnvironment));
522
+ setIsLoadingEnvironments(false);
523
+ }
515
524
  }).catch((err) => {
516
- if (!cancelled)
525
+ if (!cancelled) {
517
526
  setLoadError(
518
527
  err instanceof Error ? err.message : "Failed to load environments"
519
528
  );
529
+ setIsLoadingEnvironments(false);
530
+ }
520
531
  });
521
532
  } else if (environmentsProp) {
522
533
  setEnvList(environmentsProp);
@@ -531,9 +542,15 @@ function ProvisioningWizard({
531
542
  effectiveDefault ?? environments[0]?.id ?? ""
532
543
  );
533
544
  React2.useEffect(() => {
545
+ if (envList.length === 0) return;
534
546
  if (effectiveDefault && envList.some((e) => e.id === effectiveDefault)) {
535
547
  setSelectedEnv(effectiveDefault);
548
+ return;
536
549
  }
550
+ setSelectedEnv((prev) => {
551
+ if (prev && envList.some((e) => e.id === prev)) return prev;
552
+ return envList[0]?.id ?? "";
553
+ });
537
554
  }, [envList, effectiveDefault]);
538
555
  const [cpuCores, setCpuCores] = React2.useState(
539
556
  snapSliderValue(dc?.cpuCores ?? 4, CPU_MIN, cpuMax, cpuStep)
@@ -797,7 +814,23 @@ function ProvisioningWizard({
797
814
  /* @__PURE__ */ jsx2("div", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-primary/10 border border-primary/20 text-primary", children: /* @__PURE__ */ jsx2(Layers, { className: "h-5 w-5" }) }),
798
815
  /* @__PURE__ */ jsx2("h2", { className: "text-lg font-bold text-foreground tracking-tight", children: "Environment Selection" })
799
816
  ] }),
800
- /* @__PURE__ */ jsx2("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-3", children: environments.map((env) => /* @__PURE__ */ jsxs2(
817
+ /* @__PURE__ */ jsx2("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-3", children: isLoadingEnvironments && environments.length === 0 ? Array.from({ length: 3 }).map((_, i) => /* @__PURE__ */ jsxs2(
818
+ "div",
819
+ {
820
+ className: "p-4 rounded-[16px] border border-border bg-card/50 animate-pulse",
821
+ "aria-hidden": "true",
822
+ children: [
823
+ /* @__PURE__ */ jsxs2("div", { className: "flex justify-between items-start mb-3", children: [
824
+ /* @__PURE__ */ jsx2("div", { className: "w-10 h-10 rounded-full bg-muted/60 border border-border" }),
825
+ /* @__PURE__ */ jsx2("div", { className: "w-5 h-5 rounded-full border-2 border-border" })
826
+ ] }),
827
+ /* @__PURE__ */ jsx2("div", { className: "h-3 w-1/3 rounded bg-muted/60 mb-2" }),
828
+ /* @__PURE__ */ jsx2("div", { className: "h-2.5 w-5/6 rounded bg-muted/50 mb-1.5" }),
829
+ /* @__PURE__ */ jsx2("div", { className: "h-2.5 w-2/3 rounded bg-muted/50" })
830
+ ]
831
+ },
832
+ `env-skeleton-${i}`
833
+ )) : environments.map((env) => /* @__PURE__ */ jsxs2(
801
834
  "button",
802
835
  {
803
836
  type: "button",
package/dist/run.js CHANGED
@@ -8,8 +8,8 @@ import {
8
8
  InlineToolItem,
9
9
  LiveDuration,
10
10
  RunGroup
11
- } from "./chunk-QOL4ZB24.js";
12
- import "./chunk-HRMUF35V.js";
11
+ } from "./chunk-MQXABZTB.js";
12
+ import "./chunk-4CLN43XT.js";
13
13
  import {
14
14
  ToolCallGroup,
15
15
  ToolCallStep
package/dist/styles.css CHANGED
@@ -1,5 +1,4 @@
1
1
  /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */
2
- @import url("https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500;600&family=Outfit:wght@400;500;600;700&family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&display=swap");
3
2
  @layer properties;
4
3
  :root, [data-sandbox-ui] {
5
4
  --md3-surface: #0a0a14;
@@ -661,6 +660,9 @@
661
660
  .collapse {
662
661
  visibility: collapse;
663
662
  }
663
+ .invisible {
664
+ visibility: hidden;
665
+ }
664
666
  .visible {
665
667
  visibility: visible;
666
668
  }
@@ -1464,6 +1466,9 @@
1464
1466
  .shrink-0 {
1465
1467
  flex-shrink: 0;
1466
1468
  }
1469
+ .grow {
1470
+ flex-grow: 1;
1471
+ }
1467
1472
  .caption-bottom {
1468
1473
  caption-side: bottom;
1469
1474
  }
package/dist/terminal.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  usePtySession
3
- } from "./chunk-5UM2XMEJ.js";
3
+ } from "./chunk-ESVYBDGA.js";
4
4
 
5
5
  // src/terminal/terminal-view.tsx
6
6
  import "@xterm/xterm/css/xterm.css";
@@ -0,0 +1,141 @@
1
+ interface AuthUser {
2
+ customer_id: string;
3
+ email: string;
4
+ name?: string;
5
+ tier: string;
6
+ github?: {
7
+ login: string;
8
+ connected: boolean;
9
+ } | null;
10
+ session_expires_at?: string;
11
+ }
12
+ interface UseAuthOptions {
13
+ apiBaseUrl: string;
14
+ revalidateOnFocus?: boolean;
15
+ shouldRetryOnError?: boolean;
16
+ }
17
+ interface UseAuthResult {
18
+ user: AuthUser | null;
19
+ isLoading: boolean;
20
+ isError: boolean;
21
+ error: Error | null;
22
+ mutate: () => Promise<void>;
23
+ }
24
+ /**
25
+ * Hook for managing authentication state.
26
+ * Fetches user session from the API and provides loading/error states.
27
+ */
28
+ declare function useAuth({ apiBaseUrl, revalidateOnFocus, shouldRetryOnError, }: UseAuthOptions): UseAuthResult;
29
+ /**
30
+ * Creates a fetcher function that includes auth credentials.
31
+ * Uses both cookie-based session and localStorage API key for backwards compatibility.
32
+ */
33
+ declare function createAuthFetcher(_apiBaseUrl: string): <T = unknown>(url: string, options?: RequestInit) => Promise<T>;
34
+ /**
35
+ * Hook to get the API key from localStorage.
36
+ * For backwards compatibility with API key-based auth.
37
+ */
38
+ declare function useApiKey(): string | null;
39
+
40
+ interface UsePtySessionOptions {
41
+ /** Base URL of the sidecar (e.g. "http://localhost:9100"). */
42
+ apiUrl: string;
43
+ /** Bearer token for authentication. */
44
+ token: string;
45
+ /** Called with raw PTY output (may contain ANSI escape codes). */
46
+ onData: (data: string) => void;
47
+ }
48
+ interface UsePtySessionReturn {
49
+ /** Whether the SSE stream is connected and receiving data. */
50
+ isConnected: boolean;
51
+ /** Connection or API error, if any. */
52
+ error: string | null;
53
+ /** Send a command to the PTY session. */
54
+ sendCommand: (command: string) => Promise<void>;
55
+ /** Safely resize the remote PTY. */
56
+ resizeTerminal: (cols: number, rows: number) => Promise<void>;
57
+ /** Tear down and reconnect. */
58
+ reconnect: () => void;
59
+ }
60
+ /**
61
+ * Manages a PTY session against the sidecar terminal API.
62
+ *
63
+ * Protocol:
64
+ * - POST /terminals → create session → { data: { sessionId } }
65
+ * - GET /terminals/{id}/stream → SSE output (raw PTY with ANSI codes)
66
+ * - POST /terminals/{id}/input → send input { data: "..." }
67
+ * - DELETE /terminals/{id} → close session
68
+ */
69
+ declare function usePtySession({ apiUrl, token, onData }: UsePtySessionOptions): UsePtySessionReturn;
70
+
71
+ /**
72
+ * Returns `Date.now()` and re-renders every `intervalMs` so derived
73
+ * values like "uptime" tick forward without polling upstream data.
74
+ */
75
+ declare function useLiveTime(intervalMs?: number): number;
76
+
77
+ /**
78
+ * Shape returned by the sidecar `/metrics/json` endpoint. Only the
79
+ * fields read by this hook are modeled; the sidecar may add more.
80
+ */
81
+ interface SidecarMetricsPayload {
82
+ process?: {
83
+ memoryBytes?: {
84
+ rss?: number;
85
+ heapTotal?: number;
86
+ heapUsed?: number;
87
+ external?: number;
88
+ arrayBuffers?: number;
89
+ };
90
+ cpuSeconds?: {
91
+ user?: number;
92
+ system?: number;
93
+ };
94
+ };
95
+ }
96
+ interface SandboxMetrics {
97
+ /**
98
+ * CPU% derived from consecutive samples. `null` on the first sample
99
+ * because a delta is required. Can exceed 100 on multi-core hosts.
100
+ */
101
+ cpuPercent: number | null;
102
+ rssBytes: number;
103
+ heapUsedBytes: number;
104
+ heapTotalBytes: number;
105
+ }
106
+ interface UseSandboxMetricsOptions {
107
+ /** Sandbox API base URL, e.g. `https://api.tangle.tools`. */
108
+ apiBaseUrl: string;
109
+ /** Sandbox id; when falsy the hook stays idle. */
110
+ sandboxId?: string | null;
111
+ /**
112
+ * Optional bearer token. When omitted the fetch still sends
113
+ * credentials so a cookie session can authenticate the proxy.
114
+ */
115
+ token?: string | null;
116
+ /** Pause polling when false. Defaults to true. */
117
+ enabled?: boolean;
118
+ /** Poll cadence; clamped to a 500ms floor. Defaults to 3000. */
119
+ intervalMs?: number;
120
+ }
121
+ interface UseSandboxMetricsResult {
122
+ metrics: SandboxMetrics | null;
123
+ /**
124
+ * True only until the first successful sample has arrived (or the
125
+ * first one after the target `sandboxId` changes). Subsequent polls
126
+ * do not flip this back to true, so consumers can gate a spinner
127
+ * on it without it flashing on every cycle.
128
+ */
129
+ loading: boolean;
130
+ error: Error | null;
131
+ /** Wall-clock ms of the last successful sample, or null. */
132
+ lastUpdatedAt: number | null;
133
+ }
134
+ /**
135
+ * Polls the sandbox's sidecar metrics through the API proxy and
136
+ * derives a CPU% value from consecutive cumulative-CPU samples. Used
137
+ * by the sandbox overview dashboard to drive live CPU/memory panels.
138
+ */
139
+ declare function useSandboxMetrics({ apiBaseUrl, sandboxId, token, enabled, intervalMs, }: UseSandboxMetricsOptions): UseSandboxMetricsResult;
140
+
141
+ export { type AuthUser as A, type SandboxMetrics as S, type UseAuthOptions as U, type SidecarMetricsPayload as a, type UseAuthResult as b, type UsePtySessionOptions as c, type UsePtySessionReturn as d, type UseSandboxMetricsOptions as e, type UseSandboxMetricsResult as f, createAuthFetcher as g, useAuth as h, useLiveTime as i, usePtySession as j, useSandboxMetrics as k, useApiKey as u };
package/dist/utils.d.ts CHANGED
@@ -16,6 +16,22 @@ declare function copyText(text: string): Promise<boolean>;
16
16
  declare function formatDuration(ms: number): string;
17
17
  /** Truncate text to `max` characters, appending "..." if truncated. */
18
18
  declare function truncateText(text: string, max: number): string;
19
+ /**
20
+ * Format an uptime duration in milliseconds with progressive
21
+ * granularity, so short-lived sandboxes don't render as "0d 0h".
22
+ * - < 60s → "Ns"
23
+ * - < 60m → "Nm Ss"
24
+ * - < 24h → "Nh Mm"
25
+ * - otherwise → "Nd Hh"
26
+ */
27
+ declare function formatUptime(ms: number): string;
28
+ /**
29
+ * Format a byte count using binary units (KiB/MiB/GiB, surfaced as
30
+ * "KB/MB/GB" for readability). KB and MB use one decimal below 10 and
31
+ * round above; GB keeps two decimals below 10 so half-GB changes stay
32
+ * visible on memory dashboards, and drops to one decimal above.
33
+ */
34
+ declare function formatBytes(bytes: number): string;
19
35
 
20
36
  declare function timeAgo(ts: number): string;
21
37
 
@@ -25,4 +41,4 @@ declare function getToolDisplayMetadata(part: ToolPart): ToolDisplayMetadata;
25
41
  /** Extract error text from a tool part, if any. */
26
42
  declare function getToolErrorText(part: ToolPart, fallback?: string): string | undefined;
27
43
 
28
- export { TOOL_CATEGORY_ICONS, cn, copyText, formatDuration, getToolCategory, getToolDisplayMetadata, getToolErrorText, timeAgo, truncateText };
44
+ export { TOOL_CATEGORY_ICONS, cn, copyText, formatBytes, formatDuration, formatUptime, getToolCategory, getToolDisplayMetadata, getToolErrorText, timeAgo, truncateText };
package/dist/utils.js CHANGED
@@ -3,9 +3,11 @@ import {
3
3
  timeAgo
4
4
  } from "./chunk-QD4QE5P5.js";
5
5
  import {
6
+ formatBytes,
6
7
  formatDuration,
8
+ formatUptime,
7
9
  truncateText
8
- } from "./chunk-HRMUF35V.js";
10
+ } from "./chunk-4CLN43XT.js";
9
11
  import {
10
12
  TOOL_CATEGORY_ICONS,
11
13
  getToolCategory,
@@ -19,7 +21,9 @@ export {
19
21
  TOOL_CATEGORY_ICONS,
20
22
  cn,
21
23
  copyText,
24
+ formatBytes,
22
25
  formatDuration,
26
+ formatUptime,
23
27
  getToolCategory,
24
28
  getToolDisplayMetadata,
25
29
  getToolErrorText,
package/dist/workspace.js CHANGED
@@ -13,13 +13,13 @@ import {
13
13
  TaskBoard,
14
14
  TerminalPanel,
15
15
  WorkspaceLayout
16
- } from "./chunk-HEXQVHXJ.js";
16
+ } from "./chunk-5OQ27N57.js";
17
17
  import "./chunk-OEX7NZE3.js";
18
18
  import "./chunk-MA7YKRUP.js";
19
- import "./chunk-WKSGQVLI.js";
19
+ import "./chunk-XLG757B6.js";
20
20
  import "./chunk-54SQQMMM.js";
21
- import "./chunk-QOL4ZB24.js";
22
- import "./chunk-HRMUF35V.js";
21
+ import "./chunk-MQXABZTB.js";
22
+ import "./chunk-4CLN43XT.js";
23
23
  import "./chunk-MT5FJ3ZT.js";
24
24
  import "./chunk-BX6AQMUS.js";
25
25
  import "./chunk-ZNCEM5CD.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/sandbox-ui",
3
- "version": "0.10.7",
3
+ "version": "0.10.9",
4
4
  "description": "Unified UI component library for Tangle Sandbox — primitives, chat, dashboard, terminal, editor, and workspace components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,19 +0,0 @@
1
- // src/utils/format.ts
2
- function formatDuration(ms) {
3
- if (ms < 1e3) return "<1s";
4
- const seconds = Math.floor(ms / 1e3);
5
- if (seconds < 60) return `${seconds}s`;
6
- const minutes = Math.floor(seconds / 60);
7
- const remaining = seconds % 60;
8
- return remaining > 0 ? `${minutes}m ${remaining}s` : `${minutes}m`;
9
- }
10
- function truncateText(text, max) {
11
- const cleaned = text.replace(/\s+/g, " ").trim();
12
- if (cleaned.length <= max) return cleaned;
13
- return cleaned.slice(0, max).trim() + "...";
14
- }
15
-
16
- export {
17
- formatDuration,
18
- truncateText
19
- };
@@ -1,99 +0,0 @@
1
- // src/hooks/use-auth.ts
2
- import * as React from "react";
3
- function useAuth({
4
- apiBaseUrl,
5
- revalidateOnFocus = false,
6
- shouldRetryOnError = false
7
- }) {
8
- const [user, setUser] = React.useState(null);
9
- const [isLoading, setIsLoading] = React.useState(true);
10
- const [error, setError] = React.useState(null);
11
- const retryTimerRef = React.useRef(null);
12
- const abortRef = React.useRef(null);
13
- const fetchSession = React.useCallback(async () => {
14
- abortRef.current?.abort();
15
- const controller = new AbortController();
16
- abortRef.current = controller;
17
- setIsLoading(true);
18
- setError(null);
19
- try {
20
- const res = await fetch(`${apiBaseUrl}/auth/session`, {
21
- credentials: "include",
22
- signal: controller.signal
23
- });
24
- if (!res.ok) {
25
- throw new Error("Not authenticated");
26
- }
27
- const data = await res.json();
28
- if (data.success && data.data) {
29
- setUser(data.data);
30
- } else {
31
- setUser(null);
32
- }
33
- } catch (err) {
34
- if (err.name === "AbortError") return;
35
- setError(err instanceof Error ? err : new Error("Unknown error"));
36
- setUser(null);
37
- if (shouldRetryOnError) {
38
- if (retryTimerRef.current) clearTimeout(retryTimerRef.current);
39
- retryTimerRef.current = setTimeout(fetchSession, 5e3);
40
- }
41
- } finally {
42
- if (!controller.signal.aborted) {
43
- setIsLoading(false);
44
- }
45
- }
46
- }, [apiBaseUrl, shouldRetryOnError]);
47
- React.useEffect(() => {
48
- fetchSession();
49
- return () => {
50
- abortRef.current?.abort();
51
- if (retryTimerRef.current) clearTimeout(retryTimerRef.current);
52
- };
53
- }, [fetchSession]);
54
- React.useEffect(() => {
55
- if (!revalidateOnFocus) return;
56
- const handleFocus = () => {
57
- fetchSession();
58
- };
59
- window.addEventListener("focus", handleFocus);
60
- return () => window.removeEventListener("focus", handleFocus);
61
- }, [revalidateOnFocus, fetchSession]);
62
- return {
63
- user,
64
- isLoading,
65
- isError: !!error,
66
- error,
67
- mutate: fetchSession
68
- };
69
- }
70
- function createAuthFetcher(_apiBaseUrl) {
71
- return async function authFetcher(url, options) {
72
- const res = await fetch(url, {
73
- ...options,
74
- credentials: "include",
75
- headers: {
76
- ...options?.headers
77
- }
78
- });
79
- if (!res.ok) {
80
- throw new Error(`Request failed with status ${res.status}`);
81
- }
82
- return res.json();
83
- };
84
- }
85
- function useApiKey() {
86
- const [apiKey, setApiKey] = React.useState(null);
87
- React.useEffect(() => {
88
- if (typeof window !== "undefined") {
89
- setApiKey(localStorage.getItem("apiKey"));
90
- }
91
- }, []);
92
- return apiKey;
93
- }
94
-
95
- export {
96
- useAuth,
97
- createAuthFetcher,
98
- useApiKey
99
- };
@@ -1,71 +0,0 @@
1
- interface AuthUser {
2
- customer_id: string;
3
- email: string;
4
- name?: string;
5
- tier: string;
6
- github?: {
7
- login: string;
8
- connected: boolean;
9
- } | null;
10
- session_expires_at?: string;
11
- }
12
- interface UseAuthOptions {
13
- apiBaseUrl: string;
14
- revalidateOnFocus?: boolean;
15
- shouldRetryOnError?: boolean;
16
- }
17
- interface UseAuthResult {
18
- user: AuthUser | null;
19
- isLoading: boolean;
20
- isError: boolean;
21
- error: Error | null;
22
- mutate: () => Promise<void>;
23
- }
24
- /**
25
- * Hook for managing authentication state.
26
- * Fetches user session from the API and provides loading/error states.
27
- */
28
- declare function useAuth({ apiBaseUrl, revalidateOnFocus, shouldRetryOnError, }: UseAuthOptions): UseAuthResult;
29
- /**
30
- * Creates a fetcher function that includes auth credentials.
31
- * Uses both cookie-based session and localStorage API key for backwards compatibility.
32
- */
33
- declare function createAuthFetcher(_apiBaseUrl: string): <T = unknown>(url: string, options?: RequestInit) => Promise<T>;
34
- /**
35
- * Hook to get the API key from localStorage.
36
- * For backwards compatibility with API key-based auth.
37
- */
38
- declare function useApiKey(): string | null;
39
-
40
- interface UsePtySessionOptions {
41
- /** Base URL of the sidecar (e.g. "http://localhost:9100"). */
42
- apiUrl: string;
43
- /** Bearer token for authentication. */
44
- token: string;
45
- /** Called with raw PTY output (may contain ANSI escape codes). */
46
- onData: (data: string) => void;
47
- }
48
- interface UsePtySessionReturn {
49
- /** Whether the SSE stream is connected and receiving data. */
50
- isConnected: boolean;
51
- /** Connection or API error, if any. */
52
- error: string | null;
53
- /** Send a command to the PTY session. */
54
- sendCommand: (command: string) => Promise<void>;
55
- /** Safely resize the remote PTY. */
56
- resizeTerminal: (cols: number, rows: number) => Promise<void>;
57
- /** Tear down and reconnect. */
58
- reconnect: () => void;
59
- }
60
- /**
61
- * Manages a PTY session against the sidecar terminal API.
62
- *
63
- * Protocol:
64
- * - POST /terminals → create session → { data: { sessionId } }
65
- * - GET /terminals/{id}/stream → SSE output (raw PTY with ANSI codes)
66
- * - POST /terminals/{id}/input → send input { data: "..." }
67
- * - DELETE /terminals/{id} → close session
68
- */
69
- declare function usePtySession({ apiUrl, token, onData }: UsePtySessionOptions): UsePtySessionReturn;
70
-
71
- export { type AuthUser as A, type UseAuthOptions as U, type UseAuthResult as a, type UsePtySessionOptions as b, type UsePtySessionReturn as c, createAuthFetcher as d, useAuth as e, usePtySession as f, useApiKey as u };