From 17e6cd6118aa5d0a0c886d83ff05c9154d54ffb5 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 6 Aug 2026 15:54:17 +0200 Subject: [PATCH] fix(app): stop a dispatcher exception from killing the whole app Ctrl+C in a Mission Control terminal tile is bound by the terminal library to CopyAsync, which throws IndexOutOfRangeException out of XTerm's selection buffer on some selections. It runs from an async void key handler, so the exception reached the dispatcher unhandled and terminated the process -- every open ConPTY session with it. Handle it instead and surface the message in the footer error strip. Iciclecreek.Avalonia.Terminal 2.0.3 is the newest release, so there is no upstream fix to take. --- src/ClaudeDo.App/App.axaml.cs | 17 ++++++++++++++++- src/ClaudeDo.Localization/locales/de.json | 2 +- src/ClaudeDo.Localization/locales/en.json | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/ClaudeDo.App/App.axaml.cs b/src/ClaudeDo.App/App.axaml.cs index 0aaead40..5d7c6733 100644 --- a/src/ClaudeDo.App/App.axaml.cs +++ b/src/ClaudeDo.App/App.axaml.cs @@ -3,7 +3,9 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using Avalonia.Threading; using ClaudeDo.Ui; +using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels; using ClaudeDo.Ui.Views; @@ -40,9 +42,22 @@ public partial class App : Application // modeless Mission Control window is still open. desktop.ShutdownMode = ShutdownMode.OnMainWindowClose; + var shell = services.GetRequiredService(); + + // Last-resort backstop: an exception escaping a dispatcher job kills the process, and + // ClaudeDo hosts third-party UI (the ConPTY terminal control) whose async void key and + // render handlers have done exactly that — one bad keystroke took the whole app down, + // losing every open session. Swallowing is the lesser evil here: the failing job is + // already dead either way, and the user still gets told via the footer error strip. + Dispatcher.UIThread.UnhandledException += (_, e) => + { + e.Handled = true; + shell.FlashFooterError(Loc.T("vm.shell.unexpectedError", e.Exception.Message)); + }; + desktop.MainWindow = new MainWindow { - DataContext = services.GetRequiredService(), + DataContext = shell, }; // Kick off the SignalR retry loop — reconnects indefinitely if the worker diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index e025438e..8921d76e 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -642,7 +642,7 @@ }, "vm": { "connection": { "online": "Online", "connecting": "Verbinden…", "offline": "Offline" }, - "shell": { "restartingWorker": "Worker wird neu gestartet…" }, + "shell": { "restartingWorker": "Worker wird neu gestartet…", "unexpectedError": "Unerwarteter Fehler: {0}" }, "agentStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "review": "Prüfung", "children": "Wartet auf Teilaufgaben", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" }, "taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "waitingForChildren": "Wartet auf Teilaufgaben", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen", "parked": "Geparkt", "interactive": "Interaktiv" }, "planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" }, diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index e8116ff6..372d2d9a 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -642,7 +642,7 @@ }, "vm": { "connection": { "online": "Online", "connecting": "Connecting…", "offline": "Offline" }, - "shell": { "restartingWorker": "Restarting worker…" }, + "shell": { "restartingWorker": "Restarting worker…", "unexpectedError": "Unexpected error: {0}" }, "agentStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "review": "Review", "children": "Waiting for Subtasks", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" }, "taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "waitingForChildren": "Waiting for Subtasks", "done": "Done", "failed": "Failed", "cancelled": "Cancelled", "parked": "Parked", "interactive": "Interactive" }, "planningBadge": { "active": "PLANNING", "finalized": "PLANNED" },