feat(claude-do): merge feat(ui): Worker-Menü "Shutdown" beendet nach Bestätigung Wo
ClaudeDo-Task: f97295c1e25a44db9db23df07a38062a
This commit is contained in:
@@ -785,6 +785,7 @@
|
|||||||
"checkForUpdates": "Nach Updates suchen",
|
"checkForUpdates": "Nach Updates suchen",
|
||||||
"restartWorker": "Worker neu starten",
|
"restartWorker": "Worker neu starten",
|
||||||
"updateClaudeCli": "Claude CLI aktualisieren",
|
"updateClaudeCli": "Claude CLI aktualisieren",
|
||||||
|
"shutdown": "Beenden",
|
||||||
"worktrees": "Worktrees…",
|
"worktrees": "Worktrees…",
|
||||||
"weeklyReport": "Wochenbericht…",
|
"weeklyReport": "Wochenbericht…",
|
||||||
"usageMonitor": "Usage Monitor…",
|
"usageMonitor": "Usage Monitor…",
|
||||||
@@ -810,7 +811,7 @@
|
|||||||
},
|
},
|
||||||
"vm": {
|
"vm": {
|
||||||
"connection": { "online": "Online", "connecting": "Verbinden…", "offline": "Offline" },
|
"connection": { "online": "Online", "connecting": "Verbinden…", "offline": "Offline" },
|
||||||
"shell": { "unexpectedError": "Unerwarteter Fehler: {0}" },
|
"shell": { "unexpectedError": "Unerwarteter Fehler: {0}", "shutdownConfirm": "Dadurch wird der ClaudeDo-Worker beendet (laufende Task-Runs und interaktive Sitzungen brechen ab) und die App geschlossen. Fortfahren?" },
|
||||||
"mainWindow": { "closeConfirm": { "message": "{0} Mission-Control-Sitzung(en) laufen noch. Beim Beenden werden sie abgebrochen. Trotzdem schließen?" } },
|
"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" },
|
"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", "waiting": "Wartet" },
|
"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", "waiting": "Wartet" },
|
||||||
|
|||||||
@@ -785,6 +785,7 @@
|
|||||||
"checkForUpdates": "Check for updates",
|
"checkForUpdates": "Check for updates",
|
||||||
"restartWorker": "Restart worker",
|
"restartWorker": "Restart worker",
|
||||||
"updateClaudeCli": "Update Claude CLI",
|
"updateClaudeCli": "Update Claude CLI",
|
||||||
|
"shutdown": "Shutdown",
|
||||||
"worktrees": "Worktrees…",
|
"worktrees": "Worktrees…",
|
||||||
"weeklyReport": "Weekly Report…",
|
"weeklyReport": "Weekly Report…",
|
||||||
"usageMonitor": "Usage Monitor…",
|
"usageMonitor": "Usage Monitor…",
|
||||||
@@ -810,7 +811,7 @@
|
|||||||
},
|
},
|
||||||
"vm": {
|
"vm": {
|
||||||
"connection": { "online": "Online", "connecting": "Connecting…", "offline": "Offline" },
|
"connection": { "online": "Online", "connecting": "Connecting…", "offline": "Offline" },
|
||||||
"shell": { "unexpectedError": "Unexpected error: {0}" },
|
"shell": { "unexpectedError": "Unexpected error: {0}", "shutdownConfirm": "This stops the ClaudeDo worker (any running task or interactive session is interrupted) and closes the app. Continue?" },
|
||||||
"mainWindow": { "closeConfirm": { "message": "{0} Mission Control session(s) are still running. Closing the app will end them. Close anyway?" } },
|
"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" },
|
"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", "waiting": "Waiting" },
|
"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", "waiting": "Waiting" },
|
||||||
|
|||||||
@@ -658,8 +658,17 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
var exe = _workerLocator.Find();
|
var exe = _workerLocator.Find();
|
||||||
if (exe is null) throw new InvalidOperationException("Worker executable not found.");
|
if (exe is null) throw new InvalidOperationException("Worker executable not found.");
|
||||||
|
|
||||||
// Only kill the worker belonging to THIS installation — not any other
|
KillMatchingWorkerProcesses(exe);
|
||||||
// ClaudeDo.Worker on the machine (e.g. a second install).
|
|
||||||
|
var started = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(exe) { UseShellExecute = true });
|
||||||
|
if (started is not null && started.WaitForExit(RestartSurvivalCheckMs))
|
||||||
|
throw new InvalidOperationException($"worker process exited immediately (code {started.ExitCode})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only kill the worker belonging to THIS installation — not any other
|
||||||
|
// ClaudeDo.Worker on the machine (e.g. a second install).
|
||||||
|
private static void KillMatchingWorkerProcesses(string exe)
|
||||||
|
{
|
||||||
var exeFull = System.IO.Path.GetFullPath(exe);
|
var exeFull = System.IO.Path.GetFullPath(exe);
|
||||||
foreach (var p in System.Diagnostics.Process.GetProcessesByName("ClaudeDo.Worker"))
|
foreach (var p in System.Diagnostics.Process.GetProcessesByName("ClaudeDo.Worker"))
|
||||||
{
|
{
|
||||||
@@ -675,10 +684,29 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
catch { /* may have exited or be inaccessible */ }
|
catch { /* may have exited or be inaccessible */ }
|
||||||
finally { p.Dispose(); }
|
finally { p.Dispose(); }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var started = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(exe) { UseShellExecute = true });
|
// Set by MainWindow: force-closes the window without re-triggering its own close
|
||||||
if (started is not null && started.WaitForExit(RestartSurvivalCheckMs))
|
// confirmation (the shutdown dialog above already confirmed).
|
||||||
throw new InvalidOperationException($"worker process exited immediately (code {started.ExitCode})");
|
public Action? CloseWindow { get; set; }
|
||||||
|
|
||||||
|
// Test seam: overrides the real kill-worker + close-window body.
|
||||||
|
internal Action? ShutdownOperation { get; set; }
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task ShutdownAsync()
|
||||||
|
{
|
||||||
|
if (Dialogs is null) return;
|
||||||
|
var confirmed = await Dialogs.ConfirmAsync(Loc.T("vm.shell.shutdownConfirm"));
|
||||||
|
if (!confirmed) return;
|
||||||
|
(ShutdownOperation ?? ShutdownWorkerAndCloseApp)();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShutdownWorkerAndCloseApp()
|
||||||
|
{
|
||||||
|
var exe = _workerLocator.Find();
|
||||||
|
if (exe is not null) KillMatchingWorkerProcesses(exe);
|
||||||
|
CloseWindow?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _updatingClaudeCli;
|
private bool _updatingClaudeCli;
|
||||||
|
|||||||
@@ -68,6 +68,9 @@
|
|||||||
Command="{Binding OpenUsageMonitorCommand}"/>
|
Command="{Binding OpenUsageMonitorCommand}"/>
|
||||||
<MenuItem Header="{loc:Tr shell.menu.updateClaudeCli}"
|
<MenuItem Header="{loc:Tr shell.menu.updateClaudeCli}"
|
||||||
Command="{Binding UpdateClaudeCliCommand}"/>
|
Command="{Binding UpdateClaudeCliCommand}"/>
|
||||||
|
<Separator/>
|
||||||
|
<MenuItem Header="{loc:Tr shell.menu.shutdown}"
|
||||||
|
Command="{Binding ShutdownCommand}"/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem Header="{loc:Tr shell.menu.repositories}"
|
<MenuItem Header="{loc:Tr shell.menu.repositories}"
|
||||||
FontSize="{StaticResource FontSizeMono}"
|
FontSize="{StaticResource FontSizeMono}"
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ public partial class MainWindow : Window
|
|||||||
WindowState = WindowState.Normal;
|
WindowState = WindowState.Normal;
|
||||||
Activate();
|
Activate();
|
||||||
};
|
};
|
||||||
|
vm.CloseWindow = () =>
|
||||||
|
{
|
||||||
|
_forceClose = true;
|
||||||
|
Close();
|
||||||
|
};
|
||||||
if (vm.Tasks is not null)
|
if (vm.Tasks is not null)
|
||||||
vm.Tasks.IsWindowVisible = WindowState != WindowState.Minimized;
|
vm.Tasks.IsWindowVisible = WindowState != WindowState.Minimized;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
using ClaudeDo.Ui.ViewModels;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Conflicts;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Tests;
|
||||||
|
|
||||||
|
// Covers the worker-menu "Shutdown" command: a confirm dialog gates the actual kill-worker +
|
||||||
|
// close-app action. ShutdownOperation is a test seam standing in for the real body, so these
|
||||||
|
// tests only assert the confirm gate, never a real process kill.
|
||||||
|
public class IslandsShellViewModelShutdownTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Declined_DoesNotInvokeShutdownOperation()
|
||||||
|
{
|
||||||
|
var invoked = false;
|
||||||
|
var vm = new IslandsShellViewModel
|
||||||
|
{
|
||||||
|
Dialogs = new FakeDialogService(confirmResult: false),
|
||||||
|
ShutdownOperation = () => invoked = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await vm.ShutdownCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.False(invoked);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Confirmed_InvokesShutdownOperation()
|
||||||
|
{
|
||||||
|
var invoked = false;
|
||||||
|
var vm = new IslandsShellViewModel
|
||||||
|
{
|
||||||
|
Dialogs = new FakeDialogService(confirmResult: true),
|
||||||
|
ShutdownOperation = () => invoked = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await vm.ShutdownCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.True(invoked);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeDialogService(bool confirmResult) : IDialogService
|
||||||
|
{
|
||||||
|
public Task<bool> ConfirmAsync(string message) => Task.FromResult(confirmResult);
|
||||||
|
|
||||||
|
public Task ShowAboutAsync(AboutModalViewModel vm) => throw new NotImplementedException();
|
||||||
|
public Task ShowWeeklyReportAsync(WeeklyReportModalViewModel vm) => throw new NotImplementedException();
|
||||||
|
public Task ShowUsageMonitorAsync(UsageMonitorModalViewModel vm) => throw new NotImplementedException();
|
||||||
|
public Task ShowSettingsAsync(SettingsModalViewModel vm) => throw new NotImplementedException();
|
||||||
|
public Task ShowListSettingsAsync(ListSettingsModalViewModel vm) => throw new NotImplementedException();
|
||||||
|
public Task ShowRepoImportAsync(RepoImportModalViewModel vm) => throw new NotImplementedException();
|
||||||
|
public Task ShowWorktreesOverviewAsync(WorktreesOverviewModalViewModel vm) => throw new NotImplementedException();
|
||||||
|
public Task<IReadOnlyList<string>?> ShowMergeHelperSelectionAsync(MergeHelperSelectionModalViewModel vm) => throw new NotImplementedException();
|
||||||
|
public Task ShowWorkerConnectionAsync(WorkerConnectionModalViewModel vm) => throw new NotImplementedException();
|
||||||
|
public Task ShowConflictResolverAsync(ConflictResolverViewModel vm) => throw new NotImplementedException();
|
||||||
|
public Task ShowLogVisualizerAsync(LogVisualizerViewModel vm) => throw new NotImplementedException();
|
||||||
|
public Task ShowErrorAsync(string message) => Task.CompletedTask;
|
||||||
|
public void ShowMissionControl(MissionControlViewModel vm) => throw new NotImplementedException();
|
||||||
|
public void ShowCheatSheets(CheatSheetsViewModel vm) => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user