Summary
Several UI command paths let exceptions escape unobserved or crash the app, deviating from the pattern the rest of the codebase follows (ClipboardViewModel, RecipeFileViewModel, MainWindowViewModel all subscribe ThrownExceptions).
1. Keyboard shortcuts crash the app on any command exception
MainWindow.axaml.cs:87-105 — all four OnKeyDown paths use Execute().Subscribe() with no onError. A ReactiveCommand execution exception errors the Execute() observable, and the parameterless Subscribe rethrows it unhandled on the UI thread — even for commands whose ThrownExceptions are otherwise handled.
2. RecipeCommandsViewModel subscribes ThrownExceptions nowhere
RecipeGrid/RecipeCommandsViewModel.cs:38-46 — AddStepCommand/DeleteStepCommand/UndoCommand/RedoCommand have no ThrownExceptions handler, and AddStep reaches RecipeCoordinator.GetDefaultActionId() (RecipeCoordinator.cs:168-172) which throws InvalidOperationException. Combined with finding 1: a hotkey → unhandled crash.
Also (RecipeCommandsViewModel.cs:70-100): AddStep/DeleteStep check result.IsSuccess only to drive selection; a failed Result is dropped without a message-panel report, unlike every other VM.
3. Fire-and-forget conflict dialog swallows exceptions
MainWindow/MainWindowViewModel.cs:83 — _ = HandleConflictAsync(conflict.Local, conflict.Plc) discards the task. Inside, only ShowDialog is guarded (212-222); an exception from _coordinator.ResolveConflict or MessagePanel.ReportFailure faults the discarded task and vanishes as an unobserved exception. The codebase already has the right pattern: PlcLifecycleManager.cs:279-281 uses .ContinueWith(t => log, OnlyOnFaulted).
4. Unguarded async void around dialogs
StyleEditor/GridStyleEditorWindow.axaml.cs:30-40 — OnSaveCompleted is async void invoked from an Rx Subscribe callback (not an event handler); any exception from ShowDialog/Close crashes the process. MainWindow.axaml.cs:206-243 — OnWindowClosing is a legitimate async void event handler but has no try/catch around its awaits.
Suggested fix
- Add
ThrownExceptions subscriptions in RecipeCommandsViewModel reporting to the message panel; report failed Results there too.
- Pass an
onError at the hotkey call sites (or a small SafeExecute extension used everywhere a command is invoked imperatively).
- Wrap
HandleConflictAsync body in try/catch-log, or use the ContinueWith(OnlyOnFaulted) pattern.
- Pump
OnSaveCompleted through the Rx pipeline (SelectMany) or add try/catch; same for OnWindowClosing.
Summary
Several UI command paths let exceptions escape unobserved or crash the app, deviating from the pattern the rest of the codebase follows (
ClipboardViewModel,RecipeFileViewModel,MainWindowViewModelall subscribeThrownExceptions).1. Keyboard shortcuts crash the app on any command exception
MainWindow.axaml.cs:87-105— all fourOnKeyDownpaths useExecute().Subscribe()with noonError. A ReactiveCommand execution exception errors theExecute()observable, and the parameterlessSubscriberethrows it unhandled on the UI thread — even for commands whoseThrownExceptionsare otherwise handled.2.
RecipeCommandsViewModelsubscribesThrownExceptionsnowhereRecipeGrid/RecipeCommandsViewModel.cs:38-46—AddStepCommand/DeleteStepCommand/UndoCommand/RedoCommandhave noThrownExceptionshandler, andAddStepreachesRecipeCoordinator.GetDefaultActionId()(RecipeCoordinator.cs:168-172) which throwsInvalidOperationException. Combined with finding 1: a hotkey → unhandled crash.Also (
RecipeCommandsViewModel.cs:70-100):AddStep/DeleteStepcheckresult.IsSuccessonly to drive selection; a failedResultis dropped without a message-panel report, unlike every other VM.3. Fire-and-forget conflict dialog swallows exceptions
MainWindow/MainWindowViewModel.cs:83—_ = HandleConflictAsync(conflict.Local, conflict.Plc)discards the task. Inside, onlyShowDialogis guarded (212-222); an exception from_coordinator.ResolveConflictorMessagePanel.ReportFailurefaults the discarded task and vanishes as an unobserved exception. The codebase already has the right pattern:PlcLifecycleManager.cs:279-281uses.ContinueWith(t => log, OnlyOnFaulted).4. Unguarded
async voidaround dialogsStyleEditor/GridStyleEditorWindow.axaml.cs:30-40—OnSaveCompletedisasync voidinvoked from an RxSubscribecallback (not an event handler); any exception fromShowDialog/Closecrashes the process.MainWindow.axaml.cs:206-243—OnWindowClosingis a legitimateasync voidevent handler but has no try/catch around its awaits.Suggested fix
ThrownExceptionssubscriptions inRecipeCommandsViewModelreporting to the message panel; report failedResults there too.onErrorat the hotkey call sites (or a smallSafeExecuteextension used everywhere a command is invoked imperatively).HandleConflictAsyncbody in try/catch-log, or use theContinueWith(OnlyOnFaulted)pattern.OnSaveCompletedthrough the Rx pipeline (SelectMany) or add try/catch; same forOnWindowClosing.