@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.
- package/dist/main/index.js +30 -1
- package/dist/main/index.mjs +30 -1
- package/dist/renderer/index.js +646 -68
- package/dist/renderer/index.mjs +646 -68
- 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,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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
804
|
-
activeStreamContextRef.current = null;
|
|
805
|
-
}
|
|
1291
|
+
finalizeStreamContext(streamContext);
|
|
806
1292
|
return false;
|
|
807
1293
|
}
|
|
808
|
-
}, [
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
clearPendingCancel();
|
|
819
|
-
cancelRef.current = null;
|
|
1315
|
+
const targetContext = getVisibleStreamContext();
|
|
1316
|
+
if (!targetContext) {
|
|
1317
|
+
setIsStreaming(false);
|
|
1318
|
+
setIsLoading(false);
|
|
1319
|
+
return;
|
|
820
1320
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
-
|
|
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
|
-
|
|
845
|
-
|
|
846
|
-
pendingInterruptStreamIdRef.current = null;
|
|
1410
|
+
streamContextsRef.current.clear();
|
|
1411
|
+
conversationStreamTokensRef.current.clear();
|
|
847
1412
|
activeStreamContextRef.current = null;
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
}, [clearPendingCancel, flushTypewriter, resetStreamBuffers]);
|
|
1413
|
+
cancelRef.current = null;
|
|
1414
|
+
}, []);
|
|
851
1415
|
const clearMessages = (0, import_react.useCallback)(() => {
|
|
852
|
-
|
|
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
|
-
|
|
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
|
|
868
|
-
if (
|
|
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
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
}, [
|
|
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
|
|
899
|
-
if (
|
|
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
|
|
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
|
-
},
|
|
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((
|
|
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: `${
|
|
10944
|
+
animationDelay: `${delay2}ms`
|
|
10367
10945
|
}
|
|
10368
10946
|
},
|
|
10369
|
-
|
|
10947
|
+
delay2
|
|
10370
10948
|
)) }) });
|
|
10371
10949
|
if (isLoading && conversations.length === 0) {
|
|
10372
10950
|
return loadingDots;
|