@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.
@@ -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 delay = 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;
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, delay);
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
- setMessages(detail.messages.filter((m) => m.role !== "tool").map((m) => ({
881
- ...m,
882
- isStreaming: false,
883
- isComplete: true
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 delay = baseDelay + jitter;
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
- }, delay);
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((delay) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
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: `${delay}ms`
10495
+ animationDelay: `${delay2}ms`
10367
10496
  }
10368
10497
  },
10369
- delay
10498
+ delay2
10370
10499
  )) }) });
10371
10500
  if (isLoading && conversations.length === 0) {
10372
10501
  return loadingDots;
@@ -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 delay = 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;
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, delay);
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
- setMessages(detail.messages.filter((m) => m.role !== "tool").map((m) => ({
793
- ...m,
794
- isStreaming: false,
795
- isComplete: true
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 delay = baseDelay + jitter;
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
- }, delay);
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((delay) => /* @__PURE__ */ jsx23(
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: `${delay}ms`
10423
+ animationDelay: `${delay2}ms`
10295
10424
  }
10296
10425
  },
10297
- delay
10426
+ delay2
10298
10427
  )) }) });
10299
10428
  if (isLoading && conversations.length === 0) {
10300
10429
  return loadingDots;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yushaw/sanqian-chat",
3
- "version": "0.2.43",
3
+ "version": "0.2.44",
4
4
  "description": "Floating chat window SDK for Sanqian AI Assistant",
5
5
  "main": "./dist/main/index.js",
6
6
  "types": "./dist/main/index.d.ts",