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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,6 @@
## 2025-05-17 - Replace O(N²) array search with O(N) Set lookup in session listing
**Learning:** Using `array.some(...)` with string operations (like `replace()`) inside a loop over files creates an unnecessary O(N²) performance bottleneck, particularly when reading directories with many files (like the sessions history directory).
**Action:** When filtering files against another list of files, pre-compute a `Set` of base names outside the loop for O(1) lookups, changing the overall time complexity from O(N²) to O(N).
## 2023-10-27 - O(N) Re-renders in List Components
**Learning:** React list components mapped from arrays (e.g. `sessions.map(...)`) will experience severe O(N) re-renders when a single element changes state if the evaluation is done inline inside the map without memoization. Passing the active check (e.g., `isActive={activeSessionId === s.id}`) down to a `React.memo` wrapper component is necessary to convert this into an O(1) re-render (only the two affected components will update).
**Action:** When creating high-frequency updating list elements with individual active states in React, extract the items into `React.memo` components and pass simple boolean flags to prevent massive VDOM updates.
Comment on lines +70 to +72
71 changes: 46 additions & 25 deletions packages/desktop/src/renderer/components/ChatHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,44 @@ function relativeTime(iso: string): string {
return new Date(iso).toLocaleDateString();
}

const ChatHistoryItem = memo(function ChatHistoryItem({
session,
isActive,
onSelect,
onDelete
}: {
session: SessionItem;
isActive: boolean;
onSelect: (id: string) => void;
onDelete: (e: React.MouseEvent, id: string) => void;
}) {
return (
<button
onClick={() => onSelect(session.id)}
className={cn(
"flex w-full items-center gap-2.5 rounded-lg px-2 py-1 text-left transition-all duration-200 group",
isActive
? "text-xibe-text font-medium bg-xibe-surface-hover"
: "text-xibe-text-secondary hover:bg-xibe-surface-hover hover:text-xibe-text"
)}
>
<span className="flex-1 truncate text-[13px] leading-tight">{session.title}</span>

{/* ⚡ Bolt: Used CSS group-hover instead of React state to toggle delete button visibility to prevent O(N) re-renders of the entire list on hover */}
<span
onClick={(e) => onDelete(e, session.id)}
className="shrink-0 rounded p-1 text-xibe-text-dim/50 hover:text-xibe-error hover:bg-xibe-error/10 transition-colors animate-fade-in hidden group-hover:block"
title="Delete chat"
>
<Trash2 className="h-3.5 w-3.5" />
</span>

{/* ⚡ Bolt: Hide timestamp on hover using group-hover to make room for delete button without React state overhead */}
<span className="shrink-0 text-[10px] text-xibe-text-dim/40 tabular-nums block group-hover:hidden">{relativeTime(session.updated)}</span>
</button>
Comment on lines +46 to +68
);
});

const ChatHistory = memo(function ChatHistory({ activeSessionId, onSelectSession, onNewChat }: ChatHistoryProps) {
const [sessions, setSessions] = useState<SessionItem[]>([]);

Expand All @@ -41,11 +79,11 @@ const ChatHistory = memo(function ChatHistory({ activeSessionId, onSelectSession

useEffect(() => { refresh(); }, [refresh]);

const handleDelete = async (e: React.MouseEvent, id: string) => {
const handleDelete = useCallback(async (e: React.MouseEvent, id: string) => {
e.stopPropagation();
await xibe.session.delete(id);
refresh();
};
}, [refresh]);
Comment on lines +82 to +86

const today = new Date();
today.setHours(0, 0, 0, 0);
Expand Down Expand Up @@ -89,30 +127,13 @@ const ChatHistory = memo(function ChatHistory({ activeSessionId, onSelectSession
<div className="mb-2 px-1 text-[10px] font-bold uppercase tracking-widest text-xibe-text-dim/50">{group.label}</div>
<div className="space-y-0.5">
{group.items.map((s) => (
<button
<ChatHistoryItem
key={s.id}
onClick={() => onSelectSession(s.id)}
className={cn(
"flex w-full items-center gap-2.5 rounded-lg px-2 py-1 text-left transition-all duration-200 group",
activeSessionId === s.id
? "text-xibe-text font-medium bg-xibe-surface-hover"
: "text-xibe-text-secondary hover:bg-xibe-surface-hover hover:text-xibe-text"
)}
>
<span className="flex-1 truncate text-[13px] leading-tight">{s.title}</span>

{/* ⚡ Bolt: Used CSS group-hover instead of React state to toggle delete button visibility to prevent O(N) re-renders of the entire list on hover */}
<span
onClick={(e) => handleDelete(e, s.id)}
className="shrink-0 rounded p-1 text-xibe-text-dim/50 hover:text-xibe-error hover:bg-xibe-error/10 transition-colors animate-fade-in hidden group-hover:block"
title="Delete chat"
>
<Trash2 className="h-3.5 w-3.5" />
</span>

{/* ⚡ Bolt: Hide timestamp on hover using group-hover to make room for delete button without React state overhead */}
<span className="shrink-0 text-[10px] text-xibe-text-dim/40 tabular-nums block group-hover:hidden">{relativeTime(s.updated)}</span>
</button>
session={s}
isActive={activeSessionId === s.id}
onSelect={onSelectSession}
onDelete={handleDelete}
/>
Comment on lines +130 to +136
))}
</div>
</div>
Expand Down
Loading