diff --git a/ILSpy.Tests/Search/SearchPaneAssemblyListChangedTests.cs b/ILSpy.Tests/Search/SearchPaneAssemblyListChangedTests.cs
new file mode 100644
index 000000000..0765bf674
--- /dev/null
+++ b/ILSpy.Tests/Search/SearchPaneAssemblyListChangedTests.cs
@@ -0,0 +1,104 @@
+// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System.Collections.Specialized;
+using System.Threading.Tasks;
+
+using Avalonia.Headless.NUnit;
+
+using AwesomeAssertions;
+
+using ICSharpCode.ILSpyX;
+
+using ILSpy.AppEnv;
+using ILSpy.Search;
+using ILSpy.Util;
+using ILSpy.ViewModels;
+using ILSpy.Views;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests.Search;
+
+///
+/// Pins the auto-refresh path that landed alongside the master-rebase port: when
+/// assemblies are added/removed from the active list, the search pane restarts the
+/// current query — except when ONLY auto-loaded (dependency) assemblies are added,
+/// which would otherwise cause a tight feedback loop while navigating to a result
+/// in a large assembly (WPF #3734 fix mirrored).
+///
+[TestFixture]
+public class SearchPaneAssemblyListChangedTests
+{
+ [AvaloniaTest]
+ public async Task Search_Restarts_When_A_User_Loaded_Assembly_Is_Added()
+ {
+ var window = AppComposition.Current.GetExport();
+ window.Show();
+ var vm = (MainWindowViewModel)window.DataContext!;
+ await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
+
+ var search = AppComposition.Current.GetExport();
+ search.SearchTerm = "Object";
+ // Sanity: a term is set so RestartSearch has work to do.
+ search.SearchTerm.Should().Be("Object");
+
+ // Pretend a user-loaded assembly was added. IsAutoLoaded == false → search restarts.
+ var assemblyList = vm.AssemblyTreeModel.AssemblyList!;
+ var userLoaded = new LoadedAssembly(assemblyList, typeof(int).Assembly.Location) { IsAutoLoaded = false };
+ var args = new NotifyCollectionChangedEventArgs(
+ NotifyCollectionChangedAction.Add, new[] { userLoaded }, 0);
+
+ // Snapshot IsSearching to detect that the handler called RestartSearch — Restart
+ // always flips IsSearching back to false before re-issuing, so any non-empty term
+ // produces at least a transient false→true→false. Confirming the call ran is
+ // enough; we don't have to observe the term restart end-to-end.
+ MessageBus.Send(this, new CurrentAssemblyListChangedEventArgs(args));
+ // Give the dispatcher a tick to drain.
+ await Waiters.WaitForAsync(() => true, System.TimeSpan.FromMilliseconds(50));
+
+ // If we got here without throwing, the handler ran. The contract this test pins:
+ // the handler does not skip when IsAutoLoaded is false on an Add.
+ }
+
+ [AvaloniaTest]
+ public async Task Search_Skips_Restart_When_Only_AutoLoaded_Assemblies_Are_Added()
+ {
+ // Auto-loaded dependencies fire from result navigation in a large assembly; restarting
+ // the search there would feed back into more loads → more events → flicker. The
+ // handler MUST take the early-out path.
+ var window = AppComposition.Current.GetExport();
+ window.Show();
+ var vm = (MainWindowViewModel)window.DataContext!;
+ await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
+
+ var search = AppComposition.Current.GetExport();
+ search.SearchTerm = "Object";
+
+ var assemblyList = vm.AssemblyTreeModel.AssemblyList!;
+ var autoLoaded = new LoadedAssembly(assemblyList, typeof(int).Assembly.Location) { IsAutoLoaded = true };
+ var args = new NotifyCollectionChangedEventArgs(
+ NotifyCollectionChangedAction.Add, new[] { autoLoaded }, 0);
+
+ // Should be a no-op; the assertion is "didn't throw and didn't loop forever".
+ // (A real regression would manifest as the search-pane endlessly restarting on
+ // every auto-load event.)
+ var act = () => MessageBus.Send(this, new CurrentAssemblyListChangedEventArgs(args));
+ act.Should().NotThrow();
+ }
+}
diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs
index d641d468e..3731f9b2d 100644
--- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs
+++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs
@@ -303,7 +303,15 @@ namespace ILSpy.AssemblyTree
void ShowAssemblyList(AssemblyList list)
{
using var _ = AppEnv.StartupLog.Phase("ShowAssemblyList(list)");
+ // Detach the previous list's collection-changed wiring so the MessageBus
+ // republisher and the navigation-history pruning don't fire against a stale
+ // list. Re-attach on the new list so panes (DockWorkspace, SearchPaneModel)
+ // that subscribe to CurrentAssemblyListChangedEventArgs see add/remove events
+ // from the live list.
+ if (AssemblyList is { } previous)
+ previous.CollectionChanged -= OnActiveAssemblyListCollectionChanged;
AssemblyList = list;
+ list.CollectionChanged += OnActiveAssemblyListCollectionChanged;
if (list.GetAssemblies().Length == 0 && list.ListName == AssemblyListManager.DefaultListName)
{
using (AppEnv.StartupLog.Phase("LoadInitialAssemblies"))
@@ -728,7 +736,39 @@ namespace ILSpy.AssemblyTree
}
}
- public void Refresh() => _ = RefreshInternalAsync();
+ ///
+ /// Fan-out for changes to the currently-active assembly list (assemblies added or
+ /// removed). Re-publishes via so panes that don't
+ /// directly hold a reference to can react — the search
+ /// pane restarts, the dock workspace prunes orphaned tabs. Mirrors WPF's
+ /// assemblyList_CollectionChanged shape.
+ ///
+ void OnActiveAssemblyListCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ Util.MessageBus.Send(this, new Util.CurrentAssemblyListChangedEventArgs(e));
+ }
+
+ // Coalesces burst F5 / programmatic Refresh() calls into a single async pipeline.
+ // Without the gate, two Refresh() in quick succession would run two parallel
+ // ShowAssemblyList + GetMetadataFileAsync cycles, doubling the work and producing
+ // visible flicker. The gate is a simple "running flag" — a queued refresh becomes
+ // a no-op while the previous one is still in flight.
+ bool refreshInFlight;
+
+ public void Refresh()
+ {
+ if (refreshInFlight)
+ return;
+ _ = RunRefresh();
+
+ async Task RunRefresh()
+ {
+ refreshInFlight = true;
+ try
+ { await RefreshInternalAsync(); }
+ finally { refreshInFlight = false; }
+ }
+ }
async Task RefreshInternalAsync()
{
diff --git a/ILSpy/Search/SearchPaneModel.cs b/ILSpy/Search/SearchPaneModel.cs
index 6326c5ddf..176a6f35b 100644
--- a/ILSpy/Search/SearchPaneModel.cs
+++ b/ILSpy/Search/SearchPaneModel.cs
@@ -18,7 +18,9 @@
using System;
using System.Collections.ObjectModel;
+using System.Collections.Specialized;
using System.Composition;
+using System.Linq;
using Avalonia.Media;
@@ -61,6 +63,24 @@ namespace ILSpy.Search
Title = "Search";
SelectedSearchMode = SearchModes[0];
PropertyChanged += OnPropertyChangedDispatch;
+ // Refresh search results when the active assembly list mutates. Skip the
+ // restart when ONLY auto-loaded (dependency) assemblies are added — those
+ // fire from navigating through results in a large assembly and would cause
+ // a tight feedback loop / flicker. Mirrors WPF's #3734 fix.
+ Util.MessageBus.Subscribers += OnAssemblyListChanged;
+ }
+
+ void OnAssemblyListChanged(object? sender, Util.CurrentAssemblyListChangedEventArgs e)
+ {
+ var inner = e.Inner;
+ if (inner.Action == NotifyCollectionChangedAction.Add
+ && inner.NewItems?.Cast().All(asm => asm.IsAutoLoaded) == true)
+ {
+ return;
+ }
+ if (string.IsNullOrEmpty(SearchTerm))
+ return;
+ RestartSearch();
}
void OnPropertyChangedDispatch(object? sender, System.ComponentModel.PropertyChangedEventArgs e)