fix(ui): duplizierter Confirm-Dialog mit "Delete"-Knopf – zusammenfuehren und lokalisieren

F4: DetailsIslandView/TasksIslandView hatten je eine view-lokale Confirm/Error-Dialog-Kopie,
teils mit hartkodiertem "Delete"-Knopf fuer jede Aktion (inkl. der Stash+Merge-Bestaetigung,
die nichts loescht). Beide Kopien entfernt; DetailsIslandViewModel/TasksIslandViewModel routen
Confirm/Error jetzt durch den geteilten Dialogs-Seam (IDialogService, propagiert vom Shell wie
schon bei Lists/Tasks) statt ueber eine vom Code-behind gesetzte Func-Property.

IDialogService.ConfirmAsync bekommt einen optionalen confirmLabel-Parameter (Default: lokalisiertes
"Confirm"); nur die echten Delete-Aktionen (Task loeschen, abgeschlossene Tasks leeren) uebergeben
das lokalisierte "Delete"-Label.

F5: "Cancel"/"Confirm"/"Delete"/"OK" und die Titel "Confirm"/"Error" durch Loc.T mit neuen Keys
unter "dialog.*" ersetzt (en.json + de.json). Mission Controls eigene Dialog-Kopie bleibt bestehen
(eigenes Fenster, WindowDialogService ist an den Haupt-Owner gebunden) — nur ihre Labels wurden
lokalisiert.

Out of scope (siehe Task-Beschreibung): die Nachrichtentexte der Dialoge bleiben unlokalisiert.
This commit is contained in:
Mika Kuns
2026-09-16 15:43:38 +02:00
committed by mika kuns
parent 684b0115e5
commit 86cc309ba9
17 changed files with 126 additions and 199 deletions
@@ -291,11 +291,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
// Set by shell so DeleteTaskCommand can remove from list
public Func<TaskRowViewModel, System.Threading.Tasks.Task>? DeleteFromList { get; set; }
// Set by the view so DeleteTaskCommand can prompt yes/no before deleting
public Func<string, System.Threading.Tasks.Task<bool>>? ConfirmAsync { get; set; }
// Set by the view so DeleteTaskCommand can show an error message
public Func<string, System.Threading.Tasks.Task>? ShowErrorAsync { get; set; }
// Set by the shell (mirrors ListsIslandViewModel.Dialogs) so confirm/error prompts go
// through the one shared dialog implementation instead of a view-local copy.
public IDialogService? Dialogs { get; set; }
// ── Review ──────────────────────────────────────────────────────────────
[ObservableProperty]
@@ -1042,9 +1040,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
{
if (Task == null) return;
var row = Task;
if (ConfirmAsync != null)
if (Dialogs != null)
{
var ok = await ConfirmAsync($"Delete \"{row.Title}\"? This cannot be undone.");
var ok = await Dialogs.ConfirmAsync($"Delete \"{row.Title}\"? This cannot be undone.", Loc.T("dialog.delete"));
if (!ok) return;
}
// Routed through the worker (mirrors the MCP delete_task tool) so a deleted child
@@ -1060,14 +1058,14 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
{
// Belt and braces: the connection can drop between the CanExecute check and
// this call, so a stray throw here must surface, not vanish silently.
if (ShowErrorAsync != null)
await ShowErrorAsync(ex.Message);
if (Dialogs != null)
await Dialogs.ShowErrorAsync(ex.Message);
return;
}
if (!deleted)
{
if (ShowErrorAsync != null)
await ShowErrorAsync(error ?? "Delete failed.");
if (Dialogs != null)
await Dialogs.ShowErrorAsync(error ?? "Delete failed.");
return;
}
if (DeleteFromList != null)
@@ -1216,10 +1214,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
private async System.Threading.Tasks.Task ResetAndRetryAsync()
{
if (Task == null) return;
if (ConfirmAsync == null) return;
if (Dialogs == null) return;
var branchName = $"claudedo/{Task.Id.Replace("-", "")}";
var ok = await ConfirmAsync(
var ok = await Dialogs.ConfirmAsync(
$"Reset and retry?\nThis discards branch {branchName} (and uncommitted changes), then queues the task to run from the beginning.");
if (!ok) return;
@@ -1279,31 +1277,31 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
// Nothing landed -- the target working dir has uncommitted changes (often another
// session's WIP in the shared main checkout). Offer the one-shot stash retry; git
// restores the changes itself once the merge is through.
if (result?.Status == "dirty_target" && ConfirmAsync != null
&& await ConfirmAsync(Loc.T("vm.detailsIsland.stashAndMergeConfirm")))
if (result?.Status == "dirty_target" && Dialogs != null
&& await Dialogs.ConfirmAsync(Loc.T("vm.detailsIsland.stashAndMergeConfirm")))
{
ApproveOp.Report(Loc.T("ops.merge.merging"));
result = await _worker.ApproveReviewAsync(
taskId, Merge.SelectedMergeTarget ?? "", autostash: true);
}
if (result?.Status == "dirty_target" && ShowErrorAsync != null)
await ShowErrorAsync(result.ErrorMessage ?? Loc.T("vm.detailsIsland.dirtyTarget"));
if (result?.Status == "dirty_target" && Dialogs != null)
await Dialogs.ShowErrorAsync(result.ErrorMessage ?? Loc.T("vm.detailsIsland.dirtyTarget"));
else if (!hasChildren && result?.Status == "conflict")
await _merge.ResolveConflictAsync(taskId, Merge.SelectedMergeTarget ?? "");
// The merge itself already landed; the verify command failed, so the task stayed
// out of Done. Surface that instead of silently looking like nothing happened.
else if (!hasChildren && result?.Status == "verify_failed" && ShowErrorAsync != null)
await ShowErrorAsync(result.ErrorMessage ?? Loc.T("vm.detailsIsland.verifyFailed"));
else if (!hasChildren && result?.Status == "verify_failed" && Dialogs != null)
await Dialogs.ShowErrorAsync(result.ErrorMessage ?? Loc.T("vm.detailsIsland.verifyFailed"));
// Nothing landed -- the merge was refused before touching the repo because the
// branch would have overwritten an untracked file in the target working directory.
else if (!hasChildren && result?.Status == "untracked_collision" && ShowErrorAsync != null)
await ShowErrorAsync(result.ErrorMessage ?? Loc.T("vm.detailsIsland.untrackedCollision"));
else if (!hasChildren && result?.Status == "untracked_collision" && Dialogs != null)
await Dialogs.ShowErrorAsync(result.ErrorMessage ?? Loc.T("vm.detailsIsland.untrackedCollision"));
}
catch (Exception ex)
{
if (ShowErrorAsync != null)
await ShowErrorAsync(ex.Message);
if (Dialogs != null)
await Dialogs.ShowErrorAsync(ex.Message);
}
finally
{
@@ -1348,7 +1346,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
try { await _worker.SubmitTaskForReviewAsync(Task.Id); }
catch (Exception ex)
{
if (ShowErrorAsync != null) await ShowErrorAsync(ex.Message);
if (Dialogs != null) await Dialogs.ShowErrorAsync(ex.Message);
}
}
@@ -1379,9 +1377,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
[RelayCommand]
private async System.Threading.Tasks.Task ResetReviewAsync()
{
if (Task is null || !_worker.IsConnected || ConfirmAsync is null) return;
if (Task is null || !_worker.IsConnected || Dialogs is null) return;
var branchName = $"claudedo/{Task.Id.Replace("-", "")}";
var ok = await ConfirmAsync(
var ok = await Dialogs.ConfirmAsync(
$"Reset working tree?\nThis discards branch {branchName} (and all changes) and returns the task to Idle.");
if (!ok) return;
try { await _worker.ResetTaskAsync(Task.Id); }