mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-31 16:33:34 +08:00
feat: group consecutive tool calls into collapsible summaries in MessageList
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
458945da52
commit
001994d0bd
@ -4,47 +4,98 @@ import { UserMessage } from './UserMessage'
|
|||||||
import { AssistantMessage } from './AssistantMessage'
|
import { AssistantMessage } from './AssistantMessage'
|
||||||
import { ThinkingBlock } from './ThinkingBlock'
|
import { ThinkingBlock } from './ThinkingBlock'
|
||||||
import { ToolCallBlock } from './ToolCallBlock'
|
import { ToolCallBlock } from './ToolCallBlock'
|
||||||
|
import { ToolCallGroup } from './ToolCallGroup'
|
||||||
import { ToolResultBlock } from './ToolResultBlock'
|
import { ToolResultBlock } from './ToolResultBlock'
|
||||||
import { PermissionDialog } from './PermissionDialog'
|
import { PermissionDialog } from './PermissionDialog'
|
||||||
import { AskUserQuestion } from './AskUserQuestion'
|
import { AskUserQuestion } from './AskUserQuestion'
|
||||||
import { StreamingIndicator } from './StreamingIndicator'
|
import { StreamingIndicator } from './StreamingIndicator'
|
||||||
import type { UIMessage } from '../../types/chat'
|
import type { UIMessage } from '../../types/chat'
|
||||||
|
|
||||||
|
type ToolCall = Extract<UIMessage, { type: 'tool_use' }>
|
||||||
|
type ToolResult = Extract<UIMessage, { type: 'tool_result' }>
|
||||||
|
|
||||||
|
type RenderItem =
|
||||||
|
| { kind: 'tool_group'; toolCalls: ToolCall[]; id: string }
|
||||||
|
| { kind: 'message'; message: UIMessage }
|
||||||
|
|
||||||
|
function buildRenderItems(messages: UIMessage[], toolUseIds: Set<string>): RenderItem[] {
|
||||||
|
const items: RenderItem[] = []
|
||||||
|
let pendingToolCalls: ToolCall[] = []
|
||||||
|
|
||||||
|
const flushGroup = () => {
|
||||||
|
if (pendingToolCalls.length > 0) {
|
||||||
|
items.push({
|
||||||
|
kind: 'tool_group',
|
||||||
|
toolCalls: [...pendingToolCalls],
|
||||||
|
id: `group-${pendingToolCalls[0]!.id}`,
|
||||||
|
})
|
||||||
|
pendingToolCalls = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const msg of messages) {
|
||||||
|
if (msg.type === 'tool_result' && toolUseIds.has(msg.toolUseId)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'tool_use') {
|
||||||
|
if (msg.toolName === 'AskUserQuestion') {
|
||||||
|
flushGroup()
|
||||||
|
items.push({ kind: 'message', message: msg })
|
||||||
|
} else {
|
||||||
|
pendingToolCalls.push(msg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
flushGroup()
|
||||||
|
items.push({ kind: 'message', message: msg })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flushGroup()
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
export function MessageList() {
|
export function MessageList() {
|
||||||
const { messages, chatState, streamingText, activeThinkingId } = useChatStore()
|
const { messages, chatState, streamingText, activeThinkingId } = useChatStore()
|
||||||
const bottomRef = useRef<HTMLDivElement>(null)
|
const bottomRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
// Auto-scroll to bottom on new messages
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
bottomRef.current?.scrollIntoView?.({ behavior: 'smooth' })
|
bottomRef.current?.scrollIntoView?.({ behavior: 'smooth' })
|
||||||
}, [messages.length, streamingText])
|
}, [messages.length, streamingText])
|
||||||
|
|
||||||
// Build a map of toolUseId → tool_use message for linking results
|
const toolUseIds = new Set<string>()
|
||||||
const toolUseMap = new Map<string, Extract<UIMessage, { type: 'tool_use' }>>()
|
const toolResultMap = new Map<string, ToolResult>()
|
||||||
|
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
if (msg.type === 'tool_use') {
|
if (msg.type === 'tool_use') {
|
||||||
toolUseMap.set(msg.toolUseId, msg)
|
toolUseIds.add(msg.toolUseId)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// For each tool_use, find its matching tool_result
|
|
||||||
const toolResultMap = new Map<string, Extract<UIMessage, { type: 'tool_result' }>>()
|
|
||||||
for (const msg of messages) {
|
|
||||||
if (msg.type === 'tool_result' && msg.toolUseId) {
|
if (msg.type === 'tool_result' && msg.toolUseId) {
|
||||||
toolResultMap.set(msg.toolUseId, msg)
|
toolResultMap.set(msg.toolUseId, msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderItems = buildRenderItems(messages, toolUseIds)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto px-4 py-4">
|
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||||||
<div className="mx-auto max-w-[860px]">
|
<div className="mx-auto max-w-[860px]">
|
||||||
{messages.map((msg) => {
|
{renderItems.map((item) => {
|
||||||
// Skip tool_results that are rendered inline within their ToolCallBlock
|
if (item.kind === 'tool_group') {
|
||||||
if (msg.type === 'tool_result' && toolUseMap.has(msg.toolUseId)) {
|
return (
|
||||||
return null
|
<ToolCallGroup
|
||||||
|
key={item.id}
|
||||||
|
toolCalls={item.toolCalls}
|
||||||
|
resultMap={toolResultMap}
|
||||||
|
isStreaming={
|
||||||
|
chatState === 'tool_executing' &&
|
||||||
|
item.toolCalls.some((tc) => !toolResultMap.has(tc.toolUseId))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const msg = item.message
|
||||||
return (
|
return (
|
||||||
<MessageBlock
|
<MessageBlock
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
@ -52,19 +103,20 @@ export function MessageList() {
|
|||||||
activeThinkingId={activeThinkingId}
|
activeThinkingId={activeThinkingId}
|
||||||
toolResult={
|
toolResult={
|
||||||
msg.type === 'tool_use'
|
msg.type === 'tool_use'
|
||||||
? toolResultMap.get(msg.toolUseId) ?? null
|
? (() => {
|
||||||
|
const r = toolResultMap.get(msg.toolUseId)
|
||||||
|
return r ? { content: r.content, isError: r.isError } : null
|
||||||
|
})()
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Streaming text (not yet committed as a message) */}
|
|
||||||
{streamingText && chatState === 'streaming' && (
|
{streamingText && chatState === 'streaming' && (
|
||||||
<AssistantMessage content={streamingText} isStreaming />
|
<AssistantMessage content={streamingText} isStreaming />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Loading indicator */}
|
|
||||||
{chatState !== 'idle' && chatState !== 'streaming' && chatState !== 'permission_pending' && (
|
{chatState !== 'idle' && chatState !== 'streaming' && chatState !== 'permission_pending' && (
|
||||||
<StreamingIndicator />
|
<StreamingIndicator />
|
||||||
)}
|
)}
|
||||||
@ -92,7 +144,6 @@ function MessageBlock({
|
|||||||
case 'thinking':
|
case 'thinking':
|
||||||
return <ThinkingBlock content={message.content} isActive={message.id === activeThinkingId} />
|
return <ThinkingBlock content={message.content} isActive={message.id === activeThinkingId} />
|
||||||
case 'tool_use':
|
case 'tool_use':
|
||||||
// Special handling for AskUserQuestion tool calls
|
|
||||||
if (message.toolName === 'AskUserQuestion') {
|
if (message.toolName === 'AskUserQuestion') {
|
||||||
return (
|
return (
|
||||||
<AskUserQuestion
|
<AskUserQuestion
|
||||||
@ -109,7 +160,6 @@ function MessageBlock({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'tool_result':
|
case 'tool_result':
|
||||||
// Standalone tool_result (no matching tool_use found)
|
|
||||||
return (
|
return (
|
||||||
<ToolResultBlock
|
<ToolResultBlock
|
||||||
content={message.content}
|
content={message.content}
|
||||||
@ -128,13 +178,13 @@ function MessageBlock({
|
|||||||
)
|
)
|
||||||
case 'error':
|
case 'error':
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 px-4 py-3 rounded-[var(--radius-md)] bg-red-50 border border-red-200 text-sm text-[var(--color-error)]">
|
<div className="mb-3 px-4 py-2.5 rounded-lg bg-red-50 border border-red-200 text-sm text-[var(--color-error)]">
|
||||||
<strong>Error:</strong> {message.message}
|
<strong>Error:</strong> {message.message}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
case 'system':
|
case 'system':
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 text-center text-xs text-[var(--color-text-tertiary)]">
|
<div className="mb-3 text-center text-xs text-[var(--color-text-tertiary)]">
|
||||||
{message.content}
|
{message.content}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user