@tangle-network/sandbox-ui 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/dist/auth.d.ts +57 -0
- package/dist/auth.js +14 -0
- package/dist/branding-DCi5VEik.d.ts +13 -0
- package/dist/button-BidTtuRS.d.ts +15 -0
- package/dist/chat.d.ts +121 -0
- package/dist/chat.js +25 -0
- package/dist/chunk-2UHPE5T7.js +201 -0
- package/dist/chunk-4EIWPJMJ.js +545 -0
- package/dist/chunk-6MQIDUPA.js +502 -0
- package/dist/chunk-B26TQ7SA.js +47 -0
- package/dist/chunk-E6FS7R4X.js +109 -0
- package/dist/chunk-GRYHFH5O.js +110 -0
- package/dist/chunk-HMND7JPA.js +868 -0
- package/dist/chunk-HRMUF35V.js +19 -0
- package/dist/chunk-HYEAX3DC.js +822 -0
- package/dist/chunk-KMXV7DDX.js +174 -0
- package/dist/chunk-KYY2X6LY.js +318 -0
- package/dist/chunk-L6ZDH5F4.js +334 -0
- package/dist/chunk-LTFK464G.js +103 -0
- package/dist/chunk-M34OA6PQ.js +233 -0
- package/dist/chunk-M6VLC32S.js +219 -0
- package/dist/chunk-MCGKDCOR.js +173 -0
- package/dist/chunk-NI2EI43H.js +294 -0
- package/dist/chunk-OU4TRNQZ.js +173 -0
- package/dist/chunk-QD4QE5P5.js +40 -0
- package/dist/chunk-QSQBDR3N.js +180 -0
- package/dist/chunk-RQHJBTEU.js +10 -0
- package/dist/chunk-U62G5TS7.js +472 -0
- package/dist/chunk-ZOL2TR5M.js +475 -0
- package/dist/dashboard.d.ts +111 -0
- package/dist/dashboard.js +26 -0
- package/dist/editor.d.ts +196 -0
- package/dist/editor.js +713 -0
- package/dist/expanded-tool-detail-OkXGqTHe.d.ts +52 -0
- package/dist/files.d.ts +66 -0
- package/dist/files.js +11 -0
- package/dist/hooks.d.ts +22 -0
- package/dist/hooks.js +107 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.js +551 -0
- package/dist/markdown.d.ts +55 -0
- package/dist/markdown.js +17 -0
- package/dist/pages.d.ts +89 -0
- package/dist/pages.js +1181 -0
- package/dist/parts-CyGkM6Fp.d.ts +50 -0
- package/dist/primitives.d.ts +189 -0
- package/dist/primitives.js +161 -0
- package/dist/run-CtFZ6s-D.d.ts +41 -0
- package/dist/run.d.ts +14 -0
- package/dist/run.js +29 -0
- package/dist/sidecar-CFU2W9j1.d.ts +8 -0
- package/dist/stores.d.ts +28 -0
- package/dist/stores.js +49 -0
- package/dist/terminal.d.ts +44 -0
- package/dist/terminal.js +160 -0
- package/dist/tool-call-feed-D5Ume-Pt.d.ts +66 -0
- package/dist/tool-display-BvsVW_Ur.d.ts +32 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +0 -0
- package/dist/usage-chart-DINgSVL5.d.ts +60 -0
- package/dist/use-sidecar-auth-Bb0-w3lX.d.ts +339 -0
- package/dist/utils.d.ts +28 -0
- package/dist/utils.js +28 -0
- package/dist/workspace.d.ts +113 -0
- package/dist/workspace.js +15 -0
- package/package.json +174 -0
- package/src/styles/globals.css +230 -0
- package/src/styles/tokens.css +73 -0
- package/tailwind.config.cjs +99 -0
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseToolEvent
|
|
3
|
+
} from "./chunk-M6VLC32S.js";
|
|
4
|
+
|
|
5
|
+
// src/hooks/use-tool-call-stream.ts
|
|
6
|
+
import { useState, useCallback, useRef } from "react";
|
|
7
|
+
function useToolCallStream() {
|
|
8
|
+
const [segments, setSegments] = useState([]);
|
|
9
|
+
const pendingToolsRef = useRef(/* @__PURE__ */ new Map());
|
|
10
|
+
const lastSegmentKindRef = useRef(null);
|
|
11
|
+
const pushText = useCallback((delta) => {
|
|
12
|
+
setSegments((prev) => {
|
|
13
|
+
const last = prev[prev.length - 1];
|
|
14
|
+
if (last && last.kind === "text") {
|
|
15
|
+
return [...prev.slice(0, -1), { kind: "text", content: last.content + delta }];
|
|
16
|
+
}
|
|
17
|
+
return [...prev, { kind: "text", content: delta }];
|
|
18
|
+
});
|
|
19
|
+
lastSegmentKindRef.current = "text";
|
|
20
|
+
}, []);
|
|
21
|
+
const pushEvent = useCallback((event) => {
|
|
22
|
+
const toolCall = parseToolEvent(event);
|
|
23
|
+
if (!toolCall) return;
|
|
24
|
+
if (event.type === "tool.invocation" || event.type === "tool_use") {
|
|
25
|
+
pendingToolsRef.current.set(toolCall.id, toolCall);
|
|
26
|
+
setSegments((prev) => [
|
|
27
|
+
...prev,
|
|
28
|
+
{ kind: "tool_call", call: { ...toolCall, status: "running" } }
|
|
29
|
+
]);
|
|
30
|
+
lastSegmentKindRef.current = "tool";
|
|
31
|
+
}
|
|
32
|
+
}, []);
|
|
33
|
+
const completeToolCall = useCallback(
|
|
34
|
+
(id, result) => {
|
|
35
|
+
const pending = pendingToolsRef.current.get(id);
|
|
36
|
+
if (pending) {
|
|
37
|
+
pending.status = result.error ? "error" : "success";
|
|
38
|
+
pending.output = result.output || result.error;
|
|
39
|
+
pending.duration = result.duration;
|
|
40
|
+
pendingToolsRef.current.delete(id);
|
|
41
|
+
}
|
|
42
|
+
setSegments(
|
|
43
|
+
(prev) => prev.map((seg) => {
|
|
44
|
+
if (seg.kind === "tool_call" && seg.call.id === id) {
|
|
45
|
+
return {
|
|
46
|
+
kind: "tool_call",
|
|
47
|
+
call: {
|
|
48
|
+
...seg.call,
|
|
49
|
+
status: result.error ? "error" : "success",
|
|
50
|
+
output: result.output || result.error,
|
|
51
|
+
duration: result.duration
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return seg;
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
[]
|
|
60
|
+
);
|
|
61
|
+
const reset = useCallback(() => {
|
|
62
|
+
setSegments([]);
|
|
63
|
+
pendingToolsRef.current.clear();
|
|
64
|
+
lastSegmentKindRef.current = null;
|
|
65
|
+
}, []);
|
|
66
|
+
return { segments, pushEvent, pushText, completeToolCall, reset };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/hooks/use-auth.ts
|
|
70
|
+
import * as React from "react";
|
|
71
|
+
function useAuth({
|
|
72
|
+
apiBaseUrl,
|
|
73
|
+
revalidateOnFocus = false,
|
|
74
|
+
shouldRetryOnError = false
|
|
75
|
+
}) {
|
|
76
|
+
const [user, setUser] = React.useState(null);
|
|
77
|
+
const [isLoading, setIsLoading] = React.useState(true);
|
|
78
|
+
const [error, setError] = React.useState(null);
|
|
79
|
+
const fetchSession = React.useCallback(async () => {
|
|
80
|
+
setIsLoading(true);
|
|
81
|
+
setError(null);
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch(`${apiBaseUrl}/auth/session`, {
|
|
84
|
+
credentials: "include"
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
throw new Error("Not authenticated");
|
|
88
|
+
}
|
|
89
|
+
const data = await res.json();
|
|
90
|
+
if (data.success && data.data) {
|
|
91
|
+
setUser(data.data);
|
|
92
|
+
} else {
|
|
93
|
+
setUser(null);
|
|
94
|
+
}
|
|
95
|
+
} catch (err) {
|
|
96
|
+
setError(err instanceof Error ? err : new Error("Unknown error"));
|
|
97
|
+
setUser(null);
|
|
98
|
+
if (shouldRetryOnError) {
|
|
99
|
+
setTimeout(fetchSession, 5e3);
|
|
100
|
+
}
|
|
101
|
+
} finally {
|
|
102
|
+
setIsLoading(false);
|
|
103
|
+
}
|
|
104
|
+
}, [apiBaseUrl, shouldRetryOnError]);
|
|
105
|
+
React.useEffect(() => {
|
|
106
|
+
fetchSession();
|
|
107
|
+
}, [fetchSession]);
|
|
108
|
+
React.useEffect(() => {
|
|
109
|
+
if (!revalidateOnFocus) return;
|
|
110
|
+
const handleFocus = () => {
|
|
111
|
+
fetchSession();
|
|
112
|
+
};
|
|
113
|
+
window.addEventListener("focus", handleFocus);
|
|
114
|
+
return () => window.removeEventListener("focus", handleFocus);
|
|
115
|
+
}, [revalidateOnFocus, fetchSession]);
|
|
116
|
+
return {
|
|
117
|
+
user,
|
|
118
|
+
isLoading,
|
|
119
|
+
isError: !!error,
|
|
120
|
+
error,
|
|
121
|
+
mutate: fetchSession
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function createAuthFetcher(_apiBaseUrl) {
|
|
125
|
+
return async function authFetcher(url, options) {
|
|
126
|
+
const res = await fetch(url, {
|
|
127
|
+
...options,
|
|
128
|
+
credentials: "include",
|
|
129
|
+
headers: {
|
|
130
|
+
...options?.headers
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
if (!res.ok) {
|
|
134
|
+
throw new Error(`Request failed with status ${res.status}`);
|
|
135
|
+
}
|
|
136
|
+
return res.json();
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function useApiKey() {
|
|
140
|
+
const [apiKey, setApiKey] = React.useState(null);
|
|
141
|
+
React.useEffect(() => {
|
|
142
|
+
if (typeof window !== "undefined") {
|
|
143
|
+
setApiKey(localStorage.getItem("apiKey"));
|
|
144
|
+
}
|
|
145
|
+
}, []);
|
|
146
|
+
return apiKey;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/hooks/use-sse-stream.ts
|
|
150
|
+
import * as React2 from "react";
|
|
151
|
+
function useSSEStream(options) {
|
|
152
|
+
const {
|
|
153
|
+
url,
|
|
154
|
+
authToken,
|
|
155
|
+
autoReconnect = true,
|
|
156
|
+
maxRetries = 5,
|
|
157
|
+
reconnectDelay = 1e3,
|
|
158
|
+
eventTypes,
|
|
159
|
+
onEvent,
|
|
160
|
+
onStateChange,
|
|
161
|
+
onError,
|
|
162
|
+
headers,
|
|
163
|
+
enabled = true
|
|
164
|
+
} = options;
|
|
165
|
+
const [state, setState] = React2.useState("disconnected");
|
|
166
|
+
const [events, setEvents] = React2.useState([]);
|
|
167
|
+
const [lastEvent, setLastEvent] = React2.useState(null);
|
|
168
|
+
const [error, setError] = React2.useState(null);
|
|
169
|
+
const [retryCount, setRetryCount] = React2.useState(0);
|
|
170
|
+
const [lastEventTime, setLastEventTime] = React2.useState(Date.now());
|
|
171
|
+
const [timeSinceLastEvent, setTimeSinceLastEvent] = React2.useState(0);
|
|
172
|
+
const eventSourceRef = React2.useRef(null);
|
|
173
|
+
const abortControllerRef = React2.useRef(null);
|
|
174
|
+
const reconnectTimeoutRef = React2.useRef(null);
|
|
175
|
+
const lastEventIdRef = React2.useRef(void 0);
|
|
176
|
+
React2.useEffect(() => {
|
|
177
|
+
const interval = setInterval(() => {
|
|
178
|
+
setTimeSinceLastEvent(Date.now() - lastEventTime);
|
|
179
|
+
}, 1e3);
|
|
180
|
+
return () => clearInterval(interval);
|
|
181
|
+
}, [lastEventTime]);
|
|
182
|
+
React2.useEffect(() => {
|
|
183
|
+
onStateChange?.(state);
|
|
184
|
+
}, [state, onStateChange]);
|
|
185
|
+
const handleEvent = React2.useCallback(
|
|
186
|
+
(eventType, data, id) => {
|
|
187
|
+
const event = {
|
|
188
|
+
id,
|
|
189
|
+
event: eventType,
|
|
190
|
+
data,
|
|
191
|
+
timestamp: Date.now()
|
|
192
|
+
};
|
|
193
|
+
if (id) {
|
|
194
|
+
lastEventIdRef.current = id;
|
|
195
|
+
}
|
|
196
|
+
setLastEventTime(Date.now());
|
|
197
|
+
setLastEvent(event);
|
|
198
|
+
setEvents((prev) => [...prev, event]);
|
|
199
|
+
onEvent?.(event);
|
|
200
|
+
},
|
|
201
|
+
[onEvent]
|
|
202
|
+
);
|
|
203
|
+
const disconnect = React2.useCallback(() => {
|
|
204
|
+
if (reconnectTimeoutRef.current) {
|
|
205
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
206
|
+
reconnectTimeoutRef.current = null;
|
|
207
|
+
}
|
|
208
|
+
if (abortControllerRef.current) {
|
|
209
|
+
abortControllerRef.current.abort();
|
|
210
|
+
abortControllerRef.current = null;
|
|
211
|
+
}
|
|
212
|
+
if (eventSourceRef.current) {
|
|
213
|
+
eventSourceRef.current.close();
|
|
214
|
+
eventSourceRef.current = null;
|
|
215
|
+
}
|
|
216
|
+
setState("disconnected");
|
|
217
|
+
}, []);
|
|
218
|
+
const connect = React2.useCallback(() => {
|
|
219
|
+
disconnect();
|
|
220
|
+
if (!url || !enabled) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
setState("connecting");
|
|
224
|
+
setError(null);
|
|
225
|
+
const connectUrl = new URL(url, window.location.origin);
|
|
226
|
+
if (lastEventIdRef.current) {
|
|
227
|
+
connectUrl.searchParams.set("lastEventId", lastEventIdRef.current);
|
|
228
|
+
}
|
|
229
|
+
if (authToken || headers) {
|
|
230
|
+
abortControllerRef.current = new AbortController();
|
|
231
|
+
const fetchHeaders = {
|
|
232
|
+
Accept: "text/event-stream",
|
|
233
|
+
"Cache-Control": "no-cache",
|
|
234
|
+
...headers
|
|
235
|
+
};
|
|
236
|
+
if (authToken) {
|
|
237
|
+
fetchHeaders.Authorization = authToken.startsWith("Bearer ") ? authToken : `Bearer ${authToken}`;
|
|
238
|
+
}
|
|
239
|
+
if (lastEventIdRef.current) {
|
|
240
|
+
fetchHeaders["Last-Event-ID"] = lastEventIdRef.current;
|
|
241
|
+
}
|
|
242
|
+
fetch(connectUrl.toString(), {
|
|
243
|
+
headers: fetchHeaders,
|
|
244
|
+
signal: abortControllerRef.current.signal
|
|
245
|
+
}).then(async (response) => {
|
|
246
|
+
if (!response.ok) {
|
|
247
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
248
|
+
}
|
|
249
|
+
if (!response.body) {
|
|
250
|
+
throw new Error("Response body is null");
|
|
251
|
+
}
|
|
252
|
+
setState("connected");
|
|
253
|
+
setRetryCount(0);
|
|
254
|
+
const reader = response.body.getReader();
|
|
255
|
+
const decoder = new TextDecoder();
|
|
256
|
+
let buffer = "";
|
|
257
|
+
while (true) {
|
|
258
|
+
const { done, value } = await reader.read();
|
|
259
|
+
if (done) {
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
buffer += decoder.decode(value, { stream: true });
|
|
263
|
+
const lines = buffer.split("\n");
|
|
264
|
+
buffer = lines.pop() || "";
|
|
265
|
+
let currentEvent = "";
|
|
266
|
+
let currentData = "";
|
|
267
|
+
let currentId;
|
|
268
|
+
for (const line of lines) {
|
|
269
|
+
if (line.startsWith("event:")) {
|
|
270
|
+
currentEvent = line.slice(6).trim();
|
|
271
|
+
} else if (line.startsWith("data:")) {
|
|
272
|
+
currentData += (currentData ? "\n" : "") + line.slice(5).trim();
|
|
273
|
+
} else if (line.startsWith("id:")) {
|
|
274
|
+
currentId = line.slice(3).trim();
|
|
275
|
+
} else if (line === "" && currentData) {
|
|
276
|
+
try {
|
|
277
|
+
const parsedData = JSON.parse(currentData);
|
|
278
|
+
const eventType = currentEvent || "message";
|
|
279
|
+
if (!eventTypes || eventTypes.includes(eventType)) {
|
|
280
|
+
handleEvent(eventType, parsedData, currentId);
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
if (!eventTypes || eventTypes.includes(currentEvent || "message")) {
|
|
284
|
+
handleEvent(
|
|
285
|
+
currentEvent || "message",
|
|
286
|
+
currentData,
|
|
287
|
+
currentId
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
currentEvent = "";
|
|
292
|
+
currentData = "";
|
|
293
|
+
currentId = void 0;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (autoReconnect && enabled) {
|
|
298
|
+
setState("reconnecting");
|
|
299
|
+
const delay = reconnectDelay * 2 ** retryCount;
|
|
300
|
+
reconnectTimeoutRef.current = setTimeout(
|
|
301
|
+
() => {
|
|
302
|
+
setRetryCount((c) => c + 1);
|
|
303
|
+
connect();
|
|
304
|
+
},
|
|
305
|
+
Math.min(delay, 3e4)
|
|
306
|
+
);
|
|
307
|
+
} else {
|
|
308
|
+
setState("disconnected");
|
|
309
|
+
}
|
|
310
|
+
}).catch((err) => {
|
|
311
|
+
if (err.name === "AbortError") {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
setError(err);
|
|
315
|
+
onError?.(err);
|
|
316
|
+
setState("error");
|
|
317
|
+
if (autoReconnect && enabled && retryCount < maxRetries) {
|
|
318
|
+
setState("reconnecting");
|
|
319
|
+
const delay = reconnectDelay * 2 ** retryCount;
|
|
320
|
+
reconnectTimeoutRef.current = setTimeout(
|
|
321
|
+
() => {
|
|
322
|
+
setRetryCount((c) => c + 1);
|
|
323
|
+
connect();
|
|
324
|
+
},
|
|
325
|
+
Math.min(delay, 3e4)
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
} else {
|
|
330
|
+
const es = new EventSource(connectUrl.toString());
|
|
331
|
+
eventSourceRef.current = es;
|
|
332
|
+
es.onopen = () => {
|
|
333
|
+
setState("connected");
|
|
334
|
+
setRetryCount(0);
|
|
335
|
+
};
|
|
336
|
+
es.onerror = () => {
|
|
337
|
+
const err = new Error("EventSource connection error");
|
|
338
|
+
setError(err);
|
|
339
|
+
onError?.(err);
|
|
340
|
+
if (autoReconnect && enabled && retryCount < maxRetries) {
|
|
341
|
+
setState("reconnecting");
|
|
342
|
+
es.close();
|
|
343
|
+
const delay = reconnectDelay * 2 ** retryCount;
|
|
344
|
+
reconnectTimeoutRef.current = setTimeout(
|
|
345
|
+
() => {
|
|
346
|
+
setRetryCount((c) => c + 1);
|
|
347
|
+
connect();
|
|
348
|
+
},
|
|
349
|
+
Math.min(delay, 3e4)
|
|
350
|
+
);
|
|
351
|
+
} else {
|
|
352
|
+
setState("error");
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
const types = eventTypes || ["message"];
|
|
356
|
+
for (const type of types) {
|
|
357
|
+
es.addEventListener(type, (e) => {
|
|
358
|
+
try {
|
|
359
|
+
const data = JSON.parse(e.data);
|
|
360
|
+
handleEvent(type, data, e.lastEventId);
|
|
361
|
+
} catch {
|
|
362
|
+
handleEvent(type, e.data, e.lastEventId);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
if (!eventTypes) {
|
|
367
|
+
es.onmessage = (e) => {
|
|
368
|
+
try {
|
|
369
|
+
const data = JSON.parse(e.data);
|
|
370
|
+
handleEvent("message", data, e.lastEventId);
|
|
371
|
+
} catch {
|
|
372
|
+
handleEvent("message", e.data, e.lastEventId);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}, [
|
|
378
|
+
url,
|
|
379
|
+
authToken,
|
|
380
|
+
headers,
|
|
381
|
+
enabled,
|
|
382
|
+
autoReconnect,
|
|
383
|
+
maxRetries,
|
|
384
|
+
reconnectDelay,
|
|
385
|
+
retryCount,
|
|
386
|
+
eventTypes,
|
|
387
|
+
handleEvent,
|
|
388
|
+
onError,
|
|
389
|
+
disconnect
|
|
390
|
+
]);
|
|
391
|
+
React2.useEffect(() => {
|
|
392
|
+
if (enabled) {
|
|
393
|
+
connect();
|
|
394
|
+
}
|
|
395
|
+
return () => disconnect();
|
|
396
|
+
}, [enabled, connect, disconnect]);
|
|
397
|
+
const clearEvents = React2.useCallback(() => {
|
|
398
|
+
setEvents([]);
|
|
399
|
+
setLastEvent(null);
|
|
400
|
+
}, []);
|
|
401
|
+
return {
|
|
402
|
+
state,
|
|
403
|
+
events,
|
|
404
|
+
lastEvent,
|
|
405
|
+
error,
|
|
406
|
+
connect,
|
|
407
|
+
disconnect,
|
|
408
|
+
clearEvents,
|
|
409
|
+
retryCount,
|
|
410
|
+
timeSinceLastEvent
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/hooks/use-session-stream.ts
|
|
415
|
+
import { useCallback as useCallback4, useEffect as useEffect3, useRef as useRef3, useState as useState4 } from "react";
|
|
416
|
+
var _insertionCounter = 0;
|
|
417
|
+
function mapApiMessage(msg) {
|
|
418
|
+
const created = msg.info.timestamp ? new Date(msg.info.timestamp).getTime() : Date.now();
|
|
419
|
+
const message = {
|
|
420
|
+
id: msg.info.id,
|
|
421
|
+
role: msg.info.role,
|
|
422
|
+
time: { created },
|
|
423
|
+
_insertionIndex: _insertionCounter++
|
|
424
|
+
};
|
|
425
|
+
const parts = (msg.parts ?? []).map((p, i) => {
|
|
426
|
+
if (p.type === "tool" && p.tool) {
|
|
427
|
+
return {
|
|
428
|
+
type: "tool",
|
|
429
|
+
id: p.id ?? `${msg.info.id}-tool-${i}`,
|
|
430
|
+
tool: p.tool,
|
|
431
|
+
state: {
|
|
432
|
+
status: p.state?.status ?? "completed",
|
|
433
|
+
input: p.state?.input,
|
|
434
|
+
output: p.state?.output,
|
|
435
|
+
error: p.state?.error,
|
|
436
|
+
metadata: p.state?.metadata,
|
|
437
|
+
time: p.time
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
if (p.type === "reasoning") {
|
|
442
|
+
return {
|
|
443
|
+
type: "reasoning",
|
|
444
|
+
text: p.text ?? "",
|
|
445
|
+
time: p.time
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
return { type: "text", text: p.text ?? "" };
|
|
449
|
+
});
|
|
450
|
+
return { message, parts };
|
|
451
|
+
}
|
|
452
|
+
async function fetchJson(url, token, init) {
|
|
453
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
454
|
+
if (init?.body) headers["Content-Type"] = "application/json";
|
|
455
|
+
const res = await fetch(url, { ...init, headers: { ...headers, ...init?.headers } });
|
|
456
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
457
|
+
return res.json();
|
|
458
|
+
}
|
|
459
|
+
function useSessionStream({
|
|
460
|
+
apiUrl,
|
|
461
|
+
token,
|
|
462
|
+
sessionId,
|
|
463
|
+
enabled = true
|
|
464
|
+
}) {
|
|
465
|
+
const [messages, setMessages] = useState4([]);
|
|
466
|
+
const [partMap, setPartMap] = useState4({});
|
|
467
|
+
const [isStreaming, setIsStreaming] = useState4(false);
|
|
468
|
+
const [error, setError] = useState4(null);
|
|
469
|
+
const [connected, setConnected] = useState4(false);
|
|
470
|
+
const abortRef = useRef3(null);
|
|
471
|
+
const streamingMsgIdRef = useRef3(null);
|
|
472
|
+
const refetch = useCallback4(async () => {
|
|
473
|
+
if (!token || !sessionId || !apiUrl) return;
|
|
474
|
+
try {
|
|
475
|
+
const url = `${apiUrl}/session/sessions/${encodeURIComponent(sessionId)}/messages?limit=200`;
|
|
476
|
+
const data = await fetchJson(url, token);
|
|
477
|
+
const apiMessages = Array.isArray(data) ? data : data.messages ?? [];
|
|
478
|
+
const newMessages = [];
|
|
479
|
+
const newPartMap = {};
|
|
480
|
+
for (const apiMsg of apiMessages) {
|
|
481
|
+
const { message, parts } = mapApiMessage(apiMsg);
|
|
482
|
+
newMessages.push(message);
|
|
483
|
+
newPartMap[message.id] = parts;
|
|
484
|
+
}
|
|
485
|
+
setMessages(newMessages);
|
|
486
|
+
setPartMap(newPartMap);
|
|
487
|
+
streamingMsgIdRef.current = null;
|
|
488
|
+
} catch (err) {
|
|
489
|
+
const msg = err instanceof Error ? err.message : "Failed to fetch messages";
|
|
490
|
+
setError(msg);
|
|
491
|
+
}
|
|
492
|
+
}, [apiUrl, token, sessionId]);
|
|
493
|
+
const connectSSE = useCallback4(async () => {
|
|
494
|
+
if (!token || !sessionId || !apiUrl || !enabled) return;
|
|
495
|
+
abortRef.current?.abort();
|
|
496
|
+
const controller = new AbortController();
|
|
497
|
+
abortRef.current = controller;
|
|
498
|
+
try {
|
|
499
|
+
const url = `${apiUrl}/session/events?sessionId=${encodeURIComponent(sessionId)}`;
|
|
500
|
+
const res = await fetch(url, {
|
|
501
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
502
|
+
signal: controller.signal
|
|
503
|
+
});
|
|
504
|
+
if (!res.ok) throw new Error(`SSE connection failed: ${res.status}`);
|
|
505
|
+
setConnected(true);
|
|
506
|
+
setError(null);
|
|
507
|
+
const reader = res.body?.getReader();
|
|
508
|
+
if (!reader) throw new Error("No response body");
|
|
509
|
+
const decoder = new TextDecoder();
|
|
510
|
+
let buffer = "";
|
|
511
|
+
while (true) {
|
|
512
|
+
const { done, value } = await reader.read();
|
|
513
|
+
if (done) break;
|
|
514
|
+
buffer += decoder.decode(value, { stream: true });
|
|
515
|
+
const frames = buffer.split("\n\n");
|
|
516
|
+
buffer = frames.pop() ?? "";
|
|
517
|
+
for (const frame of frames) {
|
|
518
|
+
if (!frame.trim()) continue;
|
|
519
|
+
let eventType = "message";
|
|
520
|
+
const dataLines = [];
|
|
521
|
+
for (const line of frame.split("\n")) {
|
|
522
|
+
if (line.startsWith("event:")) {
|
|
523
|
+
eventType = line.slice(6).trim();
|
|
524
|
+
} else if (line.startsWith("data:")) {
|
|
525
|
+
dataLines.push(line.slice(5).trim());
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (dataLines.length === 0) continue;
|
|
529
|
+
let parsed;
|
|
530
|
+
try {
|
|
531
|
+
parsed = JSON.parse(dataLines.join("\n"));
|
|
532
|
+
} catch {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
handleSSEEvent(eventType, parsed);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
} catch (err) {
|
|
539
|
+
if (err.name === "AbortError") return;
|
|
540
|
+
const msg = err instanceof Error ? err.message : "SSE connection error";
|
|
541
|
+
setError(msg);
|
|
542
|
+
setConnected(false);
|
|
543
|
+
if (!controller.signal.aborted) {
|
|
544
|
+
setTimeout(() => connectSSE(), 3e3);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}, [apiUrl, token, sessionId, enabled]);
|
|
548
|
+
const handleSSEEvent = useCallback4((type, props) => {
|
|
549
|
+
if (type === "message.updated") {
|
|
550
|
+
const id = props.id ?? props.messageId ?? "";
|
|
551
|
+
const role = props.role ?? "assistant";
|
|
552
|
+
if (!id) return;
|
|
553
|
+
setMessages((prev) => {
|
|
554
|
+
const exists = prev.some((m) => m.id === id);
|
|
555
|
+
if (exists) return prev;
|
|
556
|
+
return [
|
|
557
|
+
...prev,
|
|
558
|
+
{
|
|
559
|
+
id,
|
|
560
|
+
role,
|
|
561
|
+
time: { created: Date.now() },
|
|
562
|
+
_insertionIndex: _insertionCounter++
|
|
563
|
+
}
|
|
564
|
+
];
|
|
565
|
+
});
|
|
566
|
+
if (role === "assistant") {
|
|
567
|
+
streamingMsgIdRef.current = id;
|
|
568
|
+
setIsStreaming(true);
|
|
569
|
+
}
|
|
570
|
+
} else if (type === "message.part.updated") {
|
|
571
|
+
const msgId = streamingMsgIdRef.current;
|
|
572
|
+
if (!msgId) return;
|
|
573
|
+
const partType = props.type ?? "text";
|
|
574
|
+
setIsStreaming(true);
|
|
575
|
+
setPartMap((prev) => {
|
|
576
|
+
const existing = prev[msgId] ?? [];
|
|
577
|
+
const updated = [...existing];
|
|
578
|
+
if (partType === "text") {
|
|
579
|
+
const text = props.text ?? props.content ?? "";
|
|
580
|
+
const idx = updated.findIndex((p) => p.type === "text");
|
|
581
|
+
const textPart = { type: "text", text };
|
|
582
|
+
if (idx >= 0) {
|
|
583
|
+
updated[idx] = textPart;
|
|
584
|
+
} else {
|
|
585
|
+
updated.push(textPart);
|
|
586
|
+
}
|
|
587
|
+
} else if (partType === "tool") {
|
|
588
|
+
const toolId = props.id ?? props.toolId ?? `tool-${Date.now()}`;
|
|
589
|
+
const toolName = props.tool ?? props.name ?? "unknown";
|
|
590
|
+
const state = props.state ?? { status: "running" };
|
|
591
|
+
const toolPart = {
|
|
592
|
+
type: "tool",
|
|
593
|
+
id: toolId,
|
|
594
|
+
tool: toolName,
|
|
595
|
+
state: {
|
|
596
|
+
status: state.status ?? "running",
|
|
597
|
+
input: state.input,
|
|
598
|
+
output: state.output,
|
|
599
|
+
error: state.error,
|
|
600
|
+
metadata: state.metadata,
|
|
601
|
+
time: state.time
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
const idx = updated.findIndex((p) => p.type === "tool" && p.id === toolId);
|
|
605
|
+
if (idx >= 0) {
|
|
606
|
+
updated[idx] = toolPart;
|
|
607
|
+
} else {
|
|
608
|
+
updated.push(toolPart);
|
|
609
|
+
}
|
|
610
|
+
} else if (partType === "reasoning") {
|
|
611
|
+
const text = props.text ?? "";
|
|
612
|
+
const idx = updated.findIndex((p) => p.type === "reasoning");
|
|
613
|
+
const reasoningPart = { type: "reasoning", text };
|
|
614
|
+
if (idx >= 0) {
|
|
615
|
+
updated[idx] = reasoningPart;
|
|
616
|
+
} else {
|
|
617
|
+
updated.push(reasoningPart);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return { ...prev, [msgId]: updated };
|
|
621
|
+
});
|
|
622
|
+
} else if (type === "session.idle") {
|
|
623
|
+
setIsStreaming(false);
|
|
624
|
+
streamingMsgIdRef.current = null;
|
|
625
|
+
refetch();
|
|
626
|
+
} else if (type === "session.error") {
|
|
627
|
+
setIsStreaming(false);
|
|
628
|
+
streamingMsgIdRef.current = null;
|
|
629
|
+
const errorMsg = props.error ?? props.message ?? "Agent error";
|
|
630
|
+
setError(errorMsg);
|
|
631
|
+
refetch();
|
|
632
|
+
}
|
|
633
|
+
}, [refetch]);
|
|
634
|
+
const send = useCallback4(async (text) => {
|
|
635
|
+
if (!token || !sessionId || !apiUrl) return;
|
|
636
|
+
try {
|
|
637
|
+
const url = `${apiUrl}/session/sessions/${encodeURIComponent(sessionId)}/messages`;
|
|
638
|
+
await fetchJson(url, token, {
|
|
639
|
+
method: "POST",
|
|
640
|
+
body: JSON.stringify({ parts: [{ type: "text", text }] })
|
|
641
|
+
});
|
|
642
|
+
setIsStreaming(true);
|
|
643
|
+
} catch (err) {
|
|
644
|
+
const msg = err instanceof Error ? err.message : "Failed to send message";
|
|
645
|
+
setError(msg);
|
|
646
|
+
}
|
|
647
|
+
}, [apiUrl, token, sessionId]);
|
|
648
|
+
const abort = useCallback4(async () => {
|
|
649
|
+
if (!token || !sessionId || !apiUrl) return;
|
|
650
|
+
try {
|
|
651
|
+
const url = `${apiUrl}/session/sessions/${encodeURIComponent(sessionId)}/abort`;
|
|
652
|
+
await fetchJson(url, token, { method: "POST" });
|
|
653
|
+
} catch (err) {
|
|
654
|
+
const msg = err instanceof Error ? err.message : "Failed to abort";
|
|
655
|
+
setError(msg);
|
|
656
|
+
}
|
|
657
|
+
}, [apiUrl, token, sessionId]);
|
|
658
|
+
useEffect3(() => {
|
|
659
|
+
if (!enabled || !token || !sessionId) return;
|
|
660
|
+
refetch();
|
|
661
|
+
connectSSE();
|
|
662
|
+
return () => {
|
|
663
|
+
abortRef.current?.abort();
|
|
664
|
+
setConnected(false);
|
|
665
|
+
};
|
|
666
|
+
}, [enabled, token, sessionId, refetch, connectSSE]);
|
|
667
|
+
return { messages, partMap, isStreaming, send, abort, refetch, error, connected };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/hooks/use-dropdown-menu.ts
|
|
671
|
+
import { useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
|
|
672
|
+
function useDropdownMenu(options) {
|
|
673
|
+
const closeOnEsc = options?.closeOnEsc ?? true;
|
|
674
|
+
const [open, setOpen] = useState5(false);
|
|
675
|
+
const ref = useRef4(null);
|
|
676
|
+
useEffect4(() => {
|
|
677
|
+
function handleClick(e) {
|
|
678
|
+
if (ref.current && !ref.current.contains(e.target)) {
|
|
679
|
+
setOpen(false);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (open) {
|
|
683
|
+
document.addEventListener("mousedown", handleClick);
|
|
684
|
+
}
|
|
685
|
+
return () => document.removeEventListener("mousedown", handleClick);
|
|
686
|
+
}, [open]);
|
|
687
|
+
useEffect4(() => {
|
|
688
|
+
if (!open || !closeOnEsc) return;
|
|
689
|
+
function handleKey(e) {
|
|
690
|
+
if (e.key === "Escape") setOpen(false);
|
|
691
|
+
}
|
|
692
|
+
document.addEventListener("keydown", handleKey);
|
|
693
|
+
return () => document.removeEventListener("keydown", handleKey);
|
|
694
|
+
}, [open, closeOnEsc]);
|
|
695
|
+
return {
|
|
696
|
+
open,
|
|
697
|
+
setOpen,
|
|
698
|
+
ref,
|
|
699
|
+
toggle: () => setOpen((prev) => !prev),
|
|
700
|
+
close: () => setOpen(false)
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/hooks/use-sidecar-auth.ts
|
|
705
|
+
import { useState as useState6, useCallback as useCallback5, useEffect as useEffect5, useRef as useRef5 } from "react";
|
|
706
|
+
function storageKey(resourceId, apiUrl) {
|
|
707
|
+
return `sidecar_session_${resourceId}__${apiUrl}`;
|
|
708
|
+
}
|
|
709
|
+
function loadSession(resourceId, apiUrl) {
|
|
710
|
+
if (typeof window === "undefined") return null;
|
|
711
|
+
try {
|
|
712
|
+
const raw = localStorage.getItem(storageKey(resourceId, apiUrl));
|
|
713
|
+
if (!raw) return null;
|
|
714
|
+
const data = JSON.parse(raw);
|
|
715
|
+
if (data.expiresAt * 1e3 - Date.now() < 6e4) {
|
|
716
|
+
localStorage.removeItem(storageKey(resourceId, apiUrl));
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
return data;
|
|
720
|
+
} catch {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function saveSession(resourceId, apiUrl, token, expiresAt) {
|
|
725
|
+
if (typeof window === "undefined") return;
|
|
726
|
+
try {
|
|
727
|
+
localStorage.setItem(storageKey(resourceId, apiUrl), JSON.stringify({ token, expiresAt }));
|
|
728
|
+
} catch {
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function clearSession(resourceId, apiUrl) {
|
|
732
|
+
if (typeof window === "undefined") return;
|
|
733
|
+
localStorage.removeItem(storageKey(resourceId, apiUrl));
|
|
734
|
+
}
|
|
735
|
+
function useSidecarAuth({ resourceId, apiUrl, signMessage }) {
|
|
736
|
+
const cached = loadSession(resourceId, apiUrl);
|
|
737
|
+
const [token, setToken] = useState6(cached?.token ?? null);
|
|
738
|
+
const [expiresAt, setExpiresAt] = useState6(cached?.expiresAt ?? 0);
|
|
739
|
+
const [isAuthenticating, setIsAuthenticating] = useState6(false);
|
|
740
|
+
const [error, setError] = useState6(null);
|
|
741
|
+
const refreshTimerRef = useRef5(void 0);
|
|
742
|
+
const clearCachedToken = useCallback5(() => {
|
|
743
|
+
setToken(null);
|
|
744
|
+
setExpiresAt(0);
|
|
745
|
+
clearSession(resourceId, apiUrl);
|
|
746
|
+
}, [resourceId, apiUrl]);
|
|
747
|
+
const authenticate = useCallback5(async () => {
|
|
748
|
+
if (!apiUrl) return null;
|
|
749
|
+
setIsAuthenticating(true);
|
|
750
|
+
setError(null);
|
|
751
|
+
try {
|
|
752
|
+
const challengeRes = await fetch(`${apiUrl}/api/auth/challenge`, {
|
|
753
|
+
method: "POST"
|
|
754
|
+
});
|
|
755
|
+
if (!challengeRes.ok) {
|
|
756
|
+
throw new Error(`Challenge failed: ${challengeRes.status}`);
|
|
757
|
+
}
|
|
758
|
+
const { nonce, message } = await challengeRes.json();
|
|
759
|
+
const signature = await signMessage(message);
|
|
760
|
+
const sessionRes = await fetch(`${apiUrl}/api/auth/session`, {
|
|
761
|
+
method: "POST",
|
|
762
|
+
headers: { "Content-Type": "application/json" },
|
|
763
|
+
body: JSON.stringify({ nonce, signature })
|
|
764
|
+
});
|
|
765
|
+
if (!sessionRes.ok) {
|
|
766
|
+
const text = await sessionRes.text();
|
|
767
|
+
throw new Error(text || `Session exchange failed: ${sessionRes.status}`);
|
|
768
|
+
}
|
|
769
|
+
const { token: newToken, expires_at } = await sessionRes.json();
|
|
770
|
+
setToken(newToken);
|
|
771
|
+
setExpiresAt(expires_at);
|
|
772
|
+
saveSession(resourceId, apiUrl, newToken, expires_at);
|
|
773
|
+
return newToken;
|
|
774
|
+
} catch (err) {
|
|
775
|
+
setError(err instanceof Error ? err.message : "Authentication failed");
|
|
776
|
+
clearCachedToken();
|
|
777
|
+
return null;
|
|
778
|
+
} finally {
|
|
779
|
+
setIsAuthenticating(false);
|
|
780
|
+
}
|
|
781
|
+
}, [resourceId, apiUrl, signMessage, clearCachedToken]);
|
|
782
|
+
useEffect5(() => {
|
|
783
|
+
if (refreshTimerRef.current) {
|
|
784
|
+
clearTimeout(refreshTimerRef.current);
|
|
785
|
+
}
|
|
786
|
+
if (!token || !expiresAt) return;
|
|
787
|
+
const msUntilRefresh = (expiresAt - 300) * 1e3 - Date.now();
|
|
788
|
+
if (msUntilRefresh <= 0) {
|
|
789
|
+
clearCachedToken();
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
refreshTimerRef.current = setTimeout(() => {
|
|
793
|
+
authenticate().catch(() => {
|
|
794
|
+
clearCachedToken();
|
|
795
|
+
});
|
|
796
|
+
}, msUntilRefresh);
|
|
797
|
+
return () => {
|
|
798
|
+
if (refreshTimerRef.current) {
|
|
799
|
+
clearTimeout(refreshTimerRef.current);
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
}, [token, expiresAt, authenticate, clearCachedToken]);
|
|
803
|
+
return {
|
|
804
|
+
token,
|
|
805
|
+
isAuthenticated: token !== null,
|
|
806
|
+
isAuthenticating,
|
|
807
|
+
authenticate,
|
|
808
|
+
clearCachedToken,
|
|
809
|
+
error
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export {
|
|
814
|
+
useToolCallStream,
|
|
815
|
+
useAuth,
|
|
816
|
+
createAuthFetcher,
|
|
817
|
+
useApiKey,
|
|
818
|
+
useSSEStream,
|
|
819
|
+
useSessionStream,
|
|
820
|
+
useDropdownMenu,
|
|
821
|
+
useSidecarAuth
|
|
822
|
+
};
|