From ab44ba5e418f91307b939235691e435e4ea52570 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Mon, 1 Jun 2026 15:28:17 +0200 Subject: [PATCH] feat(ui): list reordering, quick actions, and resizable modals - Drag-to-reorder user lists in the sidebar, persisted via a new list sort_order column (AddListSortOrder migration, backfilled by creation time) and ListRepository.ReorderAsync - "Open in Explorer" / "Open in Terminal" context-menu actions on lists - "Clear all completed" button on the Tasks island - Inline-edit subtask titles (empty text deletes the step) and click-to-copy task ID in the Details island - Make modal and planning windows resizable (BorderOnly decorations with min sizes) instead of fixed-size borderless Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Configuration/ListEntityConfiguration.cs | 3 + ...0260601114247_AddListSortOrder.Designer.cs | 600 ++++++++++++++++++ .../20260601114247_AddListSortOrder.cs | 48 ++ .../ClaudeDoDbContextModelSnapshot.cs | 9 + src/ClaudeDo.Data/Models/ListEntity.cs | 1 + .../Repositories/ListRepository.cs | 14 +- src/ClaudeDo.Ui/Design/IslandStyles.axaml | 9 + .../Islands/DetailsIslandViewModel.cs | 30 + .../Islands/ListNavItemViewModel.cs | 2 + .../Islands/ListsIslandViewModel.cs | 97 +++ .../Islands/TasksIslandViewModel.cs | 32 + .../Views/Islands/DetailsIslandView.axaml | 34 +- .../Views/Islands/DetailsIslandView.axaml.cs | 29 + .../Views/Islands/ListsIslandView.axaml | 29 +- .../Views/Islands/ListsIslandView.axaml.cs | 127 ++++ .../Views/Islands/TasksIslandView.axaml | 13 +- .../Views/Modals/DiffModalView.axaml | 6 +- .../Views/Modals/ListSettingsModalView.axaml | 3 +- .../Views/Modals/MergeModalView.axaml | 7 +- .../Views/Modals/RepoImportModalView.axaml | 6 +- .../Views/Modals/SettingsModalView.axaml | 6 +- .../Modals/WorktreesOverviewModalView.axaml | 3 +- .../Planning/ConflictResolutionView.axaml | 6 +- .../Views/Planning/PlanningDiffView.axaml | 6 +- 24 files changed, 1093 insertions(+), 27 deletions(-) create mode 100644 src/ClaudeDo.Data/Migrations/20260601114247_AddListSortOrder.Designer.cs create mode 100644 src/ClaudeDo.Data/Migrations/20260601114247_AddListSortOrder.cs diff --git a/src/ClaudeDo.Data/Configuration/ListEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/ListEntityConfiguration.cs index 2c77187..cd48beb 100644 --- a/src/ClaudeDo.Data/Configuration/ListEntityConfiguration.cs +++ b/src/ClaudeDo.Data/Configuration/ListEntityConfiguration.cs @@ -16,6 +16,9 @@ public class ListEntityConfiguration : IEntityTypeConfiguration builder.Property(l => l.CreatedAt).HasColumnName("created_at").IsRequired(); builder.Property(l => l.WorkingDir).HasColumnName("working_dir"); builder.Property(l => l.DefaultCommitType).HasColumnName("default_commit_type").IsRequired().HasDefaultValue("chore"); + builder.Property(l => l.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0); + + builder.HasIndex(l => l.SortOrder).HasDatabaseName("idx_lists_sort"); builder.HasOne(l => l.Config) .WithOne(c => c.List) diff --git a/src/ClaudeDo.Data/Migrations/20260601114247_AddListSortOrder.Designer.cs b/src/ClaudeDo.Data/Migrations/20260601114247_AddListSortOrder.Designer.cs new file mode 100644 index 0000000..3cd274a --- /dev/null +++ b/src/ClaudeDo.Data/Migrations/20260601114247_AddListSortOrder.Designer.cs @@ -0,0 +1,600 @@ +// +using System; +using ClaudeDo.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ClaudeDo.Data.Migrations +{ + [DbContext(typeof(ClaudeDoDbContext))] + [Migration("20260601114247_AddListSortOrder")] + partial class AddListSortOrder + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.11"); + + modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b => + { + b.Property("Id") + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CentralWorktreeRoot") + .HasColumnType("TEXT") + .HasColumnName("central_worktree_root"); + + b.Property("DefaultClaudeInstructions") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("") + .HasColumnName("default_claude_instructions"); + + b.Property("DefaultMaxTurns") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(30) + .HasColumnName("default_max_turns"); + + b.Property("DefaultModel") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("sonnet") + .HasColumnName("default_model"); + + b.Property("DefaultPermissionMode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("bypassPermissions") + .HasColumnName("default_permission_mode"); + + b.Property("RepoImportFolders") + .HasColumnType("TEXT") + .HasColumnName("repo_import_folders"); + + b.Property("WorktreeAutoCleanupDays") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(7) + .HasColumnName("worktree_auto_cleanup_days"); + + b.Property("WorktreeAutoCleanupEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("worktree_auto_cleanup_enabled"); + + b.Property("WorktreeStrategy") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("sibling") + .HasColumnName("worktree_strategy"); + + b.HasKey("Id"); + + b.ToTable("app_settings", (string)null); + + b.HasData( + new + { + Id = 1, + DefaultClaudeInstructions = "", + DefaultMaxTurns = 100, + DefaultModel = "sonnet", + DefaultPermissionMode = "auto", + WorktreeAutoCleanupDays = 7, + WorktreeAutoCleanupEnabled = false, + WorktreeStrategy = "sibling" + }); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b => + { + b.Property("ListId") + .HasColumnType("TEXT") + .HasColumnName("list_id"); + + b.Property("AgentPath") + .HasColumnType("TEXT") + .HasColumnName("agent_path"); + + b.Property("Model") + .HasColumnType("TEXT") + .HasColumnName("model"); + + b.Property("SystemPrompt") + .HasColumnType("TEXT") + .HasColumnName("system_prompt"); + + b.HasKey("ListId"); + + b.ToTable("list_config", (string)null); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DefaultCommitType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("chore") + .HasColumnName("default_commit_type"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("WorkingDir") + .HasColumnType("TEXT") + .HasColumnName("working_dir"); + + b.HasKey("Id"); + + b.HasIndex("SortOrder") + .HasDatabaseName("idx_lists_sort"); + + b.ToTable("lists", (string)null); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true) + .HasColumnName("enabled"); + + b.Property("EndDate") + .HasColumnType("TEXT") + .HasColumnName("end_date"); + + b.Property("LastRunAt") + .HasColumnType("TEXT") + .HasColumnName("last_run_at"); + + b.Property("PromptOverride") + .HasColumnType("TEXT") + .HasColumnName("prompt_override"); + + b.Property("StartDate") + .HasColumnType("TEXT") + .HasColumnName("start_date"); + + b.Property("TimeOfDay") + .HasColumnType("TEXT") + .HasColumnName("time_of_day"); + + b.Property("WorkdaysOnly") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true) + .HasColumnName("workdays_only"); + + b.HasKey("Id"); + + b.ToTable("prime_schedules", (string)null); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Completed") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("completed"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("OrderNum") + .HasColumnType("INTEGER") + .HasColumnName("order_num"); + + b.Property("TaskId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("task_id"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("Id"); + + b.HasIndex("TaskId") + .HasDatabaseName("idx_subtasks_task_id"); + + b.ToTable("subtasks", (string)null); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AgentPath") + .HasColumnType("TEXT") + .HasColumnName("agent_path"); + + b.Property("BlockedByTaskId") + .HasColumnType("TEXT") + .HasColumnName("blocked_by_task_id"); + + b.Property("CommitType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("chore") + .HasColumnName("commit_type"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .HasColumnType("TEXT") + .HasColumnName("created_by"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasColumnName("description"); + + b.Property("FinishedAt") + .HasColumnType("TEXT") + .HasColumnName("finished_at"); + + b.Property("IsMyDay") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("is_my_day"); + + b.Property("IsStarred") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("is_starred"); + + b.Property("ListId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("list_id"); + + b.Property("LogPath") + .HasColumnType("TEXT") + .HasColumnName("log_path"); + + b.Property("Model") + .HasColumnType("TEXT") + .HasColumnName("model"); + + b.Property("Notes") + .HasColumnType("TEXT") + .HasColumnName("notes"); + + b.Property("ParentTaskId") + .HasColumnType("TEXT") + .HasColumnName("parent_task_id"); + + b.Property("PlanningFinalizedAt") + .HasColumnType("TEXT") + .HasColumnName("planning_finalized_at"); + + b.Property("PlanningPhase") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("none") + .HasColumnName("planning_phase"); + + b.Property("PlanningSessionId") + .HasColumnType("TEXT") + .HasColumnName("planning_session_id"); + + b.Property("PlanningSessionToken") + .HasColumnType("TEXT") + .HasColumnName("planning_session_token"); + + b.Property("Result") + .HasColumnType("TEXT") + .HasColumnName("result"); + + b.Property("ScheduledFor") + .HasColumnType("TEXT") + .HasColumnName("scheduled_for"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("StartedAt") + .HasColumnType("TEXT") + .HasColumnName("started_at"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("status"); + + b.Property("SystemPrompt") + .HasColumnType("TEXT") + .HasColumnName("system_prompt"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("Id"); + + b.HasIndex("BlockedByTaskId") + .HasDatabaseName("idx_tasks_blocked_by"); + + b.HasIndex("ListId") + .HasDatabaseName("idx_tasks_list_id"); + + b.HasIndex("ParentTaskId") + .HasDatabaseName("idx_tasks_parent_task_id"); + + b.HasIndex("Status") + .HasDatabaseName("idx_tasks_status"); + + b.HasIndex("ListId", "SortOrder") + .HasDatabaseName("idx_tasks_list_sort"); + + b.ToTable("tasks", (string)null); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("ErrorMarkdown") + .HasColumnType("TEXT") + .HasColumnName("error_markdown"); + + b.Property("ExitCode") + .HasColumnType("INTEGER") + .HasColumnName("exit_code"); + + b.Property("FinishedAt") + .HasColumnType("TEXT") + .HasColumnName("finished_at"); + + b.Property("IsRetry") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("is_retry"); + + b.Property("LogPath") + .HasColumnType("TEXT") + .HasColumnName("log_path"); + + b.Property("Prompt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("prompt"); + + b.Property("ResultMarkdown") + .HasColumnType("TEXT") + .HasColumnName("result_markdown"); + + b.Property("RunNumber") + .HasColumnType("INTEGER") + .HasColumnName("run_number"); + + b.Property("SessionId") + .HasColumnType("TEXT") + .HasColumnName("session_id"); + + b.Property("StartedAt") + .HasColumnType("TEXT") + .HasColumnName("started_at"); + + b.Property("StructuredOutputJson") + .HasColumnType("TEXT") + .HasColumnName("structured_output"); + + b.Property("TaskId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("task_id"); + + b.Property("TokensIn") + .HasColumnType("INTEGER") + .HasColumnName("tokens_in"); + + b.Property("TokensOut") + .HasColumnType("INTEGER") + .HasColumnName("tokens_out"); + + b.Property("TurnCount") + .HasColumnType("INTEGER") + .HasColumnName("turn_count"); + + b.HasKey("Id"); + + b.HasIndex("TaskId") + .HasDatabaseName("idx_task_runs_task_id"); + + b.ToTable("task_runs", (string)null); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b => + { + b.Property("TaskId") + .HasColumnType("TEXT") + .HasColumnName("task_id"); + + b.Property("BaseCommit") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("base_commit"); + + b.Property("BranchName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("branch_name"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DiffStat") + .HasColumnType("TEXT") + .HasColumnName("diff_stat"); + + b.Property("HeadCommit") + .HasColumnType("TEXT") + .HasColumnName("head_commit"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property("State") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("active") + .HasColumnName("state"); + + b.HasKey("TaskId"); + + b.ToTable("worktrees", (string)null); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b => + { + b.HasOne("ClaudeDo.Data.Models.ListEntity", "List") + .WithOne("Config") + .HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("List"); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b => + { + b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task") + .WithMany("Subtasks") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Task"); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b => + { + b.HasOne("ClaudeDo.Data.Models.TaskEntity", null) + .WithMany() + .HasForeignKey("BlockedByTaskId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ClaudeDo.Data.Models.ListEntity", "List") + .WithMany("Tasks") + .HasForeignKey("ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent") + .WithMany("Children") + .HasForeignKey("ParentTaskId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("List"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b => + { + b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task") + .WithMany("Runs") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Task"); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b => + { + b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task") + .WithOne("Worktree") + .HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Task"); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b => + { + b.Navigation("Config"); + + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b => + { + b.Navigation("Children"); + + b.Navigation("Runs"); + + b.Navigation("Subtasks"); + + b.Navigation("Worktree"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/ClaudeDo.Data/Migrations/20260601114247_AddListSortOrder.cs b/src/ClaudeDo.Data/Migrations/20260601114247_AddListSortOrder.cs new file mode 100644 index 0000000..36716c4 --- /dev/null +++ b/src/ClaudeDo.Data/Migrations/20260601114247_AddListSortOrder.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ClaudeDo.Data.Migrations +{ + /// + public partial class AddListSortOrder : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "sort_order", + table: "lists", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + // Backfill existing rows with a dense order (0..N-1) by creation time + // so today's sidebar order is preserved after the migration. + migrationBuilder.Sql(""" + WITH ordered AS ( + SELECT id, (row_number() OVER (ORDER BY created_at) - 1) AS rn + FROM lists + ) + UPDATE lists SET sort_order = (SELECT rn FROM ordered WHERE ordered.id = lists.id); + """); + + migrationBuilder.CreateIndex( + name: "idx_lists_sort", + table: "lists", + column: "sort_order"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "idx_lists_sort", + table: "lists"); + + migrationBuilder.DropColumn( + name: "sort_order", + table: "lists"); + } + } +} diff --git a/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs b/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs index 814f265..d88f9f9 100644 --- a/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs +++ b/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs @@ -140,12 +140,21 @@ namespace ClaudeDo.Data.Migrations .HasColumnType("TEXT") .HasColumnName("name"); + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + b.Property("WorkingDir") .HasColumnType("TEXT") .HasColumnName("working_dir"); b.HasKey("Id"); + b.HasIndex("SortOrder") + .HasDatabaseName("idx_lists_sort"); + b.ToTable("lists", (string)null); }); diff --git a/src/ClaudeDo.Data/Models/ListEntity.cs b/src/ClaudeDo.Data/Models/ListEntity.cs index 1c97481..b8ec9f1 100644 --- a/src/ClaudeDo.Data/Models/ListEntity.cs +++ b/src/ClaudeDo.Data/Models/ListEntity.cs @@ -7,6 +7,7 @@ public sealed class ListEntity public required DateTime CreatedAt { get; init; } public string? WorkingDir { get; set; } public string DefaultCommitType { get; set; } = CommitTypeRegistry.DefaultType; + public int SortOrder { get; set; } // Navigation properties public ListConfigEntity? Config { get; set; } diff --git a/src/ClaudeDo.Data/Repositories/ListRepository.cs b/src/ClaudeDo.Data/Repositories/ListRepository.cs index ab968f0..99054db 100644 --- a/src/ClaudeDo.Data/Repositories/ListRepository.cs +++ b/src/ClaudeDo.Data/Repositories/ListRepository.cs @@ -33,7 +33,19 @@ public sealed class ListRepository public async Task> GetAllAsync(CancellationToken ct = default) { - return await _context.Lists.OrderBy(l => l.CreatedAt).ToListAsync(ct); + return await _context.Lists.OrderBy(l => l.SortOrder).ThenBy(l => l.CreatedAt).ToListAsync(ct); + } + + public async Task ReorderAsync(IReadOnlyList orderedListIds, CancellationToken ct = default) + { + var idSet = orderedListIds.ToHashSet(); + var entities = await _context.Lists.Where(l => idSet.Contains(l.Id)).ToListAsync(ct); + for (int i = 0; i < orderedListIds.Count; i++) + { + var e = entities.FirstOrDefault(x => x.Id == orderedListIds[i]); + if (e is not null) e.SortOrder = i; + } + await _context.SaveChangesAsync(ct); } public async Task GetConfigAsync(string listId, CancellationToken ct = default) diff --git a/src/ClaudeDo.Ui/Design/IslandStyles.axaml b/src/ClaudeDo.Ui/Design/IslandStyles.axaml index 1844588..ebb997b 100644 --- a/src/ClaudeDo.Ui/Design/IslandStyles.axaml +++ b/src/ClaudeDo.Ui/Design/IslandStyles.axaml @@ -826,6 +826,15 @@ + diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index 7ca1c1c..9e46341 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -840,6 +840,35 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase CloseDetail?.Invoke(); } + [RelayCommand] + private async System.Threading.Tasks.Task CommitSubtaskEditAsync(SubtaskRowViewModel? row) + { + if (row is null || !row.IsEditing) return; + row.IsEditing = false; + + var title = row.Title?.Trim() ?? ""; + await using var ctx = _dbFactory.CreateDbContext(); + var repo = new SubtaskRepository(ctx); + + // Emptying the text removes the step. + if (string.IsNullOrEmpty(title)) + { + await repo.DeleteAsync(row.Id); + Subtasks.Remove(row); + return; + } + + var subs = await repo.GetByTaskIdAsync(Task?.Id ?? ""); + var entity = subs.FirstOrDefault(s => s.Id == row.Id); + if (entity is null) return; + if (entity.Title != title) + { + entity.Title = title; + await repo.UpdateAsync(entity); + } + row.Title = title; + } + [RelayCommand] private async System.Threading.Tasks.Task AddSubtaskAsync() { @@ -943,6 +972,7 @@ public sealed partial class SubtaskRowViewModel : ViewModelBase public required string Id { get; init; } [ObservableProperty] private string _title = ""; [ObservableProperty] private bool _done; + [ObservableProperty] private bool _isEditing; [ObservableProperty] private ClaudeDo.Data.Models.TaskStatus _status; [ObservableProperty] private ClaudeDo.Data.Models.WorktreeState _worktreeState = ClaudeDo.Data.Models.WorktreeState.Active; } diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs index 74423d1..80eab9d 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs @@ -12,6 +12,8 @@ public sealed partial class ListNavItemViewModel : ViewModelBase [ObservableProperty] private bool _isActive; [ObservableProperty] private string? _workingDir; [ObservableProperty] private string _defaultCommitType = CommitTypeRegistry.DefaultType; + [ObservableProperty] private bool _dropHintAbove; + [ObservableProperty] private bool _dropHintBelow; public string? IconKey { get; init; } public string? DotColorKey { get; init; } } diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs index 744bdea..f5ac544 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs @@ -82,6 +82,52 @@ public sealed partial class ListsIslandViewModel : ViewModelBase finally { _worktreesOverviewOpen = false; } } + [RelayCommand] + private void OpenInExplorer(ListNavItemViewModel? row) + { + var dir = row?.WorkingDir; + if (string.IsNullOrWhiteSpace(dir) || !System.IO.Directory.Exists(dir)) return; + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = dir, + UseShellExecute = true, + }); + } + catch { /* best-effort */ } + } + + [RelayCommand] + private void OpenInTerminal(ListNavItemViewModel? row) + { + var dir = row?.WorkingDir; + if (string.IsNullOrWhiteSpace(dir) || !System.IO.Directory.Exists(dir)) return; + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = "wt.exe", + Arguments = $"-d \"{dir}\"", + UseShellExecute = true, + }); + } + catch + { + // Windows Terminal not installed — fall back to a plain console at the directory. + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = "cmd.exe", + WorkingDirectory = dir, + UseShellExecute = true, + }); + } + catch { /* best-effort */ } + } + } + public ObservableCollection Items { get; } = new(); public ObservableCollection SmartLists { get; } = new(); public ObservableCollection UserLists { get; } = new(); @@ -231,6 +277,57 @@ public sealed partial class ListsIslandViewModel : ViewModelBase } } + public void ClearDropHints() + { + foreach (var r in UserLists) + { + r.DropHintAbove = false; + r.DropHintBelow = false; + } + } + + public void SetDropHint(ListNavItemViewModel target, bool placeBelow) + { + foreach (var r in UserLists) + { + var isTarget = ReferenceEquals(r, target); + r.DropHintAbove = isTarget && !placeBelow; + r.DropHintBelow = isTarget && placeBelow; + } + } + + public async Task ReorderAsync(ListNavItemViewModel source, ListNavItemViewModel target, bool placeBelow) + { + if (source.Kind != ListKind.User || target.Kind != ListKind.User) return; + if (ReferenceEquals(source, target)) return; + + MoveWithinCollection(UserLists, source, target, placeBelow); + + var orderedIds = UserLists.Select(i => i.Id["user:".Length..]).ToList(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); + var lists = new ListRepository(ctx); + await lists.ReorderAsync(orderedIds); + } + + private static void MoveWithinCollection( + ObservableCollection coll, + ListNavItemViewModel source, + ListNavItemViewModel target, + bool placeBelow) + { + var srcIdx = coll.IndexOf(source); + var tgtIdx = coll.IndexOf(target); + if (srcIdx < 0 || tgtIdx < 0 || srcIdx == tgtIdx) return; + + var finalIdx = placeBelow ? tgtIdx + 1 : tgtIdx; + if (srcIdx < finalIdx) finalIdx--; + if (finalIdx < 0) finalIdx = 0; + if (finalIdx >= coll.Count) finalIdx = coll.Count - 1; + if (finalIdx == srcIdx) return; + + coll.Move(srcIdx, finalIdx); + } + partial void OnSelectedListChanged(ListNavItemViewModel? value) { foreach (var i in Items) i.IsActive = ReferenceEquals(i, value); diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs index 3f7cde2..3369e29 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs @@ -487,6 +487,38 @@ public sealed partial class TasksIslandViewModel : ViewModelBase TasksChanged?.Invoke(this, EventArgs.Empty); } + [RelayCommand] + private async Task ClearCompletedAsync() + { + if (CompletedItems.Count == 0) return; + + // Delete children before parents so the parent-child FK (Restrict) doesn't + // block removing a completed planning parent together with its done children. + var toDelete = CompletedItems.OrderByDescending(r => r.IsChild).ToList(); + + if (ConfirmAsync is not null) + { + var ok = await ConfirmAsync($"Clear {toDelete.Count} completed task(s)? This cannot be undone."); + if (!ok) return; + } + + await using var db = await _dbFactory.CreateDbContextAsync(); + var repo = new TaskRepository(db); + foreach (var row in toDelete) + { + try + { + await repo.DeleteAsync(row.Id); + Items.Remove(row); + } + catch { /* still referenced by open child tasks; leave it visible */ } + } + + Regroup(); + UpdateSubtitle(); + TasksChanged?.Invoke(this, EventArgs.Empty); + } + [RelayCommand] private async Task ToggleStarAsync(TaskRowViewModel row) { diff --git a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml index a920347..16d07da 100644 --- a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml @@ -50,7 +50,10 @@ + Margin="0,0,0,4" + Cursor="Hand" + ToolTip.Tip="Copy task ID" + Tapped="OnTaskIdTapped"/> - + + + AcceptsReturn="False" + TextWrapping="Wrap" + LostFocus="OnSubtaskEditLostFocus"> + + + + + diff --git a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs index 14f200c..3abdf57 100644 --- a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs @@ -1,9 +1,13 @@ +using System.Linq; using Avalonia; using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Threading; +using Avalonia.VisualTree; using ClaudeDo.Ui.ViewModels.Islands; using ClaudeDo.Ui.Views.Modals; using ClaudeDo.Ui.Views.Planning; @@ -135,6 +139,31 @@ public partial class DetailsIslandView : UserControl return await tcs.Task; } + private void OnSubtaskTitleTapped(object? sender, TappedEventArgs e) + { + if (sender is not Control c || c.DataContext is not SubtaskRowViewModel row) return; + row.IsEditing = true; + + var box = (c.GetVisualParent() as Panel)?.GetVisualDescendants().OfType().FirstOrDefault(); + if (box is not null) + Dispatcher.UIThread.Post(() => { box.Focus(); box.SelectAll(); }, DispatcherPriority.Background); + } + + private void OnSubtaskEditLostFocus(object? sender, RoutedEventArgs e) + { + if (DataContext is DetailsIslandViewModel vm + && sender is Control c && c.DataContext is SubtaskRowViewModel row) + vm.CommitSubtaskEditCommand.Execute(row); + } + + private async void OnTaskIdTapped(object? sender, TappedEventArgs e) + { + if (DataContext is not DetailsIslandViewModel vm || vm.Task is null) return; + var clipboard = TopLevel.GetTopLevel(this)?.Clipboard; + if (clipboard is null) return; + await clipboard.SetTextAsync(vm.Task.Id); + } + private async void OnCopyDescriptionClick(object? sender, RoutedEventArgs e) { if (DataContext is not DetailsIslandViewModel vm) return; diff --git a/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml b/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml index 0f83374..7f1296f 100644 --- a/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml @@ -113,8 +113,18 @@ - + + + + + + + + + @@ -152,6 +171,12 @@ Text="{Binding Count}"/> + + + + diff --git a/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml.cs b/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml.cs index 5067bc5..3bbadd7 100644 --- a/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml.cs @@ -1,9 +1,11 @@ using System.Linq; using Avalonia; using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.VisualTree; using ClaudeDo.Ui.ViewModels; using ClaudeDo.Ui.ViewModels.Islands; using ClaudeDo.Ui.ViewModels.Modals; @@ -13,9 +15,13 @@ namespace ClaudeDo.Ui.Views.Islands; public partial class ListsIslandView : UserControl { + private static readonly DataFormat ListRowFormat = + DataFormat.CreateStringApplicationFormat("claudedo-list-row"); + public ListsIslandView() { InitializeComponent(); + AddHandler(PointerPressedEvent, OnTunnelPointerPressed, RoutingStrategies.Tunnel); DataContextChanged += (_, _) => { if (DataContext is ListsIslandViewModel vm) @@ -84,6 +90,127 @@ public partial class ListsIslandView : UserControl vm.SelectCommand.Execute(item); } + private async void OnTunnelPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (DataContext is not ListsIslandViewModel vm) return; + if (e.Source is not Visual src) return; + + var border = FindListItemBorder(src); + if (border?.DataContext is not ListNavItemViewModel row || row.Kind != ListKind.User) return; + if (!e.GetCurrentPoint(border).Properties.IsLeftButtonPressed) return; + + // Double-click opens the list's settings instead of starting a drag. Handled here + // because DoDragDropAsync captures the pointer and would swallow a DoubleTapped event. + if (e.ClickCount == 2) + { + vm.OpenListSettingsCommand.Execute(row); + return; + } + + // Select now so the right pane updates whether the gesture becomes a click or a drag + // (the Tapped handler doesn't fire once DoDragDropAsync captures the pointer). + vm.SelectCommand.Execute(row); + + var data = new DataTransfer(); + data.Add(DataTransferItem.Create(ListRowFormat, row.Id)); + try + { + await DragDrop.DoDragDropAsync(e, data, DragDropEffects.Move); + } + finally + { + vm.ClearDropHints(); + } + } + + private void OnListDragOver(object? sender, DragEventArgs e) + { + if (DataContext is not ListsIslandViewModel vm) { e.DragEffects = DragDropEffects.None; return; } + if (!e.DataTransfer?.Contains(ListRowFormat) ?? true) + { + e.DragEffects = DragDropEffects.None; + vm.ClearDropHints(); + return; + } + if (sender is not Border b || b.DataContext is not ListNavItemViewModel target || target.Kind != ListKind.User) + { + e.DragEffects = DragDropEffects.None; + vm.ClearDropHints(); + return; + } + + var sourceId = e.DataTransfer?.TryGetValue(ListRowFormat); + if (string.IsNullOrEmpty(sourceId) || sourceId == target.Id) + { + e.DragEffects = DragDropEffects.None; + vm.ClearDropHints(); + return; + } + + var placeBelow = e.GetPosition(b).Y > b.Bounds.Height / 2; + + // Canonicalize: "drop below X" == "drop above X+1". Only the last row shows a below-line. + ListNavItemViewModel hintRow = target; + bool hintBelow = false; + if (placeBelow) + { + var next = FindNextUserList(vm, target); + if (next is not null) { hintRow = next; hintBelow = false; } + else { hintRow = target; hintBelow = true; } + } + + if (hintRow.Id == sourceId) + { + e.DragEffects = DragDropEffects.None; + vm.ClearDropHints(); + return; + } + + vm.SetDropHint(hintRow, hintBelow); + e.DragEffects = DragDropEffects.Move; + } + + private async void OnListDrop(object? sender, DragEventArgs e) + { + if (DataContext is not ListsIslandViewModel vm) return; + try + { + if (sender is not Border b || b.DataContext is not ListNavItemViewModel target || target.Kind != ListKind.User) return; + + var sourceId = e.DataTransfer?.TryGetValue(ListRowFormat); + if (string.IsNullOrEmpty(sourceId) || sourceId == target.Id) return; + + var source = vm.UserLists.FirstOrDefault(r => r.Id == sourceId); + if (source is null) return; + + var placeBelow = e.GetPosition(b).Y > b.Bounds.Height / 2; + vm.ClearDropHints(); + await vm.ReorderAsync(source, target, placeBelow); + } + catch + { + vm.ClearDropHints(); + throw; + } + } + + private static Border? FindListItemBorder(Visual? v) + { + while (v is not null) + { + if (v is Border b && b.Classes.Contains("list-item")) return b; + v = v.GetVisualParent(); + } + return null; + } + + private static ListNavItemViewModel? FindNextUserList(ListsIslandViewModel vm, ListNavItemViewModel row) + { + var idx = vm.UserLists.IndexOf(row); + if (idx < 0) return null; + return idx + 1 < vm.UserLists.Count ? vm.UserLists[idx + 1] : null; + } + private async System.Threading.Tasks.Task ShowSettingsAsync(SettingsModalViewModel settingsVm) { var owner = TopLevel.GetTopLevel(this) as Window; diff --git a/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml b/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml index 1d17c1d..f9e43fa 100644 --- a/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml @@ -126,8 +126,17 @@ - + + + + diff --git a/src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml index 115268d..4287b72 100644 --- a/src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml @@ -5,9 +5,11 @@ x:Class="ClaudeDo.Ui.Views.Modals.DiffModalView" x:DataType="vm:DiffModalViewModel" Title="Diff" - Width="1200" Height="800" - WindowDecorations="None" + Width="1200" Height="800" MinWidth="700" MinHeight="450" + CanResize="True" + WindowDecorations="BorderOnly" ExtendClientAreaToDecorationsHint="True" + ExtendClientAreaTitleBarHeightHint="-1" WindowStartupLocation="CenterOwner" Background="{DynamicResource SurfaceBrush}"> diff --git a/src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml index 0dfdef0..ad64292 100644 --- a/src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml @@ -8,8 +8,9 @@ Width="520" Height="720" CanResize="True" MinWidth="460" MinHeight="520" - WindowDecorations="None" + WindowDecorations="BorderOnly" ExtendClientAreaToDecorationsHint="True" + ExtendClientAreaTitleBarHeightHint="-1" WindowStartupLocation="CenterOwner" Background="{DynamicResource SurfaceBrush}"> diff --git a/src/ClaudeDo.Ui/Views/Modals/MergeModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/MergeModalView.axaml index 6996fde..bb5bbc2 100644 --- a/src/ClaudeDo.Ui/Views/Modals/MergeModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/MergeModalView.axaml @@ -5,10 +5,11 @@ x:Class="ClaudeDo.Ui.Views.Modals.MergeModalView" x:DataType="vm:MergeModalViewModel" Title="Merge worktree" - Width="560" Height="460" - CanResize="False" - WindowDecorations="None" + Width="560" Height="460" MinWidth="460" MinHeight="360" + CanResize="True" + WindowDecorations="BorderOnly" ExtendClientAreaToDecorationsHint="True" + ExtendClientAreaTitleBarHeightHint="-1" WindowStartupLocation="CenterOwner" Background="{DynamicResource SurfaceBrush}"> diff --git a/src/ClaudeDo.Ui/Views/Modals/RepoImportModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/RepoImportModalView.axaml index 8e2894c..9f5bef6 100644 --- a/src/ClaudeDo.Ui/Views/Modals/RepoImportModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/RepoImportModalView.axaml @@ -5,9 +5,11 @@ x:Class="ClaudeDo.Ui.Views.Modals.RepoImportModalView" x:DataType="vm:RepoImportModalViewModel" Title="Add repos as lists" - Width="560" Height="480" - WindowDecorations="None" + Width="560" Height="480" MinWidth="420" MinHeight="320" + CanResize="True" + WindowDecorations="BorderOnly" ExtendClientAreaToDecorationsHint="True" + ExtendClientAreaTitleBarHeightHint="-1" WindowStartupLocation="CenterOwner" Background="{DynamicResource SurfaceBrush}"> diff --git a/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml index 0e800a9..5b2dcd7 100644 --- a/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml @@ -7,9 +7,11 @@ x:Class="ClaudeDo.Ui.Views.Modals.SettingsModalView" x:DataType="vm:SettingsModalViewModel" Title="Settings" - Width="580" Height="760" - WindowDecorations="None" + Width="580" Height="760" MinWidth="480" MinHeight="520" + CanResize="True" + WindowDecorations="BorderOnly" ExtendClientAreaToDecorationsHint="True" + ExtendClientAreaTitleBarHeightHint="-1" WindowStartupLocation="CenterOwner" Background="{DynamicResource SurfaceBrush}"> diff --git a/src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml index 8bc6dbf..f621885 100644 --- a/src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml @@ -11,7 +11,8 @@ WindowStartupLocation="CenterOwner" Background="{DynamicResource SurfaceBrush}" WindowDecorations="BorderOnly" - ExtendClientAreaToDecorationsHint="True"> + ExtendClientAreaToDecorationsHint="True" + ExtendClientAreaTitleBarHeightHint="-1"> diff --git a/src/ClaudeDo.Ui/Views/Planning/ConflictResolutionView.axaml b/src/ClaudeDo.Ui/Views/Planning/ConflictResolutionView.axaml index cc68ea9..e561319 100644 --- a/src/ClaudeDo.Ui/Views/Planning/ConflictResolutionView.axaml +++ b/src/ClaudeDo.Ui/Views/Planning/ConflictResolutionView.axaml @@ -5,9 +5,11 @@ x:DataType="vm:ConflictResolutionViewModel" x:Class="ClaudeDo.Ui.Views.Planning.ConflictResolutionView" Title="Merge conflict" - Width="560" SizeToContent="Height" - WindowDecorations="None" + Width="560" SizeToContent="Height" MinWidth="460" + CanResize="True" + WindowDecorations="BorderOnly" ExtendClientAreaToDecorationsHint="True" + ExtendClientAreaTitleBarHeightHint="-1" WindowStartupLocation="CenterOwner" Background="{DynamicResource SurfaceBrush}"> diff --git a/src/ClaudeDo.Ui/Views/Planning/PlanningDiffView.axaml b/src/ClaudeDo.Ui/Views/Planning/PlanningDiffView.axaml index 2330320..954fa29 100644 --- a/src/ClaudeDo.Ui/Views/Planning/PlanningDiffView.axaml +++ b/src/ClaudeDo.Ui/Views/Planning/PlanningDiffView.axaml @@ -5,9 +5,11 @@ x:Class="ClaudeDo.Ui.Views.Planning.PlanningDiffView" x:DataType="vm:PlanningDiffViewModel" Title="Planning — Combined diff" - Width="1100" Height="700" - WindowDecorations="None" + Width="1100" Height="700" MinWidth="700" MinHeight="450" + CanResize="True" + WindowDecorations="BorderOnly" ExtendClientAreaToDecorationsHint="True" + ExtendClientAreaTitleBarHeightHint="-1" WindowStartupLocation="CenterOwner" Background="{DynamicResource SurfaceBrush}">