@tangle-network/ui 1.0.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 (220) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +33 -0
  4. package/dist/active-sessions-store-CeOmXgv5.d.ts +85 -0
  5. package/dist/artifact-pane-DvJyPWV4.d.ts +24 -0
  6. package/dist/auth.d.ts +74 -0
  7. package/dist/auth.js +15 -0
  8. package/dist/button-CMQuQEW_.d.ts +17 -0
  9. package/dist/chat.d.ts +232 -0
  10. package/dist/chat.js +30 -0
  11. package/dist/chunk-2NFQRQOD.js +1009 -0
  12. package/dist/chunk-2VH6PUXD.js +186 -0
  13. package/dist/chunk-34A66VBG.js +214 -0
  14. package/dist/chunk-3OI2QKFD.js +0 -0
  15. package/dist/chunk-4CLN43XT.js +45 -0
  16. package/dist/chunk-54SQQMMM.js +156 -0
  17. package/dist/chunk-5Z5ZYMOJ.js +0 -0
  18. package/dist/chunk-66BNMOVT.js +167 -0
  19. package/dist/chunk-6BGQA4BQ.js +0 -0
  20. package/dist/chunk-7UO2ZMRQ.js +133 -0
  21. package/dist/chunk-BX6AQMUS.js +183 -0
  22. package/dist/chunk-CD53GZOM.js +59 -0
  23. package/dist/chunk-CSAIKY36.js +54 -0
  24. package/dist/chunk-EEE55AVS.js +1201 -0
  25. package/dist/chunk-GYPQXTJU.js +230 -0
  26. package/dist/chunk-HFL6R6IF.js +37 -0
  27. package/dist/chunk-HJKCSXCH.js +737 -0
  28. package/dist/chunk-LISXUB4D.js +1222 -0
  29. package/dist/chunk-LQS34IGP.js +0 -0
  30. package/dist/chunk-MKTSMWVD.js +109 -0
  31. package/dist/chunk-NKDZ7GZE.js +192 -0
  32. package/dist/chunk-OEX7NZE3.js +321 -0
  33. package/dist/chunk-Q56BYXQF.js +61 -0
  34. package/dist/chunk-Q7EIIWTC.js +0 -0
  35. package/dist/chunk-REJESC5U.js +117 -0
  36. package/dist/chunk-RQGKSCEZ.js +0 -0
  37. package/dist/chunk-RQHJBTEU.js +10 -0
  38. package/dist/chunk-TMFOPHHN.js +299 -0
  39. package/dist/chunk-XGKULLYE.js +40 -0
  40. package/dist/chunk-XIHMJ7ZQ.js +614 -0
  41. package/dist/chunk-YJ2G3XO5.js +1048 -0
  42. package/dist/chunk-YNN4O57I.js +754 -0
  43. package/dist/code-block-DjXf8eOG.d.ts +19 -0
  44. package/dist/document-editor-pane-A5LT5H4N.js +12 -0
  45. package/dist/document-editor-pane-DyDEX_Zm.d.ts +124 -0
  46. package/dist/editor.d.ts +120 -0
  47. package/dist/editor.js +34 -0
  48. package/dist/files.d.ts +175 -0
  49. package/dist/files.js +20 -0
  50. package/dist/hooks.d.ts +56 -0
  51. package/dist/hooks.js +41 -0
  52. package/dist/index.d.ts +43 -0
  53. package/dist/index.js +446 -0
  54. package/dist/markdown.d.ts +15 -0
  55. package/dist/markdown.js +14 -0
  56. package/dist/message-BHWbxBtT.d.ts +15 -0
  57. package/dist/openui.d.ts +115 -0
  58. package/dist/openui.js +12 -0
  59. package/dist/parts-dj7AcUg0.d.ts +36 -0
  60. package/dist/primitives.d.ts +332 -0
  61. package/dist/primitives.js +191 -0
  62. package/dist/run-PfLmDAox.d.ts +41 -0
  63. package/dist/run.d.ts +69 -0
  64. package/dist/run.js +36 -0
  65. package/dist/sdk-hooks.d.ts +285 -0
  66. package/dist/sdk-hooks.js +31 -0
  67. package/dist/stores.d.ts +17 -0
  68. package/dist/stores.js +76 -0
  69. package/dist/tool-call-feed-Bs3MyQMT.d.ts +68 -0
  70. package/dist/tool-display-z4JcDmMQ.d.ts +32 -0
  71. package/dist/tool-previews.d.ts +48 -0
  72. package/dist/tool-previews.js +21 -0
  73. package/dist/types.d.ts +19 -0
  74. package/dist/types.js +1 -0
  75. package/dist/utils.d.ts +45 -0
  76. package/dist/utils.js +32 -0
  77. package/package.json +193 -0
  78. package/src/auth/auth.tsx +228 -0
  79. package/src/auth/index.ts +13 -0
  80. package/src/auth/login-layout.tsx +46 -0
  81. package/src/chat/agent-timeline.stories.tsx +429 -0
  82. package/src/chat/agent-timeline.tsx +360 -0
  83. package/src/chat/chat-container.tsx +486 -0
  84. package/src/chat/chat-input.stories.tsx +142 -0
  85. package/src/chat/chat-input.tsx +389 -0
  86. package/src/chat/chat-message.stories.tsx +237 -0
  87. package/src/chat/chat-message.tsx +129 -0
  88. package/src/chat/index.ts +18 -0
  89. package/src/chat/message-list.stories.tsx +336 -0
  90. package/src/chat/message-list.tsx +79 -0
  91. package/src/chat/thinking-indicator.stories.tsx +56 -0
  92. package/src/chat/thinking-indicator.tsx +30 -0
  93. package/src/chat/user-message.stories.tsx +92 -0
  94. package/src/chat/user-message.tsx +43 -0
  95. package/src/editor/document-editor-pane.tsx +351 -0
  96. package/src/editor/editor-provider.tsx +428 -0
  97. package/src/editor/editor-toolbar.tsx +130 -0
  98. package/src/editor/index.ts +31 -0
  99. package/src/editor/markdown-conversion.ts +21 -0
  100. package/src/editor/markdown-document-editor.tsx +137 -0
  101. package/src/editor/tiptap-editor.tsx +331 -0
  102. package/src/editor/use-editor.ts +221 -0
  103. package/src/files/file-artifact-pane.tsx +183 -0
  104. package/src/files/file-preview.tsx +342 -0
  105. package/src/files/file-tabs.tsx +71 -0
  106. package/src/files/file-tree.tsx +258 -0
  107. package/src/files/index.ts +17 -0
  108. package/src/files/rich-file-tree.stories.tsx +104 -0
  109. package/src/files/rich-file-tree.test.tsx +42 -0
  110. package/src/files/rich-file-tree.tsx +232 -0
  111. package/src/hooks/index.ts +10 -0
  112. package/src/hooks/use-auth.ts +153 -0
  113. package/src/hooks/use-auto-scroll.ts +59 -0
  114. package/src/hooks/use-dropdown-menu.ts +40 -0
  115. package/src/hooks/use-live-time.test.tsx +40 -0
  116. package/src/hooks/use-live-time.ts +27 -0
  117. package/src/hooks/use-realtime-session.ts +319 -0
  118. package/src/hooks/use-run-collapse-state.ts +25 -0
  119. package/src/hooks/use-run-groups.ts +111 -0
  120. package/src/hooks/use-sdk-session.ts +575 -0
  121. package/src/hooks/use-sse-stream.ts +475 -0
  122. package/src/hooks/use-tool-call-stream.ts +96 -0
  123. package/src/index.ts +14 -0
  124. package/src/lib/utils.ts +6 -0
  125. package/src/markdown/code-block.tsx +198 -0
  126. package/src/markdown/index.ts +2 -0
  127. package/src/markdown/markdown.stories.tsx +190 -0
  128. package/src/markdown/markdown.tsx +62 -0
  129. package/src/openui/index.ts +20 -0
  130. package/src/openui/openui-artifact-renderer.tsx +542 -0
  131. package/src/primitives/artifact-pane.tsx +91 -0
  132. package/src/primitives/avatar.stories.tsx +95 -0
  133. package/src/primitives/avatar.tsx +47 -0
  134. package/src/primitives/badge.stories.tsx +57 -0
  135. package/src/primitives/badge.tsx +97 -0
  136. package/src/primitives/button.stories.tsx +48 -0
  137. package/src/primitives/button.tsx +115 -0
  138. package/src/primitives/card.stories.tsx +53 -0
  139. package/src/primitives/card.tsx +98 -0
  140. package/src/primitives/code-block.stories.tsx +115 -0
  141. package/src/primitives/code-block.tsx +22 -0
  142. package/src/primitives/design-tokens.stories.tsx +162 -0
  143. package/src/primitives/dialog.stories.tsx +176 -0
  144. package/src/primitives/dialog.tsx +137 -0
  145. package/src/primitives/drop-zone.stories.tsx +123 -0
  146. package/src/primitives/drop-zone.tsx +131 -0
  147. package/src/primitives/dropdown-menu.stories.tsx +122 -0
  148. package/src/primitives/dropdown-menu.tsx +214 -0
  149. package/src/primitives/empty-state.stories.tsx +81 -0
  150. package/src/primitives/empty-state.tsx +40 -0
  151. package/src/primitives/index.ts +118 -0
  152. package/src/primitives/input.stories.tsx +113 -0
  153. package/src/primitives/input.tsx +136 -0
  154. package/src/primitives/label.stories.tsx +84 -0
  155. package/src/primitives/label.tsx +24 -0
  156. package/src/primitives/progress.stories.tsx +93 -0
  157. package/src/primitives/progress.tsx +50 -0
  158. package/src/primitives/segmented-control.test.tsx +328 -0
  159. package/src/primitives/segmented-control.tsx +154 -0
  160. package/src/primitives/select.stories.tsx +164 -0
  161. package/src/primitives/select.tsx +158 -0
  162. package/src/primitives/sidebar-drop-zone.stories.tsx +100 -0
  163. package/src/primitives/sidebar-drop-zone.tsx +149 -0
  164. package/src/primitives/skeleton.stories.tsx +79 -0
  165. package/src/primitives/skeleton.tsx +55 -0
  166. package/src/primitives/stat-card.stories.tsx +137 -0
  167. package/src/primitives/stat-card.tsx +97 -0
  168. package/src/primitives/switch.stories.tsx +85 -0
  169. package/src/primitives/switch.tsx +28 -0
  170. package/src/primitives/table.stories.tsx +170 -0
  171. package/src/primitives/table.tsx +116 -0
  172. package/src/primitives/tabs.stories.tsx +180 -0
  173. package/src/primitives/tabs.tsx +71 -0
  174. package/src/primitives/terminal-display.stories.tsx +191 -0
  175. package/src/primitives/terminal-display.tsx +189 -0
  176. package/src/primitives/theme-toggle.stories.tsx +32 -0
  177. package/src/primitives/theme-toggle.tsx +96 -0
  178. package/src/primitives/toast.stories.tsx +155 -0
  179. package/src/primitives/toast.tsx +190 -0
  180. package/src/primitives/upload-progress.stories.tsx +120 -0
  181. package/src/primitives/upload-progress.tsx +110 -0
  182. package/src/run/expanded-tool-detail.stories.tsx +182 -0
  183. package/src/run/expanded-tool-detail.tsx +186 -0
  184. package/src/run/index.ts +13 -0
  185. package/src/run/inline-thinking-item.stories.tsx +136 -0
  186. package/src/run/inline-thinking-item.tsx +120 -0
  187. package/src/run/inline-tool-item.stories.tsx +222 -0
  188. package/src/run/inline-tool-item.tsx +190 -0
  189. package/src/run/run-group.stories.tsx +322 -0
  190. package/src/run/run-group.tsx +569 -0
  191. package/src/run/run-item-primitives.tsx +17 -0
  192. package/src/run/tool-call-feed.stories.tsx +294 -0
  193. package/src/run/tool-call-feed.tsx +192 -0
  194. package/src/run/tool-call-step.stories.tsx +198 -0
  195. package/src/run/tool-call-step.tsx +240 -0
  196. package/src/sdk-hooks.ts +38 -0
  197. package/src/stores/active-sessions-store.ts +455 -0
  198. package/src/stores/chat-store.ts +43 -0
  199. package/src/stores/index.ts +2 -0
  200. package/src/tool-previews/command-preview.tsx +116 -0
  201. package/src/tool-previews/diff-preview.tsx +85 -0
  202. package/src/tool-previews/glob-results-preview.tsx +98 -0
  203. package/src/tool-previews/grep-results-preview.tsx +157 -0
  204. package/src/tool-previews/index.ts +22 -0
  205. package/src/tool-previews/preview-primitives.tsx +84 -0
  206. package/src/tool-previews/question-preview.tsx +101 -0
  207. package/src/tool-previews/web-search-preview.tsx +117 -0
  208. package/src/tool-previews/write-file-preview.tsx +80 -0
  209. package/src/types/branding.ts +11 -0
  210. package/src/types/index.ts +5 -0
  211. package/src/types/message.ts +13 -0
  212. package/src/types/parts.ts +51 -0
  213. package/src/types/run.ts +56 -0
  214. package/src/types/tool-display.ts +41 -0
  215. package/src/utils/copy-text.ts +30 -0
  216. package/src/utils/format.test.ts +43 -0
  217. package/src/utils/format.ts +56 -0
  218. package/src/utils/index.ts +10 -0
  219. package/src/utils/time-ago.ts +9 -0
  220. package/src/utils/tool-display.ts +238 -0
@@ -0,0 +1,475 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ /**
6
+ * SSE Event data structure
7
+ */
8
+ export interface SSEEvent<T = unknown> {
9
+ id?: string;
10
+ event: string;
11
+ data: T;
12
+ timestamp: number;
13
+ }
14
+
15
+ /**
16
+ * Connection state for SSE streams
17
+ */
18
+ export type ConnectionState =
19
+ | "connecting"
20
+ | "connected"
21
+ | "reconnecting"
22
+ | "disconnected"
23
+ | "error";
24
+
25
+ /**
26
+ * SSE stream options
27
+ */
28
+ export interface UseSSEStreamOptions<T = unknown> {
29
+ /** URL to connect to */
30
+ url: string;
31
+ /** Authorization header value */
32
+ authToken?: string;
33
+ /** Enable automatic reconnection (default: true) */
34
+ autoReconnect?: boolean;
35
+ /** Max reconnection attempts (default: 5) */
36
+ maxRetries?: number;
37
+ /** Base delay between reconnects in ms (default: 1000) */
38
+ reconnectDelay?: number;
39
+ /** Event types to listen for (default: all) */
40
+ eventTypes?: string[];
41
+ /** Callback for each event */
42
+ onEvent?: (event: SSEEvent<T>) => void;
43
+ /** Callback on connection state change */
44
+ onStateChange?: (state: ConnectionState) => void;
45
+ /** Callback on error */
46
+ onError?: (error: Error) => void;
47
+ /** Custom headers */
48
+ headers?: Record<string, string>;
49
+ /** Whether to start connected (default: true) */
50
+ enabled?: boolean;
51
+ }
52
+
53
+ /**
54
+ * SSE stream result
55
+ */
56
+ export interface UseSSEStreamResult<T = unknown> {
57
+ /** Current connection state */
58
+ state: ConnectionState;
59
+ /** All received events */
60
+ events: SSEEvent<T>[];
61
+ /** Most recent event */
62
+ lastEvent: SSEEvent<T> | null;
63
+ /** Error if any */
64
+ error: Error | null;
65
+ /** Manually connect */
66
+ connect: () => void;
67
+ /** Manually disconnect */
68
+ disconnect: () => void;
69
+ /** Clear events buffer */
70
+ clearEvents: () => void;
71
+ /** Retry count */
72
+ retryCount: number;
73
+ /** Time since last event (ms) */
74
+ timeSinceLastEvent: number;
75
+ }
76
+
77
+ /**
78
+ * React hook for consuming SSE streams with automatic reconnection,
79
+ * event buffering, and state management.
80
+ *
81
+ * @example
82
+ * ```tsx
83
+ * const { events, state, lastEvent } = useSSEStream({
84
+ * url: '/api/v1/tasks/123/stream',
85
+ * authToken: 'Bearer xxx',
86
+ * onEvent: (e) => console.log('Event:', e),
87
+ * });
88
+ *
89
+ * return (
90
+ * <div>
91
+ * <p>Status: {state}</p>
92
+ * {events.map((e, i) => (
93
+ * <div key={i}>{e.event}: {JSON.stringify(e.data)}</div>
94
+ * ))}
95
+ * </div>
96
+ * );
97
+ * ```
98
+ */
99
+ export function useSSEStream<T = unknown>(
100
+ options: UseSSEStreamOptions<T>,
101
+ ): UseSSEStreamResult<T> {
102
+ const {
103
+ url,
104
+ authToken,
105
+ autoReconnect = true,
106
+ maxRetries = 5,
107
+ reconnectDelay = 1000,
108
+ eventTypes,
109
+ onEvent,
110
+ onStateChange,
111
+ onError,
112
+ headers,
113
+ enabled = true,
114
+ } = options;
115
+
116
+ const [state, setState] = React.useState<ConnectionState>("disconnected");
117
+ const [events, setEvents] = React.useState<SSEEvent<T>[]>([]);
118
+ const [lastEvent, setLastEvent] = React.useState<SSEEvent<T> | null>(null);
119
+ const [error, setError] = React.useState<Error | null>(null);
120
+ const [lastEventTime, setLastEventTime] = React.useState<number>(Date.now());
121
+ const [timeSinceLastEvent, setTimeSinceLastEvent] = React.useState(0);
122
+
123
+ const retryCountRef = React.useRef(0);
124
+ const eventSourceRef = React.useRef<EventSource | null>(null);
125
+ const abortControllerRef = React.useRef<AbortController | null>(null);
126
+ const reconnectTimeoutRef = React.useRef<ReturnType<
127
+ typeof setTimeout
128
+ > | null>(null);
129
+ const lastEventIdRef = React.useRef<string | undefined>(undefined);
130
+
131
+ // Update time since last event
132
+ React.useEffect(() => {
133
+ const interval = setInterval(() => {
134
+ setTimeSinceLastEvent(Date.now() - lastEventTime);
135
+ }, 1000);
136
+ return () => clearInterval(interval);
137
+ }, [lastEventTime]);
138
+
139
+ // Notify state changes
140
+ React.useEffect(() => {
141
+ onStateChange?.(state);
142
+ }, [state, onStateChange]);
143
+
144
+ const handleEvent = React.useCallback(
145
+ (eventType: string, data: T, id?: string) => {
146
+ const event: SSEEvent<T> = {
147
+ id,
148
+ event: eventType,
149
+ data,
150
+ timestamp: Date.now(),
151
+ };
152
+
153
+ if (id) {
154
+ lastEventIdRef.current = id;
155
+ }
156
+
157
+ setLastEventTime(Date.now());
158
+ setLastEvent(event);
159
+ setEvents((prev) => {
160
+ const next = [...prev, event];
161
+ return next.length > 1000 ? next.slice(-1000) : next;
162
+ });
163
+ onEvent?.(event);
164
+ },
165
+ [onEvent],
166
+ );
167
+
168
+ const disconnect = React.useCallback(() => {
169
+ if (reconnectTimeoutRef.current) {
170
+ clearTimeout(reconnectTimeoutRef.current);
171
+ reconnectTimeoutRef.current = null;
172
+ }
173
+ if (abortControllerRef.current) {
174
+ abortControllerRef.current.abort();
175
+ abortControllerRef.current = null;
176
+ }
177
+ if (eventSourceRef.current) {
178
+ eventSourceRef.current.close();
179
+ eventSourceRef.current = null;
180
+ }
181
+ setState("disconnected");
182
+ }, []);
183
+
184
+ const connect = React.useCallback(() => {
185
+ // Cleanup existing connection
186
+ disconnect();
187
+
188
+ if (!url || !enabled) {
189
+ return;
190
+ }
191
+
192
+ setState("connecting");
193
+ setError(null);
194
+
195
+ // Build URL with Last-Event-ID if available
196
+ const connectUrl = new URL(url, window.location.origin);
197
+ if (lastEventIdRef.current) {
198
+ connectUrl.searchParams.set("lastEventId", lastEventIdRef.current);
199
+ }
200
+
201
+ // Use fetch-based SSE for custom headers support
202
+ if (authToken || headers) {
203
+ abortControllerRef.current = new AbortController();
204
+
205
+ const fetchHeaders: Record<string, string> = {
206
+ Accept: "text/event-stream",
207
+ "Cache-Control": "no-cache",
208
+ ...headers,
209
+ };
210
+
211
+ if (authToken) {
212
+ fetchHeaders.Authorization = authToken.startsWith("Bearer ")
213
+ ? authToken
214
+ : `Bearer ${authToken}`;
215
+ }
216
+
217
+ if (lastEventIdRef.current) {
218
+ fetchHeaders["Last-Event-ID"] = lastEventIdRef.current;
219
+ }
220
+
221
+ fetch(connectUrl.toString(), {
222
+ headers: fetchHeaders,
223
+ signal: abortControllerRef.current.signal,
224
+ })
225
+ .then(async (response) => {
226
+ if (!response.ok) {
227
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
228
+ }
229
+
230
+ if (!response.body) {
231
+ throw new Error("Response body is null");
232
+ }
233
+
234
+ setState("connected");
235
+ retryCountRef.current = 0;
236
+
237
+ const reader = response.body.getReader();
238
+ const decoder = new TextDecoder();
239
+ let buffer = "";
240
+
241
+ while (true) {
242
+ const { done, value } = await reader.read();
243
+
244
+ if (done) {
245
+ break;
246
+ }
247
+
248
+ buffer += decoder.decode(value, { stream: true });
249
+
250
+ // Parse SSE events from buffer
251
+ const lines = buffer.split("\n");
252
+ buffer = lines.pop() || ""; // Keep incomplete line in buffer
253
+
254
+ let currentEvent = "";
255
+ let currentData = "";
256
+ let currentId: string | undefined;
257
+
258
+ for (const line of lines) {
259
+ if (line.startsWith("event:")) {
260
+ currentEvent = line.slice(6).trim();
261
+ } else if (line.startsWith("data:")) {
262
+ currentData += (currentData ? "\n" : "") + line.slice(5).trim();
263
+ } else if (line.startsWith("id:")) {
264
+ currentId = line.slice(3).trim();
265
+ } else if (line === "" && currentData) {
266
+ // Empty line = end of event
267
+ try {
268
+ const parsedData = JSON.parse(currentData) as T;
269
+ const eventType = currentEvent || "message";
270
+
271
+ if (!eventTypes || eventTypes.includes(eventType)) {
272
+ handleEvent(eventType, parsedData, currentId);
273
+ }
274
+ } catch {
275
+ // Handle non-JSON data
276
+ if (
277
+ !eventTypes ||
278
+ eventTypes.includes(currentEvent || "message")
279
+ ) {
280
+ handleEvent(
281
+ currentEvent || "message",
282
+ currentData as T,
283
+ currentId,
284
+ );
285
+ }
286
+ }
287
+
288
+ currentEvent = "";
289
+ currentData = "";
290
+ currentId = undefined;
291
+ }
292
+ }
293
+ }
294
+
295
+ // Stream ended - reconnect if enabled
296
+ if (autoReconnect && enabled) {
297
+ setState("reconnecting");
298
+ const delay = reconnectDelay * 2 ** retryCountRef.current;
299
+ reconnectTimeoutRef.current = setTimeout(
300
+ () => {
301
+ retryCountRef.current += 1;
302
+ connect();
303
+ },
304
+ Math.min(delay, 30000),
305
+ );
306
+ } else {
307
+ setState("disconnected");
308
+ }
309
+ })
310
+ .catch((err) => {
311
+ if (err.name === "AbortError") {
312
+ return; // Intentional disconnect
313
+ }
314
+
315
+ setError(err);
316
+ onError?.(err);
317
+ setState("error");
318
+
319
+ // Reconnect on error
320
+ if (autoReconnect && enabled && retryCountRef.current < maxRetries) {
321
+ setState("reconnecting");
322
+ const delay = reconnectDelay * 2 ** retryCountRef.current;
323
+ reconnectTimeoutRef.current = setTimeout(
324
+ () => {
325
+ retryCountRef.current += 1;
326
+ connect();
327
+ },
328
+ Math.min(delay, 30000),
329
+ );
330
+ }
331
+ });
332
+ } else {
333
+ // Use native EventSource for simple cases
334
+ const es = new EventSource(connectUrl.toString());
335
+ eventSourceRef.current = es;
336
+
337
+ es.onopen = () => {
338
+ setState("connected");
339
+ retryCountRef.current = 0;
340
+ };
341
+
342
+ es.onerror = () => {
343
+ const err = new Error("EventSource connection error");
344
+ setError(err);
345
+ onError?.(err);
346
+
347
+ if (autoReconnect && enabled && retryCountRef.current < maxRetries) {
348
+ setState("reconnecting");
349
+ es.close();
350
+ const delay = reconnectDelay * 2 ** retryCountRef.current;
351
+ reconnectTimeoutRef.current = setTimeout(
352
+ () => {
353
+ retryCountRef.current += 1;
354
+ connect();
355
+ },
356
+ Math.min(delay, 30000),
357
+ );
358
+ } else {
359
+ setState("error");
360
+ }
361
+ };
362
+
363
+ // Listen for specific event types or default 'message'
364
+ const types = eventTypes || ["message"];
365
+ for (const type of types) {
366
+ es.addEventListener(type, (e: MessageEvent) => {
367
+ try {
368
+ const data = JSON.parse(e.data) as T;
369
+ handleEvent(type, data, e.lastEventId);
370
+ } catch {
371
+ handleEvent(type, e.data as T, e.lastEventId);
372
+ }
373
+ });
374
+ }
375
+
376
+ // Also listen for unnamed events
377
+ if (!eventTypes) {
378
+ es.onmessage = (e: MessageEvent) => {
379
+ try {
380
+ const data = JSON.parse(e.data) as T;
381
+ handleEvent("message", data, e.lastEventId);
382
+ } catch {
383
+ handleEvent("message", e.data as T, e.lastEventId);
384
+ }
385
+ };
386
+ }
387
+ }
388
+ }, [
389
+ url,
390
+ authToken,
391
+ headers,
392
+ enabled,
393
+ autoReconnect,
394
+ maxRetries,
395
+ reconnectDelay,
396
+ eventTypes,
397
+ handleEvent,
398
+ onError,
399
+ disconnect,
400
+ ]);
401
+
402
+ // Auto-connect on mount and url/enabled change
403
+ React.useEffect(() => {
404
+ if (enabled) {
405
+ connect();
406
+ }
407
+ return () => disconnect();
408
+ }, [enabled, connect, disconnect]); // eslint-disable-line react-hooks/exhaustive-deps
409
+
410
+ const clearEvents = React.useCallback(() => {
411
+ setEvents([]);
412
+ setLastEvent(null);
413
+ }, []);
414
+
415
+ return {
416
+ state,
417
+ events,
418
+ lastEvent,
419
+ error,
420
+ connect,
421
+ disconnect,
422
+ clearEvents,
423
+ retryCount: retryCountRef.current,
424
+ timeSinceLastEvent,
425
+ };
426
+ }
427
+
428
+ /**
429
+ * Typed event types for common streaming scenarios
430
+ */
431
+ export interface TaskStreamEvent {
432
+ task_id: string;
433
+ status?: string;
434
+ progress?: number;
435
+ message?: string;
436
+ result?: unknown;
437
+ error?: string;
438
+ }
439
+
440
+ export interface AgentStreamEvent {
441
+ type:
442
+ | "message.updated"
443
+ | "tool_call"
444
+ | "tool_result"
445
+ | "llm_response"
446
+ | "error"
447
+ | "session.idle"
448
+ | "execution.started"
449
+ | "execution.result";
450
+ data: unknown;
451
+ timestamp?: number;
452
+ }
453
+
454
+ export interface TerminalStreamEvent {
455
+ type: "output" | "input" | "error" | "exit";
456
+ data: string;
457
+ timestamp: number;
458
+ }
459
+
460
+ export interface AutomationStreamEvent {
461
+ automation_id: string;
462
+ event_id?: string;
463
+ status?: string;
464
+ variant_id?: string;
465
+ output?: string;
466
+ action_result?: unknown;
467
+ error?: string;
468
+ }
469
+
470
+ export interface BotStreamEvent {
471
+ bot_id: string;
472
+ type: "balance" | "trade" | "decision" | "log" | "error";
473
+ data: unknown;
474
+ timestamp: number;
475
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * useToolCallStream — parses SSE events into ToolCallFeed segments.
3
+ *
4
+ * Takes raw SSE event data from the orchestrator stream and produces
5
+ * structured FeedSegments that ToolCallFeed can render.
6
+ */
7
+
8
+ import { useState, useCallback, useRef } from "react";
9
+ import type { FeedSegment, ToolCallData } from "../run/tool-call-feed";
10
+ import { parseToolEvent } from "../run/tool-call-feed";
11
+
12
+ export interface UseToolCallStreamReturn {
13
+ /** Current feed segments (text + tool calls interleaved) */
14
+ segments: FeedSegment[];
15
+ /** Push a raw SSE event into the stream */
16
+ pushEvent: (event: { type: string; data: Record<string, unknown> }) => void;
17
+ /** Push a text delta (from message.part.updated) */
18
+ pushText: (delta: string) => void;
19
+ /** Mark a tool call as complete */
20
+ completeToolCall: (id: string, result: { output?: string; error?: string; duration?: number }) => void;
21
+ /** Reset the stream */
22
+ reset: () => void;
23
+ }
24
+
25
+ export function useToolCallStream(): UseToolCallStreamReturn {
26
+ const [segments, setSegments] = useState<FeedSegment[]>([]);
27
+ const pendingToolsRef = useRef<Map<string, ToolCallData>>(new Map());
28
+ const lastSegmentKindRef = useRef<"text" | "tool" | null>(null);
29
+
30
+ const pushText = useCallback((delta: string) => {
31
+ setSegments((prev) => {
32
+ const last = prev[prev.length - 1];
33
+ if (last && last.kind === "text") {
34
+ // Append to existing text segment
35
+ return [...prev.slice(0, -1), { kind: "text", content: last.content + delta }];
36
+ }
37
+ // New text segment
38
+ return [...prev, { kind: "text", content: delta }];
39
+ });
40
+ lastSegmentKindRef.current = "text";
41
+ }, []);
42
+
43
+ const pushEvent = useCallback((event: { type: string; data: Record<string, unknown> }) => {
44
+ const toolCall = parseToolEvent(event);
45
+ if (!toolCall) return;
46
+
47
+ if (event.type === "tool.invocation" || event.type === "tool_use") {
48
+ // Store pending tool call
49
+ pendingToolsRef.current.set(toolCall.id, toolCall);
50
+
51
+ setSegments((prev) => [
52
+ ...prev,
53
+ { kind: "tool_call", call: { ...toolCall, status: "running" } },
54
+ ]);
55
+ lastSegmentKindRef.current = "tool";
56
+ }
57
+ }, []);
58
+
59
+ const completeToolCall = useCallback(
60
+ (id: string, result: { output?: string; error?: string; duration?: number }) => {
61
+ const pending = pendingToolsRef.current.get(id);
62
+ if (pending) {
63
+ pending.status = result.error ? "error" : "success";
64
+ pending.output = result.output || result.error;
65
+ pending.duration = result.duration;
66
+ pendingToolsRef.current.delete(id);
67
+ }
68
+
69
+ setSegments((prev) =>
70
+ prev.map((seg) => {
71
+ if (seg.kind === "tool_call" && seg.call.id === id) {
72
+ return {
73
+ kind: "tool_call",
74
+ call: {
75
+ ...seg.call,
76
+ status: result.error ? "error" : "success",
77
+ output: result.output || result.error,
78
+ duration: result.duration,
79
+ },
80
+ };
81
+ }
82
+ return seg;
83
+ }),
84
+ );
85
+ },
86
+ [],
87
+ );
88
+
89
+ const reset = useCallback(() => {
90
+ setSegments([]);
91
+ pendingToolsRef.current.clear();
92
+ lastSegmentKindRef.current = null;
93
+ }, []);
94
+
95
+ return { segments, pushEvent, pushText, completeToolCall, reset };
96
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export * from "./primitives";
2
+ export * from "./chat";
3
+ export * from "./run";
4
+ export * from "./openui";
5
+ export * from "./files";
6
+ export { type ConnectionState } from "./editor";
7
+ export * from "./editor";
8
+ export * from "./markdown";
9
+ export * from "./auth";
10
+ export * from "./hooks";
11
+ export * from "./stores";
12
+ export * from "./types";
13
+ export * from "./utils";
14
+ export * from "./tool-previews";
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }