@yushaw/sanqian-chat 0.2.44 → 0.3.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/main/index.js +30 -1
- package/dist/main/index.mjs +30 -1
- package/dist/renderer/index.js +513 -64
- package/dist/renderer/index.mjs +513 -64
- package/package.json +1 -1
package/dist/renderer/index.js
CHANGED
|
@@ -114,6 +114,213 @@ function cloneBlocks(blocks) {
|
|
|
114
114
|
toolArgs: block.toolArgs ? { ...block.toolArgs } : block.toolArgs
|
|
115
115
|
}));
|
|
116
116
|
}
|
|
117
|
+
function updateDetachedSnapshotForEvent(streamContext, event) {
|
|
118
|
+
const snapshot = streamContext.detachedSnapshot;
|
|
119
|
+
if (!snapshot?.length) return false;
|
|
120
|
+
const assistantIndex = snapshot.findIndex((message) => message.id === streamContext.assistantMessageId);
|
|
121
|
+
if (assistantIndex === -1) return false;
|
|
122
|
+
const assistant = snapshot[assistantIndex];
|
|
123
|
+
switch (event.type) {
|
|
124
|
+
case "text": {
|
|
125
|
+
const rawContent = event.content;
|
|
126
|
+
if (!rawContent) return false;
|
|
127
|
+
const nextChunk = assistant.content ? rawContent : rawContent.trimStart();
|
|
128
|
+
if (!nextChunk) return false;
|
|
129
|
+
snapshot[assistantIndex] = {
|
|
130
|
+
...assistant,
|
|
131
|
+
content: `${assistant.content}${nextChunk}`,
|
|
132
|
+
isStreaming: true,
|
|
133
|
+
isComplete: false
|
|
134
|
+
};
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
case "thinking": {
|
|
138
|
+
const rawThinking = event.content;
|
|
139
|
+
if (!rawThinking) return false;
|
|
140
|
+
const nextChunk = assistant.thinking ? rawThinking : rawThinking.trimStart();
|
|
141
|
+
if (!nextChunk) return false;
|
|
142
|
+
snapshot[assistantIndex] = {
|
|
143
|
+
...assistant,
|
|
144
|
+
thinking: `${assistant.thinking ?? ""}${nextChunk}`,
|
|
145
|
+
currentThinking: `${assistant.currentThinking ?? ""}${nextChunk}`,
|
|
146
|
+
isStreaming: true,
|
|
147
|
+
isComplete: false,
|
|
148
|
+
isThinkingStreaming: true,
|
|
149
|
+
isThinkingPaused: false
|
|
150
|
+
};
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
case "tool_call": {
|
|
154
|
+
const tc = event.tool_call;
|
|
155
|
+
if (!tc) return false;
|
|
156
|
+
const toolName = tc.function?.name || tc.name || "";
|
|
157
|
+
const toolId = tc.id || "";
|
|
158
|
+
let args = {};
|
|
159
|
+
if (tc.args) {
|
|
160
|
+
args = tc.args;
|
|
161
|
+
} else if (tc.function?.arguments) {
|
|
162
|
+
try {
|
|
163
|
+
args = JSON.parse(tc.function.arguments);
|
|
164
|
+
} catch {
|
|
165
|
+
args = {};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const toolCalls = [...assistant.toolCalls || []];
|
|
169
|
+
toolCalls.push({
|
|
170
|
+
id: toolId,
|
|
171
|
+
name: toolName,
|
|
172
|
+
args,
|
|
173
|
+
status: "running"
|
|
174
|
+
});
|
|
175
|
+
const blocks = cloneBlocks(assistant.blocks) || [];
|
|
176
|
+
blocks.push({
|
|
177
|
+
type: "tool_call",
|
|
178
|
+
content: "",
|
|
179
|
+
timestamp: Date.now(),
|
|
180
|
+
toolName,
|
|
181
|
+
toolArgs: args,
|
|
182
|
+
toolCallId: toolId,
|
|
183
|
+
toolStatus: "running",
|
|
184
|
+
isIntermediate: true
|
|
185
|
+
});
|
|
186
|
+
snapshot[assistantIndex] = {
|
|
187
|
+
...assistant,
|
|
188
|
+
toolCalls,
|
|
189
|
+
blocks,
|
|
190
|
+
isStreaming: true,
|
|
191
|
+
isComplete: false,
|
|
192
|
+
isThinkingStreaming: false,
|
|
193
|
+
isThinkingPaused: !!assistant.thinking,
|
|
194
|
+
isToolCallsStreaming: true
|
|
195
|
+
};
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
case "tool_args_chunk": {
|
|
199
|
+
const chunk = event.chunk;
|
|
200
|
+
if (!chunk) return false;
|
|
201
|
+
const toolCalls = [...assistant.toolCalls || []];
|
|
202
|
+
const toolCallIndex = findToolCallIndex(toolCalls, event.tool_call_id, event.tool_name);
|
|
203
|
+
if (toolCallIndex === -1) return false;
|
|
204
|
+
const existingRaw = toolCalls[toolCallIndex].argsRaw || "";
|
|
205
|
+
toolCalls[toolCallIndex] = {
|
|
206
|
+
...toolCalls[toolCallIndex],
|
|
207
|
+
argsRaw: `${existingRaw}${chunk}`
|
|
208
|
+
};
|
|
209
|
+
const blocks = cloneBlocks(assistant.blocks) || [];
|
|
210
|
+
const blockIndex = blocks.findIndex(
|
|
211
|
+
(block) => block.type === "tool_call" && (block.toolCallId === event.tool_call_id || !event.tool_call_id && event.tool_name && block.toolName === event.tool_name)
|
|
212
|
+
);
|
|
213
|
+
if (blockIndex !== -1) {
|
|
214
|
+
blocks[blockIndex] = {
|
|
215
|
+
...blocks[blockIndex],
|
|
216
|
+
toolArgsRaw: `${blocks[blockIndex].toolArgsRaw || ""}${chunk}`
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
snapshot[assistantIndex] = {
|
|
220
|
+
...assistant,
|
|
221
|
+
toolCalls,
|
|
222
|
+
blocks
|
|
223
|
+
};
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
case "tool_args": {
|
|
227
|
+
const toolCalls = [...assistant.toolCalls || []];
|
|
228
|
+
const toolCallIndex = findToolCallIndex(toolCalls, event.tool_call_id, event.tool_name);
|
|
229
|
+
if (toolCallIndex === -1) return false;
|
|
230
|
+
toolCalls[toolCallIndex] = {
|
|
231
|
+
...toolCalls[toolCallIndex],
|
|
232
|
+
args: event.args || {},
|
|
233
|
+
argsRaw: void 0
|
|
234
|
+
};
|
|
235
|
+
const blocks = cloneBlocks(assistant.blocks) || [];
|
|
236
|
+
const blockIndex = blocks.findIndex(
|
|
237
|
+
(block) => block.type === "tool_call" && (block.toolCallId === event.tool_call_id || !event.tool_call_id && event.tool_name && block.toolName === event.tool_name)
|
|
238
|
+
);
|
|
239
|
+
if (blockIndex !== -1) {
|
|
240
|
+
blocks[blockIndex] = {
|
|
241
|
+
...blocks[blockIndex],
|
|
242
|
+
toolArgs: event.args || {},
|
|
243
|
+
toolArgsRaw: void 0
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
snapshot[assistantIndex] = {
|
|
247
|
+
...assistant,
|
|
248
|
+
toolCalls,
|
|
249
|
+
blocks
|
|
250
|
+
};
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
case "tool_result": {
|
|
254
|
+
const toolCalls = (assistant.toolCalls || []).map((toolCall) => toolCall.id === event.tool_call_id ? { ...toolCall, status: "completed", result: event.result } : toolCall);
|
|
255
|
+
const hasRunning = toolCalls.some((toolCall) => toolCall.status === "running");
|
|
256
|
+
const blocks = cloneBlocks(assistant.blocks) || [];
|
|
257
|
+
const toolBlockIndex = blocks.findIndex(
|
|
258
|
+
(block) => block.type === "tool_call" && block.toolCallId === event.tool_call_id
|
|
259
|
+
);
|
|
260
|
+
if (toolBlockIndex !== -1) {
|
|
261
|
+
blocks[toolBlockIndex] = {
|
|
262
|
+
...blocks[toolBlockIndex],
|
|
263
|
+
toolStatus: "completed"
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
blocks.push({
|
|
267
|
+
type: "tool_result",
|
|
268
|
+
content: typeof event.result === "string" ? event.result : JSON.stringify(event.result),
|
|
269
|
+
timestamp: Date.now(),
|
|
270
|
+
toolName: toolBlockIndex !== -1 ? blocks[toolBlockIndex].toolName : void 0,
|
|
271
|
+
toolCallId: event.tool_call_id,
|
|
272
|
+
isIntermediate: true
|
|
273
|
+
});
|
|
274
|
+
snapshot[assistantIndex] = {
|
|
275
|
+
...assistant,
|
|
276
|
+
toolCalls,
|
|
277
|
+
blocks,
|
|
278
|
+
isToolCallsStreaming: hasRunning
|
|
279
|
+
};
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
case "done": {
|
|
283
|
+
snapshot[assistantIndex] = {
|
|
284
|
+
...assistant,
|
|
285
|
+
thinking: assistant.thinking?.trimEnd(),
|
|
286
|
+
currentThinking: void 0,
|
|
287
|
+
isStreaming: false,
|
|
288
|
+
isComplete: true,
|
|
289
|
+
isThinkingStreaming: false,
|
|
290
|
+
isThinkingPaused: false,
|
|
291
|
+
isToolCallsStreaming: false
|
|
292
|
+
};
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
case "cancelled": {
|
|
296
|
+
snapshot[assistantIndex] = {
|
|
297
|
+
...assistant,
|
|
298
|
+
thinking: assistant.thinking?.trimEnd(),
|
|
299
|
+
currentThinking: void 0,
|
|
300
|
+
isStreaming: false,
|
|
301
|
+
isComplete: true,
|
|
302
|
+
isThinkingStreaming: false,
|
|
303
|
+
isThinkingPaused: false,
|
|
304
|
+
isToolCallsStreaming: false
|
|
305
|
+
};
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
case "error": {
|
|
309
|
+
snapshot[assistantIndex] = {
|
|
310
|
+
...assistant,
|
|
311
|
+
content: assistant.content || `Error: ${event.error}`,
|
|
312
|
+
isStreaming: false,
|
|
313
|
+
isComplete: true,
|
|
314
|
+
isThinkingStreaming: false,
|
|
315
|
+
isThinkingPaused: false,
|
|
316
|
+
isToolCallsStreaming: false
|
|
317
|
+
};
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
default:
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
117
324
|
function cloneMessages(messages) {
|
|
118
325
|
return messages.map((message) => ({
|
|
119
326
|
...message,
|
|
@@ -164,6 +371,8 @@ function useChat(options) {
|
|
|
164
371
|
const pendingCancelFnRef = (0, import_react.useRef)(null);
|
|
165
372
|
const suppressStreamRef = (0, import_react.useRef)(false);
|
|
166
373
|
const activeStreamContextRef = (0, import_react.useRef)(null);
|
|
374
|
+
const streamContextsRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
375
|
+
const conversationStreamTokensRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
167
376
|
const currentBlocksRef = (0, import_react.useRef)([]);
|
|
168
377
|
const currentTextBlockIndexRef = (0, import_react.useRef)(-1);
|
|
169
378
|
const needsContentClearRef = (0, import_react.useRef)(false);
|
|
@@ -187,6 +396,13 @@ function useChat(options) {
|
|
|
187
396
|
return () => {
|
|
188
397
|
isMountedRef.current = false;
|
|
189
398
|
cancelRef.current?.();
|
|
399
|
+
streamContextsRef.current.forEach((context) => {
|
|
400
|
+
context.suppressed = true;
|
|
401
|
+
context.terminal = true;
|
|
402
|
+
context.cancel?.();
|
|
403
|
+
});
|
|
404
|
+
streamContextsRef.current.clear();
|
|
405
|
+
conversationStreamTokensRef.current.clear();
|
|
190
406
|
if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
|
|
191
407
|
};
|
|
192
408
|
}, []);
|
|
@@ -264,12 +480,89 @@ function useChat(options) {
|
|
|
264
480
|
map.delete(oldestKey);
|
|
265
481
|
}
|
|
266
482
|
}, []);
|
|
483
|
+
const upsertConversationStreamToken = (0, import_react.useCallback)((id, token) => {
|
|
484
|
+
const map = conversationStreamTokensRef.current;
|
|
485
|
+
const existing = map.get(id);
|
|
486
|
+
if (existing) {
|
|
487
|
+
existing.delete(token);
|
|
488
|
+
existing.add(token);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
map.set(id, /* @__PURE__ */ new Set([token]));
|
|
492
|
+
}, []);
|
|
493
|
+
const removeConversationStreamToken = (0, import_react.useCallback)((id, token) => {
|
|
494
|
+
const map = conversationStreamTokensRef.current;
|
|
495
|
+
const tokens = map.get(id);
|
|
496
|
+
if (!tokens) return;
|
|
497
|
+
tokens.delete(token);
|
|
498
|
+
if (tokens.size === 0) {
|
|
499
|
+
map.delete(id);
|
|
500
|
+
}
|
|
501
|
+
}, []);
|
|
502
|
+
const bindStreamToConversation = (0, import_react.useCallback)((streamContext, id) => {
|
|
503
|
+
const nextConversationId = typeof id === "string" && id.trim().length > 0 ? id : null;
|
|
504
|
+
const previousConversationId = streamContext.conversationId;
|
|
505
|
+
if (previousConversationId && previousConversationId !== nextConversationId) {
|
|
506
|
+
removeConversationStreamToken(previousConversationId, streamContext.token);
|
|
507
|
+
}
|
|
508
|
+
streamContext.conversationId = nextConversationId;
|
|
509
|
+
if (nextConversationId) {
|
|
510
|
+
upsertConversationStreamToken(nextConversationId, streamContext.token);
|
|
511
|
+
}
|
|
512
|
+
}, [removeConversationStreamToken, upsertConversationStreamToken]);
|
|
513
|
+
const touchStreamContext = (0, import_react.useCallback)((streamContext) => {
|
|
514
|
+
streamContext.updatedAt = Date.now();
|
|
515
|
+
const id = streamContext.conversationId;
|
|
516
|
+
if (!id) return;
|
|
517
|
+
upsertConversationStreamToken(id, streamContext.token);
|
|
518
|
+
}, [upsertConversationStreamToken]);
|
|
519
|
+
const getLatestConversationStreamContext = (0, import_react.useCallback)((id) => {
|
|
520
|
+
if (!id) return null;
|
|
521
|
+
const tokenSet = conversationStreamTokensRef.current.get(id);
|
|
522
|
+
if (!tokenSet || tokenSet.size === 0) return null;
|
|
523
|
+
const tokens = Array.from(tokenSet.values()).reverse();
|
|
524
|
+
for (const token of tokens) {
|
|
525
|
+
const context = streamContextsRef.current.get(token);
|
|
526
|
+
if (!context || context.terminal) {
|
|
527
|
+
tokenSet.delete(token);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if (context.suppressed) {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
return context;
|
|
534
|
+
}
|
|
535
|
+
if (tokenSet.size === 0) {
|
|
536
|
+
conversationStreamTokensRef.current.delete(id);
|
|
537
|
+
}
|
|
538
|
+
return null;
|
|
539
|
+
}, []);
|
|
540
|
+
const hasLiveConversationStream = (0, import_react.useCallback)((id) => {
|
|
541
|
+
return getLatestConversationStreamContext(id) !== null;
|
|
542
|
+
}, [getLatestConversationStreamContext]);
|
|
543
|
+
const finalizeStreamContext = (0, import_react.useCallback)((streamContext) => {
|
|
544
|
+
if (streamContext.terminal) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
streamContext.terminal = true;
|
|
548
|
+
if (streamContext.conversationId) {
|
|
549
|
+
removeConversationStreamToken(streamContext.conversationId, streamContext.token);
|
|
550
|
+
}
|
|
551
|
+
streamContextsRef.current.delete(streamContext.token);
|
|
552
|
+
if (activeStreamContextRef.current?.token === streamContext.token) {
|
|
553
|
+
activeStreamContextRef.current = null;
|
|
554
|
+
}
|
|
555
|
+
}, [removeConversationStreamToken]);
|
|
267
556
|
const detachActiveStream = (0, import_react.useCallback)((detachContext) => {
|
|
268
557
|
const context = activeStreamContextRef.current;
|
|
269
|
-
if (!context || context.detached) {
|
|
558
|
+
if (!context || context.detached || context.terminal) {
|
|
270
559
|
return;
|
|
271
560
|
}
|
|
272
|
-
const
|
|
561
|
+
const normalizedLiveMessages = cloneMessages(normalizeConversationMessages(messagesRef.current));
|
|
562
|
+
const liveSnapshotHasAssistant = normalizedLiveMessages.some(
|
|
563
|
+
(message) => message.id === context.assistantMessageId
|
|
564
|
+
);
|
|
565
|
+
const detachedSnapshot = liveSnapshotHasAssistant ? normalizedLiveMessages : context.initialSnapshot ? cloneMessages(context.initialSnapshot) : normalizedLiveMessages;
|
|
273
566
|
const detachedAssistantMessageIndex = detachedSnapshot.findIndex(
|
|
274
567
|
(message) => message.id === context.assistantMessageId
|
|
275
568
|
);
|
|
@@ -295,7 +588,9 @@ function useChat(options) {
|
|
|
295
588
|
setDetachedConversationSnapshot(context.conversationId, detachedSnapshot);
|
|
296
589
|
}
|
|
297
590
|
context.detached = true;
|
|
591
|
+
context.suppressed = false;
|
|
298
592
|
context.detachContext = detachContext;
|
|
593
|
+
touchStreamContext(context);
|
|
299
594
|
if (typewriterIntervalRef.current) {
|
|
300
595
|
clearTimeout(typewriterIntervalRef.current);
|
|
301
596
|
typewriterIntervalRef.current = null;
|
|
@@ -309,7 +604,7 @@ function useChat(options) {
|
|
|
309
604
|
pendingInterruptStreamIdRef.current = null;
|
|
310
605
|
clearPendingCancel();
|
|
311
606
|
suppressStreamRef.current = false;
|
|
312
|
-
}, [clearPendingCancel, resetStreamBuffers, setDetachedConversationSnapshot]);
|
|
607
|
+
}, [clearPendingCancel, resetStreamBuffers, setDetachedConversationSnapshot, touchStreamContext]);
|
|
313
608
|
const refreshConversationIfVisible = (0, import_react.useCallback)(async (id) => {
|
|
314
609
|
if (!id || conversationIdRef.current !== id) return false;
|
|
315
610
|
try {
|
|
@@ -320,12 +615,13 @@ function useChat(options) {
|
|
|
320
615
|
detachedConversationSnapshotsRef.current.delete(id);
|
|
321
616
|
setMessages(normalizedMessages);
|
|
322
617
|
setConversationTitle(detail.title ?? null);
|
|
618
|
+
setIsStreaming(hasLiveConversationStream(id) || normalizedMessages.some((message) => !!message.isStreaming));
|
|
323
619
|
return true;
|
|
324
620
|
} catch (refreshError) {
|
|
325
621
|
console.warn("[useChat] Failed to refresh conversation after detached stream completion:", refreshError);
|
|
326
622
|
return false;
|
|
327
623
|
}
|
|
328
|
-
}, [adapter]);
|
|
624
|
+
}, [adapter, hasLiveConversationStream]);
|
|
329
625
|
const refreshConversationIfVisibleWithRetry = (0, import_react.useCallback)((id, retryDelaysMs) => {
|
|
330
626
|
if (!id) return;
|
|
331
627
|
void (async () => {
|
|
@@ -343,15 +639,49 @@ function useChat(options) {
|
|
|
343
639
|
}
|
|
344
640
|
})();
|
|
345
641
|
}, [refreshConversationIfVisible]);
|
|
642
|
+
const syncDetachedSnapshotToVisibleConversation = (0, import_react.useCallback)((streamContext) => {
|
|
643
|
+
const id = streamContext.conversationId;
|
|
644
|
+
if (!id || conversationIdRef.current !== id) return;
|
|
645
|
+
const activeContext = activeStreamContextRef.current;
|
|
646
|
+
if (activeContext && !activeContext.detached && activeContext.conversationId === id) return;
|
|
647
|
+
if (!streamContext.detachedSnapshot?.length) return;
|
|
648
|
+
const nextMessages = cloneMessages(streamContext.detachedSnapshot);
|
|
649
|
+
setMessages(nextMessages);
|
|
650
|
+
setIsStreaming(
|
|
651
|
+
hasLiveConversationStream(id) || nextMessages.some((message) => message.isStreaming)
|
|
652
|
+
);
|
|
653
|
+
}, [hasLiveConversationStream]);
|
|
346
654
|
const handleStreamEvent = (0, import_react.useCallback)((event, streamContext) => {
|
|
347
655
|
if (!isMountedRef.current) return;
|
|
656
|
+
if (streamContext.terminal) return;
|
|
657
|
+
touchStreamContext(streamContext);
|
|
658
|
+
const isTerminalEvent = event.type === "done" || event.type === "cancelled" || event.type === "error";
|
|
659
|
+
if (streamContext.suppressed && event.type !== "start" && !isTerminalEvent) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
348
662
|
const isActiveStream = activeStreamContextRef.current?.token === streamContext.token && !streamContext.detached;
|
|
349
663
|
if (!isActiveStream) {
|
|
664
|
+
if (event.type === "start" && event.conversationId) {
|
|
665
|
+
if (streamContext.conversationId && streamContext.conversationId !== event.conversationId) {
|
|
666
|
+
detachedConversationSnapshotsRef.current.delete(streamContext.conversationId);
|
|
667
|
+
}
|
|
668
|
+
bindStreamToConversation(streamContext, event.conversationId);
|
|
669
|
+
if (streamContext.detachedSnapshot?.length) {
|
|
670
|
+
setDetachedConversationSnapshot(event.conversationId, streamContext.detachedSnapshot);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const snapshotUpdated = updateDetachedSnapshotForEvent(streamContext, event);
|
|
674
|
+
if (snapshotUpdated && streamContext.conversationId && streamContext.detachedSnapshot?.length) {
|
|
675
|
+
setDetachedConversationSnapshot(streamContext.conversationId, streamContext.detachedSnapshot);
|
|
676
|
+
}
|
|
677
|
+
if (snapshotUpdated) {
|
|
678
|
+
syncDetachedSnapshotToVisibleConversation(streamContext);
|
|
679
|
+
}
|
|
350
680
|
if (event.type === "done" && event.conversationId && !streamContext.didReportConversationChange) {
|
|
351
681
|
if (streamContext.conversationId && streamContext.conversationId !== event.conversationId) {
|
|
352
682
|
detachedConversationSnapshotsRef.current.delete(streamContext.conversationId);
|
|
353
683
|
}
|
|
354
|
-
streamContext
|
|
684
|
+
bindStreamToConversation(streamContext, event.conversationId);
|
|
355
685
|
streamContext.didReportConversationChange = true;
|
|
356
686
|
if (streamContext.detachedSnapshot?.length) {
|
|
357
687
|
setDetachedConversationSnapshot(event.conversationId, streamContext.detachedSnapshot);
|
|
@@ -364,17 +694,19 @@ function useChat(options) {
|
|
|
364
694
|
});
|
|
365
695
|
refreshConversationIfVisibleWithRetry(event.conversationId, [0, 300, 900]);
|
|
366
696
|
} else if (event.type === "done" && event.conversationId && streamContext.didReportConversationChange) {
|
|
697
|
+
bindStreamToConversation(streamContext, event.conversationId);
|
|
367
698
|
if (streamContext.detachedSnapshot?.length) {
|
|
368
699
|
setDetachedConversationSnapshot(event.conversationId, streamContext.detachedSnapshot);
|
|
369
700
|
}
|
|
370
701
|
refreshConversationIfVisibleWithRetry(event.conversationId, [0, 300, 900]);
|
|
371
702
|
}
|
|
372
|
-
const isTerminalEvent = event.type === "done" || event.type === "cancelled" || event.type === "error";
|
|
373
703
|
if (isTerminalEvent) {
|
|
374
704
|
streamContext.detachedSnapshot = void 0;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
705
|
+
streamContext.initialSnapshot = void 0;
|
|
706
|
+
finalizeStreamContext(streamContext);
|
|
707
|
+
if (conversationIdRef.current === streamContext.conversationId) {
|
|
708
|
+
setIsStreaming(hasLiveConversationStream(streamContext.conversationId));
|
|
709
|
+
}
|
|
378
710
|
}
|
|
379
711
|
return;
|
|
380
712
|
}
|
|
@@ -386,6 +718,9 @@ function useChat(options) {
|
|
|
386
718
|
switch (event.type) {
|
|
387
719
|
case "start": {
|
|
388
720
|
currentRunIdRef.current = event.run_id;
|
|
721
|
+
if (event.conversationId) {
|
|
722
|
+
bindStreamToConversation(streamContext, event.conversationId);
|
|
723
|
+
}
|
|
389
724
|
if (pendingCancelRef.current) {
|
|
390
725
|
const pendingCancel = pendingCancelFnRef.current || cancelRef.current;
|
|
391
726
|
if (pendingCancel) {
|
|
@@ -724,7 +1059,7 @@ function useChat(options) {
|
|
|
724
1059
|
});
|
|
725
1060
|
resetStreamBuffers();
|
|
726
1061
|
if (event.conversationId) {
|
|
727
|
-
streamContext
|
|
1062
|
+
bindStreamToConversation(streamContext, event.conversationId);
|
|
728
1063
|
streamContext.didReportConversationChange = true;
|
|
729
1064
|
setConversationId(event.conversationId);
|
|
730
1065
|
onConversationChange?.(event.conversationId, event.title, {
|
|
@@ -734,13 +1069,13 @@ function useChat(options) {
|
|
|
734
1069
|
});
|
|
735
1070
|
}
|
|
736
1071
|
if (event.title) setConversationTitle(event.title);
|
|
737
|
-
setIsStreaming(false);
|
|
738
1072
|
setIsLoading(false);
|
|
739
1073
|
currentRunIdRef.current = null;
|
|
740
1074
|
pendingInterruptStreamIdRef.current = null;
|
|
741
1075
|
suppressStreamRef.current = false;
|
|
742
1076
|
clearPendingCancel();
|
|
743
|
-
|
|
1077
|
+
finalizeStreamContext(streamContext);
|
|
1078
|
+
setIsStreaming(hasLiveConversationStream(conversationIdRef.current));
|
|
744
1079
|
break;
|
|
745
1080
|
}
|
|
746
1081
|
case "cancelled": {
|
|
@@ -771,14 +1106,14 @@ function useChat(options) {
|
|
|
771
1106
|
return updated;
|
|
772
1107
|
});
|
|
773
1108
|
resetStreamBuffers();
|
|
774
|
-
setIsStreaming(false);
|
|
775
1109
|
setIsLoading(false);
|
|
776
1110
|
setPendingInterrupt(null);
|
|
777
1111
|
currentRunIdRef.current = null;
|
|
778
1112
|
pendingInterruptStreamIdRef.current = null;
|
|
779
1113
|
suppressStreamRef.current = false;
|
|
780
1114
|
clearPendingCancel();
|
|
781
|
-
|
|
1115
|
+
finalizeStreamContext(streamContext);
|
|
1116
|
+
setIsStreaming(hasLiveConversationStream(conversationIdRef.current));
|
|
782
1117
|
break;
|
|
783
1118
|
}
|
|
784
1119
|
case "error": {
|
|
@@ -801,13 +1136,13 @@ function useChat(options) {
|
|
|
801
1136
|
resetStreamBuffers();
|
|
802
1137
|
setError(event.error);
|
|
803
1138
|
onError?.(new Error(event.error));
|
|
804
|
-
setIsStreaming(false);
|
|
805
1139
|
setIsLoading(false);
|
|
806
1140
|
currentRunIdRef.current = null;
|
|
807
1141
|
pendingInterruptStreamIdRef.current = null;
|
|
808
1142
|
suppressStreamRef.current = false;
|
|
809
1143
|
clearPendingCancel();
|
|
810
|
-
|
|
1144
|
+
finalizeStreamContext(streamContext);
|
|
1145
|
+
setIsStreaming(hasLiveConversationStream(conversationIdRef.current));
|
|
811
1146
|
break;
|
|
812
1147
|
}
|
|
813
1148
|
case "interrupt": {
|
|
@@ -825,7 +1160,20 @@ function useChat(options) {
|
|
|
825
1160
|
break;
|
|
826
1161
|
}
|
|
827
1162
|
}
|
|
828
|
-
}, [
|
|
1163
|
+
}, [
|
|
1164
|
+
bindStreamToConversation,
|
|
1165
|
+
clearPendingCancel,
|
|
1166
|
+
finalizeStreamContext,
|
|
1167
|
+
flushTypewriter,
|
|
1168
|
+
hasLiveConversationStream,
|
|
1169
|
+
onConversationChange,
|
|
1170
|
+
onError,
|
|
1171
|
+
refreshConversationIfVisibleWithRetry,
|
|
1172
|
+
resetStreamBuffers,
|
|
1173
|
+
setDetachedConversationSnapshot,
|
|
1174
|
+
syncDetachedSnapshotToVisibleConversation,
|
|
1175
|
+
touchStreamContext
|
|
1176
|
+
]);
|
|
829
1177
|
const trySendMessage = (0, import_react.useCallback)(async (content, sendOptions) => {
|
|
830
1178
|
const trimmedContent = content.trim();
|
|
831
1179
|
const hasAttachedResources = (sendOptions?.attachedResources?.length ?? 0) > 0;
|
|
@@ -858,15 +1206,27 @@ function useChat(options) {
|
|
|
858
1206
|
blocks: [],
|
|
859
1207
|
isComplete: false
|
|
860
1208
|
};
|
|
1209
|
+
const shouldRenderUserMessage = trimmedContent.length > 0 || hasAttachedResources;
|
|
1210
|
+
const initialSnapshot = cloneMessages(normalizeConversationMessages(
|
|
1211
|
+
shouldRenderUserMessage ? [...messagesRef.current, userMessage, assistantMessage] : [...messagesRef.current, assistantMessage]
|
|
1212
|
+
));
|
|
861
1213
|
const streamContext = {
|
|
862
1214
|
token: crypto.randomUUID(),
|
|
863
1215
|
assistantMessageId: assistantMessage.id,
|
|
864
1216
|
conversationId: conversationIdRef.current,
|
|
865
1217
|
detached: false,
|
|
1218
|
+
terminal: false,
|
|
1219
|
+
suppressed: false,
|
|
1220
|
+
updatedAt: Date.now(),
|
|
1221
|
+
pendingCancel: false,
|
|
1222
|
+
initialSnapshot,
|
|
866
1223
|
didReportConversationChange: false
|
|
867
1224
|
};
|
|
1225
|
+
streamContextsRef.current.set(streamContext.token, streamContext);
|
|
1226
|
+
if (streamContext.conversationId) {
|
|
1227
|
+
bindStreamToConversation(streamContext, streamContext.conversationId);
|
|
1228
|
+
}
|
|
868
1229
|
activeStreamContextRef.current = streamContext;
|
|
869
|
-
const shouldRenderUserMessage = trimmedContent.length > 0 || hasAttachedResources;
|
|
870
1230
|
setMessages(
|
|
871
1231
|
(prev) => shouldRenderUserMessage ? [...prev, userMessage, assistantMessage] : [...prev, assistantMessage]
|
|
872
1232
|
);
|
|
@@ -895,6 +1255,11 @@ function useChat(options) {
|
|
|
895
1255
|
sessionResources: sessionResourceIds.length ? sessionResourceIds : void 0
|
|
896
1256
|
}
|
|
897
1257
|
);
|
|
1258
|
+
streamContext.cancel = cancel;
|
|
1259
|
+
if (streamContext.pendingCancel) {
|
|
1260
|
+
streamContext.pendingCancel = false;
|
|
1261
|
+
cancel();
|
|
1262
|
+
}
|
|
898
1263
|
cancelRef.current = cancel;
|
|
899
1264
|
if (pendingCancelRef.current) {
|
|
900
1265
|
pendingCancelFnRef.current = cancel;
|
|
@@ -923,56 +1288,132 @@ function useChat(options) {
|
|
|
923
1288
|
pendingInterruptStreamIdRef.current = null;
|
|
924
1289
|
setIsLoading(false);
|
|
925
1290
|
setIsStreaming(false);
|
|
926
|
-
|
|
927
|
-
activeStreamContextRef.current = null;
|
|
928
|
-
}
|
|
1291
|
+
finalizeStreamContext(streamContext);
|
|
929
1292
|
return false;
|
|
930
1293
|
}
|
|
931
|
-
}, [
|
|
1294
|
+
}, [
|
|
1295
|
+
adapter,
|
|
1296
|
+
bindStreamToConversation,
|
|
1297
|
+
clearPendingCancel,
|
|
1298
|
+
finalizeStreamContext,
|
|
1299
|
+
handleStreamEvent,
|
|
1300
|
+
onError,
|
|
1301
|
+
resetStreamBuffers,
|
|
1302
|
+
sessionResources
|
|
1303
|
+
]);
|
|
932
1304
|
const sendMessage = (0, import_react.useCallback)(async (content, sendOptions) => {
|
|
933
1305
|
await trySendMessage(content, sendOptions);
|
|
934
1306
|
}, [trySendMessage]);
|
|
1307
|
+
const getVisibleStreamContext = (0, import_react.useCallback)(() => {
|
|
1308
|
+
const activeContext = activeStreamContextRef.current;
|
|
1309
|
+
if (activeContext && !activeContext.detached && !activeContext.terminal) {
|
|
1310
|
+
return activeContext;
|
|
1311
|
+
}
|
|
1312
|
+
return getLatestConversationStreamContext(conversationIdRef.current);
|
|
1313
|
+
}, [getLatestConversationStreamContext]);
|
|
935
1314
|
const stopStreaming = (0, import_react.useCallback)(() => {
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
clearPendingCancel();
|
|
942
|
-
cancelRef.current = null;
|
|
1315
|
+
const targetContext = getVisibleStreamContext();
|
|
1316
|
+
if (!targetContext) {
|
|
1317
|
+
setIsStreaming(false);
|
|
1318
|
+
setIsLoading(false);
|
|
1319
|
+
return;
|
|
943
1320
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1321
|
+
targetContext.suppressed = true;
|
|
1322
|
+
touchStreamContext(targetContext);
|
|
1323
|
+
const isActiveVisibleStream = activeStreamContextRef.current?.token === targetContext.token && !targetContext.detached;
|
|
1324
|
+
if (isActiveVisibleStream) {
|
|
1325
|
+
pendingCancelRef.current = true;
|
|
1326
|
+
pendingCancelFnRef.current = cancelRef.current;
|
|
1327
|
+
suppressStreamRef.current = true;
|
|
1328
|
+
if (cancelRef.current) {
|
|
1329
|
+
cancelRef.current();
|
|
1330
|
+
clearPendingCancel();
|
|
1331
|
+
cancelRef.current = null;
|
|
1332
|
+
}
|
|
1333
|
+
flushTypewriter();
|
|
1334
|
+
setMessages((prev) => {
|
|
1335
|
+
const last = [...prev].reverse().find((m) => m.role === "assistant");
|
|
1336
|
+
if (!last?.isStreaming) return prev;
|
|
1337
|
+
return prev.map((m) => {
|
|
1338
|
+
if (m.id !== last.id) return m;
|
|
1339
|
+
return {
|
|
1340
|
+
...m,
|
|
1341
|
+
content: fullContentRef.current || m.content,
|
|
1342
|
+
thinking: m.thinking?.trimEnd(),
|
|
1343
|
+
currentThinking: void 0,
|
|
1344
|
+
isStreaming: false,
|
|
1345
|
+
isThinkingStreaming: false,
|
|
1346
|
+
isThinkingPaused: false,
|
|
1347
|
+
isToolCallsStreaming: false,
|
|
1348
|
+
isComplete: true,
|
|
1349
|
+
toolCalls: m.toolCalls?.map(
|
|
1350
|
+
(tc) => tc.status === "running" ? { ...tc, status: "cancelled" } : tc
|
|
1351
|
+
),
|
|
1352
|
+
blocks: [...currentBlocksRef.current]
|
|
1353
|
+
};
|
|
1354
|
+
});
|
|
1355
|
+
});
|
|
1356
|
+
resetStreamBuffers();
|
|
1357
|
+
currentRunIdRef.current = null;
|
|
1358
|
+
pendingInterruptStreamIdRef.current = null;
|
|
1359
|
+
activeStreamContextRef.current = null;
|
|
1360
|
+
setIsStreaming(false);
|
|
1361
|
+
setIsLoading(false);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
suppressStreamRef.current = false;
|
|
1365
|
+
const cancelStream = targetContext.cancel;
|
|
1366
|
+
if (cancelStream) {
|
|
1367
|
+
cancelStream();
|
|
1368
|
+
} else {
|
|
1369
|
+
targetContext.pendingCancel = true;
|
|
1370
|
+
}
|
|
1371
|
+
const detachedSnapshot = targetContext.detachedSnapshot;
|
|
1372
|
+
if (detachedSnapshot?.length) {
|
|
1373
|
+
const assistantIndex = detachedSnapshot.findIndex((message) => message.id === targetContext.assistantMessageId);
|
|
1374
|
+
if (assistantIndex !== -1) {
|
|
1375
|
+
const assistant = detachedSnapshot[assistantIndex];
|
|
1376
|
+
detachedSnapshot[assistantIndex] = {
|
|
1377
|
+
...assistant,
|
|
1378
|
+
thinking: assistant.thinking?.trimEnd(),
|
|
954
1379
|
currentThinking: void 0,
|
|
955
1380
|
isStreaming: false,
|
|
1381
|
+
isComplete: true,
|
|
956
1382
|
isThinkingStreaming: false,
|
|
957
1383
|
isThinkingPaused: false,
|
|
958
1384
|
isToolCallsStreaming: false,
|
|
959
|
-
|
|
960
|
-
toolCalls: m.toolCalls?.map(
|
|
961
|
-
(tc) => tc.status === "running" ? { ...tc, status: "cancelled" } : tc
|
|
962
|
-
),
|
|
963
|
-
blocks: [...currentBlocksRef.current]
|
|
1385
|
+
toolCalls: assistant.toolCalls?.map((tc) => tc.status === "running" ? { ...tc, status: "cancelled" } : tc)
|
|
964
1386
|
};
|
|
965
|
-
}
|
|
1387
|
+
}
|
|
1388
|
+
setDetachedConversationSnapshot(targetContext.conversationId, detachedSnapshot);
|
|
1389
|
+
if (targetContext.conversationId && targetContext.conversationId === conversationIdRef.current) {
|
|
1390
|
+
setMessages(cloneMessages(detachedSnapshot));
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
setIsStreaming(hasLiveConversationStream(conversationIdRef.current));
|
|
1394
|
+
setIsLoading(false);
|
|
1395
|
+
}, [
|
|
1396
|
+
clearPendingCancel,
|
|
1397
|
+
flushTypewriter,
|
|
1398
|
+
getVisibleStreamContext,
|
|
1399
|
+
hasLiveConversationStream,
|
|
1400
|
+
resetStreamBuffers,
|
|
1401
|
+
setDetachedConversationSnapshot,
|
|
1402
|
+
touchStreamContext
|
|
1403
|
+
]);
|
|
1404
|
+
const cancelAllStreamContexts = (0, import_react.useCallback)(() => {
|
|
1405
|
+
streamContextsRef.current.forEach((context) => {
|
|
1406
|
+
context.suppressed = true;
|
|
1407
|
+
context.terminal = true;
|
|
1408
|
+
context.cancel?.();
|
|
966
1409
|
});
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
pendingInterruptStreamIdRef.current = null;
|
|
1410
|
+
streamContextsRef.current.clear();
|
|
1411
|
+
conversationStreamTokensRef.current.clear();
|
|
970
1412
|
activeStreamContextRef.current = null;
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}, [clearPendingCancel, flushTypewriter, resetStreamBuffers]);
|
|
1413
|
+
cancelRef.current = null;
|
|
1414
|
+
}, []);
|
|
974
1415
|
const clearMessages = (0, import_react.useCallback)(() => {
|
|
975
|
-
|
|
1416
|
+
cancelAllStreamContexts();
|
|
976
1417
|
if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
|
|
977
1418
|
setMessages([]);
|
|
978
1419
|
setError(null);
|
|
@@ -981,18 +1422,17 @@ function useChat(options) {
|
|
|
981
1422
|
resetStreamBuffers();
|
|
982
1423
|
currentRunIdRef.current = null;
|
|
983
1424
|
pendingInterruptStreamIdRef.current = null;
|
|
984
|
-
activeStreamContextRef.current = null;
|
|
985
1425
|
clearPendingCancel();
|
|
986
1426
|
suppressStreamRef.current = false;
|
|
987
1427
|
detachedConversationSnapshotsRef.current.clear();
|
|
988
|
-
}, [clearPendingCancel, resetStreamBuffers]);
|
|
1428
|
+
}, [cancelAllStreamContexts, clearPendingCancel, resetStreamBuffers]);
|
|
989
1429
|
const loadConversation = (0, import_react.useCallback)(async (id, optionsArg) => {
|
|
990
1430
|
const cancelActiveStream = optionsArg?.cancelActiveStream ?? true;
|
|
991
|
-
const
|
|
992
|
-
if (
|
|
1431
|
+
const visibleStream = getVisibleStreamContext();
|
|
1432
|
+
if (visibleStream) {
|
|
993
1433
|
if (cancelActiveStream) {
|
|
994
1434
|
stopStreaming();
|
|
995
|
-
} else {
|
|
1435
|
+
} else if (activeStreamContextRef.current && !activeStreamContextRef.current.detached) {
|
|
996
1436
|
detachActiveStream(optionsArg?.detachContext);
|
|
997
1437
|
}
|
|
998
1438
|
}
|
|
@@ -1011,6 +1451,9 @@ function useChat(options) {
|
|
|
1011
1451
|
refreshConversationIfVisibleWithRetry(id, [250, 800, 1800]);
|
|
1012
1452
|
}
|
|
1013
1453
|
setMessages(nextMessages);
|
|
1454
|
+
setIsStreaming(
|
|
1455
|
+
hasLiveConversationStream(detail.id) || nextMessages.some((message) => !!message.isStreaming)
|
|
1456
|
+
);
|
|
1014
1457
|
setConversationId(detail.id);
|
|
1015
1458
|
setConversationTitle(detail.title);
|
|
1016
1459
|
currentAgentIdRef.current = detail.agentId ?? currentAgentIdRef.current;
|
|
@@ -1021,18 +1464,24 @@ function useChat(options) {
|
|
|
1021
1464
|
} finally {
|
|
1022
1465
|
if (isMountedRef.current) setIsLoading(false);
|
|
1023
1466
|
}
|
|
1024
|
-
}, [
|
|
1467
|
+
}, [
|
|
1468
|
+
adapter,
|
|
1469
|
+
detachActiveStream,
|
|
1470
|
+
getVisibleStreamContext,
|
|
1471
|
+
hasLiveConversationStream,
|
|
1472
|
+
onError,
|
|
1473
|
+
refreshConversationIfVisibleWithRetry,
|
|
1474
|
+
stopStreaming
|
|
1475
|
+
]);
|
|
1025
1476
|
const newConversation = (0, import_react.useCallback)((optionsArg) => {
|
|
1026
1477
|
const cancelActiveStream = optionsArg?.cancelActiveStream ?? true;
|
|
1027
|
-
const
|
|
1028
|
-
if (
|
|
1478
|
+
const visibleStream = getVisibleStreamContext();
|
|
1479
|
+
if (visibleStream) {
|
|
1029
1480
|
if (cancelActiveStream) {
|
|
1030
1481
|
stopStreaming();
|
|
1031
|
-
} else {
|
|
1482
|
+
} else if (activeStreamContextRef.current && !activeStreamContextRef.current.detached) {
|
|
1032
1483
|
detachActiveStream(optionsArg?.detachContext);
|
|
1033
1484
|
}
|
|
1034
|
-
} else if (cancelActiveStream) {
|
|
1035
|
-
cancelRef.current?.();
|
|
1036
1485
|
}
|
|
1037
1486
|
if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
|
|
1038
1487
|
setMessages([]);
|
|
@@ -1047,7 +1496,7 @@ function useChat(options) {
|
|
|
1047
1496
|
pendingInterruptStreamIdRef.current = null;
|
|
1048
1497
|
activeStreamContextRef.current = null;
|
|
1049
1498
|
clearPendingCancel();
|
|
1050
|
-
}, [clearPendingCancel, detachActiveStream, resetStreamBuffers, stopStreaming]);
|
|
1499
|
+
}, [clearPendingCancel, detachActiveStream, getVisibleStreamContext, resetStreamBuffers, stopStreaming]);
|
|
1051
1500
|
const sendHitlResponse = (0, import_react.useCallback)((response) => {
|
|
1052
1501
|
const runId = currentRunIdRef.current ?? void 0;
|
|
1053
1502
|
const streamId = pendingInterruptStreamIdRef.current ?? void 0;
|