@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.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/active-sessions-store-CeOmXgv5.d.ts +85 -0
- package/dist/artifact-pane-DvJyPWV4.d.ts +24 -0
- package/dist/auth.d.ts +74 -0
- package/dist/auth.js +15 -0
- package/dist/button-CMQuQEW_.d.ts +17 -0
- package/dist/chat.d.ts +232 -0
- package/dist/chat.js +30 -0
- package/dist/chunk-2NFQRQOD.js +1009 -0
- package/dist/chunk-2VH6PUXD.js +186 -0
- package/dist/chunk-34A66VBG.js +214 -0
- package/dist/chunk-3OI2QKFD.js +0 -0
- package/dist/chunk-4CLN43XT.js +45 -0
- package/dist/chunk-54SQQMMM.js +156 -0
- package/dist/chunk-5Z5ZYMOJ.js +0 -0
- package/dist/chunk-66BNMOVT.js +167 -0
- package/dist/chunk-6BGQA4BQ.js +0 -0
- package/dist/chunk-7UO2ZMRQ.js +133 -0
- package/dist/chunk-BX6AQMUS.js +183 -0
- package/dist/chunk-CD53GZOM.js +59 -0
- package/dist/chunk-CSAIKY36.js +54 -0
- package/dist/chunk-EEE55AVS.js +1201 -0
- package/dist/chunk-GYPQXTJU.js +230 -0
- package/dist/chunk-HFL6R6IF.js +37 -0
- package/dist/chunk-HJKCSXCH.js +737 -0
- package/dist/chunk-LISXUB4D.js +1222 -0
- package/dist/chunk-LQS34IGP.js +0 -0
- package/dist/chunk-MKTSMWVD.js +109 -0
- package/dist/chunk-NKDZ7GZE.js +192 -0
- package/dist/chunk-OEX7NZE3.js +321 -0
- package/dist/chunk-Q56BYXQF.js +61 -0
- package/dist/chunk-Q7EIIWTC.js +0 -0
- package/dist/chunk-REJESC5U.js +117 -0
- package/dist/chunk-RQGKSCEZ.js +0 -0
- package/dist/chunk-RQHJBTEU.js +10 -0
- package/dist/chunk-TMFOPHHN.js +299 -0
- package/dist/chunk-XGKULLYE.js +40 -0
- package/dist/chunk-XIHMJ7ZQ.js +614 -0
- package/dist/chunk-YJ2G3XO5.js +1048 -0
- package/dist/chunk-YNN4O57I.js +754 -0
- package/dist/code-block-DjXf8eOG.d.ts +19 -0
- package/dist/document-editor-pane-A5LT5H4N.js +12 -0
- package/dist/document-editor-pane-DyDEX_Zm.d.ts +124 -0
- package/dist/editor.d.ts +120 -0
- package/dist/editor.js +34 -0
- package/dist/files.d.ts +175 -0
- package/dist/files.js +20 -0
- package/dist/hooks.d.ts +56 -0
- package/dist/hooks.js +41 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +446 -0
- package/dist/markdown.d.ts +15 -0
- package/dist/markdown.js +14 -0
- package/dist/message-BHWbxBtT.d.ts +15 -0
- package/dist/openui.d.ts +115 -0
- package/dist/openui.js +12 -0
- package/dist/parts-dj7AcUg0.d.ts +36 -0
- package/dist/primitives.d.ts +332 -0
- package/dist/primitives.js +191 -0
- package/dist/run-PfLmDAox.d.ts +41 -0
- package/dist/run.d.ts +69 -0
- package/dist/run.js +36 -0
- package/dist/sdk-hooks.d.ts +285 -0
- package/dist/sdk-hooks.js +31 -0
- package/dist/stores.d.ts +17 -0
- package/dist/stores.js +76 -0
- package/dist/tool-call-feed-Bs3MyQMT.d.ts +68 -0
- package/dist/tool-display-z4JcDmMQ.d.ts +32 -0
- package/dist/tool-previews.d.ts +48 -0
- package/dist/tool-previews.js +21 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +45 -0
- package/dist/utils.js +32 -0
- package/package.json +193 -0
- package/src/auth/auth.tsx +228 -0
- package/src/auth/index.ts +13 -0
- package/src/auth/login-layout.tsx +46 -0
- package/src/chat/agent-timeline.stories.tsx +429 -0
- package/src/chat/agent-timeline.tsx +360 -0
- package/src/chat/chat-container.tsx +486 -0
- package/src/chat/chat-input.stories.tsx +142 -0
- package/src/chat/chat-input.tsx +389 -0
- package/src/chat/chat-message.stories.tsx +237 -0
- package/src/chat/chat-message.tsx +129 -0
- package/src/chat/index.ts +18 -0
- package/src/chat/message-list.stories.tsx +336 -0
- package/src/chat/message-list.tsx +79 -0
- package/src/chat/thinking-indicator.stories.tsx +56 -0
- package/src/chat/thinking-indicator.tsx +30 -0
- package/src/chat/user-message.stories.tsx +92 -0
- package/src/chat/user-message.tsx +43 -0
- package/src/editor/document-editor-pane.tsx +351 -0
- package/src/editor/editor-provider.tsx +428 -0
- package/src/editor/editor-toolbar.tsx +130 -0
- package/src/editor/index.ts +31 -0
- package/src/editor/markdown-conversion.ts +21 -0
- package/src/editor/markdown-document-editor.tsx +137 -0
- package/src/editor/tiptap-editor.tsx +331 -0
- package/src/editor/use-editor.ts +221 -0
- package/src/files/file-artifact-pane.tsx +183 -0
- package/src/files/file-preview.tsx +342 -0
- package/src/files/file-tabs.tsx +71 -0
- package/src/files/file-tree.tsx +258 -0
- package/src/files/index.ts +17 -0
- package/src/files/rich-file-tree.stories.tsx +104 -0
- package/src/files/rich-file-tree.test.tsx +42 -0
- package/src/files/rich-file-tree.tsx +232 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-auth.ts +153 -0
- package/src/hooks/use-auto-scroll.ts +59 -0
- package/src/hooks/use-dropdown-menu.ts +40 -0
- package/src/hooks/use-live-time.test.tsx +40 -0
- package/src/hooks/use-live-time.ts +27 -0
- package/src/hooks/use-realtime-session.ts +319 -0
- package/src/hooks/use-run-collapse-state.ts +25 -0
- package/src/hooks/use-run-groups.ts +111 -0
- package/src/hooks/use-sdk-session.ts +575 -0
- package/src/hooks/use-sse-stream.ts +475 -0
- package/src/hooks/use-tool-call-stream.ts +96 -0
- package/src/index.ts +14 -0
- package/src/lib/utils.ts +6 -0
- package/src/markdown/code-block.tsx +198 -0
- package/src/markdown/index.ts +2 -0
- package/src/markdown/markdown.stories.tsx +190 -0
- package/src/markdown/markdown.tsx +62 -0
- package/src/openui/index.ts +20 -0
- package/src/openui/openui-artifact-renderer.tsx +542 -0
- package/src/primitives/artifact-pane.tsx +91 -0
- package/src/primitives/avatar.stories.tsx +95 -0
- package/src/primitives/avatar.tsx +47 -0
- package/src/primitives/badge.stories.tsx +57 -0
- package/src/primitives/badge.tsx +97 -0
- package/src/primitives/button.stories.tsx +48 -0
- package/src/primitives/button.tsx +115 -0
- package/src/primitives/card.stories.tsx +53 -0
- package/src/primitives/card.tsx +98 -0
- package/src/primitives/code-block.stories.tsx +115 -0
- package/src/primitives/code-block.tsx +22 -0
- package/src/primitives/design-tokens.stories.tsx +162 -0
- package/src/primitives/dialog.stories.tsx +176 -0
- package/src/primitives/dialog.tsx +137 -0
- package/src/primitives/drop-zone.stories.tsx +123 -0
- package/src/primitives/drop-zone.tsx +131 -0
- package/src/primitives/dropdown-menu.stories.tsx +122 -0
- package/src/primitives/dropdown-menu.tsx +214 -0
- package/src/primitives/empty-state.stories.tsx +81 -0
- package/src/primitives/empty-state.tsx +40 -0
- package/src/primitives/index.ts +118 -0
- package/src/primitives/input.stories.tsx +113 -0
- package/src/primitives/input.tsx +136 -0
- package/src/primitives/label.stories.tsx +84 -0
- package/src/primitives/label.tsx +24 -0
- package/src/primitives/progress.stories.tsx +93 -0
- package/src/primitives/progress.tsx +50 -0
- package/src/primitives/segmented-control.test.tsx +328 -0
- package/src/primitives/segmented-control.tsx +154 -0
- package/src/primitives/select.stories.tsx +164 -0
- package/src/primitives/select.tsx +158 -0
- package/src/primitives/sidebar-drop-zone.stories.tsx +100 -0
- package/src/primitives/sidebar-drop-zone.tsx +149 -0
- package/src/primitives/skeleton.stories.tsx +79 -0
- package/src/primitives/skeleton.tsx +55 -0
- package/src/primitives/stat-card.stories.tsx +137 -0
- package/src/primitives/stat-card.tsx +97 -0
- package/src/primitives/switch.stories.tsx +85 -0
- package/src/primitives/switch.tsx +28 -0
- package/src/primitives/table.stories.tsx +170 -0
- package/src/primitives/table.tsx +116 -0
- package/src/primitives/tabs.stories.tsx +180 -0
- package/src/primitives/tabs.tsx +71 -0
- package/src/primitives/terminal-display.stories.tsx +191 -0
- package/src/primitives/terminal-display.tsx +189 -0
- package/src/primitives/theme-toggle.stories.tsx +32 -0
- package/src/primitives/theme-toggle.tsx +96 -0
- package/src/primitives/toast.stories.tsx +155 -0
- package/src/primitives/toast.tsx +190 -0
- package/src/primitives/upload-progress.stories.tsx +120 -0
- package/src/primitives/upload-progress.tsx +110 -0
- package/src/run/expanded-tool-detail.stories.tsx +182 -0
- package/src/run/expanded-tool-detail.tsx +186 -0
- package/src/run/index.ts +13 -0
- package/src/run/inline-thinking-item.stories.tsx +136 -0
- package/src/run/inline-thinking-item.tsx +120 -0
- package/src/run/inline-tool-item.stories.tsx +222 -0
- package/src/run/inline-tool-item.tsx +190 -0
- package/src/run/run-group.stories.tsx +322 -0
- package/src/run/run-group.tsx +569 -0
- package/src/run/run-item-primitives.tsx +17 -0
- package/src/run/tool-call-feed.stories.tsx +294 -0
- package/src/run/tool-call-feed.tsx +192 -0
- package/src/run/tool-call-step.stories.tsx +198 -0
- package/src/run/tool-call-step.tsx +240 -0
- package/src/sdk-hooks.ts +38 -0
- package/src/stores/active-sessions-store.ts +455 -0
- package/src/stores/chat-store.ts +43 -0
- package/src/stores/index.ts +2 -0
- package/src/tool-previews/command-preview.tsx +116 -0
- package/src/tool-previews/diff-preview.tsx +85 -0
- package/src/tool-previews/glob-results-preview.tsx +98 -0
- package/src/tool-previews/grep-results-preview.tsx +157 -0
- package/src/tool-previews/index.ts +22 -0
- package/src/tool-previews/preview-primitives.tsx +84 -0
- package/src/tool-previews/question-preview.tsx +101 -0
- package/src/tool-previews/web-search-preview.tsx +117 -0
- package/src/tool-previews/write-file-preview.tsx +80 -0
- package/src/types/branding.ts +11 -0
- package/src/types/index.ts +5 -0
- package/src/types/message.ts +13 -0
- package/src/types/parts.ts +51 -0
- package/src/types/run.ts +56 -0
- package/src/types/tool-display.ts +41 -0
- package/src/utils/copy-text.ts +30 -0
- package/src/utils/format.test.ts +43 -0
- package/src/utils/format.ts +56 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/time-ago.ts +9 -0
- 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";
|