From 8d5169cb3999c6c95ce13ac161591e4506117430 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 2 Dec 2025 13:45:35 +0000 Subject: [PATCH] Memoize ToolCall task summary rendering --- packages/ui/src/components/tool-call.tsx | 112 ++++++++++++++--------- 1 file changed, 68 insertions(+), 44 deletions(-) diff --git a/packages/ui/src/components/tool-call.tsx b/packages/ui/src/components/tool-call.tsx index 501fce2b..64419a2e 100644 --- a/packages/ui/src/components/tool-call.tsx +++ b/packages/ui/src/components/tool-call.tsx @@ -34,6 +34,7 @@ function isToolStateError(state: ToolState): state is ToolStateError { const TOOL_CALL_CACHE_SCOPE = "tool-call" +const taskSummaryCache = new Map() function makeRenderCacheKey( toolCallId?: string | null, @@ -318,13 +319,15 @@ function renderDiagnosticsSection( export default function ToolCall(props: ToolCallProps) { const { preferences, setDiffViewMode } = useConfig() const { isDark } = useTheme() - const toolCallId = () => props.toolCallId || props.toolCall?.id || "" + const toolCallMemo = createMemo(() => props.toolCall) + const toolCallId = () => props.toolCallId || toolCallMemo()?.id || "" + const toolState = createMemo(() => toolCallMemo()?.state) const store = createMemo(() => messageStoreBus.getOrCreate(props.instanceId)) const cacheContext = createMemo(() => ({ toolCallId: toolCallId(), messageId: props.messageId, - partId: props.toolCall?.id ?? null, + partId: toolCallMemo()?.id ?? null, })) const createVariantCache = (variant: string) => @@ -341,20 +344,20 @@ export default function ToolCall(props: ToolCallProps) { const diffCache = createVariantCache("diff") const permissionDiffCache = createVariantCache("permission-diff") const markdownCache = createVariantCache("markdown") - const permissionState = createMemo(() => store().getPermissionState(props.messageId, props.toolCall?.id)) + const permissionState = createMemo(() => store().getPermissionState(props.messageId, toolCallMemo()?.id)) const pendingPermission = createMemo(() => { const state = permissionState() if (state) { return { permission: state.entry.permission, active: state.active } } - return props.toolCall.pendingPermission + return toolCallMemo()?.pendingPermission }) const toolOutputDefaultExpanded = createMemo(() => (preferences().toolOutputExpansion || "expanded") === "expanded") const diagnosticsDefaultExpanded = createMemo(() => (preferences().diagnosticsExpansion || "expanded") === "expanded") const defaultExpandedForTool = createMemo(() => { const prefExpanded = toolOutputDefaultExpanded() - const toolName = props.toolCall?.tool || "" + const toolName = toolCallMemo()?.tool || "" if (toolName === "read") { return false } @@ -390,8 +393,8 @@ export default function ToolCall(props: ToolCallProps) { } const diagnosticsEntries = createMemo(() => { - const tool = props.toolCall?.tool || "" - const state = props.toolCall?.state + const tool = toolCallMemo()?.tool || "" + const state = toolState() if (!state) return [] return extractDiagnostics(tool, state) }) @@ -455,7 +458,7 @@ export default function ToolCall(props: ToolCallProps) { }) const statusIcon = () => { - const status = props.toolCall?.state?.status || "" + const status = toolState()?.status || "" switch (status) { case "pending": return "⏸" @@ -471,7 +474,7 @@ export default function ToolCall(props: ToolCallProps) { } const statusClass = () => { - const status = props.toolCall?.state?.status || "pending" + const status = toolState()?.status || "pending" return `tool-call-status-${status}` } @@ -492,7 +495,7 @@ export default function ToolCall(props: ToolCallProps) { } const renderToolAction = () => { - const toolName = props.toolCall?.tool || "" + const toolName = toolCallMemo()?.tool || "" switch (toolName) { case "task": return "Delegating..." @@ -598,7 +601,7 @@ export default function ToolCall(props: ToolCallProps) { } const getTodoTitle = () => { - const state = props.toolCall?.state + const state = toolState() if (!state) return "Plan" const todos = extractTodosFromState(state) @@ -611,8 +614,8 @@ export default function ToolCall(props: ToolCallProps) { } const renderToolTitle = () => { - const toolName = props.toolCall?.tool || "" - const state = props.toolCall?.state + const toolName = toolCallMemo()?.tool || "" + const state = toolState() if (!state) return renderToolAction() if (state.status === "pending") return renderToolAction() @@ -685,8 +688,8 @@ export default function ToolCall(props: ToolCallProps) { } function renderToolBody() { - const toolName = props.toolCall?.tool || "" - const state = props.toolCall?.state || {} + const toolName = toolCallMemo()?.tool || "" + const state = toolState() || {} if (toolName === "todoread" || toolName === "todowrite") { return renderTodoTool() @@ -957,7 +960,7 @@ export default function ToolCall(props: ToolCallProps) { } const renderTodoTool = () => { - const state = props.toolCall?.state + const state = toolState() if (!state) return null const todos = extractTodosFromState(state) @@ -1000,49 +1003,69 @@ export default function ToolCall(props: ToolCallProps) { ) } - const renderTaskTool = () => { - const state = props.toolCall?.state - if (!state) return null - - const metadata = (isToolStateRunning(state) || isToolStateCompleted(state) || isToolStateError(state)) - ? state.metadata || {} - : {} - const summary = metadata.summary || [] + type TaskSummaryItem = { + id: string + tool: string + input: Record + } - if (!Array.isArray(summary) || summary.length === 0) { - return null + const taskSummary = createMemo(() => { + const state = toolState() + if (!state) return [] + const metadata = (isToolStateRunning(state) || isToolStateCompleted(state) || isToolStateError(state)) + ? (state.metadata || {}) as Record + : ({} as Record) + const rawSummary = Array.isArray((metadata as any).summary) ? ((metadata as any).summary as any[]) : [] + if (rawSummary.length === 0) { + taskSummaryCache.delete(toolCallId()) + return [] } + const signature = JSON.stringify(rawSummary) + const cacheKey = toolCallId() || "__unknown__" + const cached = taskSummaryCache.get(cacheKey) + if (cached && cached.signature === signature) { + return cached.items + } + const normalized: TaskSummaryItem[] = rawSummary.map((entry, index) => { + const tool = typeof entry?.tool === "string" ? (entry.tool as string) : "unknown" + const input = typeof (entry as any)?.state?.input === "object" && entry.state?.input ? entry.state.input : {} + const id = typeof entry?.id === "string" && entry.id.length > 0 ? entry.id : `${tool}-${index}` + return { id, tool, input } + }) + taskSummaryCache.set(cacheKey, { signature, items: normalized }) + return normalized + }) + + const renderTaskTool = () => { + const items = taskSummary() + if (items.length === 0) return null return ( -
initializeScrollContainer(element)} - onScroll={(event) => persistScrollSnapshot(event.currentTarget)} - > +
- + {(item) => { - const tool = item.tool || "unknown" - const itemInput = item.state?.input || {} - const icon = getToolIcon(tool) + const icon = getToolIcon(item.tool) + const input = item.input || {} let description = "" - switch (tool) { + switch (item.tool) { case "bash": - description = itemInput.description || itemInput.command || "" + description = input.description || input.command || "" break case "edit": case "read": case "write": - description = `${tool} ${getRelativePath(itemInput.filePath || "")}` + description = `${item.tool} ${getRelativePath(input.filePath || "")}` break default: - description = tool + description = item.tool } return ( -
- {icon} {description} +
+ {icon} + {description}
) }} @@ -1052,8 +1075,9 @@ export default function ToolCall(props: ToolCallProps) { ) } + const renderError = () => { - const state = props.toolCall?.state || {} + const state = toolState() || {} if (state.status === "error" && state.error) { return (
@@ -1151,8 +1175,8 @@ export default function ToolCall(props: ToolCallProps) { ) } - const toolName = () => props.toolCall?.tool || "" - const status = () => props.toolCall?.state?.status || "" + const toolName = () => toolCallMemo()?.tool || "" + const status = () => toolState()?.status || "" return (