Skip to content
Open
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
@@ -1,3 +1,7 @@
## 2026-10-12 - [Performance] Optimize Chat Message Signature Generation
**Learning:** During chat polling, \`messagesSignature\` generates a unique string for the whole thread. Using \`messages.map(...).join(',')\` nested with another \`map().join()\` created a lot of intermediate arrays and strings on every poll, causing CPU/Memory overhead on long lists. The previous signature was also very bloated because it used empty spaces for messages without reactions (e.g. \`,,,,\`).
**Action:** Replace nested \`map().join()\` calls with a simple \`for\` loop and string concatenation. This is about 4-5x faster and significantly more memory efficient by skipping empty slots instead of allocating intermediate arrays. When constructing signatures or serializing large arrays on a hot path, prefer manual loops over array map/filter chains.

## 2026-10-15 - [Performance] Prevent unnecessary UI array recalculations
**Learning:** In the chat component (`useThreadList`), derived state that requires filtering and sorting large arrays (like generating nested agent lists) was running synchronously on every render. Given that chat polling triggers frequent re-renders, this caused unnecessary CPU strain on lists that only rarely change.
**Action:** Use `useMemo` for derived states like `rows` and `current` in custom hooks when they involve `.filter()`, `.map()`, or `.sort()` and depend on data that updates less frequently than the component renders.
17 changes: 12 additions & 5 deletions apps/web/src/components/chat/useThreadList.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef, useState } from 'preact/hooks';
import { useRef, useState, useMemo } from 'preact/hooks';
import { api, type ProjectAgent } from '../../lib/api';
import { useToast } from '../ui';
import { nestAgentRows, readStoredThread, sortAgentsPinned, sortByRecent, storeThread, titleFromMessage } from './thread-meta';
Expand All @@ -25,10 +25,17 @@ export function useThreadList({ projectUuid, threadsMode, persona, defaultAgentU
const bootstrapped = useRef(false);

const list = threadsMode ? threads : agents;
const current = list.find((a) => a.uuid === selected) ?? agents.find((a) => a.uuid === selected) ?? null;
const rows = threadsMode
? nestAgentRows(list.filter((a) => a.kind !== 'subagent'), list)
: nestAgentRows(sortAgentsPinned(agents.filter((a) => a.kind !== 'subagent')), agents);

const current = useMemo(
() => list.find((a) => a.uuid === selected) ?? agents.find((a) => a.uuid === selected) ?? null,
[list, agents, selected]
);

const rows = useMemo(() => {
return threadsMode
? nestAgentRows(list.filter((a) => a.kind !== 'subagent'), list)
: nestAgentRows(sortAgentsPinned(agents.filter((a) => a.kind !== 'subagent')), agents);
}, [threadsMode, list, agents]);

async function resolveThreads(main: ProjectAgent[]): Promise<ProjectAgent[]> {
// Workspace = fil coordinateur permanent + tâches isolées. Ops/Reviewer restent dans Agents.
Expand Down
Loading