mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
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:
@@ -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[] = [
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user