@yushaw/sanqian-chat 0.2.43 → 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.
@@ -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,241 @@ 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 updateDetachedSnapshotForEvent(streamContext, event) {
118
+ const snapshot = streamContext.detachedSnapshot;
119
+ if (!snapshot?.length) return false;
120
+ const assistantIndex = snapshot.findIndex((message) => message.id === streamContext.assistantMessageId);
121
+ if (assistantIndex === -1) return false;
122
+ const assistant = snapshot[assistantIndex];
123
+ switch (event.type) {
124
+ case "text": {
125
+ const rawContent = event.content;
126
+ if (!rawContent) return false;
127
+ const nextChunk = assistant.content ? rawContent : rawContent.trimStart();
128
+ if (!nextChunk) return false;
129
+ snapshot[assistantIndex] = {
130
+ ...assistant,
131
+ content: `${assistant.content}${nextChunk}`,
132
+ isStreaming: true,
133
+ isComplete: false
134
+ };
135
+ return true;
136
+ }
137
+ case "thinking": {
138
+ const rawThinking = event.content;
139
+ if (!rawThinking) return false;
140
+ const nextChunk = assistant.thinking ? rawThinking : rawThinking.trimStart();
141
+ if (!nextChunk) return false;
142
+ snapshot[assistantIndex] = {
143
+ ...assistant,
144
+ thinking: `${assistant.thinking ?? ""}${nextChunk}`,
145
+ currentThinking: `${assistant.currentThinking ?? ""}${nextChunk}`,
146
+ isStreaming: true,
147
+ isComplete: false,
148
+ isThinkingStreaming: true,
149
+ isThinkingPaused: false
150
+ };
151
+ return true;
152
+ }
153
+ case "tool_call": {
154
+ const tc = event.tool_call;
155
+ if (!tc) return false;
156
+ const toolName = tc.function?.name || tc.name || "";
157
+ const toolId = tc.id || "";
158
+ let args = {};
159
+ if (tc.args) {
160
+ args = tc.args;
161
+ } else if (tc.function?.arguments) {
162
+ try {
163
+ args = JSON.parse(tc.function.arguments);
164
+ } catch {
165
+ args = {};
166
+ }
167
+ }
168
+ const toolCalls = [...assistant.toolCalls || []];
169
+ toolCalls.push({
170
+ id: toolId,
171
+ name: toolName,
172
+ args,
173
+ status: "running"
174
+ });
175
+ const blocks = cloneBlocks(assistant.blocks) || [];
176
+ blocks.push({
177
+ type: "tool_call",
178
+ content: "",
179
+ timestamp: Date.now(),
180
+ toolName,
181
+ toolArgs: args,
182
+ toolCallId: toolId,
183
+ toolStatus: "running",
184
+ isIntermediate: true
185
+ });
186
+ snapshot[assistantIndex] = {
187
+ ...assistant,
188
+ toolCalls,
189
+ blocks,
190
+ isStreaming: true,
191
+ isComplete: false,
192
+ isThinkingStreaming: false,
193
+ isThinkingPaused: !!assistant.thinking,
194
+ isToolCallsStreaming: true
195
+ };
196
+ return true;
197
+ }
198
+ case "tool_args_chunk": {
199
+ const chunk = event.chunk;
200
+ if (!chunk) return false;
201
+ const toolCalls = [...assistant.toolCalls || []];
202
+ const toolCallIndex = findToolCallIndex(toolCalls, event.tool_call_id, event.tool_name);
203
+ if (toolCallIndex === -1) return false;
204
+ const existingRaw = toolCalls[toolCallIndex].argsRaw || "";
205
+ toolCalls[toolCallIndex] = {
206
+ ...toolCalls[toolCallIndex],
207
+ argsRaw: `${existingRaw}${chunk}`
208
+ };
209
+ const blocks = cloneBlocks(assistant.blocks) || [];
210
+ const blockIndex = blocks.findIndex(
211
+ (block) => block.type === "tool_call" && (block.toolCallId === event.tool_call_id || !event.tool_call_id && event.tool_name && block.toolName === event.tool_name)
212
+ );
213
+ if (blockIndex !== -1) {
214
+ blocks[blockIndex] = {
215
+ ...blocks[blockIndex],
216
+ toolArgsRaw: `${blocks[blockIndex].toolArgsRaw || ""}${chunk}`
217
+ };
218
+ }
219
+ snapshot[assistantIndex] = {
220
+ ...assistant,
221
+ toolCalls,
222
+ blocks
223
+ };
224
+ return true;
225
+ }
226
+ case "tool_args": {
227
+ const toolCalls = [...assistant.toolCalls || []];
228
+ const toolCallIndex = findToolCallIndex(toolCalls, event.tool_call_id, event.tool_name);
229
+ if (toolCallIndex === -1) return false;
230
+ toolCalls[toolCallIndex] = {
231
+ ...toolCalls[toolCallIndex],
232
+ args: event.args || {},
233
+ argsRaw: void 0
234
+ };
235
+ const blocks = cloneBlocks(assistant.blocks) || [];
236
+ const blockIndex = blocks.findIndex(
237
+ (block) => block.type === "tool_call" && (block.toolCallId === event.tool_call_id || !event.tool_call_id && event.tool_name && block.toolName === event.tool_name)
238
+ );
239
+ if (blockIndex !== -1) {
240
+ blocks[blockIndex] = {
241
+ ...blocks[blockIndex],
242
+ toolArgs: event.args || {},
243
+ toolArgsRaw: void 0
244
+ };
245
+ }
246
+ snapshot[assistantIndex] = {
247
+ ...assistant,
248
+ toolCalls,
249
+ blocks
250
+ };
251
+ return true;
252
+ }
253
+ case "tool_result": {
254
+ const toolCalls = (assistant.toolCalls || []).map((toolCall) => toolCall.id === event.tool_call_id ? { ...toolCall, status: "completed", result: event.result } : toolCall);
255
+ const hasRunning = toolCalls.some((toolCall) => toolCall.status === "running");
256
+ const blocks = cloneBlocks(assistant.blocks) || [];
257
+ const toolBlockIndex = blocks.findIndex(
258
+ (block) => block.type === "tool_call" && block.toolCallId === event.tool_call_id
259
+ );
260
+ if (toolBlockIndex !== -1) {
261
+ blocks[toolBlockIndex] = {
262
+ ...blocks[toolBlockIndex],
263
+ toolStatus: "completed"
264
+ };
265
+ }
266
+ blocks.push({
267
+ type: "tool_result",
268
+ content: typeof event.result === "string" ? event.result : JSON.stringify(event.result),
269
+ timestamp: Date.now(),
270
+ toolName: toolBlockIndex !== -1 ? blocks[toolBlockIndex].toolName : void 0,
271
+ toolCallId: event.tool_call_id,
272
+ isIntermediate: true
273
+ });
274
+ snapshot[assistantIndex] = {
275
+ ...assistant,
276
+ toolCalls,
277
+ blocks,
278
+ isToolCallsStreaming: hasRunning
279
+ };
280
+ return true;
281
+ }
282
+ case "done": {
283
+ snapshot[assistantIndex] = {
284
+ ...assistant,
285
+ thinking: assistant.thinking?.trimEnd(),
286
+ currentThinking: void 0,
287
+ isStreaming: false,
288
+ isComplete: true,
289
+ isThinkingStreaming: false,
290
+ isThinkingPaused: false,
291
+ isToolCallsStreaming: false
292
+ };
293
+ return true;
294
+ }
295
+ case "cancelled": {
296
+ snapshot[assistantIndex] = {
297
+ ...assistant,
298
+ thinking: assistant.thinking?.trimEnd(),
299
+ currentThinking: void 0,
300
+ isStreaming: false,
301
+ isComplete: true,
302
+ isThinkingStreaming: false,
303
+ isThinkingPaused: false,
304
+ isToolCallsStreaming: false
305
+ };
306
+ return true;
307
+ }
308
+ case "error": {
309
+ snapshot[assistantIndex] = {
310
+ ...assistant,
311
+ content: assistant.content || `Error: ${event.error}`,
312
+ isStreaming: false,
313
+ isComplete: true,
314
+ isThinkingStreaming: false,
315
+ isThinkingPaused: false,
316
+ isToolCallsStreaming: false
317
+ };
318
+ return true;
319
+ }
320
+ default:
321
+ return false;
322
+ }
323
+ }
324
+ function cloneMessages(messages) {
325
+ return messages.map((message) => ({
326
+ ...message,
327
+ toolCalls: message.toolCalls?.map((toolCall) => ({
328
+ ...toolCall,
329
+ args: toolCall.args ? { ...toolCall.args } : toolCall.args
330
+ })),
331
+ blocks: cloneBlocks(message.blocks),
332
+ filePaths: message.filePaths ? [...message.filePaths] : message.filePaths,
333
+ attachedResources: message.attachedResources?.map((resource) => ({ ...resource }))
334
+ }));
335
+ }
336
+ function normalizeConversationMessages(messages) {
337
+ return messages.filter((message) => message.role !== "tool").map((message) => ({
338
+ ...message,
339
+ isStreaming: false,
340
+ isComplete: true
341
+ }));
342
+ }
343
+ function delay(ms) {
344
+ return new Promise((resolve) => setTimeout(resolve, ms));
345
+ }
110
346
  var CHAT_CAPABILITIES = {
111
347
  conversationSwitch: {
112
348
  supportsCancelActiveStream: true,
@@ -127,6 +363,7 @@ function useChat(options) {
127
363
  const isMountedRef = (0, import_react.useRef)(true);
128
364
  const messagesRef = (0, import_react.useRef)(messages);
129
365
  const conversationIdRef = (0, import_react.useRef)(conversationId);
366
+ const detachedConversationSnapshotsRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
130
367
  const currentRunIdRef = (0, import_react.useRef)(null);
131
368
  const pendingInterruptStreamIdRef = (0, import_react.useRef)(null);
132
369
  const currentAgentIdRef = (0, import_react.useRef)(null);
@@ -134,6 +371,8 @@ function useChat(options) {
134
371
  const pendingCancelFnRef = (0, import_react.useRef)(null);
135
372
  const suppressStreamRef = (0, import_react.useRef)(false);
136
373
  const activeStreamContextRef = (0, import_react.useRef)(null);
374
+ const streamContextsRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
375
+ const conversationStreamTokensRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
137
376
  const currentBlocksRef = (0, import_react.useRef)([]);
138
377
  const currentTextBlockIndexRef = (0, import_react.useRef)(-1);
139
378
  const needsContentClearRef = (0, import_react.useRef)(false);
@@ -157,6 +396,13 @@ function useChat(options) {
157
396
  return () => {
158
397
  isMountedRef.current = false;
159
398
  cancelRef.current?.();
399
+ streamContextsRef.current.forEach((context) => {
400
+ context.suppressed = true;
401
+ context.terminal = true;
402
+ context.cancel?.();
403
+ });
404
+ streamContextsRef.current.clear();
405
+ conversationStreamTokensRef.current.clear();
160
406
  if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
161
407
  };
162
408
  }, []);
@@ -221,13 +467,130 @@ function useChat(options) {
221
467
  pendingCancelRef.current = false;
222
468
  pendingCancelFnRef.current = null;
223
469
  }, []);
470
+ const setDetachedConversationSnapshot = (0, import_react.useCallback)((id, snapshot) => {
471
+ if (!id || snapshot.length === 0) return;
472
+ const map = detachedConversationSnapshotsRef.current;
473
+ if (map.has(id)) {
474
+ map.delete(id);
475
+ }
476
+ map.set(id, cloneMessages(snapshot));
477
+ while (map.size > MAX_DETACHED_SNAPSHOTS) {
478
+ const oldestKey = map.keys().next().value;
479
+ if (!oldestKey) break;
480
+ map.delete(oldestKey);
481
+ }
482
+ }, []);
483
+ const upsertConversationStreamToken = (0, import_react.useCallback)((id, token) => {
484
+ const map = conversationStreamTokensRef.current;
485
+ const existing = map.get(id);
486
+ if (existing) {
487
+ existing.delete(token);
488
+ existing.add(token);
489
+ return;
490
+ }
491
+ map.set(id, /* @__PURE__ */ new Set([token]));
492
+ }, []);
493
+ const removeConversationStreamToken = (0, import_react.useCallback)((id, token) => {
494
+ const map = conversationStreamTokensRef.current;
495
+ const tokens = map.get(id);
496
+ if (!tokens) return;
497
+ tokens.delete(token);
498
+ if (tokens.size === 0) {
499
+ map.delete(id);
500
+ }
501
+ }, []);
502
+ const bindStreamToConversation = (0, import_react.useCallback)((streamContext, id) => {
503
+ const nextConversationId = typeof id === "string" && id.trim().length > 0 ? id : null;
504
+ const previousConversationId = streamContext.conversationId;
505
+ if (previousConversationId && previousConversationId !== nextConversationId) {
506
+ removeConversationStreamToken(previousConversationId, streamContext.token);
507
+ }
508
+ streamContext.conversationId = nextConversationId;
509
+ if (nextConversationId) {
510
+ upsertConversationStreamToken(nextConversationId, streamContext.token);
511
+ }
512
+ }, [removeConversationStreamToken, upsertConversationStreamToken]);
513
+ const touchStreamContext = (0, import_react.useCallback)((streamContext) => {
514
+ streamContext.updatedAt = Date.now();
515
+ const id = streamContext.conversationId;
516
+ if (!id) return;
517
+ upsertConversationStreamToken(id, streamContext.token);
518
+ }, [upsertConversationStreamToken]);
519
+ const getLatestConversationStreamContext = (0, import_react.useCallback)((id) => {
520
+ if (!id) return null;
521
+ const tokenSet = conversationStreamTokensRef.current.get(id);
522
+ if (!tokenSet || tokenSet.size === 0) return null;
523
+ const tokens = Array.from(tokenSet.values()).reverse();
524
+ for (const token of tokens) {
525
+ const context = streamContextsRef.current.get(token);
526
+ if (!context || context.terminal) {
527
+ tokenSet.delete(token);
528
+ continue;
529
+ }
530
+ if (context.suppressed) {
531
+ continue;
532
+ }
533
+ return context;
534
+ }
535
+ if (tokenSet.size === 0) {
536
+ conversationStreamTokensRef.current.delete(id);
537
+ }
538
+ return null;
539
+ }, []);
540
+ const hasLiveConversationStream = (0, import_react.useCallback)((id) => {
541
+ return getLatestConversationStreamContext(id) !== null;
542
+ }, [getLatestConversationStreamContext]);
543
+ const finalizeStreamContext = (0, import_react.useCallback)((streamContext) => {
544
+ if (streamContext.terminal) {
545
+ return;
546
+ }
547
+ streamContext.terminal = true;
548
+ if (streamContext.conversationId) {
549
+ removeConversationStreamToken(streamContext.conversationId, streamContext.token);
550
+ }
551
+ streamContextsRef.current.delete(streamContext.token);
552
+ if (activeStreamContextRef.current?.token === streamContext.token) {
553
+ activeStreamContextRef.current = null;
554
+ }
555
+ }, [removeConversationStreamToken]);
224
556
  const detachActiveStream = (0, import_react.useCallback)((detachContext) => {
225
557
  const context = activeStreamContextRef.current;
226
- if (!context || context.detached) {
558
+ if (!context || context.detached || context.terminal) {
227
559
  return;
228
560
  }
561
+ const normalizedLiveMessages = cloneMessages(normalizeConversationMessages(messagesRef.current));
562
+ const liveSnapshotHasAssistant = normalizedLiveMessages.some(
563
+ (message) => message.id === context.assistantMessageId
564
+ );
565
+ const detachedSnapshot = liveSnapshotHasAssistant ? normalizedLiveMessages : context.initialSnapshot ? cloneMessages(context.initialSnapshot) : normalizedLiveMessages;
566
+ const detachedAssistantMessageIndex = detachedSnapshot.findIndex(
567
+ (message) => message.id === context.assistantMessageId
568
+ );
569
+ if (detachedAssistantMessageIndex !== -1) {
570
+ const detachedAssistantMessage = detachedSnapshot[detachedAssistantMessageIndex];
571
+ const detachedContent = fullContentRef.current || displayedContentRef.current || detachedAssistantMessage.content;
572
+ detachedSnapshot[detachedAssistantMessageIndex] = {
573
+ ...detachedAssistantMessage,
574
+ content: detachedContent,
575
+ finalContent: detachedContent || detachedAssistantMessage.finalContent,
576
+ blocks: currentBlocksRef.current.length > 0 ? cloneBlocks(currentBlocksRef.current) : detachedAssistantMessage.blocks,
577
+ thinking: detachedAssistantMessage.thinking?.trimEnd(),
578
+ currentThinking: void 0,
579
+ isStreaming: false,
580
+ isThinkingStreaming: false,
581
+ isThinkingPaused: false,
582
+ isToolCallsStreaming: false,
583
+ isComplete: true
584
+ };
585
+ }
586
+ if (detachedSnapshot.length > 0) {
587
+ context.detachedSnapshot = detachedSnapshot;
588
+ setDetachedConversationSnapshot(context.conversationId, detachedSnapshot);
589
+ }
229
590
  context.detached = true;
591
+ context.suppressed = false;
230
592
  context.detachContext = detachContext;
593
+ touchStreamContext(context);
231
594
  if (typewriterIntervalRef.current) {
232
595
  clearTimeout(typewriterIntervalRef.current);
233
596
  typewriterIntervalRef.current = null;
@@ -241,19 +604,109 @@ function useChat(options) {
241
604
  pendingInterruptStreamIdRef.current = null;
242
605
  clearPendingCancel();
243
606
  suppressStreamRef.current = false;
244
- }, [clearPendingCancel, resetStreamBuffers]);
607
+ }, [clearPendingCancel, resetStreamBuffers, setDetachedConversationSnapshot, touchStreamContext]);
608
+ const refreshConversationIfVisible = (0, import_react.useCallback)(async (id) => {
609
+ if (!id || conversationIdRef.current !== id) return false;
610
+ try {
611
+ const detail = await adapter.getConversation(id);
612
+ if (!isMountedRef.current || conversationIdRef.current !== id) return false;
613
+ const normalizedMessages = normalizeConversationMessages(detail.messages);
614
+ if (normalizedMessages.length === 0) return false;
615
+ detachedConversationSnapshotsRef.current.delete(id);
616
+ setMessages(normalizedMessages);
617
+ setConversationTitle(detail.title ?? null);
618
+ setIsStreaming(hasLiveConversationStream(id) || normalizedMessages.some((message) => !!message.isStreaming));
619
+ return true;
620
+ } catch (refreshError) {
621
+ console.warn("[useChat] Failed to refresh conversation after detached stream completion:", refreshError);
622
+ return false;
623
+ }
624
+ }, [adapter, hasLiveConversationStream]);
625
+ const refreshConversationIfVisibleWithRetry = (0, import_react.useCallback)((id, retryDelaysMs) => {
626
+ if (!id) return;
627
+ void (async () => {
628
+ for (const retryDelayMs of retryDelaysMs) {
629
+ if (retryDelayMs > 0) {
630
+ await delay(retryDelayMs);
631
+ }
632
+ if (!isMountedRef.current || conversationIdRef.current !== id) {
633
+ return;
634
+ }
635
+ const refreshed = await refreshConversationIfVisible(id);
636
+ if (refreshed) {
637
+ return;
638
+ }
639
+ }
640
+ })();
641
+ }, [refreshConversationIfVisible]);
642
+ const syncDetachedSnapshotToVisibleConversation = (0, import_react.useCallback)((streamContext) => {
643
+ const id = streamContext.conversationId;
644
+ if (!id || conversationIdRef.current !== id) return;
645
+ const activeContext = activeStreamContextRef.current;
646
+ if (activeContext && !activeContext.detached && activeContext.conversationId === id) return;
647
+ if (!streamContext.detachedSnapshot?.length) return;
648
+ const nextMessages = cloneMessages(streamContext.detachedSnapshot);
649
+ setMessages(nextMessages);
650
+ setIsStreaming(
651
+ hasLiveConversationStream(id) || nextMessages.some((message) => message.isStreaming)
652
+ );
653
+ }, [hasLiveConversationStream]);
245
654
  const handleStreamEvent = (0, import_react.useCallback)((event, streamContext) => {
246
655
  if (!isMountedRef.current) return;
656
+ if (streamContext.terminal) return;
657
+ touchStreamContext(streamContext);
658
+ const isTerminalEvent = event.type === "done" || event.type === "cancelled" || event.type === "error";
659
+ if (streamContext.suppressed && event.type !== "start" && !isTerminalEvent) {
660
+ return;
661
+ }
247
662
  const isActiveStream = activeStreamContextRef.current?.token === streamContext.token && !streamContext.detached;
248
663
  if (!isActiveStream) {
664
+ if (event.type === "start" && event.conversationId) {
665
+ if (streamContext.conversationId && streamContext.conversationId !== event.conversationId) {
666
+ detachedConversationSnapshotsRef.current.delete(streamContext.conversationId);
667
+ }
668
+ bindStreamToConversation(streamContext, event.conversationId);
669
+ if (streamContext.detachedSnapshot?.length) {
670
+ setDetachedConversationSnapshot(event.conversationId, streamContext.detachedSnapshot);
671
+ }
672
+ }
673
+ const snapshotUpdated = updateDetachedSnapshotForEvent(streamContext, event);
674
+ if (snapshotUpdated && streamContext.conversationId && streamContext.detachedSnapshot?.length) {
675
+ setDetachedConversationSnapshot(streamContext.conversationId, streamContext.detachedSnapshot);
676
+ }
677
+ if (snapshotUpdated) {
678
+ syncDetachedSnapshotToVisibleConversation(streamContext);
679
+ }
249
680
  if (event.type === "done" && event.conversationId && !streamContext.didReportConversationChange) {
681
+ if (streamContext.conversationId && streamContext.conversationId !== event.conversationId) {
682
+ detachedConversationSnapshotsRef.current.delete(streamContext.conversationId);
683
+ }
684
+ bindStreamToConversation(streamContext, event.conversationId);
250
685
  streamContext.didReportConversationChange = true;
686
+ if (streamContext.detachedSnapshot?.length) {
687
+ setDetachedConversationSnapshot(event.conversationId, streamContext.detachedSnapshot);
688
+ }
251
689
  onConversationChange?.(event.conversationId, event.title, {
252
690
  source: "background",
253
691
  streamToken: streamContext.token,
254
692
  detached: true,
255
693
  detachContext: streamContext.detachContext
256
694
  });
695
+ refreshConversationIfVisibleWithRetry(event.conversationId, [0, 300, 900]);
696
+ } else if (event.type === "done" && event.conversationId && streamContext.didReportConversationChange) {
697
+ bindStreamToConversation(streamContext, event.conversationId);
698
+ if (streamContext.detachedSnapshot?.length) {
699
+ setDetachedConversationSnapshot(event.conversationId, streamContext.detachedSnapshot);
700
+ }
701
+ refreshConversationIfVisibleWithRetry(event.conversationId, [0, 300, 900]);
702
+ }
703
+ if (isTerminalEvent) {
704
+ streamContext.detachedSnapshot = void 0;
705
+ streamContext.initialSnapshot = void 0;
706
+ finalizeStreamContext(streamContext);
707
+ if (conversationIdRef.current === streamContext.conversationId) {
708
+ setIsStreaming(hasLiveConversationStream(streamContext.conversationId));
709
+ }
257
710
  }
258
711
  return;
259
712
  }
@@ -265,6 +718,9 @@ function useChat(options) {
265
718
  switch (event.type) {
266
719
  case "start": {
267
720
  currentRunIdRef.current = event.run_id;
721
+ if (event.conversationId) {
722
+ bindStreamToConversation(streamContext, event.conversationId);
723
+ }
268
724
  if (pendingCancelRef.current) {
269
725
  const pendingCancel = pendingCancelFnRef.current || cancelRef.current;
270
726
  if (pendingCancel) {
@@ -338,9 +794,9 @@ function useChat(options) {
338
794
  return updated;
339
795
  });
340
796
  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;
797
+ 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
798
  if (typewriterIntervalRef.current !== null) {
343
- typewriterIntervalRef.current = setTimeout(tick, delay);
799
+ typewriterIntervalRef.current = setTimeout(tick, delay2);
344
800
  }
345
801
  } else {
346
802
  typewriterIntervalRef.current = null;
@@ -603,6 +1059,7 @@ function useChat(options) {
603
1059
  });
604
1060
  resetStreamBuffers();
605
1061
  if (event.conversationId) {
1062
+ bindStreamToConversation(streamContext, event.conversationId);
606
1063
  streamContext.didReportConversationChange = true;
607
1064
  setConversationId(event.conversationId);
608
1065
  onConversationChange?.(event.conversationId, event.title, {
@@ -612,13 +1069,13 @@ function useChat(options) {
612
1069
  });
613
1070
  }
614
1071
  if (event.title) setConversationTitle(event.title);
615
- setIsStreaming(false);
616
1072
  setIsLoading(false);
617
1073
  currentRunIdRef.current = null;
618
1074
  pendingInterruptStreamIdRef.current = null;
619
1075
  suppressStreamRef.current = false;
620
1076
  clearPendingCancel();
621
- activeStreamContextRef.current = null;
1077
+ finalizeStreamContext(streamContext);
1078
+ setIsStreaming(hasLiveConversationStream(conversationIdRef.current));
622
1079
  break;
623
1080
  }
624
1081
  case "cancelled": {
@@ -649,14 +1106,14 @@ function useChat(options) {
649
1106
  return updated;
650
1107
  });
651
1108
  resetStreamBuffers();
652
- setIsStreaming(false);
653
1109
  setIsLoading(false);
654
1110
  setPendingInterrupt(null);
655
1111
  currentRunIdRef.current = null;
656
1112
  pendingInterruptStreamIdRef.current = null;
657
1113
  suppressStreamRef.current = false;
658
1114
  clearPendingCancel();
659
- activeStreamContextRef.current = null;
1115
+ finalizeStreamContext(streamContext);
1116
+ setIsStreaming(hasLiveConversationStream(conversationIdRef.current));
660
1117
  break;
661
1118
  }
662
1119
  case "error": {
@@ -679,13 +1136,13 @@ function useChat(options) {
679
1136
  resetStreamBuffers();
680
1137
  setError(event.error);
681
1138
  onError?.(new Error(event.error));
682
- setIsStreaming(false);
683
1139
  setIsLoading(false);
684
1140
  currentRunIdRef.current = null;
685
1141
  pendingInterruptStreamIdRef.current = null;
686
1142
  suppressStreamRef.current = false;
687
1143
  clearPendingCancel();
688
- activeStreamContextRef.current = null;
1144
+ finalizeStreamContext(streamContext);
1145
+ setIsStreaming(hasLiveConversationStream(conversationIdRef.current));
689
1146
  break;
690
1147
  }
691
1148
  case "interrupt": {
@@ -703,7 +1160,20 @@ function useChat(options) {
703
1160
  break;
704
1161
  }
705
1162
  }
706
- }, [clearPendingCancel, flushTypewriter, onConversationChange, onError, resetStreamBuffers]);
1163
+ }, [
1164
+ bindStreamToConversation,
1165
+ clearPendingCancel,
1166
+ finalizeStreamContext,
1167
+ flushTypewriter,
1168
+ hasLiveConversationStream,
1169
+ onConversationChange,
1170
+ onError,
1171
+ refreshConversationIfVisibleWithRetry,
1172
+ resetStreamBuffers,
1173
+ setDetachedConversationSnapshot,
1174
+ syncDetachedSnapshotToVisibleConversation,
1175
+ touchStreamContext
1176
+ ]);
707
1177
  const trySendMessage = (0, import_react.useCallback)(async (content, sendOptions) => {
708
1178
  const trimmedContent = content.trim();
709
1179
  const hasAttachedResources = (sendOptions?.attachedResources?.length ?? 0) > 0;
@@ -736,14 +1206,27 @@ function useChat(options) {
736
1206
  blocks: [],
737
1207
  isComplete: false
738
1208
  };
1209
+ const shouldRenderUserMessage = trimmedContent.length > 0 || hasAttachedResources;
1210
+ const initialSnapshot = cloneMessages(normalizeConversationMessages(
1211
+ shouldRenderUserMessage ? [...messagesRef.current, userMessage, assistantMessage] : [...messagesRef.current, assistantMessage]
1212
+ ));
739
1213
  const streamContext = {
740
1214
  token: crypto.randomUUID(),
741
1215
  assistantMessageId: assistantMessage.id,
1216
+ conversationId: conversationIdRef.current,
742
1217
  detached: false,
1218
+ terminal: false,
1219
+ suppressed: false,
1220
+ updatedAt: Date.now(),
1221
+ pendingCancel: false,
1222
+ initialSnapshot,
743
1223
  didReportConversationChange: false
744
1224
  };
1225
+ streamContextsRef.current.set(streamContext.token, streamContext);
1226
+ if (streamContext.conversationId) {
1227
+ bindStreamToConversation(streamContext, streamContext.conversationId);
1228
+ }
745
1229
  activeStreamContextRef.current = streamContext;
746
- const shouldRenderUserMessage = trimmedContent.length > 0 || hasAttachedResources;
747
1230
  setMessages(
748
1231
  (prev) => shouldRenderUserMessage ? [...prev, userMessage, assistantMessage] : [...prev, assistantMessage]
749
1232
  );
@@ -772,6 +1255,11 @@ function useChat(options) {
772
1255
  sessionResources: sessionResourceIds.length ? sessionResourceIds : void 0
773
1256
  }
774
1257
  );
1258
+ streamContext.cancel = cancel;
1259
+ if (streamContext.pendingCancel) {
1260
+ streamContext.pendingCancel = false;
1261
+ cancel();
1262
+ }
775
1263
  cancelRef.current = cancel;
776
1264
  if (pendingCancelRef.current) {
777
1265
  pendingCancelFnRef.current = cancel;
@@ -800,56 +1288,132 @@ function useChat(options) {
800
1288
  pendingInterruptStreamIdRef.current = null;
801
1289
  setIsLoading(false);
802
1290
  setIsStreaming(false);
803
- if (activeStreamContextRef.current?.token === streamContext.token) {
804
- activeStreamContextRef.current = null;
805
- }
1291
+ finalizeStreamContext(streamContext);
806
1292
  return false;
807
1293
  }
808
- }, [adapter, clearPendingCancel, handleStreamEvent, onError, resetStreamBuffers, sessionResources]);
1294
+ }, [
1295
+ adapter,
1296
+ bindStreamToConversation,
1297
+ clearPendingCancel,
1298
+ finalizeStreamContext,
1299
+ handleStreamEvent,
1300
+ onError,
1301
+ resetStreamBuffers,
1302
+ sessionResources
1303
+ ]);
809
1304
  const sendMessage = (0, import_react.useCallback)(async (content, sendOptions) => {
810
1305
  await trySendMessage(content, sendOptions);
811
1306
  }, [trySendMessage]);
1307
+ const getVisibleStreamContext = (0, import_react.useCallback)(() => {
1308
+ const activeContext = activeStreamContextRef.current;
1309
+ if (activeContext && !activeContext.detached && !activeContext.terminal) {
1310
+ return activeContext;
1311
+ }
1312
+ return getLatestConversationStreamContext(conversationIdRef.current);
1313
+ }, [getLatestConversationStreamContext]);
812
1314
  const stopStreaming = (0, import_react.useCallback)(() => {
813
- pendingCancelRef.current = true;
814
- pendingCancelFnRef.current = cancelRef.current;
815
- suppressStreamRef.current = true;
816
- if (cancelRef.current) {
817
- cancelRef.current();
818
- clearPendingCancel();
819
- cancelRef.current = null;
1315
+ const targetContext = getVisibleStreamContext();
1316
+ if (!targetContext) {
1317
+ setIsStreaming(false);
1318
+ setIsLoading(false);
1319
+ return;
820
1320
  }
821
- flushTypewriter();
822
- setMessages((prev) => {
823
- const last = [...prev].reverse().find((m) => m.role === "assistant");
824
- if (!last?.isStreaming) return prev;
825
- return prev.map((m) => {
826
- if (m.id !== last.id) return m;
827
- return {
828
- ...m,
829
- content: fullContentRef.current || m.content,
830
- thinking: m.thinking?.trimEnd(),
1321
+ targetContext.suppressed = true;
1322
+ touchStreamContext(targetContext);
1323
+ const isActiveVisibleStream = activeStreamContextRef.current?.token === targetContext.token && !targetContext.detached;
1324
+ if (isActiveVisibleStream) {
1325
+ pendingCancelRef.current = true;
1326
+ pendingCancelFnRef.current = cancelRef.current;
1327
+ suppressStreamRef.current = true;
1328
+ if (cancelRef.current) {
1329
+ cancelRef.current();
1330
+ clearPendingCancel();
1331
+ cancelRef.current = null;
1332
+ }
1333
+ flushTypewriter();
1334
+ setMessages((prev) => {
1335
+ const last = [...prev].reverse().find((m) => m.role === "assistant");
1336
+ if (!last?.isStreaming) return prev;
1337
+ return prev.map((m) => {
1338
+ if (m.id !== last.id) return m;
1339
+ return {
1340
+ ...m,
1341
+ content: fullContentRef.current || m.content,
1342
+ thinking: m.thinking?.trimEnd(),
1343
+ currentThinking: void 0,
1344
+ isStreaming: false,
1345
+ isThinkingStreaming: false,
1346
+ isThinkingPaused: false,
1347
+ isToolCallsStreaming: false,
1348
+ isComplete: true,
1349
+ toolCalls: m.toolCalls?.map(
1350
+ (tc) => tc.status === "running" ? { ...tc, status: "cancelled" } : tc
1351
+ ),
1352
+ blocks: [...currentBlocksRef.current]
1353
+ };
1354
+ });
1355
+ });
1356
+ resetStreamBuffers();
1357
+ currentRunIdRef.current = null;
1358
+ pendingInterruptStreamIdRef.current = null;
1359
+ activeStreamContextRef.current = null;
1360
+ setIsStreaming(false);
1361
+ setIsLoading(false);
1362
+ return;
1363
+ }
1364
+ suppressStreamRef.current = false;
1365
+ const cancelStream = targetContext.cancel;
1366
+ if (cancelStream) {
1367
+ cancelStream();
1368
+ } else {
1369
+ targetContext.pendingCancel = true;
1370
+ }
1371
+ const detachedSnapshot = targetContext.detachedSnapshot;
1372
+ if (detachedSnapshot?.length) {
1373
+ const assistantIndex = detachedSnapshot.findIndex((message) => message.id === targetContext.assistantMessageId);
1374
+ if (assistantIndex !== -1) {
1375
+ const assistant = detachedSnapshot[assistantIndex];
1376
+ detachedSnapshot[assistantIndex] = {
1377
+ ...assistant,
1378
+ thinking: assistant.thinking?.trimEnd(),
831
1379
  currentThinking: void 0,
832
1380
  isStreaming: false,
1381
+ isComplete: true,
833
1382
  isThinkingStreaming: false,
834
1383
  isThinkingPaused: false,
835
1384
  isToolCallsStreaming: false,
836
- isComplete: true,
837
- toolCalls: m.toolCalls?.map(
838
- (tc) => tc.status === "running" ? { ...tc, status: "cancelled" } : tc
839
- ),
840
- blocks: [...currentBlocksRef.current]
1385
+ toolCalls: assistant.toolCalls?.map((tc) => tc.status === "running" ? { ...tc, status: "cancelled" } : tc)
841
1386
  };
842
- });
1387
+ }
1388
+ setDetachedConversationSnapshot(targetContext.conversationId, detachedSnapshot);
1389
+ if (targetContext.conversationId && targetContext.conversationId === conversationIdRef.current) {
1390
+ setMessages(cloneMessages(detachedSnapshot));
1391
+ }
1392
+ }
1393
+ setIsStreaming(hasLiveConversationStream(conversationIdRef.current));
1394
+ setIsLoading(false);
1395
+ }, [
1396
+ clearPendingCancel,
1397
+ flushTypewriter,
1398
+ getVisibleStreamContext,
1399
+ hasLiveConversationStream,
1400
+ resetStreamBuffers,
1401
+ setDetachedConversationSnapshot,
1402
+ touchStreamContext
1403
+ ]);
1404
+ const cancelAllStreamContexts = (0, import_react.useCallback)(() => {
1405
+ streamContextsRef.current.forEach((context) => {
1406
+ context.suppressed = true;
1407
+ context.terminal = true;
1408
+ context.cancel?.();
843
1409
  });
844
- resetStreamBuffers();
845
- currentRunIdRef.current = null;
846
- pendingInterruptStreamIdRef.current = null;
1410
+ streamContextsRef.current.clear();
1411
+ conversationStreamTokensRef.current.clear();
847
1412
  activeStreamContextRef.current = null;
848
- setIsStreaming(false);
849
- setIsLoading(false);
850
- }, [clearPendingCancel, flushTypewriter, resetStreamBuffers]);
1413
+ cancelRef.current = null;
1414
+ }, []);
851
1415
  const clearMessages = (0, import_react.useCallback)(() => {
852
- cancelRef.current?.();
1416
+ cancelAllStreamContexts();
853
1417
  if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
854
1418
  setMessages([]);
855
1419
  setError(null);
@@ -858,17 +1422,17 @@ function useChat(options) {
858
1422
  resetStreamBuffers();
859
1423
  currentRunIdRef.current = null;
860
1424
  pendingInterruptStreamIdRef.current = null;
861
- activeStreamContextRef.current = null;
862
1425
  clearPendingCancel();
863
1426
  suppressStreamRef.current = false;
864
- }, [clearPendingCancel, resetStreamBuffers]);
1427
+ detachedConversationSnapshotsRef.current.clear();
1428
+ }, [cancelAllStreamContexts, clearPendingCancel, resetStreamBuffers]);
865
1429
  const loadConversation = (0, import_react.useCallback)(async (id, optionsArg) => {
866
1430
  const cancelActiveStream = optionsArg?.cancelActiveStream ?? true;
867
- const activeStream = activeStreamContextRef.current;
868
- if (activeStream && !activeStream.detached) {
1431
+ const visibleStream = getVisibleStreamContext();
1432
+ if (visibleStream) {
869
1433
  if (cancelActiveStream) {
870
1434
  stopStreaming();
871
- } else {
1435
+ } else if (activeStreamContextRef.current && !activeStreamContextRef.current.detached) {
872
1436
  detachActiveStream(optionsArg?.detachContext);
873
1437
  }
874
1438
  }
@@ -877,11 +1441,19 @@ function useChat(options) {
877
1441
  setError(null);
878
1442
  const detail = await adapter.getConversation(id);
879
1443
  if (!isMountedRef.current) return;
880
- setMessages(detail.messages.filter((m) => m.role !== "tool").map((m) => ({
881
- ...m,
882
- isStreaming: false,
883
- isComplete: true
884
- })));
1444
+ const normalizedMessages = normalizeConversationMessages(detail.messages);
1445
+ const detachedSnapshot = detachedConversationSnapshotsRef.current.get(id);
1446
+ const shouldUseDetachedSnapshot = normalizedMessages.length === 0 && !!detachedSnapshot?.length;
1447
+ const nextMessages = shouldUseDetachedSnapshot ? cloneMessages(detachedSnapshot) : normalizedMessages;
1448
+ if (!shouldUseDetachedSnapshot) {
1449
+ detachedConversationSnapshotsRef.current.delete(id);
1450
+ } else {
1451
+ refreshConversationIfVisibleWithRetry(id, [250, 800, 1800]);
1452
+ }
1453
+ setMessages(nextMessages);
1454
+ setIsStreaming(
1455
+ hasLiveConversationStream(detail.id) || nextMessages.some((message) => !!message.isStreaming)
1456
+ );
885
1457
  setConversationId(detail.id);
886
1458
  setConversationTitle(detail.title);
887
1459
  currentAgentIdRef.current = detail.agentId ?? currentAgentIdRef.current;
@@ -892,18 +1464,24 @@ function useChat(options) {
892
1464
  } finally {
893
1465
  if (isMountedRef.current) setIsLoading(false);
894
1466
  }
895
- }, [adapter, detachActiveStream, onError, stopStreaming]);
1467
+ }, [
1468
+ adapter,
1469
+ detachActiveStream,
1470
+ getVisibleStreamContext,
1471
+ hasLiveConversationStream,
1472
+ onError,
1473
+ refreshConversationIfVisibleWithRetry,
1474
+ stopStreaming
1475
+ ]);
896
1476
  const newConversation = (0, import_react.useCallback)((optionsArg) => {
897
1477
  const cancelActiveStream = optionsArg?.cancelActiveStream ?? true;
898
- const activeStream = activeStreamContextRef.current;
899
- if (activeStream && !activeStream.detached) {
1478
+ const visibleStream = getVisibleStreamContext();
1479
+ if (visibleStream) {
900
1480
  if (cancelActiveStream) {
901
1481
  stopStreaming();
902
- } else {
1482
+ } else if (activeStreamContextRef.current && !activeStreamContextRef.current.detached) {
903
1483
  detachActiveStream(optionsArg?.detachContext);
904
1484
  }
905
- } else if (cancelActiveStream) {
906
- cancelRef.current?.();
907
1485
  }
908
1486
  if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
909
1487
  setMessages([]);
@@ -918,7 +1496,7 @@ function useChat(options) {
918
1496
  pendingInterruptStreamIdRef.current = null;
919
1497
  activeStreamContextRef.current = null;
920
1498
  clearPendingCancel();
921
- }, [clearPendingCancel, detachActiveStream, resetStreamBuffers, stopStreaming]);
1499
+ }, [clearPendingCancel, detachActiveStream, getVisibleStreamContext, resetStreamBuffers, stopStreaming]);
922
1500
  const sendHitlResponse = (0, import_react.useCallback)((response) => {
923
1501
  const runId = currentRunIdRef.current ?? void 0;
924
1502
  const streamId = pendingInterruptStreamIdRef.current ?? void 0;
@@ -1363,13 +1941,13 @@ function useConnection(options) {
1363
1941
  const isFirstAttempt = status === "disconnected" && reconnectAttemptsRef.current === 0;
1364
1942
  const baseDelay = isFirstAttempt ? 0 : Math.min(500 * Math.pow(2, reconnectAttemptsRef.current), 5e3);
1365
1943
  const jitter = isFirstAttempt ? 0 : Math.random() * 500;
1366
- const delay = baseDelay + jitter;
1944
+ const delay2 = baseDelay + jitter;
1367
1945
  const timer = setTimeout(() => {
1368
1946
  if (!isMountedRef.current) return;
1369
1947
  reconnectAttemptsRef.current++;
1370
1948
  adapter.connect().catch(() => {
1371
1949
  });
1372
- }, delay);
1950
+ }, delay2);
1373
1951
  return () => clearTimeout(timer);
1374
1952
  }, [status, autoConnect, adapter]);
1375
1953
  (0, import_react4.useEffect)(() => {
@@ -10354,7 +10932,7 @@ var HistoryList = (0, import_react36.memo)(function HistoryList2({
10354
10932
  deleteText: "#ef4444",
10355
10933
  loadingDot: isDarkMode ? "rgba(255, 255, 255, 0.3)" : "rgba(0, 0, 0, 0.2)"
10356
10934
  };
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)(
10935
+ 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
10936
  "span",
10359
10937
  {
10360
10938
  style: {
@@ -10363,10 +10941,10 @@ var HistoryList = (0, import_react36.memo)(function HistoryList2({
10363
10941
  borderRadius: "50%",
10364
10942
  background: colors.loadingDot,
10365
10943
  animation: "bounce 1s infinite",
10366
- animationDelay: `${delay}ms`
10944
+ animationDelay: `${delay2}ms`
10367
10945
  }
10368
10946
  },
10369
- delay
10947
+ delay2
10370
10948
  )) }) });
10371
10949
  if (isLoading && conversations.length === 0) {
10372
10950
  return loadingDots;