From dfc839fcb5df0ed6245e047ee8e6f4dfcc46acb9 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 6 Jul 2026 02:01:14 -0700 Subject: [PATCH] perf(renderer): gate WorktreeCardAgentsBody's send-target subscriptions (#7545) Co-authored-by: Orca --- .../components/sidebar/WorktreeCardAgents.tsx | 35 ++++---- .../worktree-card-send-target-inputs.test.ts | 87 +++++++++++++++++++ .../worktree-card-send-target-inputs.ts | 47 ++++++++++ 3 files changed, 149 insertions(+), 20 deletions(-) create mode 100644 src/renderer/src/components/sidebar/worktree-card-send-target-inputs.test.ts create mode 100644 src/renderer/src/components/sidebar/worktree-card-send-target-inputs.ts diff --git a/src/renderer/src/components/sidebar/WorktreeCardAgents.tsx b/src/renderer/src/components/sidebar/WorktreeCardAgents.tsx index b6a0f9f88e0..aa6f02d8378 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardAgents.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardAgents.tsx @@ -2,12 +2,14 @@ agent rendering and lineage disclosure behavior together; splitting during this bug fix would risk divergent parent-child row behavior. */ import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { useShallow } from 'zustand/react/shallow' import { useAppStore } from '@/store' import { activateAndRevealWorktree } from '@/lib/worktree-activation' import { activateTabAndFocusPane } from '@/lib/activate-tab-and-focus-pane' import DashboardAgentRow from '@/components/dashboard/DashboardAgentRow' import { useNow } from '@/components/dashboard/useNow' import { deriveRunningAgentSendTargets } from '@/lib/running-agent-targets' +import { selectSendTargetInputs } from './worktree-card-send-target-inputs' import { useWorktreeAgentRows } from './useWorktreeAgentRows' import { cn } from '@/lib/utils' import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData' @@ -89,12 +91,15 @@ const WorktreeCardAgentsBody = React.memo(function WorktreeCardAgentsBody({ const dropAgentStatus = useAppStore((s) => s.dropAgentStatus) const dismissRetainedAgent = useAppStore((s) => s.dismissRetainedAgent) const agentSendPopoverTargetMode = useAppStore((s) => s.agentSendPopoverTargetMode) - const agentStatusByPaneKey = useAppStore((s) => s.agentStatusByPaneKey) const agentStatusEpoch = useAppStore((s) => s.agentStatusEpoch) - const tabsByWorktree = useAppStore((s) => s.tabsByWorktree) - const terminalLayoutsByTabId = useAppStore((s) => s.terminalLayoutsByTabId) - const ptyIdsByTabId = useAppStore((s) => s.ptyIdsByTabId) - const runtimePaneTitlesByTabId = useAppStore((s) => s.runtimePaneTitlesByTabId) + // Why: these five maps are read only to derive send-target eligibility, which + // matters only while the send-target popover targets THIS card. Two of them + // (runtimePaneTitlesByTabId, agentStatusByPaneKey) churn on every pane-title + // and agent-status write app-wide, so subscribing to them unconditionally made + // every mounted agent body re-render on unrelated terminals. Gate the + // subscription: return a stable empty constant when the popover isn't ours, so + // useShallow keeps the same result and idle bodies stop reacting to the churn. + const sendTargetInputs = useAppStore(useShallow((s) => selectSendTargetInputs(s, worktreeId))) const sendPromptToSidebarAgentTarget = useAppStore((s) => s.sendPromptToSidebarAgentTarget) const focusedAgentPaneKey = useFocusedAgentPaneKey(worktreeId) const compactAgentListRootRef = useRef(null) @@ -134,16 +139,7 @@ const WorktreeCardAgentsBody = React.memo(function WorktreeCardAgentsBody({ } return new Map( - deriveRunningAgentSendTargets( - { - agentStatusByPaneKey, - tabsByWorktree, - terminalLayoutsByTabId, - ptyIdsByTabId, - runtimePaneTitlesByTabId - }, - worktreeId - ).map((target) => [ + deriveRunningAgentSendTargets(sendTargetInputs, worktreeId).map((target) => [ target.paneKey, agentSendPopoverTargetMode?.status === 'sending' && agentSendPopoverTargetMode.sendingPaneKey === target.paneKey @@ -159,12 +155,11 @@ const WorktreeCardAgentsBody = React.memo(function WorktreeCardAgentsBody({ agentStatusEpoch, agentSendPopoverTargetMode?.sendingPaneKey, agentSendPopoverTargetMode?.status, - agentStatusByPaneKey, isAgentSendTargetModeActive, - ptyIdsByTabId, - runtimePaneTitlesByTabId, - tabsByWorktree, - terminalLayoutsByTabId, + // sendTargetInputs is a stable empty constant while inactive and a + // shallow-compared bundle of the five maps while active, so it covers all + // five former deps in one reference. + sendTargetInputs, worktreeId ]) diff --git a/src/renderer/src/components/sidebar/worktree-card-send-target-inputs.test.ts b/src/renderer/src/components/sidebar/worktree-card-send-target-inputs.test.ts new file mode 100644 index 00000000000..03286c4f330 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-card-send-target-inputs.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from 'vitest' +import { shallow } from 'zustand/shallow' +import type { AgentSendPopoverTargetMode } from '@/store/slices/ui' +import { + EMPTY_SEND_TARGET_INPUTS, + selectSendTargetInputs, + type SendTargetInputsState +} from './worktree-card-send-target-inputs' + +const BASE: SendTargetInputsState = { + agentSendPopoverTargetMode: null, + agentStatusByPaneKey: {}, + tabsByWorktree: {}, + terminalLayoutsByTabId: {}, + ptyIdsByTabId: {}, + runtimePaneTitlesByTabId: {} +} + +function makeMode(worktreeId: string): AgentSendPopoverTargetMode { + return { + id: 'mode-1', + instanceId: 'inst-1', + worktreeId, + source: 'diff-notes', + prompt: 'do it', + label: 'Send', + launchSource: 'sidebar', + eligiblePaneKeys: [], + disabledPaneKeys: {}, + status: 'open' + } +} + +describe('selectSendTargetInputs', () => { + it('returns the shared empty constant when the popover does not target this worktree', () => { + // Inactive (popover closed): stable empty reference. + const inactive = selectSendTargetInputs(BASE, 'wt-A') + expect(inactive).toBe(EMPTY_SEND_TARGET_INPUTS) + + // Churning the hottest maps while inactive must NOT change the selected + // reference, so a useShallow subscription skips the re-render. + const churned: SendTargetInputsState = { + ...BASE, + runtimePaneTitlesByTabId: { 'tab-1': 'claude' }, + agentStatusByPaneKey: { 'tab-1:leaf-1': {} as never } + } + const afterChurn = selectSendTargetInputs(churned, 'wt-A') + expect(afterChurn).toBe(EMPTY_SEND_TARGET_INPUTS) + expect(shallow(inactive, afterChurn)).toBe(true) + }) + + it('stays the stable empty constant when the popover targets a different worktree', () => { + const s: SendTargetInputsState = { ...BASE, agentSendPopoverTargetMode: makeMode('wt-OTHER') } + expect(selectSendTargetInputs(s, 'wt-A')).toBe(EMPTY_SEND_TARGET_INPUTS) + }) + + it('exposes the live maps when the popover targets this worktree', () => { + const titles = { 'tab-1': 'claude' } + const s: SendTargetInputsState = { + ...BASE, + agentSendPopoverTargetMode: makeMode('wt-A'), + runtimePaneTitlesByTabId: titles + } + const active = selectSendTargetInputs(s, 'wt-A') + // Live map references pass straight through so eligibility derives correctly. + expect(active.runtimePaneTitlesByTabId).toBe(titles) + expect(active).not.toBe(EMPTY_SEND_TARGET_INPUTS) + }) + + it('shallow-changes only when a subscribed map reference actually changes while active', () => { + const titles = { 'tab-1': 'claude' } + const s1: SendTargetInputsState = { + ...BASE, + agentSendPopoverTargetMode: makeMode('wt-A'), + runtimePaneTitlesByTabId: titles + } + const r1 = selectSendTargetInputs(s1, 'wt-A') + + // Same underlying map refs -> shallow-equal -> no re-render. + expect(shallow(r1, selectSendTargetInputs(s1, 'wt-A'))).toBe(true) + + // A real pane-title write replaces the map ref -> shallow-unequal -> the + // open popover re-derives eligibility, exactly as before this change. + const s2: SendTargetInputsState = { ...s1, runtimePaneTitlesByTabId: { 'tab-1': 'codex' } } + expect(shallow(r1, selectSendTargetInputs(s2, 'wt-A'))).toBe(false) + }) +}) diff --git a/src/renderer/src/components/sidebar/worktree-card-send-target-inputs.ts b/src/renderer/src/components/sidebar/worktree-card-send-target-inputs.ts new file mode 100644 index 00000000000..a7e7efadc4e --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-card-send-target-inputs.ts @@ -0,0 +1,47 @@ +import type { AppState } from '@/store/types' +import type { RunningAgentTargetState } from '@/lib/running-agent-targets' + +export type SendTargetInputsState = Pick< + AppState, + | 'agentSendPopoverTargetMode' + | 'agentStatusByPaneKey' + | 'tabsByWorktree' + | 'terminalLayoutsByTabId' + | 'ptyIdsByTabId' + | 'runtimePaneTitlesByTabId' +> + +// Why: shared stable reference returned whenever the send-target popover isn't +// targeting this card. useShallow keeps the same result across unrelated +// pane-title / agent-status churn, so idle agent bodies stop re-rendering. +// Frozen so this shared singleton can never be mutated by a consumer. +export const EMPTY_SEND_TARGET_INPUTS: RunningAgentTargetState = Object.freeze({ + agentStatusByPaneKey: {}, + tabsByWorktree: {}, + terminalLayoutsByTabId: {}, + ptyIdsByTabId: {}, + runtimePaneTitlesByTabId: {} +}) + +/** + * Select the five maps `deriveRunningAgentSendTargets` needs — but only while + * the send-target popover targets this worktree. When it doesn't, return a + * stable empty constant so a useShallow-wrapped subscription stays referentially + * equal across the (very hot) pane-title / agent-status writes and skips the + * re-render that would otherwise fire on every mounted agent body app-wide. + */ +export function selectSendTargetInputs( + s: SendTargetInputsState, + worktreeId: string +): RunningAgentTargetState { + if (s.agentSendPopoverTargetMode?.worktreeId !== worktreeId) { + return EMPTY_SEND_TARGET_INPUTS + } + return { + agentStatusByPaneKey: s.agentStatusByPaneKey, + tabsByWorktree: s.tabsByWorktree, + terminalLayoutsByTabId: s.terminalLayoutsByTabId, + ptyIdsByTabId: s.ptyIdsByTabId, + runtimePaneTitlesByTabId: s.runtimePaneTitlesByTabId + } +}