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.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 { public ListsIslandViewModel? Lists { get; } public TasksIslandViewModel? Tasks { get; } public DetailsIslandViewModel? Details { get; } public WorkerClient? Worker { get; } public UpdateCheckService UpdateCheck => _updateCheck; public string ConnectionText => Worker?.IsConnected == true ? "Online" : Worker?.IsReconnecting == true ? "Connecting…" : "Offline"; public bool IsOffline => Worker?.IsConnected != true && Worker?.IsReconnecting != true; private readonly UpdateCheckService _updateCheck; private readonly InstallerLocator _installerLocator; private readonly IDbContextFactory? _dbFactory; // Set by MainWindow to open the conflict resolution dialog. public Func? ShowConflictDialog { get; set; } // Set by MainWindow to open the About dialog. public Func? ShowAboutModal { 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 }; [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; string worktreePath = System.Environment.CurrentDirectory; string targetBranch = Worker?.LastMergeAllTarget ?? "main"; try { await using var ctx = await _dbFactory.CreateDbContextAsync(); var entity = await ctx.Tasks .Include(t => t.Worktree) .AsNoTracking() .FirstOrDefaultAsync(t => t.Id == subtaskId); if (entity != null) { subtaskTitle = entity.Title; if (entity.Worktree?.Path is { } p) worktreePath = p; } } catch { /* Non-fatal: fall back to subtaskId and cwd */ } var vm = new ConflictResolutionViewModel( Worker!, planningTaskId, subtaskTitle, targetBranch, conflictedFiles, worktreePath); await ShowConflictDialog(vm); } // For tests only — does NOT wire up events. internal IslandsShellViewModel() { } public IslandsShellViewModel( ListsIslandViewModel lists, TasksIslandViewModel tasks, DetailsIslandViewModel details, WorkerClient worker, UpdateCheckService updateCheck, InstallerLocator installerLocator, IDbContextFactory dbFactory) { Lists = lists; Tasks = tasks; Details = details; Worker = worker; _updateCheck = updateCheck; _installerLocator = installerLocator; _dbFactory = dbFactory; Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList); Tasks.SelectionChanged += (_, _) => Details.Bind(Tasks.SelectedTask); 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; }; Worker.PropertyChanged += (_, e) => { if (e.PropertyName is nameof(WorkerClient.IsConnected) or nameof(WorkerClient.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); _ = 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 { } }); } 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); } [RelayCommand] private async Task CheckForUpdatesAsync() { await _updateCheck.CheckNowAsync(CancellationToken.None); } [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. } } }