@yushaw/sanqian-chat 0.2.43 → 0.2.44
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/renderer/index.js +144 -15
- package/dist/renderer/index.mjs +144 -15
- package/package.json +1 -1
package/dist/renderer/index.js
CHANGED
|
@@ -90,6 +90,7 @@ module.exports = __toCommonJS(renderer_exports);
|
|
|
90
90
|
var import_react = require("react");
|
|
91
91
|
var TYPEWRITER_DELAYS = { VERY_FAST: 2, FAST: 5, NORMAL: 10, SLOW: 20 };
|
|
92
92
|
var TYPEWRITER_THRESHOLDS = { VERY_FAST: 100, FAST: 50, NORMAL: 20 };
|
|
93
|
+
var MAX_DETACHED_SNAPSHOTS = 30;
|
|
93
94
|
var findToolCallIndex = (toolCalls, toolId, toolName) => {
|
|
94
95
|
if (!toolCalls || toolCalls.length === 0) return -1;
|
|
95
96
|
if (toolId) {
|
|
@@ -107,6 +108,34 @@ var findLastBlock = (blocks, predicate) => {
|
|
|
107
108
|
}
|
|
108
109
|
return void 0;
|
|
109
110
|
};
|
|
111
|
+
function cloneBlocks(blocks) {
|
|
112
|
+
return blocks?.map((block) => ({
|
|
113
|
+
...block,
|
|
114
|
+
toolArgs: block.toolArgs ? { ...block.toolArgs } : block.toolArgs
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
function cloneMessages(messages) {
|
|
118
|
+
return messages.map((message) => ({
|
|
119
|
+
...message,
|
|
120
|
+
toolCalls: message.toolCalls?.map((toolCall) => ({
|
|
121
|
+
...toolCall,
|
|
122
|
+
args: toolCall.args ? { ...toolCall.args } : toolCall.args
|
|
123
|
+
})),
|
|
124
|
+
blocks: cloneBlocks(message.blocks),
|
|
125
|
+
filePaths: message.filePaths ? [...message.filePaths] : message.filePaths,
|
|
126
|
+
attachedResources: message.attachedResources?.map((resource) => ({ ...resource }))
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
function normalizeConversationMessages(messages) {
|
|
130
|
+
return messages.filter((message) => message.role !== "tool").map((message) => ({
|
|
131
|
+
...message,
|
|
132
|
+
isStreaming: false,
|
|
133
|
+
isComplete: true
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
function delay(ms) {
|
|
137
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
138
|
+
}
|
|
110
139
|
var CHAT_CAPABILITIES = {
|
|
111
140
|
conversationSwitch: {
|
|
112
141
|
supportsCancelActiveStream: true,
|
|
@@ -127,6 +156,7 @@ function useChat(options) {
|
|
|
127
156
|
const isMountedRef = (0, import_react.useRef)(true);
|
|
128
157
|
const messagesRef = (0, import_react.useRef)(messages);
|
|
129
158
|
const conversationIdRef = (0, import_react.useRef)(conversationId);
|
|
159
|
+
const detachedConversationSnapshotsRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
130
160
|
const currentRunIdRef = (0, import_react.useRef)(null);
|
|
131
161
|
const pendingInterruptStreamIdRef = (0, import_react.useRef)(null);
|
|
132
162
|
const currentAgentIdRef = (0, import_react.useRef)(null);
|
|
@@ -221,11 +251,49 @@ function useChat(options) {
|
|
|
221
251
|
pendingCancelRef.current = false;
|
|
222
252
|
pendingCancelFnRef.current = null;
|
|
223
253
|
}, []);
|
|
254
|
+
const setDetachedConversationSnapshot = (0, import_react.useCallback)((id, snapshot) => {
|
|
255
|
+
if (!id || snapshot.length === 0) return;
|
|
256
|
+
const map = detachedConversationSnapshotsRef.current;
|
|
257
|
+
if (map.has(id)) {
|
|
258
|
+
map.delete(id);
|
|
259
|
+
}
|
|
260
|
+
map.set(id, cloneMessages(snapshot));
|
|
261
|
+
while (map.size > MAX_DETACHED_SNAPSHOTS) {
|
|
262
|
+
const oldestKey = map.keys().next().value;
|
|
263
|
+
if (!oldestKey) break;
|
|
264
|
+
map.delete(oldestKey);
|
|
265
|
+
}
|
|
266
|
+
}, []);
|
|
224
267
|
const detachActiveStream = (0, import_react.useCallback)((detachContext) => {
|
|
225
268
|
const context = activeStreamContextRef.current;
|
|
226
269
|
if (!context || context.detached) {
|
|
227
270
|
return;
|
|
228
271
|
}
|
|
272
|
+
const detachedSnapshot = cloneMessages(normalizeConversationMessages(messagesRef.current));
|
|
273
|
+
const detachedAssistantMessageIndex = detachedSnapshot.findIndex(
|
|
274
|
+
(message) => message.id === context.assistantMessageId
|
|
275
|
+
);
|
|
276
|
+
if (detachedAssistantMessageIndex !== -1) {
|
|
277
|
+
const detachedAssistantMessage = detachedSnapshot[detachedAssistantMessageIndex];
|
|
278
|
+
const detachedContent = fullContentRef.current || displayedContentRef.current || detachedAssistantMessage.content;
|
|
279
|
+
detachedSnapshot[detachedAssistantMessageIndex] = {
|
|
280
|
+
...detachedAssistantMessage,
|
|
281
|
+
content: detachedContent,
|
|
282
|
+
finalContent: detachedContent || detachedAssistantMessage.finalContent,
|
|
283
|
+
blocks: currentBlocksRef.current.length > 0 ? cloneBlocks(currentBlocksRef.current) : detachedAssistantMessage.blocks,
|
|
284
|
+
thinking: detachedAssistantMessage.thinking?.trimEnd(),
|
|
285
|
+
currentThinking: void 0,
|
|
286
|
+
isStreaming: false,
|
|
287
|
+
isThinkingStreaming: false,
|
|
288
|
+
isThinkingPaused: false,
|
|
289
|
+
isToolCallsStreaming: false,
|
|
290
|
+
isComplete: true
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (detachedSnapshot.length > 0) {
|
|
294
|
+
context.detachedSnapshot = detachedSnapshot;
|
|
295
|
+
setDetachedConversationSnapshot(context.conversationId, detachedSnapshot);
|
|
296
|
+
}
|
|
229
297
|
context.detached = true;
|
|
230
298
|
context.detachContext = detachContext;
|
|
231
299
|
if (typewriterIntervalRef.current) {
|
|
@@ -241,19 +309,72 @@ function useChat(options) {
|
|
|
241
309
|
pendingInterruptStreamIdRef.current = null;
|
|
242
310
|
clearPendingCancel();
|
|
243
311
|
suppressStreamRef.current = false;
|
|
244
|
-
}, [clearPendingCancel, resetStreamBuffers]);
|
|
312
|
+
}, [clearPendingCancel, resetStreamBuffers, setDetachedConversationSnapshot]);
|
|
313
|
+
const refreshConversationIfVisible = (0, import_react.useCallback)(async (id) => {
|
|
314
|
+
if (!id || conversationIdRef.current !== id) return false;
|
|
315
|
+
try {
|
|
316
|
+
const detail = await adapter.getConversation(id);
|
|
317
|
+
if (!isMountedRef.current || conversationIdRef.current !== id) return false;
|
|
318
|
+
const normalizedMessages = normalizeConversationMessages(detail.messages);
|
|
319
|
+
if (normalizedMessages.length === 0) return false;
|
|
320
|
+
detachedConversationSnapshotsRef.current.delete(id);
|
|
321
|
+
setMessages(normalizedMessages);
|
|
322
|
+
setConversationTitle(detail.title ?? null);
|
|
323
|
+
return true;
|
|
324
|
+
} catch (refreshError) {
|
|
325
|
+
console.warn("[useChat] Failed to refresh conversation after detached stream completion:", refreshError);
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
}, [adapter]);
|
|
329
|
+
const refreshConversationIfVisibleWithRetry = (0, import_react.useCallback)((id, retryDelaysMs) => {
|
|
330
|
+
if (!id) return;
|
|
331
|
+
void (async () => {
|
|
332
|
+
for (const retryDelayMs of retryDelaysMs) {
|
|
333
|
+
if (retryDelayMs > 0) {
|
|
334
|
+
await delay(retryDelayMs);
|
|
335
|
+
}
|
|
336
|
+
if (!isMountedRef.current || conversationIdRef.current !== id) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const refreshed = await refreshConversationIfVisible(id);
|
|
340
|
+
if (refreshed) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
})();
|
|
345
|
+
}, [refreshConversationIfVisible]);
|
|
245
346
|
const handleStreamEvent = (0, import_react.useCallback)((event, streamContext) => {
|
|
246
347
|
if (!isMountedRef.current) return;
|
|
247
348
|
const isActiveStream = activeStreamContextRef.current?.token === streamContext.token && !streamContext.detached;
|
|
248
349
|
if (!isActiveStream) {
|
|
249
350
|
if (event.type === "done" && event.conversationId && !streamContext.didReportConversationChange) {
|
|
351
|
+
if (streamContext.conversationId && streamContext.conversationId !== event.conversationId) {
|
|
352
|
+
detachedConversationSnapshotsRef.current.delete(streamContext.conversationId);
|
|
353
|
+
}
|
|
354
|
+
streamContext.conversationId = event.conversationId;
|
|
250
355
|
streamContext.didReportConversationChange = true;
|
|
356
|
+
if (streamContext.detachedSnapshot?.length) {
|
|
357
|
+
setDetachedConversationSnapshot(event.conversationId, streamContext.detachedSnapshot);
|
|
358
|
+
}
|
|
251
359
|
onConversationChange?.(event.conversationId, event.title, {
|
|
252
360
|
source: "background",
|
|
253
361
|
streamToken: streamContext.token,
|
|
254
362
|
detached: true,
|
|
255
363
|
detachContext: streamContext.detachContext
|
|
256
364
|
});
|
|
365
|
+
refreshConversationIfVisibleWithRetry(event.conversationId, [0, 300, 900]);
|
|
366
|
+
} else if (event.type === "done" && event.conversationId && streamContext.didReportConversationChange) {
|
|
367
|
+
if (streamContext.detachedSnapshot?.length) {
|
|
368
|
+
setDetachedConversationSnapshot(event.conversationId, streamContext.detachedSnapshot);
|
|
369
|
+
}
|
|
370
|
+
refreshConversationIfVisibleWithRetry(event.conversationId, [0, 300, 900]);
|
|
371
|
+
}
|
|
372
|
+
const isTerminalEvent = event.type === "done" || event.type === "cancelled" || event.type === "error";
|
|
373
|
+
if (isTerminalEvent) {
|
|
374
|
+
streamContext.detachedSnapshot = void 0;
|
|
375
|
+
}
|
|
376
|
+
if (isTerminalEvent && activeStreamContextRef.current?.token === streamContext.token) {
|
|
377
|
+
activeStreamContextRef.current = null;
|
|
257
378
|
}
|
|
258
379
|
return;
|
|
259
380
|
}
|
|
@@ -338,9 +459,9 @@ function useChat(options) {
|
|
|
338
459
|
return updated;
|
|
339
460
|
});
|
|
340
461
|
const qLen = tokenQueueRef.current.length;
|
|
341
|
-
const
|
|
462
|
+
const delay2 = qLen > TYPEWRITER_THRESHOLDS.VERY_FAST ? TYPEWRITER_DELAYS.VERY_FAST : qLen > TYPEWRITER_THRESHOLDS.FAST ? TYPEWRITER_DELAYS.FAST : qLen > TYPEWRITER_THRESHOLDS.NORMAL ? TYPEWRITER_DELAYS.NORMAL : TYPEWRITER_DELAYS.SLOW;
|
|
342
463
|
if (typewriterIntervalRef.current !== null) {
|
|
343
|
-
typewriterIntervalRef.current = setTimeout(tick,
|
|
464
|
+
typewriterIntervalRef.current = setTimeout(tick, delay2);
|
|
344
465
|
}
|
|
345
466
|
} else {
|
|
346
467
|
typewriterIntervalRef.current = null;
|
|
@@ -603,6 +724,7 @@ function useChat(options) {
|
|
|
603
724
|
});
|
|
604
725
|
resetStreamBuffers();
|
|
605
726
|
if (event.conversationId) {
|
|
727
|
+
streamContext.conversationId = event.conversationId;
|
|
606
728
|
streamContext.didReportConversationChange = true;
|
|
607
729
|
setConversationId(event.conversationId);
|
|
608
730
|
onConversationChange?.(event.conversationId, event.title, {
|
|
@@ -703,7 +825,7 @@ function useChat(options) {
|
|
|
703
825
|
break;
|
|
704
826
|
}
|
|
705
827
|
}
|
|
706
|
-
}, [clearPendingCancel, flushTypewriter, onConversationChange, onError, resetStreamBuffers]);
|
|
828
|
+
}, [clearPendingCancel, flushTypewriter, onConversationChange, onError, refreshConversationIfVisibleWithRetry, resetStreamBuffers, setDetachedConversationSnapshot]);
|
|
707
829
|
const trySendMessage = (0, import_react.useCallback)(async (content, sendOptions) => {
|
|
708
830
|
const trimmedContent = content.trim();
|
|
709
831
|
const hasAttachedResources = (sendOptions?.attachedResources?.length ?? 0) > 0;
|
|
@@ -739,6 +861,7 @@ function useChat(options) {
|
|
|
739
861
|
const streamContext = {
|
|
740
862
|
token: crypto.randomUUID(),
|
|
741
863
|
assistantMessageId: assistantMessage.id,
|
|
864
|
+
conversationId: conversationIdRef.current,
|
|
742
865
|
detached: false,
|
|
743
866
|
didReportConversationChange: false
|
|
744
867
|
};
|
|
@@ -861,6 +984,7 @@ function useChat(options) {
|
|
|
861
984
|
activeStreamContextRef.current = null;
|
|
862
985
|
clearPendingCancel();
|
|
863
986
|
suppressStreamRef.current = false;
|
|
987
|
+
detachedConversationSnapshotsRef.current.clear();
|
|
864
988
|
}, [clearPendingCancel, resetStreamBuffers]);
|
|
865
989
|
const loadConversation = (0, import_react.useCallback)(async (id, optionsArg) => {
|
|
866
990
|
const cancelActiveStream = optionsArg?.cancelActiveStream ?? true;
|
|
@@ -877,11 +1001,16 @@ function useChat(options) {
|
|
|
877
1001
|
setError(null);
|
|
878
1002
|
const detail = await adapter.getConversation(id);
|
|
879
1003
|
if (!isMountedRef.current) return;
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1004
|
+
const normalizedMessages = normalizeConversationMessages(detail.messages);
|
|
1005
|
+
const detachedSnapshot = detachedConversationSnapshotsRef.current.get(id);
|
|
1006
|
+
const shouldUseDetachedSnapshot = normalizedMessages.length === 0 && !!detachedSnapshot?.length;
|
|
1007
|
+
const nextMessages = shouldUseDetachedSnapshot ? cloneMessages(detachedSnapshot) : normalizedMessages;
|
|
1008
|
+
if (!shouldUseDetachedSnapshot) {
|
|
1009
|
+
detachedConversationSnapshotsRef.current.delete(id);
|
|
1010
|
+
} else {
|
|
1011
|
+
refreshConversationIfVisibleWithRetry(id, [250, 800, 1800]);
|
|
1012
|
+
}
|
|
1013
|
+
setMessages(nextMessages);
|
|
885
1014
|
setConversationId(detail.id);
|
|
886
1015
|
setConversationTitle(detail.title);
|
|
887
1016
|
currentAgentIdRef.current = detail.agentId ?? currentAgentIdRef.current;
|
|
@@ -892,7 +1021,7 @@ function useChat(options) {
|
|
|
892
1021
|
} finally {
|
|
893
1022
|
if (isMountedRef.current) setIsLoading(false);
|
|
894
1023
|
}
|
|
895
|
-
}, [adapter, detachActiveStream, onError, stopStreaming]);
|
|
1024
|
+
}, [adapter, detachActiveStream, onError, refreshConversationIfVisibleWithRetry, stopStreaming]);
|
|
896
1025
|
const newConversation = (0, import_react.useCallback)((optionsArg) => {
|
|
897
1026
|
const cancelActiveStream = optionsArg?.cancelActiveStream ?? true;
|
|
898
1027
|
const activeStream = activeStreamContextRef.current;
|
|
@@ -1363,13 +1492,13 @@ function useConnection(options) {
|
|
|
1363
1492
|
const isFirstAttempt = status === "disconnected" && reconnectAttemptsRef.current === 0;
|
|
1364
1493
|
const baseDelay = isFirstAttempt ? 0 : Math.min(500 * Math.pow(2, reconnectAttemptsRef.current), 5e3);
|
|
1365
1494
|
const jitter = isFirstAttempt ? 0 : Math.random() * 500;
|
|
1366
|
-
const
|
|
1495
|
+
const delay2 = baseDelay + jitter;
|
|
1367
1496
|
const timer = setTimeout(() => {
|
|
1368
1497
|
if (!isMountedRef.current) return;
|
|
1369
1498
|
reconnectAttemptsRef.current++;
|
|
1370
1499
|
adapter.connect().catch(() => {
|
|
1371
1500
|
});
|
|
1372
|
-
},
|
|
1501
|
+
}, delay2);
|
|
1373
1502
|
return () => clearTimeout(timer);
|
|
1374
1503
|
}, [status, autoConnect, adapter]);
|
|
1375
1504
|
(0, import_react4.useEffect)(() => {
|
|
@@ -10354,7 +10483,7 @@ var HistoryList = (0, import_react36.memo)(function HistoryList2({
|
|
|
10354
10483
|
deleteText: "#ef4444",
|
|
10355
10484
|
loadingDot: isDarkMode ? "rgba(255, 255, 255, 0.3)" : "rgba(0, 0, 0, 0.2)"
|
|
10356
10485
|
};
|
|
10357
|
-
const loadingDots = /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { style: { display: "flex", justifyContent: "center", padding: "1.5rem 0" }, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { style: { display: "flex", gap: "0.25rem" }, children: [0, 150, 300].map((
|
|
10486
|
+
const loadingDots = /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { style: { display: "flex", justifyContent: "center", padding: "1.5rem 0" }, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { style: { display: "flex", gap: "0.25rem" }, children: [0, 150, 300].map((delay2) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
|
|
10358
10487
|
"span",
|
|
10359
10488
|
{
|
|
10360
10489
|
style: {
|
|
@@ -10363,10 +10492,10 @@ var HistoryList = (0, import_react36.memo)(function HistoryList2({
|
|
|
10363
10492
|
borderRadius: "50%",
|
|
10364
10493
|
background: colors.loadingDot,
|
|
10365
10494
|
animation: "bounce 1s infinite",
|
|
10366
|
-
animationDelay: `${
|
|
10495
|
+
animationDelay: `${delay2}ms`
|
|
10367
10496
|
}
|
|
10368
10497
|
},
|
|
10369
|
-
|
|
10498
|
+
delay2
|
|
10370
10499
|
)) }) });
|
|
10371
10500
|
if (isLoading && conversations.length === 0) {
|
|
10372
10501
|
return loadingDots;
|
package/dist/renderer/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
3
3
|
var TYPEWRITER_DELAYS = { VERY_FAST: 2, FAST: 5, NORMAL: 10, SLOW: 20 };
|
|
4
4
|
var TYPEWRITER_THRESHOLDS = { VERY_FAST: 100, FAST: 50, NORMAL: 20 };
|
|
5
|
+
var MAX_DETACHED_SNAPSHOTS = 30;
|
|
5
6
|
var findToolCallIndex = (toolCalls, toolId, toolName) => {
|
|
6
7
|
if (!toolCalls || toolCalls.length === 0) return -1;
|
|
7
8
|
if (toolId) {
|
|
@@ -19,6 +20,34 @@ var findLastBlock = (blocks, predicate) => {
|
|
|
19
20
|
}
|
|
20
21
|
return void 0;
|
|
21
22
|
};
|
|
23
|
+
function cloneBlocks(blocks) {
|
|
24
|
+
return blocks?.map((block) => ({
|
|
25
|
+
...block,
|
|
26
|
+
toolArgs: block.toolArgs ? { ...block.toolArgs } : block.toolArgs
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
function cloneMessages(messages) {
|
|
30
|
+
return messages.map((message) => ({
|
|
31
|
+
...message,
|
|
32
|
+
toolCalls: message.toolCalls?.map((toolCall) => ({
|
|
33
|
+
...toolCall,
|
|
34
|
+
args: toolCall.args ? { ...toolCall.args } : toolCall.args
|
|
35
|
+
})),
|
|
36
|
+
blocks: cloneBlocks(message.blocks),
|
|
37
|
+
filePaths: message.filePaths ? [...message.filePaths] : message.filePaths,
|
|
38
|
+
attachedResources: message.attachedResources?.map((resource) => ({ ...resource }))
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
function normalizeConversationMessages(messages) {
|
|
42
|
+
return messages.filter((message) => message.role !== "tool").map((message) => ({
|
|
43
|
+
...message,
|
|
44
|
+
isStreaming: false,
|
|
45
|
+
isComplete: true
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
function delay(ms) {
|
|
49
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
+
}
|
|
22
51
|
var CHAT_CAPABILITIES = {
|
|
23
52
|
conversationSwitch: {
|
|
24
53
|
supportsCancelActiveStream: true,
|
|
@@ -39,6 +68,7 @@ function useChat(options) {
|
|
|
39
68
|
const isMountedRef = useRef(true);
|
|
40
69
|
const messagesRef = useRef(messages);
|
|
41
70
|
const conversationIdRef = useRef(conversationId);
|
|
71
|
+
const detachedConversationSnapshotsRef = useRef(/* @__PURE__ */ new Map());
|
|
42
72
|
const currentRunIdRef = useRef(null);
|
|
43
73
|
const pendingInterruptStreamIdRef = useRef(null);
|
|
44
74
|
const currentAgentIdRef = useRef(null);
|
|
@@ -133,11 +163,49 @@ function useChat(options) {
|
|
|
133
163
|
pendingCancelRef.current = false;
|
|
134
164
|
pendingCancelFnRef.current = null;
|
|
135
165
|
}, []);
|
|
166
|
+
const setDetachedConversationSnapshot = useCallback((id, snapshot) => {
|
|
167
|
+
if (!id || snapshot.length === 0) return;
|
|
168
|
+
const map = detachedConversationSnapshotsRef.current;
|
|
169
|
+
if (map.has(id)) {
|
|
170
|
+
map.delete(id);
|
|
171
|
+
}
|
|
172
|
+
map.set(id, cloneMessages(snapshot));
|
|
173
|
+
while (map.size > MAX_DETACHED_SNAPSHOTS) {
|
|
174
|
+
const oldestKey = map.keys().next().value;
|
|
175
|
+
if (!oldestKey) break;
|
|
176
|
+
map.delete(oldestKey);
|
|
177
|
+
}
|
|
178
|
+
}, []);
|
|
136
179
|
const detachActiveStream = useCallback((detachContext) => {
|
|
137
180
|
const context = activeStreamContextRef.current;
|
|
138
181
|
if (!context || context.detached) {
|
|
139
182
|
return;
|
|
140
183
|
}
|
|
184
|
+
const detachedSnapshot = cloneMessages(normalizeConversationMessages(messagesRef.current));
|
|
185
|
+
const detachedAssistantMessageIndex = detachedSnapshot.findIndex(
|
|
186
|
+
(message) => message.id === context.assistantMessageId
|
|
187
|
+
);
|
|
188
|
+
if (detachedAssistantMessageIndex !== -1) {
|
|
189
|
+
const detachedAssistantMessage = detachedSnapshot[detachedAssistantMessageIndex];
|
|
190
|
+
const detachedContent = fullContentRef.current || displayedContentRef.current || detachedAssistantMessage.content;
|
|
191
|
+
detachedSnapshot[detachedAssistantMessageIndex] = {
|
|
192
|
+
...detachedAssistantMessage,
|
|
193
|
+
content: detachedContent,
|
|
194
|
+
finalContent: detachedContent || detachedAssistantMessage.finalContent,
|
|
195
|
+
blocks: currentBlocksRef.current.length > 0 ? cloneBlocks(currentBlocksRef.current) : detachedAssistantMessage.blocks,
|
|
196
|
+
thinking: detachedAssistantMessage.thinking?.trimEnd(),
|
|
197
|
+
currentThinking: void 0,
|
|
198
|
+
isStreaming: false,
|
|
199
|
+
isThinkingStreaming: false,
|
|
200
|
+
isThinkingPaused: false,
|
|
201
|
+
isToolCallsStreaming: false,
|
|
202
|
+
isComplete: true
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (detachedSnapshot.length > 0) {
|
|
206
|
+
context.detachedSnapshot = detachedSnapshot;
|
|
207
|
+
setDetachedConversationSnapshot(context.conversationId, detachedSnapshot);
|
|
208
|
+
}
|
|
141
209
|
context.detached = true;
|
|
142
210
|
context.detachContext = detachContext;
|
|
143
211
|
if (typewriterIntervalRef.current) {
|
|
@@ -153,19 +221,72 @@ function useChat(options) {
|
|
|
153
221
|
pendingInterruptStreamIdRef.current = null;
|
|
154
222
|
clearPendingCancel();
|
|
155
223
|
suppressStreamRef.current = false;
|
|
156
|
-
}, [clearPendingCancel, resetStreamBuffers]);
|
|
224
|
+
}, [clearPendingCancel, resetStreamBuffers, setDetachedConversationSnapshot]);
|
|
225
|
+
const refreshConversationIfVisible = useCallback(async (id) => {
|
|
226
|
+
if (!id || conversationIdRef.current !== id) return false;
|
|
227
|
+
try {
|
|
228
|
+
const detail = await adapter.getConversation(id);
|
|
229
|
+
if (!isMountedRef.current || conversationIdRef.current !== id) return false;
|
|
230
|
+
const normalizedMessages = normalizeConversationMessages(detail.messages);
|
|
231
|
+
if (normalizedMessages.length === 0) return false;
|
|
232
|
+
detachedConversationSnapshotsRef.current.delete(id);
|
|
233
|
+
setMessages(normalizedMessages);
|
|
234
|
+
setConversationTitle(detail.title ?? null);
|
|
235
|
+
return true;
|
|
236
|
+
} catch (refreshError) {
|
|
237
|
+
console.warn("[useChat] Failed to refresh conversation after detached stream completion:", refreshError);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}, [adapter]);
|
|
241
|
+
const refreshConversationIfVisibleWithRetry = useCallback((id, retryDelaysMs) => {
|
|
242
|
+
if (!id) return;
|
|
243
|
+
void (async () => {
|
|
244
|
+
for (const retryDelayMs of retryDelaysMs) {
|
|
245
|
+
if (retryDelayMs > 0) {
|
|
246
|
+
await delay(retryDelayMs);
|
|
247
|
+
}
|
|
248
|
+
if (!isMountedRef.current || conversationIdRef.current !== id) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const refreshed = await refreshConversationIfVisible(id);
|
|
252
|
+
if (refreshed) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
})();
|
|
257
|
+
}, [refreshConversationIfVisible]);
|
|
157
258
|
const handleStreamEvent = useCallback((event, streamContext) => {
|
|
158
259
|
if (!isMountedRef.current) return;
|
|
159
260
|
const isActiveStream = activeStreamContextRef.current?.token === streamContext.token && !streamContext.detached;
|
|
160
261
|
if (!isActiveStream) {
|
|
161
262
|
if (event.type === "done" && event.conversationId && !streamContext.didReportConversationChange) {
|
|
263
|
+
if (streamContext.conversationId && streamContext.conversationId !== event.conversationId) {
|
|
264
|
+
detachedConversationSnapshotsRef.current.delete(streamContext.conversationId);
|
|
265
|
+
}
|
|
266
|
+
streamContext.conversationId = event.conversationId;
|
|
162
267
|
streamContext.didReportConversationChange = true;
|
|
268
|
+
if (streamContext.detachedSnapshot?.length) {
|
|
269
|
+
setDetachedConversationSnapshot(event.conversationId, streamContext.detachedSnapshot);
|
|
270
|
+
}
|
|
163
271
|
onConversationChange?.(event.conversationId, event.title, {
|
|
164
272
|
source: "background",
|
|
165
273
|
streamToken: streamContext.token,
|
|
166
274
|
detached: true,
|
|
167
275
|
detachContext: streamContext.detachContext
|
|
168
276
|
});
|
|
277
|
+
refreshConversationIfVisibleWithRetry(event.conversationId, [0, 300, 900]);
|
|
278
|
+
} else if (event.type === "done" && event.conversationId && streamContext.didReportConversationChange) {
|
|
279
|
+
if (streamContext.detachedSnapshot?.length) {
|
|
280
|
+
setDetachedConversationSnapshot(event.conversationId, streamContext.detachedSnapshot);
|
|
281
|
+
}
|
|
282
|
+
refreshConversationIfVisibleWithRetry(event.conversationId, [0, 300, 900]);
|
|
283
|
+
}
|
|
284
|
+
const isTerminalEvent = event.type === "done" || event.type === "cancelled" || event.type === "error";
|
|
285
|
+
if (isTerminalEvent) {
|
|
286
|
+
streamContext.detachedSnapshot = void 0;
|
|
287
|
+
}
|
|
288
|
+
if (isTerminalEvent && activeStreamContextRef.current?.token === streamContext.token) {
|
|
289
|
+
activeStreamContextRef.current = null;
|
|
169
290
|
}
|
|
170
291
|
return;
|
|
171
292
|
}
|
|
@@ -250,9 +371,9 @@ function useChat(options) {
|
|
|
250
371
|
return updated;
|
|
251
372
|
});
|
|
252
373
|
const qLen = tokenQueueRef.current.length;
|
|
253
|
-
const
|
|
374
|
+
const delay2 = qLen > TYPEWRITER_THRESHOLDS.VERY_FAST ? TYPEWRITER_DELAYS.VERY_FAST : qLen > TYPEWRITER_THRESHOLDS.FAST ? TYPEWRITER_DELAYS.FAST : qLen > TYPEWRITER_THRESHOLDS.NORMAL ? TYPEWRITER_DELAYS.NORMAL : TYPEWRITER_DELAYS.SLOW;
|
|
254
375
|
if (typewriterIntervalRef.current !== null) {
|
|
255
|
-
typewriterIntervalRef.current = setTimeout(tick,
|
|
376
|
+
typewriterIntervalRef.current = setTimeout(tick, delay2);
|
|
256
377
|
}
|
|
257
378
|
} else {
|
|
258
379
|
typewriterIntervalRef.current = null;
|
|
@@ -515,6 +636,7 @@ function useChat(options) {
|
|
|
515
636
|
});
|
|
516
637
|
resetStreamBuffers();
|
|
517
638
|
if (event.conversationId) {
|
|
639
|
+
streamContext.conversationId = event.conversationId;
|
|
518
640
|
streamContext.didReportConversationChange = true;
|
|
519
641
|
setConversationId(event.conversationId);
|
|
520
642
|
onConversationChange?.(event.conversationId, event.title, {
|
|
@@ -615,7 +737,7 @@ function useChat(options) {
|
|
|
615
737
|
break;
|
|
616
738
|
}
|
|
617
739
|
}
|
|
618
|
-
}, [clearPendingCancel, flushTypewriter, onConversationChange, onError, resetStreamBuffers]);
|
|
740
|
+
}, [clearPendingCancel, flushTypewriter, onConversationChange, onError, refreshConversationIfVisibleWithRetry, resetStreamBuffers, setDetachedConversationSnapshot]);
|
|
619
741
|
const trySendMessage = useCallback(async (content, sendOptions) => {
|
|
620
742
|
const trimmedContent = content.trim();
|
|
621
743
|
const hasAttachedResources = (sendOptions?.attachedResources?.length ?? 0) > 0;
|
|
@@ -651,6 +773,7 @@ function useChat(options) {
|
|
|
651
773
|
const streamContext = {
|
|
652
774
|
token: crypto.randomUUID(),
|
|
653
775
|
assistantMessageId: assistantMessage.id,
|
|
776
|
+
conversationId: conversationIdRef.current,
|
|
654
777
|
detached: false,
|
|
655
778
|
didReportConversationChange: false
|
|
656
779
|
};
|
|
@@ -773,6 +896,7 @@ function useChat(options) {
|
|
|
773
896
|
activeStreamContextRef.current = null;
|
|
774
897
|
clearPendingCancel();
|
|
775
898
|
suppressStreamRef.current = false;
|
|
899
|
+
detachedConversationSnapshotsRef.current.clear();
|
|
776
900
|
}, [clearPendingCancel, resetStreamBuffers]);
|
|
777
901
|
const loadConversation = useCallback(async (id, optionsArg) => {
|
|
778
902
|
const cancelActiveStream = optionsArg?.cancelActiveStream ?? true;
|
|
@@ -789,11 +913,16 @@ function useChat(options) {
|
|
|
789
913
|
setError(null);
|
|
790
914
|
const detail = await adapter.getConversation(id);
|
|
791
915
|
if (!isMountedRef.current) return;
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
916
|
+
const normalizedMessages = normalizeConversationMessages(detail.messages);
|
|
917
|
+
const detachedSnapshot = detachedConversationSnapshotsRef.current.get(id);
|
|
918
|
+
const shouldUseDetachedSnapshot = normalizedMessages.length === 0 && !!detachedSnapshot?.length;
|
|
919
|
+
const nextMessages = shouldUseDetachedSnapshot ? cloneMessages(detachedSnapshot) : normalizedMessages;
|
|
920
|
+
if (!shouldUseDetachedSnapshot) {
|
|
921
|
+
detachedConversationSnapshotsRef.current.delete(id);
|
|
922
|
+
} else {
|
|
923
|
+
refreshConversationIfVisibleWithRetry(id, [250, 800, 1800]);
|
|
924
|
+
}
|
|
925
|
+
setMessages(nextMessages);
|
|
797
926
|
setConversationId(detail.id);
|
|
798
927
|
setConversationTitle(detail.title);
|
|
799
928
|
currentAgentIdRef.current = detail.agentId ?? currentAgentIdRef.current;
|
|
@@ -804,7 +933,7 @@ function useChat(options) {
|
|
|
804
933
|
} finally {
|
|
805
934
|
if (isMountedRef.current) setIsLoading(false);
|
|
806
935
|
}
|
|
807
|
-
}, [adapter, detachActiveStream, onError, stopStreaming]);
|
|
936
|
+
}, [adapter, detachActiveStream, onError, refreshConversationIfVisibleWithRetry, stopStreaming]);
|
|
808
937
|
const newConversation = useCallback((optionsArg) => {
|
|
809
938
|
const cancelActiveStream = optionsArg?.cancelActiveStream ?? true;
|
|
810
939
|
const activeStream = activeStreamContextRef.current;
|
|
@@ -1275,13 +1404,13 @@ function useConnection(options) {
|
|
|
1275
1404
|
const isFirstAttempt = status === "disconnected" && reconnectAttemptsRef.current === 0;
|
|
1276
1405
|
const baseDelay = isFirstAttempt ? 0 : Math.min(500 * Math.pow(2, reconnectAttemptsRef.current), 5e3);
|
|
1277
1406
|
const jitter = isFirstAttempt ? 0 : Math.random() * 500;
|
|
1278
|
-
const
|
|
1407
|
+
const delay2 = baseDelay + jitter;
|
|
1279
1408
|
const timer = setTimeout(() => {
|
|
1280
1409
|
if (!isMountedRef.current) return;
|
|
1281
1410
|
reconnectAttemptsRef.current++;
|
|
1282
1411
|
adapter.connect().catch(() => {
|
|
1283
1412
|
});
|
|
1284
|
-
},
|
|
1413
|
+
}, delay2);
|
|
1285
1414
|
return () => clearTimeout(timer);
|
|
1286
1415
|
}, [status, autoConnect, adapter]);
|
|
1287
1416
|
useEffect3(() => {
|
|
@@ -10282,7 +10411,7 @@ var HistoryList = memo16(function HistoryList2({
|
|
|
10282
10411
|
deleteText: "#ef4444",
|
|
10283
10412
|
loadingDot: isDarkMode ? "rgba(255, 255, 255, 0.3)" : "rgba(0, 0, 0, 0.2)"
|
|
10284
10413
|
};
|
|
10285
|
-
const loadingDots = /* @__PURE__ */ jsx23("div", { style: { display: "flex", justifyContent: "center", padding: "1.5rem 0" }, children: /* @__PURE__ */ jsx23("div", { style: { display: "flex", gap: "0.25rem" }, children: [0, 150, 300].map((
|
|
10414
|
+
const loadingDots = /* @__PURE__ */ jsx23("div", { style: { display: "flex", justifyContent: "center", padding: "1.5rem 0" }, children: /* @__PURE__ */ jsx23("div", { style: { display: "flex", gap: "0.25rem" }, children: [0, 150, 300].map((delay2) => /* @__PURE__ */ jsx23(
|
|
10286
10415
|
"span",
|
|
10287
10416
|
{
|
|
10288
10417
|
style: {
|
|
@@ -10291,10 +10420,10 @@ var HistoryList = memo16(function HistoryList2({
|
|
|
10291
10420
|
borderRadius: "50%",
|
|
10292
10421
|
background: colors.loadingDot,
|
|
10293
10422
|
animation: "bounce 1s infinite",
|
|
10294
|
-
animationDelay: `${
|
|
10423
|
+
animationDelay: `${delay2}ms`
|
|
10295
10424
|
}
|
|
10296
10425
|
},
|
|
10297
|
-
|
|
10426
|
+
delay2
|
|
10298
10427
|
)) }) });
|
|
10299
10428
|
if (isLoading && conversations.length === 0) {
|
|
10300
10429
|
return loadingDots;
|