diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json
index 7352a2b4..f928db3c 100644
--- a/src/ClaudeDo.Localization/locales/de.json
+++ b/src/ClaudeDo.Localization/locales/de.json
@@ -805,6 +805,14 @@
"done": "Fertig"
}
},
+ "dialog": {
+ "cancel": "Abbrechen",
+ "confirm": "Bestätigen",
+ "delete": "Löschen",
+ "ok": "OK",
+ "confirmTitle": "Bestätigen",
+ "errorTitle": "Fehler"
+ },
"shell": {
"menu": {
"help": "Hilfe",
diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json
index 48b391d6..d8293800 100644
--- a/src/ClaudeDo.Localization/locales/en.json
+++ b/src/ClaudeDo.Localization/locales/en.json
@@ -805,6 +805,14 @@
"done": "Done"
}
},
+ "dialog": {
+ "cancel": "Cancel",
+ "confirm": "Confirm",
+ "delete": "Delete",
+ "ok": "OK",
+ "confirmTitle": "Confirm",
+ "errorTitle": "Error"
+ },
"shell": {
"menu": {
"help": "Help",
diff --git a/src/ClaudeDo.Ui/Services/IDialogService.cs b/src/ClaudeDo.Ui/Services/IDialogService.cs
index b047c529..06f8e7b2 100644
--- a/src/ClaudeDo.Ui/Services/IDialogService.cs
+++ b/src/ClaudeDo.Ui/Services/IDialogService.cs
@@ -28,8 +28,9 @@ public interface IDialogService
Task ShowConflictResolverAsync(ConflictResolverViewModel vm);
Task ShowLogVisualizerAsync(LogVisualizerViewModel vm);
- /// Modal yes/no confirmation. Returns true only when confirmed.
- Task ConfirmAsync(string message);
+ /// Modal yes/no confirmation. Returns true only when confirmed.
+ /// overrides the default localized "Confirm" button text (e.g. a localized "Delete" for destructive actions).
+ Task ConfirmAsync(string message, string? confirmLabel = null);
/// Modal error notice with a single dismiss button.
Task ShowErrorAsync(string message);
diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs
index 9b40b0e1..32fb5885 100644
--- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs
@@ -291,11 +291,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
// Set by shell so DeleteTaskCommand can remove from list
public Func? DeleteFromList { get; set; }
- // Set by the view so DeleteTaskCommand can prompt yes/no before deleting
- public Func>? ConfirmAsync { get; set; }
-
- // Set by the view so DeleteTaskCommand can show an error message
- public Func? 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); }
diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
index 2591ee6f..c604eed9 100644
--- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
@@ -1209,9 +1209,9 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
// block removing a completed planning parent together with its done children.
var toDelete = completed.OrderByDescending(r => r.IsChild).ToList();
- if (ConfirmAsync is not null)
+ if (Dialogs is not null)
{
- var ok = await ConfirmAsync($"Clear {toDelete.Count} completed task(s)? This cannot be undone.");
+ var ok = await Dialogs.ConfirmAsync($"Clear {toDelete.Count} completed task(s)? This cannot be undone.", Loc.T("dialog.delete"));
if (!ok) return;
}
@@ -1339,9 +1339,9 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
private async Task DeleteTaskAsync(TaskRowViewModel? row)
{
if (row is null || row.HasInteractiveSession || _worker is null) return;
- if (ConfirmAsync is not null)
+ if (Dialogs is not null)
{
- var ok = await ConfirmAsync(Loc.T("vm.tasksIsland.deleteTaskConfirm", row.Title));
+ var ok = await Dialogs.ConfirmAsync(Loc.T("vm.tasksIsland.deleteTaskConfirm", row.Title), Loc.T("dialog.delete"));
if (!ok) return;
}
@@ -1653,8 +1653,8 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
if (outcome.Result == DiscardPlanningResult.BlockedByQueuedChildren)
{
- if (ConfirmAsync is null) return;
- var ok = await ConfirmAsync(
+ if (Dialogs is null) return;
+ var ok = await Dialogs.ConfirmAsync(
$"{outcome.QueuedChildrenCount} child task(s) are queued.\n" +
"Dequeue them and discard the planning session?");
if (!ok) return;
@@ -1663,18 +1663,13 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
}
else if (outcome.Result == DiscardPlanningResult.BlockedByRunningChildren)
{
- if (ConfirmAsync is null) return;
- await ConfirmAsync(
+ if (Dialogs is null) return;
+ await Dialogs.ConfirmAsync(
$"{outcome.RunningChildrenCount} child task(s) are still running.\n" +
"Cancel them first, then try again.");
}
}
- ///
- /// Wired by the view via . Returns true when the user confirms.
- ///
- public Func>? ConfirmAsync { get; set; }
-
private bool CanRunPlanningOperation() => !PlanningOperation.IsRunning;
[RelayCommand(CanExecute = nameof(CanRunPlanningOperation))]
diff --git a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
index d7f5b80c..8ee08b02 100644
--- a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
@@ -68,7 +68,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
// Set by MainWindow so a reveal can bring the main window to the foreground.
public Action? BringToFront { get; set; }
- // Single dialog seam (set by MainWindow); propagated to the lists island.
+ // Single dialog seam (set by MainWindow); propagated to the islands.
private IDialogService? _dialogs;
public IDialogService? Dialogs
{
@@ -78,6 +78,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
_dialogs = value;
if (Lists is not null) Lists.Dialogs = value;
if (Tasks is not null) Tasks.Dialogs = value;
+ if (Details is not null) Details.Dialogs = value;
}
}
diff --git a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs
index 4eaa90c2..671c456e 100644
--- a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs
+++ b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs
@@ -173,8 +173,6 @@ public partial class DetailsIslandView : UserControl
await modal.ShowDialog(owner);
};
- vm.ConfirmAsync = ShowConfirmAsync;
- vm.ShowErrorAsync = ShowErrorDialogAsync;
}
}
@@ -213,87 +211,4 @@ public partial class DetailsIslandView : UserControl
_descriptionHeights[id] = DetailBodyGrid.RowDefinitions[0].Height.Value;
}
- private async System.Threading.Tasks.Task ShowErrorDialogAsync(string message)
- {
- var owner = TopLevel.GetTopLevel(this) as Window;
- if (owner == null) return;
-
- var ok = new Button { Content = "OK", MinWidth = 90 };
-
- var dialog = new Window
- {
- Title = "Error",
- Width = 360,
- SizeToContent = SizeToContent.Height,
- CanResize = false,
- WindowStartupLocation = WindowStartupLocation.CenterOwner,
- ShowInTaskbar = false,
- Background = this.FindResource("SurfaceBrush") as IBrush,
- Content = new StackPanel
- {
- Spacing = 16,
- Margin = new Thickness(20),
- Children =
- {
- new TextBlock { Text = message, TextWrapping = TextWrapping.Wrap },
- new StackPanel
- {
- Orientation = Orientation.Horizontal,
- Spacing = 8,
- HorizontalAlignment = HorizontalAlignment.Right,
- Children = { ok }
- }
- }
- }
- };
-
- ok.Click += (_, _) => dialog.Close();
-
- await dialog.ShowDialog(owner);
- }
-
- private async System.Threading.Tasks.Task ShowConfirmAsync(string message)
- {
- var owner = TopLevel.GetTopLevel(this) as Window;
- if (owner == null) return false;
-
- var tcs = new TaskCompletionSource();
-
- var cancel = new Button { Content = "Cancel", MinWidth = 90 };
- var confirm = new Button { Content = "Delete", MinWidth = 90, Classes = { "danger" } };
-
- var dialog = new Window
- {
- Title = "Confirm",
- Width = 360,
- SizeToContent = SizeToContent.Height,
- CanResize = false,
- WindowStartupLocation = WindowStartupLocation.CenterOwner,
- ShowInTaskbar = false,
- Background = this.FindResource("SurfaceBrush") as IBrush,
- Content = new StackPanel
- {
- Spacing = 16,
- Margin = new Thickness(20),
- Children =
- {
- new TextBlock { Text = message, TextWrapping = TextWrapping.Wrap },
- new StackPanel
- {
- Orientation = Orientation.Horizontal,
- Spacing = 8,
- HorizontalAlignment = HorizontalAlignment.Right,
- Children = { cancel, confirm }
- }
- }
- }
- };
-
- cancel.Click += (_, _) => { tcs.TrySetResult(false); dialog.Close(); };
- confirm.Click += (_, _) => { tcs.TrySetResult(true); dialog.Close(); };
- dialog.Closed += (_, _) => tcs.TrySetResult(false);
-
- _ = dialog.ShowDialog(owner);
- return await tcs.Task;
- }
}
diff --git a/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml.cs b/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml.cs
index ffa18f42..69c7b2e5 100644
--- a/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml.cs
+++ b/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml.cs
@@ -55,7 +55,6 @@ public partial class TasksIslandView : UserControl
await modal.ShowDialog(owner);
// ShowDialog completes once the window is closed (CloseAction or OS close).
};
- vm.ConfirmAsync = ShowConfirmAsync;
vm.SelectionChanged += (_, _) => ScrollSelectedIntoView();
}
};
@@ -71,50 +70,6 @@ public partial class TasksIslandView : UserControl
Dispatcher.UIThread.Post(() => RowsListBox.ScrollIntoView(target), DispatcherPriority.Background);
}
- private async System.Threading.Tasks.Task ShowConfirmAsync(string message)
- {
- var owner = TopLevel.GetTopLevel(this) as Window;
- if (owner is null) return false;
-
- var tcs = new TaskCompletionSource();
- var cancel = new Button { Content = "Cancel", MinWidth = 90 };
- var confirm = new Button { Content = "Confirm", MinWidth = 90, Classes = { "danger" } };
-
- var dialog = new Window
- {
- Title = "Confirm",
- Width = 380,
- SizeToContent = SizeToContent.Height,
- CanResize = false,
- WindowStartupLocation = WindowStartupLocation.CenterOwner,
- ShowInTaskbar = false,
- Background = this.FindResource("SurfaceBrush") as IBrush,
- Content = new StackPanel
- {
- Margin = new Thickness(20),
- Spacing = 16,
- Children =
- {
- new TextBlock { Text = message, TextWrapping = TextWrapping.Wrap },
- new StackPanel
- {
- Orientation = Orientation.Horizontal,
- HorizontalAlignment = HorizontalAlignment.Right,
- Spacing = 8,
- Children = { cancel, confirm },
- },
- },
- },
- };
-
- cancel.Click += (_, _) => { tcs.TrySetResult(false); dialog.Close(); };
- confirm.Click += (_, _) => { tcs.TrySetResult(true); dialog.Close(); };
- dialog.Closed += (_, _) => tcs.TrySetResult(false);
-
- _ = dialog.ShowDialog(owner);
- return await tcs.Task;
- }
-
// ── Custom ghost drag ────────────────────────────────────────────────────
// Replaces both the OLE DoDragDropAsync reorder and the OLE drop-to-queue path: a hand-built
// drag (pointer capture + a transparent topmost ghost window) is the only way to get a
diff --git a/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml.cs b/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml.cs
index 5a59c6f3..dba2143e 100644
--- a/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml.cs
+++ b/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml.cs
@@ -8,6 +8,7 @@ using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.VisualTree;
+using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.ViewModels;
namespace ClaudeDo.Ui.Views.MissionControl;
@@ -73,20 +74,21 @@ public partial class MissionControlView : UserControl
UpdateQueueColumn();
}
- // Abandon-run confirmation -- same TaskCompletionSource-backed plain Window as
- // TasksIslandView.ShowConfirmAsync (no shared control for this shape yet).
+ // Abandon-run confirmation -- own plain-Window TaskCompletionSource dialog, not routed
+ // through WindowDialogService: it's owned by the main window, so showing it while Mission
+ // Control (a separate Window) is focused would put it behind Mission Control.
private async Task ShowConfirmAsync(string message)
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return false;
var tcs = new TaskCompletionSource();
- var cancel = new Button { Content = "Cancel", MinWidth = 90 };
- var confirm = new Button { Content = "Confirm", MinWidth = 90, Classes = { "danger" } };
+ var cancel = new Button { Content = Loc.T("dialog.cancel"), MinWidth = 90 };
+ var confirm = new Button { Content = Loc.T("dialog.confirm"), MinWidth = 90, Classes = { "danger" } };
var dialog = new Window
{
- Title = "Confirm",
+ Title = Loc.T("dialog.confirmTitle"),
Width = 380,
SizeToContent = SizeToContent.Height,
CanResize = false,
diff --git a/src/ClaudeDo.Ui/Views/WindowDialogService.cs b/src/ClaudeDo.Ui/Views/WindowDialogService.cs
index 86df0296..d9b1ca10 100644
--- a/src/ClaudeDo.Ui/Views/WindowDialogService.cs
+++ b/src/ClaudeDo.Ui/Views/WindowDialogService.cs
@@ -6,6 +6,7 @@ using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Layout;
using Avalonia.Media;
+using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels;
using ClaudeDo.Ui.ViewModels.Conflicts;
@@ -71,7 +72,7 @@ public sealed class WindowDialogService : IDialogService
public async Task ShowSettingsAsync(SettingsModalViewModel vm)
{
- vm.Worktrees.ConfirmAsync = ConfirmAsync;
+ vm.Worktrees.ConfirmAsync = message => ConfirmAsync(message);
var dlg = new SettingsModalView { DataContext = vm };
vm.RequestRepoImport = () =>
{
@@ -90,7 +91,7 @@ public sealed class WindowDialogService : IDialogService
{
var dlg = new ListSettingsModalView { DataContext = vm };
vm.CloseAction = () => dlg.Close();
- vm.ConfirmAsync = ConfirmAsync;
+ vm.ConfirmAsync = message => ConfirmAsync(message);
vm.ShowErrorAsync = ShowErrorAsync;
await dlg.ShowDialog(_owner);
}
@@ -144,7 +145,7 @@ public sealed class WindowDialogService : IDialogService
_diffViewerWindow = diffDlg;
diffDlg.Show(_owner);
};
- vm.ConfirmAction = ConfirmAsync;
+ vm.ConfirmAction = message => ConfirmAsync(message);
if (Shell is { } shell)
{
vm.ResolveMergeVm = shell.ResolveMergeVm;
@@ -206,13 +207,13 @@ public sealed class WindowDialogService : IDialogService
_cheatSheets.Activate();
}
- public Task ConfirmAsync(string message)
+ public Task ConfirmAsync(string message, string? confirmLabel = null)
{
var tcs = new TaskCompletionSource();
- var cancel = new Button { Content = "Cancel", MinWidth = 90 };
- var confirm = new Button { Content = "Confirm", MinWidth = 90, Classes = { "danger" } };
+ var cancel = new Button { Content = Loc.T("dialog.cancel"), MinWidth = 90 };
+ var confirm = new Button { Content = confirmLabel ?? Loc.T("dialog.confirm"), MinWidth = 90, Classes = { "danger" } };
- var dialog = NoticeWindow("Confirm", 380, message,
+ var dialog = NoticeWindow(Loc.T("dialog.confirmTitle"), 380, message,
new StackPanel
{
Orientation = Orientation.Horizontal,
@@ -231,8 +232,8 @@ public sealed class WindowDialogService : IDialogService
public Task ShowErrorAsync(string message)
{
- var ok = new Button { Content = "OK", MinWidth = 90 };
- var dialog = NoticeWindow("Error", 360, message,
+ var ok = new Button { Content = Loc.T("dialog.ok"), MinWidth = 90 };
+ var dialog = NoticeWindow(Loc.T("dialog.errorTitle"), 360, message,
new StackPanel
{
Orientation = Orientation.Horizontal,
diff --git a/tests/ClaudeDo.Ui.Tests/IslandsShellViewModelShutdownTests.cs b/tests/ClaudeDo.Ui.Tests/IslandsShellViewModelShutdownTests.cs
index 7b292fc2..fba3d148 100644
--- a/tests/ClaudeDo.Ui.Tests/IslandsShellViewModelShutdownTests.cs
+++ b/tests/ClaudeDo.Ui.Tests/IslandsShellViewModelShutdownTests.cs
@@ -44,7 +44,7 @@ public class IslandsShellViewModelShutdownTests
private sealed class FakeDialogService(bool confirmResult) : IDialogService
{
- public Task ConfirmAsync(string message) => Task.FromResult(confirmResult);
+ public Task ConfirmAsync(string message, string? confirmLabel = null) => Task.FromResult(confirmResult);
public Task ShowAboutAsync(AboutModalViewModel vm) => throw new NotImplementedException();
public Task ShowWeeklyReportAsync(WeeklyReportModalViewModel vm) => throw new NotImplementedException();
diff --git a/tests/ClaudeDo.Ui.Tests/TestDoubles.cs b/tests/ClaudeDo.Ui.Tests/TestDoubles.cs
index 11fc917e..b4770683 100644
--- a/tests/ClaudeDo.Ui.Tests/TestDoubles.cs
+++ b/tests/ClaudeDo.Ui.Tests/TestDoubles.cs
@@ -1,6 +1,9 @@
using ClaudeDo.Data;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.Services.Interfaces;
+using ClaudeDo.Ui.ViewModels;
+using ClaudeDo.Ui.ViewModels.Conflicts;
+using ClaudeDo.Ui.ViewModels.Modals;
using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Ui.Tests;
@@ -31,3 +34,43 @@ public sealed class StubNotesApi : INotesApi
/// is abstract — this is the concrete no-op subclass.
public sealed class DefaultStub : StubWorkerClient { }
+
+///
+/// Records the message/label of the last Confirm/Error call and returns a configurable
+/// Confirm result. Covers the ViewModels (DetailsIsland, TasksIsland, ...) that take their
+/// dialog seam via a settable Dialogs property instead of a per-call constructor arg.
+///
+public sealed class RecordingDialogService : IDialogService
+{
+ public bool ConfirmResult { get; set; } = true;
+ public string? LastConfirmMessage { get; private set; }
+ public string? LastConfirmLabel { get; private set; }
+ public string? LastErrorMessage { get; private set; }
+
+ public Task ConfirmAsync(string message, string? confirmLabel = null)
+ {
+ LastConfirmMessage = message;
+ LastConfirmLabel = confirmLabel;
+ return Task.FromResult(ConfirmResult);
+ }
+
+ public Task ShowErrorAsync(string message)
+ {
+ LastErrorMessage = message;
+ return Task.CompletedTask;
+ }
+
+ 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?> 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 void ShowMissionControl(MissionControlViewModel vm) => throw new NotImplementedException();
+ public void ShowCheatSheets(CheatSheetsViewModel vm) => throw new NotImplementedException();
+}
diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandDeleteTaskTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandDeleteTaskTests.cs
index dbb79fe6..53db74c7 100644
--- a/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandDeleteTaskTests.cs
+++ b/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandDeleteTaskTests.cs
@@ -90,12 +90,12 @@ public class DetailsIslandDeleteTaskTests : IDisposable
vm.DeleteFromList = _ => { deleteFromListCalled = true; return Task.CompletedTask; };
var closed = false;
vm.CloseDetail = () => closed = true;
- string? reportedError = null;
- vm.ShowErrorAsync = msg => { reportedError = msg; return Task.CompletedTask; };
+ var dialogs = new RecordingDialogService();
+ vm.Dialogs = dialogs;
await vm.DeleteTaskCommand.ExecuteAsync(null);
- Assert.Equal(fkMessage, reportedError);
+ Assert.Equal(fkMessage, dialogs.LastErrorMessage);
Assert.False(deleteFromListCalled);
Assert.False(closed);
}
@@ -136,12 +136,12 @@ public class DetailsIslandDeleteTaskTests : IDisposable
vm.DeleteFromList = _ => { deleteFromListCalled = true; return Task.CompletedTask; };
var closed = false;
vm.CloseDetail = () => closed = true;
- string? reportedError = null;
- vm.ShowErrorAsync = msg => { reportedError = msg; return Task.CompletedTask; };
+ var dialogs = new RecordingDialogService();
+ vm.Dialogs = dialogs;
await vm.DeleteTaskCommand.ExecuteAsync(null);
- Assert.Equal("Connection is not active.", reportedError);
+ Assert.Equal("Connection is not active.", dialogs.LastErrorMessage);
Assert.False(deleteFromListCalled);
Assert.False(closed);
}
diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandErrorFeedbackTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandErrorFeedbackTests.cs
index 940803d4..1aaa1858 100644
--- a/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandErrorFeedbackTests.cs
+++ b/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandErrorFeedbackTests.cs
@@ -164,7 +164,7 @@ public class DetailsIslandErrorFeedbackTests : IDisposable
var vm = BuildVm(worker);
vm.Bind(new TaskRowViewModel { Id = "task-reset-throw", Status = TaskStatus.Failed });
vm.Monitor.ApplyState(TaskStatus.Failed);
- vm.ConfirmAsync = _ => Task.FromResult(true);
+ vm.Dialogs = new RecordingDialogService { ConfirmResult = true };
string? reportedError = null;
vm.ErrorReported += msg => reportedError = msg;
diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandReviewActionsTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandReviewActionsTests.cs
index 28365936..075b89f0 100644
--- a/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandReviewActionsTests.cs
+++ b/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandReviewActionsTests.cs
@@ -153,12 +153,12 @@ public class DetailsIslandReviewActionsTests : IDisposable
vm.Bind(new TaskRowViewModel { Id = "task-err-1", Status = TaskStatus.WaitingForReview });
vm.Monitor.ApplyState(TaskStatus.WaitingForReview);
- string? reportedError = null;
- vm.ShowErrorAsync = msg => { reportedError = msg; return Task.CompletedTask; };
+ var dialogs = new RecordingDialogService();
+ vm.Dialogs = dialogs;
await vm.ApproveReviewCommand.ExecuteAsync(null);
- Assert.Equal(worker.ExceptionMessage, reportedError);
+ Assert.Equal(worker.ExceptionMessage, dialogs.LastErrorMessage);
Assert.False(vm.ApproveOp.IsRunning);
Assert.True(vm.ApproveReviewCommand.CanExecute(null));
}
diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/ListsIslandCreateListCancelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/ListsIslandCreateListCancelTests.cs
index 752b685d..eb1e9a2d 100644
--- a/tests/ClaudeDo.Ui.Tests/ViewModels/ListsIslandCreateListCancelTests.cs
+++ b/tests/ClaudeDo.Ui.Tests/ViewModels/ListsIslandCreateListCancelTests.cs
@@ -62,7 +62,7 @@ public class ListsIslandCreateListCancelTests : IDisposable
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 ConfirmAsync(string message) => Task.FromResult(true);
+ public Task ConfirmAsync(string message, string? confirmLabel = null) => Task.FromResult(true);
public Task ShowErrorAsync(string message) => Task.CompletedTask;
public void ShowMissionControl(MissionControlViewModel vm) => throw new NotImplementedException();
public void ShowCheatSheets(CheatSheetsViewModel vm) => throw new NotImplementedException();
diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/TasksIslandMoveToListTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/TasksIslandMoveToListTests.cs
index 34699d4a..3b758071 100644
--- a/tests/ClaudeDo.Ui.Tests/ViewModels/TasksIslandMoveToListTests.cs
+++ b/tests/ClaudeDo.Ui.Tests/ViewModels/TasksIslandMoveToListTests.cs
@@ -43,7 +43,7 @@ public class TasksIslandMoveToListTests : IDisposable
public int ConfirmCalls;
public string? LastMessage;
- public Task ConfirmAsync(string message)
+ public Task ConfirmAsync(string message, string? confirmLabel = null)
{
ConfirmCalls++;
LastMessage = message;