From 364c5c6471ff61eda1cb7582d88659d7f16945ca Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 5 May 2026 15:41:47 +0300 Subject: [PATCH] perf(team): gate hidden team screen effects --- .../components/layout/PaneContent.tsx | 6 ++- .../components/team/TeamDetailView.tsx | 49 ++++++++++++------- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/renderer/components/layout/PaneContent.tsx b/src/renderer/components/layout/PaneContent.tsx index e65e0a9e..b4d96187 100644 --- a/src/renderer/components/layout/PaneContent.tsx +++ b/src/renderer/components/layout/PaneContent.tsx @@ -52,7 +52,11 @@ export const PaneContent = ({ pane, isPaneFocused }: PaneContentProps): React.JS {tab.type === 'teams' && } {tab.type === 'team' && ( - + )} {tab.type === 'session' && ( diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index 2c04dd15..4804eb03 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -157,6 +157,7 @@ import type { ContextUsageLike } from '@shared/utils/contextMetrics'; interface TeamDetailViewProps { teamName: string; + isActive?: boolean; isPaneFocused?: boolean; } @@ -377,10 +378,12 @@ const TeamSpawnStatusWatcher = memo(function TeamSpawnStatusWatcher({ teamName, isTeamProvisioning, isTeamAlive, + isThisTabActive, }: { teamName: string; isTeamProvisioning: boolean; isTeamAlive?: boolean; + isThisTabActive: boolean; }): null { const { leadActivity, memberSpawnStatuses, memberSpawnSnapshot, fetchMemberSpawnStatuses } = useStore( @@ -393,6 +396,8 @@ const TeamSpawnStatusWatcher = memo(function TeamSpawnStatusWatcher({ ); useEffect(() => { + if (!isThisTabActive) return; + const hasUnresolvedSpawn = hasUnresolvedMemberSpawnStatus( memberSpawnStatuses, memberSpawnSnapshot @@ -420,6 +425,7 @@ const TeamSpawnStatusWatcher = memo(function TeamSpawnStatusWatcher({ fetchMemberSpawnStatuses, isTeamAlive, isTeamProvisioning, + isThisTabActive, leadActivity, memberSpawnSnapshot, memberSpawnStatuses, @@ -888,6 +894,7 @@ const TeamMemberDetailDialogBridge = memo(function TeamMemberDetailDialogBridge( export const TeamDetailView = memo(function TeamDetailView({ teamName, + isActive = true, isPaneFocused = false, }: TeamDetailViewProps): React.JSX.Element { const { isLight } = useTheme(); @@ -1279,8 +1286,7 @@ export const TeamDetailView = memo(function TeamDetailView({ ); const tabId = useTabIdOptional(); - const activeTabId = useStore((s) => s.activeTabId); - const isThisTabActive = tabId ? activeTabId === tabId : false; + const isThisTabActive = isActive; const wasInteractiveRef = useRef(false); // Messages panel resize @@ -1324,33 +1330,32 @@ export const TeamDetailView = memo(function TeamDetailView({ useEffect(() => { const wasProvisioning = wasProvisioningRef.current; wasProvisioningRef.current = isTeamProvisioning; + if (!isThisTabActive) return; if (!wasProvisioning && isTeamProvisioning) { provisioningBannerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); } - }, [isTeamProvisioning]); + }, [isTeamProvisioning, isThisTabActive]); const [kanbanSearch, setKanbanSearch] = useState(''); // Open editor overlay when a file reveal is requested (e.g. from chip click) const pendingRevealFile = useStore((s) => s.editorPendingRevealFile); useEffect(() => { + if (!isThisTabActive) return; if (pendingRevealFile && data?.config.projectPath) { setEditorOpen(true); } - }, [pendingRevealFile, data?.config.projectPath]); + }, [isThisTabActive, pendingRevealFile, data?.config.projectPath]); useEffect(() => { - if (!teamName) { + if (!isThisTabActive || !teamName) { return; } void selectTeam(teamName); void fetchDeletedTasks(teamName); - }, [teamName, selectTeam, fetchDeletedTasks]); + }, [isThisTabActive, teamName, selectTeam, fetchDeletedTasks]); - // Recovery: after HMR, all mounted TeamDetailView effects re-run simultaneously. - // With CSS display-toggle (all tabs stay mounted), the last selectTeam() call wins - // and other tabs get stuck with mismatched data (permanent skeleton). - // Re-trigger selectTeam when this tab becomes active and store data is stale. + // Re-trigger selectTeam when this visible tab becomes active and store data is stale. const storedTeamName = data?.teamName; useEffect(() => { if (!isThisTabActive || !teamName || loading) return; @@ -1387,7 +1392,7 @@ export const TeamDetailView = memo(function TeamDetailView({ // Fetch active teams when launch dialog opens (for conflict warning) useEffect(() => { - if (!launchDialogOpen) return; + if (!isThisTabActive || !launchDialogOpen) return; let cancelled = false; const teamsSnapshot = useStore.getState().teams; void (async () => { @@ -1410,7 +1415,7 @@ export const TeamDetailView = memo(function TeamDetailView({ return () => { cancelled = true; }; - }, [launchDialogOpen]); + }, [isThisTabActive, launchDialogOpen]); useEffect(() => { if (kanbanFilterQuery) { @@ -1456,7 +1461,7 @@ export const TeamDetailView = memo(function TeamDetailView({ ]); useEffect(() => { - if (!projectId) return; + if (!isThisTabActive || !projectId) return; let cancelled = false; setSessionsLoading(true); @@ -1485,7 +1490,7 @@ export const TeamDetailView = memo(function TeamDetailView({ return () => { cancelled = true; }; - }, [data?.config.leadSessionId, projectId, sessionHistoryKey]); + }, [data?.config.leadSessionId, isThisTabActive, projectId, sessionHistoryKey]); // Live git branch tracking for the lead project and member worktrees const teamProjectPath = data?.config.projectPath?.trim() ?? null; @@ -1510,7 +1515,11 @@ export const TeamDetailView = memo(function TeamDetailView({ return Array.from(uniquePaths.values()); }, [members, leadProjectPath]); - useBranchSync(branchSyncPaths, { live: true }); + const activeBranchSyncPaths = useMemo( + () => (isThisTabActive ? branchSyncPaths : []), + [branchSyncPaths, isThisTabActive] + ); + useBranchSync(activeBranchSyncPaths, { live: isThisTabActive }); const trackedBranches = useStore( useShallow((s) => Object.fromEntries( @@ -1830,6 +1839,7 @@ export const TeamDetailView = memo(function TeamDetailView({ // Pick up pending review request from GlobalTaskDetailDialog useEffect(() => { + if (!isThisTabActive) return; if (!pendingReviewRequest) return; setReviewDialogState({ open: true, @@ -1842,11 +1852,12 @@ export const TeamDetailView = memo(function TeamDetailView({ selectReviewFile(pendingReviewRequest.filePath); } setPendingReviewRequest(null); - }, [pendingReviewRequest, selectReviewFile, setPendingReviewRequest]); + }, [isThisTabActive, pendingReviewRequest, selectReviewFile, setPendingReviewRequest]); const pendingTeamSectionFocus = useStore((s) => s.pendingTeamSectionFocus); const clearTeamSectionFocus = useStore((s) => s.clearTeamSectionFocus); useEffect(() => { + if (!isThisTabActive) return; if (pendingTeamSectionFocus?.teamName !== teamName) return; const sectionId = @@ -1868,11 +1879,12 @@ export const TeamDetailView = memo(function TeamDetailView({ if (!section) return; section.dispatchEvent(new CustomEvent('team-section-navigate')); clearTeamSectionFocus(); - }, [pendingTeamSectionFocus, clearTeamSectionFocus, teamName, data]); + }, [pendingTeamSectionFocus, clearTeamSectionFocus, isThisTabActive, teamName, data]); // Pick up pending member profile request from MemberHoverCard const pendingMemberProfile = useStore((s) => s.pendingMemberProfile); useEffect(() => { + if (!isThisTabActive) return; if (!pendingMemberProfile || !data) return; if (pendingMemberProfile.teamName && pendingMemberProfile.teamName !== teamName) return; @@ -1889,7 +1901,7 @@ export const TeamDetailView = memo(function TeamDetailView({ }); } useStore.getState().closeMemberProfile(); - }, [pendingMemberProfile, membersWithLiveBranches, teamName, data]); + }, [isThisTabActive, pendingMemberProfile, membersWithLiveBranches, teamName, data]); const handleDeleteTask = useCallback( (taskId: string) => { @@ -2058,6 +2070,7 @@ export const TeamDetailView = memo(function TeamDetailView({ teamName={teamName} isTeamProvisioning={isTeamProvisioning} isTeamAlive={data?.isAlive} + isThisTabActive={isThisTabActive} /> ); const teamAgentRuntimeWatcher = (