perf: prevent HomeScreen rerenders during agent streaming

- Add top-level agentLastActivity Map to store (prevents cascade rerenders)
- Remove lastActivityAt updates from agent_stream handler (only status changes update timestamps)
- useAggregatedAgents now uses agent.lastActivityAt directly
- Remove investigation logging from previous debug session

The root cause was that every stream event updated agent.lastActivityAt,
causing useAggregatedAgents to recalculate, which caused HomeScreen to
rerender even when not visible. Now timestamps only update on meaningful
events (agent status changes), not individual stream tokens.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-12-03 17:34:07 +07:00
co-authored by Claude
parent 4c1ba8f1b1
commit 644c654274
6 changed files with 70 additions and 27 deletions
@@ -62,14 +62,6 @@ export function AgentStreamView({
streamItems, streamItems,
pendingPermissions, pendingPermissions,
}: AgentStreamViewProps) { }: AgentStreamViewProps) {
// INVESTIGATION: Log rerenders
const renderCountRef = useRef(0);
renderCountRef.current++;
console.log(`[INVESTIGATION] AgentStreamView render #${renderCountRef.current}`, {
agentId,
streamItemsLength: streamItems.length,
pendingPermissionsSize: pendingPermissions.size,
});
const flatListRef = useRef<FlatList<StreamItem>>(null); const flatListRef = useRef<FlatList<StreamItem>>(null);
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const [isNearBottom, setIsNearBottom] = useState(true); const [isNearBottom, setIsNearBottom] = useState(true);
@@ -262,8 +262,6 @@ function AgentFlowModal({
}, [serverId, daemonEntries]); }, [serverId, daemonEntries]);
const [selectedServerId, setSelectedServerId] = useState<string | null>(initialServerId); const [selectedServerId, setSelectedServerId] = useState<string | null>(initialServerId);
// [INVESTIGATION] Using useSessionStore directly with granular selectors
// Get the complete session state for the selected server
const sessionState = useSessionStore((state) => const sessionState = useSessionStore((state) =>
selectedServerId ? state.sessions[selectedServerId] : undefined selectedServerId ? state.sessions[selectedServerId] : undefined
); );
@@ -2155,9 +2153,6 @@ function AgentFlowModal({
value={initialPrompt} value={initialPrompt}
isLoading={isLoading} isLoading={isLoading}
onChange={(text) => { onChange={(text) => {
console.log('[INVESTIGATION] CreateModal onChange called', {
textLength: text.length,
});
setInitialPrompt(text); setInitialPrompt(text);
setErrorMessage(""); setErrorMessage("");
}} }}
+36 -11
View File
@@ -223,6 +223,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
const setInitializingAgents = useSessionStore((state) => state.setInitializingAgents); const setInitializingAgents = useSessionStore((state) => state.setInitializingAgents);
const setHasHydratedAgents = useSessionStore((state) => state.setHasHydratedAgents); const setHasHydratedAgents = useSessionStore((state) => state.setHasHydratedAgents);
const setAgents = useSessionStore((state) => state.setAgents); const setAgents = useSessionStore((state) => state.setAgents);
const setAgentLastActivity = useSessionStore((state) => state.setAgentLastActivity);
const setCommands = useSessionStore((state) => state.setCommands); const setCommands = useSessionStore((state) => state.setCommands);
const setPendingPermissions = useSessionStore((state) => state.setPendingPermissions); const setPendingPermissions = useSessionStore((state) => state.setPendingPermissions);
const setGitDiffs = useSessionStore((state) => state.setGitDiffs); const setGitDiffs = useSessionStore((state) => state.setGitDiffs);
@@ -324,10 +325,12 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
const agents = new Map(); const agents = new Map();
const pendingPermissions = new Map(); const pendingPermissions = new Map();
const agentLastActivity = new Map();
for (const agentSnapshot of snapshot.agents) { for (const agentSnapshot of snapshot.agents) {
const agent = normalizeAgentSnapshot(agentSnapshot, serverId); const agent = normalizeAgentSnapshot(agentSnapshot, serverId);
agents.set(agent.id, agent); agents.set(agent.id, agent);
agentLastActivity.set(agent.id, agent.lastActivityAt);
for (const request of agent.pendingPermissions) { for (const request of agent.pendingPermissions) {
const key = derivePendingPermissionKey(agent.id, request); const key = derivePendingPermissionKey(agent.id, request);
pendingPermissions.set(key, { key, agentId: agent.id, request }); pendingPermissions.set(key, { key, agentId: agent.id, request });
@@ -341,6 +344,11 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
return agents; return agents;
}); });
// Initialize agentLastActivity slice (top-level)
for (const [agentId, timestamp] of agentLastActivity.entries()) {
setAgentLastActivity(agentId, timestamp);
}
setPendingPermissions(serverId, pendingPermissions); setPendingPermissions(serverId, pendingPermissions);
const commandEntries = snapshot.commands ?? []; const commandEntries = snapshot.commands ?? [];
setCommands(serverId, (prev) => { setCommands(serverId, (prev) => {
@@ -579,10 +587,12 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
const agents = new Map(); const agents = new Map();
const pendingPermissions = new Map(); const pendingPermissions = new Map();
const agentLastActivity = new Map();
for (const agentSnapshot of agentsList) { for (const agentSnapshot of agentsList) {
const agent = normalizeAgentSnapshot(agentSnapshot, serverId); const agent = normalizeAgentSnapshot(agentSnapshot, serverId);
agents.set(agent.id, agent); agents.set(agent.id, agent);
agentLastActivity.set(agent.id, agent.lastActivityAt);
for (const request of agent.pendingPermissions) { for (const request of agent.pendingPermissions) {
const key = derivePendingPermissionKey(agent.id, request); const key = derivePendingPermissionKey(agent.id, request);
pendingPermissions.set(key, { key, agentId: agent.id, request }); pendingPermissions.set(key, { key, agentId: agent.id, request });
@@ -592,6 +602,12 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
const normalizedCommands = commandsList.map((command) => command); const normalizedCommands = commandsList.map((command) => command);
setAgents(serverId, agents); setAgents(serverId, agents);
// Initialize agentLastActivity slice (top-level)
for (const [agentId, timestamp] of agentLastActivity.entries()) {
setAgentLastActivity(agentId, timestamp);
}
setPendingPermissions(serverId, pendingPermissions); setPendingPermissions(serverId, pendingPermissions);
setCommands(serverId, new Map(normalizedCommands.map((command: any) => [command.id, command]))); setCommands(serverId, new Map(normalizedCommands.map((command: any) => [command.id, command])));
setAgentStreamState(serverId, (prev) => { setAgentStreamState(serverId, (prev) => {
@@ -648,6 +664,9 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
return next; return next;
}); });
// Update agentLastActivity slice (top-level)
setAgentLastActivity(agent.id, agent.lastActivityAt);
setPendingPermissions(serverId, (prev) => { setPendingPermissions(serverId, (prev) => {
const next = new Map(prev); const next = new Map(prev);
for (const [key, pending] of Array.from(next.entries())) { for (const [key, pending] of Array.from(next.entries())) {
@@ -702,16 +721,9 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
return next; return next;
}); });
// Update agent's lastActivityAt to track stream activity // NOTE: We don't update lastActivityAt on every stream event to prevent
setAgents(serverId, (prev) => { // cascading rerenders. The agent_state handler updates agent.lastActivityAt
const agent = prev.get(agentId); // on status changes, which is sufficient for sorting and display purposes.
if (!agent) {
return prev;
}
const next = new Map(prev);
next.set(agentId, { ...agent, lastActivityAt: parsedTimestamp });
return next;
});
}); });
const unsubAgentStreamSnapshot = ws.on("agent_stream_snapshot", (message) => { const unsubAgentStreamSnapshot = ws.on("agent_stream_snapshot", (message) => {
@@ -1079,6 +1091,19 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
return next; return next;
}); });
// Remove from agentLastActivity slice (top-level)
useSessionStore.setState((state) => {
if (!state.agentLastActivity.has(agentId)) {
return state;
}
const nextActivity = new Map(state.agentLastActivity);
nextActivity.delete(agentId);
return {
...state,
agentLastActivity: nextActivity,
};
});
setAgentStreamState(serverId, (prev) => { setAgentStreamState(serverId, (prev) => {
if (!prev.has(agentId)) { if (!prev.has(agentId)) {
return prev; return prev;
@@ -1146,7 +1171,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
unsubProviderModels(); unsubProviderModels();
unsubAgentDeleted(); unsubAgentDeleted();
}; };
}, [ws, audioPlayer, serverId, setIsPlayingAudio, setMessages, setCurrentAssistantMessage, setAgentStreamState, setInitializingAgents, setAgents, setCommands, setPendingPermissions, setGitDiffs, setFileExplorer, setProviderModels, setHasHydratedAgents, updateConnectionStatus, getSession, saveDraftInput]); }, [ws, audioPlayer, serverId, setIsPlayingAudio, setMessages, setCurrentAssistantMessage, setAgentStreamState, setInitializingAgents, setAgents, setAgentLastActivity, setCommands, setPendingPermissions, setGitDiffs, setFileExplorer, setProviderModels, setHasHydratedAgents, updateConnectionStatus, getSession, saveDraftInput]);
// Auto-flush queued messages when agent transitions from running -> not running // Auto-flush queued messages when agent transitions from running -> not running
useEffect(() => { useEffect(() => {
@@ -40,6 +40,7 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
} }
const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId; const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId;
for (const agent of session.agents.values()) { for (const agent of session.agents.values()) {
// Use agent's own lastActivityAt field directly
allAgents.push({ allAgents.push({
id: agent.id, id: agent.id,
serverId, serverId,
+28 -2
View File
@@ -243,6 +243,9 @@ interface SessionStoreState {
// Single draft for the create agent modal // Single draft for the create agent modal
createModalDraft: DraftInput | null; createModalDraft: DraftInput | null;
// Agent activity timestamps (top-level, keyed by agentId to prevent cascade rerenders)
agentLastActivity: Map<string, Date>;
} }
// Action types // Action types
@@ -272,6 +275,9 @@ interface SessionStoreActions {
// Agents // Agents
setAgents: (serverId: string, agents: Map<string, Agent> | ((prev: Map<string, Agent>) => Map<string, Agent>)) => void; setAgents: (serverId: string, agents: Map<string, Agent> | ((prev: Map<string, Agent>) => Map<string, Agent>)) => void;
// Agent activity timestamps
setAgentLastActivity: (agentId: string, timestamp: Date) => void;
// Commands // Commands
setCommands: (serverId: string, commands: Map<string, Command> | ((prev: Map<string, Command>) => Map<string, Command>)) => void; setCommands: (serverId: string, commands: Map<string, Command> | ((prev: Map<string, Command>) => Map<string, Command>)) => void;
@@ -363,6 +369,7 @@ export const useSessionStore = create<SessionStore>()(
sessions: {}, sessions: {},
drafts: new Map(), drafts: new Map(),
createModalDraft: null, createModalDraft: null,
agentLastActivity: new Map(),
// Session management // Session management
initializeSession: (serverId, ws, audioPlayer) => { initializeSession: (serverId, ws, audioPlayer) => {
@@ -573,6 +580,22 @@ export const useSessionStore = create<SessionStore>()(
}); });
}, },
// Agent activity timestamps (top-level, does NOT mutate session object)
setAgentLastActivity: (agentId, timestamp) => {
set((prev) => {
const currentTimestamp = prev.agentLastActivity.get(agentId);
if (currentTimestamp && currentTimestamp.getTime() === timestamp.getTime()) {
return prev;
}
const nextActivity = new Map(prev.agentLastActivity);
nextActivity.set(agentId, timestamp);
return {
...prev,
agentLastActivity: nextActivity,
};
});
},
// Commands // Commands
setCommands: (serverId, commands) => { setCommands: (serverId, commands) => {
set((prev) => { set((prev) => {
@@ -775,19 +798,22 @@ export const useSessionStore = create<SessionStore>()(
// Agent directory - derived from agents (computed on-demand) // Agent directory - derived from agents (computed on-demand)
getAgentDirectory: (serverId) => { getAgentDirectory: (serverId) => {
const session = get().sessions[serverId]; const state = get();
const session = state.sessions[serverId];
if (!session) { if (!session) {
return undefined; return undefined;
} }
const entries: AgentDirectoryEntry[] = []; const entries: AgentDirectoryEntry[] = [];
for (const agent of session.agents.values()) { for (const agent of session.agents.values()) {
// Get lastActivityAt from top-level slice, fallback to agent.lastActivityAt
const lastActivityAt = state.agentLastActivity.get(agent.id) ?? agent.lastActivityAt;
entries.push({ entries.push({
id: agent.id, id: agent.id,
serverId, serverId,
title: agent.title ?? null, title: agent.title ?? null,
status: agent.status, status: agent.status,
lastActivityAt: agent.lastActivityAt, lastActivityAt,
cwd: agent.cwd, cwd: agent.cwd,
provider: agent.provider, provider: agent.provider,
}); });
+5 -1
View File
@@ -514,7 +514,11 @@ export class Session {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
} as const; } as const;
console.log(`[SERVER AGENT_STREAM] timestamp=${Date.now()} agentId=${event.agentId} eventType=${event.event.type}`); const itemType = event.event.type === "timeline" ? event.event.item.type : undefined;
const logMessage = itemType
? `[SERVER AGENT_STREAM] timestamp=${Date.now()} agentId=${event.agentId} eventType=${event.event.type} itemType=${itemType}`
: `[SERVER AGENT_STREAM] timestamp=${Date.now()} agentId=${event.agentId} eventType=${event.event.type}`;
console.log(logMessage);
this.emit({ this.emit({
type: "agent_stream", type: "agent_stream",
payload, payload,