diff --git a/ViewModels/MainViewModel.Recording.cs b/ViewModels/MainViewModel.Recording.cs
index e0f8385..1b77188 100644
--- a/ViewModels/MainViewModel.Recording.cs
+++ b/ViewModels/MainViewModel.Recording.cs
@@ -77,11 +77,21 @@ public partial class MainViewModel : ViewModelBase
// CREATOR RULING 2026-09-26: Cancel means DISCARD, not "save under the
// default name". This used to fall through and File.Move anyway, so a
// cancelled dialog silently kept the recording — Cancel was a lie.
- var dialog = new RenameRecordingDialog(autoStem);
- var creatorSaved = dialog.ShowDialog() == true;
+ //
+ // APP CLOSE (2026-11-13): a modal cannot open while the window is closing — WPF throws
+ // "Cannot call ShowDialog while a Window is closing", which killed the app mid-shutdown.
+ // So closing saves under the automatic name instead: never discard footage the creator
+ // did not choose to discard, and never hold the window hostage to a dialog.
+ var creatorSaved = true;
+ string? chosenName = null;
+ if (!_shuttingDown)
+ {
+ var dialog = new RenameRecordingDialog(autoStem);
+ creatorSaved = dialog.ShowDialog() == true;
+ chosenName = dialog.ResultFileName;
+ }
- var result = CompleteRecordingSave(
- startPath, dir, autoStem, dialog.ResultFileName, creatorSaved);
+ var result = CompleteRecordingSave(startPath, dir, autoStem, chosenName, creatorSaved);
switch (result.Outcome)
{
case RecordingSaveOutcome.Saved:
diff --git a/ViewModels/MainViewModel.Streaming.Operations.cs b/ViewModels/MainViewModel.Streaming.Operations.cs
index e33b5df..2080b33 100644
--- a/ViewModels/MainViewModel.Streaming.Operations.cs
+++ b/ViewModels/MainViewModel.Streaming.Operations.cs
@@ -690,7 +690,11 @@ public partial class MainViewModel : ViewModelBase
{
var title = BroadcastForm.Title;
var (outcome, error) = await ClosePrivateBroadcastAsync(_youtubeStream, broadcastId,
- () => KeepBroadcastPromptOverride?.Invoke(title) ?? new KeepBroadcastDialog(title).ShowDialog() == true);
+ // Closing the app cannot open a modal (WPF forbids ShowDialog while a window closes), so the
+ // safe default is KEEP: the broadcast stays on YouTube as a private video for the creator to
+ // deal with, instead of being deleted by a prompt nobody could answer.
+ () => _shuttingDown
+ || (KeepBroadcastPromptOverride?.Invoke(title) ?? new KeepBroadcastDialog(title).ShowDialog() == true));
switch (outcome)
{
case PrivateBroadcastOutcome.Kept:
diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs
index 9dc8da6..f53d538 100644
--- a/ViewModels/MainViewModel.cs
+++ b/ViewModels/MainViewModel.cs
@@ -624,10 +624,16 @@ public partial class MainViewModel : ViewModelBase
/// while it was still working. The window now holds itself open until the ordinary stop completes
/// (bounded — a stuck encoder must not trap the creator in an unclosable app).
///
+ /// True while the app is closing. Dialogs cannot open then, so the stop path skips its
+ /// prompts and takes the safe default (save under the automatic name, keep the broadcast).
+ internal bool IsShuttingDown => _shuttingDown;
+ private bool _shuttingDown;
+
internal async Task StopBeforeShutdownAsync()
{
if (!IsLive && !IsRecording) return true;
+ _shuttingDown = true;
StopStream();
for (var i = 0; i < 120 && (IsLive || IsRecording); i++) // up to ~6 s
await Task.Delay(50).ConfigureAwait(true);