@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.
@@ -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 detachedSnapshot = cloneMessages(normalizeConversationMessages(messagesRef.current));
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.conversationId = event.conversationId;
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
- if (isTerminalEvent && activeStreamContextRef.current?.token === streamContext.token) {
377
- activeStreamContextRef.current = null;
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 result = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
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 = "completed";
664
- currentBlocksRef.current.push({
665
- type: "tool_result",
666
- content: result,
667
- timestamp: Date.now(),
668
- toolName,
669
- toolCallId: toolId,
670
- isIntermediate: true
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 ? { ...t, status: "completed", result: event.result } : t
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.conversationId = event.conversationId;
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
- activeStreamContextRef.current = null;
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
- activeStreamContextRef.current = null;
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
- activeStreamContextRef.current = null;
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
- }, [clearPendingCancel, flushTypewriter, onConversationChange, onError, refreshConversationIfVisibleWithRetry, resetStreamBuffers, setDetachedConversationSnapshot]);
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
- if (activeStreamContextRef.current?.token === streamContext.token) {
927
- activeStreamContextRef.current = null;
928
- }
1384
+ finalizeStreamContext(streamContext);
929
1385
  return false;
930
1386
  }
931
- }, [adapter, clearPendingCancel, handleStreamEvent, onError, resetStreamBuffers, sessionResources]);
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
- pendingCancelRef.current = true;
937
- pendingCancelFnRef.current = cancelRef.current;
938
- suppressStreamRef.current = true;
939
- if (cancelRef.current) {
940
- cancelRef.current();
941
- clearPendingCancel();
942
- cancelRef.current = null;
1408
+ const targetContext = getVisibleStreamContext();
1409
+ if (!targetContext) {
1410
+ setIsStreaming(false);
1411
+ setIsLoading(false);
1412
+ return;
943
1413
  }
944
- flushTypewriter();
945
- setMessages((prev) => {
946
- const last = [...prev].reverse().find((m) => m.role === "assistant");
947
- if (!last?.isStreaming) return prev;
948
- return prev.map((m) => {
949
- if (m.id !== last.id) return m;
950
- return {
951
- ...m,
952
- content: fullContentRef.current || m.content,
953
- thinking: m.thinking?.trimEnd(),
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
- isComplete: true,
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
- resetStreamBuffers();
968
- currentRunIdRef.current = null;
969
- pendingInterruptStreamIdRef.current = null;
1503
+ streamContextsRef.current.clear();
1504
+ conversationStreamTokensRef.current.clear();
970
1505
  activeStreamContextRef.current = null;
971
- setIsStreaming(false);
972
- setIsLoading(false);
973
- }, [clearPendingCancel, flushTypewriter, resetStreamBuffers]);
1506
+ cancelRef.current = null;
1507
+ }, []);
974
1508
  const clearMessages = (0, import_react.useCallback)(() => {
975
- cancelRef.current?.();
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 activeStream = activeStreamContextRef.current;
992
- if (activeStream && !activeStream.detached) {
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
- }, [adapter, detachActiveStream, onError, refreshConversationIfVisibleWithRetry, stopStreaming]);
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 activeStream = activeStreamContextRef.current;
1028
- if (activeStream && !activeStream.detached) {
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
- status: "completed",
5954
- result: tc.result
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: "completed",
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: "completed",
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
- forward({ type: "tool_result", tool_call_id: event.tool_call_id || "", result: event.result });
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
- toolResult && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-green-600 dark:text-green-400", children: TimelineIcons.check }),
7484
- !toolResult && toolCall.toolStatus === "error" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-red-500", children: TimelineIcons.error }),
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
- toolResult && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-green-600 dark:text-green-400", children: TimelineIcons.check }),
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 = toolCall.status || (toolCall.result !== void 0 ? "completed" : "running");
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: "",