Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@
## 2024-05-04 - Unnecessary Timer Re-renders
**Learning:** App.tsx has a 250ms setInterval updating state (runElapsed) while the agent is running, which forces the entire app tree (including large lists in FileExplorer and ChatPanel) to re-render 4 times a second.
**Action:** Always wrap large list components and recursive tree components (like FileExplorer's Row) in React.memo() when they are descendants of a component with high-frequency state updates. Also use useMemo for mapping large lists (like chat messages) to prevent unnecessary VDOM recreation.

## 2024-05-18 - [O(N) VDOM Recreation in ChatPanel with React]
**Learning:** Even if list item components (like `MessageBubble` and `ToolCallCard`) are memoized using `React.memo()`, mapping over a large array of messages inside the render cycle of a component that receives high-frequency prop updates (like `runElapsed` from a 250ms `setInterval`) still creates O(N) evaluations and recreates VDOM elements. This adds significant garbage collection overhead and reduces responsiveness.
**Action:** Always wrap the array mapping logic (`messages.map(...)`) itself with `useMemo` in parent components to completely prevent the O(N) iteration and VDOM element recreation on unrelated state changes.
33 changes: 19 additions & 14 deletions packages/desktop/src/renderer/components/ChatPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useRef, useEffect } from 'react';
import { useState, useRef, useEffect, useMemo } from 'react';
import { Send, Terminal, Zap, BookOpen } from 'lucide-react';
import type { ChatMessage } from '../App';
import type { ModeState } from '../../preload/index';
Expand Down Expand Up @@ -79,6 +79,23 @@ export default function ChatPanel({
if (inputRef.current) inputRef.current.style.height = 'auto';
};

// ⚑ Bolt: Memoize the mapping of messages to prevent O(N) VDOM recreation on frequent timer ticks (e.g. runElapsed)
const renderedMessages = useMemo(() => {
return messages.map((msg) =>
msg.role === 'tool' && msg.toolName ? (
<ToolCallCard key={msg.id} toolName={msg.toolName} toolInput={msg.toolInput} toolOutput={msg.toolOutput} timestamp={msg.timestamp} />
) : msg.role === 'info' ? (
<div key={msg.id} className="flex justify-center py-2">
<span className="text-[11px] font-medium text-xibe-text-dim bg-xibe-surface px-3 py-1 rounded-full border border-xibe-border-subtle">{msg.content}</span>
</div>
) : msg.role === 'error' ? (
<div key={msg.id} className="text-sm text-xibe-error bg-xibe-error/10 border border-xibe-error/20 rounded-xl px-4 py-3 shadow-sm">{msg.content}</div>
) : (
<MessageBubble key={msg.id} role={msg.role as 'user' | 'assistant'} content={msg.content} isStreaming={msg.isStreaming} timestamp={msg.timestamp} />
),
);
}, [messages]);

return (
<div className="flex flex-1 flex-col min-h-0">
{/* Messages area */}
Expand Down Expand Up @@ -141,19 +158,7 @@ export default function ChatPanel({
</div>
) : (
<div className="mx-auto max-w-3xl px-4 py-8 space-y-6">
{messages.map((msg) =>
msg.role === 'tool' && msg.toolName ? (
<ToolCallCard key={msg.id} toolName={msg.toolName} toolInput={msg.toolInput} toolOutput={msg.toolOutput} timestamp={msg.timestamp} />
) : msg.role === 'info' ? (
<div key={msg.id} className="flex justify-center py-2">
<span className="text-[11px] font-medium text-xibe-text-dim bg-xibe-surface px-3 py-1 rounded-full border border-xibe-border-subtle">{msg.content}</span>
</div>
) : msg.role === 'error' ? (
<div key={msg.id} className="text-sm text-xibe-error bg-xibe-error/10 border border-xibe-error/20 rounded-xl px-4 py-3 shadow-sm">{msg.content}</div>
) : (
<MessageBubble key={msg.id} role={msg.role as 'user' | 'assistant'} content={msg.content} isStreaming={msg.isStreaming} timestamp={msg.timestamp} />
),
)}
{renderedMessages}
{isRunning && (
<div className="flex items-center gap-2 text-xs text-xibe-text-dim animate-fade-in pl-2">
<div className="flex gap-1">
Expand Down
Loading