feat(claude-do): merge feat(ui): Hauptfenster-Close mit offenen Mission-Control-Ses

ClaudeDo-Task: a963f289b38449a3a8d0c52fc9bfd9e7
This commit is contained in:
mika kuns
2026-08-21 18:20:44 +02:00
5 changed files with 65 additions and 0 deletions
@@ -714,6 +714,7 @@
"vm": {
"connection": { "online": "Online", "connecting": "Verbinden…", "offline": "Offline" },
"shell": { "restartingWorker": "Worker wird neu gestartet…", "unexpectedError": "Unerwarteter Fehler: {0}" },
"mainWindow": { "closeConfirm": { "message": "{0} Mission-Control-Sitzung(en) laufen noch. Beim Beenden werden sie abgebrochen. Trotzdem schließen?" } },
"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" },
"failureReason": { "maxTurns": "Turn-Limit erreicht", "timeout": "Zeitüberschreitung", "error": "Fehler", "cancelled": "Abgebrochen", "unknown": "Grund unbekannt" },
@@ -714,6 +714,7 @@
"vm": {
"connection": { "online": "Online", "connecting": "Connecting…", "offline": "Offline" },
"shell": { "restartingWorker": "Restarting worker…", "unexpectedError": "Unexpected error: {0}" },
"mainWindow": { "closeConfirm": { "message": "{0} Mission Control session(s) are still running. Closing the app will end them. Close anyway?" } },
"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" },
"failureReason": { "maxTurns": "Turn limit reached", "timeout": "Timed out", "error": "Error", "cancelled": "Cancelled", "unknown": "Unknown reason" },
@@ -520,6 +520,16 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
Dialogs.ShowMissionControl(MissionControl);
}
/// <summary>Number of Mission Control ConPTY panes currently open — the main window's
/// close guard asks for confirmation whenever this is greater than zero.</summary>
public int OpenMissionControlSessionCount => MissionControl?.ConPtySessions.Count ?? 0;
// Pure decision extracted for testability — closing the main window kills every open
// Mission Control pane's `claude` process (ConPtyPaneViewModel.Dispose), so ask first
// rather than let it happen silently.
internal static bool RequiresCloseConfirmation(int openMissionControlSessionCount) =>
openMissionControlSessionCount > 0;
private void SyncInteractiveSessionChips()
{
if (MissionControl is null || Tasks is null) return;
+28
View File
@@ -1,9 +1,12 @@
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels;
using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.ViewModels.Modals;
@@ -13,6 +16,8 @@ namespace ClaudeDo.Ui.Views;
public partial class MainWindow : Window
{
private bool _forceClose;
public MainWindow()
{
InitializeComponent();
@@ -21,6 +26,29 @@ public partial class MainWindow : Window
UpdateMaxIcon();
}
// Closing the main window kills every open Mission Control ConPTY session without
// warning (App.axaml.cs: ShutdownMode.OnMainWindowClose). Ask first — never silently.
protected override void OnClosing(WindowClosingEventArgs e)
{
if (!_forceClose
&& DataContext is IslandsShellViewModel { Dialogs: { } dialogs } vm
&& IslandsShellViewModel.RequiresCloseConfirmation(vm.OpenMissionControlSessionCount))
{
e.Cancel = true;
_ = ConfirmCloseWithOpenSessionsAsync(dialogs, vm.OpenMissionControlSessionCount);
return;
}
base.OnClosing(e);
}
private async Task ConfirmCloseWithOpenSessionsAsync(IDialogService dialogs, int openSessionCount)
{
var confirmed = await dialogs.ConfirmAsync(Loc.T("vm.mainWindow.closeConfirm.message", openSessionCount));
if (!confirmed) return;
_forceClose = true;
Close();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
@@ -0,0 +1,25 @@
using ClaudeDo.Ui.ViewModels;
using Xunit;
namespace ClaudeDo.Ui.Tests;
public class MainWindowCloseConfirmDecisionTests
{
[Fact]
public void True_when_at_least_one_mission_control_session_is_open()
{
Assert.True(IslandsShellViewModel.RequiresCloseConfirmation(1));
}
[Fact]
public void True_when_multiple_mission_control_sessions_are_open()
{
Assert.True(IslandsShellViewModel.RequiresCloseConfirmation(3));
}
[Fact]
public void False_when_no_mission_control_sessions_are_open()
{
Assert.False(IslandsShellViewModel.RequiresCloseConfirmation(0));
}
}