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",
|
||||
"restartWorker": "Worker neu starten",
|
||||
"updateClaudeCli": "Claude CLI aktualisieren",
|
||||
"shutdown": "Beenden",
|
||||
"worktrees": "Worktrees…",
|
||||
"weeklyReport": "Wochenbericht…",
|
||||
"usageMonitor": "Usage Monitor…",
|
||||
@@ -810,7 +811,7 @@
|
||||
},
|
||||
"vm": {
|
||||
"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?" } },
|
||||
"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" },
|
||||
|
||||
@@ -785,6 +785,7 @@
|
||||
"checkForUpdates": "Check for updates",
|
||||
"restartWorker": "Restart worker",
|
||||
"updateClaudeCli": "Update Claude CLI",
|
||||
"shutdown": "Shutdown",
|
||||
"worktrees": "Worktrees…",
|
||||
"weeklyReport": "Weekly Report…",
|
||||
"usageMonitor": "Usage Monitor…",
|
||||
@@ -810,7 +811,7 @@
|
||||
},
|
||||
"vm": {
|
||||
"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?" } },
|
||||
"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" },
|
||||
|
||||
@@ -658,8 +658,17 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
var exe = _workerLocator.Find();
|
||||
if (exe is null) throw new InvalidOperationException("Worker executable not found.");
|
||||
|
||||
// Only kill the worker belonging to THIS installation — not any other
|
||||
// ClaudeDo.Worker on the machine (e.g. a second install).
|
||||
KillMatchingWorkerProcesses(exe);
|
||||
|
||||
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);
|
||||
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 */ }
|
||||
finally { p.Dispose(); }
|
||||
}
|
||||
}
|
||||
|
||||
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})");
|
||||
// Set by MainWindow: force-closes the window without re-triggering its own close
|
||||
// confirmation (the shutdown dialog above already confirmed).
|
||||
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;
|
||||
|
||||
@@ -68,6 +68,9 @@
|
||||
Command="{Binding OpenUsageMonitorCommand}"/>
|
||||
<MenuItem Header="{loc:Tr shell.menu.updateClaudeCli}"
|
||||
Command="{Binding UpdateClaudeCliCommand}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="{loc:Tr shell.menu.shutdown}"
|
||||
Command="{Binding ShutdownCommand}"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="{loc:Tr shell.menu.repositories}"
|
||||
FontSize="{StaticResource FontSizeMono}"
|
||||
|
||||
@@ -80,6 +80,11 @@ public partial class MainWindow : Window
|
||||
WindowState = WindowState.Normal;
|
||||
Activate();
|
||||
};
|
||||
vm.CloseWindow = () =>
|
||||
{
|
||||
_forceClose = true;
|
||||
Close();
|
||||
};
|
||||
if (vm.Tasks is not null)
|
||||
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