@tangle-network/sandbox-ui 0.113.7 → 0.114.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/WorkflowGraph-B-NsdYz_.js +1961 -0
- package/dist/assets.d.ts +214 -206
- package/dist/assets.js +1057 -908
- package/dist/auth-CWe17Hly.js +37 -0
- package/dist/auth.d.ts +2 -30
- package/dist/auth.js +2 -15
- package/dist/chat-BOvFAe6p.js +903 -0
- package/dist/chat.d.ts +2 -230
- package/dist/chat.js +2 -34
- package/dist/connectors.d.ts +52 -49
- package/dist/connectors.js +595 -643
- package/dist/dashboard-CZ3VgNPR.js +4969 -0
- package/dist/dashboard.d.ts +4 -1297
- package/dist/dashboard.js +4 -152
- package/dist/editor.d.ts +2 -1
- package/dist/editor.js +2 -31
- package/dist/files.d.ts +2 -6
- package/dist/files.js +2 -33
- package/dist/globals.css +63 -3
- package/dist/hooks.d.ts +146 -148
- package/dist/hooks.js +222 -55
- package/dist/index-CKRaj1pM.d.ts +776 -0
- package/dist/index-CXX5IpH2.d.ts +2 -0
- package/dist/index-DPePq7c8.d.ts +1340 -0
- package/dist/index-DiyLLZuJ.d.ts +230 -0
- package/dist/index-DvmZLMT1.d.ts +30 -0
- package/dist/index-I8ZLVtHd.d.ts +330 -0
- package/dist/index-PRM636Ex.d.ts +86 -0
- package/dist/index.d.ts +21 -33
- package/dist/index.js +21 -701
- package/dist/info-panel-C9cqDzOL.js +594 -0
- package/dist/integrations.d.ts +133 -141
- package/dist/integrations.js +491 -673
- package/dist/markdown.d.ts +2 -1
- package/dist/markdown.js +2 -13
- package/dist/model-I-NIxeM-.js +1250 -0
- package/dist/model-brand-B9A4H9pJ.d.ts +98 -0
- package/dist/model-brand-hMyDTUqB.js +624 -0
- package/dist/motion-DkyBRJb9.js +37 -0
- package/dist/openui.d.ts +2 -1
- package/dist/openui.js +2 -7
- package/dist/pages.d.ts +307 -300
- package/dist/pages.js +4404 -4181
- package/dist/primitives-Dh1vG6J3.js +113 -0
- package/dist/primitives.d.ts +2 -74
- package/dist/primitives.js +2 -204
- package/dist/provider-logo-DjqmConF.js +311 -0
- package/dist/run.d.ts +2 -1
- package/dist/run.js +2 -23
- package/dist/sdk-hooks.d.ts +3 -4
- package/dist/sdk-hooks.js +3 -31
- package/dist/sidecar-DgH_vQ6m.d.ts +9 -0
- package/dist/stores.d.ts +14 -15
- package/dist/stores.js +13 -81
- package/dist/styles.css +63 -3
- package/dist/template-card-L-4gmjx2.d.ts +90 -0
- package/dist/terminal-CN9ZgHRt.js +197 -0
- package/dist/terminal.d.ts +60 -60
- package/dist/terminal.js +2 -10
- package/dist/tokens.css +51 -0
- package/dist/types.d.ts +4 -11
- package/dist/types.js +0 -1
- package/dist/use-pty-session-C8cE0adT.js +669 -0
- package/dist/use-sidecar-auth-CCTJcrq7.d.ts +145 -0
- package/dist/use-sidecar-auth-DkdcufOz.js +742 -0
- package/dist/utils-DzWbktWG.js +2 -0
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +2 -37
- package/dist/workbench-BCaCKl-J.js +8347 -0
- package/dist/workbench.d.ts +2 -310
- package/dist/workbench.js +2 -44
- package/dist/workflows.d.ts +341 -339
- package/dist/workflows.js +74 -109
- package/dist/workspace-B6AtI6ld.js +2510 -0
- package/dist/workspace.d.ts +2 -761
- package/dist/workspace.js +2 -51
- package/package.json +46 -40
- package/dist/WorkflowGraph-2QOORYIR.js +0 -1722
- package/dist/chunk-2BUPSB7O.js +0 -0
- package/dist/chunk-3KBNPQQF.js +0 -2697
- package/dist/chunk-3LIXVEEE.js +0 -279
- package/dist/chunk-72UF2H5H.js +0 -707
- package/dist/chunk-AI25G5MX.js +0 -944
- package/dist/chunk-ANSC5AFC.js +0 -314
- package/dist/chunk-AZ3AWMTM.js +0 -8
- package/dist/chunk-BGVEHENC.js +0 -10
- package/dist/chunk-CZ7CSFO4.js +0 -39
- package/dist/chunk-DYXXYGS4.js +0 -4700
- package/dist/chunk-EI44GEQ5.js +0 -6
- package/dist/chunk-IACRDXER.js +0 -219
- package/dist/chunk-IOB2PW5Z.js +0 -55
- package/dist/chunk-M4K7ZGR2.js +0 -24
- package/dist/chunk-MYZAW4RX.js +0 -665
- package/dist/chunk-OMUIPJQZ.js +0 -555
- package/dist/chunk-RQZRXKMR.js +0 -398
- package/dist/chunk-SOKKTB7W.js +0 -230
- package/dist/chunk-TADGS4Q7.js +0 -1010
- package/dist/chunk-UCQNOUN3.js +0 -7964
- package/dist/chunk-ULPLRBMF.js +0 -34
- package/dist/chunk-WID73FPH.js +0 -89
- package/dist/chunk-X3YXP7FG.js +0 -38
- package/dist/logo-DiAjq759.d.ts +0 -15
- package/dist/model-brand-BNnaUKKZ.d.ts +0 -99
- package/dist/ports-list-CDIvTNnX.d.ts +0 -17
- package/dist/sdk-hooks-Djkjq4cp.d.ts +0 -145
- package/dist/template-card-Bn-Bw5xC.d.ts +0 -87
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { createParser } from "eventsource-parser";
|
|
3
|
+
//#region src/hooks/wire-encoding.ts
|
|
4
|
+
/** btoa handles only latin-1; widen each UTF-8 byte into its own char first. */
|
|
5
|
+
function utf8ToLatin1(text) {
|
|
6
|
+
return encodeURIComponent(text).replace(/%([0-9A-F]{2})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
|
|
7
|
+
}
|
|
8
|
+
function encodeTextForWire(text) {
|
|
9
|
+
if (typeof btoa === "function") return btoa(utf8ToLatin1(text));
|
|
10
|
+
const bufferCtor = globalThis.Buffer;
|
|
11
|
+
if (bufferCtor) return bufferCtor.from(text, "utf8").toString("base64");
|
|
12
|
+
throw new Error("encodeTextForWire: no base64 encoder available (btoa and Buffer both undefined)");
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/hooks/use-session-stream.ts
|
|
16
|
+
var SessionStreamError = class extends Error {
|
|
17
|
+
code;
|
|
18
|
+
status;
|
|
19
|
+
constructor(code, message, status) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "SessionStreamError";
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.status = status;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const SESSION_STREAM_RECONNECT_DELAY_MS = 1e3;
|
|
27
|
+
function mapApiMessage(msg, counterRef) {
|
|
28
|
+
const created = msg.info.timestamp ? new Date(msg.info.timestamp).getTime() : Date.now();
|
|
29
|
+
return {
|
|
30
|
+
message: {
|
|
31
|
+
id: msg.info.id,
|
|
32
|
+
role: msg.info.role,
|
|
33
|
+
time: { created },
|
|
34
|
+
_insertionIndex: counterRef.current++
|
|
35
|
+
},
|
|
36
|
+
parts: (msg.parts ?? []).map((p, i) => {
|
|
37
|
+
if (p.type === "tool" && p.tool) return {
|
|
38
|
+
type: "tool",
|
|
39
|
+
id: p.id ?? `${msg.info.id}-tool-${i}`,
|
|
40
|
+
tool: p.tool,
|
|
41
|
+
state: {
|
|
42
|
+
status: p.state?.status ?? "completed",
|
|
43
|
+
input: p.state?.input,
|
|
44
|
+
output: p.state?.output,
|
|
45
|
+
error: p.state?.error,
|
|
46
|
+
metadata: p.state?.metadata,
|
|
47
|
+
time: p.time
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
if (p.type === "reasoning") return {
|
|
51
|
+
type: "reasoning",
|
|
52
|
+
text: p.text ?? "",
|
|
53
|
+
time: p.time
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
type: "text",
|
|
57
|
+
text: p.text ?? ""
|
|
58
|
+
};
|
|
59
|
+
})
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function readErrorDetails(value) {
|
|
63
|
+
if (typeof value === "string") return { message: value };
|
|
64
|
+
if (!value || typeof value !== "object") return {};
|
|
65
|
+
const record = value;
|
|
66
|
+
const nested = readErrorDetails(record.error);
|
|
67
|
+
return {
|
|
68
|
+
code: (typeof record.code === "string" ? record.code : void 0) ?? nested.code,
|
|
69
|
+
message: (typeof record.message === "string" ? record.message : void 0) ?? nested.message
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function formatError(details, fallback) {
|
|
73
|
+
const message = details.message ?? fallback;
|
|
74
|
+
return details.code && !message.includes(details.code) ? `${message} (${details.code})` : message;
|
|
75
|
+
}
|
|
76
|
+
async function responseError(res, prefix) {
|
|
77
|
+
let details = {};
|
|
78
|
+
try {
|
|
79
|
+
details = readErrorDetails(await res.json());
|
|
80
|
+
} catch {}
|
|
81
|
+
const code = details.code ?? `HTTP_${res.status}`;
|
|
82
|
+
const fallback = `${prefix}: HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ""}`;
|
|
83
|
+
return new SessionStreamError(code, formatError(details, fallback), res.status);
|
|
84
|
+
}
|
|
85
|
+
async function fetchJson(url, token, init) {
|
|
86
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
87
|
+
if (init?.body) headers["Content-Type"] = "application/json";
|
|
88
|
+
const res = await fetch(url, {
|
|
89
|
+
...init,
|
|
90
|
+
headers: {
|
|
91
|
+
...headers,
|
|
92
|
+
...init?.headers
|
|
93
|
+
},
|
|
94
|
+
credentials: "include"
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok) throw await responseError(res, "Request failed");
|
|
97
|
+
return res.json();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Parse a Hub degradation payload.
|
|
101
|
+
*
|
|
102
|
+
* ONE rule for both halves of the same state: the `hubConnectionsDegraded`
|
|
103
|
+
* field of a session status response, and the properties of a live
|
|
104
|
+
* `hub.connections.degraded` frame. A live notice and the one that survives a
|
|
105
|
+
* reload describe the same thing, so a payload either path rejects must be one
|
|
106
|
+
* the other rejects too — otherwise a status read parses to `null` and erases
|
|
107
|
+
* a notice the frame just raised.
|
|
108
|
+
*
|
|
109
|
+
* `message` is required. It is the only field a user reads, the producer's
|
|
110
|
+
* schema makes it mandatory, and inventing prose for a payload that omits it
|
|
111
|
+
* would assert a cause the sidecar never gave: `reason` is a free string, so a
|
|
112
|
+
* generic "started with no integrations attached" can contradict it. A
|
|
113
|
+
* partially-understood degradation is worse than none.
|
|
114
|
+
*
|
|
115
|
+
* Returns `null` for anything unreadable. What that MEANS is the caller's to
|
|
116
|
+
* decide — an absent status field is health, an unreadable one is no evidence
|
|
117
|
+
* either way.
|
|
118
|
+
*/
|
|
119
|
+
function readDegradation(raw) {
|
|
120
|
+
if (!raw || typeof raw !== "object") return null;
|
|
121
|
+
const record = raw;
|
|
122
|
+
if (typeof record.message !== "string") return null;
|
|
123
|
+
return {
|
|
124
|
+
kind: "hub-connections",
|
|
125
|
+
reason: typeof record.reason === "string" ? record.reason : "unknown",
|
|
126
|
+
message: record.message,
|
|
127
|
+
requestShape: typeof record.requestShape === "string" ? record.requestShape : "unknown"
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Record that a live Hub frame has been applied for `sessionId`. */
|
|
131
|
+
function bumpHubFrameEpoch(epochs, sessionId) {
|
|
132
|
+
epochs.set(sessionId, (epochs.get(sessionId) ?? 0) + 1);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Set or clear one session's entry, returning the SAME map when nothing changes.
|
|
136
|
+
*
|
|
137
|
+
* Identity stability matters: this map is read during render, so a new
|
|
138
|
+
* reference on every no-op frame would re-render every consumer of the hook for
|
|
139
|
+
* a session whose state did not move.
|
|
140
|
+
*/
|
|
141
|
+
function withDegradation(prev, sessionId, value) {
|
|
142
|
+
const current = prev.get(sessionId) ?? null;
|
|
143
|
+
if (value === null) {
|
|
144
|
+
if (current === null) return prev;
|
|
145
|
+
const next = new Map(prev);
|
|
146
|
+
next.delete(sessionId);
|
|
147
|
+
return next;
|
|
148
|
+
}
|
|
149
|
+
if (current && current.reason === value.reason && current.message === value.message && current.requestShape === value.requestShape) return prev;
|
|
150
|
+
return new Map(prev).set(sessionId, value);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Streams chat messages from a sidecar session gateway via SSE.
|
|
154
|
+
*
|
|
155
|
+
* Fetches existing message history on mount, then subscribes to SSE for
|
|
156
|
+
* real-time updates. Maps the sidecar's message format to agent-ui's
|
|
157
|
+
* SessionMessage + partMap format.
|
|
158
|
+
*/
|
|
159
|
+
function useSessionStream({ apiUrl, token, sessionId, enabled = true }) {
|
|
160
|
+
const [messages, setMessages] = useState([]);
|
|
161
|
+
const [partsState, setPartsState] = useState({
|
|
162
|
+
values: {},
|
|
163
|
+
sourceIds: {}
|
|
164
|
+
});
|
|
165
|
+
const partMap = partsState.values;
|
|
166
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
167
|
+
const [error, setError] = useState(null);
|
|
168
|
+
const [connected, setConnected] = useState(false);
|
|
169
|
+
const [degradationBySession, setDegradationBySession] = useState(() => /* @__PURE__ */ new Map());
|
|
170
|
+
const degradation = degradationBySession.get(sessionId) ?? null;
|
|
171
|
+
const abortRef = useRef(null);
|
|
172
|
+
const reconnectTimerRef = useRef(null);
|
|
173
|
+
const activeAssistantMessageIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
174
|
+
const completedExecutionIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
175
|
+
const lastEventIdRef = useRef("");
|
|
176
|
+
const hubFrameEpochRef = useRef(/* @__PURE__ */ new Map());
|
|
177
|
+
const seedGenerationRef = useRef(/* @__PURE__ */ new Map());
|
|
178
|
+
const executionScopedStreamRef = useRef(false);
|
|
179
|
+
const insertionCounterRef = useRef(0);
|
|
180
|
+
const echoCounterRef = useRef(0);
|
|
181
|
+
const pendingEchoesRef = useRef(/* @__PURE__ */ new Map());
|
|
182
|
+
const handleSSEEventRef = useRef(null);
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
pendingEchoesRef.current.clear();
|
|
185
|
+
activeAssistantMessageIdsRef.current.clear();
|
|
186
|
+
completedExecutionIdsRef.current.clear();
|
|
187
|
+
lastEventIdRef.current = "";
|
|
188
|
+
}, [sessionId]);
|
|
189
|
+
const refetch = useCallback(async () => {
|
|
190
|
+
if (!token || !sessionId || !apiUrl) return;
|
|
191
|
+
try {
|
|
192
|
+
const data = await fetchJson(`${apiUrl}/session/sessions/${encodeURIComponent(sessionId)}/messages?limit=200`, token);
|
|
193
|
+
const apiMessages = Array.isArray(data) ? data : data.messages ?? [];
|
|
194
|
+
const newMessages = [];
|
|
195
|
+
const newPartMap = {};
|
|
196
|
+
for (const apiMsg of apiMessages) {
|
|
197
|
+
const { message, parts } = mapApiMessage(apiMsg, insertionCounterRef);
|
|
198
|
+
newMessages.push(message);
|
|
199
|
+
newPartMap[message.id] = parts;
|
|
200
|
+
}
|
|
201
|
+
const canonicalIds = new Set(newMessages.map((message) => message.id));
|
|
202
|
+
const echoes = [...pendingEchoesRef.current.entries()].filter(([, echo]) => !echo.canonicalMessageId || !canonicalIds.has(echo.canonicalMessageId)).map(([, echo]) => echo);
|
|
203
|
+
for (const [echoId, echo] of pendingEchoesRef.current) if (echo.canonicalMessageId && canonicalIds.has(echo.canonicalMessageId)) pendingEchoesRef.current.delete(echoId);
|
|
204
|
+
setMessages([...newMessages, ...echoes.map((echo) => echo.message)]);
|
|
205
|
+
setPartsState({
|
|
206
|
+
values: {
|
|
207
|
+
...newPartMap,
|
|
208
|
+
...Object.fromEntries(echoes.map((echo) => [echo.message.id, echo.parts]))
|
|
209
|
+
},
|
|
210
|
+
sourceIds: {}
|
|
211
|
+
});
|
|
212
|
+
} catch (err) {
|
|
213
|
+
const msg = err instanceof Error ? err.message : "Failed to fetch messages";
|
|
214
|
+
setError(msg);
|
|
215
|
+
}
|
|
216
|
+
}, [
|
|
217
|
+
apiUrl,
|
|
218
|
+
token,
|
|
219
|
+
sessionId
|
|
220
|
+
]);
|
|
221
|
+
/**
|
|
222
|
+
* Read the session's degradation back from session status.
|
|
223
|
+
*
|
|
224
|
+
* The sidecar's event bus is live-only and never replays, so the
|
|
225
|
+
* `hub.connections.degraded` frame is gone for a client that reloads or
|
|
226
|
+
* attaches after spawn — while the toolbelt stays empty. Status is the
|
|
227
|
+
* durable half of the same state, and it is also what retires the notice
|
|
228
|
+
* after a credential refresh.
|
|
229
|
+
*
|
|
230
|
+
* Failures are swallowed: this is supplementary information, and surfacing a
|
|
231
|
+
* status fetch error as a session error would be worse than not showing the
|
|
232
|
+
* notice. `refetch` already reports a broken session gateway.
|
|
233
|
+
*/
|
|
234
|
+
const seedDegradation = useCallback(async () => {
|
|
235
|
+
if (!token || !sessionId || !apiUrl) return;
|
|
236
|
+
const epochAtRequest = hubFrameEpochRef.current.get(sessionId) ?? 0;
|
|
237
|
+
const generation = (seedGenerationRef.current.get(sessionId) ?? 0) + 1;
|
|
238
|
+
seedGenerationRef.current.set(sessionId, generation);
|
|
239
|
+
let seeded;
|
|
240
|
+
try {
|
|
241
|
+
const field = (await fetchJson(`${apiUrl}/session/sessions/${encodeURIComponent(sessionId)}`, token)).hubConnectionsDegraded;
|
|
242
|
+
if (field === void 0 || field === null) seeded = null;
|
|
243
|
+
else {
|
|
244
|
+
seeded = readDegradation(field);
|
|
245
|
+
if (!seeded) return;
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if ((hubFrameEpochRef.current.get(sessionId) ?? 0) !== epochAtRequest) return;
|
|
251
|
+
if (seedGenerationRef.current.get(sessionId) !== generation) return;
|
|
252
|
+
setDegradationBySession((prev) => withDegradation(prev, sessionId, seeded));
|
|
253
|
+
}, [
|
|
254
|
+
apiUrl,
|
|
255
|
+
token,
|
|
256
|
+
sessionId
|
|
257
|
+
]);
|
|
258
|
+
const connectSSE = useCallback(async () => {
|
|
259
|
+
if (!token || !sessionId || !apiUrl || !enabled) return;
|
|
260
|
+
if (reconnectTimerRef.current) {
|
|
261
|
+
clearTimeout(reconnectTimerRef.current);
|
|
262
|
+
reconnectTimerRef.current = null;
|
|
263
|
+
}
|
|
264
|
+
abortRef.current?.abort();
|
|
265
|
+
const controller = new AbortController();
|
|
266
|
+
abortRef.current = controller;
|
|
267
|
+
try {
|
|
268
|
+
const eventQuery = new URLSearchParams({ sessionId });
|
|
269
|
+
const activeExecutionIds = [...activeAssistantMessageIdsRef.current];
|
|
270
|
+
executionScopedStreamRef.current = activeExecutionIds.length > 0;
|
|
271
|
+
if (activeExecutionIds.length === 1) eventQuery.set("executionId", activeExecutionIds[0]);
|
|
272
|
+
else if (activeExecutionIds.length > 1) eventQuery.set("replayExecutionIds", activeExecutionIds.join(","));
|
|
273
|
+
if (lastEventIdRef.current) eventQuery.set("since", lastEventIdRef.current);
|
|
274
|
+
const url = `${apiUrl}/session/events?${eventQuery.toString()}`;
|
|
275
|
+
const res = await fetch(url, {
|
|
276
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
277
|
+
signal: controller.signal,
|
|
278
|
+
credentials: "include"
|
|
279
|
+
});
|
|
280
|
+
if (!res.ok) throw await responseError(res, "Live response connection failed");
|
|
281
|
+
setConnected(true);
|
|
282
|
+
setError(null);
|
|
283
|
+
seedDegradation();
|
|
284
|
+
const reader = res.body?.getReader();
|
|
285
|
+
if (!reader) throw new Error("No response body");
|
|
286
|
+
const decoder = new TextDecoder();
|
|
287
|
+
let parserFailure = null;
|
|
288
|
+
const parser = createParser({
|
|
289
|
+
onEvent(event) {
|
|
290
|
+
if (event.id) lastEventIdRef.current = event.id;
|
|
291
|
+
if (!event.data) return;
|
|
292
|
+
try {
|
|
293
|
+
const parsed = JSON.parse(event.data);
|
|
294
|
+
handleSSEEventRef.current?.(event.event ?? "message", parsed);
|
|
295
|
+
} catch {
|
|
296
|
+
parserFailure = new SessionStreamError("INVALID_SESSION_EVENT", "The live response stream sent malformed data.");
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
onError(parseError) {
|
|
300
|
+
parserFailure = new SessionStreamError("INVALID_EVENT_STREAM", `The live response stream could not be parsed: ${parseError.message}`);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
while (true) {
|
|
304
|
+
const { done, value } = await reader.read();
|
|
305
|
+
if (done) break;
|
|
306
|
+
parser.feed(decoder.decode(value, { stream: true }));
|
|
307
|
+
if (parserFailure) throw parserFailure;
|
|
308
|
+
}
|
|
309
|
+
const tail = decoder.decode();
|
|
310
|
+
if (tail) parser.feed(tail);
|
|
311
|
+
parser.reset({ consume: true });
|
|
312
|
+
if (parserFailure) throw parserFailure;
|
|
313
|
+
throw new SessionStreamError("SESSION_STREAM_CLOSED", "The live response stream closed before the session ended. Reconnecting…");
|
|
314
|
+
} catch (err) {
|
|
315
|
+
if (err.name === "AbortError") return;
|
|
316
|
+
const msg = err instanceof Error ? err.message : "SSE connection error";
|
|
317
|
+
setError(msg);
|
|
318
|
+
setConnected(false);
|
|
319
|
+
if (!controller.signal.aborted) reconnectTimerRef.current = setTimeout(() => connectSSE(), SESSION_STREAM_RECONNECT_DELAY_MS);
|
|
320
|
+
}
|
|
321
|
+
}, [
|
|
322
|
+
apiUrl,
|
|
323
|
+
token,
|
|
324
|
+
sessionId,
|
|
325
|
+
enabled
|
|
326
|
+
]);
|
|
327
|
+
handleSSEEventRef.current = useCallback((type, raw) => {
|
|
328
|
+
const envelope = raw?.properties;
|
|
329
|
+
const props = envelope?.info ?? envelope?.part ?? envelope ?? raw;
|
|
330
|
+
const associatePendingEcho = (executionId) => {
|
|
331
|
+
for (const echo of pendingEchoesRef.current.values()) if (!echo.executionId) {
|
|
332
|
+
echo.executionId = executionId;
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
const retireExecution = (executionId) => {
|
|
337
|
+
if (!executionId) {
|
|
338
|
+
const activeExecutionIds = [...activeAssistantMessageIdsRef.current];
|
|
339
|
+
if (activeExecutionIds.length === 1) return retireExecution(activeExecutionIds[0]);
|
|
340
|
+
if (activeExecutionIds.length > 1) return true;
|
|
341
|
+
pendingEchoesRef.current.clear();
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
completedExecutionIdsRef.current.add(executionId);
|
|
345
|
+
activeAssistantMessageIdsRef.current.delete(executionId);
|
|
346
|
+
for (const [echoId, echo] of pendingEchoesRef.current) if (echo.executionId === executionId || !echo.executionId && pendingEchoesRef.current.size === 1) pendingEchoesRef.current.delete(echoId);
|
|
347
|
+
return activeAssistantMessageIdsRef.current.size > 0;
|
|
348
|
+
};
|
|
349
|
+
if (type === "message.updated") {
|
|
350
|
+
const id = props.id ?? props.messageId ?? "";
|
|
351
|
+
const role = props.role ?? "assistant";
|
|
352
|
+
if (!id) return;
|
|
353
|
+
setMessages((prev) => {
|
|
354
|
+
if (prev.some((m) => m.id === id)) return prev;
|
|
355
|
+
return [...prev, {
|
|
356
|
+
id,
|
|
357
|
+
role,
|
|
358
|
+
time: { created: Date.now() },
|
|
359
|
+
_insertionIndex: insertionCounterRef.current++
|
|
360
|
+
}];
|
|
361
|
+
});
|
|
362
|
+
if (role === "assistant") {
|
|
363
|
+
associatePendingEcho(id);
|
|
364
|
+
if (!completedExecutionIdsRef.current.has(id)) {
|
|
365
|
+
activeAssistantMessageIdsRef.current.add(id);
|
|
366
|
+
setIsStreaming(true);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
} else if (type === "message.part.updated") {
|
|
370
|
+
const msgId = props.messageID ?? props.messageId ?? "";
|
|
371
|
+
if (!msgId) return;
|
|
372
|
+
const partType = props.type ?? "text";
|
|
373
|
+
const sourcePartId = props.id ?? `${partType}-${msgId}`;
|
|
374
|
+
associatePendingEcho(msgId);
|
|
375
|
+
if (!completedExecutionIdsRef.current.has(msgId)) {
|
|
376
|
+
activeAssistantMessageIdsRef.current.add(msgId);
|
|
377
|
+
setIsStreaming(true);
|
|
378
|
+
}
|
|
379
|
+
setMessages((prev) => {
|
|
380
|
+
if (prev.some((message) => message.id === msgId)) return prev;
|
|
381
|
+
return [...prev, {
|
|
382
|
+
id: msgId,
|
|
383
|
+
role: "assistant",
|
|
384
|
+
time: { created: Date.now() },
|
|
385
|
+
_insertionIndex: insertionCounterRef.current++
|
|
386
|
+
}];
|
|
387
|
+
});
|
|
388
|
+
setPartsState((prev) => {
|
|
389
|
+
const existing = prev.values[msgId] ?? [];
|
|
390
|
+
const sourceIds = prev.sourceIds[msgId] ?? [];
|
|
391
|
+
const updated = [...existing];
|
|
392
|
+
const updatedSourceIds = [...sourceIds];
|
|
393
|
+
const knownIndex = sourceIds.indexOf(sourcePartId);
|
|
394
|
+
if (partType === "text") {
|
|
395
|
+
const textPart = {
|
|
396
|
+
type: "text",
|
|
397
|
+
text: props.text ?? props.content ?? ""
|
|
398
|
+
};
|
|
399
|
+
if (knownIndex >= 0) updated[knownIndex] = textPart;
|
|
400
|
+
else {
|
|
401
|
+
updatedSourceIds.push(sourcePartId);
|
|
402
|
+
updated.push(textPart);
|
|
403
|
+
}
|
|
404
|
+
} else if (partType === "tool") {
|
|
405
|
+
const toolId = props.id ?? props.toolId ?? sourcePartId;
|
|
406
|
+
const toolName = props.tool ?? props.name ?? "unknown";
|
|
407
|
+
const state = props.state ?? { status: "running" };
|
|
408
|
+
const toolPart = {
|
|
409
|
+
type: "tool",
|
|
410
|
+
id: toolId,
|
|
411
|
+
tool: toolName,
|
|
412
|
+
state: {
|
|
413
|
+
status: state.status ?? "running",
|
|
414
|
+
input: state.input,
|
|
415
|
+
output: state.output,
|
|
416
|
+
error: state.error,
|
|
417
|
+
metadata: state.metadata,
|
|
418
|
+
time: state.time
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
if (knownIndex >= 0) updated[knownIndex] = toolPart;
|
|
422
|
+
else {
|
|
423
|
+
updatedSourceIds.push(sourcePartId);
|
|
424
|
+
updated.push(toolPart);
|
|
425
|
+
}
|
|
426
|
+
} else if (partType === "reasoning") {
|
|
427
|
+
const reasoningPart = {
|
|
428
|
+
type: "reasoning",
|
|
429
|
+
text: props.text ?? ""
|
|
430
|
+
};
|
|
431
|
+
if (knownIndex >= 0) updated[knownIndex] = reasoningPart;
|
|
432
|
+
else {
|
|
433
|
+
updatedSourceIds.push(sourcePartId);
|
|
434
|
+
updated.push(reasoningPart);
|
|
435
|
+
}
|
|
436
|
+
} else return prev;
|
|
437
|
+
return {
|
|
438
|
+
values: {
|
|
439
|
+
...prev.values,
|
|
440
|
+
[msgId]: updated
|
|
441
|
+
},
|
|
442
|
+
sourceIds: {
|
|
443
|
+
...prev.sourceIds,
|
|
444
|
+
[msgId]: updatedSourceIds
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
});
|
|
448
|
+
} else if (type === "session.idle") {
|
|
449
|
+
const stillStreaming = retireExecution(props.executionId ?? props.executionID ?? "");
|
|
450
|
+
setIsStreaming(stillStreaming);
|
|
451
|
+
if (!stillStreaming) {
|
|
452
|
+
refetch();
|
|
453
|
+
if (executionScopedStreamRef.current) seedDegradation();
|
|
454
|
+
}
|
|
455
|
+
} else if (type === "hub.connections.degraded") {
|
|
456
|
+
const framedSessionId = typeof props.sessionId === "string" ? props.sessionId : null;
|
|
457
|
+
if (!framedSessionId) return;
|
|
458
|
+
const value = readDegradation(props);
|
|
459
|
+
if (!value) return;
|
|
460
|
+
bumpHubFrameEpoch(hubFrameEpochRef.current, framedSessionId);
|
|
461
|
+
setDegradationBySession((prev) => withDegradation(prev, framedSessionId, value));
|
|
462
|
+
} else if (type === "hub.connections.restored") {
|
|
463
|
+
const framedSessionId = typeof props.sessionId === "string" ? props.sessionId : null;
|
|
464
|
+
if (!framedSessionId) return;
|
|
465
|
+
bumpHubFrameEpoch(hubFrameEpochRef.current, framedSessionId);
|
|
466
|
+
setDegradationBySession((prev) => withDegradation(prev, framedSessionId, null));
|
|
467
|
+
} else if (type === "session.error") {
|
|
468
|
+
const stillStreaming = retireExecution(props.executionId ?? props.executionID ?? "");
|
|
469
|
+
setIsStreaming(stillStreaming);
|
|
470
|
+
setError(formatError(readErrorDetails(props), "Agent response failed"));
|
|
471
|
+
if (!stillStreaming) refetch();
|
|
472
|
+
}
|
|
473
|
+
}, [refetch, seedDegradation]);
|
|
474
|
+
/**
|
|
475
|
+
* Sends the text and echoes it into the transcript immediately.
|
|
476
|
+
*
|
|
477
|
+
* The echo is what makes the sender's own message visible during the turn:
|
|
478
|
+
* the session bus carries `message.updated` only for the assistant's reply,
|
|
479
|
+
* so the user's message otherwise first surfaces in the `session.idle`
|
|
480
|
+
* refetch — after the agent has finished answering it. The echo carries a
|
|
481
|
+
* client-minted id and never has to agree with the backend's, because it is
|
|
482
|
+
* retired wholesale when the turn ends rather than matched against a
|
|
483
|
+
* canonical message.
|
|
484
|
+
*/
|
|
485
|
+
const send = useCallback(async (text, options) => {
|
|
486
|
+
if (!token || !sessionId || !apiUrl || !enabled) {
|
|
487
|
+
const admissionError = new SessionStreamError("CHAT_NOT_READY", "Chat is not ready yet. Wait for the session connection and try again.");
|
|
488
|
+
setError(admissionError.message);
|
|
489
|
+
throw admissionError;
|
|
490
|
+
}
|
|
491
|
+
if (!connected) {
|
|
492
|
+
const admissionError = new SessionStreamError("CHAT_DISCONNECTED", "Chat is reconnecting. Your message was not sent.");
|
|
493
|
+
setError(admissionError.message);
|
|
494
|
+
throw admissionError;
|
|
495
|
+
}
|
|
496
|
+
if (pendingEchoesRef.current.size > 0 || activeAssistantMessageIdsRef.current.size > 0) {
|
|
497
|
+
const admissionError = new SessionStreamError("CHAT_BUSY", "The agent is still responding. Wait for this turn to finish before sending another message.");
|
|
498
|
+
setError(admissionError.message);
|
|
499
|
+
throw admissionError;
|
|
500
|
+
}
|
|
501
|
+
setError(null);
|
|
502
|
+
const echoId = `local-echo-${echoCounterRef.current++}`;
|
|
503
|
+
const echo = {
|
|
504
|
+
message: {
|
|
505
|
+
id: echoId,
|
|
506
|
+
role: "user",
|
|
507
|
+
time: { created: Date.now() },
|
|
508
|
+
_insertionIndex: insertionCounterRef.current++
|
|
509
|
+
},
|
|
510
|
+
parts: [{
|
|
511
|
+
type: "text",
|
|
512
|
+
text
|
|
513
|
+
}]
|
|
514
|
+
};
|
|
515
|
+
pendingEchoesRef.current.set(echoId, echo);
|
|
516
|
+
setMessages((prev) => [...prev, echo.message]);
|
|
517
|
+
setPartsState((prev) => ({
|
|
518
|
+
values: {
|
|
519
|
+
...prev.values,
|
|
520
|
+
[echoId]: echo.parts
|
|
521
|
+
},
|
|
522
|
+
sourceIds: prev.sourceIds
|
|
523
|
+
}));
|
|
524
|
+
setIsStreaming(true);
|
|
525
|
+
try {
|
|
526
|
+
const response = await fetchJson(`${apiUrl}/session/sessions/${encodeURIComponent(sessionId)}/messages`, token, {
|
|
527
|
+
method: "POST",
|
|
528
|
+
body: JSON.stringify({
|
|
529
|
+
parts: [{
|
|
530
|
+
type: "text",
|
|
531
|
+
text: encodeTextForWire(text)
|
|
532
|
+
}],
|
|
533
|
+
...options?.agent ? { agent: options.agent } : {},
|
|
534
|
+
...options?.model ? { model: options.model } : {},
|
|
535
|
+
...options?.system ? { system: options.system } : {},
|
|
536
|
+
...options?.reasoningEffort ? { reasoningEffort: options.reasoningEffort } : {}
|
|
537
|
+
})
|
|
538
|
+
});
|
|
539
|
+
if (response.userMessageId) {
|
|
540
|
+
const pending = pendingEchoesRef.current.get(echoId);
|
|
541
|
+
if (pending) pending.canonicalMessageId = response.userMessageId;
|
|
542
|
+
}
|
|
543
|
+
const executionId = response.info?.id;
|
|
544
|
+
if (executionId) {
|
|
545
|
+
const pending = pendingEchoesRef.current.get(echoId);
|
|
546
|
+
if (pending) pending.executionId = executionId;
|
|
547
|
+
if (!completedExecutionIdsRef.current.has(executionId)) activeAssistantMessageIdsRef.current.add(executionId);
|
|
548
|
+
}
|
|
549
|
+
} catch (err) {
|
|
550
|
+
pendingEchoesRef.current.delete(echoId);
|
|
551
|
+
setMessages((prev) => prev.filter((message) => message.id !== echoId));
|
|
552
|
+
setPartsState((prev) => {
|
|
553
|
+
if (!(echoId in prev.values)) return prev;
|
|
554
|
+
const values = { ...prev.values };
|
|
555
|
+
const sourceIds = { ...prev.sourceIds };
|
|
556
|
+
delete values[echoId];
|
|
557
|
+
delete sourceIds[echoId];
|
|
558
|
+
return {
|
|
559
|
+
values,
|
|
560
|
+
sourceIds
|
|
561
|
+
};
|
|
562
|
+
});
|
|
563
|
+
if (pendingEchoesRef.current.size === 0 && activeAssistantMessageIdsRef.current.size === 0) setIsStreaming(false);
|
|
564
|
+
const msg = err instanceof Error ? err.message : "Failed to send message";
|
|
565
|
+
setError(msg);
|
|
566
|
+
throw err;
|
|
567
|
+
}
|
|
568
|
+
}, [
|
|
569
|
+
apiUrl,
|
|
570
|
+
connected,
|
|
571
|
+
enabled,
|
|
572
|
+
token,
|
|
573
|
+
sessionId
|
|
574
|
+
]);
|
|
575
|
+
const abort = useCallback(async () => {
|
|
576
|
+
if (!token || !sessionId || !apiUrl) return;
|
|
577
|
+
try {
|
|
578
|
+
await fetchJson(`${apiUrl}/session/sessions/${encodeURIComponent(sessionId)}/abort`, token, { method: "POST" });
|
|
579
|
+
} catch (err) {
|
|
580
|
+
const msg = err instanceof Error ? err.message : "Failed to abort";
|
|
581
|
+
setError(msg);
|
|
582
|
+
}
|
|
583
|
+
}, [
|
|
584
|
+
apiUrl,
|
|
585
|
+
token,
|
|
586
|
+
sessionId
|
|
587
|
+
]);
|
|
588
|
+
useEffect(() => {
|
|
589
|
+
if (!enabled || !token || !sessionId) return;
|
|
590
|
+
refetch();
|
|
591
|
+
connectSSE();
|
|
592
|
+
return () => {
|
|
593
|
+
abortRef.current?.abort();
|
|
594
|
+
if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
|
|
595
|
+
setConnected(false);
|
|
596
|
+
};
|
|
597
|
+
}, [
|
|
598
|
+
enabled,
|
|
599
|
+
token,
|
|
600
|
+
sessionId,
|
|
601
|
+
refetch,
|
|
602
|
+
connectSSE
|
|
603
|
+
]);
|
|
604
|
+
return {
|
|
605
|
+
messages,
|
|
606
|
+
partMap,
|
|
607
|
+
isStreaming,
|
|
608
|
+
send,
|
|
609
|
+
abort,
|
|
610
|
+
refetch,
|
|
611
|
+
error,
|
|
612
|
+
connected,
|
|
613
|
+
degradation
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/hooks/use-sidecar-auth.ts
|
|
618
|
+
function storageKey(resourceId, apiUrl) {
|
|
619
|
+
return `sidecar_session_${resourceId}__${apiUrl}`;
|
|
620
|
+
}
|
|
621
|
+
function loadSession(resourceId, apiUrl) {
|
|
622
|
+
if (typeof window === "undefined") return null;
|
|
623
|
+
try {
|
|
624
|
+
const raw = localStorage.getItem(storageKey(resourceId, apiUrl));
|
|
625
|
+
if (!raw) return null;
|
|
626
|
+
const data = JSON.parse(raw);
|
|
627
|
+
if (data.expiresAt * 1e3 - Date.now() < 6e4) {
|
|
628
|
+
localStorage.removeItem(storageKey(resourceId, apiUrl));
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
return data;
|
|
632
|
+
} catch {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function saveSession(resourceId, apiUrl, token, expiresAt) {
|
|
637
|
+
if (typeof window === "undefined") return;
|
|
638
|
+
try {
|
|
639
|
+
localStorage.setItem(storageKey(resourceId, apiUrl), JSON.stringify({
|
|
640
|
+
token,
|
|
641
|
+
expiresAt
|
|
642
|
+
}));
|
|
643
|
+
} catch {}
|
|
644
|
+
}
|
|
645
|
+
function clearSession(resourceId, apiUrl) {
|
|
646
|
+
if (typeof window === "undefined") return;
|
|
647
|
+
localStorage.removeItem(storageKey(resourceId, apiUrl));
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Generic sidecar PASETO challenge/response auth.
|
|
651
|
+
*
|
|
652
|
+
* Flow:
|
|
653
|
+
* 1. POST /api/auth/challenge → { nonce, message, expires_at }
|
|
654
|
+
* 2. signMessage(message) → signature (provided by consuming app)
|
|
655
|
+
* 3. POST /api/auth/session → { token, address, expires_at }
|
|
656
|
+
*
|
|
657
|
+
* Tokens are cached in localStorage and auto-refreshed 5 minutes before expiry.
|
|
658
|
+
*/
|
|
659
|
+
function useSidecarAuth({ resourceId, apiUrl, signMessage }) {
|
|
660
|
+
const cached = loadSession(resourceId, apiUrl);
|
|
661
|
+
const [token, setToken] = useState(cached?.token ?? null);
|
|
662
|
+
const [expiresAt, setExpiresAt] = useState(cached?.expiresAt ?? 0);
|
|
663
|
+
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
|
664
|
+
const [error, setError] = useState(null);
|
|
665
|
+
const refreshTimerRef = useRef(void 0);
|
|
666
|
+
const clearCachedToken = useCallback(() => {
|
|
667
|
+
setToken(null);
|
|
668
|
+
setExpiresAt(0);
|
|
669
|
+
clearSession(resourceId, apiUrl);
|
|
670
|
+
}, [resourceId, apiUrl]);
|
|
671
|
+
const authenticate = useCallback(async () => {
|
|
672
|
+
if (!apiUrl) return null;
|
|
673
|
+
setIsAuthenticating(true);
|
|
674
|
+
setError(null);
|
|
675
|
+
try {
|
|
676
|
+
const challengeRes = await fetch(`${apiUrl}/api/auth/challenge`, { method: "POST" });
|
|
677
|
+
if (!challengeRes.ok) throw new Error(`Challenge failed: ${challengeRes.status}`);
|
|
678
|
+
const { nonce, message } = await challengeRes.json();
|
|
679
|
+
const signature = await signMessage(message);
|
|
680
|
+
const sessionRes = await fetch(`${apiUrl}/api/auth/session`, {
|
|
681
|
+
method: "POST",
|
|
682
|
+
headers: { "Content-Type": "application/json" },
|
|
683
|
+
body: JSON.stringify({
|
|
684
|
+
nonce,
|
|
685
|
+
signature
|
|
686
|
+
})
|
|
687
|
+
});
|
|
688
|
+
if (!sessionRes.ok) {
|
|
689
|
+
const text = await sessionRes.text();
|
|
690
|
+
throw new Error(text || `Session exchange failed: ${sessionRes.status}`);
|
|
691
|
+
}
|
|
692
|
+
const { token: newToken, expires_at } = await sessionRes.json();
|
|
693
|
+
setToken(newToken);
|
|
694
|
+
setExpiresAt(expires_at);
|
|
695
|
+
saveSession(resourceId, apiUrl, newToken, expires_at);
|
|
696
|
+
return newToken;
|
|
697
|
+
} catch (err) {
|
|
698
|
+
setError(err instanceof Error ? err.message : "Authentication failed");
|
|
699
|
+
clearCachedToken();
|
|
700
|
+
return null;
|
|
701
|
+
} finally {
|
|
702
|
+
setIsAuthenticating(false);
|
|
703
|
+
}
|
|
704
|
+
}, [
|
|
705
|
+
resourceId,
|
|
706
|
+
apiUrl,
|
|
707
|
+
signMessage,
|
|
708
|
+
clearCachedToken
|
|
709
|
+
]);
|
|
710
|
+
useEffect(() => {
|
|
711
|
+
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
712
|
+
if (!token || !expiresAt) return;
|
|
713
|
+
const msUntilRefresh = (expiresAt - 300) * 1e3 - Date.now();
|
|
714
|
+
if (msUntilRefresh <= 0) {
|
|
715
|
+
clearCachedToken();
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
refreshTimerRef.current = setTimeout(() => {
|
|
719
|
+
authenticate().catch(() => {
|
|
720
|
+
clearCachedToken();
|
|
721
|
+
});
|
|
722
|
+
}, msUntilRefresh);
|
|
723
|
+
return () => {
|
|
724
|
+
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
725
|
+
};
|
|
726
|
+
}, [
|
|
727
|
+
token,
|
|
728
|
+
expiresAt,
|
|
729
|
+
authenticate,
|
|
730
|
+
clearCachedToken
|
|
731
|
+
]);
|
|
732
|
+
return {
|
|
733
|
+
token,
|
|
734
|
+
isAuthenticated: token !== null,
|
|
735
|
+
isAuthenticating,
|
|
736
|
+
authenticate,
|
|
737
|
+
clearCachedToken,
|
|
738
|
+
error
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
//#endregion
|
|
742
|
+
export { useSessionStream as i, SESSION_STREAM_RECONNECT_DELAY_MS as n, SessionStreamError as r, useSidecarAuth as t };
|