feat(claude-do): merge feat(ui): Rückfrage bevor das X eine laufende ConPTY-Session

ClaudeDo-Task: 50c0d3ff-ad95-4612-8b16-344ac1c24f41
This commit is contained in:
mika kuns
2026-09-10 13:54:26 +02:00
4 changed files with 76 additions and 3 deletions
@@ -380,6 +380,7 @@
"focusMode": "Fokus",
"overviewMode": "Übersicht",
"closeSession": "Sitzung beenden",
"closeSessionConfirm": "Laufende Sitzung beenden? Der Claude-Prozess wird abgebrochen.",
"conptyLaunchFailed": "Interaktive Sitzung konnte nicht geöffnet werden: {0}",
"conptyStarting": "Sitzung wird gestartet…",
"conptyExited": "Sitzung beendet (Code {0}).",
@@ -380,6 +380,7 @@
"focusMode": "Focus",
"overviewMode": "Overview",
"closeSession": "Kill session",
"closeSessionConfirm": "End the running session? The Claude process will be aborted.",
"conptyLaunchFailed": "Couldn't open interactive session: {0}",
"conptyStarting": "Starting session…",
"conptyExited": "Session ended (code {0}).",
@@ -449,7 +449,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
private void AddConPtyPane(ConPtyPaneViewModel pane)
{
pane.ErrorReported += OnConPtyPaneError;
pane.CloseRequested += CloseConPtySession;
pane.CloseRequested += OnPaneCloseRequested;
pane.SubmitForReviewRequested += OnPaneSubmitForReview;
pane.SessionDiedAfterStartup += OnPaneSessionDied;
pane.AbandonRunRequested += OnPaneAbandonRun;
@@ -543,11 +543,27 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
}
}
// The tile's own X: a reflex click must not silently kill a live session the way it would a
// regular window. Confirm only when the terminal is actually running -- a dead one (StartError,
// or HasExited i.e. the #290 exited banner) is pure cleanup and closes straight away, same as it
// always did. Every other close path (handoff replacing a pane, submit-for-review, abandon-run)
// still calls CloseConPtySession directly and is unaffected.
private async void OnPaneCloseRequested(ConPtyPaneViewModel pane)
{
if (pane.Terminal.IsRunning)
{
if (ConfirmAsync is null) return;
if (!await ConfirmAsync(Loc.T("missionControl.closeSessionConfirm"))) return;
}
CloseConPtySession(pane);
}
private void CloseConPtySession(ConPtyPaneViewModel pane)
{
if (!ConPtySessions.Contains(pane)) return;
pane.ErrorReported -= OnConPtyPaneError;
pane.CloseRequested -= CloseConPtySession;
pane.CloseRequested -= OnPaneCloseRequested;
pane.SubmitForReviewRequested -= OnPaneSubmitForReview;
pane.SessionDiedAfterStartup -= OnPaneSessionDied;
pane.AbandonRunRequested -= OnPaneAbandonRun;
@@ -601,7 +617,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
foreach (var c in ConPtySessions.ToList())
{
c.ErrorReported -= OnConPtyPaneError;
c.CloseRequested -= CloseConPtySession;
c.CloseRequested -= OnPaneCloseRequested;
c.SubmitForReviewRequested -= OnPaneSubmitForReview;
c.SessionDiedAfterStartup -= OnPaneSessionDied;
c.AbandonRunRequested -= OnPaneAbandonRun;
@@ -1072,4 +1072,59 @@ public class MissionControlViewModelTests : IDisposable
Assert.Equal(new[] { "t1", "t2" }, worker.Cancelled);
Assert.Empty(vm.ConPtySessions);
}
// The tile's own X must not silently kill a live session -- a reflex click on a running pane
// has to go through the same confirm mechanism as Abandon Run.
[Fact]
public async Task Close_RunningSession_ConfirmDeclined_LeavesSessionOpen()
{
var worker = new FakeWorker();
using var vm = BuildVm(worker);
await vm.OpenConPtySessionAsync("t1");
var pane = vm.ConPtySessions[0];
pane.Terminal.IsRunning = true;
string? confirmMessage = null;
vm.ConfirmAsync = msg => { confirmMessage = msg; return Task.FromResult(false); };
pane.CloseCommand.Execute(null);
await Task.Delay(20);
Assert.NotNull(confirmMessage);
Assert.Single(vm.ConPtySessions);
}
[Fact]
public async Task Close_RunningSession_ConfirmAccepted_ClosesPane()
{
var worker = new FakeWorker();
using var vm = BuildVm(worker);
await vm.OpenConPtySessionAsync("t1");
var pane = vm.ConPtySessions[0];
pane.Terminal.IsRunning = true;
vm.ConfirmAsync = _ => Task.FromResult(true);
pane.CloseCommand.Execute(null);
await Task.Delay(20);
Assert.Empty(vm.ConPtySessions);
}
// A dead session (start failure or exited-after-startup) is pure cleanup -- no confirm needed.
[Fact]
public async Task Close_DeadSession_ClosesWithoutConfirming()
{
var worker = new FakeWorker();
using var vm = BuildVm(worker);
await vm.OpenConPtySessionAsync("t1");
var pane = vm.ConPtySessions[0];
pane.Terminal.ReportStartFailure("boom");
var confirmCalled = false;
vm.ConfirmAsync = _ => { confirmCalled = true; return Task.FromResult(true); };
pane.CloseCommand.Execute(null);
await Task.Delay(20);
Assert.False(confirmCalled);
Assert.Empty(vm.ConPtySessions);
}
}