using Avalonia.Threading; using System; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Islands; using ClaudeDo.Ui.ViewModels.Modals; using ClaudeDo.Ui.ViewModels.Planning; using Microsoft.EntityFrameworkCore; namespace ClaudeDo.Ui.ViewModels; public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable { public ListsIslandViewModel? Lists { get; } public TasksIslandViewModel? Tasks { get; } public DetailsIslandViewModel? Details { get; } public IWorkerClient? Worker { get; } public UpdateCheckService UpdateCheck => _updateCheck; public string ConnectionText => Worker?.IsConnected == true ? Loc.T("vm.connection.online") : Worker?.IsReconnecting == true ? Loc.T("vm.connection.connecting") : Loc.T("vm.connection.offline"); public bool IsOffline => Worker?.IsConnected != true && Worker?.IsReconnecting != true; private readonly UpdateCheckService _updateCheck = null!; private readonly InstallerLocator _installerLocator = null!; private readonly WorkerLocator _workerLocator = null!; private readonly IDbContextFactory? _dbFactory; private readonly Func _worktreesOverviewVmFactory = () => null!; private readonly Func _weeklyReportVmFactory = () => null!; private readonly Func _mergeVmFactory = () => null!; private readonly Func? _repoImportVmFactory; public Func ResolveMergeVm => _mergeVmFactory; // Set by MainWindow to open the conflict resolution dialog. public Func? ShowConflictDialog { get; set; } // Layer C seam: composition root sets the factory; MainWindow sets the dialog opener. // The integrator connects Layer A/B's RequestConflictResolution(taskId, target) to this method. public Func? ConflictResolverFactory { get; set; } public Func? ShowConflictResolver { get; set; } public async Task RequestConflictResolutionAsync(string taskId, string targetBranch) { if (ConflictResolverFactory is null || ShowConflictResolver is null) return; var vm = ConflictResolverFactory(taskId); var hasConflicts = await vm.OpenAsync(targetBranch); if (hasConflicts) await ShowConflictResolver(vm); } // Set by MainWindow to open the About dialog. public Func? ShowAboutModal { get; set; } // Set by MainWindow to open the repo-import dialog. public Func? ShowRepoImportModal { get; set; } // Set by MainWindow to open the global worktrees overview dialog. public Func? ShowWorktreesOverviewModal { get; set; } // Set by MainWindow to open the weekly report dialog. public Func? ShowWeeklyReportModal { get; set; } // Set by MainWindow to open the worker-connection help dialog. public Func? ShowWorkerConnectionModal { get; set; } [ObservableProperty] private bool _isUpdateBannerVisible; [ObservableProperty] private string? _updateBannerLatestVersion; [ObservableProperty] private string? _inlineUpdateStatus; private bool _bannerDismissedThisSession; [ObservableProperty] private double _windowWidth = 1280; [ObservableProperty] private string? _workerLogText; [ObservableProperty] private WorkerLogLevel _workerLogLevel; [ObservableProperty] private bool _isWorkerLogVisible; public bool ShowDetails => WindowWidth >= 1100; public bool ShowLists => WindowWidth >= 780; private readonly System.Timers.Timer _clearTimer = new(30_000) { AutoReset = false }; private readonly System.Timers.Timer _connectTimer = new(12_000) { AutoReset = false }; [ObservableProperty] private string? _primeStatus; private readonly System.Timers.Timer _primeStatusTimer = new(5_000) { AutoReset = false }; [RelayCommand] private void FocusSearch() => Lists?.RequestFocusSearch(); [RelayCommand] private void FocusAddTask() => Tasks?.RequestFocusAddTask(); public async Task ToggleSelectedDoneAsync() { if (Tasks?.SelectedTask is { } row) await Tasks.ToggleDoneCommand.ExecuteAsync(row); } partial void OnWindowWidthChanged(double value) { OnPropertyChanged(nameof(ShowDetails)); OnPropertyChanged(nameof(ShowLists)); } public void OnWorkerLogReceived(WorkerLogEntry entry) { var hhmm = entry.TimestampUtc.ToLocalTime().ToString("HH:mm"); WorkerLogText = $"{hhmm} · {entry.Message}"; WorkerLogLevel = entry.Level; IsWorkerLogVisible = true; _clearTimer.Stop(); _clearTimer.Start(); } public void ClearWorkerLog() { IsWorkerLogVisible = false; WorkerLogText = null; } private void OnPrimeFired(PrimeFiredEvent evt) { var when = evt.FiredAt.LocalDateTime.ToString("HH:mm"); PrimeStatus = evt.Success ? $"✓ Primed Claude at {when}" : $"⚠ Prime failed: {evt.Message}"; _primeStatusTimer.Stop(); _primeStatusTimer.Start(); } private void OnPlanningMergeConflict(string planningTaskId, string subtaskId, IReadOnlyList conflictedFiles) { // Already on UI thread (WorkerClient dispatches via Dispatcher.UIThread.Post). _ = OpenConflictDialogAsync(planningTaskId, subtaskId, conflictedFiles); } private async Task OpenConflictDialogAsync(string planningTaskId, string subtaskId, IReadOnlyList conflictedFiles) { if (ShowConflictDialog == null || _dbFactory == null) return; string subtaskTitle = subtaskId; // The conflict lives in the list's working dir (the repo being merged into), // not the subtask worktree. VS Code must open this folder to show the merge UI. string repoDirectory = System.Environment.CurrentDirectory; string targetBranch = Worker?.LastApproveTarget ?? "main"; try { await using var ctx = await _dbFactory.CreateDbContextAsync(); var entity = await ctx.Tasks .Include(t => t.List) .AsNoTracking() .FirstOrDefaultAsync(t => t.Id == subtaskId); if (entity != null) { subtaskTitle = entity.Title; if (entity.List?.WorkingDir is { } dir && !string.IsNullOrWhiteSpace(dir)) repoDirectory = dir; } } catch { /* Non-fatal: fall back to subtaskId and cwd */ } var vm = new ConflictResolutionViewModel( Worker!, planningTaskId, subtaskTitle, targetBranch, conflictedFiles, repoDirectory); await ShowConflictDialog(vm); } // For tests only — does NOT wire up events. internal IslandsShellViewModel() { } public IslandsShellViewModel( ListsIslandViewModel lists, TasksIslandViewModel tasks, DetailsIslandViewModel details, IWorkerClient worker, UpdateCheckService updateCheck, InstallerLocator installerLocator, WorkerLocator workerLocator, IDbContextFactory dbFactory, Func worktreesOverviewVmFactory, Func weeklyReportVmFactory, Func mergeVmFactory, Func repoImportVmFactory) { Lists = lists; Tasks = tasks; Details = details; Worker = worker; _updateCheck = updateCheck; _installerLocator = installerLocator; _workerLocator = workerLocator; _dbFactory = dbFactory; _worktreesOverviewVmFactory = worktreesOverviewVmFactory; _weeklyReportVmFactory = weeklyReportVmFactory; _mergeVmFactory = mergeVmFactory; _repoImportVmFactory = repoImportVmFactory; Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList); Tasks.SelectionChanged += (_, _) => Details.Bind(Tasks.SelectedTask); Tasks.NotesRequested += () => Details.ShowNotes(); Tasks.PrepRequested += () => Details.ShowPrep(); Tasks.TasksChanged += (_, _) => _ = Lists.RefreshCountsAsync(); Tasks.OpenListSettingsRequested += (_, _) => { if (Lists.SelectedList is { } row) Lists.OpenListSettingsCommand.Execute(row); }; Details.CloseDetail = () => Tasks.SelectedTask = null; Details.DeleteFromList = row => { Tasks.LoadForList(Lists.SelectedList); _ = Lists.RefreshCountsAsync(); return System.Threading.Tasks.Task.CompletedTask; }; Details.RequestConflictResolution = RequestConflictResolutionAsync; Worker.PropertyChanged += (_, e) => { if (e.PropertyName is nameof(IWorkerClient.IsConnected) or nameof(IWorkerClient.IsReconnecting)) { OnPropertyChanged(nameof(ConnectionText)); OnPropertyChanged(nameof(IsOffline)); } }; Worker.WorkerLogReceivedEvent += OnWorkerLogReceived; Worker.PlanningMergeConflictEvent += OnPlanningMergeConflict; Worker.PrimeFired += OnPrimeFired; _clearTimer.Elapsed += (_, _) => { if (Dispatcher.UIThread.CheckAccess()) ClearWorkerLog(); else Dispatcher.UIThread.Post(ClearWorkerLog); }; _primeStatusTimer.Elapsed += (_, _) => Avalonia.Threading.Dispatcher.UIThread.Post(() => PrimeStatus = null); _connectTimer.Elapsed += (_, _) => Dispatcher.UIThread.Post(() => { if (DecideShowConnectionPrompt(IsOffline)) _ = OpenWorkerConnectionHelpAsync(); }); _connectTimer.Start(); _ = Lists.LoadAsync(); _updateCheck.PropertyChanged += (_, e) => { if (e.PropertyName == nameof(UpdateCheckService.LastCheckStatus)) { RefreshBannerFromStatus(); } }; // Fire-and-forget startup check — never block UI. _ = Task.Run(async () => { try { await _updateCheck.CheckNowAsync(CancellationToken.None); } catch { } }); } public void Dispose() { _clearTimer.Stop(); _clearTimer.Dispose(); _connectTimer.Stop(); _connectTimer.Dispose(); _primeStatusTimer.Stop(); _primeStatusTimer.Dispose(); } private void RefreshBannerFromStatus() { switch (_updateCheck.LastCheckStatus) { case UpdateCheckStatus.UpdateAvailable: if (_bannerDismissedThisSession) { IsUpdateBannerVisible = false; break; } UpdateBannerLatestVersion = _updateCheck.LatestVersion; IsUpdateBannerVisible = true; InlineUpdateStatus = null; break; case UpdateCheckStatus.UpToDate: IsUpdateBannerVisible = false; ShowInlineStatus($"You're up to date (v{_updateCheck.CurrentVersion})"); break; case UpdateCheckStatus.CheckFailed: ShowInlineStatus("Could not check for updates"); break; } } private async void ShowInlineStatus(string text) { InlineUpdateStatus = text; await Task.Delay(3000); if (InlineUpdateStatus == text) InlineUpdateStatus = null; } [RelayCommand] private async Task OpenAbout() { var vm = new AboutModalViewModel(); if (ShowAboutModal is not null) await ShowAboutModal(vm); } private bool _connectionPromptShown; internal bool DecideShowConnectionPrompt(bool isOffline) { if (!isOffline) return false; if (_connectionPromptShown) return false; _connectionPromptShown = true; return true; } private async Task OpenWorkerConnectionHelpAsync() { var vm = new WorkerConnectionModalViewModel(_workerLocator, _installerLocator); if (ShowWorkerConnectionModal is not null) await ShowWorkerConnectionModal(vm); } [RelayCommand] private Task OpenWorkerConnectionHelp() => OpenWorkerConnectionHelpAsync(); [RelayCommand] private async Task OpenRepoImport() { if (ShowRepoImportModal is null || _repoImportVmFactory is null) return; var vm = _repoImportVmFactory(); await vm.LoadAsync(); await ShowRepoImportModal(vm); if (Lists is not null) await Lists.LoadAsync(); } private bool _worktreesOverviewOpen; [RelayCommand] private async Task OpenWorktreesOverviewGlobalAsync() { if (ShowWorktreesOverviewModal is null || _worktreesOverviewOpen) return; _worktreesOverviewOpen = true; try { var vm = _worktreesOverviewVmFactory(); vm.Configure(null, null); await vm.LoadAsync(); await ShowWorktreesOverviewModal(vm); } finally { _worktreesOverviewOpen = false; } } private bool _weeklyReportOpen; [RelayCommand] private async Task OpenWeeklyReport() { if (ShowWeeklyReportModal is null || _weeklyReportOpen) return; _weeklyReportOpen = true; try { var vm = _weeklyReportVmFactory(); await vm.InitializeAsync(); await ShowWeeklyReportModal(vm); } finally { _weeklyReportOpen = false; } } [RelayCommand] private async Task CheckForUpdatesAsync() { await _updateCheck.CheckNowAsync(CancellationToken.None); } [ObservableProperty] private string? _restartWorkerStatus; [RelayCommand] private async Task RestartWorkerAsync() { RestartWorkerStatus = Loc.T("vm.shell.restartingWorker"); try { await Task.Run(RestartWorkerService); await FlashRestartStatusAsync("Worker restarted."); } catch (Exception ex) { await FlashRestartStatusAsync($"Restart failed: {ex.Message}"); } } private void RestartWorkerService() { 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). var exeFull = System.IO.Path.GetFullPath(exe); foreach (var p in System.Diagnostics.Process.GetProcessesByName("ClaudeDo.Worker")) { try { var path = p.MainModule?.FileName; if (path is not null && !string.Equals(System.IO.Path.GetFullPath(path), exeFull, StringComparison.OrdinalIgnoreCase)) continue; p.Kill(entireProcessTree: true); p.WaitForExit(10000); } catch { /* may have exited or be inaccessible */ } finally { p.Dispose(); } } System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(exe) { UseShellExecute = true }); } private async Task FlashRestartStatusAsync(string text) { RestartWorkerStatus = text; await Task.Delay(3000); if (RestartWorkerStatus == text) RestartWorkerStatus = null; } [RelayCommand] private void DismissBanner() { _bannerDismissedThisSession = true; IsUpdateBannerVisible = false; } [RelayCommand] private void UpdateNow() { var path = _installerLocator.Find(); if (path is null) return; try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true }); Environment.Exit(0); } catch { // Intentionally silent — if this fails there's nothing useful to show. } } }