diff --git a/src/ClaudeDo.Data/Migrations/20260416064948_InitialCreate.cs b/src/ClaudeDo.Data/Migrations/20260416064948_InitialCreate.cs index 5d0f072..301b931 100644 --- a/src/ClaudeDo.Data/Migrations/20260416064948_InitialCreate.cs +++ b/src/ClaudeDo.Data/Migrations/20260416064948_InitialCreate.cs @@ -1,6 +1,4 @@ using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -10,8 +8,6 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace ClaudeDo.Data.Migrations { /// - [DbContext(typeof(ClaudeDoDbContext))] - [Migration("20260416064948_InitialCreate")] public partial class InitialCreate : Migration { /// diff --git a/src/ClaudeDo.Data/Migrations/20260420075929_AddTaskFlagsAndNotes.cs b/src/ClaudeDo.Data/Migrations/20260420075929_AddTaskFlagsAndNotes.cs index 27b2fe8..7546767 100644 --- a/src/ClaudeDo.Data/Migrations/20260420075929_AddTaskFlagsAndNotes.cs +++ b/src/ClaudeDo.Data/Migrations/20260420075929_AddTaskFlagsAndNotes.cs @@ -1,5 +1,3 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -7,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace ClaudeDo.Data.Migrations { /// - [DbContext(typeof(ClaudeDoDbContext))] - [Migration("20260420075929_AddTaskFlagsAndNotes")] public partial class AddTaskFlagsAndNotes : Migration { /// diff --git a/src/ClaudeDo.Data/Migrations/20260421113614_AddAppSettings.cs b/src/ClaudeDo.Data/Migrations/20260421113614_AddAppSettings.cs index 99eef30..083c4cb 100644 --- a/src/ClaudeDo.Data/Migrations/20260421113614_AddAppSettings.cs +++ b/src/ClaudeDo.Data/Migrations/20260421113614_AddAppSettings.cs @@ -1,14 +1,10 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace ClaudeDo.Data.Migrations { /// - [DbContext(typeof(ClaudeDoDbContext))] - [Migration("20260421113614_AddAppSettings")] public partial class AddAppSettings : Migration { /// diff --git a/src/ClaudeDo.Data/Migrations/20260422120000_AddTaskSortOrder.cs b/src/ClaudeDo.Data/Migrations/20260422120000_AddTaskSortOrder.cs index 3f275bd..6918635 100644 --- a/src/ClaudeDo.Data/Migrations/20260422120000_AddTaskSortOrder.cs +++ b/src/ClaudeDo.Data/Migrations/20260422120000_AddTaskSortOrder.cs @@ -1,5 +1,3 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -7,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace ClaudeDo.Data.Migrations { /// - [DbContext(typeof(ClaudeDoDbContext))] - [Migration("20260422120000_AddTaskSortOrder")] public partial class AddTaskSortOrder : Migration { /// diff --git a/src/ClaudeDo.Data/Migrations/20260423154708_AddPlanningSupport.cs b/src/ClaudeDo.Data/Migrations/20260423154708_AddPlanningSupport.cs index 9efb30a..56e0468 100644 --- a/src/ClaudeDo.Data/Migrations/20260423154708_AddPlanningSupport.cs +++ b/src/ClaudeDo.Data/Migrations/20260423154708_AddPlanningSupport.cs @@ -1,15 +1,10 @@ -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace ClaudeDo.Data.Migrations { /// - [DbContext(typeof(ClaudeDoDbContext))] - [Migration("20260423154708_AddPlanningSupport")] public partial class AddPlanningSupport : Migration { /// diff --git a/src/ClaudeDo.Ui/Services/ForegroundHelper.cs b/src/ClaudeDo.Ui/Services/ForegroundHelper.cs new file mode 100644 index 0000000..240417c --- /dev/null +++ b/src/ClaudeDo.Ui/Services/ForegroundHelper.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; + +namespace ClaudeDo.Ui.Services; + +internal static class ForegroundHelper +{ + private const int ASFW_ANY = -1; + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool AllowSetForegroundWindow(int dwProcessId); + + // Grants any process the right to take foreground on next SetForegroundWindow call. + // Used before RPCs that cause a helper process (e.g. wt.exe) to spawn a new window. + public static void AllowAny() + { + if (!OperatingSystem.IsWindows()) return; + try { AllowSetForegroundWindow(ASFW_ANY); } catch { } + } +} diff --git a/src/ClaudeDo.Ui/Services/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/IWorkerClient.cs index 08cf2fc..b6b04c5 100644 --- a/src/ClaudeDo.Ui/Services/IWorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/IWorkerClient.cs @@ -2,6 +2,10 @@ namespace ClaudeDo.Ui.Services; public interface IWorkerClient { + event Action? TaskUpdatedEvent; + event Action? WorktreeUpdatedEvent; + event Action? TaskMessageEvent; + Task WakeQueueAsync(); Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default); Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default); diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs index 082ec33..da4c1fd 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs @@ -5,8 +5,8 @@ namespace ClaudeDo.Ui.ViewModels.Islands; public sealed partial class ListNavItemViewModel : ViewModelBase { public required string Id { get; init; } - public required string Name { get; init; } public required ListKind Kind { get; init; } + [ObservableProperty] private string _name = ""; [ObservableProperty] private int _count; [ObservableProperty] private bool _isActive; [ObservableProperty] private string? _workingDir; diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs index 430e05a..a47e1e3 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs @@ -40,8 +40,9 @@ public sealed partial class ListsIslandViewModel : ViewModelBase private async System.Threading.Tasks.Task OpenListSettingsAsync(ListNavItemViewModel? row) { if (row is null || ShowListSettingsModal is null || _services is null) return; + var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id; var vm = _services.GetRequiredService(); - await vm.LoadAsync(row.Id, row.Name, row.WorkingDir, row.DefaultCommitType); + await vm.LoadAsync(rawId, row.Name, row.WorkingDir, row.DefaultCommitType); await ShowListSettingsModal(vm); await RefreshRowAsync(row.Id); } @@ -169,6 +170,46 @@ public sealed partial class ListsIslandViewModel : ViewModelBase [RelayCommand] private void Select(ListNavItemViewModel item) => SelectedList = item; + [RelayCommand] + private async Task CreateListAsync() + { + var entity = new ListEntity + { + Id = Guid.NewGuid().ToString("N"), + Name = "New list", + DefaultCommitType = "chore", + CreatedAt = DateTime.UtcNow, + }; + + await using (var ctx = await _dbFactory.CreateDbContextAsync()) + { + var lists = new ListRepository(ctx); + await lists.AddAsync(entity); + } + + var item = new ListNavItemViewModel + { + Id = $"user:{entity.Id}", + Name = entity.Name, + Kind = ListKind.User, + IconKey = "Folder", + DotColorKey = "Moss", + WorkingDir = entity.WorkingDir, + DefaultCommitType = entity.DefaultCommitType, + }; + Items.Add(item); + UserLists.Add(item); + SelectedList = item; + + if (ShowListSettingsModal is not null && _services is not null) + { + var vm = _services.GetRequiredService(); + await vm.LoadAsync(entity.Id, entity.Name, entity.WorkingDir, entity.DefaultCommitType); + await ShowListSettingsModal(vm); + await RefreshRowAsync(item.Id); + } + } + partial void OnSelectedListChanged(ListNavItemViewModel? value) { foreach (var i in Items) i.IsActive = ReferenceEquals(i, value); @@ -188,6 +229,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase var entity = await lists.GetByIdAsync(rawId); if (entity is null) return; + row.Name = entity.Name; row.WorkingDir = entity.WorkingDir; row.DefaultCommitType = entity.DefaultCommitType; } diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs index 3d4a5bc..5f5142f 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs @@ -101,25 +101,27 @@ public sealed partial class TaskRowViewModel : ViewModelBase partial void OnDiffDeletionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffDeletionsText)); } public static TaskRowViewModel FromEntity(TaskEntity t) + { + var row = new TaskRowViewModel { Id = t.Id, CreatedAt = t.CreatedAt }; + row.UpdateFromEntity(t); + return row; + } + + public void UpdateFromEntity(TaskEntity t) { var (add, del) = ParseDiffStat(t.Worktree?.DiffStat); - return new TaskRowViewModel - { - Id = t.Id, - Title = t.Title, - ListName = t.List?.Name ?? "", - Done = t.Status == TaskStatus.Done, - IsStarred = t.IsStarred, - IsMyDay = t.IsMyDay, - Status = t.Status, - Branch = t.Worktree?.BranchName, - DiffStat = t.Worktree?.DiffStat, - ScheduledFor = t.ScheduledFor, - DiffAdditions = add, - DiffDeletions = del, - CreatedAt = t.CreatedAt, - ParentTaskId = t.ParentTaskId, - }; + Title = t.Title; + ListName = t.List?.Name ?? ""; + Done = t.Status == TaskStatus.Done; + IsStarred = t.IsStarred; + IsMyDay = t.IsMyDay; + Status = t.Status; + Branch = t.Worktree?.BranchName; + DiffStat = t.Worktree?.DiffStat; + ScheduledFor = t.ScheduledFor; + DiffAdditions = add; + DiffDeletions = del; + ParentTaskId = t.ParentTaskId; } // Best-effort parse of diff stat strings like "+12 -3" or "12 additions, 3 deletions". diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs index baa02c9..af83223 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs @@ -49,6 +49,70 @@ public sealed partial class TasksIslandViewModel : ViewModelBase { _dbFactory = dbFactory; _worker = worker; + if (_worker is not null) + { + _worker.TaskUpdatedEvent += OnWorkerTaskUpdated; + _worker.WorktreeUpdatedEvent += OnWorkerTaskUpdated; + _worker.TaskMessageEvent += OnWorkerTaskMessage; + } + } + + private void OnWorkerTaskMessage(string taskId, string line) + { + var row = Items.FirstOrDefault(r => r.Id == taskId); + if (row is not null) row.LiveTail = line; + } + + private async void OnWorkerTaskUpdated(string taskId) + { + var list = _currentList; + if (list is null) return; + + try + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var entity = await db.Tasks + .Include(t => t.List) + .Include(t => t.Worktree) + .FirstOrDefaultAsync(t => t.Id == taskId); + + var existing = Items.FirstOrDefault(r => r.Id == taskId); + + if (entity is null) + { + if (existing is not null) Items.Remove(existing); + } + else + { + var matches = TaskMatchesList(entity, list); + if (existing is not null && matches) existing.UpdateFromEntity(entity); + else if (existing is not null) Items.Remove(existing); + else if (matches) { LoadForList(list); return; } + else return; + } + + Regroup(); + UpdateSubtitle(); + } + catch { } + } + + private static bool TaskMatchesList(TaskEntity t, ListNavItemViewModel list) => list.Kind switch + { + ListKind.Smart when list.Id == "smart:my-day" => t.IsMyDay, + ListKind.Smart when list.Id == "smart:important" => t.IsStarred, + ListKind.Smart when list.Id == "smart:planned" => t.ScheduledFor != null, + ListKind.Virtual when list.Id == "virtual:queued" => t.Status == TaskStatus.Queued, + ListKind.Virtual when list.Id == "virtual:running" => t.Status == TaskStatus.Running, + ListKind.Virtual when list.Id == "virtual:review" => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active, + ListKind.User => $"user:{t.ListId}" == list.Id, + _ => false, + }; + + private void OnCurrentListPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ListNavItemViewModel.Name) && sender is ListNavItemViewModel vm) + HeaderTitle = vm.Name; } public void LoadForList(ListNavItemViewModel? list) @@ -58,7 +122,12 @@ public sealed partial class TasksIslandViewModel : ViewModelBase _loadCts = new CancellationTokenSource(); var ct = _loadCts.Token; + if (_currentList is not null) + _currentList.PropertyChanged -= OnCurrentListPropertyChanged; _currentList = list; + if (_currentList is not null) + _currentList.PropertyChanged += OnCurrentListPropertyChanged; + Items.Clear(); OverdueItems.Clear(); OpenItems.Clear(); @@ -385,6 +454,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase private async Task OpenPlanningSessionAsync(TaskRowViewModel? row) { if (row is null || row.Status != TaskStatus.Manual) return; + ForegroundHelper.AllowAny(); try { await _worker!.StartPlanningSessionAsync(row.Id); } catch { } } @@ -412,6 +482,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase switch (choice) { case UnfinishedPlanningModalResult.Resume: + ForegroundHelper.AllowAny(); await _worker.ResumePlanningSessionAsync(row.Id); break; case UnfinishedPlanningModalResult.FinalizeNow: diff --git a/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml b/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml index 89f09d5..7d34843 100644 --- a/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml @@ -166,7 +166,8 @@ -