Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 61 additions & 64 deletions src/renderer/src/stores/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,34 +592,32 @@ export const useChatStore = defineStore('chat', () => {
// 处理普通内容
else if (msg.content) {
const lastContentBlock = curMsg.content[curMsg.content.length - 1]
if (lastContentBlock && lastContentBlock.type === 'content') {
lastContentBlock.content += msg.content
// 打字机音效,与实际数据流同步
playTypewriterSound()
} else {
if (lastContentBlock) {
lastContentBlock.status = 'success'
if (lastContentBlock) {
lastContentBlock.status = 'success'
if (lastContentBlock.type === 'content') {
lastContentBlock.content += msg.content
}
} else {
curMsg.content.push({
type: 'content',
content: msg.content,
status: 'loading',
timestamp: Date.now()
})
Comment on lines 592 to 606

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Append a new content block when previous block is not text

In the streaming handler, when msg.content arrives and the last block exists but is not of type content, the new code marks that block as success and returns without pushing a new text block. Previously the else branch covered both "no block" and "non‑content block" so the plain response text was appended. With the current logic, any stream that first emits a tool/action/reasoning block and then regular content will drop the textual response entirely while still playing the typing sound. Users will observe empty assistant replies whenever content follows a different block type.

Useful? React with 👍 / 👎.

// 如果是新块的第一个字符,也播放声音
playTypewriterSound()
}
Comment on lines +595 to 607
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Content chunks are dropped when the previous block isn’t content.

As soon as a text chunk arrives while the last block is a tool call/action/etc., this branch marks that block success but never appends a new content block, so the incoming text is lost. It also forces running tool-call blocks to success, breaking their lifecycle. Reuse finalizeLastBlock() before creating a fresh content block when the prior block isn’t textual, and only append to the existing block when it’s already content.

-          const lastContentBlock = curMsg.content[curMsg.content.length - 1]
-          if (lastContentBlock) {
-            lastContentBlock.status = 'success'
-            if (lastContentBlock.type === 'content') {
-              lastContentBlock.content += msg.content
-            }
-          } else {
+          const lastContentBlock = curMsg.content[curMsg.content.length - 1]
+          if (lastContentBlock?.type === 'content') {
+            lastContentBlock.content += msg.content
+          } else {
+            finalizeLastBlock()
             curMsg.content.push({
               type: 'content',
               content: msg.content,
               status: 'loading',
               timestamp: Date.now()
             })
           }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (lastContentBlock) {
lastContentBlock.status = 'success'
if (lastContentBlock.type === 'content') {
lastContentBlock.content += msg.content
}
} else {
curMsg.content.push({
type: 'content',
content: msg.content,
status: 'loading',
timestamp: Date.now()
})
// 如果是新块的第一个字符,也播放声音
playTypewriterSound()
}
const lastContentBlock = curMsg.content[curMsg.content.length - 1]
if (lastContentBlock?.type === 'content') {
lastContentBlock.content += msg.content
} else {
finalizeLastBlock()
curMsg.content.push({
type: 'content',
content: msg.content,
status: 'loading',
timestamp: Date.now()
})
}
🤖 Prompt for AI Agents
In src/renderer/src/stores/chat.ts around lines 595 to 607: the current branch
marks the last block `success` and drops incoming text when the previous block
isn’t of type 'content'; instead call finalizeLastBlock() to properly complete
non-content blocks, then create and push a new 'content' block for the incoming
msg.content; only append to lastContentBlock.content when lastContentBlock
exists and lastContentBlock.type === 'content', otherwise create a fresh
{type:'content', content: msg.content, status:'loading', timestamp: Date.now()}
entry on curMsg.content so text is not lost and tool-call blocks keep their
lifecycle.

// 打字机音效,与实际数据流同步
playTypewriterSound()
}

// 处理推理内容
if (msg.reasoning_content) {
const lastReasoningBlock = curMsg.content[curMsg.content.length - 1]
if (lastReasoningBlock && lastReasoningBlock.type === 'reasoning_content') {
lastReasoningBlock.content += msg.reasoning_content
} else {
if (lastReasoningBlock) {
lastReasoningBlock.status = 'success'
if (lastReasoningBlock) {
lastReasoningBlock.status = 'success'
if (lastReasoningBlock.type === 'reasoning_content') {
lastReasoningBlock.content += msg.reasoning_content
}
} else {
curMsg.content.push({
type: 'reasoning_content',
content: msg.reasoning_content,
Comment on lines 612 to 623

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Push reasoning block when switching from other block types

The refactor applies the same pattern to msg.reasoning_content: if any block exists, it is marked success but a new reasoning block is only created when there were no blocks at all. When reasoning content is streamed after another block (e.g., a tool call or prior content), no reasoning block is appended and the explanation text disappears. The previous code handled this case by finalizing the prior block and then pushing a new reasoning block; that behaviour is now lost, so reasoning output never renders when it arrives after a different block type.

Useful? React with 👍 / 👎.

Expand Down Expand Up @@ -727,63 +725,62 @@ export const useChatStore = defineStore('chat', () => {
// 从缓存中获取消息
const cached = getGeneratingMessagesCache().get(msg.eventId)
if (cached) {
if (getActiveThreadId() === getActiveThreadId()) {
try {
const updatedMessage = await threadP.getMessage(msg.eventId)
const enrichedMessage = await enrichMessageWithExtra(updatedMessage)

if (enrichedMessage.is_variant && enrichedMessage.parentId) {
// 处理变体消息的错误状态
const parentMsgIndex = getMessages().findIndex((m) => m.id === enrichedMessage.parentId)
if (parentMsgIndex !== -1) {
const parentMsg = getMessages()[parentMsgIndex] as AssistantMessage
if (!parentMsg.variants) {
parentMsg.variants = []
}
const variantIndex = parentMsg.variants.findIndex((v) => v.id === enrichedMessage.id)
if (variantIndex !== -1) {
parentMsg.variants[variantIndex] = enrichedMessage
} else {
parentMsg.variants.push(enrichedMessage)
}
getMessages()[parentMsgIndex] = { ...parentMsg }
try {
const updatedMessage = await threadP.getMessage(msg.eventId)
const enrichedMessage = await enrichMessageWithExtra(updatedMessage)

if (enrichedMessage.is_variant && enrichedMessage.parentId) {
// 处理变体消息的错误状态
const parentMsgIndex = getMessages().findIndex((m) => m.id === enrichedMessage.parentId)
if (parentMsgIndex !== -1) {
const parentMsg = getMessages()[parentMsgIndex] as AssistantMessage
if (!parentMsg.variants) {
parentMsg.variants = []
}
} else {
// 非变体消息的原有错误处理逻辑
const messageIndex = getMessages().findIndex((m) => m.id === msg.eventId)
if (messageIndex !== -1) {
getMessages()[messageIndex] = enrichedMessage as AssistantMessage | UserMessage
const variantIndex = parentMsg.variants.findIndex((v) => v.id === enrichedMessage.id)
if (variantIndex !== -1) {
parentMsg.variants[variantIndex] = enrichedMessage
} else {
parentMsg.variants.push(enrichedMessage)
}
getMessages()[parentMsgIndex] = { ...parentMsg }
}
const wid = window.api.getWindowId() || 0
// 检查窗口是否聚焦,如果未聚焦则发送错误通知
const isFocused = await windowP.isMainWindowFocused(wid)
if (!isFocused) {
// 获取错误信息
let errorMessage = t('chat.notify.generationError')
if (enrichedMessage && (enrichedMessage as AssistantMessage).content) {
const assistantMsg = enrichedMessage as AssistantMessage
// 查找错误信息块
for (const block of assistantMsg.content) {
if (block.status === 'error' && block.content) {
errorMessage = block.content.substring(0, 20)
if (block.content.length > 20) errorMessage += '...'
break
}
} else {
// 非变体消息的原有错误处理逻辑
const messageIndex = getMessages().findIndex((m) => m.id === msg.eventId)
if (messageIndex !== -1) {
getMessages()[messageIndex] = enrichedMessage as AssistantMessage | UserMessage
}
}
const wid = window.api.getWindowId() || 0
// 检查窗口是否聚焦,如果未聚焦则发送错误通知
const isFocused = await windowP.isMainWindowFocused(wid)
if (!isFocused) {
// 获取错误信息
let errorMessage = t('chat.notify.generationError')
if (enrichedMessage && (enrichedMessage as AssistantMessage).content) {
const assistantMsg = enrichedMessage as AssistantMessage
// 查找错误信息块
for (const block of assistantMsg.content) {
if (block.status === 'error' && block.content) {
errorMessage = block.content.substring(0, 20)
if (block.content.length > 20) errorMessage += '...'
break
}
}

// 发送错误通知
await notificationP.showNotification({
id: `error-${msg.eventId}`,
title: t('chat.notify.generationError'),
body: errorMessage
})
}
} catch (error) {
console.error('Failed to load error message:', error)

// 发送错误通知
await notificationP.showNotification({
id: `error-${msg.eventId}`,
title: t('chat.notify.generationError'),
body: errorMessage
})
}
} catch (error) {
console.error('Failed to load error message:', error)
}

getGeneratingMessagesCache().delete(msg.eventId)
generatingThreadIds.value.delete(cached.threadId)
// 设置会话的workingStatus为error
Expand Down Expand Up @@ -921,7 +918,7 @@ export const useChatStore = defineStore('chat', () => {
const cache = getGeneratingMessagesCache()
const generatingMessage = Array.from(cache.entries()).find(
([, cached]) => cached.threadId === threadId
)
) as string[]
if (generatingMessage) {
const [messageId] = generatingMessage
await threadP.stopMessageGeneration(messageId)
Expand Down