@yushaw/sanqian-chat 0.2.44 → 0.3.1
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/core/index.d.mts +8 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.js +98 -9
- package/dist/core/index.mjs +98 -9
- package/dist/main/index.js +43 -2
- package/dist/main/index.mjs +43 -2
- package/dist/renderer/index.d.mts +8 -0
- package/dist/renderer/index.d.ts +8 -0
- package/dist/renderer/index.js +678 -88
- package/dist/renderer/index.mjs +678 -88
- package/package.json +2 -2
package/dist/renderer/index.js
CHANGED
|
@@ -88,6 +88,67 @@ module.exports = __toCommonJS(renderer_exports);
|
|
|
88
88
|
|
|
89
89
|
// src/renderer/hooks/useChat.ts
|
|
90
90
|
var import_react = require("react");
|
|
91
|
+
|
|
92
|
+
// src/core/tool-status.ts
|
|
93
|
+
function normalizeToolExecutionStatus(status) {
|
|
94
|
+
if (typeof status !== "string") return void 0;
|
|
95
|
+
switch (status.trim().toLowerCase()) {
|
|
96
|
+
case "success":
|
|
97
|
+
case "completed":
|
|
98
|
+
case "ok":
|
|
99
|
+
return "completed";
|
|
100
|
+
case "error":
|
|
101
|
+
case "failed":
|
|
102
|
+
return "error";
|
|
103
|
+
case "cancelled":
|
|
104
|
+
case "canceled":
|
|
105
|
+
return "cancelled";
|
|
106
|
+
case "running":
|
|
107
|
+
return "running";
|
|
108
|
+
case "pending":
|
|
109
|
+
return "pending";
|
|
110
|
+
default:
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function deriveToolExecutionStatus(options) {
|
|
115
|
+
if (typeof options.actionRequired === "string" && options.actionRequired.length > 0) {
|
|
116
|
+
return "error";
|
|
117
|
+
}
|
|
118
|
+
if (options.error === true) {
|
|
119
|
+
return "error";
|
|
120
|
+
}
|
|
121
|
+
if (typeof options.error === "string" && options.error.trim().length > 0) {
|
|
122
|
+
return "error";
|
|
123
|
+
}
|
|
124
|
+
const normalizedStatus = normalizeToolExecutionStatus(options.status);
|
|
125
|
+
if (normalizedStatus) {
|
|
126
|
+
return normalizedStatus;
|
|
127
|
+
}
|
|
128
|
+
if (options.result !== void 0 && options.result !== null) {
|
|
129
|
+
if (typeof options.result === "string") {
|
|
130
|
+
const normalizedResult = options.result.trimStart();
|
|
131
|
+
if (normalizedResult.startsWith("\u274C") || normalizedResult.startsWith("Error:")) {
|
|
132
|
+
return "error";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return "completed";
|
|
136
|
+
}
|
|
137
|
+
return options.fallback ?? "running";
|
|
138
|
+
}
|
|
139
|
+
function formatToolResultContent(result, error) {
|
|
140
|
+
const value = result ?? error;
|
|
141
|
+
if (value === void 0 || value === null) return void 0;
|
|
142
|
+
if (typeof value === "string") return value;
|
|
143
|
+
try {
|
|
144
|
+
const serialized = JSON.stringify(value);
|
|
145
|
+
return serialized === void 0 ? String(value) : serialized;
|
|
146
|
+
} catch {
|
|
147
|
+
return String(value);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/renderer/hooks/useChat.ts
|
|
91
152
|
var TYPEWRITER_DELAYS = { VERY_FAST: 2, FAST: 5, NORMAL: 10, SLOW: 20 };
|
|
92
153
|
var TYPEWRITER_THRESHOLDS = { VERY_FAST: 100, FAST: 50, NORMAL: 20 };
|
|
93
154
|
var MAX_DETACHED_SNAPSHOTS = 30;
|
|
@@ -108,12 +169,240 @@ var findLastBlock = (blocks, predicate) => {
|
|
|
108
169
|
}
|
|
109
170
|
return void 0;
|
|
110
171
|
};
|
|
172
|
+
function getToolResultStatus(event) {
|
|
173
|
+
return deriveToolExecutionStatus({
|
|
174
|
+
status: event.status,
|
|
175
|
+
result: event.result,
|
|
176
|
+
actionRequired: event.action_required,
|
|
177
|
+
error: event.error,
|
|
178
|
+
fallback: "completed"
|
|
179
|
+
});
|
|
180
|
+
}
|
|
111
181
|
function cloneBlocks(blocks) {
|
|
112
182
|
return blocks?.map((block) => ({
|
|
113
183
|
...block,
|
|
114
184
|
toolArgs: block.toolArgs ? { ...block.toolArgs } : block.toolArgs
|
|
115
185
|
}));
|
|
116
186
|
}
|
|
187
|
+
function updateDetachedSnapshotForEvent(streamContext, event) {
|
|
188
|
+
const snapshot = streamContext.detachedSnapshot;
|
|
189
|
+
if (!snapshot?.length) return false;
|
|
190
|
+
const assistantIndex = snapshot.findIndex((message) => message.id === streamContext.assistantMessageId);
|
|
191
|
+
if (assistantIndex === -1) return false;
|
|
192
|
+
const assistant = snapshot[assistantIndex];
|
|
193
|
+
switch (event.type) {
|
|
194
|
+
case "text": {
|
|
195
|
+
const rawContent = event.content;
|
|
196
|
+
if (!rawContent) return false;
|
|
197
|
+
const nextChunk = assistant.content ? rawContent : rawContent.trimStart();
|
|
198
|
+
if (!nextChunk) return false;
|
|
199
|
+
snapshot[assistantIndex] = {
|
|
200
|
+
...assistant,
|
|
201
|
+
content: `${assistant.content}${nextChunk}`,
|
|
202
|
+
isStreaming: true,
|
|
203
|
+
isComplete: false
|
|
204
|
+
};
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
case "thinking": {
|
|
208
|
+
const rawThinking = event.content;
|
|
209
|
+
if (!rawThinking) return false;
|
|
210
|
+
const nextChunk = assistant.thinking ? rawThinking : rawThinking.trimStart();
|
|
211
|
+
if (!nextChunk) return false;
|
|
212
|
+
snapshot[assistantIndex] = {
|
|
213
|
+
...assistant,
|
|
214
|
+
thinking: `${assistant.thinking ?? ""}${nextChunk}`,
|
|
215
|
+
currentThinking: `${assistant.currentThinking ?? ""}${nextChunk}`,
|
|
216
|
+
isStreaming: true,
|
|
217
|
+
isComplete: false,
|
|
218
|
+
isThinkingStreaming: true,
|
|
219
|
+
isThinkingPaused: false
|
|
220
|
+
};
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
case "tool_call": {
|
|
224
|
+
const tc = event.tool_call;
|
|
225
|
+
if (!tc) return false;
|
|
226
|
+
const toolName = tc.function?.name || tc.name || "";
|
|
227
|
+
const toolId = tc.id || "";
|
|
228
|
+
let args = {};
|
|
229
|
+
if (tc.args) {
|
|
230
|
+
args = tc.args;
|
|
231
|
+
} else if (tc.function?.arguments) {
|
|
232
|
+
try {
|
|
233
|
+
args = JSON.parse(tc.function.arguments);
|
|
234
|
+
} catch {
|
|
235
|
+
args = {};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const toolCalls = [...assistant.toolCalls || []];
|
|
239
|
+
toolCalls.push({
|
|
240
|
+
id: toolId,
|
|
241
|
+
name: toolName,
|
|
242
|
+
args,
|
|
243
|
+
status: "running"
|
|
244
|
+
});
|
|
245
|
+
const blocks = cloneBlocks(assistant.blocks) || [];
|
|
246
|
+
blocks.push({
|
|
247
|
+
type: "tool_call",
|
|
248
|
+
content: "",
|
|
249
|
+
timestamp: Date.now(),
|
|
250
|
+
toolName,
|
|
251
|
+
toolArgs: args,
|
|
252
|
+
toolCallId: toolId,
|
|
253
|
+
toolStatus: "running",
|
|
254
|
+
isIntermediate: true
|
|
255
|
+
});
|
|
256
|
+
snapshot[assistantIndex] = {
|
|
257
|
+
...assistant,
|
|
258
|
+
toolCalls,
|
|
259
|
+
blocks,
|
|
260
|
+
isStreaming: true,
|
|
261
|
+
isComplete: false,
|
|
262
|
+
isThinkingStreaming: false,
|
|
263
|
+
isThinkingPaused: !!assistant.thinking,
|
|
264
|
+
isToolCallsStreaming: true
|
|
265
|
+
};
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
case "tool_args_chunk": {
|
|
269
|
+
const chunk = event.chunk;
|
|
270
|
+
if (!chunk) return false;
|
|
271
|
+
const toolCalls = [...assistant.toolCalls || []];
|
|
272
|
+
const toolCallIndex = findToolCallIndex(toolCalls, event.tool_call_id, event.tool_name);
|
|
273
|
+
if (toolCallIndex === -1) return false;
|
|
274
|
+
const existingRaw = toolCalls[toolCallIndex].argsRaw || "";
|
|
275
|
+
toolCalls[toolCallIndex] = {
|
|
276
|
+
...toolCalls[toolCallIndex],
|
|
277
|
+
argsRaw: `${existingRaw}${chunk}`
|
|
278
|
+
};
|
|
279
|
+
const blocks = cloneBlocks(assistant.blocks) || [];
|
|
280
|
+
const blockIndex = blocks.findIndex(
|
|
281
|
+
(block) => block.type === "tool_call" && (block.toolCallId === event.tool_call_id || !event.tool_call_id && event.tool_name && block.toolName === event.tool_name)
|
|
282
|
+
);
|
|
283
|
+
if (blockIndex !== -1) {
|
|
284
|
+
blocks[blockIndex] = {
|
|
285
|
+
...blocks[blockIndex],
|
|
286
|
+
toolArgsRaw: `${blocks[blockIndex].toolArgsRaw || ""}${chunk}`
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
snapshot[assistantIndex] = {
|
|
290
|
+
...assistant,
|
|
291
|
+
toolCalls,
|
|
292
|
+
blocks
|
|
293
|
+
};
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
case "tool_args": {
|
|
297
|
+
const toolCalls = [...assistant.toolCalls || []];
|
|
298
|
+
const toolCallIndex = findToolCallIndex(toolCalls, event.tool_call_id, event.tool_name);
|
|
299
|
+
if (toolCallIndex === -1) return false;
|
|
300
|
+
toolCalls[toolCallIndex] = {
|
|
301
|
+
...toolCalls[toolCallIndex],
|
|
302
|
+
args: event.args || {},
|
|
303
|
+
argsRaw: void 0
|
|
304
|
+
};
|
|
305
|
+
const blocks = cloneBlocks(assistant.blocks) || [];
|
|
306
|
+
const blockIndex = blocks.findIndex(
|
|
307
|
+
(block) => block.type === "tool_call" && (block.toolCallId === event.tool_call_id || !event.tool_call_id && event.tool_name && block.toolName === event.tool_name)
|
|
308
|
+
);
|
|
309
|
+
if (blockIndex !== -1) {
|
|
310
|
+
blocks[blockIndex] = {
|
|
311
|
+
...blocks[blockIndex],
|
|
312
|
+
toolArgs: event.args || {},
|
|
313
|
+
toolArgsRaw: void 0
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
snapshot[assistantIndex] = {
|
|
317
|
+
...assistant,
|
|
318
|
+
toolCalls,
|
|
319
|
+
blocks
|
|
320
|
+
};
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
case "tool_result": {
|
|
324
|
+
const toolStatus = getToolResultStatus(event);
|
|
325
|
+
const resultContent = formatToolResultContent(event.result, event.error);
|
|
326
|
+
const toolCalls = (assistant.toolCalls || []).map((toolCall) => toolCall.id === event.tool_call_id ? {
|
|
327
|
+
...toolCall,
|
|
328
|
+
status: toolStatus,
|
|
329
|
+
result: event.result,
|
|
330
|
+
error: toolStatus === "error" ? typeof event.error === "string" && event.error.trim().length > 0 ? event.error : resultContent : void 0,
|
|
331
|
+
actionRequired: event.action_required,
|
|
332
|
+
settingsTab: event.settings_tab,
|
|
333
|
+
settingsSubTab: event.settings_sub_tab
|
|
334
|
+
} : toolCall);
|
|
335
|
+
const hasRunning = toolCalls.some((toolCall) => toolCall.status === "running");
|
|
336
|
+
const blocks = cloneBlocks(assistant.blocks) || [];
|
|
337
|
+
const toolBlockIndex = blocks.findIndex(
|
|
338
|
+
(block) => block.type === "tool_call" && block.toolCallId === event.tool_call_id
|
|
339
|
+
);
|
|
340
|
+
if (toolBlockIndex !== -1) {
|
|
341
|
+
blocks[toolBlockIndex] = {
|
|
342
|
+
...blocks[toolBlockIndex],
|
|
343
|
+
toolStatus
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (resultContent) {
|
|
347
|
+
blocks.push({
|
|
348
|
+
type: "tool_result",
|
|
349
|
+
content: resultContent,
|
|
350
|
+
timestamp: Date.now(),
|
|
351
|
+
toolName: toolBlockIndex !== -1 ? blocks[toolBlockIndex].toolName : void 0,
|
|
352
|
+
toolCallId: event.tool_call_id,
|
|
353
|
+
isIntermediate: true
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
snapshot[assistantIndex] = {
|
|
357
|
+
...assistant,
|
|
358
|
+
toolCalls,
|
|
359
|
+
blocks,
|
|
360
|
+
isToolCallsStreaming: hasRunning
|
|
361
|
+
};
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
case "done": {
|
|
365
|
+
snapshot[assistantIndex] = {
|
|
366
|
+
...assistant,
|
|
367
|
+
thinking: assistant.thinking?.trimEnd(),
|
|
368
|
+
currentThinking: void 0,
|
|
369
|
+
isStreaming: false,
|
|
370
|
+
isComplete: true,
|
|
371
|
+
isThinkingStreaming: false,
|
|
372
|
+
isThinkingPaused: false,
|
|
373
|
+
isToolCallsStreaming: false
|
|
374
|
+
};
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
case "cancelled": {
|
|
378
|
+
snapshot[assistantIndex] = {
|
|
379
|
+
...assistant,
|
|
380
|
+
thinking: assistant.thinking?.trimEnd(),
|
|
381
|
+
currentThinking: void 0,
|
|
382
|
+
isStreaming: false,
|
|
383
|
+
isComplete: true,
|
|
384
|
+
isThinkingStreaming: false,
|
|
385
|
+
isThinkingPaused: false,
|
|
386
|
+
isToolCallsStreaming: false
|
|
387
|
+
};
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
case "error": {
|
|
391
|
+
snapshot[assistantIndex] = {
|
|
392
|
+
...assistant,
|
|
393
|
+
content: assistant.content || `Error: ${event.error}`,
|
|
394
|
+
isStreaming: false,
|
|
395
|
+
isComplete: true,
|
|
396
|
+
isThinkingStreaming: false,
|
|
397
|
+
isThinkingPaused: false,
|
|
398
|
+
isToolCallsStreaming: false
|
|
399
|
+
};
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
default:
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
117
406
|
function cloneMessages(messages) {
|
|
118
407
|
return messages.map((message) => ({
|
|
119
408
|
...message,
|
|
@@ -164,6 +453,8 @@ function useChat(options) {
|
|
|
164
453
|
const pendingCancelFnRef = (0, import_react.useRef)(null);
|
|
165
454
|
const suppressStreamRef = (0, import_react.useRef)(false);
|
|
166
455
|
const activeStreamContextRef = (0, import_react.useRef)(null);
|
|
456
|
+
const streamContextsRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
457
|
+
const conversationStreamTokensRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
167
458
|
const currentBlocksRef = (0, import_react.useRef)([]);
|
|
168
459
|
const currentTextBlockIndexRef = (0, import_react.useRef)(-1);
|
|
169
460
|
const needsContentClearRef = (0, import_react.useRef)(false);
|
|
@@ -187,6 +478,13 @@ function useChat(options) {
|
|
|
187
478
|
return () => {
|
|
188
479
|
isMountedRef.current = false;
|
|
189
480
|
cancelRef.current?.();
|
|
481
|
+
streamContextsRef.current.forEach((context) => {
|
|
482
|
+
context.suppressed = true;
|
|
483
|
+
context.terminal = true;
|
|
484
|
+
context.cancel?.();
|
|
485
|
+
});
|
|
486
|
+
streamContextsRef.current.clear();
|
|
487
|
+
conversationStreamTokensRef.current.clear();
|
|
190
488
|
if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
|
|
191
489
|
};
|
|
192
490
|
}, []);
|
|
@@ -264,12 +562,89 @@ function useChat(options) {
|
|
|
264
562
|
map.delete(oldestKey);
|
|
265
563
|
}
|
|
266
564
|
}, []);
|
|
565
|
+
const upsertConversationStreamToken = (0, import_react.useCallback)((id, token) => {
|
|
566
|
+
const map = conversationStreamTokensRef.current;
|
|
567
|
+
const existing = map.get(id);
|
|
568
|
+
if (existing) {
|
|
569
|
+
existing.delete(token);
|
|
570
|
+
existing.add(token);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
map.set(id, /* @__PURE__ */ new Set([token]));
|
|
574
|
+
}, []);
|
|
575
|
+
const removeConversationStreamToken = (0, import_react.useCallback)((id, token) => {
|
|
576
|
+
const map = conversationStreamTokensRef.current;
|
|
577
|
+
const tokens = map.get(id);
|
|
578
|
+
if (!tokens) return;
|
|
579
|
+
tokens.delete(token);
|
|
580
|
+
if (tokens.size === 0) {
|
|
581
|
+
map.delete(id);
|
|
582
|
+
}
|
|
583
|
+
}, []);
|
|
584
|
+
const bindStreamToConversation = (0, import_react.useCallback)((streamContext, id) => {
|
|
585
|
+
const nextConversationId = typeof id === "string" && id.trim().length > 0 ? id : null;
|
|
586
|
+
const previousConversationId = streamContext.conversationId;
|
|
587
|
+
if (previousConversationId && previousConversationId !== nextConversationId) {
|
|
588
|
+
removeConversationStreamToken(previousConversationId, streamContext.token);
|
|
589
|
+
}
|
|
590
|
+
streamContext.conversationId = nextConversationId;
|
|
591
|
+
if (nextConversationId) {
|
|
592
|
+
upsertConversationStreamToken(nextConversationId, streamContext.token);
|
|
593
|
+
}
|
|
594
|
+
}, [removeConversationStreamToken, upsertConversationStreamToken]);
|
|
595
|
+
const touchStreamContext = (0, import_react.useCallback)((streamContext) => {
|
|
596
|
+
streamContext.updatedAt = Date.now();
|
|
597
|
+
const id = streamContext.conversationId;
|
|
598
|
+
if (!id) return;
|
|
599
|
+
upsertConversationStreamToken(id, streamContext.token);
|
|
600
|
+
}, [upsertConversationStreamToken]);
|
|
601
|
+
const getLatestConversationStreamContext = (0, import_react.useCallback)((id) => {
|
|
602
|
+
if (!id) return null;
|
|
603
|
+
const tokenSet = conversationStreamTokensRef.current.get(id);
|
|
604
|
+
if (!tokenSet || tokenSet.size === 0) return null;
|
|
605
|
+
const tokens = Array.from(tokenSet.values()).reverse();
|
|
606
|
+
for (const token of tokens) {
|
|
607
|
+
const context = streamContextsRef.current.get(token);
|
|
608
|
+
if (!context || context.terminal) {
|
|
609
|
+
tokenSet.delete(token);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (context.suppressed) {
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
return context;
|
|
616
|
+
}
|
|
617
|
+
if (tokenSet.size === 0) {
|
|
618
|
+
conversationStreamTokensRef.current.delete(id);
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
}, []);
|
|
622
|
+
const hasLiveConversationStream = (0, import_react.useCallback)((id) => {
|
|
623
|
+
return getLatestConversationStreamContext(id) !== null;
|
|
624
|
+
}, [getLatestConversationStreamContext]);
|
|
625
|
+
const finalizeStreamContext = (0, import_react.useCallback)((streamContext) => {
|
|
626
|
+
if (streamContext.terminal) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
streamContext.terminal = true;
|
|
630
|
+
if (streamContext.conversationId) {
|
|
631
|
+
removeConversationStreamToken(streamContext.conversationId, streamContext.token);
|
|
632
|
+
}
|
|
633
|
+
streamContextsRef.current.delete(streamContext.token);
|
|
634
|
+
if (activeStreamContextRef.current?.token === streamContext.token) {
|
|
635
|
+
activeStreamContextRef.current = null;
|
|
636
|
+
}
|
|
637
|
+
}, [removeConversationStreamToken]);
|
|
267
638
|
const detachActiveStream = (0, import_react.useCallback)((detachContext) => {
|
|
268
639
|
const context = activeStreamContextRef.current;
|
|
269
|
-
if (!context || context.detached) {
|
|
640
|
+
if (!context || context.detached || context.terminal) {
|
|
270
641
|
return;
|
|
271
642
|
}
|
|
272
|
-
const
|
|
643
|
+
const normalizedLiveMessages = cloneMessages(normalizeConversationMessages(messagesRef.current));
|
|
644
|
+
const liveSnapshotHasAssistant = normalizedLiveMessages.some(
|
|
645
|
+
(message) => message.id === context.assistantMessageId
|
|
646
|
+
);
|
|
647
|
+
const detachedSnapshot = liveSnapshotHasAssistant ? normalizedLiveMessages : context.initialSnapshot ? cloneMessages(context.initialSnapshot) : normalizedLiveMessages;
|
|
273
648
|
const detachedAssistantMessageIndex = detachedSnapshot.findIndex(
|
|
274
649
|
(message) => message.id === context.assistantMessageId
|
|
275
650
|
);
|
|
@@ -295,7 +670,9 @@ function useChat(options) {
|
|
|
295
670
|
setDetachedConversationSnapshot(context.conversationId, detachedSnapshot);
|
|
296
671
|
}
|
|
297
672
|
context.detached = true;
|
|
673
|
+
context.suppressed = false;
|
|
298
674
|
context.detachContext = detachContext;
|
|
675
|
+
touchStreamContext(context);
|
|
299
676
|
if (typewriterIntervalRef.current) {
|
|
300
677
|
clearTimeout(typewriterIntervalRef.current);
|
|
301
678
|
typewriterIntervalRef.current = null;
|
|
@@ -309,7 +686,7 @@ function useChat(options) {
|
|
|
309
686
|
pendingInterruptStreamIdRef.current = null;
|
|
310
687
|
clearPendingCancel();
|
|
311
688
|
suppressStreamRef.current = false;
|
|
312
|
-
}, [clearPendingCancel, resetStreamBuffers, setDetachedConversationSnapshot]);
|
|
689
|
+
}, [clearPendingCancel, resetStreamBuffers, setDetachedConversationSnapshot, touchStreamContext]);
|
|
313
690
|
const refreshConversationIfVisible = (0, import_react.useCallback)(async (id) => {
|
|
314
691
|
if (!id || conversationIdRef.current !== id) return false;
|
|
315
692
|
try {
|
|
@@ -320,12 +697,13 @@ function useChat(options) {
|
|
|
320
697
|
detachedConversationSnapshotsRef.current.delete(id);
|
|
321
698
|
setMessages(normalizedMessages);
|
|
322
699
|
setConversationTitle(detail.title ?? null);
|
|
700
|
+
setIsStreaming(hasLiveConversationStream(id) || normalizedMessages.some((message) => !!message.isStreaming));
|
|
323
701
|
return true;
|
|
324
702
|
} catch (refreshError) {
|
|
325
703
|
console.warn("[useChat] Failed to refresh conversation after detached stream completion:", refreshError);
|
|
326
704
|
return false;
|
|
327
705
|
}
|
|
328
|
-
}, [adapter]);
|
|
706
|
+
}, [adapter, hasLiveConversationStream]);
|
|
329
707
|
const refreshConversationIfVisibleWithRetry = (0, import_react.useCallback)((id, retryDelaysMs) => {
|
|
330
708
|
if (!id) return;
|
|
331
709
|
void (async () => {
|
|
@@ -343,15 +721,49 @@ function useChat(options) {
|
|
|
343
721
|
}
|
|
344
722
|
})();
|
|
345
723
|
}, [refreshConversationIfVisible]);
|
|
724
|
+
const syncDetachedSnapshotToVisibleConversation = (0, import_react.useCallback)((streamContext) => {
|
|
725
|
+
const id = streamContext.conversationId;
|
|
726
|
+
if (!id || conversationIdRef.current !== id) return;
|
|
727
|
+
const activeContext = activeStreamContextRef.current;
|
|
728
|
+
if (activeContext && !activeContext.detached && activeContext.conversationId === id) return;
|
|
729
|
+
if (!streamContext.detachedSnapshot?.length) return;
|
|
730
|
+
const nextMessages = cloneMessages(streamContext.detachedSnapshot);
|
|
731
|
+
setMessages(nextMessages);
|
|
732
|
+
setIsStreaming(
|
|
733
|
+
hasLiveConversationStream(id) || nextMessages.some((message) => message.isStreaming)
|
|
734
|
+
);
|
|
735
|
+
}, [hasLiveConversationStream]);
|
|
346
736
|
const handleStreamEvent = (0, import_react.useCallback)((event, streamContext) => {
|
|
347
737
|
if (!isMountedRef.current) return;
|
|
738
|
+
if (streamContext.terminal) return;
|
|
739
|
+
touchStreamContext(streamContext);
|
|
740
|
+
const isTerminalEvent = event.type === "done" || event.type === "cancelled" || event.type === "error";
|
|
741
|
+
if (streamContext.suppressed && event.type !== "start" && !isTerminalEvent) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
348
744
|
const isActiveStream = activeStreamContextRef.current?.token === streamContext.token && !streamContext.detached;
|
|
349
745
|
if (!isActiveStream) {
|
|
746
|
+
if (event.type === "start" && event.conversationId) {
|
|
747
|
+
if (streamContext.conversationId && streamContext.conversationId !== event.conversationId) {
|
|
748
|
+
detachedConversationSnapshotsRef.current.delete(streamContext.conversationId);
|
|
749
|
+
}
|
|
750
|
+
bindStreamToConversation(streamContext, event.conversationId);
|
|
751
|
+
if (streamContext.detachedSnapshot?.length) {
|
|
752
|
+
setDetachedConversationSnapshot(event.conversationId, streamContext.detachedSnapshot);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const snapshotUpdated = updateDetachedSnapshotForEvent(streamContext, event);
|
|
756
|
+
if (snapshotUpdated && streamContext.conversationId && streamContext.detachedSnapshot?.length) {
|
|
757
|
+
setDetachedConversationSnapshot(streamContext.conversationId, streamContext.detachedSnapshot);
|
|
758
|
+
}
|
|
759
|
+
if (snapshotUpdated) {
|
|
760
|
+
syncDetachedSnapshotToVisibleConversation(streamContext);
|
|
761
|
+
}
|
|
350
762
|
if (event.type === "done" && event.conversationId && !streamContext.didReportConversationChange) {
|
|
351
763
|
if (streamContext.conversationId && streamContext.conversationId !== event.conversationId) {
|
|
352
764
|
detachedConversationSnapshotsRef.current.delete(streamContext.conversationId);
|
|
353
765
|
}
|
|
354
|
-
streamContext
|
|
766
|
+
bindStreamToConversation(streamContext, event.conversationId);
|
|
355
767
|
streamContext.didReportConversationChange = true;
|
|
356
768
|
if (streamContext.detachedSnapshot?.length) {
|
|
357
769
|
setDetachedConversationSnapshot(event.conversationId, streamContext.detachedSnapshot);
|
|
@@ -364,17 +776,19 @@ function useChat(options) {
|
|
|
364
776
|
});
|
|
365
777
|
refreshConversationIfVisibleWithRetry(event.conversationId, [0, 300, 900]);
|
|
366
778
|
} else if (event.type === "done" && event.conversationId && streamContext.didReportConversationChange) {
|
|
779
|
+
bindStreamToConversation(streamContext, event.conversationId);
|
|
367
780
|
if (streamContext.detachedSnapshot?.length) {
|
|
368
781
|
setDetachedConversationSnapshot(event.conversationId, streamContext.detachedSnapshot);
|
|
369
782
|
}
|
|
370
783
|
refreshConversationIfVisibleWithRetry(event.conversationId, [0, 300, 900]);
|
|
371
784
|
}
|
|
372
|
-
const isTerminalEvent = event.type === "done" || event.type === "cancelled" || event.type === "error";
|
|
373
785
|
if (isTerminalEvent) {
|
|
374
786
|
streamContext.detachedSnapshot = void 0;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
787
|
+
streamContext.initialSnapshot = void 0;
|
|
788
|
+
finalizeStreamContext(streamContext);
|
|
789
|
+
if (conversationIdRef.current === streamContext.conversationId) {
|
|
790
|
+
setIsStreaming(hasLiveConversationStream(streamContext.conversationId));
|
|
791
|
+
}
|
|
378
792
|
}
|
|
379
793
|
return;
|
|
380
794
|
}
|
|
@@ -386,6 +800,9 @@ function useChat(options) {
|
|
|
386
800
|
switch (event.type) {
|
|
387
801
|
case "start": {
|
|
388
802
|
currentRunIdRef.current = event.run_id;
|
|
803
|
+
if (event.conversationId) {
|
|
804
|
+
bindStreamToConversation(streamContext, event.conversationId);
|
|
805
|
+
}
|
|
389
806
|
if (pendingCancelRef.current) {
|
|
390
807
|
const pendingCancel = pendingCancelFnRef.current || cancelRef.current;
|
|
391
808
|
if (pendingCancel) {
|
|
@@ -657,24 +1074,35 @@ function useChat(options) {
|
|
|
657
1074
|
case "tool_result": {
|
|
658
1075
|
flushTypewriter();
|
|
659
1076
|
const toolId = event.tool_call_id;
|
|
660
|
-
const
|
|
1077
|
+
const toolStatus = getToolResultStatus(event);
|
|
1078
|
+
const resultContent = formatToolResultContent(event.result, event.error);
|
|
661
1079
|
const blockIdx = currentBlocksRef.current.findIndex((b) => b.type === "tool_call" && b.toolCallId === toolId);
|
|
662
1080
|
const toolName = blockIdx !== -1 ? currentBlocksRef.current[blockIdx].toolName : void 0;
|
|
663
|
-
if (blockIdx !== -1) currentBlocksRef.current[blockIdx].toolStatus =
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1081
|
+
if (blockIdx !== -1) currentBlocksRef.current[blockIdx].toolStatus = toolStatus;
|
|
1082
|
+
if (resultContent) {
|
|
1083
|
+
currentBlocksRef.current.push({
|
|
1084
|
+
type: "tool_result",
|
|
1085
|
+
content: resultContent,
|
|
1086
|
+
timestamp: Date.now(),
|
|
1087
|
+
toolName,
|
|
1088
|
+
toolCallId: toolId,
|
|
1089
|
+
isIntermediate: true
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
672
1092
|
setMessages((prev) => {
|
|
673
1093
|
const idx = prev.findIndex((m) => m.id === assistantMessageId);
|
|
674
1094
|
if (idx === -1) return prev;
|
|
675
1095
|
const msg = prev[idx];
|
|
676
1096
|
const updatedCalls = (msg.toolCalls || []).map(
|
|
677
|
-
(t) => t.id === toolId ? {
|
|
1097
|
+
(t) => t.id === toolId ? {
|
|
1098
|
+
...t,
|
|
1099
|
+
status: toolStatus,
|
|
1100
|
+
result: event.result,
|
|
1101
|
+
error: toolStatus === "error" ? typeof event.error === "string" && event.error.trim().length > 0 ? event.error : resultContent : void 0,
|
|
1102
|
+
actionRequired: event.action_required,
|
|
1103
|
+
settingsTab: event.settings_tab,
|
|
1104
|
+
settingsSubTab: event.settings_sub_tab
|
|
1105
|
+
} : t
|
|
678
1106
|
);
|
|
679
1107
|
const hasRunning = updatedCalls.some((tc) => tc.status === "running");
|
|
680
1108
|
const updated = [...prev];
|
|
@@ -724,7 +1152,7 @@ function useChat(options) {
|
|
|
724
1152
|
});
|
|
725
1153
|
resetStreamBuffers();
|
|
726
1154
|
if (event.conversationId) {
|
|
727
|
-
streamContext
|
|
1155
|
+
bindStreamToConversation(streamContext, event.conversationId);
|
|
728
1156
|
streamContext.didReportConversationChange = true;
|
|
729
1157
|
setConversationId(event.conversationId);
|
|
730
1158
|
onConversationChange?.(event.conversationId, event.title, {
|
|
@@ -734,13 +1162,13 @@ function useChat(options) {
|
|
|
734
1162
|
});
|
|
735
1163
|
}
|
|
736
1164
|
if (event.title) setConversationTitle(event.title);
|
|
737
|
-
setIsStreaming(false);
|
|
738
1165
|
setIsLoading(false);
|
|
739
1166
|
currentRunIdRef.current = null;
|
|
740
1167
|
pendingInterruptStreamIdRef.current = null;
|
|
741
1168
|
suppressStreamRef.current = false;
|
|
742
1169
|
clearPendingCancel();
|
|
743
|
-
|
|
1170
|
+
finalizeStreamContext(streamContext);
|
|
1171
|
+
setIsStreaming(hasLiveConversationStream(conversationIdRef.current));
|
|
744
1172
|
break;
|
|
745
1173
|
}
|
|
746
1174
|
case "cancelled": {
|
|
@@ -771,14 +1199,14 @@ function useChat(options) {
|
|
|
771
1199
|
return updated;
|
|
772
1200
|
});
|
|
773
1201
|
resetStreamBuffers();
|
|
774
|
-
setIsStreaming(false);
|
|
775
1202
|
setIsLoading(false);
|
|
776
1203
|
setPendingInterrupt(null);
|
|
777
1204
|
currentRunIdRef.current = null;
|
|
778
1205
|
pendingInterruptStreamIdRef.current = null;
|
|
779
1206
|
suppressStreamRef.current = false;
|
|
780
1207
|
clearPendingCancel();
|
|
781
|
-
|
|
1208
|
+
finalizeStreamContext(streamContext);
|
|
1209
|
+
setIsStreaming(hasLiveConversationStream(conversationIdRef.current));
|
|
782
1210
|
break;
|
|
783
1211
|
}
|
|
784
1212
|
case "error": {
|
|
@@ -801,13 +1229,13 @@ function useChat(options) {
|
|
|
801
1229
|
resetStreamBuffers();
|
|
802
1230
|
setError(event.error);
|
|
803
1231
|
onError?.(new Error(event.error));
|
|
804
|
-
setIsStreaming(false);
|
|
805
1232
|
setIsLoading(false);
|
|
806
1233
|
currentRunIdRef.current = null;
|
|
807
1234
|
pendingInterruptStreamIdRef.current = null;
|
|
808
1235
|
suppressStreamRef.current = false;
|
|
809
1236
|
clearPendingCancel();
|
|
810
|
-
|
|
1237
|
+
finalizeStreamContext(streamContext);
|
|
1238
|
+
setIsStreaming(hasLiveConversationStream(conversationIdRef.current));
|
|
811
1239
|
break;
|
|
812
1240
|
}
|
|
813
1241
|
case "interrupt": {
|
|
@@ -825,7 +1253,20 @@ function useChat(options) {
|
|
|
825
1253
|
break;
|
|
826
1254
|
}
|
|
827
1255
|
}
|
|
828
|
-
}, [
|
|
1256
|
+
}, [
|
|
1257
|
+
bindStreamToConversation,
|
|
1258
|
+
clearPendingCancel,
|
|
1259
|
+
finalizeStreamContext,
|
|
1260
|
+
flushTypewriter,
|
|
1261
|
+
hasLiveConversationStream,
|
|
1262
|
+
onConversationChange,
|
|
1263
|
+
onError,
|
|
1264
|
+
refreshConversationIfVisibleWithRetry,
|
|
1265
|
+
resetStreamBuffers,
|
|
1266
|
+
setDetachedConversationSnapshot,
|
|
1267
|
+
syncDetachedSnapshotToVisibleConversation,
|
|
1268
|
+
touchStreamContext
|
|
1269
|
+
]);
|
|
829
1270
|
const trySendMessage = (0, import_react.useCallback)(async (content, sendOptions) => {
|
|
830
1271
|
const trimmedContent = content.trim();
|
|
831
1272
|
const hasAttachedResources = (sendOptions?.attachedResources?.length ?? 0) > 0;
|
|
@@ -858,15 +1299,27 @@ function useChat(options) {
|
|
|
858
1299
|
blocks: [],
|
|
859
1300
|
isComplete: false
|
|
860
1301
|
};
|
|
1302
|
+
const shouldRenderUserMessage = trimmedContent.length > 0 || hasAttachedResources;
|
|
1303
|
+
const initialSnapshot = cloneMessages(normalizeConversationMessages(
|
|
1304
|
+
shouldRenderUserMessage ? [...messagesRef.current, userMessage, assistantMessage] : [...messagesRef.current, assistantMessage]
|
|
1305
|
+
));
|
|
861
1306
|
const streamContext = {
|
|
862
1307
|
token: crypto.randomUUID(),
|
|
863
1308
|
assistantMessageId: assistantMessage.id,
|
|
864
1309
|
conversationId: conversationIdRef.current,
|
|
865
1310
|
detached: false,
|
|
1311
|
+
terminal: false,
|
|
1312
|
+
suppressed: false,
|
|
1313
|
+
updatedAt: Date.now(),
|
|
1314
|
+
pendingCancel: false,
|
|
1315
|
+
initialSnapshot,
|
|
866
1316
|
didReportConversationChange: false
|
|
867
1317
|
};
|
|
1318
|
+
streamContextsRef.current.set(streamContext.token, streamContext);
|
|
1319
|
+
if (streamContext.conversationId) {
|
|
1320
|
+
bindStreamToConversation(streamContext, streamContext.conversationId);
|
|
1321
|
+
}
|
|
868
1322
|
activeStreamContextRef.current = streamContext;
|
|
869
|
-
const shouldRenderUserMessage = trimmedContent.length > 0 || hasAttachedResources;
|
|
870
1323
|
setMessages(
|
|
871
1324
|
(prev) => shouldRenderUserMessage ? [...prev, userMessage, assistantMessage] : [...prev, assistantMessage]
|
|
872
1325
|
);
|
|
@@ -895,6 +1348,11 @@ function useChat(options) {
|
|
|
895
1348
|
sessionResources: sessionResourceIds.length ? sessionResourceIds : void 0
|
|
896
1349
|
}
|
|
897
1350
|
);
|
|
1351
|
+
streamContext.cancel = cancel;
|
|
1352
|
+
if (streamContext.pendingCancel) {
|
|
1353
|
+
streamContext.pendingCancel = false;
|
|
1354
|
+
cancel();
|
|
1355
|
+
}
|
|
898
1356
|
cancelRef.current = cancel;
|
|
899
1357
|
if (pendingCancelRef.current) {
|
|
900
1358
|
pendingCancelFnRef.current = cancel;
|
|
@@ -923,56 +1381,132 @@ function useChat(options) {
|
|
|
923
1381
|
pendingInterruptStreamIdRef.current = null;
|
|
924
1382
|
setIsLoading(false);
|
|
925
1383
|
setIsStreaming(false);
|
|
926
|
-
|
|
927
|
-
activeStreamContextRef.current = null;
|
|
928
|
-
}
|
|
1384
|
+
finalizeStreamContext(streamContext);
|
|
929
1385
|
return false;
|
|
930
1386
|
}
|
|
931
|
-
}, [
|
|
1387
|
+
}, [
|
|
1388
|
+
adapter,
|
|
1389
|
+
bindStreamToConversation,
|
|
1390
|
+
clearPendingCancel,
|
|
1391
|
+
finalizeStreamContext,
|
|
1392
|
+
handleStreamEvent,
|
|
1393
|
+
onError,
|
|
1394
|
+
resetStreamBuffers,
|
|
1395
|
+
sessionResources
|
|
1396
|
+
]);
|
|
932
1397
|
const sendMessage = (0, import_react.useCallback)(async (content, sendOptions) => {
|
|
933
1398
|
await trySendMessage(content, sendOptions);
|
|
934
1399
|
}, [trySendMessage]);
|
|
1400
|
+
const getVisibleStreamContext = (0, import_react.useCallback)(() => {
|
|
1401
|
+
const activeContext = activeStreamContextRef.current;
|
|
1402
|
+
if (activeContext && !activeContext.detached && !activeContext.terminal) {
|
|
1403
|
+
return activeContext;
|
|
1404
|
+
}
|
|
1405
|
+
return getLatestConversationStreamContext(conversationIdRef.current);
|
|
1406
|
+
}, [getLatestConversationStreamContext]);
|
|
935
1407
|
const stopStreaming = (0, import_react.useCallback)(() => {
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
clearPendingCancel();
|
|
942
|
-
cancelRef.current = null;
|
|
1408
|
+
const targetContext = getVisibleStreamContext();
|
|
1409
|
+
if (!targetContext) {
|
|
1410
|
+
setIsStreaming(false);
|
|
1411
|
+
setIsLoading(false);
|
|
1412
|
+
return;
|
|
943
1413
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1414
|
+
targetContext.suppressed = true;
|
|
1415
|
+
touchStreamContext(targetContext);
|
|
1416
|
+
const isActiveVisibleStream = activeStreamContextRef.current?.token === targetContext.token && !targetContext.detached;
|
|
1417
|
+
if (isActiveVisibleStream) {
|
|
1418
|
+
pendingCancelRef.current = true;
|
|
1419
|
+
pendingCancelFnRef.current = cancelRef.current;
|
|
1420
|
+
suppressStreamRef.current = true;
|
|
1421
|
+
if (cancelRef.current) {
|
|
1422
|
+
cancelRef.current();
|
|
1423
|
+
clearPendingCancel();
|
|
1424
|
+
cancelRef.current = null;
|
|
1425
|
+
}
|
|
1426
|
+
flushTypewriter();
|
|
1427
|
+
setMessages((prev) => {
|
|
1428
|
+
const last = [...prev].reverse().find((m) => m.role === "assistant");
|
|
1429
|
+
if (!last?.isStreaming) return prev;
|
|
1430
|
+
return prev.map((m) => {
|
|
1431
|
+
if (m.id !== last.id) return m;
|
|
1432
|
+
return {
|
|
1433
|
+
...m,
|
|
1434
|
+
content: fullContentRef.current || m.content,
|
|
1435
|
+
thinking: m.thinking?.trimEnd(),
|
|
1436
|
+
currentThinking: void 0,
|
|
1437
|
+
isStreaming: false,
|
|
1438
|
+
isThinkingStreaming: false,
|
|
1439
|
+
isThinkingPaused: false,
|
|
1440
|
+
isToolCallsStreaming: false,
|
|
1441
|
+
isComplete: true,
|
|
1442
|
+
toolCalls: m.toolCalls?.map(
|
|
1443
|
+
(tc) => tc.status === "running" ? { ...tc, status: "cancelled" } : tc
|
|
1444
|
+
),
|
|
1445
|
+
blocks: [...currentBlocksRef.current]
|
|
1446
|
+
};
|
|
1447
|
+
});
|
|
1448
|
+
});
|
|
1449
|
+
resetStreamBuffers();
|
|
1450
|
+
currentRunIdRef.current = null;
|
|
1451
|
+
pendingInterruptStreamIdRef.current = null;
|
|
1452
|
+
activeStreamContextRef.current = null;
|
|
1453
|
+
setIsStreaming(false);
|
|
1454
|
+
setIsLoading(false);
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
suppressStreamRef.current = false;
|
|
1458
|
+
const cancelStream = targetContext.cancel;
|
|
1459
|
+
if (cancelStream) {
|
|
1460
|
+
cancelStream();
|
|
1461
|
+
} else {
|
|
1462
|
+
targetContext.pendingCancel = true;
|
|
1463
|
+
}
|
|
1464
|
+
const detachedSnapshot = targetContext.detachedSnapshot;
|
|
1465
|
+
if (detachedSnapshot?.length) {
|
|
1466
|
+
const assistantIndex = detachedSnapshot.findIndex((message) => message.id === targetContext.assistantMessageId);
|
|
1467
|
+
if (assistantIndex !== -1) {
|
|
1468
|
+
const assistant = detachedSnapshot[assistantIndex];
|
|
1469
|
+
detachedSnapshot[assistantIndex] = {
|
|
1470
|
+
...assistant,
|
|
1471
|
+
thinking: assistant.thinking?.trimEnd(),
|
|
954
1472
|
currentThinking: void 0,
|
|
955
1473
|
isStreaming: false,
|
|
1474
|
+
isComplete: true,
|
|
956
1475
|
isThinkingStreaming: false,
|
|
957
1476
|
isThinkingPaused: false,
|
|
958
1477
|
isToolCallsStreaming: false,
|
|
959
|
-
|
|
960
|
-
toolCalls: m.toolCalls?.map(
|
|
961
|
-
(tc) => tc.status === "running" ? { ...tc, status: "cancelled" } : tc
|
|
962
|
-
),
|
|
963
|
-
blocks: [...currentBlocksRef.current]
|
|
1478
|
+
toolCalls: assistant.toolCalls?.map((tc) => tc.status === "running" ? { ...tc, status: "cancelled" } : tc)
|
|
964
1479
|
};
|
|
965
|
-
}
|
|
1480
|
+
}
|
|
1481
|
+
setDetachedConversationSnapshot(targetContext.conversationId, detachedSnapshot);
|
|
1482
|
+
if (targetContext.conversationId && targetContext.conversationId === conversationIdRef.current) {
|
|
1483
|
+
setMessages(cloneMessages(detachedSnapshot));
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
setIsStreaming(hasLiveConversationStream(conversationIdRef.current));
|
|
1487
|
+
setIsLoading(false);
|
|
1488
|
+
}, [
|
|
1489
|
+
clearPendingCancel,
|
|
1490
|
+
flushTypewriter,
|
|
1491
|
+
getVisibleStreamContext,
|
|
1492
|
+
hasLiveConversationStream,
|
|
1493
|
+
resetStreamBuffers,
|
|
1494
|
+
setDetachedConversationSnapshot,
|
|
1495
|
+
touchStreamContext
|
|
1496
|
+
]);
|
|
1497
|
+
const cancelAllStreamContexts = (0, import_react.useCallback)(() => {
|
|
1498
|
+
streamContextsRef.current.forEach((context) => {
|
|
1499
|
+
context.suppressed = true;
|
|
1500
|
+
context.terminal = true;
|
|
1501
|
+
context.cancel?.();
|
|
966
1502
|
});
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
pendingInterruptStreamIdRef.current = null;
|
|
1503
|
+
streamContextsRef.current.clear();
|
|
1504
|
+
conversationStreamTokensRef.current.clear();
|
|
970
1505
|
activeStreamContextRef.current = null;
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}, [clearPendingCancel, flushTypewriter, resetStreamBuffers]);
|
|
1506
|
+
cancelRef.current = null;
|
|
1507
|
+
}, []);
|
|
974
1508
|
const clearMessages = (0, import_react.useCallback)(() => {
|
|
975
|
-
|
|
1509
|
+
cancelAllStreamContexts();
|
|
976
1510
|
if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
|
|
977
1511
|
setMessages([]);
|
|
978
1512
|
setError(null);
|
|
@@ -981,18 +1515,17 @@ function useChat(options) {
|
|
|
981
1515
|
resetStreamBuffers();
|
|
982
1516
|
currentRunIdRef.current = null;
|
|
983
1517
|
pendingInterruptStreamIdRef.current = null;
|
|
984
|
-
activeStreamContextRef.current = null;
|
|
985
1518
|
clearPendingCancel();
|
|
986
1519
|
suppressStreamRef.current = false;
|
|
987
1520
|
detachedConversationSnapshotsRef.current.clear();
|
|
988
|
-
}, [clearPendingCancel, resetStreamBuffers]);
|
|
1521
|
+
}, [cancelAllStreamContexts, clearPendingCancel, resetStreamBuffers]);
|
|
989
1522
|
const loadConversation = (0, import_react.useCallback)(async (id, optionsArg) => {
|
|
990
1523
|
const cancelActiveStream = optionsArg?.cancelActiveStream ?? true;
|
|
991
|
-
const
|
|
992
|
-
if (
|
|
1524
|
+
const visibleStream = getVisibleStreamContext();
|
|
1525
|
+
if (visibleStream) {
|
|
993
1526
|
if (cancelActiveStream) {
|
|
994
1527
|
stopStreaming();
|
|
995
|
-
} else {
|
|
1528
|
+
} else if (activeStreamContextRef.current && !activeStreamContextRef.current.detached) {
|
|
996
1529
|
detachActiveStream(optionsArg?.detachContext);
|
|
997
1530
|
}
|
|
998
1531
|
}
|
|
@@ -1011,6 +1544,9 @@ function useChat(options) {
|
|
|
1011
1544
|
refreshConversationIfVisibleWithRetry(id, [250, 800, 1800]);
|
|
1012
1545
|
}
|
|
1013
1546
|
setMessages(nextMessages);
|
|
1547
|
+
setIsStreaming(
|
|
1548
|
+
hasLiveConversationStream(detail.id) || nextMessages.some((message) => !!message.isStreaming)
|
|
1549
|
+
);
|
|
1014
1550
|
setConversationId(detail.id);
|
|
1015
1551
|
setConversationTitle(detail.title);
|
|
1016
1552
|
currentAgentIdRef.current = detail.agentId ?? currentAgentIdRef.current;
|
|
@@ -1021,18 +1557,24 @@ function useChat(options) {
|
|
|
1021
1557
|
} finally {
|
|
1022
1558
|
if (isMountedRef.current) setIsLoading(false);
|
|
1023
1559
|
}
|
|
1024
|
-
}, [
|
|
1560
|
+
}, [
|
|
1561
|
+
adapter,
|
|
1562
|
+
detachActiveStream,
|
|
1563
|
+
getVisibleStreamContext,
|
|
1564
|
+
hasLiveConversationStream,
|
|
1565
|
+
onError,
|
|
1566
|
+
refreshConversationIfVisibleWithRetry,
|
|
1567
|
+
stopStreaming
|
|
1568
|
+
]);
|
|
1025
1569
|
const newConversation = (0, import_react.useCallback)((optionsArg) => {
|
|
1026
1570
|
const cancelActiveStream = optionsArg?.cancelActiveStream ?? true;
|
|
1027
|
-
const
|
|
1028
|
-
if (
|
|
1571
|
+
const visibleStream = getVisibleStreamContext();
|
|
1572
|
+
if (visibleStream) {
|
|
1029
1573
|
if (cancelActiveStream) {
|
|
1030
1574
|
stopStreaming();
|
|
1031
|
-
} else {
|
|
1575
|
+
} else if (activeStreamContextRef.current && !activeStreamContextRef.current.detached) {
|
|
1032
1576
|
detachActiveStream(optionsArg?.detachContext);
|
|
1033
1577
|
}
|
|
1034
|
-
} else if (cancelActiveStream) {
|
|
1035
|
-
cancelRef.current?.();
|
|
1036
1578
|
}
|
|
1037
1579
|
if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
|
|
1038
1580
|
setMessages([]);
|
|
@@ -1047,7 +1589,7 @@ function useChat(options) {
|
|
|
1047
1589
|
pendingInterruptStreamIdRef.current = null;
|
|
1048
1590
|
activeStreamContextRef.current = null;
|
|
1049
1591
|
clearPendingCancel();
|
|
1050
|
-
}, [clearPendingCancel, detachActiveStream, resetStreamBuffers, stopStreaming]);
|
|
1592
|
+
}, [clearPendingCancel, detachActiveStream, getVisibleStreamContext, resetStreamBuffers, stopStreaming]);
|
|
1051
1593
|
const sendHitlResponse = (0, import_react.useCallback)((response) => {
|
|
1052
1594
|
const runId = currentRunIdRef.current ?? void 0;
|
|
1053
1595
|
const streamId = pendingInterruptStreamIdRef.current ?? void 0;
|
|
@@ -5946,12 +6488,25 @@ function parseToolCalls(toolCalls) {
|
|
|
5946
6488
|
const rawArgs = fn?.arguments ?? tc.args ?? tc.arguments;
|
|
5947
6489
|
const args = safeParseArgs(rawArgs);
|
|
5948
6490
|
const id = tc.id || tc.tool_call_id || tc.call_id;
|
|
6491
|
+
const actionRequired = tc.action_required || tc.actionRequired;
|
|
6492
|
+
const settingsTab = tc.settings_tab || tc.settingsTab;
|
|
6493
|
+
const settingsSubTab = tc.settings_sub_tab || tc.settingsSubTab;
|
|
5949
6494
|
return {
|
|
5950
6495
|
id,
|
|
5951
6496
|
name,
|
|
5952
6497
|
args,
|
|
5953
|
-
|
|
5954
|
-
|
|
6498
|
+
result: tc.result,
|
|
6499
|
+
error: typeof tc.error === "string" ? tc.error : void 0,
|
|
6500
|
+
actionRequired,
|
|
6501
|
+
settingsTab,
|
|
6502
|
+
settingsSubTab,
|
|
6503
|
+
status: deriveToolExecutionStatus({
|
|
6504
|
+
status: tc.status,
|
|
6505
|
+
result: tc.result,
|
|
6506
|
+
actionRequired,
|
|
6507
|
+
error: tc.error,
|
|
6508
|
+
fallback: "completed"
|
|
6509
|
+
})
|
|
5955
6510
|
};
|
|
5956
6511
|
}).filter((tc) => tc.name || tc.id);
|
|
5957
6512
|
}
|
|
@@ -6042,7 +6597,10 @@ function mergeConsecutiveAssistantMessages(rawMessages) {
|
|
|
6042
6597
|
id: (toolMessage.tool_call_id || void 0) ?? `tool-${idx}`,
|
|
6043
6598
|
name: "",
|
|
6044
6599
|
args: void 0,
|
|
6045
|
-
status:
|
|
6600
|
+
status: deriveToolExecutionStatus({
|
|
6601
|
+
result: toolMessage.content,
|
|
6602
|
+
fallback: "completed"
|
|
6603
|
+
}),
|
|
6046
6604
|
result: toolMessage.content
|
|
6047
6605
|
})) : void 0;
|
|
6048
6606
|
const effectiveToolCalls = msgToolCalls && msgToolCalls.length > 0 ? msgToolCalls : fallbackCalls;
|
|
@@ -6075,6 +6633,16 @@ function mergeConsecutiveAssistantMessages(rawMessages) {
|
|
|
6075
6633
|
const toolResultBlocks = [];
|
|
6076
6634
|
for (const tc of effectiveToolCalls) {
|
|
6077
6635
|
const toolIndex = effectiveToolCalls.indexOf(tc);
|
|
6636
|
+
const toolResultById = tc.id ? toolMessages.find((tm) => tm.tool_call_id === tc.id) : void 0;
|
|
6637
|
+
const toolResultByIndex = toolMessages[toolIndex];
|
|
6638
|
+
const toolResult = toolResultById || toolResultByIndex;
|
|
6639
|
+
const toolStatus = deriveToolExecutionStatus({
|
|
6640
|
+
status: tc.status,
|
|
6641
|
+
result: tc.result ?? toolResult?.content,
|
|
6642
|
+
actionRequired: tc.actionRequired,
|
|
6643
|
+
error: tc.error,
|
|
6644
|
+
fallback: "completed"
|
|
6645
|
+
});
|
|
6078
6646
|
blocks.push({
|
|
6079
6647
|
type: "tool_call",
|
|
6080
6648
|
content: "",
|
|
@@ -6082,12 +6650,9 @@ function mergeConsecutiveAssistantMessages(rawMessages) {
|
|
|
6082
6650
|
toolName: tc.name,
|
|
6083
6651
|
toolArgs: tc.args,
|
|
6084
6652
|
toolCallId: tc.id,
|
|
6085
|
-
toolStatus
|
|
6653
|
+
toolStatus,
|
|
6086
6654
|
isIntermediate: true
|
|
6087
6655
|
});
|
|
6088
|
-
const toolResultById = tc.id ? toolMessages.find((tm) => tm.tool_call_id === tc.id) : void 0;
|
|
6089
|
-
const toolResultByIndex = toolMessages[toolIndex];
|
|
6090
|
-
const toolResult = toolResultById || toolResultByIndex;
|
|
6091
6656
|
const resultContent = tc.result ?? toolResult?.content;
|
|
6092
6657
|
const toolResultId = toolResult?.tool_call_id || void 0;
|
|
6093
6658
|
if (resultContent) {
|
|
@@ -6130,6 +6695,13 @@ function mergeConsecutiveAssistantMessages(rawMessages) {
|
|
|
6130
6695
|
);
|
|
6131
6696
|
if (matchingToolCall) {
|
|
6132
6697
|
matchingToolCall.result = toolMsg.content;
|
|
6698
|
+
matchingToolCall.status = deriveToolExecutionStatus({
|
|
6699
|
+
status: matchingToolCall.status,
|
|
6700
|
+
result: toolMsg.content,
|
|
6701
|
+
actionRequired: matchingToolCall.actionRequired,
|
|
6702
|
+
error: matchingToolCall.error,
|
|
6703
|
+
fallback: "completed"
|
|
6704
|
+
});
|
|
6133
6705
|
}
|
|
6134
6706
|
}
|
|
6135
6707
|
const thinkingParts = consecutiveAssistantMsgs.map((m) => m.thinking).filter((t) => t && t.trim());
|
|
@@ -6728,9 +7300,20 @@ function processStreamEvents(stream, onEvent, sdk, setCurrentRunId, streamId) {
|
|
|
6728
7300
|
args: event.args || {}
|
|
6729
7301
|
});
|
|
6730
7302
|
break;
|
|
6731
|
-
case "tool_result":
|
|
6732
|
-
|
|
7303
|
+
case "tool_result": {
|
|
7304
|
+
const toolResultEvent = event;
|
|
7305
|
+
forward({
|
|
7306
|
+
type: "tool_result",
|
|
7307
|
+
tool_call_id: toolResultEvent.tool_call_id || "",
|
|
7308
|
+
result: toolResultEvent.result,
|
|
7309
|
+
error: toolResultEvent.error,
|
|
7310
|
+
status: toolResultEvent.status,
|
|
7311
|
+
action_required: toolResultEvent.action_required,
|
|
7312
|
+
settings_tab: toolResultEvent.settings_tab,
|
|
7313
|
+
settings_sub_tab: toolResultEvent.settings_sub_tab
|
|
7314
|
+
});
|
|
6733
7315
|
break;
|
|
7316
|
+
}
|
|
6734
7317
|
case "done":
|
|
6735
7318
|
forward({ type: "done", conversationId: event.conversationId || "", title: event.title });
|
|
6736
7319
|
currentRunId = null;
|
|
@@ -7480,8 +8063,8 @@ function ToolCallItem({ toolCall, toolResult }) {
|
|
|
7480
8063
|
style: { color: "var(--chat-text)" },
|
|
7481
8064
|
children: [
|
|
7482
8065
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "font-mono", children: displayName }),
|
|
7483
|
-
|
|
7484
|
-
|
|
8066
|
+
toolCall.toolStatus === "completed" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-green-600 dark:text-green-400", children: TimelineIcons.check }),
|
|
8067
|
+
toolCall.toolStatus === "error" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-red-500", children: TimelineIcons.error }),
|
|
7485
8068
|
hasArgs && !expanded && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "font-mono", style: { color: "var(--chat-text)", opacity: 0.6, fontSize: "0.75rem" }, children: [
|
|
7486
8069
|
"(",
|
|
7487
8070
|
Object.values(toolCall.toolArgs).slice(0, 1).map((v) => typeof v === "string" ? v.length > 18 ? v.slice(0, 18) + "\u2026" : v : "\u2026"),
|
|
@@ -7518,7 +8101,8 @@ function StreamingToolCallItem({
|
|
|
7518
8101
|
style: { color: "var(--chat-text)" },
|
|
7519
8102
|
children: [
|
|
7520
8103
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "font-mono", children: displayName }),
|
|
7521
|
-
|
|
8104
|
+
toolCall.toolStatus === "completed" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-green-600 dark:text-green-400", children: TimelineIcons.check }),
|
|
8105
|
+
toolCall.toolStatus === "error" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-red-500", children: TimelineIcons.error }),
|
|
7522
8106
|
isActive && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "ml-0.5 text-amber-500 animate-pulse", children: "\u25C6" }),
|
|
7523
8107
|
hasArgs && !expanded && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "font-mono", style: { color: "var(--chat-text)", opacity: 0.6, fontSize: "0.75rem" }, children: [
|
|
7524
8108
|
"(",
|
|
@@ -7939,7 +8523,13 @@ function buildFallbackBlocksFromToolCalls(toolCalls) {
|
|
|
7939
8523
|
const blocks = [];
|
|
7940
8524
|
let timestamp = Date.now();
|
|
7941
8525
|
for (const toolCall of toolCalls) {
|
|
7942
|
-
const toolStatus =
|
|
8526
|
+
const toolStatus = deriveToolExecutionStatus({
|
|
8527
|
+
status: toolCall.status,
|
|
8528
|
+
result: toolCall.result,
|
|
8529
|
+
actionRequired: toolCall.actionRequired,
|
|
8530
|
+
error: toolCall.error,
|
|
8531
|
+
fallback: "running"
|
|
8532
|
+
});
|
|
7943
8533
|
blocks.push({
|
|
7944
8534
|
type: "tool_call",
|
|
7945
8535
|
content: "",
|