diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj
index 616bb4aaa..67ca973f3 100644
--- a/NetNewsWire.xcodeproj/project.pbxproj
+++ b/NetNewsWire.xcodeproj/project.pbxproj
@@ -143,8 +143,6 @@
 		5137C2EA26F63AE6009EFEDB /* ArticleThemeImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5137C2E926F63AE6009EFEDB /* ArticleThemeImporter.swift */; };
 		51386A8E25673277005F3762 /* AccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51386A8D25673276005F3762 /* AccountCell.swift */; };
 		51386A8F25673277005F3762 /* AccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51386A8D25673276005F3762 /* AccountCell.swift */; };
-		5138E93A24D33E5600AFF0FE /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 5138E93924D33E5600AFF0FE /* RSTree */; };
-		5138E93B24D33E5600AFF0FE /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 5138E93924D33E5600AFF0FE /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		5138E95224D3418100AFF0FE /* RSParser in Frameworks */ = {isa = PBXBuildFile; productRef = 5138E95124D3418100AFF0FE /* RSParser */; };
 		5138E95324D3418100AFF0FE /* RSParser in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 5138E95124D3418100AFF0FE /* RSParser */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		5138E95824D3419000AFF0FE /* RSWeb in Frameworks */ = {isa = PBXBuildFile; productRef = 5138E95724D3419000AFF0FE /* RSWeb */; };
@@ -177,8 +175,6 @@
 		5148F4552336DB7000F8CD8B /* TimelineTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5148F4542336DB7000F8CD8B /* TimelineTitleView.swift */; };
 		514B7C8323205EFB00BAC947 /* RootSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514B7C8223205EFB00BAC947 /* RootSplitViewController.swift */; };
 		514C16CE24D2E63F009A3AFA /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 514C16CD24D2E63F009A3AFA /* Account */; };
-		514C16DE24D2EF15009A3AFA /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 514C16DD24D2EF15009A3AFA /* RSTree */; };
-		514C16DF24D2EF15009A3AFA /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 514C16DD24D2EF15009A3AFA /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		5154368B229404D1005E1CDF /* FaviconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F76227716200050506E /* FaviconGenerator.swift */; };
 		515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97971ED9EFAA007D329B /* Node-Extensions.swift */; };
 		515D4FCC2325815A00EE1167 /* SafariExt.js in Resources */ = {isa = PBXBuildFile; fileRef = 515D4FCB2325815A00EE1167 /* SafariExt.js */; };
@@ -263,10 +259,6 @@
 		51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */; };
 		51BB7C312335ACDE008E8144 /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = 51BB7C302335ACDE008E8144 /* page.html */; };
 		51BC2F3824D3439A00E90810 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F3724D3439A00E90810 /* Account */; };
-		51BC2F4824D3439E00E90810 /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F4724D3439E00E90810 /* RSTree */; };
-		51BC2F4924D3439E00E90810 /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F4724D3439E00E90810 /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
-		51BC2F4D24D343AB00E90810 /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F4C24D343AB00E90810 /* RSTree */; };
-		51BC2F4E24D343AB00E90810 /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F4C24D343AB00E90810 /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		51BC4AFF247277E0000A6ED8 /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */; };
 		51BC4B00247277E0000A6ED8 /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */; };
 		51BC4B01247277E0000A6ED8 /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */; };
@@ -401,8 +393,6 @@
 		653813282680E1EC007A082C /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 653813272680E1EC007A082C /* CrashReporter */; };
 		653813302680E20C007A082C /* RSParser in Frameworks */ = {isa = PBXBuildFile; productRef = 6538132F2680E20C007A082C /* RSParser */; };
 		653813312680E20C007A082C /* RSParser in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 6538132F2680E20C007A082C /* RSParser */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
-		653813332680E220007A082C /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 653813322680E220007A082C /* RSTree */; };
-		653813342680E220007A082C /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 653813322680E220007A082C /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		653813362680E224007A082C /* RSWeb in Frameworks */ = {isa = PBXBuildFile; productRef = 653813352680E224007A082C /* RSWeb */; };
 		653813372680E224007A082C /* RSWeb in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 653813352680E224007A082C /* RSWeb */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		653813392680E22B007A082C /* Secrets in Frameworks */ = {isa = PBXBuildFile; productRef = 653813382680E22B007A082C /* Secrets */; };
@@ -607,6 +597,9 @@
 		841ABA4E20145E7300980E11 /* NothingInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA4D20145E7300980E11 /* NothingInspectorViewController.swift */; };
 		841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA5D20145E9200980E11 /* FolderInspectorViewController.swift */; };
 		841ABA6020145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA5F20145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift */; };
+		841CECD82BAD04B20001EE72 /* Tree in Frameworks */ = {isa = PBXBuildFile; productRef = 841CECD72BAD04B20001EE72 /* Tree */; };
+		841CECDA2BAD04B80001EE72 /* Tree in Frameworks */ = {isa = PBXBuildFile; productRef = 841CECD92BAD04B80001EE72 /* Tree */; };
+		841CECDC2BAD04BF0001EE72 /* Tree in Frameworks */ = {isa = PBXBuildFile; productRef = 841CECDB2BAD04BF0001EE72 /* Tree */; };
 		84216D0322128B9D0049B9B9 /* DetailWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84216D0222128B9D0049B9B9 /* DetailWebViewController.swift */; };
 		8426118A1FCB67AA0086A189 /* FeedIconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611891FCB67AA0086A189 /* FeedIconDownloader.swift */; };
 		8426119E1FCB6ED40086A189 /* HTMLMetadataDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8426119D1FCB6ED40086A189 /* HTMLMetadataDownloader.swift */; };
@@ -903,7 +896,6 @@
 			dstPath = "";
 			dstSubfolderSpec = 10;
 			files = (
-				51BC2F4924D3439E00E90810 /* RSTree in Embed Frameworks */,
 			);
 			name = "Embed Frameworks";
 			runOnlyForDeploymentPostprocessing = 0;
@@ -914,7 +906,6 @@
 			dstPath = "";
 			dstSubfolderSpec = 10;
 			files = (
-				51BC2F4E24D343AB00E90810 /* RSTree in Embed Frameworks */,
 			);
 			name = "Embed Frameworks";
 			runOnlyForDeploymentPostprocessing = 0;
@@ -954,7 +945,6 @@
 				513F327B2593EE6F0003048F /* SyncDatabase in Embed Frameworks */,
 				513F32722593EE6F0003048F /* Articles in Embed Frameworks */,
 				513F32812593EF180003048F /* Account in Embed Frameworks */,
-				5138E93B24D33E5600AFF0FE /* RSTree in Embed Frameworks */,
 				513F32752593EE6F0003048F /* ArticlesDatabase in Embed Frameworks */,
 			);
 			name = "Embed Frameworks";
@@ -992,7 +982,6 @@
 				653813312680E20C007A082C /* RSParser in Embed Frameworks */,
 				6538133A2680E22B007A082C /* Secrets in Embed Frameworks */,
 				653813252680E1D6007A082C /* ArticlesDatabase in Embed Frameworks */,
-				653813342680E220007A082C /* RSTree in Embed Frameworks */,
 				653813222680E1D0007A082C /* Articles in Embed Frameworks */,
 				6538131F2680E1CA007A082C /* Account in Embed Frameworks */,
 			);
@@ -1038,7 +1027,6 @@
 				513277652590FC640064F1E7 /* SyncDatabase in Embed Frameworks */,
 				513277622590FC640064F1E7 /* ArticlesDatabase in Embed Frameworks */,
 				51A737C924DB19CC0015FA66 /* RSParser in Embed Frameworks */,
-				514C16DF24D2EF15009A3AFA /* RSTree in Embed Frameworks */,
 			);
 			name = "Embed Frameworks";
 			runOnlyForDeploymentPostprocessing = 0;
@@ -1325,6 +1313,7 @@
 		841ABA4D20145E7300980E11 /* NothingInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NothingInspectorViewController.swift; sourceTree = "<group>"; };
 		841ABA5D20145E9200980E11 /* FolderInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderInspectorViewController.swift; sourceTree = "<group>"; };
 		841ABA5F20145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuiltinSmartFeedInspectorViewController.swift; sourceTree = "<group>"; };
+		841CECD62BAD03C60001EE72 /* Tree */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Tree; sourceTree = "<group>"; };
 		84216D0222128B9D0049B9B9 /* DetailWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailWebViewController.swift; sourceTree = "<group>"; };
 		842611891FCB67AA0086A189 /* FeedIconDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedIconDownloader.swift; sourceTree = "<group>"; };
 		8426119D1FCB6ED40086A189 /* HTMLMetadataDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadataDownloader.swift; sourceTree = "<group>"; };
@@ -1535,7 +1524,6 @@
 			buildActionMask = 2147483647;
 			files = (
 				27B86EEB25A53AAB00264340 /* Account in Frameworks */,
-				51BC2F4D24D343AB00E90810 /* RSTree in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -1544,7 +1532,6 @@
 			buildActionMask = 2147483647;
 			files = (
 				51BC2F3824D3439A00E90810 /* Account in Frameworks */,
-				51BC2F4824D3439E00E90810 /* RSTree in Frameworks */,
 				84D9582C2BABE53B0053E7B2 /* FoundationExtras in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -1579,11 +1566,11 @@
 				653813362680E224007A082C /* RSWeb in Frameworks */,
 				84DCA51C2BABB78E00792720 /* CloudKitExtras in Frameworks */,
 				84DCA5272BABBB6200792720 /* Core in Frameworks */,
+				841CECDA2BAD04B80001EE72 /* Tree in Frameworks */,
 				84DCA5182BABB77E00792720 /* FoundationExtras in Frameworks */,
 				653813302680E20C007A082C /* RSParser in Frameworks */,
 				6538131E2680E1CA007A082C /* Account in Frameworks */,
 				653813282680E1EC007A082C /* CrashReporter in Frameworks */,
-				653813332680E220007A082C /* RSTree in Frameworks */,
 				84DCA51A2BABB78700792720 /* AppKitExtras in Frameworks */,
 				653813262680E1E4007A082C /* CloudKit.framework in Frameworks */,
 				653813242680E1D6007A082C /* ArticlesDatabase in Frameworks */,
@@ -1614,9 +1601,9 @@
 				84DCA5222BABB7A800792720 /* CloudKitExtras in Frameworks */,
 				513F32742593EE6F0003048F /* ArticlesDatabase in Frameworks */,
 				513F327A2593EE6F0003048F /* SyncDatabase in Frameworks */,
+				841CECDC2BAD04BF0001EE72 /* Tree in Frameworks */,
 				848565512B9E910200F4BAE0 /* FMDB in Frameworks */,
 				8485654F2B9E90FD00F4BAE0 /* Database in Frameworks */,
-				5138E93A24D33E5600AFF0FE /* RSTree in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -1628,7 +1615,6 @@
 				17192ADA2567B3D500AAEACA /* RSSparkle in Frameworks */,
 				51A737C524DB19B50015FA66 /* RSWeb in Frameworks */,
 				8438C2DB2BABE0B00040C9EE /* CoreResources in Frameworks */,
-				514C16DE24D2EF15009A3AFA /* RSTree in Frameworks */,
 				5132775E2590FC640064F1E7 /* Articles in Frameworks */,
 				84DCA5252BABBB5A00792720 /* Core in Frameworks */,
 				8479ABE32B9E906E00F84C4D /* Database in Frameworks */,
@@ -1643,6 +1629,7 @@
 				84DCA5162BABB76B00792720 /* CloudKitExtras in Frameworks */,
 				514C16CE24D2E63F009A3AFA /* Account in Frameworks */,
 				519CA8E525841DB700EB079A /* CrashReporter in Frameworks */,
+				841CECD82BAD04B20001EE72 /* Tree in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -2375,6 +2362,7 @@
 				841550F52B9E4D6800D4B345 /* FMDB */,
 				84DCA50F2BABB65600792720 /* CloudKitExtras */,
 				84DCA5232BABBA8100792720 /* Core */,
+				841CECD62BAD03C60001EE72 /* Tree */,
 				84DCA5102BABB6A100792720 /* UIKitExtras */,
 				84DCA50E2BABB5D800792720 /* AppKitExtras */,
 				84DCA50D2BAB643700792720 /* FoundationExtras */,
@@ -2807,7 +2795,6 @@
 			name = "NetNewsWire iOS Intents Extension";
 			packageProductDependencies = (
 				51BC2F4A24D343A500E90810 /* Account */,
-				51BC2F4C24D343AB00E90810 /* RSTree */,
 			);
 			productName = "NetNewsWire iOS Intents Extension";
 			productReference = 51314637235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex */;
@@ -2830,7 +2817,6 @@
 			name = "NetNewsWire iOS Share Extension";
 			packageProductDependencies = (
 				51BC2F3724D3439A00E90810 /* Account */,
-				51BC2F4724D3439E00E90810 /* RSTree */,
 				84D9582B2BABE53B0053E7B2 /* FoundationExtras */,
 			);
 			productName = "NetNewsWire iOS Share Extension";
@@ -2920,13 +2906,13 @@
 				653813232680E1D6007A082C /* ArticlesDatabase */,
 				653813272680E1EC007A082C /* CrashReporter */,
 				6538132F2680E20C007A082C /* RSParser */,
-				653813322680E220007A082C /* RSTree */,
 				653813352680E224007A082C /* RSWeb */,
 				653813382680E22B007A082C /* Secrets */,
 				84DCA5172BABB77E00792720 /* FoundationExtras */,
 				84DCA5192BABB78700792720 /* AppKitExtras */,
 				84DCA51B2BABB78E00792720 /* CloudKitExtras */,
 				84DCA5262BABBB6200792720 /* Core */,
+				841CECD92BAD04B80001EE72 /* Tree */,
 			);
 			productName = NetNewsWire;
 			productReference = 65ED4083235DEF6C0081F399 /* NetNewsWire.app */;
@@ -2970,7 +2956,6 @@
 			name = "NetNewsWire-iOS";
 			packageProductDependencies = (
 				516B695E24D2F33B00B5702F /* Account */,
-				5138E93924D33E5600AFF0FE /* RSTree */,
 				5138E95124D3418100AFF0FE /* RSParser */,
 				5138E95724D3419000AFF0FE /* RSWeb */,
 				513F32702593EE6F0003048F /* Articles */,
@@ -2984,6 +2969,7 @@
 				84DCA51F2BABB7A200792720 /* UIKitExtras */,
 				84DCA5212BABB7A800792720 /* CloudKitExtras */,
 				84DCA5282BABBB6A00792720 /* Core */,
+				841CECDB2BAD04BF0001EE72 /* Tree */,
 			);
 			productName = "NetNewsWire-iOS";
 			productReference = 840D617C2029031C009BC708 /* NetNewsWire.app */;
@@ -3013,7 +2999,6 @@
 			name = NetNewsWire;
 			packageProductDependencies = (
 				514C16CD24D2E63F009A3AFA /* Account */,
-				514C16DD24D2EF15009A3AFA /* RSTree */,
 				51C4CFF524D37DD500AF9874 /* Secrets */,
 				51A737C424DB19B50015FA66 /* RSWeb */,
 				51A737C724DB19CC0015FA66 /* RSParser */,
@@ -3030,6 +3015,7 @@
 				84DCA5152BABB76B00792720 /* CloudKitExtras */,
 				84DCA5242BABBB5A00792720 /* Core */,
 				8438C2DA2BABE0B00040C9EE /* CoreResources */,
+				841CECD72BAD04B20001EE72 /* Tree */,
 			);
 			productName = NetNewsWire;
 			productReference = 849C64601ED37A5D003D8FC0 /* NetNewsWire.app */;
@@ -3143,7 +3129,6 @@
 			);
 			mainGroup = 849C64571ED37A5D003D8FC0;
 			packageReferences = (
-				510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */,
 				51383A3024D1F90E0027E272 /* XCRemoteSwiftPackageReference "RSWeb" */,
 				51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */,
 				17192AD82567B3D500AAEACA /* XCRemoteSwiftPackageReference "Sparkle-Binary" */,
@@ -4770,14 +4755,6 @@
 				revision = 059e7346082d02de16220cd79df7db18ddeba8c3;
 			};
 		};
-		510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */ = {
-			isa = XCRemoteSwiftPackageReference;
-			repositoryURL = "https://github.com/Ranchero-Software/RSTree.git";
-			requirement = {
-				kind = upToNextMajorVersion;
-				minimumVersion = 1.0.0;
-			};
-		};
 		51383A3024D1F90E0027E272 /* XCRemoteSwiftPackageReference "RSWeb" */ = {
 			isa = XCRemoteSwiftPackageReference;
 			repositoryURL = "https://github.com/Ranchero-Software/RSWeb.git";
@@ -4845,11 +4822,6 @@
 			isa = XCSwiftPackageProductDependency;
 			productName = SyncDatabase;
 		};
-		5138E93924D33E5600AFF0FE /* RSTree */ = {
-			isa = XCSwiftPackageProductDependency;
-			package = 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */;
-			productName = RSTree;
-		};
 		5138E95124D3418100AFF0FE /* RSParser */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = 51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */;
@@ -4880,11 +4852,6 @@
 			isa = XCSwiftPackageProductDependency;
 			productName = Account;
 		};
-		514C16DD24D2EF15009A3AFA /* RSTree */ = {
-			isa = XCSwiftPackageProductDependency;
-			package = 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */;
-			productName = RSTree;
-		};
 		516B695E24D2F33B00B5702F /* Account */ = {
 			isa = XCSwiftPackageProductDependency;
 			productName = Account;
@@ -4908,20 +4875,10 @@
 			isa = XCSwiftPackageProductDependency;
 			productName = Account;
 		};
-		51BC2F4724D3439E00E90810 /* RSTree */ = {
-			isa = XCSwiftPackageProductDependency;
-			package = 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */;
-			productName = RSTree;
-		};
 		51BC2F4A24D343A500E90810 /* Account */ = {
 			isa = XCSwiftPackageProductDependency;
 			productName = Account;
 		};
-		51BC2F4C24D343AB00E90810 /* RSTree */ = {
-			isa = XCSwiftPackageProductDependency;
-			package = 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */;
-			productName = RSTree;
-		};
 		51C4CFF524D37DD500AF9874 /* Secrets */ = {
 			isa = XCSwiftPackageProductDependency;
 			productName = Secrets;
@@ -4948,11 +4905,6 @@
 			package = 51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */;
 			productName = RSParser;
 		};
-		653813322680E220007A082C /* RSTree */ = {
-			isa = XCSwiftPackageProductDependency;
-			package = 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */;
-			productName = RSTree;
-		};
 		653813352680E224007A082C /* RSWeb */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = 51383A3024D1F90E0027E272 /* XCRemoteSwiftPackageReference "RSWeb" */;
@@ -4966,6 +4918,18 @@
 			isa = XCSwiftPackageProductDependency;
 			productName = Account;
 		};
+		841CECD72BAD04B20001EE72 /* Tree */ = {
+			isa = XCSwiftPackageProductDependency;
+			productName = Tree;
+		};
+		841CECD92BAD04B80001EE72 /* Tree */ = {
+			isa = XCSwiftPackageProductDependency;
+			productName = Tree;
+		};
+		841CECDB2BAD04BF0001EE72 /* Tree */ = {
+			isa = XCSwiftPackageProductDependency;
+			productName = Tree;
+		};
 		8438C2DA2BABE0B00040C9EE /* CoreResources */ = {
 			isa = XCSwiftPackageProductDependency;
 			productName = CoreResources;
diff --git a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 800ed313e..b1ab83243 100644
--- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -1,5 +1,5 @@
 {
-  "originHash" : "4d91e68a1cd512b0fa806978f5c3b759c8c6defc0a648d1e0fb0db9e944c07c1",
+  "originHash" : "143979811fdacc63ea101f03b6eeae2ff838828510390a5acdd415b90ffc549f",
   "pins" : [
     {
       "identity" : "plcrashreporter",
@@ -19,15 +19,6 @@
         "version" : "2.0.3"
       }
     },
-    {
-      "identity" : "rstree",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/Ranchero-Software/RSTree.git",
-      "state" : {
-        "revision" : "9d051f42cfc4faa991fd79cdb32e4cc8c545e334",
-        "version" : "1.0.0"
-      }
-    },
     {
       "identity" : "rsweb",
       "kind" : "remoteSourceControl",
diff --git a/Tree/.gitignore b/Tree/.gitignore
new file mode 100644
index 000000000..0023a5340
--- /dev/null
+++ b/Tree/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+/.build
+/Packages
+xcuserdata/
+DerivedData/
+.swiftpm/configuration/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
diff --git a/Tree/Package.swift b/Tree/Package.swift
new file mode 100644
index 000000000..f50a4d043
--- /dev/null
+++ b/Tree/Package.swift
@@ -0,0 +1,21 @@
+// swift-tools-version: 5.10
+
+import PackageDescription
+
+let package = Package(
+	name: "Tree",
+	products: [
+		.library(
+			name: "Tree",
+			targets: ["Tree"]),
+	],
+	targets: [
+		.target(
+			name: "Tree",
+			dependencies: [],
+			swiftSettings: [
+				.enableExperimentalFeature("StrictConcurrency")
+			]
+		)
+	]
+)
diff --git a/Tree/Sources/Tree/NSOutlineView+RSTree.swift b/Tree/Sources/Tree/NSOutlineView+RSTree.swift
new file mode 100644
index 000000000..bd26475c9
--- /dev/null
+++ b/Tree/Sources/Tree/NSOutlineView+RSTree.swift
@@ -0,0 +1,58 @@
+//
+//  NSOutlineView+RSTree.swift
+//  RSTree
+//
+//  Created by Brent Simmons on 9/5/16.
+//  Copyright © 2016 Ranchero Software, LLC. All rights reserved.
+//
+
+#if os(OSX)
+
+import AppKit
+
+public extension NSOutlineView {
+
+	@discardableResult
+	func revealAndSelectNodeAtPath(_ nodePath: NodePath) -> Bool {
+
+		// Returns true on success. Expands folders on the way. May succeed partially (returns false, in that case).
+
+		let numberOfNodes = nodePath.components.count
+		if numberOfNodes < 2 {
+			return false
+		}
+		
+		let indexOfNodeToSelect = numberOfNodes - 1
+
+		for i in 1...indexOfNodeToSelect { // Start at 1 to skip root node.
+
+			let oneNode = nodePath.components[i]
+			let oneRow = row(forItem: oneNode)
+			if oneRow < 0 {
+				return false
+			}
+
+			if i == indexOfNodeToSelect {
+				selectRowIndexes(NSIndexSet(index: oneRow) as IndexSet, byExtendingSelection: false)
+				scrollRowToVisible(oneRow)
+				return true
+			}
+			else {
+				expandItem(oneNode)
+			}
+		}
+
+		return false
+	}
+
+	@discardableResult
+	func revealAndSelectRepresentedObject(_ representedObject: AnyObject, _ treeController: TreeController) -> Bool {
+
+		guard let nodePath = NodePath(representedObject: representedObject, treeController: treeController) else {
+			return false
+		}
+		return revealAndSelectNodeAtPath(nodePath)
+	}
+}
+
+#endif
diff --git a/Tree/Sources/Tree/Node.swift b/Tree/Sources/Tree/Node.swift
new file mode 100644
index 000000000..3ff9408b0
--- /dev/null
+++ b/Tree/Sources/Tree/Node.swift
@@ -0,0 +1,224 @@
+//
+//  Node.swift
+//  NetNewsWire
+//
+//  Created by Brent Simmons on 7/21/15.
+//  Copyright © 2015 Ranchero Software, LLC. All rights reserved.
+//
+
+import Foundation
+
+// Main thread only.
+
+@MainActor public final class Node: Hashable {
+	
+	public weak var parent: Node?
+	public let representedObject: AnyObject
+	public var canHaveChildNodes = false
+	public var isGroupItem = false
+	public var childNodes = [Node]()
+	public let uniqueID: Int
+	private static var incrementingID = 0
+
+	public var isRoot: Bool {
+		if let _ = parent {
+			return false
+		}
+		return true
+	}
+	
+	public var numberOfChildNodes: Int {
+		return childNodes.count
+	}
+	
+	public var indexPath: IndexPath {
+		if let parent = parent {
+			let parentPath = parent.indexPath
+			if let childIndex = parent.indexOfChild(self) {
+				return parentPath.appending(childIndex)
+			}
+			preconditionFailure("A Node’s parent must contain it as a child.")
+		}
+		return IndexPath(index: 0) //root node
+	}
+	
+	public var level: Int {
+		if let parent = parent {
+			return parent.level + 1
+		}
+		return 0
+	}
+	
+	public var isLeaf: Bool {
+		return numberOfChildNodes < 1
+	}
+	
+	public init(representedObject: AnyObject, parent: Node?) {
+		
+		precondition(Thread.isMainThread)
+
+		self.representedObject = representedObject
+		self.parent = parent
+
+		self.uniqueID = Node.incrementingID
+		Node.incrementingID += 1
+	}
+	
+	public class func genericRootNode() -> Node {
+		
+		let node = Node(representedObject: TopLevelRepresentedObject(), parent: nil)
+		node.canHaveChildNodes = true
+		return node
+	}
+
+	public func existingOrNewChildNode(with representedObject: AnyObject) -> Node {
+
+		if let node = childNodeRepresentingObject(representedObject) {
+			return node
+		}
+		return createChildNode(representedObject)
+	}
+
+	public func createChildNode(_ representedObject: AnyObject) -> Node {
+
+		// Just creates — doesn’t add it.
+		return Node(representedObject: representedObject, parent: self)
+	}
+
+	public func childAtIndex(_ index: Int) -> Node? {
+		
+		if index >= childNodes.count || index < 0 {
+			return nil
+		}
+		return childNodes[index]
+	}
+
+	public func indexOfChild(_ node: Node) -> Int? {
+		
+		return childNodes.firstIndex{ (oneChildNode) -> Bool in
+			oneChildNode === node
+		}
+	}
+	
+	public func childNodeRepresentingObject(_ obj: AnyObject) -> Node? {
+		return findNodeRepresentingObject(obj, recursively: false)
+	}
+
+	public func descendantNodeRepresentingObject(_ obj: AnyObject) -> Node? {
+		return findNodeRepresentingObject(obj, recursively: true)
+	}
+
+	public func descendantNode(where test: (Node) -> Bool) -> Node? {
+		return findNode(where: test, recursively: true)
+	}
+
+	public func hasAncestor(in nodes: [Node]) -> Bool {
+
+		for node in nodes {
+			if node.isAncestor(of: self) {
+				return true
+			}
+		}
+		return false
+	}
+
+	public func isAncestor(of node: Node) -> Bool {
+
+		if node == self {
+			return false
+		}
+
+		var nomad = node
+		while true {
+			guard let parent = nomad.parent else {
+				return false
+			}
+			if parent == self {
+				return true
+			}
+			nomad = parent
+		}
+	}
+
+	public class func nodesOrganizedByParent(_ nodes: [Node]) -> [Node: [Node]] {
+
+		let nodesWithParents = nodes.filter { $0.parent != nil }
+		return Dictionary(grouping: nodesWithParents, by: { $0.parent! })
+	}
+
+	public class func indexSetsGroupedByParent(_ nodes: [Node]) -> [Node: IndexSet] {
+
+		let d = nodesOrganizedByParent(nodes)
+		let indexSetDictionary = d.mapValues { (nodes) -> IndexSet in
+
+			var indexSet = IndexSet()
+			if nodes.isEmpty {
+				return indexSet
+			}
+
+			let parent = nodes.first!.parent!
+			for node in nodes {
+				if let index = parent.indexOfChild(node) {
+					indexSet.insert(index)
+				}
+			}
+
+			return indexSet
+		}
+
+		return indexSetDictionary
+	}
+
+	// MARK: - Hashable
+
+	nonisolated public func hash(into hasher: inout Hasher) {
+		hasher.combine(uniqueID)
+	}
+
+	// MARK: - Equatable
+
+	nonisolated public class func ==(lhs: Node, rhs: Node) -> Bool {
+		return lhs === rhs
+	}
+}
+
+
+public extension Array where Element == Node {
+
+	@MainActor func representedObjects() -> [AnyObject] {
+
+		return self.map{ $0.representedObject }
+	}
+}
+
+private extension Node {
+
+	func findNodeRepresentingObject(_ obj: AnyObject, recursively: Bool = false) -> Node? {
+
+		for childNode in childNodes {
+			if childNode.representedObject === obj {
+				return childNode
+			}
+			if recursively, let foundNode = childNode.descendantNodeRepresentingObject(obj) {
+				return foundNode
+			}
+		}
+
+		return nil
+	}
+
+	func findNode(where test: (Node) -> Bool, recursively: Bool = false) -> Node? {
+
+		for childNode in childNodes {
+			if test(childNode) {
+				return childNode
+			}
+			if recursively, let foundNode = childNode.findNode(where: test, recursively: recursively) {
+				return foundNode
+			}
+		}
+
+		return nil
+	}
+
+}
diff --git a/Tree/Sources/Tree/NodePath.swift b/Tree/Sources/Tree/NodePath.swift
new file mode 100644
index 000000000..bb280ad03
--- /dev/null
+++ b/Tree/Sources/Tree/NodePath.swift
@@ -0,0 +1,42 @@
+//
+//  NodePath.swift
+//  RSTree
+//
+//  Created by Brent Simmons on 9/5/16.
+//  Copyright © 2016 Ranchero Software, LLC. All rights reserved.
+//
+
+import Foundation
+
+@MainActor public struct NodePath {
+
+	let components: [Node]
+
+	public init(node: Node) {
+
+		var tempArray = [node]
+
+		var nomad: Node = node
+		while true {
+			if let parent = nomad.parent {
+				tempArray.append(parent)
+				nomad = parent
+			}
+			else {
+				break
+			}
+		}
+
+		self.components = tempArray.reversed()
+	}
+
+	public init?(representedObject: AnyObject, treeController: TreeController) {
+
+		if let node = treeController.nodeInTreeRepresentingObject(representedObject) {
+			self.init(node: node)
+		}
+		else {
+			return nil
+		}
+	}
+}
diff --git a/Tree/Sources/Tree/RSTree.swift b/Tree/Sources/Tree/RSTree.swift
new file mode 100644
index 000000000..ac0aa000a
--- /dev/null
+++ b/Tree/Sources/Tree/RSTree.swift
@@ -0,0 +1,3 @@
+struct RSTree {
+    var text = "Hello, World!"
+}
diff --git a/Tree/Sources/Tree/TopLevelRepresentedObject.swift b/Tree/Sources/Tree/TopLevelRepresentedObject.swift
new file mode 100644
index 000000000..ec4619a7b
--- /dev/null
+++ b/Tree/Sources/Tree/TopLevelRepresentedObject.swift
@@ -0,0 +1,15 @@
+//
+//  TopLevelRepresentedObject.swift
+//  RSTree
+//
+//  Created by Brent Simmons on 8/10/16.
+//  Copyright © 2016 Ranchero Software, LLC. All rights reserved.
+//
+
+import Foundation
+
+// Handy to use as the represented object for a root node. Not required to use it, though.
+
+final class TopLevelRepresentedObject {
+
+}
diff --git a/Tree/Sources/Tree/TreeController.swift b/Tree/Sources/Tree/TreeController.swift
new file mode 100644
index 000000000..1d976cebb
--- /dev/null
+++ b/Tree/Sources/Tree/TreeController.swift
@@ -0,0 +1,135 @@
+//
+//  TreeController.swift
+//  NetNewsWire
+//
+//  Created by Brent Simmons on 5/29/16.
+//  Copyright © 2016 Ranchero Software, LLC. All rights reserved.
+//
+
+import Foundation
+
+public protocol TreeControllerDelegate: AnyObject {
+	
+	func treeController(treeController: TreeController, childNodesFor: Node) -> [Node]?
+}
+
+public typealias NodeVisitBlock = (_ : Node) -> Void
+
+@MainActor public final class TreeController {
+
+	private weak var delegate: TreeControllerDelegate?
+	public let rootNode: Node
+
+	public init(delegate: TreeControllerDelegate, rootNode: Node) {
+		
+		self.delegate = delegate
+		self.rootNode = rootNode
+		rebuild()
+	}
+
+	public convenience init(delegate: TreeControllerDelegate) {
+		
+		self.init(delegate: delegate, rootNode: Node.genericRootNode())
+	}
+	
+	@discardableResult
+	public func rebuild() -> Bool {
+
+		// Rebuild and re-sort. Return true if any changes in the entire tree.
+		
+		return rebuildChildNodes(node: rootNode)
+	}
+	
+	public func visitNodes(_ visitBlock: NodeVisitBlock) {
+		
+		visitNode(rootNode, visitBlock)
+	}
+	
+	public func nodeInArrayRepresentingObject(nodes: [Node], representedObject: AnyObject, recurse: Bool = false) -> Node? {
+		
+		for oneNode in nodes {
+
+			if oneNode.representedObject === representedObject {
+				return oneNode
+			}
+
+			if recurse, oneNode.canHaveChildNodes {
+				if let foundNode = nodeInArrayRepresentingObject(nodes: oneNode.childNodes, representedObject: representedObject, recurse: recurse) {
+					return foundNode
+				}
+
+			}
+		}
+		return nil
+	}
+
+	public func nodeInTreeRepresentingObject(_ representedObject: AnyObject) -> Node? {
+
+		return nodeInArrayRepresentingObject(nodes: [rootNode], representedObject: representedObject, recurse: true)
+	}
+
+	public func normalizedSelectedNodes(_ nodes: [Node]) -> [Node] {
+
+		// An array of nodes might include a leaf node and its parent. Remove the leaf node.
+
+		var normalizedNodes = [Node]()
+
+		for node in nodes {
+			if !node.hasAncestor(in: nodes) {
+				normalizedNodes += [node]
+			}
+		}
+
+		return normalizedNodes
+	}
+}
+
+private extension TreeController {
+	
+	func visitNode(_ node: Node, _ visitBlock: NodeVisitBlock) {
+		
+		visitBlock(node)
+		node.childNodes.forEach{ (oneChildNode) in
+			visitNode(oneChildNode, visitBlock)
+		}
+	}
+	
+	func nodeArraysAreEqual(_ nodeArray1: [Node]?, _ nodeArray2: [Node]?) -> Bool {
+		
+		if nodeArray1 == nil && nodeArray2 == nil {
+			return true
+		}
+		if nodeArray1 != nil && nodeArray2 == nil {
+			return false
+		}
+		if nodeArray1 == nil && nodeArray2 != nil {
+			return false
+		}
+		
+		return nodeArray1! == nodeArray2!
+	}
+	
+	func rebuildChildNodes(node: Node) -> Bool {
+		
+		if !node.canHaveChildNodes {
+			return false
+		}
+		
+		var childNodesDidChange = false
+		
+		let childNodes = delegate?.treeController(treeController: self, childNodesFor: node) ?? [Node]()
+		
+		childNodesDidChange = !nodeArraysAreEqual(childNodes, node.childNodes)
+		if (childNodesDidChange) {
+			node.childNodes = childNodes
+		}
+		
+		childNodes.forEach{ (oneChildNode) in
+			if rebuildChildNodes(node: oneChildNode) {
+				childNodesDidChange = true
+			}
+		}
+
+		return childNodesDidChange
+	}
+}