fix(desktop): collapse duplicate thinking blocks in history mapping

Subagent transcripts persist streamed thinking snapshots, and the
history-mapping path pushed every block verbatim, so the subagent run
page rendered long runs of repeated empty "thought" bubbles. Give the
history path the same guards as the streaming path: drop blank blocks,
drop verbatim replays, replace prefix-growth snapshots, and merge
adjacent blocks while keeping deterministic ids for stable React keys.
This commit is contained in:
程序员阿江(Relakkes)
2026-08-04 17:03:51 +08:00
parent 0a9789a73c
commit 30a55a1733
2 changed files with 90 additions and 1 deletions
+63
View File
@@ -484,6 +484,69 @@ describe('chatStore history mapping', () => {
expect(mapped[3]).toMatchObject({ parentToolUseId: 'agent-1' })
})
it('collapses replayed and blank thinking blocks from history mapping', () => {
const messages: MessageEntry[] = [
{
id: 'assistant-snap-1',
type: 'assistant',
timestamp: '2026-04-06T00:00:00.000Z',
content: [{ type: 'thinking', thinking: 'plan the fix' }],
},
// 流式快照整块重发:逐字相同,应丢弃
{
id: 'assistant-snap-2',
type: 'assistant',
timestamp: '2026-04-06T00:00:01.000Z',
content: [{ type: 'thinking', thinking: 'plan the fix' }],
},
// 流式快照前缀增长:应替换为更全的新块而非追加
{
id: 'assistant-snap-3',
type: 'assistant',
timestamp: '2026-04-06T00:00:02.000Z',
content: [{ type: 'thinking', thinking: 'plan the fix carefully' }],
},
// 纯空白块:不应产生空壳气泡
{
id: 'assistant-blank',
type: 'assistant',
timestamp: '2026-04-06T00:00:03.000Z',
content: [{ type: 'thinking', thinking: ' \n ' }],
},
// 相邻但内容无关的思考:合并为一个气泡
{
id: 'assistant-more',
type: 'assistant',
timestamp: '2026-04-06T00:00:04.000Z',
content: [{ type: 'thinking', thinking: 'then run tests' }],
},
// 被工具打断后的新思考:保持独立
{
id: 'assistant-tools',
type: 'assistant',
timestamp: '2026-04-06T00:00:05.000Z',
content: [
{ type: 'tool_use', name: 'Bash', id: 'bash-1', input: { command: 'pwd' } },
],
},
{
id: 'assistant-final',
type: 'assistant',
timestamp: '2026-04-06T00:00:06.000Z',
content: [{ type: 'thinking', thinking: 'tests pass' }],
},
]
const mapped = mapHistoryMessagesToUiMessages(messages)
expect(mapped.map((message) => message.type)).toEqual(['thinking', 'tool_use', 'thinking'])
expect(mapped[0]).toMatchObject({
id: 'assistant-snap-1-block-0',
content: 'plan the fix carefullythen run tests',
})
expect(mapped[2]).toMatchObject({ id: 'assistant-final-block-0', content: 'tests pass' })
})
it('maps AskUserQuestion transcript answers from toolUseResult metadata', () => {
const messages: MessageEntry[] = [
{
+27 -1
View File
@@ -4150,6 +4150,32 @@ function pushAssistantHistoryText(
})
}
function pushAssistantHistoryThinking(
messages: UIMessage[],
id: string,
content: string,
timestamp: number,
): void {
// 与流式路径(case 'thinking')保持同等防护:纯空白块不产生空壳气泡。
if (!content.trim()) return
const last = messages[messages.length - 1]
if (last?.type === 'thinking') {
// 流式落盘的快照会让同一段思考在 jsonl 里以"整块重发"或"前缀增长"的
// 形态重复出现,逐字相同直接丢弃,前缀包含则用更全的新块替换旧块。
// 合并时保留首个块的确定性 id,保证轮询重映射时 React key 稳定。
if (last.content === content) return
if (content.startsWith(last.content)) {
last.content = content
return
}
last.content += content
return
}
messages.push({ id, type: 'thinking', content, timestamp })
}
type HistoryMappingOptions = {
includeTeammateMessages?: boolean
}
@@ -4767,7 +4793,7 @@ export function mapHistoryMessagesToUiMessages(
}
if ((msg.type === 'assistant' || msg.type === 'tool_use') && Array.isArray(msg.content)) {
for (const [blockIndex, block] of (msg.content as AssistantHistoryBlock[]).entries()) {
if (block.type === 'thinking' && block.thinking) uiMessages.push({ id: `${msg.id}-block-${blockIndex}`, type: 'thinking', content: block.thinking, timestamp })
if (block.type === 'thinking' && block.thinking) pushAssistantHistoryThinking(uiMessages, `${msg.id}-block-${blockIndex}`, block.thinking, timestamp)
else if (block.type === 'text' && block.text) {
pushAssistantHistoryText(uiMessages, block.text, timestamp, msg.model, msg.id || undefined)
}