@tangle-network/sandbox-ui 0.113.6 → 0.114.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/WorkflowGraph-B-NsdYz_.js +1961 -0
  2. package/dist/assets.d.ts +214 -206
  3. package/dist/assets.js +1057 -908
  4. package/dist/auth-CWe17Hly.js +37 -0
  5. package/dist/auth.d.ts +2 -30
  6. package/dist/auth.js +2 -15
  7. package/dist/chat-BOvFAe6p.js +903 -0
  8. package/dist/chat.d.ts +2 -230
  9. package/dist/chat.js +2 -34
  10. package/dist/connectors.d.ts +52 -49
  11. package/dist/connectors.js +595 -643
  12. package/dist/dashboard-CZ3VgNPR.js +4969 -0
  13. package/dist/dashboard.d.ts +4 -1297
  14. package/dist/dashboard.js +4 -152
  15. package/dist/editor.d.ts +2 -1
  16. package/dist/editor.js +2 -31
  17. package/dist/files.d.ts +2 -6
  18. package/dist/files.js +2 -33
  19. package/dist/globals.css +63 -3
  20. package/dist/hooks.d.ts +146 -148
  21. package/dist/hooks.js +222 -55
  22. package/dist/index-CKRaj1pM.d.ts +776 -0
  23. package/dist/index-CXX5IpH2.d.ts +2 -0
  24. package/dist/index-DPePq7c8.d.ts +1340 -0
  25. package/dist/index-DiyLLZuJ.d.ts +230 -0
  26. package/dist/index-DvmZLMT1.d.ts +30 -0
  27. package/dist/index-I8ZLVtHd.d.ts +330 -0
  28. package/dist/index-PRM636Ex.d.ts +86 -0
  29. package/dist/index.d.ts +21 -33
  30. package/dist/index.js +21 -701
  31. package/dist/info-panel-C9cqDzOL.js +594 -0
  32. package/dist/integrations.d.ts +133 -139
  33. package/dist/integrations.js +491 -673
  34. package/dist/markdown.d.ts +2 -1
  35. package/dist/markdown.js +2 -13
  36. package/dist/model-I-NIxeM-.js +1250 -0
  37. package/dist/model-brand-B9A4H9pJ.d.ts +98 -0
  38. package/dist/model-brand-hMyDTUqB.js +624 -0
  39. package/dist/motion-DkyBRJb9.js +37 -0
  40. package/dist/openui.d.ts +2 -1
  41. package/dist/openui.js +2 -7
  42. package/dist/pages.d.ts +307 -300
  43. package/dist/pages.js +4404 -4181
  44. package/dist/primitives-Dh1vG6J3.js +113 -0
  45. package/dist/primitives.d.ts +2 -74
  46. package/dist/primitives.js +2 -204
  47. package/dist/provider-logo-DjqmConF.js +311 -0
  48. package/dist/run.d.ts +2 -1
  49. package/dist/run.js +2 -23
  50. package/dist/sdk-hooks.d.ts +3 -4
  51. package/dist/sdk-hooks.js +3 -31
  52. package/dist/sidecar-DgH_vQ6m.d.ts +9 -0
  53. package/dist/stores.d.ts +14 -15
  54. package/dist/stores.js +13 -81
  55. package/dist/styles.css +63 -3
  56. package/dist/template-card-L-4gmjx2.d.ts +90 -0
  57. package/dist/terminal-CN9ZgHRt.js +197 -0
  58. package/dist/terminal.d.ts +60 -60
  59. package/dist/terminal.js +2 -10
  60. package/dist/tokens.css +51 -0
  61. package/dist/types.d.ts +4 -11
  62. package/dist/types.js +0 -1
  63. package/dist/use-pty-session-C8cE0adT.js +669 -0
  64. package/dist/use-sidecar-auth-CCTJcrq7.d.ts +145 -0
  65. package/dist/use-sidecar-auth-DkdcufOz.js +742 -0
  66. package/dist/utils-DzWbktWG.js +2 -0
  67. package/dist/utils.d.ts +2 -1
  68. package/dist/utils.js +2 -37
  69. package/dist/workbench-BCaCKl-J.js +8347 -0
  70. package/dist/workbench.d.ts +2 -310
  71. package/dist/workbench.js +2 -44
  72. package/dist/workflows.d.ts +341 -339
  73. package/dist/workflows.js +74 -109
  74. package/dist/workspace-B6AtI6ld.js +2510 -0
  75. package/dist/workspace.d.ts +2 -761
  76. package/dist/workspace.js +2 -51
  77. package/package.json +46 -40
  78. package/dist/WorkflowGraph-2QOORYIR.js +0 -1722
  79. package/dist/chunk-2BUPSB7O.js +0 -0
  80. package/dist/chunk-3KBNPQQF.js +0 -2697
  81. package/dist/chunk-3LIXVEEE.js +0 -279
  82. package/dist/chunk-72UF2H5H.js +0 -707
  83. package/dist/chunk-AI25G5MX.js +0 -944
  84. package/dist/chunk-ANSC5AFC.js +0 -314
  85. package/dist/chunk-AZ3AWMTM.js +0 -8
  86. package/dist/chunk-BGVEHENC.js +0 -10
  87. package/dist/chunk-CZ7CSFO4.js +0 -39
  88. package/dist/chunk-DYXXYGS4.js +0 -4700
  89. package/dist/chunk-EI44GEQ5.js +0 -6
  90. package/dist/chunk-IACRDXER.js +0 -219
  91. package/dist/chunk-IOB2PW5Z.js +0 -55
  92. package/dist/chunk-M4K7ZGR2.js +0 -24
  93. package/dist/chunk-MYZAW4RX.js +0 -665
  94. package/dist/chunk-OMUIPJQZ.js +0 -555
  95. package/dist/chunk-RQZRXKMR.js +0 -398
  96. package/dist/chunk-SOKKTB7W.js +0 -230
  97. package/dist/chunk-TADGS4Q7.js +0 -1010
  98. package/dist/chunk-UCQNOUN3.js +0 -7964
  99. package/dist/chunk-ULPLRBMF.js +0 -34
  100. package/dist/chunk-WID73FPH.js +0 -89
  101. package/dist/chunk-X3YXP7FG.js +0 -38
  102. package/dist/logo-DiAjq759.d.ts +0 -15
  103. package/dist/model-brand-BNnaUKKZ.d.ts +0 -99
  104. package/dist/ports-list-CDIvTNnX.d.ts +0 -17
  105. package/dist/sdk-hooks-Djkjq4cp.d.ts +0 -145
  106. package/dist/template-card-Bn-Bw5xC.d.ts +0 -87
@@ -0,0 +1,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 };