@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.mjs
CHANGED
|
@@ -26,6 +26,213 @@ function cloneBlocks(blocks) {
|
|
|
26
26
|
toolArgs: block.toolArgs ? { ...block.toolArgs } : block.toolArgs
|
|
27
27
|
}));
|
|
28
28
|
}
|
|
29
|
+
function updateDetachedSnapshotForEvent(streamContext, event) {
|
|
30
|
+
const snapshot = streamContext.detachedSnapshot;
|
|
31
|
+
if (!snapshot?.length) return false;
|
|
32
|
+
const assistantIndex = snapshot.findIndex((message) => message.id === streamContext.assistantMessageId);
|
|
33
|
+
if (assistantIndex === -1) return false;
|
|
34
|
+
const assistant = snapshot[assistantIndex];
|
|
35
|
+
switch (event.type) {
|
|
36
|
+
case "text": {
|
|
37
|
+
const rawContent = event.content;
|
|
38
|
+
if (!rawContent) return false;
|
|
39
|
+
const nextChunk = assistant.content ? rawContent : rawContent.trimStart();
|
|
40
|
+
if (!nextChunk) return false;
|
|
41
|
+
snapshot[assistantIndex] = {
|
|
42
|
+
...assistant,
|
|
43
|
+
content: `${assistant.content}${nextChunk}`,
|
|
44
|
+
isStreaming: true,
|
|
45
|
+
isComplete: false
|
|
46
|
+
};
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
case "thinking": {
|
|
50
|
+
const rawThinking = event.content;
|
|
51
|
+
if (!rawThinking) return false;
|
|
52
|
+
const nextChunk = assistant.thinking ? rawThinking : rawThinking.trimStart();
|
|
53
|
+
if (!nextChunk) return false;
|
|
54
|
+
snapshot[assistantIndex] = {
|
|
55
|
+
...assistant,
|
|
56
|
+
thinking: `${assistant.thinking ?? ""}${nextChunk}`,
|
|
57
|
+
currentThinking: `${assistant.currentThinking ?? ""}${nextChunk}`,
|
|
58
|
+
isStreaming: true,
|
|
59
|
+
isComplete: false,
|
|
60
|
+
isThinkingStreaming: true,
|
|
61
|
+
isThinkingPaused: false
|
|
62
|
+
};
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
case "tool_call": {
|
|
66
|
+
const tc = event.tool_call;
|
|
67
|
+
if (!tc) return false;
|
|
68
|
+
const toolName = tc.function?.name || tc.name || "";
|
|
69
|
+
const toolId = tc.id || "";
|
|
70
|
+
let args = {};
|
|
71
|
+
if (tc.args) {
|
|
72
|
+
args = tc.args;
|
|
73
|
+
} else if (tc.function?.arguments) {
|
|
74
|
+
try {
|
|
75
|
+
args = JSON.parse(tc.function.arguments);
|
|
76
|
+
} catch {
|
|
77
|
+
args = {};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const toolCalls = [...assistant.toolCalls || []];
|
|
81
|
+
toolCalls.push({
|
|
82
|
+
id: toolId,
|
|
83
|
+
name: toolName,
|
|
84
|
+
args,
|
|
85
|
+
status: "running"
|
|
86
|
+
});
|
|
87
|
+
const blocks = cloneBlocks(assistant.blocks) || [];
|
|
88
|
+
blocks.push({
|
|
89
|
+
type: "tool_call",
|
|
90
|
+
content: "",
|
|
91
|
+
timestamp: Date.now(),
|
|
92
|
+
toolName,
|
|
93
|
+
toolArgs: args,
|
|
94
|
+
toolCallId: toolId,
|
|
95
|
+
toolStatus: "running",
|
|
96
|
+
isIntermediate: true
|
|
97
|
+
});
|
|
98
|
+
snapshot[assistantIndex] = {
|
|
99
|
+
...assistant,
|
|
100
|
+
toolCalls,
|
|
101
|
+
blocks,
|
|
102
|
+
isStreaming: true,
|
|
103
|
+
isComplete: false,
|
|
104
|
+
isThinkingStreaming: false,
|
|
105
|
+
isThinkingPaused: !!assistant.thinking,
|
|
106
|
+
isToolCallsStreaming: true
|
|
107
|
+
};
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
case "tool_args_chunk": {
|
|
111
|
+
const chunk = event.chunk;
|
|
112
|
+
if (!chunk) return false;
|
|
113
|
+
const toolCalls = [...assistant.toolCalls || []];
|
|
114
|
+
const toolCallIndex = findToolCallIndex(toolCalls, event.tool_call_id, event.tool_name);
|
|
115
|
+
if (toolCallIndex === -1) return false;
|
|
116
|
+
const existingRaw = toolCalls[toolCallIndex].argsRaw || "";
|
|
117
|
+
toolCalls[toolCallIndex] = {
|
|
118
|
+
...toolCalls[toolCallIndex],
|
|
119
|
+
argsRaw: `${existingRaw}${chunk}`
|
|
120
|
+
};
|
|
121
|
+
const blocks = cloneBlocks(assistant.blocks) || [];
|
|
122
|
+
const blockIndex = blocks.findIndex(
|
|
123
|
+
(block) => block.type === "tool_call" && (block.toolCallId === event.tool_call_id || !event.tool_call_id && event.tool_name && block.toolName === event.tool_name)
|
|
124
|
+
);
|
|
125
|
+
if (blockIndex !== -1) {
|
|
126
|
+
blocks[blockIndex] = {
|
|
127
|
+
...blocks[blockIndex],
|
|
128
|
+
toolArgsRaw: `${blocks[blockIndex].toolArgsRaw || ""}${chunk}`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
snapshot[assistantIndex] = {
|
|
132
|
+
...assistant,
|
|
133
|
+
toolCalls,
|
|
134
|
+
blocks
|
|
135
|
+
};
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
case "tool_args": {
|
|
139
|
+
const toolCalls = [...assistant.toolCalls || []];
|
|
140
|
+
const toolCallIndex = findToolCallIndex(toolCalls, event.tool_call_id, event.tool_name);
|
|
141
|
+
if (toolCallIndex === -1) return false;
|
|
142
|
+
toolCalls[toolCallIndex] = {
|
|
143
|
+
...toolCalls[toolCallIndex],
|
|
144
|
+
args: event.args || {},
|
|
145
|
+
argsRaw: void 0
|
|
146
|
+
};
|
|
147
|
+
const blocks = cloneBlocks(assistant.blocks) || [];
|
|
148
|
+
const blockIndex = blocks.findIndex(
|
|
149
|
+
(block) => block.type === "tool_call" && (block.toolCallId === event.tool_call_id || !event.tool_call_id && event.tool_name && block.toolName === event.tool_name)
|
|
150
|
+
);
|
|
151
|
+
if (blockIndex !== -1) {
|
|
152
|
+
blocks[blockIndex] = {
|
|
153
|
+
...blocks[blockIndex],
|
|
154
|
+
toolArgs: event.args || {},
|
|
155
|
+
toolArgsRaw: void 0
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
snapshot[assistantIndex] = {
|
|
159
|
+
...assistant,
|
|
160
|
+
toolCalls,
|
|
161
|
+
blocks
|
|
162
|
+
};
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
case "tool_result": {
|
|
166
|
+
const toolCalls = (assistant.toolCalls || []).map((toolCall) => toolCall.id === event.tool_call_id ? { ...toolCall, status: "completed", result: event.result } : toolCall);
|
|
167
|
+
const hasRunning = toolCalls.some((toolCall) => toolCall.status === "running");
|
|
168
|
+
const blocks = cloneBlocks(assistant.blocks) || [];
|
|
169
|
+
const toolBlockIndex = blocks.findIndex(
|
|
170
|
+
(block) => block.type === "tool_call" && block.toolCallId === event.tool_call_id
|
|
171
|
+
);
|
|
172
|
+
if (toolBlockIndex !== -1) {
|
|
173
|
+
blocks[toolBlockIndex] = {
|
|
174
|
+
...blocks[toolBlockIndex],
|
|
175
|
+
toolStatus: "completed"
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
blocks.push({
|
|
179
|
+
type: "tool_result",
|
|
180
|
+
content: typeof event.result === "string" ? event.result : JSON.stringify(event.result),
|
|
181
|
+
timestamp: Date.now(),
|
|
182
|
+
toolName: toolBlockIndex !== -1 ? blocks[toolBlockIndex].toolName : void 0,
|
|
183
|
+
toolCallId: event.tool_call_id,
|
|
184
|
+
isIntermediate: true
|
|
185
|
+
});
|
|
186
|
+
snapshot[assistantIndex] = {
|
|
187
|
+
...assistant,
|
|
188
|
+
toolCalls,
|
|
189
|
+
blocks,
|
|
190
|
+
isToolCallsStreaming: hasRunning
|
|
191
|
+
};
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
case "done": {
|
|
195
|
+
snapshot[assistantIndex] = {
|
|
196
|
+
...assistant,
|
|
197
|
+
thinking: assistant.thinking?.trimEnd(),
|
|
198
|
+
currentThinking: void 0,
|
|
199
|
+
isStreaming: false,
|
|
200
|
+
isComplete: true,
|
|
201
|
+
isThinkingStreaming: false,
|
|
202
|
+
isThinkingPaused: false,
|
|
203
|
+
isToolCallsStreaming: false
|
|
204
|
+
};
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
case "cancelled": {
|
|
208
|
+
snapshot[assistantIndex] = {
|
|
209
|
+
...assistant,
|
|
210
|
+
thinking: assistant.thinking?.trimEnd(),
|
|
211
|
+
currentThinking: void 0,
|
|
212
|
+
isStreaming: false,
|
|
213
|
+
isComplete: true,
|
|
214
|
+
isThinkingStreaming: false,
|
|
215
|
+
isThinkingPaused: false,
|
|
216
|
+
isToolCallsStreaming: false
|
|
217
|
+
};
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
case "error": {
|
|
221
|
+
snapshot[assistantIndex] = {
|
|
222
|
+
...assistant,
|
|
223
|
+
content: assistant.content || `Error: ${event.error}`,
|
|
224
|
+
isStreaming: false,
|
|
225
|
+
isComplete: true,
|
|
226
|
+
isThinkingStreaming: false,
|
|
227
|
+
isThinkingPaused: false,
|
|
228
|
+
isToolCallsStreaming: false
|
|
229
|
+
};
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
default:
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
29
236
|
function cloneMessages(messages) {
|
|
30
237
|
return messages.map((message) => ({
|
|
31
238
|
...message,
|
|
@@ -76,6 +283,8 @@ function useChat(options) {
|
|
|
76
283
|
const pendingCancelFnRef = useRef(null);
|
|
77
284
|
const suppressStreamRef = useRef(false);
|
|
78
285
|
const activeStreamContextRef = useRef(null);
|
|
286
|
+
const streamContextsRef = useRef(/* @__PURE__ */ new Map());
|
|
287
|
+
const conversationStreamTokensRef = useRef(/* @__PURE__ */ new Map());
|
|
79
288
|
const currentBlocksRef = useRef([]);
|
|
80
289
|
const currentTextBlockIndexRef = useRef(-1);
|
|
81
290
|
const needsContentClearRef = useRef(false);
|
|
@@ -99,6 +308,13 @@ function useChat(options) {
|
|
|
99
308
|
return () => {
|
|
100
309
|
isMountedRef.current = false;
|
|
101
310
|
cancelRef.current?.();
|
|
311
|
+
streamContextsRef.current.forEach((context) => {
|
|
312
|
+
context.suppressed = true;
|
|
313
|
+
context.terminal = true;
|
|
314
|
+
context.cancel?.();
|
|
315
|
+
});
|
|
316
|
+
streamContextsRef.current.clear();
|
|
317
|
+
conversationStreamTokensRef.current.clear();
|
|
102
318
|
if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
|
|
103
319
|
};
|
|
104
320
|
}, []);
|
|
@@ -176,12 +392,89 @@ function useChat(options) {
|
|
|
176
392
|
map.delete(oldestKey);
|
|
177
393
|
}
|
|
178
394
|
}, []);
|
|
395
|
+
const upsertConversationStreamToken = useCallback((id, token) => {
|
|
396
|
+
const map = conversationStreamTokensRef.current;
|
|
397
|
+
const existing = map.get(id);
|
|
398
|
+
if (existing) {
|
|
399
|
+
existing.delete(token);
|
|
400
|
+
existing.add(token);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
map.set(id, /* @__PURE__ */ new Set([token]));
|
|
404
|
+
}, []);
|
|
405
|
+
const removeConversationStreamToken = useCallback((id, token) => {
|
|
406
|
+
const map = conversationStreamTokensRef.current;
|
|
407
|
+
const tokens = map.get(id);
|
|
408
|
+
if (!tokens) return;
|
|
409
|
+
tokens.delete(token);
|
|
410
|
+
if (tokens.size === 0) {
|
|
411
|
+
map.delete(id);
|
|
412
|
+
}
|
|
413
|
+
}, []);
|
|
414
|
+
const bindStreamToConversation = useCallback((streamContext, id) => {
|
|
415
|
+
const nextConversationId = typeof id === "string" && id.trim().length > 0 ? id : null;
|
|
416
|
+
const previousConversationId = streamContext.conversationId;
|
|
417
|
+
if (previousConversationId && previousConversationId !== nextConversationId) {
|
|
418
|
+
removeConversationStreamToken(previousConversationId, streamContext.token);
|
|
419
|
+
}
|
|
420
|
+
streamContext.conversationId = nextConversationId;
|
|
421
|
+
if (nextConversationId) {
|
|
422
|
+
upsertConversationStreamToken(nextConversationId, streamContext.token);
|
|
423
|
+
}
|
|
424
|
+
}, [removeConversationStreamToken, upsertConversationStreamToken]);
|
|
425
|
+
const touchStreamContext = useCallback((streamContext) => {
|
|
426
|
+
streamContext.updatedAt = Date.now();
|
|
427
|
+
const id = streamContext.conversationId;
|
|
428
|
+
if (!id) return;
|
|
429
|
+
upsertConversationStreamToken(id, streamContext.token);
|
|
430
|
+
}, [upsertConversationStreamToken]);
|
|
431
|
+
const getLatestConversationStreamContext = useCallback((id) => {
|
|
432
|
+
if (!id) return null;
|
|
433
|
+
const tokenSet = conversationStreamTokensRef.current.get(id);
|
|
434
|
+
if (!tokenSet || tokenSet.size === 0) return null;
|
|
435
|
+
const tokens = Array.from(tokenSet.values()).reverse();
|
|
436
|
+
for (const token of tokens) {
|
|
437
|
+
const context = streamContextsRef.current.get(token);
|
|
438
|
+
if (!context || context.terminal) {
|
|
439
|
+
tokenSet.delete(token);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (context.suppressed) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
return context;
|
|
446
|
+
}
|
|
447
|
+
if (tokenSet.size === 0) {
|
|
448
|
+
conversationStreamTokensRef.current.delete(id);
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
451
|
+
}, []);
|
|
452
|
+
const hasLiveConversationStream = useCallback((id) => {
|
|
453
|
+
return getLatestConversationStreamContext(id) !== null;
|
|
454
|
+
}, [getLatestConversationStreamContext]);
|
|
455
|
+
const finalizeStreamContext = useCallback((streamContext) => {
|
|
456
|
+
if (streamContext.terminal) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
streamContext.terminal = true;
|
|
460
|
+
if (streamContext.conversationId) {
|
|
461
|
+
removeConversationStreamToken(streamContext.conversationId, streamContext.token);
|
|
462
|
+
}
|
|
463
|
+
streamContextsRef.current.delete(streamContext.token);
|
|
464
|
+
if (activeStreamContextRef.current?.token === streamContext.token) {
|
|
465
|
+
activeStreamContextRef.current = null;
|
|
466
|
+
}
|
|
467
|
+
}, [removeConversationStreamToken]);
|
|
179
468
|
const detachActiveStream = useCallback((detachContext) => {
|
|
180
469
|
const context = activeStreamContextRef.current;
|
|
181
|
-
if (!context || context.detached) {
|
|
470
|
+
if (!context || context.detached || context.terminal) {
|
|
182
471
|
return;
|
|
183
472
|
}
|
|
184
|
-
const
|
|
473
|
+
const normalizedLiveMessages = cloneMessages(normalizeConversationMessages(messagesRef.current));
|
|
474
|
+
const liveSnapshotHasAssistant = normalizedLiveMessages.some(
|
|
475
|
+
(message) => message.id === context.assistantMessageId
|
|
476
|
+
);
|
|
477
|
+
const detachedSnapshot = liveSnapshotHasAssistant ? normalizedLiveMessages : context.initialSnapshot ? cloneMessages(context.initialSnapshot) : normalizedLiveMessages;
|
|
185
478
|
const detachedAssistantMessageIndex = detachedSnapshot.findIndex(
|
|
186
479
|
(message) => message.id === context.assistantMessageId
|
|
187
480
|
);
|
|
@@ -207,7 +500,9 @@ function useChat(options) {
|
|
|
207
500
|
setDetachedConversationSnapshot(context.conversationId, detachedSnapshot);
|
|
208
501
|
}
|
|
209
502
|
context.detached = true;
|
|
503
|
+
context.suppressed = false;
|
|
210
504
|
context.detachContext = detachContext;
|
|
505
|
+
touchStreamContext(context);
|
|
211
506
|
if (typewriterIntervalRef.current) {
|
|
212
507
|
clearTimeout(typewriterIntervalRef.current);
|
|
213
508
|
typewriterIntervalRef.current = null;
|
|
@@ -221,7 +516,7 @@ function useChat(options) {
|
|
|
221
516
|
pendingInterruptStreamIdRef.current = null;
|
|
222
517
|
clearPendingCancel();
|
|
223
518
|
suppressStreamRef.current = false;
|
|
224
|
-
}, [clearPendingCancel, resetStreamBuffers, setDetachedConversationSnapshot]);
|
|
519
|
+
}, [clearPendingCancel, resetStreamBuffers, setDetachedConversationSnapshot, touchStreamContext]);
|
|
225
520
|
const refreshConversationIfVisible = useCallback(async (id) => {
|
|
226
521
|
if (!id || conversationIdRef.current !== id) return false;
|
|
227
522
|
try {
|
|
@@ -232,12 +527,13 @@ function useChat(options) {
|
|
|
232
527
|
detachedConversationSnapshotsRef.current.delete(id);
|
|
233
528
|
setMessages(normalizedMessages);
|
|
234
529
|
setConversationTitle(detail.title ?? null);
|
|
530
|
+
setIsStreaming(hasLiveConversationStream(id) || normalizedMessages.some((message) => !!message.isStreaming));
|
|
235
531
|
return true;
|
|
236
532
|
} catch (refreshError) {
|
|
237
533
|
console.warn("[useChat] Failed to refresh conversation after detached stream completion:", refreshError);
|
|
238
534
|
return false;
|
|
239
535
|
}
|
|
240
|
-
}, [adapter]);
|
|
536
|
+
}, [adapter, hasLiveConversationStream]);
|
|
241
537
|
const refreshConversationIfVisibleWithRetry = useCallback((id, retryDelaysMs) => {
|
|
242
538
|
if (!id) return;
|
|
243
539
|
void (async () => {
|
|
@@ -255,15 +551,49 @@ function useChat(options) {
|
|
|
255
551
|
}
|
|
256
552
|
})();
|
|
257
553
|
}, [refreshConversationIfVisible]);
|
|
554
|
+
const syncDetachedSnapshotToVisibleConversation = useCallback((streamContext) => {
|
|
555
|
+
const id = streamContext.conversationId;
|
|
556
|
+
if (!id || conversationIdRef.current !== id) return;
|
|
557
|
+
const activeContext = activeStreamContextRef.current;
|
|
558
|
+
if (activeContext && !activeContext.detached && activeContext.conversationId === id) return;
|
|
559
|
+
if (!streamContext.detachedSnapshot?.length) return;
|
|
560
|
+
const nextMessages = cloneMessages(streamContext.detachedSnapshot);
|
|
561
|
+
setMessages(nextMessages);
|
|
562
|
+
setIsStreaming(
|
|
563
|
+
hasLiveConversationStream(id) || nextMessages.some((message) => message.isStreaming)
|
|
564
|
+
);
|
|
565
|
+
}, [hasLiveConversationStream]);
|
|
258
566
|
const handleStreamEvent = useCallback((event, streamContext) => {
|
|
259
567
|
if (!isMountedRef.current) return;
|
|
568
|
+
if (streamContext.terminal) return;
|
|
569
|
+
touchStreamContext(streamContext);
|
|
570
|
+
const isTerminalEvent = event.type === "done" || event.type === "cancelled" || event.type === "error";
|
|
571
|
+
if (streamContext.suppressed && event.type !== "start" && !isTerminalEvent) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
260
574
|
const isActiveStream = activeStreamContextRef.current?.token === streamContext.token && !streamContext.detached;
|
|
261
575
|
if (!isActiveStream) {
|
|
576
|
+
if (event.type === "start" && event.conversationId) {
|
|
577
|
+
if (streamContext.conversationId && streamContext.conversationId !== event.conversationId) {
|
|
578
|
+
detachedConversationSnapshotsRef.current.delete(streamContext.conversationId);
|
|
579
|
+
}
|
|
580
|
+
bindStreamToConversation(streamContext, event.conversationId);
|
|
581
|
+
if (streamContext.detachedSnapshot?.length) {
|
|
582
|
+
setDetachedConversationSnapshot(event.conversationId, streamContext.detachedSnapshot);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const snapshotUpdated = updateDetachedSnapshotForEvent(streamContext, event);
|
|
586
|
+
if (snapshotUpdated && streamContext.conversationId && streamContext.detachedSnapshot?.length) {
|
|
587
|
+
setDetachedConversationSnapshot(streamContext.conversationId, streamContext.detachedSnapshot);
|
|
588
|
+
}
|
|
589
|
+
if (snapshotUpdated) {
|
|
590
|
+
syncDetachedSnapshotToVisibleConversation(streamContext);
|
|
591
|
+
}
|
|
262
592
|
if (event.type === "done" && event.conversationId && !streamContext.didReportConversationChange) {
|
|
263
593
|
if (streamContext.conversationId && streamContext.conversationId !== event.conversationId) {
|
|
264
594
|
detachedConversationSnapshotsRef.current.delete(streamContext.conversationId);
|
|
265
595
|
}
|
|
266
|
-
streamContext
|
|
596
|
+
bindStreamToConversation(streamContext, event.conversationId);
|
|
267
597
|
streamContext.didReportConversationChange = true;
|
|
268
598
|
if (streamContext.detachedSnapshot?.length) {
|
|
269
599
|
setDetachedConversationSnapshot(event.conversationId, streamContext.detachedSnapshot);
|
|
@@ -276,17 +606,19 @@ function useChat(options) {
|
|
|
276
606
|
});
|
|
277
607
|
refreshConversationIfVisibleWithRetry(event.conversationId, [0, 300, 900]);
|
|
278
608
|
} else if (event.type === "done" && event.conversationId && streamContext.didReportConversationChange) {
|
|
609
|
+
bindStreamToConversation(streamContext, event.conversationId);
|
|
279
610
|
if (streamContext.detachedSnapshot?.length) {
|
|
280
611
|
setDetachedConversationSnapshot(event.conversationId, streamContext.detachedSnapshot);
|
|
281
612
|
}
|
|
282
613
|
refreshConversationIfVisibleWithRetry(event.conversationId, [0, 300, 900]);
|
|
283
614
|
}
|
|
284
|
-
const isTerminalEvent = event.type === "done" || event.type === "cancelled" || event.type === "error";
|
|
285
615
|
if (isTerminalEvent) {
|
|
286
616
|
streamContext.detachedSnapshot = void 0;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
617
|
+
streamContext.initialSnapshot = void 0;
|
|
618
|
+
finalizeStreamContext(streamContext);
|
|
619
|
+
if (conversationIdRef.current === streamContext.conversationId) {
|
|
620
|
+
setIsStreaming(hasLiveConversationStream(streamContext.conversationId));
|
|
621
|
+
}
|
|
290
622
|
}
|
|
291
623
|
return;
|
|
292
624
|
}
|
|
@@ -298,6 +630,9 @@ function useChat(options) {
|
|
|
298
630
|
switch (event.type) {
|
|
299
631
|
case "start": {
|
|
300
632
|
currentRunIdRef.current = event.run_id;
|
|
633
|
+
if (event.conversationId) {
|
|
634
|
+
bindStreamToConversation(streamContext, event.conversationId);
|
|
635
|
+
}
|
|
301
636
|
if (pendingCancelRef.current) {
|
|
302
637
|
const pendingCancel = pendingCancelFnRef.current || cancelRef.current;
|
|
303
638
|
if (pendingCancel) {
|
|
@@ -636,7 +971,7 @@ function useChat(options) {
|
|
|
636
971
|
});
|
|
637
972
|
resetStreamBuffers();
|
|
638
973
|
if (event.conversationId) {
|
|
639
|
-
streamContext
|
|
974
|
+
bindStreamToConversation(streamContext, event.conversationId);
|
|
640
975
|
streamContext.didReportConversationChange = true;
|
|
641
976
|
setConversationId(event.conversationId);
|
|
642
977
|
onConversationChange?.(event.conversationId, event.title, {
|
|
@@ -646,13 +981,13 @@ function useChat(options) {
|
|
|
646
981
|
});
|
|
647
982
|
}
|
|
648
983
|
if (event.title) setConversationTitle(event.title);
|
|
649
|
-
setIsStreaming(false);
|
|
650
984
|
setIsLoading(false);
|
|
651
985
|
currentRunIdRef.current = null;
|
|
652
986
|
pendingInterruptStreamIdRef.current = null;
|
|
653
987
|
suppressStreamRef.current = false;
|
|
654
988
|
clearPendingCancel();
|
|
655
|
-
|
|
989
|
+
finalizeStreamContext(streamContext);
|
|
990
|
+
setIsStreaming(hasLiveConversationStream(conversationIdRef.current));
|
|
656
991
|
break;
|
|
657
992
|
}
|
|
658
993
|
case "cancelled": {
|
|
@@ -683,14 +1018,14 @@ function useChat(options) {
|
|
|
683
1018
|
return updated;
|
|
684
1019
|
});
|
|
685
1020
|
resetStreamBuffers();
|
|
686
|
-
setIsStreaming(false);
|
|
687
1021
|
setIsLoading(false);
|
|
688
1022
|
setPendingInterrupt(null);
|
|
689
1023
|
currentRunIdRef.current = null;
|
|
690
1024
|
pendingInterruptStreamIdRef.current = null;
|
|
691
1025
|
suppressStreamRef.current = false;
|
|
692
1026
|
clearPendingCancel();
|
|
693
|
-
|
|
1027
|
+
finalizeStreamContext(streamContext);
|
|
1028
|
+
setIsStreaming(hasLiveConversationStream(conversationIdRef.current));
|
|
694
1029
|
break;
|
|
695
1030
|
}
|
|
696
1031
|
case "error": {
|
|
@@ -713,13 +1048,13 @@ function useChat(options) {
|
|
|
713
1048
|
resetStreamBuffers();
|
|
714
1049
|
setError(event.error);
|
|
715
1050
|
onError?.(new Error(event.error));
|
|
716
|
-
setIsStreaming(false);
|
|
717
1051
|
setIsLoading(false);
|
|
718
1052
|
currentRunIdRef.current = null;
|
|
719
1053
|
pendingInterruptStreamIdRef.current = null;
|
|
720
1054
|
suppressStreamRef.current = false;
|
|
721
1055
|
clearPendingCancel();
|
|
722
|
-
|
|
1056
|
+
finalizeStreamContext(streamContext);
|
|
1057
|
+
setIsStreaming(hasLiveConversationStream(conversationIdRef.current));
|
|
723
1058
|
break;
|
|
724
1059
|
}
|
|
725
1060
|
case "interrupt": {
|
|
@@ -737,7 +1072,20 @@ function useChat(options) {
|
|
|
737
1072
|
break;
|
|
738
1073
|
}
|
|
739
1074
|
}
|
|
740
|
-
}, [
|
|
1075
|
+
}, [
|
|
1076
|
+
bindStreamToConversation,
|
|
1077
|
+
clearPendingCancel,
|
|
1078
|
+
finalizeStreamContext,
|
|
1079
|
+
flushTypewriter,
|
|
1080
|
+
hasLiveConversationStream,
|
|
1081
|
+
onConversationChange,
|
|
1082
|
+
onError,
|
|
1083
|
+
refreshConversationIfVisibleWithRetry,
|
|
1084
|
+
resetStreamBuffers,
|
|
1085
|
+
setDetachedConversationSnapshot,
|
|
1086
|
+
syncDetachedSnapshotToVisibleConversation,
|
|
1087
|
+
touchStreamContext
|
|
1088
|
+
]);
|
|
741
1089
|
const trySendMessage = useCallback(async (content, sendOptions) => {
|
|
742
1090
|
const trimmedContent = content.trim();
|
|
743
1091
|
const hasAttachedResources = (sendOptions?.attachedResources?.length ?? 0) > 0;
|
|
@@ -770,15 +1118,27 @@ function useChat(options) {
|
|
|
770
1118
|
blocks: [],
|
|
771
1119
|
isComplete: false
|
|
772
1120
|
};
|
|
1121
|
+
const shouldRenderUserMessage = trimmedContent.length > 0 || hasAttachedResources;
|
|
1122
|
+
const initialSnapshot = cloneMessages(normalizeConversationMessages(
|
|
1123
|
+
shouldRenderUserMessage ? [...messagesRef.current, userMessage, assistantMessage] : [...messagesRef.current, assistantMessage]
|
|
1124
|
+
));
|
|
773
1125
|
const streamContext = {
|
|
774
1126
|
token: crypto.randomUUID(),
|
|
775
1127
|
assistantMessageId: assistantMessage.id,
|
|
776
1128
|
conversationId: conversationIdRef.current,
|
|
777
1129
|
detached: false,
|
|
1130
|
+
terminal: false,
|
|
1131
|
+
suppressed: false,
|
|
1132
|
+
updatedAt: Date.now(),
|
|
1133
|
+
pendingCancel: false,
|
|
1134
|
+
initialSnapshot,
|
|
778
1135
|
didReportConversationChange: false
|
|
779
1136
|
};
|
|
1137
|
+
streamContextsRef.current.set(streamContext.token, streamContext);
|
|
1138
|
+
if (streamContext.conversationId) {
|
|
1139
|
+
bindStreamToConversation(streamContext, streamContext.conversationId);
|
|
1140
|
+
}
|
|
780
1141
|
activeStreamContextRef.current = streamContext;
|
|
781
|
-
const shouldRenderUserMessage = trimmedContent.length > 0 || hasAttachedResources;
|
|
782
1142
|
setMessages(
|
|
783
1143
|
(prev) => shouldRenderUserMessage ? [...prev, userMessage, assistantMessage] : [...prev, assistantMessage]
|
|
784
1144
|
);
|
|
@@ -807,6 +1167,11 @@ function useChat(options) {
|
|
|
807
1167
|
sessionResources: sessionResourceIds.length ? sessionResourceIds : void 0
|
|
808
1168
|
}
|
|
809
1169
|
);
|
|
1170
|
+
streamContext.cancel = cancel;
|
|
1171
|
+
if (streamContext.pendingCancel) {
|
|
1172
|
+
streamContext.pendingCancel = false;
|
|
1173
|
+
cancel();
|
|
1174
|
+
}
|
|
810
1175
|
cancelRef.current = cancel;
|
|
811
1176
|
if (pendingCancelRef.current) {
|
|
812
1177
|
pendingCancelFnRef.current = cancel;
|
|
@@ -835,56 +1200,132 @@ function useChat(options) {
|
|
|
835
1200
|
pendingInterruptStreamIdRef.current = null;
|
|
836
1201
|
setIsLoading(false);
|
|
837
1202
|
setIsStreaming(false);
|
|
838
|
-
|
|
839
|
-
activeStreamContextRef.current = null;
|
|
840
|
-
}
|
|
1203
|
+
finalizeStreamContext(streamContext);
|
|
841
1204
|
return false;
|
|
842
1205
|
}
|
|
843
|
-
}, [
|
|
1206
|
+
}, [
|
|
1207
|
+
adapter,
|
|
1208
|
+
bindStreamToConversation,
|
|
1209
|
+
clearPendingCancel,
|
|
1210
|
+
finalizeStreamContext,
|
|
1211
|
+
handleStreamEvent,
|
|
1212
|
+
onError,
|
|
1213
|
+
resetStreamBuffers,
|
|
1214
|
+
sessionResources
|
|
1215
|
+
]);
|
|
844
1216
|
const sendMessage = useCallback(async (content, sendOptions) => {
|
|
845
1217
|
await trySendMessage(content, sendOptions);
|
|
846
1218
|
}, [trySendMessage]);
|
|
1219
|
+
const getVisibleStreamContext = useCallback(() => {
|
|
1220
|
+
const activeContext = activeStreamContextRef.current;
|
|
1221
|
+
if (activeContext && !activeContext.detached && !activeContext.terminal) {
|
|
1222
|
+
return activeContext;
|
|
1223
|
+
}
|
|
1224
|
+
return getLatestConversationStreamContext(conversationIdRef.current);
|
|
1225
|
+
}, [getLatestConversationStreamContext]);
|
|
847
1226
|
const stopStreaming = useCallback(() => {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
clearPendingCancel();
|
|
854
|
-
cancelRef.current = null;
|
|
1227
|
+
const targetContext = getVisibleStreamContext();
|
|
1228
|
+
if (!targetContext) {
|
|
1229
|
+
setIsStreaming(false);
|
|
1230
|
+
setIsLoading(false);
|
|
1231
|
+
return;
|
|
855
1232
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1233
|
+
targetContext.suppressed = true;
|
|
1234
|
+
touchStreamContext(targetContext);
|
|
1235
|
+
const isActiveVisibleStream = activeStreamContextRef.current?.token === targetContext.token && !targetContext.detached;
|
|
1236
|
+
if (isActiveVisibleStream) {
|
|
1237
|
+
pendingCancelRef.current = true;
|
|
1238
|
+
pendingCancelFnRef.current = cancelRef.current;
|
|
1239
|
+
suppressStreamRef.current = true;
|
|
1240
|
+
if (cancelRef.current) {
|
|
1241
|
+
cancelRef.current();
|
|
1242
|
+
clearPendingCancel();
|
|
1243
|
+
cancelRef.current = null;
|
|
1244
|
+
}
|
|
1245
|
+
flushTypewriter();
|
|
1246
|
+
setMessages((prev) => {
|
|
1247
|
+
const last = [...prev].reverse().find((m) => m.role === "assistant");
|
|
1248
|
+
if (!last?.isStreaming) return prev;
|
|
1249
|
+
return prev.map((m) => {
|
|
1250
|
+
if (m.id !== last.id) return m;
|
|
1251
|
+
return {
|
|
1252
|
+
...m,
|
|
1253
|
+
content: fullContentRef.current || m.content,
|
|
1254
|
+
thinking: m.thinking?.trimEnd(),
|
|
1255
|
+
currentThinking: void 0,
|
|
1256
|
+
isStreaming: false,
|
|
1257
|
+
isThinkingStreaming: false,
|
|
1258
|
+
isThinkingPaused: false,
|
|
1259
|
+
isToolCallsStreaming: false,
|
|
1260
|
+
isComplete: true,
|
|
1261
|
+
toolCalls: m.toolCalls?.map(
|
|
1262
|
+
(tc) => tc.status === "running" ? { ...tc, status: "cancelled" } : tc
|
|
1263
|
+
),
|
|
1264
|
+
blocks: [...currentBlocksRef.current]
|
|
1265
|
+
};
|
|
1266
|
+
});
|
|
1267
|
+
});
|
|
1268
|
+
resetStreamBuffers();
|
|
1269
|
+
currentRunIdRef.current = null;
|
|
1270
|
+
pendingInterruptStreamIdRef.current = null;
|
|
1271
|
+
activeStreamContextRef.current = null;
|
|
1272
|
+
setIsStreaming(false);
|
|
1273
|
+
setIsLoading(false);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
suppressStreamRef.current = false;
|
|
1277
|
+
const cancelStream = targetContext.cancel;
|
|
1278
|
+
if (cancelStream) {
|
|
1279
|
+
cancelStream();
|
|
1280
|
+
} else {
|
|
1281
|
+
targetContext.pendingCancel = true;
|
|
1282
|
+
}
|
|
1283
|
+
const detachedSnapshot = targetContext.detachedSnapshot;
|
|
1284
|
+
if (detachedSnapshot?.length) {
|
|
1285
|
+
const assistantIndex = detachedSnapshot.findIndex((message) => message.id === targetContext.assistantMessageId);
|
|
1286
|
+
if (assistantIndex !== -1) {
|
|
1287
|
+
const assistant = detachedSnapshot[assistantIndex];
|
|
1288
|
+
detachedSnapshot[assistantIndex] = {
|
|
1289
|
+
...assistant,
|
|
1290
|
+
thinking: assistant.thinking?.trimEnd(),
|
|
866
1291
|
currentThinking: void 0,
|
|
867
1292
|
isStreaming: false,
|
|
1293
|
+
isComplete: true,
|
|
868
1294
|
isThinkingStreaming: false,
|
|
869
1295
|
isThinkingPaused: false,
|
|
870
1296
|
isToolCallsStreaming: false,
|
|
871
|
-
|
|
872
|
-
toolCalls: m.toolCalls?.map(
|
|
873
|
-
(tc) => tc.status === "running" ? { ...tc, status: "cancelled" } : tc
|
|
874
|
-
),
|
|
875
|
-
blocks: [...currentBlocksRef.current]
|
|
1297
|
+
toolCalls: assistant.toolCalls?.map((tc) => tc.status === "running" ? { ...tc, status: "cancelled" } : tc)
|
|
876
1298
|
};
|
|
877
|
-
}
|
|
1299
|
+
}
|
|
1300
|
+
setDetachedConversationSnapshot(targetContext.conversationId, detachedSnapshot);
|
|
1301
|
+
if (targetContext.conversationId && targetContext.conversationId === conversationIdRef.current) {
|
|
1302
|
+
setMessages(cloneMessages(detachedSnapshot));
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
setIsStreaming(hasLiveConversationStream(conversationIdRef.current));
|
|
1306
|
+
setIsLoading(false);
|
|
1307
|
+
}, [
|
|
1308
|
+
clearPendingCancel,
|
|
1309
|
+
flushTypewriter,
|
|
1310
|
+
getVisibleStreamContext,
|
|
1311
|
+
hasLiveConversationStream,
|
|
1312
|
+
resetStreamBuffers,
|
|
1313
|
+
setDetachedConversationSnapshot,
|
|
1314
|
+
touchStreamContext
|
|
1315
|
+
]);
|
|
1316
|
+
const cancelAllStreamContexts = useCallback(() => {
|
|
1317
|
+
streamContextsRef.current.forEach((context) => {
|
|
1318
|
+
context.suppressed = true;
|
|
1319
|
+
context.terminal = true;
|
|
1320
|
+
context.cancel?.();
|
|
878
1321
|
});
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
pendingInterruptStreamIdRef.current = null;
|
|
1322
|
+
streamContextsRef.current.clear();
|
|
1323
|
+
conversationStreamTokensRef.current.clear();
|
|
882
1324
|
activeStreamContextRef.current = null;
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}, [clearPendingCancel, flushTypewriter, resetStreamBuffers]);
|
|
1325
|
+
cancelRef.current = null;
|
|
1326
|
+
}, []);
|
|
886
1327
|
const clearMessages = useCallback(() => {
|
|
887
|
-
|
|
1328
|
+
cancelAllStreamContexts();
|
|
888
1329
|
if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
|
|
889
1330
|
setMessages([]);
|
|
890
1331
|
setError(null);
|
|
@@ -893,18 +1334,17 @@ function useChat(options) {
|
|
|
893
1334
|
resetStreamBuffers();
|
|
894
1335
|
currentRunIdRef.current = null;
|
|
895
1336
|
pendingInterruptStreamIdRef.current = null;
|
|
896
|
-
activeStreamContextRef.current = null;
|
|
897
1337
|
clearPendingCancel();
|
|
898
1338
|
suppressStreamRef.current = false;
|
|
899
1339
|
detachedConversationSnapshotsRef.current.clear();
|
|
900
|
-
}, [clearPendingCancel, resetStreamBuffers]);
|
|
1340
|
+
}, [cancelAllStreamContexts, clearPendingCancel, resetStreamBuffers]);
|
|
901
1341
|
const loadConversation = useCallback(async (id, optionsArg) => {
|
|
902
1342
|
const cancelActiveStream = optionsArg?.cancelActiveStream ?? true;
|
|
903
|
-
const
|
|
904
|
-
if (
|
|
1343
|
+
const visibleStream = getVisibleStreamContext();
|
|
1344
|
+
if (visibleStream) {
|
|
905
1345
|
if (cancelActiveStream) {
|
|
906
1346
|
stopStreaming();
|
|
907
|
-
} else {
|
|
1347
|
+
} else if (activeStreamContextRef.current && !activeStreamContextRef.current.detached) {
|
|
908
1348
|
detachActiveStream(optionsArg?.detachContext);
|
|
909
1349
|
}
|
|
910
1350
|
}
|
|
@@ -923,6 +1363,9 @@ function useChat(options) {
|
|
|
923
1363
|
refreshConversationIfVisibleWithRetry(id, [250, 800, 1800]);
|
|
924
1364
|
}
|
|
925
1365
|
setMessages(nextMessages);
|
|
1366
|
+
setIsStreaming(
|
|
1367
|
+
hasLiveConversationStream(detail.id) || nextMessages.some((message) => !!message.isStreaming)
|
|
1368
|
+
);
|
|
926
1369
|
setConversationId(detail.id);
|
|
927
1370
|
setConversationTitle(detail.title);
|
|
928
1371
|
currentAgentIdRef.current = detail.agentId ?? currentAgentIdRef.current;
|
|
@@ -933,18 +1376,24 @@ function useChat(options) {
|
|
|
933
1376
|
} finally {
|
|
934
1377
|
if (isMountedRef.current) setIsLoading(false);
|
|
935
1378
|
}
|
|
936
|
-
}, [
|
|
1379
|
+
}, [
|
|
1380
|
+
adapter,
|
|
1381
|
+
detachActiveStream,
|
|
1382
|
+
getVisibleStreamContext,
|
|
1383
|
+
hasLiveConversationStream,
|
|
1384
|
+
onError,
|
|
1385
|
+
refreshConversationIfVisibleWithRetry,
|
|
1386
|
+
stopStreaming
|
|
1387
|
+
]);
|
|
937
1388
|
const newConversation = useCallback((optionsArg) => {
|
|
938
1389
|
const cancelActiveStream = optionsArg?.cancelActiveStream ?? true;
|
|
939
|
-
const
|
|
940
|
-
if (
|
|
1390
|
+
const visibleStream = getVisibleStreamContext();
|
|
1391
|
+
if (visibleStream) {
|
|
941
1392
|
if (cancelActiveStream) {
|
|
942
1393
|
stopStreaming();
|
|
943
|
-
} else {
|
|
1394
|
+
} else if (activeStreamContextRef.current && !activeStreamContextRef.current.detached) {
|
|
944
1395
|
detachActiveStream(optionsArg?.detachContext);
|
|
945
1396
|
}
|
|
946
|
-
} else if (cancelActiveStream) {
|
|
947
|
-
cancelRef.current?.();
|
|
948
1397
|
}
|
|
949
1398
|
if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
|
|
950
1399
|
setMessages([]);
|
|
@@ -959,7 +1408,7 @@ function useChat(options) {
|
|
|
959
1408
|
pendingInterruptStreamIdRef.current = null;
|
|
960
1409
|
activeStreamContextRef.current = null;
|
|
961
1410
|
clearPendingCancel();
|
|
962
|
-
}, [clearPendingCancel, detachActiveStream, resetStreamBuffers, stopStreaming]);
|
|
1411
|
+
}, [clearPendingCancel, detachActiveStream, getVisibleStreamContext, resetStreamBuffers, stopStreaming]);
|
|
963
1412
|
const sendHitlResponse = useCallback((response) => {
|
|
964
1413
|
const runId = currentRunIdRef.current ?? void 0;
|
|
965
1414
|
const streamId = pendingInterruptStreamIdRef.current ?? void 0;
|