diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index 1f7322cfa..cdd494f42 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -62,14 +62,6 @@ export function AgentStreamView({ streamItems, pendingPermissions, }: 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>(null); const insets = useSafeAreaInsets(); const [isNearBottom, setIsNearBottom] = useState(true); diff --git a/packages/app/src/components/create-agent-modal.tsx b/packages/app/src/components/create-agent-modal.tsx index fba0afb55..1a395c436 100644 --- a/packages/app/src/components/create-agent-modal.tsx +++ b/packages/app/src/components/create-agent-modal.tsx @@ -262,8 +262,6 @@ function AgentFlowModal({ }, [serverId, daemonEntries]); const [selectedServerId, setSelectedServerId] = useState(initialServerId); - // [INVESTIGATION] Using useSessionStore directly with granular selectors - // Get the complete session state for the selected server const sessionState = useSessionStore((state) => selectedServerId ? state.sessions[selectedServerId] : undefined ); @@ -2155,9 +2153,6 @@ function AgentFlowModal({ value={initialPrompt} isLoading={isLoading} onChange={(text) => { - console.log('[INVESTIGATION] CreateModal onChange called', { - textLength: text.length, - }); setInitialPrompt(text); setErrorMessage(""); }} diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index b49d0b3eb..bd04588d0 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -223,6 +223,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid const setInitializingAgents = useSessionStore((state) => state.setInitializingAgents); const setHasHydratedAgents = useSessionStore((state) => state.setHasHydratedAgents); const setAgents = useSessionStore((state) => state.setAgents); + const setAgentLastActivity = useSessionStore((state) => state.setAgentLastActivity); const setCommands = useSessionStore((state) => state.setCommands); const setPendingPermissions = useSessionStore((state) => state.setPendingPermissions); const setGitDiffs = useSessionStore((state) => state.setGitDiffs); @@ -324,10 +325,12 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid const agents = new Map(); const pendingPermissions = new Map(); + const agentLastActivity = new Map(); for (const agentSnapshot of snapshot.agents) { const agent = normalizeAgentSnapshot(agentSnapshot, serverId); agents.set(agent.id, agent); + agentLastActivity.set(agent.id, agent.lastActivityAt); for (const request of agent.pendingPermissions) { const key = derivePendingPermissionKey(agent.id, request); pendingPermissions.set(key, { key, agentId: agent.id, request }); @@ -341,6 +344,11 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid return agents; }); + // Initialize agentLastActivity slice (top-level) + for (const [agentId, timestamp] of agentLastActivity.entries()) { + setAgentLastActivity(agentId, timestamp); + } + setPendingPermissions(serverId, pendingPermissions); const commandEntries = snapshot.commands ?? []; setCommands(serverId, (prev) => { @@ -579,10 +587,12 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid const agents = new Map(); const pendingPermissions = new Map(); + const agentLastActivity = new Map(); for (const agentSnapshot of agentsList) { const agent = normalizeAgentSnapshot(agentSnapshot, serverId); agents.set(agent.id, agent); + agentLastActivity.set(agent.id, agent.lastActivityAt); for (const request of agent.pendingPermissions) { const key = derivePendingPermissionKey(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); setAgents(serverId, agents); + + // Initialize agentLastActivity slice (top-level) + for (const [agentId, timestamp] of agentLastActivity.entries()) { + setAgentLastActivity(agentId, timestamp); + } + setPendingPermissions(serverId, pendingPermissions); setCommands(serverId, new Map(normalizedCommands.map((command: any) => [command.id, command]))); setAgentStreamState(serverId, (prev) => { @@ -648,6 +664,9 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid return next; }); + // Update agentLastActivity slice (top-level) + setAgentLastActivity(agent.id, agent.lastActivityAt); + setPendingPermissions(serverId, (prev) => { const next = new Map(prev); for (const [key, pending] of Array.from(next.entries())) { @@ -702,16 +721,9 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid return next; }); - // Update agent's lastActivityAt to track stream activity - setAgents(serverId, (prev) => { - const agent = prev.get(agentId); - if (!agent) { - return prev; - } - const next = new Map(prev); - next.set(agentId, { ...agent, lastActivityAt: parsedTimestamp }); - return next; - }); + // NOTE: We don't update lastActivityAt on every stream event to prevent + // cascading rerenders. The agent_state handler updates agent.lastActivityAt + // on status changes, which is sufficient for sorting and display purposes. }); const unsubAgentStreamSnapshot = ws.on("agent_stream_snapshot", (message) => { @@ -1079,6 +1091,19 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid 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) => { if (!prev.has(agentId)) { return prev; @@ -1146,7 +1171,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid unsubProviderModels(); 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 useEffect(() => { diff --git a/packages/app/src/hooks/use-aggregated-agents.ts b/packages/app/src/hooks/use-aggregated-agents.ts index be5cf791a..5c35cdf0e 100644 --- a/packages/app/src/hooks/use-aggregated-agents.ts +++ b/packages/app/src/hooks/use-aggregated-agents.ts @@ -40,6 +40,7 @@ export function useAggregatedAgents(): AggregatedAgentsResult { } const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId; for (const agent of session.agents.values()) { + // Use agent's own lastActivityAt field directly allAgents.push({ id: agent.id, serverId, diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index 5cedaee25..17a907566 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -243,6 +243,9 @@ interface SessionStoreState { // Single draft for the create agent modal createModalDraft: DraftInput | null; + + // Agent activity timestamps (top-level, keyed by agentId to prevent cascade rerenders) + agentLastActivity: Map; } // Action types @@ -272,6 +275,9 @@ interface SessionStoreActions { // Agents setAgents: (serverId: string, agents: Map | ((prev: Map) => Map)) => void; + // Agent activity timestamps + setAgentLastActivity: (agentId: string, timestamp: Date) => void; + // Commands setCommands: (serverId: string, commands: Map | ((prev: Map) => Map)) => void; @@ -363,6 +369,7 @@ export const useSessionStore = create()( sessions: {}, drafts: new Map(), createModalDraft: null, + agentLastActivity: new Map(), // Session management initializeSession: (serverId, ws, audioPlayer) => { @@ -573,6 +580,22 @@ export const useSessionStore = create()( }); }, + // 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 setCommands: (serverId, commands) => { set((prev) => { @@ -775,19 +798,22 @@ export const useSessionStore = create()( // Agent directory - derived from agents (computed on-demand) getAgentDirectory: (serverId) => { - const session = get().sessions[serverId]; + const state = get(); + const session = state.sessions[serverId]; if (!session) { return undefined; } const entries: AgentDirectoryEntry[] = []; 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({ id: agent.id, serverId, title: agent.title ?? null, status: agent.status, - lastActivityAt: agent.lastActivityAt, + lastActivityAt, cwd: agent.cwd, provider: agent.provider, }); diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 3e4dc2713..d6d78e1b7 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -514,7 +514,11 @@ export class Session { timestamp: new Date().toISOString(), } 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({ type: "agent_stream", payload,