From 5bf73e0aa77f3ab92b1cc6bfa16533330026f75d Mon Sep 17 00:00:00 2001 From: Allan Thraen Date: Fri, 15 May 2026 10:48:56 +0200 Subject: [PATCH] fix(shutdown): defer reclose via Dispatcher so OnClosing yields first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OnClosing is async void using a cancel-then-reclose pattern: first entry sets e.Cancel=true, runs async cleanup, then calls Close() to re-enter through the _shutdownComplete branch. The cancel only takes effect once OnClosing yields control back to WPF — that's when WPF reads e.Cancel and resets its internal _isClosing flag. In --clean mode none of the awaits actually yield: SaveStateAsync short-circuits (if App.CleanStart return), the foreach over _vm.Sessions is empty because clean mode clears the session list, and the remaining work is synchronous. So OnClosing runs straight through to the final Close() with WPF still in the closing state, and Close() throws InvalidOperationException: "Cannot set Visibility to Visible or call Show, ShowDialog, Close, or WindowInteropHelper.EnsureHandle while a Window is closing." Queue the reclose via Dispatcher.BeginInvoke so OnClosing returns first, WPF resets _isClosing, and the queued Close() then re-enters cleanly. Works for both the no-yield clean case and the normal case with live sessions. Co-Authored-By: Claude Opus 4.7 --- src/CodeShellManager/MainWindow.xaml.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/CodeShellManager/MainWindow.xaml.cs b/src/CodeShellManager/MainWindow.xaml.cs index 751c315..610eef2 100644 --- a/src/CodeShellManager/MainWindow.xaml.cs +++ b/src/CodeShellManager/MainWindow.xaml.cs @@ -4767,7 +4767,15 @@ protected override async void OnClosing(System.ComponentModel.CancelEventArgs e) App.TrayIcon?.Dispose(); _shutdownComplete = true; - Close(); + // Queue Close() on the dispatcher rather than calling it inline. If none of + // the awaits above actually yielded (e.g. --clean mode with no sessions — + // SaveStateAsync short-circuits, and the foreach loops over an empty list + // never await), control never returns to WPF between e.Cancel=true and this + // point, so WPF's internal _isClosing flag is still set and Close() throws + // "Cannot ... call Close ... while a Window is closing." Posting via + // BeginInvoke lets OnClosing return first, WPF resets _isClosing, then the + // queued Close() re-enters cleanly through the _shutdownComplete branch. + _ = Dispatcher.BeginInvoke(new System.Action(Close)); } ///