using System; using System.Linq; using System.Threading.Tasks; using Avalonia; 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; using ClaudeDo.Ui.ViewModels.Modals; using ClaudeDo.Ui.Views.Conflicts; using ClaudeDo.Ui.Views.MissionControl; using ClaudeDo.Ui.Views.Modals; namespace ClaudeDo.Ui.Views; /// /// Window-backed . Owns every modal-view construction, the /// shared Confirm/Error dialogs, and the worktrees-overview sub-callbacks that reach back /// into the shell (jump-to-task, merge, conflict resolution). Created by /// with itself as the owner window; the shell resolves lazily from the owner's DataContext so /// the service and shell don't form a construction cycle. /// public sealed class WindowDialogService : IDialogService { private readonly Window _owner; private MissionControlWindow? _missionControl; private DiffViewerView? _diffViewerWindow; private CheatSheetsWindow? _cheatSheets; public WindowDialogService(Window owner) => _owner = owner; private IslandsShellViewModel? Shell => _owner.DataContext as IslandsShellViewModel; // Own modals to the window the user is actually looking at (e.g. the Mission Control // window when Settings is opened from there), falling back to the main window. private Window ActiveOwner() { if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { var active = desktop.Windows.FirstOrDefault(w => w.IsActive); if (active is not null) return active; } return _owner; } public async Task ShowAboutAsync(AboutModalViewModel vm) { var dlg = new AboutModalView { DataContext = vm }; vm.CloseAction = () => dlg.Close(); await dlg.ShowDialog(_owner); } public async Task ShowWeeklyReportAsync(WeeklyReportModalViewModel vm) { var dlg = new WeeklyReportModalView { DataContext = vm }; vm.CloseAction = () => dlg.Close(); await dlg.ShowDialog(_owner); } public async Task ShowUsageMonitorAsync(UsageMonitorModalViewModel vm) { var dlg = new UsageMonitorModalView { DataContext = vm }; vm.CloseAction = () => dlg.Close(); // The pill sits in both the footer and the Mission Control header, so own the dialog to // whichever window is active — otherwise it opens behind Mission Control. await dlg.ShowDialog(ActiveOwner()); } public async Task ShowSettingsAsync(SettingsModalViewModel vm) { vm.Worktrees.ConfirmAsync = message => ConfirmAsync(message); var dlg = new SettingsModalView { DataContext = vm }; vm.RequestRepoImport = () => { dlg.Close(); if (Shell is not null) _ = Shell.OpenRepoImportCommand.ExecuteAsync(null); }; vm.RequestUsageMonitor = () => { dlg.Close(); if (Shell is not null) _ = Shell.OpenUsageMonitorCommand.ExecuteAsync(null); }; await dlg.ShowDialog(ActiveOwner()); } public async Task ShowListSettingsAsync(ListSettingsModalViewModel vm) { var dlg = new ListSettingsModalView { DataContext = vm }; vm.CloseAction = () => dlg.Close(); vm.ConfirmAsync = message => ConfirmAsync(message); vm.ShowErrorAsync = ShowErrorAsync; await dlg.ShowDialog(_owner); } public async Task ShowRepoImportAsync(RepoImportModalViewModel vm) { var dlg = new RepoImportModalView { DataContext = vm }; vm.CloseAction = () => dlg.Close(); await dlg.ShowDialog(_owner); } public async Task?> ShowMergeHelperSelectionAsync(MergeHelperSelectionModalViewModel vm) { var dlg = new MergeHelperSelectionModal { DataContext = vm }; vm.CloseAction = () => dlg.Close(); dlg.Closed += (_, _) => { vm.StopReconcileTick(); vm.Result.TrySetResult(null); // native close counts as cancel }; await dlg.ShowDialog(_owner); return await vm.Result.Task; } public async Task ShowWorktreesOverviewAsync(WorktreesOverviewModalViewModel vm) { var dlg = new WorktreesOverviewModalView { DataContext = vm }; vm.CloseAction = () => dlg.Close(); dlg.Closed += (_, _) => vm.StopReconcileTick(); // native close bypasses CloseCommand vm.JumpToTaskAction = (listId, taskId) => { if (Shell is { } s) _ = JumpToTaskHelper.SelectAsync(s, listId, taskId); }; vm.ShowDiffAction = diffVm => { _ = diffVm.LoadAsync(); // Non-modal: must not block the (still-open) worktrees overview or the main // window behind it. Reuse the one open window rather than stacking a second. if (_diffViewerWindow is { } existing) { existing.DataContext = diffVm; diffVm.CloseAction = () => existing.Close(); if (existing.WindowState == WindowState.Minimized) existing.WindowState = WindowState.Normal; existing.Activate(); return; } var diffDlg = new DiffViewerView { DataContext = diffVm }; diffVm.CloseAction = () => diffDlg.Close(); diffDlg.Closed += (_, _) => _diffViewerWindow = null; _diffViewerWindow = diffDlg; diffDlg.Show(_owner); }; vm.ConfirmAction = message => ConfirmAsync(message); if (Shell is { } shell) { vm.ResolveMergeVm = shell.ResolveMergeVm; vm.ShowMergeAction = async mergeVm => { var mergeDlg = new MergeModalView { DataContext = mergeVm }; await mergeDlg.ShowDialog(_owner); }; } await dlg.ShowDialog(_owner); } public async Task ShowWorkerConnectionAsync(WorkerConnectionModalViewModel vm) { var dlg = new WorkerConnectionModalView { DataContext = vm }; vm.CloseAction = () => dlg.Close(); await dlg.ShowDialog(_owner); } public async Task ShowConflictResolverAsync(ConflictResolverViewModel vm) { var dlg = new ConflictResolverView { DataContext = vm }; await dlg.ShowDialog(_owner); } public async Task ShowLogVisualizerAsync(LogVisualizerViewModel vm) { var dlg = new LogVisualizerView { DataContext = vm }; vm.CloseAction = () => dlg.Close(); dlg.Closed += (_, _) => vm.StopReconcileTick(); // native close bypasses CloseCommand await dlg.ShowDialog(_owner); } public void ShowMissionControl(MissionControlViewModel vm) { _missionControl ??= new MissionControlWindow { DataContext = vm }; if (!_missionControl.IsVisible) _missionControl.Show(); // modeless, independent top-level window // Activate() alone does not restore a minimised window — it stays in the taskbar and the // button looks dead. Un-minimise first, then take focus. if (_missionControl.WindowState == WindowState.Minimized) _missionControl.WindowState = WindowState.Normal; _missionControl.Activate(); // bring to front / focus } public void ShowCheatSheets(CheatSheetsViewModel vm) { // Modeless and single-instance, like Mission Control: the point is to park it beside the // shell, so a second window (or a modal that blocks the shell) would defeat it. if (_cheatSheets is null) { _cheatSheets = new CheatSheetsWindow { DataContext = vm }; vm.CloseAction = () => _cheatSheets?.Close(); _cheatSheets.Closed += (_, _) => _cheatSheets = null; _cheatSheets.Show(_owner); } if (_cheatSheets.WindowState == WindowState.Minimized) _cheatSheets.WindowState = WindowState.Normal; _cheatSheets.Activate(); } public Task ConfirmAsync(string message, string? confirmLabel = null) { var tcs = new TaskCompletionSource(); 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(Loc.T("dialog.confirmTitle"), 380, message, 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 tcs.Task; } public Task ShowErrorAsync(string 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, HorizontalAlignment = HorizontalAlignment.Right, Spacing = 8, Children = { ok }, }); ok.Click += (_, _) => dialog.Close(); return dialog.ShowDialog(_owner); } private Window NoticeWindow(string title, double width, string message, StackPanel buttons) => new() { Title = title, Width = width, SizeToContent = SizeToContent.Height, CanResize = false, WindowStartupLocation = WindowStartupLocation.CenterOwner, ShowInTaskbar = false, Background = _owner.FindResource("SurfaceBrush") as IBrush, Content = new StackPanel { Margin = new Thickness(20), Spacing = 16, Children = { new TextBlock { Text = message, TextWrapping = TextWrapping.Wrap }, buttons, }, }, }; }