', '')}
- components={linkComponents}
- />
+
{!isToolCalls && (
diff --git a/web-app/src/hooks/useChat.ts b/web-app/src/hooks/useChat.ts
index 60541a1a1..80d1a5914 100644
--- a/web-app/src/hooks/useChat.ts
+++ b/web-app/src/hooks/useChat.ts
@@ -41,6 +41,12 @@ import { TEMPORARY_CHAT_QUERY_ID, TEMPORARY_CHAT_ID } from '@/constants/chat'
import { toast } from 'sonner'
import { Attachment } from '@/types/attachment'
+type StreamEvent = {
+ timestamp: number
+ type: 'reasoning_chunk' | 'tool_call' | 'tool_output'
+ data: any
+}
+
export const useChat = () => {
const [
updateTokenSpeed,
@@ -279,6 +285,8 @@ export const useChat = () => {
const selectedProvider = useModelProvider.getState().selectedProvider
let activeProvider = getProviderByName(selectedProvider)
+ const streamEvents: StreamEvent[] = []
+
resetTokenSpeed()
if (!activeThread || !activeProvider) return
@@ -555,6 +563,7 @@ export const useChat = () => {
...e,
state: 'pending',
})),
+ streamEvents: streamEvents,
}
)
updateStreamingContent(currentContent)
@@ -591,6 +600,7 @@ export const useChat = () => {
...e,
state: 'pending',
})),
+ streamEvents: streamEvents,
}
)
updateStreamingContent(currentContent)
@@ -636,16 +646,37 @@ export const useChat = () => {
if ('usage' in part && part.usage) {
tokenUsage = part.usage
}
+ const deltaToolCalls = part.choices[0]?.delta?.tool_calls
+ if (deltaToolCalls) {
+ const index = deltaToolCalls[0]?.index
+ // Check if this chunk starts a brand new tool call
+ const isNewToolCallStart =
+ index !== undefined && toolCalls[index] === undefined
- if (part.choices[0]?.delta?.tool_calls) {
extractToolCall(part, currentCall, toolCalls)
- // Schedule a flush to reflect tool update
- scheduleFlush()
+
+ if (isNewToolCallStart) {
+ // Track tool call event only when it begins
+ // toolCalls[index] is the newly created object due to extractToolCall
+ streamEvents.push({
+ timestamp: Date.now(),
+ type: 'tool_call',
+ data: { toolCall: toolCalls[index] },
+ })
+ // Schedule a flush to reflect tool update
+ scheduleFlush()
+ }
}
const deltaReasoning =
reasoningProcessor.processReasoningChunk(part)
if (deltaReasoning) {
accumulatedText += deltaReasoning
+ // Track reasoning event
+ streamEvents.push({
+ timestamp: Date.now(),
+ type: 'reasoning_chunk',
+ data: { content: deltaReasoning },
+ })
pendingDeltaCount += 1
// Schedule flush for reasoning updates
scheduleFlush()
@@ -728,6 +759,7 @@ export const useChat = () => {
const messageMetadata: Record = {
tokenSpeed: useAppState.getState().tokenSpeed,
assistant: currentAssistant,
+ streamEvents, // Add chronological events
}
if (accumulatedText.includes('') || toolCalls.length > 0) {
diff --git a/web-app/src/lib/completion.ts b/web-app/src/lib/completion.ts
index 14f4ff148..3b71333c0 100644
--- a/web-app/src/lib/completion.ts
+++ b/web-app/src/lib/completion.ts
@@ -687,6 +687,16 @@ export const postMessageProcessing = async (
toolCallEntry.response = result
toolCallEntry.state = 'ready'
if (updateStreamingUI) updateStreamingUI({ ...message }) // Show result
+ const streamEvents = (message.metadata?.streamEvents || []) as any[]
+ streamEvents.push({
+ timestamp: Date.now(),
+ type: 'tool_output',
+ data: { result: result },
+ })
+ message.metadata = {
+ ...(message.metadata ?? {}),
+ streamEvents: streamEvents,
+ }
builder.addToolMessage(result as ToolResult, toolCall.id)
// Proactive mode: Capture screenshot/snapshot after browser tool execution
@@ -734,6 +744,7 @@ export const postMessageProcessing = async (
if (followUpCompletion) {
let followUpText = ''
const newToolCalls: ChatCompletionMessageToolCall[] = []
+ const streamEvents = (message.metadata?.streamEvents || []) as any[]
const textContent = message.content.find(
(c) => c.type === ContentType.Text
)
@@ -758,19 +769,56 @@ export const postMessageProcessing = async (
if (textContent?.text) {
if (deltaReasoning) textContent.text.value += deltaReasoning
- if (deltaContent) textContent.text.value += deltaContent
+ if (deltaContent) {
+ textContent.text.value += deltaContent
+ followUpText += deltaContent
+ console.log(`delta content from followup:\n${deltaContent}`)
+ }
}
- if (deltaContent) followUpText += deltaContent
+ if (deltaReasoning) {
+ streamEvents.push({
+ timestamp: Date.now(),
+ type: 'reasoning_chunk',
+ data: { content: deltaReasoning },
+ })
+ }
+ const initialToolCallCount = newToolCalls.length
if (chunk.choices[0]?.delta?.tool_calls) {
extractToolCall(chunk, null, newToolCalls)
+ if (newToolCalls.length > initialToolCallCount) {
+ // The new tool call is the last element added
+ streamEvents.push({
+ timestamp: Date.now(),
+ type: 'tool_call',
+ data: { toolCall: newToolCalls[newToolCalls.length - 1] },
+ })
+ }
+ }
+ // Ensure the metadata is updated before calling updateStreamingUI
+ message.metadata = {
+ ...(message.metadata ?? {}),
+ streamEvents: streamEvents,
}
- if (updateStreamingUI) updateStreamingUI({ ...message })
+ if (updateStreamingUI) {
+ // FIX: Create a new object reference for the content array
+ // This forces the memoized component to detect the change in the mutated text
+ const uiMessage: ThreadMessage = {
+ ...message,
+ content: message.content.map((c) => ({ ...c })), // Shallow copy array and its parts
+ }
+ updateStreamingUI(uiMessage)
+ }
}
- if (textContent?.text) {
- textContent.text.value += reasoningProcessor.finalize()
- if (updateStreamingUI) updateStreamingUI({ ...message })
+ if (textContent?.text && updateStreamingUI) {
+ // FIX: Create a new object reference for the content array
+ // This forces the memoized component to detect the change in the mutated text
+ const uiMessage: ThreadMessage = {
+ ...message,
+ content: message.content.map((c) => ({ ...c })), // Shallow copy array and its parts
+ }
+ updateStreamingUI(uiMessage)
}
}